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