lottie: Newly added support for the text feature.

This enhancement enables embedded glyphs rendering.
The 'fonts' and 'chars' properties are now supported.
This commit is contained in:
Hermet Park 2024-01-05 10:54:56 +09:00
parent 5a7de430e0
commit 388631be68
12 changed files with 399 additions and 17 deletions

View file

@ -256,8 +256,7 @@ ThorVG aims to fully support Lottie Animation features. Lottie is a JSON-based v
Currently, ThorVG provides experimental support for Lottie Animation, and while most features are supported, a few advanced properties of Lottie may not be available yet:
<br />
- Texts
- Shape Modifiers (Pucker/Bloat, Twist, Merge, ZigZag)
- Shape Modifiers
- Layer Effects
- Expressions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -418,7 +418,7 @@ static void _updateEllipse(LottieGroup* parent, LottieObject** child, float fram
static void _updatePath(LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist<RenderContext>& contexts, RenderContext* ctx)
{
auto path= static_cast<LottiePath*>(*child);
auto path = static_cast<LottiePath*>(*child);
if (ctx->repeater) {
auto p = Shape::gen();
@ -439,6 +439,77 @@ static void _updatePath(LottieGroup* parent, LottieObject** child, float frameNo
}
static void _updateText(LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist<RenderContext>& contexts, TVG_UNUSED RenderContext* ctx)
{
auto text = static_cast<LottieText*>(*child);
auto& doc = text->doc(frameNo);
auto p = doc.text;
if (!p || !text->font) return;
auto scale = doc.size * 0.01f;
float spacing = text->spacing(frameNo) / scale;
Point cursor = {0.0f, 0.0f};
auto scene = Scene::gen();
//text string
while (*p != '\0') {
//find the glyph
for (auto g = text->font->chars.data; g < text->font->chars.end(); ++g) {
auto glyph = *g;
//draw matched glyphs
if (!strncmp(glyph->code, p, glyph->len)) {
//TODO: caching?
auto shape = Shape::gen();
for (auto g = glyph->children.data; g < glyph->children.end(); ++g) {
auto group = static_cast<LottieGroup*>(*g);
for (auto p = group->children.data; p < group->children.end(); ++p) {
if (static_cast<LottiePath*>(*p)->pathset(frameNo, P(shape)->rs.path.cmds, P(shape)->rs.path.pts)) {
P(shape)->update(RenderUpdateFlag::Path);
}
}
}
shape->fill(doc.color.rgb[0], doc.color.rgb[1], doc.color.rgb[2]);
shape->translate(cursor.x, cursor.y);
if (doc.stroke.render) {
shape->stroke(StrokeJoin::Round);
shape->stroke(doc.stroke.width / scale);
shape->stroke(doc.stroke.color.rgb[0], doc.stroke.color.rgb[1], doc.stroke.color.rgb[2]);
}
scene->push(std::move(shape));
p += glyph->len;
//end of text, new line of the cursor position
if (*p == 13 || *p == 3) {
cursor.x = 0.0f;
cursor.y += (doc.height / scale);
++p;
//advance the cursor position horizontally
} else {
cursor.x += glyph->width + spacing;
}
break;
}
}
}
//text layout position
Point layout = {doc.bbox.pos.x, (doc.bbox.pos.y * 0.5f) - doc.shift};
//adjust the layout
if (doc.justify == 1) layout.x -= (cursor.x * scale); //right aligned
else if (doc.justify == 2) layout.x -= (cursor.x * 0.5f * scale); //center aligned
scene->translate(layout.x, layout.y);
scene->scale(scale);
parent->scene->push(std::move(scene));
}
static void _updateStar(LottieGroup* parent, LottiePolyStar* star, Matrix* transform, float frameNo, Shape* merging)
{
static constexpr auto POLYSTAR_MAGIC_NUMBER = 0.47829f / 0.28f;
@ -790,6 +861,10 @@ static void _updateChildren(LottieGroup* parent, float frameNo, Inlist<RenderCon
_updateTrimpath(parent, child, frameNo, contexts, ctx);
break;
}
case LottieObject::Text: {
_updateText(parent, child, frameNo, contexts, ctx);
break;
}
case LottieObject::Repeater: {
_updateRepeater(parent, child, frameNo, contexts, ctx);
break;
@ -1028,6 +1103,26 @@ static void _checkFragment(LottieGroup* parent)
}
static void _attachFont(LottieComposition* comp, LottieLayer* parent)
{
//TODO: Consider to migrate this attachment to the frame update time.
for (auto c = parent->children.data; c < parent->children.end(); ++c) {
auto text = static_cast<LottieText*>(*c);
auto& doc = text->doc(0);
if (!doc.name) continue;
auto len = strlen(doc.name);
for (uint32_t i = 0; i < comp->fonts.count; ++i) {
auto font = comp->fonts[i];
auto len2 = strlen(font->name);
if (!strncmp(font->name, doc.name, len < len2 ? len : len2)) {
text->font = font;
break;
}
}
}
}
static bool _buildComposition(LottieComposition* comp, LottieGroup* parent)
{
if (parent->children.count == 0) return false;
@ -1049,6 +1144,9 @@ static bool _buildComposition(LottieComposition* comp, LottieGroup* parent)
_checkFragment(static_cast<LottieGroup*>(*c));
//attach the necessary font data
if (child->type == LottieLayer::Text) _attachFont(comp, child);
child->statical &= parent->statical;
parent->statical &= child->statical;
}

View file

@ -203,4 +203,9 @@ LottieComposition::~LottieComposition()
for (auto a = assets.data; a < assets.end(); ++a) {
delete(*a);
}
//delete fonts
for (auto f = fonts.data; f < fonts.end(); ++f) {
delete(*f);
}
}

View file

@ -219,6 +219,7 @@ struct LottieObject
Polystar,
Image,
Trimpath,
Text,
Repeater,
RoundedCorner
};
@ -235,6 +236,61 @@ struct LottieObject
};
struct LottieGlyph
{
Array<LottieObject*> children; //glyph shapes.
float width;
char* code;
uint16_t size;
uint8_t len;
void prepare()
{
len = strlen(code);
}
~LottieGlyph()
{
for (auto p = children.data; p < children.end(); ++p) delete(*p);
free(code);
}
};
struct LottieFont
{
enum Origin : uint8_t { Local = 0, CssURL, ScriptURL, FontURL, Embedded };
~LottieFont()
{
for (auto c = chars.data; c < chars.end(); ++c) delete(*c);
free(style);
free(family);
free(name);
}
Array<LottieGlyph*> chars;
char* name = nullptr;
char* family = nullptr;
char* style = nullptr;
float ascent = 0.0f;
Origin origin = Embedded;
};
struct LottieText : LottieObject
{
void prepare()
{
LottieObject::type = LottieObject::Text;
}
LottieTextDoc doc;
LottieFont* font;
LottieFloat spacing = 0.0f; //letter spacing
};
struct LottieTrimpath : LottieObject
{
enum Type : uint8_t { Individual = 1, Simultaneous = 2 };
@ -561,6 +617,7 @@ struct LottieComposition
float frameRate;
Array<LottieObject*> assets;
Array<LottieInterpolator*> interpolators;
Array<LottieFont*> fonts;
bool initiated = false;
};

View file

@ -153,6 +153,28 @@ StrokeJoin LottieParser::getStrokeJoin()
}
void LottieParser::getValue(TextDocument& doc)
{
enterObject();
while (auto key = nextObjectKey()) {
if (!strcmp(key, "s")) doc.size = getFloat();
else if (!strcmp(key, "f")) doc.name = getStringCopy();
else if (!strcmp(key, "t")) doc.text = getStringCopy();
else if (!strcmp(key, "j")) doc.justify = getInt();
else if (!strcmp(key, "tr")) doc.tracking = getInt();
else if (!strcmp(key, "lh")) doc.height = getFloat();
else if (!strcmp(key, "ls")) doc.shift = getFloat();
else if (!strcmp(key, "fc")) getValue(doc.color);
else if (!strcmp(key, "ps")) getValue(doc.bbox.pos);
else if (!strcmp(key, "sz")) getValue(doc.bbox.size);
else if (!strcmp(key, "sc")) getValue(doc.stroke.color);
else if (!strcmp(key, "sw")) doc.stroke.width = getFloat();
else if (!strcmp(key, "of")) doc.stroke.render = getBool();
else skip(key);
}
}
void LottieParser::getValue(PathSet& path)
{
Array<Point> outs, ins, pts;
@ -427,6 +449,8 @@ void LottieParser::parsePropertyInternal(T& prop)
getValue(prop.value);
//multi value property
} else {
//TODO: Here might be a single frame.
//Can we figure out the frame number in advance?
enterArray();
while (nextArrayValue()) {
//keyframes value
@ -816,14 +840,14 @@ LottieObject* LottieParser::parseObject()
}
void LottieParser::parseObject(LottieGroup* parent)
void LottieParser::parseObject(Array<LottieObject*>& parent)
{
enterObject();
while (auto key = nextObjectKey()) {
if (!strcmp(key, "ty")) {
if (auto child = parseObject()) {
if (child->hidden) delete(child);
else parent->children.push(child);
else parent.push(child);
}
} else skip(key);
}
@ -907,6 +931,24 @@ LottieObject* LottieParser::parseAsset()
}
LottieFont* LottieParser::parseFont()
{
enterObject();
auto font = new LottieFont;
while (auto key = nextObjectKey()) {
if (!strcmp(key, "fName")) font->name = getStringCopy();
else if (!strcmp(key, "fFamily")) font->family = getStringCopy();
else if (!strcmp(key, "fStyle")) font->style = getStringCopy();
else if (!strcmp(key, "ascent")) font->ascent = getFloat();
else if (!strcmp(key, "origin")) font->origin = (LottieFont::Origin) getInt();
else skip(key);
}
return font;
}
void LottieParser::parseAssets()
{
enterArray();
@ -918,6 +960,58 @@ void LottieParser::parseAssets()
}
void LottieParser::parseChars()
{
if (comp->fonts.count == 0) {
TVGERR("LOTTIE", "No font data?");
return;
}
enterArray();
while (nextArrayValue()) {
enterObject();
//a new glyph
auto glyph = new LottieGlyph;
char* style = nullptr;
char* family = nullptr;
while (auto key = nextObjectKey()) {
if (!strcmp("ch", key)) glyph->code = getStringCopy();
else if (!strcmp("size", key)) glyph->size = static_cast<uint16_t>(getFloat());
else if (!strcmp("style", key)) style = const_cast<char*>(getString());
else if (!strcmp("w", key)) glyph->width = getFloat();
else if (!strcmp("fFamily", key)) family = const_cast<char*>(getString());
else if (!strcmp("data", key))
{ //glyph shapes
enterObject();
while (auto key = nextObjectKey()) {
if (!strcmp(key, "shapes")) parseShapes(glyph->children);
}
} else skip(key);
}
//aggregate the font characters
for (uint32_t i = 0; i < comp->fonts.count; ++i) {
auto& font = comp->fonts[i];
if (!strcmp(font->family, family) && !strcmp(font->style, style)) font->chars.push(glyph);
else TVGERR("LOTTIE", "No font data?");
}
glyph->prepare();
}
}
void LottieParser::parseFonts()
{
enterObject();
while (auto key = nextObjectKey()) {
if (!strcmp("list", key)) {
enterArray();
while (nextArrayValue()) {
comp->fonts.push(parseFont());
}
} else skip(key);
}
}
LottieObject* LottieParser::parseGroup()
{
auto group = new LottieGroup;
@ -928,7 +1022,7 @@ LottieObject* LottieParser::parseGroup()
group->name = getStringCopy();
} else if (!strcmp(key, "it")) {
enterArray();
while (nextArrayValue()) parseObject(group);
while (nextArrayValue()) parseObject(group->children);
} else skip(key);
}
if (group->children.empty()) {
@ -947,7 +1041,7 @@ void LottieParser::parseTimeRemap(LottieLayer* layer)
}
void LottieParser::parseShapes(LottieLayer* layer)
void LottieParser::parseShapes(Array<LottieObject*>& parent)
{
enterArray();
while (nextArrayValue()) {
@ -955,11 +1049,11 @@ void LottieParser::parseShapes(LottieLayer* layer)
while (auto key = nextObjectKey()) {
if (!strcmp(key, "it")) {
enterArray();
while (nextArrayValue()) parseObject(layer);
while (nextArrayValue()) parseObject(parent);
} else if (!strcmp(key, "ty")) {
if (auto child = parseObject()) {
if (child->hidden) delete(child);
else layer->children.push(child);
else parent.push(child);
}
} else skip(key);
}
@ -967,6 +1061,43 @@ void LottieParser::parseShapes(LottieLayer* layer)
}
void LottieParser::parseTextRange(LottieText* text)
{
enterArray();
while (nextArrayValue()) {
enterObject();
while (auto key2 = nextObjectKey()) {
if (!strcmp(key2, "a")) { //text style
enterObject();
while (auto key3 = nextObjectKey()) {
if (!strcmp(key3, "t")) parseProperty(text->spacing);
else skip(key3);
}
} else skip(key2);
}
}
}
void LottieParser::parseText(Array<LottieObject*>& parent)
{
enterObject();
auto text = new LottieText;
while (auto key = nextObjectKey()) {
if (!strcmp(key, "d")) parseProperty(text->doc);
else if (!strcmp(key, "a")) parseTextRange(text);
//else if (!strcmp(key, "p")) TVGLOG("LOTTIE", "Text Follow Path (p) is not supported");
//else if (!strcmp(key, "m")) TVGLOG("LOTTIE", "Text Alignment Option (m) is not supported");
else skip(key);
}
text->prepare();
parent.push(text);
}
void LottieParser::getLayerSize(uint32_t& val)
{
if (val == 0) {
@ -1032,7 +1163,7 @@ LottieLayer* LottieParser::parseLayer()
layer->transform = parseTransform(ddd);
}
else if (!strcmp(key, "ao")) layer->autoOrient = getInt();
else if (!strcmp(key, "shapes")) parseShapes(layer);
else if (!strcmp(key, "shapes")) parseShapes(layer->children);
else if (!strcmp(key, "ip")) layer->inFrame = getFloat();
else if (!strcmp(key, "op")) layer->outFrame = getFloat();
else if (!strcmp(key, "st")) layer->startFrame = getFloat();
@ -1047,6 +1178,7 @@ LottieLayer* LottieParser::parseLayer()
else if (!strcmp(key, "hd")) layer->hidden = getBool();
else if (!strcmp(key, "refId")) layer->refId = getStringCopy();
else if (!strcmp(key, "td")) layer->matteSrc = getInt(); //used for matte layer
else if (!strcmp(key, "t")) parseText(layer->children);
else if (!strcmp(key, "ef"))
{
TVGERR("LOTTIE", "layer effect(ef) is not supported!");
@ -1126,6 +1258,8 @@ bool LottieParser::parse()
else if (!strcmp(key, "nm")) comp->name = getStringCopy();
else if (!strcmp(key, "assets")) parseAssets();
else if (!strcmp(key, "layers")) comp->root = parseLayers();
else if (!strcmp(key, "fonts")) parseFonts();
else if (!strcmp(key, "chars")) parseChars();
else skip(key);
}

View file

@ -51,6 +51,8 @@ private:
void getInperpolatorPoint(Point& pt);
void getPathSet(LottiePathSet& path);
void getLayerSize(uint32_t& val);
void getValue(TextDocument& doc);
void getValue(PathSet& path);
void getValue(Array<Point>& pts);
void getValue(ColorStop& color);
@ -58,7 +60,6 @@ private:
void getValue(uint8_t& val);
void getValue(Point& pt);
void getValue(RGB24& color);
void getLayerSize(uint32_t& val);
template<typename T> bool parseTangent(const char *key, LottieVectorFrame<T>& value);
template<typename T> bool parseTangent(const char *key, LottieScalarFrame<T>& value);
@ -85,14 +86,19 @@ private:
LottieMask* parseMask();
LottieTrimpath* parseTrimpath();
LottieRepeater* parseRepeater();
LottieFont* parseFont();
void parseObject(LottieGroup* parent);
void parseShapes(LottieLayer* layer);
void parseObject(Array<LottieObject*>& parent);
void parseShapes(Array<LottieObject*>& parent);
void parseText(Array<LottieObject*>& parent);
void parseMasks(LottieLayer* layer);
void parseTimeRemap(LottieLayer* layer);
void parseStrokeDash(LottieStroke* stroke);
void parseGradient(LottieGradient* gradient, const char* key);
void parseTextRange(LottieText* text);
void parseAssets();
void parseFonts();
void parseChars();
//Current parsing context
struct Context {

View file

@ -51,6 +51,30 @@ struct ColorStop
};
struct LottieFont;
struct TextDocument
{
char* text = nullptr;
float height;
float shift;
RGB24 color;
struct {
Point pos;
Point size;
} bbox;
struct {
RGB24 color;
float width;
bool render = false;
} stroke;
char* name = nullptr;
uint16_t size;
uint8_t justify;
uint8_t tracking;
};
static inline RGB24 operator-(const RGB24& lhs, const RGB24& rhs)
{
return {lhs.rgb[0] - rhs.rgb[0], lhs.rgb[1] - rhs.rgb[1], lhs.rgb[2] - rhs.rgb[2]};
@ -505,6 +529,66 @@ struct LottiePosition
};
struct LottieTextDoc
{
Array<LottieScalarFrame<TextDocument>>* frames = nullptr;
TextDocument value;
~LottieTextDoc()
{
free(value.text);
free(value.name);
if (!frames) return;
for (auto p = frames->data; p < frames->end(); ++p) {
free((*p).value.text);
free((*p).value.name);
}
delete(frames);
}
LottieScalarFrame<TextDocument>& newFrame()
{
if (!frames) frames = new Array<LottieScalarFrame<TextDocument>>;
if (frames->count + 1 >= frames->reserved) {
auto old = frames->reserved;
frames->grow(frames->count + 2);
memset((void*)(frames->data + old), 0x00, sizeof(LottieScalarFrame<TextDocument>) * (frames->reserved - old));
}
++frames->count;
return frames->last();
}
LottieScalarFrame<TextDocument>& nextFrame()
{
return (*frames)[frames->count];
}
TextDocument& operator()(float frameNo)
{
if (!frames) return value;
if (frames->count == 1 || frameNo <= frames->first().no) return frames->first().value;
if (frameNo >= frames->last().no) return frames->last().value;
uint32_t low = 0;
uint32_t high = frames->count - 1;
while (low <= high) {
auto mid = low + (high - low) / 2;
auto frame = frames->data + mid;
if (mathEqual(frameNo, frame->no)) return frame->value;
else if (frameNo < frame->no) high = mid - 1;
else low = mid + 1;
}
if (high < low) low = high;
return (*frames)[low].value;
}
void prepare() {}
};
using LottiePoint = LottieProperty<Point>;
using LottieFloat = LottieProperty<float>;
using LottieOpacity = LottieProperty<uint8_t>;

View file

@ -1108,8 +1108,7 @@ static bool _rasterTexmapPolygon(SwSurface* surface, const SwImage* image, const
float ys = FLT_MAX, ye = -1.0f;
for (int i = 0; i < 4; i++) {
mathMultiply(&vertices[i].pt, transform);
if (transform) mathMultiply(&vertices[i].pt, transform);
if (vertices[i].pt.y < ys) ys = vertices[i].pt.y;
if (vertices[i].pt.y > ye) ye = vertices[i].pt.y;
}