// 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 :