blob: b657752b7d8883bad1bcd991ed08e1be0311b6ad [file]
/*
* Copyright 2026 Google LLC
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "include/core/SkSpan.h"
#include "include/core/SkString.h"
#include "include/core/SkTypes.h"
#include "include/private/SkAssert.h"
#include "include/private/SkTArray.h"
#include "modules/skunicode/include/SkUnicode.h"
#include "modules/skunicode/include/SkUnicode_apple.h"
#include "modules/skunicode/src/SkBidiFactory_icu_subset.h"
#include "modules/skunicode/src/SkUnicode_hardcoded.h"
#include "modules/skunicode/src/SkUnicode_icu_bidi.h"
#include "src/core/SkBitmaskEnum.h"
#include "src/core/SkUTF.h"
#include "src/utils/mac/SkUniqueCFRef.h"
#include <CoreFoundation/CoreFoundation.h>
#include <algorithm>
#include <memory>
#include <string>
#include <utility>
#include <vector>
using namespace skia_private;
namespace {
// Wrap caller-owned buffers with kCFAllocatorNull to avoid copying text when the encoding is valid
SkUniqueCFRef<CFStringRef> MakeCFStringNoCopy(const char utf8[], int utf8Units) {
return SkUniqueCFRef<CFStringRef>(CFStringCreateWithBytesNoCopy(kCFAllocatorDefault,
(const UInt8*)utf8,
utf8Units,
kCFStringEncodingUTF8,
false,
kCFAllocatorNull));
}
SkUniqueCFRef<CFStringRef> MakeCFStringNoCopy(const char16_t utf16[], int utf16Units) {
return SkUniqueCFRef<CFStringRef>(CFStringCreateWithCharactersNoCopy(
kCFAllocatorDefault, (const UniChar*)utf16, utf16Units, kCFAllocatorNull));
}
// Returning nullptr instructs CoreFoundation to use the default locale without allocating
SkUniqueCFRef<CFLocaleRef> MakeCFLocale(const char* locale) {
if (locale == nullptr || locale[0] == '\0') {
return nullptr;
}
SkUniqueCFRef<CFStringRef> locStr(
CFStringCreateWithCString(kCFAllocatorDefault, locale, kCFStringEncodingUTF8));
if (!locStr) {
return nullptr;
}
return SkUniqueCFRef<CFLocaleRef>(CFLocaleCreate(kCFAllocatorDefault, locStr.get()));
}
// CoreFoundation reports boundaries in UTF-16 code units; build an inline UTF-16-to-UTF-8 offset
// table once per string
struct Utf16ToUtf8Mapper {
STArray<256, size_t> fU16ToU8;
int fUtf8Units;
Utf16ToUtf8Mapper(const char utf8[], int utf8Units) : fUtf8Units(utf8Units) {
SkUnicode::extractUtfConversionMapping(
SkSpan<const char>(utf8, utf8Units),
[&](size_t u8) { fU16ToU8.push_back(u8); },
[](size_t) {});
}
// Clamp UTF-16 indices to [0, fUtf8Units] so past-the-end CFRange endpoints map to the buffer end
SkUnicode::Position operator()(CFIndex pos16) const {
if (pos16 <= 0) {
return 0;
}
if (SkToInt(pos16) < fU16ToU8.size()) {
return SkTo<SkUnicode::Position>(fU16ToU8[pos16]);
}
return fUtf8Units;
}
};
// Tokenize [0, len16) with CFStringTokenizer and invoke callback with each token's UTF-16 CFRange
template <typename Callback>
bool Tokenize(CFStringRef str,
CFIndex len16,
CFOptionFlags unit,
const char* locale,
Callback&& callback) {
if (len16 <= 0 || !str) {
return true;
}
SkUniqueCFRef<CFLocaleRef> cfLoc = MakeCFLocale(locale);
SkUniqueCFRef<CFStringTokenizerRef> tokenizer(CFStringTokenizerCreate(
kCFAllocatorDefault, str, CFRangeMake(0, len16), unit, cfLoc.get()));
if (!tokenizer) {
return false;
}
CFStringTokenizerTokenType tokenType = CFStringTokenizerAdvanceToNextToken(tokenizer.get());
// Bound iteration count so degenerate zero-length tokens cannot cause an infinite loop
CFIndex guard = 0;
const CFIndex maxSteps = 2 * len16 + 2;
while (tokenType != kCFStringTokenizerTokenNone && ++guard <= maxSteps) {
callback(CFStringTokenizerGetCurrentTokenRange(tokenizer.get()));
tokenType = CFStringTokenizerAdvanceToNextToken(tokenizer.get());
}
return true;
}
// Emit start-of-cluster and end-of-text UTF-16 offsets with guaranteed forward progress even on
// zero-length ranges from malformed surrogates
template <typename Callback>
void ForEachGraphemeBoundary(CFStringRef str, CFIndex len16, Callback&& callback) {
CFIndex idx = 0;
while (idx < len16) {
CFRange r = CFStringGetRangeOfComposedCharactersAtIndex(str, idx);
callback(r.location);
CFIndex next = std::max(idx + 1, r.location + std::max<CFIndex>(r.length, 1));
// CFStringGetRangeOfComposedCharactersAtIndex splits CRLF; coalesce per UAX #29 GB3
if (next < len16 &&
CFStringGetCharacterAtIndex(str, idx) == '\r' &&
CFStringGetCharacterAtIndex(str, next) == '\n') {
++next;
}
idx = next;
}
callback(len16);
}
class SkBreakIterator_apple : public SkBreakIterator {
public:
SkBreakIterator_apple(SkUnicode* unicode, SkUnicode::BreakType type, const char* locale)
: fUnicode(unicode), fType(type), fLocale(locale ? locale : "") {}
~SkBreakIterator_apple() override = default;
Position first() override { return fBreaks.empty() ? 0 : fBreaks[(fIndex = 0)]; }
Position current() override { return fBreaks.empty() ? 0 : fBreaks[fIndex]; }
Position next() override {
if (fIndex + 1 < SkTo<Position>(fBreaks.size())) {
return fBreaks[++fIndex];
}
fIndex = fBreaks.size();
return -1;
}
Status status() override {
if (fIndex < SkTo<Position>(fStatuses.size())) {
return fStatuses[fIndex];
}
return 0;
}
bool isDone() override { return fIndex >= SkTo<Position>(fBreaks.size()); }
bool setText(const char utftext8[], int utf8Units) override {
this->reset();
// Reject negative lengths or null non-empty buffers
if (utf8Units < 0 || (utf8Units > 0 && !utftext8)) {
return false;
}
// Empty inputs still emit a boundary at 0 to match ICU BreakIterator::first() and
// SkParagraph empty-text expectations
if (utf8Units == 0) {
this->addBreak(0, fType == SkUnicode::BreakType::kLines
? SkUnicode::CodeUnitFlags::kSoftLineBreakBefore
: 0);
return true;
}
// CFStringCreateWithBytesNoCopy validates UTF-8 and returns nullptr on ill-formed sequences
SkUniqueCFRef<CFStringRef> str = MakeCFStringNoCopy(utftext8, utf8Units);
if (!str) {
return false;
}
// Populate soft line, grapheme, word, or sentence boundaries mapped to UTF-8 byte offsets
if (!this->populateBreaks(str.get(),
CFStringGetLength(str.get()),
utf8Units,
Utf16ToUtf8Mapper(utftext8, utf8Units))) {
return false;
}
// CFStringTokenizer does not distinguish mandatory breaks from soft wrap opportunities;
// scan codepoints to tag positions after LF/BK characters with kHardLineBreakBefore
if (fType == SkUnicode::BreakType::kLines) {
fUnicode->forEachCodepoint(
utftext8, utf8Units, [&](SkUnichar unichar, int32_t, int32_t end, size_t) {
if (fUnicode->isHardBreak(unichar)) {
this->addHardBreak(end);
}
});
}
return true;
}
bool setText(const char16_t utftext16[], int utf16Units) override {
this->reset();
// Reject negative lengths or null non-empty buffers
if (utf16Units < 0 || (utf16Units > 0 && !utftext16)) {
return false;
}
// Empty inputs still emit a boundary at 0 to match ICU BreakIterator::first() and
// SkParagraph empty-text expectations
if (utf16Units == 0) {
this->addBreak(0, fType == SkUnicode::BreakType::kLines
? SkUnicode::CodeUnitFlags::kSoftLineBreakBefore
: 0);
return true;
}
SkUniqueCFRef<CFStringRef> str = MakeCFStringNoCopy(utftext16, utf16Units);
if (!str) {
return false;
}
// Populate soft line, grapheme, word, or sentence boundaries directly in UTF-16 coordinates
if (!this->populateBreaks(str.get(),
utf16Units,
utf16Units,
[](CFIndex pos16) { return SkTo<Position>(pos16); })) {
return false;
}
// Tag positions immediately following UAX #14 LF/BK codepoints with kHardLineBreakBefore
if (fType == SkUnicode::BreakType::kLines) {
fUnicode->forEachCodepoint(
utftext16, utf16Units, [&](SkUnichar unichar, int32_t, int32_t end) {
if (fUnicode->isHardBreak(unichar)) {
this->addHardBreak(end);
}
});
}
return true;
}
private:
void reset() {
fBreaks.clear();
fStatuses.clear();
fIndex = 0;
}
// Maintain strictly increasing break positions and deduplicate overlapping tokenizer boundaries
void addBreak(Position pos, Status status) {
if (fBreaks.empty() || fBreaks.back() < pos) {
fBreaks.push_back(pos);
fStatuses.push_back(status);
}
}
// Upgrade an existing soft break at pos to a hard break, or insert a new break carrying both
// hard and soft flags when CFStringTokenizer omitted the boundary
void addHardBreak(Position pos) {
auto it = std::lower_bound(fBreaks.begin(), fBreaks.end(), pos);
auto idx = std::distance(fBreaks.begin(), it);
if (it != fBreaks.end() && *it == pos) {
fStatuses[idx] |= SkUnicode::CodeUnitFlags::kHardLineBreakBefore;
} else {
fBreaks.insert(it, pos);
fStatuses.insert(fStatuses.begin() + idx,
SkUnicode::CodeUnitFlags::kHardLineBreakBefore |
SkUnicode::CodeUnitFlags::kSoftLineBreakBefore);
}
}
// Always bracket token boundaries with 0 and totalUnits so inputs skipped by CFStringTokenizer
// (such as standalone CR or punctuation-only strings) still yield valid [0, end] break ranges
template <typename MapFn>
bool populateBreaks(CFStringRef str, CFIndex len16, Position totalUnits, MapFn&& map16) {
const char* loc = fLocale.empty() ? nullptr : fLocale.c_str();
if (fType == SkUnicode::BreakType::kLines) {
// Line-break tokens span between wrap opportunities, so candidate breaks occur at the
// end of each token (r.location + r.length)
this->addBreak(0, SkUnicode::CodeUnitFlags::kSoftLineBreakBefore);
if (!Tokenize(str, len16, kCFStringTokenizerUnitLineBreak, loc,
[&](CFRange r) {
this->addBreak(map16(r.location + r.length),
SkUnicode::CodeUnitFlags::kSoftLineBreakBefore);
})) {
return false;
}
this->addBreak(totalUnits, SkUnicode::CodeUnitFlags::kSoftLineBreakBefore);
} else if (fType == SkUnicode::BreakType::kGraphemes) {
// CFStringTokenizer does not expose extended grapheme clusters; use composed character
// ranges instead
ForEachGraphemeBoundary(str, len16, [&](CFIndex pos16) {
this->addBreak(map16(pos16), 0);
});
this->addBreak(totalUnits, 0);
} else {
// Word and sentence tokens report unit starts at r.location
CFOptionFlags unit = (fType == SkUnicode::BreakType::kWords)
? kCFStringTokenizerUnitWordBoundary
: kCFStringTokenizerUnitSentence;
this->addBreak(0, 0);
if (!Tokenize(str, len16, unit, loc, [&](CFRange r) {
this->addBreak(map16(r.location), 0);
})) {
return false;
}
this->addBreak(totalUnits, 0);
}
return true;
}
SkUnicode* fUnicode;
SkUnicode::BreakType fType;
std::string fLocale;
std::vector<Position> fBreaks;
std::vector<Status> fStatuses;
Position fIndex = 0;
};
} // namespace
class SkUnicode_apple : public SkUnicodeHardCodedCharProperties {
public:
SkUnicode_apple() = default;
~SkUnicode_apple() override = default;
std::unique_ptr<SkBidiIterator> makeBidiIterator(const uint16_t text[],
int count,
SkBidiIterator::Direction dir) override {
return fBidiFact->MakeIterator(text, count, dir);
}
std::unique_ptr<SkBidiIterator> makeBidiIterator(const char text[],
int count,
SkBidiIterator::Direction dir) override {
return fBidiFact->MakeIterator(text, count, dir);
}
std::unique_ptr<SkBreakIterator> makeBreakIterator(const char locale[],
BreakType breakType) override {
return std::make_unique<SkBreakIterator_apple>(this, breakType, locale);
}
std::unique_ptr<SkBreakIterator> makeBreakIterator(BreakType breakType) override {
return this->makeBreakIterator(nullptr, breakType);
}
SkString toUpper(const SkString& str) override { return this->toUpper(str, nullptr); }
SkString toUpper(const SkString& str, const char* locale) override {
if (str.isEmpty()) {
return SkString();
}
// Manage CFString and CFLocale lifetimes via SkUniqueCFRef across early returns
SkUniqueCFRef<CFStringRef> cfStr = MakeCFStringNoCopy(str.c_str(), SkToInt(str.size()));
if (!cfStr) {
return str;
}
SkUniqueCFRef<CFMutableStringRef> mutStr(
CFStringCreateMutableCopy(kCFAllocatorDefault, 0, cfStr.get()));
if (!mutStr) {
return str;
}
SkUniqueCFRef<CFLocaleRef> cfLoc = MakeCFLocale(locale);
CFStringUppercase(mutStr.get(), cfLoc.get());
CFIndex len = CFStringGetLength(mutStr.get());
CFIndex maxBytes = CFStringGetMaximumSizeForEncoding(len, kCFStringEncodingUTF8);
SkString result;
result.resize(maxBytes);
CFIndex usedBytes = 0;
CFStringGetBytes(mutStr.get(),
CFRangeMake(0, len),
kCFStringEncodingUTF8,
0,
false,
(UInt8*)result.data(),
maxBytes,
&usedBytes);
// Trim output buffer from worst-case UTF-8 reservation to the actual encoded byte count
result.resize(usedBytes);
return result;
}
void reorderVisual(const BidiLevel runLevels[],
int levelsCount,
int32_t logicalFromVisual[]) override {
if (levelsCount <= 0) {
return;
}
SkASSERT(runLevels != nullptr);
SkASSERT(logicalFromVisual != nullptr);
fBidiFact->bidi_reorderVisual(runLevels, levelsCount, logicalFromVisual);
}
bool getBidiRegions(const char utf8[],
int utf8Units,
TextDirection dir,
std::vector<BidiRegion>* results) override {
return fBidiFact->ExtractBidi(utf8, utf8Units, dir, results);
}
bool getWords(const char utf8[],
int utf8Units,
const char* locale,
std::vector<Position>* results) override {
results->clear();
// Reject negative lengths or null non-empty buffers
if (utf8Units < 0 || (utf8Units > 0 && !utf8)) {
return false;
}
// Empty text is a valid no-op that returns true with an empty result vector
if (utf8Units == 0) {
return true;
}
SkUniqueCFRef<CFStringRef> str = MakeCFStringNoCopy(utf8, utf8Units);
if (!str) {
return false;
}
// Tokenize word boundaries in UTF-16 code units; only append the end-of-text sentinel when
// at least one word token was found so whitespace-only strings stay empty
CFIndex len16 = CFStringGetLength(str.get());
bool ok = Tokenize(str.get(), len16, kCFStringTokenizerUnitWordBoundary, locale,
[&](CFRange r) { results->push_back(r.location); });
if (!results->empty()) {
results->push_back(len16);
}
return ok;
}
bool getUtf8Words(const char utf8[],
int utf8Units,
const char* locale,
std::vector<Position>* results) override {
// Delegate input validation and UTF-16 word tokenization to getWords
if (!this->getWords(utf8, utf8Units, locale, results)) {
return false;
}
if (results->empty()) {
return true;
}
// Convert UTF-16 word boundaries populated by getWords in-place to UTF-8 byte offsets
Utf16ToUtf8Mapper map16(utf8, utf8Units);
for (Position& pos : *results) {
pos = map16(pos);
}
return true;
}
bool getSentences(const char utf8[],
int utf8Units,
const char* locale,
std::vector<Position>* results) override {
results->clear();
// Reject negative lengths or null non-empty buffers
if (utf8Units < 0 || (utf8Units > 0 && !utf8)) {
return false;
}
// Empty text is a valid no-op that returns true with an empty result vector
if (utf8Units == 0) {
return true;
}
SkUniqueCFRef<CFStringRef> str = MakeCFStringNoCopy(utf8, utf8Units);
if (!str) {
return false;
}
// Tokenize sentence starts in UTF-16 and map them directly to UTF-8 byte offsets, appending
// the trailing end-of-text sentinel only when at least one sentence was emitted
CFIndex len16 = CFStringGetLength(str.get());
Utf16ToUtf8Mapper map16(utf8, utf8Units);
bool ok = Tokenize(str.get(), len16, kCFStringTokenizerUnitSentence, locale,
[&](CFRange r) { results->push_back(map16(r.location)); });
if (!results->empty()) {
results->push_back(utf8Units);
}
return ok;
}
bool computeCodeUnitFlags(char utf8[],
int utf8Units,
bool replaceTabs,
TArray<CodeUnitFlags, true>* results) override {
results->clear();
// Reject negative lengths or null non-empty buffers
if (utf8Units < 0 || (utf8Units > 0 && !utf8)) {
return false;
}
results->push_back_n(utf8Units + 1, CodeUnitFlags::kNoCodeUnitFlag);
// Empty text still requires a 1-element sentinel with kSoftLineBreakBefore | kGraphemeStart
// at index 0 to match ICU and Libgrapheme
if (utf8Units == 0) {
(*results)[0] = CodeUnitFlags::kSoftLineBreakBefore | CodeUnitFlags::kGraphemeStart;
return true;
}
SkUniqueCFRef<CFStringRef> str = MakeCFStringNoCopy(utf8, utf8Units);
if (!str) {
return false;
}
// Map UTF-16 grapheme and line break boundaries to UTF-8 byte positions and apply
// per-codepoint property flags across all bytes of each sequence
this->computeBreakFlags(str.get(),
CFStringGetLength(str.get()),
utf8Units,
Utf16ToUtf8Mapper(utf8, utf8Units),
results);
this->forEachCodepoint(utf8,
utf8Units,
[&](SkUnichar unichar, int32_t start, int32_t end, size_t) {
this->applyCodepointFlags(
unichar, start, end, replaceTabs, utf8, results);
});
return true;
}
bool computeCodeUnitFlags(char16_t utf16[],
int utf16Units,
bool replaceTabs,
TArray<CodeUnitFlags, true>* results) override {
results->clear();
// Reject negative lengths or null non-empty buffers
if (utf16Units < 0 || (utf16Units > 0 && !utf16)) {
return false;
}
results->push_back_n(utf16Units + 1, CodeUnitFlags::kNoCodeUnitFlag);
// Empty text still requires a 1-element sentinel with kSoftLineBreakBefore | kGraphemeStart
// at index 0 to match ICU and Libgrapheme
if (utf16Units == 0) {
(*results)[0] = CodeUnitFlags::kSoftLineBreakBefore | CodeUnitFlags::kGraphemeStart;
return true;
}
SkUniqueCFRef<CFStringRef> str = MakeCFStringNoCopy(utf16, utf16Units);
if (!str) {
return false;
}
// Populate grapheme, line break, and codepoint property flags directly in UTF-16 coordinates
this->computeBreakFlags(str.get(),
utf16Units,
utf16Units,
[](CFIndex pos16) { return SkTo<Position>(pos16); },
results);
this->forEachCodepoint(
utf16, utf16Units, [&](SkUnichar unichar, int32_t start, int32_t end) {
this->applyCodepointFlags(unichar, start, end, replaceTabs, utf16, results);
});
return true;
}
private:
// Share grapheme and soft line break flag population between UTF-8 and UTF-16 overloads via
// a coordinate-mapping callback, always marking index 0 and the trailing sentinel at units
template <typename MapFn>
void computeBreakFlags(CFStringRef str,
CFIndex len16,
int units,
MapFn&& map16,
TArray<CodeUnitFlags, true>* results) {
ForEachGraphemeBoundary(str, len16, [&](CFIndex pos16) {
(*results)[map16(pos16)] |= CodeUnitFlags::kGraphemeStart;
});
(*results)[units] |= CodeUnitFlags::kGraphemeStart;
(*results)[0] |= CodeUnitFlags::kSoftLineBreakBefore;
Tokenize(str, len16, kCFStringTokenizerUnitLineBreak, nullptr,
[&](CFRange r) {
(*results)[map16(r.location + r.length)] |=
CodeUnitFlags::kSoftLineBreakBefore;
});
(*results)[units] |= CodeUnitFlags::kSoftLineBreakBefore;
}
// Share per-codepoint property flag assignment and optional tab-to-space replacement between
// UTF-8 and UTF-16 overloads
template <typename CharT>
void applyCodepointFlags(SkUnichar unichar,
int32_t start,
int32_t end,
bool replaceTabs,
CharT text[],
TArray<CodeUnitFlags, true>* results) {
// Line break flags describe the boundary before a code unit, so a mandatory break on
// [start, end) marks index end immediately after the newline
if (this->isHardBreak(unichar)) {
(*results)[end] |=
(CodeUnitFlags::kHardLineBreakBefore | CodeUnitFlags::kSoftLineBreakBefore);
}
// Rewrite tabs to spaces before evaluating character classes so replaced tabs receive
// space/whitespace break flags instead of kControl
if (replaceTabs && this->isTabulation(unichar)) {
results->at(start) |= CodeUnitFlags::kTabulation;
unichar = ' ';
text[start] = ' ';
}
CodeUnitFlags flags = CodeUnitFlags::kNoCodeUnitFlag;
if (this->isSpace(unichar)) {
flags |= CodeUnitFlags::kPartOfIntraWordBreak;
}
if (this->isWhitespace(unichar)) {
flags |= CodeUnitFlags::kPartOfWhiteSpaceBreak;
}
if (this->isControl(unichar)) {
flags |= CodeUnitFlags::kControl;
}
if (this->isIdeographic(unichar)) {
flags |= CodeUnitFlags::kIdeographic;
}
// Replicate character-class flags across all UTF-8 bytes or UTF-16 surrogates in [start, end)
for (auto i = start; i < end; ++i) {
results->at(i) |= flags;
}
}
sk_sp<SkBidiFactory> fBidiFact = sk_make_sp<SkBidiSubsetFactory>();
};
namespace SkUnicodes::Apple {
sk_sp<SkUnicode> Make() { return sk_make_sp<SkUnicode_apple>(); }
} // namespace SkUnicodes::Apple