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

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 
0010 #include <QCborValue>
0011 #include <QCborArray>
0012 
0013 #include "cbor_write_json.hpp"
0014 #include "lottie_private_common.hpp"
0015 #include "model/animation/join_animatables.hpp"
0016 #include "app_info.hpp"
0017 #include "io/utils.hpp"
0018 
0019 namespace glaxnimate::io::lottie::detail {
0020 
0021 inline QLatin1String operator "" _l(const char* c, std::size_t sz)
0022 {
0023     return QLatin1String(c, sz);
0024 }
0025 
0026 // inline QString debug_value(QCborValue v)
0027 // {
0028 //     QCborMap map;
0029 //     map["v"_l] = v;
0030 //     return QString(cbor_write_json(map, false));
0031 // }
0032 
0033 class LottieExporterState
0034 {
0035     static constexpr const char* version = "5.7.1";
0036 
0037 public:
0038     explicit LottieExporterState(ImportExport* format, model::Composition* comp, bool strip, bool strip_raster, const QVariantMap& settings )
0039         : format(format),
0040         main(comp),
0041         document(comp->document()),
0042         strip(strip),
0043         strip_raster( strip_raster ),
0044         auto_embed(settings["auto_embed"].toBool()),
0045         old_kf(settings["old_kf"].toBool())
0046     {}
0047 
0048     QCborMap to_json()
0049     {
0050         return convert_main(main);
0051     }
0052 
0053     void convert_animation_container(model::AnimationContainer* animation, QCborMap& json)
0054     {
0055         json["ip"_l] = animation->first_frame.get();
0056         json["op"_l] = animation->last_frame.get();
0057     }
0058 
0059     void convert_composition(model::Composition* composition, QCborMap& json)
0060     {
0061         QCborArray layers;
0062         for ( const auto& layer : composition->shapes )
0063             if ( !strip || layer->visible.get() )
0064                 convert_layer(layer_type(layer.get()), layer.get(), layers);
0065 
0066         json["layers"_l] = layers;
0067     }
0068 
0069     QCborMap convert_main(model::Composition* animation)
0070     {
0071         layer_indices.clear();
0072         QCborMap json;
0073         json["v"_l] = version;
0074         convert_animation_container(animation->animation.get(), json);
0075         convert_object_basic(animation, json);
0076         json["assets"_l] = convert_assets(animation);
0077         convert_composition(animation, json);
0078         if ( !strip )
0079             convert_meta(json);
0080         return json;
0081     }
0082 
0083     void convert_meta(QCborMap& json)
0084     {
0085         QCborMap meta;
0086         meta["g"_l] = QString("%1 %2").arg(AppInfo::instance().name(), AppInfo::instance().version());
0087 
0088         if ( !document->info().description.isEmpty() )
0089             meta["d"_l] = document->info().description;
0090 
0091         if ( !document->info().author.isEmpty() )
0092             meta["a"_l] = document->info().author;
0093 
0094         if ( !document->info().keywords.isEmpty() )
0095         {
0096             QCborArray k;
0097             for ( const auto& kw : document->info().keywords )
0098                 k.push_back(kw);
0099             meta["k"_l] = k;
0100         }
0101 
0102         json["meta"_l] = meta;
0103     }
0104 
0105     int layer_index(model::DocumentNode* layer)
0106     {
0107         if ( !layer )
0108             return -1;
0109         if ( !layer_indices.contains(layer->uuid.get()) )
0110             layer_indices[layer->uuid.get()] = layer_indices.size();
0111         return layer_indices[layer->uuid.get()];
0112     }
0113 
0114     QCborMap wrap_layer_shape(model::ShapeElement* shape, model::Layer* forced_parent)
0115     {
0116         QCborMap json;
0117         json["ddd"_l] = 0;
0118         json["ty"_l] = 4;
0119         convert_fake_layer_parent(forced_parent, json);
0120         json["ind"_l] = layer_index(shape);
0121         json["st"_l] = 0;
0122         if ( !shape->visible.get() )
0123             json["hd"_l] = true;
0124 
0125         if ( auto grp = shape->cast<model::Group>() )
0126         {
0127             QCborMap transform;
0128             convert_transform(grp->transform.get(), &grp->opacity, transform);
0129             json["ks"_l] = transform;
0130             json["ao"_l] = int(grp->auto_orient.get());
0131 
0132             json["shapes"_l] = convert_shapes(grp->shapes, false);
0133         }
0134         else
0135         {
0136             QCborMap transform;
0137             model::Transform tf(document);
0138             convert_transform(&tf, nullptr, transform);
0139             json["ks"_l] = transform;
0140 
0141             QCborArray shapes;
0142             shapes.push_back(convert_shape(shape, false));
0143             json["shapes"_l] = shapes;
0144         }
0145 
0146         return json;
0147     }
0148 
0149     enum class LayerType { Shape, Layer, Image, PreComp };
0150 
0151     LayerType layer_type(model::ShapeElement* shape)
0152     {
0153         auto meta = shape->metaObject();
0154         if ( meta->inherits(&model::Layer::staticMetaObject) )
0155             return LayerType::Layer;
0156         if ( meta->inherits(&model::Image::staticMetaObject) )
0157             return LayerType::Image;
0158         if ( meta->inherits(&model::PreCompLayer::staticMetaObject) )
0159             return LayerType::PreComp;
0160         return LayerType::Shape;
0161     }
0162 
0163     QCborMap convert_single_layer(LayerType type, model::ShapeElement* shape, QCborArray& output, model::Layer* forced_parent, bool force_all_shapes)
0164     {
0165         switch ( type )
0166         {
0167             case LayerType::Shape:
0168                 return wrap_layer_shape(shape, forced_parent);
0169             case LayerType::Image:
0170                 return convert_image_layer(static_cast<model::Image*>(shape), forced_parent);
0171             case LayerType::PreComp:
0172                 return convert_precomp_layer(static_cast<model::PreCompLayer*>(shape), forced_parent);
0173             case LayerType::Layer:
0174                 break;
0175         }
0176 
0177         auto layer = static_cast<model::Layer*>(shape);
0178 
0179         int parent_index = layer_index(forced_parent ? forced_parent : layer->parent.get());
0180 
0181         QCborMap json;
0182         json["ddd"_l] = 0;
0183         json["ty"_l] = 3;
0184         int index = layer_index(layer);
0185         json["ind"_l] = index;
0186         json["st"_l] = 0;
0187         if ( !shape->visible.get() )
0188             json["hd"_l] = true;
0189 
0190         convert_animation_container(layer->animation.get(), json);
0191         convert_object_properties(layer, fields["DocumentNode"], json);
0192         convert_object_properties(layer, fields["__Layer__"], json);
0193 
0194         QCborMap transform;
0195         convert_transform(layer->transform.get(), &layer->opacity, transform);
0196         json["ks"_l] = transform;
0197         if ( parent_index != -1 )
0198             json["parent"_l] = parent_index;
0199 
0200         if ( !layer->shapes.empty() )
0201         {
0202             std::vector<LayerType> children_types;
0203             children_types.reserve(layer->shapes.size());
0204 
0205             bool all_shapes = true;
0206             if ( !force_all_shapes )
0207             {
0208                 for ( const auto& shape : layer->shapes )
0209                 {
0210                     children_types.push_back(layer_type(shape.get()));
0211                     if ( children_types.back() != LayerType::Shape )
0212                         all_shapes = false;
0213                 }
0214             }
0215 
0216             if ( all_shapes && !layer->mask->has_mask() )
0217             {
0218                 json["ty"_l] = 4;
0219                 json["shapes"_l] = convert_shapes(layer->shapes, false);
0220             }
0221             else
0222             {
0223                 int i = 0;
0224                 QCborMap mask;
0225                 if ( layer->mask->has_mask() && !layer->shapes.empty() )
0226                 {
0227                     if ( layer->shapes[0]->visible.get() )
0228                     {
0229                         mask = convert_single_layer(children_types[0], layer->shapes[0], output, layer, true);
0230                         if ( !mask.isEmpty() )
0231                             mask["td"_l] = 1;
0232                     }
0233                     i = 1;
0234                 }
0235 
0236                 for ( ; i < layer->shapes.size(); i++ )
0237                 {
0238                     if ( !strip || layer->shapes[i]->visible.get() )
0239                         convert_layer(children_types[i], layer->shapes[i], output, layer, mask);
0240                 }
0241             }
0242         }
0243 
0244         return json;
0245     }
0246 
0247     QCborMap convert_layer(LayerType type, model::ShapeElement* shape, QCborArray& output,
0248                            model::Layer* forced_parent = nullptr, const QCborMap& mask = {})
0249     {
0250         if ( !shape->visible.get() )
0251             return {};
0252 
0253         model::Layer* layer = nullptr;
0254         if ( type == LayerType::Layer )
0255         {
0256             layer = static_cast<model::Layer*>(shape);
0257 
0258             if ( !layer->render.get() )
0259                 return {};
0260         }
0261 
0262         auto json = convert_single_layer(type, shape, output, forced_parent, false);
0263 
0264         if ( !mask.isEmpty() )
0265         {
0266             json["tt"_l] = 1;
0267             output.push_front(json);
0268             output.push_front(mask);
0269         }
0270         else
0271         {
0272             output.push_front(json);
0273         }
0274 
0275         return json;
0276     }
0277 
0278     void convert_transform(model::Transform* tf, model::AnimatableBase* opacity, QCborMap& json)
0279     {
0280         convert_object_basic(tf, json);
0281         if ( opacity )
0282             json["o"_l] = convert_animated(opacity, FloatMult(100));
0283         else
0284             json["o"_l] = fake_animated(100);
0285     }
0286 
0287     QCborArray point_to_lottie(const QPointF& vv)
0288     {
0289         return QCborArray{vv.x(), vv.y()};
0290     }
0291 
0292     QCborValue value_from_variant(const QVariant& v)
0293     {
0294         switch ( v.userType() )
0295         {
0296             case QMetaType::QPointF:
0297                 return point_to_lottie(v.toPointF());
0298             case QMetaType::QVector2D:
0299             {
0300                 auto vv = v.value<QVector2D>() * 100;
0301                 return QCborArray{vv.x(), vv.y()};
0302             }
0303             case QMetaType::QSizeF:
0304             {
0305                 auto vv = v.toSizeF();
0306                 return QCborArray{vv.width(), vv.height()};
0307             }
0308             case QMetaType::QColor:
0309             {
0310                 auto vv = v.value<QColor>().toRgb();
0311                 return QCborArray{vv.redF(), vv.greenF(), vv.blueF()};
0312             }
0313             case QMetaType::QUuid:
0314                 return v.toString();
0315         }
0316 
0317         if ( v.userType() == qMetaTypeId<math::bezier::Bezier>() )
0318         {
0319             math::bezier::Bezier bezier = v.value<math::bezier::Bezier>();
0320             QCborMap jsbez;
0321             jsbez["c"_l] = bezier.closed();
0322             QCborArray pos, tan_in, tan_out;
0323             for ( const auto& p : bezier )
0324             {
0325                 pos.push_back(point_to_lottie(p.pos));
0326                 tan_in.push_back(point_to_lottie(p.tan_in - p.pos));
0327                 tan_out.push_back(point_to_lottie(p.tan_out - p.pos));
0328             }
0329             jsbez["v"_l] = pos;
0330             jsbez["i"_l] = tan_in;
0331             jsbez["o"_l] = tan_out;
0332             return jsbez;
0333         }
0334         else if ( v.userType() == qMetaTypeId<math::bezier::Point>() )
0335         {
0336             return point_to_lottie(v.value<math::bezier::Point>().pos);
0337         }
0338         else if ( v.userType() == qMetaTypeId<QGradientStops>() )
0339         {
0340             QCborArray weird_ass_representation;
0341             auto gradient = v.value<QGradientStops>();
0342             bool alpha = false;
0343             for ( const auto& stop : gradient )
0344             {
0345                 weird_ass_representation.push_back(stop.first);
0346                 weird_ass_representation.push_back(stop.second.redF());
0347                 weird_ass_representation.push_back(stop.second.greenF());
0348                 weird_ass_representation.push_back(stop.second.blueF());
0349                 alpha = alpha || stop.second.alpha() != 0;
0350             }
0351             if ( alpha )
0352             {
0353                 for ( const auto& stop : gradient )
0354                 {
0355                     weird_ass_representation.push_back(stop.first);
0356                     weird_ass_representation.push_back(stop.second.alphaF());
0357                 }
0358             }
0359             return weird_ass_representation;
0360         }
0361         else if ( v.userType() >= QMetaType::User && v.canConvert<int>() )
0362         {
0363             return v.toInt();
0364         }
0365         return QCborValue::fromVariant(v);
0366     }
0367 
0368     void convert_object_from_meta(model::Object* obj, const QMetaObject* mo, QCborMap& json_obj)
0369     {
0370         if ( auto super = mo->superClass() )
0371             convert_object_from_meta(obj, super, json_obj);
0372 
0373         auto it = fields.find(model::detail::naked_type_name(mo));
0374         if ( it != fields.end() )
0375             convert_object_properties(obj, *it, json_obj);
0376     }
0377 
0378     void convert_object_basic(model::Object* obj, QCborMap& json_obj)
0379     {
0380         convert_object_from_meta(obj, obj->metaObject(), json_obj);
0381     }
0382 
0383     void convert_object_properties(model::Object* obj, const QVector<FieldInfo>& fields, QCborMap& json_obj)
0384     {
0385         for ( const auto& field : fields )
0386         {
0387             if ( field.mode != Auto || (strip && !field.essential) )
0388                 continue;
0389 
0390             model::BaseProperty * prop = obj->get_property(field.name);
0391             if ( !prop )
0392             {
0393                 logger.stream() << field.name << "is not a property";
0394                 continue;
0395             }
0396 
0397             if ( prop->traits().flags & model::PropertyTraits::Animated )
0398             {
0399                 json_obj[field.lottie] = convert_animated(static_cast<model::AnimatableBase*>(prop), field.transform);
0400             }
0401             else
0402             {
0403                 json_obj[field.lottie] = value_from_variant(field.transform.to_lottie(prop->value(), 0));
0404             }
0405         }
0406     }
0407 
0408     QCborValue keyframe_value_from_variant(const QVariant& v)
0409     {
0410         auto cb = value_from_variant(v);
0411         if ( cb.isArray() )
0412             return cb;
0413 
0414         return QCborArray{cb};
0415     }
0416 
0417     QCborMap convert_animated(
0418         model::AnimatableBase* prop,
0419         const TransformFunc& transform_values
0420     )
0421     {
0422         bool position = prop->traits().type == model::PropertyTraits::Point;
0423 
0424         QCborMap jobj;
0425         if ( prop->keyframe_count() > 1 )
0426         {
0427             jobj["a"_l] = 1;
0428             std::vector<std::unique_ptr<model::KeyframeBase>> split_kfs = split_keyframes(prop);
0429 
0430             QCborArray keyframes;
0431             QCborMap jkf;
0432             for ( int i = 0, e = split_kfs.size(); i < e; i++ )
0433             {
0434                 auto kf = split_kfs[i].get();
0435                 QVariant v = transform_values.to_lottie(kf->value(), kf->time());
0436                 QCborValue kf_value = keyframe_value_from_variant(v);
0437 
0438                 if ( i != 0 )
0439                 {
0440                     if ( old_kf )
0441                         jkf["e"_l] = kf_value;
0442 
0443                     if ( position )
0444                     {
0445                         auto pkf = static_cast<model::Keyframe<QPointF>*>(kf);
0446                         jkf["ti"_l] = point_to_lottie(pkf->point().tan_in - pkf->get());
0447                     }
0448 
0449                     keyframes.push_back(jkf);
0450                 }
0451 
0452                 jkf.clear();
0453                 jkf["t"_l] = kf->time();
0454                 jkf["s"_l] = kf_value;
0455 
0456                 if ( i != e - 1 )
0457                 {
0458                     if ( kf->transition().hold() )
0459                     {
0460                         jkf["h"_l] =  1;
0461                     }
0462                     else
0463                     {
0464                         jkf["h"_l] =  0;
0465                         jkf["o"_l] = keyframe_bezier_handle(kf->transition().before());
0466                         jkf["i"_l] = keyframe_bezier_handle(kf->transition().after());
0467                     }
0468                 }
0469 
0470                 if ( position )
0471                 {
0472                     auto pkf = static_cast<model::Keyframe<QPointF>*>(kf);
0473                     jkf["to"_l] = point_to_lottie(pkf->point().tan_out - pkf->get());
0474                 }
0475             }
0476             if ( position )
0477                 jkf.remove("to"_l);
0478             keyframes.push_back(jkf);
0479             jobj["k"_l] = keyframes;
0480         }
0481         else
0482         {
0483             jobj["a"_l] = 0;
0484             QVariant v = transform_values.to_lottie(prop->value(), 0);
0485             jobj["k"_l] = value_from_variant(v);
0486         }
0487         return jobj;
0488     }
0489 
0490     QCborMap keyframe_bezier_handle(const QPointF& p)
0491     {
0492         QCborMap jobj;
0493         QCborArray x;
0494         x.push_back(p.x());
0495         QCborArray y;
0496         y.push_back(p.y());
0497         jobj["x"_l] = x;
0498         jobj["y"_l] = y;
0499         return jobj;
0500     }
0501 
0502     void convert_styler(model::Styler* shape, QCborMap& jsh)
0503     {
0504         auto used = shape->use.get();
0505 
0506         auto gradient = qobject_cast<model::Gradient*>(used);
0507         if ( !gradient || !gradient->colors.get() )
0508         {
0509             auto color_prop = &shape->color;
0510             if ( auto color = qobject_cast<model::NamedColor*>(used) )
0511                 color_prop = &color->color;
0512             jsh["c"_l] = convert_animated(color_prop, {});
0513 
0514             auto join_func = [](const std::vector<QVariant>& args) -> QVariant {
0515                 return args[0].value<QColor>().alphaF() * args[1].toFloat() * 100;
0516             };
0517             model::JoinedAnimatable join({color_prop, &shape->opacity}, join_func);
0518             jsh["o"_l] = convert_animated(&join, {});
0519             return;
0520         }
0521 
0522         convert_object_basic(gradient, jsh);
0523 
0524         if ( shape->type_name() == "Fill" )
0525             jsh["ty"_l] = "gf";
0526         else
0527             jsh["ty"_l] = "gs";
0528 
0529         /// \todo highlight
0530         jsh["h"_l] = fake_animated(0);
0531         jsh["a"_l] = fake_animated(0);
0532 
0533         auto colors = gradient->colors.get();
0534         QCborMap jcolors;
0535         jcolors["p"_l] = colors->colors.get().size();
0536         jcolors["k"_l] = convert_animated(&colors->colors, {});
0537         jsh["g"_l] = jcolors;
0538     }
0539 
0540     QCborMap convert_shape(model::ShapeElement* shape, bool force_hidden)
0541     {
0542         if ( auto text = shape->cast<model::TextShape>() )
0543         {
0544             auto conv = text->to_path();
0545             return convert_shape(conv.get(), force_hidden || !shape->visible.get());
0546         }
0547 
0548         QCborMap jsh;
0549         jsh["ty"_l] = shape_types[shape->type_name()];
0550 //         jsh["d"] = 0;
0551         if ( force_hidden || !shape->visible.get() )
0552             jsh["hd"_l] = true;
0553 
0554         convert_object_basic(shape, jsh);
0555 
0556         if ( auto gr = qobject_cast<model::Group*>(shape) )
0557         {
0558             if ( qobject_cast<model::Layer*>(gr) )
0559                 format->information(i18n("Lottie only supports layers in the top level"));
0560             else if ( gr->auto_orient.get() )
0561                 format->information(i18n("Lottie only supports auto-orient layers in the top level"));
0562             auto shapes = convert_shapes(gr->shapes, force_hidden || !gr->visible.get());
0563             QCborMap transform;
0564             transform["ty"_l] = "tr";
0565             convert_transform(gr->transform.get(), &gr->opacity, transform);
0566             shapes.push_back(transform);
0567             jsh["it"_l] = shapes;
0568         }
0569         else if ( auto styler = shape->cast<model::Styler>() )
0570         {
0571             convert_styler(styler, jsh);
0572         }
0573         else if ( auto polystar = shape->cast<model::PolyStar>() )
0574         {
0575             if ( polystar->type.get() == model::PolyStar::Polygon )
0576             {
0577                 jsh.remove("is"_l);
0578                 jsh.remove("ir"_l);
0579             }
0580         }
0581         else if ( auto styler = shape->cast<model::Repeater>() )
0582         {
0583             QCborMap transform;
0584             convert_transform(styler->transform.get(), nullptr, transform);
0585             transform.remove("o"_l);
0586             transform["so"_l] = convert_animated(&styler->start_opacity, FloatMult(100));
0587             transform["eo"_l] = convert_animated(&styler->end_opacity, FloatMult(100));
0588             jsh["o"_l] = fake_animated(0);
0589             jsh["m"_l] = 1;
0590             jsh["tr"_l] = transform;
0591         }
0592 
0593         return jsh;
0594     }
0595 
0596     QCborMap fake_animated(const QCborValue& val)
0597     {
0598         QCborMap fake;
0599         fake["a"_l] = 0;
0600         fake["k"_l] = val;
0601         return fake;
0602     }
0603 
0604     QCborArray convert_shapes(const model::ShapeListProperty& shapes, bool force_hidden)
0605     {
0606         QCborArray jshapes;
0607         for ( const auto& shape : shapes )
0608         {
0609             if ( shape->is_instance<model::Image>() )
0610                 format->warning(i18n("Images cannot be grouped with other shapes, they must be inside a layer"));
0611             else if ( shape->is_instance<model::PreCompLayer>() )
0612                 format->warning(i18n("Composition layers cannot be grouped with other shapes, they must be inside a layer"));
0613             else if ( !strip || shape->visible.get() )
0614                 jshapes.push_front(convert_shape(shape.get(), force_hidden));
0615         }
0616         return jshapes;
0617     }
0618 
0619     QCborArray convert_assets(model::Composition* animation)
0620     {
0621         QCborArray assets;
0622 
0623         if ( !strip_raster )
0624         {
0625             for ( const auto& bmp : document->assets()->images->values )
0626             {
0627                 if ( auto_embed && !bmp->embedded() )
0628                 {
0629                     auto clone = bmp->clone_covariant();
0630                     clone->embed(true);
0631                     assets.push_back(convert_bitmat(clone.get()));
0632                 }
0633                 else
0634                 {
0635                     assets.push_back(convert_bitmat(bmp.get()));
0636                 }
0637             }
0638         }
0639 
0640         for ( const auto& comp : document->assets()->compositions->values )
0641         {
0642             if ( comp.get() != animation )
0643                 assets.push_back(convert_precomp(comp.get()));
0644         }
0645 
0646         return assets;
0647     }
0648 
0649     QCborMap convert_bitmat(model::Bitmap* bmp)
0650     {
0651         QCborMap out;
0652         convert_object_basic(bmp, out);
0653         out["id"_l] = bmp->uuid.get().toString();
0654         out["e"_l] = int(bmp->embedded());
0655         if ( bmp->embedded() )
0656         {
0657             out["u"_l] = "";
0658             out["p"_l] = bmp->to_url().toString();
0659         }
0660         else
0661         {
0662             auto finfo = bmp->file_info();
0663             out["u"_l] = finfo.absolutePath();
0664             out["p"_l] = finfo.fileName();
0665         }
0666         return out;
0667     }
0668 
0669     void convert_fake_layer_parent(model::Layer* parent, QCborMap& json)
0670     {
0671         if ( parent )
0672         {
0673             convert_animation_container(parent->animation.get(), json);
0674             json["parent"_l] = layer_index(parent);
0675         }
0676         else
0677         {
0678             convert_animation_container(main->animation.get(), json);
0679         }
0680     }
0681 
0682     void convert_fake_layer(model::DocumentNode* node, model::Layer* parent, QCborMap& json)
0683     {
0684         json["ddd"_l] = 0;
0685         if ( !strip )
0686         {
0687             json["nm"_l] = node->name.get();
0688             json["mn"_l] = node->uuid.get().toString();
0689         }
0690         convert_fake_layer_parent(parent, json);
0691         json["ind"_l] = layer_index(node);
0692     }
0693 
0694     QCborMap convert_image_layer(model::Image* image, model::Layer* parent)
0695     {
0696         QCborMap json;
0697         convert_fake_layer(image, parent, json);
0698         if ( !strip_raster )
0699             json["ty"_l] = 2;
0700         json["ind"_l] = layer_index(image);
0701         json["st"_l] = 0;
0702         QCborMap transform;
0703         convert_object_basic(image->transform.get(), transform);
0704         transform["o"_l] = QCborMap{
0705             {"a"_l, 0},
0706             {"k"_l, 100},
0707         };
0708         json["ks"_l] = transform;
0709         if ( !strip_raster && image->image.get() )
0710             json["refId"_l] = image->image->uuid.get().toString();
0711         return json;
0712     }
0713 
0714     QCborMap convert_precomp(model::Composition* comp)
0715     {
0716         QCborMap out;
0717         convert_object_basic(comp, out);
0718         out["id"_l] = comp->uuid.get().toString();
0719         convert_composition(comp, out);
0720         return out;
0721     }
0722 
0723     QCborMap convert_precomp_layer(model::PreCompLayer* layer, model::Layer* parent)
0724     {
0725         QCborMap json;
0726         json["ty"_l] = 0;
0727         convert_fake_layer(layer, parent, json);
0728         json["ind"_l] = layer_index(layer);
0729         json["st"_l] = layer->timing->start_time.get();
0730         json["sr"_l] = layer->timing->stretch.get();
0731         QCborMap transform;
0732         convert_transform(layer->transform.get(), &layer->opacity, transform);
0733         json["ks"_l] = transform;
0734         if ( layer->composition.get() )
0735             json["refId"_l] = layer->composition->uuid.get().toString();
0736         json["w"_l] = layer->size.get().width();
0737         json["h"_l] = layer->size.get().height();
0738         return json;
0739     }
0740 
0741     ImportExport* format;
0742     model::Composition* main;
0743     model::Document* document;
0744     bool strip;
0745     QMap<QUuid, int> layer_indices;
0746     app::log::Log logger{"Lottie Export"};
0747     model::Layer* mask = nullptr;
0748     bool strip_raster;
0749     bool auto_embed;
0750     bool old_kf;
0751 };
0752 
0753 
0754 
0755 } // namespace glaxnimate::io::lottie::detail