| /* | |
| * Copyright 2019 Google LLC | |
| * | |
| * Use of this source code is governed by a BSD-style license that can be | |
| * found in the LICENSE file. | |
| */ | |
| #include "modules/skottie/src/text/TextValue.h" | |
| #include "include/core/SkColor.h" | |
| #include "include/core/SkPaint.h" | |
| #include "include/core/SkRect.h" | |
| #include "include/core/SkRefCnt.h" | |
| #include "include/core/SkScalar.h" | |
| #include "include/core/SkString.h" | |
| #include "include/core/SkTypeface.h" | |
| #include "include/utils/SkTextUtils.h" | |
| #include "modules/jsonreader/SkJSONReader.h" | |
| #include "modules/skottie/include/Skottie.h" | |
| #include "modules/skottie/include/TextShaper.h" | |
| #include "modules/skottie/src/SkottieJson.h" | |
| #include "modules/skottie/src/SkottiePriv.h" | |
| #include "modules/skottie/src/SkottieValue.h" | |
| #include <algorithm> | |
| #include <array> | |
| #include <cstddef> | |
| #include <limits> | |
| namespace skottie::internal { | |
| bool Parse(const skjson::Value& jv, const internal::AnimationBuilder& abuilder, TextValue* v) { | |
| const skjson::ObjectValue* jtxt = jv; | |
| if (!jtxt) { | |
| return false; | |
| } | |
| const skjson::StringValue* font_name = (*jtxt)["f"]; | |
| const skjson::StringValue* text = (*jtxt)["t"]; | |
| const skjson::NumberValue* text_size = (*jtxt)["s"]; | |
| const skjson::NumberValue* line_height = (*jtxt)["lh"]; | |
| if (!font_name || !text || !text_size || !line_height) { | |
| return false; | |
| } | |
| const auto* font = abuilder.findFont(SkString(font_name->begin(), font_name->size())); | |
| if (!font) { | |
| abuilder.log(Logger::Level::kError, nullptr, "Unknown font: \"%s\".", font_name->begin()); | |
| return false; | |
| } | |
| v->fText.set(text->begin(), text->size()); | |
| v->fTextSize = **text_size; | |
| v->fLineHeight = **line_height; | |
| v->fTypeface = font->fTypeface; | |
| v->fFontFamily = font->fFamily; | |
| v->fAscent = font->fAscentPct * -0.01f * v->fTextSize; // negative ascent per SkFontMetrics | |
| v->fLineShift = ParseDefault((*jtxt)["ls"], 0.0f); | |
| #if !defined(SK_DISABLE_SKOTTIE_TRACKING) | |
| v->fTextTracking = ParseDefault((*jtxt)["tr"], 0.0f); | |
| #endif | |
| static constexpr auto gAlignMap = std::to_array<SkTextUtils::Align>({ | |
| SkTextUtils::kLeft_Align, // 'j': 0 | |
| SkTextUtils::kRight_Align, // 'j': 1 | |
| SkTextUtils::kCenter_Align // 'j': 2 | |
| }); | |
| v->fHAlign = gAlignMap[std::min<size_t>(ParseDefault<size_t>((*jtxt)["j"], 0), | |
| std::size(gAlignMap) - 1)]; | |
| // Optional text box size. | |
| if (const skjson::ArrayValue* jsz = (*jtxt)["sz"]) { | |
| if (jsz->size() == 2) { | |
| v->fBox.setWH(ParseDefault<SkScalar>((*jsz)[0], 0), | |
| ParseDefault<SkScalar>((*jsz)[1], 0)); | |
| } | |
| } | |
| // Optional text box position. | |
| if (const skjson::ArrayValue* jps = (*jtxt)["ps"]) { | |
| if (jps->size() == 2) { | |
| v->fBox.offset(ParseDefault<SkScalar>((*jps)[0], 0), | |
| ParseDefault<SkScalar>((*jps)[1], 0)); | |
| } | |
| } | |
| static constexpr auto gDirectionMap = std::to_array<Shaper::Direction>({ | |
| Shaper::Direction::kLTR, // 'd': 0 | |
| Shaper::Direction::kRTL, // 'd': 1 | |
| }); | |
| v->fDirection = gDirectionMap[std::min(ParseDefault<size_t>((*jtxt)["d"], 0), | |
| std::size(gDirectionMap) - 1)]; | |
| static constexpr auto gResizeMap = std::to_array<Shaper::ResizePolicy>({ | |
| Shaper::ResizePolicy::kNone, // 'rs': 0 | |
| Shaper::ResizePolicy::kScaleToFit, // 'rs': 1 | |
| Shaper::ResizePolicy::kDownscaleToFit, // 'rs': 2 | |
| }); | |
| // TODO: remove "sk_rs" support after migrating clients. | |
| v->fResize = gResizeMap[std::min(std::max(ParseDefault<size_t>((*jtxt)[ "rs"], 0), | |
| ParseDefault<size_t>((*jtxt)["sk_rs"], 0)), | |
| std::size(gResizeMap) - 1)]; | |
| // Optional min/max font size and line count (used when aute-resizing) | |
| v->fMinTextSize = ParseDefault<SkScalar>((*jtxt)["mf"], 0.0f); | |
| v->fMaxTextSize = ParseDefault<SkScalar>((*jtxt)["xf"], std::numeric_limits<float>::max()); | |
| v->fMaxLines = ParseDefault<size_t> ((*jtxt)["xl"], 0); | |
| // At the moment, BM uses the paragraph box to discriminate point mode vs. paragraph mode. | |
| v->fLineBreak = v->fBox.isEmpty() | |
| ? Shaper::LinebreakPolicy::kExplicit | |
| : Shaper::LinebreakPolicy::kParagraph; | |
| // Optional explicit text mode. | |
| // N.b.: this is not being exported by BM, only used for testing. | |
| auto text_mode = ParseDefault((*jtxt)["m"], -1); | |
| if (text_mode >= 0) { | |
| // Explicit text mode. | |
| v->fLineBreak = (text_mode == 0) | |
| ? Shaper::LinebreakPolicy::kExplicit // 'm': 0 -> point text | |
| : Shaper::LinebreakPolicy::kParagraph; // 'm': 1 -> paragraph text | |
| } | |
| // Optional capitalization. | |
| static constexpr auto gCapMap = std::to_array<Shaper::Capitalization>({ | |
| Shaper::Capitalization::kNone, // 'ca': 0 | |
| Shaper::Capitalization::kUpperCase, // 'ca': 1 | |
| }); | |
| v->fCapitalization = gCapMap[std::min<size_t>(ParseDefault<size_t>((*jtxt)["ca"], 0), | |
| std::size(gCapMap) - 1)]; | |
| // In point mode, the text is baseline-aligned. | |
| v->fVAlign = v->fBox.isEmpty() ? Shaper::VAlign::kTopBaseline | |
| : Shaper::VAlign::kTop; | |
| static constexpr auto gVAlignMap = std::to_array<Shaper::VAlign>({ | |
| Shaper::VAlign::kHybridTop, // 'vj': 0 | |
| Shaper::VAlign::kHybridCenter, // 'vj': 1 | |
| Shaper::VAlign::kHybridBottom, // 'vj': 2 | |
| Shaper::VAlign::kVisualTop, // 'vj': 3 | |
| Shaper::VAlign::kVisualCenter, // 'vj': 4 | |
| Shaper::VAlign::kVisualBottom, // 'vj': 5 | |
| }); | |
| size_t vj; | |
| if (skottie::Parse((*jtxt)["vj"], &vj)) { | |
| if (vj < std::size(gVAlignMap)) { | |
| v->fVAlign = gVAlignMap[vj]; | |
| } else { | |
| abuilder.log(Logger::Level::kWarning, nullptr, "Ignoring unknown 'vj' value: %zu", vj); | |
| } | |
| } else if (skottie::Parse((*jtxt)["sk_vj"], &vj)) { | |
| // Legacy sk_vj values. | |
| // TODO: remove after clients update. | |
| switch (vj) { | |
| case 0: | |
| case 1: | |
| case 2: | |
| static_assert(std::size(gVAlignMap) > 2); | |
| v->fVAlign = gVAlignMap[vj]; | |
| break; | |
| case 3: | |
| // 'sk_vj': 3 -> kHybridCenter/kScaleToFit | |
| v->fVAlign = Shaper::VAlign::kHybridCenter; | |
| v->fResize = Shaper::ResizePolicy::kScaleToFit; | |
| break; | |
| case 4: | |
| // 'sk_vj': 4 -> kHybridCenter/kDownscaleToFit | |
| v->fVAlign = Shaper::VAlign::kHybridCenter; | |
| v->fResize = Shaper::ResizePolicy::kDownscaleToFit; | |
| break; | |
| default: | |
| abuilder.log(Logger::Level::kWarning, nullptr, | |
| "Ignoring unknown 'sk_vj' value: %zu", vj); | |
| break; | |
| } | |
| } | |
| const auto& parse_color = [] (const skjson::ArrayValue* jcolor, | |
| SkColor* c) { | |
| if (!jcolor) { | |
| return false; | |
| } | |
| ColorValue color_vec; | |
| if (!skottie::Parse(*jcolor, static_cast<VectorValue*>(&color_vec))) { | |
| return false; | |
| } | |
| *c = color_vec; | |
| return true; | |
| }; | |
| v->fHasFill = parse_color((*jtxt)["fc"], &v->fFillColor); | |
| v->fHasStroke = parse_color((*jtxt)["sc"], &v->fStrokeColor); | |
| if (v->fHasStroke) { | |
| v->fStrokeWidth = ParseDefault((*jtxt)["sw"], 1.0f); | |
| v->fPaintOrder = ParseDefault((*jtxt)["of"], true) | |
| ? TextPaintOrder::kFillStroke | |
| : TextPaintOrder::kStrokeFill; | |
| static constexpr auto gJoins = std::to_array<SkPaint::Join>({ | |
| SkPaint::kMiter_Join, // lj: 1 | |
| SkPaint::kRound_Join, // lj: 2 | |
| SkPaint::kBevel_Join, // lj: 3 | |
| }); | |
| v->fStrokeJoin = gJoins[std::min<size_t>(ParseDefault<size_t>((*jtxt)["lj"], 1) - 1, | |
| std::size(gJoins) - 1)]; | |
| } | |
| return true; | |
| } | |
| } // namespace skottie::internal |