| /* |
| * Copyright 2019 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/include/TextShaper.h" |
| |
| #include "include/core/SkCanvas.h" |
| #include "include/core/SkFontMetrics.h" |
| #include "include/core/SkFontMgr.h" |
| #include "include/core/SkTypes.h" |
| #include "include/private/base/SkTArray.h" |
| #include "include/private/base/SkTPin.h" |
| #include "include/private/base/SkTemplates.h" |
| #include "include/private/base/SkTo.h" |
| #include "modules/skshaper/include/SkShaper.h" |
| #include "src/base/SkTLazy.h" |
| #include "src/base/SkUTF.h" |
| #include "src/core/SkFontPriv.h" |
| |
| #ifdef SK_UNICODE_AVAILABLE |
| #include "modules/skunicode/include/SkUnicode.h" |
| #endif |
| |
| #include <algorithm> |
| #include <limits.h> |
| #include <numeric> |
| |
| 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) |
| : fDesc(desc) |
| , fBox(box) |
| , fHAlignFactor(HAlignFactor(fDesc.fHAlign)) |
| , fFont(fDesc.fTypeface, fDesc.fTextSize) |
| , fFontMgr(fontmgr) |
| , fShaper(SkShaper::Make(fontmgr)) { |
| 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 { |
| 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}); |
| |
| SkVector alignmentOffset { fHAlignFactor * (fPendingLineAdvance.x() - 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. |
| fAdvanceBuffer.resize(ws_count); |
| fLineRuns.back().fFont.getWidths(fLineGlyphs.data() + fLineGlyphCount - ws_count, |
| SkToInt(ws_count), fAdvanceBuffer.data(), nullptr); |
| |
| const auto ws_advance = std::accumulate(fAdvanceBuffer.begin(), |
| fAdvanceBuffer.end(), |
| 0.0f); |
| |
| // 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. |
| SkTLazy<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.init(extent_box(fDesc.fVAlign == Shaper::VAlign::kHybridTop)); |
| v_offset += fBox.fTop - ebox->fTop; |
| break; |
| case Shaper::VAlign::kHybridCenter: |
| case Shaper::VAlign::kVisualCenter: |
| ebox.init(extent_box(fDesc.fVAlign == Shaper::VAlign::kHybridCenter)); |
| v_offset += fBox.centerY() - ebox->centerY(); |
| break; |
| case Shaper::VAlign::kHybridBottom: |
| case Shaper::VAlign::kVisualBottom: |
| ebox.init(extent_box(fDesc.fVAlign == Shaper::VAlign::kHybridBottom)); |
| v_offset += fBox.fBottom - ebox->fBottom; |
| break; |
| } |
| |
| if (shaped_size) { |
| if (!ebox.isValid()) { |
| ebox.init(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 size_t utf8_bytes = SkToSizeT(end - start); |
| |
| SkASSERT(fFontMgr); |
| 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); |
| const auto font_iter = SkShaper::MakeFontMgrRunIterator( |
| start, utf8_bytes, fFont, fFontMgr, |
| nullptr, |
| SkFontPriv::RefTypefaceOrDefault(fFont)->fontStyle(), |
| lang_iter.get()); |
| const auto bidi_iter = SkShaper::MakeBiDiRunIterator(start, utf8_bytes, |
| shape_ltr ? kBidiLevelLTR : kBidiLevelRTL); |
| const auto scpt_iter = SkShaper::MakeScriptRunIterator(start, utf8_bytes, |
| SkSetFourByteTag('Z', 'z', 'z', 'z')); |
| |
| 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, |
| shape_width, 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); |
| fFont.getWidths(glyphs, SkToInt(run.fSize), fAdvanceBuffer.data()); |
| } |
| |
| // 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; |
| } |
| |
| inline static constexpr SkGlyphID kMissingGlyphID = 0; |
| |
| const Shaper::TextDesc& fDesc; |
| const SkRect& fBox; |
| const float fHAlignFactor; |
| |
| SkFont fFont; |
| const sk_sp<SkFontMgr> fFontMgr; |
| const std::unique_ptr<SkShaper> fShaper; |
| |
| 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, |
| SkSize* shaped_size = nullptr) { |
| 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); |
| 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) { |
| 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, &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) |
| : fText(txt) { |
| switch (desc.fCapitalization) { |
| case Shaper::Capitalization::kNone: |
| break; |
| case Shaper::Capitalization::kUpperCase: |
| #ifdef SK_UNICODE_AVAILABLE |
| if (auto skuni = SkUnicode::Make()) { |
| *fText.writable() = skuni->toUpper(*fText); |
| } |
| #endif |
| break; |
| } |
| } |
| |
| operator const SkString&() const { return *fText; } |
| |
| private: |
| SkTCopyOnFirstWrite<SkString> fText; |
| }; |
| |
| } // namespace |
| |
| Shaper::Result Shaper::Shape(const SkString& text, const TextDesc& desc, const SkPoint& point, |
| const sk_sp<SkFontMgr>& fontmgr) { |
| const AdjustedText adjText(text, desc); |
| |
| 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); |
| } |
| |
| Shaper::Result Shaper::Shape(const SkString& text, const TextDesc& desc, const SkRect& box, |
| const sk_sp<SkFontMgr>& fontmgr) { |
| const AdjustedText adjText(text, desc); |
| |
| switch(desc.fResize) { |
| case ResizePolicy::kNone: |
| return ShapeImpl(adjText, desc, box, fontmgr); |
| case ResizePolicy::kScaleToFit: |
| return ShapeToFit(adjText, desc, box, fontmgr); |
| case ResizePolicy::kDownscaleToFit: { |
| SkSize size; |
| auto result = ShapeImpl(adjText, desc, box, fontmgr, &size); |
| |
| return result_fits(result, size, box, desc) |
| ? result |
| : ShapeToFit(adjText, desc, box, fontmgr); |
| } |
| } |
| |
| 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; |
| run_bounds.setBounds(fGlyphPos.data() + offset, SkToInt(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, |
| SkToInt(run.fSize), glyphBounds.data(), 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(SkToInt(run.fSize), |
| fGlyphIDs.data() + offset, |
| fGlyphPos.data() + offset, |
| 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 |