diff --git a/.github/workflows/eepp-linux-build-check.yml b/.github/workflows/eepp-linux-build-check.yml index 10d2d7dcd..87a0ac5fe 100644 --- a/.github/workflows/eepp-linux-build-check.yml +++ b/.github/workflows/eepp-linux-build-check.yml @@ -19,7 +19,7 @@ jobs: sudo add-apt-repository -y universe sudo add-apt-repository -y multiverse sudo apt update - sudo apt install -y gcc-12 g++-12 wget libsdl2-2.0-0 libsdl2-dev + sudo apt install -y gcc-12 g++-12 wget libsdl2-2.0-0 libsdl2-dev mesa-utils xvfb sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 10 sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-12 10 sudo update-alternatives --install /usr/bin/cc cc /usr/bin/gcc 30 @@ -38,4 +38,4 @@ jobs: - name: Unit Tests run: | cd bin/unit_tests - ./eepp-unit_tests + xvfb-run ./eepp-unit_tests diff --git a/.github/workflows/eepp-macos-build-check.yml b/.github/workflows/eepp-macos-build-check.yml index 3e31adb00..462662a8e 100644 --- a/.github/workflows/eepp-macos-build-check.yml +++ b/.github/workflows/eepp-macos-build-check.yml @@ -16,7 +16,7 @@ jobs: git submodule update --init --recursive - name: Install dependencies run: | - brew install wget SDL2 premake + brew install wget SDL2 premake mesa - name: Build run: | premake5 --with-text-shaper --disable-static-build gmake @@ -24,5 +24,9 @@ jobs: - name: Unit Tests run: | cd bin/unit_tests + export LIBGL_ALWAYS_SOFTWARE=1 + export GALLIUM_DRIVER=llvmpipe + export SDL_VIDEODRIVER=cocoa ./eepp-unit_tests - + env: + DYLD_LIBRARY_PATH: /opt/homebrew/lib # Ensures Mesa's libGL is used diff --git a/.github/workflows/eepp-windows-build-check.yml b/.github/workflows/eepp-windows-build-check.yml index 861a160bc..02b8491ff 100644 --- a/.github/workflows/eepp-windows-build-check.yml +++ b/.github/workflows/eepp-windows-build-check.yml @@ -33,7 +33,21 @@ jobs: shell: cmd run: | "%MSBUILD_PATH%\MSBuild.exe" .\make\windows\eepp.sln -m /p:Platform="x64" /p:Configuration="release" + - name: Download Mesa3D for Windows + run: | + Invoke-WebRequest -Uri "https://github.com/pal1000/mesa-dist-win/releases/download/25.2.4/mesa3d-25.2.4-release-msvc.7z" -OutFile "mesa.7z" + 7z x mesa.7z -o"C:\mesa" + shell: powershell + - name: Set up Mesa DLLs + run: | + copy C:\mesa\x64\opengl32.dll . + copy C:\mesa\x64\libglapi.dll . + # Add Mesa to PATH for dependencies + set PATH=C:\mesa\x64;%PATH% + shell: cmd - name: Unit Tests shell: cmd run: | + set LIBGL_ALWAYS_SOFTWARE=1 + set GALLIUM_DRIVER=llvmpipe bin\unit_tests\eepp-unit_tests.exe diff --git a/bin/unit_tests/assets/fontrendering/eepp-fonts.webp b/bin/unit_tests/assets/fontrendering/eepp-fonts.webp new file mode 100644 index 000000000..53c543eeb Binary files /dev/null and b/bin/unit_tests/assets/fontrendering/eepp-fonts.webp differ diff --git a/include/eepp/graphics/image.hpp b/include/eepp/graphics/image.hpp index 2abefe862..97afec088 100644 --- a/include/eepp/graphics/image.hpp +++ b/include/eepp/graphics/image.hpp @@ -310,6 +310,17 @@ class EE_API Image { Image( IOStream& stream, const unsigned int& forceChannels = 0, const FormatConfiguration& formatConfiguration = FormatConfiguration() ); + Image( const Image& other ); + + /** Overload the assignment operator to ensure the image copy */ + Image& operator=( const Image& right ); + + /** @brief Move constructor */ + Image( Image&& other ) noexcept; + + /** @brief Move assignment operator */ + Image& operator=( Image&& other ) noexcept; + virtual ~Image(); /** Create an empty image data */ @@ -413,15 +424,36 @@ class EE_API Image { /** @return A copy of the original image */ Graphics::Image* copy(); - /** Overload the assignment operator to ensure the image copy */ - Graphics::Image& operator=( const Image& right ); - /** Set the image format configuration */ void setImageFormatConfiguration( const FormatConfiguration& imageFormatConfiguration ); /** @return The image format configuration */ const FormatConfiguration& getImageFormatConfiguration() const; + struct DiffResult { + Image* diffImage{ nullptr }; ///< The visual diff image. Null if dimensions mismatched. + long long numDifferentPixels{ 0 }; ///< The number of pixels that exceeded the threshold. + double maxDeltaE{ 0.0 }; ///< The maximum perceptual difference (Delta E) found. + bool areSame() const { return numDifferentPixels == 0; } + ~DiffResult() { eeSAFE_DELETE( diffImage ); } + }; + + /** + * @brief Performs a visual and perceptual diff against another image. + * + * This method compares the current image with another using the CIEDE2000 Delta E formula, + * which mimics human perception of color differences. + * + * @param other The image to compare against. + * @param threshold The Delta E threshold. Differences below this value are ignored. + * A value of 1.0 is roughly the limit of human perception. + * A common default for tests is 2.3 (a "just noticeable difference"). + * @param diffColor The color used to highlight differing pixels in the output image. + * @return A DiffResult struct containing the diff image and statistics. + */ + DiffResult diff( const Image& other, float threshold = 2.3f, + const Color& diffColor = Color( 255, 0, 255, 255 ) ) const; + protected: Uint8* mPixels; unsigned int mWidth; diff --git a/include/eepp/window/window.hpp b/include/eepp/window/window.hpp index 90ba03933..7501f3a2f 100644 --- a/include/eepp/window/window.hpp +++ b/include/eepp/window/window.hpp @@ -366,6 +366,12 @@ class EE_API Window { bool takeScreenshot( std::string filepath = "", const Image::SaveType& Format = Image::SaveType::PNG ); + /** Captures the window front buffer into an Image + * You have to call it before Display, and after render all the objects. + * @return False if failed, otherwise returns True + */ + Image getFrontBufferImage(); + /** @return The pointer to the Window Info ( read only ) */ const WindowInfo* getWindowInfo() const; diff --git a/src/eepp/graphics/fontbmfont.cpp b/src/eepp/graphics/fontbmfont.cpp index be7acab63..2763a8d03 100644 --- a/src/eepp/graphics/fontbmfont.cpp +++ b/src/eepp/graphics/fontbmfont.cpp @@ -26,11 +26,6 @@ FontBMFont::FontBMFont( const std::string FontName ) : void FontBMFont::cleanup() { sendEvent( Event::Unload ); - Texture* texture = mPages[mFontSize].texture; - - if ( NULL != texture && TextureFactory::existsSingleton() ) - TextureFactory::instance()->remove( texture->getTextureId() ); - mPages.clear(); mFontSize = 0; mFilePath = ""; diff --git a/src/eepp/graphics/fontsprite.cpp b/src/eepp/graphics/fontsprite.cpp index 987005b25..fcd51031e 100644 --- a/src/eepp/graphics/fontsprite.cpp +++ b/src/eepp/graphics/fontsprite.cpp @@ -26,11 +26,6 @@ FontSprite::FontSprite( const std::string FontName ) : void FontSprite::cleanup() { sendEvent( Event::Unload ); - Texture* texture = mPages[mFontSize].texture; - - if ( NULL != texture && TextureFactory::existsSingleton() ) - TextureFactory::instance()->remove( texture->getTextureId() ); - mPages.clear(); mFontSize = 0; mFilePath = ""; diff --git a/src/eepp/graphics/image.cpp b/src/eepp/graphics/image.cpp index 6bdbd4edc..43e1d485c 100644 --- a/src/eepp/graphics/image.cpp +++ b/src/eepp/graphics/image.cpp @@ -27,6 +27,129 @@ namespace EE { namespace Graphics { +// Helper structs for color spaces +struct XYZ { + double x, y, z; +}; + +struct Lab { + double l, a, b; +}; + +// sRGB to Linear RGB conversion +static constexpr double srgbToLinear( double c ) { + return ( c > 0.04045 ) ? pow( ( c + 0.055 ) / 1.055, 2.4 ) : ( c / 12.92 ); +} + +// RGB to XYZ conversion (D65 illuminant) +static constexpr XYZ rgbToXyz( const Color& c ) { + double r = srgbToLinear( c.r / 255.0 ); + double g = srgbToLinear( c.g / 255.0 ); + double b = srgbToLinear( c.b / 255.0 ); + return { + .x = r * 0.4124564 + g * 0.3575761 + b * 0.1804375, + .y = r * 0.2126729 + g * 0.7151522 + b * 0.0721750, + .z = r * 0.0193339 + g * 0.1191920 + b * 0.9503041, + }; +} + +// XYZ to CIELAB conversion (D65 illuminant) +static constexpr Lab xyzToLab( const XYZ& xyz ) { + auto f = []( double t ) { return ( t > 0.008856 ) ? cbrt( t ) : ( 7.787 * t + 16.0 / 116.0 ); }; + // Reference white point for D65 + constexpr double ref_x = 0.95047; + constexpr double ref_y = 1.00000; + constexpr double ref_z = 1.08883; + + double fx = f( xyz.x / ref_x ); + double fy = f( xyz.y / ref_y ); + double fz = f( xyz.z / ref_z ); + + return { + .l = 116.0 * fy - 16.0, + .a = 500.0 * ( fx - fy ), + .b = 200.0 * ( fy - fz ), + }; +} + +// Calculate CIEDE2000 Delta E between two Lab colors +static constexpr double deltaECIEDE2000( const Lab& lab1, const Lab& lab2 ) { + double l1 = lab1.l, a1 = lab1.a, b1 = lab1.b; + double l2 = lab2.l, a2 = lab2.a, b2 = lab2.b; + + // Constants + constexpr double kL = 1.0, kC = 1.0, kH = 1.0; + constexpr double pi = 3.14159265358979323846; + + double c1 = sqrt( a1 * a1 + b1 * b1 ); + double c2 = sqrt( a2 * a2 + b2 * b2 ); + double c_bar = ( c1 + c2 ) / 2.0; + double c_bar7 = pow( c_bar, 7 ); + double g = 0.5 * ( 1.0 - sqrt( c_bar7 / ( c_bar7 + 6103515625.0 ) ) ); // 25^7 + + double a1_prime = a1 * ( 1.0 + g ); + double a2_prime = a2 * ( 1.0 + g ); + + double c1_prime = sqrt( a1_prime * a1_prime + b1 * b1 ); + double c2_prime = sqrt( a2_prime * a2_prime + b2 * b2 ); + + double h1_prime = ( a1_prime == 0 && b1 == 0 ) ? 0 : atan2( b1, a1_prime ); + if ( h1_prime < 0 ) + h1_prime += 2 * pi; + double h2_prime = ( a2_prime == 0 && b2 == 0 ) ? 0 : atan2( b2, a2_prime ); + if ( h2_prime < 0 ) + h2_prime += 2 * pi; + + double delta_l_prime = l2 - l1; + double delta_c_prime = c2_prime - c1_prime; + + double delta_h_prime; + double h_diff = h2_prime - h1_prime; + if ( c1_prime * c2_prime == 0 ) { + delta_h_prime = 0; + } else if ( fabs( h_diff ) <= pi ) { + delta_h_prime = h_diff; + } else if ( h_diff > pi ) { + delta_h_prime = h_diff - 2 * pi; + } else { + delta_h_prime = h_diff + 2 * pi; + } + delta_h_prime = 2.0 * sqrt( c1_prime * c2_prime ) * sin( delta_h_prime / 2.0 ); + + double l_bar_prime = ( l1 + l2 ) / 2.0; + double c_bar_prime = ( c1_prime + c2_prime ) / 2.0; + double h_bar_prime; + if ( c1_prime * c2_prime == 0 ) { + h_bar_prime = h1_prime + h2_prime; + } else if ( fabs( h1_prime - h2_prime ) <= pi ) { + h_bar_prime = ( h1_prime + h2_prime ) / 2.0; + } else if ( ( h1_prime + h2_prime ) < 2 * pi ) { + h_bar_prime = ( h1_prime + h2_prime + 2 * pi ) / 2.0; + } else { + h_bar_prime = ( h1_prime + h2_prime - 2 * pi ) / 2.0; + } + + double t = 1.0 - 0.17 * cos( h_bar_prime - pi / 6.0 ) + 0.24 * cos( 2.0 * h_bar_prime ) + + 0.32 * cos( 3.0 * h_bar_prime + pi / 30.0 ) - + 0.20 * cos( 4.0 * h_bar_prime - 63.0 * pi / 180.0 ); + + double s_l = 1.0 + ( 0.015 * pow( l_bar_prime - 50.0, 2 ) ) / + sqrt( 20.0 + pow( l_bar_prime - 50.0, 2 ) ); + double s_c = 1.0 + 0.045 * c_bar_prime; + double s_h = 1.0 + 0.015 * c_bar_prime * t; + + double delta_theta = + ( 30.0 * pi / 180.0 ) * exp( -pow( ( h_bar_prime * 180.0 / pi - 275.0 ) / 25.0, 2 ) ); + double r_c = 2.0 * sqrt( pow( c_bar_prime, 7 ) / ( pow( c_bar_prime, 7 ) + 6103515625.0 ) ); + double r_t = -r_c * sin( 2.0 * delta_theta ); + + double term_l = delta_l_prime / ( kL * s_l ); + double term_c = delta_c_prime / ( kC * s_c ); + double term_h = delta_h_prime / ( kH * s_h ); + + return sqrt( term_l * term_l + term_c * term_c + term_h * term_h + r_t * term_c * term_h ); +} + static const char* get_resampler_name( Image::ResamplerFilter filter ) { switch ( filter ) { case Image::ResamplerFilter::RESAMPLER_BOX: @@ -736,6 +859,57 @@ Image::Image( IOStream& stream, const unsigned int& forceChannels, } } +Image::Image( const Image& other ) : + mPixels( nullptr ), + mWidth( 0 ), + mHeight( 0 ), + mChannels( 0 ), + mSize( 0 ), + mAvoidFree( false ), + mLoadedFromStbi( false ) { + *this = other; // Delegate to copy assignment operator +} + +Image::Image( Image&& other ) noexcept : + mPixels( other.mPixels ), + mWidth( other.mWidth ), + mHeight( other.mHeight ), + mChannels( other.mChannels ), + mSize( other.mSize ), + mAvoidFree( other.mAvoidFree ), + mLoadedFromStbi( other.mLoadedFromStbi ), + mFormatConfiguration( std::move( other.mFormatConfiguration ) ) { + other.mPixels = nullptr; + other.mWidth = 0; + other.mHeight = 0; + other.mChannels = 0; + other.mSize = 0; +} + +Image& Image::operator=( Image&& other ) noexcept { + if ( this != &other ) { + if ( !mAvoidFree ) { + clearCache(); + } + + mPixels = other.mPixels; + mWidth = other.mWidth; + mHeight = other.mHeight; + mChannels = other.mChannels; + mSize = other.mSize; + mAvoidFree = other.mAvoidFree; + mLoadedFromStbi = other.mLoadedFromStbi; + mFormatConfiguration = std::move( other.mFormatConfiguration ); + + other.mPixels = nullptr; + other.mWidth = 0; + other.mHeight = 0; + other.mChannels = 0; + other.mSize = 0; + } + return *this; +} + Image::~Image() { if ( !mAvoidFree ) clearCache(); @@ -1264,6 +1438,9 @@ Graphics::Image* Image::copy() { } Graphics::Image& Image::operator=( const Image& right ) { + if (this == &right) + return *this; + mWidth = right.mWidth; mHeight = right.mHeight; mChannels = right.mChannels; @@ -1333,4 +1510,77 @@ std::pair, int> Image::loadGif( IOStream& stream ) { return { std::move( gif ), delay ? delay : 100 }; } +Image::DiffResult Image::diff( const Image& other, float threshold, const Color& diffColor ) const { + DiffResult result; + + if ( getWidth() != other.getWidth() || getHeight() != other.getHeight() ) { + Log::error( "Image::diff: Image dimensions do not match." ); + result.numDifferentPixels = getWidth() * getHeight(); + return result; + } + + if ( getChannels() < 3 || other.getChannels() < 3 ) { + Log::warning( "Image::diff: Perceptual diff requires at least 3 color channels. " + "Falling back to simple comparison." ); + return result; + } + + // Create a 4-channel RGBA image for the diff output + result.diffImage = Image::New( getWidth(), getHeight(), 4 ); + if ( !result.diffImage ) { + Log::error( "Image::diff: Failed to create diff image." ); + return result; + } + + const Uint8* p1 = this->getPixelsPtr(); + const Uint8* p2 = other.getPixelsPtr(); + Uint8* pDiff = result.diffImage->getPixels(); + + unsigned int channels1 = this->getChannels(); + unsigned int channels2 = other.getChannels(); + unsigned int channelsDiff = result.diffImage->getChannels(); + + for ( unsigned int y = 0; y < getHeight(); ++y ) { + for ( unsigned int x = 0; x < getWidth(); ++x ) { + size_t offset1 = ( y * getWidth() + x ) * channels1; + size_t offset2 = ( y * getWidth() + x ) * channels2; + size_t offsetDiff = ( y * getWidth() + x ) * channelsDiff; + + Color c1( p1[offset1], p1[offset1 + 1], p1[offset1 + 2], + channels1 == 4 ? p1[offset1 + 3] : 255 ); + Color c2( p2[offset2], p2[offset2 + 1], p2[offset2 + 2], + channels2 == 4 ? p2[offset2 + 3] : 255 ); + + // Also compare alpha channels directly, as perceptual diff is for color only + bool alphaDiffers = ( c1.a != c2.a ); + + Lab lab1 = xyzToLab( rgbToXyz( c1 ) ); + Lab lab2 = xyzToLab( rgbToXyz( c2 ) ); + double delta = deltaECIEDE2000( lab1, lab2 ); + + if ( delta > result.maxDeltaE ) { + result.maxDeltaE = delta; + } + + if ( delta > threshold || alphaDiffers ) { + result.numDifferentPixels++; + pDiff[offsetDiff] = diffColor.r; + pDiff[offsetDiff + 1] = diffColor.g; + pDiff[offsetDiff + 2] = diffColor.b; + pDiff[offsetDiff + 3] = diffColor.a; + } else { + // Blend original pixel with a transparent gray to fade it out + // This makes the highlighted differences stand out more. + Uint8 avg = ( c1.r + c1.g + c1.b ) / 3; + pDiff[offsetDiff] = avg; + pDiff[offsetDiff + 1] = avg; + pDiff[offsetDiff + 2] = avg; + pDiff[offsetDiff + 3] = 128; // Semi-transparent + } + } + } + + return result; +} + }} // namespace EE::Graphics diff --git a/src/eepp/window/window.cpp b/src/eepp/window/window.cpp index 2db1582cb..ce71cfd4c 100644 --- a/src/eepp/window/window.cpp +++ b/src/eepp/window/window.cpp @@ -252,56 +252,86 @@ RGB Window::getClearColor() const { return mWindow.ClearColor; } -bool Window::takeScreenshot( std::string filepath, const Image::SaveType& Format ) { +bool Window::takeScreenshot( std::string filepath, const Image::SaveType& format ) { GlobalBatchRenderer::instance()->draw(); - bool CreateNewFile = false; - std::string File, Ext; + bool createNewFile = false; + std::string file; + std::string ext; if ( filepath.size() ) { - File = filepath.substr( filepath.find_last_of( "/\\" ) + 1 ); - Ext = File.substr( File.find_last_of( "." ) + 1 ); - - if ( FileSystem::isDirectory( filepath ) || !Ext.size() ) - CreateNewFile = true; + file = filepath.substr( filepath.find_last_of( "/\\" ) + 1 ); + ext = file.substr( file.find_last_of( "." ) + 1 ); + if ( FileSystem::isDirectory( filepath ) || !ext.size() ) + createNewFile = true; } else { filepath = Sys::getProcessPath(); - CreateNewFile = true; + createNewFile = true; } - if ( CreateNewFile ) { // Search if file path is given, and if have and extension + std::string finalPath; + + if ( createNewFile ) { // Search if file path is given, and if have and extension bool find = false; - Int32 FileNum = 1; - std::string TmpPath = filepath; + int fileNum = 1; + std::string tmpPath = filepath; if ( !FileSystem::isDirectory( filepath ) ) FileSystem::makeDir( filepath ); - Ext = "." + Image::saveTypeToExtension( Format ); + ext = "." + Image::saveTypeToExtension( format ); - while ( !find && FileNum < 10000 ) { - TmpPath = String::format( "%s%05d%s", filepath.c_str(), FileNum, Ext.c_str() ); + while ( !find && fileNum < 10000 ) { + tmpPath = String::format( "%s%05d%s", filepath.c_str(), fileNum, ext.c_str() ); + fileNum++; - FileNum++; - - if ( !FileSystem::fileExists( TmpPath ) ) + if ( !FileSystem::fileExists( tmpPath ) ) find = true; - if ( FileNum == 10000 && find == false ) + if ( fileNum == 10000 && find == false ) return false; } - return 0 != SOIL_save_screenshot( TmpPath.c_str(), (int)Format, 0, 0, - mWindow.WindowConfig.Width, mWindow.WindowConfig.Height ); + finalPath = std::move( tmpPath ); } else { std::string Direc = FileSystem::fileRemoveFileName( filepath ); - if ( !FileSystem::isDirectory( Direc ) ) FileSystem::makeDir( Direc ); - - return 0 != SOIL_save_screenshot( filepath.c_str(), (int)Format, 0, 0, - mWindow.WindowConfig.Width, mWindow.WindowConfig.Height ); + finalPath = filepath; } + + return getFrontBufferImage().saveToFile( finalPath, format ); +} + +Image Window::getFrontBufferImage() { + GlobalBatchRenderer::instance()->draw(); + Image fb( mWindow.WindowConfig.Width, mWindow.WindowConfig.Height, 3 ); + + GLint pack_alignment; + glGetIntegerv( GL_PACK_ALIGNMENT, &pack_alignment ); + if ( 1 != pack_alignment ) + glPixelStorei( GL_PACK_ALIGNMENT, 1 ); + + Uint8* pixelData = fb.getPixels(); + glReadPixels( 0, 0, mWindow.WindowConfig.Width, mWindow.WindowConfig.Height, GL_RGB, + GL_UNSIGNED_BYTE, pixelData ); + + if ( 1 != pack_alignment ) + glPixelStorei( GL_PACK_ALIGNMENT, pack_alignment ); + + for ( unsigned int j = 0; j * 2 < fb.getHeight(); ++j ) { + int index1 = j * fb.getWidth() * fb.getChannels(); + int index2 = ( fb.getHeight() - 1 - j ) * fb.getWidth() * fb.getChannels(); + for ( int i = fb.getWidth() * fb.getChannels(); i > 0; --i ) { + Uint8 temp = pixelData[index1]; + pixelData[index1] = pixelData[index2]; + pixelData[index2] = temp; + ++index1; + ++index2; + } + } + + return fb; } bool Window::isRunning() const { diff --git a/src/tests/unit_tests/fontrendering.cpp b/src/tests/unit_tests/fontrendering.cpp new file mode 100644 index 000000000..0cd627f53 --- /dev/null +++ b/src/tests/unit_tests/fontrendering.cpp @@ -0,0 +1,149 @@ +#include "utest.h" +#include +#include + +using namespace EE::System; + +UTEST( FontRendering, fontsTest ) { + FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); + + auto win = Engine::instance()->createWindow( WindowSettings( 1220, 850, "eepp - Fonts" ) ); + + if ( !win->isOpen() ) { + std::cout << "OpenGL context not available, skipping" << std::endl; + return; + } + + // This will never trigger for the moment + ASSERT_TRUE_MSG( win->isOpen(), "Failed to create Window" ); + + win->setClearColor( RGB( 230, 230, 230 ) ); + + String 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." ); + + FontTrueType* fontTest = FontTrueType::New( "DejaVuSansMono" ); + fontTest->loadFromFile( "../assets/fonts/DejaVuSansMono.ttf" ); + + FontTrueType* fontTest2 = FontTrueType::New( "NotoSans-Regular" ); + fontTest2->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" ); + + FontTrueType* fontEmoji = FontTrueType::New( "NotoEmoji-Regular" ); + fontEmoji->loadFromFile( "../assets/fonts/NotoEmoji-Regular.ttf" ); + + FontTrueType* fontEmojiColor = FontTrueType::New( "NotoColorEmoji" ); + fontEmojiColor->loadFromFile( "../assets/fonts/NotoColorEmoji.ttf" ); + + FontBMFont* fontBMFont = FontBMFont::New( "bmfont" ); + fontBMFont->loadFromFile( "../assets/fonts/bmfont.fnt" ); + + FontSprite* fontSprite = FontSprite::New( "alagard" ); + fontSprite->loadFromFile( "../assets/fonts/custom_alagard.png", Color::Fuchsia, 32, -4 ); + + Text text; + text.setFont( fontTest ); + text.setFontSize( 24 ); + text.setAlign( TEXT_ALIGN_CENTER ); + text.setString( Txt ); + text.wrapText( win->getWidth() - 96 ); + + int size = (int)Txt.size(); + + for ( int i = 0; i < size; i++ ) { + 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 ); + text3.setFillColor( Color( 255, 255, 255, 255 ) ); + 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( "👽 😀 💩 😃 👻" ); + text7.setFillColor( Color::Gray ); + text7.setOutlineThickness( 2 ); + text7.setOutlineColor( Color( 0, 0, 0, 255 ) ); + + win->clear(); + text.draw( std::floor( ( win->getWidth() - text.getTextWidth() ) * 0.5f ), 32 ); + text2.draw( std::floor( ( win->getWidth() - text2.getTextWidth() ) * 0.5f ), 300 ); + text7.draw( std::floor( ( win->getWidth() - text7.getTextWidth() ) * 0.5f ), 400 ); + text2.draw( std::floor( ( win->getWidth() - text2.getTextWidth() ) * 0.5f ), 430, + Vector2f( 1.1f, 1.1f ), 12.5f ); + text3.draw( std::floor( ( win->getWidth() - text3.getTextWidth() ) * 0.5f ), 560 ); + text4.draw( std::floor( ( win->getWidth() - text4.getTextWidth() ) * 0.5f ), 590 ); + text5.draw( std::floor( ( win->getWidth() - text5.getTextWidth() ) * 0.5f ), 640 ); + text6.draw( std::floor( ( win->getWidth() - text6.getTextWidth() ) * 0.5f ), 690 ); + + { + Image::FormatConfiguration fconf; + fconf.webpSaveLossless( true ); + Image actualImage = win->getFrontBufferImage(); + actualImage.setImageFormatConfiguration( fconf ); + auto saveType = Image::SaveType::WEBP; + auto saveExt( Image::saveTypeToExtension( saveType ) ); + std::string expectedImagePath( "assets/fontrendering/eepp-fonts." + saveExt ); + + if ( !FileSystem::fileExists( expectedImagePath ) ) + actualImage.saveToFile( expectedImagePath, saveType ); + + Image expectedImage( expectedImagePath ); + + ASSERT_TRUE( expectedImage.getPixelsPtr() != nullptr ); + + Image::DiffResult result = actualImage.diff( expectedImage ); + + EXPECT_TRUE( result.areSame() ); + + if ( !result.areSame() ) { + std::cerr << "Test FAILED: " << result.numDifferentPixels << " pixels differ." + << std::endl; + std::cerr << "Maximum perceptual difference (Delta E): " << result.maxDeltaE + << std::endl; + if ( result.diffImage ) { + std::string actualImagePath = + Sys::getTempPath() + "eepp-fonts_actual_output." + saveExt; + std::string diffImagePath = + Sys::getTempPath() + "eepp-fonts_diff_output." + saveExt; + result.diffImage->setImageFormatConfiguration( fconf ); + result.diffImage->saveToFile( diffImagePath, saveType ); + actualImage.saveToFile( actualImagePath, saveType ); + std::cerr << "Visual diff saved to: " << diffImagePath << std::endl; + std::cerr << "Actual image saved to: " << actualImagePath << std::endl; + } + } + } + + Engine::destroySingleton(); +}