Improve color emoji rendering.

Add new test for emojis + text.
Enabled text-shaper by default.
Updated the NotoColorEmoji font with the latest version.
This commit is contained in:
Martín Lucas Golini
2025-12-30 17:52:31 -03:00
parent 34d6115db8
commit d5e10aa790
11 changed files with 161 additions and 44 deletions

View File

@@ -204,7 +204,7 @@
},
"run": [
{
"args": "--text-shaper",
"args": "",
"command": "${project_root}/bin/ecode-debug",
"name": "ecode-debug",
"working_dir": "${project_root}/bin"

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 85 KiB

View File

@@ -158,7 +158,7 @@ class EE_API FontTrueType : public Font {
protected:
friend class Text;
friend class TextLayouter;
friend class TextLayout;
explicit FontTrueType( const std::string& FontName );
@@ -175,17 +175,19 @@ class EE_API FontTrueType : public Font {
typedef UnorderedMap<Uint64, GlyphDrawable*> GlyphDrawableTable;
struct Page {
explicit Page( const Uint32 fontInternalId, const std::string& pageName );
explicit Page( const Uint32 fontInternalId, const std::string& pageName,
const FontTrueType* font );
~Page();
GlyphTable glyphs; ///< Table mapping code points to their corresponding glyph
GlyphDrawableTable
drawables; ///> Table mapping code points to their corresponding glyph drawables.
Texture* texture; ///< Texture containing the pixels of the glyphs
unsigned int nextRow; ///< Y position of the next new row in the texture
drawables; ///> Table mapping code points to their corresponding glyph drawables.
Texture* texture; ///< Texture containing the pixels of the glyphs
std::vector<Row> rows; ///< List containing the position of all the existing rows
Uint32 fontInternalId{ 0 };
Uint32 fontInternalId{ 0 }; // The font internal id
unsigned int nextRow; ///< Y position of the next new row in the texture
const FontTrueType* font{ nullptr };
};
void cleanup();

View File

@@ -39,6 +39,7 @@ RETAIN_SYMBOL( FT_Palette_Select );
using namespace EE;
#ifdef EE_TRUETYPE_SVG_FONT_ENABLED
struct SVG_Data {
NSVGrasterizer* rasterizer;
};
@@ -181,6 +182,7 @@ FT_Error svg_render( FT_GlyphSlot slot, FT_Pointer* data_pointer ) {
}
static SVG_RendererHooks svg_hooks = { svg_init, svg_free, svg_render, svg_preset };
#endif
// FreeType callbacks that operate on a IOStream
unsigned long read( FT_Stream rec, unsigned long offset, unsigned char* buffer,
@@ -430,8 +432,13 @@ bool FontTrueType::setFontFace( void* _face ) {
mIsBold = face->style_flags & FT_STYLE_FLAG_BOLD;
mIsItalic = face->style_flags & FT_STYLE_FLAG_ITALIC;
if ( mHasSvgGlyphs )
FT_Property_Set( static_cast<FT_Library>( mLibrary ), "ot-svg", "svg-hooks", &svg_hooks );
if ( mHasSvgGlyphs ) {
#ifdef EE_TRUETYPE_SVG_FONT_ENABLED
FT_Property_Set( static_cast<FT_Library>( mLibrary ), "ot-svg", "svg-hooks", &svg_hooks );
#else
return false;
#endif
}
if ( ( mIsColorEmojiFont || mHasSvgGlyphs || mHasColrGlyphs ) &&
FontManager::instance()->getColorEmojiFont() == nullptr )
@@ -1175,14 +1182,23 @@ Glyph FontTrueType::loadGlyphByIndex( Uint32 index, unsigned int characterSize,
const int padding = 2;
Float scale = 1.f;
if ( mIsColorEmojiFont || mIsEmojiFont ) {
scale = eemin( 1.f, (Float)characterSize / height );
}
int destWidth = width;
int destHeight = height;
if ( mIsColorEmojiFont ) {
// Browsers scale emojis slightly larger than the EM size to match the visual weight
// of the text's Ascender.
// Noto Sans Ascent is ~1.07em. Applying 1.1x scaling results in ~1.18em,
// which matches Chrome/Firefox rendering behavior for Noto Color Emoji.
Float targetSize = ( page.font != this )
? (Float)page.font->getAscent( characterSize ) * 1.1f
: (Float)characterSize;
scale = eemin( 1.f, targetSize / height );
} else if ( mIsEmojiFont ) {
scale = eemin( 1.f, (Float)characterSize / height );
}
glyph.advance = eeceil( glyph.advance * scale );
if ( scale >= 1.f ) {
@@ -1502,7 +1518,7 @@ FontTrueType::Page& FontTrueType::getPage( unsigned int characterSize ) const {
name += ":bold";
if ( mIsItalic )
name += ":italic";
mPages[characterSize] = std::make_unique<Page>( mFontInternalId, name );
mPages[characterSize] = std::make_unique<Page>( mFontInternalId, name, this );
pageIt = mPages.find( characterSize );
}
return *pageIt->second;
@@ -1681,8 +1697,9 @@ bool FontTrueType::hasColrGlyphs() const {
return mHasColrGlyphs;
}
FontTrueType::Page::Page( const Uint32 fontInternalId, const std::string& pageName ) :
texture( NULL ), nextRow( 3 ), fontInternalId( fontInternalId ) {
FontTrueType::Page::Page( const Uint32 fontInternalId, const std::string& pageName,
const FontTrueType* font ) :
texture( NULL ), fontInternalId( fontInternalId ), nextRow( 3 ), font( font ) {
// Make sure that the texture is initialized by default
Image image;
image.create( 128, 128, 4 );

View File

@@ -14,7 +14,11 @@
namespace EE { namespace Graphics {
#ifdef EE_TEXT_SHAPER_ENABLED
bool Text::TextShaperEnabled = true;
#else
bool Text::TextShaperEnabled = false;
#endif
bool Text::TextShaperOptimizations = true;
Uint32 Text::GlobalInvalidationId = 0;

View File

@@ -283,7 +283,8 @@ TextLayout::Cache TextLayout::layout( const StringType& string, Font* font,
}
Glyph currentGlyph = currentRunFont->getGlyphByIndex(
glyphInfo[i].codepoint, characterSize, bold, italic, outlineThickness );
glyphInfo[i].codepoint, characterSize, bold, italic, outlineThickness,
rFont->getPage( characterSize ) );
if ( ch != '\n' && ch != '\r' &&
!( textDrawHints & TextHints::NoKerning ) ) {
@@ -340,7 +341,8 @@ TextLayout::Cache TextLayout::layout( const StringType& string, Font* font,
pen.x += Font::isEmojiCodePoint( ch )
? currentRunFont
->getGlyphByIndex( glyphInfo[i].codepoint, characterSize,
bold, italic, outlineThickness )
bold, italic, outlineThickness,
rFont->getPage( characterSize ) )
.advance
: sg.advance.x;
pen.y += sg.advance.y;

View File

@@ -181,7 +181,10 @@ UTEST( FontRendering, fontsTest ) {
};
UTEST_PRINT_STEP( "Text Shaper disabled" );
runTest();
{
BoolScopedOp op( Text::TextShaperEnabled, false );
runTest();
}
UTEST_PRINT_STEP( "Text Shaper enabled" );
{
@@ -213,7 +216,10 @@ UTEST( FontRendering, editorTest ) {
};
UTEST_PRINT_STEP( "Text Shaper disabled" );
runTest();
{
BoolScopedOp op( Text::TextShaperEnabled, false );
runTest();
}
UTEST_PRINT_STEP( "Text Shaper enabled" );
{
@@ -242,7 +248,10 @@ UTEST( FontRendering, textEditTest ) {
};
UTEST_PRINT_STEP( "Text Shaper disabled" );
runTest();
{
BoolScopedOp op( Text::TextShaperEnabled, false );
runTest();
}
UTEST_PRINT_STEP( "Text Shaper enabled" );
{
@@ -272,7 +281,10 @@ UTEST( FontRendering, tabsTest ) {
};
UTEST_PRINT_STEP( "Text Shaper disabled" );
runTest();
{
BoolScopedOp op( Text::TextShaperEnabled, false );
runTest();
}
UTEST_PRINT_STEP( "Text Shaper enabled" );
{
@@ -303,7 +315,10 @@ UTEST( FontRendering, tabStopTest ) {
};
UTEST_PRINT_STEP( "Text Shaper disabled" );
runTest();
{
BoolScopedOp op( Text::TextShaperEnabled, false );
runTest();
}
UTEST_PRINT_STEP( "Text Shaper enabled" );
{
@@ -332,7 +347,10 @@ UTEST( FontRendering, tabsTextEditTest ) {
};
UTEST_PRINT_STEP( "Text Shaper disabled" );
runTest();
{
BoolScopedOp op( Text::TextShaperEnabled, false );
runTest();
}
UTEST_PRINT_STEP( "Text Shaper enabled" );
{
@@ -362,7 +380,10 @@ UTEST( FontRendering, tabStopTextEditTest ) {
};
UTEST_PRINT_STEP( "Text Shaper disabled" );
runTest();
{
BoolScopedOp op( Text::TextShaperEnabled, false );
runTest();
}
UTEST_PRINT_STEP( "Text Shaper enabled" );
{
@@ -393,7 +414,10 @@ UTEST( FontRendering, textViewTest ) {
};
UTEST_PRINT_STEP( "Text Shaper disabled" );
runTest();
{
BoolScopedOp op( Text::TextShaperEnabled, false );
runTest();
}
UTEST_PRINT_STEP( "Text Shaper enabled" );
{
@@ -546,7 +570,10 @@ UTEST( FontRendering, textSizes ) {
};
UTEST_PRINT_STEP( "Text Shaper disabled" );
runTest();
{
BoolScopedOp op( Text::TextShaperEnabled, false );
runTest();
}
UTEST_PRINT_STEP( "Text Shaper enabled" );
{
@@ -608,16 +635,22 @@ UTEST( FontRendering, textStyles ) {
config.Style = style;
UTEST_PRINT_STEP( styleName.data() );
UTEST_PRINT_STEP( " Text Shaper disabled" );
runTest( styleName, textAlign );
UTEST_PRINT_STEP( " Text Shaper enabled" );
BoolScopedOp op( Text::TextShaperEnabled, true );
runTest( styleName, textAlign );
{
UTEST_PRINT_STEP( " Text Shaper disabled" );
BoolScopedOp op( Text::TextShaperEnabled, false );
runTest( styleName, textAlign );
}
UTEST_PRINT_STEP( " Text Shaper enabled w/o optimizations" );
BoolScopedOp op2( Text::TextShaperOptimizations, false );
runTest( styleName, textAlign );
{
UTEST_PRINT_STEP( " Text Shaper enabled" );
BoolScopedOp op( Text::TextShaperEnabled, true );
runTest( styleName, textAlign );
UTEST_PRINT_STEP( " Text Shaper enabled w/o optimizations" );
BoolScopedOp op2( Text::TextShaperOptimizations, false );
runTest( styleName, textAlign );
}
};
runTestSuite( Text::Regular, "regular" );
@@ -640,3 +673,58 @@ UTEST( FontRendering, textStyles ) {
Engine::destroySingleton();
}
UTEST( FontRendering, emojisWithText ) {
auto win = Engine::instance()->createWindow(
WindowSettings( 1024, 230, "eepp - Emojis With Text", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false, true ) );
ASSERT_TRUE_MSG( win->isOpen(), "Failed to create Window" );
Text::TextShaperEnabled = false;
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" );
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
FontFamily::loadFromRegular( font );
FontTrueType* fontEmojiColor = FontTrueType::New( "NotoColorEmoji" );
fontEmojiColor->loadFromFile( "../assets/fonts/NotoColorEmoji.ttf" );
win->setClearColor( RGB( 255, 255, 255 ) );
FontStyleConfig config;
config.Font = font;
config.FontColor = Color::Black;
config.CharacterSize = 16;
String txt(
R"txt(👻 Lorem ipsum dolor sit amet, 👻 consectetur adipisicing elit, sed do eiusmod🤯 tempor incididunt ut labore et dolore magna
aliqua. Ut enim ad😎 minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat 🤖.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur 🧐. Excepteur sint occaecat
cupidatat non proident👽, sunt in culpa qui officia deserunt mollit anim id est laborum. 😀)txt" );
const auto runTest = [&]() {
win->clear();
Text text;
text.setStyleConfig( config );
text.setString( txt );
text.draw( 32, 32 );
compareImages( utest_state, utest_result, win, "eepp-emojis-with-text" );
};
UTEST_PRINT_STEP( " Text Shaper disabled" );
{
BoolScopedOp op( Text::TextShaperEnabled, false );
runTest();
}
UTEST_PRINT_STEP( " Text Shaper enabled" );
BoolScopedOp op( Text::TextShaperEnabled, true );
runTest();
UTEST_PRINT_STEP( " Text Shaper enabled w/o optimizations" );
BoolScopedOp op2( Text::TextShaperOptimizations, false );
runTest();
Engine::destroySingleton();
}

View File

@@ -144,8 +144,6 @@ void AppConfig::load( const std::string& confPath, std::string& keybindingsPath,
FontTrueType::fontHintingFromString( ini.getValue( "ui", "font_hinting", "full" ) );
ui.fontAntialiasing = FontTrueType::fontAntialiasingFromString(
ini.getValue( "ui", "font_antialiasing", "grayscale" ) );
Text::TextShaperEnabled |= ini.getValueB( "ui", "text_shaper", false );
Text::TextShaperOptimizations |= ini.getValueB( "ui", "text_shaper_optimizations", true );
ui.editorFontInInputFields = ini.getValueB( "ui", "editor_font_in_input_fields", true );
doc.trimTrailingWhitespaces = ini.getValueB( "document", "trim_trailing_whitespaces", false );

View File

@@ -2678,8 +2678,9 @@ void App::onCodeEditorCreated( UICodeEditor* editor, TextDocument& doc ) {
if ( mSplitter->curEditorExistsAndFocused() && mSplitter->getCurEditor() ) {
UICodeEditor* editor = mSplitter->getCurEditor();
auto d = mSplitter->createCodeEditorInTabWidget(
mConfig.editor.openDocumentsInMainSplit ? mSplitter->getPreferredTabWidget()
: mSplitter->tabWidgetFromWidget( editor ) );
mConfig.editor.openDocumentsInMainSplit
? mSplitter->getPreferredTabWidget()
: mSplitter->tabWidgetFromWidget( editor ) );
if ( d.first == nullptr && d.second == nullptr && !mSplitter->getTabWidgets().empty() )
d = mSplitter->createCodeEditorInTabWidget( mSplitter->getTabWidgets()[0] );
if ( d.first != nullptr || d.second != nullptr ) {
@@ -4670,8 +4671,13 @@ EE_MAIN_FUNC int main( int argc, char* argv[] ) {
{ "first-instance" } );
#ifdef EE_TEXT_SHAPER_ENABLED
args::Flag textShaper( parser, "text-shaper", "Enables text-shaping capabilities",
{ "text-shaper" } );
args::Flag textShaper(
parser, "text-shaper",
"WARNING: Do not use this option. It will be completely "
"removed soon. It does nothing since text-shaper is enabled by default.",
{ "text-shaper" } );
args::Flag noTextShaper( parser, "no-text-shaper", "Disables text-shaping capabilities",
{ "no-text-shaper" } );
#endif
std::vector<std::string> args;
@@ -4726,8 +4732,8 @@ EE_MAIN_FUNC int main( int argc, char* argv[] ) {
}
#ifdef EE_TEXT_SHAPER_ENABLED
if ( textShaper.Get() )
Text::TextShaperEnabled = true;
if ( noTextShaper.Get() )
Text::TextShaperEnabled = false;
#endif
App::InitParameters params;