blob: 6acc3ec2a2188f66feb807f4cfe7c03eccda4400 [file] [log] [blame]
/*
* Copyright 2018 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "modules/skottie/src/SkottiePriv.h"
#include "include/core/SkData.h"
#include "include/core/SkFontMgr.h"
#include "include/core/SkTypes.h"
#include "modules/skottie/src/SkottieJson.h"
#include "modules/skottie/src/text/TextAdapter.h"
#include "modules/skottie/src/text/TextAnimator.h"
#include "modules/skottie/src/text/TextValue.h"
#include "modules/sksg/include/SkSGDraw.h"
#include "modules/sksg/include/SkSGGroup.h"
#include "modules/sksg/include/SkSGPaint.h"
#include "modules/sksg/include/SkSGText.h"
#include "src/core/SkMakeUnique.h"
#include <string.h>
namespace skottie {
namespace internal {
namespace {
SkFontStyle FontStyle(const AnimationBuilder* abuilder, const char* style) {
static constexpr struct {
const char* fName;
const SkFontStyle::Weight fWeight;
} gWeightMap[] = {
{ "Regular" , SkFontStyle::kNormal_Weight },
{ "Medium" , SkFontStyle::kMedium_Weight },
{ "Bold" , SkFontStyle::kBold_Weight },
{ "Light" , SkFontStyle::kLight_Weight },
{ "Black" , SkFontStyle::kBlack_Weight },
{ "Thin" , SkFontStyle::kThin_Weight },
{ "Extra" , SkFontStyle::kExtraBold_Weight },
{ "ExtraBold" , SkFontStyle::kExtraBold_Weight },
{ "ExtraLight", SkFontStyle::kExtraLight_Weight },
{ "ExtraBlack", SkFontStyle::kExtraBlack_Weight },
{ "SemiBold" , SkFontStyle::kSemiBold_Weight },
{ "Hairline" , SkFontStyle::kThin_Weight },
{ "Normal" , SkFontStyle::kNormal_Weight },
{ "Plain" , SkFontStyle::kNormal_Weight },
{ "Standard" , SkFontStyle::kNormal_Weight },
{ "Roman" , SkFontStyle::kNormal_Weight },
{ "Heavy" , SkFontStyle::kBlack_Weight },
{ "Demi" , SkFontStyle::kSemiBold_Weight },
{ "DemiBold" , SkFontStyle::kSemiBold_Weight },
{ "Ultra" , SkFontStyle::kExtraBold_Weight },
{ "UltraBold" , SkFontStyle::kExtraBold_Weight },
{ "UltraBlack", SkFontStyle::kExtraBlack_Weight },
{ "UltraHeavy", SkFontStyle::kExtraBlack_Weight },
{ "UltraLight", SkFontStyle::kExtraLight_Weight },
};
SkFontStyle::Weight weight = SkFontStyle::kNormal_Weight;
for (const auto& w : gWeightMap) {
const auto name_len = strlen(w.fName);
if (!strncmp(style, w.fName, name_len)) {
weight = w.fWeight;
style += name_len;
break;
}
}
static constexpr struct {
const char* fName;
const SkFontStyle::Slant fSlant;
} gSlantMap[] = {
{ "Italic" , SkFontStyle::kItalic_Slant },
{ "Oblique", SkFontStyle::kOblique_Slant },
};
SkFontStyle::Slant slant = SkFontStyle::kUpright_Slant;
if (*style != '\0') {
for (const auto& s : gSlantMap) {
if (!strcmp(style, s.fName)) {
slant = s.fSlant;
style += strlen(s.fName);
break;
}
}
}
if (*style != '\0') {
abuilder->log(Logger::Level::kWarning, nullptr, "Unknown font style: %s.", style);
}
return SkFontStyle(weight, SkFontStyle::kNormal_Width, slant);
}
} // namespace
bool AnimationBuilder::FontInfo::matches(const char family[], const char style[]) const {
return 0 == strcmp(fFamily.c_str(), family)
&& 0 == strcmp(fStyle.c_str(), style);
}
void AnimationBuilder::parseFonts(const skjson::ObjectValue* jfonts,
const skjson::ArrayValue* jchars) {
// Optional array of font entries, referenced (by name) from text layer document nodes. E.g.
// "fonts": {
// "list": [
// {
// "ascent": 75,
// "fClass": "",
// "fFamily": "Roboto",
// "fName": "Roboto-Regular",
// "fPath": "https://fonts.googleapis.com/css?family=Roboto",
// "fPath": "",
// "fStyle": "Regular",
// "fWeight": "",
// "origin": 1
// }
// ]
// },
if (jfonts) {
if (const skjson::ArrayValue* jlist = (*jfonts)["list"]) {
for (const skjson::ObjectValue* jfont : *jlist) {
if (!jfont) {
continue;
}
const skjson::StringValue* jname = (*jfont)["fName"];
const skjson::StringValue* jfamily = (*jfont)["fFamily"];
const skjson::StringValue* jstyle = (*jfont)["fStyle"];
const skjson::StringValue* jpath = (*jfont)["fPath"];
if (!jname || !jname->size() ||
!jfamily || !jfamily->size() ||
!jstyle || !jstyle->size()) {
this->log(Logger::Level::kError, jfont, "Invalid font.");
continue;
}
const auto& fmgr = fLazyFontMgr.get();
// Typeface fallback order:
// 1) externally-loaded font (provided by the embedder)
// 2) system font (family/style)
// 3) system default
sk_sp<SkTypeface> tf =
fmgr->makeFromData(fResourceProvider->loadFont(jname->begin(),
jpath ? jpath->begin()
: nullptr));
if (!tf) {
tf.reset(fmgr->matchFamilyStyle(jfamily->begin(),
FontStyle(this, jstyle->begin())));
}
if (!tf) {
this->log(Logger::Level::kError, nullptr,
"Could not create typeface for %s|%s.",
jfamily->begin(), jstyle->begin());
// Last resort.
tf = fmgr->legacyMakeTypeface(nullptr, FontStyle(this, jstyle->begin()));
if (!tf) {
continue;
}
}
fFonts.set(SkString(jname->begin(), jname->size()),
{
SkString(jfamily->begin(), jfamily->size()),
SkString(jstyle->begin(), jstyle->size()),
ParseDefault((*jfont)["ascent"] , 0.0f),
std::move(tf)
});
}
}
}
// Optional array of glyphs, to be associated with one of the declared fonts. E.g.
// "chars": [
// {
// "ch": "t",
// "data": {
// "shapes": [...]
// },
// "fFamily": "Roboto",
// "size": 50,
// "style": "Regular",
// "w": 32.67
// }
// ]
if (jchars) {
FontInfo* current_font = nullptr;
for (const skjson::ObjectValue* jchar : *jchars) {
if (!jchar) {
continue;
}
const skjson::StringValue* jch = (*jchar)["ch"];
if (!jch) {
continue;
}
const skjson::StringValue* jfamily = (*jchar)["fFamily"];
const skjson::StringValue* jstyle = (*jchar)["style"]; // "style", not "fStyle"...
const auto* ch_ptr = jch->begin();
const auto ch_len = jch->size();
if (!jfamily || !jstyle || (SkUTF::CountUTF8(ch_ptr, ch_len) != 1)) {
this->log(Logger::Level::kError, jchar, "Invalid glyph.");
continue;
}
const auto uni = SkUTF::NextUTF8(&ch_ptr, ch_ptr + ch_len);
SkASSERT(uni != -1);
const auto* family = jfamily->begin();
const auto* style = jstyle->begin();
// Locate (and cache) the font info. Unlike text nodes, glyphs reference the font by
// (family, style) -- not by name :( For now this performs a linear search over *all*
// fonts: generally there are few of them, and glyph definitions are font-clustered.
// If problematic, we can refactor as a two-level hashmap.
if (!current_font || !current_font->matches(family, style)) {
current_font = nullptr;
fFonts.foreach([&](const SkString& name, FontInfo* finfo) {
if (finfo->matches(family, style)) {
current_font = finfo;
// TODO: would be nice to break early here...
}
});
if (!current_font) {
this->log(Logger::Level::kError, nullptr,
"Font not found for codepoint (%d, %s, %s).", uni, family, style);
continue;
}
}
// TODO: parse glyphs
}
}
}
sk_sp<SkTypeface> AnimationBuilder::findFont(const SkString& font_name) const {
if (const auto* font = fFonts.find(font_name)) {
return font->fTypeface;
}
this->log(Logger::Level::kError, nullptr, "Unknown font: \"%s\".", font_name.c_str());
return nullptr;
}
sk_sp<sksg::RenderNode> AnimationBuilder::attachTextLayer(const skjson::ObjectValue& layer,
LayerInfo*,
AnimatorScope* ascope) const {
// General text node format:
// "t": {
// "a": [], // animators (see TextAnimator.cpp)
// "d": {
// "k": [
// {
// "s": {
// "f": "Roboto-Regular",
// "fc": [
// 0.42,
// 0.15,
// 0.15
// ],
// "j": 1,
// "lh": 60,
// "ls": 0,
// "s": 50,
// "t": "text align right",
// "tr": 0
// },
// "t": 0
// }
// ]
// },
// "m": {}, // "more options" (TODO)
// "p": {} // "path options" (TODO)
// },
const skjson::ObjectValue* jt = layer["t"];
if (!jt) {
this->log(Logger::Level::kError, &layer, "Missing text layer \"t\" property.");
return nullptr;
}
const skjson::ArrayValue* animated_props = (*jt)["a"];
const auto has_animators = (animated_props && animated_props->size() > 0);
const skjson::ObjectValue* jd = (*jt)["d"];
if (!jd) {
return nullptr;
}
auto text_root = sksg::Group::Make();
auto adapter = sk_make_sp<TextAdapter>(text_root, has_animators);
this->bindProperty<TextValue>(*jd, ascope, [adapter] (const TextValue& txt) {
adapter->setText(txt);
});
if (has_animators) {
if (auto alist = TextAnimatorList::Make(*animated_props, this, adapter)) {
ascope->push_back(std::move(alist));
}
}
return std::move(text_root);
}
} // namespace internal
} // namespace skottie