From 819d03776d3bbd0701bf1d8529f2dfdd7adc7e3d Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Mon, 7 Jun 2021 23:02:42 +0200 Subject: [PATCH] Added a miximage generator. --- es-app/CMakeLists.txt | 2 + es-app/src/MiximageGenerator.cpp | 772 +++++++++++++++++++++++++++ es-app/src/MiximageGenerator.h | 66 +++ es-app/src/guis/GuiScraperMenu.cpp | 163 +++++- es-app/src/guis/GuiScraperMenu.h | 7 +- es-app/src/guis/GuiScraperSearch.cpp | 61 ++- es-app/src/guis/GuiScraperSearch.h | 13 + es-core/src/Settings.cpp | 12 + 8 files changed, 1077 insertions(+), 19 deletions(-) create mode 100644 es-app/src/MiximageGenerator.cpp create mode 100644 es-app/src/MiximageGenerator.h diff --git a/es-app/CMakeLists.txt b/es-app/CMakeLists.txt index 20779f1d7..d8316fe47 100644 --- a/es-app/CMakeLists.txt +++ b/es-app/CMakeLists.txt @@ -9,6 +9,7 @@ set(ES_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/src/Gamelist.h ${CMAKE_CURRENT_SOURCE_DIR}/src/MediaViewer.h ${CMAKE_CURRENT_SOURCE_DIR}/src/MetaData.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/MiximageGenerator.h ${CMAKE_CURRENT_SOURCE_DIR}/src/PlatformId.h ${CMAKE_CURRENT_SOURCE_DIR}/src/SystemData.h ${CMAKE_CURRENT_SOURCE_DIR}/src/SystemScreensaver.h @@ -59,6 +60,7 @@ set(ES_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/MediaViewer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/MetaData.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/MiximageGenerator.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/PlatformId.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/SystemData.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/SystemScreensaver.cpp diff --git a/es-app/src/MiximageGenerator.cpp b/es-app/src/MiximageGenerator.cpp new file mode 100644 index 000000000..7448e3b68 --- /dev/null +++ b/es-app/src/MiximageGenerator.cpp @@ -0,0 +1,772 @@ +// SPDX-License-Identifier: MIT +// +// EmulationStation Desktop Edition +// MiximageGenerator.cpp +// +// Generates miximages from screenshots, marquees and 3D box/cover images. +// Called from GuiScraperSearch. +// + +#include "MiximageGenerator.h" + +#include "math/Misc.h" +#include "utils/StringUtil.h" +#include "Log.h" +#include "Settings.h" +#include "SystemData.h" + +#include + +MiximageGenerator::MiximageGenerator(FileData* game, bool& result, std::string& resultMessage) + : mGame(game), + mResult(result), + mResultMessage(resultMessage), + mWidth(1280), + mHeight(960), + mMarquee(false), + mBox3D(false), + mCover(false) +{ +} + +MiximageGenerator::~MiximageGenerator() +{ +} + +void MiximageGenerator::startThread(std::promise* miximagePromise) +{ + mMiximagePromise = miximagePromise; + + LOG(LogDebug) << "MiximageGenerator::MiximageGenerator(): Creating image for \"" + << mGame->getFileName() << "\""; + + if (mGame->getMiximagePath() != "" && !Settings::getInstance()->getBool("MiximageOverwrite")) { + LOG(LogDebug) << "MiximageGenerator::MiximageGenerator(): File already exists and miximage " + "overwriting has not been enabled, aborting"; + mMiximagePromise->set_value(true); + return; + } + + if ((mScreenshotPath = mGame->getScreenshotPath()) == "") { + LOG(LogDebug) << "MiximageGenerator::MiximageGenerator(): " + "No screenshot image found, aborting"; + mResultMessage = "NO SCREENSHOT IMAGE FOUND, COULDN'T GENERATE MIXIMAGE"; + mMiximagePromise->set_value(true); + return; + } + + if (Settings::getInstance()->getBool("MiximageIncludeMarquee")) { + if ((mMarqueePath = mGame->getMarqueePath()) != "") { + mMarquee = true; + } + else { + LOG(LogDebug) << "MiximageGenerator::MiximageGenerator(): No marquee image found"; + } + } + + if (Settings::getInstance()->getBool("MiximageIncludeBox")) { + if ((mBox3DPath = mGame->get3DBoxPath()) != "") { + mBox3D = true; + } + else if (Settings::getInstance()->getBool("MiximageCoverFallback") && + (mCoverPath= mGame->getCoverPath()) != "") { + LOG(LogDebug) << "MiximageGenerator::MiximageGenerator(): " + "No 3D box image found, using cover image as fallback"; + mCover = true; + } + else if (Settings::getInstance()->getBool("MiximageCoverFallback")) { + LOG(LogDebug) << "MiximageGenerator::MiximageGenerator(): " + "No 3D box or cover images found"; + } + else { + LOG(LogDebug) << "MiximageGenerator::MiximageGenerator(): No 3D box image found"; + } + } + + const auto startTime = std::chrono::system_clock::now(); + + if (generateImage()) { + LOG(LogError) << "Failed to generate miximage"; + mResult = true; + } + else { + const auto endTime = std::chrono::system_clock::now(); + + LOG(LogDebug) << "MiximageGenerator::MiximageGenerator(): Processing completed in: " << + std::chrono::duration_cast + (endTime - startTime).count() << " ms"; + } + + mResultMessage = mMessage; + mMiximagePromise->set_value(false); +} + +bool MiximageGenerator::generateImage() +{ + FREE_IMAGE_FORMAT fileFormat; + FIBITMAP* screenshotFile = nullptr; + FIBITMAP* marqueeFile = nullptr; + FIBITMAP* boxFile = nullptr; + + unsigned int fileWidth = 0; + unsigned int fileHeight = 0; + unsigned int filePitch = 0; + + fileFormat = FreeImage_GetFileType(mScreenshotPath.c_str()); + + if (fileFormat == FIF_UNKNOWN) + fileFormat = FreeImage_GetFIFFromFilename(mScreenshotPath.c_str()); + + if (fileFormat == FIF_UNKNOWN) { + LOG(LogError) << "Screenshot image in unknown image format, aborting"; + mMessage = "SCREENSHOT IMAGE IN UNKNOWN FORMAT, COULDN'T GENERATE MIXIMAGE"; + return true; + } + + // Make sure that we can actually read this format. + if (FreeImage_FIFSupportsReading(fileFormat)) { + screenshotFile = FreeImage_Load(fileFormat, mScreenshotPath.c_str()); + } + else { + LOG(LogError) << "Screenshot file format not supported"; + mMessage = "SCREENSHOT IMAGE IN UNSUPPORTED FORMAT, COULDN'T GENERATE MIXIMAGE"; + return true; + } + + if (!screenshotFile) { + LOG(LogError) << "Error loading screenshot image, corrupt file?"; + mMessage = "ERROR LOADING SCREENSHOT IMAGE, COULDN'T GENERATE MIXIMAGE"; + return true; + } + + if (mMarquee) { + fileFormat = FreeImage_GetFileType(mMarqueePath.c_str()); + + if (fileFormat == FIF_UNKNOWN) + fileFormat = FreeImage_GetFIFFromFilename(mMarqueePath.c_str()); + + if (fileFormat == FIF_UNKNOWN) { + LOG(LogDebug) << "Marquee in unknown format, skipping image"; + mMarquee = false; + } + + if (!FreeImage_FIFSupportsReading(fileFormat)) { + LOG(LogDebug) << "Marquee file format not supported, skipping image"; + mMarquee = false; + } + else { + marqueeFile = FreeImage_Load(fileFormat, mMarqueePath.c_str()); + if (!marqueeFile) { + LOG(LogError) << "Couldn't load marquee image, corrupt file?"; + mMessage = "ERROR LOADING MARQUEE IMAGE, CORRUPT FILE?"; + mMarquee = false; + } + } + } + + if (mBox3D) { + fileFormat = FreeImage_GetFileType(mBox3DPath.c_str()); + + if (fileFormat == FIF_UNKNOWN) + fileFormat = FreeImage_GetFIFFromFilename(mBox3DPath.c_str()); + + if (fileFormat == FIF_UNKNOWN) { + LOG(LogDebug) << "3D box in unknown format, skipping image"; + mBox3D = false; + } + + if (!FreeImage_FIFSupportsReading(fileFormat)) { + LOG(LogDebug) << "3D box file format not supported, skipping image"; + mBox3D = false; + } + else { + boxFile = FreeImage_Load(fileFormat, mBox3DPath.c_str()); + if (!boxFile) { + LOG(LogError) << "Couldn't load 3D box image, corrupt file?"; + mMessage = "ERROR LOADING 3D BOX IMAGE, CORRUPT FILE?"; + mBox3D = false; + } + } + } + else if (mCover) { + fileFormat = FreeImage_GetFileType(mCoverPath.c_str()); + + if (fileFormat == FIF_UNKNOWN) + fileFormat = FreeImage_GetFIFFromFilename(mCoverPath.c_str()); + + if (fileFormat == FIF_UNKNOWN) { + LOG(LogDebug) << "Box cover in unknown format, skipping image"; + mCover = false; + } + + if (!FreeImage_FIFSupportsReading(fileFormat)) { + LOG(LogDebug) << "Box cover file format not supported, skipping image"; + mCover = false; + } + else { + boxFile = FreeImage_Load(fileFormat, mCoverPath.c_str()); + if (!boxFile) { + LOG(LogError) << "Couldn't load box cover image, corrupt file?"; + mMessage = "ERROR LOADING BOX COVER IMAGE, CORRUPT FILE?"; + mCover = false; + } + } + } + + unsigned int resolutionMultiplier = 0; + + if (Settings::getInstance()->getString("MiximageResolution") == "640x480") { + mWidth = 640; + mHeight = 480; + resolutionMultiplier = 1; + } + else if (Settings::getInstance()->getString("MiximageResolution") == "1920x1440") { + mWidth = 1920; + mHeight = 1440; + resolutionMultiplier = 3; + } + else { + mWidth = 1280; + mHeight = 960; + resolutionMultiplier = 2; + } + + const unsigned int screenshotWidth = 530 * resolutionMultiplier; + const unsigned int screenshotOffset = 20 * resolutionMultiplier; + const unsigned int screenshotFrameWidth = 5 * resolutionMultiplier; + const unsigned int screenshotHeight = 400 * resolutionMultiplier; + const unsigned int marqueeWidth = 260 * resolutionMultiplier; + const unsigned int marqueeMaxHeight = 220 * resolutionMultiplier; + const unsigned int boxHeight = 300 * resolutionMultiplier; + const unsigned int boxMaxWidth = 340 * resolutionMultiplier; + const unsigned int coverMaxWidth = 250 * resolutionMultiplier; + const unsigned int marqueeShadow = 10; + const unsigned int boxShadow = 14; + + if (FreeImage_GetBPP(screenshotFile) != 32) { + FIBITMAP* screenshotTemp = FreeImage_ConvertTo32Bits(screenshotFile); + FreeImage_Unload(screenshotFile); + screenshotFile = screenshotTemp; + } + + fileWidth = FreeImage_GetWidth(screenshotFile); + fileHeight = FreeImage_GetHeight(screenshotFile); + filePitch = FreeImage_GetPitch(screenshotFile); + + std::vector screenshotVector(fileWidth * fileHeight * 4); + + FreeImage_ConvertToRawBits(reinterpret_cast(&screenshotVector.at(0)), screenshotFile, + filePitch, 32, FI_RGBA_RED, FI_RGBA_GREEN, FI_RGBA_BLUE, 1); + + CImg screenshotImage(fileWidth, fileHeight, 1, 4, 0); + + // Convert image to CImg internal format. + convertToCImgFormat(screenshotImage, screenshotVector); + screenshotVector.clear(); + + if (Settings::getInstance()->getBool("MiximageRemoveLetterboxes")) + cropLetterboxes(screenshotImage); + if (Settings::getInstance()->getBool("MiximageRemovePillarboxes")) + cropPillarboxes(screenshotImage); + + if (Settings::getInstance()->getString("MiximageScreenshotScaling") == "smooth") { + // Lanczos scaling is normally not recommended for low resolution graphics as + // it makes the pixels appear smooth when scaling, but for more modern game + // platforms it may be a good idea to use it. + screenshotImage.resize(screenshotWidth, screenshotHeight, 1, 4, 6); + } + else { + // Box interpolation gives completely sharp pixels, which is best suited for + // low resolution retro games. + screenshotImage.resize(screenshotWidth, screenshotHeight, 1, 4, 1); + } + + // Remove any transparency information from the screenshot. There really should be no + // alpha channel for these images, but if there is, it could interfere with the compositing + // of the miximage. + screenshotImage.get_shared_channel(3).fill(255); + + int xPosScreenshot = 0; + int yPosScreenshot = 0; + + int xPosMarquee = 0; + int yPosMarquee = 0; + + int xPosBox = 0; + int yPosBox = 0; + + CImg canvasImage(mWidth, mHeight, 1, 4, 0); + + CImg marqueeImage; + CImg marqueeImageRGB; + CImg marqueeImageAlpha; + + CImg boxImage; + CImg boxImageRGB; + CImg boxImageAlpha; + + CImg frameImage(mWidth, mHeight, 1, 4, 0); + + xPosScreenshot = canvasImage.width() / 2 - screenshotImage.width() / 2 + screenshotOffset; + yPosScreenshot = canvasImage.height() / 2 - screenshotImage.height() / 2; + + if (mMarquee) { + if (FreeImage_GetBPP(marqueeFile) != 32) { + FIBITMAP* marqueeTemp = FreeImage_ConvertTo32Bits(marqueeFile); + FreeImage_Unload(marqueeFile); + marqueeFile = marqueeTemp; + } + + fileWidth = FreeImage_GetWidth(marqueeFile); + fileHeight = FreeImage_GetHeight(marqueeFile); + filePitch = FreeImage_GetPitch(marqueeFile); + + std::vector marqueeVector(fileWidth * fileHeight * 4); + + FreeImage_ConvertToRawBits(reinterpret_cast(&marqueeVector.at(0)), marqueeFile, + filePitch, 32, FI_RGBA_RED, FI_RGBA_GREEN, FI_RGBA_BLUE, 1); + + marqueeImage = CImg(FreeImage_GetWidth(marqueeFile), + FreeImage_GetHeight(marqueeFile), 1, 4, 0); + + convertToCImgFormat(marqueeImage, marqueeVector); + removeTransparentPadding(marqueeImage); + addDropShadow(marqueeImage, marqueeShadow); + + float scaleFactor = static_cast(marqueeWidth) / + static_cast(marqueeImage.width()); + unsigned int height = + static_cast(static_cast(marqueeImage.height()) * scaleFactor); + + if (height > marqueeMaxHeight) { + scaleFactor = static_cast(marqueeMaxHeight) / + static_cast(marqueeImage.height()); + int width = static_cast(static_cast(marqueeImage.width()) * scaleFactor); + // We use Lanczos3 which is the highest quality resampling method available. + marqueeImage.resize(width, marqueeMaxHeight, 1, 4, 6); + } + else { + marqueeImage.resize(marqueeWidth, height, 1, 4, 6); + } + + xPosMarquee = canvasImage.width() - marqueeImage.width(); + yPosMarquee = 0; + + // Only RGB channels for the image. + marqueeImageRGB = CImg(marqueeImage.get_shared_channels(0,2)); + // Only alpha channel for the image. + marqueeImageAlpha = CImg(marqueeImage.get_shared_channel(3)); + } + + if (mBox3D || mCover) { + if (FreeImage_GetBPP(boxFile) != 32) { + FIBITMAP* boxTemp = FreeImage_ConvertTo32Bits(boxFile); + FreeImage_Unload(boxFile); + boxFile = boxTemp; + } + + fileWidth = FreeImage_GetWidth(boxFile); + fileHeight = FreeImage_GetHeight(boxFile); + filePitch = FreeImage_GetPitch(boxFile); + + std::vector boxVector(fileWidth * fileHeight * 4); + + FreeImage_ConvertToRawBits(reinterpret_cast(&boxVector.at(0)), boxFile, + filePitch, 32, FI_RGBA_RED, FI_RGBA_GREEN, FI_RGBA_BLUE, 1); + + boxImage = CImg(FreeImage_GetWidth(boxFile), + FreeImage_GetHeight(boxFile), 1, 4); + + convertToCImgFormat(boxImage, boxVector); + removeTransparentPadding(boxImage); + addDropShadow(boxImage, boxShadow); + + float scaleFactor = static_cast(boxHeight) / static_cast(boxImage.height()); + unsigned int width = static_cast(static_cast(boxImage.width()) * scaleFactor); + unsigned int maxWidth = 0; + + // We make this distinction as some cover images are in square format and would cover + // too much surface otherwise. + if (mBox3D) + maxWidth = boxMaxWidth; + else + maxWidth = coverMaxWidth; + + if (width > maxWidth) { + scaleFactor = static_cast(maxWidth) / static_cast(boxImage.width()); + int height = static_cast(static_cast(boxImage.height()) * scaleFactor); + // We use Lanczos3 which is the highest quality resampling method available. + boxImage.resize(maxWidth, height, 1, 4, 6); + } + else { + boxImage.resize(width, boxHeight, 1, 4, 6); + } + + xPosBox = 0; + yPosBox = canvasImage.height() - boxImage.height(); + + // Only RGB channels for the image. + boxImageRGB = CImg(boxImage.get_shared_channels(0,2)); + // Only alpha channel for the image. + boxImageAlpha = CImg(boxImage.get_shared_channel(3)); + } + + CImg frameImageAlpha(frameImage.get_shared_channel(3)); + frameImageAlpha.draw_image(xPosBox, yPosBox, boxImageAlpha); + frameImageAlpha.draw_image(xPosMarquee, yPosMarquee, marqueeImageAlpha); + + // Set a frame color based on an average of the screenshot contents. + unsigned char frameColor[] = { 0, 0, 0, 0 }; + sampleFrameColor(screenshotImage, frameColor); + + // Upper / lower frame. + frameImage.draw_rectangle( + xPosScreenshot + 2, + yPosScreenshot - screenshotFrameWidth, + xPosScreenshot + screenshotWidth - 2, + yPosScreenshot + screenshotHeight + screenshotFrameWidth - 1, + frameColor); + + // Left / right frame. + frameImage.draw_rectangle( + xPosScreenshot - screenshotFrameWidth, + yPosScreenshot + 2, + xPosScreenshot + screenshotWidth + screenshotFrameWidth - 1, + yPosScreenshot + screenshotHeight - 2, + frameColor); + + // We draw circles in order to get rounded corners for the frame. + const unsigned int circleRadius = 7 * resolutionMultiplier; + const unsigned int circleOffset = 2 * resolutionMultiplier; + + // Upper left corner. + frameImage.draw_circle(xPosScreenshot + circleOffset, yPosScreenshot + circleOffset, + circleRadius, frameColor); + // Upper right corner. + frameImage.draw_circle(xPosScreenshot + screenshotWidth - circleOffset - 1, + yPosScreenshot + circleOffset, circleRadius, frameColor); + // Lower right corner. + frameImage.draw_circle(xPosScreenshot + screenshotWidth - circleOffset - 1, + yPosScreenshot + screenshotHeight - circleOffset - 1, circleRadius, frameColor); + // Lower left corner. + frameImage.draw_circle(xPosScreenshot + circleOffset, + yPosScreenshot + screenshotHeight - circleOffset - 1, circleRadius, frameColor); + + CImg frameImageRGB(frameImage.get_shared_channels(0, 2)); + + canvasImage.draw_image(0, 0, frameImage); + canvasImage.draw_image(xPosScreenshot, yPosScreenshot, screenshotImage); + + if (mMarquee) + canvasImage.draw_image(xPosMarquee, yPosMarquee, marqueeImageRGB, + marqueeImageAlpha, 1, 255); + if (mBox3D || mCover) + canvasImage.draw_image(xPosBox, yPosBox, boxImageRGB, boxImageAlpha, 1, 255); + + std::vector canvasVector; + + // Convert image from CImg internal format. + convertFromCImgFormat(canvasImage, canvasVector); + + FIBITMAP* mixImage = nullptr; + mixImage = FreeImage_ConvertFromRawBits(&canvasVector.at(0), canvasImage.width(), + canvasImage.height(), canvasImage.width() * 4, 32, + FI_RGBA_RED, FI_RGBA_GREEN, FI_RGBA_BLUE); + + bool savedImage = (FreeImage_Save(FIF_PNG, mixImage, getSavePath().c_str()) != 0); + + if (!savedImage) { + LOG(LogError) << "Couldn't save miximage, permission problems or disk full?"; + } + + FreeImage_Unload(screenshotFile); + FreeImage_Unload(marqueeFile); + FreeImage_Unload(boxFile); + FreeImage_Unload(mixImage); + + // Success. + if (savedImage) + return false; + else + 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(shadowDistance, shadowDistance, 1, true, 2); + shadowImage.blur(3, 0); + + // 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::sampleFrameColor(CImg& screenshotImage, + unsigned char (&frameColor)[4]) +{ + // Calculate the number of samples relative to the configured resolution so we get + // the same result regardless of miximage target size seting. + unsigned int samples = static_cast(static_cast(mWidth) * 0.03125f); + + unsigned int red = 0; + unsigned int green = 0; + unsigned int blue = 0; + + unsigned int redLine = 0; + unsigned int greenLine = 0; + unsigned int blueLine = 0; + + unsigned int counter = 0; + + // This is a very simple method to get an average pixel value. It's limited in that it + // does not consider dominant colors and such, so the result could possibly be a value + // that does not match the perceived color palette of the image. In most cases it works + // good enough though. + for (int r = samples / 2; r < screenshotImage.height(); r += samples) { + for (int c = samples / 2; c < screenshotImage.width(); c += samples) { + red += screenshotImage(c, r, 0, 0); + green += screenshotImage(c, r, 0, 1); + blue += screenshotImage(c, r, 0, 2); + counter++; + } + + if (counter > 0) { + redLine += red / counter; + greenLine += green / counter; + blueLine += blue / counter; + counter = 0; + } + } + + unsigned char redC = Math::clamp(static_cast(redLine / 255), 0, 255); + unsigned char greenC = Math::clamp(static_cast(greenLine / 255), 0, 255); + unsigned char blueC = Math::clamp(static_cast(blueLine / 255), 0, 255); + + // Convert to the HSL color space to be able to modify saturation and lightness. + CImg colorHSL = CImg<>(1,1,1,3).fill(redC, greenC, blueC).RGBtoHSL(); + + float hue = colorHSL(0, 0, 0, 0); + float saturation = colorHSL(0, 0, 0, 1); + float lightness = colorHSL(0, 0, 0, 2); + + // Decrease saturation slightly and increase lightness a bit, these adjustments + // makes the end result look better than the raw average pixel value. + colorHSL(0, 0, 0, 1) = Math::clamp(saturation * 0.9f, 0.0f, 1.0f); + colorHSL(0, 0, 0, 2) = Math::clamp(lightness * 1.2f, 0.0f, 1.0f); + + const CImg colorRGB = colorHSL.HSLtoRGB(); + + frameColor[0] = colorRGB(0, 0, 0, 0); + frameColor[1] = colorRGB(0, 0, 0, 1); + frameColor[2] = colorRGB(0, 0, 0, 2); + 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 systemsubdirectory = mGame->getSystem()->getName(); + const std::string name = Utils::FileSystem::getStem(mGame->getPath()); + std::string subFolders; + + // Extract possible subfolders from the path. + if (mGame->getSystemEnvData()->mStartPath != "") + subFolders = Utils::String::replace(Utils::FileSystem::getParent(mGame->getPath()), + mGame->getSystem()->getSystemEnvData()->mStartPath, ""); + + std::string path = FileData::getMediaDirectory(); + + if (!Utils::FileSystem::exists(path)) + Utils::FileSystem::createDirectory(path); + + path += systemsubdirectory + "/miximages" + subFolders + "/"; + + if (!Utils::FileSystem::exists(path)) + Utils::FileSystem::createDirectory(path); + + path += name + ".png"; + + // Success. + return path; +} diff --git a/es-app/src/MiximageGenerator.h b/es-app/src/MiximageGenerator.h new file mode 100644 index 000000000..472b9884b --- /dev/null +++ b/es-app/src/MiximageGenerator.h @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT +// +// EmulationStation Desktop Edition +// MiximageGenerator.h +// +// Generates miximages from screenshots, marquees and 3D box/cover images. +// Called from GuiScraperSearch. +// + +#ifndef ES_APP_SCRAPERS_MIXIMAGE_GENERATOR_H +#define ES_APP_SCRAPERS_MIXIMAGE_GENERATOR_H + +// Disable the CImg display capabilities. +#define cimg_display 0 + +#include "FileData.h" +#include "GuiComponent.h" + +#include +#include +#include + +using namespace cimg_library; + +class MiximageGenerator +{ +public: + MiximageGenerator(FileData* game, bool& result, std::string& resultMessage); + ~MiximageGenerator(); + + void startThread(std::promise* miximagePromise); + +private: + bool generateImage(); + void cropLetterboxes(CImg& image); + void cropPillarboxes(CImg& image); + void removeTransparentPadding(CImg& image); + void addDropShadow(CImg& image, unsigned int shadowDistance); + 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; + bool& mResult; + std::string& mResultMessage; + std::string mMessage; + + std::promise* mMiximagePromise; + + std::string mScreenshotPath; + std::string mMarqueePath; + std::string mBox3DPath; + std::string mCoverPath; + + int mWidth; + int mHeight; + + bool mMarquee; + bool mBox3D; + bool mCover; +}; + +#endif // ES_APP_SCRAPERS_MIXIMAGE_GENERATOR_H diff --git a/es-app/src/guis/GuiScraperMenu.cpp b/es-app/src/guis/GuiScraperMenu.cpp index a196a15da..fb2223fb9 100644 --- a/es-app/src/guis/GuiScraperMenu.cpp +++ b/es-app/src/guis/GuiScraperMenu.cpp @@ -14,10 +14,11 @@ #include "components/SwitchComponent.h" #include "guis/GuiMsgBox.h" #include "guis/GuiScraperMulti.h" +#include "guis/GuiSettings.h" #include "views/ViewController.h" #include "FileData.h" #include "SystemData.h" -#include "guis/GuiSettings.h" + GuiScraperMenu::GuiScraperMenu(Window* window, std::string title) : GuiComponent(window), mMenu(window, title) @@ -80,7 +81,7 @@ GuiScraperMenu::GuiScraperMenu(Window* window, std::string title) mMenu.addWithLabel("SCRAPE THESE SYSTEMS", mSystems); addEntry("ACCOUNT SETTINGS", 0x777777FF, true, [this] { - openAccountSettings(); + openAccountOptions(); }); addEntry("CONTENT SETTINGS", 0x777777FF, true, [this] { // If the scraper service has been changed before entering this menu, then save the @@ -88,7 +89,10 @@ GuiScraperMenu::GuiScraperMenu(Window* window, std::string title) // can be enabled or disabled. if (mScraper->getSelected() != Settings::getInstance()->getString("Scraper")) mMenu.save(); - openContentSettings(); + openContentOptions(); + }); + addEntry("MIXIMAGE SETTINGS", 0x777777FF, true, [this] { + openMiximageOptions(); }); addEntry("OTHER SETTINGS", 0x777777FF, true, [this] { // If the scraper service has been changed before entering this menu, then save the @@ -96,7 +100,7 @@ GuiScraperMenu::GuiScraperMenu(Window* window, std::string title) // can be enabled or disabled. if (mScraper->getSelected() != Settings::getInstance()->getString("Scraper")) mMenu.save(); - openOtherSettings(); + openOtherOptions(); }); addChild(&mMenu); @@ -125,7 +129,7 @@ GuiScraperMenu::~GuiScraperMenu() } } -void GuiScraperMenu::openAccountSettings() +void GuiScraperMenu::openAccountOptions() { auto s = new GuiSettings(mWindow, "ACCOUNT SETTINGS"); @@ -180,9 +184,9 @@ void GuiScraperMenu::openAccountSettings() mWindow->pushGui(s); } -void GuiScraperMenu::openContentSettings() +void GuiScraperMenu::openContentOptions() { - auto s = new GuiSettings(mWindow, "SCRAPER CONTENT SETTINGS"); + auto s = new GuiSettings(mWindow, "CONTENT SETTINGS"); // Scrape game names. auto scrape_game_names = std::make_shared(mWindow); @@ -301,9 +305,146 @@ void GuiScraperMenu::openContentSettings() mWindow->pushGui(s); } -void GuiScraperMenu::openOtherSettings() +void GuiScraperMenu::openMiximageOptions() { - auto s = new GuiSettings(mWindow, "OTHER SCRAPER SETTINGS"); + auto s = new GuiSettings(mWindow, "MIXIMAGE SETTINGS"); + + // Miximage resolution. + auto miximage_resolution = std::make_shared> + (mWindow, getHelpStyle(), "MIXIMAGE RESOLUTION", false); + std::string selectedResolution = Settings::getInstance()->getString("MiximageResolution"); + miximage_resolution->add("1280x960", "1280x960", selectedResolution == "1280x960"); + miximage_resolution->add("1920x1440", "1920x1440", selectedResolution == "1920x1440"); + miximage_resolution->add("640x480", "640x480", selectedResolution == "640x480"); + // If there are no objects returned, then there must be a manually modified entry in the + // configuration file. Simply set the resolution to "1280x960" in this case. + if (miximage_resolution->getSelectedObjects().size() == 0) + miximage_resolution->selectEntry(0); + s->addWithLabel("MIXIMAGE RESOLUTION", miximage_resolution); + s->addSaveFunc([miximage_resolution, s] { + if (miximage_resolution->getSelected() != + Settings::getInstance()->getString("MiximageResolution")) { + Settings::getInstance()-> + setString("MiximageResolution", miximage_resolution->getSelected()); + s->setNeedsSaving(); + } + }); + + // Screenshot scaling method. + auto miximage_scaling = std::make_shared> + (mWindow, getHelpStyle(), "SCREENSHOT SCALING", false); + std::string selectedScaling = Settings::getInstance()->getString("MiximageScreenshotScaling"); + miximage_scaling->add("sharp", "sharp", selectedScaling == "sharp"); + miximage_scaling->add("smooth", "smooth", selectedScaling == "smooth"); + // If there are no objects returned, then there must be a manually modified entry in the + // configuration file. Simply set the scaling method to "sharp" in this case. + if (miximage_scaling->getSelectedObjects().size() == 0) + miximage_scaling->selectEntry(0); + s->addWithLabel("SCREENSHOT SCALING METHOD", miximage_scaling); + s->addSaveFunc([miximage_scaling, s] { + if (miximage_scaling->getSelected() != + Settings::getInstance()->getString("MiximageScreenshotScaling")) { + Settings::getInstance()-> + setString("MiximageScreenshotScaling", miximage_scaling->getSelected()); + s->setNeedsSaving(); + } + }); + + // Whether to generate miximages when scraping. + auto miximage_generate = std::make_shared(mWindow); + miximage_generate->setState(Settings::getInstance()->getBool("MiximageGenerate")); + s->addWithLabel("GENERATE MIXIMAGES WHEN SCRAPING", miximage_generate); + s->addSaveFunc([miximage_generate, s] { + if (miximage_generate->getState() != + Settings::getInstance()->getBool("MiximageGenerate")) { + Settings::getInstance()->setBool("MiximageGenerate", miximage_generate->getState()); + s->setNeedsSaving(); + } + }); + + // Whether to overwrite miximages (both for the scraper and offline generator). + auto miximage_overwrite = std::make_shared(mWindow); + miximage_overwrite->setState(Settings::getInstance()->getBool("MiximageOverwrite")); + s->addWithLabel("OVERWRITE MIXIMAGES (SCRAPER/OFFLINE GENERATOR)", miximage_overwrite); + s->addSaveFunc([miximage_overwrite, s] { + if (miximage_overwrite->getState() != + Settings::getInstance()->getBool("MiximageOverwrite")) { + Settings::getInstance()->setBool("MiximageOverwrite", miximage_overwrite->getState()); + s->setNeedsSaving(); + } + }); + + // Whether to remove letterboxes from the screenshots. + auto remove_letterboxes = std::make_shared(mWindow); + remove_letterboxes->setState(Settings::getInstance()->getBool("MiximageRemoveLetterboxes")); + s->addWithLabel("REMOVE LETTERBOXES FROM SCREENSHOTS", remove_letterboxes); + s->addSaveFunc([remove_letterboxes, s] { + if (remove_letterboxes->getState() != + Settings::getInstance()->getBool("MiximageRemoveLetterboxes")) { + Settings::getInstance()->setBool("MiximageRemoveLetterboxes", + remove_letterboxes->getState()); + s->setNeedsSaving(); + } + }); + + // Whether to remove pillarboxes from the screenshots. + auto remove_pillarboxes = std::make_shared(mWindow); + remove_pillarboxes->setState(Settings::getInstance()->getBool("MiximageRemovePillarboxes")); + s->addWithLabel("REMOVE PILLARBOXES FROM SCREENSHOTS", remove_pillarboxes); + s->addSaveFunc([remove_pillarboxes, s] { + if (remove_pillarboxes->getState() != + Settings::getInstance()->getBool("MiximageRemovePillarboxes")) { + Settings::getInstance()->setBool("MiximageRemovePillarboxes", + remove_pillarboxes->getState()); + s->setNeedsSaving(); + } + }); + + // Whether to include marquee images. + auto miximage_marquee = std::make_shared(mWindow); + miximage_marquee->setState(Settings::getInstance()->getBool("MiximageIncludeMarquee")); + s->addWithLabel("INCLUDE MARQUEE IMAGE", miximage_marquee); + s->addSaveFunc([miximage_marquee, s] { + if (miximage_marquee->getState() != + Settings::getInstance()->getBool("MiximageIncludeMarquee")) { + Settings::getInstance()-> + setBool("MiximageIncludeMarquee", miximage_marquee->getState()); + s->setNeedsSaving(); + } + }); + + // Whether to include box images. + auto miximage_box = std::make_shared(mWindow); + miximage_box->setState(Settings::getInstance()->getBool("MiximageIncludeBox")); + s->addWithLabel("INCLUDE BOX IMAGE", miximage_box); + s->addSaveFunc([miximage_box, s] { + if (miximage_box->getState() != + Settings::getInstance()->getBool("MiximageIncludeBox")) { + Settings::getInstance()-> + setBool("MiximageIncludeBox", miximage_box->getState()); + s->setNeedsSaving(); + } + }); + + // Whether to use cover image if there is no 3D box image. + auto miximage_cover_fallback = std::make_shared(mWindow); + miximage_cover_fallback->setState(Settings::getInstance()->getBool("MiximageCoverFallback")); + s->addWithLabel("USE COVER IMAGE IF 3D BOX IS MISSING", miximage_cover_fallback); + s->addSaveFunc([miximage_cover_fallback, s] { + if (miximage_cover_fallback->getState() != + Settings::getInstance()->getBool("MiximageCoverFallback")) { + Settings::getInstance()-> + setBool("MiximageCoverFallback", miximage_cover_fallback->getState()); + s->setNeedsSaving(); + } + }); + + mWindow->pushGui(s); +} + +void GuiScraperMenu::openOtherOptions() +{ + auto s = new GuiSettings(mWindow, "OTHER SETTINGS"); // Scraper region. auto scraper_region = std::make_shared> @@ -314,7 +455,7 @@ void GuiScraperMenu::openOtherSettings() scraper_region->add("USA", "us", selectedScraperRegion == "us"); scraper_region->add("World", "wor", selectedScraperRegion == "wor"); // If there are no objects returned, then there must be a manually modified entry in the - // configuration file. Simply set the region to Europe in this case. + // configuration file. Simply set the region to "Europe" in this case. if (scraper_region->getSelectedObjects().size() == 0) scraper_region->selectEntry(0); s->addWithLabel("REGION", scraper_region); @@ -358,7 +499,7 @@ void GuiScraperMenu::openOtherSettings() scraper_language->add("Slovenčina", "sk", selectedScraperLanguage == "sk"); scraper_language->add("Türkçe", "tr", selectedScraperLanguage == "tr"); // If there are no objects returned, then there must be a manually modified entry in the - // configuration file. Simply set the language to English in this case. + // configuration file. Simply set the language to "English" in this case. if (scraper_language->getSelectedObjects().size() == 0) scraper_language->selectEntry(0); s->addWithLabel("PREFERRED LANGUAGE", scraper_language); diff --git a/es-app/src/guis/GuiScraperMenu.h b/es-app/src/guis/GuiScraperMenu.h index 70f7bc292..450e5f79c 100644 --- a/es-app/src/guis/GuiScraperMenu.h +++ b/es-app/src/guis/GuiScraperMenu.h @@ -39,9 +39,10 @@ private: void addEntry(const std::string&, unsigned int color, bool add_arrow, const std::function& func); - void openAccountSettings(); - void openContentSettings(); - void openOtherSettings(); + void openAccountOptions(); + void openContentOptions(); + void openMiximageOptions(); + void openOtherOptions(); std::queue getSearches( std::vector systems, GameFilterFunc selector); diff --git a/es-app/src/guis/GuiScraperSearch.cpp b/es-app/src/guis/GuiScraperSearch.cpp index 5612d247f..7973164e0 100644 --- a/es-app/src/guis/GuiScraperSearch.cpp +++ b/es-app/src/guis/GuiScraperSearch.cpp @@ -159,6 +159,18 @@ GuiScraperSearch::~GuiScraperSearch() mThumbnailReqMap.clear(); HttpReq::cleanupCurlMulti(); + + // This is required to properly refresh the gamelist view if the user aborted the + // scraping when the miximage was getting generated. + if (Settings::getInstance()->getBool("MiximageGenerate") && + mMiximageGeneratorThread.joinable()) { + mScrapeResult.savedNewMedia = true; + // We always let the miximage generator thread complete. + mMiximageGeneratorThread.join(); + mMiximageGenerator.reset(); + TextureResource::manualUnload(mLastSearch.game->getMiximagePath(), false); + ViewController::get()->onFileChanged(mLastSearch.game, true); + } } void GuiScraperSearch::onSizeChanged() @@ -306,6 +318,7 @@ void GuiScraperSearch::updateViewStyle() void GuiScraperSearch::search(const ScraperSearchParams& params) { mBlockAccept = true; + mScrapeResult = {}; mResultList->clear(); mScraperResults.clear(); @@ -324,7 +337,9 @@ void GuiScraperSearch::stop() mSearchHandle.reset(); mMDResolveHandle.reset(); mMDRetrieveURLsHandle.reset(); + mMiximageGenerator.reset(); mBlockAccept = false; + mScrapeResult = {}; } void GuiScraperSearch::onSearchDone(const std::vector& results) @@ -643,14 +658,50 @@ void GuiScraperSearch::update(int deltaTime) } } + // Check if a miximage generator thread was started, and if the processing has been completed. + if (mMiximageGenerator && mGeneratorFuture.valid()) { + // Only wait one millisecond, this update() function runs very frequently. + if (mGeneratorFuture.wait_for(std::chrono::milliseconds(1)) == std::future_status::ready) { + mMDResolveHandle.reset(); + if (!mMiximageResult) + mScrapeResult.savedNewMedia = true; + returnResult(mScrapeResult); + // We always let the miximage generator thread complete. + mMiximageGeneratorThread.join(); + mMiximageGenerator.reset(); + } + } + if (mMDResolveHandle && mMDResolveHandle->status() != ASYNC_IN_PROGRESS) { if (mMDResolveHandle->status() == ASYNC_DONE) { - ScraperSearchResult result = mMDResolveHandle->getResult(); - result.mediaFilesDownloadStatus = COMPLETED; + mScrapeResult = mMDResolveHandle->getResult(); + mScrapeResult.mediaFilesDownloadStatus = COMPLETED; mMDResolveHandle.reset(); - // This might end in us being deleted, depending on mAcceptCallback - - // so make sure this is the last thing we do in update(). - returnResult(result); + + if (mScrapeResult.mediaFilesDownloadStatus == COMPLETED && + Settings::getInstance()->getBool("MiximageGenerate")) { + std::string currentMiximage = mLastSearch.game->getMiximagePath(); + if (currentMiximage == "" || (currentMiximage != "" && + Settings::getInstance()->getBool("ScraperOverwriteData"))) { + + mMiximageGenerator = std::make_unique(mLastSearch.game, + mMiximageResult, mResultMessage); + + // The promise/future mechanism is used as signaling for the thread to + // indicate that processing has been completed. The reason to run a separate + // thread is that the busy animation will then be played and that the user + // interface does not become completely unresponsive during the miximage + // generation. + std::promise().swap(mGeneratorPromise); + mGeneratorFuture = mGeneratorPromise.get_future(); + + mMiximageGeneratorThread = std::thread(&MiximageGenerator::startThread, + mMiximageGenerator.get(), &mGeneratorPromise); + } + } + else { + returnResult(mScrapeResult); + } } else if (mMDResolveHandle->status() == ASYNC_ERROR) { onSearchError(mMDResolveHandle->getStatusString()); diff --git a/es-app/src/guis/GuiScraperSearch.h b/es-app/src/guis/GuiScraperSearch.h index 8af205b88..99053f733 100644 --- a/es-app/src/guis/GuiScraperSearch.h +++ b/es-app/src/guis/GuiScraperSearch.h @@ -20,6 +20,10 @@ #include "components/ComponentGrid.h" #include "scrapers/Scraper.h" #include "GuiComponent.h" +#include "MiximageGenerator.h" + +#include +#include class ComponentList; class DateTimeEditComponent; @@ -121,6 +125,7 @@ private: SearchType mSearchType; ScraperSearchParams mLastSearch; + ScraperSearchResult mScrapeResult; std::function mAcceptCallback; std::function mSkipCallback; std::function mCancelCallback; @@ -139,6 +144,14 @@ private: std::vector mScraperResults; std::map> mThumbnailReqMap; + std::unique_ptr mMiximageGenerator; + std::thread mMiximageGeneratorThread; + std::promise mGeneratorPromise; + std::future mGeneratorFuture; + + bool mMiximageResult; + std::string mResultMessage; + BusyComponent mBusyAnim; }; diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index b0fd64747..971c69aa9 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -98,6 +98,7 @@ void Settings::setDefaults() mBoolMap["ScraperUseAccountScreenScraper"] = { false, false }; mStringMap["ScraperUsernameScreenScraper"] = { "", "" }; mStringMap["ScraperPasswordScreenScraper"] = { "", "" }; + mBoolMap["ScrapeGameNames"] = { true, true }; mBoolMap["ScrapeRatings"] = { true, true }; mBoolMap["ScrapeMetadata"] = { true, true }; @@ -106,6 +107,17 @@ void Settings::setDefaults() mBoolMap["ScrapeCovers"] = { true, true }; mBoolMap["ScrapeMarquees"] = { true, true }; mBoolMap["Scrape3DBoxes"] = { true, true }; + + mStringMap["MiximageResolution"] = { "1280x960", "1280x960" }; + mStringMap["MiximageScreenshotScaling"] = { "sharp", "sharp" }; + mBoolMap["MiximageGenerate"] = { true, true }; + mBoolMap["MiximageOverwrite"] = { true, true }; + mBoolMap["MiximageRemoveLetterboxes"] = { true, true }; + mBoolMap["MiximageRemovePillarboxes"] = { true, true }; + mBoolMap["MiximageIncludeMarquee"] = { true, true }; + mBoolMap["MiximageIncludeBox"] = { true, true }; + mBoolMap["MiximageCoverFallback"] = { true, true }; + mStringMap["ScraperRegion"] = { "eu", "eu" }; mStringMap["ScraperLanguage"] = { "en", "en" }; mBoolMap["ScraperOverwriteData"] = { true, true };