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

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_renderer.hpp"
0008 
0009 #include "model/document.hpp"
0010 #include "model/shapes/group.hpp"
0011 #include "model/shapes/layer.hpp"
0012 #include "model/shapes/precomp_layer.hpp"
0013 #include "model/shapes/rect.hpp"
0014 #include "model/shapes/ellipse.hpp"
0015 #include "model/shapes/path.hpp"
0016 #include "model/shapes/polystar.hpp"
0017 #include "model/shapes/fill.hpp"
0018 #include "model/shapes/stroke.hpp"
0019 #include "model/shapes/image.hpp"
0020 #include "model/shapes/text.hpp"
0021 #include "model/shapes/repeater.hpp"
0022 #include "model/animation/join_animatables.hpp"
0023 #include "model/custom_font.hpp"
0024 #include "math/math.hpp"
0025 
0026 #include "detail.hpp"
0027 #include "font_weight.hpp"
0028 #include "io/utils.hpp"
0029 
0030 using namespace glaxnimate::io::svg::detail;
0031 using namespace glaxnimate;
0032 
0033 class io::svg::SvgRenderer::Private
0034 {
0035 public:
0036     void collect_defs(model::Composition* comp)
0037     {
0038         if ( !at_start )
0039             return;
0040 
0041         fps = comp->fps.get();
0042         ip = comp->animation->first_frame.get();
0043         op = comp->animation->last_frame.get();
0044         if ( ip >= op )
0045             animated = NotAnimated;
0046 
0047         at_start = false;
0048         defs = element(svg, "defs");
0049         for ( const auto& color : comp->document()->assets()->colors->values )
0050             write_named_color(defs, color.get());
0051         for ( const auto& color : comp->document()->assets()->gradient_colors->values )
0052             write_gradient_colors(defs, color.get());
0053         for ( const auto& gradient : comp->document()->assets()->gradients->values )
0054             write_gradient(defs, gradient.get());
0055 
0056         auto view = element(svg, "sodipodi:namedview");
0057         view.setAttribute("inkscape:pagecheckerboard", "true");
0058         view.setAttribute("borderlayer", "true");
0059         view.setAttribute("bordercolor", "#666666");
0060         view.setAttribute("pagecolor", "#ffffff");
0061         view.setAttribute("inkscape:document-units", "px");
0062 
0063         add_fonts(comp->document());
0064 
0065         write_meta(comp);
0066     }
0067 
0068     void write_meta(model::Composition* comp)
0069     {
0070         auto rdf = element(element(svg, "metadata"), "rdf:RDF");
0071         auto work = element(rdf, "cc:Work");
0072         element(work, "dc:format").appendChild(dom.createTextNode("image/svg+xml"));
0073         QString dc_type = animated ? "MovingImage" : "StillImage";
0074         element(work, "dc:type").setAttribute("rdf:resource", "http://purl.org/dc/dcmitype/" + dc_type);
0075         element(work, "dc:title").appendChild(dom.createTextNode(comp->name.get()));
0076         auto document = comp->document();
0077 
0078         if ( document->info().empty() )
0079             return;
0080 
0081         if ( !document->info().author.isEmpty() )
0082             element(element(element(work, "dc:creator"), "cc:Agent"), "dc:title").appendChild(dom.createTextNode(document->info().author));
0083 
0084         if ( !document->info().description.isEmpty() )
0085             element(work, "dc:description").appendChild(dom.createTextNode(document->info().description));
0086 
0087         if ( !document->info().keywords.empty() )
0088         {
0089             auto bag = element(element(work, "dc:subject"), "rdf:Bag");
0090             for ( const auto& kw: document->info().keywords )
0091                 element(bag, "rdf:li").appendChild(dom.createTextNode(kw));
0092         }
0093     }
0094 
0095     void add_fonts(model::Document* document)
0096     {
0097         if ( font_type == CssFontType::None )
0098             return;
0099 
0100         QString css;
0101         static QString font_face = R"(
0102 @font-face {
0103     font-family: '%1';
0104     font-style: %2;
0105     font-weight: %3;
0106     src: url(%4);
0107 }
0108 )";
0109 
0110         for ( const auto & font : document->assets()->fonts->values )
0111         {
0112             auto custom = font->custom_font();
0113             if ( !custom.is_valid() )
0114                 continue;
0115 
0116             QRawFont raw = custom.raw_font();
0117             auto type = qMin(suggested_type(font.get()), font_type);
0118 
0119             if ( type == CssFontType::Link )
0120             {
0121                 auto link = element(svg, "link");
0122                 link.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
0123                 link.setAttribute("rel", "stylesheet");
0124                 link.setAttribute("href", font->css_url.get());
0125                 link.setAttribute("type", "text/css");
0126             }
0127             else if ( type == CssFontType::FontFace )
0128             {
0129                 css += font_face
0130                     .arg(custom.family())
0131                     .arg(WeightConverter::convert(raw.weight(), WeightConverter::qt, WeightConverter::css))
0132                     .arg(raw.style() == QFont::StyleNormal ? 0 : 1)
0133                     .arg(font->source_url.get())
0134                 ;
0135             }
0136             else if ( type == CssFontType::Embedded )
0137             {
0138                 QString base64_encoded = font->data.get().toBase64(QByteArray::Base64UrlEncoding);
0139                 QString format = model::CustomFontDatabase::font_data_format(font->data.get()) == model::FontFileFormat::OpenType ? "opentype" : "ttf";
0140 
0141                 css += font_face
0142                     .arg(custom.family())
0143                     .arg(WeightConverter::convert(raw.weight(), WeightConverter::qt, WeightConverter::css))
0144                     .arg(raw.style() == QFont::StyleNormal ? 0 : 1)
0145                     .arg("data:application/x-font-" + format + ";charset=utf-8;base64," + base64_encoded)
0146                 ;
0147             }
0148         }
0149 
0150         if ( !css.isEmpty() )
0151             element(svg, "style").appendChild(dom.createTextNode(css));
0152     }
0153 
0154     QDomElement element(QDomNode parent, const char* tag)
0155     {
0156         QDomElement e = dom.createElement(tag);
0157         parent.appendChild(e);
0158         return e;
0159     }
0160 
0161     void write_composition(QDomElement& parent, model::Composition* comp)
0162     {
0163         for ( const auto& lay : comp->shapes )
0164             write_shape(parent, lay.get(), false);
0165     }
0166 
0167     void write_visibility_attributes(QDomElement& parent, model::VisualNode* node)
0168     {
0169         if ( !node->visible.get() )
0170             parent.setAttribute("display", "none");
0171         if ( node->locked.get() )
0172             parent.setAttribute("sodipodi:insensitive", "true");
0173     }
0174 
0175     void write_shapes(QDomElement& parent, const model::ShapeListProperty& shapes, bool has_mask = false)
0176     {
0177         if ( shapes.empty() )
0178             return;
0179 
0180         auto it = shapes.begin();
0181         if ( has_mask )
0182             ++it;
0183 
0184         for ( ; it != shapes.end(); ++it )
0185             write_shape(parent, it->get(), false);
0186     }
0187 
0188 
0189     QString styler_to_css(model::Styler* styler)
0190     {
0191         if ( styler->use.get() )
0192             return "url(#" + non_uuid_ids_map[styler->use.get()] + ")";
0193         if ( styler->color.get().alpha() == 0 )
0194             return "transparent";
0195         return styler->color.get().name();
0196     }
0197 
0198     QDomElement write_styler_shapes(QDomElement& parent, model::Styler* styler, const Style::Map& style)
0199     {
0200         if ( styler->affected().size() == 1 )
0201         {
0202             write_shape_shape(parent, styler->affected()[0], style);
0203             write_visibility_attributes(parent, styler);
0204             parent.setAttribute("id", id(styler));
0205             return parent;
0206         }
0207 
0208         auto g = start_group(parent, styler);
0209         write_style(g, style);
0210         write_visibility_attributes(g, styler);
0211         g.setAttribute("id", id(styler));
0212 
0213         for ( model::ShapeElement* subshape : styler->affected() )
0214         {
0215             write_shape_shape(g, subshape, style);
0216         }
0217 
0218         return g;
0219     }
0220 
0221     QString unlerp_time(model::FrameTime time) const
0222     {
0223         return QString::number(math::unlerp(ip, op, time), 'f');
0224     }
0225 
0226     struct AnimationData
0227     {
0228         struct Attribute
0229         {
0230             QString attribute;
0231             QStringList values = {};
0232         };
0233 
0234         AnimationData(SvgRenderer::Private* parent, const std::vector<QString>& attrs, int n_keyframes,
0235                       qreal time_stretch, model::FrameTime time_start)
0236             : parent(parent), time_stretch(time_stretch), time_start(time_start)
0237         {
0238             attributes.reserve(attrs.size());
0239             for ( const auto& attr : attrs )
0240             {
0241                 attributes.push_back({attr});
0242                 attributes.back().values.reserve(n_keyframes);
0243             }
0244         }
0245 
0246         QString key_spline(const model::KeyframeTransition& trans)
0247         {
0248             return QString("%1 %2 %3 %4")
0249                 .arg(trans.before().x(), 0, 'f')
0250                 .arg(trans.before().y(), 0, 'f')
0251                 .arg(trans.after().x(), 0, 'f')
0252                 .arg(trans.after().y(), 0, 'f')
0253             ;
0254         }
0255 
0256         void add_values(const std::vector<QString>& vals)
0257         {
0258             for ( std::size_t i = 0; i != attributes.size(); i++ )
0259                 attributes[i].values.push_back(vals[i]);
0260         }
0261 
0262         void add_keyframe(model::FrameTime time, const std::vector<QString>& vals,
0263                           const model::KeyframeTransition& trans)
0264         {
0265             if ( time < parent->ip || time > parent->op )
0266                 return;
0267 
0268             if ( key_times.empty() && time > parent->ip )
0269             {
0270                 key_times.push_back("0");
0271                 key_splines.push_back("0 0 1 1");
0272                 add_values(vals);
0273             }
0274             else if ( hold && last + 1 < time )
0275             {
0276 
0277                 key_times.push_back(parent->unlerp_time(time - 1));
0278                 key_splines.push_back("0 0 1 1");
0279                 for ( std::size_t i = 0; i != attributes.size(); i++ )
0280                     attributes[i].values.push_back(attributes[i].values.back());
0281 
0282             }
0283 
0284             key_times.push_back(parent->unlerp_time(time));
0285             key_splines.push_back(key_spline(trans));
0286 
0287             for ( std::size_t i = 0; i != attributes.size(); i++ )
0288                 attributes[i].values.push_back(vals[i]);
0289 
0290             last = time;
0291             hold = trans.hold();
0292         }
0293 
0294         void add_dom(
0295             QDomElement& element, const char* tag = "animate", const QString& type = {},
0296             const QString& path = {}, bool auto_orient = false
0297         )
0298         {
0299             if ( last < parent->op && path.isEmpty() )
0300             {
0301                 key_times.push_back("1");
0302                 for ( auto& attr : attributes )
0303                 {
0304                     if ( !attr.values.empty() )
0305                         attr.values.push_back(attr.values.back());
0306                 }
0307             }
0308             else
0309             {
0310                 key_splines.pop_back();
0311             }
0312 
0313             QString key_times_str = key_times.join("; ");
0314             QString key_splines_str = key_splines.join("; ");
0315             for ( const auto& data : attributes )
0316             {
0317                 QDomElement animation = parent->element(element, tag);
0318                 animation.setAttribute("begin", parent->clock(time_start + time_stretch * parent->ip));
0319                 animation.setAttribute("dur", parent->clock(time_start + time_stretch * parent->op-parent->ip));
0320                 animation.setAttribute("attributeName", data.attribute);
0321                 animation.setAttribute("calcMode", "spline");
0322                 if ( !path.isEmpty() )
0323                 {
0324                     animation.setAttribute("path", path);
0325                     if ( auto_orient )
0326                         animation.setAttribute("rotate", "auto");
0327                 }
0328                 animation.setAttribute("keyTimes", key_times_str);
0329                 animation.setAttribute("keySplines", key_splines_str);
0330                 animation.setAttribute("repeatCount", "indefinite");
0331                 if ( !type.isEmpty() )
0332                     animation.setAttribute("type", type);
0333             }
0334         }
0335 
0336         SvgRenderer::Private* parent;
0337         std::vector<Attribute> attributes;
0338         QStringList key_times = {};
0339         QStringList key_splines = {};
0340         model::FrameTime last = 0;
0341         bool hold = false;
0342         qreal time_stretch = 1;
0343         model::FrameTime time_start = 0;
0344     };
0345 
0346     void write_property(
0347         QDomElement& element,
0348         model::AnimatableBase* property,
0349         const QString& attr
0350     )
0351     {
0352         element.setAttribute(attr, property->value().toString());
0353 
0354         if ( animated )
0355         {
0356             if ( property->keyframe_count() < 2 )
0357                 return;
0358 
0359             auto keyframes = split_keyframes(property);
0360 
0361             AnimationData data(this, {attr}, keyframes.size(), time_stretch, time_start);
0362 
0363             for ( int i = 0; i < int(keyframes.size()); i++ )
0364             {
0365                 auto kf = keyframes[i].get();
0366                 data.add_keyframe(time_to_global(kf->time()), {kf->value().toString()}, kf->transition());
0367             }
0368 
0369             data.add_dom(element);
0370         }
0371     }
0372 
0373     qreal time_to_global(qreal time)
0374     {
0375         for ( auto it = timing.rbegin(), end = timing.rend(); it != end; ++it )
0376             time = (*it)->time_from_local(time);
0377         return time;
0378     }
0379 
0380     template<class Callback>
0381     void write_properties(
0382         QDomElement& element,
0383         std::vector<const model::AnimatableBase*> properties,
0384         const std::vector<QString>& attrs,
0385         const Callback& callback
0386     )
0387     {
0388         auto jflags = animated == NotAnimated ? model::JoinAnimatables::NoKeyframes : model::JoinAnimatables::Normal;
0389         model::JoinedAnimatable j(std::move(properties), {}, jflags);
0390 
0391         {
0392             auto vals = callback(j.current_value());
0393             for ( std::size_t i = 0; i != attrs.size(); i++ )
0394                 element.setAttribute(attrs[i], vals[i]);
0395         }
0396 
0397         if ( j.animated() && animated )
0398         {
0399             auto keys = split_keyframes(&j);
0400             AnimationData data(this, attrs, keys.size(), time_stretch, time_start);
0401 
0402             for ( const auto& kf : keys )
0403                 data.add_keyframe(time_to_global(kf->time()), callback(j.value_at(kf->time())), kf->transition());
0404 
0405             data.add_dom(element);
0406         }
0407     }
0408 
0409     static std::vector<QString> callback_point(const std::vector<QVariant>& values)
0410     {
0411         return callback_point_result(values[0].toPointF());
0412     }
0413 
0414     static std::vector<QString> callback_point_result(const QPointF& c)
0415     {
0416         return std::vector<QString>{
0417             QString::number(c.x()),
0418             QString::number(c.y())
0419         };
0420     }
0421 
0422     void write_shape_rect(QDomElement& parent, model::Rect* rect, const Style::Map& style)
0423     {
0424         auto e = element(parent, "rect");
0425         write_style(e, style);
0426         write_properties(e, {&rect->position, &rect->size}, {"x", "y"},
0427             [](const std::vector<QVariant>& values){
0428                 QPointF c = values[0].toPointF();
0429                 QSizeF s = values[1].toSizeF();
0430                 return std::vector<QString>{
0431                     QString::number(c.x() - s.width()/2),
0432                     QString::number(c.y() - s.height()/2)
0433                 };
0434             }
0435         );
0436         write_properties(e, {&rect->size}, {"width", "height"},
0437             [](const std::vector<QVariant>& values){
0438                 QSizeF s = values[0].toSizeF();
0439                 return std::vector<QString>{
0440                     QString::number(s.width()),
0441                     QString::number(s.height())
0442                 };
0443             }
0444         );
0445         write_property(e, &rect->rounded, "ry");
0446     }
0447 
0448     void write_shape_ellipse(QDomElement& parent, model::Ellipse* ellipse, const Style::Map& style)
0449     {
0450         auto e = element(parent, "ellipse");
0451         write_style(e, style);
0452         write_properties(e, {&ellipse->position}, {"cx", "cy"}, &Private::callback_point);
0453         write_properties(e, {&ellipse->size}, {"rx", "ry"},
0454             [](const std::vector<QVariant>& values){
0455                 QSizeF s = values[0].toSizeF();
0456                 return std::vector<QString>{
0457                     QString::number(s.width() / 2),
0458                     QString::number(s.height() / 2)
0459                 };
0460             }
0461         );
0462     }
0463 
0464     void write_shape_star(QDomElement& parent, model::PolyStar* star, const Style::Map& style)
0465     {
0466         model::FrameTime time = star->time();
0467 
0468         auto e = write_bezier(parent, star, style);
0469 
0470         if ( star->outer_roundness.animated() || !qFuzzyIsNull(star->outer_roundness.get()) ||
0471              star->inner_roundness.animated() || !qFuzzyIsNull(star->inner_roundness.get()) )
0472             return;
0473 
0474         set_attribute(e, "sodipodi:type", "star");
0475         set_attribute(e, "inkscape:randomized", "0");
0476         // inkscape:rounded Works differently than lottie so we leave it as 0
0477         set_attribute(e, "inkscape:rounded", "0");
0478         int sides = star->points.get_at(time);
0479         set_attribute(e, "sodipodi:sides", sides);
0480         set_attribute(e, "inkscape:flatsided", star->type.get() == model::PolyStar::Polygon);
0481         QPointF c = star->position.get_at(time);
0482         set_attribute(e, "sodipodi:cx", c.x());
0483         set_attribute(e, "sodipodi:cy", c.y());
0484         set_attribute(e, "sodipodi:r1", star->outer_radius.get_at(time));
0485         set_attribute(e, "sodipodi:r2", star->inner_radius.get_at(time));
0486         qreal angle = math::deg2rad(star->angle.get_at(time) - 90);
0487         set_attribute(e, "sodipodi:arg1", angle);
0488         set_attribute(e, "sodipodi:arg2", angle + math::pi / sides);
0489     }
0490 
0491     void write_shape_text(QDomElement& parent, model::TextShape* text, Style::Map style)
0492     {
0493         QFontInfo font_info(text->font->query());
0494 
0495         // QFontInfo is broken, so we do something else
0496 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
0497         int weight = QFontDatabase::weight(font_info.family(), font_info.styleName());
0498         QFont::Style font_style = QFontDatabase::italic(font_info.family(), font_info.styleName()) ? QFont::StyleItalic : QFont::StyleNormal;
0499 #else
0500         QFontDatabase db;
0501         int weight = db.weight(font_info.family(), font_info.styleName());
0502         QFont::Style font_style = db.italic(font_info.family(), font_info.styleName()) ? QFont::StyleItalic : QFont::StyleNormal;
0503 #endif
0504 
0505         // Convert weight
0506         weight = WeightConverter::convert(weight, WeightConverter::qt, WeightConverter::css);
0507 
0508         style["font-family"] = font_info.family();
0509         style["font-size"] = QString("%1pt").arg(font_info.pointSizeF());
0510         style["line-height"] = QString("%1px").arg(text->font->line_spacing());
0511         style["font-weight"] = QString::number(weight);
0512         switch ( font_style )
0513         {
0514             case QFont::StyleNormal: style["font-style"] = "normal"; break;
0515             case QFont::StyleItalic: style["font-style"] = "italic"; break;
0516             case QFont::StyleOblique: style["font-style"] = "oblique"; break;
0517         }
0518 
0519         auto e = element(parent, "text");
0520         write_style(e, style);
0521         write_properties(e, {&text->position}, {"x", "y"}, &Private::callback_point);
0522 
0523         model::Font::CharDataCache cache;
0524         for ( const auto& line : text->font->layout(text->text.get()) )
0525         {
0526             auto tspan = element(e, "tspan");
0527             tspan.appendChild(dom.createTextNode(line.text));
0528             set_attribute(tspan, "sodipodi:role", "line");
0529 
0530             write_properties(tspan, {&text->position}, {"x", "y"}, [base=line.baseline](const std::vector<QVariant>& values){
0531                 return callback_point_result(values[0].toPointF() + base);
0532             });
0533             tspan.setAttribute("xml:space", "preserve");
0534         }
0535     }
0536 
0537     void write_shape_shape(QDomElement& parent, model::ShapeElement* shape, const Style::Map& style)
0538     {
0539         if ( auto rect = qobject_cast<model::Rect*>(shape) )
0540         {
0541             write_shape_rect(parent, rect, style);
0542         }
0543         else if ( auto ellipse = qobject_cast<model::Ellipse*>(shape) )
0544         {
0545             write_shape_ellipse(parent, ellipse, style);
0546         }
0547         else if ( auto star = qobject_cast<model::PolyStar*>(shape) )
0548         {
0549             write_shape_star(parent, star, style);
0550         }
0551         else if ( auto text = shape->cast<model::TextShape>() )
0552         {
0553             write_shape_text(parent, text, style);
0554         }
0555         else if ( !qobject_cast<model::Styler*>(shape) )
0556         {
0557             write_bezier(parent, shape, style);
0558         }
0559     }
0560 
0561     void write_styler_attrs(QDomElement& element, model::Styler* styler, const QString& attr)
0562     {
0563         if ( styler->use.get() )
0564         {
0565             element.setAttribute(attr, "url(#" + non_uuid_ids_map[styler->use.get()] + ")");
0566             return;
0567         }
0568 
0569         write_property(element, &styler->color, attr);
0570         write_property(element, &styler->opacity, attr+"-opacity");
0571     }
0572     void write_image(model::Image* img, QDomElement& parent)
0573     {
0574         if ( img->image.get() )
0575         {
0576             auto e = element(parent, "image");
0577             set_attribute(e, "x", 0);
0578             set_attribute(e, "y", 0);
0579             set_attribute(e, "width", img->image->width.get());
0580             set_attribute(e, "height", img->image->height.get());
0581             transform_to_attr(e, img->transform.get());
0582             set_attribute(e, "xlink:href", img->image->to_url().toString());
0583         }
0584     }
0585 
0586     void write_stroke(model::Stroke* stroke, QDomElement& parent)
0587     {
0588         Style::Map style;
0589         style["fill"] = "none";
0590         if ( !animated )
0591         {
0592             style["stroke"] = styler_to_css(stroke);
0593             style["stroke-opacity"] = QString::number(stroke->opacity.get());
0594             style["stroke-width"] = QString::number(stroke->width.get());
0595         }
0596         switch ( stroke->cap.get() )
0597         {
0598             case model::Stroke::Cap::ButtCap:
0599                 style["stroke-linecap"] = "butt";
0600                 break;
0601             case model::Stroke::Cap::RoundCap:
0602                 style["stroke-linecap"] = "round";
0603                 break;
0604             case model::Stroke::Cap::SquareCap:
0605                 style["stroke-linecap"] = "square";
0606                 break;
0607 
0608         }
0609         switch ( stroke->join.get() )
0610         {
0611             case model::Stroke::Join::BevelJoin:
0612                 style["stroke-linejoin"] = "bevel";
0613                 break;
0614             case model::Stroke::Join::RoundJoin:
0615                 style["stroke-linejoin"] = "round";
0616                 break;
0617             case model::Stroke::Join::MiterJoin:
0618                 style["stroke-linejoin"] = "miter";
0619                 style["stroke-miterlimit"] = QString::number(stroke->miter_limit.get());
0620                 break;
0621         }
0622         style["stroke-dasharray"] = "none";
0623         QDomElement g = write_styler_shapes(parent, stroke, style);
0624         if ( animated )
0625         {
0626             write_styler_attrs(g, stroke, "stroke");
0627             write_property(g, &stroke->width, "stroke-width");
0628         }
0629     }
0630 
0631     void write_fill(model::Fill* fill, QDomElement& parent)
0632     {
0633         Style::Map style;
0634         if ( !animated )
0635         {
0636             style["fill"] = styler_to_css(fill);
0637             style["fill-opacity"] = QString::number(fill->opacity.get());
0638         }
0639         style["stroke"] = "none";
0640         QDomElement g = write_styler_shapes(parent, fill, style);
0641         if ( animated )
0642             write_styler_attrs(g, fill, "fill");
0643     }
0644 
0645     void write_precomp_layer(model::PreCompLayer* layer, QDomElement& parent)
0646     {
0647         if ( layer->composition.get() )
0648         {
0649             timing.push_back(layer->timing.get());
0650             auto clip = element(defs, "clipPath");
0651             set_attribute(clip, "id", "clip_" + id(layer));
0652             set_attribute(clip, "clipPathUnits", "userSpaceOnUse");
0653             auto clip_rect = element(clip, "rect");
0654             set_attribute(clip_rect, "x", "0");
0655             set_attribute(clip_rect, "y", "0");
0656             set_attribute(clip_rect, "width", layer->size.get().width());
0657             set_attribute(clip_rect, "height", layer->size.get().height());
0658 
0659             auto e = start_layer(parent, layer);
0660             transform_to_attr(e, layer->transform.get());
0661             write_property(e, &layer->opacity, "opacity");
0662             write_visibility_attributes(parent, layer);
0663             time_stretch = layer->timing->stretch.get();
0664             time_start = layer->timing->start_time.get();
0665             write_composition(e, layer->composition.get());
0666             time_stretch = 1;
0667             time_start = 0;
0668             timing.pop_back();
0669         }
0670     }
0671 
0672     void write_repeater_vis(QDomElement& element, model::Repeater* repeater, int index, int n_copies)
0673     {
0674         element.setAttribute("display", index < repeater->copies.get() ? "block" : "none");
0675 
0676         float alpha_lerp = float(index) / (n_copies == 1 ? 1 : n_copies - 1);
0677         model::JoinAnimatables opacity({&repeater->start_opacity, &repeater->end_opacity}, model::JoinAnimatables::NoValues);
0678         auto opacity_func = [&alpha_lerp](float a, float b){
0679             return math::lerp(a, b, alpha_lerp);
0680         };
0681         set_attribute(element, "opacity", opacity.combine_current_value<float, float>(opacity_func));
0682 
0683         if ( animated )
0684         {
0685             int kf_count = repeater->copies.keyframe_count();
0686             if ( kf_count >= 2 )
0687             {
0688                 AnimationData anim_display(this, {"display"}, kf_count, time_stretch, time_start);
0689 
0690                 for ( int i = 0; i < kf_count; i++ )
0691                 {
0692                     auto kf = repeater->copies.keyframe(i);
0693                     anim_display.add_keyframe(time_to_global(kf->time()), {index < kf->get() ? "block" : "none"}, kf->transition());
0694                 }
0695 
0696                 anim_display.add_dom(element);
0697             }
0698 
0699             if ( opacity.animated() )
0700             {
0701                 AnimationData anim_opacity(this, {"opacity"}, opacity.keyframes().size(), time_stretch, time_start);
0702                 for ( const auto& keyframe : opacity.keyframes() )
0703                 {
0704                     anim_opacity.add_keyframe(
0705                         time_to_global(keyframe.time),
0706                         {QString::number(opacity.combine_value_at<float, float>(keyframe.time, opacity_func))},
0707                         keyframe.transition()
0708                     );
0709                 }
0710             }
0711         }
0712     }
0713 
0714     void write_repeater(model::Repeater* repeater, QDomElement& parent, bool force_draw)
0715     {
0716         int n_copies = repeater->max_copies();
0717         if ( n_copies < 1 )
0718             return;
0719 
0720         QDomElement container = start_group(parent, repeater);
0721         QString base_id = id(repeater);
0722         QString prev_clone_id = base_id + "_0";
0723         QDomElement og = element(container, "g");
0724         og.setAttribute("id", prev_clone_id);
0725         for ( const auto& sib : repeater->affected() )
0726             write_shape(og, sib, force_draw);
0727 
0728         write_repeater_vis(og, repeater, 0, n_copies);
0729 
0730         for ( int i = 1; i < n_copies; i++ )
0731         {
0732             QString clone_id = base_id + "_" + QString::number(i);;
0733             QDomElement use = element(container, "use");
0734             use.setAttribute("xlink:href", "#" + prev_clone_id);
0735             use.setAttribute("id", clone_id);
0736             write_repeater_vis(use, repeater, i, n_copies);
0737             transform_to_attr(use, repeater->transform.get());
0738             prev_clone_id = clone_id;
0739         }
0740     }
0741 
0742     void write_shape(QDomElement& parent, model::ShapeElement* shape, bool force_draw)
0743     {
0744         if ( auto grp = qobject_cast<model::Group*>(shape) )
0745         {
0746             write_group_shape(parent, grp);
0747         }
0748         else if ( auto stroke = qobject_cast<model::Stroke*>(shape) )
0749         {
0750             if ( stroke->visible.get() )
0751                 write_stroke(stroke, parent);
0752         }
0753         else if ( auto fill = qobject_cast<model::Fill*>(shape) )
0754         {
0755             if ( fill->visible.get() )
0756                 write_fill(fill, parent);
0757         }
0758         else if ( auto img = qobject_cast<model::Image*>(shape) )
0759         {
0760             write_image(img, parent);
0761         }
0762         else if ( auto layer = qobject_cast<model::PreCompLayer*>(shape) )
0763         {
0764             write_precomp_layer(layer, parent);
0765         }
0766         else if ( auto repeater = qobject_cast<model::Repeater*>(shape) )
0767         {
0768             write_repeater(repeater, parent, force_draw);
0769         }
0770         else if ( force_draw )
0771         {
0772             write_shape_shape(parent, shape, {});
0773             write_visibility_attributes(parent, shape);
0774             set_attribute(parent, "id", id(shape));
0775         }
0776     }
0777 
0778     QDomElement write_bezier(QDomElement& parent, model::ShapeElement* shape, const Style::Map& style)
0779     {
0780         QDomElement path = element(parent, "path");
0781         write_style(path, style);
0782         QString d;
0783         QString nodetypes;
0784         std::tie(d, nodetypes) = path_data(shape->shapes(shape->time()));
0785         set_attribute(path, "d", d);
0786         set_attribute(path, "sodipodi:nodetypes", nodetypes);
0787 
0788         if ( animated )
0789         {
0790             std::vector<const model::AnimatableBase*> props;
0791             for ( auto prop : shape->properties() )
0792             {
0793                 if ( prop->traits().flags & model::PropertyTraits::Animated )
0794                     props.push_back(static_cast<model::AnimatableBase*>(prop));
0795             }
0796 
0797             model::JoinAnimatables j(std::move(props), model::JoinAnimatables::NoValues);
0798 
0799             if ( j.animated() )
0800             {
0801                 AnimationData data(this, {"d"}, j.keyframes().size(), time_stretch, time_start);
0802 
0803                 for ( const auto& kf : j )
0804                     data.add_keyframe(time_to_global(kf.time), {path_data(shape->shapes(kf.time)).first}, kf.transition());
0805 
0806                 data.add_dom(path);
0807             }
0808         }
0809         return path;
0810     }
0811 
0812     /**
0813      * \brief Creates a <g> element for recurse_parents
0814      * \param parent        DOM element to add the <g> into
0815      * \param ancestor      Ancestor layer (to create the <g> for)
0816      * \param descendant    Descendant layer
0817      */
0818     QDomElement start_layer_recurse_parents(const QDomElement& parent, model::Layer* ancestor, model::Layer* descendant)
0819     {
0820         QDomElement g = element(parent, "g");
0821         g.setAttribute("id", id(descendant) + "_" + id(ancestor));
0822         g.setAttribute("inkscape:label", i18n("%1 (%2)", descendant->object_name(), ancestor->object_name()));
0823         g.setAttribute("inkscape:groupmode", "layer");
0824         transform_to_attr(g, ancestor->transform.get());
0825         return g;
0826     }
0827 
0828     /**
0829      * \brief Creates nested <g> elements for each layer parent (using the parent property)
0830      * \param parent        DOM element to add the elements into
0831      * \param ancestor      Ancestor layer (searched recursively for parents)
0832      * \param descendant    Descendant layer
0833      */
0834     QDomElement recurse_parents(const QDomElement& parent, model::Layer* ancestor, model::Layer* descendant)
0835     {
0836         if ( !ancestor->parent.get() )
0837             return start_layer_recurse_parents(parent, ancestor, descendant);
0838         return start_layer_recurse_parents(recurse_parents(parent, ancestor->parent.get(), descendant), ancestor, descendant);
0839     }
0840 
0841     void write_group_shape(QDomElement& parent, model::Group* group)
0842     {
0843         QDomElement g;
0844         bool has_mask = false;
0845         if ( auto layer = group->cast<model::Layer>() )
0846         {
0847             if ( !layer->render.get() )
0848                 return;
0849 
0850             if ( layer->parent.get() )
0851             {
0852                 QDomElement parent_g = recurse_parents(parent, layer->parent.get(), layer);
0853                 g = start_layer(parent_g, group);
0854             }
0855             else
0856             {
0857                 g = start_layer(parent, group);
0858             }
0859 
0860             if ( layer->mask->has_mask() )
0861             {
0862                 has_mask = true;
0863 
0864                 QDomElement clip = element(defs, "mask");
0865                 QString mask_id = "clip_" + id(layer);
0866                 clip.setAttribute("id", mask_id);
0867                 clip.setAttribute("mask-type", "alpha");
0868                 if ( layer->shapes.size() > 1 )
0869                     write_shape(clip, layer->shapes[0], false);
0870 
0871                 g.setAttribute("mask", "url(#" + mask_id + ")");
0872             }
0873 
0874             if ( animated && layer->visible.get() )
0875             {
0876                 auto* lay_range = layer->animation.get();
0877                 auto* doc_range = layer->owner_composition()->animation.get();
0878                 bool has_start = lay_range->first_frame.get() > doc_range->first_frame.get();
0879                 bool has_end = lay_range->last_frame.get() < doc_range->last_frame.get();
0880 
0881                 if ( has_start || has_end )
0882                 {
0883                     QDomElement animation = element(g, "animate");
0884                     animation.setAttribute("begin", clock(ip));
0885                     animation.setAttribute("dur", clock(op-ip));
0886                     animation.setAttribute("calcMode", "discrete");
0887                     animation.setAttribute("attributeName", "display");
0888                     animation.setAttribute("repeatCount", "indefinite");
0889                     QString times;
0890                     QString vals;
0891 
0892                     times += "0;";
0893 
0894                     if ( has_start )
0895                     {
0896                         vals += "none;inline;";
0897                         times += unlerp_time(lay_range->first_frame.get()) + ";";
0898                     }
0899                     else
0900                     {
0901                         vals += "inline;";
0902                     }
0903 
0904                     if ( has_end )
0905                     {
0906                         vals += "none;";
0907                         times += unlerp_time(lay_range->last_frame.get()) + ";";
0908                     }
0909 
0910                     animation.setAttribute("values", vals);
0911                     animation.setAttribute("keyTimes", times);
0912                 }
0913             }
0914         }
0915         else
0916         {
0917             g = start_group(parent, group);
0918         }
0919 
0920         transform_to_attr(g, group->transform.get(), group->auto_orient.get());
0921         write_property(g, &group->opacity, "opacity");
0922         write_visibility_attributes(g, group);
0923         write_shapes(g, group->shapes, has_mask);
0924     }
0925 
0926     template<class PropT, class Callback>
0927     QDomElement transform_property(
0928         QDomElement& e, const char* name, PropT* prop, const Callback& callback,
0929         const QString& path = {}, bool auto_orient = false
0930     )
0931     {
0932         model::JoinAnimatables j({prop}, model::JoinAnimatables::NoValues);
0933 
0934         auto parent = e.parentNode();
0935         QDomElement g = dom.createElement("g");
0936         parent.insertBefore(g, e);
0937         parent.removeChild(e);
0938         g.appendChild(e);
0939 
0940         if ( j.animated() )
0941         {
0942             AnimationData data(this, {"transform"}, j.keyframes().size(), time_stretch, time_start);
0943 
0944             if ( !path.isEmpty() )
0945             {
0946                 for ( const auto& kf : j )
0947                     data.add_keyframe(time_to_global(kf.time), {""}, kf.transition());
0948                 data.add_dom(g, "animateMotion", "", path, auto_orient);
0949             }
0950             else
0951             {
0952 
0953                 for ( const auto& kf : j )
0954                     data.add_keyframe(time_to_global(kf.time), {callback(prop->get_at(kf.time))}, kf.transition());
0955                 data.add_dom(g, "animateTransform", name);
0956             }
0957         }
0958 
0959         g.setAttribute("transform", QString("%1(%2)").arg(name).arg(callback(prop->get())));
0960         return g;
0961     }
0962 
0963     void transform_to_attr(QDomElement& parent, model::Transform* transf, bool auto_orient = false)
0964     {
0965         if ( animated && (transf->position.animated() || transf->scale.animated() || transf->rotation.animated() || transf->anchor_point.animated()) )
0966         {
0967             QDomElement subject = parent;
0968             subject = transform_property(subject, "translate", &transf->anchor_point, [](const QPointF& val){
0969                 return QString("%1 %2").arg(-val.x()).arg(-val.y());
0970             });
0971             subject = transform_property(subject, "scale", &transf->scale, [](const QVector2D& val){
0972                 return QString("%1 %2").arg(val.x()).arg(val.y());
0973             });
0974             subject = transform_property(subject, "rotate", &transf->rotation, [](qreal val){
0975                 return QString::number(val);
0976             });
0977             math::bezier::MultiBezier mb;
0978             mb.beziers().push_back(transf->position.bezier());
0979             subject = transform_property(subject, "translate", &transf->position, [](const QPointF& val){
0980                 return QString("%1 %2").arg(val.x()).arg(val.y());
0981             }, path_data(mb).first, auto_orient);
0982         }
0983         else
0984         {
0985             auto matr = transf->transform_matrix(transf->time());
0986             parent.setAttribute("transform", QString("matrix(%1, %2, %3, %4, %5, %6)")
0987                 .arg(matr.m11())
0988                 .arg(matr.m12())
0989                 .arg(matr.m21())
0990                 .arg(matr.m22())
0991                 .arg(matr.m31())
0992                 .arg(matr.m32())
0993             );
0994         }
0995     }
0996 
0997     void write_style(QDomElement& element, const Style::Map& s)
0998     {
0999         QString st;
1000         for ( auto it : s )
1001         {
1002             st.append(it.first);
1003             st.append(':');
1004             st.append(it.second);
1005             st.append(';');
1006         }
1007         element.setAttribute("style", st);
1008     }
1009 
1010     QDomElement start_group(QDomElement& parent, model::DocumentNode* node)
1011     {
1012         QDomElement g = element(parent, "g");
1013         g.setAttribute("id", id(node));
1014         g.setAttribute("inkscape:label", node->object_name());
1015         return g;
1016     }
1017 
1018     QDomElement start_layer(QDomElement& parent, model::DocumentNode* node)
1019     {
1020         auto g = start_group(parent, node);
1021         g.setAttribute("inkscape:groupmode", "layer");
1022         return g;
1023     }
1024 
1025     QString id(model::DocumentNode* node)
1026     {
1027         return node->type_name() + "_" + node->uuid.get().toString(QUuid::Id128);
1028     }
1029 
1030     /// Avoid locale nonsense by defining these functions (on ASCII chars) manually
1031     static constexpr bool valid_id_start(char c) noexcept
1032     {
1033         return  ( c >= 'a' && c <= 'z') ||
1034                 ( c >= 'A' && c <= 'Z') ||
1035                 c == '_';
1036     }
1037 
1038     static constexpr bool valid_id(char c) noexcept
1039     {
1040         return  valid_id_start(c) ||
1041                 ( c >= '0' && c <= '9') ||
1042                 c == '-';
1043     }
1044 
1045     void write_named_color(QDomElement& parent, model::NamedColor* color)
1046     {
1047         auto gradient = element(parent, "linearGradient");
1048         gradient.setAttribute("osb:paint", "solid");
1049         QString id = pretty_id(color->name.get(), color);
1050         non_uuid_ids_map[color] = id;
1051         gradient.setAttribute("id", id);
1052 
1053         auto stop = element(gradient, "stop");
1054         stop.setAttribute("offset", "0");
1055         write_property(stop, &color->color, "stop-color");
1056     }
1057 
1058     QString pretty_id(const QString& s, model::DocumentNode* node)
1059     {
1060         if ( s.isEmpty() )
1061             return id(node);
1062 
1063         QByteArray str = s.toLatin1();
1064         QString id_attempt;
1065         if ( !valid_id_start(str[0]) )
1066             id_attempt.push_back('_');
1067 
1068         for ( char c : str )
1069         {
1070             if ( c == ' ' )
1071                 id_attempt.push_back('_');
1072             else if ( valid_id(c) )
1073                 id_attempt.push_back(c);
1074         }
1075 
1076         if ( id_attempt.isEmpty() )
1077             return id(node);
1078 
1079         QString id_final = id_attempt;
1080         int i = 1;
1081         while ( non_uuid_ids.count(id_final) )
1082             id_final = id_attempt + QString::number(i++);
1083 
1084         return id_final;
1085     }
1086 
1087     template<class T>
1088     std::enable_if_t<std::is_arithmetic_v<T> && !std::is_same_v<T, bool>>
1089     set_attribute(QDomElement& e, const QString& name, T val)
1090     {
1091         // not using e.setAttribute overloads to bypass locale settings
1092         e.setAttribute(name, QString::number(val));
1093     }
1094 
1095     void set_attribute(QDomElement& e, const QString& name, bool val)
1096     {
1097         e.setAttribute(name, val ? "true" : "false");
1098     }
1099 
1100     void set_attribute(QDomElement& e, const QString& name, const char* val)
1101     {
1102         e.setAttribute(name, val);
1103     }
1104 
1105     void set_attribute(QDomElement& e, const QString& name, const QString& val)
1106     {
1107         e.setAttribute(name, val);
1108     }
1109 
1110 
1111     void write_gradient_colors(QDomElement& parent, model::GradientColors* gradient)
1112     {
1113         auto e = element(parent, "linearGradient");
1114         QString id = pretty_id(gradient->name.get(), gradient);
1115         non_uuid_ids_map[gradient] = id;
1116         e.setAttribute("id", id);
1117 
1118         if ( animated && gradient->colors.keyframe_count() > 1 )
1119         {
1120             int n_stops = std::numeric_limits<int>::max();
1121             for ( const auto& kf : gradient->colors )
1122                 if ( kf.get().size() < n_stops )
1123                     n_stops = kf.get().size();
1124 
1125             auto stops = gradient->colors.get();
1126             for ( int i = 0; i < n_stops; i++ )
1127             {
1128                 AnimationData data(this, {"offset", "stop-color"}, gradient->colors.keyframe_count(), time_stretch, time_start);
1129                 for ( const auto& kf : gradient->colors )
1130                 {
1131                     auto stop = kf.get()[i];
1132                     data.add_keyframe(
1133                         time_to_global(kf.time()),
1134                         {QString::number(stop.first), stop.second.name()},
1135                         kf.transition()
1136                     );
1137                 }
1138 
1139                 auto s = element(e, "stop");
1140                 s.setAttribute("stop-opacity", "1");
1141                 set_attribute(s, "offset", stops[i].first);
1142                 s.setAttribute("stop-color", stops[i].second.name());
1143                 data.add_dom(s);
1144             }
1145         }
1146         else
1147         {
1148             for ( const auto& stop : gradient->colors.get() )
1149             {
1150                 auto s = element(e, "stop");
1151                 s.setAttribute("stop-opacity", "1");
1152                 set_attribute(s, "offset", stop.first);
1153                 s.setAttribute("stop-color", stop.second.name());
1154             }
1155         }
1156     }
1157 
1158     void write_gradient(QDomElement& parent, model::Gradient* gradient)
1159     {
1160         QDomElement e;
1161         if ( gradient->type.get() == model::Gradient::Radial || gradient->type.get() == model::Gradient::Conical )
1162         {
1163             e = element(parent, "radialGradient");
1164             write_properties(e, {&gradient->start_point}, {"cx", "cy"}, &Private::callback_point);
1165             write_properties(e, {&gradient->highlight}, {"fx", "fy"}, &Private::callback_point);
1166 
1167             write_properties(e, {&gradient->start_point, &gradient->end_point}, {"r"},
1168                 [](const std::vector<QVariant>& values) -> std::vector<QString> {
1169                     return { QString::number(
1170                         math::length(values[1].toPointF() - values[0].toPointF())
1171                     )};
1172             });
1173         }
1174         else
1175         {
1176             e = element(parent, "linearGradient");
1177             write_properties(e, {&gradient->start_point}, {"x1", "y1"}, &Private::callback_point);
1178             write_properties(e, {&gradient->end_point}, {"x2", "y2"}, &Private::callback_point);
1179         }
1180 
1181         QString id = pretty_id(gradient->name.get(), gradient);
1182         non_uuid_ids_map[gradient] = id;
1183         e.setAttribute("id", id);
1184         e.setAttribute("gradientUnits", "userSpaceOnUse");
1185 
1186         auto it = non_uuid_ids_map.find(gradient->colors.get());
1187         if ( it != non_uuid_ids_map.end() )
1188             e.setAttribute("xlink:href", "#" + it->second);
1189     }
1190 
1191     QString clock(model::FrameTime time)
1192     {
1193         return QString::number(time / fps, 'f');
1194     }
1195 
1196     std::vector<model::StretchableTime*> timing;
1197     QDomDocument dom;
1198     qreal fps = 60;
1199     qreal ip = 0;
1200     qreal op = 60;
1201     bool at_start = true;
1202     std::set<QString> non_uuid_ids;
1203     std::map<model::DocumentNode*, QString> non_uuid_ids_map;
1204     AnimationType animated;
1205     QDomElement svg;
1206     QDomElement defs;
1207     CssFontType font_type;
1208     qreal time_stretch = 1;
1209     model::FrameTime time_start = 0;
1210 };
1211 
1212 
1213 io::svg::SvgRenderer::SvgRenderer(AnimationType animated, CssFontType font_type)
1214     : d(std::make_unique<Private>())
1215 {
1216     d->animated = animated;
1217     d->font_type = font_type;
1218     d->svg = d->dom.createElement("svg");
1219     d->dom.appendChild(d->svg);
1220     d->svg.setAttribute("xmlns", detail::xmlns.at("svg"));
1221     for ( const auto& p : detail::xmlns )
1222     {
1223         if ( !p.second.contains("android") )
1224             d->svg.setAttribute("xmlns:" + p.first, p.second);
1225     }
1226 
1227     d->write_style(d->svg, {
1228         {"fill", "none"},
1229         {"stroke", "none"}
1230     });
1231     d->svg.setAttribute("inkscape:export-xdpi", "96");
1232     d->svg.setAttribute("inkscape:export-ydpi", "96");
1233     d->svg.setAttribute("version", "1.1");
1234 }
1235 
1236 io::svg::SvgRenderer::~SvgRenderer()
1237 {
1238 }
1239 
1240 void io::svg::SvgRenderer::write_composition(model::Composition* comp)
1241 {
1242     d->collect_defs(comp);
1243     auto g = d->start_layer(d->svg, comp);
1244     d->write_composition(g, comp);
1245 }
1246 
1247 
1248 void io::svg::SvgRenderer::write_main(model::Composition* comp)
1249 {
1250     if ( d->at_start )
1251     {
1252         QString w  = QString::number(comp->width.get());
1253         QString h = QString::number(comp->height.get());
1254         d->svg.setAttribute("width", w);
1255         d->svg.setAttribute("height", h);
1256         d->svg.setAttribute("viewBox", QString("0 0 %1 %2").arg(w).arg(h));
1257         d->svg.appendChild(d->dom.createElement("title")).appendChild(d->dom.createTextNode(comp->name.get()));
1258         write_composition(comp);
1259     }
1260     else
1261     {
1262         write_composition(comp);
1263     }
1264 }
1265 
1266 void io::svg::SvgRenderer::write_shape(model::ShapeElement* shape)
1267 {
1268     d->collect_defs(shape->owner_composition());
1269     d->write_shape(d->svg, shape, true);
1270 }
1271 
1272 void io::svg::SvgRenderer::write_node(model::DocumentNode* node)
1273 {
1274     if ( auto co = qobject_cast<model::Composition*>(node) )
1275         write_main(co);
1276     else if ( auto sh = qobject_cast<model::ShapeElement*>(node) )
1277         write_shape(sh);
1278 }
1279 
1280 QDomDocument io::svg::SvgRenderer::dom() const
1281 {
1282     return d->dom;
1283 }
1284 
1285 void io::svg::SvgRenderer::write(QIODevice* device, bool indent)
1286 {
1287     device->write(d->dom.toByteArray(indent ? 4 : -1));
1288 }
1289 
1290 glaxnimate::io::svg::CssFontType glaxnimate::io::svg::SvgRenderer::suggested_type(model::EmbeddedFont* font)
1291 {
1292     if ( !font->css_url.get().isEmpty() )
1293         return CssFontType::Link;
1294     if ( !font->source_url.get().isEmpty() )
1295         return CssFontType::FontFace;
1296     if ( !font->data.get().isEmpty() )
1297         return CssFontType::Embedded;
1298     return CssFontType::None;
1299 }
1300 
1301 static char bezier_node_type(const math::bezier::Point& p)
1302 {
1303     switch ( p.type )
1304     {
1305         case math::bezier::PointType::Smooth:
1306             return 's';
1307         case math::bezier::PointType::Symmetrical:
1308             return 'z';
1309         case math::bezier::PointType::Corner:
1310         default:
1311             return 'c';
1312     }
1313 }
1314 
1315 std::pair<QString, QString> glaxnimate::io::svg::path_data(const math::bezier::MultiBezier& shape)
1316 {
1317     QString d;
1318     QString nodetypes;
1319     for ( const math::bezier::Bezier& b : shape.beziers() )
1320     {
1321         if ( b.empty() )
1322             continue;
1323 
1324         d += QString("M %1,%2 C").arg(b[0].pos.x()).arg(b[0].pos.y());
1325         nodetypes += bezier_node_type(b[0]);
1326 
1327         for ( int i = 1; i < b.size(); i++ )
1328         {
1329             d += QString(" %1,%2 %3,%4 %5,%6")
1330                 .arg(b[i-1].tan_out.x()).arg(b[i-1].tan_out.y())
1331                 .arg(b[i].tan_in.x()).arg(b[i].tan_in.y())
1332                 .arg(b[i].pos.x()).arg(b[i].pos.y())
1333             ;
1334             nodetypes += bezier_node_type(b[i]);
1335         }
1336 
1337         if ( b.closed() )
1338         {
1339             d += QString(" %1,%2 %3,%4 %5,%6")
1340                 .arg(b.back().tan_out.x()).arg(b.back().tan_out.y())
1341                 .arg(b[0].tan_in.x()).arg(b[0].tan_in.y())
1342                 .arg(b[0].pos.x()).arg(b[0].pos.y())
1343             ;
1344             d += " Z";
1345         }
1346     }
1347     return {d, nodetypes};
1348 }