Skip to content

Commit b50c94d

Browse files
fix: Unicode Text length Calculation (#16219)
* fix: Unicode Text length Calculation * Change files * yarn format fix
1 parent a1f1321 commit b50c94d

6 files changed

Lines changed: 59 additions & 6 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "prerelease",
3+
"comment": "fix: Unicode Text length Calculation",
4+
"packageName": "react-native-windows",
5+
"email": "66076509+vineethkuttan@users.noreply.github.com",
6+
"dependentChangeType": "patch"
7+
}

vnext/Common/unicode.cpp

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,42 @@ std::wstring Utf8ToUtf16(const std::string &utf8) {
9393
return Utf8ToUtf16(utf8.c_str(), utf8.length());
9494
}
9595

96+
size_t Utf8ToUtf16Length(const char *utf8, size_t utf8Len) {
97+
if (utf8Len == 0) {
98+
return 0;
99+
}
100+
101+
if (utf8Len > static_cast<size_t>((std::numeric_limits<int>::max)())) {
102+
throw std::overflow_error("Length of input string to Utf8ToUtf16Length() must fit into an int.");
103+
}
104+
105+
const int utf8Length = static_cast<int>(utf8Len);
106+
107+
constexpr DWORD flags = 0;
108+
109+
const int utf16Length = ::MultiByteToWideChar(
110+
CP_UTF8, // Source string is in UTF-8.
111+
flags, // Conversion flags.
112+
utf8, // Source UTF-8 string pointer.
113+
utf8Length, // Length of the source UTF-8 string, in chars.
114+
nullptr, // Do not convert, just request the size.
115+
0 // Request size of destination buffer, in wchar_ts.
116+
);
117+
118+
if (utf16Length == 0) {
119+
throw UnicodeConversionException(
120+
"Cannot get result string length when converting from UTF-8 to UTF-16 "
121+
"(MultiByteToWideChar failed).",
122+
GetLastError());
123+
}
124+
125+
return static_cast<size_t>(utf16Length);
126+
}
127+
128+
size_t Utf8ToUtf16Length(const std::string &utf8) {
129+
return Utf8ToUtf16Length(utf8.c_str(), utf8.length());
130+
}
131+
96132
#if _HAS_CXX17
97133
std::wstring Utf8ToUtf16(const std::string_view &utf8) {
98134
return Utf8ToUtf16(utf8.data(), utf8.length());

vnext/Common/unicode.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,14 @@ class UnicodeConversionException : public std::runtime_error {
5555
/* (4) */ std::wstring Utf8ToUtf16(const std::string_view &utf8);
5656
#endif
5757

58+
// The following functions return the length of the UTF-16 string that would
59+
// result from converting the input UTF-8 string, without actually performing
60+
// the conversion or allocating a temporary std::wstring. This is useful in
61+
// hot paths where only the length is needed (e.g. DirectWrite text ranges).
62+
//
63+
size_t Utf8ToUtf16Length(const char *utf8, size_t utf8Len);
64+
size_t Utf8ToUtf16Length(const std::string &utf8);
65+
5866
// The following functions convert UTF-16BE strings to UTF-8 strings. Their
5967
// behaviors mirror those of the above Utf8ToUtf16 functions.
6068
//

vnext/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.cpp

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,11 +153,12 @@ facebook::react::SharedViewEventEmitter ParagraphComponentView::eventEmitterAtPo
153153
uint32_t textPosition = metrics.textPosition;
154154

155155
for (auto fragment : m_attributedStringBox.getValue().getFragments()) {
156-
if (textPosition < fragment.string.length()) {
156+
uint32_t utf16Length = static_cast<uint32_t>(::Microsoft::Common::Unicode::Utf8ToUtf16Length(fragment.string));
157+
if (textPosition < utf16Length) {
157158
return std::static_pointer_cast<const facebook::react::ViewEventEmitter>(
158159
fragment.parentShadowView.eventEmitter);
159160
}
160-
textPosition -= static_cast<uint32_t>(fragment.string.length());
161+
textPosition -= utf16Length;
161162
}
162163
}
163164
}
@@ -210,10 +211,11 @@ bool ParagraphComponentView::IsTextSelectableAtPoint(facebook::react::Point pt)
210211

211212
// Finds which fragment contains this text position
212213
for (auto fragment : m_attributedStringBox.getValue().getFragments()) {
213-
if (textPosition < fragment.string.length()) {
214+
uint32_t utf16Length = static_cast<uint32_t>(::Microsoft::Common::Unicode::Utf8ToUtf16Length(fragment.string));
215+
if (textPosition < utf16Length) {
214216
return true;
215217
}
216-
textPosition -= static_cast<uint32_t>(fragment.string.length());
218+
textPosition -= utf16Length;
217219
}
218220
}
219221
}

vnext/Microsoft.ReactNative/Fabric/Composition/TextDrawing.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ void RenderText(
6666
unsigned int position = 0;
6767
unsigned int length = 0;
6868
for (auto fragment : attributedString.getFragments()) {
69-
length = static_cast<UINT32>(fragment.string.length());
69+
length = static_cast<UINT32>(::Microsoft::Common::Unicode::Utf8ToUtf16Length(fragment.string));
7070
DWRITE_TEXT_RANGE range = {position, length};
7171
if (fragment.textAttributes.foregroundColor &&
7272
(fragment.textAttributes.foregroundColor != textAttributes.foregroundColor) ||

vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/textlayoutmanager/WindowsTextLayoutManager.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ void WindowsTextLayoutManager::GetTextLayout(
262262
attachments.push_back(attachment);
263263
position += 1;
264264
} else {
265-
unsigned int length = static_cast<UINT32>(fragment.string.length());
265+
unsigned int length = static_cast<UINT32>(Microsoft::Common::Unicode::Utf8ToUtf16Length(fragment.string));
266266
DWRITE_TEXT_RANGE range = {position, length};
267267
TextAttributes attributes = fragment.textAttributes;
268268
DWRITE_FONT_STYLE fragmentStyle = DWRITE_FONT_STYLE_NORMAL;

0 commit comments

Comments
 (0)