| /* | |
| * 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/include/TextShaper.h" | |
| #include "include/core/SkCanvas.h" | |
| #include "include/core/SkFontMetrics.h" | |
| #include "include/core/SkFontMgr.h" | |
| #include "include/core/SkFontTypes.h" | |
| #include "include/core/SkFourByteTag.h" | |
| #include "include/core/SkRect.h" | |
| #include "include/core/SkRefCnt.h" | |
| #include "include/core/SkSize.h" | |
| #include "include/core/SkString.h" | |
| #include "include/core/SkTypeface.h" | |
| #include "include/core/SkTypes.h" | |
| #include "include/private/SkTArray.h" | |
| #include "include/private/SkTPin.h" | |
| #include "include/private/SkTemplates.h" | |
| #include "include/private/SkTo.h" | |
| #include "modules/skshaper/include/SkShaper.h" | |
| #include "modules/skshaper/include/SkShaper_factory.h" | |
| #include "modules/skunicode/include/SkUnicode.h" | |
| #include "src/core/SkFontPriv.h" | |
| #include "src/core/SkUTF.h" | |
| #include <algorithm> | |
| #include <memory> | |
| #include <numeric> | |
| #include <optional> | |
| #include <utility> | |
| class SkPaint; | |
| using namespace skia_private; | |
| namespace skottie { | |
| namespace { | |
| static bool is_whitespace(char c) { | |
| // TODO: we've been getting away with this simple heuristic, | |
| // but ideally we should use SkUicode::isWhiteSpace(). | |
| return c == ' ' || c == '\t' || c == '\r' || c == '\n'; | |
| } | |
| // Helper for interfacing with SkShaper: buffers shaper-fed runs and performs | |
| // per-line position adjustments (for external line breaking, horizontal alignment, etc). | |
| class ResultBuilder final : public SkShaper::RunHandler { | |
| public: | |
| ResultBuilder(const Shaper::TextDesc& desc, const SkRect& box, const sk_sp<SkFontMgr>& fontmgr, | |
| const sk_sp<SkShapers::Factory>& shapingFactory) | |
| : fDesc(desc) | |
| , fBox(box) | |
| , fHAlignFactor(HAlignFactor(fDesc.fHAlign)) | |
| , fFont(fDesc.fTypeface, fDesc.fTextSize) | |
| , fFontMgr(fontmgr) | |
| , fShapingFactory(shapingFactory) { | |
| // If the shaper callback returns null, fallback to the primitive shaper. | |
| SkASSERT(fShapingFactory); | |
| fShaper = fShapingFactory->makeShaper(fFontMgr); | |
| if (!fShaper) { | |
| fShaper = SkShapers::Primitive::PrimitiveText(); | |
| fShapingFactory = SkShapers::Primitive::Factory(); | |
| } | |
| fFont.setHinting(SkFontHinting::kNone); | |
| fFont.setSubpixel(true); | |
| fFont.setLinearMetrics(true); | |
| fFont.setBaselineSnap(false); | |
| fFont.setEdging(SkFont::Edging::kAntiAlias); | |
| } | |
| void beginLine() override { | |
| fLineGlyphs.reset(0); | |
| fLinePos.reset(0); | |
| fLineClusters.reset(0); | |
| fLineRuns.clear(); | |
| fLineGlyphCount = 0; | |
| fCurrentPosition = fOffset; | |
| fPendingLineAdvance = { 0, 0 }; | |
| fLastLineDescent = 0; | |
| } | |
| void runInfo(const RunInfo& info) override { | |
| fPendingLineAdvance += info.fAdvance; | |
| SkFontMetrics metrics; | |
| info.fFont.getMetrics(&metrics); | |
| if (!fLineCount) { | |
| fFirstLineAscent = std::min(fFirstLineAscent, metrics.fAscent); | |
| } | |
| fLastLineDescent = std::max(fLastLineDescent, metrics.fDescent); | |
| } | |
| void commitRunInfo() override {} | |
| Buffer runBuffer(const RunInfo& info) override { | |
| // Skipping glyphless runs keeps every fLineRuns entry non-empty, which commitLine() and | |
| // the trailing tracking discount below rely on | |
| if (!info.glyphCount) { | |
| return {}; | |
| } | |
| const auto run_start_index = fLineGlyphCount; | |
| fLineGlyphCount += info.glyphCount; | |
| fLineGlyphs.realloc(fLineGlyphCount); | |
| fLinePos.realloc(fLineGlyphCount); | |
| fLineClusters.realloc(fLineGlyphCount); | |
| fLineRuns.push_back({info.fFont, info.glyphCount}); | |
| // Tracking is inter-glyph: the shaper appends it after every glyph, including the last | |
| // one on the line, but that trailing slack should not count toward the line width (else | |
| // it would e.g. push right-aligned text off the right edge). runBuffer() is only called | |
| // for non-empty runs, so the line has at least one glyph -> discount one trailing unit. | |
| const auto line_advance = fPendingLineAdvance.x() - this->trackingAdvance(); | |
| SkVector alignmentOffset { fHAlignFactor * (line_advance - fBox.width()), 0 }; | |
| return { | |
| fLineGlyphs.get() + run_start_index, | |
| fLinePos.get() + run_start_index, | |
| nullptr, | |
| fLineClusters.get() + run_start_index, | |
| fCurrentPosition + alignmentOffset | |
| }; | |
| } | |
| void commitRunBuffer(const RunInfo& info) override { | |
| fCurrentPosition += info.fAdvance; | |
| } | |
| void commitLine() override { | |
| fOffset.fY += fDesc.fLineHeight; | |
| // Observed AE handling of whitespace, for alignment purposes: | |
| // | |
| // - leading whitespace contributes to alignment | |
| // - trailing whitespace is ignored | |
| // - auto line breaking retains all separating whitespace on the first line (no artificial | |
| // leading WS is created). | |
| auto adjust_trailing_whitespace = [this]() { | |
| // For left-alignment, trailing WS doesn't make any difference. | |
| if (fLineRuns.empty() || fDesc.fHAlign == SkTextUtils::Align::kLeft_Align) { | |
| return; | |
| } | |
| // Technically, trailing whitespace could span multiple runs, but realistically, | |
| // SkShaper has no reason to split it. Hence we're only checking the last run. | |
| size_t ws_count = 0; | |
| for (size_t i = 0; i < fLineRuns.back().fSize; ++i) { | |
| if (is_whitespace(fUTF8[fLineClusters[SkToInt(fLineGlyphCount - i - 1)]])) { | |
| ++ws_count; | |
| } else { | |
| break; | |
| } | |
| } | |
| // No trailing whitespace. | |
| if (!ws_count) { | |
| return; | |
| } | |
| // Compute the cumulative whitespace advance. The shaper also appends tracking after | |
| // each of these glyphs, so include it to fully compensate for trailing whitespace. | |
| fAdvanceBuffer.resize(ws_count); | |
| fLineRuns.back().fFont.getWidths( | |
| {fLineGlyphs.data() + fLineGlyphCount - ws_count, ws_count}, | |
| {fAdvanceBuffer.data(), ws_count}); | |
| const auto ws_advance = std::accumulate(fAdvanceBuffer.begin(), | |
| fAdvanceBuffer.end(), | |
| 0.0f) | |
| + ws_count*this->trackingAdvance(); | |
| // Offset needed to compensate for whitespace. | |
| const auto offset = ws_advance*-fHAlignFactor; | |
| // Shift the whole line horizontally by the computed offset. | |
| std::transform(fLinePos.data(), | |
| fLinePos.data() + fLineGlyphCount, | |
| fLinePos.data(), | |
| [&offset](SkPoint pos) { return SkPoint{pos.fX + offset, pos.fY}; }); | |
| }; | |
| adjust_trailing_whitespace(); | |
| const auto commit_proc = (fDesc.fFlags & Shaper::Flags::kFragmentGlyphs) | |
| ? &ResultBuilder::commitFragementedRun | |
| : &ResultBuilder::commitConsolidatedRun; | |
| size_t run_offset = 0; | |
| for (const auto& rec : fLineRuns) { | |
| SkASSERT(run_offset < fLineGlyphCount); | |
| (this->*commit_proc)(rec, | |
| fLineGlyphs.get() + run_offset, | |
| fLinePos.get() + run_offset, | |
| fLineClusters.get() + run_offset, | |
| fLineCount); | |
| run_offset += rec.fSize; | |
| } | |
| fLineCount++; | |
| } | |
| Shaper::Result finalize(SkSize* shaped_size) { | |
| if (!(fDesc.fFlags & Shaper::Flags::kFragmentGlyphs)) { | |
| // All glyphs (if any) are pending in a single fragment. | |
| SkASSERT(fResult.fFragments.size() <= 1); | |
| } | |
| const auto ascent = this->ascent(); | |
| // For visual VAlign modes, we use a hybrid extent box computed as the union of | |
| // actual visual bounds and the vertical typographical extent. | |
| // | |
| // This ensures that | |
| // | |
| // a) text doesn't visually overflow the alignment boundaries | |
| // | |
| // b) leading/trailing empty lines are still taken into account for alignment purposes | |
| auto extent_box = [&](bool include_typographical_extent) { | |
| auto box = fResult.computeVisualBounds(); | |
| if (include_typographical_extent) { | |
| // Hybrid visual alignment mode, based on typographical extent. | |
| // By default, first line is vertically-aligned on a baseline of 0. | |
| // The typographical height considered for vertical alignment is the distance | |
| // between the first line top (ascent) to the last line bottom (descent). | |
| const auto typographical_top = fBox.fTop + ascent, | |
| typographical_bottom = fBox.fTop + fLastLineDescent + | |
| fDesc.fLineHeight*(fLineCount > 0 ? fLineCount - 1 : 0ul); | |
| box.fTop = std::min(box.fTop, typographical_top); | |
| box.fBottom = std::max(box.fBottom, typographical_bottom); | |
| } | |
| return box; | |
| }; | |
| // Only compute the extent box when needed. | |
| std::optional<SkRect> ebox; | |
| // Vertical adjustments. | |
| float v_offset = -fDesc.fLineShift; | |
| switch (fDesc.fVAlign) { | |
| case Shaper::VAlign::kTop: | |
| v_offset -= ascent; | |
| break; | |
| case Shaper::VAlign::kTopBaseline: | |
| // Default behavior. | |
| break; | |
| case Shaper::VAlign::kHybridTop: | |
| case Shaper::VAlign::kVisualTop: | |
| ebox.emplace(extent_box(fDesc.fVAlign == Shaper::VAlign::kHybridTop)); | |
| v_offset += fBox.fTop - ebox->fTop; | |
| break; | |
| case Shaper::VAlign::kHybridCenter: | |
| case Shaper::VAlign::kVisualCenter: | |
| ebox.emplace(extent_box(fDesc.fVAlign == Shaper::VAlign::kHybridCenter)); | |
| v_offset += fBox.centerY() - ebox->centerY(); | |
| break; | |
| case Shaper::VAlign::kHybridBottom: | |
| case Shaper::VAlign::kVisualBottom: | |
| ebox.emplace(extent_box(fDesc.fVAlign == Shaper::VAlign::kHybridBottom)); | |
| v_offset += fBox.fBottom - ebox->fBottom; | |
| break; | |
| } | |
| if (shaped_size) { | |
| if (!ebox.has_value()) { | |
| ebox.emplace(extent_box(true)); | |
| } | |
| *shaped_size = SkSize::Make(ebox->width(), ebox->height()); | |
| } | |
| if (v_offset) { | |
| for (auto& fragment : fResult.fFragments) { | |
| fragment.fOrigin.fY += v_offset; | |
| } | |
| } | |
| return std::move(fResult); | |
| } | |
| void shapeLine(const char* start, const char* end, size_t utf8_offset) { | |
| if (!fShaper) { | |
| return; | |
| } | |
| SkASSERT(start <= end); | |
| if (start == end) { | |
| // SkShaper doesn't care for empty lines. | |
| this->beginLine(); | |
| this->commitLine(); | |
| // The calls above perform bookkeeping, but they do not add any fragments (since there | |
| // are no runs to commit). | |
| // | |
| // Certain Skottie features (line-based range selectors) do require accurate indexing | |
| // information even for empty lines though -- so we inject empty fragments solely for | |
| // line index tracking. | |
| // | |
| // Note: we don't add empty fragments in consolidated mode because 1) consolidated mode | |
| // assumes there is a single result fragment and 2) kFragmentGlyphs is always enabled | |
| // for cases where line index tracking is relevant. | |
| // | |
| // TODO(fmalita): investigate whether it makes sense to move this special case down | |
| // to commitFragmentedRun(). | |
| if (fDesc.fFlags & Shaper::Flags::kFragmentGlyphs) { | |
| fResult.fFragments.push_back({ | |
| Shaper::ShapedGlyphs(), | |
| {fBox.x(),fBox.y()}, | |
| 0, 0, | |
| fLineCount - 1, | |
| false | |
| }); | |
| } | |
| return; | |
| } | |
| // In default paragraph mode (VAlign::kTop), AE clips out lines when the baseline | |
| // goes below the box lower edge. | |
| if (fDesc.fVAlign == Shaper::VAlign::kTop) { | |
| // fOffset is relative to the first line baseline. | |
| const auto max_offset = fBox.height() + this->ascent(); // NB: ascent is negative | |
| if (fOffset.y() > max_offset) { | |
| return; | |
| } | |
| } | |
| const auto shape_width = fDesc.fLinebreak == Shaper::LinebreakPolicy::kExplicit | |
| ? SK_ScalarMax | |
| : fBox.width(); | |
| const auto shape_ltr = fDesc.fDirection == Shaper::Direction::kLTR; | |
| const auto shape_track = this->trackingEm(); | |
| const size_t utf8_bytes = SkToSizeT(end - start); | |
| static constexpr uint8_t kBidiLevelLTR = 0, | |
| kBidiLevelRTL = 1; | |
| const auto lang_iter = fDesc.fLocale | |
| ? std::make_unique<SkShaper::TrivialLanguageRunIterator>(fDesc.fLocale, utf8_bytes) | |
| : SkShaper::MakeStdLanguageRunIterator(start, utf8_bytes); | |
| #if defined(SKOTTIE_TRIVIAL_FONTRUN_ITER) | |
| // Chrome Linux/CrOS does not have a fallback-capable fontmgr, and crashes if fallback is | |
| // triggered. Using a TrivialFontRunIterator avoids the issue (https://crbug.com/1520148). | |
| const auto font_iter = std::make_unique<SkShaper::TrivialFontRunIterator>(fFont, | |
| utf8_bytes); | |
| #else | |
| const auto font_iter = SkShaper::MakeFontMgrRunIterator( | |
| start, utf8_bytes, fFont, | |
| fFontMgr ? fFontMgr : SkFontMgr::RefEmpty(), // used as fallback | |
| fDesc.fFontFamily, | |
| fFont.getTypeface()->fontStyle(), | |
| lang_iter.get()); | |
| #endif | |
| std::unique_ptr<SkShaper::BiDiRunIterator> bidi_iter = | |
| fShapingFactory->makeBidiRunIterator(start, utf8_bytes, | |
| shape_ltr ? kBidiLevelLTR : kBidiLevelRTL); | |
| if (!bidi_iter) { | |
| bidi_iter = std::make_unique<SkShaper::TrivialBiDiRunIterator>( | |
| shape_ltr ? kBidiLevelLTR : kBidiLevelRTL, utf8_bytes); | |
| } | |
| constexpr SkFourByteTag unknownScript = SkSetFourByteTag('Z', 'z', 'z', 'z'); | |
| std::unique_ptr<SkShaper::ScriptRunIterator> scpt_iter = | |
| fShapingFactory->makeScriptRunIterator(start, utf8_bytes, unknownScript); | |
| if (!scpt_iter) { | |
| scpt_iter = std::make_unique<SkShaper::TrivialScriptRunIterator>(unknownScript, utf8_bytes); | |
| } | |
| if (!font_iter || !bidi_iter || !scpt_iter || !lang_iter) { | |
| return; | |
| } | |
| fUTF8 = start; | |
| fUTF8Offset = utf8_offset; | |
| fShaper->shape({ start, utf8_bytes }, | |
| *font_iter, | |
| *bidi_iter, | |
| *scpt_iter, | |
| *lang_iter, | |
| {}, | |
| { .width = shape_width, .tracking = shape_track }, | |
| this); | |
| fUTF8 = nullptr; | |
| } | |
| private: | |
| void commitFragementedRun(const skottie::Shaper::RunRec& run, | |
| const SkGlyphID* glyphs, | |
| const SkPoint* pos, | |
| const uint32_t* clusters, | |
| uint32_t line_index) { | |
| float ascent = 0; | |
| if (fDesc.fFlags & Shaper::Flags::kTrackFragmentAdvanceAscent) { | |
| SkFontMetrics metrics; | |
| run.fFont.getMetrics(&metrics); | |
| ascent = metrics.fAscent; | |
| // Note: we use per-glyph advances for anchoring, but it's unclear whether this | |
| // is exactly the same as AE. E.g. are 'acute' glyphs anchored separately for fonts | |
| // in which they're distinct? | |
| fAdvanceBuffer.resize(run.fSize); | |
| run.fFont.getWidths({glyphs, run.fSize}, {fAdvanceBuffer.data(), run.fSize}); | |
| } | |
| // In fragmented mode we immediately push the glyphs to fResult, | |
| // one fragment per glyph. Glyph positioning is externalized | |
| // (positions returned in Fragment::fPos). | |
| for (size_t i = 0; i < run.fSize; ++i) { | |
| const auto advance = (fDesc.fFlags & Shaper::Flags::kTrackFragmentAdvanceAscent) | |
| ? fAdvanceBuffer[SkToInt(i)] | |
| : 0.0f; | |
| fResult.fFragments.push_back({ | |
| { | |
| { {run.fFont, 1} }, | |
| { glyphs[i] }, | |
| { {0,0} }, | |
| fDesc.fFlags & Shaper::kClusters | |
| ? std::vector<size_t>{ fUTF8Offset + clusters[i] } | |
| : std::vector<size_t>({}), | |
| }, | |
| { fBox.x() + pos[i].fX, fBox.y() + pos[i].fY }, | |
| advance, ascent, | |
| line_index, is_whitespace(fUTF8[clusters[i]]) | |
| }); | |
| // Note: we only check the first code point in the cluster for whitespace. | |
| // It's unclear whether thers's a saner approach. | |
| fResult.fMissingGlyphCount += (glyphs[i] == kMissingGlyphID); | |
| } | |
| } | |
| void commitConsolidatedRun(const skottie::Shaper::RunRec& run, | |
| const SkGlyphID* glyphs, | |
| const SkPoint* pos, | |
| const uint32_t* clusters, | |
| uint32_t) { | |
| // In consolidated mode we just accumulate glyphs to a single fragment in ResultBuilder. | |
| // Glyph positions are baked in the fragment runs (Fragment::fPos only reflects the | |
| // box origin). | |
| if (fResult.fFragments.empty()) { | |
| fResult.fFragments.push_back({{{}, {}, {}, {}}, {fBox.x(), fBox.y()}, 0, 0, 0, false}); | |
| } | |
| auto& current_glyphs = fResult.fFragments.back().fGlyphs; | |
| current_glyphs.fRuns.push_back(run); | |
| current_glyphs.fGlyphIDs.insert(current_glyphs.fGlyphIDs.end(), glyphs, glyphs + run.fSize); | |
| current_glyphs.fGlyphPos.insert(current_glyphs.fGlyphPos.end(), pos , pos + run.fSize); | |
| for (size_t i = 0; i < run.fSize; ++i) { | |
| fResult.fMissingGlyphCount += (glyphs[i] == kMissingGlyphID); | |
| } | |
| if (fDesc.fFlags & Shaper::kClusters) { | |
| current_glyphs.fClusters.reserve(current_glyphs.fClusters.size() + run.fSize); | |
| for (size_t i = 0; i < run.fSize; ++i) { | |
| current_glyphs.fClusters.push_back(fUTF8Offset + clusters[i]); | |
| } | |
| } | |
| } | |
| static float HAlignFactor(SkTextUtils::Align align) { | |
| switch (align) { | |
| case SkTextUtils::kLeft_Align: return 0.0f; | |
| case SkTextUtils::kCenter_Align: return -0.5f; | |
| case SkTextUtils::kRight_Align: return -1.0f; | |
| } | |
| return 0.0f; // go home, msvc... | |
| } | |
| SkScalar ascent() const { | |
| // Use the explicit ascent, when specified. | |
| // Note: ascent values are negative (relative to the baseline). | |
| return fDesc.fAscent ? fDesc.fAscent : fFirstLineAscent; | |
| } | |
| // Tracking expressed as a fraction of the font size (the unit SkShaper expects). | |
| float trackingEm() const { | |
| static constexpr float kTrackingUnitsPerEm = 1000; | |
| return fDesc.fTextTracking / kTrackingUnitsPerEm; | |
| } | |
| // Tracking in absolute (text-space) units, applied by the shaper after each glyph. | |
| // Mirrors the backends' formula (trackingEm * size * scaleX) so the discount matches. | |
| SkScalar trackingAdvance() const { | |
| return this->trackingEm() * fFont.getSize() * fFont.getScaleX(); | |
| } | |
| inline static constexpr SkGlyphID kMissingGlyphID = 0; | |
| const Shaper::TextDesc& fDesc; | |
| const SkRect& fBox; | |
| const float fHAlignFactor; | |
| SkFont fFont; | |
| const sk_sp<SkFontMgr> fFontMgr; | |
| std::unique_ptr<SkShaper> fShaper; | |
| sk_sp<SkShapers::Factory> fShapingFactory; | |
| AutoSTMalloc<64, SkGlyphID> fLineGlyphs; | |
| AutoSTMalloc<64, SkPoint> fLinePos; | |
| AutoSTMalloc<64, uint32_t> fLineClusters; | |
| STArray<16, skottie::Shaper::RunRec> fLineRuns; | |
| size_t fLineGlyphCount = 0; | |
| STArray<64, float, true> fAdvanceBuffer; | |
| SkPoint fCurrentPosition{ 0, 0 }; | |
| SkPoint fOffset{ 0, 0 }; | |
| SkVector fPendingLineAdvance{ 0, 0 }; | |
| uint32_t fLineCount = 0; | |
| float fFirstLineAscent = 0, | |
| fLastLineDescent = 0; | |
| const char* fUTF8 = nullptr; // only valid during shapeLine() calls | |
| size_t fUTF8Offset = 0; // current line offset within the original string | |
| Shaper::Result fResult; | |
| }; | |
| Shaper::Result ShapeImpl(const SkString& txt, const Shaper::TextDesc& desc, | |
| const SkRect& box, const sk_sp<SkFontMgr>& fontmgr, | |
| const sk_sp<SkShapers::Factory>& shapingFactory, | |
| SkSize* shaped_size) { | |
| const auto& is_line_break = [](SkUnichar uch) { | |
| // TODO: other explicit breaks? | |
| return uch == '\r'; | |
| }; | |
| const char* ptr = txt.c_str(); | |
| const char* line_start = ptr; | |
| const char* begin = ptr; | |
| const char* end = ptr + txt.size(); | |
| ResultBuilder rbuilder(desc, box, fontmgr, shapingFactory); | |
| while (ptr < end) { | |
| if (is_line_break(SkUTF::NextUTF8(&ptr, end))) { | |
| rbuilder.shapeLine(line_start, ptr - 1, SkToSizeT(line_start - begin)); | |
| line_start = ptr; | |
| } | |
| } | |
| rbuilder.shapeLine(line_start, ptr, SkToSizeT(line_start - begin)); | |
| return rbuilder.finalize(shaped_size); | |
| } | |
| bool result_fits(const Shaper::Result& res, const SkSize& res_size, | |
| const SkRect& box, const Shaper::TextDesc& desc) { | |
| // optional max line count constraint | |
| if (desc.fMaxLines) { | |
| const auto line_count = res.fFragments.empty() | |
| ? 0 | |
| : res.fFragments.back().fLineIndex + 1; | |
| if (line_count > desc.fMaxLines) { | |
| return false; | |
| } | |
| } | |
| // geometric constraint | |
| return res_size.width() <= box.width() && res_size.height() <= box.height(); | |
| } | |
| Shaper::Result ShapeToFit(const SkString& txt, const Shaper::TextDesc& orig_desc, | |
| const SkRect& box, const sk_sp<SkFontMgr>& fontmgr, | |
| const sk_sp<SkShapers::Factory>& shapingFactory) { | |
| Shaper::Result best_result; | |
| if (box.isEmpty() || orig_desc.fTextSize <= 0) { | |
| return best_result; | |
| } | |
| auto desc = orig_desc; | |
| const auto min_scale = std::max(desc.fMinTextSize / desc.fTextSize, 0.0f), | |
| max_scale = std::max(desc.fMaxTextSize / desc.fTextSize, min_scale); | |
| float in_scale = min_scale, // maximum scale that fits inside | |
| out_scale = max_scale, // minimum scale that doesn't fit | |
| try_scale = SkTPin(1.0f, min_scale, max_scale); // current probe | |
| // Perform a binary search for the best vertical fit (SkShaper already handles | |
| // horizontal fitting), starting with the specified text size. | |
| // | |
| // This hybrid loop handles both the binary search (when in/out extremes are known), and an | |
| // exponential search for the extremes. | |
| static constexpr size_t kMaxIter = 16; | |
| for (size_t i = 0; i < kMaxIter; ++i) { | |
| SkASSERT(try_scale >= in_scale && try_scale <= out_scale); | |
| desc.fTextSize = try_scale * orig_desc.fTextSize; | |
| desc.fLineHeight = try_scale * orig_desc.fLineHeight; | |
| desc.fLineShift = try_scale * orig_desc.fLineShift; | |
| desc.fAscent = try_scale * orig_desc.fAscent; | |
| SkSize res_size = {0, 0}; | |
| auto res = ShapeImpl(txt, desc, box, fontmgr, shapingFactory, &res_size); | |
| const auto prev_scale = try_scale; | |
| if (!result_fits(res, res_size, box, desc)) { | |
| out_scale = try_scale; | |
| try_scale = (in_scale == min_scale) | |
| // initial in_scale not found yet - search exponentially | |
| ? std::max(min_scale, try_scale * 0.5f) | |
| // in_scale found - binary search | |
| : (in_scale + out_scale) * 0.5f; | |
| } else { | |
| // It fits - so it's a candidate. | |
| best_result = std::move(res); | |
| best_result.fScale = try_scale; | |
| in_scale = try_scale; | |
| try_scale = (out_scale == max_scale) | |
| // initial out_scale not found yet - search exponentially | |
| ? std::min(max_scale, try_scale * 2) | |
| // out_scale found - binary search | |
| : (in_scale + out_scale) * 0.5f; | |
| } | |
| if (try_scale == prev_scale) { | |
| // no more progress | |
| break; | |
| } | |
| } | |
| return best_result; | |
| } | |
| // Applies capitalization rules. | |
| class AdjustedText { | |
| public: | |
| AdjustedText(const SkString& txt, const Shaper::TextDesc& desc, SkUnicode* unicode) | |
| : fText(txt) { | |
| switch (desc.fCapitalization) { | |
| case Shaper::Capitalization::kNone: | |
| break; | |
| case Shaper::Capitalization::kUpperCase: | |
| if (unicode) { | |
| fText = unicode->toUpper(fText); | |
| } | |
| break; | |
| } | |
| } | |
| operator const SkString&() const { return fText; } | |
| private: | |
| SkString fText; | |
| }; | |
| } // namespace | |
| Shaper::Result Shaper::Shape(const SkString& text, const TextDesc& desc, const SkPoint& point, | |
| const sk_sp<SkFontMgr>& fontmgr, const sk_sp<SkShapers::Factory>& shapingFactory) { | |
| const AdjustedText adjText(text, desc, shapingFactory->getUnicode()); | |
| return (desc.fResize == ResizePolicy::kScaleToFit || | |
| desc.fResize == ResizePolicy::kDownscaleToFit) // makes no sense in point mode | |
| ? Result() | |
| : ShapeImpl(adjText, desc, SkRect::MakeEmpty().makeOffset(point.x(), point.y()), | |
| fontmgr, shapingFactory, nullptr); | |
| } | |
| Shaper::Result Shaper::Shape(const SkString& text, const TextDesc& desc, const SkRect& box, | |
| const sk_sp<SkFontMgr>& fontmgr, const sk_sp<SkShapers::Factory>& shapingFactory) { | |
| const AdjustedText adjText(text, desc, shapingFactory->getUnicode()); | |
| switch(desc.fResize) { | |
| case ResizePolicy::kNone: | |
| return ShapeImpl(adjText, desc, box, fontmgr, shapingFactory, nullptr); | |
| case ResizePolicy::kScaleToFit: | |
| return ShapeToFit(adjText, desc, box, fontmgr, shapingFactory); | |
| case ResizePolicy::kDownscaleToFit: { | |
| SkSize size; | |
| auto result = ShapeImpl(adjText, desc, box, fontmgr, shapingFactory, &size); | |
| return result_fits(result, size, box, desc) | |
| ? result | |
| : ShapeToFit(adjText, desc, box, fontmgr, shapingFactory); | |
| } | |
| } | |
| SkUNREACHABLE; | |
| } | |
| SkRect Shaper::ShapedGlyphs::computeBounds(BoundsType btype) const { | |
| auto bounds = SkRect::MakeEmpty(); | |
| AutoSTArray<16, SkRect> glyphBounds; | |
| size_t offset = 0; | |
| for (const auto& run : fRuns) { | |
| SkRect font_bounds; | |
| if (btype == BoundsType::kConservative) { | |
| font_bounds = SkFontPriv::GetFontBounds(run.fFont); | |
| // Empty font bounds is likely a font bug -- fall back to tight bounds. | |
| if (font_bounds.isEmpty()) { | |
| btype = BoundsType::kTight; | |
| } | |
| } | |
| switch (btype) { | |
| case BoundsType::kConservative: { | |
| SkRect run_bounds = SkRect::BoundsOrEmpty({fGlyphPos.data() + offset, run.fSize}); | |
| run_bounds.fLeft += font_bounds.left(); | |
| run_bounds.fTop += font_bounds.top(); | |
| run_bounds.fRight += font_bounds.right(); | |
| run_bounds.fBottom += font_bounds.bottom(); | |
| bounds.join(run_bounds); | |
| } break; | |
| case BoundsType::kTight: { | |
| glyphBounds.reset(SkToInt(run.fSize)); | |
| run.fFont.getBounds({fGlyphIDs.data() + offset, run.fSize}, glyphBounds, nullptr); | |
| for (size_t i = 0; i < run.fSize; ++i) { | |
| bounds.join(glyphBounds[SkToInt(i)].makeOffset(fGlyphPos[offset + i])); | |
| } | |
| } break; | |
| } | |
| offset += run.fSize; | |
| } | |
| return bounds; | |
| } | |
| void Shaper::ShapedGlyphs::draw(SkCanvas* canvas, | |
| const SkPoint& origin, | |
| const SkPaint& paint) const { | |
| size_t offset = 0; | |
| for (const auto& run : fRuns) { | |
| canvas->drawGlyphs({fGlyphIDs.data() + offset, run.fSize}, | |
| {fGlyphPos.data() + offset, run.fSize}, | |
| origin, | |
| run.fFont, | |
| paint); | |
| offset += run.fSize; | |
| } | |
| } | |
| SkRect Shaper::Result::computeVisualBounds() const { | |
| auto bounds = SkRect::MakeEmpty(); | |
| for (const auto& frag: fFragments) { | |
| bounds.join(frag.fGlyphs.computeBounds(ShapedGlyphs::BoundsType::kTight) | |
| .makeOffset(frag.fOrigin)); | |
| } | |
| return bounds; | |
| } | |
| } // namespace skottie |