/***************************************************************************** * * This file is part of Mapnik (c++ mapping toolkit) * * Copyright (C) 2021 Artem Pavlenko * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA * *****************************************************************************/ #include #include #include #include #include #include #include #include #include #include #include #include #include MAPNIK_DISABLE_WARNING_PUSH #include #include "agg_ellipse.h" #include "agg_rounded_rect.h" #include "agg_span_gradient.h" #include "agg_color_rgba.h" #include #include #include #include #include #include MAPNIK_DISABLE_WARNING_POP #include #include #include #include #include #include namespace mapnik { namespace svg { using util::name_to_int; using util::operator"" _case; } // namespace svg } // namespace mapnik BOOST_FUSION_ADAPT_STRUCT(mapnik::svg::viewbox, (double, x0)(double, y0)(double, width)(double, height)) namespace mapnik { namespace svg { namespace rapidxml = boost::property_tree::detail::rapidxml; void traverse_tree(svg_parser& parser, rapidxml::xml_node const* node); void end_element(svg_parser& parser, rapidxml::xml_node const* node); void parse_path(svg_parser& parser, rapidxml::xml_node const* node); void parse_element(svg_parser& parser, char const* name, rapidxml::xml_node const* node); void parse_use(svg_parser& parser, rapidxml::xml_node const* node); void parse_dimensions(svg_parser& parser, rapidxml::xml_node const* node); void parse_polygon(svg_parser& parser, rapidxml::xml_node const* node); void parse_polyline(svg_parser& parser, rapidxml::xml_node const* node); void parse_line(svg_parser& parser, rapidxml::xml_node const* node); void parse_rect(svg_parser& parser, rapidxml::xml_node const* node); void parse_circle(svg_parser& parser, rapidxml::xml_node const* node); void parse_ellipse(svg_parser& parser, rapidxml::xml_node const* node); void parse_linear_gradient(svg_parser& parser, rapidxml::xml_node const* node); void parse_radial_gradient(svg_parser& parser, rapidxml::xml_node const* node); bool parse_common_gradient(svg_parser& parser, std::string const& id, mapnik::gradient& gr, rapidxml::xml_node const* node); void parse_gradient_stop(svg_parser& parser, mapnik::gradient& gr, rapidxml::xml_node const* node); void parse_attr(svg_parser& parser, rapidxml::xml_node const* node); void parse_attr(svg_parser& parser, char const* name, char const* value); namespace { static std::array const unsupported_elements{{"symbol"_case, "marker"_case, "view"_case, "text"_case, "switch"_case, "image"_case, "a"_case, "clipPath"_case, "pattern"_case}}; static std::array const unsupported_attributes{{"alignment-baseline"_case, "baseline-shift"_case, "clip"_case, "clip-path"_case, "clip-rule"_case, "color-interpolation"_case, "color-interpolation-filters"_case, "color-profile"_case, "color-rendering"_case, "cursor"_case, "direction"_case, "dominant-baseline"_case, "enable-background"_case, "filter"_case, "flood-color"_case, "flood-opacity"_case, "font-family"_case, "font-size-adjust"_case, "font-stretch"_case, "font-style"_case, "font-variant"_case, "font-weight"_case, "glyph-orientation-horizontal"_case, "glyph-orientation-vertical"_case, "image-rendering"_case, "kerning"_case, "letter-spacing"_case, "lighting-color"_case, "marker-end"_case, "marker-mid"_case, "marker-start"_case, "mask"_case, "overflow"_case, "pointer-events"_case, "shape-rendering"_case, "text-anchor"_case, "text-decoration"_case, "text-rendering"_case, "unicode-bidi"_case, "word-spacing"_case, "writing-mode"_case}}; template void handle_unsupported(svg_parser& parser, T const& ar, char const* name, char const* type) { unsigned element = name_to_int(name); for (auto const& e : ar) { if (e == element) { parser.err_handler().on_error( std::string("SVG support error: <" + std::string(name) + "> " + std::string(type) + " is not supported")); break; } } } namespace grammar { namespace x3 = boost::spirit::x3; using color_lookup_type = std::vector>; using pairs_type = std::vector>; x3::rule const key_value_sequence_ordered("key_value_sequence_ordered"); x3::rule> const key_value("key_value"); x3::rule const key("key"); x3::rule const value("value"); auto const key_def = x3::char_("a-zA-Z_") > *x3::char_("a-zA-Z_0-9-"); auto const value_def = +(x3::char_ - ';'); auto const key_value_def = key > -(':' > value); auto const key_value_sequence_ordered_def = key_value % ';'; BOOST_SPIRIT_DEFINE(key, value, key_value, key_value_sequence_ordered); } // namespace grammar } // namespace boost::property_tree::detail::rapidxml::xml_attribute const* parse_id(svg_parser& parser, rapidxml::xml_node const* node) { auto const* id_attr = node->first_attribute("xml:id"); if (id_attr == nullptr) id_attr = node->first_attribute("id"); if (id_attr && parser.node_cache_.count(id_attr->value()) == 0) { parser.node_cache_.emplace(id_attr->value(), node); } return id_attr; } boost::property_tree::detail::rapidxml::xml_attribute const* parse_href(rapidxml::xml_node const* node) { auto const* attr = node->first_attribute("xlink:href"); if (attr == nullptr) attr = node->first_attribute("href"); return attr; } template mapnik::color parse_color(T& err_handler, const char* str) { mapnik::color c(100, 100, 100); try { c = mapnik::parse_color(str); } catch (mapnik::config_error const& ex) { err_handler.on_error("SVG parse error: failed to parse with value \"" + std::string(str) + "\""); } return c; } template agg::rgba8 parse_color_agg(T& err_handler, const char* str) { auto c = parse_color(err_handler, str); return agg::rgba8(c.red(), c.green(), c.blue(), c.alpha()); } template double parse_double(T& err_handler, const char* str) { using namespace boost::spirit::x3; double val = 0.0; if (!parse(str, str + std::strlen(str), double_, val)) { err_handler.on_error("SVG parse error: failed to parse with value \"" + std::string(str) + "\""); } return val; } // https://www.w3.org/TR/SVG/coords.html#Units template double parse_svg_value(T& parser, char const* str, bool& is_percent) { namespace x3 = boost::spirit::x3; double val = 0.0; css_unit_value units; const char* cur = str; // phrase_parse mutates the first iterator const char* end = str + std::strlen(str); double font_size = parser.font_sizes_.back(); bool is_percent_; auto apply_value = [&](auto const& ctx) { val = _attr(ctx); is_percent_ = false; }; auto apply_units = [&](auto const& ctx) { val *= _attr(ctx); }; auto apply_em = [&](auto const& ctx) { val *= font_size; }; auto apply_percent = [&](auto const& ctx) { val *= 0.01; is_percent_ = true; }; if (!x3::phrase_parse(cur, end, x3::double_[apply_value] > -(units[apply_units] | x3::lit("em")[apply_em] | x3::lit('%')[apply_percent]), x3::space) || (cur != end)) { val = 0.0; // restore to default on parsing failure parser.err_handler().on_error("SVG parse error: failed to parse with value \"" + std::string(str) + "\""); } else { is_percent = is_percent_; // update only on success } return val; } template bool parse_font_size(T& parser, char const* str) { namespace x3 = boost::spirit::x3; double val = 0.0; css_unit_value units; css_absolute_size absolute; css_relative_size relative; std::size_t size = parser.font_sizes_.size(); double parent_font_size = size > 1 ? parser.font_sizes_[size - 2] : 10.0; const char* cur = str; // phrase_parse mutates the first iterator const char* end = str + std::strlen(str); auto apply_value = [&](auto const& ctx) { val = _attr(ctx); }; auto apply_relative = [&](auto const& ctx) { val = parent_font_size * _attr(ctx); }; auto apply_units = [&](auto const& ctx) { val *= _attr(ctx); }; auto apply_percent = [&](auto const& ctx) { val = val * parent_font_size / 100.0; }; auto apply_em = [&](auto const& ctx) { val = val * parent_font_size; }; if (!x3::phrase_parse(cur, end, absolute[apply_value] | relative[apply_relative] | x3::double_[apply_value] > -(units[apply_units] | x3::lit("em")[apply_em] | x3::lit('%')[apply_percent]), x3::space) || (cur != end)) { parser.err_handler().on_error("SVG parse error: failed to parse with value \"" + std::string(str) + "\""); return false; } if (!parser.font_sizes_.empty()) parser.font_sizes_.back() = val; return true; } template bool parse_viewbox(T& err_handler, char const* str, V& viewbox) { namespace x3 = boost::spirit::x3; if (!x3::phrase_parse(str, str + std::strlen(str), x3::double_ > -x3::lit(',') > x3::double_ > -x3::lit(',') > x3::double_ > -x3::lit(',') > x3::double_, x3::space, viewbox)) { err_handler.on_error("SVG parse error: failed to parse with value \"" + std::string(str) + "\""); return false; } return true; } enum aspect_ratio_alignment { none = 0, xMinYMin, xMidYMin, xMaxYMin, xMinYMid, xMidYMid, xMaxYMid, xMinYMax, xMidYMax, xMaxYMax }; template std::pair parse_preserve_aspect_ratio(T& err_handler, char const* str) { namespace x3 = boost::spirit::x3; x3::symbols align; align.add("none", none)("xMinYMin", xMinYMin)("xMidYMin", xMidYMin)("xMaxYMin", xMaxYMin)("xMinYMid", xMinYMid)( "xMidYMid", xMidYMid)("xMaxYMid", xMaxYMid)("xMinYMax", xMinYMax)("xMidYMax", xMidYMax)("xMaxYMax", xMaxYMax); std::pair preserve_aspect_ratio{xMidYMid, true}; char const* cur = str; // phrase_parse mutates the first iterator char const* end = str + std::strlen(str); auto apply_align = [&](auto const& ctx) { preserve_aspect_ratio.first = _attr(ctx); }; auto apply_slice = [&](auto const& ctx) { preserve_aspect_ratio.second = false; }; try { x3::phrase_parse(cur, end, -x3::lit("defer") // only applicable to which we don't support currently > align[apply_align] > -(x3::lit("meet") | x3::lit("slice")[apply_slice]), x3::space); } catch (x3::expectation_failure const& ex) { err_handler.on_error("SVG parse error: failed to parse with value \"" + std::string(str) + "\""); return {xMidYMid, true}; // default } return preserve_aspect_ratio; } bool parse_style(char const* str, grammar::pairs_type& v) { namespace x3 = boost::spirit::x3; return x3::phrase_parse(str, str + std::strlen(str), grammar::key_value_sequence_ordered, x3::space, v); } bool parse_id_from_url(char const* str, std::string& id) { namespace x3 = boost::spirit::x3; auto extract_id = [&](auto const& ctx) { id += _attr(ctx); }; return x3::phrase_parse(str, str + std::strlen(str), x3::lit("url") > '(' > '#' > *(x3::char_ - ')')[extract_id] > ')', x3::space); } void process_css(svg_parser& parser, rapidxml::xml_node const* node) { auto const& css = parser.css_data_; std::unordered_map style; std::string element_name = node->name(); auto itr = css.find(element_name); if (itr != css.end()) { for (auto const& def : std::get<1>(*itr)) { style[std::get<0>(def)] = std::get<1>(def); } } auto const* class_attr = node->first_attribute("class"); if (class_attr != nullptr) { auto const* value = class_attr->value(); if (std::strlen(value) > 0) { char const* first = value; char const* last = first + std::strlen(value); std::vector classes; bool result = boost::spirit::x3::phrase_parse(first, last, mapnik::classes(), mapnik::skipper(), classes); if (result) { for (auto const& class_name : classes) { std::string solitary_class_key = std::string(".") + class_name; auto range = css.equal_range(solitary_class_key); for (auto itr = range.first; itr != range.second; ++itr) { for (auto const& def : std::get<1>(*itr)) { style[std::get<0>(def)] = std::get<1>(def); } } std::string class_key = element_name + solitary_class_key; range = css.equal_range(class_key); for (auto itr = range.first; itr != range.second; ++itr) { for (auto const& def : std::get<1>(*itr)) { style[std::get<0>(def)] = std::get<1>(def); } } } } } } auto const* id_attr = node->first_attribute("id"); if (id_attr != nullptr) { auto const* value = id_attr->value(); if (std::strlen(value) > 0) { std::string id_key = std::string("#") + value; itr = css.find(id_key); if (itr != css.end()) { for (auto const& def : std::get<1>(*itr)) { style[std::get<0>(def)] = std::get<1>(def); } } } } // If style has properties, create or update style attribute if (style.size() > 0) { std::ostringstream ss; for (auto const& def : style) { auto const& r = std::get<1>(def); std::string val{r.begin(), r.end()}; parse_attr(parser, std::get<0>(def).c_str(), val.c_str()); } } } void traverse_tree(svg_parser& parser, rapidxml::xml_node const* node) { if (parser.ignore_) return; auto name = name_to_int(node->name()); switch (node->type()) { case rapidxml::node_element: { parser.font_sizes_.push_back(parser.font_sizes_.back()); switch (name) { case "defs"_case: { if (node->first_node() != nullptr) { parser.is_defs_ = true; } break; } case "clipPath"_case: case "symbol"_case: case "pattern"_case: { parser.ignore_ = true; break; } // the gradient tags *should* be in defs, but illustrator seems not to put them in there so // accept them anywhere case "linearGradient"_case: parse_linear_gradient(parser, node); break; case "radialGradient"_case: parse_radial_gradient(parser, node); break; } if (!parser.is_defs_) // FIXME { switch (name) { case "g"_case: if (node->first_node() != nullptr) { parser.path_.push_attr(); parse_id(parser, node); if (parser.css_style_) process_css(parser, node); parse_attr(parser, node); parser.path_.begin_group(); } break; case "use"_case: parser.path_.push_attr(); parse_id(parser, node); if (parser.css_style_) process_css(parser, node); parse_attr(parser, node); if (parser.path_.cur_attr().opacity < 1.0) parser.path_.begin_group(); parse_use(parser, node); if (parser.path_.cur_attr().opacity < 1.0) parser.path_.end_group(); parser.path_.pop_attr(); break; default: parser.path_.push_attr(); parse_id(parser, node); if (parser.css_style_) process_css(parser, node); parse_attr(parser, node); if (parser.path_.display()) { if (parser.path_.cur_attr().opacity < 1.0) parser.path_.begin_group(); parse_element(parser, node->name(), node); if (parser.path_.cur_attr().opacity < 1.0) parser.path_.end_group(); } parser.path_.pop_attr(); } } else { // save node for later parse_id(parser, node); } if ("style"_case == name) { //