From 3bed787795d21f0948be66ebdc2837fb2852bced Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Fri, 14 Jun 2024 02:23:53 -0300 Subject: [PATCH] More improvements for text shaping. --- include/eepp/graphics/fonttruetype.hpp | 4 +- src/eepp/graphics/text.cpp | 339 ++++++++++++++++--------- src/examples/fonts/fonts.cpp | 106 ++++---- 3 files changed, 268 insertions(+), 181 deletions(-) diff --git a/include/eepp/graphics/fonttruetype.hpp b/include/eepp/graphics/fonttruetype.hpp index c4e1ce31a..61d184a0a 100644 --- a/include/eepp/graphics/fonttruetype.hpp +++ b/include/eepp/graphics/fonttruetype.hpp @@ -133,6 +133,8 @@ class EE_API FontTrueType : public Font { void* hb() const { return mHBFont; } + bool setCurrentSize( unsigned int characterSize ) const; + protected: friend class Text; @@ -179,8 +181,6 @@ class EE_API FontTrueType : public Font { Rect findGlyphRect( Page& page, unsigned int width, unsigned int height ) const; - bool setCurrentSize( unsigned int characterSize ) const; - Page& getPage( unsigned int characterSize ) const; typedef UnorderedMap> diff --git a/src/eepp/graphics/text.cpp b/src/eepp/graphics/text.cpp index 68e6b0706..63075b6a4 100644 --- a/src/eepp/graphics/text.cpp +++ b/src/eepp/graphics/text.cpp @@ -21,10 +21,15 @@ namespace EE { namespace Graphics { namespace { +#ifdef EE_TEXT_SHAPER_ENABLED +static constexpr bool TextShaperEnabled = true; +#endif + // helper class that divides the string into lines and font runs. class TextShapeRun { public: - TextShapeRun( const String& str, FontStyleConfig& config ) : mString( str ), mConfig( config ) { + TextShapeRun( const String& str, const FontStyleConfig& config ) : + mString( str ), mConfig( config ) { findNextEnd(); } @@ -79,9 +84,48 @@ class TextShapeRun { Font* mFont{ nullptr }; Font* mStartFont{ nullptr }; bool mIsNewLine{ false }; - FontStyleConfig& mConfig; + const FontStyleConfig& mConfig; }; +static void shapeAndRun( const String& string, const FontStyleConfig& config, + const std::function& cb ) { + hb_buffer_t* hbBuffer = hb_buffer_create(); + TextShapeRun run( string, config ); + + while ( run.hasNext() ) { + String::View curRun( run.curRun() ); + FontTrueType* font = run.font(); + font->setCurrentSize( config.CharacterSize ); + + hb_buffer_reset( hbBuffer ); + hb_buffer_add_utf32( hbBuffer, (Uint32*)curRun.data(), curRun.size(), 0, curRun.size() ); + hb_buffer_guess_segment_properties( hbBuffer ); + + // Enable kerning + static const hb_feature_t features[] = { + hb_feature_t{ HB_TAG( 'k', 'e', 'r', 'n' ), 1, HB_FEATURE_GLOBAL_START, + HB_FEATURE_GLOBAL_END }, + }; + + // whitelist cross-platforms shapers only + static const char* shaper_list[] = { "graphite2", "ot", "fallback", nullptr }; + + hb_shape_full( static_cast( font->hb() ), hbBuffer, features, 1, shaper_list ); + + // from the shaped text we get the glyphs and positions + unsigned int glyphCount; + hb_glyph_info_t* glyphInfo = hb_buffer_get_glyph_infos( hbBuffer, &glyphCount ); + hb_glyph_position_t* glyphPos = hb_buffer_get_glyph_positions( hbBuffer, &glyphCount ); + + cb( glyphInfo, glyphPos, glyphCount, run ); + + run.next(); + } + + hb_buffer_destroy( hbBuffer ); +} + } // namespace std::string Text::styleFlagToString( const Uint32& flags ) { @@ -992,16 +1036,66 @@ void Text::updateWidthCache() { Float width = 0; Float maxWidth = 0; - Uint32 rune; - Uint32 prevChar = 0; bool bold = ( mFontStyleConfig.Style & Bold ) != 0; bool italic = ( mFontStyleConfig.Style & Italic ) != 0; - Float hspace = static_cast( mFontStyleConfig.Font ->getGlyph( L' ', mFontStyleConfig.CharacterSize, bold, italic, mFontStyleConfig.OutlineThickness ) .advance ); +#ifdef EE_TEXT_SHAPER_ENABLED + if ( TextShaperEnabled && mFontStyleConfig.Font->getType() == FontType::TTF ) { + FontTrueType* rFont = static_cast( mFontStyleConfig.Font ); + shapeAndRun( mString, mFontStyleConfig, + [&]( hb_glyph_info_t* glyphInfo, hb_glyph_position_t*, Uint32 glyphCount, + TextShapeRun& run ) { + FontTrueType* font = run.font(); + Uint32 prevGlyphIndex = 0; + + for ( std::size_t i = 0; i < glyphCount; ++i ) { + hb_glyph_info_t curGlyph = glyphInfo[i]; + auto curChar = mString[curGlyph.cluster]; + + if ( curChar == '\t' ) { + width += hspace * mTabWidth; + prevGlyphIndex = curGlyph.codepoint; + continue; + } + + const Glyph& glyph = font->getGlyphByIndex( + curGlyph.codepoint, mFontStyleConfig.CharacterSize, bold, italic, + 0, rFont->getPage( mFontStyleConfig.CharacterSize ), 0 ); + + width += rFont->getKerningFromGlyphIndex( + prevGlyphIndex, curGlyph.codepoint, mFontStyleConfig.CharacterSize, + bold, italic, mFontStyleConfig.OutlineThickness ); + + width += font->isColorEmojiFont() && ' ' != curChar + ? glyph.size.getWidth() + : glyph.advance; + + if ( width > maxWidth ) + maxWidth = width; + + prevGlyphIndex = curGlyph.codepoint; + } + + if ( run.runIsNewLine() ) { + mLinesWidth.push_back( width ); + width = 0; + } + } ); + + if ( !mString.empty() && mString[mString.size() - 1] != '\n' ) + mLinesWidth.push_back( width ); + + mCachedWidth = maxWidth; + return; + } +#endif + + Uint32 rune; + Uint32 prevChar = 0; size_t size = mString.size(); for ( std::size_t i = 0; i < size; ++i ) { rune = mString[i]; @@ -1306,55 +1400,79 @@ void Text::ensureGeometryUpdate() { } #ifdef EE_TEXT_SHAPER_ENABLED - if ( mFontStyleConfig.Font->getType() == FontType::TTF ) { + if ( TextShaperEnabled && mFontStyleConfig.Font->getType() == FontType::TTF ) { FontTrueType* rFont = static_cast( mFontStyleConfig.Font ); - hb_buffer_t* hbBuffer = hb_buffer_create(); - TextShapeRun run( mString, mFontStyleConfig ); - while ( run.hasNext() ) { - String::View curRun( run.curRun() ); - FontTrueType* font = run.font(); - font->setCurrentSize( mFontStyleConfig.CharacterSize ); + shapeAndRun( + mString, mFontStyleConfig, + [&]( hb_glyph_info_t* glyphInfo, hb_glyph_position_t* glyphPos, Uint32 glyphCount, + TextShapeRun& run ) { + FontTrueType* font = run.font(); + Uint32 prevGlyphIndex = 0; - hb_buffer_reset( hbBuffer ); - hb_buffer_add_utf32( hbBuffer, (Uint32*)curRun.data(), curRun.size(), 0, - curRun.size() ); - hb_buffer_guess_segment_properties( hbBuffer ); + for ( std::size_t i = 0; i < glyphCount; ++i ) { + hb_glyph_info_t curGlyph = glyphInfo[i]; + hb_glyph_position_t curGlyphPos = glyphPos[i]; + auto curChar = mString[curGlyph.cluster]; - // Enable kerning - static const hb_feature_t features[] = { - hb_feature_t{ HB_TAG( 'k', 'e', 'r', 'n' ), 1, HB_FEATURE_GLOBAL_START, - HB_FEATURE_GLOBAL_END }, - }; + if ( curChar == '\t' ) { + minX = std::min( minX, x ); - // whitelist cross-platforms shapers only - static const char* shaper_list[] = { "graphite2", "ot", "fallback", nullptr }; + if ( curChar == '\t' ) + x += hspace * mTabWidth; + else + x += hspace; - hb_shape_full( static_cast( font->hb() ), hbBuffer, features, 1, - shaper_list ); + maxX = std::max( maxX, x ); - // from the shaped text we get the glyphs and positions - Uint32 prevGlyphIndex = 0; - unsigned int glyphCount; - hb_glyph_info_t* glyphInfo = hb_buffer_get_glyph_infos( hbBuffer, &glyphCount ); - hb_glyph_position_t* glyphPos = hb_buffer_get_glyph_positions( hbBuffer, &glyphCount ); + if ( mCachedWidthNeedUpdate ) + maxW = std::max( maxW, x ); - for ( std::size_t i = 0; i < glyphCount; ++i ) { - hb_glyph_info_t curGlyph = glyphInfo[i]; - hb_glyph_position_t curGlyphPos = glyphPos[i]; + prevGlyphIndex = curGlyph.codepoint; + continue; + } - x += rFont->getKerningFromGlyphIndex( - prevGlyphIndex, curGlyph.codepoint, mFontStyleConfig.CharacterSize, bold, - reqItalic, mFontStyleConfig.OutlineThickness ); + x += rFont->getKerningFromGlyphIndex( + prevGlyphIndex, curGlyph.codepoint, mFontStyleConfig.CharacterSize, bold, + reqItalic, mFontStyleConfig.OutlineThickness ); - Float currentX = x + ( curGlyphPos.x_offset / 64.f ); - Float currentY = y + ( curGlyphPos.y_offset / 64.f ); + Float currentX = x + ( curGlyphPos.x_offset / 64.f ); + Float currentY = y + ( curGlyphPos.y_offset / 64.f ); - // Apply the outline - if ( mFontStyleConfig.OutlineThickness != 0 ) { + // Apply the outline + if ( mFontStyleConfig.OutlineThickness != 0 ) { + const Glyph& glyph = font->getGlyphByIndex( + curGlyph.codepoint, mFontStyleConfig.CharacterSize, bold, reqItalic, + mFontStyleConfig.OutlineThickness, + rFont->getPage( mFontStyleConfig.CharacterSize ), 0 ); + + Float left = glyph.bounds.Left; + Float top = glyph.bounds.Top; + Float right = glyph.bounds.Left + glyph.bounds.Right; + Float bottom = glyph.bounds.Top + glyph.bounds.Bottom; + + // Add the outline glyph to the vertices + if ( glyph.bounds.Right > 0 && glyph.bounds.Bottom > 0 ) { + addGlyphQuad( mOutlineVertices, Vector2f( currentX, currentY ), glyph, + italic, mFontStyleConfig.OutlineThickness, centerDiffX ); + } + + // Update the current bounds with the outlined glyph bounds + minX = std::min( minX, x + left - italic * bottom - + mFontStyleConfig.OutlineThickness ); + maxX = std::max( maxX, x + right - italic * top - + mFontStyleConfig.OutlineThickness ); + minY = std::min( minY, y + top - mFontStyleConfig.OutlineThickness ); + maxY = std::max( maxY, y + bottom - mFontStyleConfig.OutlineThickness ); + if ( mCachedWidthNeedUpdate ) { + maxW = std::max( maxW, x + glyph.advance - italic * top - + mFontStyleConfig.OutlineThickness ); + } + } + + // Extract the current glyph's description const Glyph& glyph = font->getGlyphByIndex( - curGlyph.codepoint, mFontStyleConfig.CharacterSize, bold, reqItalic, - mFontStyleConfig.OutlineThickness, + curGlyph.codepoint, mFontStyleConfig.CharacterSize, bold, reqItalic, 0, rFont->getPage( mFontStyleConfig.CharacterSize ), 0 ); Float left = glyph.bounds.Left; @@ -1362,89 +1480,68 @@ void Text::ensureGeometryUpdate() { Float right = glyph.bounds.Left + glyph.bounds.Right; Float bottom = glyph.bounds.Top + glyph.bounds.Bottom; - // Add the outline glyph to the vertices + // Add a quad for the current character if ( glyph.bounds.Right > 0 && glyph.bounds.Bottom > 0 ) { - addGlyphQuad( mOutlineVertices, Vector2f( currentX, currentY ), glyph, - italic, mFontStyleConfig.OutlineThickness, centerDiffX ); + addGlyphQuad( mVertices, Vector2f( currentX, currentY ), glyph, italic, 0, + centerDiffX ); } - // Update the current bounds with the outlined glyph bounds - minX = std::min( minX, x + left - italic * bottom - - mFontStyleConfig.OutlineThickness ); - maxX = std::max( maxX, - x + right - italic * top - mFontStyleConfig.OutlineThickness ); - minY = std::min( minY, y + top - mFontStyleConfig.OutlineThickness ); - maxY = std::max( maxY, y + bottom - mFontStyleConfig.OutlineThickness ); - if ( mCachedWidthNeedUpdate ) { - maxW = std::max( maxW, x + glyph.advance - italic * top - - mFontStyleConfig.OutlineThickness ); + // Update the current bounds + minX = std::min( minX, currentX + left - italic * bottom ); + maxX = std::max( maxX, currentX + right - italic * top ); + minY = std::min( minY, currentY + top ); + maxY = std::max( maxY, currentY + bottom ); + + // Advance to the next character + x += font->isColorEmojiFont() && ' ' != curChar ? glyph.size.getWidth() + : glyph.advance; + + prevGlyphIndex = curGlyph.codepoint; + } + + // If we're using the underlined style, add the last line + if ( underlined && run.runIsNewLine() ) { + addLine( mVertices, x, y, underlineOffset, underlineThickness, 0, centerDiffX ); + + if ( mFontStyleConfig.OutlineThickness != 0 ) + addLine( mOutlineVertices, x, y, underlineOffset, underlineThickness, + mFontStyleConfig.OutlineThickness, centerDiffX ); + } + + // If we're using the strike through style, add the last line across all characters + if ( strikeThrough && run.runIsNewLine() ) { + addLine( mVertices, x, y, strikeThroughOffset, underlineThickness, 0, + centerDiffX ); + + if ( mFontStyleConfig.OutlineThickness != 0 ) + addLine( mOutlineVertices, x, y, strikeThroughOffset, underlineThickness, + mFontStyleConfig.OutlineThickness, centerDiffX ); + } + + if ( mCachedWidthNeedUpdate ) + mLinesWidth.push_back( x ); + + // next line + if ( run.runIsNewLine() ) { + y += vspace; + x = 0; + switch ( Font::getHorizontalAlign( mAlign ) ) { + case TEXT_ALIGN_CENTER: + centerDiffX = + line < mLinesWidth.size() + ? (Float)( (Int32)( ( mCachedWidth - mLinesWidth[line] ) * + 0.5f ) ) + : 0.f; + line++; + break; + case TEXT_ALIGN_RIGHT: + centerDiffX = + line < mLinesWidth.size() ? mCachedWidth - mLinesWidth[line] : 0.f; + line++; + break; } } - - // Extract the current glyph's description - const Glyph& glyph = font->getGlyphByIndex( - curGlyph.codepoint, mFontStyleConfig.CharacterSize, bold, reqItalic, 0, - rFont->getPage( mFontStyleConfig.CharacterSize ), 0 ); - - Float left = glyph.bounds.Left; - Float top = glyph.bounds.Top; - Float right = glyph.bounds.Left + glyph.bounds.Right; - Float bottom = glyph.bounds.Top + glyph.bounds.Bottom; - - // Add a quad for the current character - if ( glyph.bounds.Right > 0 && glyph.bounds.Bottom > 0 ) { - addGlyphQuad( mVertices, Vector2f( currentX, currentY ), glyph, italic, 0, - centerDiffX ); - } - - // Update the current bounds - minX = std::min( minX, currentX + left - italic * bottom ); - maxX = std::max( maxX, currentX + right - italic * top ); - minY = std::min( minY, currentY + top ); - maxY = std::max( maxY, currentY + bottom ); - - // Advance to the next character - if ( font->isColorEmojiFont() ) { - x += glyph.size.getWidth(); - y += ( curGlyphPos.y_advance / 64.f ) * - ( mFontStyleConfig.CharacterSize / - static_cast( font->face() )->available_sizes[0].height ); - } else { - x += glyph.advance; - y += curGlyphPos.y_advance / 64.f; - } - - prevGlyphIndex = curGlyph.codepoint; - } - - // If we're using the underlined style, add the last line - if ( underlined && run.runIsNewLine() ) { - addLine( mVertices, x, y, underlineOffset, underlineThickness, 0, centerDiffX ); - - if ( mFontStyleConfig.OutlineThickness != 0 ) - addLine( mOutlineVertices, x, y, underlineOffset, underlineThickness, - mFontStyleConfig.OutlineThickness, centerDiffX ); - } - - // If we're using the strike through style, add the last line across all characters - if ( strikeThrough && run.runIsNewLine() ) { - addLine( mVertices, x, y, strikeThroughOffset, underlineThickness, 0, centerDiffX ); - - if ( mFontStyleConfig.OutlineThickness != 0 ) - addLine( mOutlineVertices, x, y, strikeThroughOffset, underlineThickness, - mFontStyleConfig.OutlineThickness, centerDiffX ); - } - - if ( mCachedWidthNeedUpdate ) - mLinesWidth.push_back( x ); - - // next line - if ( run.runIsNewLine() ) { - y += vspace; - x = 0; - } - run.next(); - } + } ); // Update the bounding rectangle mBounds.Left = minX; @@ -1457,8 +1554,6 @@ void Text::ensureGeometryUpdate() { mCachedWidthNeedUpdate = false; } - hb_buffer_destroy( hbBuffer ); - return; } #endif diff --git a/src/examples/fonts/fonts.cpp b/src/examples/fonts/fonts.cpp index 8362350d3..aef510d6b 100644 --- a/src/examples/fonts/fonts.cpp +++ b/src/examples/fonts/fonts.cpp @@ -1,56 +1,8 @@ #include -EE::Window::Window* win = NULL; -FontTrueType* fontTest; -FontTrueType* fontTest2; -FontTrueType* fontEmoji; -FontTrueType* fontEmojiColor; -FontBMFont* fontBMFont; -FontSprite* fontSprite; -Text text; -Text text2; -Text text3; -Text text4; -Text text5; -Text text6; -Text text7; +EE_MAIN_FUNC int main( int, char*[] ) { + EE::Window::Window* win = NULL; -void mainLoop() { - // Clear the screen buffer - win->clear(); - - // Update the input - win->getInput()->update(); - - // Check if ESCAPE key is pressed - if ( win->getInput()->isKeyDown( KEY_ESCAPE ) ) { - // Close the window - win->close(); - } - - text.draw( ( win->getWidth() - text.getTextWidth() ) * 0.5f, 32 ); - - text2.draw( ( win->getWidth() - text2.getTextWidth() ) * 0.5f, 300 ); - - text7.draw( ( win->getWidth() - text7.getTextWidth() ) * 0.5f, 400 ); - - // Text rotated and scaled - text2.draw( ( win->getWidth() - text2.getTextWidth() ) * 0.5f, 430, Vector2f( 1.1f, 1.1f ), - 12.5f ); - - text3.draw( ( win->getWidth() - text3.getTextWidth() ) * 0.5f, 560 ); - - text4.draw( ( win->getWidth() - text4.getTextWidth() ) * 0.5f, 590 ); - - text5.draw( ( win->getWidth() - text5.getTextWidth() ) * 0.5f, 640 ); - - text6.draw( ( win->getWidth() - text6.getTextWidth() ) * 0.5f, 690 ); - - // Draw frame - win->display(); -} - -EE_MAIN_FUNC int main( int argc, char* argv[] ) { // Create a new window win = Engine::instance()->createWindow( WindowSettings( 1024, 768, "eepp - Fonts" ), ContextSettings( true ) ); @@ -73,25 +25,26 @@ EE_MAIN_FUNC int main( int argc, char* argv[] ) { "cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non " "proident, sunt in culpa qui officia deserunt mollit anim id est laborum." ); - fontTest = FontTrueType::New( "DejaVuSansMono" ); + FontTrueType* fontTest = FontTrueType::New( "DejaVuSansMono" ); fontTest->loadFromFile( "assets/fonts/DejaVuSansMono.ttf" ); - fontTest2 = FontTrueType::New( "NotoSans-Regular" ); + FontTrueType* fontTest2 = FontTrueType::New( "NotoSans-Regular" ); fontTest2->loadFromFile( "assets/fonts/NotoSans-Regular.ttf" ); - fontEmoji= FontTrueType::New( "NotoEmoji-Regular" ); + FontTrueType* fontEmoji = FontTrueType::New( "NotoEmoji-Regular" ); fontEmoji->loadFromFile( "assets/fonts/NotoEmoji-Regular.ttf" ); - fontEmojiColor = FontTrueType::New( "NotoColorEmoji" ); + FontTrueType* fontEmojiColor = FontTrueType::New( "NotoColorEmoji" ); fontEmojiColor->loadFromFile( "assets/fonts/NotoColorEmoji.ttf" ); - fontBMFont = FontBMFont::New( "bmfont" ); + FontBMFont* fontBMFont = FontBMFont::New( "bmfont" ); fontBMFont->loadFromFile( "assets/fonts/bmfont.fnt" ); - fontSprite = FontSprite::New( + FontSprite* fontSprite = FontSprite::New( "alagard" ); // Alagard - Hewett Tsoi ( https://www.dafont.com/alagard.font ) fontSprite->loadFromFile( "assets/fonts/custom_alagard.png", Color::Fuchsia, 32, -4 ); + Text text; text.setFont( fontTest ); text.setFontSize( 24 ); text.setAlign( TEXT_ALIGN_CENTER ); @@ -106,11 +59,13 @@ EE_MAIN_FUNC int main( int argc, char* argv[] ) { text.setFillColor( Color( 255 * i / size, 0, 0, 255 ), i, i + 1 ); } + Text text2; text2.setFont( fontTest2 ); text2.setString( "Lorem ipsum dolor sit amet, consectetur adipisicing elit. 👽" ); text2.setFontSize( 32 ); text2.setFillColor( Color::Black ); + Text text3; text3.setFont( fontTest ); text3.setString( text2.getString() ); text3.setFontSize( 24 ); @@ -118,19 +73,23 @@ EE_MAIN_FUNC int main( int argc, char* argv[] ) { text3.setOutlineThickness( 2 ); text3.setOutlineColor( Color( 0, 0, 0, 255 ) ); + Text text4; text4.setFont( fontBMFont ); text4.setString( text2.getString() ); text4.setFontSize( 45 ); text4.setFillColor( Color::Black ); + Text text5; text5.setFont( fontSprite ); text5.setString( text2.getString() ); text5.setFontSize( 38 ); + Text text6; text6.setFont( fontEmojiColor ); text6.setFontSize( 64 ); text6.setString( "👽 😀 💩 😃 👻" ); + Text text7; text7.setFont( fontEmoji ); text7.setFontSize( 32 ); text7.setString( "👽 😀 💩 😃 👻" ); @@ -139,7 +98,40 @@ EE_MAIN_FUNC int main( int argc, char* argv[] ) { text7.setOutlineColor( Color( 0, 0, 0, 255 ) ); // Application loop - win->runMainLoop( &mainLoop ); + win->runMainLoop( [&] { + // Clear the screen buffer + win->clear(); + + // Update the input + win->getInput()->update(); + + // Check if ESCAPE key is pressed + if ( win->getInput()->isKeyDown( KEY_ESCAPE ) ) { + // Close the window + win->close(); + } + + text.draw( ( win->getWidth() - text.getTextWidth() ) * 0.5f, 32 ); + + text2.draw( ( win->getWidth() - text2.getTextWidth() ) * 0.5f, 300 ); + + text7.draw( ( win->getWidth() - text7.getTextWidth() ) * 0.5f, 400 ); + + // Text rotated and scaled + text2.draw( ( win->getWidth() - text2.getTextWidth() ) * 0.5f, 430, + Vector2f( 1.1f, 1.1f ), 12.5f ); + + text3.draw( ( win->getWidth() - text3.getTextWidth() ) * 0.5f, 560 ); + + text4.draw( ( win->getWidth() - text4.getTextWidth() ) * 0.5f, 590 ); + + text5.draw( ( win->getWidth() - text5.getTextWidth() ) * 0.5f, 640 ); + + text6.draw( ( win->getWidth() - text6.getTextWidth() ) * 0.5f, 690 ); + + // Draw frame + win->display(); + } ); } // Destroy the engine instance. Destroys all the windows and engine singletons.