1670 lines
53 KiB
C++
1670 lines
53 KiB
C++
/*****************************************************************************
|
|
*
|
|
* 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 <mapnik/debug.hpp>
|
|
#include <mapnik/css/css_unit_value.hpp>
|
|
#include <mapnik/color_factory.hpp>
|
|
#include <mapnik/svg/svg_parser.hpp>
|
|
#include <mapnik/svg/svg_path_parser.hpp>
|
|
#include <mapnik/config_error.hpp>
|
|
#include <mapnik/safe_cast.hpp>
|
|
#include <mapnik/svg/svg_parser_exception.hpp>
|
|
#include <mapnik/util/file_io.hpp>
|
|
#include <mapnik/util/utf_conv_win.hpp>
|
|
#include <mapnik/util/dasharray_parser.hpp>
|
|
#include <mapnik/util/name_to_int.hpp>
|
|
#include <mapnik/warning.hpp>
|
|
MAPNIK_DISABLE_WARNING_PUSH
|
|
#include <mapnik/warning_ignore_agg.hpp>
|
|
#include "agg_ellipse.h"
|
|
#include "agg_rounded_rect.h"
|
|
#include "agg_span_gradient.h"
|
|
#include "agg_color_rgba.h"
|
|
#include <mapnik/warning_ignore.hpp>
|
|
#include <boost/spirit/home/x3.hpp>
|
|
#include <boost/fusion/adapted/struct.hpp>
|
|
#include <boost/fusion/include/std_pair.hpp>
|
|
#include <boost/algorithm/string/predicate.hpp>
|
|
#include <boost/property_tree/detail/xml_parser_read_rapidxml.hpp>
|
|
MAPNIK_DISABLE_WARNING_POP
|
|
|
|
#include <string>
|
|
#include <stdexcept>
|
|
#include <vector>
|
|
#include <cstring>
|
|
#include <fstream>
|
|
#include <array>
|
|
|
|
namespace mapnik { namespace svg {
|
|
|
|
using util::name_to_int;
|
|
using util::operator"" _case;
|
|
}}
|
|
|
|
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<char> const* node);
|
|
void end_element(svg_parser& parser, rapidxml::xml_node<char> const* node);
|
|
void parse_path(svg_parser& parser, rapidxml::xml_node<char> const* node);
|
|
void parse_element(svg_parser& parser, char const* name, rapidxml::xml_node<char> const* node);
|
|
void parse_use(svg_parser& parser, rapidxml::xml_node<char> const* node);
|
|
void parse_dimensions(svg_parser& parser, rapidxml::xml_node<char> const* node);
|
|
void parse_polygon(svg_parser& parser, rapidxml::xml_node<char> const* node);
|
|
void parse_polyline(svg_parser& parser, rapidxml::xml_node<char> const* node);
|
|
void parse_line(svg_parser& parser, rapidxml::xml_node<char> const* node);
|
|
void parse_rect(svg_parser& parser, rapidxml::xml_node<char> const* node);
|
|
void parse_circle(svg_parser& parser, rapidxml::xml_node<char> const* node);
|
|
void parse_ellipse(svg_parser& parser, rapidxml::xml_node<char> const* node);
|
|
void parse_linear_gradient(svg_parser& parser, rapidxml::xml_node<char> const* node);
|
|
void parse_radial_gradient(svg_parser& parser, rapidxml::xml_node<char> const* node);
|
|
bool parse_common_gradient(svg_parser& parser, std::string const& id,
|
|
mapnik::gradient& gr, rapidxml::xml_node<char> const* node);
|
|
void parse_gradient_stop(svg_parser& parser, mapnik::gradient& gr, rapidxml::xml_node<char> const* node);
|
|
void parse_attr(svg_parser& parser, rapidxml::xml_node<char> const* node);
|
|
void parse_attr(svg_parser& parser, char const* name, char const* value);
|
|
|
|
namespace {
|
|
|
|
static std::array<unsigned, 9> const unsupported_elements
|
|
{ {"symbol"_case,
|
|
"marker"_case,
|
|
"view"_case,
|
|
"text"_case,
|
|
"switch"_case,
|
|
"image"_case,
|
|
"a"_case,
|
|
"clipPath"_case,
|
|
"pattern"_case}
|
|
};
|
|
|
|
static std::array<unsigned, 42> 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 <typename T>
|
|
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<std::pair<double, agg::rgba8> >;
|
|
using pairs_type = std::vector<std::pair<std::string, std::string> >;
|
|
|
|
x3::rule<class key_value_sequence_ordered_tag, pairs_type> const key_value_sequence_ordered("key_value_sequence_ordered");
|
|
x3::rule<class key_value_tag, std::pair<std::string, std::string> > const key_value("key_value");
|
|
x3::rule<class key_tag, std::string> const key("key");
|
|
x3::rule<class value_tag, std::string> 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);
|
|
|
|
}}
|
|
|
|
|
|
boost::property_tree::detail::rapidxml::xml_attribute<char> const * parse_id(svg_parser & parser, rapidxml::xml_node<char> 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<char> const * parse_href(rapidxml::xml_node<char> const* node)
|
|
{
|
|
auto const* attr = node->first_attribute("xlink:href");
|
|
if (attr == nullptr) attr = node->first_attribute("href");
|
|
return attr;
|
|
}
|
|
|
|
template <typename T>
|
|
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 <color> with value \"" + std::string(str) + "\"");
|
|
}
|
|
return c;
|
|
}
|
|
|
|
template <typename T>
|
|
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 <typename T>
|
|
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 <number> with value \"" + std::string(str) + "\"");
|
|
}
|
|
return val;
|
|
}
|
|
|
|
// https://www.w3.org/TR/SVG/coords.html#Units
|
|
template <typename T>
|
|
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 <number> with value \"" + std::string(str) + "\"");
|
|
}
|
|
is_percent = is_percent_; // update only on success
|
|
return val;
|
|
}
|
|
|
|
template <typename T>
|
|
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 <font-size> with value \"" + std::string(str) + "\"");
|
|
return false;
|
|
}
|
|
if (!parser.font_sizes_.empty()) parser.font_sizes_.back() = val;
|
|
return true;
|
|
}
|
|
|
|
template <typename T, typename V>
|
|
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 <viewbox> 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 <typename T>
|
|
std::pair<unsigned,bool> parse_preserve_aspect_ratio(T & err_handler, char const* str)
|
|
{
|
|
namespace x3 = boost::spirit::x3;
|
|
x3::symbols<unsigned> 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<unsigned,bool> 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 <image> which we don't support currently
|
|
> align[apply_align]
|
|
> -(x3::lit("meet") | x3::lit("slice")[apply_slice]), x3::space);
|
|
}
|
|
catch (x3::expectation_failure<char const*> const& ex)
|
|
{
|
|
err_handler.on_error("SVG parse error: failed to parse <preserveAspectRatio> 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<char> const* node)
|
|
{
|
|
auto const& css = parser.css_data_;
|
|
std::unordered_map<std::string, mapnik::property_value_type> 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<std::string> 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<char> 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);
|
|
}
|
|
break;
|
|
case "use"_case:
|
|
parser.path_.push_attr();
|
|
parse_id(parser, node);
|
|
if (parser.css_style_)
|
|
process_css(parser, node);
|
|
parse_attr(parser, node);
|
|
parse_use(parser, node);
|
|
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())
|
|
{
|
|
parse_element(parser, node->name(), node);
|
|
}
|
|
parser.path_.pop_attr();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// save node for later
|
|
parse_id(parser, node);
|
|
}
|
|
|
|
if ("style"_case == name)
|
|
{
|
|
// <style> element is not expected to have nested elements
|
|
// we're only interested in DATA or CDATA
|
|
for (auto const* child = node->first_node();
|
|
child; child = child->next_sibling())
|
|
{
|
|
if (child->type() == rapidxml::node_data ||
|
|
child->type() == rapidxml::node_cdata)
|
|
{
|
|
auto const grammar = mapnik::grammar();
|
|
auto const skipper = mapnik::skipper();
|
|
char const* first = child->value();
|
|
char const* last = first + child->value_size();
|
|
bool result = boost::spirit::x3::phrase_parse(first, last, grammar, skipper, parser.css_data_);
|
|
if (result && first == last && !parser.css_data_.empty())
|
|
{
|
|
parser.css_style_ = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (auto const* child = node->first_node();
|
|
child; child = child->next_sibling())
|
|
{
|
|
traverse_tree(parser, child);
|
|
}
|
|
}
|
|
|
|
end_element(parser, node);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
void end_element(svg_parser & parser, rapidxml::xml_node<char> const* node)
|
|
{
|
|
parser.font_sizes_.pop_back();
|
|
auto name = name_to_int(node->name());
|
|
if (!parser.is_defs_ && (name == "g"_case))
|
|
{
|
|
if (node->first_node() != nullptr)
|
|
{
|
|
parser.path_.pop_attr();
|
|
}
|
|
}
|
|
else if (name == "svg"_case)
|
|
{
|
|
if (node->first_node() != nullptr)
|
|
{
|
|
parser.path_.pop_attr();
|
|
}
|
|
}
|
|
else if (name == "defs"_case)
|
|
{
|
|
if (node->first_node() != nullptr)
|
|
{
|
|
parser.is_defs_ = false;
|
|
}
|
|
}
|
|
else if(name == "clipPath"_case || name == "symbol"_case || name == "pattern"_case)
|
|
{
|
|
parser.ignore_ = false;
|
|
handle_unsupported(parser, unsupported_elements, node->name(), "element");
|
|
}
|
|
}
|
|
|
|
void parse_element(svg_parser & parser, char const* name, rapidxml::xml_node<char> const* node)
|
|
{
|
|
switch (name_to_int(name))
|
|
{
|
|
case "path"_case:
|
|
parser.path_.transform().multiply(parser.viewbox_tr_);
|
|
parse_path(parser, node);
|
|
break;
|
|
case "polygon"_case:
|
|
parser.path_.transform().multiply(parser.viewbox_tr_);
|
|
parse_polygon(parser, node);
|
|
break;
|
|
case "polyline"_case:
|
|
parser.path_.transform().multiply(parser.viewbox_tr_);
|
|
parse_polyline(parser, node);
|
|
break;
|
|
case "line"_case:
|
|
parser.path_.transform().multiply(parser.viewbox_tr_);
|
|
parse_line(parser, node);
|
|
break;
|
|
case "rect"_case:
|
|
parser.path_.transform().multiply(parser.viewbox_tr_);
|
|
parse_rect(parser, node);
|
|
break;
|
|
case "circle"_case:
|
|
parser.path_.transform().multiply(parser.viewbox_tr_);
|
|
parse_circle(parser, node);
|
|
break;
|
|
case "ellipse"_case:
|
|
parser.path_.transform().multiply(parser.viewbox_tr_);
|
|
parse_ellipse(parser, node);
|
|
break;
|
|
case "svg"_case:
|
|
parser.path_.push_attr();
|
|
parse_dimensions(parser, node);
|
|
parse_attr(parser, node);
|
|
break;
|
|
default:
|
|
handle_unsupported(parser, unsupported_elements, name, "element");
|
|
break;
|
|
}
|
|
}
|
|
|
|
void parse_stroke(svg_parser& parser, char const* value)
|
|
{
|
|
std::string id;
|
|
if (std::strcmp(value, "none") == 0)
|
|
{
|
|
parser.path_.stroke_none();
|
|
}
|
|
else if (parse_id_from_url(value, id))
|
|
{
|
|
// see if we have a known gradient stroke
|
|
if (parser.gradient_map_.count(id) > 0)
|
|
{
|
|
parser.path_.add_stroke_gradient(parser.gradient_map_[id]);
|
|
}
|
|
else if (parser.node_cache_.count(id) > 0)
|
|
{
|
|
// try parsing again
|
|
auto const* gradient_node = parser.node_cache_[id];
|
|
traverse_tree(parser, gradient_node);
|
|
if (parser.gradient_map_.count(id) > 0)
|
|
{
|
|
parser.path_.add_stroke_gradient(parser.gradient_map_[id]);
|
|
}
|
|
else
|
|
{
|
|
std::stringstream ss;
|
|
ss << "SVG parse error: failed to locate stroke with <id> \"" << id << "\"";
|
|
parser.err_handler().on_error(ss.str());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
std::stringstream ss;
|
|
ss << "SVG parse error: failed to locate stroke with <id> \"" << id << "\"";
|
|
parser.err_handler().on_error(ss.str());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
parser.path_.stroke(parse_color_agg(parser.err_handler(), value));
|
|
}
|
|
}
|
|
|
|
void parse_fill(svg_parser& parser, char const* value)
|
|
{
|
|
std::string id;
|
|
if (std::strcmp(value, "none") == 0)
|
|
{
|
|
parser.path_.fill_none();
|
|
}
|
|
else if (parse_id_from_url(value, id))
|
|
{
|
|
// see if we have a known gradient fill
|
|
if (parser.gradient_map_.count(id) > 0)
|
|
{
|
|
parser.path_.add_fill_gradient(parser.gradient_map_[id]);
|
|
}
|
|
else if (parser.node_cache_.count(id) > 0)
|
|
{
|
|
// try parsing again
|
|
auto const* gradient_node = parser.node_cache_[id];
|
|
traverse_tree(parser, gradient_node);
|
|
if (parser.gradient_map_.count(id) > 0)
|
|
{
|
|
parser.path_.add_fill_gradient(parser.gradient_map_[id]);
|
|
}
|
|
else
|
|
{
|
|
std::stringstream ss;
|
|
ss << "SVG parse error: failed to locate fill with <id> \"" << id << "\"";
|
|
parser.err_handler().on_error(ss.str());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
std::stringstream ss;
|
|
ss << "SVG parse error: failed to locate fill with <id> \"" << id << "\"";
|
|
parser.err_handler().on_error(ss.str());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
parser.path_.fill(parse_color_agg(parser.err_handler(), value));
|
|
}
|
|
}
|
|
|
|
void parse_transform(svg_parser & parser, char const* value)
|
|
{
|
|
agg::trans_affine tr;
|
|
mapnik::svg::parse_svg_transform(value,tr);
|
|
parser.path_.transform().premultiply(tr);
|
|
}
|
|
|
|
void parse_stroke_dash(svg_parser & parser, char const* value)
|
|
{
|
|
dash_array dash;
|
|
if (util::parse_dasharray(value, dash))
|
|
{
|
|
parser.path_.dash_array(std::move(dash));
|
|
}
|
|
}
|
|
|
|
void parse_attr(svg_parser & parser, char const* name, char const* value )
|
|
{
|
|
switch (name_to_int(name))
|
|
{
|
|
case "transform"_case:
|
|
parse_transform(parser, value);
|
|
break;
|
|
case "fill"_case:
|
|
parse_fill(parser, value);
|
|
break;
|
|
case "fill-opacity"_case:
|
|
parser.path_.fill_opacity(parse_double(parser.err_handler(), value));
|
|
break;
|
|
case "fill-rule"_case:
|
|
if (std::strcmp(value, "evenodd") == 0)
|
|
{
|
|
parser.path_.even_odd(true);
|
|
}
|
|
break;
|
|
case "stroke"_case:
|
|
parse_stroke(parser, value);
|
|
break;
|
|
case "stroke-width"_case:
|
|
{
|
|
bool percent = false;
|
|
double stroke_width = parse_svg_value(parser, value, percent);
|
|
if (percent && parser.vbox_) stroke_width *= parser.normalized_diagonal_;
|
|
parser.path_.stroke_width(stroke_width);
|
|
break;
|
|
}
|
|
case "stroke-opacity"_case:
|
|
parser.path_.stroke_opacity(parse_double(parser.err_handler(), value));
|
|
break;
|
|
case "stroke-linecap"_case:
|
|
if(std::strcmp(value, "butt") == 0)
|
|
parser.path_.line_cap(agg::butt_cap);
|
|
else if(std::strcmp(value, "round") == 0)
|
|
parser.path_.line_cap(agg::round_cap);
|
|
else if(std::strcmp(value, "square") == 0)
|
|
parser.path_.line_cap(agg::square_cap);
|
|
break;
|
|
case "stroke-linejoin"_case:
|
|
if (std::strcmp(value, "miter") == 0)
|
|
parser.path_.line_join(agg::miter_join);
|
|
else if (std::strcmp(value, "round") == 0)
|
|
parser.path_.line_join(agg::round_join);
|
|
else if (std::strcmp(value, "bevel") == 0)
|
|
parser.path_.line_join(agg::bevel_join);
|
|
break;
|
|
case "stroke-miterlimit"_case:
|
|
parser.path_.miter_limit(parse_double(parser.err_handler(),value));
|
|
break;
|
|
case "stroke-dasharray"_case:
|
|
parse_stroke_dash(parser, value);
|
|
break;
|
|
case "stroke-dashoffset"_case:
|
|
parser.path_.dash_offset(parse_double(parser.err_handler(), value));
|
|
break;
|
|
case "opacity"_case:
|
|
parser.path_.opacity(parse_double(parser.err_handler(), value));
|
|
break;
|
|
case "visibility"_case:
|
|
parser.path_.visibility(std::strcmp(value, "hidden") != 0);
|
|
break;
|
|
case "display"_case:
|
|
if (std::strcmp(value, "none") == 0)
|
|
{
|
|
parser.path_.display(false);
|
|
}
|
|
break;
|
|
case "font-size"_case:
|
|
{
|
|
parse_font_size(parser, value);
|
|
break;
|
|
}
|
|
default:
|
|
handle_unsupported(parser, unsupported_attributes, name, "attribute");
|
|
break;
|
|
}
|
|
}
|
|
|
|
void parse_attr(svg_parser & parser, rapidxml::xml_node<char> const* node)
|
|
{
|
|
for (rapidxml::xml_attribute<char> const* attr = node->first_attribute();
|
|
attr; attr = attr->next_attribute())
|
|
{
|
|
auto const* name = attr->name();
|
|
if (std::strcmp(name, "style") == 0)
|
|
{
|
|
using cont_type = std::vector<std::pair<std::string,std::string> >;
|
|
using value_type = cont_type::value_type;
|
|
cont_type vec;
|
|
parse_style(attr->value(), vec);
|
|
for (value_type kv : vec )
|
|
{
|
|
parse_attr(parser, kv.first.c_str(), kv.second.c_str());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
parse_attr(parser,name, attr->value());
|
|
}
|
|
}
|
|
}
|
|
|
|
void parse_dimensions(svg_parser & parser, rapidxml::xml_node<char> const* node)
|
|
{
|
|
// https://www.w3.org/TR/SVG11/struct.html#SVGElement
|
|
double width = 1; // 100%
|
|
double height = 1; // 100%
|
|
viewbox vbox = {0, 0, 0, 0};
|
|
bool has_percent_height = true;
|
|
bool has_percent_width = true;
|
|
|
|
auto const* width_attr = node->first_attribute("width");
|
|
if (width_attr)
|
|
{
|
|
width = parse_svg_value(parser, width_attr->value(), has_percent_width);
|
|
}
|
|
auto const* height_attr = node->first_attribute("height");
|
|
if (height_attr)
|
|
{
|
|
height = parse_svg_value(parser, height_attr->value(), has_percent_height);
|
|
}
|
|
parser.vbox_ = viewbox{0, 0, width, height} ;
|
|
auto const* viewbox_attr = node->first_attribute("viewBox");
|
|
if (viewbox_attr && parse_viewbox(parser.err_handler(), viewbox_attr->value(), vbox))
|
|
{
|
|
agg::trans_affine t{};
|
|
parser.vbox_ = vbox;
|
|
parser.normalized_diagonal_ = std::sqrt(vbox.width * vbox.width + vbox.height * vbox.height)/std::sqrt(2.0);
|
|
|
|
if (has_percent_width) width = vbox.width * width;
|
|
else if (!width_attr || width == 0) width = vbox.width;
|
|
if (has_percent_height) height = vbox.height * height;
|
|
else if (!height_attr || height == 0) height = vbox.height;
|
|
|
|
if (width > 0 && height > 0 && vbox.width > 0 && vbox.height > 0)
|
|
{
|
|
std::pair<unsigned,bool> preserve_aspect_ratio {xMidYMid, true};
|
|
auto const* aspect_ratio_attr = node->first_attribute("preserveAspectRatio");
|
|
if (aspect_ratio_attr)
|
|
{
|
|
preserve_aspect_ratio = parse_preserve_aspect_ratio(parser.err_handler(), aspect_ratio_attr->value());
|
|
}
|
|
|
|
double sx = width / vbox.width;
|
|
double sy = height / vbox.height;
|
|
double scale = preserve_aspect_ratio.second ? std::min(sx, sy) : std::max(sx, sy);
|
|
switch (preserve_aspect_ratio.first)
|
|
{
|
|
case none:
|
|
t = agg::trans_affine_scaling(sx, sy) * t;
|
|
break;
|
|
case xMinYMin:
|
|
t = agg::trans_affine_scaling(scale, scale) * t;
|
|
break;
|
|
case xMinYMid:
|
|
t = agg::trans_affine_scaling(scale, scale) * t;
|
|
t = agg::trans_affine_translation(0, -0.5 * (vbox.height - height / scale)) * t;
|
|
break;
|
|
case xMinYMax:
|
|
t = agg::trans_affine_scaling(scale, scale) * t;
|
|
t = agg::trans_affine_translation(0, -1.0 * (vbox.height - height / scale)) * t;
|
|
break;
|
|
case xMidYMin:
|
|
t = agg::trans_affine_scaling(scale, scale) * t;
|
|
t = agg::trans_affine_translation(-0.5 * (vbox.width - width / scale), 0.0) * t;
|
|
break;
|
|
case xMidYMid: // (the default)
|
|
t = agg::trans_affine_scaling(scale, scale) * t;
|
|
t = agg::trans_affine_translation(-0.5 * (vbox.width - width / scale),
|
|
-0.5 * (vbox.height - height / scale)) * t;
|
|
break;
|
|
case xMidYMax:
|
|
t = agg::trans_affine_scaling(scale, scale) * t;
|
|
t = agg::trans_affine_translation(-0.5 * (vbox.width - width / scale),
|
|
-1.0 * (vbox.height - height / scale)) * t;
|
|
break;
|
|
case xMaxYMin:
|
|
t = agg::trans_affine_scaling(scale, scale) * t;
|
|
t = agg::trans_affine_translation(-1.0 * (vbox.width - width / scale), 0.0) * t;
|
|
break;
|
|
case xMaxYMid:
|
|
t = agg::trans_affine_scaling(scale, scale) * t;
|
|
t = agg::trans_affine_translation(-1.0 * (vbox.width - width / scale),
|
|
-0.5 * (vbox.height - height / scale)) * t;
|
|
break;
|
|
case xMaxYMax:
|
|
t = agg::trans_affine_scaling(scale, scale) * t;
|
|
t = agg::trans_affine_translation(-1.0 * (vbox.width - width / scale),
|
|
-1.0 * (vbox.height - height / scale)) * t;
|
|
break;
|
|
};
|
|
}
|
|
t = agg::trans_affine_translation(-vbox.x0, -vbox.y0) * t;
|
|
parser.viewbox_tr_ = t;
|
|
}
|
|
else if (width == 0 || height == 0 || has_percent_width || has_percent_height)
|
|
{
|
|
std::stringstream ss;
|
|
ss << "SVG parse error: can't infer valid image dimensions from width:\"";
|
|
if (has_percent_width) ss << width * 100 << "%";
|
|
else ss << width;
|
|
ss << "\" height:\"";
|
|
if (has_percent_height) ss << height * 100 << "%";
|
|
else ss << height;
|
|
ss << "\"";
|
|
parser.err_handler().on_error(ss.str());
|
|
parser.path_.set_dimensions(0, 0);
|
|
return;
|
|
}
|
|
parser.path_.set_dimensions(width, height);
|
|
}
|
|
|
|
void parse_path(svg_parser & parser, rapidxml::xml_node<char> const* node)
|
|
{
|
|
auto const* attr = node->first_attribute("d");
|
|
if (attr != nullptr)
|
|
{
|
|
auto const* value = attr->value();
|
|
if (std::strlen(value) > 0)
|
|
{
|
|
parser.path_.begin_path();
|
|
|
|
if (!mapnik::svg::parse_path(value, parser.path_))
|
|
{
|
|
auto const* id_attr = parse_id(parser, node);
|
|
if (id_attr)
|
|
{
|
|
parser.err_handler().on_error(std::string("SVG parse error: failed to parse <path> with <id> \"")
|
|
+ id_attr->value() + "\"");
|
|
}
|
|
else
|
|
{
|
|
parser.err_handler().on_error(std::string("SVG parse error: failed to parse <path>"));
|
|
}
|
|
}
|
|
parser.path_.end_path();
|
|
}
|
|
}
|
|
}
|
|
|
|
void parse_use(svg_parser & parser, rapidxml::xml_node<char> const* node)
|
|
{
|
|
auto * attr = parse_href(node);
|
|
if (attr)
|
|
{
|
|
auto const* value = attr->value();
|
|
if (std::strlen(value) > 1 && value[0] == '#')
|
|
{
|
|
std::string id(&value[1]);
|
|
if (parser.node_cache_.count(id) > 0)
|
|
{
|
|
auto const* base_node = parser.node_cache_[id];
|
|
double x = 0.0;
|
|
double y = 0.0;
|
|
double w = 0.0;
|
|
double h = 0.0;
|
|
bool percent = false;
|
|
attr = node->first_attribute("x");
|
|
if (attr != nullptr)
|
|
{
|
|
x = parse_svg_value(parser, attr->value(), percent);
|
|
}
|
|
|
|
attr = node->first_attribute("y");
|
|
if (attr != nullptr)
|
|
{
|
|
y = parse_svg_value(parser, attr->value(), percent);
|
|
}
|
|
|
|
attr = node->first_attribute("width");
|
|
if (attr != nullptr)
|
|
{
|
|
w = parse_svg_value(parser, attr->value(), percent);
|
|
if (percent) w *= parser.path_.width();
|
|
}
|
|
attr = node->first_attribute("height");
|
|
if (attr)
|
|
{
|
|
h = parse_svg_value(parser, attr->value(), percent);
|
|
if (percent) h *= parser.path_.height();
|
|
}
|
|
if (w < 0.0)
|
|
{
|
|
std::stringstream ss;
|
|
ss << "SVG validation error: invalid <use> width \"" << w << "\"";
|
|
parser.err_handler().on_error(ss.str());
|
|
}
|
|
else if (h < 0.0)
|
|
{
|
|
std::stringstream ss;
|
|
ss << "SVG validation error: invalid <use> height \"" << w << "\"";
|
|
parser.err_handler().on_error(ss.str());
|
|
}
|
|
agg::trans_affine t{};
|
|
if (!node->first_attribute("transform") && w != 0.0 && h != 0.0)
|
|
{
|
|
// FIXME
|
|
double scale = std::min(double(w / parser.path_.width()), double(h / parser.path_.height()));
|
|
t *= agg::trans_affine_scaling(scale);
|
|
}
|
|
t *= agg::trans_affine_translation(x, y);
|
|
parser.path_.transform().premultiply(t);
|
|
traverse_tree(parser, base_node);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void parse_polygon(svg_parser & parser, rapidxml::xml_node<char> const* node)
|
|
{
|
|
auto const* attr = node->first_attribute("points");
|
|
if (attr != nullptr)
|
|
{
|
|
parser.path_.begin_path();
|
|
if (!mapnik::svg::parse_points(attr->value(), parser.path_))
|
|
{
|
|
parser.err_handler().on_error(std::string("SVG parse error: failed to parse <polygon> points"));
|
|
}
|
|
parser.path_.close_subpath();
|
|
parser.path_.end_path();
|
|
}
|
|
}
|
|
|
|
void parse_polyline(svg_parser & parser, rapidxml::xml_node<char> const* node)
|
|
{
|
|
auto const* attr = node->first_attribute("points");
|
|
if (attr != nullptr)
|
|
{
|
|
parser.path_.begin_path();
|
|
if (!mapnik::svg::parse_points(attr->value(), parser.path_))
|
|
{
|
|
parser.err_handler().on_error(std::string("SVG parse error: failed to parse <polyline> points"));
|
|
}
|
|
parser.path_.end_path();
|
|
}
|
|
}
|
|
|
|
void parse_line(svg_parser & parser, rapidxml::xml_node<char> const* node)
|
|
{
|
|
double x1 = 0.0;
|
|
double y1 = 0.0;
|
|
double x2 = 0.0;
|
|
double y2 = 0.0;
|
|
bool percent = false;
|
|
auto const* x1_attr = node->first_attribute("x1");
|
|
if (x1_attr)
|
|
{
|
|
x1 = parse_svg_value(parser, x1_attr->value(), percent);
|
|
if (percent && parser.vbox_) x1 *= parser.vbox_->width;
|
|
}
|
|
auto const* y1_attr = node->first_attribute("y1");
|
|
if (y1_attr)
|
|
{
|
|
y1 = parse_svg_value(parser, y1_attr->value(), percent);
|
|
if (percent && parser.vbox_) y1 *= parser.vbox_->height;
|
|
}
|
|
|
|
auto const* x2_attr = node->first_attribute("x2");
|
|
if (x2_attr)
|
|
{
|
|
x2 = parse_svg_value(parser, x2_attr->value(), percent);
|
|
if (percent && parser.vbox_) x2 *= parser.vbox_->width;
|
|
}
|
|
auto const* y2_attr = node->first_attribute("y2");
|
|
if (y2_attr)
|
|
{
|
|
y2 = parse_svg_value(parser, y2_attr->value(), percent);
|
|
if (percent && parser.vbox_) y2 *= parser.vbox_->height;
|
|
}
|
|
parser.path_.begin_path();
|
|
parser.path_.move_to(x1, y1);
|
|
parser.path_.line_to(x2, y2);
|
|
parser.path_.end_path();
|
|
}
|
|
|
|
void parse_circle(svg_parser & parser, rapidxml::xml_node<char> const* node)
|
|
{
|
|
double cx = 0.0;
|
|
double cy = 0.0;
|
|
double r = 0.0;
|
|
bool percent = false;
|
|
auto * attr = node->first_attribute("cx");
|
|
if (attr != nullptr)
|
|
{
|
|
cx = parse_svg_value(parser, attr->value(), percent);
|
|
if (percent && parser.vbox_) cx *= parser.vbox_->width;
|
|
}
|
|
|
|
attr = node->first_attribute("cy");
|
|
if (attr != nullptr)
|
|
{
|
|
cy = parse_svg_value(parser, attr->value(), percent);
|
|
if (percent && parser.vbox_) cy *= parser.vbox_->height;
|
|
}
|
|
|
|
attr = node->first_attribute("r");
|
|
if (attr != nullptr)
|
|
{
|
|
r = parse_svg_value(parser, attr->value(), percent);
|
|
if (percent && parser.vbox_) r *= parser.normalized_diagonal_;
|
|
}
|
|
|
|
if (r != 0.0)
|
|
{
|
|
if (r < 0.0)
|
|
{
|
|
std::stringstream ss;
|
|
ss << "SVG validation error: invalid <circle> radius \"" << r << "\"";
|
|
parser.err_handler().on_error(ss.str());
|
|
}
|
|
else
|
|
{
|
|
parser.path_.begin_path();
|
|
agg::ellipse c(cx, cy, r, r);
|
|
parser.path_.storage().concat_path(c);
|
|
parser.path_.end_path();
|
|
}
|
|
}
|
|
}
|
|
|
|
void parse_ellipse(svg_parser & parser, rapidxml::xml_node<char> const * node)
|
|
{
|
|
double cx = 0.0;
|
|
double cy = 0.0;
|
|
double rx = 0.0;
|
|
double ry = 0.0;
|
|
bool percent = false;
|
|
auto * attr = node->first_attribute("cx");
|
|
if (attr != nullptr)
|
|
{
|
|
cx = parse_svg_value(parser, attr->value(), percent);
|
|
if (percent && parser.vbox_) cx *= parser.vbox_->width;
|
|
}
|
|
|
|
attr = node->first_attribute("cy");
|
|
if (attr)
|
|
{
|
|
cy = parse_svg_value(parser, attr->value(), percent);
|
|
if (percent && parser.vbox_) cy *= parser.vbox_->height;
|
|
}
|
|
|
|
attr = node->first_attribute("rx");
|
|
if (attr != nullptr)
|
|
{
|
|
rx = parse_svg_value(parser, attr->value(), percent);
|
|
if (percent && parser.vbox_) rx *= parser.normalized_diagonal_;
|
|
}
|
|
|
|
attr = node->first_attribute("ry");
|
|
if (attr != nullptr)
|
|
{
|
|
ry = parse_svg_value(parser, attr->value(), percent);
|
|
if (percent && parser.vbox_) ry *= parser.normalized_diagonal_;
|
|
}
|
|
|
|
if (rx != 0.0 && ry != 0.0)
|
|
{
|
|
if (rx < 0.0)
|
|
{
|
|
std::stringstream ss;
|
|
ss << "SVG validation error: invalid <ellipse> rx \"" << rx << "\"";
|
|
parser.err_handler().on_error(ss.str());
|
|
}
|
|
else if (ry < 0.0)
|
|
{
|
|
std::stringstream ss;
|
|
ss << "SVG validation error: invalid <ellipse> ry \"" << ry << "\"";
|
|
parser.err_handler().on_error(ss.str());
|
|
}
|
|
else
|
|
{
|
|
parser.path_.begin_path();
|
|
agg::ellipse c(cx, cy, rx, ry);
|
|
parser.path_.storage().concat_path(c);
|
|
parser.path_.end_path();
|
|
}
|
|
}
|
|
}
|
|
|
|
void parse_rect(svg_parser & parser, rapidxml::xml_node<char> const* node)
|
|
{
|
|
// http://www.w3.org/TR/SVGTiny12/shapes.html#RectElement
|
|
double x = 0.0;
|
|
double y = 0.0;
|
|
double w = 0.0;
|
|
double h = 0.0;
|
|
double rx = 0.0;
|
|
double ry = 0.0;
|
|
bool percent = false;
|
|
auto * attr = node->first_attribute("x");
|
|
if (attr != nullptr)
|
|
{
|
|
x = parse_svg_value(parser, attr->value(), percent);
|
|
if (percent && parser.vbox_) x *= parser.vbox_->width;
|
|
}
|
|
|
|
attr = node->first_attribute("y");
|
|
if (attr != nullptr)
|
|
{
|
|
y = parse_svg_value(parser, attr->value(), percent);
|
|
if (percent && parser.vbox_) y *= parser.vbox_->height;
|
|
}
|
|
|
|
attr = node->first_attribute("width");
|
|
if (attr != nullptr)
|
|
{
|
|
w = parse_svg_value(parser, attr->value(), percent);
|
|
if (percent && parser.vbox_) w *= parser.vbox_->width;
|
|
}
|
|
attr = node->first_attribute("height");
|
|
if (attr)
|
|
{
|
|
h = parse_svg_value(parser, attr->value(), percent);
|
|
if (percent && parser.vbox_) h *= parser.vbox_->height;
|
|
}
|
|
|
|
bool rounded = true;
|
|
attr = node->first_attribute("rx");
|
|
if (attr != nullptr)
|
|
{
|
|
rx = parse_svg_value(parser, attr->value(), percent);
|
|
if (percent && parser.vbox_) rx *= parser.vbox_->width;
|
|
if ( rx > 0.5 * w ) rx = 0.5 * w;
|
|
}
|
|
else rounded = false;
|
|
|
|
attr = node->first_attribute("ry");
|
|
if (attr != nullptr)
|
|
{
|
|
ry = parse_svg_value(parser, attr->value(), percent);
|
|
if (percent && parser.vbox_) ry *= parser.vbox_->height;
|
|
if ( ry > 0.5 * h ) ry = 0.5 * h;
|
|
if (!rounded)
|
|
{
|
|
rx = ry;
|
|
rounded = true;
|
|
}
|
|
}
|
|
else if (rounded)
|
|
{
|
|
ry = rx;
|
|
}
|
|
|
|
if (w != 0.0 && h != 0.0)
|
|
{
|
|
if (w < 0.0)
|
|
{
|
|
std::stringstream ss;
|
|
ss << "SVG validation error: invalid <rect> width \"" << w << "\"";
|
|
parser.err_handler().on_error(ss.str());
|
|
}
|
|
else if (h < 0.0)
|
|
{
|
|
std::stringstream ss;
|
|
ss << "SVG validation error: invalid <rect> height \"" << h << "\"";
|
|
parser.err_handler().on_error(ss.str());
|
|
}
|
|
else if (rx < 0.0)
|
|
{
|
|
std::stringstream ss;
|
|
ss << "SVG validation error: invalid <rect> rx \"" << rx << "\"";
|
|
parser.err_handler().on_error(ss.str());
|
|
}
|
|
else if (ry < 0.0)
|
|
{
|
|
std::stringstream ss;
|
|
ss << "SVG validation error: invalid <rect> ry \"" << ry << "\"";
|
|
parser.err_handler().on_error(ss.str());
|
|
}
|
|
else
|
|
{
|
|
parser.path_.begin_path();
|
|
|
|
if(rounded)
|
|
{
|
|
agg::rounded_rect r;
|
|
r.rect(x,y,x+w,y+h);
|
|
r.radius(rx,ry);
|
|
parser.path_.storage().concat_path(r);
|
|
}
|
|
else
|
|
{
|
|
parser.path_.move_to(x, y);
|
|
parser.path_.line_to(x + w, y);
|
|
parser.path_.line_to(x + w, y + h);
|
|
parser.path_.line_to(x, y + h);
|
|
parser.path_.close_subpath();
|
|
}
|
|
parser.path_.end_path();
|
|
}
|
|
}
|
|
}
|
|
|
|
void parse_gradient_stop(svg_parser & parser, mapnik::gradient& gr, rapidxml::xml_node<char> const* node)
|
|
{
|
|
double offset = 0.0;
|
|
mapnik::color stop_color{0,0,0,255};
|
|
double opacity = 1.0;
|
|
|
|
auto * attr = node->first_attribute("offset");
|
|
if (attr != nullptr)
|
|
{
|
|
bool percent = false;
|
|
offset = parse_svg_value(parser,attr->value(), percent);
|
|
}
|
|
|
|
attr = node->first_attribute("style");
|
|
if (attr != nullptr)
|
|
{
|
|
using cont_type = std::vector<std::pair<std::string,std::string> >;
|
|
using value_type = cont_type::value_type;
|
|
cont_type vec;
|
|
parse_style(attr->value(), vec);
|
|
|
|
for (value_type kv : vec )
|
|
{
|
|
if (kv.first == "stop-color")
|
|
{
|
|
stop_color = parse_color(parser.err_handler(), kv.second.c_str());
|
|
}
|
|
else if (kv.first == "stop-opacity")
|
|
{
|
|
opacity = parse_double(parser.err_handler(),kv.second.c_str());
|
|
}
|
|
}
|
|
}
|
|
|
|
attr = node->first_attribute("stop-color");
|
|
if (attr != nullptr)
|
|
{
|
|
stop_color = parse_color(parser.err_handler(), attr->value());
|
|
}
|
|
|
|
attr = node->first_attribute("stop-opacity");
|
|
if (attr != nullptr)
|
|
{
|
|
opacity = parse_double(parser.err_handler(), attr->value());
|
|
}
|
|
|
|
stop_color.set_alpha(static_cast<uint8_t>(opacity * 255));
|
|
gr.add_stop(offset, stop_color);
|
|
}
|
|
|
|
bool parse_common_gradient(svg_parser & parser, std::string const& id, mapnik::gradient& gr, rapidxml::xml_node<char> const* node)
|
|
{
|
|
// check if we should inherit from another tag
|
|
auto * attr = parse_href(node);
|
|
if (attr != nullptr)
|
|
{
|
|
auto const* value = attr->value();
|
|
if (std::strlen(value) > 1 && value[0] == '#')
|
|
{
|
|
std::string linkid(&value[1]); // FIXME !!!
|
|
if (parser.gradient_map_.count(linkid))
|
|
{
|
|
gr = parser.gradient_map_[linkid];
|
|
}
|
|
else
|
|
{
|
|
// save node for later
|
|
parser.node_cache_.emplace(id, node);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
attr = node->first_attribute("gradientUnits");
|
|
if (attr != nullptr)
|
|
{
|
|
if (std::strcmp(attr->value(), "userSpaceOnUse") == 0)
|
|
{
|
|
gr.set_units(USER_SPACE_ON_USE);
|
|
}
|
|
else
|
|
{
|
|
gr.set_units(OBJECT_BOUNDING_BOX);
|
|
}
|
|
}
|
|
|
|
attr = node->first_attribute("gradientTransform");
|
|
if (attr != nullptr)
|
|
{
|
|
agg::trans_affine tr;
|
|
mapnik::svg::parse_svg_transform(attr->value(),tr);
|
|
gr.set_transform(tr);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void parse_radial_gradient(svg_parser & parser, rapidxml::xml_node<char> const* node)
|
|
{
|
|
auto * attr = parse_id(parser, node);
|
|
if (attr == nullptr) return;
|
|
std::string id = attr->value();
|
|
|
|
mapnik::gradient gr;
|
|
if (!parse_common_gradient(parser, id, gr, node)) return;
|
|
double cx = 0.5;
|
|
double cy = 0.5;
|
|
double fx = 0.0;
|
|
double fy = 0.0;
|
|
double r = 0.5;
|
|
bool has_percent = false;
|
|
|
|
attr = node->first_attribute("cx");
|
|
if (attr != nullptr)
|
|
{
|
|
cx = parse_svg_value(parser, attr->value(), has_percent);
|
|
}
|
|
|
|
attr = node->first_attribute("cy");
|
|
if (attr != nullptr)
|
|
{
|
|
cy = parse_svg_value(parser, attr->value(), has_percent);
|
|
}
|
|
|
|
attr = node->first_attribute("fx");
|
|
if (attr != nullptr)
|
|
{
|
|
fx = parse_svg_value(parser,attr->value(), has_percent);
|
|
}
|
|
else fx = cx;
|
|
|
|
attr = node->first_attribute("fy");
|
|
if (attr != nullptr)
|
|
{
|
|
fy = parse_svg_value(parser, attr->value(), has_percent);
|
|
}
|
|
else fy = cy;
|
|
|
|
attr = node->first_attribute("r");
|
|
if (attr != nullptr)
|
|
{
|
|
r = parse_svg_value(parser, attr->value(), has_percent);
|
|
}
|
|
// this logic for detecting %'s will not support mixed coordinates.
|
|
if (has_percent && gr.get_units() == USER_SPACE_ON_USE)
|
|
{
|
|
gr.set_units(USER_SPACE_ON_USE_BOUNDING_BOX);
|
|
}
|
|
|
|
gr.set_gradient_type(RADIAL);
|
|
gr.set_control_points(fx, fy, cx, cy, r);
|
|
|
|
// parse stops
|
|
for (auto const* child = node->first_node();
|
|
child; child = child->next_sibling())
|
|
{
|
|
if (std::strcmp(child->name(), "stop") == 0)
|
|
{
|
|
parse_gradient_stop(parser, gr, child);
|
|
}
|
|
}
|
|
parser.gradient_map_[id] = gr;
|
|
}
|
|
|
|
void parse_linear_gradient(svg_parser & parser, rapidxml::xml_node<char> const* node)
|
|
{
|
|
auto const* attr = parse_id(parser, node);
|
|
if (attr == nullptr) return;
|
|
|
|
std::string id = attr->value();
|
|
mapnik::gradient gr;
|
|
if (!parse_common_gradient(parser, id, gr, node)) return;
|
|
|
|
double x1 = 0.0;
|
|
double y1 = 0.0;
|
|
double x2 = 1.0;
|
|
double y2 = 0.0;
|
|
|
|
bool has_percent = true;
|
|
attr = node->first_attribute("x1");
|
|
if (attr != nullptr)
|
|
{
|
|
x1 = parse_svg_value(parser, attr->value(), has_percent);
|
|
}
|
|
|
|
attr = node->first_attribute("y1");
|
|
if (attr != nullptr)
|
|
{
|
|
y1 = parse_svg_value(parser, attr->value(), has_percent);
|
|
}
|
|
|
|
attr = node->first_attribute("x2");
|
|
if (attr != nullptr)
|
|
{
|
|
x2 = parse_svg_value(parser, attr->value(), has_percent);
|
|
}
|
|
|
|
attr = node->first_attribute("y2");
|
|
if (attr != nullptr)
|
|
{
|
|
y2 = parse_svg_value(parser, attr->value(), has_percent);
|
|
}
|
|
// this logic for detecting %'s will not support mixed coordinates.
|
|
if (has_percent && gr.get_units() == USER_SPACE_ON_USE)
|
|
{
|
|
gr.set_units(USER_SPACE_ON_USE_BOUNDING_BOX);
|
|
}
|
|
gr.set_gradient_type(LINEAR);
|
|
gr.set_control_points(x1, y1, x2, y2);
|
|
|
|
// parse stops
|
|
for (auto const* child = node->first_node();
|
|
child; child = child->next_sibling())
|
|
{
|
|
if (std::strcmp(child->name(), "stop") == 0)
|
|
{
|
|
parse_gradient_stop(parser, gr, child);
|
|
}
|
|
}
|
|
parser.gradient_map_[id] = gr;
|
|
}
|
|
|
|
|
|
svg_parser::svg_parser(svg_converter_type & path, bool strict)
|
|
: path_(path),
|
|
is_defs_(false),
|
|
ignore_(false),
|
|
css_style_(false),
|
|
err_handler_(strict)
|
|
{
|
|
font_sizes_.push_back(10.0);
|
|
}
|
|
|
|
svg_parser::~svg_parser() {}
|
|
|
|
void svg_parser::parse(std::string const& filename)
|
|
{
|
|
#ifdef _WINDOWS
|
|
std::basic_ifstream<char> stream(mapnik::utf8_to_utf16(filename));
|
|
#else
|
|
std::basic_ifstream<char> stream(filename.c_str());
|
|
#endif
|
|
if (!stream)
|
|
{
|
|
std::stringstream ss;
|
|
ss << "SVG error: unable to open \"" << filename << "\"";
|
|
throw std::runtime_error(ss.str());
|
|
}
|
|
|
|
stream.unsetf(std::ios::skipws);
|
|
std::vector<char> buffer(std::istreambuf_iterator<char>(stream.rdbuf()),
|
|
std::istreambuf_iterator<char>());
|
|
buffer.push_back(0);
|
|
|
|
const int flags = rapidxml::parse_trim_whitespace | rapidxml::parse_validate_closing_tags;
|
|
rapidxml::xml_document<> doc;
|
|
try
|
|
{
|
|
doc.parse<flags>(buffer.data());
|
|
}
|
|
catch (rapidxml::parse_error const& ex)
|
|
{
|
|
std::stringstream ss;
|
|
ss << "SVG error: unable to parse \"" << filename << "\"";
|
|
throw std::runtime_error(ss.str());
|
|
}
|
|
|
|
for (rapidxml::xml_node<char> const* child = doc.first_node();
|
|
child; child = child->next_sibling())
|
|
{
|
|
traverse_tree(*this, child);
|
|
}
|
|
}
|
|
|
|
void svg_parser::parse_from_string(std::string const& svg)
|
|
{
|
|
const int flags = rapidxml::parse_trim_whitespace | rapidxml::parse_validate_closing_tags;
|
|
rapidxml::xml_document<> doc;
|
|
std::vector<char> buffer(svg.begin(), svg.end());
|
|
buffer.push_back(0);
|
|
try
|
|
{
|
|
doc.parse<flags>(buffer.data());
|
|
}
|
|
catch (rapidxml::parse_error const& ex)
|
|
{
|
|
std::stringstream ss;
|
|
std::string str = (svg.length() > 1024) ? svg.substr(0, 1024) + "..." : svg;
|
|
ss << "SVG error: unable to parse \"" << str << "\"";
|
|
throw std::runtime_error(ss.str());
|
|
}
|
|
for (rapidxml::xml_node<char> const* child = doc.first_node();
|
|
child; child = child->next_sibling())
|
|
{
|
|
traverse_tree(*this, child);
|
|
}
|
|
}
|
|
|
|
svg_parser::error_handler & svg_parser::err_handler()
|
|
{
|
|
return err_handler_;
|
|
}
|
|
|
|
}}
|