2 * Copyright (c) 2021 Samsung Electronics Co., Ltd.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
19 #include <dali-toolkit/internal/text/multi-language-support-impl.h>
22 #include <dali/devel-api/common/singleton-service.h>
23 #include <dali/devel-api/text-abstraction/font-client.h>
24 #include <dali/integration-api/debug.h>
27 #include <dali-toolkit/internal/text/multi-language-helper-functions.h>
35 #if defined(DEBUG_ENABLED)
36 Debug::Filter* gLogFilter = Debug::Filter::New(Debug::NoLogging, true, "LOG_MULTI_LANGUAGE_SUPPORT");
39 const Dali::Toolkit::Text::Character UTF32_A = 0x0041;
46 bool ValidateFontsPerScript::IsValidFont(FontId fontId) const
48 for(Vector<FontId>::ConstIterator it = mValidFonts.Begin(),
49 endIt = mValidFonts.End();
62 FontId DefaultFonts::FindFont(TextAbstraction::FontClient& fontClient,
63 const TextAbstraction::FontDescription& description,
64 PointSize26Dot6 size) const
66 for(std::vector<CacheItem>::const_iterator it = mFonts.begin(),
71 const CacheItem& item = *it;
73 if(((TextAbstraction::FontWeight::NONE == description.weight) || (description.weight == item.description.weight)) &&
74 ((TextAbstraction::FontWidth::NONE == description.width) || (description.width == item.description.width)) &&
75 ((TextAbstraction::FontSlant::NONE == description.slant) || (description.slant == item.description.slant)) &&
76 (size == fontClient.GetPointSize(item.fontId)) &&
77 (description.family.empty() || (description.family == item.description.family)))
86 void DefaultFonts::Cache(const TextAbstraction::FontDescription& description, FontId fontId)
89 item.description = description;
91 mFonts.push_back(item);
94 MultilanguageSupport::MultilanguageSupport()
95 : mDefaultFontPerScriptCache(),
96 mValidFontsPerScriptCache()
98 // Initializes the default font cache to zero (invalid font).
99 // Reserves space to cache the default fonts and access them with the script as an index.
100 mDefaultFontPerScriptCache.Resize(TextAbstraction::UNKNOWN + 1, NULL);
102 // Initializes the valid fonts cache to NULL (no valid fonts).
103 // Reserves space to cache the valid fonts and access them with the script as an index.
104 mValidFontsPerScriptCache.Resize(TextAbstraction::UNKNOWN + 1, NULL);
107 MultilanguageSupport::~MultilanguageSupport()
109 // Destroy the default font per script cache.
110 for(Vector<DefaultFonts*>::Iterator it = mDefaultFontPerScriptCache.Begin(),
111 endIt = mDefaultFontPerScriptCache.End();
118 // Destroy the valid fonts per script cache.
119 for(Vector<ValidateFontsPerScript*>::Iterator it = mValidFontsPerScriptCache.Begin(),
120 endIt = mValidFontsPerScriptCache.End();
128 Text::MultilanguageSupport MultilanguageSupport::Get()
130 Text::MultilanguageSupport multilanguageSupportHandle;
132 SingletonService service(SingletonService::Get());
135 // Check whether the singleton is already created
136 Dali::BaseHandle handle = service.GetSingleton(typeid(Text::MultilanguageSupport));
139 // If so, downcast the handle
140 MultilanguageSupport* impl = dynamic_cast<Internal::MultilanguageSupport*>(handle.GetObjectPtr());
141 multilanguageSupportHandle = Text::MultilanguageSupport(impl);
143 else // create and register the object
145 multilanguageSupportHandle = Text::MultilanguageSupport(new MultilanguageSupport);
146 service.Register(typeid(multilanguageSupportHandle), multilanguageSupportHandle);
150 return multilanguageSupportHandle;
153 void MultilanguageSupport::SetScripts(const Vector<Character>& text,
154 CharacterIndex startIndex,
155 Length numberOfCharacters,
156 Vector<ScriptRun>& scripts)
158 if(0u == numberOfCharacters)
160 // Nothing to do if there are no characters.
164 // Find the first index where to insert the script.
165 ScriptRunIndex scriptIndex = 0u;
168 for(Vector<ScriptRun>::ConstIterator it = scripts.Begin(),
169 endIt = scripts.End();
173 const ScriptRun& run = *it;
174 if(startIndex < run.characterRun.characterIndex + run.characterRun.numberOfCharacters)
182 // Stores the current script run.
183 ScriptRun currentScriptRun;
184 currentScriptRun.characterRun.characterIndex = startIndex;
185 currentScriptRun.characterRun.numberOfCharacters = 0u;
186 currentScriptRun.script = TextAbstraction::UNKNOWN;
188 // Reserve some space to reduce the number of reallocations.
189 scripts.Reserve(text.Count() << 2u);
191 // Whether the first valid script needs to be set.
192 bool isFirstScriptToBeSet = true;
194 // Whether the first valid script is a right to left script.
195 bool isParagraphRTL = false;
197 // Count the number of characters which are valid for all scripts. i.e. white spaces or '\n'.
198 Length numberOfAllScriptCharacters = 0u;
200 // Pointers to the text buffer.
201 const Character* const textBuffer = text.Begin();
203 // Initialize whether is right to left direction
204 currentScriptRun.isRightToLeft = false;
206 // Traverse all characters and set the scripts.
207 const Length lastCharacter = startIndex + numberOfCharacters;
208 for(Length index = startIndex; index < lastCharacter; ++index)
210 Character character = *(textBuffer + index);
212 // Get the script of the character.
213 Script script = TextAbstraction::GetCharacterScript(character);
215 // Some characters (like white spaces) are valid for many scripts. The rules to set a script
217 // - If they are at the begining of a paragraph they get the script of the first character with
218 // a defined script. If they are at the end, they get the script of the last one.
219 // - If they are between two scripts with the same direction, they get the script of the previous
220 // character with a defined script. If the two scripts have different directions, they get the
221 // script of the first character of the paragraph with a defined script.
223 // Skip those characters valid for many scripts like white spaces or '\n'.
224 bool endOfText = index == lastCharacter;
226 (TextAbstraction::COMMON == script))
228 // Check if whether is right to left markup and Keeps true if the previous value was true.
229 currentScriptRun.isRightToLeft = currentScriptRun.isRightToLeft || TextAbstraction::IsRightToLeftMark(character);
231 // ZWJ, ZWNJ between emojis should be treated as EMOJI.
232 if(TextAbstraction::EMOJI == currentScriptRun.script && !(TextAbstraction::IsZeroWidthJoiner(character) || TextAbstraction::IsZeroWidthNonJoiner(character)))
234 // Emojis doesn't mix well with characters common to all scripts. Insert the emoji run.
235 scripts.Insert(scripts.Begin() + scriptIndex, currentScriptRun);
238 // Initialize the new one.
239 currentScriptRun.characterRun.characterIndex = currentScriptRun.characterRun.characterIndex + currentScriptRun.characterRun.numberOfCharacters;
240 currentScriptRun.characterRun.numberOfCharacters = 0u;
241 currentScriptRun.script = TextAbstraction::UNKNOWN;
242 numberOfAllScriptCharacters = 0u;
245 // Count all these characters to be added into a script.
246 ++numberOfAllScriptCharacters;
248 if(TextAbstraction::IsNewParagraph(character))
250 // The character is a new paragraph.
251 // To know when there is a new paragraph is needed because if there is a white space
252 // between two scripts with different directions, it is added to the script with
253 // the same direction than the first script of the paragraph.
254 isFirstScriptToBeSet = true;
256 // Characters common to all scripts at the end of the paragraph are added to the last script.
257 currentScriptRun.characterRun.numberOfCharacters += numberOfAllScriptCharacters;
259 // Store the script run.
260 scripts.Insert(scripts.Begin() + scriptIndex, currentScriptRun);
263 // Initialize the new one.
264 currentScriptRun.characterRun.characterIndex = currentScriptRun.characterRun.characterIndex + currentScriptRun.characterRun.numberOfCharacters;
265 currentScriptRun.characterRun.numberOfCharacters = 0u;
266 currentScriptRun.script = TextAbstraction::UNKNOWN;
267 numberOfAllScriptCharacters = 0u;
268 // Initialize whether is right to left direction
269 currentScriptRun.isRightToLeft = false;
272 // Get the next character.
274 endOfText = index == lastCharacter;
277 character = *(textBuffer + index);
278 script = TextAbstraction::GetCharacterScript(character);
280 } // end while( !endOfText && ( TextAbstraction::COMMON == script ) )
284 // Last characters of the text are 'white spaces'.
285 // There is nothing else to do. Just add the remaining characters to the last script after this bucle.
289 // Check if it is the first character of a paragraph.
290 if(isFirstScriptToBeSet &&
291 (TextAbstraction::UNKNOWN != script) &&
292 (TextAbstraction::COMMON != script) &&
293 (TextAbstraction::EMOJI != script))
295 // Sets the direction of the first valid script.
296 isParagraphRTL = currentScriptRun.isRightToLeft || TextAbstraction::IsRightToLeftScript(script);
297 isFirstScriptToBeSet = false;
300 if((script != currentScriptRun.script) &&
301 (TextAbstraction::COMMON != script))
303 // Current run needs to be stored and a new one initialized.
305 if((isParagraphRTL == TextAbstraction::IsRightToLeftScript(currentScriptRun.script)) &&
306 (TextAbstraction::UNKNOWN != currentScriptRun.script))
308 // Previous script has the same direction than the first script of the paragraph.
309 // All the previously skipped characters need to be added to the previous script before it's stored.
310 currentScriptRun.characterRun.numberOfCharacters += numberOfAllScriptCharacters;
311 numberOfAllScriptCharacters = 0u;
313 else if((TextAbstraction::IsRightToLeftScript(currentScriptRun.script) == TextAbstraction::IsRightToLeftScript(script)) &&
314 (TextAbstraction::UNKNOWN != currentScriptRun.script))
316 // Current script and previous one have the same direction.
317 // All the previously skipped characters need to be added to the previous script before it's stored.
318 currentScriptRun.characterRun.numberOfCharacters += numberOfAllScriptCharacters;
319 numberOfAllScriptCharacters = 0u;
321 else if((TextAbstraction::UNKNOWN == currentScriptRun.script) &&
322 (TextAbstraction::EMOJI == script))
324 currentScriptRun.characterRun.numberOfCharacters += numberOfAllScriptCharacters;
325 numberOfAllScriptCharacters = 0u;
328 if(0u != currentScriptRun.characterRun.numberOfCharacters)
330 // Store the script run.
331 scripts.Insert(scripts.Begin() + scriptIndex, currentScriptRun);
335 // Initialize the new one.
336 currentScriptRun.characterRun.characterIndex = currentScriptRun.characterRun.characterIndex + currentScriptRun.characterRun.numberOfCharacters;
337 currentScriptRun.characterRun.numberOfCharacters = numberOfAllScriptCharacters + 1u; // Adds the white spaces which are at the begining of the script.
338 currentScriptRun.script = script;
339 numberOfAllScriptCharacters = 0u;
340 // Check if whether is right to left script.
341 currentScriptRun.isRightToLeft = TextAbstraction::IsRightToLeftScript(currentScriptRun.script);
345 if(TextAbstraction::UNKNOWN != currentScriptRun.script)
347 // Adds white spaces between characters.
348 currentScriptRun.characterRun.numberOfCharacters += numberOfAllScriptCharacters;
349 numberOfAllScriptCharacters = 0u;
352 // Add one more character to the run.
353 ++currentScriptRun.characterRun.numberOfCharacters;
357 // Add remaining characters into the last script.
358 currentScriptRun.characterRun.numberOfCharacters += numberOfAllScriptCharacters;
360 if(0u != currentScriptRun.characterRun.numberOfCharacters)
362 // Store the last run.
363 scripts.Insert(scripts.Begin() + scriptIndex, currentScriptRun);
367 if(scriptIndex < scripts.Count())
369 // Update the indices of the next script runs.
370 const ScriptRun& run = *(scripts.Begin() + scriptIndex - 1u);
371 CharacterIndex nextCharacterIndex = run.characterRun.characterIndex + run.characterRun.numberOfCharacters;
373 for(Vector<ScriptRun>::Iterator it = scripts.Begin() + scriptIndex,
374 endIt = scripts.End();
378 ScriptRun& run = *it;
379 run.characterRun.characterIndex = nextCharacterIndex;
380 nextCharacterIndex += run.characterRun.numberOfCharacters;
385 void MultilanguageSupport::ValidateFonts(const Vector<Character>& text,
386 const Vector<ScriptRun>& scripts,
387 const Vector<FontDescriptionRun>& fontDescriptions,
388 const TextAbstraction::FontDescription& defaultFontDescription,
389 TextAbstraction::PointSize26Dot6 defaultFontPointSize,
390 CharacterIndex startIndex,
391 Length numberOfCharacters,
392 Vector<FontRun>& fonts)
394 DALI_LOG_INFO(gLogFilter, Debug::General, "-->MultilanguageSupport::ValidateFonts\n");
396 if(0u == numberOfCharacters)
398 DALI_LOG_INFO(gLogFilter, Debug::General, "<--MultilanguageSupport::ValidateFonts\n");
399 // Nothing to do if there are no characters.
403 // Find the first index where to insert the font run.
404 FontRunIndex fontIndex = 0u;
407 for(Vector<FontRun>::ConstIterator it = fonts.Begin(),
412 const FontRun& run = *it;
413 if(startIndex < run.characterRun.characterIndex + run.characterRun.numberOfCharacters)
421 // Traverse the characters and validate/set the fonts.
424 DefaultFonts** defaultFontPerScriptCacheBuffer = mDefaultFontPerScriptCache.Begin();
425 ValidateFontsPerScript** validFontsPerScriptCacheBuffer = mValidFontsPerScriptCache.Begin();
427 // Stores the validated font runs.
428 fonts.Reserve(fontDescriptions.Count());
430 // Initializes a validated font run.
431 FontRun currentFontRun;
432 currentFontRun.characterRun.characterIndex = startIndex;
433 currentFontRun.characterRun.numberOfCharacters = 0u;
434 currentFontRun.fontId = 0u;
435 currentFontRun.isBoldRequired = false;
436 currentFontRun.isItalicRequired = false;
438 // Get the font client.
439 TextAbstraction::FontClient fontClient = TextAbstraction::FontClient::Get();
441 const Character* const textBuffer = text.Begin();
443 // Iterators of the script runs.
444 Vector<ScriptRun>::ConstIterator scriptRunIt = scripts.Begin();
445 Vector<ScriptRun>::ConstIterator scriptRunEndIt = scripts.End();
446 bool isNewParagraphCharacter = false;
448 bool isPreviousEmojiScript = false;
449 FontId previousEmojiFontId = 0u;
451 CharacterIndex lastCharacter = startIndex + numberOfCharacters;
452 for(Length index = startIndex; index < lastCharacter; ++index)
454 // Get the current character.
455 const Character character = *(textBuffer + index);
456 bool isItalicRequired = false;
457 bool isBoldRequired = false;
459 // new description for current character
460 TextAbstraction::FontDescription currentFontDescription;
461 TextAbstraction::PointSize26Dot6 currentFontPointSize = defaultFontPointSize;
462 bool isDefaultFont = true;
463 MergeFontDescriptions(fontDescriptions,
464 defaultFontDescription,
465 defaultFontPointSize,
467 currentFontDescription,
468 currentFontPointSize,
471 // Get the font for the current character.
472 FontId fontId = fontClient.GetFontId(currentFontDescription, currentFontPointSize);
474 // Get the script for the current character.
475 Script script = GetScript(index,
481 Dali::TextAbstraction::FontDescription description;
482 fontClient.GetDescription(fontId, description);
484 DALI_LOG_INFO(gLogFilter,
486 " Initial font set\n Character : %x, Script : %s, Font : %s \n",
488 Dali::TextAbstraction::ScriptName[script],
489 description.path.c_str());
493 // Validate whether the current character is supported by the given font.
494 bool isValidFont = false;
496 // Check first in the cache of default fonts per script and size.
498 FontId cachedDefaultFontId = 0u;
499 DefaultFonts* defaultFonts = *(defaultFontPerScriptCacheBuffer + script);
500 if(NULL != defaultFonts)
502 // This cache stores fall-back fonts.
503 cachedDefaultFontId = defaultFonts->FindFont(fontClient,
504 currentFontDescription,
505 currentFontPointSize);
508 // Whether the cached default font is valid.
509 const bool isValidCachedDefaultFont = 0u != cachedDefaultFontId;
511 // The font is valid if it matches with the default one for the current script and size and it's different than zero.
512 isValidFont = isValidCachedDefaultFont && (fontId == cachedDefaultFontId);
516 // Check if the font supports the character.
517 isValidFont = fontClient.IsCharacterSupportedByFont(fontId, character);
520 bool isCommonScript = false;
521 bool isEmojiScript = TextAbstraction::EMOJI == script;
523 if(isEmojiScript && !isPreviousEmojiScript)
525 if(0u != currentFontRun.characterRun.numberOfCharacters)
527 // Store the font run.
528 fonts.Insert(fonts.Begin() + fontIndex, currentFontRun);
532 // Initialize the new one.
533 currentFontRun.characterRun.characterIndex = currentFontRun.characterRun.characterIndex + currentFontRun.characterRun.numberOfCharacters;
534 currentFontRun.characterRun.numberOfCharacters = 0u;
535 currentFontRun.fontId = fontId;
536 currentFontRun.isItalicRequired = false;
537 currentFontRun.isBoldRequired = false;
540 // ZWJ, ZWNJ between emojis should use the previous emoji font.
541 if(isEmojiScript && (TextAbstraction::IsZeroWidthJoiner(character) || TextAbstraction::IsZeroWidthNonJoiner(character)))
543 if(0u != previousEmojiFontId)
545 fontId = previousEmojiFontId;
550 // If the given font is not valid, it means either:
551 // - there is no cached font for the current script yet or,
552 // - the user has set a different font than the default one for the current script or,
553 // - the platform default font is different than the default font for the current script.
555 // Need to check if the given font supports the current character.
556 if(!isValidFont) // (1)
558 // Whether the current character is common for all scripts (i.e. white spaces, ...)
560 // Is not desirable to cache fonts for the common script.
562 // i.e. Consider the text " हिंदी", the 'white space' has assigned the DEVANAGARI script.
563 // The user may have set a font or the platform's default is used.
565 // As the 'white space' is the first character, no font is cached so the font validation
566 // retrieves a glyph from the given font.
568 // Many fonts support 'white spaces' so probably the font set by the user or the platform's default
569 // supports the 'white space'. However, that font may not support the DEVANAGARI script.
570 isCommonScript = TextAbstraction::IsCommonScript(character);
572 // Check in the valid fonts cache.
573 ValidateFontsPerScript* validateFontsPerScript = *(validFontsPerScriptCacheBuffer + script);
575 if(NULL != validateFontsPerScript)
577 // This cache stores valid fonts set by the user.
578 isValidFont = validateFontsPerScript->IsValidFont(fontId);
580 // It may happen that a validated font for a script doesn't have all the glyphs for that script.
581 // i.e a font validated for the CJK script may contain glyphs for the chinese language but not for the Japanese.
584 // Checks if the current character is supported by the font is needed.
585 isValidFont = fontClient.IsCharacterSupportedByFont(fontId, character);
589 if(!isValidFont) // (2)
591 // The selected font is not stored in any cache.
593 // Checks if the current character is supported by the selected font.
594 isValidFont = fontClient.IsCharacterSupportedByFont(fontId, character);
596 // If there is a valid font, cache it.
597 if(isValidFont && !isCommonScript)
599 if(NULL == validateFontsPerScript)
601 validateFontsPerScript = new ValidateFontsPerScript();
603 *(validFontsPerScriptCacheBuffer + script) = validateFontsPerScript;
606 validateFontsPerScript->mValidFonts.PushBack(fontId);
609 if(!isValidFont && (fontId != cachedDefaultFontId) && (!TextAbstraction::IsNewParagraph(character))) // (3)
611 // The selected font by the user or the platform's default font has failed to validate the character.
613 // Checks if the previously discarted cached default font supports the character.
614 bool isValidCachedFont = false;
615 if(isValidCachedDefaultFont)
617 isValidCachedFont = fontClient.IsCharacterSupportedByFont(cachedDefaultFontId, character);
620 if(isValidCachedFont)
622 // Use the cached default font for the script if there is one.
623 fontId = cachedDefaultFontId;
627 // There is no valid cached default font for the script.
629 DefaultFonts* defaultFontsPerScript = NULL;
631 // Find a fallback-font.
632 fontId = fontClient.FindFallbackFont(character,
633 currentFontDescription,
634 currentFontPointSize,
639 fontId = fontClient.FindDefaultFont(UTF32_A, currentFontPointSize);
642 if(!isCommonScript && (script != TextAbstraction::UNKNOWN))
644 // Cache the font if it is not an unknown script
645 if(NULL == defaultFontsPerScript)
647 defaultFontsPerScript = *(defaultFontPerScriptCacheBuffer + script);
649 if(NULL == defaultFontsPerScript)
651 defaultFontsPerScript = new DefaultFonts();
652 *(defaultFontPerScriptCacheBuffer + script) = defaultFontsPerScript;
655 defaultFontsPerScript->Cache(currentFontDescription, fontId);
658 } // !isValidFont (3)
659 } // !isValidFont (2)
660 } // !isValidFont (1)
662 // Store the font id when the first character is an emoji.
663 if(isEmojiScript && !isPreviousEmojiScript)
667 previousEmojiFontId = fontId;
673 Dali::TextAbstraction::FontDescription description;
674 fontClient.GetDescription(fontId, description);
675 DALI_LOG_INFO(gLogFilter,
677 " Validated font set\n Character : %x, Script : %s, Font : %s \n",
679 Dali::TextAbstraction::ScriptName[script],
680 description.path.c_str());
684 // Whether bols style is required.
685 isBoldRequired = (currentFontDescription.weight >= TextAbstraction::FontWeight::BOLD);
687 // Whether italic style is required.
688 isItalicRequired = (currentFontDescription.slant >= TextAbstraction::FontSlant::ITALIC);
690 // The font is now validated.
691 if((fontId != currentFontRun.fontId) ||
692 isNewParagraphCharacter ||
693 // If font id is same as previous but style is diffrent, initialize new one
694 ((fontId == currentFontRun.fontId) && ((isBoldRequired != currentFontRun.isBoldRequired) || (isItalicRequired != currentFontRun.isItalicRequired))))
696 // Current run needs to be stored and a new one initialized.
698 if(0u != currentFontRun.characterRun.numberOfCharacters)
700 // Store the font run.
701 fonts.Insert(fonts.Begin() + fontIndex, currentFontRun);
705 // Initialize the new one.
706 currentFontRun.characterRun.characterIndex = currentFontRun.characterRun.characterIndex + currentFontRun.characterRun.numberOfCharacters;
707 currentFontRun.characterRun.numberOfCharacters = 0u;
708 currentFontRun.fontId = fontId;
709 currentFontRun.isBoldRequired = isBoldRequired;
710 currentFontRun.isItalicRequired = isItalicRequired;
713 // Add one more character to the run.
714 ++currentFontRun.characterRun.numberOfCharacters;
716 // Whether the current character is a new paragraph character.
717 isNewParagraphCharacter = TextAbstraction::IsNewParagraph(character);
718 isPreviousEmojiScript = isEmojiScript;
719 } // end traverse characters.
721 if(0u != currentFontRun.characterRun.numberOfCharacters)
723 // Store the last run.
724 fonts.Insert(fonts.Begin() + fontIndex, currentFontRun);
728 if(fontIndex < fonts.Count())
730 // Update the indices of the next font runs.
731 const FontRun& run = *(fonts.Begin() + fontIndex - 1u);
732 CharacterIndex nextCharacterIndex = run.characterRun.characterIndex + run.characterRun.numberOfCharacters;
734 for(Vector<FontRun>::Iterator it = fonts.Begin() + fontIndex,
741 run.characterRun.characterIndex = nextCharacterIndex;
742 nextCharacterIndex += run.characterRun.numberOfCharacters;
746 DALI_LOG_INFO(gLogFilter, Debug::General, "<--MultilanguageSupport::ValidateFonts\n");
749 } // namespace Internal
753 } // namespace Toolkit