ES-DE/es-app/src/MiximageGenerator.cpp
XargonWan 1167c4be41
feat/update 3.1.0 (#7)
* Added initial text shaping support

* Fixed some font issues

* (Windows) Added initial text shaping support

* (macOS) Added initial text shaping support

* Disabled building of HarfBuzz-subset on Windows and macOS

* (Android) Added initial text shaping support

* Added the nl_NL locale to locale/languages

* Changed the font VRAM usage calculation to actually only include texture data

* Moved the HarfBuzz segment building to a separate function

Also implemented segment caching and fixed an issue where missing glyphs were not handled correctly

* Moved the text shaping to a separate function

* Fixed a text shaping issue when there was a font change for the last character of a string

* Added support for the pl_PL locale

* Changed two font calculation functions to use shaped text

Also consolidated the HarfBuzz segment creation and shaping into a single function

* Added a hack to make shaped text wrap somehow correctly

* Changed the text shaping function to return the segment vector

* Text shaping segments are no longer created by space characters

* RTL text segments are now flagged as such

* Fixed an issue where text was not correctly centered after line breaks

* Reverted some font changes that were not needed after all

* Changed to having HarfBuzz set the horizontal glyph advance

* Fixed another failure mode for the wrapText shaped text hack

* Added a precaution to prevent crashes in case of broken fonts being used

* Made accurate text layout work correctly using HarfBuzz

* Removed the offensive wrapText hacks and added some optimizations

Also changed the three dots to an actual ellipsis Unicode character when abbreviating text

* Reverted a change in TextComponent as it caused unforeseen issues

* Changed Font::shapeText() to pass the segments vector by reference

* Removed a temporary member variable in Font and replaced it with proper argument passing

* Fixed a regression where text shaping stopped working

* Added sharing of glyph atlas entries between shaped glyph entries that need the same texture

* Added support for the ar_EG locale

* Some font-related code and comments cleanup

* Fixed a source file header typo

* Documentation update

* Removed a lot of unnecessary text processing

* Added the ICU library as a dependency

* (Android) Added the ICU library as a dependency

* (macOS) Added the ICU library as a dependency

* (Windows) Added the ICU library as a dependency

* (Windows) Fixed an MSVC compiler warning

* Replaced all built-in Unicode case conversion logic and lookup tables with facilities from the ICU library

* Documentation update

* Updated the pl_PL translations

* Added a menu title font size adjustment for the pl_PL translations

* Removed support for NetBSD and OpenBSD

* Changed a code comment that referred to BSD Unix

* Documentation update

* Silenced some Clang compiler warnings

* Added experimental support for building on Haiku

* (Haiku) Added a ScreenScraper platform identifier

* (Haiku) Added support for the Sony PlayStation Portable (psp) game system

* (Haiku) Added support for the ScummVM Game Engine (scummvm) game system

* Documentation update

* Updated the pl_PL translations

* Changed ScreenSaver to use TextComponent instead of using Font facilities directly

* Changed Window to use TextComponent instead of using Font facilities directly

* Changed ButtonComponent to use TextComponent instead of using Font facilities directly

* Changed SliderComponent to use TextComponent instead of using Font facilities directly

* Reverted ButtonComponent and SliderComponent to render the debug overlays themselves

* Changed DateTimeEditComponent to use TextComponent instead of using Font facilities directly

* Minor code cleanup

* Changed TextEditComponent to use TextComponent instead of using Font facilities directly

* Changed Font::buildTextCache() and Font::renderTextCache() to protected functions

* Changed a compiler silencing option to only apply to Clang

* (Haiku) Updated CMake configuration to make ES-DE build on Haiku Nightly (but no longer on R1/beta4)

* Documentation update

* (Haiku) Added find rule configuration for RetroArch

Also added a single core for testing purposes

* Removed direct use of Font::wrapText() from OptionListComponent, TextEditComponent and TextListComponent

* Removed direct use of Font::wrapText() from TextComponent

* Fixed an issue where ComponentList could generate elements with negative widths

* Added an assertion to GuiComponent::setSize() to check for negative mSize values

* DateTimeEditComponent no longer renders the debug overlay unless there is a string to display

* (FreeBSD) Added support for building with DEINIT_ON_LAUNCH

* (FreeBSD) Added the man page to the CPack configuration

* (FreeBSD) Added support for rebooting and powering off from inside ES-DE

* (FreeBSD) Added fallback method to locate binary

* Added layout and line wrapping support for shaped text and for mixing of LTR and RTL scripts

* Fixed a special line wrapping scenario where a trailing space should be removed

* (Windows) Fixed some MSVC compiler warnings

* Fixed some Clang compiler warnings

* Fixed an issue where theme names in the theme downloader could get abbreviated

* Added support for the ca_AD locale

* Documentation update

* (Android) Fonts and locales are now copied earlier than the other assets as HarfBuzz and libintl need them earlier in the startup process

* Documentation update

* Added support for the de_DE locale

* (Android) Added a new default find rule entry for Flycast as its application ID has been changed

* Documentation update

* Fixed an issue where text shaping could be permanently disabled after editing text

* Fixed a potential issue where globally disabling text shaping could cause space detection to fail

* Added a check for whether a text element has a width defined when the container property is set

* (Android) Changed ePSXe to use %ROM% instead of %ROMSAF%

* (Haiku) Added support for the PDF viewer

* Updated the el_GR.po, es_ES.po, fr_FR.po, it_IT.po, ja_JP.po, ru_RU.po and zh_CN.po locale files

* Documentation update

* (Haiku) Added correct installation directories to the CMake configuration

* (Haiku) Changed to correct installation directories

* (Haiku) Added support for the correct system resource directories

* (Haiku) Made sure es-pdf-convert is found under all circumstances

* Updated the fr_FR translations

* Updated the es_ES translations

* Updated the it_IT translations

* Added a menu title font size adjustment for the it_IT translations

* Updated the ja_JP translations

* Updated the zh_CN translations

* Fixed an issue where scraping using TheGamesDB would crash the application

* Added an extra check in OptionListComponent to avoid potential crashes

* Removed support for the ca_AD locale

* Added a code comment clarification in FileSystemUtil

* Updated the pl_PL translations

* Some minor code modernization in MameNames

* Fixed an issue where returning from a game would sometimes make the helpsystem use the dimmed theme properties

* (Haiku) Added a resource file

* Added a menu title font size adjustment for the de_DE translations

* (Haiku) Added support for some game systems

* (Haiku) Added a HaikuPorts recipe

* (Haiku) Fixed an URI issue in the HaikuPorts recipe

* Documentation update

* (Haiku) Added configuration for a number of game systems

* Updated the it_IT translations

* Documentation update

* (Haiku) Updated the srcGitRev value in the HaikuPorts recipe

* (Haiku) Added configuration for a number of game systems

* Documentation update

* (Haiku) Updated the srcGitRev value in the HaikuPorts recipe

* (Haiku) Added configuration for a number of game systems

* Documentation update

* (Haiku) Added configuration for a number of game systems

* Documentation update

* (Haiku) Updated the srcGitRev value in the HaikuPorts recipe

* Added basic configuration support and menu entries for theme localization

* Changed a theme loading debug message

* (linear-es-de) Fixed an issue where the system logo for saturnjp was incorrectly showing the western variant

* (modern-es-de) Fixed an issue where the system logo for saturnjp was incorrectly showing the western variant

* Updated the it_IT translations

* Added support for using language variables in the theme configuration

* Added localization support to DateTimeComponent

* Added translations for the automatic collection names when used as theme system variables

* Added localization support for the theme game counter

* Added theme contextual hinting to the custom collection summary text in CollectionSystemsManager

Also added translation support for a string that was previously missed

* Added localization support to the label entries in capabilities.xml

* Fixed a regression where horizontal text containers would sometimes not work correctly

* Fixed an issue where text elements defined as gamecount using the systemdata property could not scroll horizontally

* Added support for including theme files from within the colorScheme and fontSize tag pairs

* Added translations for the automatic collection names (short name versions) when used as theme system variables

* Fixed an incorrect code comment in CollectionSystemsManager

* Added translations for the name and fullname systemdata properties for the text element

* Added translation support for the metadata property for the text element

* Updated all locale (.po) files with the theme engine localization additions

* (linear-es-de) Added translations for en_US, en_GB and sv_SE

* Documentation update

* Updated the fr_FR translations

* (linear-es-de) Added translations for fr_FR

* Updated the ja_JP translations

* Updated the zh_CN translations

* (modern-es-de) Added translations for en_US, en_GB, fr_FR and sv_SE

* Updated the es_ES translations

* Updated the ro_RO translations

* (linear-es-de) Added translations for es_ES

* (linear-es-de) Added translations for ro_RO

* (slate-es-de) Added translations for en_US, en_GB and sv_SE

* (linear-es-de) Updated the es_ES translations

* (modern-es-de) Updated the fr_FR translations

* (linear-es-de) Some minor translation changes

* (modern-es-de) Added translations for ro_RO

* (slate-es-de) Added translations for ro_RO

* Updated the it_IT translations

* Updated the pt_BR translations

* (linear-es-de) Added translations for it_IT

* (modern-es-de) Decreased the helpsystem entry spacing

* (modern-es-de) Added translations for es_ES and it_IT

* (slate-es-de) Added translations for es_ES, fr_FR and it_IT

* (linear-es-de) Added translations for pt_BR

* (modern-es-de) Added translations for pt_BR

* (slate-es-de) Added translations for pt_BR

* (Haiku) Added support for the c64, plus4 and vic20 systems

* Documentation update

* (Haiku) Updated the srcGitRev value in the HaikuPorts recipe

* Updated SDL to 2.30.6 on Android, Windows, macOS and the Linux AppImage builds

* Added an ICU filter configuration file

* (macOS) Reduced the ICU library size via a data filter file

* (Windows) Reduced the ICU library size via a data filter file

* Updated the ru_RU translations

* (linear-es-de) Added translations for ru_RU

* (modern-es-de) Added translations for ru_RU

* (slate-es-de) Added translations for ru_RU

* Added a menu title font size adjustment for the ru_RU translations

* Removed an unnecessary element resize in ScrollableContainer

* Fixed a line breaking issue

* Added theme engine translations for 'unknown' metadata values for developer, publisher, genre and players

* Added theme engine translations for 'never' and 'unknown' date values

* (linear-es-de) Added translations for ja_JP and zh_CN

* (modern-es-de) Added translations for ja_JP and zh_CN

* (slate-es-de) Added translations for ja_JP and zh_CN

* Updated all locales with new theme engine translations

* Fixed an issue where the text element defaultValue property no longer worked correctly

* (modern-es-de) Added some capitalized default metadata values

* Documentation update

* pdated the el_GR translations

* (linear-es-de) Updated the system metadata

* (linear-es-de) Added sv_SE translations for all system hardware types

* Updated the de_DE translations

* Updated the pl_PL translations

* Bundled the July 2024 release of the Mozilla TLS/SSL certificates

* Updated the MAME index files to include ROMs up to MAME version 0.269

* (linear-es-de) Added translations for pl_PL

* Added the VirtualXT RetroArch core as an alternative emulator for the dos and pc systems

* Added the Stella 2023 RetroArch core as an alternative emulator for the atari2600 system

* Removed support for the ar_EG, de_DE, el_GR and nl_NL locales and moved their .po files to an archive directory

* Documentation update

* (modern-es-de) Added translations for pl_PL

* (slate-es-de) Added translations for pl_PL

* Updated SDL to 2.30.7 on Android, Windows, macOS and the Linux AppImage builds

* Updated the fr_FR translations

* Added support for the new Lime3DS binary names on Linux, macOS and Windows

* Added some missing find rules for Lime3DS

* (Windows) Added 'Shortcut' as an alternative emulator for the switch system

Also added the .lnk file extension

* Added jgenesis as an alternative emulator for the famicom, gamegear, gb, gbc, genesis, mastersystem, megacd, megacdjp, megadrive, megadrivejp, nes, segacd, sfc, snes and snesna systems on Linux and Windows

* Documentation update

* Added izapple2 standalone as an alternative emulator for the apple2 system on Linux and Windows

* (Android) Added support for the Microsoft Windows (windows) game system using the Winlator emulator

* (Android) Added Winlator PRoot Cmod standalone as an alternative emulator for the windows system

* Documentation update

* (Android) Added support for the PC Arcade Systems (pcarcade) and Taito Type X (type-x) game systems

* Bumped the version to 3.1.0

* (modern-es-de) Eliminated an annoying debug message

* (linear-es-de) Added some missing metadata files

* (linear-es-de) Added some missing sv_SE translations

* Updated the Winlator emulator names

* Documentation update

* Documentation update for the 3.1.0 release

* Updated latest_release.json for the 3.1.0 release

* Fixed a typo in the changelog

* Documentation update

* (Haiku) Updated the srcGitRev value in the HaikuPorts recipe

* Bumped the version to 3.1.1-alpha

* Video player resources are now completely freed up after finishing view transitions

* Changed a rounding in ScrollableContainer to slightly decrease the risk of glyphs getting cut off at the bottom of the container

* Added the Nanum Square Neo Korean font

* Added support for the ko_KR locale

* Fixed an issue where newly entered ScreenScraper username and password values were positioned incorrectly vertically in the account settings menu

* Documentation update

* Changed the position of the ko_KR language

* Changed the ja_JP position in the languages file

* Fixed an issue where attempting to view media for a game that had no downloaded media paused the playback of all static theme videos

* Documentation update

* Added support for the de_DE locale

* Updated the fr_FR translations

* Documentation update

* Updated the de_DE translations

---------

Co-authored-by: Leon Styhre <leon@leonstyhre.com>
2024-09-18 02:23:26 +02:00

920 lines
37 KiB
C++

// SPDX-License-Identifier: MIT
//
// ES-DE Frontend
// MiximageGenerator.cpp
//
// Generates miximages from screenshots, marquees, 3D boxes/covers and physical media images.
// Called from GuiScraperSearch and GuiOfflineGenerator.
//
#include "MiximageGenerator.h"
#include "Log.h"
#include "Settings.h"
#include "SystemData.h"
#include "utils/LocalizationUtil.h"
#include "utils/StringUtil.h"
#include <chrono>
MiximageGenerator::MiximageGenerator(FileData* game, std::string& resultMessage)
: mGame {game}
, mResultMessage {resultMessage}
, mWidth {1280}
, mHeight {960}
, mMarquee {false}
, mBox3D {false}
, mCover {false}
, mPhysicalMedia {false}
{
}
void MiximageGenerator::startThread(std::promise<bool>* miximagePromise)
{
mMiximagePromise = miximagePromise;
LOG(LogDebug) << "MiximageGenerator::MiximageGenerator(): Creating miximage 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 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";
}
}
if (Settings::getInstance()->getBool("MiximageIncludePhysicalMedia")) {
if ((mPhysicalMediaPath = mGame->getPhysicalMediaPath()) != "") {
mPhysicalMedia = true;
}
else {
LOG(LogDebug)
<< "MiximageGenerator::MiximageGenerator(): No physical media image found";
}
}
const auto startTime = std::chrono::system_clock::now();
if (generateImage()) {
LOG(LogError) << "Failed to generate miximage";
mMiximagePromise->set_value(true);
mResultMessage = mMessage;
return;
}
else {
LOG(LogDebug) << "MiximageGenerator::MiximageGenerator(): Processing completed in: "
<< std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now() - 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};
FIBITMAP* physicalMediaFile {nullptr};
unsigned int fileWidth {0};
unsigned int fileHeight {0};
unsigned int filePitch {0};
#if defined(_WIN64)
fileFormat = FreeImage_GetFileTypeU(Utils::String::stringToWideString(mScreenshotPath).c_str());
#else
fileFormat = FreeImage_GetFileType(mScreenshotPath.c_str());
#endif
if (fileFormat == FIF_UNKNOWN)
#if defined(_WIN64)
fileFormat = FreeImage_GetFIFFromFilenameU(
Utils::String::stringToWideString(mScreenshotPath).c_str());
#else
fileFormat = FreeImage_GetFIFFromFilename(mScreenshotPath.c_str());
#endif
if (fileFormat == FIF_UNKNOWN) {
LOG(LogError) << "Screenshot image in unknown image format, aborting";
mMessage = _("Screenshot in unknown format, couldn't generate miximage");
return true;
}
// Make sure that we can actually read this format.
if (FreeImage_FIFSupportsReading(fileFormat)) {
#if defined(_WIN64)
screenshotFile =
FreeImage_LoadU(fileFormat, Utils::String::stringToWideString(mScreenshotPath).c_str());
#else
screenshotFile = FreeImage_Load(fileFormat, mScreenshotPath.c_str());
#endif
}
else {
LOG(LogError) << "Screenshot file format not supported";
mMessage = _("Screenshot in unsupported format, couldn't generate miximage");
return true;
}
if (!screenshotFile) {
LOG(LogError) << "Error loading screenshot image, corrupt file?";
mMessage = _("Error loading screenshot, couldn't generate miximage");
return true;
}
if (mMarquee) {
#if defined(_WIN64)
fileFormat =
FreeImage_GetFileTypeU(Utils::String::stringToWideString(mMarqueePath).c_str());
#else
fileFormat = FreeImage_GetFileType(mMarqueePath.c_str());
#endif
if (fileFormat == FIF_UNKNOWN)
#if defined(_WIN64)
fileFormat = FreeImage_GetFIFFromFilenameU(
Utils::String::stringToWideString(mMarqueePath).c_str());
#else
fileFormat = FreeImage_GetFIFFromFilename(mMarqueePath.c_str());
#endif
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 {
#if defined(_WIN64)
marqueeFile = FreeImage_LoadU(fileFormat,
Utils::String::stringToWideString(mMarqueePath).c_str());
#else
marqueeFile = FreeImage_Load(fileFormat, mMarqueePath.c_str());
#endif
if (!marqueeFile) {
LOG(LogError) << "Couldn't load marquee image, corrupt file?";
mMessage = _("Error loading marquee image, corrupt file?");
mMarquee = false;
}
}
}
if (mBox3D) {
#if defined(_WIN64)
fileFormat = FreeImage_GetFileTypeU(Utils::String::stringToWideString(mBox3DPath).c_str());
#else
fileFormat = FreeImage_GetFileType(mBox3DPath.c_str());
#endif
if (fileFormat == FIF_UNKNOWN)
#if defined(_WIN64)
fileFormat = FreeImage_GetFIFFromFilenameU(
Utils::String::stringToWideString(mBox3DPath).c_str());
#else
fileFormat = FreeImage_GetFIFFromFilename(mBox3DPath.c_str());
#endif
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 {
#if defined(_WIN64)
boxFile =
FreeImage_LoadU(fileFormat, Utils::String::stringToWideString(mBox3DPath).c_str());
#else
boxFile = FreeImage_Load(fileFormat, mBox3DPath.c_str());
#endif
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) {
#if defined(_WIN64)
fileFormat = FreeImage_GetFileTypeU(Utils::String::stringToWideString(mCoverPath).c_str());
#else
fileFormat = FreeImage_GetFileType(mCoverPath.c_str());
#endif
if (fileFormat == FIF_UNKNOWN)
#if defined(_WIN64)
fileFormat = FreeImage_GetFIFFromFilenameU(
Utils::String::stringToWideString(mCoverPath).c_str());
#else
fileFormat = FreeImage_GetFIFFromFilename(mCoverPath.c_str());
#endif
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 {
#if defined(_WIN64)
boxFile =
FreeImage_LoadU(fileFormat, Utils::String::stringToWideString(mCoverPath).c_str());
#else
boxFile = FreeImage_Load(fileFormat, mCoverPath.c_str());
#endif
if (!boxFile) {
LOG(LogError) << "Couldn't load box cover image, corrupt file?";
mMessage = _("Error loading box cover image, corrupt file?");
mCover = false;
}
}
}
if (mPhysicalMedia) {
#if defined(_WIN64)
fileFormat =
FreeImage_GetFileTypeU(Utils::String::stringToWideString(mPhysicalMediaPath).c_str());
#else
fileFormat = FreeImage_GetFileType(mPhysicalMediaPath.c_str());
#endif
if (fileFormat == FIF_UNKNOWN)
#if defined(_WIN64)
fileFormat = FreeImage_GetFIFFromFilenameU(
Utils::String::stringToWideString(mPhysicalMediaPath).c_str());
#else
fileFormat = FreeImage_GetFIFFromFilename(mPhysicalMediaPath.c_str());
#endif
if (fileFormat == FIF_UNKNOWN) {
LOG(LogDebug) << "Physical media in unknown format, skipping image";
mPhysicalMedia = false;
}
if (!FreeImage_FIFSupportsReading(fileFormat)) {
LOG(LogDebug) << "Physical media file format not supported, skipping image";
mPhysicalMedia = false;
}
else {
#if defined(_WIN64)
physicalMediaFile = FreeImage_LoadU(
fileFormat, Utils::String::stringToWideString(mPhysicalMediaPath).c_str());
#else
physicalMediaFile = FreeImage_Load(fileFormat, mPhysicalMediaPath.c_str());
#endif
if (!physicalMediaFile) {
LOG(LogError) << "Couldn't load physical media image, corrupt file?";
mMessage = _("Error loading physical media image, corrupt file?");
mPhysicalMedia = 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 {6 * resolutionMultiplier};
const unsigned int screenshotHeight {400 * resolutionMultiplier};
// These sizes are increased slightly when adding the drop shadow.
const unsigned int marqueeTargetWidth {310 * resolutionMultiplier};
const unsigned int marqueeTargetHeight {230 * resolutionMultiplier};
unsigned int boxTargetWidth {0};
unsigned int boxTargetHeight {0};
unsigned int coverTargetWidth {0};
unsigned int physicalMediaTargetWidth {0};
unsigned int physicalMediaTargetHeight {0};
if (Settings::getInstance()->getString("MiximageBoxSize") == "small") {
boxTargetWidth = 264 * resolutionMultiplier;
boxTargetHeight = 254 * resolutionMultiplier;
coverTargetWidth = 212 * resolutionMultiplier;
}
else if (Settings::getInstance()->getString("MiximageBoxSize") == "large") {
boxTargetWidth = 372 * resolutionMultiplier;
boxTargetHeight = 360 * resolutionMultiplier;
coverTargetWidth = 300 * resolutionMultiplier;
}
else { // Medium size.
boxTargetWidth = 310 * resolutionMultiplier;
boxTargetHeight = 300 * resolutionMultiplier;
coverTargetWidth = 250 * resolutionMultiplier;
}
if (Settings::getInstance()->getString("MiximagePhysicalMediaSize") == "small") {
physicalMediaTargetWidth = 120 * resolutionMultiplier;
physicalMediaTargetHeight = 96 * resolutionMultiplier;
}
else if (Settings::getInstance()->getString("MiximagePhysicalMediaSize") == "large") {
physicalMediaTargetWidth = 196 * resolutionMultiplier;
physicalMediaTargetHeight = 156 * resolutionMultiplier;
}
else { // Medium size.
physicalMediaTargetWidth = 150 * resolutionMultiplier;
physicalMediaTargetHeight = 120 * resolutionMultiplier;
}
const unsigned int marqueeShadowSize {6 * resolutionMultiplier};
const unsigned int boxShadowSize {6 * resolutionMultiplier};
const unsigned int physicalMediaShadowSize {6 * resolutionMultiplier};
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<unsigned char> screenshotVector(fileWidth * fileHeight * 4);
FreeImage_ConvertToRawBits(reinterpret_cast<BYTE*>(&screenshotVector.at(0)), screenshotFile,
filePitch, 32, FI_RGBA_BLUE, FI_RGBA_GREEN, FI_RGBA_RED, 1);
CImg<unsigned char> screenshotImage(fileWidth, fileHeight, 1, 4, 0);
// Convert the RGBA image to CImg internal format.
Utils::CImg::convertBGRAToCImg(screenshotVector, screenshotImage);
screenshotVector.clear();
if (Settings::getInstance()->getBool("MiximageRemoveLetterboxes"))
Utils::CImg::cropLetterboxes(screenshotImage);
if (Settings::getInstance()->getBool("MiximageRemovePillarboxes"))
Utils::CImg::cropPillarboxes(screenshotImage);
const float fileAspect {static_cast<float>(fileWidth) / static_cast<float>(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<int>(std::round(static_cast<float>(screenshotHeight) * fileAspect)),
screenshotHeight, 1, 4, scalingMethod);
const int offsetX {(screenshotImage.width() - static_cast<int>(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<int>(std::round(static_cast<float>(screenshotWidth) / fileAspect)), 1,
4, scalingMethod);
const int offsetY {(screenshotImage.height() - static_cast<int>(screenshotHeight)) / 2};
screenshotImage.crop(0, offsetY, 0, 3, screenshotWidth - 2, screenshotHeight + offsetY,
0, 0);
}
}
else {
screenshotImage.resize(screenshotWidth, screenshotHeight, 1, 4, scalingMethod);
}
if (containImage && (fileAspect > maxAspectValue || fileAspect < minAspectValue)) {
CImg<unsigned char> 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<float>(screenshotWidth) /
static_cast<float>(fileWidth)};
const int resizeHeight {static_cast<int>(static_cast<float>(fileHeight) * sizeRatio)};
tempImage.resize(
screenshotWidth,
static_cast<int>(std::round(static_cast<float>(fileHeight) * sizeRatio)), 1, 4,
scalingMethod);
screenshotImage.draw_image(0, (screenshotHeight - resizeHeight) / 2, tempImage);
}
else {
const float sizeRatio {static_cast<float>(screenshotHeight) /
static_cast<float>(fileHeight)};
const int resizeWidth {static_cast<int>(static_cast<float>(fileWidth) * sizeRatio)};
tempImage.resize(
static_cast<int>(std::round(static_cast<float>(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
// 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};
int xPosPhysicalMedia {0};
int yPosPhysicalMedia {0};
CImg<unsigned char> canvasImage(mWidth, mHeight, 1, 4, 0);
CImg<unsigned char> marqueeImage;
CImg<unsigned char> marqueeImageRGB;
CImg<unsigned char> marqueeImageAlpha;
CImg<unsigned char> boxImage;
CImg<unsigned char> boxImageRGB;
CImg<unsigned char> boxImageAlpha;
CImg<unsigned char> physicalMediaImage;
CImg<unsigned char> physicalMediaImageRGB;
CImg<unsigned char> physicalMediaImageAlpha;
CImg<unsigned char> frameImage(mWidth, mHeight, 1, 4, 0);
xPosScreenshot = canvasImage.width() / 2 - screenshotWidth / 2 + screenshotOffset;
yPosScreenshot = canvasImage.height() / 2 - screenshotHeight / 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<unsigned char> marqueeVector(fileWidth * fileHeight * 4);
FreeImage_ConvertToRawBits(reinterpret_cast<BYTE*>(&marqueeVector.at(0)), marqueeFile,
filePitch, 32, FI_RGBA_BLUE, FI_RGBA_GREEN, FI_RGBA_RED, 1);
marqueeImage = CImg<unsigned char>(FreeImage_GetWidth(marqueeFile),
FreeImage_GetHeight(marqueeFile), 1, 4, 0);
Utils::CImg::convertBGRAToCImg(marqueeVector, marqueeImage);
Utils::CImg::removeTransparentPadding(marqueeImage);
unsigned int marqueeWidth {static_cast<unsigned int>(marqueeImage.width())};
unsigned int marqueeHeight {static_cast<unsigned int>(marqueeImage.height())};
calculateMarqueeSize(marqueeTargetWidth, marqueeTargetHeight, marqueeWidth, marqueeHeight);
// We use Lanczos3 which is the highest quality resampling method available.
marqueeImage.resize(marqueeWidth, marqueeHeight, 1, 4, 6);
// Add a drop shadow using 4 iterations of box blur.
Utils::CImg::addDropShadow(marqueeImage, marqueeShadowSize, 0.6f, 4);
xPosMarquee = canvasImage.width() - marqueeImage.width();
yPosMarquee = 0;
// Only RGB channels for the image.
marqueeImageRGB = CImg<unsigned char>(marqueeImage.get_shared_channels(0, 2));
// Only alpha channel for the image.
marqueeImageAlpha = CImg<unsigned char>(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<unsigned char> boxVector(fileWidth * fileHeight * 4);
FreeImage_ConvertToRawBits(reinterpret_cast<BYTE*>(&boxVector.at(0)), boxFile, filePitch,
32, FI_RGBA_BLUE, FI_RGBA_GREEN, FI_RGBA_RED, 1);
boxImage =
CImg<unsigned char>(FreeImage_GetWidth(boxFile), FreeImage_GetHeight(boxFile), 1, 4);
Utils::CImg::convertBGRAToCImg(boxVector, boxImage);
Utils::CImg::removeTransparentPadding(boxImage);
float sizeRatio {static_cast<float>(boxImage.width()) /
static_cast<float>(boxImage.height())};
if (sizeRatio > 1.14f && Settings::getInstance()->getBool("MiximageRotateHorizontalBoxes"))
boxImage.rotate(90.0f);
float scaleFactor {static_cast<float>(boxTargetHeight) /
static_cast<float>(boxImage.height())};
unsigned int width {
static_cast<unsigned int>(static_cast<float>(boxImage.width()) * scaleFactor)};
unsigned int targetWidth {0};
// We make this distinction as some cover images are in square format and would cover
// too much surface otherwise.
if (mBox3D)
targetWidth = boxTargetWidth;
else
targetWidth = coverTargetWidth;
if (width > targetWidth) {
scaleFactor = static_cast<float>(targetWidth) / static_cast<float>(boxImage.width());
int height {static_cast<int>(static_cast<float>(boxImage.height()) * scaleFactor)};
// We use Lanczos3 which is the highest quality resampling method available.
boxImage.resize(targetWidth, height, 1, 4, 6);
}
else {
boxImage.resize(width, boxTargetHeight, 1, 4, 6);
}
Utils::CImg::addDropShadow(boxImage, boxShadowSize, 0.6f, 4);
xPosBox = 0;
yPosBox = canvasImage.height() - boxImage.height();
// Only RGB channels for the image.
boxImageRGB = CImg<unsigned char>(boxImage.get_shared_channels(0, 2));
// Only alpha channel for the image.
boxImageAlpha = CImg<unsigned char>(boxImage.get_shared_channel(3));
}
if (mPhysicalMedia) {
if (FreeImage_GetBPP(physicalMediaFile) != 32) {
FIBITMAP* physicalMediaTemp {FreeImage_ConvertTo32Bits(physicalMediaFile)};
FreeImage_Unload(physicalMediaFile);
physicalMediaFile = physicalMediaTemp;
}
fileWidth = FreeImage_GetWidth(physicalMediaFile);
fileHeight = FreeImage_GetHeight(physicalMediaFile);
filePitch = FreeImage_GetPitch(physicalMediaFile);
std::vector<unsigned char> physicalMediaVector(fileWidth * fileHeight * 4);
FreeImage_ConvertToRawBits(reinterpret_cast<BYTE*>(&physicalMediaVector.at(0)),
physicalMediaFile, filePitch, 32, FI_RGBA_BLUE, FI_RGBA_GREEN,
FI_RGBA_RED, 1);
physicalMediaImage = CImg<unsigned char>(FreeImage_GetWidth(physicalMediaFile),
FreeImage_GetHeight(physicalMediaFile), 1, 4, 0);
Utils::CImg::convertBGRAToCImg(physicalMediaVector, physicalMediaImage);
Utils::CImg::removeTransparentPadding(physicalMediaImage);
// Make sure the image size is not exceeding either the target width or height.
float scaleFactorX {static_cast<float>(physicalMediaTargetWidth) /
static_cast<float>(physicalMediaImage.width())};
float scaleFactorY {static_cast<float>(physicalMediaTargetHeight) /
static_cast<float>(physicalMediaImage.height())};
float scaleFactor {std::min(scaleFactorX, scaleFactorY)};
unsigned int width {static_cast<unsigned int>(
static_cast<float>(physicalMediaImage.width()) * scaleFactor)};
unsigned int height {static_cast<unsigned int>(
static_cast<float>(physicalMediaImage.height()) * scaleFactor)};
// We use Lanczos3 which is the highest quality resampling method available.
physicalMediaImage.resize(width, height, 1, 4, 6);
// Add a drop shadow using 4 iterations of box blur.
Utils::CImg::addDropShadow(physicalMediaImage, physicalMediaShadowSize, 0.6f, 4);
// Place it to the right of the 3D box or cover with a small margin in between.
xPosPhysicalMedia = xPosBox + boxImage.width() + 16 * resolutionMultiplier;
yPosPhysicalMedia = canvasImage.height() - physicalMediaImage.height();
// Only RGB channels for the image.
physicalMediaImageRGB = CImg<unsigned char>(physicalMediaImage.get_shared_channels(0, 2));
// Only alpha channel for the image.
physicalMediaImageAlpha = CImg<unsigned char>(physicalMediaImage.get_shared_channel(3));
}
CImg<unsigned char> frameImageAlpha(frameImage.get_shared_channel(3));
frameImageAlpha.draw_image(xPosBox, yPosBox, boxImageAlpha);
frameImageAlpha.draw_image(xPosPhysicalMedia, yPosPhysicalMedia, physicalMediaImageAlpha);
frameImageAlpha.draw_image(xPosMarquee, yPosMarquee, marqueeImageAlpha);
// 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 {8 * 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<unsigned char> 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);
if (mPhysicalMedia)
canvasImage.draw_image(xPosPhysicalMedia, yPosPhysicalMedia, physicalMediaImageRGB,
physicalMediaImageAlpha, 1, 255);
std::vector<unsigned char> canvasVector;
// Convert the image from CImg internal format.
Utils::CImg::convertCImgToBGRA(canvasImage, canvasVector);
FIBITMAP* mixImage {nullptr};
mixImage = FreeImage_ConvertFromRawBits(&canvasVector.at(0), canvasImage.width(),
canvasImage.height(), canvasImage.width() * 4, 32,
FI_RGBA_BLUE, FI_RGBA_GREEN, FI_RGBA_RED);
#if defined(_WIN64)
bool savedImage {FreeImage_SaveU(FIF_PNG, mixImage,
Utils::String::stringToWideString(getSavePath()).c_str()) !=
0};
#else
bool savedImage {FreeImage_Save(FIF_PNG, mixImage, getSavePath().c_str()) != 0};
#endif
if (!savedImage) {
LOG(LogError) << "Couldn't save miximage, permission problems or disk full?";
}
FreeImage_Unload(screenshotFile);
FreeImage_Unload(marqueeFile);
FreeImage_Unload(boxFile);
FreeImage_Unload(physicalMediaFile);
FreeImage_Unload(mixImage);
// Success.
if (savedImage)
return false;
else
return true;
}
void MiximageGenerator::calculateMarqueeSize(const unsigned int& targetWidth,
const unsigned int& targetHeight,
unsigned int& width,
unsigned int& height)
{
unsigned int adjustedTargetWidth {0};
float widthModifier {0.5f};
float scaleFactor {0.0f};
// The idea is to adjust the size of the marquee based on its surface area, so that
// wider but shorter images get a larger width than taller images in order to use
// an approximately equivalent amount of space on the miximage.
float widthRatio {static_cast<float>(width) / static_cast<float>(height)};
widthModifier = glm::clamp(widthModifier + widthRatio / 6.5f, 0.0f, 1.0f);
// Hack to increase the size slightly for wider and shorter images.
if (widthRatio >= 4)
widthModifier += glm::clamp(widthRatio / 40.0f, 0.0f, 0.3f);
adjustedTargetWidth =
static_cast<unsigned int>(static_cast<float>(targetWidth) * widthModifier);
scaleFactor = static_cast<float>(adjustedTargetWidth) / static_cast<float>(width);
// For really tall and narrow images, we may have exceeded the target height.
if (static_cast<int>(scaleFactor * static_cast<float>(height)) >
static_cast<float>(targetHeight))
scaleFactor = static_cast<float>(targetHeight) / static_cast<float>(height);
width = static_cast<int>(static_cast<float>(width) * scaleFactor);
height = static_cast<int>(static_cast<float>(height) * scaleFactor);
}
void MiximageGenerator::sampleFrameColor(CImg<unsigned char>& 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 setting.
unsigned int samples {static_cast<unsigned int>(static_cast<float>(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 {
static_cast<unsigned char>(glm::clamp(static_cast<int>(redLine / 255), 0, 255))};
unsigned char greenC {
static_cast<unsigned char>(glm::clamp(static_cast<int>(greenLine / 255), 0, 255))};
unsigned char blueC {
static_cast<unsigned char>(glm::clamp(static_cast<int>(blueLine / 255), 0, 255))};
// Convert to the HSL color space to be able to modify saturation and lightness.
CImg<float> 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. Also clamp
// the lightness to a low value so we don't get a frame that is nearly pitch black
// if the screenshot mostly contains blacks or dark colors.
colorHSL(0, 0, 0, 1) = glm::clamp(saturation * 0.9f, 0.0f, 1.0f);
colorHSL(0, 0, 0, 2) = glm::clamp(lightness * 1.25f, 0.10f, 1.0f);
const CImg<unsigned char> 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;
}
std::string MiximageGenerator::getSavePath() const
{
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->getSystemEnvData()->mStartPath, "");
std::string path {FileData::getMediaDirectory()};
if (!Utils::FileSystem::exists(path))
Utils::FileSystem::createDirectory(path);
#if defined(__ANDROID__)
if (!Utils::FileSystem::exists(path + ".nomedia")) {
LOG(LogInfo) << "Creating \"no media\" file \"" << path + ".nomedia"
<< "\"...";
Utils::FileSystem::createEmptyFile(path + ".nomedia");
if (!Utils::FileSystem::exists(path + ".nomedia")) {
LOG(LogWarning) << "Couldn't create file, permission problems?";
}
}
#endif
path += mGame->getSystemName() + "/miximages" + subFolders + "/";
if (!Utils::FileSystem::exists(path))
Utils::FileSystem::createDirectory(path);
path += name + ".png";
// Success.
return path;
}