| /* | |
| * Copyright 2020 Google LLC | |
| * | |
| * Use of this source code is governed by a BSD-style license that can be | |
| * found in the LICENSE file. | |
| */ | |
| #include "modules/skshaper/include/SkShaper.h" | |
| #include "modules/skshaper/include/SkShaper_coretext.h" | |
| #ifdef SK_BUILD_FOR_MAC | |
| #import <ApplicationServices/ApplicationServices.h> | |
| #endif | |
| #ifdef SK_BUILD_FOR_IOS | |
| #include <CoreText/CoreText.h> | |
| #include <CoreText/CTFontManager.h> | |
| #include <CoreGraphics/CoreGraphics.h> | |
| #include <CoreFoundation/CoreFoundation.h> | |
| #endif | |
| #include "include/ports/SkTypeface_mac.h" | |
| #include "include/private/SkTemplates.h" | |
| #include "src/core/SkFontPriv.h" | |
| #include "src/core/SkUTF.h" | |
| #include "src/utils/mac/SkCGBase.h" | |
| #include "src/utils/mac/SkUniqueCFRef.h" | |
| #include <vector> | |
| #include <utility> | |
| using namespace skia_private; | |
| class SkShaper_CoreText final : public SkShaper { | |
| public: | |
| explicit SkShaper_CoreText(SkShapers::CT::LineBreakMode lbm) : fLineBreakMode(lbm) {} | |
| private: | |
| #if !defined(SK_DISABLE_LEGACY_SKSHAPER_FUNCTIONS) | |
| void shape(const char* utf8, size_t utf8Bytes, | |
| const SkFont& srcFont, | |
| bool leftToRight, | |
| SkScalar width, | |
| RunHandler*) const override; | |
| void shape(const char* utf8, size_t utf8Bytes, | |
| FontRunIterator&, | |
| BiDiRunIterator&, | |
| ScriptRunIterator&, | |
| LanguageRunIterator&, | |
| SkScalar width, | |
| RunHandler*) const override; | |
| #endif // !defined(SK_DISABLE_LEGACY_SKSHAPER_FUNCTIONS) | |
| void shape(const char* utf8, size_t utf8Bytes, | |
| FontRunIterator&, | |
| BiDiRunIterator&, | |
| ScriptRunIterator&, | |
| LanguageRunIterator&, | |
| const Feature*, size_t featureSize, | |
| SkScalar width, | |
| RunHandler*) const override; | |
| void shape(SkSpan<const char> utf8, | |
| FontRunIterator&, | |
| BiDiRunIterator&, | |
| ScriptRunIterator&, | |
| LanguageRunIterator&, | |
| SkSpan<const Feature>, | |
| const Options&, | |
| RunHandler*) const override; | |
| const SkShapers::CT::LineBreakMode fLineBreakMode; | |
| }; | |
| // CTFramesetter/CTFrame can do this, but require version 10.14 | |
| class LineBreakIter { | |
| const CTTypesetterRef fTypesetter; | |
| const double fWidth; | |
| CFIndex fStart = 0; | |
| const SkUniqueCFRef<CFStringTokenizerRef> fWordTokenizer; | |
| static SkUniqueCFRef<CFStringTokenizerRef> MakeWordTokenizer(CFStringRef txt, | |
| SkShapers::CT::LineBreakMode lbm) { | |
| if (lbm != SkShapers::CT::LineBreakMode::kStrict) { | |
| return nullptr; | |
| } | |
| // TODO: plumb a locale arg for more accurate results with complex scripts? | |
| SkUniqueCFRef<CFLocaleRef> locale(CFLocaleCopyCurrent()); | |
| CFRange full_range = CFRangeMake(0, CFStringGetLength(txt)); | |
| SkUniqueCFRef<CFStringTokenizerRef> tokenizer( | |
| CFStringTokenizerCreate(kCFAllocatorDefault, txt, | |
| full_range, kCFStringTokenizerUnitWord, locale.get())); | |
| // Advance to the first word. | |
| CFStringTokenizerAdvanceToNextToken(tokenizer.get()); | |
| return tokenizer; | |
| } | |
| // If the line range happens to break a word, return the range padding needed to include | |
| // the full word. This is only done for LineBreakMode::kStrict. | |
| CFIndex computeWordBreakPadding(const CFRange line_range) { | |
| if (!fWordTokenizer) { | |
| return 0; | |
| } | |
| CFRange word_range = CFStringTokenizerGetCurrentTokenRange(fWordTokenizer.get()); | |
| if (word_range.location == kCFNotFound) { | |
| // No words left. | |
| return 0; | |
| } | |
| // Consume words until past current line. | |
| const CFIndex line_end = line_range.location + line_range.length; | |
| while (word_range.location < line_end) { | |
| // Do we have a word break at the end of the current line? | |
| const CFIndex word_end = word_range.location + word_range.length; | |
| if (word_range.location < line_end && line_end < word_end) { | |
| // We should never see a word break for a word starting on the previous line. | |
| SkASSERT(word_range.location >= line_range.location); | |
| // Pad the current line with the rest of this word. | |
| return word_end - line_end; | |
| } | |
| if (CFStringTokenizerAdvanceToNextToken(fWordTokenizer.get()) == | |
| kCFStringTokenizerTokenNone) { | |
| // all words consumed, this is the end of the string | |
| break; | |
| } | |
| word_range = CFStringTokenizerGetCurrentTokenRange(fWordTokenizer.get()); | |
| } | |
| // No word breaks -> no padding. | |
| return 0; | |
| } | |
| public: | |
| LineBreakIter(CFStringRef txt, CTTypesetterRef ts, SkScalar width, | |
| SkShapers::CT::LineBreakMode lbm) | |
| : fTypesetter(ts), fWidth(width), fWordTokenizer(MakeWordTokenizer(txt, lbm)) {} | |
| SkUniqueCFRef<CTLineRef> nextLine() { | |
| CFRange line_range {fStart, CTTypesetterSuggestLineBreak(fTypesetter, fStart, fWidth)}; | |
| // Pad to avoid word breaks if needed. | |
| line_range.length += computeWordBreakPadding(line_range); | |
| if (!line_range.length) { | |
| return nullptr; | |
| } | |
| fStart += line_range.length; | |
| return SkUniqueCFRef<CTLineRef>(CTTypesetterCreateLine(fTypesetter, line_range)); | |
| } | |
| }; | |
| [[maybe_unused]] static void dict_add_double(CFMutableDictionaryRef d, const void* name, double value) { | |
| SkUniqueCFRef<CFNumberRef> number( | |
| CFNumberCreate(kCFAllocatorDefault, kCFNumberDoubleType, &value)); | |
| CFDictionaryAddValue(d, name, number.get()); | |
| } | |
| static SkUniqueCFRef<CTFontRef> create_ctfont_from_font(const SkFont& font) { | |
| auto typeface = font.getTypeface(); | |
| auto ctfont = SkTypeface_GetCTFontRef(typeface); | |
| if (!ctfont) { | |
| return nullptr; | |
| } | |
| return SkUniqueCFRef<CTFontRef>( | |
| CTFontCreateCopyWithAttributes(ctfont, font.getSize(), nullptr, nullptr)); | |
| } | |
| static SkFont run_to_font(CTRunRef run, const SkFont& orig) { | |
| CFDictionaryRef attr = CTRunGetAttributes(run); | |
| CTFontRef ct = (CTFontRef)CFDictionaryGetValue(attr, kCTFontAttributeName); | |
| if (!ct) { | |
| SkDebugf("no ctfont in Run Attributes\n"); | |
| CFShow(attr); | |
| return orig; | |
| } | |
| // Do I need to add a local cache, or allow the caller to manage this lookup? | |
| SkFont font(orig); | |
| font.setTypeface(SkMakeTypefaceFromCTFont(ct)); | |
| return font; | |
| } | |
| namespace { | |
| class UTF16ToUTF8IndicesMap { | |
| public: | |
| /** Builds a UTF-16 to UTF-8 indices map; the text is not retained | |
| * @return true if successful | |
| */ | |
| bool setUTF8(const char* utf8, size_t size) { | |
| SkASSERT(utf8 != nullptr); | |
| if (!SkTFitsIn<int32_t>(size)) { | |
| SkDEBUGF("UTF16ToUTF8IndicesMap: text too long"); | |
| return false; | |
| } | |
| auto utf16Size = SkUTF::UTF8ToUTF16(nullptr, 0, utf8, size); | |
| if (utf16Size < 0) { | |
| SkDEBUGF("UTF16ToUTF8IndicesMap: Invalid utf8 input"); | |
| return false; | |
| } | |
| // utf16Size+1 to also store the size | |
| fUtf16ToUtf8Indices = std::vector<size_t>(utf16Size + 1); | |
| auto utf16 = fUtf16ToUtf8Indices.begin(); | |
| auto utf8Begin = utf8, utf8End = utf8 + size; | |
| while (utf8Begin < utf8End) { | |
| *utf16 = utf8Begin - utf8; | |
| utf16 += SkUTF::ToUTF16(SkUTF::NextUTF8(&utf8Begin, utf8End), nullptr); | |
| } | |
| *utf16 = size; | |
| return true; | |
| } | |
| size_t mapIndex(size_t index) const { | |
| SkASSERT(index < fUtf16ToUtf8Indices.size()); | |
| return fUtf16ToUtf8Indices[index]; | |
| } | |
| std::pair<size_t, size_t> mapRange(size_t start, size_t size) const { | |
| auto utf8Start = mapIndex(start); | |
| return {utf8Start, mapIndex(start + size) - utf8Start}; | |
| } | |
| private: | |
| std::vector<size_t> fUtf16ToUtf8Indices; | |
| }; | |
| } // namespace | |
| // kCTTrackingAttributeName not available until 10.12 | |
| const CFStringRef kCTTracking_AttributeName = CFSTR("CTTracking"); | |
| #if !defined(SK_DISABLE_LEGACY_SKSHAPER_FUNCTIONS) | |
| void SkShaper_CoreText::shape(const char* utf8, | |
| size_t utf8Bytes, | |
| FontRunIterator& font, | |
| BiDiRunIterator& bidi, | |
| ScriptRunIterator& script, | |
| LanguageRunIterator& lang, | |
| SkScalar width, | |
| RunHandler* handler) const { | |
| return this->shape(utf8, utf8Bytes, font, bidi, script, lang, nullptr, 0, width, handler); | |
| } | |
| void SkShaper_CoreText::shape(const char* utf8, | |
| size_t utf8Bytes, | |
| const SkFont& font, | |
| bool, | |
| SkScalar width, | |
| RunHandler* handler) const { | |
| std::unique_ptr<FontRunIterator> fontRuns( | |
| MakeFontMgrRunIterator(utf8, utf8Bytes, font, nullptr)); | |
| if (!fontRuns) { | |
| return; | |
| } | |
| // bidi, script, and lang are all unused so we can construct them with empty data. | |
| TrivialBiDiRunIterator bidi{0, 0}; | |
| TrivialScriptRunIterator script{0, 0}; | |
| TrivialLanguageRunIterator lang{nullptr, 0}; | |
| return this->shape(utf8, utf8Bytes, *fontRuns, bidi, script, lang, nullptr, 0, width, handler); | |
| } | |
| #endif // !defined(SK_DISABLE_LEGACY_SKSHAPER_FUNCTIONS) | |
| void SkShaper_CoreText::shape(const char* utf8, | |
| size_t utf8Bytes, | |
| FontRunIterator& fontRuns, | |
| BiDiRunIterator& bidi, | |
| ScriptRunIterator& script, | |
| LanguageRunIterator& lang, | |
| const Feature* features, size_t featuresSize, | |
| SkScalar width, | |
| RunHandler* handler) const { | |
| return this->shape({utf8, utf8Bytes}, fontRuns, bidi, script, lang, {features, featuresSize}, | |
| { .width = width}, handler); | |
| } | |
| void SkShaper_CoreText::shape(SkSpan<const char> utf8, | |
| FontRunIterator& fontRuns, | |
| BiDiRunIterator&, | |
| ScriptRunIterator&, | |
| LanguageRunIterator&, | |
| SkSpan<const Feature>, | |
| const Options& opts, | |
| RunHandler* handler) const { | |
| SkFont font; | |
| if (!fontRuns.atEnd()) { | |
| fontRuns.consume(); | |
| font = fontRuns.currentFont(); | |
| } | |
| SkUniqueCFRef<CFStringRef> textString( | |
| CFStringCreateWithBytes(kCFAllocatorDefault, (const uint8_t*)utf8.data(), utf8.size(), | |
| kCFStringEncodingUTF8, false)); | |
| UTF16ToUTF8IndicesMap utf8IndicesMap; | |
| if (!utf8IndicesMap.setUTF8(utf8.data(), utf8.size())) { | |
| return; | |
| } | |
| SkUniqueCFRef<CTFontRef> ctfont = create_ctfont_from_font(font); | |
| if (!ctfont) { | |
| return; | |
| } | |
| SkUniqueCFRef<CFMutableDictionaryRef> attr( | |
| CFDictionaryCreateMutable(kCFAllocatorDefault, 0, | |
| &kCFTypeDictionaryKeyCallBacks, | |
| &kCFTypeDictionaryValueCallBacks)); | |
| CFDictionaryAddValue(attr.get(), kCTFontAttributeName, ctfont.get()); | |
| if (opts.tracking) { | |
| // Tracking is specified in em units, while CoreText expects absolute values. | |
| dict_add_double(attr.get(), kCTTracking_AttributeName, opts.tracking * font.getSize()); | |
| } | |
| if ((false)) { | |
| // trying to see what these affect | |
| dict_add_double(attr.get(), kCTTracking_AttributeName, 1); | |
| dict_add_double(attr.get(), kCTKernAttributeName, 0.0); | |
| } | |
| SkUniqueCFRef<CFAttributedStringRef> attrString( | |
| CFAttributedStringCreate(kCFAllocatorDefault, textString.get(), attr.get())); | |
| SkUniqueCFRef<CTTypesetterRef> typesetter( | |
| CTTypesetterCreateWithAttributedString(attrString.get())); | |
| // We have to compute RunInfos in a loop, and then reuse them in a 2nd loop, | |
| // so we store them in an array (we reuse the array's storage for each line). | |
| std::vector<SkFont> fontStorage; | |
| std::vector<SkShaper::RunHandler::RunInfo> infos; | |
| LineBreakIter iter(textString.get(), typesetter.get(), opts.width, fLineBreakMode); | |
| while (SkUniqueCFRef<CTLineRef> line = iter.nextLine()) { | |
| CFArrayRef run_array = CTLineGetGlyphRuns(line.get()); | |
| CFIndex runCount = CFArrayGetCount(run_array); | |
| if (runCount == 0) { | |
| continue; | |
| } | |
| handler->beginLine(); | |
| fontStorage.clear(); | |
| fontStorage.reserve(runCount); // ensure the refs won't get invalidated | |
| infos.clear(); | |
| for (CFIndex j = 0; j < runCount; ++j) { | |
| CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(run_array, j); | |
| CFIndex runGlyphs = CTRunGetGlyphCount(run); | |
| SkASSERT(sizeof(CGGlyph) == sizeof(uint16_t)); | |
| AutoSTArray<4096, CGSize> advances(runGlyphs); | |
| CTRunGetAdvances(run, {0, runGlyphs}, advances.data()); | |
| SkScalar adv = 0; | |
| for (CFIndex k = 0; k < runGlyphs; ++k) { | |
| adv += advances[k].width; | |
| } | |
| CFRange cfRange = CTRunGetStringRange(run); | |
| auto range = utf8IndicesMap.mapRange(cfRange.location, cfRange.length); | |
| fontStorage.push_back(run_to_font(run, font)); | |
| infos.push_back(SkShaper::RunHandler::RunInfo{ | |
| fontStorage.back(), // info just stores a ref to the font | |
| 0, // TODO: need fBidiLevel | |
| 0, // TODO: need fScript | |
| "", // TODO: need fLanguage | |
| {adv, 0}, | |
| (size_t)runGlyphs, | |
| {range.first, range.second}, | |
| }); | |
| handler->runInfo(infos.back()); | |
| } | |
| handler->commitRunInfo(); | |
| // Now loop through again and fill in the buffers | |
| SkScalar lineAdvance = 0; | |
| for (CFIndex j = 0; j < runCount; ++j) { | |
| const auto& info = infos[j]; | |
| auto buffer = handler->runBuffer(info); | |
| CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(run_array, j); | |
| CFIndex runGlyphs = info.glyphCount; | |
| SkASSERT(CTRunGetGlyphCount(run) == (CFIndex)info.glyphCount); | |
| CTRunGetGlyphs(run, {0, runGlyphs}, buffer.glyphs); | |
| AutoSTArray<4096, CGPoint> positions(runGlyphs); | |
| CTRunGetPositions(run, {0, runGlyphs}, positions.data()); | |
| AutoSTArray<4096, CFIndex> indices; | |
| if (buffer.clusters) { | |
| indices.reset(runGlyphs); | |
| CTRunGetStringIndices(run, {0, runGlyphs}, indices.data()); | |
| } | |
| for (CFIndex k = 0; k < runGlyphs; ++k) { | |
| buffer.positions[k] = { | |
| buffer.point.fX + SkScalarFromCGFloat(positions[k].x) - lineAdvance, | |
| buffer.point.fY, | |
| }; | |
| if (buffer.offsets) { | |
| buffer.offsets[k] = {0, 0}; // offset relative to the origin for this glyph | |
| } | |
| if (buffer.clusters) { | |
| buffer.clusters[k] = utf8IndicesMap.mapIndex(indices[k]); | |
| } | |
| } | |
| handler->commitRunBuffer(info); | |
| lineAdvance += info.fAdvance.fX; | |
| } | |
| handler->commitLine(); | |
| } | |
| } | |
| namespace SkShapers::CT { | |
| std::unique_ptr<SkShaper> CoreText(LineBreakMode lbm) { | |
| return std::make_unique<SkShaper_CoreText>(lbm); | |
| } | |
| } // namespace SkShapers::CT |