mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-05-28 17:16:29 +03:00
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:
@@ -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.
BIN
bin/unit_tests/assets/fontrendering/eepp-emojis-with-text.webp
Normal file
BIN
bin/unit_tests/assets/fontrendering/eepp-emojis-with-text.webp
Normal file
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 |
@@ -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();
|
||||
|
||||
@@ -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 );
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 );
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user