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

0001 /*
0002  * SPDX-FileCopyrightText: 2019-2023 Mattia Basaglia <dev@dragon.best>
0003  *
0004  * SPDX-License-Identifier: GPL-3.0-or-later
0005  */
0006 
0007 #pragma once
0008 
0009 #include <set>
0010 #include <array>
0011 
0012 #include <QJsonDocument>
0013 #include <QJsonObject>
0014 #include <QJsonArray>
0015 
0016 #include "lottie_private_common.hpp"
0017 #include "io/svg/svg_parser.hpp"
0018 #include "model/animation/join_animatables.hpp"
0019 
0020 namespace glaxnimate::io::lottie::detail {
0021 
0022 struct FontInfo
0023 {
0024     QString name;
0025     QString family;
0026     QString style;
0027 };
0028 
0029 class LottieImporterState
0030 {
0031 public:
0032     LottieImporterState(
0033         model::Document* document,
0034         io::lottie::LottieFormat* format
0035     ) : document(document), format(format)
0036     {}
0037 
0038     void load(const QJsonObject& json)
0039     {
0040         load_version(json);
0041         load_meta(json["meta"]);
0042         main = document->assets()->compositions->values.insert(std::make_unique<model::Composition>(document));
0043         auto comps = load_assets(json["assets"].toArray());
0044         load_fonts(json["fonts"]["list"].toArray());
0045         load_composition(json, main);
0046         load_comps(comps);
0047     }
0048 
0049 private:
0050     void load_version(const QJsonObject& json)
0051     {
0052         if ( json.contains("v") )
0053         {
0054             auto parts = json["v"].toString().split(".");
0055             if ( parts.size() == 3 )
0056             {
0057                 for ( int i = 0; i < 3; i++ )
0058                     version[i] = parts[i].toInt();
0059             }
0060         }
0061     }
0062 
0063     bool animated(const QJsonObject& obj)
0064     {
0065         if ( obj.contains("a") )
0066             return obj["a"].toInt();
0067 
0068         if ( !obj["k"].isArray() )
0069             return 0;
0070 
0071         auto karr = obj["k"].toArray();
0072         return karr.size() > 0 && karr[0].isObject() && karr[0].toObject().contains("s");
0073     }
0074 
0075     template<class T>
0076     auto make_node(model::Document* document)
0077     {
0078         auto ptr = std::make_unique<T>(document);
0079         current_node = ptr.get();
0080         return ptr;
0081     }
0082 
0083     void warning(QString str, const QJsonObject& json)
0084     {
0085         if ( json.contains("nm") )
0086             str = json["nm"].toString() + ": " + str;
0087         Q_EMIT format->warning(str);
0088     }
0089 
0090     void load_stretchable_animation_container(const QJsonObject& json, model::StretchableTime* animation)
0091     {
0092         animation->start_time.set(json["st"].toDouble());
0093         animation->stretch.set(json["sr"].toDouble(1));
0094     }
0095 
0096     void load_animation_container(const QJsonObject& json, model::AnimationContainer* animation)
0097     {
0098         animation->first_frame.set(json["ip"].toDouble());
0099         animation->last_frame.set(json["op"].toDouble());
0100     }
0101 
0102     void load_composition(const QJsonObject& json, model::Composition* composition)
0103     {
0104         this->composition = composition;
0105         invalid_indices.clear();
0106         layer_indices.clear();
0107         deferred.clear();
0108 
0109         if ( composition != main )
0110         {
0111             composition->width.set(main->width.get());
0112             composition->height.set(main->height.get());
0113             composition->fps.set(main->fps.get());
0114             composition->animation->first_frame.set(main->animation->first_frame.get());
0115             composition->animation->last_frame.set(main->animation->last_frame.get());
0116         }
0117 
0118         if ( json.contains("fr") )
0119             composition->fps.set(json["fr"].toDouble());
0120         if ( json.contains("w") )
0121             composition->width.set(json["w"].toInt());
0122         if ( json.contains("h") )
0123             composition->height.set(json["h"].toInt());
0124 
0125         load_animation_container(json, composition->animation.get());
0126         load_basic(json, composition);
0127 
0128         {
0129             std::set<int> referenced;
0130             std::vector<QJsonObject> layer_jsons;
0131             auto layer_array = json["layers"].toArray();
0132             layer_jsons.reserve(layer_array.size());
0133             for ( auto val : layer_array )
0134             {
0135                 QJsonObject obj = val.toObject();
0136                 if ( obj.contains("parent") )
0137                     referenced.insert(obj["parent"].toInt());
0138                 layer_array.push_back(obj);
0139             }
0140 
0141             for ( auto layer : json["layers"].toArray() )
0142                 create_layer(layer.toObject(), referenced);
0143         }
0144 
0145         auto deferred_layers = std::move(deferred);
0146         deferred.clear();
0147         for ( const auto& pair: deferred_layers )
0148             load_layer(pair.second, static_cast<model::Layer*>(pair.first));
0149     }
0150 
0151     void load_visibility(model::VisualNode* node, const QJsonObject& json)
0152     {
0153         if ( json.contains("hd") && json["hd"].toBool() )
0154             node->visible.set(false);
0155     }
0156 
0157     void create_layer(const QJsonObject& json, std::set<int>& referenced)
0158     {
0159         int index = json["ind"].toInt();
0160         if ( !json.contains("ty") || !json["ty"].isDouble() )
0161         {
0162             warning(i18n("Missing layer type for %1", index), json);
0163             invalid_indices.insert(index);
0164             return;
0165         }
0166 
0167         int ty = json["ty"].toInt();
0168 
0169         std::unique_ptr<model::ShapeElement> inner_shape;
0170         bool start_mask = json["td"].toInt();
0171         start_mask = false;
0172 
0173         if ( ty == 0 )
0174         {
0175             inner_shape = load_precomp_layer(json);
0176 
0177             auto op = this->composition->animation->last_frame.get();
0178             if ( json.contains("parent") || referenced.count(index) || json["ip"].toDouble() != 0 ||
0179                 json["op"].toDouble(op) != op || start_mask
0180             )
0181             {
0182                 auto layer = make_node<model::Layer>(document);
0183                 layer->name.set(inner_shape->name.get());
0184                 layer->shapes.insert(std::move(inner_shape), 0);
0185                 layer_indices[index] = layer.get();
0186                 deferred.emplace_back(layer.get(), json);
0187                 inner_shape = std::move(layer);
0188             }
0189         }
0190         else
0191         {
0192             auto layer = std::make_unique<model::Layer>(document);
0193             layer_indices[index] = layer.get();
0194             deferred.emplace_back(layer.get(), json);
0195             inner_shape = std::move(layer);
0196         }
0197 
0198         if ( start_mask )
0199         {
0200             auto layer = std::make_unique<model::Layer>(document);
0201             mask = layer.get();
0202             layer->name.set(json["nm"].toString());
0203             layer->shapes.insert(std::move(inner_shape), 0);
0204             composition->shapes.insert(std::move(layer), 0);
0205         }
0206         else
0207         {
0208             auto tt = json["tt"].toInt();
0209 
0210             if ( mask && tt )
0211             {
0212                 mask->shapes.insert(std::move(inner_shape), 1);
0213                 auto mode = model::MaskSettings::MaskMode((tt + 1) / 2);
0214                 mask->mask->mask.set(mode);
0215                 mask->mask->inverted.set(tt > 0 && tt % 2 == 0);
0216             }
0217             else
0218             {
0219                 composition->shapes.insert(std::move(inner_shape), 0);
0220             }
0221             mask = nullptr;
0222         }
0223     }
0224 
0225     std::unique_ptr<model::PreCompLayer> load_precomp_layer(const QJsonObject& json)
0226     {
0227         auto props = load_basic_setup(json);
0228 
0229         auto precomp = make_node<model::PreCompLayer>(document);
0230         load_visibility(precomp.get(), json);
0231 
0232         load_stretchable_animation_container(json, precomp->timing.get());
0233 
0234         for ( const FieldInfo& field : fields["__Layer__"] )
0235             props.erase(field.lottie);
0236 
0237         for ( const QMetaObject* mo = precomp->metaObject(); mo; mo = mo->superClass() )
0238             load_properties(
0239                 precomp.get(),
0240                 fields[model::detail::naked_type_name(mo)],
0241                 json,
0242                 props
0243             );
0244 
0245         auto comp = precomp_ids[json["refId"].toString()];
0246         if ( comp )
0247         {
0248             precomp->composition.set(comp);
0249             if ( !json.contains("nm") )
0250                 precomp->name.set(comp->name.get());
0251         }
0252         props.erase("w");
0253         props.erase("h");
0254         precomp->size.set(QSize(
0255             json["w"].toInt(),
0256             json["h"].toInt()
0257         ));
0258 
0259         load_transform(json["ks"].toObject(), precomp->transform.get(), &precomp->opacity);
0260 
0261         return precomp;
0262     }
0263 
0264     void load_mask(const QJsonObject& json, model::Group* group)
0265     {
0266         auto fill = make_node<model::Fill>(document);
0267         fill->color.set(QColor(255, 255, 255));
0268         document->set_best_name(fill.get());
0269         load_animated(&fill->opacity, json["o"], {});
0270         group->shapes.insert(std::move(fill));
0271 
0272         auto j_stroke = json["x"].toObject();
0273         if ( animated(j_stroke) || j_stroke["k"].toDouble() != 0 )
0274         {
0275             auto stroke = make_node<model::Stroke>(document);
0276             stroke->color.set(QColor(255, 255, 255));
0277             load_animated(&stroke->opacity, json["o"], {});
0278             document->set_best_name(stroke.get());
0279             load_animated(&stroke->width, json["x"], {});
0280             group->shapes.insert(std::move(stroke));
0281         }
0282 
0283         auto path = make_node<model::Path>(document);
0284         document->set_best_name(path.get());
0285         load_animated(&path->shape, json["pt"], {});
0286         group->shapes.insert(std::move(path));
0287     }
0288 
0289     void load_layer(const QJsonObject& json, model::Layer* layer)
0290     {
0291         current_node = current_layer = layer;
0292 
0293         if ( json.contains("parent") )
0294         {
0295             int parent_index = json["parent"].toInt();
0296             if ( invalid_indices.count(parent_index) )
0297             {
0298                 warning(
0299                     i18n("Cannot use %1 as parent as it couldn't be loaded")
0300                     .arg(parent_index),
0301                     json
0302                 );
0303             }
0304             else
0305             {
0306                 auto it = layer_indices.find(parent_index);
0307                 if ( it == layer_indices.end() )
0308                 {
0309                     warning(
0310                         i18n("Invalid parent layer %1")
0311                         .arg(parent_index),
0312                         json
0313                     );
0314                 }
0315                 else
0316                 {
0317                     auto parent_layer = layer->docnode_parent()->cast<model::Layer>();
0318                     if ( parent_layer && parent_layer->mask->has_mask() )
0319                         parent_layer->parent.set(*it);
0320                     else
0321                         layer->parent.set(*it);
0322                 }
0323             }
0324         }
0325 
0326         if ( !json.contains("ip") && !json.contains("op") )
0327         {
0328             auto comp = layer->owner_composition();
0329             layer->animation->first_frame.set(comp->animation->first_frame.get());
0330             layer->animation->last_frame.set(comp->animation->last_frame.get());
0331         }
0332         else
0333         {
0334             load_animation_container(json, layer->animation.get());
0335         }
0336 
0337         if ( !layer->shapes.empty() )
0338             return;
0339 
0340         auto props = load_basic_setup(json);
0341         props.erase("ind");
0342 
0343         load_properties(layer, fields["DocumentNode"], json, props);
0344         load_properties(layer, fields["__Layer__"], json, props);
0345 
0346         load_transform(json["ks"].toObject(), layer->transform.get(), &layer->opacity);
0347         load_visibility(layer, json);
0348 
0349         model::Layer* target = layer;
0350         props.erase("hasMask");
0351         props.erase("masksProperties");
0352         if ( json.contains("masksProperties") )
0353         {
0354             auto masks = json["masksProperties"].toArray();
0355             if ( !masks.empty() )
0356             {
0357                 layer->mask->mask.set(model::MaskSettings::Alpha);
0358 
0359                 auto clip_p = make_node<model::Group>(document);
0360                 auto clip = clip_p.get();
0361                 layer->shapes.insert(std::move(clip_p));
0362                 auto shape_target = std::make_unique<model::Layer>(document);
0363                 target = shape_target.get();
0364                 shape_target->name.set(layer->name.get());
0365                 shape_target->animation->first_frame.set(layer->animation->first_frame.get());
0366                 shape_target->animation->last_frame.set(layer->animation->last_frame.get());
0367                 layer->shapes.insert(std::move(shape_target));
0368 
0369                 document->set_best_name(clip, i18n("Clip"));
0370                 if ( masks.size() == 1 )
0371                 {
0372                     load_mask(masks[0].toObject(), clip);
0373                 }
0374                 else
0375                 {
0376                     for ( const auto& mask : masks )
0377                     {
0378                         auto clip_group_p = make_node<model::Group>(document);
0379                         auto clip_group = clip_group_p.get();
0380                         clip->shapes.insert(std::move(clip_group_p));
0381                         document->set_best_name(clip_group, i18n("Clip"));
0382                         load_mask(mask.toObject(), clip_group);
0383                     }
0384                 }
0385             }
0386         }
0387 
0388         switch ( json["ty"].toInt(-1) )
0389         {
0390             case 0: // precomp
0391                 break;
0392             case 1: // solid color
0393             {
0394                 props.erase("sw");
0395                 props.erase("sh");
0396                 props.erase("sc");
0397 
0398                 auto color_name = json["sc"].toString();
0399                 auto fill = std::make_unique<model::Fill>(document);
0400                 fill->color.set(svg::parse_color(color_name));
0401                 target->shapes.insert(std::move(fill));
0402 
0403                 auto rect = std::make_unique<model::Rect>(document);
0404                 auto w = json["sw"].toDouble();
0405                 auto h = json["sh"].toDouble();
0406                 rect->size.set(QSizeF(w, h));
0407                 rect->position.set(QPointF(w/2, h/2));
0408                 target->shapes.insert(std::move(rect));
0409 
0410                 break;
0411             }
0412             case 2: // image layer
0413             {
0414                 auto image = make_node<model::Image>(document);
0415                 image->image.set(bitmap_ids[json["refId"].toString()]);
0416                 target->shapes.insert(std::move(image));
0417                 props.erase("refId");
0418                 break;
0419             }
0420             case 3: // empty
0421                 break;
0422             case 4: // shape
0423                 props.erase("shapes");
0424                 load_shapes(target->shapes, json["shapes"].toArray());
0425                 break;
0426             case 5: // text
0427                 props.erase("t");
0428                 load_text_layer(target->shapes, json["t"].toObject());
0429                 break;
0430             default:
0431             {
0432                 QString type = json["ty"].toVariant().toString();
0433                 auto it = unsupported_layers.find(json["ty"].toInt());
0434                 if ( it != unsupported_layers.end() )
0435                     type = *it;
0436                 warning(i18n("Unsupported layer of type %1", type), json);
0437             }
0438         }
0439 
0440         load_basic_check(props);
0441     }
0442 
0443     void load_shapes(model::ShapeListProperty& shapes, const QJsonArray& jshapes)
0444     {
0445         deferred.clear();
0446 
0447         for ( int i = jshapes.size() - 1; i >= 0; i-- )
0448             create_shape(jshapes[i].toObject(), shapes);
0449 
0450         auto deferred_shapes = std::move(deferred);
0451         deferred.clear();
0452 
0453         for ( const auto& pair: deferred_shapes )
0454             load_shape(pair.second, static_cast<model::ShapeElement*>(pair.first));
0455     }
0456 
0457     void create_shape(const QJsonObject& json, model::ShapeListProperty& shapes)
0458     {
0459         if ( !json.contains("ty") || !json["ty"].isString() )
0460         {
0461             warning(i18n("Missing shape type"), json);
0462             return;
0463         }
0464 
0465         QString base_type = json["ty"].toString();
0466         QString type = shape_types.key(base_type);
0467         if ( type.isEmpty() )
0468         {
0469             type = shape_types_repeat[base_type];
0470             if ( type.isEmpty() )
0471             {
0472                 // "mm" is marked as unsupported by lottie and it appears in several animations so we ignore the warning
0473                 if ( base_type != "mm" )
0474                     warning(i18n("Unsupported shape type %1", json["ty"].toString()), json);
0475                 return;
0476             }
0477         }
0478 
0479         model::ShapeElement* shape = static_cast<model::ShapeElement*>(
0480             model::Factory::instance().build(type, document)
0481         );
0482         if ( !shape )
0483         {
0484             warning(i18n("Unsupported shape type %1", json["ty"].toString()), json);
0485             return;
0486         }
0487 
0488         deferred.emplace_back(shape, json);
0489         shapes.insert(std::unique_ptr<model::ShapeElement>(shape), shapes.size());
0490     }
0491 
0492     std::set<QString> load_basic_setup(const QJsonObject& json_obj)
0493     {
0494         std::set<QString> props;
0495 
0496         for ( auto it = json_obj.begin(); it != json_obj.end(); ++it )
0497             props.insert(it.key());
0498 
0499         return props;
0500     }
0501 
0502     void load_basic_check(const std::set<QString>& props)
0503     {
0504         for ( const auto& not_found : props )
0505         {
0506             Q_EMIT format->information(
0507                 i18n("Unknown field %2%1")
0508                 .arg(not_found)
0509                 .arg(object_error_string(nullptr))
0510             );
0511         }
0512     }
0513 
0514     void load_basic(const QJsonObject& json_obj, model::Object* obj)
0515     {
0516         std::set<QString> props = load_basic_setup(json_obj);
0517 
0518         for ( const QMetaObject* mo = obj->metaObject(); mo; mo = mo->superClass() )
0519             load_properties(
0520                 obj,
0521                 fields[model::detail::naked_type_name(mo)],
0522                 json_obj,
0523                 props
0524             );
0525 
0526         load_basic_check(props);
0527     }
0528 
0529     void load_basic(const QJsonObject& json_obj, model::DocumentNode* obj)
0530     {
0531         load_basic(json_obj, static_cast<model::Object*>(obj));
0532         if ( obj->name.get().isEmpty() )
0533             document->set_best_name(obj);
0534     }
0535 
0536     void load_transform(const QJsonObject& transform, model::Transform* tf, model::AnimatableBase* opacity)
0537     {
0538         load_basic(transform, tf);
0539         if ( transform.contains("o") && opacity )
0540             load_animated(opacity, transform["o"], FloatMult(100));
0541 
0542         if ( transform.contains("p") )
0543         {
0544             auto pos = transform["p"].toObject();
0545             if ( pos.contains("x") && pos.contains("y") )
0546             {
0547                 model::Document dummydoc("");
0548                 model::Object dummy(&dummydoc);
0549                 model::AnimatedProperty<float> px(&dummy, {}, 0);
0550                 model::AnimatedProperty<float> py(&dummy, {}, 0);
0551                 load_animated(&px, pos["x"], {});
0552                 load_animated(&py, pos["y"], {});
0553 
0554                 model::JoinAnimatables join({&px, &py});
0555                 join.apply_to(&tf->position, [](float x, float y) -> QPointF {
0556                     return QPointF(x, y);
0557                 }, &px, &py);
0558             }
0559             else
0560             {
0561                 load_animated(&tf->position, transform["p"], {});
0562             }
0563         }
0564     }
0565 
0566     void load_styler(model::Styler* styler, const QJsonObject& json_obj)
0567     {
0568         load_visibility(styler, json_obj);
0569 
0570         std::set<QString> props = load_basic_setup(json_obj);
0571         for ( const QMetaObject* mo = styler->metaObject(); mo; mo = mo->superClass() )
0572             load_properties(
0573                 styler,
0574                 fields[model::detail::naked_type_name(mo)],
0575                 json_obj,
0576                 props
0577             );
0578 
0579         if ( json_obj.contains("fillEnabled") )
0580             styler->visible.set(json_obj["fillEnabled"].toBool());
0581 
0582         if ( json_obj["ty"].toString().startsWith('g') )
0583         {
0584             auto gradient = document->assets()->gradients->values.insert(std::make_unique<model::Gradient>(document));
0585             styler->use.set(gradient);
0586             auto colors = document->assets()->gradient_colors->values.insert(std::make_unique<model::GradientColors>(document));
0587             gradient->colors.set(colors);
0588             load_properties(gradient, fields["Gradient"], json_obj, props);
0589 
0590             if ( json_obj.contains("h") || json_obj.contains("a") )
0591             {
0592                 model::Document dummydoc("");
0593                 model::Object dummy(&dummydoc);
0594                 model::AnimatedProperty<float> length(&dummy, {}, 0);
0595                 model::AnimatedProperty<float> angle(&dummy, {}, 0);
0596                 if ( json_obj.contains("h") )
0597                     load_animated(&length, json_obj["h"], {});
0598                 if ( json_obj.contains("a") )
0599                     load_animated(&angle, json_obj["a"], {});
0600 
0601                 glaxnimate::model::JoinAnimatables join({&gradient->start_point, &gradient->end_point, &length, &angle});
0602                 join.apply_to(&gradient->highlight, [](const QPointF& p, const QPointF& e, float length, float angle) -> QPointF {
0603                     angle = math::deg2rad(angle + 90);
0604                     length = math::length(e - p) * length / 100;
0605                     return p + math::from_polar<QPointF>(length, angle);
0606                 }, &gradient->start_point, &gradient->end_point, &length, &angle);
0607             }
0608             else
0609             {
0610                 gradient->highlight.set(gradient->start_point.get());
0611             }
0612 
0613             auto jcolors = json_obj["g"].toObject();
0614             load_animated(&colors->colors, jcolors["k"], GradientLoad{jcolors["p"].toInt()});
0615         }
0616         else
0617         {
0618             load_animated(&styler->color, json_obj["c"], {});
0619         }
0620 
0621         if ( styler->name.get().isEmpty() )
0622             document->set_best_name(styler);
0623 
0624         load_basic_check(props);
0625     }
0626 
0627     void load_shape(const QJsonObject& json, model::ShapeElement* shape)
0628     {
0629         current_node = shape;
0630 
0631         if ( auto styler = shape->cast<model::Styler>() )
0632             return load_styler(styler, json);
0633 
0634         load_basic(json, shape);
0635         load_visibility(shape, json);
0636 
0637         QString type_name = shape->type_name();
0638         if ( type_name == "Group" )
0639         {
0640             auto gr = static_cast<model::Group*>(shape);
0641             QJsonArray shapes = json["it"].toArray();
0642             QJsonObject transform;
0643 
0644             for ( int i = shapes.size() - 1; i >= 0; i-- )
0645             {
0646                 QJsonObject shi = shapes[i].toObject();
0647                 if ( shi["ty"] == "tr" )
0648                 {
0649                     transform = shi;
0650                     transform.remove("ty");
0651                     shapes.erase(shapes.begin() + i);
0652                     break;
0653                 }
0654             }
0655             if ( !transform.empty() )
0656                 load_transform(transform, gr->transform.get(), &gr->opacity);
0657 
0658             load_shapes(gr->shapes, shapes);
0659         }
0660         else if ( type_name == "Repeater" )
0661         {
0662             auto repeater = static_cast<model::Repeater*>(shape);
0663             QJsonObject transform = json["tr"].toObject();
0664             load_animated(&repeater->start_opacity, transform["so"], FloatMult(100));
0665             load_animated(&repeater->end_opacity, transform["eo"], FloatMult(100));
0666             transform.remove("so");
0667             transform.remove("eo");
0668             transform.remove("ty");
0669             load_transform(transform, repeater->transform.get(), nullptr);
0670         }
0671         else if ( version[0] < 5 && type_name == "Path" && json.contains("closed") )
0672         {
0673             auto path = static_cast<model::Path*>(shape);
0674             path->shape.set_closed(json["closed"].toBool());
0675         }
0676     }
0677 
0678     void load_properties(
0679         model::Object* obj,
0680         const QVector<FieldInfo>& fields,
0681         const QJsonObject& json_obj,
0682         std::set<QString>& avail_obj_keys
0683     )
0684     {
0685         for ( const FieldInfo& field : fields )
0686         {
0687             avail_obj_keys.erase(field.lottie);
0688             if ( field.mode >= Ignored || !json_obj.contains(field.lottie) )
0689                 continue;
0690 
0691             model::BaseProperty * prop = obj->get_property(field.name);
0692             if ( !prop )
0693             {
0694                 logger.stream() << field.name << "is not a property";
0695                 continue;
0696             }
0697 
0698             if ( prop->traits().flags & model::PropertyTraits::Animated )
0699             {
0700                 load_animated(static_cast<model::AnimatableBase*>(prop), json_obj[field.lottie], field.transform);
0701             }
0702             else if ( field.mode == AnimatedToStatic )
0703             {
0704                 load_static(prop, json_obj[field.lottie], field.transform);
0705             }
0706             else
0707             {
0708                 load_value(prop, json_obj[field.lottie], field.transform);
0709             }
0710         }
0711     }
0712 
0713     template<class T>
0714     bool compound_value_2d_raw(const QJsonValue& val, T& out, double mul = 1)
0715     {
0716         QJsonArray arr = val.toArray();
0717         if ( arr.size() < 2 || !arr[0].isDouble() || !arr[1].isDouble() )
0718             return false;
0719 
0720         out = T(arr[0].toDouble() * mul, arr[1].toDouble() * mul);
0721         return true;
0722     }
0723 
0724     template<class T>
0725     std::optional<QVariant> compound_value_2d(const QJsonValue& val, double mul = 1)
0726     {
0727         T v;
0728         if ( !compound_value_2d_raw(val, v, mul) )
0729             return {};
0730         return QVariant::fromValue(v);
0731     }
0732 
0733     bool is_scalar(model::BaseProperty * prop)
0734     {
0735         switch ( prop->traits().type )
0736         {
0737             case model::PropertyTraits::Bool:
0738             case model::PropertyTraits::Int:
0739             case model::PropertyTraits::Float:
0740             case model::PropertyTraits::String:
0741             case model::PropertyTraits::Uuid:
0742             case model::PropertyTraits::Enum:
0743             case model::PropertyTraits::Bezier:
0744                 return true;
0745             default:
0746                 return false;
0747         }
0748     }
0749 
0750     bool compound_value_color(const QJsonValue& val, QColor& out)
0751     {
0752         QJsonArray arr = val.toArray();
0753 
0754         if ( version[0] < 5 )
0755         {
0756             auto iter = std::find_if(arr.begin(), arr.end(), [](const QJsonValue& v){ return v.toDouble() > 1; });
0757             if ( iter != arr.end() )
0758             {
0759                 if ( arr.size() == 3 )
0760                     out = QColor::fromRgb(
0761                         arr[0].toInt(), arr[1].toInt(), arr[2].toInt()
0762                     );
0763                 else if ( arr.size() == 4 )
0764                     out = QColor::fromRgb(
0765                         arr[0].toInt(), arr[1].toInt(), arr[2].toInt(), qMin(255, arr[3].toInt())
0766                     );
0767                 else
0768                     return false;
0769 
0770                 return true;
0771             }
0772         }
0773 
0774         if ( arr.size() == 3 )
0775             out = QColor::fromRgbF(
0776                 arr[0].toDouble(), arr[1].toDouble(), arr[2].toDouble()
0777             );
0778         else if ( arr.size() == 4 )
0779             out = QColor::fromRgbF(
0780                 arr[0].toDouble(), arr[1].toDouble(), arr[2].toDouble(), qMin(1., arr[3].toDouble())
0781             );
0782         else
0783             return false;
0784 
0785         return true;
0786     }
0787 
0788     std::optional<QVariant> value_to_variant(model::BaseProperty * prop, const QJsonValue& val)
0789     {
0790         switch ( prop->traits().type )
0791         {
0792             case model::PropertyTraits::Bool:
0793             case model::PropertyTraits::Int:
0794             case model::PropertyTraits::Float:
0795             case model::PropertyTraits::String:
0796                 return val.toVariant();
0797             case model::PropertyTraits::Uuid:
0798             {
0799                 QUuid uuid = val.toVariant().toUuid();
0800                 if ( uuid.isNull() )
0801                     uuid = QUuid::createUuid();
0802                 return QVariant::fromValue(uuid);
0803             }
0804             case model::PropertyTraits::Point:
0805                 return compound_value_2d<QPointF>(val);
0806             case model::PropertyTraits::Size:
0807                 return compound_value_2d<QSizeF>(val);
0808             case model::PropertyTraits::Scale:
0809                 return compound_value_2d<QVector2D>(val, 0.01);
0810             case model::PropertyTraits::Color:
0811             {
0812                 QColor col;
0813                 if ( compound_value_color(val, col) )
0814                     return QVariant::fromValue(col);
0815                 return {};
0816             }
0817             case model::PropertyTraits::Bezier:
0818             {
0819                 QJsonObject jsbez = val.toObject();
0820                 math::bezier::Bezier bezier;
0821                 bezier.set_closed(jsbez["c"].toBool());
0822                 QJsonArray pos = jsbez["v"].toArray();
0823                 QJsonArray tan_in = jsbez["i"].toArray();
0824                 QJsonArray tan_out = jsbez["o"].toArray();
0825                 int sz = std::min(pos.size(), std::min(tan_in.size(), tan_out.size()));
0826                 for ( int i = 0; i < sz; i++ )
0827                 {
0828                     QPointF p, ti, to;
0829                     if ( !compound_value_2d_raw(pos[i], p) )
0830                     {
0831                         Q_EMIT format->warning(
0832                             i18n("Invalid bezier point %1 in %2")
0833                             .arg(i)
0834                             .arg(property_error_string(prop))
0835                         );
0836                         continue;
0837                     }
0838                     compound_value_2d_raw(tan_in[i], ti);
0839                     compound_value_2d_raw(tan_out[i], to);
0840                     bezier.push_back(math::bezier::Point::from_relative(p, ti, to));
0841                 }
0842                 return QVariant::fromValue(bezier);
0843             }
0844             case model::PropertyTraits::Enum:
0845                 return val.toInt();
0846             case model::PropertyTraits::Gradient:
0847                 return val.toArray().toVariantList();
0848             default:
0849                 logger.stream(app::log::Error) << "Unsupported type" << prop->traits().type << "for" << property_error_string(prop);
0850                 return {};
0851         }
0852     }
0853 
0854     QString object_error_string(model::Object* ignore)
0855     {
0856         QString str;
0857         if ( current_layer && current_node != current_layer )
0858             str = "(" + current_layer->object_name() + ") ";
0859 
0860         if ( current_node && current_node != ignore )
0861             str += current_node->object_name() + ".";
0862 
0863         return str;
0864 
0865     }
0866 
0867     QString property_error_string(model::BaseProperty * prop)
0868     {
0869         QString str = object_error_string(prop->object());
0870         str += prop->object()->object_name() + "." + prop->name();
0871 
0872         return str;
0873     }
0874 
0875     void load_value(model::BaseProperty * prop, const QJsonValue& val, const TransformFunc& trans)
0876     {
0877         auto v = value_to_variant(prop, val);
0878         if ( !v || !prop->set_value(trans.from_lottie(*v, 0)) )
0879             Q_EMIT format->warning(i18n("Invalid value for %1", prop->name()));
0880     }
0881 
0882     void load_static(model::BaseProperty * prop, const QJsonValue& val, const TransformFunc& trans)
0883     {
0884         if ( val.isObject() )
0885         {
0886             QJsonObject obj = val.toObject();
0887             if ( obj.contains("k") )
0888             {
0889                 load_value(prop, obj["k"], trans);
0890                 return;
0891             }
0892         }
0893 
0894         load_value(prop, val, trans);
0895     }
0896 
0897     void load_animated(model::AnimatableBase* prop, const QJsonValue& val, const TransformFunc& trans)
0898     {
0899         if ( !val.isObject() )
0900         {
0901             Q_EMIT format->warning(i18n("Invalid value for %1", property_error_string(prop)));
0902             return;
0903         }
0904 
0905         QJsonObject obj = val.toObject();
0906         if ( !obj.contains("k") )
0907         {
0908             Q_EMIT format->warning(i18n("Invalid value for %1", property_error_string(prop)));
0909             return;
0910         }
0911 
0912         if ( animated(obj) )
0913         {
0914             if ( !obj["k"].isArray() )
0915             {
0916                 Q_EMIT format->warning(i18n("Invalid keyframes for %1", property_error_string(prop)));
0917                 return;
0918             }
0919 
0920             bool position = prop->traits().type == model::PropertyTraits::Point;
0921 
0922             auto karr = obj["k"].toArray();
0923             for ( int i = 0; i < karr.size(); i++ )
0924             {
0925                 QJsonValue jkf = karr[i];
0926                 model::FrameTime time = jkf["t"].toDouble();
0927                 QJsonValue s = jkf["s"];
0928                 if ( s.isUndefined() && i == karr.size() - 1 && i > 0 )
0929                     s = karr[i-1].toObject()["e"];
0930                 if ( s.isArray() && is_scalar(prop) )
0931                     s = s.toArray()[0];
0932 
0933                 auto v = value_to_variant(prop, s);
0934                 model::KeyframeBase* kf = nullptr;
0935                 if ( v )
0936                     kf = prop->set_keyframe(time, trans.from_lottie(*v, time));
0937 
0938                 if ( kf )
0939                 {
0940                     kf->set_transition({
0941                         keyframe_bezier_handle(jkf["o"]),
0942                         keyframe_bezier_handle(jkf["i"]),
0943                         bool(jkf["h"].toInt())
0944                     });
0945 
0946                     if ( position )
0947                     {
0948                         auto pkf = static_cast<model::Keyframe<QPointF>*>(kf);
0949                         QPointF tan_out;
0950                         compound_value_2d_raw(jkf["to"], tan_out);
0951                         tan_out += pkf->get();
0952 
0953                         QPointF tan_in;
0954                         if ( i > 0 )
0955                             compound_value_2d_raw(karr[i-1].toObject()["ti"], tan_in);
0956                         tan_in += pkf->get();
0957 
0958                         pkf->set_point({pkf->get(), tan_in, tan_out});
0959                     }
0960                 }
0961                 else
0962                 {
0963                     QString value;
0964                     if ( !v )
0965                     {
0966                         value = i18n("(null)");
0967                     }
0968                     else
0969                     {
0970                         value = v->toString();
0971                         if ( value == "" )
0972                             value = i18n("(empty)");
0973                         value += " ";
0974 #if QT_VERSION_MAJOR >= 6
0975                         value += QMetaType(v->userType()).name();
0976 #else
0977                         value += QMetaType::typeName(v->userType());
0978 #endif
0979                     }
0980                     Q_EMIT format->warning(i18n("Cannot load keyframe at %1 for %2 with value %3")
0981                         .arg(time).arg(property_error_string(prop)).arg(value)
0982                     );
0983                 }
0984             }
0985         }
0986         else
0987         {
0988             load_value(prop, obj["k"], trans);
0989         }
0990     }
0991 
0992     qreal keyframe_bezier_handle_comp(const QJsonValue& comp)
0993     {
0994         if ( comp.isArray() )
0995             return comp[0].toDouble();
0996         return comp.toDouble();
0997     }
0998 
0999     QPointF keyframe_bezier_handle(const QJsonValue& val)
1000     {
1001         return {keyframe_bezier_handle_comp(val["x"]), keyframe_bezier_handle_comp(val["y"])};
1002     }
1003 
1004     std::vector<std::pair<QJsonObject, model::Composition*>> load_assets(const QJsonArray& assets)
1005     {
1006         std::vector<std::pair<QJsonObject, model::Composition*>> comps;
1007 
1008         for ( const auto& assetv : assets )
1009         {
1010             QJsonObject asset = assetv.toObject();
1011             if ( asset.contains("e") && asset.contains("p") && asset.contains("w") )
1012                 load_asset_bitmap(asset);
1013             else if ( asset.contains("layers") )
1014                 comps.emplace_back(asset, load_asset_precomp(asset));
1015         }
1016 
1017         return comps;
1018     }
1019 
1020     void load_comps(const std::vector<std::pair<QJsonObject, model::Composition*>>& comps)
1021     {
1022         for ( const auto& p : comps )
1023             load_composition(p.first, p.second);
1024     }
1025 
1026     void load_asset_bitmap(const QJsonObject& asset)
1027     {
1028         auto bmp = document->assets()->images->values.insert(std::make_unique<model::Bitmap>(document));
1029 
1030         QString id = asset["id"].toString();
1031         if ( bitmap_ids.count(id) )
1032             format->warning(i18n("Duplicate Bitmap ID: %1", id));
1033         bitmap_ids[id] = bmp;
1034 
1035         if ( asset.contains("nm") )
1036             bmp->name.set(asset["nm"].toString());
1037 
1038         if ( asset["e"].toInt() )
1039         {
1040             bmp->from_url(QUrl(asset["p"].toString()));
1041         }
1042         else
1043         {
1044             QString path = asset["u"].toString();
1045             if ( path.contains("://") )
1046             {
1047                 path += asset["p"].toString();
1048                 bmp->from_url(QUrl(path));
1049             }
1050             else
1051             {
1052                 QDir dir(path);
1053                 bmp->from_file(dir.filePath(asset["p"].toString()));
1054             }
1055         }
1056     }
1057 
1058     model::Composition* load_asset_precomp(QJsonObject asset)
1059     {
1060         auto comp = document->assets()->compositions->values.insert(std::make_unique<model::Composition>(document));
1061 
1062         QString id = asset["id"].toString();
1063         if ( precomp_ids.count(id) )
1064             format->warning(i18n("Duplicate Composition ID: %1", id));
1065         precomp_ids[id] = comp;
1066 
1067         comp->name.set(id);
1068         return comp;
1069     }
1070 
1071     enum class FontOrigin
1072     {
1073         System = 0,
1074         CssUrl = 1,
1075         ScriptUrl = 2,
1076         FontUrl = 3,
1077     };
1078 
1079     void load_fonts(const QJsonArray& fonts_arr)
1080     {
1081         for ( const auto& fontv : fonts_arr )
1082         {
1083             QJsonObject font = fontv.toObject();
1084             FontInfo info;
1085             info.family = font["fFamily"].toString();
1086             info.name = font["fName"].toString();
1087             info.style = font["fStyle"].toString();
1088             fonts[info.name] = info;
1089 
1090             FontOrigin font_origin = FontOrigin::System;
1091             if ( font.contains("origin") )
1092             {
1093                 font_origin = FontOrigin(font["origin"].toInt());
1094             }
1095             else if ( font.contains("fOrigin") )
1096             {
1097                 QString fOrigin = font["fOrigin"].toString();
1098                 fOrigin.append(" ");
1099                 switch ( fOrigin[0].toLatin1() )
1100                 {
1101                     case 'n': font_origin = FontOrigin::System; break;
1102                     case 'g': font_origin = FontOrigin::CssUrl; break;
1103                     case 't': font_origin = FontOrigin::ScriptUrl; break;
1104                     case 'p': font_origin = FontOrigin::FontUrl; break;
1105                 }
1106             }
1107 
1108             switch ( font_origin )
1109             {
1110                 case FontOrigin::System:
1111                     // nothing to do
1112                     break;
1113                 case FontOrigin::CssUrl:
1114                 case FontOrigin::FontUrl:
1115                     // Queue dynamic font loading
1116                     document->add_pending_asset(info.family, QUrl(font["fPath"].toString()));
1117                     break;
1118                 case FontOrigin::ScriptUrl:
1119                     // idk how these work
1120                     break;
1121             }
1122         }
1123     }
1124 
1125     FontInfo get_font(const QString& name)
1126     {
1127         auto it = fonts.find(name);
1128         if ( it != fonts.end() )
1129             return *it;
1130         return {"", name, "Regular"};
1131     }
1132 
1133     void load_text_layer(model::ShapeListProperty& shapes, const QJsonObject& text)
1134     {
1135         // TODO "a" "m" "p"
1136 
1137         model::Group* prev = nullptr;
1138         model::KeyframeTransition jump({}, {}, true);
1139 
1140         for ( const auto& v : text["d"].toObject()["k"].toArray() )
1141         {
1142             auto keyframe = v.toObject();
1143             qreal time = keyframe["t"].toDouble();
1144             auto text_document = keyframe["s"].toObject();
1145 
1146             auto group = std::make_unique<model::Group>(document);
1147             if ( time > 0 )
1148                 group->opacity.set_keyframe(0, 0)->set_transition(jump);
1149             group->opacity.set_keyframe(time, 1)->set_transition(jump);
1150             if ( prev )
1151                 prev->opacity.set_keyframe(time, 0)->set_transition(jump);
1152             prev = group.get();
1153 
1154             auto fill = std::make_unique<model::Fill>(document);
1155             QColor color;
1156             compound_value_color(text_document["fc"], color);
1157             fill->color.set(color);
1158             group->shapes.insert(std::move(fill));
1159 
1160             auto shape = make_node<model::TextShape>(document);
1161             auto font = get_font(text_document["f"].toString());
1162             shape->font->family.set(font.family);
1163             shape->font->style.set(font.style);
1164             shape->font->size.set(text_document["s"].toDouble());
1165             shape->text.set(text_document["t"].toString().replace('\r', '\n'));
1166             group->shapes.insert(std::move(shape));
1167 
1168             shapes.insert(std::move(group), shapes.size());
1169         }
1170     }
1171 
1172     void load_meta(const QJsonValue& meta)
1173     {
1174         if ( !meta.isObject() )
1175             return;
1176 
1177         document->info().author = meta["a"].toString();
1178         document->info().description = meta["d"].toString();
1179         for ( const auto& kw : meta["k"].toArray() )
1180             document->info().keywords.push_back(kw.toString());
1181     }
1182 
1183     model::Document* document;
1184     io::lottie::LottieFormat* format;
1185     QMap<int, model::Layer*> layer_indices;
1186     std::set<int> invalid_indices;
1187     std::vector<std::pair<model::Object*, QJsonObject>> deferred;
1188     model::Composition* composition = nullptr;
1189     app::log::Log logger{"Lottie Import"};
1190     QMap<QString, model::Bitmap*> bitmap_ids;
1191     QMap<QString, model::Composition*> precomp_ids;
1192     QMap<QString, FontInfo> fonts;
1193     model::Layer* mask = nullptr;
1194     model::DocumentNode* current_node = nullptr;
1195     model::Layer* current_layer = nullptr;
1196     std::array<int, 3> version = {5,5,1};
1197     model::Composition* main = nullptr;
1198 };
1199 
1200 
1201 } // namespace glaxnimate::io::lottie::detail