File indexing completed on 2025-01-05 04:01:20

0001 /*
0002  * SPDX-FileCopyrightText: 2019-2023 Mattia Basaglia <dev@dragon.best>
0003  *
0004  * SPDX-License-Identifier: GPL-3.0-or-later
0005  */
0006 
0007 #include "svg_parser.hpp"
0008 #include "svg_parser_private.hpp"
0009 
0010 using namespace glaxnimate::io::svg::detail;
0011 
0012 class glaxnimate::io::svg::SvgParser::Private : public SvgParserPrivate
0013 {
0014 public:
0015     Private(
0016         model::Document* document,
0017         const std::function<void(const QString&)>& on_warning,
0018         ImportExport* io,
0019         QSize forced_size,
0020         model::FrameTime default_time,
0021         GroupMode group_mode,
0022         QDir default_asset_path
0023     ) : SvgParserPrivate(document, on_warning, io, forced_size, default_time),
0024         group_mode(group_mode),
0025         default_asset_path(default_asset_path)
0026     {}
0027 
0028 protected:
0029     void on_parse_prepare(const QDomElement&) override
0030     {
0031         for ( const auto& p : shape_parsers )
0032             to_process += dom.elementsByTagName(p.first).count();
0033     }
0034 
0035     QSizeF get_size(const QDomElement& svg) override
0036     {
0037         return {
0038             len_attr(svg, "width", size.width()),
0039             len_attr(svg, "height", size.height())
0040         };
0041     }
0042 
0043     void on_parse(const QDomElement& svg) override
0044     {
0045         dpi = attr(svg, "inkscape", "export-xdpi", "96").toDouble();
0046 
0047         QPointF pos;
0048         QVector2D scale{1, 1};
0049         if ( svg.hasAttribute("viewBox") )
0050         {
0051             auto vb = split_attr(svg, "viewBox");
0052             if ( vb.size() == 4 )
0053             {
0054                 qreal vbx = vb[0].toDouble();
0055                 qreal vby = vb[1].toDouble();
0056                 qreal vbw = vb[2].toDouble();
0057                 qreal vbh = vb[3].toDouble();
0058 
0059                 if ( !forced_size.isValid() )
0060                 {
0061                     if ( !svg.hasAttribute("width") )
0062                         size.setWidth(vbw);
0063                     if ( !svg.hasAttribute("height") )
0064                         size.setHeight(vbh);
0065                 }
0066 
0067                 pos = -QPointF(vbx, vby);
0068                 if ( vbw != 0 && vbh != 0 )
0069                 {
0070                     scale = QVector2D(size.width() / vbw, size.height() / vbh);
0071 
0072                     if ( forced_size.isValid() )
0073                     {
0074                         auto single = qMin(scale.x(), scale.y());
0075                         scale = QVector2D(single, single);
0076                     }
0077                 }
0078             }
0079         }
0080 
0081         for ( const auto& link_node : ItemCountRange(dom.elementsByTagName("link")) )
0082         {
0083             auto link = link_node.toElement();
0084             if ( link.attribute("rel") == "stylesheet" )
0085             {
0086                 QString url = link.attribute("href");
0087                 if ( !url.isEmpty() )
0088                     document->add_pending_asset("", QUrl(url));
0089             }
0090         }
0091 
0092         parse_css();
0093         parse_assets();
0094         parse_metadata();
0095 
0096         model::Layer* parent_layer = add_layer(&main->shapes);
0097         parent_layer->transform.get()->position.set(-pos);
0098         parent_layer->transform.get()->scale.set(scale);
0099         parent_layer->name.set(
0100             attr(svg, "sodipodi", "docname", svg.attribute("id", parent_layer->type_name_human()))
0101         );
0102 
0103         Style default_style(Style::Map{
0104             {"fill", "black"},
0105         });
0106         parse_children({svg, &parent_layer->shapes, parse_style(svg, default_style), false});
0107 
0108         main->name.set(
0109             attr(svg, "sodipodi", "docname", "")
0110         );
0111     }
0112 
0113     void parse_shape(const ParseFuncArgs& args) override
0114     {
0115         if ( handle_mask(args) )
0116             return;
0117 
0118         parse_shape_impl(args);
0119     }
0120 
0121 private:
0122     void parse_css()
0123     {
0124         CssParser parser(css_blocks);
0125 
0126         for ( const auto& style : ItemCountRange(dom.elementsByTagName("style")) )
0127         {
0128             QString data;
0129             for ( const auto & child : ItemCountRange(style.childNodes()) )
0130             {
0131                 if ( child.isText() || child.isCDATASection() )
0132                     data += child.toCharacterData().data();
0133             }
0134 
0135             if ( data.contains("@font-face") )
0136                 document->add_pending_asset("", data.toUtf8());
0137 
0138             parser.parse(data);
0139         }
0140 
0141         std::stable_sort(css_blocks.begin(), css_blocks.end());
0142     }
0143 
0144     void parse_defs(const QDomNode& node)
0145     {
0146         if ( !node.isElement() )
0147             return;
0148 
0149         auto defs = node.toElement();
0150         for ( const auto& def : ElementRange(defs) )
0151         {
0152             if ( def.tagName().startsWith("animate") )
0153             {
0154                 QString link = attr(def, "xlink", "href");
0155                 if ( link.isEmpty() || link[0] != '#' )
0156                     continue;
0157                 animate_parser.store_animate(link.mid(1), def);
0158             }
0159         }
0160     }
0161 
0162     void parse_assets()
0163     {
0164         std::vector<QDomElement> later;
0165 
0166         for ( const auto& domnode : ItemCountRange(dom.elementsByTagName("linearGradient")) )
0167             parse_gradient_node(domnode, later);
0168 
0169         for ( const auto& domnode : ItemCountRange(dom.elementsByTagName("radialGradient")) )
0170             parse_gradient_node(domnode, later);
0171 
0172         std::vector<QDomElement> unprocessed;
0173         while ( !later.empty() && unprocessed.size() != later.size() )
0174         {
0175             unprocessed.clear();
0176 
0177             for ( const auto& element : later )
0178                 parse_brush_style_check(element, unprocessed);
0179 
0180             std::swap(later, unprocessed);
0181         }
0182 
0183 
0184         for ( const auto& defs : ItemCountRange(dom.elementsByTagName("defs")) )
0185             parse_defs(defs);
0186     }
0187 
0188     void parse_gradient_node(const QDomNode& domnode, std::vector<QDomElement>& later)
0189     {
0190         if ( !domnode.isElement() )
0191             return;
0192 
0193         auto gradient = domnode.toElement();
0194         QString id = gradient.attribute("id");
0195         if ( id.isEmpty() )
0196             return;
0197 
0198         if ( parse_brush_style_check(gradient, later) )
0199             parse_gradient_nolink(gradient, id);
0200     }
0201 
0202     bool parse_brush_style_check(const QDomElement& element, std::vector<QDomElement>& later)
0203     {
0204         QString link = attr(element, "xlink", "href");
0205         if ( link.isEmpty() )
0206             return true;
0207 
0208         if ( !link.startsWith("#") )
0209             return false;
0210 
0211         auto it = brush_styles.find(link);
0212         if ( it != brush_styles.end() )
0213         {
0214             brush_styles["#" + element.attribute("id")] = it->second;
0215             return false;
0216         }
0217 
0218 
0219         auto it1 = gradients.find(link);
0220         if ( it1 != gradients.end() )
0221         {
0222             parse_gradient(element, element.attribute("id"), it1->second);
0223             return false;
0224         }
0225 
0226         later.push_back(element);
0227         return false;
0228     }
0229 
0230     QGradientStops parse_gradient_stops(const QDomElement& gradient)
0231     {
0232         QGradientStops stops;
0233 
0234         for ( const auto& domnode : ItemCountRange(gradient.childNodes()) )
0235         {
0236             if ( !domnode.isElement() )
0237                 continue;
0238 
0239             auto stop = domnode.toElement();
0240 
0241             if ( stop.tagName() != "stop" )
0242                 continue;
0243 
0244             Style style = parse_style(stop, {});
0245             if ( !style.contains("stop-color") )
0246                 continue;
0247             QColor color = parse_color(style["stop-color"], QColor());
0248             color.setAlphaF(color.alphaF() * style.get("stop-opacity", "1").toDouble());
0249 
0250             stops.push_back({stop.attribute("offset", "0").toDouble(), color});
0251         }
0252 
0253         utils::sort_gradient(stops);
0254 
0255         return stops;
0256     }
0257 
0258     void parse_gradient_nolink(const QDomElement& gradient, const QString& id)
0259     {
0260         QGradientStops stops = parse_gradient_stops(gradient);
0261 
0262         if ( stops.empty() )
0263             return;
0264 
0265         if ( stops.size() == 1 )
0266         {
0267             auto col = std::make_unique<model::NamedColor>(document);
0268             col->name.set(id);
0269             col->color.set(stops[0].second);
0270             brush_styles["#"+id] = col.get();
0271             auto anim = parse_animated(gradient.firstChildElement("stop"));
0272 
0273             for ( const auto& kf : anim.single("stop-color") )
0274                 col->color.set_keyframe(kf.time, kf.values.color())->set_transition(kf.transition);
0275 
0276             document->assets()->colors->values.insert(std::move(col));
0277             return;
0278         }
0279 
0280         auto colors = std::make_unique<model::GradientColors>(document);
0281         colors->name.set(id);
0282         colors->colors.set(stops);
0283         gradients["#"+id] = colors.get();
0284         auto ptr = colors.get();
0285         document->assets()->gradient_colors->values.insert(std::move(colors));
0286         parse_gradient(gradient, id, ptr);
0287     }
0288 
0289     void parse_gradient(const QDomElement& element, const QString& id, model::GradientColors* colors)
0290     {
0291         auto gradient = std::make_unique<model::Gradient>(document);
0292         QTransform gradient_transform;
0293 
0294         if ( element.hasAttribute("gradientTransform") )
0295             gradient_transform = svg_transform(element.attribute("gradientTransform"), {}).transform;
0296 
0297         if ( element.tagName() == "linearGradient" )
0298         {
0299             if ( !element.hasAttribute("x1") || !element.hasAttribute("x2") ||
0300                  !element.hasAttribute("y1") || !element.hasAttribute("y2") )
0301                 return;
0302 
0303             gradient->type.set(model::Gradient::Linear);
0304 
0305             gradient->start_point.set(gradient_transform.map(QPointF(
0306                 len_attr(element, "x1"),
0307                 len_attr(element, "y1")
0308             )));
0309             gradient->end_point.set(gradient_transform.map(QPointF(
0310                 len_attr(element, "x2"),
0311                 len_attr(element, "y2")
0312             )));
0313 
0314             auto anim = parse_animated(element);
0315             for ( const auto& kf : anim.joined({"x1", "y1"}) )
0316                 gradient->start_point.set_keyframe(kf.time, {kf.values[0].vector()[0], kf.values[1].vector()[0]})->set_transition(kf.transition);
0317             for ( const auto& kf : anim.joined({"x2", "y2"}) )
0318                 gradient->end_point.set_keyframe(kf.time, {kf.values[0].vector()[0], kf.values[1].vector()[0]})->set_transition(kf.transition);
0319         }
0320         else if ( element.tagName() == "radialGradient" )
0321         {
0322             if ( !element.hasAttribute("cx") || !element.hasAttribute("cy") || !element.hasAttribute("r") )
0323                 return;
0324 
0325             gradient->type.set(model::Gradient::Radial);
0326 
0327             QPointF c = QPointF(
0328                 len_attr(element, "cx"),
0329                 len_attr(element, "cy")
0330             );
0331             gradient->start_point.set(gradient_transform.map(c));
0332 
0333             if ( element.hasAttribute("fx") )
0334                 gradient->highlight.set(gradient_transform.map(QPointF(
0335                     len_attr(element, "fx"),
0336                     len_attr(element, "fy")
0337                 )));
0338             else
0339                 gradient->highlight.set(gradient_transform.map(c));
0340 
0341             gradient->end_point.set(gradient_transform.map(QPointF(
0342                 c.x() + len_attr(element, "r"), c.y()
0343             )));
0344 
0345 
0346             auto anim = parse_animated(element);
0347             for ( const auto& kf : anim.joined({"cx", "cy"}) )
0348                 gradient->start_point.set_keyframe(kf.time,
0349                     gradient_transform.map(QPointF{kf.values[0].vector()[0], kf.values[1].vector()[0]})
0350                 )->set_transition(kf.transition);
0351 
0352             for ( const auto& kf : anim.joined({"fx", "fy"}) )
0353                 gradient->highlight.set_keyframe(kf.time,
0354                     gradient_transform.map(QPointF{kf.values[0].vector()[0], kf.values[1].vector()[0]})
0355                 )->set_transition(kf.transition);
0356 
0357             for ( const auto& kf : anim.joined({"cx", "cy", "r"}) )
0358                 gradient->end_point.set_keyframe(kf.time,
0359                     gradient_transform.map(QPointF{kf.values[0].vector()[0] + kf.values[2].vector()[0], kf.values[1].vector()[0]})
0360                 )->set_transition(kf.transition);
0361 
0362         }
0363         else
0364         {
0365             return;
0366         }
0367 
0368         gradient->name.set(id);
0369         gradient->colors.set(colors);
0370         brush_styles["#"+id] = gradient.get();
0371         document->assets()->gradients->values.insert(std::move(gradient));
0372     }
0373 
0374     Style parse_style(const QDomElement& element, const Style& parent_style)
0375     {
0376         Style style = parent_style;
0377 
0378         auto class_names_list = element.attribute("class").split(" ",
0379 #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
0380         Qt::SkipEmptyParts
0381 #else
0382         QString::SkipEmptyParts
0383 #endif
0384         );
0385         std::unordered_set<QString> class_names(class_names_list.begin(), class_names_list.end());
0386         for ( const auto& rule : css_blocks )
0387         {
0388             if ( rule.selector.match(element, class_names) )
0389                 rule.merge_into(style);
0390         }
0391 
0392         if ( element.hasAttribute("style") )
0393         {
0394             for ( const auto& item : element.attribute("style").split(';') )
0395             {
0396                 auto split = ::utils::split_ref(item, ':');
0397                 if ( split.size() == 2 )
0398                 {
0399                     QString name = split[0].trimmed().toString();
0400                     if ( !name.isEmpty() && css_atrrs.count(name) )
0401                         style[name] = split[1].trimmed().toString();
0402                 }
0403             }
0404         }
0405 
0406         for ( const auto& domnode : ItemCountRange(element.attributes()) )
0407         {
0408             auto attr = domnode.toAttr();
0409             if ( css_atrrs.count(attr.name()) )
0410                 style[attr.name()] = attr.value();
0411         }
0412 
0413         for ( auto it = style.map.begin(); it != style.map.end(); )
0414         {
0415             if ( it->second == "inherit" )
0416             {
0417                 QString parent = parent_style.get(it->first, "");
0418                 if ( parent.isEmpty() || parent == "inherit" )
0419                 {
0420                     it = style.map.erase(it);
0421                     continue;
0422                 }
0423                 it->second = parent;
0424             }
0425 
0426             ++it;
0427         }
0428 
0429         if ( !style.contains("fill") )
0430             style.set("fill", parent_style.get("fill"));
0431 
0432         style.color = parse_color(style.get("color", ""), parent_style.color);
0433         return style;
0434     }
0435 
0436     bool handle_mask(const ParseFuncArgs& args)
0437     {
0438         QString mask_ref;
0439         if ( args.element.hasAttribute("clip-path") )
0440             mask_ref = args.element.attribute("clip-path");
0441         else if ( args.element.hasAttribute("mask") )
0442             mask_ref = args.element.attribute("mask");
0443 
0444         if ( mask_ref.isEmpty() )
0445             return false;
0446 
0447         auto match = url_re.match(mask_ref);
0448         if ( !match.hasMatch() )
0449             return false;
0450 
0451         QString id = match.captured(1).mid(1);
0452         QDomElement mask_element = element_by_id(id);
0453         if ( mask_element.isNull() )
0454             return false;
0455 
0456 
0457         Style style = parse_style(args.element, args.parent_style);
0458         auto layer = add_layer(args.shape_parent);
0459         apply_common_style(layer, args.element, style);
0460         set_name(layer, args.element);
0461         layer->mask->mask.set(model::MaskSettings::Alpha);
0462 
0463         QDomElement element = args.element;
0464 
0465         QDomElement trans_copy = dom.createElement("g");
0466         trans_copy.setAttribute("style", element.attribute("style"));
0467         element.removeAttribute("style");
0468         trans_copy.setAttribute("transform", element.attribute("transform"));
0469         element.removeAttribute("transform");
0470 
0471         for ( const auto& attr : detail::css_atrrs )
0472             element.removeAttribute(attr);
0473 
0474         Style mask_style;
0475         mask_style["stroke"] = "none";
0476         parse_g_to_layer({
0477             mask_element,
0478             &layer->shapes,
0479             mask_style,
0480             false
0481         });
0482 
0483         parse_shape_impl({
0484             element,
0485             &layer->shapes,
0486             style,
0487             false
0488         });
0489 
0490         parse_transform(trans_copy, layer, layer->transform.get());
0491 
0492         return true;
0493     }
0494 
0495     void parse_shape_impl(const ParseFuncArgs& args)
0496     {
0497         auto it = shape_parsers.find(args.element.tagName());
0498         if ( it != shape_parsers.end() )
0499         {
0500             mark_progress();
0501             (this->*it->second)(args);
0502         }
0503     }
0504 
0505     void parse_transform(
0506         const QDomElement& element,
0507         model::Group* node,
0508         model::Transform* transform
0509     )
0510     {
0511         auto bb = node->local_bounding_rect(0);
0512         bool anchor_from_inkscape = false;
0513         QPointF center = bb.center();
0514         if ( element.hasAttributeNS(detail::xmlns.at("inkscape"), "transform-center-x") )
0515         {
0516             anchor_from_inkscape = true;
0517             qreal ix = element.attributeNS(detail::xmlns.at("inkscape"), "transform-center-x").toDouble();
0518             qreal iy = -element.attributeNS(detail::xmlns.at("inkscape"), "transform-center-y").toDouble();
0519             center += QPointF(ix, iy);
0520         }
0521 
0522         bool anchor_from_rotate = false;
0523 
0524         if ( element.hasAttribute("transform") )
0525         {
0526             auto trans = svg_transform(
0527                 element.attribute("transform"),
0528                 transform->transform_matrix(transform->time())
0529             );
0530             transform->set_transform_matrix(trans.transform);
0531             anchor_from_rotate = trans.anchor_set;
0532             if ( trans.anchor_set )
0533                 center = trans.anchor;
0534 
0535         }
0536 
0537         /// Adjust anchor point
0538         QPointF delta_pos;
0539         if ( anchor_from_rotate )
0540         {
0541             transform->anchor_point.set(center);
0542             delta_pos = center;
0543         }
0544         else if ( anchor_from_inkscape )
0545         {
0546             auto matrix = transform->transform_matrix(transform->time());
0547             QPointF p1 = matrix.map(QPointF(0, 0));
0548             transform->anchor_point.set(center);
0549             matrix = transform->transform_matrix(transform->time());
0550             QPointF p2 = matrix.map(QPointF(0, 0));
0551             delta_pos = p1 - p2;
0552         }
0553         transform->position.set(transform->position.get() + delta_pos);
0554 
0555         auto anim = animate_parser.parse_animated_transform(element);
0556 
0557         if ( !anim.apply_motion(transform->position, delta_pos, &node->auto_orient) )
0558         {
0559             for ( const auto& kf : anim.single("translate") )
0560                 transform->position.set_keyframe(kf.time, QPointF{kf.values.vector()[0], kf.values.vector()[1]} + delta_pos)->set_transition(kf.transition);
0561         }
0562 
0563         for ( const auto& kf : anim.single("scale") )
0564             transform->scale.set_keyframe(kf.time, QVector2D(kf.values.vector()[0], kf.values.vector()[1]))->set_transition(kf.transition);
0565 
0566         for ( const auto& kf : anim.single("rotate") )
0567         {
0568             transform->rotation.set_keyframe(kf.time, kf.values.vector()[0])->set_transition(kf.transition);
0569             if ( kf.values.vector().size() == 3 )
0570             {
0571                 QPointF p = {kf.values.vector()[1], kf.values.vector()[2]};
0572                 transform->anchor_point.set_keyframe(kf.time, p)->set_transition(kf.transition);
0573                 transform->position.set_keyframe(kf.time, p)->set_transition(kf.transition);
0574             }
0575         }
0576     }
0577 
0578     struct ParsedTransformInfo
0579     {
0580         QTransform transform;
0581         QPointF anchor = {};
0582         bool anchor_set = false;
0583     };
0584 
0585     ParsedTransformInfo svg_transform(const QString& attr, const QTransform& trans)
0586     {
0587         ParsedTransformInfo info{trans};
0588         for ( const QRegularExpressionMatch& match : utils::regexp::find_all(transform_re, attr) )
0589         {
0590             auto args = double_args(match.captured(2));
0591             if ( args.empty() )
0592             {
0593                 warning("Missing transformation parameters");
0594                 continue;
0595             }
0596 
0597             QString name = match.captured(1);
0598 
0599             if ( name == "translate" )
0600             {
0601                 info.transform.translate(args[0], args.size() > 1 ? args[1] : 0);
0602             }
0603             else if ( name == "scale" )
0604             {
0605                 info.transform.scale(args[0], (args.size() > 1 ? args[1] : args[0]));
0606             }
0607             else if ( name == "rotate" )
0608             {
0609                 qreal ang = args[0];
0610                 if ( args.size() > 2 )
0611                 {
0612                     qreal x = args[1];
0613                     qreal y = args[2];
0614                     info.anchor = {x, y};
0615                     info.anchor_set = true;
0616 //                     info.transform.translate(-x, -y);
0617                     info.transform.rotate(ang);
0618 //                     info.transform.translate(x, y);
0619                 }
0620                 else
0621                 {
0622                     info.transform.rotate(ang);
0623                 }
0624             }
0625             else if ( name == "skewX" )
0626             {
0627                 info.transform *= QTransform(
0628                     1, 0, 0,
0629                     qTan(args[0]), 1, 0,
0630                     0, 0, 1
0631                 );
0632             }
0633             else if ( name == "skewY" )
0634             {
0635                 info.transform *= QTransform(
0636                     1, qTan(args[0]), 0,
0637                     0, 1, 0,
0638                     0, 0, 1
0639                 );
0640             }
0641             else if ( name == "matrix" )
0642             {
0643                 if ( args.size() == 6 )
0644                 {
0645                     info.transform *= QTransform(
0646                         args[0], args[1], 0,
0647                         args[2], args[3], 0,
0648                         args[4], args[5], 1
0649                     );
0650                 }
0651                 else
0652                 {
0653                     warning("Wrong translation matrix");
0654                 }
0655             }
0656             else
0657             {
0658                 warning(QString("Unknown transformation %1").arg(name));
0659             }
0660 
0661         }
0662         return info;
0663     }
0664 
0665     void add_shapes(const ParseFuncArgs& args, ShapeCollection&& shapes)
0666     {
0667         Style style = parse_style(args.element, args.parent_style);
0668         auto group = std::make_unique<model::Group>(document);
0669         apply_common_style(group.get(), args.element, style);
0670         set_name(group.get(), args.element);
0671 
0672         add_style_shapes(args, &group->shapes, style);
0673 
0674         for ( auto& shape : shapes )
0675             group->shapes.insert(std::move(shape));
0676 
0677         // parse_transform at the end so the bounding box isn't empty
0678         parse_transform(args.element, group.get(), group->transform.get());
0679         args.shape_parent->insert(std::move(group));
0680     }
0681 
0682     void apply_common_style(model::VisualNode* node, const QDomElement& element, const Style& style)
0683     {
0684         if ( style.get("display") == "none" || style.get("visibility") == "hidden" )
0685             node->visible.set(false);
0686         node->locked.set(attr(element, "sodipodi", "insensitive") == "true");
0687         node->set("opacity", percent_1(style.get("opacity", "1")));
0688         node->get("transform").value<model::Transform*>();
0689     }
0690 
0691     void set_name(model::DocumentNode* node, const QDomElement& element)
0692     {
0693         QString name = attr(element, "inkscape", "label");
0694         if ( name.isEmpty() )
0695         {
0696             name = attr(element, "android", "name");
0697             if ( name.isEmpty() )
0698                 name = element.attribute("id");
0699         }
0700         node->name.set(name);
0701     }
0702 
0703     void add_style_shapes(const ParseFuncArgs& args, model::ShapeListProperty* shapes, const Style& style)
0704     {
0705         QString paint_order = style.get("paint-order", "normal");
0706         if ( paint_order == "normal" )
0707             paint_order = "fill stroke";
0708 
0709         for ( const auto& sr : paint_order.split(' ',
0710 #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
0711         Qt::SkipEmptyParts
0712 #else
0713         QString::SkipEmptyParts
0714 #endif
0715         ) )
0716         {
0717             if ( sr == "fill" )
0718                 add_fill(args, shapes, style);
0719             else if ( sr == "stroke" )
0720                 add_stroke(args, shapes, style);
0721         }
0722     }
0723 
0724     void display_to_opacity(model::VisualNode* node,
0725                             const detail::AnimateParser::AnimatedProperties& anim,
0726                             model::AnimatedProperty<float>& opacity,
0727                             Style* style)
0728     {
0729         if ( !anim.has("display") )
0730             return;
0731 
0732         if ( opacity.keyframe_count() > 2 )
0733         {
0734             warning("Either animate `opacity` or `display`, not both");
0735             return;
0736         }
0737 
0738         if ( style )
0739             style->map.erase("display");
0740 
0741         model::KeyframeTransition hold;
0742         hold.set_hold(true);
0743 
0744         for ( const auto& kf : anim.single("display") )
0745         {
0746             opacity.set_keyframe(kf.time, kf.values.string() == "none" ? 0 : 1)->set_transition(hold);
0747         }
0748 
0749         node->visible.set(true);
0750     }
0751 
0752     void add_stroke(const ParseFuncArgs& args, model::ShapeListProperty* shapes, const Style& style)
0753     {
0754         QString stroke_color = style.get("stroke", "transparent");
0755         if ( stroke_color == "none" )
0756             return;
0757 
0758         auto stroke = std::make_unique<model::Stroke>(document);
0759         set_styler_style(stroke.get(), stroke_color, style.color);
0760 
0761         stroke->opacity.set(percent_1(style.get("stroke-opacity", "1")));
0762         stroke->width.set(parse_unit(style.get("stroke-width", "1")));
0763 
0764         stroke->cap.set(line_cap(style.get("stroke-linecap", "butt")));
0765         stroke->join.set(line_join(style.get("stroke-linejoin", "miter")));
0766         stroke->miter_limit.set(parse_unit(style.get("stroke-miterlimit", "4")));
0767 
0768         auto anim = parse_animated(args.element);
0769         for ( const auto& kf : anim.single("stroke") )
0770             stroke->color.set_keyframe(kf.time, kf.values.color())->set_transition(kf.transition);
0771 
0772         for ( const auto& kf : anim.single("stroke-opacity") )
0773             stroke->opacity.set_keyframe(kf.time, kf.values.vector()[0])->set_transition(kf.transition);
0774 
0775         for ( const auto& kf : anim.single("stroke-width") )
0776             stroke->width.set_keyframe(kf.time, kf.values.vector()[0])->set_transition(kf.transition);
0777 
0778         display_to_opacity(stroke.get(), anim, stroke->opacity, nullptr);
0779 
0780         shapes->insert(std::move(stroke));
0781     }
0782 
0783     void set_styler_style(model::Styler* styler, const QString& color_str, const QColor& current_color)
0784     {
0785         if ( !color_str.startsWith("url") )
0786         {
0787             styler->color.set(parse_color(color_str, current_color));
0788             return;
0789         }
0790 
0791         auto match = url_re.match(color_str);
0792         if ( match.hasMatch() )
0793         {
0794             QString id = match.captured(1);
0795             auto it = brush_styles.find(id);
0796             if ( it != brush_styles.end() )
0797             {
0798                 styler->use.set(it->second);
0799                 return;
0800             }
0801         }
0802 
0803         styler->color.set(current_color);
0804     }
0805 
0806     void add_fill(const ParseFuncArgs& args, model::ShapeListProperty* shapes, const Style& style)
0807     {
0808         QString fill_color = style.get("fill", "");
0809 
0810         auto fill = std::make_unique<model::Fill>(document);
0811         set_styler_style(fill.get(), fill_color, style.color);
0812         fill->opacity.set(percent_1(style.get("fill-opacity", "1")));
0813 
0814         if ( style.get("fill-rule", "") == "evenodd" )
0815             fill->fill_rule.set(model::Fill::EvenOdd);
0816 
0817         auto anim = parse_animated(args.element);
0818         for ( const auto& kf : anim.single("fill") )
0819             fill->color.set_keyframe(kf.time, kf.values.color())->set_transition(kf.transition);
0820 
0821         for ( const auto& kf : anim.single("fill-opacity") )
0822             fill->opacity.set_keyframe(kf.time, kf.values.vector()[0])->set_transition(kf.transition);
0823 
0824         if ( fill_color == "none" )
0825             fill->visible.set(false);
0826 
0827         display_to_opacity(fill.get(), anim, fill->opacity, nullptr);
0828 
0829         shapes->insert(std::move(fill));
0830     }
0831 
0832     QColor parse_color(const QString& color_str, const QColor& current_color)
0833     {
0834         if ( color_str.isEmpty() || color_str == "currentColor" )
0835             return current_color;
0836 
0837         return glaxnimate::io::svg::parse_color(color_str);
0838     }
0839 
0840     void parseshape_rect(const ParseFuncArgs& args)
0841     {
0842         ShapeCollection shapes;
0843         auto rect = push<model::Rect>(shapes);
0844         qreal w = len_attr(args.element, "width", 0);
0845         qreal h = len_attr(args.element, "height", 0);
0846         rect->position.set(QPointF(
0847             len_attr(args.element, "x", 0) + w / 2,
0848             len_attr(args.element, "y", 0) + h / 2
0849         ));
0850         rect->size.set(QSizeF(w, h));
0851         qreal rx = len_attr(args.element, "rx", 0);
0852         qreal ry = len_attr(args.element, "ry", 0);
0853         rect->rounded.set(qMax(rx, ry));
0854 
0855 
0856         auto anim = parse_animated(args.element);
0857 
0858         /// \todo handle offset
0859         anim.apply_motion(rect->position);
0860 
0861         for ( const auto& kf : anim.joined({"x", "y", "width", "height"}) )
0862             rect->position.set_keyframe(kf.time, {
0863                 kf.values[0].vector()[0] + kf.values[2].vector()[0] / 2,
0864                 kf.values[1].vector()[0] + kf.values[3].vector()[0] / 2
0865             })->set_transition(kf.transition);
0866 
0867         for ( const auto& kf : anim.joined({"width", "height"}) )
0868             rect->size.set_keyframe(kf.time, {kf.values[0].vector()[0], kf.values[1].vector()[0]})->set_transition(kf.transition);
0869 
0870         for ( const auto& kf : anim.joined({"rx", "ry"}) )
0871             rect->rounded.set_keyframe(kf.time, qMax(kf.values[0].vector()[0], kf.values[1].vector()[0]))->set_transition(kf.transition);
0872 
0873         add_shapes(args, std::move(shapes));
0874     }
0875 
0876     void parseshape_ellipse(const ParseFuncArgs& args)
0877     {
0878         ShapeCollection shapes;
0879         auto ellipse = push<model::Ellipse>(shapes);
0880         ellipse->position.set(QPointF(
0881             len_attr(args.element, "cx", 0),
0882             len_attr(args.element, "cy", 0)
0883         ));
0884         qreal rx = len_attr(args.element, "rx", 0);
0885         qreal ry = len_attr(args.element, "ry", 0);
0886         ellipse->size.set(QSizeF(rx * 2, ry * 2));
0887 
0888         auto anim = parse_animated(args.element);
0889         anim.apply_motion(ellipse->position);
0890         for ( const auto& kf : anim.joined({"cx", "cy"}) )
0891             ellipse->position.set_keyframe(kf.time, {kf.values[0].vector()[0], kf.values[1].vector()[0]})->set_transition(kf.transition);
0892         for ( const auto& kf : anim.joined({"rx", "ry"}) )
0893             ellipse->size.set_keyframe(kf.time, {kf.values[0].vector()[0]*2, kf.values[1].vector()[0]*2})->set_transition(kf.transition);
0894 
0895         add_shapes(args, std::move(shapes));
0896     }
0897 
0898     void parseshape_circle(const ParseFuncArgs& args)
0899     {
0900         ShapeCollection shapes;
0901         auto ellipse = push<model::Ellipse>(shapes);
0902         ellipse->position.set(QPointF(
0903             len_attr(args.element, "cx", 0),
0904             len_attr(args.element, "cy", 0)
0905         ));
0906         qreal d = len_attr(args.element, "r", 0) * 2;
0907         ellipse->size.set(QSizeF(d, d));
0908 
0909         auto anim = parse_animated(args.element);
0910         anim.apply_motion(ellipse->position);
0911         for ( const auto& kf : anim.joined({"cx", "cy"}) )
0912             ellipse->position.set_keyframe(kf.time, {kf.values[0].vector()[0], kf.values[1].vector()[0]})->set_transition(kf.transition);
0913         for ( const auto& kf : anim.single({"r"}) )
0914             ellipse->size.set_keyframe(kf.time, {kf.values.vector()[0]*2, kf.values.vector()[0]*2})->set_transition(kf.transition);
0915 
0916         add_shapes(args, std::move(shapes));
0917     }
0918 
0919     void parseshape_g(const ParseFuncArgs& args)
0920     {
0921         switch ( group_mode )
0922         {
0923             case Groups:
0924                 parse_g_to_shape(args);
0925                 break;
0926             case Layers:
0927                 parse_g_to_layer(args);
0928                 break;
0929             case Inkscape:
0930                 if ( args.in_group )
0931                     parse_g_to_shape(args);
0932                 else if ( attr(args.element, "inkscape", "groupmode") == "layer" )
0933                     parse_g_to_layer(args);
0934                 else
0935                     parse_g_to_shape(args);
0936                 break;
0937         }
0938     }
0939 
0940     void parse_g_to_layer(const ParseFuncArgs& args)
0941     {
0942         Style style = parse_style(args.element, args.parent_style);
0943         auto layer = add_layer(args.shape_parent);
0944         parse_g_common(
0945             {args.element, &layer->shapes, style, false},
0946             layer,
0947             layer->transform.get(),
0948             style
0949         );
0950     }
0951 
0952     void parse_g_to_shape(const ParseFuncArgs& args)
0953     {
0954         Style style = parse_style(args.element, args.parent_style);
0955         auto ugroup = std::make_unique<model::Group>(document);
0956         auto group = ugroup.get();
0957         args.shape_parent->insert(std::move(ugroup));
0958         parse_g_common(
0959             {args.element, &group->shapes, style, true},
0960             group,
0961             group->transform.get(),
0962             style
0963         );
0964     }
0965 
0966     void parse_g_common(
0967         const ParseFuncArgs& args,
0968         model::Group* g_node,
0969         model::Transform* transform,
0970         Style& style
0971     )
0972     {
0973         apply_common_style(g_node, args.element, args.parent_style);
0974 
0975         auto anim = parse_animated(args.element);
0976 
0977         for ( const auto& kf : anim.single("opacity") )
0978             g_node->opacity.set_keyframe(kf.time, kf.values.vector()[0])->set_transition(kf.transition);
0979 
0980         display_to_opacity(g_node, anim, g_node->opacity, &style);
0981 
0982         set_name(g_node, args.element);
0983         // Avoid doubling opacity values
0984         style.map.erase("opacity");
0985         parse_children(args);
0986         parse_transform(args.element, g_node, transform);
0987     }
0988 
0989     std::vector<model::Path*> parse_bezier_impl(const ParseFuncArgs& args, const math::bezier::MultiBezier& bez)
0990     {
0991         if ( bez.beziers().empty() )
0992             return {};
0993 
0994         ShapeCollection shapes;
0995         std::vector<model::Path*> paths;
0996         for ( const auto& bezier : bez.beziers() )
0997         {
0998             model::Path* shape = push<model::Path>(shapes);
0999             paths.push_back(shape);
1000             shape->shape.set(bezier);
1001             shape->closed.set(bezier.closed());
1002         }
1003         add_shapes(args, std::move(shapes));
1004         return paths;
1005     }
1006 
1007 
1008     model::Path* parse_bezier_impl_single(const ParseFuncArgs& args, const math::bezier::Bezier& bez)
1009     {
1010         ShapeCollection shapes;
1011         auto path = push<model::Path>(shapes);
1012         path->shape.set(bez);
1013         add_shapes(args, {std::move(shapes)});
1014         return path;
1015     }
1016 
1017     detail::AnimateParser::AnimatedProperties parse_animated(const QDomElement& element)
1018     {
1019         return animate_parser.parse_animated_properties(element);
1020     }
1021 
1022     void parseshape_line(const ParseFuncArgs& args)
1023     {
1024         math::bezier::Bezier bez;
1025         bez.add_point(QPointF(
1026             len_attr(args.element, "x1", 0),
1027             len_attr(args.element, "y1", 0)
1028         ));
1029         bez.line_to(QPointF(
1030             len_attr(args.element, "x2", 0),
1031             len_attr(args.element, "y2", 0)
1032         ));
1033         auto path = parse_bezier_impl_single(args, bez);
1034         for ( const auto& kf : parse_animated(args.element).joined({"x1", "y1", "x2", "y2"}) )
1035         {
1036             math::bezier::Bezier bez;
1037             bez.add_point({kf.values[0].vector()[0], kf.values[1].vector()[0]});
1038             bez.add_point({kf.values[2].vector()[0], kf.values[3].vector()[0]});
1039             path->shape.set_keyframe(kf.time, bez)->set_transition(kf.transition);
1040         }
1041     }
1042 
1043     math::bezier::Bezier build_poly(const std::vector<qreal>& coords, bool close)
1044     {
1045         math::bezier::Bezier bez;
1046 
1047         if ( coords.size() < 4 )
1048         {
1049             if ( !coords.empty() )
1050                 warning("Not enough `points` for `polygon` / `polyline`");
1051             return bez;
1052         }
1053 
1054         bez.add_point(QPointF(coords[0], coords[1]));
1055 
1056         for ( int i = 2; i < int(coords.size()); i+= 2 )
1057             bez.line_to(QPointF(coords[i], coords[i+1]));
1058 
1059         if ( close )
1060             bez.close();
1061 
1062         return bez;
1063     }
1064 
1065     void handle_poly(const ParseFuncArgs& args, bool close)
1066     {
1067         auto path = parse_bezier_impl_single(args, build_poly(double_args(args.element.attribute("points", "")), close));
1068         if ( !path )
1069             return;
1070 
1071         for ( const auto& kf : parse_animated(args.element).single("points") )
1072             path->shape.set_keyframe(kf.time, build_poly(kf.values.vector(), close))->set_transition(kf.transition);
1073 
1074     }
1075 
1076     void parseshape_polyline(const ParseFuncArgs& args)
1077     {
1078         handle_poly(args, false);
1079     }
1080 
1081     void parseshape_polygon(const ParseFuncArgs& args)
1082     {
1083         handle_poly(args, true);
1084     }
1085 
1086     void parseshape_path(const ParseFuncArgs& args)
1087     {
1088         if ( parse_star(args) )
1089             return;
1090         QString d = args.element.attribute("d");
1091         math::bezier::MultiBezier bez = PathDParser(d).parse();
1092         /// \todo sodipodi:nodetypes
1093         auto paths = parse_bezier_impl(args, bez);
1094 
1095         path_animation(paths, parse_animated(args.element), "d" );
1096     }
1097 
1098     bool parse_star(const ParseFuncArgs& args)
1099     {
1100         if ( attr(args.element, "sodipodi", "type") != "star" )
1101             return false;
1102 
1103         qreal randomized = attr(args.element, "inkscape", "randomized", "0").toDouble();
1104         if ( !qFuzzyCompare(randomized, 0.0) )
1105             return false;
1106 
1107         qreal rounded = attr(args.element, "inkscape", "rounded", "0").toDouble();
1108         if ( !qFuzzyCompare(rounded, 0.0) )
1109             return false;
1110 
1111 
1112         ShapeCollection shapes;
1113         auto shape = push<model::PolyStar>(shapes);
1114         shape->points.set(
1115             attr(args.element, "sodipodi", "sides").toInt()
1116         );
1117         auto flat = attr(args.element, "inkscape", "flatsided");
1118         shape->type.set(
1119             flat == "true" ?
1120             model::PolyStar::Polygon :
1121             model::PolyStar::Star
1122         );
1123         shape->position.set(QPointF(
1124             attr(args.element, "sodipodi", "cx").toDouble(),
1125             attr(args.element, "sodipodi", "cy").toDouble()
1126         ));
1127         shape->outer_radius.set(attr(args.element, "sodipodi", "r1").toDouble());
1128         shape->inner_radius.set(attr(args.element, "sodipodi", "r2").toDouble());
1129         shape->angle.set(
1130             math::rad2deg(attr(args.element, "sodipodi", "arg1").toDouble())
1131             +90
1132         );
1133 
1134         add_shapes(args, std::move(shapes));
1135         return true;
1136     }
1137 
1138     void parseshape_use(const ParseFuncArgs& args)
1139     {
1140         QString id = attr(args.element, "xlink", "href");
1141         if ( !id.startsWith('#') )
1142             return;
1143         id.remove(0,  1);
1144         QDomElement element = element_by_id(id);
1145         if ( element.isNull() )
1146             return;
1147 
1148         Style style = parse_style(args.element, args.parent_style);
1149         auto group = std::make_unique<model::Group>(document);
1150         apply_common_style(group.get(), args.element, style);
1151         set_name(group.get(), args.element);
1152 
1153         parse_shape({element, &group->shapes, style, true});
1154 
1155         group->transform.get()->position.set(
1156             QPointF(len_attr(args.element, "x", 0), len_attr(args.element, "y", 0))
1157         );
1158         parse_transform(args.element, group.get(), group->transform.get());
1159         args.shape_parent->insert(std::move(group));
1160     }
1161 
1162     QString find_asset_file(const QString& path)
1163     {
1164         QFileInfo finfo(path);
1165         if ( finfo.exists() )
1166             return path;
1167         else if ( default_asset_path.exists(path) )
1168             return default_asset_path.filePath(path);
1169         else if ( default_asset_path.exists(finfo.fileName()) )
1170             return default_asset_path.filePath(finfo.fileName());
1171 
1172         return {};
1173     }
1174 
1175     bool open_asset_file(model::Bitmap* image, const QString& path)
1176     {
1177         if ( path.isEmpty() )
1178             return false;
1179 
1180         auto file = find_asset_file(path);
1181         if ( file.isEmpty() )
1182             return false;
1183 
1184         return image->from_file(file);
1185     }
1186 
1187     void parseshape_image(const ParseFuncArgs& args)
1188     {
1189         auto bitmap = std::make_unique<model::Bitmap>(document);
1190 
1191         bool open = false;
1192         QString href = attr(args.element, "xlink", "href");
1193         QUrl url = QUrl(href);
1194 
1195         if ( url.isRelative() )
1196             open = open_asset_file(bitmap.get(), href);
1197         if ( !open )
1198         {
1199             if ( url.isLocalFile() )
1200                 open = open_asset_file(bitmap.get(), url.toLocalFile());
1201             else
1202                 open = bitmap->from_url(url);
1203         }
1204 
1205         if ( !open )
1206         {
1207             QString path = attr(args.element, "sodipodi", "absref");
1208             open = open_asset_file(bitmap.get(), path);
1209         }
1210         if ( !open )
1211             warning(QString("Could not load image %1").arg(href));
1212 
1213         auto image = std::make_unique<model::Image>(document);
1214         image->image.set(document->assets()->images->values.insert(std::move(bitmap)));
1215 
1216         QTransform trans;
1217         if ( args.element.hasAttribute("transform") )
1218             trans = svg_transform(args.element.attribute("transform"), trans).transform;
1219         trans.translate(
1220             len_attr(args.element, "x", 0),
1221             len_attr(args.element, "y", 0)
1222         );
1223         image->transform->set_transform_matrix(trans);
1224 
1225         args.shape_parent->insert(std::move(image));
1226     }
1227 
1228     struct TextStyle
1229     {
1230         QString family = "sans-serif";
1231         int weight = QFont::Normal;
1232         QFont::Style style = QFont::StyleNormal;
1233         qreal line_spacing = 0;
1234         qreal size = 64;
1235         bool keep_space = false;
1236         QPointF pos;
1237     };
1238 
1239     TextStyle parse_text_style(const ParseFuncArgs& args, const TextStyle& parent)
1240     {
1241         TextStyle out = parent;
1242 
1243         Style style = parse_style(args.element, args.parent_style);
1244 
1245         if ( style.contains("font-family") )
1246             out.family = style["font-family"];
1247 
1248         if ( style.contains("font-style") )
1249         {
1250             QString slant = style["font-style"];
1251             if ( slant == "normal" ) out.style = QFont::StyleNormal;
1252             else if ( slant == "italic" ) out.style = QFont::StyleItalic;
1253             else if ( slant == "oblique" ) out.style = QFont::StyleOblique;
1254         }
1255 
1256         if ( style.contains("font-size") )
1257         {
1258             QString size = style["font-size"];
1259             static const std::map<QString, int> size_names = {
1260                 {{"xx-small"}, {8}},
1261                 {{"x-small"}, {16}},
1262                 {{"small"}, {32}},
1263                 {{"medium"}, {64}},
1264                 {{"large"}, {128}},
1265                 {{"x-large"}, {256}},
1266                 {{"xx-large"}, {512}},
1267             };
1268             if ( size == "smaller" )
1269                 out.size /= 2;
1270             else if ( size == "larger" )
1271                 out.size *= 2;
1272             else if ( size_names.count(size) )
1273                 out.size = size_names.at(size);
1274             else
1275                 out.size = parse_unit(size);
1276         }
1277 
1278         if ( style.contains("font-weight") )
1279         {
1280             QString weight = style["font-weight"];
1281             if ( weight == "bold" )
1282                 out.weight = 700;
1283             else if ( weight == "normal" )
1284                 out.weight = 400;
1285             else if ( weight == "bolder" )
1286                 out.weight = qMin(1000, out.weight + 100);
1287             else if ( weight == "lighter")
1288                 out.weight = qMax(1, out.weight - 100);
1289             else
1290                 out.weight = weight.toInt();
1291         }
1292 
1293         if ( style.contains("line-height") )
1294             out.line_spacing = parse_unit(style["line-height"]);
1295 
1296 
1297         if ( args.element.hasAttribute("xml:space") )
1298             out.keep_space = args.element.attribute("xml:space") == "preserve";
1299 
1300         if ( args.element.hasAttribute("x") )
1301             out.pos.setX(len_attr(args.element, "x", 0));
1302         if ( args.element.hasAttribute("y") )
1303             out.pos.setY(len_attr(args.element, "y", 0));
1304 
1305         return out;
1306     }
1307 
1308     QString trim_text(const QString& text)
1309     {
1310         QString trimmed = text.simplified();
1311         if ( !text.isEmpty() && text.back().isSpace() )
1312             trimmed += ' ';
1313         return trimmed;
1314     }
1315 
1316     void apply_text_style(model::Font* font, const TextStyle& style)
1317     {
1318         font->family.set(style.family);
1319         font->size.set(unit_convert(style.size, "px", "pt"));
1320         QFont qfont;
1321         qfont.setFamily(style.family);
1322         qfont.setWeight(QFont::Weight(WeightConverter::convert(style.weight, WeightConverter::css, WeightConverter::qt)));
1323         qfont.setStyle(style.style);
1324 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
1325         QString style_string = QFontDatabase::styleString(qfont);
1326 #else
1327         QFontDatabase db;
1328         QString style_string = db.styleString(qfont);
1329 #endif
1330         font->style.set(style_string);
1331     }
1332 
1333     QPointF parse_text_element(const ParseFuncArgs& args, const TextStyle& parent_style)
1334     {
1335         TextStyle style = parse_text_style(args, parent_style);
1336         Style css_style = parse_style(args.element, args.parent_style);
1337 
1338         auto anim = parse_animated(args.element);
1339 
1340         model::TextShape* last = nullptr;
1341 
1342         QPointF offset;
1343         QPointF pos = style.pos;
1344         QString text;
1345         for ( const auto & child : ItemCountRange(args.element.childNodes()) )
1346         {
1347             ParseFuncArgs child_args = {child.toElement(), args.shape_parent, css_style, args.in_group};
1348             if ( child.isElement() )
1349             {
1350                 last = nullptr;
1351                 style.pos = pos + offset;
1352                 offset = parse_text_element(child_args, style);
1353             }
1354             else if ( child.isText() || child.isCDATASection() )
1355             {
1356                 text += child.toCharacterData().data();
1357 
1358                 if ( !last )
1359                 {
1360                     ShapeCollection shapes;
1361                     last = push<model::TextShape>(shapes);
1362 
1363                     last->position.set(pos + offset);
1364                     apply_text_style(last->font.get(), style);
1365 
1366                     for ( const auto& kf : anim.joined({"x", "y"}) )
1367                     {
1368                         last->position.set_keyframe(
1369                             kf.time,
1370                             offset + QPointF(kf.values[0].vector()[0], kf.values[1].vector()[0])
1371                         )->set_transition(kf.transition);
1372                     }
1373 
1374                     add_shapes(child_args, std::move(shapes));
1375                 }
1376 
1377                 last->text.set(style.keep_space ? text : trim_text(text));
1378 
1379                 offset = last->offset_to_next_character();
1380             }
1381         }
1382 
1383         return offset;
1384     }
1385 
1386     void parseshape_text(const ParseFuncArgs& args)
1387     {
1388         parse_text_element(args, {});
1389     }
1390 
1391     void parse_metadata()
1392     {
1393         auto meta = dom.elementsByTagNameNS(xmlns.at("cc"), "Work");
1394         if ( meta.count() == 0 )
1395             return;
1396 
1397         auto work = query_element({"metadata", "RDF", "Work"}, dom.documentElement());
1398         document->info().author = query({"creator", "Agent", "title"}, work);
1399         document->info().description = query({"description"}, work);
1400         for ( const auto& domnode : ItemCountRange(query_element({"subject", "Bag"}, work).childNodes()) )
1401         {
1402             if ( domnode.isElement() )
1403             {
1404                 auto child = domnode.toElement();
1405                 if ( child.tagName() == "li" )
1406                     document->info().keywords.push_back(child.text());
1407 
1408             }
1409         }
1410     }
1411 
1412     GroupMode group_mode;
1413     std::vector<CssStyleBlock> css_blocks;
1414     QDir default_asset_path;
1415 
1416     static const std::map<QString, void (Private::*)(const ParseFuncArgs&)> shape_parsers;
1417     static const QRegularExpression transform_re;
1418     static const QRegularExpression url_re;
1419 };
1420 
1421 const std::map<QString, void (glaxnimate::io::svg::SvgParser::Private::*)(const glaxnimate::io::svg::SvgParser::Private::ParseFuncArgs&)> glaxnimate::io::svg::SvgParser::Private::shape_parsers = {
1422     {"g",       &glaxnimate::io::svg::SvgParser::Private::parseshape_g},
1423     {"rect",    &glaxnimate::io::svg::SvgParser::Private::parseshape_rect},
1424     {"ellipse", &glaxnimate::io::svg::SvgParser::Private::parseshape_ellipse},
1425     {"circle",  &glaxnimate::io::svg::SvgParser::Private::parseshape_circle},
1426     {"line",    &glaxnimate::io::svg::SvgParser::Private::parseshape_line},
1427     {"polyline",&glaxnimate::io::svg::SvgParser::Private::parseshape_polyline},
1428     {"polygon", &glaxnimate::io::svg::SvgParser::Private::parseshape_polygon},
1429     {"path",    &glaxnimate::io::svg::SvgParser::Private::parseshape_path},
1430     {"use",     &glaxnimate::io::svg::SvgParser::Private::parseshape_use},
1431     {"image",   &glaxnimate::io::svg::SvgParser::Private::parseshape_image},
1432     {"text",    &glaxnimate::io::svg::SvgParser::Private::parseshape_text},
1433 };
1434 const QRegularExpression glaxnimate::io::svg::detail::SvgParserPrivate::unit_re{R"(([-+]?(?:[0-9]*\.[0-9]+|[0-9]+)([eE][-+]?[0-9]+)?)([a-z]*))"};
1435 const QRegularExpression glaxnimate::io::svg::SvgParser::Private::transform_re{R"(([a-zA-Z]+)\s*\(([^\)]*)\))"};
1436 const QRegularExpression glaxnimate::io::svg::SvgParser::Private::url_re{R"(url\s*\(\s*(#[-a-zA-Z0-9_]+)\s*\)\s*)"};
1437 const QRegularExpression glaxnimate::io::svg::detail::AnimateParser::separator{"\\s*,\\s*|\\s+"};
1438 const QRegularExpression glaxnimate::io::svg::detail::AnimateParser::clock_re{R"((?:(?:(?<hours>[0-9]+):)?(?:(?<minutes>[0-9]{2}):)?(?<seconds>[0-9]+(?:\.[0-9]+)?))|(?:(?<timecount>[0-9]+(?:\.[0-9]+)?)(?<unit>h|min|s|ms)))"};
1439 const QRegularExpression glaxnimate::io::svg::detail::AnimateParser::frame_separator_re{"\\s*;\\s*"};
1440 
1441 glaxnimate::io::svg::SvgParser::SvgParser(
1442     QIODevice* device,
1443     GroupMode group_mode,
1444     model::Document* document,
1445     const std::function<void(const QString&)>& on_warning,
1446     ImportExport* io,
1447     QSize forced_size,
1448     model::FrameTime default_time,
1449     QDir default_asset_path
1450 )
1451     : d(std::make_unique<Private>(document, on_warning, io, forced_size, default_time, group_mode, default_asset_path))
1452 {
1453     d->load(device);
1454 }
1455 
1456 glaxnimate::io::svg::SvgParser::~SvgParser()
1457 {
1458 }
1459 
1460 
1461 glaxnimate::io::mime::DeserializedData glaxnimate::io::svg::SvgParser::parse_to_objects()
1462 {
1463     glaxnimate::io::mime::DeserializedData data;
1464     data.initialize_data();
1465     d->parse(data.document.get());
1466     return data;
1467 }
1468 
1469 void glaxnimate::io::svg::SvgParser::parse_to_document()
1470 {
1471     d->parse();
1472 }
1473 
1474 static qreal hex(const QString& s, int start, int size)
1475 {
1476     return utils::mid_ref(s, start, size).toInt(nullptr, 16) / (size == 2 ? 255.0 : 15.0);
1477 }
1478 
1479 QColor glaxnimate::io::svg::parse_color(const QString& string)
1480 {
1481     if ( string.isEmpty() )
1482         return {};
1483 
1484     // #fff #112233
1485     if ( string[0] == '#' )
1486     {
1487         if ( string.size() == 4 || string.size() == 5 )
1488         {
1489             qreal alpha = string.size() == 4 ? 1. : hex(string, 4, 1);
1490             return QColor::fromRgbF(hex(string, 1, 1), hex(string, 2, 1), hex(string, 3, 1), alpha);
1491         }
1492         else if ( string.size() == 7 || string.size() == 9 )
1493         {
1494             qreal alpha = string.size() == 7 ? 1. : hex(string, 7, 2);
1495             return QColor::fromRgbF(hex(string, 1, 2), hex(string, 3, 2), hex(string, 5, 2), alpha);
1496         }
1497         return QColor();
1498     }
1499 
1500     // transparent
1501     if ( string == "transparent" || string == "none" )
1502         return QColor(0, 0, 0, 0);
1503 
1504     QRegularExpressionMatch match;
1505 
1506     // rgba(123, 123, 123, 0.7)
1507     static QRegularExpression rgba{R"(^rgba\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9.eE]+)\s*\)$)"};
1508     match = rgba.match(string);
1509     if ( match.hasMatch() )
1510         return QColor(match.captured(1).toInt(), match.captured(2).toInt(), match.captured(3).toInt(), match.captured(4).toDouble() * 255);
1511 
1512     // rgb(123, 123, 123)
1513     static QRegularExpression rgb{R"(^rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)$)"};
1514     match = rgb.match(string);
1515     if ( match.hasMatch() )
1516         return QColor(match.captured(1).toInt(), match.captured(2).toInt(), match.captured(3).toInt());
1517 
1518     // rgba(60%, 30%, 20%, 0.7)
1519     static QRegularExpression rgba_pc{R"(^rgba\s*\(\s*([0-9.eE]+)%\s*,\s*([0-9.eE]+)%\s*,\s*([0-9.eE]+)%\s*,\s*([0-9.eE]+)\s*\)$)"};
1520     match = rgba_pc.match(string);
1521     if ( match.hasMatch() )
1522         return QColor::fromRgbF(match.captured(1).toDouble() / 100, match.captured(2).toDouble() / 100, match.captured(3).toDouble() / 100, match.captured(4).toDouble());
1523 
1524     // rgb(60%, 30%, 20%)
1525     static QRegularExpression rgb_pc{R"(^rgb\s*\(\s*([0-9.eE]+)%\s*,\s*([0-9.eE]+)%\s*,\s*([0-9.eE]+)%\s*\)$)"};
1526     match = rgb_pc.match(string);
1527     if ( match.hasMatch() )
1528         return QColor::fromRgbF(match.captured(1).toDouble() / 100, match.captured(2).toDouble() / 100, match.captured(3).toDouble() / 100);
1529 
1530     // hsl(60, 30%, 20%)
1531     static QRegularExpression hsl{R"(^hsl\s*\(\s*([0-9.eE]+)\s*,\s*([0-9.eE]+)%\s*,\s*([0-9.eE]+)%\s*\)$)"};
1532     match = rgb_pc.match(string);
1533     if ( match.hasMatch() )
1534         return QColor::fromHslF(match.captured(1).toDouble() / 360, match.captured(2).toDouble() / 100, match.captured(3).toDouble() / 100);
1535 
1536     // hsla(60, 30%, 20%, 0.7)
1537     static QRegularExpression hsla{R"(^hsla\s*\(\s*([0-9.eE]+)\s*,\s*([0-9.eE]+)%\s*,\s*([0-9.eE]+)%\s*,\s*([0-9.eE]+)\s*\)$)"};
1538     match = rgb_pc.match(string);
1539     if ( match.hasMatch() )
1540         return QColor::fromHslF(match.captured(1).toDouble() / 360, match.captured(2).toDouble() / 100, match.captured(3).toDouble() / 100, match.captured(4).toDouble());
1541 
1542     // red
1543     return QColor(string);
1544 }