From 923240aac05d8a26c0d0312fcd31363810d8f87c Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sat, 1 Jul 2023 11:15:43 +0200 Subject: [PATCH] Added options to the miximage generator for how to fit screenshots that do not match the aspect ratio of the miximage frame --- es-app/src/MiximageGenerator.cpp | 108 +++++++++++++++++++++++++---- es-app/src/guis/GuiScraperMenu.cpp | 86 +++++++++++++++++++++++ es-core/src/Settings.cpp | 4 ++ es-core/src/utils/CImgUtil.cpp | 100 +++++++++++++------------- 4 files changed, 234 insertions(+), 64 deletions(-) diff --git a/es-app/src/MiximageGenerator.cpp b/es-app/src/MiximageGenerator.cpp index fa2b2366a..73a39ea0c 100644 --- a/es-app/src/MiximageGenerator.cpp +++ b/es-app/src/MiximageGenerator.cpp @@ -409,16 +409,100 @@ bool MiximageGenerator::generateImage() if (Settings::getInstance()->getBool("MiximageRemovePillarboxes")) Utils::CImg::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); + const float fileAspect {static_cast(fileWidth) / static_cast(fileHeight)}; + + // Options for how to handle screenshots that are not closely matching the aspect ratio of + // the miximage. Can be set to contain, crop or stretch. + const bool containImage { + (fileAspect >= 1.0f && + Settings::getInstance()->getString("MiximageScreenshotHorizontalFit") == "contain") || + (fileAspect < 1.0f && + Settings::getInstance()->getString("MiximageScreenshotVerticalFit") == "contain")}; + const bool cropImage { + (fileAspect >= 1.0f && + Settings::getInstance()->getString("MiximageScreenshotHorizontalFit") == "crop") || + (fileAspect < 1.0f && + Settings::getInstance()->getString("MiximageScreenshotVerticalFit") == "crop")}; + + // Define a threshold higher and lower than the 1.325 aspect ratio of the miximage to avoid + // fitting and cropping images which only deviate slightly. + const float miximageAspectRatio {1.325f}; + const float maxAspectValue { + Settings::getInstance()->getString("MiximageScreenshotAspectThreshold") == "high" ? 1.6f : + 1.375f}; + const float minAspectValue { + Settings::getInstance()->getString("MiximageScreenshotAspectThreshold") == "high" ? 1.05f : + 1.275f}; + + // Set the frame color based on an average of the screenshot contents. + unsigned char frameColor[] = {0, 0, 0, 0}; + + // Lanczos scaling (6) is normally not recommended for low resolution graphics as it makes the + // the pixels appear smooth when scaling, but for more modern game platforms it may be a good + // idea to use it. Box interpolation (1) gives completely sharp pixels, which is best suited + // for low resolution retro games. + const int scalingMethod { + Settings::getInstance()->getString("MiximageScreenshotScaling") == "smooth" ? 6 : 1}; + + if (cropImage && (fileAspect > maxAspectValue || fileAspect < minAspectValue)) { + if (fileWidth >= fileHeight && fileAspect > miximageAspectRatio) { + screenshotImage.resize( + static_cast(std::round(static_cast(screenshotHeight) * fileAspect)), + screenshotHeight, 1, 4, scalingMethod); + const int offsetX {(screenshotImage.width() - static_cast(screenshotWidth)) / 2}; + // Add -2 to the offset to avoid single-pixel black lines caused by inexact rounding. + screenshotImage.crop(offsetX, 0, 0, 3, screenshotWidth + offsetX, screenshotHeight - 2, + 0, 0); + } + else { + screenshotImage.resize( + screenshotWidth, + static_cast(std::round(static_cast(screenshotWidth) / fileAspect)), 1, + 4, scalingMethod); + const int offsetY {(screenshotImage.height() - static_cast(screenshotHeight)) / 2}; + screenshotImage.crop(0, offsetY, 0, 3, screenshotWidth - 2, screenshotHeight + offsetY, + 0, 0); + } } else { - // Box interpolation gives completely sharp pixels, which is best suited for - // low resolution retro games. - screenshotImage.resize(screenshotWidth, screenshotHeight, 1, 4, 1); + screenshotImage.resize(screenshotWidth, screenshotHeight, 1, 4, scalingMethod); + } + + if (containImage && (fileAspect > maxAspectValue || fileAspect < minAspectValue)) { + CImg tempImage(fileWidth, fileHeight, 1, 4, 0); + sampleFrameColor(screenshotImage, frameColor); + tempImage = screenshotImage; + if (Settings::getInstance()->getString("MiximageScreenshotBlankAreasColor") == "frame") { + screenshotImage.get_shared_channel(0).fill(frameColor[0]); + screenshotImage.get_shared_channel(1).fill(frameColor[1]); + screenshotImage.get_shared_channel(2).fill(frameColor[2]); + } + else { + screenshotImage.fill(0); + } + + if (fileWidth >= fileHeight && fileAspect > miximageAspectRatio) { + const float sizeRatio {static_cast(screenshotWidth) / + static_cast(fileWidth)}; + const int resizeHeight {static_cast(static_cast(fileHeight) * sizeRatio)}; + tempImage.resize( + screenshotWidth, + static_cast(std::round(static_cast(fileHeight) * sizeRatio)), 1, 4, + scalingMethod); + screenshotImage.draw_image(0, (screenshotHeight - resizeHeight) / 2, tempImage); + } + else { + const float sizeRatio {static_cast(screenshotHeight) / + static_cast(fileHeight)}; + const int resizeWidth {static_cast(static_cast(fileWidth) * sizeRatio)}; + tempImage.resize( + static_cast(std::round(static_cast(fileWidth) * sizeRatio)), + screenshotHeight, 1, 4, scalingMethod); + screenshotImage.draw_image((screenshotWidth - resizeWidth) / 2, 0, tempImage); + } + } + else { + sampleFrameColor(screenshotImage, frameColor); } // Remove any transparency information from the screenshot. There really should be no @@ -454,8 +538,8 @@ bool MiximageGenerator::generateImage() CImg frameImage(mWidth, mHeight, 1, 4, 0); - xPosScreenshot = canvasImage.width() / 2 - screenshotImage.width() / 2 + screenshotOffset; - yPosScreenshot = canvasImage.height() / 2 - screenshotImage.height() / 2; + xPosScreenshot = canvasImage.width() / 2 - screenshotWidth / 2 + screenshotOffset; + yPosScreenshot = canvasImage.height() / 2 - screenshotHeight / 2; if (mMarquee) { if (FreeImage_GetBPP(marqueeFile) != 32) { @@ -617,10 +701,6 @@ bool MiximageGenerator::generateImage() frameImageAlpha.draw_image(xPosPhysicalMedia, yPosPhysicalMedia, physicalMediaImageAlpha); 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, diff --git a/es-app/src/guis/GuiScraperMenu.cpp b/es-app/src/guis/GuiScraperMenu.cpp index 8a6d97787..cde671f43 100644 --- a/es-app/src/guis/GuiScraperMenu.cpp +++ b/es-app/src/guis/GuiScraperMenu.cpp @@ -468,6 +468,92 @@ void GuiScraperMenu::openMiximageOptions() } }); + // Horizontally oriented screenshots fit. + auto miximageHorizontalFit = std::make_shared>( + getHelpStyle(), "HORIZONTAL SCREENSHOT FIT", false); + const std::string selectedHorizontalFit { + Settings::getInstance()->getString("MiximageScreenshotHorizontalFit")}; + miximageHorizontalFit->add("contain", "contain", selectedHorizontalFit == "contain"); + miximageHorizontalFit->add("crop", "crop", selectedHorizontalFit == "crop"); + miximageHorizontalFit->add("stretch", "stretch", selectedHorizontalFit == "stretch"); + // If there are no objects returned, then there must be a manually modified entry in the + // configuration file. Simply set the horizontal screenshot fit to "crop" in this case. + if (miximageHorizontalFit->getSelectedObjects().size() == 0) + miximageHorizontalFit->selectEntry(1); + s->addWithLabel("HORIZONTAL SCREENSHOT FIT", miximageHorizontalFit); + s->addSaveFunc([miximageHorizontalFit, s] { + if (miximageHorizontalFit->getSelected() != + Settings::getInstance()->getString("MiximageScreenshotHorizontalFit")) { + Settings::getInstance()->setString("MiximageScreenshotHorizontalFit", + miximageHorizontalFit->getSelected()); + s->setNeedsSaving(); + } + }); + + // Vertically oriented screenshots fit. + auto miximageVerticalFit = std::make_shared>( + getHelpStyle(), "VERTICAL SCREENSHOT FIT", false); + const std::string selectedVerticalFit { + Settings::getInstance()->getString("MiximageScreenshotVerticalFit")}; + miximageVerticalFit->add("contain", "contain", selectedVerticalFit == "contain"); + miximageVerticalFit->add("crop", "crop", selectedVerticalFit == "crop"); + miximageVerticalFit->add("stretch", "stretch", selectedVerticalFit == "stretch"); + // If there are no objects returned, then there must be a manually modified entry in the + // configuration file. Simply set the vertical screenshot fit to "contain" in this case. + if (miximageVerticalFit->getSelectedObjects().size() == 0) + miximageVerticalFit->selectEntry(0); + s->addWithLabel("VERTICAL SCREENSHOT FIT", miximageVerticalFit); + s->addSaveFunc([miximageVerticalFit, s] { + if (miximageVerticalFit->getSelected() != + Settings::getInstance()->getString("MiximageScreenshotVerticalFit")) { + Settings::getInstance()->setString("MiximageScreenshotVerticalFit", + miximageVerticalFit->getSelected()); + s->setNeedsSaving(); + } + }); + + // Screenshots aspect ratio threshold. + auto miximageAspectThreshold = std::make_shared>( + getHelpStyle(), "ASPECT RATIO THRESHOLD", false); + const std::string selectedAspectThreshold { + Settings::getInstance()->getString("MiximageScreenshotAspectThreshold")}; + miximageAspectThreshold->add("high", "high", selectedAspectThreshold == "high"); + miximageAspectThreshold->add("low", "low", selectedAspectThreshold == "low"); + // If there are no objects returned, then there must be a manually modified entry in the + // configuration file. Simply set the screenshot aspect threshold to "high" in this case. + if (miximageAspectThreshold->getSelectedObjects().size() == 0) + miximageAspectThreshold->selectEntry(0); + s->addWithLabel("SCREENSHOT ASPECT RATIO THRESHOLD", miximageAspectThreshold); + s->addSaveFunc([miximageAspectThreshold, s] { + if (miximageAspectThreshold->getSelected() != + Settings::getInstance()->getString("MiximageScreenshotAspectThreshold")) { + Settings::getInstance()->setString("MiximageScreenshotAspectThreshold", + miximageAspectThreshold->getSelected()); + s->setNeedsSaving(); + } + }); + + // Blank areas fill color. + auto miximageBlankAreasColor = std::make_shared>( + getHelpStyle(), "BLANK AREAS FILL COLOR", false); + const std::string selectedBlankAreasColor { + Settings::getInstance()->getString("MiximageScreenshotBlankAreasColor")}; + miximageBlankAreasColor->add("black", "black", selectedBlankAreasColor == "black"); + miximageBlankAreasColor->add("frame", "frame", selectedBlankAreasColor == "frame"); + // If there are no objects returned, then there must be a manually modified entry in the + // configuration file. Simply set the blank area fill color to "black" in this case. + if (miximageBlankAreasColor->getSelectedObjects().size() == 0) + miximageBlankAreasColor->selectEntry(0); + s->addWithLabel("BLANK AREAS FILL COLOR", miximageBlankAreasColor); + s->addSaveFunc([miximageBlankAreasColor, s] { + if (miximageBlankAreasColor->getSelected() != + Settings::getInstance()->getString("MiximageScreenshotBlankAreasColor")) { + Settings::getInstance()->setString("MiximageScreenshotBlankAreasColor", + miximageBlankAreasColor->getSelected()); + s->setNeedsSaving(); + } + }); + // Screenshot scaling method. auto miximageScaling = std::make_shared>( getHelpStyle(), "SCREENSHOT SCALING", false); diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index c92d53be6..f99831b38 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -121,6 +121,10 @@ void Settings::setDefaults() mBoolMap["ScrapeManuals"] = {true, true}; mStringMap["MiximageResolution"] = {"1280x960", "1280x960"}; + mStringMap["MiximageScreenshotHorizontalFit"] = {"crop", "crop"}; + mStringMap["MiximageScreenshotVerticalFit"] = {"contain", "contain"}; + mStringMap["MiximageScreenshotAspectThreshold"] = {"high", "high"}; + mStringMap["MiximageScreenshotBlankAreasColor"] = {"black", "black"}; mStringMap["MiximageScreenshotScaling"] = {"sharp", "sharp"}; mStringMap["MiximageBoxSize"] = {"medium", "medium"}; mStringMap["MiximagePhysicalMediaSize"] = {"medium", "medium"}; diff --git a/es-core/src/utils/CImgUtil.cpp b/es-core/src/utils/CImgUtil.cpp index e0ff53060..b46106abe 100644 --- a/es-core/src/utils/CImgUtil.cpp +++ b/es-core/src/utils/CImgUtil.cpp @@ -16,9 +16,9 @@ namespace Utils cimg_library::CImg& image) { // CImg does not interleave pixels as in BGRABGRABGRA so a conversion is required. - int counter = 0; - for (int r = 0; r < image.height(); ++r) { - for (int c = 0; c < image.width(); ++c) { + int counter {0}; + for (int r {0}; r < image.height(); ++r) { + for (int c {0}; c < image.width(); ++c) { image(c, r, 0, 0) = imageBGRA[counter + 0]; image(c, r, 0, 1) = imageBGRA[counter + 1]; image(c, r, 0, 2) = imageBGRA[counter + 2]; @@ -31,8 +31,8 @@ namespace Utils void convertCImgToBGRA(const cimg_library::CImg& image, std::vector& imageBGRA) { - for (int r = image.height() - 1; r >= 0; --r) { - for (int c = 0; c < image.width(); ++c) { + for (int r {image.height() - 1}; r >= 0; --r) { + for (int c {0}; c < image.width(); ++c) { imageBGRA.emplace_back((unsigned char)image(c, r, 0, 0)); imageBGRA.emplace_back((unsigned char)image(c, r, 0, 1)); imageBGRA.emplace_back((unsigned char)image(c, r, 0, 2)); @@ -45,9 +45,9 @@ namespace Utils cimg_library::CImg& image) { // CImg does not interleave 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) { + 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]; @@ -60,8 +60,8 @@ namespace Utils void convertCImgToRGBA(const cimg_library::CImg& image, std::vector& imageRGBA) { - for (int r = image.height() - 1; r >= 0; --r) { - for (int c = 0; c < image.width(); ++c) { + for (int r {image.height() - 1}; r >= 0; --r) { + for (int c {0}; c < image.width(); ++c) { imageRGBA.emplace_back((unsigned char)image(c, r, 0, 2)); imageRGBA.emplace_back((unsigned char)image(c, r, 0, 1)); imageRGBA.emplace_back((unsigned char)image(c, r, 0, 0)); @@ -77,44 +77,44 @@ namespace Utils if (image.spectrum() != 4) return; - double pixelValueSum = 0.0l; - int rowCounterTop = 0; - int rowCounterBottom = 0; - unsigned int columnCounterLeft = 0; - unsigned int columnCounterRight = 0; + double pixelValueSum {0.0}; + 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) { + 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) + if (pixelValueSum == 0.0) ++rowCounterTop; else break; } - for (int i = 0; i < image.height(); ++i) { + 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) + if (pixelValueSum == 0.0) ++rowCounterBottom; else break; } - for (int i = 0; i < image.width(); ++i) { + 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) + if (pixelValueSum == 0.0) ++columnCounterLeft; else break; } - for (int i = image.width() - 1; i > 0; --i) { + 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) + if (pixelValueSum == 0.0) ++columnCounterRight; else break; @@ -132,44 +132,44 @@ namespace Utils if (image.spectrum() != 4) return; - double pixelValueSum = 0.0l; - int rowCounterTop = 0; - int rowCounterBottom = 0; - unsigned int columnCounterLeft = 0; - unsigned int columnCounterRight = 0; + double pixelValueSum {0.0}; + 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) { + 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) + if (pixelValueSum == 0.0) ++rowCounterTop; else break; } - for (int i = 0; i < image.height(); ++i) { + 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) + if (pixelValueSum == 0.0) ++rowCounterBottom; else break; } - for (int i = 0; i < image.width(); ++i) { + 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) + if (pixelValueSum == 0.0) ++columnCounterLeft; else break; } - for (int i = image.width() - 1; i > 0; --i) { + 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) + if (pixelValueSum == 0.0) ++columnCounterRight; else break; @@ -191,27 +191,27 @@ namespace Utils void cropLetterboxes(cimg_library::CImg& image) { - double pixelValueSum = 0.0l; - int rowCounterUpper = 0; - int rowCounterLower = 0; + double pixelValueSum {0.0}; + int rowCounterUpper {0}; + int rowCounterLower {0}; // Count the number of rows that are pure black. - for (int i = image.height() - 1; i > 0; --i) { + 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) + if (pixelValueSum == 0.0) ++rowCounterUpper; else break; } - for (int i = 0; i < image.height(); ++i) { + 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) + if (pixelValueSum == 0.0) ++rowCounterLower; else break; @@ -227,27 +227,27 @@ namespace Utils void cropPillarboxes(cimg_library::CImg& image) { - double pixelValueSum = 0.0l; - unsigned int columnCounterLeft = 0; - unsigned int columnCounterRight = 0; + double pixelValueSum {0.0}; + 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) { + 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) + if (pixelValueSum == 0.0) ++columnCounterLeft; else break; } - for (int i = image.width() - 1; i > 0; --i) { + 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) + if (pixelValueSum == 0.0) ++columnCounterRight; else break;