/* * Copyright (c) 2020 Samsung Electronics Co., Ltd. All rights reserved. * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ //#define DEBUG_PARSER // This parser implements JSON token-by-token parsing with an API that is // more direct; we don't have to create handler object and // callbacks. Instead, we retrieve values from the JSON stream by calling // GetInt(), GetDouble(), GetString() and GetBool(), traverse into structures // by calling EnterObject() and EnterArray(), and skip over unwanted data by // calling SkipValue(). As we know the lottie file structure this way will be // the efficient way of parsing the file. // // If you aren't sure of what's next in the JSON data, you can use PeekType() // and PeekValue() to look ahead to the next object before reading it. // // If you call the wrong retrieval method--e.g. GetInt when the next JSON token // is not an int, EnterObject or EnterArray when there isn't actually an object // or array to read--the stream parsing will end immediately and no more data // will be delivered. // // After calling EnterObject, you retrieve keys via NextObjectKey() and values // via the normal getters. When NextObjectKey() returns null, you have exited // the object, or you can call SkipObject() to skip to the end of the object // immediately. If you fetch the entire object (i.e. NextObjectKey() returned // null), you should not call SkipObject(). // // After calling EnterArray(), you must alternate between calling // NextArrayValue() to see if the array has more data, and then retrieving // values via the normal getters. You can call SkipArray() to skip to the end of // the array immediately. If you fetch the entire array (i.e. NextArrayValue() // returned null), you should not call SkipArray(). // // This parser uses in-situ strings, so the JSON buffer will be altered during // the parse. #include #include "lottiemodel.h" #include "rapidjson/document.h" #include "zip/zip.h" RAPIDJSON_DIAG_PUSH #ifdef __GNUC__ RAPIDJSON_DIAG_OFF(effc++) #endif using namespace rapidjson; using namespace rlottie::internal; class LookaheadParserHandler { public: bool Null() { st_ = kHasNull; v_.SetNull(); return true; } bool Bool(bool b) { st_ = kHasBool; v_.SetBool(b); return true; } bool Int(int i) { st_ = kHasNumber; v_.SetInt(i); return true; } bool Uint(unsigned u) { st_ = kHasNumber; v_.SetUint(u); return true; } bool Int64(int64_t i) { st_ = kHasNumber; v_.SetInt64(i); return true; } bool Uint64(int64_t u) { st_ = kHasNumber; v_.SetUint64(u); return true; } bool Double(double d) { st_ = kHasNumber; v_.SetDouble(d); return true; } bool RawNumber(const char *, SizeType, bool) { return false; } bool String(const char *str, SizeType length, bool) { st_ = kHasString; v_.SetString(str, length); return true; } bool StartObject() { st_ = kEnteringObject; return true; } bool Key(const char *str, SizeType length, bool) { st_ = kHasKey; v_.SetString(str, length); return true; } bool EndObject(SizeType) { st_ = kExitingObject; return true; } bool StartArray() { st_ = kEnteringArray; return true; } bool EndArray(SizeType) { st_ = kExitingArray; return true; } void Error() { st_ = kError; } protected: explicit LookaheadParserHandler(char *str); protected: enum LookaheadParsingState { kInit, kError, kHasNull, kHasBool, kHasNumber, kHasString, kHasKey, kEnteringObject, kExitingObject, kEnteringArray, kExitingArray }; Value v_; LookaheadParsingState st_; Reader r_; InsituStringStream ss_; static const int parseFlags = kParseDefaultFlags | kParseInsituFlag; }; class LottieParserImpl : public LookaheadParserHandler { public: LottieParserImpl(char *str, std::string dir_path, model::ColorFilter filter) : LookaheadParserHandler(str), mColorFilter(std::move(filter)), mDirPath(std::move(dir_path)) { } bool VerifyType(); bool ParseNext(); public: VArenaAlloc &allocator() { return compRef->mArenaAlloc; } bool EnterObject(); bool EnterArray(); const char * NextObjectKey(); bool NextArrayValue(); int GetInt(); double GetDouble(); const char * GetString(); std::string GetStringObject(); bool GetBool(); void GetNull(); void SkipObject(); void SkipArray(); void SkipValue(); Value *PeekValue(); int PeekType() const; bool IsValid() { return st_ != kError; } void Skip(const char *key); model::BlendMode getBlendMode(); CapStyle getLineCap(); JoinStyle getLineJoin(); FillRule getFillRule(); model::Trim::TrimType getTrimType(); model::MatteType getMatteType(); model::Layer::Type getLayerType(); std::shared_ptr composition() const { return mComposition; } void parseComposition(); void parseMarkers(); void parseMarker(); void parseAssets(model::Composition *comp); model::Asset * parseAsset(); void parseLayers(model::Composition *comp); model::Layer * parseLayer(); void parseMaskProperty(model::Layer *layer); void parseShapesAttr(model::Layer *layer); void parseObject(model::Group *parent); model::Mask * parseMaskObject(); model::Object * parseObjectTypeAttr(); model::Object * parseGroupObject(); model::Rect * parseRectObject(); model::RoundedCorner * parseRoundedCorner(); void updateRoundedCorner(model::Group *parent, model::RoundedCorner *rc); model::Ellipse * parseEllipseObject(); model::Path * parseShapeObject(); model::Polystar *parsePolystarObject(); model::Transform * parseTransformObject(bool ddd = false); model::Fill * parseFillObject(); model::GradientFill * parseGFillObject(); model::Stroke * parseStrokeObject(); model::GradientStroke *parseGStrokeObject(); model::Trim * parseTrimObject(); model::Repeater * parseReapeaterObject(); void parseGradientProperty(model::Gradient *gradient, const char *key); VPointF parseInperpolatorPoint(); void getValue(VPointF &pt); void getValue(float &fval); void getValue(model::Color &color); void getValue(int &ival); void getValue(model::PathData &shape); void getValue(model::Gradient::Data &gradient); void getValue(std::vector &v); void getValue(model::Repeater::Transform &); template bool parseKeyFrameValue(const char *, model::Value &) { return false; } template bool parseKeyFrameValue(const char * key, model::Value &value); template void parseKeyFrame(model::KeyFrames &obj); template void parseProperty(model::Property &obj); template void parsePropertyHelper(model::Property &obj); void parseShapeProperty(model::Property &obj); void parseDashProperty(model::Dash &dash); VInterpolator *interpolator(VPointF, VPointF, std::string); model::Color toColor(const char *str); void resolveLayerRefs(); void parsePathInfo(); private: model::ColorFilter mColorFilter; struct { std::vector mInPoint; /* "i" */ std::vector mOutPoint; /* "o" */ std::vector mVertices; /* "v" */ std::vector mResult; bool mClosed{false}; void convert() { // shape data could be empty. if (mInPoint.empty() || mOutPoint.empty() || mVertices.empty()) { mResult.clear(); return; } /* * Convert the AE shape format to * list of bazier curves * The final structure will be Move +size*Cubic + Cubic (if the path * is closed one) */ if (mInPoint.size() != mOutPoint.size() || mInPoint.size() != mVertices.size()) { mResult.clear(); } else { auto size = mVertices.size(); mResult.push_back(mVertices[0]); for (size_t i = 1; i < size; i++) { mResult.push_back( mVertices[i - 1] + mOutPoint[i - 1]); // CP1 = start + outTangent mResult.push_back(mVertices[i] + mInPoint[i]); // CP2 = end + inTangent mResult.push_back(mVertices[i]); // end point } if (mClosed) { mResult.push_back( mVertices[size - 1] + mOutPoint[size - 1]); // CP1 = start + outTangent mResult.push_back(mVertices[0] + mInPoint[0]); // CP2 = end + inTangent mResult.push_back(mVertices[0]); // end point } } } void reset() { mInPoint.clear(); mOutPoint.clear(); mVertices.clear(); mResult.clear(); mClosed = false; } void updatePath(VPath &out) { if (mResult.empty()) return; auto size = mResult.size(); auto points = mResult.data(); /* reserve exact memory requirement at once * ptSize = size + 1(size + close) * elmSize = size/3 cubic + 1 move + 1 close */ out.reserve(size + 1, size / 3 + 2); out.moveTo(points[0]); for (size_t i = 1; i < size; i += 3) { out.cubicTo(points[i], points[i + 1], points[i + 2]); } if (mClosed) out.close(); } } mPathInfo; protected: std::unordered_map mInterpolatorCache; std::shared_ptr mComposition; model::Composition * compRef{nullptr}; model::Layer * curLayerRef{nullptr}; std::vector mLayersToUpdate; std::string mDirPath; void SkipOut(int depth); }; LookaheadParserHandler::LookaheadParserHandler(char *str) : v_(), st_(kInit), ss_(str) { r_.IterativeParseInit(); } bool LottieParserImpl::VerifyType() { /* Verify the media type is lottie json. Could add more strict check. */ return ParseNext(); } bool LottieParserImpl::ParseNext() { if (r_.HasParseError()) { st_ = kError; return false; } if (!r_.IterativeParseNext(ss_, *this)) { vCritical << "Lottie file parsing error"; st_ = kError; return false; } return true; } bool LottieParserImpl::EnterObject() { if (st_ != kEnteringObject) { st_ = kError; return false; } ParseNext(); return true; } bool LottieParserImpl::EnterArray() { if (st_ != kEnteringArray) { st_ = kError; return false; } ParseNext(); return true; } const char *LottieParserImpl::NextObjectKey() { if (st_ == kHasKey) { const char *result = v_.GetString(); ParseNext(); return result; } /* SPECIAL CASE * The parser works with a prdefined rule that it will be only * while (NextObjectKey()) for each object but in case of our nested group * object we can call multiple time NextObjectKey() while exiting the object * so ignore those and don't put parser in the error state. * */ if (st_ == kExitingArray || st_ == kEnteringObject) { // #ifdef DEBUG_PARSER // vDebug<<"Object: Exiting nested loop"; // #endif return nullptr; } if (st_ != kExitingObject) { st_ = kError; return nullptr; } ParseNext(); return nullptr; } bool LottieParserImpl::NextArrayValue() { if (st_ == kExitingArray) { ParseNext(); return false; } /* SPECIAL CASE * same as NextObjectKey() */ if (st_ == kExitingObject) { return false; } if (st_ == kError || st_ == kHasKey) { st_ = kError; return false; } return true; } int LottieParserImpl::GetInt() { if (st_ != kHasNumber || !v_.IsInt()) { st_ = kError; return 0; } int result = v_.GetInt(); ParseNext(); return result; } double LottieParserImpl::GetDouble() { if (st_ != kHasNumber) { st_ = kError; return 0.; } double result = v_.GetDouble(); ParseNext(); return result; } bool LottieParserImpl::GetBool() { if (st_ != kHasBool) { st_ = kError; return false; } bool result = v_.GetBool(); ParseNext(); return result; } void LottieParserImpl::GetNull() { if (st_ != kHasNull) { st_ = kError; return; } ParseNext(); } const char *LottieParserImpl::GetString() { if (st_ != kHasString) { st_ = kError; return nullptr; } const char *result = v_.GetString(); ParseNext(); return result; } std::string LottieParserImpl::GetStringObject() { auto str = GetString(); if (str) { return std::string(str); } return {}; } void LottieParserImpl::SkipOut(int depth) { do { if (st_ == kEnteringArray || st_ == kEnteringObject) { ++depth; } else if (st_ == kExitingArray || st_ == kExitingObject) { --depth; } else if (st_ == kError) { return; } ParseNext(); } while (depth > 0); } void LottieParserImpl::SkipValue() { SkipOut(0); } void LottieParserImpl::SkipArray() { SkipOut(1); } void LottieParserImpl::SkipObject() { SkipOut(1); } Value *LottieParserImpl::PeekValue() { if (st_ >= kHasNull && st_ <= kHasKey) { return &v_; } return nullptr; } // returns a rapidjson::Type, or -1 for no value (at end of // object/array) int LottieParserImpl::PeekType() const { if (st_ >= kHasNull && st_ <= kHasKey) { return v_.GetType(); } if (st_ == kEnteringArray) { return kArrayType; } if (st_ == kEnteringObject) { return kObjectType; } return -1; } void LottieParserImpl::Skip(const char * /*key*/) { if (PeekType() == kArrayType) { EnterArray(); SkipArray(); } else if (PeekType() == kObjectType) { EnterObject(); SkipObject(); } else { SkipValue(); } } model::BlendMode LottieParserImpl::getBlendMode() { auto mode = model::BlendMode::Normal; switch (GetInt()) { case 1: mode = model::BlendMode::Multiply; break; case 2: mode = model::BlendMode::Screen; break; case 3: mode = model::BlendMode::OverLay; break; default: break; } return mode; } void LottieParserImpl::resolveLayerRefs() { for (const auto &layer : mLayersToUpdate) { auto search = compRef->mAssets.find(layer->extra()->mPreCompRefId); if (search != compRef->mAssets.end()) { if (layer->mLayerType == model::Layer::Type::Image) { layer->extra()->mAsset = search->second; } else if (layer->mLayerType == model::Layer::Type::Precomp) { layer->mChildren = search->second->mLayers; layer->setStatic(layer->isStatic() && search->second->isStatic()); } } } } void LottieParserImpl::parseComposition() { EnterObject(); std::shared_ptr sharedComposition = std::make_shared(); model::Composition *comp = sharedComposition.get(); compRef = comp; while (const char *key = NextObjectKey()) { if (0 == strcmp(key, "v")) { comp->mVersion = GetStringObject(); } else if (0 == strcmp(key, "w")) { comp->mSize.setWidth(GetInt()); } else if (0 == strcmp(key, "h")) { comp->mSize.setHeight(GetInt()); } else if (0 == strcmp(key, "ip")) { comp->mStartFrame = std::lround(GetDouble()); } else if (0 == strcmp(key, "op")) { comp->mEndFrame = std::lround(GetDouble()); } else if (0 == strcmp(key, "fr")) { comp->mFrameRate = GetDouble(); } else if (0 == strcmp(key, "assets")) { parseAssets(comp); } else if (0 == strcmp(key, "layers")) { parseLayers(comp); } else if (0 == strcmp(key, "markers")) { parseMarkers(); } else { #ifdef DEBUG_PARSER vWarning << "Composition Attribute Skipped : " << key; #endif Skip(key); } } if (comp->mVersion.empty() || !comp->mRootLayer) { // don't have a valid bodymovin header return; } if (comp->mStartFrame > comp->mEndFrame) { // reveresed animation? missing data? return; } if (!IsValid()) { return; } resolveLayerRefs(); comp->setStatic(comp->mRootLayer->isStatic()); comp->mRootLayer->mInFrame = comp->mStartFrame; comp->mRootLayer->mOutFrame = comp->mEndFrame; mComposition = sharedComposition; } void LottieParserImpl::parseMarker() { EnterObject(); std::string comment; int timeframe{0}; int duration{0}; while (const char *key = NextObjectKey()) { if (0 == strcmp(key, "cm")) { comment = GetStringObject(); } else if (0 == strcmp(key, "tm")) { timeframe = GetDouble(); } else if (0 == strcmp(key, "dr")) { duration = GetDouble(); } else { #ifdef DEBUG_PARSER vWarning << "Marker Attribute Skipped : " << key; #endif Skip(key); } } compRef->mMarkers.emplace_back(std::move(comment), timeframe, timeframe + duration); } void LottieParserImpl::parseMarkers() { EnterArray(); while (NextArrayValue()) { parseMarker(); } // update the precomp layers with the actual layer object } void LottieParserImpl::parseAssets(model::Composition *composition) { EnterArray(); while (NextArrayValue()) { auto asset = parseAsset(); composition->mAssets[asset->mRefId] = asset; } // update the precomp layers with the actual layer object } static constexpr const unsigned char B64index[256] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 62, 63, 62, 62, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 0, 0, 0, 0, 63, 0, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51}; std::string b64decode(const char *data, const size_t len) { auto p = reinterpret_cast(data); int pad = len > 0 && (len % 4 || p[len - 1] == '='); const size_t L = ((len + 3) / 4 - pad) * 4; std::string str(L / 4 * 3 + pad, '\0'); for (size_t i = 0, j = 0; i < L; i += 4) { int n = B64index[p[i]] << 18 | B64index[p[i + 1]] << 12 | B64index[p[i + 2]] << 6 | B64index[p[i + 3]]; str[j++] = n >> 16; str[j++] = n >> 8 & 0xFF; str[j++] = n & 0xFF; } if (pad) { int n = B64index[p[L]] << 18 | B64index[p[L + 1]] << 12; str[str.size() - 1] = n >> 16; if (len > L + 2 && p[L + 2] != '=') { n |= B64index[p[L + 2]] << 6; str.push_back(n >> 8 & 0xFF); } } return str; } static std::string convertFromBase64(const std::string &str) { // usual header look like "data:image/png;base64," // so need to skip till ','. size_t startIndex = str.find(",", 0); startIndex += 1; // skip "," size_t length = str.length() - startIndex; const char *b64Data = str.c_str() + startIndex; return b64decode(b64Data, length); } /* * std::to_string() function is missing in VS2017 * so this is workaround for windows build */ #include template static std::string toString(const T &value) { std::ostringstream os; os << value; return os.str(); } /* * https://github.com/airbnb/lottie-web/blob/master/docs/json/layers/shape.json * */ model::Asset *LottieParserImpl::parseAsset() { auto asset = allocator().make(); std::string filename; std::string relativePath; bool embededResource = false; EnterObject(); while (const char *key = NextObjectKey()) { if (0 == strcmp(key, "w")) { asset->mWidth = GetInt(); } else if (0 == strcmp(key, "h")) { asset->mHeight = GetInt(); } else if (0 == strcmp(key, "p")) { /* image name */ asset->mAssetType = model::Asset::Type::Image; filename = GetStringObject(); } else if (0 == strcmp(key, "u")) { /* relative image path */ relativePath = GetStringObject(); } else if (0 == strcmp(key, "e")) { /* relative image path */ embededResource = GetInt(); } else if (0 == strcmp(key, "id")) { /* reference id*/ if (PeekType() == kStringType) { asset->mRefId = GetStringObject(); } else { asset->mRefId = toString(GetInt()); } } else if (0 == strcmp(key, "layers")) { asset->mAssetType = model::Asset::Type::Precomp; EnterArray(); bool staticFlag = true; while (NextArrayValue()) { auto layer = parseLayer(); if (layer) { staticFlag = staticFlag && layer->isStatic(); asset->mLayers.push_back(layer); } } asset->setStatic(staticFlag); } else { #ifdef DEBUG_PARSER vWarning << "Asset Attribute Skipped : " << key; #endif Skip(key); } } if (asset->mAssetType == model::Asset::Type::Image) { if (embededResource) { // embeder resource should start with "data:" if (filename.compare(0, 5, "data:") == 0) { asset->loadImageData(convertFromBase64(filename)); } } else { asset->loadImagePath(mDirPath + relativePath + filename); } } return asset; } void LottieParserImpl::parseLayers(model::Composition *comp) { comp->mRootLayer = allocator().make(); comp->mRootLayer->mLayerType = model::Layer::Type::Precomp; comp->mRootLayer->setName("__"); bool staticFlag = true; EnterArray(); while (NextArrayValue()) { auto layer = parseLayer(); if (layer) { staticFlag = staticFlag && layer->isStatic(); comp->mRootLayer->mChildren.push_back(layer); } } comp->mRootLayer->setStatic(staticFlag); } model::Color LottieParserImpl::toColor(const char *str) { if (!str) return {}; model::Color color; auto len = strlen(str); // some resource has empty color string // return a default color for those cases. if (len != 7 || str[0] != '#') return color; char tmp[3] = {'\0', '\0', '\0'}; tmp[0] = str[1]; tmp[1] = str[2]; color.r = std::strtol(tmp, nullptr, 16) / 255.0f; tmp[0] = str[3]; tmp[1] = str[4]; color.g = std::strtol(tmp, nullptr, 16) / 255.0f; tmp[0] = str[5]; tmp[1] = str[6]; color.b = std::strtol(tmp, nullptr, 16) / 255.0f; return color; } model::MatteType LottieParserImpl::getMatteType() { switch (GetInt()) { case 1: return model::MatteType::Alpha; break; case 2: return model::MatteType::AlphaInv; break; case 3: return model::MatteType::Luma; break; case 4: return model::MatteType::LumaInv; break; default: return model::MatteType::None; break; } } model::Layer::Type LottieParserImpl::getLayerType() { switch (GetInt()) { case 0: return model::Layer::Type::Precomp; break; case 1: return model::Layer::Type::Solid; break; case 2: return model::Layer::Type::Image; break; case 3: return model::Layer::Type::Null; break; case 4: return model::Layer::Type::Shape; break; case 5: return model::Layer::Type::Text; break; default: return model::Layer::Type::Null; break; } } /* * https://github.com/airbnb/lottie-web/blob/master/docs/json/layers/shape.json * */ model::Layer *LottieParserImpl::parseLayer() { model::Layer *layer = allocator().make(); curLayerRef = layer; bool ddd = true; EnterObject(); while (const char *key = NextObjectKey()) { if (0 == strcmp(key, "ty")) { /* Type of layer*/ layer->mLayerType = getLayerType(); } else if (0 == strcmp(key, "nm")) { /*Layer name*/ layer->setName(GetString()); } else if (0 == strcmp(key, "ind")) { /*Layer index in AE. Used for parenting and expressions.*/ layer->mId = GetInt(); } else if (0 == strcmp(key, "ddd")) { /*3d layer */ ddd = GetInt(); } else if (0 == strcmp(key, "parent")) { /*Layer Parent. Uses "ind" of parent.*/ layer->mParentId = GetInt(); } else if (0 == strcmp(key, "refId")) { /*preComp Layer reference id*/ layer->extra()->mPreCompRefId = GetStringObject(); layer->mHasGradient = true; mLayersToUpdate.push_back(layer); } else if (0 == strcmp(key, "sr")) { // "Layer Time Stretching" layer->mTimeStreatch = GetDouble(); } else if (0 == strcmp(key, "tm")) { // time remapping parseProperty(layer->extra()->mTimeRemap); } else if (0 == strcmp(key, "ip")) { layer->mInFrame = std::lround(GetDouble()); } else if (0 == strcmp(key, "op")) { layer->mOutFrame = std::lround(GetDouble()); } else if (0 == strcmp(key, "st")) { layer->mStartFrame = GetDouble(); } else if (0 == strcmp(key, "bm")) { layer->mBlendMode = getBlendMode(); } else if (0 == strcmp(key, "ks")) { EnterObject(); layer->mTransform = parseTransformObject(ddd); } else if (0 == strcmp(key, "shapes")) { parseShapesAttr(layer); } else if (0 == strcmp(key, "w")) { layer->mLayerSize.setWidth(GetInt()); } else if (0 == strcmp(key, "h")) { layer->mLayerSize.setHeight(GetInt()); } else if (0 == strcmp(key, "sw")) { layer->mLayerSize.setWidth(GetInt()); } else if (0 == strcmp(key, "sh")) { layer->mLayerSize.setHeight(GetInt()); } else if (0 == strcmp(key, "sc")) { layer->extra()->mSolidColor = toColor(GetString()); } else if (0 == strcmp(key, "tt")) { layer->mMatteType = getMatteType(); } else if (0 == strcmp(key, "hasMask")) { layer->mHasMask = GetBool(); } else if (0 == strcmp(key, "masksProperties")) { parseMaskProperty(layer); } else if (0 == strcmp(key, "ao")) { layer->mAutoOrient = GetInt(); } else if (0 == strcmp(key, "hd")) { layer->setHidden(GetBool()); } else { #ifdef DEBUG_PARSER vWarning << "Layer Attribute Skipped : " << key; #endif Skip(key); } } if (!layer->mTransform) { // not a valid layer return nullptr; } // make sure layer data is not corrupted. if (layer->hasParent() && (layer->id() == layer->parentId())) return nullptr; if (layer->mExtra) layer->mExtra->mCompRef = compRef; if (layer->hidden()) { // if layer is hidden, only data that is usefull is its // transform matrix(when it is a parent of some other layer) // so force it to be a Null Layer and release all resource. layer->setStatic(layer->mTransform->isStatic()); layer->mLayerType = model::Layer::Type::Null; layer->mChildren = {}; return layer; } // update the static property of layer bool staticFlag = true; for (const auto &child : layer->mChildren) { staticFlag &= child->isStatic(); } if (layer->hasMask() && layer->mExtra) { for (const auto &mask : layer->mExtra->mMasks) { staticFlag &= mask->isStatic(); } } layer->setStatic(staticFlag && layer->mTransform->isStatic()); return layer; } void LottieParserImpl::parseMaskProperty(model::Layer *layer) { EnterArray(); while (NextArrayValue()) { layer->extra()->mMasks.push_back(parseMaskObject()); } } model::Mask *LottieParserImpl::parseMaskObject() { auto obj = allocator().make(); EnterObject(); while (const char *key = NextObjectKey()) { if (0 == strcmp(key, "inv")) { obj->mInv = GetBool(); } else if (0 == strcmp(key, "mode")) { const char *str = GetString(); if (!str) { obj->mMode = model::Mask::Mode::None; continue; } switch (str[0]) { case 'n': obj->mMode = model::Mask::Mode::None; break; case 'a': obj->mMode = model::Mask::Mode::Add; break; case 's': obj->mMode = model::Mask::Mode::Substarct; break; case 'i': obj->mMode = model::Mask::Mode::Intersect; break; case 'f': obj->mMode = model::Mask::Mode::Difference; break; default: obj->mMode = model::Mask::Mode::None; break; } } else if (0 == strcmp(key, "pt")) { parseShapeProperty(obj->mShape); } else if (0 == strcmp(key, "o")) { parseProperty(obj->mOpacity); } else { Skip(key); } } obj->mIsStatic = obj->mShape.isStatic() && obj->mOpacity.isStatic(); return obj; } void LottieParserImpl::parseShapesAttr(model::Layer *layer) { EnterArray(); while (NextArrayValue()) { parseObject(layer); } } model::Object *LottieParserImpl::parseObjectTypeAttr() { const char *type = GetString(); if (!type) { vWarning << "No object type specified"; return nullptr; } if (0 == strcmp(type, "gr")) { return parseGroupObject(); } else if (0 == strcmp(type, "rc")) { return parseRectObject(); } else if (0 == strcmp(type, "rd")) { curLayerRef->mHasRoundedCorner = true; return parseRoundedCorner(); } else if (0 == strcmp(type, "el")) { return parseEllipseObject(); } else if (0 == strcmp(type, "tr")) { return parseTransformObject(); } else if (0 == strcmp(type, "fl")) { return parseFillObject(); } else if (0 == strcmp(type, "st")) { return parseStrokeObject(); } else if (0 == strcmp(type, "gf")) { curLayerRef->mHasGradient = true; return parseGFillObject(); } else if (0 == strcmp(type, "gs")) { curLayerRef->mHasGradient = true; return parseGStrokeObject(); } else if (0 == strcmp(type, "sh")) { return parseShapeObject(); } else if (0 == strcmp(type, "sr")) { return parsePolystarObject(); } else if (0 == strcmp(type, "tm")) { curLayerRef->mHasPathOperator = true; return parseTrimObject(); } else if (0 == strcmp(type, "rp")) { curLayerRef->mHasRepeater = true; return parseReapeaterObject(); } else if (0 == strcmp(type, "mm")) { vWarning << "Merge Path is not supported yet"; return nullptr; } else { #ifdef DEBUG_PARSER vDebug << "The Object Type not yet handled = " << type; #endif return nullptr; } } void LottieParserImpl::parseObject(model::Group *parent) { EnterObject(); while (const char *key = NextObjectKey()) { if (0 == strcmp(key, "ty")) { auto child = parseObjectTypeAttr(); if (child && !child->hidden()) { if (child->type() == model::Object::Type::RoundedCorner) { updateRoundedCorner(parent, static_cast(child)); } parent->mChildren.push_back(child); } } else { Skip(key); } } } void LottieParserImpl::updateRoundedCorner(model::Group *group, model::RoundedCorner *rc) { for(auto &e : group->mChildren) { if (e->type() == model::Object::Type::Rect) { static_cast(e)->mRoundedCorner = rc; if (!rc->isStatic()) { e->setStatic(false); group->setStatic(false); //@TODO need to propagate. } } else if ( e->type() == model::Object::Type::Group) { updateRoundedCorner(static_cast(e), rc); } } } model::Object *LottieParserImpl::parseGroupObject() { auto group = allocator().make(); while (const char *key = NextObjectKey()) { if (0 == strcmp(key, "nm")) { group->setName(GetString()); } else if (0 == strcmp(key, "it")) { EnterArray(); while (NextArrayValue()) { parseObject(group); } if (!group->mChildren.empty() && group->mChildren.back()->type() == model::Object::Type::Transform) { group->mTransform = static_cast(group->mChildren.back()); group->mChildren.pop_back(); } } else { Skip(key); } } bool staticFlag = true; for (const auto &child : group->mChildren) { staticFlag &= child->isStatic(); } if (group->mTransform) { group->setStatic(staticFlag && group->mTransform->isStatic()); } return group; } /* * https://github.com/airbnb/lottie-web/blob/master/docs/json/shapes/rect.json */ model::Rect *LottieParserImpl::parseRectObject() { auto obj = allocator().make(); while (const char *key = NextObjectKey()) { if (0 == strcmp(key, "nm")) { obj->setName(GetString()); } else if (0 == strcmp(key, "p")) { parseProperty(obj->mPos); } else if (0 == strcmp(key, "s")) { parseProperty(obj->mSize); } else if (0 == strcmp(key, "r")) { parseProperty(obj->mRound); } else if (0 == strcmp(key, "d")) { obj->mDirection = GetInt(); } else if (0 == strcmp(key, "hd")) { obj->setHidden(GetBool()); } else { Skip(key); } } obj->setStatic(obj->mPos.isStatic() && obj->mSize.isStatic() && obj->mRound.isStatic()); return obj; } /* * https://github.com/airbnb/lottie-web/blob/master/docs/json/shapes/rect.json */ model::RoundedCorner *LottieParserImpl::parseRoundedCorner() { auto obj = allocator().make(); while (const char *key = NextObjectKey()) { if (0 == strcmp(key, "nm")) { obj->setName(GetString()); } else if (0 == strcmp(key, "r")) { parseProperty(obj->mRadius); } else if (0 == strcmp(key, "hd")) { obj->setHidden(GetBool()); } else { Skip(key); } } obj->setStatic(obj->mRadius.isStatic()); return obj; } /* * https://github.com/airbnb/lottie-web/blob/master/docs/json/shapes/ellipse.json */ model::Ellipse *LottieParserImpl::parseEllipseObject() { auto obj = allocator().make(); while (const char *key = NextObjectKey()) { if (0 == strcmp(key, "nm")) { obj->setName(GetString()); } else if (0 == strcmp(key, "p")) { parseProperty(obj->mPos); } else if (0 == strcmp(key, "s")) { parseProperty(obj->mSize); } else if (0 == strcmp(key, "d")) { obj->mDirection = GetInt(); } else if (0 == strcmp(key, "hd")) { obj->setHidden(GetBool()); } else { Skip(key); } } obj->setStatic(obj->mPos.isStatic() && obj->mSize.isStatic()); return obj; } /* * https://github.com/airbnb/lottie-web/blob/master/docs/json/shapes/shape.json */ model::Path *LottieParserImpl::parseShapeObject() { auto obj = allocator().make(); while (const char *key = NextObjectKey()) { if (0 == strcmp(key, "nm")) { obj->setName(GetString()); } else if (0 == strcmp(key, "ks")) { parseShapeProperty(obj->mShape); } else if (0 == strcmp(key, "d")) { obj->mDirection = GetInt(); } else if (0 == strcmp(key, "hd")) { obj->setHidden(GetBool()); } else { #ifdef DEBUG_PARSER vDebug << "Shape property ignored :" << key; #endif Skip(key); } } obj->setStatic(obj->mShape.isStatic()); return obj; } /* * https://github.com/airbnb/lottie-web/blob/master/docs/json/shapes/star.json */ model::Polystar *LottieParserImpl::parsePolystarObject() { auto obj = allocator().make(); while (const char *key = NextObjectKey()) { if (0 == strcmp(key, "nm")) { obj->setName(GetString()); } else if (0 == strcmp(key, "p")) { parseProperty(obj->mPos); } else if (0 == strcmp(key, "pt")) { parseProperty(obj->mPointCount); } else if (0 == strcmp(key, "ir")) { parseProperty(obj->mInnerRadius); } else if (0 == strcmp(key, "is")) { parseProperty(obj->mInnerRoundness); } else if (0 == strcmp(key, "or")) { parseProperty(obj->mOuterRadius); } else if (0 == strcmp(key, "os")) { parseProperty(obj->mOuterRoundness); } else if (0 == strcmp(key, "r")) { parseProperty(obj->mRotation); } else if (0 == strcmp(key, "sy")) { int starType = GetInt(); if (starType == 1) obj->mPolyType = model::Polystar::PolyType::Star; if (starType == 2) obj->mPolyType = model::Polystar::PolyType::Polygon; } else if (0 == strcmp(key, "d")) { obj->mDirection = GetInt(); } else if (0 == strcmp(key, "hd")) { obj->setHidden(GetBool()); } else { #ifdef DEBUG_PARSER vDebug << "Polystar property ignored :" << key; #endif Skip(key); } } obj->setStatic( obj->mPos.isStatic() && obj->mPointCount.isStatic() && obj->mInnerRadius.isStatic() && obj->mInnerRoundness.isStatic() && obj->mOuterRadius.isStatic() && obj->mOuterRoundness.isStatic() && obj->mRotation.isStatic()); return obj; } model::Trim::TrimType LottieParserImpl::getTrimType() { switch (GetInt()) { case 1: return model::Trim::TrimType::Simultaneously; break; case 2: return model::Trim::TrimType::Individually; break; default: Error(); return model::Trim::TrimType::Simultaneously; break; } } /* * https://github.com/airbnb/lottie-web/blob/master/docs/json/shapes/trim.json */ model::Trim *LottieParserImpl::parseTrimObject() { auto obj = allocator().make(); while (const char *key = NextObjectKey()) { if (0 == strcmp(key, "nm")) { obj->setName(GetString()); } else if (0 == strcmp(key, "s")) { parseProperty(obj->mStart); } else if (0 == strcmp(key, "e")) { parseProperty(obj->mEnd); } else if (0 == strcmp(key, "o")) { parseProperty(obj->mOffset); } else if (0 == strcmp(key, "m")) { obj->mTrimType = getTrimType(); } else if (0 == strcmp(key, "hd")) { obj->setHidden(GetBool()); } else { #ifdef DEBUG_PARSER vDebug << "Trim property ignored :" << key; #endif Skip(key); } } obj->setStatic(obj->mStart.isStatic() && obj->mEnd.isStatic() && obj->mOffset.isStatic()); return obj; } void LottieParserImpl::getValue(model::Repeater::Transform &obj) { EnterObject(); while (const char *key = NextObjectKey()) { if (0 == strcmp(key, "a")) { parseProperty(obj.mAnchor); } else if (0 == strcmp(key, "p")) { parseProperty(obj.mPosition); } else if (0 == strcmp(key, "r")) { parseProperty(obj.mRotation); } else if (0 == strcmp(key, "s")) { parseProperty(obj.mScale); } else if (0 == strcmp(key, "so")) { parseProperty(obj.mStartOpacity); } else if (0 == strcmp(key, "eo")) { parseProperty(obj.mEndOpacity); } else { Skip(key); } } } model::Repeater *LottieParserImpl::parseReapeaterObject() { auto obj = allocator().make(); obj->setContent(allocator().make()); while (const char *key = NextObjectKey()) { if (0 == strcmp(key, "nm")) { obj->setName(GetString()); } else if (0 == strcmp(key, "c")) { parseProperty(obj->mCopies); float maxCopy = 0.0; if (!obj->mCopies.isStatic()) { for (auto &keyFrame : obj->mCopies.animation().frames_) { if (maxCopy < keyFrame.value_.start_) maxCopy = keyFrame.value_.start_; if (maxCopy < keyFrame.value_.end_) maxCopy = keyFrame.value_.end_; } } else { maxCopy = obj->mCopies.value(); } obj->mMaxCopies = maxCopy; } else if (0 == strcmp(key, "o")) { parseProperty(obj->mOffset); } else if (0 == strcmp(key, "tr")) { getValue(obj->mTransform); } else if (0 == strcmp(key, "hd")) { obj->setHidden(GetBool()); } else { #ifdef DEBUG_PARSER vDebug << "Repeater property ignored :" << key; #endif Skip(key); } } obj->setStatic(obj->mCopies.isStatic() && obj->mOffset.isStatic() && obj->mTransform.isStatic()); return obj; } /* * https://github.com/airbnb/lottie-web/blob/master/docs/json/shapes/transform.json */ model::Transform *LottieParserImpl::parseTransformObject(bool ddd) { auto objT = allocator().make(); auto obj = allocator().make(); if (ddd) { obj->createExtraData(); obj->mExtra->m3DData = true; } while (const char *key = NextObjectKey()) { if (0 == strcmp(key, "nm")) { objT->setName(GetString()); } else if (0 == strcmp(key, "a")) { parseProperty(obj->mAnchor); } else if (0 == strcmp(key, "p")) { EnterObject(); bool separate = false; while (const char *key = NextObjectKey()) { if (0 == strcmp(key, "k")) { parsePropertyHelper(obj->mPosition); } else if (0 == strcmp(key, "s")) { obj->createExtraData(); obj->mExtra->mSeparate = GetBool(); separate = true; } else if (separate && (0 == strcmp(key, "x"))) { parseProperty(obj->mExtra->mSeparateX); } else if (separate && (0 == strcmp(key, "y"))) { parseProperty(obj->mExtra->mSeparateY); } else { Skip(key); } } } else if (0 == strcmp(key, "r")) { parseProperty(obj->mRotation); } else if (0 == strcmp(key, "s")) { parseProperty(obj->mScale); } else if (0 == strcmp(key, "o")) { parseProperty(obj->mOpacity); } else if (0 == strcmp(key, "hd")) { objT->setHidden(GetBool()); } else if (0 == strcmp(key, "rx")) { if (!obj->mExtra) return nullptr; parseProperty(obj->mExtra->m3DRx); } else if (0 == strcmp(key, "ry")) { if (!obj->mExtra) return nullptr; parseProperty(obj->mExtra->m3DRy); } else if (0 == strcmp(key, "rz")) { if (!obj->mExtra) return nullptr; parseProperty(obj->mExtra->m3DRz); } else { Skip(key); } } bool isStatic = obj->mAnchor.isStatic() && obj->mPosition.isStatic() && obj->mRotation.isStatic() && obj->mScale.isStatic() && obj->mOpacity.isStatic(); if (obj->mExtra) { isStatic = isStatic && obj->mExtra->m3DRx.isStatic() && obj->mExtra->m3DRy.isStatic() && obj->mExtra->m3DRz.isStatic() && obj->mExtra->mSeparateX.isStatic() && obj->mExtra->mSeparateY.isStatic(); } objT->set(obj, isStatic); return objT; } /* * https://github.com/airbnb/lottie-web/blob/master/docs/json/shapes/fill.json */ model::Fill *LottieParserImpl::parseFillObject() { auto obj = allocator().make(); while (const char *key = NextObjectKey()) { if (0 == strcmp(key, "nm")) { obj->setName(GetString()); } else if (0 == strcmp(key, "c")) { parseProperty(obj->mColor); } else if (0 == strcmp(key, "o")) { parseProperty(obj->mOpacity); } else if (0 == strcmp(key, "fillEnabled")) { obj->mEnabled = GetBool(); } else if (0 == strcmp(key, "r")) { obj->mFillRule = getFillRule(); } else if (0 == strcmp(key, "hd")) { obj->setHidden(GetBool()); } else { #ifdef DEBUG_PARSER vWarning << "Fill property skipped = " << key; #endif Skip(key); } } obj->setStatic(obj->mColor.isStatic() && obj->mOpacity.isStatic()); return obj; } /* * https://github.com/airbnb/lottie-web/blob/master/docs/json/helpers/lineCap.json */ CapStyle LottieParserImpl::getLineCap() { switch (GetInt()) { case 1: return CapStyle::Flat; break; case 2: return CapStyle::Round; break; default: return CapStyle::Square; break; } } FillRule LottieParserImpl::getFillRule() { switch (GetInt()) { case 1: return FillRule::Winding; break; case 2: return FillRule::EvenOdd; break; default: return FillRule::Winding; break; } } /* * https://github.com/airbnb/lottie-web/blob/master/docs/json/helpers/lineJoin.json */ JoinStyle LottieParserImpl::getLineJoin() { switch (GetInt()) { case 1: return JoinStyle::Miter; break; case 2: return JoinStyle::Round; break; default: return JoinStyle::Bevel; break; } } /* * https://github.com/airbnb/lottie-web/blob/master/docs/json/shapes/stroke.json */ model::Stroke *LottieParserImpl::parseStrokeObject() { auto obj = allocator().make(); while (const char *key = NextObjectKey()) { if (0 == strcmp(key, "nm")) { obj->setName(GetString()); } else if (0 == strcmp(key, "c")) { parseProperty(obj->mColor); } else if (0 == strcmp(key, "o")) { parseProperty(obj->mOpacity); } else if (0 == strcmp(key, "w")) { parseProperty(obj->mWidth); } else if (0 == strcmp(key, "fillEnabled")) { obj->mEnabled = GetBool(); } else if (0 == strcmp(key, "lc")) { obj->mCapStyle = getLineCap(); } else if (0 == strcmp(key, "lj")) { obj->mJoinStyle = getLineJoin(); } else if (0 == strcmp(key, "ml")) { obj->mMiterLimit = GetDouble(); } else if (0 == strcmp(key, "d")) { parseDashProperty(obj->mDash); } else if (0 == strcmp(key, "hd")) { obj->setHidden(GetBool()); } else { #ifdef DEBUG_PARSER vWarning << "Stroke property skipped = " << key; #endif Skip(key); } } obj->setStatic(obj->mColor.isStatic() && obj->mOpacity.isStatic() && obj->mWidth.isStatic() && obj->mDash.isStatic()); return obj; } void LottieParserImpl::parseGradientProperty(model::Gradient *obj, const char * key) { if (0 == strcmp(key, "t")) { obj->mGradientType = GetInt(); } else if (0 == strcmp(key, "o")) { parseProperty(obj->mOpacity); } else if (0 == strcmp(key, "s")) { parseProperty(obj->mStartPoint); } else if (0 == strcmp(key, "e")) { parseProperty(obj->mEndPoint); } else if (0 == strcmp(key, "h")) { parseProperty(obj->mHighlightLength); } else if (0 == strcmp(key, "a")) { parseProperty(obj->mHighlightAngle); } else if (0 == strcmp(key, "g")) { EnterObject(); while (const char *key = NextObjectKey()) { if (0 == strcmp(key, "k")) { parseProperty(obj->mGradient); } else if (0 == strcmp(key, "p")) { obj->mColorPoints = GetInt(); } else { Skip(nullptr); } } } else if (0 == strcmp(key, "hd")) { obj->setHidden(GetBool()); } else { #ifdef DEBUG_PARSER vWarning << "Gradient property skipped = " << key; #endif Skip(key); } obj->setStatic( obj->mOpacity.isStatic() && obj->mStartPoint.isStatic() && obj->mEndPoint.isStatic() && obj->mHighlightAngle.isStatic() && obj->mHighlightLength.isStatic() && obj->mGradient.isStatic()); } /* * https://github.com/airbnb/lottie-web/blob/master/docs/json/shapes/gfill.json */ model::GradientFill *LottieParserImpl::parseGFillObject() { auto obj = allocator().make(); while (const char *key = NextObjectKey()) { if (0 == strcmp(key, "nm")) { obj->setName(GetString()); } else if (0 == strcmp(key, "r")) { obj->mFillRule = getFillRule(); } else { parseGradientProperty(obj, key); } } return obj; } void LottieParserImpl::parseDashProperty(model::Dash &dash) { EnterArray(); while (NextArrayValue()) { EnterObject(); while (const char *key = NextObjectKey()) { if (0 == strcmp(key, "v")) { dash.mData.emplace_back(); parseProperty(dash.mData.back()); } else { Skip(key); } } } } /* * https://github.com/airbnb/lottie-web/blob/master/docs/json/shapes/gstroke.json */ model::GradientStroke *LottieParserImpl::parseGStrokeObject() { auto obj = allocator().make(); while (const char *key = NextObjectKey()) { if (0 == strcmp(key, "nm")) { obj->setName(GetString()); } else if (0 == strcmp(key, "w")) { parseProperty(obj->mWidth); } else if (0 == strcmp(key, "lc")) { obj->mCapStyle = getLineCap(); } else if (0 == strcmp(key, "lj")) { obj->mJoinStyle = getLineJoin(); } else if (0 == strcmp(key, "ml")) { obj->mMiterLimit = GetDouble(); } else if (0 == strcmp(key, "d")) { parseDashProperty(obj->mDash); } else { parseGradientProperty(obj, key); } } obj->setStatic(obj->isStatic() && obj->mWidth.isStatic() && obj->mDash.isStatic()); return obj; } void LottieParserImpl::getValue(std::vector &v) { EnterArray(); while (NextArrayValue()) { EnterArray(); VPointF pt; getValue(pt); v.push_back(pt); } } void LottieParserImpl::getValue(VPointF &pt) { float val[4] = {0.f}; int i = 0; if (PeekType() == kArrayType) EnterArray(); while (NextArrayValue()) { const auto value = GetDouble(); if (i < 4) { val[i++] = value; } } pt.setX(val[0]); pt.setY(val[1]); } void LottieParserImpl::getValue(float &val) { if (PeekType() == kArrayType) { EnterArray(); if (NextArrayValue()) val = GetDouble(); // discard rest while (NextArrayValue()) { GetDouble(); } } else if (PeekType() == kNumberType) { val = GetDouble(); } else { Error(); } } void LottieParserImpl::getValue(model::Color &color) { float val[4] = {0.f}; int i = 0; if (PeekType() == kArrayType) EnterArray(); while (NextArrayValue()) { const auto value = GetDouble(); if (i < 4) { val[i++] = value; } } if (mColorFilter) mColorFilter(val[0], val[1], val[2]); color.r = val[0]; color.g = val[1]; color.b = val[2]; } void LottieParserImpl::getValue(model::Gradient::Data &grad) { if (PeekType() == kArrayType) EnterArray(); while (NextArrayValue()) { grad.mGradient.push_back(GetDouble()); } } void LottieParserImpl::getValue(int &val) { if (PeekType() == kArrayType) { EnterArray(); while (NextArrayValue()) { val = GetInt(); } } else if (PeekType() == kNumberType) { val = GetInt(); } else { Error(); } } void LottieParserImpl::parsePathInfo() { mPathInfo.reset(); /* * The shape object could be wrapped by a array * if its part of the keyframe object */ bool arrayWrapper = (PeekType() == kArrayType); if (arrayWrapper) EnterArray(); EnterObject(); while (const char *key = NextObjectKey()) { if (0 == strcmp(key, "i")) { getValue(mPathInfo.mInPoint); } else if (0 == strcmp(key, "o")) { getValue(mPathInfo.mOutPoint); } else if (0 == strcmp(key, "v")) { getValue(mPathInfo.mVertices); } else if (0 == strcmp(key, "c")) { mPathInfo.mClosed = GetBool(); } else { Error(); Skip(nullptr); } } // exit properly from the array if (arrayWrapper) NextArrayValue(); mPathInfo.convert(); } void LottieParserImpl::getValue(model::PathData &obj) { parsePathInfo(); obj.mPoints = mPathInfo.mResult; obj.mClosed = mPathInfo.mClosed; } VPointF LottieParserImpl::parseInperpolatorPoint() { VPointF cp; EnterObject(); while (const char *key = NextObjectKey()) { if (0 == strcmp(key, "x")) { getValue(cp.rx()); } if (0 == strcmp(key, "y")) { getValue(cp.ry()); } } return cp; } template bool LottieParserImpl::parseKeyFrameValue( const char *key, model::Value &value) { if (0 == strcmp(key, "ti")) { value.hasTangent_ = true; getValue(value.inTangent_); } else if (0 == strcmp(key, "to")) { value.hasTangent_ = true; getValue(value.outTangent_); } else { return false; } return true; } VInterpolator *LottieParserImpl::interpolator(VPointF inTangent, VPointF outTangent, std::string key) { if (key.empty()) { std::array temp; snprintf(temp.data(), temp.size(), "%.2f_%.2f_%.2f_%.2f", inTangent.x(), inTangent.y(), outTangent.x(), outTangent.y()); key = temp.data(); } auto search = mInterpolatorCache.find(key); if (search != mInterpolatorCache.end()) { return search->second; } auto obj = allocator().make(outTangent, inTangent); mInterpolatorCache[std::move(key)] = obj; return obj; } /* * https://github.com/airbnb/lottie-web/blob/master/docs/json/properties/multiDimensionalKeyframed.json */ template void LottieParserImpl::parseKeyFrame(model::KeyFrames &obj) { struct ParsedField { std::string interpolatorKey; bool interpolator{false}; bool value{false}; bool hold{false}; bool noEndValue{true}; }; EnterObject(); ParsedField parsed; typename model::KeyFrames::Frame keyframe; VPointF inTangent; VPointF outTangent; while (const char *key = NextObjectKey()) { if (0 == strcmp(key, "i")) { parsed.interpolator = true; inTangent = parseInperpolatorPoint(); } else if (0 == strcmp(key, "o")) { outTangent = parseInperpolatorPoint(); } else if (0 == strcmp(key, "t")) { keyframe.start_ = GetDouble(); } else if (0 == strcmp(key, "s")) { parsed.value = true; getValue(keyframe.value_.start_); continue; } else if (0 == strcmp(key, "e")) { parsed.noEndValue = false; getValue(keyframe.value_.end_); continue; } else if (0 == strcmp(key, "n")) { if (PeekType() == kStringType) { parsed.interpolatorKey = GetStringObject(); } else { EnterArray(); while (NextArrayValue()) { if (parsed.interpolatorKey.empty()) { parsed.interpolatorKey = GetStringObject(); } else { // skip rest of the string Skip(nullptr); } } } continue; } else if (parseKeyFrameValue(key, keyframe.value_)) { continue; } else if (0 == strcmp(key, "h")) { parsed.hold = GetInt(); continue; } else { #ifdef DEBUG_PARSER vDebug << "key frame property skipped = " << key; #endif Skip(key); } } auto &list = obj.frames_; if (!list.empty()) { // update the endFrame value of current keyframe list.back().end_ = keyframe.start_; // if no end value provided, copy start value to previous frame if (parsed.value && parsed.noEndValue) { list.back().value_.end_ = keyframe.value_.start_; } } if (parsed.hold) { keyframe.value_.end_ = keyframe.value_.start_; keyframe.end_ = keyframe.start_; list.push_back(std::move(keyframe)); } else if (parsed.interpolator) { keyframe.interpolator_ = interpolator( inTangent, outTangent, std::move(parsed.interpolatorKey)); list.push_back(std::move(keyframe)); } else { // its the last frame discard. } } /* * https://github.com/airbnb/lottie-web/blob/master/docs/json/properties/shapeKeyframed.json */ /* * https://github.com/airbnb/lottie-web/blob/master/docs/json/properties/shape.json */ void LottieParserImpl::parseShapeProperty(model::Property &obj) { EnterObject(); while (const char *key = NextObjectKey()) { if (0 == strcmp(key, "k")) { if (PeekType() == kArrayType) { EnterArray(); while (NextArrayValue()) { parseKeyFrame(obj.animation()); } } else { if (!obj.isStatic()) { st_ = kError; return; } getValue(obj.value()); } } else { #ifdef DEBUG_PARSER vDebug << "shape property ignored = " << key; #endif Skip(nullptr); } } obj.cache(); } template void LottieParserImpl::parsePropertyHelper(model::Property &obj) { if (PeekType() == kNumberType) { if (!obj.isStatic()) { st_ = kError; return; } /*single value property with no animation*/ getValue(obj.value()); } else { EnterArray(); while (NextArrayValue()) { /* property with keyframe info*/ if (PeekType() == kObjectType) { parseKeyFrame(obj.animation()); } else { /* Read before modifying. * as there is no way of knowing if the * value of the array is either array of numbers * or array of object without entering the array * thats why this hack is there */ if (!obj.isStatic()) { st_ = kError; return; } /*multi value property with no animation*/ getValue(obj.value()); /*break here as we already reached end of array*/ break; } } obj.cache(); } } /* * https://github.com/airbnb/lottie-web/tree/master/docs/json/properties */ template void LottieParserImpl::parseProperty(model::Property &obj) { EnterObject(); while (const char *key = NextObjectKey()) { if (0 == strcmp(key, "k")) { parsePropertyHelper(obj); } else { Skip(key); } } } #ifdef LOTTIE_DUMP_TREE_SUPPORT class ObjectInspector { public: void visit(model::Composition *obj, std::string level) { vDebug << " { " << level << "Composition:: a: " << !obj->isStatic() << ", v: " << obj->mVersion << ", stFm: " << obj->startFrame() << ", endFm: " << obj->endFrame() << ", W: " << obj->size().width() << ", H: " << obj->size().height() << "\n"; level.append("\t"); visit(obj->mRootLayer, level); level.erase(level.end() - 1, level.end()); vDebug << " } " << level << "Composition End\n"; } void visit(model::Layer *obj, std::string level) { vDebug << level << "{ " << layerType(obj->mLayerType) << ", name: " << obj->name() << ", id:" << obj->mId << " Pid:" << obj->mParentId << ", a:" << !obj->isStatic() << ", " << matteType(obj->mMatteType) << ", mask:" << obj->hasMask() << ", inFm:" << obj->mInFrame << ", outFm:" << obj->mOutFrame << ", stFm:" << obj->mStartFrame << ", ts:" << obj->mTimeStreatch << ", ao:" << obj->autoOrient() << ", W:" << obj->layerSize().width() << ", H:" << obj->layerSize().height(); if (obj->mLayerType == model::Layer::Type::Image) vDebug << level << "\t{ " << "ImageInfo:" << " W :" << obj->extra()->mAsset->mWidth << ", H :" << obj->extra()->mAsset->mHeight << " }" << "\n"; else { vDebug << level; } visitChildren(static_cast(obj), level); vDebug << level << "} " << layerType(obj->mLayerType).c_str() << ", id: " << obj->mId << "\n"; } void visitChildren(model::Group *obj, std::string level) { level.append("\t"); for (const auto &child : obj->mChildren) visit(child, level); if (obj->mTransform) visit(obj->mTransform, level); } void visit(model::Object *obj, std::string level) { switch (obj->type()) { case model::Object::Type::Repeater: { auto r = static_cast(obj); vDebug << level << "{ Repeater: name: " << obj->name() << " , a:" << !obj->isStatic() << ", copies:" << r->maxCopies() << ", offset:" << r->offset(0); visitChildren(r->mContent, level); vDebug << level << "} Repeater"; break; } case model::Object::Type::Group: { vDebug << level << "{ Group: name: " << obj->name() << " , a:" << !obj->isStatic(); visitChildren(static_cast(obj), level); vDebug << level << "} Group"; break; } case model::Object::Type::Layer: { visit(static_cast(obj), level); break; } case model::Object::Type::Trim: { vDebug << level << "{ Trim: name: " << obj->name() << " , a:" << !obj->isStatic() << " }"; break; } case model::Object::Type::Rect: { vDebug << level << "{ Rect: name: " << obj->name() << " , a:" << !obj->isStatic() << " }"; break; } case model::Object::Type::RoundedCorner: { vDebug << level << "{ RoundedCorner: name: " << obj->name() << " , a:" << !obj->isStatic() << " }"; break; } case model::Object::Type::Ellipse: { vDebug << level << "{ Ellipse: name: " << obj->name() << " , a:" << !obj->isStatic() << " }"; break; } case model::Object::Type::Path: { vDebug << level << "{ Shape: name: " << obj->name() << " , a:" << !obj->isStatic() << " }"; break; } case model::Object::Type::Polystar: { vDebug << level << "{ Polystar: name: " << obj->name() << " , a:" << !obj->isStatic() << " }"; break; } case model::Object::Type::Transform: { vDebug << level << "{ Transform: name: " << obj->name() << " , a: " << !obj->isStatic() << " }"; break; } case model::Object::Type::Stroke: { vDebug << level << "{ Stroke: name: " << obj->name() << " , a:" << !obj->isStatic() << " }"; break; } case model::Object::Type::GStroke: { vDebug << level << "{ GStroke: name: " << obj->name() << " , a:" << !obj->isStatic() << " }"; break; } case model::Object::Type::Fill: { vDebug << level << "{ Fill: name: " << obj->name() << " , a:" << !obj->isStatic() << " }"; break; } case model::Object::Type::GFill: { auto f = static_cast(obj); vDebug << level << "{ GFill: name: " << obj->name() << " , a:" << !f->isStatic() << ", ty:" << f->mGradientType << ", s:" << f->mStartPoint.value(0) << ", e:" << f->mEndPoint.value(0) << " }"; break; } default: break; } } std::string matteType(model::MatteType type) { switch (type) { case model::MatteType::None: return "Matte::None"; break; case model::MatteType::Alpha: return "Matte::Alpha"; break; case model::MatteType::AlphaInv: return "Matte::AlphaInv"; break; case model::MatteType::Luma: return "Matte::Luma"; break; case model::MatteType::LumaInv: return "Matte::LumaInv"; break; default: return "Matte::Unknown"; break; } } std::string layerType(model::Layer::Type type) { switch (type) { case model::Layer::Type::Precomp: return "Layer::Precomp"; break; case model::Layer::Type::Null: return "Layer::Null"; break; case model::Layer::Type::Shape: return "Layer::Shape"; break; case model::Layer::Type::Solid: return "Layer::Solid"; break; case model::Layer::Type::Image: return "Layer::Image"; break; case model::Layer::Type::Text: return "Layer::Text"; break; default: return "Layer::Unknown"; break; } } }; #endif static char* uncompressZip(const char * str, size_t length) { const char* errMsg = "Failed to unzip dotLottie!"; auto zip = zip_stream_open(str, length, 0, 'r'); if (!zip) { vCritical << errMsg; return nullptr; } //Read a representive animation if (zip_entry_openbyindex(zip, 1)) { vCritical << errMsg; return nullptr; } char* buf = nullptr; size_t bufSize; zip_entry_read(zip, (void**)&buf, &bufSize); zip_entry_close(zip); zip_stream_close(zip); return buf; } static bool checkDotLottie(const char * str) { //check the .Lottie signature. if (str[0] == 0x50 && str[1] == 0x4B && str[2] == 0x03 && str[3] == 0x04) return true; else return false; } std::shared_ptr model::parse(char * str, size_t length, std::string dir_path, model::ColorFilter filter) { auto input = str; auto dotLottie = checkDotLottie(str); if (dotLottie) { input = uncompressZip(str, length); } LottieParserImpl obj(input, std::move(dir_path), std::move(filter)); if (dotLottie) free(input); if (obj.VerifyType()) { obj.parseComposition(); auto composition = obj.composition(); if (composition) { composition->processRepeaterObjects(); composition->updateStats(); #ifdef LOTTIE_DUMP_TREE_SUPPORT ObjectInspector inspector; inspector.visit(composition.get(), ""); #endif return composition; } } vWarning << "Input data is not Lottie format!"; return {}; } RAPIDJSON_DIAG_POP