diff options
author | Benau <Benau@users.noreply.github.com> | 2021-08-25 04:32:50 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-08-24 22:32:50 +0200 |
commit | 53cafa9f3d0c8be33821fc7338b1da97e91d9cc6 (patch) | |
tree | 964a225219099a1a1c282e27913767da588191b4 /vendor/github.com/Benau/go_rlottie/lottie_lottieparser.cpp | |
parent | d4195deb3a6305c49c50ff30e8af978c7f1bdd92 (diff) | |
download | matterbridge-msglm-53cafa9f3d0c8be33821fc7338b1da97e91d9cc6.tar.gz matterbridge-msglm-53cafa9f3d0c8be33821fc7338b1da97e91d9cc6.tar.bz2 matterbridge-msglm-53cafa9f3d0c8be33821fc7338b1da97e91d9cc6.zip |
Convert .tgs with go libraries (and cgo) (telegram) (#1569)
This commit adds support for go/cgo tgs conversion when building with the -tags `cgo`
The default binaries are still "pure" go and uses the old way of converting.
* Move lottie_convert.py conversion code to its own file
* Add optional libtgsconverter
* Update vendor
* Apply suggestions from code review
* Update bridge/helper/libtgsconverter.go
Co-authored-by: Wim <wim@42.be>
Diffstat (limited to 'vendor/github.com/Benau/go_rlottie/lottie_lottieparser.cpp')
-rw-r--r-- | vendor/github.com/Benau/go_rlottie/lottie_lottieparser.cpp | 2390 |
1 files changed, 2390 insertions, 0 deletions
diff --git a/vendor/github.com/Benau/go_rlottie/lottie_lottieparser.cpp b/vendor/github.com/Benau/go_rlottie/lottie_lottieparser.cpp new file mode 100644 index 00000000..91839d41 --- /dev/null +++ b/vendor/github.com/Benau/go_rlottie/lottie_lottieparser.cpp @@ -0,0 +1,2390 @@ +/* + * 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 <array> + +#include "lottie_lottiemodel.h" +#include "lottie_rapidjson_document.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<model::Composition> 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<VPointF> &v); + void getValue(model::Repeater::Transform &); + + template <typename T, typename Tag> + bool parseKeyFrameValue(const char *, model::Value<T, Tag> &) + { + return false; + } + + template <typename T> + bool parseKeyFrameValue(const char * key, + model::Value<T, model::Position> &value); + template <typename T, typename Tag> + void parseKeyFrame(model::KeyFrames<T, Tag> &obj); + template <typename T> + void parseProperty(model::Property<T> &obj); + template <typename T, typename Tag> + void parsePropertyHelper(model::Property<T, Tag> &obj); + + void parseShapeProperty(model::Property<model::PathData> &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<VPointF> mInPoint; /* "i" */ + std::vector<VPointF> mOutPoint; /* "o" */ + std::vector<VPointF> mVertices; /* "v" */ + std::vector<VPointF> 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<std::string, VInterpolator *> mInterpolatorCache; + std::shared_ptr<model::Composition> mComposition; + model::Composition * compRef{nullptr}; + model::Layer * curLayerRef{nullptr}; + std::vector<model::Layer *> 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<parseFlags>(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<model::Composition> sharedComposition = + std::make_shared<model::Composition>(); + 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 = GetDouble(); + } else if (0 == strcmp(key, "op")) { + comp->mEndFrame = 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<const unsigned char *>(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 <sstream> +template <class T> +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<model::Asset>(); + 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<model::Layer>(); + 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<model::Layer>(); + 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<model::Mask>(); + + 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 (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<model::RoundedCorner *>(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<model::Rect *>(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<model::Group *>(e), rc); + } + } +} + +model::Object *LottieParserImpl::parseGroupObject() +{ + auto group = allocator().make<model::Group>(); + + 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<model::Transform *>(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<model::Rect>(); + + 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<model::RoundedCorner>(); + + 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<model::Ellipse>(); + + 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<model::Path>(); + + 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<model::Polystar>(); + + 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<model::Trim>(); + + 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<model::Repeater>(); + + obj->setContent(allocator().make<model::Group>()); + + 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<model::Transform>(); + + auto obj = allocator().make<model::Transform::Data>(); + 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<model::Fill>(); + + 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<model::Stroke>(); + + 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<model::GradientFill>(); + + 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<model::GradientStroke>(); + + 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<VPointF> &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 <typename T> +bool LottieParserImpl::parseKeyFrameValue( + const char *key, model::Value<T, model::Position> &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<char, 20> 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<VInterpolator>(outTangent, inTangent); + mInterpolatorCache[std::move(key)] = obj; + return obj; +} + +/* + * https://github.com/airbnb/lottie-web/blob/master/docs/json/properties/multiDimensionalKeyframed.json + */ +template <typename T, typename Tag> +void LottieParserImpl::parseKeyFrame(model::KeyFrames<T, Tag> &obj) +{ + struct ParsedField { + std::string interpolatorKey; + bool interpolator{false}; + bool value{false}; + bool hold{false}; + bool noEndValue{true}; + }; + + EnterObject(); + ParsedField parsed; + typename model::KeyFrames<T, Tag>::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<model::PathData> &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 <typename T, typename Tag> +void LottieParserImpl::parsePropertyHelper(model::Property<T, Tag> &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 <typename T> +void LottieParserImpl::parseProperty(model::Property<T> &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<model::Group *>(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<model::Repeater *>(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<model::Group *>(obj), level); + vDebug << level << "} Group"; + break; + } + case model::Object::Type::Layer: { + visit(static_cast<model::Layer *>(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<model::GradientFill *>(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 + +std::shared_ptr<model::Composition> model::parse(char * str, + std::string dir_path, + model::ColorFilter filter) +{ + LottieParserImpl obj(str, std::move(dir_path), std::move(filter)); + + 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 |