From 0bf2b89970f9cad0b2630f0e6ce473e7fc24eaf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Sun, 26 Jul 2026 00:23:24 -0300 Subject: [PATCH] Handle downscaled LCD glyphs with a dedicated resize path before atlas upload. This preserves row stride and RGB coverage, fixing corrupted icon-font glyphs at small sizes. Add a golden-image regression test covering scaled subpixel glyphs from the Nonicons font. --- .../eepp-scaled-subpixel-glyph-atlas.webp | Bin 0 -> 4208 bytes src/eepp/graphics/fonttruetype.cpp | 56 ++++++++++++++---- src/tests/unit_tests/fontrendering_tests.cpp | 30 ++++++++++ 3 files changed, 73 insertions(+), 13 deletions(-) create mode 100644 bin/assets/fontrendering/eepp-scaled-subpixel-glyph-atlas.webp diff --git a/bin/assets/fontrendering/eepp-scaled-subpixel-glyph-atlas.webp b/bin/assets/fontrendering/eepp-scaled-subpixel-glyph-atlas.webp new file mode 100644 index 0000000000000000000000000000000000000000..15ddbc5fae97d60eb73ce6527bf0e2e498af6b6b GIT binary patch literal 4208 zcmV-$5RdOtNk&F!5C8yIMM6+kP&iCn5C8x#|G*CbHQCYse|;n8LO=Z_`7cZV0mEo! zHDEt$9`MY=%xDku7>1c=d3P8zJj=X0%uHsG$Cl)ghv7T?`%Alh2a8VkDvfd^h{x3(>9tmh^0@*;7q zq~69GX~(L^-GAx%IPNaBbTr1qL);bUzXP|C6v=&BhB}^^Com+}^y@nQyH*1XIROb% z2r|@Hx>`F*8u&X~hzf}lWFjk74wDHAqN_#KUU#J`XfPhiamvf=M1^E;Qg$}7R|N*k zOU2OG@FEi57A%0Q1X(F$1$02YDuwArBvN;AEJNFK-r3xGnKCPo0h)*J*Zprf3 zGQk=uCD!R+4Edny_PmnKB2yNitN^dtS6}+Lg;M9)vq90O%U*)S2{J~8f}y{B^1cTU zwo^1f>!{6yhe>szQX+!P%1Omg4BKz%wLAWP%{(*7T}WD!Jj_=U5)r_6xJBZ&6V z555POq|?Z(pe&gI0$XX-N(0QmV)gEU&z=1u(vonIJ1cnE-n$6Erix zr_(_P3s@Pw<-VfXKk=B_yPt)2a5{j=d5-D;VqD41k=a^k5-Uksgs4E4NVq$M-R zNM&g41E-D;zrur7pPM_-9hdpFAd57-y6r8KV=lj88 z)5a|QeaWxbNt~~o`p!!>Lw$@x%I6-tPh2GgwxrC4fWoxIx8ggaWZ&MSZ+G0zsnWo3 zXlCxIX)l>ga*tp}VPp$+ZK247BCThq3=NToA@isb;Yqth0rT8pYRXGiQ?V&n69(Pk^ZdOsN+gDp0XebENsTUStcR^h*yl2kzph* zW7?>@Z|_9k-G>4sS&P+@IjnIWSX5ge0#Hu&WqjNXhL+}MQA^vniM^N4KjrXgU4t4d zcjo=$6^72}3y*~}5k0VVD9beDA5TF&p&3Cm-Qyk<1f@E8#fxeyhS=tA^D5ESfC?^}3%JHZ}TY_X3i*<3%0A2y}w z2to0;kJ%FOY#%2UE!)%&@`h$S;%x2wr(vQE41fT1^0)spMre@z4w^s!Bb&kz&DeUb zaRN`O@$I)Nod+NfiPrvkOY}^lohx%Q^qJzWj*DS5K5hJry%VqQ4LDV21@C0nIp2i> zY*&@1T1TUhnlhV%37q^*JgOg+?(D$@%!oeJsg`Cuy0%T7X%|m?6Cc*<%3jA&SLtYmSCX=ZP3%41 zr}VkN!I+(A`d&08<4SN}6c3aUi&i#T(-rJ|=xS!~M>Wmi>t9h2d2T9wFScK*psV%1 zJQc>elgg${W7Vi}{D1y(*=MuM0i0qCz58;3#;WloN&wB)pQfU6N3R3OHw|<;m9G(v z{`kbrNO4s;{5G+RO$6OHP2mV&X40Vh-gm7o>+^X-jy)@ah!Ly2IfMc9`TYyC5r#W4 zIn#F4!#oj%L!1Ro4;=s>4Bxa75&seO_&? z*DI}!xU)n>pwO9V6tqm4LKueQ(cm=@Kt zD#8}#;lljlQcKHLre?Ck>C>Y)Z{obpD!|hMk)C5W@0}ANypPhzKBI_`Z#Z`1!O4_8m)aq}>@uit&U6 zNZC`*&X*Js5kRN$M&T(6DH@+K+V85p&W(M(o_5TuDE0tSK#?Qrd+DaSDK7$re%#iu zo?-P+T%^}l^Xw91Z-}+vVOF?GH)DHK>86~TFvyVeZ-?jB^($FyYkPSJ zvedzwSF^k*>l6~Z#}?+E6jHbofJ?*IZ8g7g`m0rwg4Qb>5x}tFf0r&qELQme&LDN| zz8F9UztG0}e4^>}deYEVtv%m%No~uMrc2`FsvCr1qfPmy45RiR@{Y@9#2#wZ%YWah zNwr8OoXF78>dN*aHLGKIB`Ir^u`+i1sn;NuoSuvGp8E%7#PY?JHU zXtfj8ypsFPcNvg6XIG#ITbQ!ZJ{?BrDU-R}lfRDd zIV3(MW||&w4}r?`&e$`*0}{kIIRm9DBoMtqy92{JkX04{&e>~2V;TosjQ1SEpG0N-xfQpHpO?dL2(7>Y%Q3tii;^C$EKaG1i`V6^eu6}Rl zL*01!sFQVbN2D}kYp7q6)>KqT=6VIs+^S>i90SZ^EOM0pE3e%ljXk!T;(Bv2bhpIN z)Zj8?Dp`nj(v|*^thaf9Ws5ten%QQ!DS1^psTYocdwyuZR!z;go-N?%YNpN-JPAb{ zOesu5{L8*GubeOdo1xThBPnPwhi0}oIpd`abG2t6!%RfqwTB{eg%>-0bLYdLc_K%6 zGufI2!}Gt%X)nE^zdxGwc>J%^Mgxi@aN}(4x`+X+wL@3PFR>!IEr31I^I617y3x`-5e^U z;Odm^26se*S`$D{*11Q}wPd*l3Qc7#uAk zmbwTmEY1Ug4BL4Le-#D9$Scjg#5JDi22Duz-{`E+bE&jQ(r@K&vKJj+P zVFW;~vROi}PB`w!Y%@HN0xCMTG;>F@(pE;vLP@4Z_h?EmV^s62!wyzbh6YM@NMSx% z)a%e5`?m_kxv6?35MP?7xvuso!mVp8(n43Fgtzx7JjCmF{Rq)x>^kZw1Pj1JAah z2efppGmVh=wvDzh#fW&$g9WA#JZ$$vi7cXS>gawH+0@}E&01-kD5x1458u)yt5+V( zZ7x4tCOR*YEavXZ?sxygzqzsCaD!-W=!>7Hn=QBBodIAb)CK~X6qrFXD`W(7J;|H&ubmDBFpujIA6&~xC9?tyzut8^TK1F#v!nS$Zp^%4o~_==G;kG7Ny z&XLo>1c^Kr1yMY*vwp$;8qrOTO(wm~N;V22_gKf|M4zUsj-+~6hx-D^9a#}XNAt3! z)=FKWVdamzxR9Yetn6%HtH~XAX>=Y*X!8~d{oD%ZRj>N_-Z79beaS*;X2Qf;KN})_ zbZq?^eJktpvg>AcWailg?vH_<8opIPd=ZQ!_Z{| literal 0 HcmV?d00001 diff --git a/src/eepp/graphics/fonttruetype.cpp b/src/eepp/graphics/fonttruetype.cpp index a402433b0..2a5f12308 100644 --- a/src/eepp/graphics/fonttruetype.cpp +++ b/src/eepp/graphics/fonttruetype.cpp @@ -1621,20 +1621,50 @@ Glyph FontTrueType::loadGlyphByIndex( Uint32 index, unsigned int characterSize, destHeight = dest.getHeight() + 2 * padding; } } else if ( bitmap.pixel_mode == FT_PIXEL_MODE_LCD ) { - for ( int y = padding; y < height - padding; ++y ) { - for ( int x = padding; x < width - padding; ++x ) { - const std::size_t index = ( x + y * width ) * 4; - const Uint8* px = &pixels[( x - padding ) * 3]; - mPixelBuffer[index + 0] = - glyph.renderMode == GlyphRenderMode::Subpixel ? px[0] : 255; - mPixelBuffer[index + 1] = - glyph.renderMode == GlyphRenderMode::Subpixel ? px[1] : 255; - mPixelBuffer[index + 2] = - glyph.renderMode == GlyphRenderMode::Subpixel ? px[2] : 255; - mPixelBuffer[index + 3] = - (Uint8)( ( (int)px[0] + (int)px[1] + (int)px[2] ) / 3.f ); + if ( scale < 1.f ) { + for ( int y = 0; y < height; ++y ) { + for ( int x = 0; x < width; ++x ) { + const std::size_t index = ( x + y * width ) * 4; + const Uint8* px = &pixels[x * 3]; + mPixelBuffer[index + 0] = + glyph.renderMode == GlyphRenderMode::Subpixel ? px[0] : 255; + mPixelBuffer[index + 1] = + glyph.renderMode == GlyphRenderMode::Subpixel ? px[1] : 255; + mPixelBuffer[index + 2] = + glyph.renderMode == GlyphRenderMode::Subpixel ? px[2] : 255; + mPixelBuffer[index + 3] = + (Uint8)( ( (int)px[0] + (int)px[1] + (int)px[2] ) / 3.f ); + } + pixels += bitmap.pitch; + } + + Image dest( &mPixelBuffer[0], width, height, 4 ); + dest.avoidFreeImage( true ); + dest.scale( scale ); + dest.avoidFreeImage( true ); + pixelPtr = dest.getPixels(); + glyph.bounds.Left *= scale; + glyph.bounds.Right *= scale; + glyph.bounds.Top *= scale; + glyph.bounds.Bottom *= scale; + destWidth = dest.getWidth() + 2 * padding; + destHeight = dest.getHeight() + 2 * padding; + } else { + for ( int y = padding; y < height - padding; ++y ) { + for ( int x = padding; x < width - padding; ++x ) { + const std::size_t index = ( x + y * width ) * 4; + const Uint8* px = &pixels[( x - padding ) * 3]; + mPixelBuffer[index + 0] = + glyph.renderMode == GlyphRenderMode::Subpixel ? px[0] : 255; + mPixelBuffer[index + 1] = + glyph.renderMode == GlyphRenderMode::Subpixel ? px[1] : 255; + mPixelBuffer[index + 2] = + glyph.renderMode == GlyphRenderMode::Subpixel ? px[2] : 255; + mPixelBuffer[index + 3] = + (Uint8)( ( (int)px[0] + (int)px[1] + (int)px[2] ) / 3.f ); + } + pixels += bitmap.pitch; } - pixels += bitmap.pitch; } } else { if ( scale < 1.f ) { diff --git a/src/tests/unit_tests/fontrendering_tests.cpp b/src/tests/unit_tests/fontrendering_tests.cpp index 523af25c8..c13dd3ba8 100644 --- a/src/tests/unit_tests/fontrendering_tests.cpp +++ b/src/tests/unit_tests/fontrendering_tests.cpp @@ -165,6 +165,36 @@ UTEST( FontRendering, subpixelCoverageCompositesPerChannel ) { "Subpixel text did not update a transparent target's alpha" ); } +UTEST( FontRendering, scaledSubpixelGlyphAtlas ) { + UIApplication app( + WindowSettings( 256, 64, "eepp - Scaled Subpixel Glyph Atlas", VisualTestWindowStyle, + WindowBackend::Default, 32 ), + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); + ResourceScope& scope = *app.getUI()->getResourceScope(); + FontTrueTypePtr font = FontTrueType::New( "ScaledSubpixelNonicons", scope ); + ASSERT_TRUE( font->loadFromFile( Sys::getProcessPath() + "../assets/fonts/nonicons.ttf" ) ); + font->setAntialiasing( FontAntialiasing::Subpixel ); + font->setIsEmojiFont( true ); + + EE::Window::Window* window = app.getWindow(); + window->setClearColor( Color( 40, 44, 52 ) ); + window->clear(); + const std::array codePoints = { 61718, 61719, 61720, 61743, + 61752, 61775, 61789, 61799 }; + Float x = 8.f; + for ( Uint32 codePoint : codePoints ) { + GlyphDrawable* glyph = font->getGlyphDrawable( codePoint, 18 ); + ASSERT_TRUE( glyph ); + ASSERT_EQ( GlyphRenderMode::Subpixel, glyph->getGlyphRenderMode() ); + const Sizef size = glyph->getPixelsSize(); + glyph->setColor( Color::White ); + glyph->draw( { std::trunc( x + ( 24.f - size.getWidth() ) * 0.5f ), + std::trunc( ( 64.f - size.getHeight() ) * 0.5f ) } ); + x += 30.f; + } + compareImages( utest_state, utest_result, window, "eepp-scaled-subpixel-glyph-atlas" ); +} + UTEST( FontRendering, loadingFontFamilyDoesNotCreateTexturePages ) { UIApplication app( WindowSettings( 320, 240, "eepp - Font Family Metrics Test", WindowStyle::Default,