Doug Felt | f7cb1f7 | 2010-07-01 16:20:43 -0700 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2010 The Android Open Source Project |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | |
| 17 | #include "TextLayout.h" |
| 18 | |
| 19 | #include <android_runtime/AndroidRuntime.h> |
| 20 | |
| 21 | #include "SkTemplates.h" |
| 22 | #include "unicode/ubidi.h" |
| 23 | #include "unicode/ushape.h" |
| 24 | #include <utils/Log.h> |
| 25 | |
Fabrice Di Meglio | dd347df | 2011-02-17 13:22:08 -0800 | [diff] [blame] | 26 | // Log debug messages from RTL related allocations |
| 27 | #define DEBUG_RTL_ALLOCATIONS 0 |
Doug Felt | f7cb1f7 | 2010-07-01 16:20:43 -0700 | [diff] [blame] | 28 | |
| 29 | namespace android { |
| 30 | // Returns true if we might need layout. If bidiFlags force LTR, assume no layout, if |
| 31 | // bidiFlags indicate there probably is RTL, assume we do, otherwise scan the text |
| 32 | // looking for a character >= the first RTL character in unicode and assume we do if |
| 33 | // we find one. |
| 34 | bool TextLayout::needsLayout(const jchar* text, jint len, jint bidiFlags) { |
| 35 | if (bidiFlags == kBidi_Force_LTR) { |
| 36 | return false; |
| 37 | } |
| 38 | if ((bidiFlags == kBidi_RTL) || (bidiFlags == kBidi_Default_RTL) || |
| 39 | bidiFlags == kBidi_Force_RTL) { |
| 40 | return true; |
| 41 | } |
| 42 | for (int i = 0; i < len; ++i) { |
Fabrice Di Meglio | dd347df | 2011-02-17 13:22:08 -0800 | [diff] [blame] | 43 | if (text[i] >= UNICODE_FIRST_RTL_CHAR) { |
Doug Felt | f7cb1f7 | 2010-07-01 16:20:43 -0700 | [diff] [blame] | 44 | return true; |
| 45 | } |
| 46 | } |
| 47 | return false; |
| 48 | } |
| 49 | |
| 50 | /** |
| 51 | * Character-based Arabic shaping. |
| 52 | * |
| 53 | * We'll use harfbuzz and glyph-based shaping instead once we're set up for it. |
| 54 | * |
| 55 | * @context the text context |
| 56 | * @start the start of the text to render |
| 57 | * @count the length of the text to render, start + count must be <= contextCount |
| 58 | * @contextCount the length of the context |
| 59 | * @shaped where to put the shaped text, must have capacity for count uchars |
| 60 | * @return the length of the shaped text, or -1 if error |
| 61 | */ |
| 62 | int TextLayout::shapeRtlText(const jchar* context, jsize start, jsize count, jsize contextCount, |
| 63 | jchar* shaped, UErrorCode &status) { |
Fabrice Di Meglio | dd347df | 2011-02-17 13:22:08 -0800 | [diff] [blame] | 64 | SkAutoSTMalloc<CHAR_BUFFER_SIZE, jchar> tempBuffer(contextCount); |
| 65 | jchar* buffer = tempBuffer.get(); |
| 66 | |
| 67 | #if DEBUG_RTL_ALLOCATIONS |
| 68 | LOGD("TextLayout::shapeRtlText - allocated buffer with size: %d", contextCount); |
| 69 | #endif |
Doug Felt | f7cb1f7 | 2010-07-01 16:20:43 -0700 | [diff] [blame] | 70 | |
| 71 | // Use fixed length since we need to keep start and count valid |
| 72 | u_shapeArabic(context, contextCount, buffer, contextCount, |
| 73 | U_SHAPE_LENGTH_FIXED_SPACES_NEAR | |
| 74 | U_SHAPE_TEXT_DIRECTION_LOGICAL | U_SHAPE_LETTERS_SHAPE | |
| 75 | U_SHAPE_X_LAMALEF_SUB_ALTERNATE, &status); |
| 76 | |
| 77 | if (U_SUCCESS(status)) { |
Fabrice Di Meglio | dd347df | 2011-02-17 13:22:08 -0800 | [diff] [blame] | 78 | // trim out UNICODE_NOT_A_CHAR following ligatures, if any |
Doug Felt | f7cb1f7 | 2010-07-01 16:20:43 -0700 | [diff] [blame] | 79 | int end = 0; |
| 80 | for (int i = start, e = start + count; i < e; ++i) { |
Fabrice Di Meglio | dd347df | 2011-02-17 13:22:08 -0800 | [diff] [blame] | 81 | if (buffer[i] != UNICODE_NOT_A_CHAR) { |
Doug Felt | f7cb1f7 | 2010-07-01 16:20:43 -0700 | [diff] [blame] | 82 | buffer[end++] = buffer[i]; |
| 83 | } |
| 84 | } |
| 85 | count = end; |
| 86 | // LOG(LOG_INFO, "CSRTL", "start %d count %d ccount %d\n", start, count, contextCount); |
| 87 | ubidi_writeReverse(buffer, count, shaped, count, UBIDI_DO_MIRRORING | UBIDI_OUTPUT_REVERSE |
| 88 | | UBIDI_KEEP_BASE_COMBINING, &status); |
| 89 | if (U_SUCCESS(status)) { |
| 90 | return count; |
| 91 | } |
| 92 | } |
Doug Felt | f7cb1f7 | 2010-07-01 16:20:43 -0700 | [diff] [blame] | 93 | return -1; |
| 94 | } |
| 95 | |
| 96 | /** |
| 97 | * Basic character-based layout supporting rtl and arabic shaping. |
| 98 | * Runs bidi on the text and generates a reordered, shaped line in buffer, returning |
| 99 | * the length. |
| 100 | * @text the text |
| 101 | * @len the length of the text in uchars |
| 102 | * @dir receives the resolved paragraph direction |
| 103 | * @buffer the buffer to receive the reordered, shaped line. Must have capacity of |
| 104 | * at least len jchars. |
| 105 | * @flags line bidi flags |
| 106 | * @return the length of the reordered, shaped line, or -1 if error |
| 107 | */ |
| 108 | jint TextLayout::layoutLine(const jchar* text, jint len, jint flags, int &dir, jchar* buffer, |
| 109 | UErrorCode &status) { |
| 110 | static const int RTL_OPTS = UBIDI_DO_MIRRORING | UBIDI_KEEP_BASE_COMBINING | |
| 111 | UBIDI_REMOVE_BIDI_CONTROLS | UBIDI_OUTPUT_REVERSE; |
| 112 | |
| 113 | UBiDiLevel bidiReq = 0; |
| 114 | switch (flags) { |
| 115 | case kBidi_LTR: bidiReq = 0; break; // no ICU constant, canonical LTR level |
| 116 | case kBidi_RTL: bidiReq = 1; break; // no ICU constant, canonical RTL level |
| 117 | case kBidi_Default_LTR: bidiReq = UBIDI_DEFAULT_LTR; break; |
| 118 | case kBidi_Default_RTL: bidiReq = UBIDI_DEFAULT_RTL; break; |
| 119 | case kBidi_Force_LTR: memcpy(buffer, text, len * sizeof(jchar)); return len; |
| 120 | case kBidi_Force_RTL: return shapeRtlText(text, 0, len, len, buffer, status); |
| 121 | } |
| 122 | |
| 123 | int32_t result = -1; |
| 124 | |
| 125 | UBiDi* bidi = ubidi_open(); |
| 126 | if (bidi) { |
| 127 | ubidi_setPara(bidi, text, len, bidiReq, NULL, &status); |
| 128 | if (U_SUCCESS(status)) { |
| 129 | dir = ubidi_getParaLevel(bidi) & 0x1; // 0 if ltr, 1 if rtl |
| 130 | |
| 131 | int rc = ubidi_countRuns(bidi, &status); |
| 132 | if (U_SUCCESS(status)) { |
| 133 | // LOG(LOG_INFO, "LAYOUT", "para bidiReq=%d dir=%d rc=%d\n", bidiReq, dir, rc); |
| 134 | |
| 135 | int32_t slen = 0; |
| 136 | for (int i = 0; i < rc; ++i) { |
| 137 | int32_t start; |
| 138 | int32_t length; |
| 139 | UBiDiDirection runDir = ubidi_getVisualRun(bidi, i, &start, &length); |
| 140 | |
| 141 | if (runDir == UBIDI_RTL) { |
| 142 | slen += shapeRtlText(text + start, 0, length, length, buffer + slen, status); |
| 143 | } else { |
| 144 | memcpy(buffer + slen, text + start, length * sizeof(jchar)); |
| 145 | slen += length; |
| 146 | } |
| 147 | } |
| 148 | if (U_SUCCESS(status)) { |
| 149 | result = slen; |
| 150 | } |
| 151 | } |
| 152 | } |
| 153 | ubidi_close(bidi); |
| 154 | } |
| 155 | |
| 156 | return result; |
| 157 | } |
| 158 | |
Romain Guy | e8e62a4 | 2010-07-23 18:55:21 -0700 | [diff] [blame] | 159 | bool TextLayout::prepareText(SkPaint *paint, const jchar* text, jsize len, jint bidiFlags, |
Romain Guy | 9226298 | 2010-07-26 11:17:54 -0700 | [diff] [blame] | 160 | const jchar** outText, int32_t* outBytes, jchar** outBuffer) { |
Doug Felt | f7cb1f7 | 2010-07-01 16:20:43 -0700 | [diff] [blame] | 161 | const jchar *workText = text; |
| 162 | jchar *buffer = NULL; |
| 163 | int dir = kDirection_LTR; |
| 164 | if (needsLayout(text, len, bidiFlags)) { |
| 165 | buffer =(jchar *) malloc(len * sizeof(jchar)); |
| 166 | if (!buffer) { |
Romain Guy | e8e62a4 | 2010-07-23 18:55:21 -0700 | [diff] [blame] | 167 | return false; |
Doug Felt | f7cb1f7 | 2010-07-01 16:20:43 -0700 | [diff] [blame] | 168 | } |
Fabrice Di Meglio | dd347df | 2011-02-17 13:22:08 -0800 | [diff] [blame] | 169 | |
| 170 | #if DEBUG_RTL_ALLOCATIONS |
| 171 | LOGD("TextLayout::prepareText - allocated buffer with size: %d", len); |
| 172 | #endif |
| 173 | |
Doug Felt | f7cb1f7 | 2010-07-01 16:20:43 -0700 | [diff] [blame] | 174 | UErrorCode status = U_ZERO_ERROR; |
| 175 | len = layoutLine(text, len, bidiFlags, dir, buffer, status); // might change len, dir |
| 176 | if (!U_SUCCESS(status)) { |
| 177 | LOG(LOG_WARN, "LAYOUT", "drawText error %d\n", status); |
| 178 | free(buffer); |
Romain Guy | e8e62a4 | 2010-07-23 18:55:21 -0700 | [diff] [blame] | 179 | return false; // can't render |
Doug Felt | f7cb1f7 | 2010-07-01 16:20:43 -0700 | [diff] [blame] | 180 | } |
| 181 | |
| 182 | workText = buffer; // use the shaped text |
| 183 | } |
| 184 | |
| 185 | bool trimLeft = false; |
| 186 | bool trimRight = false; |
| 187 | |
| 188 | SkPaint::Align horiz = paint->getTextAlign(); |
| 189 | switch (horiz) { |
Romain Guy | e8e62a4 | 2010-07-23 18:55:21 -0700 | [diff] [blame] | 190 | case SkPaint::kLeft_Align: trimLeft = dir & kDirection_Mask; break; |
| 191 | case SkPaint::kCenter_Align: trimLeft = trimRight = true; break; |
| 192 | case SkPaint::kRight_Align: trimRight = !(dir & kDirection_Mask); |
| 193 | default: break; |
Doug Felt | f7cb1f7 | 2010-07-01 16:20:43 -0700 | [diff] [blame] | 194 | } |
| 195 | const jchar* workLimit = workText + len; |
| 196 | |
| 197 | if (trimLeft) { |
| 198 | while (workText < workLimit && *workText == ' ') { |
| 199 | ++workText; |
| 200 | } |
| 201 | } |
| 202 | if (trimRight) { |
| 203 | while (workLimit > workText && *(workLimit - 1) == ' ') { |
| 204 | --workLimit; |
| 205 | } |
| 206 | } |
| 207 | |
Romain Guy | e8e62a4 | 2010-07-23 18:55:21 -0700 | [diff] [blame] | 208 | *outBytes = (workLimit - workText) << 1; |
| 209 | *outText = workText; |
Romain Guy | 9226298 | 2010-07-26 11:17:54 -0700 | [diff] [blame] | 210 | *outBuffer = buffer; |
| 211 | |
Romain Guy | e8e62a4 | 2010-07-23 18:55:21 -0700 | [diff] [blame] | 212 | return true; |
| 213 | } |
| 214 | |
| 215 | // Draws or gets the path of a paragraph of text on a single line, running bidi and shaping. |
| 216 | // This will draw if canvas is not null, otherwise path must be non-null and it will create |
| 217 | // a path representing the text that would have been drawn. |
| 218 | void TextLayout::handleText(SkPaint *paint, const jchar* text, jsize len, |
| 219 | jint bidiFlags, jfloat x, jfloat y,SkCanvas *canvas, SkPath *path) { |
| 220 | const jchar *workText; |
Romain Guy | 9226298 | 2010-07-26 11:17:54 -0700 | [diff] [blame] | 221 | jchar *buffer = NULL; |
Romain Guy | e8e62a4 | 2010-07-23 18:55:21 -0700 | [diff] [blame] | 222 | int32_t workBytes; |
Romain Guy | 9226298 | 2010-07-26 11:17:54 -0700 | [diff] [blame] | 223 | if (prepareText(paint, text, len, bidiFlags, &workText, &workBytes, &buffer)) { |
Romain Guy | e8e62a4 | 2010-07-23 18:55:21 -0700 | [diff] [blame] | 224 | SkScalar x_ = SkFloatToScalar(x); |
| 225 | SkScalar y_ = SkFloatToScalar(y); |
| 226 | if (canvas) { |
| 227 | canvas->drawText(workText, workBytes, x_, y_, *paint); |
| 228 | } else { |
| 229 | paint->getTextPath(workText, workBytes, x_, y_, path); |
| 230 | } |
Romain Guy | 9226298 | 2010-07-26 11:17:54 -0700 | [diff] [blame] | 231 | free(buffer); |
Romain Guy | e8e62a4 | 2010-07-23 18:55:21 -0700 | [diff] [blame] | 232 | } |
Doug Felt | f7cb1f7 | 2010-07-01 16:20:43 -0700 | [diff] [blame] | 233 | } |
| 234 | |
Romain Guy | 61c8c9c | 2010-08-09 20:48:09 -0700 | [diff] [blame] | 235 | bool TextLayout::prepareRtlTextRun(const jchar* context, jsize start, jsize& count, |
| 236 | jsize contextCount, jchar* shaped) { |
| 237 | UErrorCode status = U_ZERO_ERROR; |
| 238 | count = shapeRtlText(context, start, count, contextCount, shaped, status); |
| 239 | if (U_SUCCESS(status)) { |
| 240 | return true; |
| 241 | } else { |
Fabrice Di Meglio | dd347df | 2011-02-17 13:22:08 -0800 | [diff] [blame] | 242 | LOGW("drawTextRun error %d\n", status); |
Romain Guy | 61c8c9c | 2010-08-09 20:48:09 -0700 | [diff] [blame] | 243 | } |
| 244 | return false; |
| 245 | } |
| 246 | |
Doug Felt | f7cb1f7 | 2010-07-01 16:20:43 -0700 | [diff] [blame] | 247 | void TextLayout::drawTextRun(SkPaint* paint, const jchar* chars, |
| 248 | jint start, jint count, jint contextCount, |
| 249 | int dirFlags, jfloat x, jfloat y, SkCanvas* canvas) { |
| 250 | |
| 251 | SkScalar x_ = SkFloatToScalar(x); |
| 252 | SkScalar y_ = SkFloatToScalar(y); |
| 253 | |
| 254 | uint8_t rtl = dirFlags & 0x1; |
| 255 | if (rtl) { |
Fabrice Di Meglio | dd347df | 2011-02-17 13:22:08 -0800 | [diff] [blame] | 256 | SkAutoSTMalloc<CHAR_BUFFER_SIZE, jchar> buffer(contextCount); |
Romain Guy | 61c8c9c | 2010-08-09 20:48:09 -0700 | [diff] [blame] | 257 | if (prepareRtlTextRun(chars, start, count, contextCount, buffer.get())) { |
Doug Felt | f7cb1f7 | 2010-07-01 16:20:43 -0700 | [diff] [blame] | 258 | canvas->drawText(buffer.get(), count << 1, x_, y_, *paint); |
Doug Felt | f7cb1f7 | 2010-07-01 16:20:43 -0700 | [diff] [blame] | 259 | } |
| 260 | } else { |
| 261 | canvas->drawText(chars + start, count << 1, x_, y_, *paint); |
| 262 | } |
| 263 | } |
| 264 | |
| 265 | void TextLayout::getTextRunAdvances(SkPaint *paint, const jchar *chars, jint start, |
| 266 | jint count, jint contextCount, jint dirFlags, |
| 267 | jfloat *resultAdvances, jfloat &resultTotalAdvance) { |
Doug Felt | f7cb1f7 | 2010-07-01 16:20:43 -0700 | [diff] [blame] | 268 | resultTotalAdvance = 0; |
| 269 | |
Fabrice Di Meglio | dd347df | 2011-02-17 13:22:08 -0800 | [diff] [blame] | 270 | SkAutoSTMalloc<CHAR_BUFFER_SIZE, jchar> tempBuffer(contextCount); |
| 271 | jchar* buffer = tempBuffer.get(); |
| 272 | |
| 273 | #if DEBUG_RTL_ALLOCATIONS |
| 274 | LOGD("TextLayout::getTextRunAdvances - allocated buffer with size: %d", contextCount); |
| 275 | #endif |
| 276 | |
| 277 | SkScalar* scalarArray = (SkScalar*)resultAdvances; |
| 278 | |
Doug Felt | f7cb1f7 | 2010-07-01 16:20:43 -0700 | [diff] [blame] | 279 | // this is where we'd call harfbuzz |
| 280 | // for now we just use ushape.c |
| 281 | |
| 282 | int widths; |
| 283 | const jchar* text; |
| 284 | if (dirFlags & 0x1) { // rtl, call arabic shaping in case |
| 285 | UErrorCode status = U_ZERO_ERROR; |
| 286 | // Use fixed length since we need to keep start and count valid |
| 287 | u_shapeArabic(chars, contextCount, buffer, contextCount, |
| 288 | U_SHAPE_LENGTH_FIXED_SPACES_NEAR | |
| 289 | U_SHAPE_TEXT_DIRECTION_LOGICAL | U_SHAPE_LETTERS_SHAPE | |
| 290 | U_SHAPE_X_LAMALEF_SUB_ALTERNATE, &status); |
| 291 | // we shouldn't fail unless there's an out of memory condition, |
| 292 | // in which case we're hosed anyway |
| 293 | for (int i = start, e = i + count; i < e; ++i) { |
Fabrice Di Meglio | dd347df | 2011-02-17 13:22:08 -0800 | [diff] [blame] | 294 | if (buffer[i] == UNICODE_NOT_A_CHAR) { |
| 295 | buffer[i] = UNICODE_ZWSP; // zero-width-space for skia |
Doug Felt | f7cb1f7 | 2010-07-01 16:20:43 -0700 | [diff] [blame] | 296 | } |
| 297 | } |
| 298 | text = buffer + start; |
| 299 | widths = paint->getTextWidths(text, count << 1, scalarArray); |
| 300 | } else { |
| 301 | text = chars + start; |
| 302 | widths = paint->getTextWidths(text, count << 1, scalarArray); |
| 303 | } |
| 304 | |
| 305 | if (widths < count) { |
| 306 | // Skia operates on code points, not code units, so surrogate pairs return only |
| 307 | // one value. Expand the result so we have one value per UTF-16 code unit. |
| 308 | |
| 309 | // Note, skia's getTextWidth gets confused if it encounters a surrogate pair, |
| 310 | // leaving the remaining widths zero. Not nice. |
| 311 | for (int i = 0, p = 0; i < widths; ++i) { |
| 312 | resultTotalAdvance += resultAdvances[p++] = SkScalarToFloat(scalarArray[i]); |
Fabrice Di Meglio | dd347df | 2011-02-17 13:22:08 -0800 | [diff] [blame] | 313 | if (p < count && |
| 314 | text[p] >= UNICODE_FIRST_LOW_SURROGATE && |
| 315 | text[p] < UNICODE_FIRST_PRIVATE_USE && |
| 316 | text[p-1] >= UNICODE_FIRST_HIGH_SURROGATE && |
| 317 | text[p-1] < UNICODE_FIRST_LOW_SURROGATE) { |
Doug Felt | f7cb1f7 | 2010-07-01 16:20:43 -0700 | [diff] [blame] | 318 | resultAdvances[p++] = 0; |
| 319 | } |
| 320 | } |
| 321 | } else { |
| 322 | for (int i = 0; i < count; i++) { |
| 323 | resultTotalAdvance += resultAdvances[i] = SkScalarToFloat(scalarArray[i]); |
| 324 | } |
| 325 | } |
| 326 | } |
| 327 | |
| 328 | |
| 329 | // Draws a paragraph of text on a single line, running bidi and shaping |
| 330 | void TextLayout::drawText(SkPaint* paint, const jchar* text, jsize len, |
| 331 | int bidiFlags, jfloat x, jfloat y, SkCanvas* canvas) { |
| 332 | |
| 333 | handleText(paint, text, len, bidiFlags, x, y, canvas, NULL); |
| 334 | } |
| 335 | |
| 336 | void TextLayout::getTextPath(SkPaint *paint, const jchar *text, jsize len, |
| 337 | jint bidiFlags, jfloat x, jfloat y, SkPath *path) { |
| 338 | handleText(paint, text, len, bidiFlags, x, y, NULL, path); |
| 339 | } |
| 340 | |
| 341 | |
| 342 | void TextLayout::drawTextOnPath(SkPaint* paint, const jchar* text, int count, |
| 343 | int bidiFlags, jfloat hOffset, jfloat vOffset, |
| 344 | SkPath* path, SkCanvas* canvas) { |
| 345 | |
| 346 | SkScalar h_ = SkFloatToScalar(hOffset); |
| 347 | SkScalar v_ = SkFloatToScalar(vOffset); |
| 348 | |
| 349 | if (!needsLayout(text, count, bidiFlags)) { |
| 350 | canvas->drawTextOnPathHV(text, count << 1, *path, h_, v_, *paint); |
| 351 | return; |
| 352 | } |
| 353 | |
Fabrice Di Meglio | dd347df | 2011-02-17 13:22:08 -0800 | [diff] [blame] | 354 | SkAutoSTMalloc<CHAR_BUFFER_SIZE, jchar> buffer(count); |
| 355 | |
| 356 | #if DEBUG_RTL_ALLOCATIONS |
| 357 | LOGD("TextLayout::drawTextOnPath - allocated buffer with size: %d", count); |
| 358 | #endif |
| 359 | |
Doug Felt | f7cb1f7 | 2010-07-01 16:20:43 -0700 | [diff] [blame] | 360 | int dir = kDirection_LTR; |
| 361 | UErrorCode status = U_ZERO_ERROR; |
| 362 | count = layoutLine(text, count, bidiFlags, dir, buffer.get(), status); |
| 363 | if (U_SUCCESS(status)) { |
| 364 | canvas->drawTextOnPathHV(buffer.get(), count << 1, *path, h_, v_, *paint); |
| 365 | } |
| 366 | } |
| 367 | |
| 368 | } |