More improvements for text shaping.

This commit is contained in:
Martín Lucas Golini
2024-06-14 02:23:53 -03:00
parent b3cd551ff4
commit 3bed787795
3 changed files with 268 additions and 181 deletions

View File

@@ -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<unsigned int, std::unique_ptr<Page>>

View File

@@ -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<void( hb_glyph_info_t*, hb_glyph_position_t*, Uint32,
TextShapeRun& )>& 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<hb_font_t*>( 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<Float>( 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<FontTrueType*>( 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<FontTrueType*>( 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<hb_font_t*>( 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<FT_Face>( 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

View File

@@ -1,56 +1,8 @@
#include <eepp/ee.hpp>
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.