diff --git a/es-app/src/MiximageGenerator.cpp b/es-app/src/MiximageGenerator.cpp index 05fb65263..dcadb6b78 100644 --- a/es-app/src/MiximageGenerator.cpp +++ b/es-app/src/MiximageGenerator.cpp @@ -322,14 +322,14 @@ bool MiximageGenerator::generateImage() CImg screenshotImage(fileWidth, fileHeight, 1, 4, 0); - // Convert image to CImg internal format. - convertToCImgFormat(screenshotImage, screenshotVector); + // Convert the RGBA image to CImg internal format. + Utils::CImg::convertRGBAToCImg(screenshotVector, screenshotImage); screenshotVector.clear(); if (Settings::getInstance()->getBool("MiximageRemoveLetterboxes")) - cropLetterboxes(screenshotImage); + Utils::CImg::cropLetterboxes(screenshotImage); if (Settings::getInstance()->getBool("MiximageRemovePillarboxes")) - cropPillarboxes(screenshotImage); + Utils::CImg::cropPillarboxes(screenshotImage); if (Settings::getInstance()->getString("MiximageScreenshotScaling") == "smooth") { // Lanczos scaling is normally not recommended for low resolution graphics as @@ -391,8 +391,8 @@ bool MiximageGenerator::generateImage() marqueeImage = CImg(FreeImage_GetWidth(marqueeFile), FreeImage_GetHeight(marqueeFile), 1, 4, 0); - convertToCImgFormat(marqueeImage, marqueeVector); - removeTransparentPadding(marqueeImage); + Utils::CImg::convertRGBAToCImg(marqueeVector, marqueeImage); + Utils::CImg::removeTransparentPadding(marqueeImage); unsigned int marqueeWidth = static_cast(marqueeImage.width()); unsigned int marqueeHeight = static_cast(marqueeImage.height()); @@ -402,7 +402,8 @@ bool MiximageGenerator::generateImage() // We use Lanczos3 which is the highest quality resampling method available. marqueeImage.resize(marqueeWidth, marqueeHeight, 1, 4, 6); - addDropShadow(marqueeImage, marqueeShadowSize); + // Add a drop shadow using 4 iterations of box blur. + Utils::CImg::addDropShadow(marqueeImage, marqueeShadowSize, 0.6, 4); xPosMarquee = canvasImage.width() - marqueeImage.width(); yPosMarquee = 0; @@ -432,8 +433,8 @@ bool MiximageGenerator::generateImage() boxImage = CImg(FreeImage_GetWidth(boxFile), FreeImage_GetHeight(boxFile), 1, 4); - convertToCImgFormat(boxImage, boxVector); - removeTransparentPadding(boxImage); + Utils::CImg::convertRGBAToCImg(boxVector, boxImage); + Utils::CImg::removeTransparentPadding(boxImage); float scaleFactor = static_cast(boxTargetHeight) / static_cast(boxImage.height()); @@ -457,7 +458,7 @@ bool MiximageGenerator::generateImage() boxImage.resize(width, boxTargetHeight, 1, 4, 6); } - addDropShadow(boxImage, boxShadowSize); + Utils::CImg::addDropShadow(boxImage, boxShadowSize, 0.6, 4); xPosBox = 0; yPosBox = canvasImage.height() - boxImage.height(); @@ -522,8 +523,8 @@ bool MiximageGenerator::generateImage() std::vector canvasVector; - // Convert image from CImg internal format. - convertFromCImgFormat(canvasImage, canvasVector); + // Convert the image from CImg internal format. + Utils::CImg::convertCImgToRGBA(canvasImage, canvasVector); FIBITMAP* mixImage = nullptr; mixImage = FreeImage_ConvertFromRawBits(&canvasVector.at(0), canvasImage.width(), @@ -553,169 +554,6 @@ bool MiximageGenerator::generateImage() return true; } -void MiximageGenerator::cropLetterboxes(CImg& image) -{ - double pixelValueSum = 0.0l; - int rowCounterUpper = 0; - int rowCounterLower = 0; - - // Count the number of rows that are pure black. - for (int i = image.height() - 1; i > 0; i--) { - CImg imageRow = image.get_rows(i, i); - // Ignore the alpha channel. - imageRow.channels(0, 2); - pixelValueSum = imageRow.sum(); - if (pixelValueSum == 0.0l) - rowCounterUpper++; - else - break; - } - - for (int i = 0; i < image.height(); i++) { - CImg imageRow = image.get_rows(i, i); - imageRow.channels(0, 2); - pixelValueSum = imageRow.sum(); - if (pixelValueSum == 0.0l) - rowCounterLower++; - else - break; - } - - if (rowCounterUpper > 0) - image.crop(0, 0, 0, 3, image.width() - 1, image.height() - 1 - rowCounterUpper, 0, 0); - - if (rowCounterLower > 0) - image.crop(0, rowCounterLower, 0, 3, image.width() - 1, image.height() - 1, 0, 0); -} - -void MiximageGenerator::cropPillarboxes(CImg& image) -{ - double pixelValueSum = 0.0l; - unsigned int columnCounterLeft = 0; - unsigned int columnCounterRight = 0; - - // Count the number of columns that are pure black. - for (int i = 0; i < image.width(); i++) { - CImg imageColumn = image.get_columns(i, i); - // Ignore the alpha channel. - imageColumn.channels(0, 2); - pixelValueSum = imageColumn.sum(); - if (pixelValueSum == 0.0l) - columnCounterLeft++; - else - break; - } - - for (int i = image.width() - 1; i > 0; i--) { - CImg imageColumn = image.get_columns(i, i); - imageColumn.channels(0, 2); - pixelValueSum = imageColumn.sum(); - if (pixelValueSum == 0.0l) - columnCounterRight++; - else - break; - } - - if (columnCounterLeft > 0) - image.crop(columnCounterLeft, 0, 0, 3, image.width() - 1, image.height() - 1, 0, 0); - - if (columnCounterRight > 0) - image.crop(0, 0, 0, 3, image.width() - columnCounterRight - 1, image.height() - 1, 0, 0); -} - -void MiximageGenerator::removeTransparentPadding(CImg& image) -{ - if (image.spectrum() != 4) - return; - - double pixelValueSum = 0.0l; - int rowCounterUpper = 0; - int rowCounterLower = 0; - unsigned int columnCounterLeft = 0; - unsigned int columnCounterRight = 0; - - // Count the number of rows and columns that are completely transparent. - for (int i = image.height() - 1; i > 0; i--) { - CImg imageRow = image.get_rows(i, i); - pixelValueSum = imageRow.get_shared_channel(3).sum(); - if (pixelValueSum == 0.0l) - rowCounterUpper++; - else - break; - } - - for (int i = 0; i < image.height(); i++) { - CImg imageRow = image.get_rows(i, i); - pixelValueSum = imageRow.get_shared_channel(3).sum(); - if (pixelValueSum == 0.0l) - rowCounterLower++; - else - break; - } - - for (int i = 0; i < image.width(); i++) { - CImg imageColumn = image.get_columns(i, i); - pixelValueSum = imageColumn.get_shared_channel(3).sum(); - if (pixelValueSum == 0.0l) - columnCounterLeft++; - else - break; - } - - for (int i = image.width() - 1; i > 0; i--) { - CImg imageColumn = image.get_columns(i, i); - pixelValueSum = imageColumn.get_shared_channel(3).sum(); - if (pixelValueSum == 0.0l) - columnCounterRight++; - else - break; - } - - if (rowCounterUpper > 0) - image.crop(0, 0, 0, 3, image.width() - 1, image.height() - 1 - rowCounterUpper, 0, 0); - - if (rowCounterLower > 0) - image.crop(0, rowCounterLower, 0, 3, image.width() - 1, image.height() - 1, 0, 0); - - if (columnCounterLeft > 0) - image.crop(columnCounterLeft, 0, 0, 3, image.width() - 1, image.height() - 1, 0, 0); - - if (columnCounterRight > 0) - image.crop(0, 0, 0, 3, image.width() - columnCounterRight - 1, image.height() - 1, 0, 0); -} - -void MiximageGenerator::addDropShadow(CImg& image, unsigned int shadowDistance) -{ - // Make the shadow image larger than the source image to leave space for the drop shadow. - CImg shadowImage(image.width() + shadowDistance * 3, - image.height() + shadowDistance * 3, 1, 4, 0); - - // Create a mask image. - CImg maskImage(image.width(), image.height(), 1, 4, 0); - maskImage.draw_image(0, 0, image); - // Fill the RGB channels with white so we end up with a simple mask. - maskImage.get_shared_channels(0, 2).fill(255); - - // Make a black outline of the source image as a basis for the shadow. - shadowImage.draw_image(shadowDistance, shadowDistance, image); - shadowImage.get_shared_channels(0, 2).fill(0); - // Lower the transparency and apply the blur. - shadowImage.get_shared_channel(3) /= 0.6f; - shadowImage.blur_box(static_cast(shadowDistance), - static_cast(shadowDistance), 1, true, 4); - - // Add the mask to the alpha channel of the shadow image. - shadowImage.get_shared_channel(3).draw_image(0, 0, maskImage.get_shared_channels(0, 0), - maskImage.get_shared_channel(3), 1, 255); - // Draw the source image on top of the shadow image. - shadowImage.draw_image(0, 0, image.get_shared_channels(0, 2), - image.get_shared_channel(3), 1, 255); - // Remove the any unused space that we added to leave room for the shadow. - removeTransparentPadding(shadowImage); - - image = shadowImage; -} - void MiximageGenerator::calculateMarqueeSize(const unsigned int& targetWidth, const unsigned int& targetHeight, unsigned int& width, unsigned int& height) { @@ -810,35 +648,6 @@ void MiximageGenerator::sampleFrameColor(CImg& screenshotImage, frameColor[3] = 255; } -void MiximageGenerator::convertToCImgFormat(CImg& image, - std::vector imageVector) -{ - // CImg does not interleave the pixels as in RGBARGBARGBA so a conversion is required. - int counter = 0; - for (int r = 0; r < image.height(); r++) { - for (int c = 0; c < image.width(); c++) { - image(c, r, 0, 0) = imageVector[counter + 2]; - image(c, r, 0, 1) = imageVector[counter + 1]; - image(c, r, 0, 2) = imageVector[counter + 0]; - image(c, r, 0, 3) = imageVector[counter + 3]; - counter += 4; - } - } -} - -void MiximageGenerator::convertFromCImgFormat(CImg image, - std::vector& imageVector) -{ - for (int r = image.height() - 1; r >= 0; r--) { - for (int c = 0; c < image.width(); c++) { - imageVector.push_back((unsigned char)image(c,r,0,2)); - imageVector.push_back((unsigned char)image(c,r,0,1)); - imageVector.push_back((unsigned char)image(c,r,0,0)); - imageVector.push_back((unsigned char)image(c,r,0,3)); - } - } -} - std::string MiximageGenerator::getSavePath() { const std::string name = Utils::FileSystem::getStem(mGame->getPath()); diff --git a/es-app/src/MiximageGenerator.h b/es-app/src/MiximageGenerator.h index 733f92487..4d444ca45 100644 --- a/es-app/src/MiximageGenerator.h +++ b/es-app/src/MiximageGenerator.h @@ -10,13 +10,10 @@ #ifndef ES_APP_SCRAPERS_MIXIMAGE_GENERATOR_H #define ES_APP_SCRAPERS_MIXIMAGE_GENERATOR_H -// Disable the CImg display capabilities. -#define cimg_display 0 - +#include "utils/CImgUtil.h" #include "FileData.h" #include "GuiComponent.h" -#include #include #include @@ -32,17 +29,10 @@ public: private: bool generateImage(); - void cropLetterboxes(CImg& image); - void cropPillarboxes(CImg& image); - void removeTransparentPadding(CImg& image); - void addDropShadow(CImg& image, unsigned int shadowDistance); void calculateMarqueeSize(const unsigned int& targetWidth, const unsigned int& targetHeight, unsigned int& width, unsigned int& height); void sampleFrameColor(CImg& screenshotImage, unsigned char (&frameColor)[4]); - void convertToCImgFormat(CImg& image, std::vector imageVector); - void convertFromCImgFormat(CImg image, std::vector& imageVector); - std::string getSavePath(); FileData* mGame; diff --git a/es-core/CMakeLists.txt b/es-core/CMakeLists.txt index cdca4b917..bf9d750b1 100644 --- a/es-core/CMakeLists.txt +++ b/es-core/CMakeLists.txt @@ -78,6 +78,7 @@ set(CORE_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/src/resources/TextureResource.h # Utils + ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/CImgUtil.h ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/FileSystemUtil.h ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/StringUtil.h ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/TimeUtil.h @@ -157,6 +158,7 @@ set(CORE_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/resources/TextureDataManager.cpp # Utils + ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/CImgUtil.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/FileSystemUtil.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/StringUtil.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/TimeUtil.cpp diff --git a/es-core/src/utils/CImgUtil.cpp b/es-core/src/utils/CImgUtil.cpp new file mode 100644 index 000000000..e873a2077 --- /dev/null +++ b/es-core/src/utils/CImgUtil.cpp @@ -0,0 +1,275 @@ +// SPDX-License-Identifier: MIT +// +// EmulationStation Desktop Edition +// CImgUtil.cpp +// +// Utility functions using the CImg image processing library. +// + +#include "utils/CImgUtil.h" + +namespace Utils +{ + namespace CImg + { + void convertRGBAToCImg(std::vector imageRGBA, + cimg_library::CImg& image) + { + // CImg does not interleave the pixels as in RGBARGBARGBA so a conversion is required. + int counter = 0; + for (int r = 0; r < image.height(); r++) { + for (int c = 0; c < image.width(); c++) { + image(c, r, 0, 0) = imageRGBA[counter + 2]; + image(c, r, 0, 1) = imageRGBA[counter + 1]; + image(c, r, 0, 2) = imageRGBA[counter + 0]; + image(c, r, 0, 3) = imageRGBA[counter + 3]; + counter += 4; + } + } + } + + void convertCImgToRGBA(cimg_library::CImg image, + std::vector& imageRGBA) + { + for (int r = image.height() - 1; r >= 0; r--) { + for (int c = 0; c < image.width(); c++) { + imageRGBA.push_back((unsigned char)image(c,r,0,2)); + imageRGBA.push_back((unsigned char)image(c,r,0,1)); + imageRGBA.push_back((unsigned char)image(c,r,0,0)); + imageRGBA.push_back((unsigned char)image(c,r,0,3)); + } + } + } + + void getTransparentPaddingCoords(cimg_library::CImg& image, + int (&imageCoords)[4]) + { + // Check that the image actually has an alpha channel. + if (image.spectrum() != 4) + return; + + double pixelValueSum = 0.0l; + int rowCounterTop = 0; + int rowCounterBottom = 0; + unsigned int columnCounterLeft = 0; + unsigned int columnCounterRight = 0; + + // Count the number of rows and columns that are completely transparent. + for (int i = image.height() - 1; i > 0; i--) { + cimg_library::CImg imageRow = image.get_rows(i, i); + pixelValueSum = imageRow.get_shared_channel(3).sum(); + if (pixelValueSum == 0.0l) + rowCounterTop++; + else + break; + } + + for (int i = 0; i < image.height(); i++) { + cimg_library::CImg imageRow = image.get_rows(i, i); + pixelValueSum = imageRow.get_shared_channel(3).sum(); + if (pixelValueSum == 0.0l) + rowCounterBottom++; + else + break; + } + + for (int i = 0; i < image.width(); i++) { + cimg_library::CImg imageColumn = image.get_columns(i, i); + pixelValueSum = imageColumn.get_shared_channel(3).sum(); + if (pixelValueSum == 0.0l) + columnCounterLeft++; + else + break; + } + + for (int i = image.width() - 1; i > 0; i--) { + cimg_library::CImg imageColumn = image.get_columns(i, i); + pixelValueSum = imageColumn.get_shared_channel(3).sum(); + if (pixelValueSum == 0.0l) + columnCounterRight++; + else + break; + } + + imageCoords[0] = columnCounterLeft; + imageCoords[1] = rowCounterTop; + imageCoords[2] = columnCounterRight; + imageCoords[3] = rowCounterBottom; + } + + void removeTransparentPadding(cimg_library::CImg& image) + { + // Check that the image actually has an alpha channel. + if (image.spectrum() != 4) + return; + + double pixelValueSum = 0.0l; + int rowCounterTop = 0; + int rowCounterBottom = 0; + unsigned int columnCounterLeft = 0; + unsigned int columnCounterRight = 0; + + // Count the number of rows and columns that are completely transparent. + for (int i = image.height() - 1; i > 0; i--) { + cimg_library::CImg imageRow = image.get_rows(i, i); + pixelValueSum = imageRow.get_shared_channel(3).sum(); + if (pixelValueSum == 0.0l) + rowCounterTop++; + else + break; + } + + for (int i = 0; i < image.height(); i++) { + cimg_library::CImg imageRow = image.get_rows(i, i); + pixelValueSum = imageRow.get_shared_channel(3).sum(); + if (pixelValueSum == 0.0l) + rowCounterBottom++; + else + break; + } + + for (int i = 0; i < image.width(); i++) { + cimg_library::CImg imageColumn = image.get_columns(i, i); + pixelValueSum = imageColumn.get_shared_channel(3).sum(); + if (pixelValueSum == 0.0l) + columnCounterLeft++; + else + break; + } + + for (int i = image.width() - 1; i > 0; i--) { + cimg_library::CImg imageColumn = image.get_columns(i, i); + pixelValueSum = imageColumn.get_shared_channel(3).sum(); + if (pixelValueSum == 0.0l) + columnCounterRight++; + else + break; + } + + if (rowCounterTop > 0) + image.crop(0, 0, 0, 3, image.width() - 1, image.height() - 1 - + rowCounterTop, 0, 0); + + if (rowCounterBottom > 0) + image.crop(0, rowCounterBottom, 0, 3, image.width() - 1, image.height() - 1, 0, 0); + + if (columnCounterLeft > 0) + image.crop(columnCounterLeft, 0, 0, 3, image.width() - 1, image.height() - 1, 0, 0); + + if (columnCounterRight > 0) + image.crop(0, 0, 0, 3, image.width() - columnCounterRight - 1, + image.height() - 1, 0, 0); + } + + void cropLetterboxes(cimg_library::CImg& image) + { + double pixelValueSum = 0.0l; + int rowCounterUpper = 0; + int rowCounterLower = 0; + + // Count the number of rows that are pure black. + for (int i = image.height() - 1; i > 0; i--) { + cimg_library::CImg imageRow = image.get_rows(i, i); + // Ignore the alpha channel. + imageRow.channels(0, 2); + pixelValueSum = imageRow.sum(); + if (pixelValueSum == 0.0l) + rowCounterUpper++; + else + break; + } + + for (int i = 0; i < image.height(); i++) { + cimg_library::CImg imageRow = image.get_rows(i, i); + imageRow.channels(0, 2); + pixelValueSum = imageRow.sum(); + if (pixelValueSum == 0.0l) + rowCounterLower++; + else + break; + } + + if (rowCounterUpper > 0) + image.crop(0, 0, 0, 3, image.width() - 1, image.height() - 1 - + rowCounterUpper, 0, 0); + + if (rowCounterLower > 0) + image.crop(0, rowCounterLower, 0, 3, image.width() - 1, image.height() - 1, 0, 0); + } + + void cropPillarboxes(cimg_library::CImg& image) + { + double pixelValueSum = 0.0l; + unsigned int columnCounterLeft = 0; + unsigned int columnCounterRight = 0; + + // Count the number of columns that are pure black. + for (int i = 0; i < image.width(); i++) { + cimg_library::CImg imageColumn = image.get_columns(i, i); + // Ignore the alpha channel. + imageColumn.channels(0, 2); + pixelValueSum = imageColumn.sum(); + if (pixelValueSum == 0.0l) + columnCounterLeft++; + else + break; + } + + for (int i = image.width() - 1; i > 0; i--) { + cimg_library::CImg imageColumn = image.get_columns(i, i); + imageColumn.channels(0, 2); + pixelValueSum = imageColumn.sum(); + if (pixelValueSum == 0.0l) + columnCounterRight++; + else + break; + } + + if (columnCounterLeft > 0) + image.crop(columnCounterLeft, 0, 0, 3, image.width() - 1, image.height() - 1, 0, 0); + + if (columnCounterRight > 0) + image.crop(0, 0, 0, 3, image.width() - columnCounterRight - 1, + image.height() - 1, 0, 0); + } + + void addDropShadow(cimg_library::CImg& image, unsigned int shadowDistance, + float transparency, unsigned int iterations) + { + // Check that the image actually has an alpha channel. + if (image.spectrum() != 4) + return; + + // Make the shadow image larger than the source image to leave space for the drop shadow. + cimg_library::CImg shadowImage(image.width() + shadowDistance * 3, + image.height() + shadowDistance * 3, 1, 4, 0); + + // Create a mask image. + cimg_library::CImg maskImage(image.width(), image.height(), 1, 4, 0); + maskImage.draw_image(0, 0, image); + // Fill the RGB channels with white so we end up with a simple mask. + maskImage.get_shared_channels(0, 2).fill(255); + + // Make a black outline of the source image as a basis for the shadow. + shadowImage.draw_image(shadowDistance, shadowDistance, image); + shadowImage.get_shared_channels(0, 2).fill(0); + // Lower the transparency and apply the blur. + shadowImage.get_shared_channel(3) /= transparency; + shadowImage.blur_box(static_cast(shadowDistance), + static_cast(shadowDistance), 1, true, iterations); + + // Add the mask to the alpha channel of the shadow image. + shadowImage.get_shared_channel(3).draw_image(0, 0, maskImage.get_shared_channels(0, 0), + maskImage.get_shared_channel(3), 1, 255); + // Draw the source image on top of the shadow image. + shadowImage.draw_image(0, 0, image.get_shared_channels(0, 2), + image.get_shared_channel(3), 1, 255); + // Remove the any unused space that we added to leave room for the shadow. + removeTransparentPadding(shadowImage); + + image = shadowImage; + } + + } // CImg:: + +} // Utils:: diff --git a/es-core/src/utils/CImgUtil.h b/es-core/src/utils/CImgUtil.h new file mode 100644 index 000000000..e611911b4 --- /dev/null +++ b/es-core/src/utils/CImgUtil.h @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +// +// EmulationStation Desktop Edition +// CImgUtil.h +// +// Utility functions using the CImg image processing library. +// + +#ifndef ES_CORE_UTILS_CIMG_UTIL_H +#define ES_CORE_UTILS_CIMG_UTIL_H + +// Disable the CImg display capabilities. +#define cimg_display 0 + +#include +#include + +namespace Utils +{ + namespace CImg + { + void convertRGBAToCImg(std::vector imageRGBA, + cimg_library::CImg& image); + void convertCImgToRGBA(cimg_library::CImg image, + std::vector& imageRGBA); + void getTransparentPaddingCoords(cimg_library::CImg& image, + int (&imageCoords)[4]); + void removeTransparentPadding(cimg_library::CImg& image); + void cropLetterboxes(cimg_library::CImg& image); + void cropPillarboxes(cimg_library::CImg& image); + void addDropShadow(cimg_library::CImg& image, unsigned int shadowDistance, + float transparency, unsigned int iterations); + } +} + +#endif // ES_CORE_UTILS_CIMG_UTIL_H