summaryrefslogtreecommitdiffstats
path: root/src/extension/internal/pdfinput/svg-builder.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 11:50:49 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 11:50:49 +0000
commitc853ffb5b2f75f5a889ed2e3ef89b818a736e87a (patch)
tree7d13a0883bb7936b84d6ecdd7bc332b41ed04bee /src/extension/internal/pdfinput/svg-builder.cpp
parentInitial commit. (diff)
downloadinkscape-upstream.tar.xz
inkscape-upstream.zip
Adding upstream version 1.3+ds.upstream/1.3+dsupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/extension/internal/pdfinput/svg-builder.cpp')
-rw-r--r--src/extension/internal/pdfinput/svg-builder.cpp2311
1 files changed, 2311 insertions, 0 deletions
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 <jon@joncruz.org>
+ *
+ * Copyright (C) 2007 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+# include "config.h" // only include where actually required!
+#endif
+
+#include <string>
+#include <locale>
+#include <codecvt>
+
+#ifdef HAVE_POPPLER
+#define USE_CMS
+
+#include "Function.h"
+#include "GfxFont.h"
+#include "GfxState.h"
+#include "GlobalParams.h"
+#include "Page.h"
+#include "Stream.h"
+#include "UnicodeMap.h"
+#include "color.h"
+#include "display/cairo-utils.h"
+#include "display/nr-filter-utils.h"
+#include "document.h"
+#include "extract-uri.h"
+#include "libnrtype/font-factory.h"
+#include "libnrtype/font-instance.h"
+#include "object/color-profile.h"
+#include "object/sp-defs.h"
+#include "object/sp-item-group.h"
+#include "object/sp-namedview.h"
+#include "pdf-parser.h"
+#include "pdf-utils.h"
+#include "png.h"
+#include "poppler-cairo-font-engine.h"
+#include "profile-manager.h"
+#include "svg-builder.h"
+#include "svg/css-ostringstream.h"
+#include "svg/path-string.h"
+#include "svg/svg-color.h"
+#include "svg/svg.h"
+#include "util/units.h"
+#include "xml/document.h"
+#include "xml/node.h"
+#include "xml/repr.h"
+#include "xml/sp-css-attr.h"
+
+namespace Inkscape {
+namespace Extension {
+namespace Internal {
+
+//#define IFTRACE(_code) _code
+#define IFTRACE(_code)
+
+#define TRACE(_args) IFTRACE(g_print _args)
+
+
+/**
+ * \class SvgBuilder
+ *
+ */
+
+SvgBuilder::SvgBuilder(SPDocument *document, gchar *docname, XRef *xref)
+{
+ _is_top_level = true;
+ _doc = document;
+ _docname = docname;
+ _xref = xref;
+ _xml_doc = _doc->getReprDoc();
+ _container = _root = _doc->getReprRoot();
+ _init();
+
+ // Set default preference settings
+ _preferences = _xml_doc->createElement("svgbuilder:prefs");
+ _preferences->setAttribute("embedImages", "1");
+}
+
+SvgBuilder::SvgBuilder(SvgBuilder *parent, Inkscape::XML::Node *root) {
+ _is_top_level = false;
+ _doc = parent->_doc;
+ _docname = parent->_docname;
+ _xref = parent->_xref;
+ _xml_doc = parent->_xml_doc;
+ _preferences = parent->_preferences;
+ _container = this->_root = root;
+ _init();
+}
+
+SvgBuilder::~SvgBuilder()
+{
+ if (_clip_history) {
+ delete _clip_history;
+ _clip_history = nullptr;
+ }
+}
+
+void SvgBuilder::_init() {
+ _clip_history = new ClipHistoryEntry();
+ _css_font = nullptr;
+ _font_specification = nullptr;
+ _in_text_object = false;
+ _invalidated_style = true;
+ _width = 0;
+ _height = 0;
+
+ _node_stack.push_back(_container);
+}
+
+/**
+ * We're creating a multi-page document, push page number.
+ */
+void SvgBuilder::pushPage(const std::string &label, GfxState *state)
+{
+ // Move page over by the last page width
+ if (_page && this->_width) {
+ int gap = 20;
+ _page_left += this->_width + gap;
+ // TODO: A more interesting page layout could be implemented here.
+ }
+ _page_num += 1;
+ _page_offset = true;
+
+ if (_page) {
+ Inkscape::GC::release(_page);
+ }
+ _page = _xml_doc->createElement("inkscape:page");
+ _page->setAttributeSvgDouble("x", _page_left);
+ _page->setAttributeSvgDouble("y", _page_top);
+
+ // Page translation is somehow lost in the way we're using poppler and the state management
+ // Applying the state directly doesn't work as many of the flips/rotates are baked in already.
+ // The translation alone must be added back to the page position so items end up in the
+ // right places. If a better method is found, please replace this code.
+ auto st = stateToAffine(state);
+ auto tr = st.translation();
+ if (st[0] < 0 || st[2] < 0) { // Flip or rotate in X
+ tr[Geom::X] = -tr[Geom::X] + state->getPageWidth();
+ }
+ if (st[1] < 0 || st[3] < 0) { // Flip or rotate in Y
+ tr[Geom::Y] = -tr[Geom::Y] + state->getPageHeight();
+ }
+ // Note: This translation is very rare in pdf files, most of the time their initial state doesn't contain
+ // any real translations, just a flip and the because of our GfxState constructor, the pt/px scale.
+ // Please use an example pdf which produces a non-zero translation in order to change this code!
+ _page_affine = Geom::Translate(tr).inverse() * Geom::Translate(_page_left, _page_top);
+
+ if (!label.empty()) {
+ _page->setAttribute("inkscape:label", label);
+ }
+ auto _nv = _doc->getNamedView()->getRepr();
+ _nv->appendChild(_page);
+
+ // No OptionalContentGroups means no layers, so make a default layer for this page.
+ if (_ocgs.empty()) {
+ // Reset to root
+ while (_container != _root) {
+ _popGroup();
+ }
+ _pushGroup();
+ setAsLayer(label.c_str(), true);
+ }
+}
+
+void SvgBuilder::setDocumentSize(double width, double height) {
+ this->_width = width;
+ this->_height = height;
+
+ if (_page_num < 2) {
+ _root->setAttributeSvgDouble("width", width);
+ _root->setAttributeSvgDouble("height", height);
+ }
+ if (_page) {
+ _page->setAttributeSvgDouble("width", width);
+ _page->setAttributeSvgDouble("height", height);
+ }
+}
+
+/**
+ * Crop to this bounding box, do this before setMargins() but after setDocumentSize
+ */
+void SvgBuilder::cropPage(const Geom::Rect &bbox)
+{
+ if (_container == _root) {
+ // We're not going to crop when there's PDF Layers
+ return;
+ }
+ auto box = bbox * _page_affine;
+ Inkscape::CSSOStringStream val;
+ val << "M" << box.left() << " " << box.top()
+ << "H" << box.right() << "V" << box.bottom()
+ << "H" << box.left() << "Z";
+ auto clip_path = _createClip(val.str(), Geom::identity(), false);
+ gchar *urltext = g_strdup_printf("url(#%s)", clip_path->attribute("id"));
+ _container->setAttribute("clip-path", urltext);
+ g_free(urltext);
+}
+
+/**
+ * Calculate the page margin size based on the pdf settings.
+ */
+void SvgBuilder::setMargins(const Geom::Rect &page, const Geom::Rect &margins, const Geom::Rect &bleed)
+{
+ if (page.width() != _width || page.height() != _height) {
+ // We need to re-set the page size and change the page_affine.
+ _page_affine *= Geom::Translate(-page.left(), -page.top());
+ setDocumentSize(page.width(), page.height());
+ }
+ if (page != margins) {
+ if (!_page) {
+ g_warning("Can not store PDF margins in bare document.");
+ return;
+ }
+ // Calculate the margins from the pdf art box.
+ Inkscape::CSSOStringStream val;
+ val << margins.top() - page.top() << " "
+ << page.right() - margins.right() << " "
+ << page.bottom() - margins.bottom() << " "
+ << margins.left() - page.left();
+ _page->setAttribute("margin", val.str());
+ }
+ if (page != bleed) {
+ if (!_page) {
+ g_warning("Can not store PDF bleed in bare document.");
+ return;
+ }
+ Inkscape::CSSOStringStream val;
+ val << page.top() - bleed.top() << " "
+ << bleed.right() - page.right() << " "
+ << bleed.bottom() - page.bottom() << " "
+ << page.left() - bleed.left();
+ _page->setAttribute("bleed", val.str());
+ }
+}
+
+/**
+ * \brief Sets groupmode of the current container to 'layer' and sets its label if given
+ */
+void SvgBuilder::setAsLayer(const char *layer_name, bool visible)
+{
+ _container->setAttribute("inkscape:groupmode", "layer");
+ if (layer_name) {
+ _container->setAttribute("inkscape:label", layer_name);
+ }
+ if (!visible) {
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ sp_repr_css_set_property(css, "display", "none");
+ sp_repr_css_change(_container, css, "style");
+ }
+}
+
+/**
+ * \brief Sets the current container's opacity
+ */
+void SvgBuilder::setGroupOpacity(double opacity) {
+ _container->setAttributeSvgDouble("opacity", CLAMP(opacity, 0.0, 1.0));
+}
+
+void SvgBuilder::saveState(GfxState *state)
+{
+ _clip_history = _clip_history->save();
+}
+
+void SvgBuilder::restoreState(GfxState *state) {
+ _clip_history = _clip_history->restore();
+
+ if (!_mask_groups.empty()) {
+ GfxState *mask_state = _mask_groups.back();
+ if (state == mask_state) {
+ popGroup(state);
+ _mask_groups.pop_back();
+ }
+ }
+ while (_clip_groups > 0) {
+ popGroup(nullptr);
+ _clip_groups--;
+ }
+}
+
+Inkscape::XML::Node *SvgBuilder::_pushContainer(const char *name)
+{
+ return _pushContainer(_xml_doc->createElement(name));
+}
+
+Inkscape::XML::Node *SvgBuilder::_pushContainer(Inkscape::XML::Node *node)
+{
+ _node_stack.push_back(node);
+ _container = node;
+ // Clear the clip history
+ _clip_history = _clip_history->save(true);
+ return node;
+}
+
+Inkscape::XML::Node *SvgBuilder::_popContainer()
+{
+ Inkscape::XML::Node *node = nullptr;
+ if ( _node_stack.size() > 1 ) {
+ node = _node_stack.back();
+ _node_stack.pop_back();
+ _container = _node_stack.back(); // Re-set container
+ _clip_history = _clip_history->restore();
+ } else {
+ TRACE(("_popContainer() called when stack is empty\n"));
+ node = _root;
+ }
+ return node;
+}
+
+/**
+ * Create an svg element and append it to the current container object.
+ */
+Inkscape::XML::Node *SvgBuilder::_addToContainer(const char *name)
+{
+ Inkscape::XML::Node *node = _xml_doc->createElement(name);
+ _addToContainer(node);
+ return node;
+}
+
+/**
+ * Append the given xml element to the current container object, clipping and masking as needed.
+ *
+ * if release is true (default), the XML node will be GC released too.
+ */
+void SvgBuilder::_addToContainer(Inkscape::XML::Node *node, bool release)
+{
+ if (!node->parent()) {
+ _container->appendChild(node);
+ }
+ if (release) {
+ Inkscape::GC::release(node);
+ }
+}
+
+void SvgBuilder::_setClipPath(Inkscape::XML::Node *node)
+{
+ if (_clip_history->hasClipPath() || _clip_text) {
+ auto tr = Geom::identity();
+ if (auto attr = node->attribute("transform")) {
+ sp_svg_transform_read(attr, &tr);
+ }
+ if (auto clip_path = _getClip(tr)) {
+ gchar *urltext = g_strdup_printf("url(#%s)", clip_path->attribute("id"));
+ node->setAttribute("clip-path", urltext);
+ g_free(urltext);
+ }
+ }
+}
+
+Inkscape::XML::Node *SvgBuilder::_pushGroup()
+{
+ Inkscape::XML::Node *saved_container = _container;
+ Inkscape::XML::Node *node = _pushContainer("svg:g");
+ saved_container->appendChild(node);
+ Inkscape::GC::release(node);
+ return _container;
+}
+
+Inkscape::XML::Node *SvgBuilder::_popGroup()
+{
+ if (_container != _root) { // Pop if the current container isn't root
+ _popContainer();
+ }
+ return _container;
+}
+
+static gchar *svgConvertRGBToText(double r, double g, double b) {
+ using Inkscape::Filters::clamp;
+ static gchar tmp[1023] = {0};
+ snprintf(tmp, 1023,
+ "#%02x%02x%02x",
+ clamp(SP_COLOR_F_TO_U(r)),
+ clamp(SP_COLOR_F_TO_U(g)),
+ clamp(SP_COLOR_F_TO_U(b)));
+ return (gchar *)&tmp;
+}
+
+static std::string svgConvertGfxRGB(GfxRGB *color)
+{
+ double r = (double)color->r / 65535.0;
+ double g = (double)color->g / 65535.0;
+ double b = (double)color->b / 65535.0;
+ return svgConvertRGBToText(r, g, b);
+}
+
+std::string SvgBuilder::convertGfxColor(const GfxColor *color, GfxColorSpace *space)
+{
+ std::string icc = "";
+ switch (space->getMode()) {
+ case csDeviceGray:
+ case csDeviceRGB:
+ case csDeviceCMYK:
+ icc = _icc_profile;
+ break;
+ case csICCBased:
+#if POPPLER_CHECK_VERSION(0, 90, 0)
+ auto icc_space = dynamic_cast<GfxICCBasedColorSpace *>(space);
+ icc = _getColorProfile(icc_space->getProfile().get());
+#else
+ g_warning("ICC profile ignored; libpoppler >= 0.90.0 required.");
+#endif
+ break;
+ }
+
+ GfxRGB rgb;
+ space->getRGB(color, &rgb);
+ auto rgb_color = svgConvertGfxRGB(&rgb);
+
+ if (!icc.empty()) {
+ Inkscape::CSSOStringStream icc_color;
+ icc_color << rgb_color << " icc-color(" << icc;
+ for (int i = 0; i < space->getNComps(); ++i) {
+ icc_color << ", " << colToDbl((*color).c[i]);
+ }
+ icc_color << ");";
+ return icc_color.str();
+ }
+
+ return rgb_color;
+}
+
+static void svgSetTransform(Inkscape::XML::Node *node, Geom::Affine matrix) {
+ if (node->attribute("clip-path")) {
+ g_error("Adding transform AFTER clipping path.");
+ }
+ node->setAttributeOrRemoveIfEmpty("transform", sp_svg_transform_write(matrix));
+}
+
+/**
+ * \brief Generates a SVG path string from poppler's data structure
+ */
+static gchar *svgInterpretPath(_POPPLER_CONST_83 GfxPath *path) {
+ Inkscape::SVG::PathString pathString;
+ for (int i = 0 ; i < path->getNumSubpaths() ; ++i ) {
+ _POPPLER_CONST_83 GfxSubpath *subpath = path->getSubpath(i);
+ if (subpath->getNumPoints() > 0) {
+ pathString.moveTo(subpath->getX(0), subpath->getY(0));
+ int j = 1;
+ while (j < subpath->getNumPoints()) {
+ if (subpath->getCurve(j)) {
+ pathString.curveTo(subpath->getX(j), subpath->getY(j),
+ subpath->getX(j+1), subpath->getY(j+1),
+ subpath->getX(j+2), subpath->getY(j+2));
+
+ j += 3;
+ } else {
+ pathString.lineTo(subpath->getX(j), subpath->getY(j));
+ ++j;
+ }
+ }
+ if (subpath->isClosed()) {
+ pathString.closePath();
+ }
+ }
+ }
+
+ return g_strdup(pathString.c_str());
+}
+
+/**
+ * \brief Sets stroke style from poppler's GfxState data structure
+ * Uses the given SPCSSAttr for storing the style properties
+ */
+void SvgBuilder::_setStrokeStyle(SPCSSAttr *css, GfxState *state) {
+ // Stroke color/pattern
+ auto space = state->getStrokeColorSpace();
+ if (space->getMode() == csPattern) {
+ gchar *urltext = _createPattern(state->getStrokePattern(), state, true);
+ sp_repr_css_set_property(css, "stroke", urltext);
+ if (urltext) {
+ g_free(urltext);
+ }
+ } else {
+ sp_repr_css_set_property(css, "stroke", convertGfxColor(state->getStrokeColor(), space).c_str());
+ }
+
+ // Opacity
+ Inkscape::CSSOStringStream os_opacity;
+ os_opacity << state->getStrokeOpacity();
+ sp_repr_css_set_property(css, "stroke-opacity", os_opacity.str().c_str());
+
+ // Line width
+ Inkscape::CSSOStringStream os_width;
+ double lw = state->getLineWidth();
+ // emit a stroke which is 1px in toplevel user units
+ os_width << (lw > 0.0 ? lw : 1.0);
+ sp_repr_css_set_property(css, "stroke-width", os_width.str().c_str());
+
+ // Line cap
+ switch (state->getLineCap()) {
+ case 0:
+ sp_repr_css_set_property(css, "stroke-linecap", "butt");
+ break;
+ case 1:
+ sp_repr_css_set_property(css, "stroke-linecap", "round");
+ break;
+ case 2:
+ sp_repr_css_set_property(css, "stroke-linecap", "square");
+ break;
+ }
+
+ // Line join
+ switch (state->getLineJoin()) {
+ case 0:
+ sp_repr_css_set_property(css, "stroke-linejoin", "miter");
+ break;
+ case 1:
+ sp_repr_css_set_property(css, "stroke-linejoin", "round");
+ break;
+ case 2:
+ sp_repr_css_set_property(css, "stroke-linejoin", "bevel");
+ break;
+ }
+
+ // Miterlimit
+ Inkscape::CSSOStringStream os_ml;
+ os_ml << state->getMiterLimit();
+ sp_repr_css_set_property(css, "stroke-miterlimit", os_ml.str().c_str());
+
+ // Line dash
+ int dash_length;
+ double dash_start;
+#if POPPLER_CHECK_VERSION(22, 9, 0)
+ const double *dash_pattern;
+ const std::vector<double> &dash = state->getLineDash(&dash_start);
+ dash_pattern = dash.data();
+ dash_length = dash.size();
+#else
+ double *dash_pattern;
+ state->getLineDash(&dash_pattern, &dash_length, &dash_start);
+#endif
+ if ( dash_length > 0 ) {
+ Inkscape::CSSOStringStream os_array;
+ for ( int i = 0 ; i < dash_length ; i++ ) {
+ os_array << dash_pattern[i];
+ if (i < (dash_length - 1)) {
+ os_array << ",";
+ }
+ }
+ sp_repr_css_set_property(css, "stroke-dasharray", os_array.str().c_str());
+
+ Inkscape::CSSOStringStream os_offset;
+ os_offset << dash_start;
+ sp_repr_css_set_property(css, "stroke-dashoffset", os_offset.str().c_str());
+ } else {
+ sp_repr_css_set_property(css, "stroke-dasharray", "none");
+ sp_repr_css_set_property(css, "stroke-dashoffset", nullptr);
+ }
+}
+
+/**
+ * \brief Sets fill style from poppler's GfxState data structure
+ * Uses the given SPCSSAttr for storing the style properties.
+ */
+void SvgBuilder::_setFillStyle(SPCSSAttr *css, GfxState *state, bool even_odd) {
+
+ // Fill color/pattern
+ auto space = state->getFillColorSpace();
+ if (space->getMode() == csPattern) {
+ gchar *urltext = _createPattern(state->getFillPattern(), state);
+ sp_repr_css_set_property(css, "fill", urltext);
+ if (urltext) {
+ g_free(urltext);
+ }
+ } else {
+ sp_repr_css_set_property(css, "fill", convertGfxColor(state->getFillColor(), space).c_str());
+ }
+
+ // Opacity
+ Inkscape::CSSOStringStream os_opacity;
+ os_opacity << state->getFillOpacity();
+ sp_repr_css_set_property(css, "fill-opacity", os_opacity.str().c_str());
+
+ // Fill rule
+ sp_repr_css_set_property(css, "fill-rule", even_odd ? "evenodd" : "nonzero");
+}
+
+/**
+ * \brief Sets blend style properties from poppler's GfxState data structure
+ * \update a SPCSSAttr with all mix-blend-mode set
+ */
+void SvgBuilder::_setBlendMode(Inkscape::XML::Node *node, GfxState *state)
+{
+ SPCSSAttr *css = sp_repr_css_attr(node, "style");
+ GfxBlendMode blendmode = state->getBlendMode();
+ if (blendmode) {
+ sp_repr_css_set_property(css, "mix-blend-mode", enum_blend_mode[blendmode].key);
+ }
+ Glib::ustring value;
+ sp_repr_css_write_string(css, value);
+ node->setAttributeOrRemoveIfEmpty("style", value);
+ sp_repr_css_attr_unref(css);
+}
+
+void SvgBuilder::_setTransform(Inkscape::XML::Node *node, GfxState *state, Geom::Affine extra)
+{
+ svgSetTransform(node, extra * stateToAffine(state) * _page_affine);
+}
+
+/**
+ * \brief Sets style properties from poppler's GfxState data structure
+ * \return SPCSSAttr with all the relevant properties set
+ */
+SPCSSAttr *SvgBuilder::_setStyle(GfxState *state, bool fill, bool stroke, bool even_odd) {
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ if (fill) {
+ _setFillStyle(css, state, even_odd);
+ } else {
+ sp_repr_css_set_property(css, "fill", "none");
+ }
+
+ if (stroke) {
+ _setStrokeStyle(css, state);
+ } else {
+ sp_repr_css_set_property(css, "stroke", "none");
+ }
+
+ return css;
+}
+
+/**
+ * Returns the CSSAttr of the previously added path if it's exactly
+ * the same path AND is missing the fill or stroke that is now being painted.
+ */
+bool SvgBuilder::shouldMergePath(bool is_fill, const std::string &path)
+{
+ auto prev = _container->lastChild();
+ if (!prev || prev->attribute("mask"))
+ return false;
+
+ auto prev_d = prev->attribute("d");
+ if (!prev_d)
+ return false;
+
+ if (path != prev_d && path != std::string(prev_d) + " Z")
+ return false;
+
+ auto prev_css = sp_repr_css_attr(prev, "style");
+ std::string prev_val = sp_repr_css_property(prev_css, is_fill ? "fill" : "stroke", "");
+ // Very specific check excludes paths created elsewhere who's fill/stroke was unset.
+ return prev_val == "none";
+}
+
+/**
+ * Set the fill XOR stroke of the previously added path, if that path
+ * is missing the given attribute AND the path is exactly the same.
+ *
+ * This effectively merges the two objects and is an 'interpretation' step.
+ */
+bool SvgBuilder::mergePath(GfxState *state, bool is_fill, const std::string &path, bool even_odd)
+{
+ if (shouldMergePath(is_fill, path)) {
+ auto prev = _container->lastChild();
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ if (is_fill) {
+ _setFillStyle(css, state, even_odd);
+ // Fill after stroke indicates a different paint order.
+ sp_repr_css_set_property(css, "paint-order", "stroke fill markers");
+ } else {
+ _setStrokeStyle(css, state);
+ }
+ sp_repr_css_change(prev, css, "style");
+ sp_repr_css_attr_unref(css);
+ return true;
+ }
+ return false;
+}
+
+/**
+ * \brief Emits the current path in poppler's GfxState data structure
+ * Can be used to do filling and stroking at once.
+ *
+ * \param fill whether the path should be filled
+ * \param stroke whether the path should be stroked
+ * \param even_odd whether the even-odd rule should be used when filling the path
+ */
+void SvgBuilder::addPath(GfxState *state, bool fill, bool stroke, bool even_odd) {
+ gchar *pathtext = svgInterpretPath(state->getPath());
+
+ if (!pathtext)
+ return;
+
+ if (!strlen(pathtext) || (fill != stroke && mergePath(state, fill, pathtext, even_odd))) {
+ g_free(pathtext);
+ return;
+ }
+
+ Inkscape::XML::Node *path = _addToContainer("svg:path");
+ path->setAttribute("d", pathtext);
+ g_free(pathtext);
+
+ // Set style
+ SPCSSAttr *css = _setStyle(state, fill, stroke, even_odd);
+ sp_repr_css_change(path, css, "style");
+ sp_repr_css_attr_unref(css);
+ _setBlendMode(path, state);
+ _setTransform(path, state);
+ _setClipPath(path);
+}
+
+void SvgBuilder::addClippedFill(GfxShading *shading, const Geom::Affine shading_tr)
+{
+ if (_clip_history->getClipPath()) {
+ addShadedFill(shading, shading_tr, _clip_history->getClipPath(), _clip_history->getAffine(),
+ _clip_history->getClipType() == clipEO);
+ }
+}
+
+/**
+ * \brief Emits the current path in poppler's GfxState data structure
+ * The path is set to be filled with the given shading.
+ */
+void SvgBuilder::addShadedFill(GfxShading *shading, const Geom::Affine shading_tr, GfxPath *path, const Geom::Affine tr,
+ bool even_odd)
+{
+ auto prev = _container->lastChild();
+ gchar *pathtext = svgInterpretPath(path);
+
+ // Create a new gradient object before comitting to creating a path for it
+ // And package it into a css bundle which can be applied
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ // We remove the shape's affine to adjust the gradient back into place
+ gchar *id = _createGradient(shading, shading_tr * tr.inverse(), true);
+ if (id) {
+ gchar *urltext = g_strdup_printf ("url(#%s)", id);
+ sp_repr_css_set_property(css, "fill", urltext);
+ g_free(urltext);
+ g_free(id);
+ } else {
+ sp_repr_css_attr_unref(css);
+ return;
+ }
+ if (even_odd) {
+ sp_repr_css_set_property(css, "fill-rule", "evenodd");
+ }
+ // Merge the style with the previous shape
+ if (shouldMergePath(true, pathtext)) {
+ // POSSIBLE: The gradientTransform might now incorrect if the
+ // state of the transformation was different between the two paths.
+ sp_repr_css_change(prev, css, "style");
+ g_free(pathtext);
+ return;
+ }
+
+ Inkscape::XML::Node *path_node = _addToContainer("svg:path");
+ path_node->setAttribute("d", pathtext);
+ g_free(pathtext);
+
+ // Don't add transforms to mask children.
+ if (std::string("svg:mask") != _container->name()) {
+ svgSetTransform(path_node, tr * _page_affine);
+ }
+
+ // Set the gradient into this new path.
+ sp_repr_css_set_property(css, "stroke", "none");
+ sp_repr_css_change(path_node, css, "style");
+ sp_repr_css_attr_unref(css);
+}
+
+/**
+ * \brief Clips to the current path set in GfxState
+ * \param state poppler's data structure
+ * \param even_odd whether the even-odd rule should be applied
+ */
+void SvgBuilder::setClip(GfxState *state, GfxClipType clip, bool is_bbox)
+{
+ // When there's already a clip path, we add clipping groups to handle them.
+ if (!is_bbox && _clip_history->hasClipPath() && !_clip_history->isCopied()) {
+ _pushContainer("svg:g");
+ _clip_groups++;
+ }
+ if (clip == clipNormal) {
+ _clip_history->setClip(state, clipNormal, is_bbox);
+ } else {
+ _clip_history->setClip(state, clipEO);
+ }
+}
+
+/**
+ * Return the active clip as a new xml node.
+ */
+Inkscape::XML::Node *SvgBuilder::_getClip(const Geom::Affine &node_tr)
+{
+ // In SVG the path-clip transforms are compounded, so we have to do extra work to
+ // pull transforms back out of the clipping object and set them. Otherwise this
+ // would all be a lot simpler.
+ if (_clip_text) {
+ auto node = _clip_text;
+
+ auto text_tr = Geom::identity();
+ if (auto attr = node->attribute("transform")) {
+ sp_svg_transform_read(attr, &text_tr);
+ node->removeAttribute("transform");
+ }
+
+ for (auto child = node->firstChild(); child; child = child->next()) {
+ Geom::Affine child_tr = text_tr * _page_affine * node_tr.inverse();
+ svgSetTransform(child, child_tr);
+ }
+
+ _clip_text = nullptr;
+ return node;
+ }
+ if (_clip_history->hasClipPath()) {
+ std::string clip_d = svgInterpretPath(_clip_history->getClipPath());
+ Geom::Affine tr = _clip_history->getAffine() * _page_affine * node_tr.inverse();
+ return _createClip(clip_d, tr, _clip_history->evenOdd());
+ }
+ return nullptr;
+}
+
+Inkscape::XML::Node *SvgBuilder::_createClip(const std::string &d, const Geom::Affine tr, bool even_odd)
+{
+ Inkscape::XML::Node *clip_path = _xml_doc->createElement("svg:clipPath");
+ clip_path->setAttribute("clipPathUnits", "userSpaceOnUse");
+
+ // Create the path
+ Inkscape::XML::Node *path = _xml_doc->createElement("svg:path");
+ path->setAttribute("d", d);
+ svgSetTransform(path, tr);
+
+ if (even_odd) {
+ path->setAttribute("clip-rule", "evenodd");
+ }
+ clip_path->appendChild(path);
+ Inkscape::GC::release(path);
+
+ // Append clipPath to defs and get id
+ _doc->getDefs()->getRepr()->appendChild(clip_path);
+ Inkscape::GC::release(clip_path);
+ return clip_path;
+}
+
+void SvgBuilder::beginMarkedContent(const char *name, const char *group)
+{
+ if (name && group && std::string(name) == "OC") {
+ auto layer_id = std::string("layer-") + group;
+ if (auto existing = _doc->getObjectById(layer_id)) {
+ if (existing->getRepr()->parent() == _container) {
+ _container = existing->getRepr();
+ _node_stack.push_back(_container);
+ } else {
+ g_warning("Unexpected marked content group in PDF!");
+ _pushGroup();
+ }
+ } else {
+ auto node = _pushGroup();
+ node->setAttribute("id", layer_id);
+ if (_ocgs.find(group) != _ocgs.end()) {
+ auto pair = _ocgs[group];
+ setAsLayer(pair.first.c_str(), pair.second);
+ }
+ }
+ } else {
+ auto node = _pushGroup();
+ if (group) {
+ node->setAttribute("id", std::string("group-") + group);
+ }
+ }
+}
+
+void SvgBuilder::addOptionalGroup(const std::string &oc, const std::string &label, bool visible)
+{
+ _ocgs[oc] = {label, visible};
+}
+
+void SvgBuilder::endMarkedContent()
+{
+ _popGroup();
+}
+
+void SvgBuilder::addColorProfile(unsigned char *profBuf, int length)
+{
+ cmsHPROFILE hp = cmsOpenProfileFromMem(profBuf, length);
+ if (!hp) {
+ g_warning("Failed to read ICCBased color space profile from PDF file.");
+ return;
+ }
+ _icc_profile = _getColorProfile(hp);
+}
+
+/**
+ * Return the color profile name if it's already been added
+ */
+std::string SvgBuilder::_getColorProfile(cmsHPROFILE hp)
+{
+ if (!hp)
+ return "";
+
+ // Cached name of this profile by reference
+ if (_icc_profiles.find(hp) != _icc_profiles.end())
+ return _icc_profiles[hp];
+
+ std::string name = Inkscape::ColorProfile::getNameFromProfile(hp);
+ Inkscape::ColorProfile::sanitizeName(name);
+
+ // Find the named profile in the document (if already added)
+ if (_doc->getProfileManager().find(name.c_str()))
+ return name;
+
+ // Add the profile, we've never seen it before.
+ cmsUInt32Number len = 0;
+ cmsSaveProfileToMem(hp, nullptr, &len);
+ auto buf = (unsigned char *)malloc(len * sizeof(unsigned char));
+ cmsSaveProfileToMem(hp, buf, &len);
+
+ Inkscape::XML::Node *icc_node = _xml_doc->createElement("svg:color-profile");
+ std::string label = Inkscape::ColorProfile::getNameFromProfile(hp);
+ icc_node->setAttribute("inkscape:label", label);
+ icc_node->setAttribute("name", name);
+
+ auto *base64String = g_base64_encode(buf, len);
+ auto icc_data = std::string("data:application/vnd.iccprofile;base64,") + base64String;
+ g_free(base64String);
+ icc_node->setAttributeOrRemoveIfEmpty("xlink:href", icc_data);
+ _doc->getDefs()->getRepr()->appendChild(icc_node);
+ Inkscape::GC::release(icc_node);
+
+ free(buf);
+ _icc_profiles[hp] = name;
+ return name;
+}
+
+/**
+ * \brief Checks whether the given pattern type can be represented in SVG
+ * Used by PdfParser to decide when to do fallback operations.
+ */
+bool SvgBuilder::isPatternTypeSupported(GfxPattern *pattern) {
+ if ( pattern != nullptr ) {
+ if ( pattern->getType() == 2 ) { // shading pattern
+ GfxShading *shading = (static_cast<GfxShadingPattern *>(pattern))->getShading();
+ int shadingType = shading->getType();
+ if ( shadingType == 2 || // axial shading
+ shadingType == 3 ) { // radial shading
+ return true;
+ }
+ return false;
+ } else if ( pattern->getType() == 1 ) { // tiling pattern
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/**
+ * \brief Creates a pattern from poppler's data structure
+ * Handles linear and radial gradients. Creates a new PdfParser and uses it to
+ * build a tiling pattern.
+ * \return a url pointing to the created pattern
+ */
+gchar *SvgBuilder::_createPattern(GfxPattern *pattern, GfxState *state, bool is_stroke) {
+ gchar *id = nullptr;
+ if ( pattern != nullptr ) {
+ if ( pattern->getType() == 2 ) { // Shading pattern
+ GfxShadingPattern *shading_pattern = static_cast<GfxShadingPattern *>(pattern);
+ // construct a (pattern space) -> (current space) transform matrix
+ auto flip = Geom::Affine(1.0, 0.0, 0.0, -1.0, 0.0, _height);
+ auto pt = Geom::Scale(Inkscape::Util::Quantity::convert(1.0, "pt", "px"));
+ auto grad_affine = ctmToAffine(shading_pattern->getMatrix());
+ auto obj_affine = stateToAffine(state);
+ // SVG applies the object's affine on top of the gradient's affine,
+ // So we must remove the object affine to move it back into place.
+ auto affine = (grad_affine * pt * flip) * obj_affine.inverse();
+ id = _createGradient(shading_pattern->getShading(), affine, !is_stroke);
+ } else if ( pattern->getType() == 1 ) { // Tiling pattern
+ id = _createTilingPattern(static_cast<GfxTilingPattern*>(pattern), state, is_stroke);
+ }
+ } else {
+ return nullptr;
+ }
+ gchar *urltext = g_strdup_printf ("url(#%s)", id);
+ g_free(id);
+ return urltext;
+}
+
+/**
+ * \brief Creates a tiling pattern from poppler's data structure
+ * Creates a sub-page PdfParser and uses it to parse the pattern's content stream.
+ * \return id of the created pattern
+ */
+gchar *SvgBuilder::_createTilingPattern(GfxTilingPattern *tiling_pattern,
+ GfxState *state, bool is_stroke) {
+
+ Inkscape::XML::Node *pattern_node = _xml_doc->createElement("svg:pattern");
+ // Set pattern transform matrix
+ auto pat_matrix = ctmToAffine(tiling_pattern->getMatrix());
+ pattern_node->setAttributeOrRemoveIfEmpty("patternTransform", sp_svg_transform_write(pat_matrix));
+ pattern_node->setAttribute("patternUnits", "userSpaceOnUse");
+ // Set pattern tiling
+ // FIXME: don't ignore XStep and YStep
+ const double *bbox = tiling_pattern->getBBox();
+ pattern_node->setAttributeSvgDouble("x", 0.0);
+ pattern_node->setAttributeSvgDouble("y", 0.0);
+ pattern_node->setAttributeSvgDouble("width", bbox[2] - bbox[0]);
+ pattern_node->setAttributeSvgDouble("height", bbox[3] - bbox[1]);
+
+ // Convert BBox for PdfParser
+ PDFRectangle box;
+ box.x1 = bbox[0];
+ box.y1 = bbox[1];
+ box.x2 = bbox[2];
+ box.y2 = bbox[3];
+ // Create new SvgBuilder and sub-page PdfParser
+ SvgBuilder *pattern_builder = new SvgBuilder(this, pattern_node);
+ PdfParser *pdf_parser = new PdfParser(_xref, pattern_builder, tiling_pattern->getResDict(),
+ &box);
+ // Get pattern color space
+ GfxPatternColorSpace *pat_cs = (GfxPatternColorSpace *)( is_stroke ? state->getStrokeColorSpace()
+ : state->getFillColorSpace() );
+ // Set fill/stroke colors if this is an uncolored tiling pattern
+ GfxColorSpace *cs = nullptr;
+ if ( tiling_pattern->getPaintType() == 2 && ( cs = pat_cs->getUnder() ) ) {
+ GfxState *pattern_state = pdf_parser->getState();
+ pattern_state->setFillColorSpace(cs->copy());
+ pattern_state->setFillColor(state->getFillColor());
+ pattern_state->setStrokeColorSpace(cs->copy());
+ pattern_state->setStrokeColor(state->getFillColor());
+ }
+
+ // Generate the SVG pattern
+ pdf_parser->parse(tiling_pattern->getContentStream());
+
+ // Cleanup
+ delete pdf_parser;
+ delete pattern_builder;
+
+ // Append the pattern to defs
+ _doc->getDefs()->getRepr()->appendChild(pattern_node);
+ gchar *id = g_strdup(pattern_node->attribute("id"));
+ Inkscape::GC::release(pattern_node);
+
+ return id;
+}
+
+/**
+ * \brief Creates a linear or radial gradient from poppler's data structure
+ * \param shading poppler's data structure for the shading
+ * \param matrix gradient transformation, can be null
+ * \param for_shading true if we're creating this for a shading operator; false otherwise
+ * \return id of the created object
+ */
+gchar *SvgBuilder::_createGradient(GfxShading *shading, const Geom::Affine pat_matrix, bool for_shading)
+{
+ Inkscape::XML::Node *gradient;
+ _POPPLER_CONST Function *func;
+ int num_funcs;
+ bool extend0, extend1;
+
+ if ( shading->getType() == 2 ) { // Axial shading
+ gradient = _xml_doc->createElement("svg:linearGradient");
+ GfxAxialShading *axial_shading = static_cast<GfxAxialShading*>(shading);
+ double x1, y1, x2, y2;
+ axial_shading->getCoords(&x1, &y1, &x2, &y2);
+ gradient->setAttributeSvgDouble("x1", x1);
+ gradient->setAttributeSvgDouble("y1", y1);
+ gradient->setAttributeSvgDouble("x2", x2);
+ gradient->setAttributeSvgDouble("y2", y2);
+ extend0 = axial_shading->getExtend0();
+ extend1 = axial_shading->getExtend1();
+ num_funcs = axial_shading->getNFuncs();
+ func = axial_shading->getFunc(0);
+ } else if (shading->getType() == 3) { // Radial shading
+ gradient = _xml_doc->createElement("svg:radialGradient");
+ GfxRadialShading *radial_shading = static_cast<GfxRadialShading*>(shading);
+ double x1, y1, r1, x2, y2, r2;
+ radial_shading->getCoords(&x1, &y1, &r1, &x2, &y2, &r2);
+ // FIXME: the inner circle's radius is ignored here
+ gradient->setAttributeSvgDouble("fx", x1);
+ gradient->setAttributeSvgDouble("fy", y1);
+ gradient->setAttributeSvgDouble("cx", x2);
+ gradient->setAttributeSvgDouble("cy", y2);
+ gradient->setAttributeSvgDouble("r", r2);
+ extend0 = radial_shading->getExtend0();
+ extend1 = radial_shading->getExtend1();
+ num_funcs = radial_shading->getNFuncs();
+ func = radial_shading->getFunc(0);
+ } else { // Unsupported shading type
+ return nullptr;
+ }
+ gradient->setAttribute("gradientUnits", "userSpaceOnUse");
+ // If needed, flip the gradient transform around the y axis
+ if (pat_matrix != Geom::identity()) {
+ gradient->setAttributeOrRemoveIfEmpty("gradientTransform", sp_svg_transform_write(pat_matrix));
+ }
+
+ if ( extend0 && extend1 ) {
+ gradient->setAttribute("spreadMethod", "pad");
+ }
+
+ if ( num_funcs > 1 || !_addGradientStops(gradient, shading, func) ) {
+ Inkscape::GC::release(gradient);
+ return nullptr;
+ }
+
+ _doc->getDefs()->getRepr()->appendChild(gradient);
+ gchar *id = g_strdup(gradient->attribute("id"));
+ Inkscape::GC::release(gradient);
+
+ return id;
+}
+
+#define EPSILON 0.0001
+/**
+ * \brief Adds a stop with the given properties to the gradient's representation
+ */
+void SvgBuilder::_addStopToGradient(Inkscape::XML::Node *gradient, double offset, GfxColor *color, GfxColorSpace *space,
+ double opacity)
+{
+ Inkscape::XML::Node *stop = _xml_doc->createElement("svg:stop");
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ Inkscape::CSSOStringStream os_opacity;
+ std::string color_text = "#ffffff";
+ if (space->getMode() == csDeviceGray) {
+ // This is a transparency mask.
+ GfxRGB rgb;
+ space->getRGB(color, &rgb);
+ double gray = (double)rgb.r / 65535.0;
+ gray = CLAMP(gray, 0.0, 1.0);
+ os_opacity << gray;
+ } else {
+ os_opacity << opacity;
+ color_text = convertGfxColor(color, space);
+ }
+ sp_repr_css_set_property(css, "stop-opacity", os_opacity.str().c_str());
+ sp_repr_css_set_property(css, "stop-color", color_text.c_str());
+
+ sp_repr_css_change(stop, css, "style");
+ sp_repr_css_attr_unref(css);
+ stop->setAttributeCssDouble("offset", offset);
+
+ gradient->appendChild(stop);
+ Inkscape::GC::release(stop);
+}
+
+static bool svgGetShadingColor(GfxShading *shading, double offset, GfxColor *result)
+{
+ if ( shading->getType() == 2 ) { // Axial shading
+ (static_cast<GfxAxialShading *>(shading))->getColor(offset, result);
+ } else if ( shading->getType() == 3 ) { // Radial shading
+ (static_cast<GfxRadialShading *>(shading))->getColor(offset, result);
+ } else {
+ return false;
+ }
+ return true;
+}
+
+#define INT_EPSILON 8
+bool SvgBuilder::_addGradientStops(Inkscape::XML::Node *gradient, GfxShading *shading,
+ _POPPLER_CONST Function *func) {
+ int type = func->getType();
+ auto space = shading->getColorSpace();
+ if ( type == 0 || type == 2 ) { // Sampled or exponential function
+ GfxColor stop1, stop2;
+ if (!svgGetShadingColor(shading, 0.0, &stop1) || !svgGetShadingColor(shading, 1.0, &stop2)) {
+ return false;
+ } else {
+ _addStopToGradient(gradient, 0.0, &stop1, space, 1.0);
+ _addStopToGradient(gradient, 1.0, &stop2, space, 1.0);
+ }
+ } else if ( type == 3 ) { // Stitching
+ auto stitchingFunc = static_cast<_POPPLER_CONST StitchingFunction*>(func);
+ const double *bounds = stitchingFunc->getBounds();
+ const double *encode = stitchingFunc->getEncode();
+ int num_funcs = stitchingFunc->getNumFuncs();
+ // Adjust gradient so it's always between 0.0 - 1.0
+ double max_bound = std::max({1.0, bounds[num_funcs]});
+
+ // Add stops from all the stitched functions
+ GfxColor prev_color, color;
+ svgGetShadingColor(shading, bounds[0], &prev_color);
+ _addStopToGradient(gradient, bounds[0], &prev_color, space, 1.0);
+ for ( int i = 0 ; i < num_funcs ; i++ ) {
+ svgGetShadingColor(shading, bounds[i + 1], &color);
+ // Add stops
+ if (stitchingFunc->getFunc(i)->getType() == 2) { // process exponential fxn
+ double expE = (static_cast<_POPPLER_CONST ExponentialFunction*>(stitchingFunc->getFunc(i)))->getE();
+ if (expE > 1.0) {
+ expE = (bounds[i + 1] - bounds[i])/expE; // approximate exponential as a single straight line at x=1
+ if (encode[2*i] == 0) { // normal sequence
+ auto offset = (bounds[i + 1] - expE) / max_bound;
+ _addStopToGradient(gradient, offset, &prev_color, space, 1.0);
+ } else { // reflected sequence
+ auto offset = (bounds[i] + expE) / max_bound;
+ _addStopToGradient(gradient, offset, &color, space, 1.0);
+ }
+ }
+ }
+ _addStopToGradient(gradient, bounds[i + 1] / max_bound, &color, space, 1.0);
+ prev_color = color;
+ }
+ } else { // Unsupported function type
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * \brief Sets _invalidated_style to true to indicate that styles have to be updated
+ * Used for text output when glyphs are buffered till a font change
+ */
+void SvgBuilder::updateStyle(GfxState *state) {
+ if (_in_text_object) {
+ _invalidated_style = true;
+ }
+}
+
+/**
+ * \brief Updates _css_font according to the font set in parameter state
+ */
+void SvgBuilder::updateFont(GfxState *state, std::shared_ptr<CairoFont> cairo_font, bool flip)
+{
+ TRACE(("updateFont()\n"));
+ updateTextMatrix(state, flip); // Ensure that we have a text matrix built
+
+ auto font = state->getFont();
+ auto font_id = font->getID()->num;
+
+ auto new_font_size = state->getFontSize();
+ if (font->getType() == fontType3) {
+ const double *font_matrix = font->getFontMatrix();
+ if (font_matrix[0] != 0.0) {
+ new_font_size *= font_matrix[3] / font_matrix[0];
+ }
+ }
+ if (new_font_size != _css_font_size) {
+ _css_font_size = new_font_size;
+ _invalidated_style = true;
+ }
+ bool was_css_font = (bool)_css_font;
+ // Clean up any previous css font
+ if (_css_font) {
+ sp_repr_css_attr_unref(_css_font);
+ _css_font = nullptr;
+ }
+
+ auto font_strategy = FontFallback::AS_TEXT;
+ if (_font_strategies.find(font_id) != _font_strategies.end()) {
+ font_strategy = _font_strategies[font_id];
+ }
+
+ if (font_strategy == FontFallback::DELETE_TEXT) {
+ _invalidated_strategy = true;
+ _cairo_font = nullptr;
+ return;
+ }
+ if (font_strategy == FontFallback::AS_SHAPES) {
+ _invalidated_strategy = _invalidated_strategy || was_css_font;
+ _invalidated_style = (_cairo_font != cairo_font);
+ _cairo_font = cairo_font;
+ return;
+ }
+
+ auto font_data = FontData(font);
+ _font_specification = font_data.getSpecification().c_str();
+ _invalidated_strategy = (bool)_cairo_font;
+ _invalidated_style = true;
+
+ // Font family
+ _cairo_font = nullptr;
+ _css_font = sp_repr_css_attr_new();
+ if (font->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<SvgGlyph>::iterator i = _glyphs.begin();
+ const SvgGlyph& first_glyph = (*i);
+
+ // Ignore invisible characters
+ if (first_glyph.state->getRender() == 3) {
+ _glyphs.clear();
+ return;
+ }
+
+ // If cairo, then no text node is needed.
+ Inkscape::XML::Node *text_group = nullptr;
+ Inkscape::XML::Node *text_node = nullptr;
+ cairo_glyph_t *cairo_glyphs = nullptr;
+ unsigned int cairo_glyph_count = 0;
+
+ if (!first_glyph.cairo_font) {
+ // we preserve spaces in the text objects we create, this applies to any descendant
+ text_node = _addToContainer("svg:text");
+ text_node->setAttribute("xml:space", "preserve");
+ }
+
+ // Strip out text size from text_matrix and remove from text_transform
+ double text_scale = _text_matrix.expansionX();
+ Geom::Affine tr = stateToAffine(state);
+ Geom::Affine text_transform = _text_matrix * tr * Geom::Scale(text_scale).inverse();
+ // The glyph position must be moved by the document scale without flipping
+ // the text object itself. This is why the text affine is applied to the
+ // translation point and not simply used in the text element directly.
+ auto pos = first_glyph.position * tr;
+ text_transform.setTranslation(pos);
+ // Cache the text transform when clipping
+ if (_clip_text_group) {
+ svgSetTransform(_clip_text_group, text_transform);
+ }
+
+ bool new_tspan = true;
+ bool same_coords[2] = {true, true};
+ Geom::Point last_delta_pos;
+ unsigned int glyphs_in_a_row = 0;
+ Inkscape::XML::Node *tspan_node = nullptr;
+ Glib::ustring x_coords;
+ Glib::ustring y_coords;
+ Glib::ustring text_buffer;
+
+ // Output all buffered glyphs
+ while (true) {
+ const SvgGlyph& glyph = (*i);
+ auto prev_iterator = (i == _glyphs.begin()) ? _glyphs.end() : (i-1);
+ // Check if we need to make a new tspan
+ if (glyph.style_changed) {
+ new_tspan = true;
+ } else if ( i != _glyphs.begin() ) {
+ const SvgGlyph& prev_glyph = (*prev_iterator);
+ if (!((glyph.delta[Geom::Y] == 0.0 && prev_glyph.delta[Geom::Y] == 0.0 &&
+ glyph.text_position[1] == prev_glyph.text_position[1]) ||
+ (glyph.delta[Geom::X] == 0.0 && prev_glyph.delta[Geom::X] == 0.0 &&
+ glyph.text_position[0] == prev_glyph.text_position[0]))) {
+ new_tspan = true;
+ }
+ }
+
+ // Create tspan node if needed
+ if (!first_glyph.cairo_font && text_node && (new_tspan || i == _glyphs.end())) {
+ if (tspan_node) {
+ // Set the x and y coordinate arrays
+ if (same_coords[0]) {
+ tspan_node->setAttributeSvgDouble("x", last_delta_pos[0]);
+ } else {
+ tspan_node->setAttributeOrRemoveIfEmpty("x", x_coords);
+ }
+ if (same_coords[1]) {
+ tspan_node->setAttributeSvgDouble("y", last_delta_pos[1]);
+ } else {
+ tspan_node->setAttributeOrRemoveIfEmpty("y", y_coords);
+ }
+ TRACE(("tspan content: %s\n", text_buffer.c_str()));
+ if ( glyphs_in_a_row > 1 ) {
+ tspan_node->setAttribute("sodipodi:role", "line");
+ }
+ // Add text content node to tspan
+ Inkscape::XML::Node *text_content = _xml_doc->createTextNode(text_buffer.c_str());
+ tspan_node->appendChild(text_content);
+ Inkscape::GC::release(text_content);
+ text_node->appendChild(tspan_node);
+ // Clear temporary buffers
+ x_coords.clear();
+ y_coords.clear();
+ text_buffer.clear();
+ Inkscape::GC::release(tspan_node);
+ glyphs_in_a_row = 0;
+ }
+ if ( i == _glyphs.end() ) {
+ sp_repr_css_attr_unref((*prev_iterator).css_font);
+ break;
+ } else {
+ tspan_node = _xml_doc->createElement("svg:tspan");
+
+ // Set style and unref SPCSSAttr if it won't be needed anymore
+ // assume all <tspan> nodes in a <text> node share the same style
+ double text_size = text_scale * glyph.text_size;
+ sp_repr_css_set_property_double(glyph.css_font, "font-size", text_size);
+ _setTextStyle(tspan_node, glyph.state, glyph.css_font, text_transform);
+ if ( glyph.style_changed && i != _glyphs.begin() ) { // Free previous style
+ sp_repr_css_attr_unref((*prev_iterator).css_font);
+ }
+ }
+ new_tspan = false;
+ }
+ if ( glyphs_in_a_row > 0 && i != _glyphs.begin() ) {
+ x_coords.append(" ");
+ y_coords.append(" ");
+ // Check if we have the same coordinates
+ const SvgGlyph& prev_glyph = (*prev_iterator);
+ for ( int p = 0 ; p < 2 ; p++ ) {
+ if ( glyph.text_position[p] != prev_glyph.text_position[p] ) {
+ same_coords[p] = false;
+ }
+ }
+ }
+ // Append the coordinates to their respective strings
+ Geom::Point delta_pos(glyph.text_position - first_glyph.text_position);
+ delta_pos[1] += glyph.rise;
+ delta_pos[1] *= -1.0; // flip it
+ delta_pos *= Geom::Scale(text_scale);
+ Inkscape::CSSOStringStream os_x;
+ os_x << delta_pos[0];
+ x_coords.append(os_x.str());
+ Inkscape::CSSOStringStream os_y;
+ os_y << delta_pos[1];
+ y_coords.append(os_y.str());
+ last_delta_pos = delta_pos;
+
+ if (first_glyph.cairo_font) {
+ if (!cairo_glyphs) {
+ cairo_glyphs = (cairo_glyph_t *)gmallocn(_glyphs.size(), sizeof(cairo_glyph_t));
+ }
+ bool is_last_glyph = i + 1 == _glyphs.end();
+
+ // Push the data into the cairo glyph list for later rendering.
+ cairo_glyphs[cairo_glyph_count].index = glyph.cairo_index;
+ cairo_glyphs[cairo_glyph_count].x = delta_pos[Geom::X];
+ cairo_glyphs[cairo_glyph_count].y = delta_pos[Geom::Y];
+ cairo_glyph_count++;
+
+ bool style_will_change = is_last_glyph ? true : (i+1)->style_changed;
+ if (style_will_change) {
+ if (style_will_change && !is_last_glyph && !text_group) {
+ // We create a group, so each style can be contained within the resulting path.
+ text_group = _pushGroup();
+ }
+
+ // Render and set the style for this drawn text.
+ double text_size = text_scale * glyph.text_size;
+
+ // Set to 'text_node' because if the style does NOT change, we won't have a group
+ // but still need to set this text's position and blend modes.
+ text_node = _renderText(glyph.cairo_font, text_size, text_transform, cairo_glyphs, cairo_glyph_count);
+ if (text_node) {
+ _setTextStyle(text_node, glyph.state, nullptr, text_transform);
+ }
+
+ // Free up the used glyph stack.
+ gfree(cairo_glyphs);
+ cairo_glyphs = nullptr;
+ cairo_glyph_count = 0;
+
+ if (is_last_glyph) {
+ // Stop drawing text now, we have cleaned up.
+ break;
+ }
+ }
+ } else {
+ // Append the character to the text buffer
+ if (!glyph.code.empty()) {
+ text_buffer.append(1, glyph.code[0]);
+ }
+
+ /* Append any utf8 conversion doublets and request a new tspan.
+ *
+ * This is a fix for the unusual situation in some PDF files that use
+ * certain fonts where two ascii letters have been bolted together into
+ * one Unicode position and our conversion to UTF8 produces extra glyphs
+ * which if we don't add will be missing and if we add without ending the
+ * tspan will cause the rest of the glyph-positions to be off by one.
+ */
+ for (int j = 1; j < glyph.code.size(); j++) {
+ text_buffer.append(1, glyph.code[j]);
+ new_tspan = true;
+ }
+ }
+
+ glyphs_in_a_row++;
+ ++i;
+ }
+ if (text_group) {
+ // Pop the group so the clip and transform can be applied to it.
+ text_node = text_group;
+ _popGroup();
+ }
+
+ if (text_node) {
+ if (first_glyph.cairo_font) {
+ // Save aria-label for any rendered text blocks
+ text_node->setAttribute("aria-label", _aria_label);
+ }
+
+ // Set the text matrix which sits under the page's position
+ _setBlendMode(text_node, state);
+ svgSetTransform(text_node, text_transform * _page_affine);
+ _setClipPath(text_node);
+ }
+
+ _aria_label = "";
+ _glyphs.clear();
+}
+
+/**
+ * Sets the style for the text, rendered or un-rendered, preserving the text_transform for any
+ * gradients or other patterns. These values were promised to us when the font was updated.
+ */
+void SvgBuilder::_setTextStyle(Inkscape::XML::Node *node, GfxState *state, SPCSSAttr *font_style, Geom::Affine ta)
+{
+ int render_mode = state->getRender();
+ bool has_fill = !(render_mode & 1);
+ bool has_stroke = ( render_mode & 3 ) == 1 || ( render_mode & 3 ) == 2;
+
+ state = state->save();
+ state->setCTM(ta[0], ta[1], ta[2], ta[3], ta[4], ta[5]);
+ auto style = _setStyle(state, has_fill, has_stroke);
+ state = state->restore();
+ if (font_style) {
+ sp_repr_css_merge(style, font_style);
+ }
+ sp_repr_css_change(node, style, "style");
+ sp_repr_css_attr_unref(style);
+}
+
+/**
+ * Renders the text as a path object using cairo and returns the node object.
+ *
+ * cairo_font - The font that cairo can use to convert text to path.
+ * font_size - The size of the text when drawing the path.
+ * transform - The matrix which will place the text on the page, this is critical
+ * to allow cairo to render all the required parts of the text.
+ * cairo_glyphs - A pointer to a list of glyphs to render.
+ * count - A count of the number of glyphs to render.
+ */
+Inkscape::XML::Node *SvgBuilder::_renderText(std::shared_ptr<CairoFont> cairo_font, double font_size,
+ const Geom::Affine &transform,
+ cairo_glyph_t *cairo_glyphs, unsigned int count)
+{
+ if (!cairo_glyphs || !cairo_font || _aria_label.empty())
+ return nullptr;
+
+ // The surface isn't actually used, no rendering in cairo takes place.
+ cairo_surface_t *surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, _width, _height);
+ cairo_t *cairo = cairo_create(surface);
+ cairo_set_font_face(cairo, cairo_font->getFontFace());
+ cairo_set_font_size(cairo, font_size);
+ ink_cairo_transform(cairo, transform);
+ cairo_glyph_path(cairo, cairo_glyphs, count);
+ auto pathv = extract_pathvector_from_cairo(cairo);
+ cairo_destroy(cairo);
+ cairo_surface_destroy(surface);
+
+ // Failing to render text.
+ if (!pathv) {
+ g_warning("Failed to render PDF text!");
+ return nullptr;
+ }
+
+ auto textpath = sp_svg_write_path(*pathv);
+ if (textpath.empty())
+ return nullptr;
+
+ Inkscape::XML::Node *path = _addToContainer("svg:path");
+ path->setAttribute("d", textpath);
+ return path;
+}
+
+/**
+ * Begin and end string is the inner most text processing step
+ * which tells us we're about to have a certain number of chars.
+ */
+void SvgBuilder::beginString(GfxState *state, int len)
+{
+ if (!_glyphs.empty()) {
+ // What to do about unflushed text in the buffer.
+ if (_invalidated_strategy) {
+ _flushText(state);
+ _invalidated_strategy = false;
+ } else {
+ // Add seperator for aria text.
+ _aria_space = true;
+ }
+ }
+ IFTRACE(double *m = state->getTextMat());
+ TRACE(("tm: %f %f %f %f %f %f\n",m[0], m[1],m[2], m[3], m[4], m[5]));
+ IFTRACE(m = state->getCTM());
+ TRACE(("ctm: %f %f %f %f %f %f\n",m[0], m[1],m[2], m[3], m[4], m[5]));
+}
+void SvgBuilder::endString(GfxState *state)
+{
+}
+
+/**
+ * \brief Adds the specified character to the text buffer
+ * Takes care of converting it to UTF-8 and generates a new style repr if style
+ * has changed since the last call.
+ */
+void SvgBuilder::addChar(GfxState *state, double x, double y, double dx, double dy, double originX, double originY,
+ CharCode code, int /*nBytes*/, Unicode const *u, int uLen)
+{
+ if (_aria_space) {
+ const SvgGlyph& prev_glyph = _glyphs.back();
+ // This helps reconstruct the aria text, though it could be made better
+ if (prev_glyph.position[Geom::Y] != (y - originY)) {
+ _aria_label += "\n";
+ }
+ _aria_space = false;
+ }
+ static std::wstring_convert<std::codecvt_utf8<char32_t>, char32_t> conv1;
+ if (u) {
+ _aria_label += conv1.to_bytes(*u);
+ }
+
+ // Skip control characters, found in LaTeX generated PDFs
+ // https://gitlab.com/inkscape/inkscape/-/issues/1369
+ if (uLen > 0 && u[0] < 0x80 && g_ascii_iscntrl(u[0]) && !g_ascii_isspace(u[0])) {
+ g_warning("Skipping ASCII control character %u", u[0]);
+ _text_position += Geom::Point(dx, dy);
+ return;
+ }
+
+ if (!_css_font && !_cairo_font) {
+ // Deleted text.
+ return;
+ }
+
+ bool is_space = ( uLen == 1 && u[0] == 32 );
+ // Skip beginning space
+ if ( is_space && _glyphs.empty()) {
+ Geom::Point delta(dx, dy);
+ _text_position += delta;
+ return;
+ }
+ // Allow only one space in a row
+ if ( is_space && (_glyphs[_glyphs.size() - 1].code.size() == 1) &&
+ (_glyphs[_glyphs.size() - 1].code[0] == 32) ) {
+ Geom::Point delta(dx, dy);
+ _text_position += delta;
+ return;
+ }
+
+ SvgGlyph new_glyph;
+ new_glyph.is_space = is_space;
+ new_glyph.delta = Geom::Point(dx, dy);
+ new_glyph.position = Geom::Point( x - originX, y - originY );
+ new_glyph.text_position = _text_position;
+ new_glyph.text_size = _css_font_size;
+ new_glyph.state = state;
+ if (_cairo_font) {
+ new_glyph.cairo_font = _cairo_font;
+ new_glyph.cairo_index = _cairo_font->getGlyph(code, u, uLen);
+ }
+ _text_position += new_glyph.delta;
+
+ // Convert the character to UTF-8 since that's our SVG document's encoding
+ {
+ gunichar2 uu[8] = {0};
+
+ for (int i = 0; i < uLen; i++) {
+ uu[i] = u[i];
+ }
+
+ gchar *tmp = g_utf16_to_utf8(uu, uLen, nullptr, nullptr, nullptr);
+ if ( tmp && *tmp ) {
+ new_glyph.code = tmp;
+ } else {
+ new_glyph.code.clear();
+ }
+ g_free(tmp);
+ }
+
+ // Copy current style if it has changed since the previous glyph
+ if (_invalidated_style || _glyphs.empty()) {
+ _invalidated_style = false;
+ new_glyph.style_changed = true;
+ if (_css_font) {
+ new_glyph.css_font = sp_repr_css_attr_new();
+ sp_repr_css_merge(new_glyph.css_font, _css_font);
+ }
+ } else {
+ new_glyph.style_changed = false;
+ // Point to previous glyph's style information
+ const SvgGlyph& prev_glyph = _glyphs.back();
+ new_glyph.css_font = prev_glyph.css_font;
+ }
+ new_glyph.font_specification = _font_specification;
+ new_glyph.rise = state->getRise();
+
+ _glyphs.push_back(new_glyph);
+}
+
+/**
+ * These text object functions are the outer most calls for begining and
+ * ending text. No text functions should be called outside of these two calls
+ */
+void SvgBuilder::beginTextObject(GfxState *state) {
+ _in_text_object = true;
+ _invalidated_style = true; // Force copying of current state
+}
+
+void SvgBuilder::endTextObject(GfxState *state)
+{
+ _in_text_object = false;
+ _flushText(state);
+
+ if (_clip_text_group) {
+ // Use the clip as a real clip path
+ _clip_text = _popContainer();
+ _clip_text_group = nullptr;
+ }
+}
+
+/**
+ * Helper functions for supporting direct PNG output into a base64 encoded stream
+ */
+void png_write_vector(png_structp png_ptr, png_bytep data, png_size_t length)
+{
+ auto *v_ptr = reinterpret_cast<std::vector<guchar> *>(png_get_io_ptr(png_ptr)); // Get pointer to stream
+ for ( unsigned i = 0 ; i < length ; i++ ) {
+ v_ptr->push_back(data[i]);
+ }
+}
+
+/**
+ * \brief Creates an <image> element containing the given ImageStream as a PNG
+ *
+ */
+Inkscape::XML::Node *SvgBuilder::_createImage(Stream *str, int width, int height,
+ GfxImageColorMap *color_map, bool interpolate,
+ int *mask_colors, bool alpha_only,
+ bool invert_alpha) {
+
+ // Create PNG write struct
+ png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
+ if ( png_ptr == nullptr ) {
+ return nullptr;
+ }
+ // Create PNG info struct
+ png_infop info_ptr = png_create_info_struct(png_ptr);
+ if ( info_ptr == nullptr ) {
+ png_destroy_write_struct(&png_ptr, nullptr);
+ return nullptr;
+ }
+ // Set error handler
+ if (setjmp(png_jmpbuf(png_ptr))) {
+ png_destroy_write_struct(&png_ptr, &info_ptr);
+ return nullptr;
+ }
+ // Decide whether we should embed this image
+ bool embed_image = _preferences->getAttributeBoolean("embedImages", true);
+
+ // Set read/write functions
+ std::vector<guchar> png_buffer;
+ FILE *fp = nullptr;
+ gchar *file_name = nullptr;
+ if (embed_image) {
+ png_set_write_fn(png_ptr, &png_buffer, png_write_vector, nullptr);
+ } else {
+ static int counter = 0;
+ file_name = g_strdup_printf("%s_img%d.png", _docname, counter++);
+ fp = fopen(file_name, "wb");
+ if ( fp == nullptr ) {
+ png_destroy_write_struct(&png_ptr, &info_ptr);
+ g_free(file_name);
+ return nullptr;
+ }
+ png_init_io(png_ptr, fp);
+ }
+
+ // Set header data
+ if ( !invert_alpha && !alpha_only ) {
+ png_set_invert_alpha(png_ptr);
+ }
+ png_color_8 sig_bit;
+ if (alpha_only) {
+ png_set_IHDR(png_ptr, info_ptr,
+ width,
+ height,
+ 8, /* bit_depth */
+ PNG_COLOR_TYPE_GRAY,
+ PNG_INTERLACE_NONE,
+ PNG_COMPRESSION_TYPE_BASE,
+ PNG_FILTER_TYPE_BASE);
+ sig_bit.red = 0;
+ sig_bit.green = 0;
+ sig_bit.blue = 0;
+ sig_bit.gray = 8;
+ sig_bit.alpha = 0;
+ } else {
+ png_set_IHDR(png_ptr, info_ptr,
+ width,
+ height,
+ 8, /* bit_depth */
+ PNG_COLOR_TYPE_RGB_ALPHA,
+ PNG_INTERLACE_NONE,
+ PNG_COMPRESSION_TYPE_BASE,
+ PNG_FILTER_TYPE_BASE);
+ sig_bit.red = 8;
+ sig_bit.green = 8;
+ sig_bit.blue = 8;
+ sig_bit.alpha = 8;
+ }
+ png_set_sBIT(png_ptr, info_ptr, &sig_bit);
+ png_set_bgr(png_ptr);
+ // Write the file header
+ png_write_info(png_ptr, info_ptr);
+
+ // Convert pixels
+ ImageStream *image_stream;
+ if (alpha_only) {
+ if (color_map) {
+ image_stream = new ImageStream(str, width, color_map->getNumPixelComps(),
+ color_map->getBits());
+ } else {
+ image_stream = new ImageStream(str, width, 1, 1);
+ }
+ image_stream->reset();
+
+ // Convert grayscale values
+ unsigned char *buffer = new unsigned char[width];
+ int invert_bit = invert_alpha ? 1 : 0;
+ for ( int y = 0 ; y < height ; y++ ) {
+ unsigned char *row = image_stream->getLine();
+ if (color_map) {
+ color_map->getGrayLine(row, buffer, width);
+ } else {
+ unsigned char *buf_ptr = buffer;
+ for ( int x = 0 ; x < width ; x++ ) {
+ if ( row[x] ^ invert_bit ) {
+ *buf_ptr++ = 0;
+ } else {
+ *buf_ptr++ = 255;
+ }
+ }
+ }
+ png_write_row(png_ptr, (png_bytep)buffer);
+ }
+ delete [] buffer;
+ } else if (color_map) {
+ image_stream = new ImageStream(str, width,
+ color_map->getNumPixelComps(),
+ color_map->getBits());
+ image_stream->reset();
+
+ // Convert RGB values
+ unsigned int *buffer = new unsigned int[width];
+ if (mask_colors) {
+ for ( int y = 0 ; y < height ; y++ ) {
+ unsigned char *row = image_stream->getLine();
+ color_map->getRGBLine(row, buffer, width);
+
+ unsigned int *dest = buffer;
+ for ( int x = 0 ; x < width ; x++ ) {
+ // Check each color component against the mask
+ for ( int i = 0; i < color_map->getNumPixelComps() ; i++) {
+ if ( row[i] < mask_colors[2*i] * 255 ||
+ row[i] > mask_colors[2*i + 1] * 255 ) {
+ *dest = *dest | 0xff000000;
+ break;
+ }
+ }
+ // Advance to the next pixel
+ row += color_map->getNumPixelComps();
+ dest++;
+ }
+ // Write it to the PNG
+ png_write_row(png_ptr, (png_bytep)buffer);
+ }
+ } else {
+ for ( int i = 0 ; i < height ; i++ ) {
+ unsigned char *row = image_stream->getLine();
+ memset((void*)buffer, 0xff, sizeof(int) * width);
+ color_map->getRGBLine(row, buffer, width);
+ png_write_row(png_ptr, (png_bytep)buffer);
+ }
+ }
+ delete [] buffer;
+
+ } else { // A colormap must be provided, so quit
+ png_destroy_write_struct(&png_ptr, &info_ptr);
+ if (!embed_image) {
+ fclose(fp);
+ g_free(file_name);
+ }
+ return nullptr;
+ }
+ delete image_stream;
+ str->close();
+ // Close PNG
+ png_write_end(png_ptr, info_ptr);
+ png_destroy_write_struct(&png_ptr, &info_ptr);
+
+ // Create repr
+ Inkscape::XML::Node *image_node = _xml_doc->createElement("svg:image");
+ image_node->setAttributeSvgDouble("width", 1);
+ image_node->setAttributeSvgDouble("height", 1);
+ if( !interpolate ) {
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ // This should be changed after CSS4 Images widely supported.
+ sp_repr_css_set_property(css, "image-rendering", "optimizeSpeed");
+ sp_repr_css_change(image_node, css, "style");
+ sp_repr_css_attr_unref(css);
+ }
+
+ // PS/PDF images are placed via a transformation matrix, no preserveAspectRatio used
+ image_node->setAttribute("preserveAspectRatio", "none");
+
+ // Create href
+ if (embed_image) {
+ // Append format specification to the URI
+ auto *base64String = g_base64_encode(png_buffer.data(), png_buffer.size());
+ auto png_data = std::string("data:image/png;base64,") + base64String;
+ g_free(base64String);
+ image_node->setAttributeOrRemoveIfEmpty("xlink:href", png_data);
+ } else {
+ fclose(fp);
+ image_node->setAttribute("xlink:href", file_name);
+ g_free(file_name);
+ }
+
+ return image_node;
+}
+
+/**
+ * \brief Creates a <mask> with the specified width and height and adds to <defs>
+ * If we're not the top-level SvgBuilder, creates a <defs> too and adds the mask to it.
+ * \return the created XML node
+ */
+Inkscape::XML::Node *SvgBuilder::_createMask(double width, double height) {
+ Inkscape::XML::Node *mask_node = _xml_doc->createElement("svg:mask");
+ mask_node->setAttribute("maskUnits", "userSpaceOnUse");
+ mask_node->setAttributeSvgDouble("x", 0.0);
+ mask_node->setAttributeSvgDouble("y", 0.0);
+ mask_node->setAttributeSvgDouble("width", width);
+ mask_node->setAttributeSvgDouble("height", height);
+ // Append mask to defs
+ if (_is_top_level) {
+ _doc->getDefs()->getRepr()->appendChild(mask_node);
+ Inkscape::GC::release(mask_node);
+ return _doc->getDefs()->getRepr()->lastChild();
+ } else { // Work around for renderer bug when mask isn't defined in pattern
+ static int mask_count = 0;
+ gchar *mask_id = g_strdup_printf("_mask%d", mask_count++);
+ mask_node->setAttribute("id", mask_id);
+ g_free(mask_id);
+ _doc->getDefs()->getRepr()->appendChild(mask_node);
+ Inkscape::GC::release(mask_node);
+ return mask_node;
+ }
+}
+
+void SvgBuilder::addImage(GfxState *state, Stream *str, int width, int height, GfxImageColorMap *color_map,
+ bool interpolate, int *mask_colors)
+{
+ Inkscape::XML::Node *image_node = _createImage(str, width, height, color_map, interpolate, mask_colors);
+ if (image_node) {
+ _setBlendMode(image_node, state);
+ _setTransform(image_node, state, Geom::Affine(1.0, 0.0, 0.0, -1.0, 0.0, 1.0));
+ _addToContainer(image_node);
+ _setClipPath(image_node);
+ }
+}
+
+void SvgBuilder::addImageMask(GfxState *state, Stream *str, int width, int height,
+ bool invert, bool interpolate) {
+
+ // Create a rectangle
+ Inkscape::XML::Node *rect = _addToContainer("svg:rect");
+ rect->setAttributeSvgDouble("x", 0.0);
+ rect->setAttributeSvgDouble("y", 0.0);
+ rect->setAttributeSvgDouble("width", 1.0);
+ rect->setAttributeSvgDouble("height", 1.0);
+
+ // Get current fill style and set it on the rectangle
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ _setFillStyle(css, state, false);
+ sp_repr_css_change(rect, css, "style");
+ sp_repr_css_attr_unref(css);
+ _setBlendMode(rect, state);
+ _setTransform(rect, state, Geom::Affine(1.0, 0.0, 0.0, -1.0, 0.0, 1.0));
+ _setClipPath(rect);
+
+ // Scaling 1x1 surfaces might not work so skip setting a mask with this size
+ if ( width > 1 || height > 1 ) {
+ Inkscape::XML::Node *mask_image_node =
+ _createImage(str, width, height, nullptr, interpolate, nullptr, true, invert);
+ if (mask_image_node) {
+ // Create the mask
+ Inkscape::XML::Node *mask_node = _createMask(1.0, 1.0);
+ // Remove unnecessary transformation from the mask image
+ mask_image_node->removeAttribute("transform");
+ mask_node->appendChild(mask_image_node);
+ Inkscape::GC::release(mask_image_node);
+ gchar *mask_url = g_strdup_printf("url(#%s)", mask_node->attribute("id"));
+ rect->setAttribute("mask", mask_url);
+ g_free(mask_url);
+ }
+ }
+}
+
+void SvgBuilder::addMaskedImage(GfxState *state, Stream *str, int width, int height, GfxImageColorMap *color_map,
+ bool interpolate, Stream *mask_str, int mask_width, int mask_height, bool invert_mask,
+ bool mask_interpolate)
+{
+ Inkscape::XML::Node *mask_image_node = _createImage(mask_str, mask_width, mask_height,
+ nullptr, mask_interpolate, nullptr, true, invert_mask);
+ Inkscape::XML::Node *image_node = _createImage(str, width, height, color_map, interpolate, nullptr);
+ if ( mask_image_node && image_node ) {
+ // Create mask for the image
+ Inkscape::XML::Node *mask_node = _createMask(1.0, 1.0);
+ // Remove unnecessary transformation from the mask image
+ mask_image_node->removeAttribute("transform");
+ mask_node->appendChild(mask_image_node);
+ // Scale the mask to the size of the image
+ Geom::Affine mask_transform((double)width, 0.0, 0.0, (double)height, 0.0, 0.0);
+ mask_node->setAttributeOrRemoveIfEmpty("maskTransform", sp_svg_transform_write(mask_transform));
+ // Set mask and add image
+ gchar *mask_url = g_strdup_printf("url(#%s)", mask_node->attribute("id"));
+ image_node->setAttribute("mask", mask_url);
+ g_free(mask_url);
+ _setBlendMode(image_node, state);
+ _setTransform(image_node, state, Geom::Affine(1.0, 0.0, 0.0, -1.0, 0.0, 1.0));
+ _addToContainer(image_node);
+ _setClipPath(image_node);
+ } else if (image_node) {
+ Inkscape::GC::release(image_node);
+ }
+ if (mask_image_node) {
+ Inkscape::GC::release(mask_image_node);
+ }
+}
+
+void SvgBuilder::addSoftMaskedImage(GfxState *state, Stream *str, int width, int height, GfxImageColorMap *color_map,
+ bool interpolate, Stream *mask_str, int mask_width, int mask_height,
+ GfxImageColorMap *mask_color_map, bool mask_interpolate)
+{
+ Inkscape::XML::Node *mask_image_node = _createImage(mask_str, mask_width, mask_height,
+ mask_color_map, mask_interpolate, nullptr, true);
+ Inkscape::XML::Node *image_node = _createImage(str, width, height, color_map, interpolate, nullptr);
+ if ( mask_image_node && image_node ) {
+ // Create mask for the image
+ Inkscape::XML::Node *mask_node = _createMask(1.0, 1.0);
+ // Remove unnecessary transformation from the mask image
+ mask_image_node->removeAttribute("transform");
+ mask_node->appendChild(mask_image_node);
+ // Set mask and add image
+ gchar *mask_url = g_strdup_printf("url(#%s)", mask_node->attribute("id"));
+ image_node->setAttribute("mask", mask_url);
+ g_free(mask_url);
+ _addToContainer(image_node);
+ _setBlendMode(image_node, state);
+ _setTransform(image_node, state, Geom::Affine(1.0, 0.0, 0.0, -1.0, 0.0, 1.0));
+ _setClipPath(image_node);
+ } else if (image_node) {
+ Inkscape::GC::release(image_node);
+ }
+ if (mask_image_node) {
+ Inkscape::GC::release(mask_image_node);
+ }
+}
+
+/**
+ * Find the fill or stroke gradient we previously set on this node.
+ */
+Inkscape::XML::Node *SvgBuilder::_getGradientNode(Inkscape::XML::Node *node, bool is_fill)
+{
+ auto css = sp_repr_css_attr(node, "style");
+ if (auto id = try_extract_uri_id(css->attribute(is_fill ? "fill" : "stroke"))) {
+ if (auto obj = _doc->getObjectById(*id)) {
+ return obj->getRepr();
+ }
+ }
+ return nullptr;
+}
+
+bool SvgBuilder::_attrEqual(Inkscape::XML::Node *a, Inkscape::XML::Node *b, char const *attr)
+{
+ return (!a->attribute(attr) && !b->attribute(attr)) || std::string(a->attribute(attr)) == b->attribute(attr);
+}
+
+/**
+ * Take a constructed mask and decide how to apply it to the target.
+ */
+void SvgBuilder::applyOptionalMask(Inkscape::XML::Node *mask, Inkscape::XML::Node *target)
+{
+ // Merge transparency gradient back into real gradient if possible
+ if (mask->childCount() == 1) {
+ auto source = mask->firstChild();
+ auto source_gr = _getGradientNode(source, true);
+ auto target_gr = _getGradientNode(target, true);
+ // Both objects have a gradient, try and merge them
+ if (source_gr && target_gr && source_gr->childCount() == target_gr->childCount()) {
+ bool same_pos = _attrEqual(source_gr, target_gr, "x1") && _attrEqual(source_gr, target_gr, "x2")
+ && _attrEqual(source_gr, target_gr, "y1") && _attrEqual(source_gr, target_gr, "y2");
+
+ bool white_mask = false;
+ for (auto source_st = source_gr->firstChild(); source_st != nullptr; source_st = source_st->next()) {
+ auto source_css = sp_repr_css_attr(source_st, "style");
+ white_mask = white_mask or source_css->getAttributeDouble("stop-opacity") != 1.0;
+ if (std::string(source_css->attribute("stop-color")) != "#ffffff") {
+ white_mask = false;
+ break;
+ }
+ }
+
+ if (same_pos && white_mask) {
+ // We move the stop-opacity from the source to the target
+ auto target_st = target_gr->firstChild();
+ for (auto source_st = source_gr->firstChild(); source_st != nullptr; source_st = source_st->next()) {
+ auto target_css = sp_repr_css_attr(target_st, "style");
+ auto source_css = sp_repr_css_attr(source_st, "style");
+ sp_repr_css_set_property(target_css, "stop-opacity", source_css->attribute("stop-opacity"));
+ sp_repr_css_change(target_st, target_css, "style");
+ target_st = target_st->next();
+ }
+ // Remove mask and gradient xml objects
+ mask->parent()->removeChild(mask);
+ source_gr->parent()->removeChild(source_gr);
+ return;
+ }
+ }
+ }
+ gchar *mask_url = g_strdup_printf("url(#%s)", mask->attribute("id"));
+ target->setAttribute("mask", mask_url);
+ g_free(mask_url);
+}
+
+
+/**
+ * \brief Starts building a new transparency group
+ */
+void SvgBuilder::startGroup(GfxState *state, double *bbox, GfxColorSpace * /*blending_color_space*/, bool isolated,
+ bool knockout, bool for_softmask)
+{
+ // Push group node, but don't attach to previous container yet
+ _pushContainer("svg:g");
+
+ if (for_softmask) {
+ _mask_groups.push_back(state);
+ // Create a container for the mask
+ _pushContainer(_createMask(1.0, 1.0));
+ }
+
+ // TODO: In the future we could use state to insert transforms
+ // and then remove the inverse from the items added into the children
+ // to reduce the transformational duplication.
+}
+
+void SvgBuilder::finishGroup(GfxState *state, bool for_softmask)
+{
+ if (for_softmask) {
+ // Create mask
+ auto mask_node = _popContainer();
+ applyOptionalMask(mask_node, _container);
+ } else {
+ popGroup(state);
+ }
+}
+
+void SvgBuilder::popGroup(GfxState *state)
+{
+ // Restore node stack
+ auto parent = _popContainer();
+ bool will_clip = _clip_history->hasClipPath() && !_clip_history->isBoundingBox();
+
+ if (parent->childCount() == 1 && !parent->attribute("transform")) {
+ // Merge this opacity and remove unnecessary group
+ auto child = parent->firstChild();
+
+ if (will_clip && child->attribute("d")) {
+ // Note to future: this means the group contains a single path, this path is likely
+ // a fake bounding box path and the real path is contained within the clipping region
+ // Moving the clipping region out into the path object and deleting the group would
+ // improve output here.
+ }
+
+ // Do not merge masked or clipped groups, to avoid clobering
+ if (!will_clip && !child->attribute("mask") && !child->attribute("clip-path")) {
+ auto orig = child->getAttributeDouble("opacity", 1.0);
+ auto grp = parent->getAttributeDouble("opacity", 1.0);
+ child->setAttributeSvgDouble("opacity", orig * grp);
+
+ if (auto mask_id = try_extract_uri_id(parent->attribute("mask"))) {
+ if (auto obj = _doc->getObjectById(*mask_id)) {
+ applyOptionalMask(obj->getRepr(), child);
+ }
+ }
+ if (auto clip = parent->attribute("clip-path")) {
+ child->setAttribute("clip-path", clip);
+ }
+
+ // This duplicate child will get applied in the place of the group
+ parent->removeChild(child);
+ Inkscape::GC::anchor(child);
+ parent = child;
+ }
+ }
+
+ // Add the parent to the last container
+ _addToContainer(parent);
+ _setClipPath(parent);
+}
+
+/**
+ * Decide what to do for each font in the font list, with the given strategy.
+ */
+FontStrategies SvgBuilder::autoFontStrategies(FontStrategy s, FontList fonts)
+{
+ FontStrategies ret;
+ for (auto font : *fonts.get()) {
+ int id = font.first->getID()->num;
+ bool found = font.second.found;
+ switch (s) {
+ case FontStrategy::RENDER_ALL:
+ ret[id] = FontFallback::AS_SHAPES;
+ break;
+ case FontStrategy::DELETE_ALL:
+ ret[id] = FontFallback::DELETE_TEXT;
+ break;
+ case FontStrategy::RENDER_MISSING:
+ ret[id] = found ? FontFallback::AS_TEXT : FontFallback::AS_SHAPES;
+ break;
+ case FontStrategy::SUBSTITUTE_MISSING:
+ ret[id] = found ? FontFallback::AS_TEXT : FontFallback::AS_SUB;
+ break;
+ case FontStrategy::KEEP_MISSING:
+ ret[id] = FontFallback::AS_TEXT;
+ break;
+ case FontStrategy::DELETE_MISSING:
+ ret[id] = found ? FontFallback::AS_TEXT : FontFallback::DELETE_TEXT;
+ break;
+ }
+ }
+ return ret;
+}
+} } } /* namespace Inkscape, Extension, Internal */
+
+#endif /* HAVE_POPPLER */
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :