diff options
| -rw-r--r-- | src/src/envshortcut.cpp | 321 |
1 files changed, 295 insertions, 26 deletions
diff --git a/src/src/envshortcut.cpp b/src/src/envshortcut.cpp index 55600fe..e78e904 100644 --- a/src/src/envshortcut.cpp +++ b/src/src/envshortcut.cpp @@ -353,9 +353,11 @@ QString toString(Shortcut::Locations loc) #else // Linux
+#include <QBuffer>
#include <QDir>
#include <QFile>
#include <QFileInfo>
+#include <QImage>
#include <QProcessEnvironment>
#include <QStandardPaths>
#include <QString>
@@ -394,35 +396,305 @@ static QString sanitizeDesktopName(const QString& s) return result;
}
+// ---------------------------------------------------------------------------
+// PE icon extractor — reads the largest icon embedded in a Windows .exe
+// and returns it as a QImage. Returns a null QImage on failure.
+// ---------------------------------------------------------------------------
+static QImage extractIconFromExe(const QString& exePath)
+{
+ QFile f(exePath);
+ if (!f.open(QIODevice::ReadOnly))
+ return {};
+
+ auto peek = [&](qint64 offset, qint64 size) -> QByteArray {
+ if (offset < 0 || offset + size > f.size())
+ return {};
+ f.seek(offset);
+ return f.read(size);
+ };
+
+ auto u16 = [](const char* p) -> quint16 {
+ return static_cast<quint16>(static_cast<unsigned char>(p[0])) |
+ (static_cast<quint16>(static_cast<unsigned char>(p[1])) << 8);
+ };
+ auto u32 = [](const char* p) -> quint32 {
+ return static_cast<quint32>(static_cast<unsigned char>(p[0])) |
+ (static_cast<quint32>(static_cast<unsigned char>(p[1])) << 8) |
+ (static_cast<quint32>(static_cast<unsigned char>(p[2])) << 16) |
+ (static_cast<quint32>(static_cast<unsigned char>(p[3])) << 24);
+ };
+
+ // DOS header: check MZ magic, read PE offset at 0x3C.
+ QByteArray dosHdr = peek(0, 64);
+ if (dosHdr.size() < 64 || dosHdr[0] != 'M' || dosHdr[1] != 'Z')
+ return {};
+
+ quint32 peOff = u32(dosHdr.constData() + 0x3C);
+
+ // PE signature.
+ QByteArray peSig = peek(peOff, 4);
+ if (peSig.size() < 4 || peSig[0] != 'P' || peSig[1] != 'E')
+ return {};
+
+ // COFF header (20 bytes) immediately after the 4-byte PE signature.
+ QByteArray coffHdr = peek(peOff + 4, 20);
+ if (coffHdr.size() < 20)
+ return {};
+ quint16 numSections = u16(coffHdr.constData() + 2);
+ quint16 optionalHdrSize = u16(coffHdr.constData() + 16);
+
+ // Optional header — we need the magic (PE32 vs PE32+) and the
+ // resource data directory entry (index 2).
+ qint64 optOff = peOff + 4 + 20;
+ QByteArray optHdr = peek(optOff, optionalHdrSize);
+ if (optHdr.size() < 4)
+ return {};
+
+ quint16 magic = u16(optHdr.constData());
+ int ddOffset; // offset of DataDirectory[0] inside optional header
+ if (magic == 0x10b) // PE32
+ ddOffset = 96;
+ else if (magic == 0x20b) // PE32+
+ ddOffset = 112;
+ else
+ return {};
+
+ // DataDirectory[2] = resource table (each entry is 8 bytes: RVA + Size).
+ int rsrcDDOff = ddOffset + 2 * 8;
+ if (rsrcDDOff + 8 > optHdr.size())
+ return {};
+ quint32 rsrcRVA = u32(optHdr.constData() + rsrcDDOff);
+ if (rsrcRVA == 0)
+ return {};
+
+ // Section headers — find the section containing rsrcRVA.
+ qint64 secOff = optOff + optionalHdrSize;
+ quint32 rsrcFileOff = 0;
+ quint32 rsrcVA = 0;
+ for (int i = 0; i < numSections; ++i) {
+ QByteArray sec = peek(secOff + i * 40, 40);
+ if (sec.size() < 40)
+ return {};
+ quint32 virtAddr = u32(sec.constData() + 12);
+ quint32 virtSize = u32(sec.constData() + 8);
+ quint32 rawOff = u32(sec.constData() + 20);
+ if (rsrcRVA >= virtAddr && rsrcRVA < virtAddr + virtSize) {
+ rsrcFileOff = rawOff + (rsrcRVA - virtAddr);
+ rsrcVA = virtAddr;
+ break;
+ }
+ }
+ if (rsrcFileOff == 0)
+ return {};
+
+ // Helper: convert an RVA inside the .rsrc section to a file offset.
+ auto rvaToFile = [&](quint32 rva) -> qint64 {
+ return static_cast<qint64>(rsrcFileOff) + (rva - rsrcRVA);
+ };
+
+ // Parse a resource directory and return (id, offset, isDir) entries.
+ struct DirEntry { quint32 id; quint32 offset; bool isDir; };
+ auto readDir = [&](quint32 dirFileOff) -> std::vector<DirEntry> {
+ std::vector<DirEntry> entries;
+ QByteArray dh = peek(dirFileOff, 16);
+ if (dh.size() < 16) return entries;
+ quint16 numNamed = u16(dh.constData() + 12);
+ quint16 numId = u16(dh.constData() + 14);
+ int total = numNamed + numId;
+ QByteArray ea = peek(dirFileOff + 16, total * 8);
+ if (ea.size() < total * 8) return entries;
+ for (int i = 0; i < total; ++i) {
+ quint32 nameOrId = u32(ea.constData() + i * 8);
+ quint32 off = u32(ea.constData() + i * 8 + 4);
+ bool isDir = (off & 0x80000000u) != 0;
+ off &= 0x7FFFFFFFu;
+ entries.push_back({nameOrId, off, isDir});
+ }
+ return entries;
+ };
+
+ // Level 0: find RT_GROUP_ICON (14) and RT_ICON (3).
+ auto level0 = readDir(rsrcFileOff);
+ quint32 groupIconOff = 0, iconOff = 0;
+ for (auto& e : level0) {
+ if (e.id == 14 && e.isDir) groupIconOff = rsrcFileOff + e.offset;
+ if (e.id == 3 && e.isDir) iconOff = rsrcFileOff + e.offset;
+ }
+ if (groupIconOff == 0 || iconOff == 0)
+ return {};
+
+ // Level 1 of RT_GROUP_ICON: pick the first group.
+ auto groupL1 = readDir(groupIconOff);
+ if (groupL1.empty() || !groupL1[0].isDir)
+ return {};
+
+ // Level 2: pick the first language.
+ auto groupL2 = readDir(rsrcFileOff + groupL1[0].offset);
+ if (groupL2.empty() || groupL2[0].isDir)
+ return {};
+
+ // Read the data entry (RVA + size).
+ QByteArray dataEntry = peek(rsrcFileOff + groupL2[0].offset, 16);
+ if (dataEntry.size() < 16)
+ return {};
+ quint32 grpDataRVA = u32(dataEntry.constData());
+ quint32 grpDataSize = u32(dataEntry.constData() + 4);
+ QByteArray grpData = peek(rvaToFile(grpDataRVA), grpDataSize);
+ if (grpData.size() < 6)
+ return {};
+
+ // Parse the GRPICONDIR: pick the icon entry with the largest area.
+ quint16 iconCount = u16(grpData.constData() + 4);
+ if (grpData.size() < 6 + iconCount * 14)
+ return {};
+
+ int bestIdx = -1;
+ int bestArea = 0;
+ quint16 bestId = 0;
+ for (int i = 0; i < iconCount; ++i) {
+ const char* e = grpData.constData() + 6 + i * 14;
+ int w = static_cast<unsigned char>(e[0]);
+ int h = static_cast<unsigned char>(e[1]);
+ if (w == 0) w = 256;
+ if (h == 0) h = 256;
+ int area = w * h;
+ if (area > bestArea) {
+ bestArea = area;
+ bestIdx = i;
+ bestId = u16(e + 12);
+ }
+ }
+ if (bestIdx < 0)
+ return {};
+
+ // Find RT_ICON with matching ID.
+ auto iconL1 = readDir(iconOff);
+ quint32 iconEntryOff = 0;
+ for (auto& e : iconL1) {
+ if (e.id == bestId && e.isDir) {
+ auto iconL2 = readDir(rsrcFileOff + e.offset);
+ if (!iconL2.empty() && !iconL2[0].isDir)
+ iconEntryOff = rsrcFileOff + iconL2[0].offset;
+ break;
+ }
+ }
+ if (iconEntryOff == 0)
+ return {};
+
+ QByteArray iconDE = peek(iconEntryOff, 16);
+ if (iconDE.size() < 16)
+ return {};
+ quint32 iconDataRVA = u32(iconDE.constData());
+ quint32 iconDataSize = u32(iconDE.constData() + 4);
+ QByteArray iconData = peek(rvaToFile(iconDataRVA), iconDataSize);
+ if (iconData.isEmpty())
+ return {};
+
+ // Try loading as PNG first (256x256 icons are typically stored as PNG).
+ QImage img;
+ img.loadFromData(iconData, "PNG");
+ if (!img.isNull())
+ return img;
+
+ // Otherwise it's a raw BITMAPINFOHEADER (DIB). Wrap it in a minimal
+ // .ico so Qt's ICO reader can handle it.
+ QByteArray ico;
+ QBuffer buf(&ico);
+ buf.open(QIODevice::WriteOnly);
+ // ICONDIR header
+ const char icoHdr[6] = {0, 0, 1, 0, 1, 0}; // reserved=0, type=1, count=1
+ buf.write(icoHdr, 6);
+ // ICONDIRENTRY (use the values from the group icon entry)
+ const char* grpE = grpData.constData() + 6 + bestIdx * 14;
+ char entry[16];
+ memcpy(entry, grpE, 12); // copy w,h,colorCount,reserved,planes,bitCount,size
+ quint32 dataOff = 6 + 16; // offset to icon data = after header + 1 entry
+ entry[12] = dataOff & 0xFF;
+ entry[13] = (dataOff >> 8) & 0xFF;
+ entry[14] = (dataOff >> 16) & 0xFF;
+ entry[15] = (dataOff >> 24) & 0xFF;
+ // Fix the size field (bytes 8-11) to match actual data size
+ entry[8] = iconDataSize & 0xFF;
+ entry[9] = (iconDataSize >> 8) & 0xFF;
+ entry[10] = (iconDataSize >> 16) & 0xFF;
+ entry[11] = (iconDataSize >> 24) & 0xFF;
+ buf.write(entry, 16);
+ buf.write(iconData);
+ buf.close();
+
+ img.loadFromData(ico, "ICO");
+ return img;
+}
+
+// Return the path to the bundled Fluorine icon inside the AppImage,
+// or the installed hicolor copy. Empty string if neither exists.
+static QString bundledFluorineIcon()
+{
+ QString appDir = QProcessEnvironment::systemEnvironment().value("APPDIR");
+ if (!appDir.isEmpty()) {
+ QString bundled = appDir + "/usr/share/icons/hicolor/256x256/apps/com.fluorine.manager.png";
+ if (QFile::exists(bundled))
+ return bundled;
+ }
+ QString hicolor = QDir::homePath() +
+ "/.local/share/icons/hicolor/256x256/apps/com.fluorine.manager.png";
+ if (QFile::exists(hicolor))
+ return hicolor;
+ return {};
+}
+
+// Check whether an icon file is just the Fluorine fallback icon
+// (same file size as the bundled one).
+static bool isFallbackIcon(const QString& iconPath)
+{
+ QString bundled = bundledFluorineIcon();
+ if (bundled.isEmpty())
+ return false;
+ return QFileInfo(iconPath).size() == QFileInfo(bundled).size();
+}
+
// Install a game-specific icon to ~/.local/share/icons/fluorine/ and return
-// the absolute path. Falls back to the bundled Fluorine icon.
-static QString installIcon(const QString& iconBaseName)
+// the absolute path. Tries to extract the icon from exePath (.exe) first,
+// then falls back to the bundled Fluorine icon.
+//
+// If a cached icon exists but is just the fallback, re-attempts extraction
+// in case the executable has changed or was previously unavailable.
+static QString installIcon(const QString& iconBaseName, const QString& exePath = {})
{
QString iconDir = QDir::homePath() + "/.local/share/icons/fluorine";
QString iconDest = iconDir + "/" + iconBaseName + ".png";
- if (QFile::exists(iconDest)) {
+ // If the icon already exists and is NOT the fallback, keep it.
+ if (QFile::exists(iconDest) && !isFallbackIcon(iconDest)) {
return iconDest;
}
- // No game-specific icon available — install the bundled Fluorine icon
- // under the game-specific name so each shortcut gets its own file.
QDir().mkpath(iconDir);
- QString appDir = QProcessEnvironment::systemEnvironment().value("APPDIR");
- if (!appDir.isEmpty()) {
- QString bundled = appDir + "/usr/share/icons/hicolor/256x256/apps/com.fluorine.manager.png";
- if (QFile::exists(bundled)) {
- QFile::copy(bundled, iconDest);
- return iconDest;
+ // Try to extract the icon from the .exe file.
+ if (!exePath.isEmpty()) {
+ QImage icon = extractIconFromExe(exePath);
+ if (!icon.isNull()) {
+ if (icon.width() != 256 || icon.height() != 256) {
+ icon = icon.scaled(256, 256, Qt::KeepAspectRatio, Qt::SmoothTransformation);
+ }
+ // Remove stale fallback before saving the real icon.
+ QFile::remove(iconDest);
+ if (icon.save(iconDest, "PNG")) {
+ return iconDest;
+ }
}
}
- // Also keep the hicolor copy for compatibility.
- QString hicolorDir = QDir::homePath() + "/.local/share/icons/hicolor/256x256/apps";
- QString hicolorDest = hicolorDir + "/com.fluorine.manager.png";
- if (QFile::exists(hicolorDest)) {
- QFile::copy(hicolorDest, iconDest);
+ // Already have a fallback cached — no need to copy again.
+ if (QFile::exists(iconDest)) {
+ return iconDest;
+ }
+
+ // Install the bundled Fluorine icon as a fallback.
+ QString bundled = bundledFluorineIcon();
+ if (!bundled.isEmpty()) {
+ QFile::copy(bundled, iconDest);
return iconDest;
}
@@ -448,20 +720,17 @@ Shortcut::Shortcut(const Executable& exe) : Shortcut() m_description = QString("Run %1 with Fluorine").arg(exe.title());
- // .exe icons can't be used on Linux — only use native icon formats.
- // Install a game-specific icon to ~/.local/share/icons/fluorine/.
- if (exe.usesOwnIcon()) {
- QString iconPath = exe.binaryInfo().absoluteFilePath();
- if (!iconPath.endsWith(".exe", Qt::CaseInsensitive)) {
- m_icon = iconPath;
- }
+ // Try to extract the icon from the executable (works for .exe files).
+ // For native Linux binaries with a custom icon, use that directly.
+ // Falls back to the bundled Fluorine icon.
+ QString exePath = exe.binaryInfo().absoluteFilePath();
+ if (exe.usesOwnIcon() && !exePath.endsWith(".exe", Qt::CaseInsensitive)) {
+ m_icon = exePath;
}
if (m_icon.isEmpty()) {
- // Build a unique icon name from the instance + executable title,
- // e.g. "NewVegas-NVSE" → ~/.local/share/icons/fluorine/NewVegas-NVSE.png
QString iconBase = sanitizeDesktopName(m_instanceName) + "-" +
sanitizeDesktopName(m_name);
- m_icon = installIcon(iconBase);
+ m_icon = installIcon(iconBase, exePath);
}
m_workingDirectory = QFileInfo(m_target).absolutePath();
|
