/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #define LOG_TAG "Minikin" #include #include #include #include #include #include // for debugging #include #include #include #include #include #include #include #include #include #include #include "FontLanguage.h" #include "FontLanguageListCache.h" #include "HbFontCache.h" #include "LayoutUtils.h" #include "MinikinInternal.h" using std::string; using std::vector; namespace minikin { const int kDirection_Mask = 0x1; struct LayoutContext { MinikinPaint paint; FontStyle style; std::vector hbFonts; // parallel to mFaces void clearHbFonts() { for (size_t i = 0; i < hbFonts.size(); i++) { hb_font_set_funcs(hbFonts[i], nullptr, nullptr, nullptr); hb_font_destroy(hbFonts[i]); } hbFonts.clear(); } }; // Layout cache datatypes class LayoutCacheKey { public: LayoutCacheKey(const std::shared_ptr& collection, const MinikinPaint& paint, FontStyle style, const uint16_t* chars, size_t start, size_t count, size_t nchars, bool dir) : mChars(chars), mNchars(nchars), mStart(start), mCount(count), mId(collection->getId()), mStyle(style), mSize(paint.size), mScaleX(paint.scaleX), mSkewX(paint.skewX), mLetterSpacing(paint.letterSpacing), mPaintFlags(paint.paintFlags), mHyphenEdit(paint.hyphenEdit), mIsRtl(dir), mHash(computeHash()) {} bool operator==(const LayoutCacheKey& other) const; android::hash_t hash() const { return mHash; } void copyText() { uint16_t* charsCopy = new uint16_t[mNchars]; memcpy(charsCopy, mChars, mNchars * sizeof(uint16_t)); mChars = charsCopy; } void freeText() { delete[] mChars; mChars = NULL; } void doLayout(Layout* layout, LayoutContext* ctx, const std::shared_ptr& collection) const { layout->mAdvances.resize(mCount, 0); ctx->clearHbFonts(); layout->doLayoutRun(mChars, mStart, mCount, mNchars, mIsRtl, ctx, collection); } private: const uint16_t* mChars; size_t mNchars; size_t mStart; size_t mCount; uint32_t mId; // for the font collection FontStyle mStyle; float mSize; float mScaleX; float mSkewX; float mLetterSpacing; int32_t mPaintFlags; HyphenEdit mHyphenEdit; bool mIsRtl; // Note: any fields added to MinikinPaint must also be reflected here. // TODO: language matching (possibly integrate into style) android::hash_t mHash; android::hash_t computeHash() const; }; class LayoutCache : private android::OnEntryRemoved { public: LayoutCache() : mCache(kMaxEntries) { mCache.setOnEntryRemovedListener(this); } void clear() { mCache.clear(); } Layout* get(LayoutCacheKey& key, LayoutContext* ctx, const std::shared_ptr& collection) { Layout* layout = mCache.get(key); if (layout == NULL) { key.copyText(); layout = new Layout(); key.doLayout(layout, ctx, collection); mCache.put(key, layout); } return layout; } private: // callback for OnEntryRemoved void operator()(LayoutCacheKey& key, Layout*& value) { key.freeText(); delete value; } android::LruCache mCache; // static const size_t kMaxEntries = LruCache::kUnlimitedCapacity; // TODO: eviction based on memory footprint; for now, we just use a constant // number of strings static const size_t kMaxEntries = 5000; }; static unsigned int disabledDecomposeCompatibility(hb_unicode_funcs_t*, hb_codepoint_t, hb_codepoint_t*, void*) { return 0; } class LayoutEngine { public: LayoutEngine() { unicodeFunctions = hb_unicode_funcs_create(hb_icu_get_unicode_funcs()); /* Disable the function used for compatibility decomposition */ hb_unicode_funcs_set_decompose_compatibility_func( unicodeFunctions, disabledDecomposeCompatibility, NULL, NULL); hbBuffer = hb_buffer_create(); hb_buffer_set_unicode_funcs(hbBuffer, unicodeFunctions); } hb_buffer_t* hbBuffer; hb_unicode_funcs_t* unicodeFunctions; LayoutCache layoutCache; static LayoutEngine& getInstance() { static LayoutEngine* instance = new LayoutEngine(); return *instance; } }; bool LayoutCacheKey::operator==(const LayoutCacheKey& other) const { return mId == other.mId && mStart == other.mStart && mCount == other.mCount && mStyle == other.mStyle && mSize == other.mSize && mScaleX == other.mScaleX && mSkewX == other.mSkewX && mLetterSpacing == other.mLetterSpacing && mPaintFlags == other.mPaintFlags && mHyphenEdit == other.mHyphenEdit && mIsRtl == other.mIsRtl && mNchars == other.mNchars && !memcmp(mChars, other.mChars, mNchars * sizeof(uint16_t)); } android::hash_t LayoutCacheKey::computeHash() const { uint32_t hash = android::JenkinsHashMix(0, mId); hash = android::JenkinsHashMix(hash, mStart); hash = android::JenkinsHashMix(hash, mCount); hash = android::JenkinsHashMix(hash, hash_type(mStyle)); hash = android::JenkinsHashMix(hash, hash_type(mSize)); hash = android::JenkinsHashMix(hash, hash_type(mScaleX)); hash = android::JenkinsHashMix(hash, hash_type(mSkewX)); hash = android::JenkinsHashMix(hash, hash_type(mLetterSpacing)); hash = android::JenkinsHashMix(hash, hash_type(mPaintFlags)); hash = android::JenkinsHashMix(hash, hash_type(mHyphenEdit.getHyphen())); hash = android::JenkinsHashMix(hash, hash_type(mIsRtl)); hash = android::JenkinsHashMixShorts(hash, mChars, mNchars); return android::JenkinsHashWhiten(hash); } android::hash_t hash_type(const LayoutCacheKey& key) { return key.hash(); } void MinikinRect::join(const MinikinRect& r) { if (isEmpty()) { set(r); } else if (!r.isEmpty()) { mLeft = std::min(mLeft, r.mLeft); mTop = std::min(mTop, r.mTop); mRight = std::max(mRight, r.mRight); mBottom = std::max(mBottom, r.mBottom); } } void Layout::reset() { mGlyphs.clear(); mFaces.clear(); mBounds.setEmpty(); mAdvances.clear(); mAdvance = 0; } static hb_position_t harfbuzzGetGlyphHorizontalAdvance(hb_font_t* /* hbFont */, void* fontData, hb_codepoint_t glyph, void* /* userData */) { MinikinPaint* paint = reinterpret_cast(fontData); float advance = paint->font->GetHorizontalAdvance(glyph, *paint); return 256 * advance + 0.5; } static hb_bool_t harfbuzzGetGlyphHorizontalOrigin(hb_font_t* /* hbFont */, void* /* fontData */, hb_codepoint_t /* glyph */, hb_position_t* /* x */, hb_position_t* /* y */, void* /* userData */) { // Just return true, following the way that Harfbuzz-FreeType // implementation does. return true; } hb_font_funcs_t* getHbFontFuncs(bool forColorBitmapFont) { assertMinikinLocked(); static hb_font_funcs_t* hbFuncs = nullptr; static hb_font_funcs_t* hbFuncsForColorBitmap = nullptr; hb_font_funcs_t** funcs = forColorBitmapFont ? &hbFuncs : &hbFuncsForColorBitmap; if (*funcs == nullptr) { *funcs = hb_font_funcs_create(); if (forColorBitmapFont) { // Don't override the h_advance function since we use HarfBuzz's // implementation for emoji for performance reasons. Note that it is // technically possible for a TrueType font to have outline and embedded // bitmap at the same time. We ignore modified advances of hinted outline // glyphs in that case. } else { // Override the h_advance function since we can't use HarfBuzz's // implemenation. It may return the wrong value if the font uses hinting // aggressively. hb_font_funcs_set_glyph_h_advance_func( *funcs, harfbuzzGetGlyphHorizontalAdvance, 0, 0); } hb_font_funcs_set_glyph_h_origin_func( *funcs, harfbuzzGetGlyphHorizontalOrigin, 0, 0); hb_font_funcs_make_immutable(*funcs); } return *funcs; } static bool isColorBitmapFont(hb_font_t* font) { hb_face_t* face = hb_font_get_face(font); HbBlob cbdt(hb_face_reference_table(face, HB_TAG('C', 'B', 'D', 'T'))); return cbdt.size() > 0; } static float HBFixedToFloat(hb_position_t v) { return scalbnf(v, -8); } static hb_position_t HBFloatToFixed(float v) { return scalbnf(v, +8); } void Layout::dump() const { for (size_t i = 0; i < mGlyphs.size(); i++) { const LayoutGlyph& glyph = mGlyphs[i]; std::cout << glyph.glyph_id << ": " << glyph.x << ", " << glyph.y << std::endl; } } int Layout::findFace(const FakedFont& face, LayoutContext* ctx) { unsigned int ix; for (ix = 0; ix < mFaces.size(); ix++) { if (mFaces[ix].font == face.font) { return ix; } } mFaces.push_back(face); // Note: ctx == NULL means we're copying from the cache, no need to create // corresponding hb_font object. if (ctx != NULL) { hb_font_t* font = getHbFontLocked(face.font); // Temporarily removed to fix advance integer rounding. // This is likely due to very old versions of harfbuzz and ICU. // hb_font_set_funcs(font, getHbFontFuncs(isColorBitmapFont(font)), // &ctx->paint, 0); ctx->hbFonts.push_back(font); } return ix; } static hb_script_t codePointToScript(hb_codepoint_t codepoint) { static hb_unicode_funcs_t* u = 0; if (!u) { u = LayoutEngine::getInstance().unicodeFunctions; } return hb_unicode_script(u, codepoint); } static hb_codepoint_t decodeUtf16(const uint16_t* chars, size_t len, ssize_t* iter) { const uint16_t v = chars[(*iter)++]; // test whether v in (0xd800..0xdfff), lead or trail surrogate if ((v & 0xf800) == 0xd800) { // test whether v in (0xd800..0xdbff), lead surrogate if (size_t(*iter) < len && (v & 0xfc00) == 0xd800) { const uint16_t v2 = chars[(*iter)++]; // test whether v2 in (0xdc00..0xdfff), trail surrogate if ((v2 & 0xfc00) == 0xdc00) { // (0xd800 0xdc00) in utf-16 maps to 0x10000 in ucs-32 const hb_codepoint_t delta = (0xd800 << 10) + 0xdc00 - 0x10000; return (((hb_codepoint_t)v) << 10) + v2 - delta; } (*iter) -= 1; return 0xFFFDu; } else { return 0xFFFDu; } } else { return v; } } static hb_script_t getScriptRun(const uint16_t* chars, size_t len, ssize_t* iter) { if (size_t(*iter) == len) { return HB_SCRIPT_UNKNOWN; } uint32_t cp = decodeUtf16(chars, len, iter); hb_script_t current_script = codePointToScript(cp); for (;;) { if (size_t(*iter) == len) break; const ssize_t prev_iter = *iter; cp = decodeUtf16(chars, len, iter); const hb_script_t script = codePointToScript(cp); if (script != current_script) { if (current_script == HB_SCRIPT_INHERITED || current_script == HB_SCRIPT_COMMON) { current_script = script; } else if (script == HB_SCRIPT_INHERITED || script == HB_SCRIPT_COMMON) { continue; } else { *iter = prev_iter; break; } } } if (current_script == HB_SCRIPT_INHERITED) { current_script = HB_SCRIPT_COMMON; } return current_script; } /** * Disable certain scripts (mostly those with cursive connection) from having * letterspacing applied. See https://github.com/behdad/harfbuzz/issues/64 for * more details. */ static bool isScriptOkForLetterspacing(hb_script_t script) { return !(script == HB_SCRIPT_ARABIC || script == HB_SCRIPT_NKO || script == HB_SCRIPT_PSALTER_PAHLAVI || script == HB_SCRIPT_MANDAIC || script == HB_SCRIPT_MONGOLIAN || script == HB_SCRIPT_PHAGS_PA || script == HB_SCRIPT_DEVANAGARI || script == HB_SCRIPT_BENGALI || script == HB_SCRIPT_GURMUKHI || script == HB_SCRIPT_MODI || script == HB_SCRIPT_SHARADA || script == HB_SCRIPT_SYLOTI_NAGRI || script == HB_SCRIPT_TIRHUTA || script == HB_SCRIPT_OGHAM); } class BidiText { public: class Iter { public: struct RunInfo { int32_t mRunStart; int32_t mRunLength; bool mIsRtl; }; Iter(UBiDi* bidi, size_t start, size_t end, size_t runIndex, size_t runCount, bool isRtl); bool operator!=(const Iter& other) const { return mIsEnd != other.mIsEnd || mNextRunIndex != other.mNextRunIndex || mBidi != other.mBidi; } const RunInfo& operator*() const { return mRunInfo; } const Iter& operator++() { updateRunInfo(); return *this; } private: UBiDi* const mBidi; bool mIsEnd; size_t mNextRunIndex; const size_t mRunCount; const int32_t mStart; const int32_t mEnd; RunInfo mRunInfo; void updateRunInfo(); }; BidiText(const uint16_t* buf, size_t start, size_t count, size_t bufSize, int bidiFlags); ~BidiText() { if (mBidi) { ubidi_close(mBidi); } } Iter begin() const { return Iter(mBidi, mStart, mEnd, 0, mRunCount, mIsRtl); } Iter end() const { return Iter(mBidi, mStart, mEnd, mRunCount, mRunCount, mIsRtl); } private: const size_t mStart; const size_t mEnd; const size_t mBufSize; UBiDi* mBidi; size_t mRunCount; bool mIsRtl; BidiText(const BidiText&) = delete; void operator=(const BidiText&) = delete; }; BidiText::Iter::Iter(UBiDi* bidi, size_t start, size_t end, size_t runIndex, size_t runCount, bool isRtl) : mBidi(bidi), mIsEnd(runIndex == runCount), mNextRunIndex(runIndex), mRunCount(runCount), mStart(start), mEnd(end), mRunInfo() { if (mRunCount == 1) { mRunInfo.mRunStart = start; mRunInfo.mRunLength = end - start; mRunInfo.mIsRtl = isRtl; mNextRunIndex = mRunCount; return; } updateRunInfo(); } void BidiText::Iter::updateRunInfo() { if (mNextRunIndex == mRunCount) { // All runs have been iterated. mIsEnd = true; return; } int32_t startRun = -1; int32_t lengthRun = -1; const UBiDiDirection runDir = ubidi_getVisualRun(mBidi, mNextRunIndex, &startRun, &lengthRun); mNextRunIndex++; if (startRun == -1 || lengthRun == -1) { ALOGE("invalid visual run"); // skip the invalid run. updateRunInfo(); return; } const int32_t runEnd = std::min(startRun + lengthRun, mEnd); mRunInfo.mRunStart = std::max(startRun, mStart); mRunInfo.mRunLength = runEnd - mRunInfo.mRunStart; if (mRunInfo.mRunLength <= 0) { // skip the empty run. updateRunInfo(); return; } mRunInfo.mIsRtl = (runDir == UBIDI_RTL); } BidiText::BidiText(const uint16_t* buf, size_t start, size_t count, size_t bufSize, int bidiFlags) : mStart(start), mEnd(start + count), mBufSize(bufSize), mBidi(NULL), mRunCount(1), mIsRtl((bidiFlags & kDirection_Mask) != 0) { if (bidiFlags == kBidi_Force_LTR || bidiFlags == kBidi_Force_RTL) { // force single run. return; } mBidi = ubidi_open(); if (!mBidi) { ALOGE("error creating bidi object"); return; } UErrorCode status = U_ZERO_ERROR; // Set callbacks to override bidi classes of new emoji ubidi_setClassCallback(mBidi, emojiBidiOverride, nullptr, nullptr, nullptr, &status); if (!U_SUCCESS(status)) { ALOGE("error setting bidi callback function, status = %d", status); return; } UBiDiLevel bidiReq = bidiFlags; if (bidiFlags == kBidi_Default_LTR) { bidiReq = UBIDI_DEFAULT_LTR; } else if (bidiFlags == kBidi_Default_RTL) { bidiReq = UBIDI_DEFAULT_RTL; } ubidi_setPara(mBidi, reinterpret_cast(buf), mBufSize, bidiReq, NULL, &status); if (!U_SUCCESS(status)) { ALOGE("error calling ubidi_setPara, status = %d", status); return; } const int paraDir = ubidi_getParaLevel(mBidi) & kDirection_Mask; const ssize_t rc = ubidi_countRuns(mBidi, &status); if (!U_SUCCESS(status) || rc < 0) { ALOGW("error counting bidi runs, status = %d", status); } if (!U_SUCCESS(status) || rc <= 1) { mIsRtl = (paraDir == kBidi_RTL); return; } mRunCount = rc; } void Layout::doLayout(const uint16_t* buf, size_t start, size_t count, size_t bufSize, bool isRtl, const FontStyle& style, const MinikinPaint& paint, const std::shared_ptr& collection) { std::lock_guard _l(gMinikinLock); LayoutContext ctx; ctx.style = style; ctx.paint = paint; reset(); mAdvances.resize(count, 0); doLayoutRunCached(buf, start, count, bufSize, isRtl, &ctx, start, collection, this, NULL); ctx.clearHbFonts(); } float Layout::measureText(const uint16_t* buf, size_t start, size_t count, size_t bufSize, bool isRtl, const FontStyle& style, const MinikinPaint& paint, const std::shared_ptr& collection, float* advances) { std::lock_guard _l(gMinikinLock); LayoutContext ctx; ctx.style = style; ctx.paint = paint; float advance = doLayoutRunCached(buf, start, count, bufSize, isRtl, &ctx, 0, collection, NULL, advances); ctx.clearHbFonts(); return advance; } float Layout::doLayoutRunCached( const uint16_t* buf, size_t start, size_t count, size_t bufSize, bool isRtl, LayoutContext* ctx, size_t dstStart, const std::shared_ptr& collection, Layout* layout, float* advances) { const uint32_t originalHyphen = ctx->paint.hyphenEdit.getHyphen(); float advance = 0; if (!isRtl) { // left to right size_t wordstart = start == bufSize ? start : getPrevWordBreakForCache(buf, start + 1, bufSize); size_t wordend; for (size_t iter = start; iter < start + count; iter = wordend) { wordend = getNextWordBreakForCache(buf, iter, bufSize); // Only apply hyphen to the first or last word in the string. uint32_t hyphen = originalHyphen; if (iter != start) { // Not the first word hyphen &= ~HyphenEdit::MASK_START_OF_LINE; } if (wordend < start + count) { // Not the last word hyphen &= ~HyphenEdit::MASK_END_OF_LINE; } ctx->paint.hyphenEdit = hyphen; size_t wordcount = std::min(start + count, wordend) - iter; advance += doLayoutWord(buf + wordstart, iter - wordstart, wordcount, wordend - wordstart, isRtl, ctx, iter - dstStart, collection, layout, advances ? advances + (iter - start) : advances); wordstart = wordend; } } else { // right to left size_t wordstart; size_t end = start + count; size_t wordend = end == 0 ? 0 : getNextWordBreakForCache(buf, end - 1, bufSize); for (size_t iter = end; iter > start; iter = wordstart) { wordstart = getPrevWordBreakForCache(buf, iter, bufSize); // Only apply hyphen to the first (rightmost) or last (leftmost) word in // the string. uint32_t hyphen = originalHyphen; if (wordstart > start) { // Not the first word hyphen &= ~HyphenEdit::MASK_START_OF_LINE; } if (iter != end) { // Not the last word hyphen &= ~HyphenEdit::MASK_END_OF_LINE; } ctx->paint.hyphenEdit = hyphen; size_t bufStart = std::max(start, wordstart); advance += doLayoutWord( buf + wordstart, bufStart - wordstart, iter - bufStart, wordend - wordstart, isRtl, ctx, bufStart - dstStart, collection, layout, advances ? advances + (bufStart - start) : advances); wordend = wordstart; } } return advance; } float Layout::doLayoutWord(const uint16_t* buf, size_t start, size_t count, size_t bufSize, bool isRtl, LayoutContext* ctx, size_t bufStart, const std::shared_ptr& collection, Layout* layout, float* advances) { LayoutCache& cache = LayoutEngine::getInstance().layoutCache; LayoutCacheKey key(collection, ctx->paint, ctx->style, buf, start, count, bufSize, isRtl); float wordSpacing = count == 1 && isWordSpace(buf[start]) ? ctx->paint.wordSpacing : 0; float advance; if (ctx->paint.skipCache()) { Layout layoutForWord; key.doLayout(&layoutForWord, ctx, collection); if (layout) { layout->appendLayout(&layoutForWord, bufStart, wordSpacing); } if (advances) { layoutForWord.getAdvances(advances); } advance = layoutForWord.getAdvance(); } else { Layout* layoutForWord = cache.get(key, ctx, collection); if (layout) { layout->appendLayout(layoutForWord, bufStart, wordSpacing); } if (advances) { layoutForWord->getAdvances(advances); } advance = layoutForWord->getAdvance(); } if (wordSpacing != 0) { advance += wordSpacing; if (advances) { advances[0] += wordSpacing; } } return advance; } static void addFeatures(const string& str, vector* features) { if (!str.size()) return; const char* start = str.c_str(); const char* end = start + str.size(); while (start < end) { static hb_feature_t feature; const char* p = strchr(start, ','); if (!p) p = end; /* We do not allow setting features on ranges. As such, reject any * setting that has non-universal range. */ if (hb_feature_from_string(start, p - start, &feature) && feature.start == 0 && feature.end == (unsigned int)-1) features->push_back(feature); start = p + 1; } } static const hb_codepoint_t CHAR_HYPHEN = 0x2010; /* HYPHEN */ static inline hb_codepoint_t determineHyphenChar(hb_codepoint_t preferredHyphen, hb_font_t* font) { hb_codepoint_t glyph; if (preferredHyphen == 0x058A /* ARMENIAN_HYPHEN */ || preferredHyphen == 0x05BE /* HEBREW PUNCTUATION MAQAF */ || preferredHyphen == 0x1400 /* CANADIAN SYLLABIC HYPHEN */) { if (hb_font_get_nominal_glyph(font, preferredHyphen, &glyph)) { return preferredHyphen; } else { // The original hyphen requested was not supported. Let's try and see if // the Unicode hyphen is supported. preferredHyphen = CHAR_HYPHEN; } } if (preferredHyphen == CHAR_HYPHEN) { /* HYPHEN */ // Fallback to ASCII HYPHEN-MINUS if the font didn't have a glyph for the // preferred hyphen. Note that we intentionally don't do anything special if // the font doesn't have a HYPHEN-MINUS either, so a tofu could be shown, // hinting towards something missing. if (!hb_font_get_nominal_glyph(font, preferredHyphen, &glyph)) { return 0x002D; // HYPHEN-MINUS } } return preferredHyphen; } static inline void addHyphenToHbBuffer(hb_buffer_t* buffer, hb_font_t* font, uint32_t hyphen, uint32_t cluster) { const uint32_t* hyphenStr = HyphenEdit::getHyphenString(hyphen); while (*hyphenStr != 0) { hb_codepoint_t hyphenChar = determineHyphenChar(*hyphenStr, font); hb_buffer_add(buffer, hyphenChar, cluster); hyphenStr++; } } // Returns the cluster value assigned to the first codepoint added to the // buffer, which can be used to translate cluster values returned by HarfBuzz to // input indices. static inline uint32_t addToHbBuffer(hb_buffer_t* buffer, const uint16_t* buf, size_t start, size_t count, size_t bufSize, ssize_t scriptRunStart, ssize_t scriptRunEnd, HyphenEdit hyphenEdit, hb_font_t* hbFont) { // Only hyphenate the very first script run for starting hyphens. const uint32_t startHyphen = (scriptRunStart == 0) ? hyphenEdit.getStart() : HyphenEdit::NO_EDIT; // Only hyphenate the very last script run for ending hyphens. const uint32_t endHyphen = (static_cast(scriptRunEnd) == count) ? hyphenEdit.getEnd() : HyphenEdit::NO_EDIT; // In the following code, we drop the pre-context and/or post-context if there // is a hyphen edit at that end. This is not absolutely necessary, since // HarfBuzz uses contexts only for joining scripts at the moment, e.g. to // determine if the first or last letter of a text range to shape should take // a joining form based on an adjacent letter or joiner (that comes from the // context). // // TODO: Revisit this for: // 1. Desperate breaks for joining scripts like Arabic (where it may be better // to keep // the context); // 2. Special features like start-of-word font features (not implemented in // HarfBuzz // yet). // We don't have any start-of-line replacement edit yet, so we don't need to // check for those. if (HyphenEdit::isInsertion(startHyphen)) { // A cluster value of zero guarantees that the inserted hyphen will be in // the same cluster with the next codepoint, since there is no pre-context. addHyphenToHbBuffer(buffer, hbFont, startHyphen, 0 /* cluster value */); } const uint16_t* hbText; int hbTextLength; unsigned int hbItemOffset; unsigned int hbItemLength = scriptRunEnd - scriptRunStart; // This is >= 1. const bool hasEndInsertion = HyphenEdit::isInsertion(endHyphen); const bool hasEndReplacement = HyphenEdit::isReplacement(endHyphen); if (hasEndReplacement) { // Skip the last code unit while copying the buffer for HarfBuzz if it's a // replacement. We don't need to worry about non-BMP characters yet since // replacements are only done for code units at the moment. hbItemLength -= 1; } if (startHyphen == HyphenEdit::NO_EDIT) { // No edit at the beginning. Use the whole pre-context. hbText = buf; hbItemOffset = start + scriptRunStart; } else { // There's an edit at the beginning. Drop the pre-context and start the // buffer at where we want to start shaping. hbText = buf + start + scriptRunStart; hbItemOffset = 0; } if (endHyphen == HyphenEdit::NO_EDIT) { // No edit at the end, use the whole post-context. hbTextLength = (buf + bufSize) - hbText; } else { // There is an edit at the end. Drop the post-context. hbTextLength = hbItemOffset + hbItemLength; } hb_buffer_add_utf16(buffer, hbText, hbTextLength, hbItemOffset, hbItemLength); unsigned int numCodepoints; hb_glyph_info_t* cpInfo = hb_buffer_get_glyph_infos(buffer, &numCodepoints); // Add the hyphen at the end, if there's any. if (hasEndInsertion || hasEndReplacement) { // When a hyphen is inserted, by assigning the added hyphen and the last // codepoint added to the HarfBuzz buffer to the same cluster, we can make // sure that they always remain in the same cluster, even if the last // codepoint gets merged into another cluster (for example when it's a // combining mark). // // When a replacement happens instead, we want it to get the cluster value // of the character it's replacing, which is one "codepoint length" larger // than the last cluster. But since the character replaced is always just // one code unit, we can just add 1. uint32_t hyphenCluster; if (numCodepoints == 0) { // Nothing was added to the HarfBuzz buffer. This can only happen if // we have a replacement that is replacing a one-code unit script run. hyphenCluster = 0; } else { hyphenCluster = cpInfo[numCodepoints - 1].cluster + (uint32_t)hasEndReplacement; } addHyphenToHbBuffer(buffer, hbFont, endHyphen, hyphenCluster); // Since we have just added to the buffer, cpInfo no longer necessarily // points to the right place. Refresh it. cpInfo = hb_buffer_get_glyph_infos(buffer, nullptr /* we don't need the size */); } return cpInfo[0].cluster; } void Layout::doLayoutRun(const uint16_t* buf, size_t start, size_t count, size_t bufSize, bool isRtl, LayoutContext* ctx, const std::shared_ptr& collection) { hb_buffer_t* buffer = LayoutEngine::getInstance().hbBuffer; vector items; collection->itemize(buf + start, count, ctx->style, &items); vector features; // Disable default-on non-required ligature features if letter-spacing // See http://dev.w3.org/csswg/css-text-3/#letter-spacing-property // "When the effective spacing between two characters is not zero (due to // either justification or a non-zero value of letter-spacing), user agents // should not apply optional ligatures." if (fabs(ctx->paint.letterSpacing) > 0.03) { static const hb_feature_t no_liga = {HB_TAG('l', 'i', 'g', 'a'), 0, 0, ~0u}; static const hb_feature_t no_clig = {HB_TAG('c', 'l', 'i', 'g'), 0, 0, ~0u}; features.push_back(no_liga); features.push_back(no_clig); } addFeatures(ctx->paint.fontFeatureSettings, &features); double size = ctx->paint.size; double scaleX = ctx->paint.scaleX; float x = mAdvance; float y = 0; for (int run_ix = isRtl ? items.size() - 1 : 0; isRtl ? run_ix >= 0 : run_ix < static_cast(items.size()); isRtl ? --run_ix : ++run_ix) { FontCollection::Run& run = items[run_ix]; if (run.fakedFont.font == NULL) { ALOGE("no font for run starting u+%04x length %d", buf[run.start], run.end - run.start); continue; } int font_ix = findFace(run.fakedFont, ctx); ctx->paint.font = mFaces[font_ix].font; ctx->paint.fakery = mFaces[font_ix].fakery; hb_font_t* hbFont = ctx->hbFonts[font_ix]; #ifdef VERBOSE_DEBUG ALOGD("Run %zu, font %d [%d:%d]", run_ix, font_ix, run.start, run.end); #endif hb_font_set_ppem(hbFont, size * scaleX, size); hb_font_set_scale(hbFont, HBFloatToFixed(size * scaleX), HBFloatToFixed(size)); const bool is_color_bitmap_font = isColorBitmapFont(hbFont); // TODO: if there are multiple scripts within a font in an RTL run, // we need to reorder those runs. This is unlikely with our current // font stack, but should be done for correctness. // Note: scriptRunStart and scriptRunEnd, as well as run.start and run.end, // run between 0 and count. ssize_t scriptRunEnd; for (ssize_t scriptRunStart = run.start; scriptRunStart < run.end; scriptRunStart = scriptRunEnd) { scriptRunEnd = scriptRunStart; hb_script_t script = getScriptRun(buf + start, run.end, &scriptRunEnd /* iterator */); // After the last line, scriptRunEnd is guaranteed to have increased, // since the only time getScriptRun does not increase its iterator is when // it has already reached the end of the buffer. But that can't happen, // since if we have already reached the end of the buffer, we should have // had (scriptRunEnd == run.end), which means (scriptRunStart == run.end) // which is impossible due to the exit condition of the for loop. So we // can be sure that scriptRunEnd > scriptRunStart. double letterSpace = 0.0; double letterSpaceHalfLeft = 0.0; double letterSpaceHalfRight = 0.0; if (ctx->paint.letterSpacing != 0.0 && isScriptOkForLetterspacing(script)) { letterSpace = ctx->paint.letterSpacing * size * scaleX; if ((ctx->paint.paintFlags & LinearTextFlag) == 0) { letterSpace = round(letterSpace); letterSpaceHalfLeft = floor(letterSpace * 0.5); } else { letterSpaceHalfLeft = letterSpace * 0.5; } letterSpaceHalfRight = letterSpace - letterSpaceHalfLeft; } hb_buffer_clear_contents(buffer); hb_buffer_set_script(buffer, script); hb_buffer_set_direction(buffer, isRtl ? HB_DIRECTION_RTL : HB_DIRECTION_LTR); const FontLanguages& langList = FontLanguageListCache::getById(ctx->style.getLanguageListId()); if (langList.size() != 0) { const FontLanguage* hbLanguage = &langList[0]; for (size_t i = 0; i < langList.size(); ++i) { if (langList[i].supportsHbScript(script)) { hbLanguage = &langList[i]; break; } } hb_buffer_set_language(buffer, hbLanguage->getHbLanguage()); } const uint32_t clusterStart = addToHbBuffer(buffer, buf, start, count, bufSize, scriptRunStart, scriptRunEnd, ctx->paint.hyphenEdit, hbFont); hb_shape(hbFont, buffer, features.empty() ? NULL : &features[0], features.size()); unsigned int numGlyphs; hb_glyph_info_t* info = hb_buffer_get_glyph_infos(buffer, &numGlyphs); hb_glyph_position_t* positions = hb_buffer_get_glyph_positions(buffer, NULL); // At this point in the code, the cluster values in the info buffer // correspond to the input characters with some shift. The cluster value // clusterStart corresponds to the first character passed to HarfBuzz, // which is at buf[start + scriptRunStart] whose advance needs to be saved // into mAdvances[scriptRunStart]. So cluster values need to be reduced by // (clusterStart - scriptRunStart) to get converted to indices of // mAdvances. const ssize_t clusterOffset = clusterStart - scriptRunStart; if (numGlyphs) { mAdvances[info[0].cluster - clusterOffset] += letterSpaceHalfLeft; x += letterSpaceHalfLeft; } for (unsigned int i = 0; i < numGlyphs; i++) { #ifdef VERBOSE_DEBUG ALOGD("%d %d %d %d", positions[i].x_advance, positions[i].y_advance, positions[i].x_offset, positions[i].y_offset); ALOGD("DoLayout %u: %f; %d, %d", info[i].codepoint, HBFixedToFloat(positions[i].x_advance), positions[i].x_offset, positions[i].y_offset); #endif if (i > 0 && info[i - 1].cluster != info[i].cluster) { mAdvances[info[i - 1].cluster - clusterOffset] += letterSpaceHalfRight; mAdvances[info[i].cluster - clusterOffset] += letterSpaceHalfLeft; x += letterSpace; } hb_codepoint_t glyph_ix = info[i].codepoint; float xoff = HBFixedToFloat(positions[i].x_offset); float yoff = -HBFixedToFloat(positions[i].y_offset); xoff += yoff * ctx->paint.skewX; LayoutGlyph glyph = { font_ix, glyph_ix, x + xoff, y + yoff, static_cast(info[i].cluster - clusterOffset)}; mGlyphs.push_back(glyph); float xAdvance = HBFixedToFloat(positions[i].x_advance); if ((ctx->paint.paintFlags & LinearTextFlag) == 0) { xAdvance = roundf(xAdvance); } MinikinRect glyphBounds; hb_glyph_extents_t extents = {}; if (is_color_bitmap_font && hb_font_get_glyph_extents(hbFont, glyph_ix, &extents)) { // Note that it is technically possible for a TrueType font to have // outline and embedded bitmap at the same time. We ignore modified // bbox of hinted outline glyphs in that case. glyphBounds.mLeft = roundf(HBFixedToFloat(extents.x_bearing)); glyphBounds.mTop = roundf(HBFixedToFloat(-extents.y_bearing)); glyphBounds.mRight = roundf(HBFixedToFloat(extents.x_bearing + extents.width)); glyphBounds.mBottom = roundf(HBFixedToFloat(-extents.y_bearing - extents.height)); } else { ctx->paint.font->GetBounds(&glyphBounds, glyph_ix, ctx->paint); } glyphBounds.offset(x + xoff, y + yoff); mBounds.join(glyphBounds); if (static_cast(info[i].cluster - clusterOffset) < count) { mAdvances[info[i].cluster - clusterOffset] += xAdvance; } else { ALOGE("cluster %zu (start %zu) out of bounds of count %zu", info[i].cluster - clusterOffset, start, count); } x += xAdvance; } if (numGlyphs) { mAdvances[info[numGlyphs - 1].cluster - clusterOffset] += letterSpaceHalfRight; x += letterSpaceHalfRight; } } } mAdvance = x; } void Layout::appendLayout(Layout* src, size_t start, float extraAdvance) { int fontMapStack[16]; int* fontMap; if (src->mFaces.size() < sizeof(fontMapStack) / sizeof(fontMapStack[0])) { fontMap = fontMapStack; } else { fontMap = new int[src->mFaces.size()]; } for (size_t i = 0; i < src->mFaces.size(); i++) { int font_ix = findFace(src->mFaces[i], NULL); fontMap[i] = font_ix; } // LibTxt: Changed x0 from int to float to prevent rounding that causes text // jitter. float x0 = mAdvance; for (size_t i = 0; i < src->mGlyphs.size(); i++) { LayoutGlyph& srcGlyph = src->mGlyphs[i]; int font_ix = fontMap[srcGlyph.font_ix]; unsigned int glyph_id = srcGlyph.glyph_id; float x = x0 + srcGlyph.x; float y = srcGlyph.y; LayoutGlyph glyph = {font_ix, glyph_id, x, y, static_cast(srcGlyph.cluster + start)}; mGlyphs.push_back(glyph); } for (size_t i = 0; i < src->mAdvances.size(); i++) { mAdvances[i + start] = src->mAdvances[i]; if (i == 0) mAdvances[i + start] += extraAdvance; } MinikinRect srcBounds(src->mBounds); srcBounds.offset(x0, 0); mBounds.join(srcBounds); mAdvance += src->mAdvance + extraAdvance; if (fontMap != fontMapStack) { delete[] fontMap; } } size_t Layout::nGlyphs() const { return mGlyphs.size(); } const MinikinFont* Layout::getFont(int i) const { const LayoutGlyph& glyph = mGlyphs[i]; return mFaces[glyph.font_ix].font; } FontFakery Layout::getFakery(int i) const { const LayoutGlyph& glyph = mGlyphs[i]; return mFaces[glyph.font_ix].fakery; } unsigned int Layout::getGlyphId(int i) const { const LayoutGlyph& glyph = mGlyphs[i]; return glyph.glyph_id; } // libtxt extension unsigned int Layout::getGlyphCluster(int i) const { const LayoutGlyph& glyph = mGlyphs[i]; return glyph.cluster; } float Layout::getX(int i) const { const LayoutGlyph& glyph = mGlyphs[i]; return glyph.x; } float Layout::getY(int i) const { const LayoutGlyph& glyph = mGlyphs[i]; return glyph.y; } float Layout::getAdvance() const { return mAdvance; } void Layout::getAdvances(float* advances) { memcpy(advances, &mAdvances[0], mAdvances.size() * sizeof(float)); } void Layout::getBounds(MinikinRect* bounds) const { bounds->set(mBounds); } void Layout::purgeCaches() { std::lock_guard _l(gMinikinLock); LayoutCache& layoutCache = LayoutEngine::getInstance().layoutCache; layoutCache.clear(); purgeHbFontCacheLocked(); } } // namespace minikin