From 84a43a2e6afbdfab92c610d47aa75e3876ffacd6 Mon Sep 17 00:00:00 2001 From: SulfurNitride Date: Thu, 12 Mar 2026 13:48:25 -0500 Subject: Replace wrestool with native Rust PE icon extraction (pelite) - Add icons.rs to nak crate: parses PE resource sections, extracts RT_GROUP_ICON/RT_ICON and builds ICO files in memory - Expose nak_extract_exe_icon() FFI for C++ consumption - Update iconForExecutable() to use Rust FFI instead of shelling out to wrestool; reads all ICO entries for correct color depth - Fix BasicGamePlugin::gameIcon() to call iconForExecutable() (was returning empty QIcon, causing placeholder icons for BG3/Cyberpunk) - Move app icon setup: setDesktopFileName + setWindowIcon to MOApplication constructor (fixes Wayland taskbar/decoration icon) - Move data/com.fluorine.* to data/icons/, install to XDG paths at launch for Wayland compositor icon resolution - Remove icoutils dependency from Docker image and build output Co-Authored-By: Claude Opus 4.6 --- libs/basic_games_native/src/basicgameplugin.cpp | 4 +- libs/nak/Cargo.lock | 63 ++++++++ libs/nak/Cargo.toml | 1 + libs/nak/src/icons.rs | 195 ++++++++++++++++++++++++ libs/nak/src/lib.rs | 1 + libs/nak_ffi/CMakeLists.txt | 2 + libs/nak_ffi/Cargo.lock | 63 ++++++++ libs/nak_ffi/include/nak_ffi.h | 17 +++ libs/nak_ffi/src/lib.rs | 51 +++++++ libs/uibase/src/CMakeLists.txt | 4 + libs/uibase/src/utility.cpp | 100 ++++-------- 11 files changed, 431 insertions(+), 70 deletions(-) create mode 100644 libs/nak/src/icons.rs (limited to 'libs') diff --git a/libs/basic_games_native/src/basicgameplugin.cpp b/libs/basic_games_native/src/basicgameplugin.cpp index 8c73e4c..042f899 100644 --- a/libs/basic_games_native/src/basicgameplugin.cpp +++ b/libs/basic_games_native/src/basicgameplugin.cpp @@ -1,6 +1,7 @@ #include "basicgameplugin.h" #include "steamutils.h" +#include #include #include @@ -166,7 +167,8 @@ bool BasicGamePlugin::isInstalled() const QIcon BasicGamePlugin::gameIcon() const { - return QIcon(); + return MOBase::iconForExecutable( + gameDirectory().absoluteFilePath(binaryName())); } QDir BasicGamePlugin::gameDirectory() const diff --git a/libs/nak/Cargo.lock b/libs/nak/Cargo.lock index 5bb63b4..03b8ffd 100644 --- a/libs/nak/Cargo.lock +++ b/libs/nak/Cargo.lock @@ -79,6 +79,21 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "dataview" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daba87f72c730b508641c9fb6411fc9bba73939eed2cab611c399500511880d0" +dependencies = [ + "derive_pod", +] + +[[package]] +name = "derive_pod" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2ea6706d74fca54e15f1d40b5cf7fe7f764aaec61352a9fcec58fe27e042fc8" + [[package]] name = "displaydoc" version = "0.2.5" @@ -307,12 +322,19 @@ name = "nak_rust" version = "0.1.0" dependencies = [ "chrono", + "pelite", "serde", "serde_json", "ureq", "walkdir", ] +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" + [[package]] name = "num-traits" version = "0.2.19" @@ -328,6 +350,25 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "pelite" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88dccf4bd32294364aeb7bd55d749604450e9db54605887551f21baea7617685" +dependencies = [ + "dataview", + "libc", + "no-std-compat", + "pelite-macros", + "winapi", +] + +[[package]] +name = "pelite-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a7cf3f8ecebb0f4895f4892a8be0a0dc81b498f9d56735cb769dc31bf00815b" + [[package]] name = "percent-encoding" version = "2.3.2" @@ -655,6 +696,22 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.11" @@ -664,6 +721,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-core" version = "0.62.2" diff --git a/libs/nak/Cargo.toml b/libs/nak/Cargo.toml index 3b3c460..c51a804 100644 --- a/libs/nak/Cargo.toml +++ b/libs/nak/Cargo.toml @@ -13,3 +13,4 @@ serde_json = "1" walkdir = "2" chrono = "0.4" ureq = "2" +pelite = "0.10" diff --git a/libs/nak/src/icons.rs b/libs/nak/src/icons.rs new file mode 100644 index 0000000..940d0d1 --- /dev/null +++ b/libs/nak/src/icons.rs @@ -0,0 +1,195 @@ +//! Extract icons from Windows PE (.exe/.dll) files using pelite. +//! +//! Returns raw ICO file bytes that can be loaded directly by Qt's QIcon/QImage. + +use std::path::Path; + +use pelite::resources::{Entry, Name, Resources}; + +/// Extract the best (largest) icon from a PE executable as raw ICO bytes. +/// +/// Returns `None` if the file can't be read, isn't a valid PE, or has no icons. +pub fn extract_icon(exe_path: &Path) -> Option> { + let data = std::fs::read(exe_path).ok()?; + + // Try PE32+ (64-bit) first, fall back to PE32 (32-bit) + if let Ok(icon) = extract_from_pe64(&data) { + return Some(icon); + } + if let Ok(icon) = extract_from_pe32(&data) { + return Some(icon); + } + + None +} + +fn extract_from_pe64(data: &[u8]) -> Result, Box> { + use pelite::pe64::{Pe, PeFile}; + let pe = PeFile::from_bytes(data)?; + let resources = pe.resources()?; + build_ico_from_resources(&resources) +} + +fn extract_from_pe32(data: &[u8]) -> Result, Box> { + use pelite::pe32::{Pe, PeFile}; + let pe = PeFile::from_bytes(data)?; + let resources = pe.resources()?; + build_ico_from_resources(&resources) +} + +/// Build an ICO file from PE resources, picking the group icon with the +/// largest total pixel area. +fn build_ico_from_resources( + resources: &Resources, +) -> Result, Box> { + let root = resources.root()?; + + // Find RT_GROUP_ICON directory (type 14) + let group_icon_dir = root + .get_dir(Name::Id(14)) + .map_err(|_| "no RT_GROUP_ICON")?; + + let mut best_group: Option<(i32, Vec)> = None; + + for dir_entry in group_icon_dir.entries() { + // Each entry is a group; get its subdirectory (language variants) + let sub_dir = match dir_entry.entry() { + Ok(Entry::Directory(d)) => d, + _ => continue, + }; + + // Get the first language variant's data + let data_entry = match sub_dir.entries().next() { + Some(e) => match e.entry() { + Ok(Entry::DataEntry(d)) => d, + _ => continue, + }, + None => continue, + }; + + let grp_data = match data_entry.bytes() { + Ok(d) => d, + Err(_) => continue, + }; + + // Parse GRPICONDIR header: reserved(2) + type(2) + count(2) = 6 bytes + if grp_data.len() < 6 { + continue; + } + let count = u16::from_le_bytes([grp_data[4], grp_data[5]]) as usize; + // Each GRPICONDIRENTRY is 14 bytes + if grp_data.len() < 6 + count * 14 { + continue; + } + + // Calculate total pixel area for ranking + let mut total_area: i32 = 0; + for i in 0..count { + let offset = 6 + i * 14; + let w = if grp_data[offset] == 0 { + 256i32 + } else { + grp_data[offset] as i32 + }; + let h = if grp_data[offset + 1] == 0 { + 256i32 + } else { + grp_data[offset + 1] as i32 + }; + total_area += w * h; + } + + if let Ok(ico_bytes) = build_ico_file(resources, grp_data, count) { + if best_group.is_none() || total_area > best_group.as_ref().unwrap().0 { + best_group = Some((total_area, ico_bytes)); + } + } + } + + best_group + .map(|(_, bytes)| bytes) + .ok_or_else(|| "no valid icon group found".into()) +} + +/// Build a complete ICO file from a GRPICONDIR and the corresponding RT_ICON resources. +fn build_ico_file( + resources: &Resources, + grp_data: &[u8], + count: usize, +) -> Result, Box> { + let root = resources.root()?; + + // RT_ICON = type 3 + let icon_dir = root.get_dir(Name::Id(3)).map_err(|_| "no RT_ICON")?; + + // Collect individual icon image data + struct IconEntry<'a> { + header: [u8; 8], // first 8 bytes of GRPICONDIRENTRY (w, h, colors, reserved, planes, bpp) + data: &'a [u8], + } + let mut entries: Vec = Vec::new(); + + for i in 0..count { + let grp_offset = 6 + i * 14; + let icon_id = + u16::from_le_bytes([grp_data[grp_offset + 12], grp_data[grp_offset + 13]]) as u32; + + // Find the RT_ICON with this ID + let icon_sub = match icon_dir.get_dir(Name::Id(icon_id)) { + Ok(d) => d, + Err(_) => continue, + }; + + let data_entry = match icon_sub.entries().next() { + Some(e) => match e.entry() { + Ok(Entry::DataEntry(d)) => d, + _ => continue, + }, + None => continue, + }; + + let image_data = match data_entry.bytes() { + Ok(d) => d, + Err(_) => continue, + }; + + let mut header = [0u8; 8]; + header.copy_from_slice(&grp_data[grp_offset..grp_offset + 8]); + + entries.push(IconEntry { + header, + data: image_data, + }); + } + + if entries.is_empty() { + return Err("no icon entries found".into()); + } + + let entry_count = entries.len() as u16; + let header_size = 6 + (entry_count as usize) * 16; // ICONDIR + ICONDIRENTRYs + + let mut ico = Vec::new(); + + // ICONDIR header + ico.extend_from_slice(&0u16.to_le_bytes()); // reserved + ico.extend_from_slice(&1u16.to_le_bytes()); // type = ICO + ico.extend_from_slice(&entry_count.to_le_bytes()); + + // Write ICONDIRENTRY records (16 bytes each) + let mut data_offset = header_size as u32; + for entry in &entries { + ico.extend_from_slice(&entry.header); // width, height, colors, reserved, planes, bpp (8 bytes) + let size = entry.data.len() as u32; + ico.extend_from_slice(&size.to_le_bytes()); // dwBytesInRes (4 bytes) + ico.extend_from_slice(&data_offset.to_le_bytes()); // dwImageOffset (4 bytes) + data_offset += size; + } + + // Write image data + for entry in &entries { + ico.extend_from_slice(entry.data); + } + + Ok(ico) +} diff --git a/libs/nak/src/lib.rs b/libs/nak/src/lib.rs index 59f41c6..13bc90e 100644 --- a/libs/nak/src/lib.rs +++ b/libs/nak/src/lib.rs @@ -13,4 +13,5 @@ pub mod steam; pub mod utils; pub mod deps; +pub mod icons; pub mod installers; diff --git a/libs/nak_ffi/CMakeLists.txt b/libs/nak_ffi/CMakeLists.txt index 6fde501..bdfac9a 100644 --- a/libs/nak_ffi/CMakeLists.txt +++ b/libs/nak_ffi/CMakeLists.txt @@ -18,6 +18,8 @@ add_custom_command( WORKING_DIRECTORY ${NAK_FFI_DIR} COMMENT "Building NaK FFI library (Rust)" DEPENDS ${NAK_FFI_DIR}/Cargo.toml ${NAK_FFI_DIR}/src/lib.rs + ${NAK_FFI_DIR}/../nak/Cargo.toml ${NAK_FFI_DIR}/../nak/src/lib.rs + ${NAK_FFI_DIR}/../nak/src/icons.rs ) add_custom_target(nak_ffi_build DEPENDS ${NAK_FFI_LIB}) diff --git a/libs/nak_ffi/Cargo.lock b/libs/nak_ffi/Cargo.lock index 55ab8db..e8be402 100644 --- a/libs/nak_ffi/Cargo.lock +++ b/libs/nak_ffi/Cargo.lock @@ -79,6 +79,21 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "dataview" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daba87f72c730b508641c9fb6411fc9bba73939eed2cab611c399500511880d0" +dependencies = [ + "derive_pod", +] + +[[package]] +name = "derive_pod" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2ea6706d74fca54e15f1d40b5cf7fe7f764aaec61352a9fcec58fe27e042fc8" + [[package]] name = "displaydoc" version = "0.2.5" @@ -314,12 +329,19 @@ name = "nak_rust" version = "0.1.0" dependencies = [ "chrono", + "pelite", "serde", "serde_json", "ureq", "walkdir", ] +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" + [[package]] name = "num-traits" version = "0.2.19" @@ -335,6 +357,25 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "pelite" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88dccf4bd32294364aeb7bd55d749604450e9db54605887551f21baea7617685" +dependencies = [ + "dataview", + "libc", + "no-std-compat", + "pelite-macros", + "winapi", +] + +[[package]] +name = "pelite-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a7cf3f8ecebb0f4895f4892a8be0a0dc81b498f9d56735cb769dc31bf00815b" + [[package]] name = "percent-encoding" version = "2.3.2" @@ -662,6 +703,22 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.11" @@ -671,6 +728,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-core" version = "0.62.2" diff --git a/libs/nak_ffi/include/nak_ffi.h b/libs/nak_ffi/include/nak_ffi.h index cc66d9e..e1a9185 100644 --- a/libs/nak_ffi/include/nak_ffi.h +++ b/libs/nak_ffi/include/nak_ffi.h @@ -175,6 +175,23 @@ char *nak_ensure_dxvk_conf(void); * Returns newly allocated string (free with nak_string_free). */ char *nak_get_dxvk_conf_path(void); +/* ======================================================================== + * Tier 8: PE Icon Extraction + * ======================================================================== */ + +/** Result of icon extraction */ +typedef struct { + uint8_t *data; /**< Raw ICO file bytes (NULL if extraction failed) */ + size_t len; /**< Length in bytes (0 if extraction failed) */ +} NakIconData; + +/** Extract the best icon from a Windows PE executable (.exe/.dll). + * Returns raw ICO bytes. Free with nak_icon_data_free(). */ +NakIconData nak_extract_exe_icon(const char *exe_path); + +/** Free icon data returned by nak_extract_exe_icon */ +void nak_icon_data_free(NakIconData icon); + /* ======================================================================== * General * ======================================================================== */ diff --git a/libs/nak_ffi/src/lib.rs b/libs/nak_ffi/src/lib.rs index 65592fa..c45481b 100644 --- a/libs/nak_ffi/src/lib.rs +++ b/libs/nak_ffi/src/lib.rs @@ -614,6 +614,57 @@ pub extern "C" fn nak_get_dxvk_conf_path() -> *mut c_char { to_cstring(&path.to_string_lossy()) } +// ============================================================================ +// Tier 8: PE Icon Extraction +// ============================================================================ + +/// Result of icon extraction +#[repr(C)] +pub struct NakIconData { + /// Raw ICO file bytes (caller must free with nak_icon_data_free) + pub data: *mut u8, + /// Length of the data in bytes (0 if extraction failed) + pub len: usize, +} + +/// Extract the best icon from a Windows PE executable (.exe/.dll). +/// +/// Returns NakIconData with the raw ICO file bytes. +/// If extraction fails, data is null and len is 0. +/// Caller must free the result with nak_icon_data_free(). +#[no_mangle] +pub unsafe extern "C" fn nak_extract_exe_icon(exe_path: *const c_char) -> NakIconData { + let path_str = unsafe { from_cstr(exe_path) }; + if path_str.is_empty() { + return NakIconData { + data: ptr::null_mut(), + len: 0, + }; + } + + match nak_rust::icons::extract_icon(std::path::Path::new(path_str)) { + Some(bytes) => { + let len = bytes.len(); + let mut boxed = bytes.into_boxed_slice(); + let ptr = boxed.as_mut_ptr(); + std::mem::forget(boxed); + NakIconData { data: ptr, len } + } + None => NakIconData { + data: ptr::null_mut(), + len: 0, + }, + } +} + +/// Free icon data returned by nak_extract_exe_icon +#[no_mangle] +pub unsafe extern "C" fn nak_icon_data_free(icon: NakIconData) { + if !icon.data.is_null() && icon.len > 0 { + let _ = unsafe { Vec::from_raw_parts(icon.data, icon.len, icon.len) }; + } +} + // ============================================================================ // General: String free // ============================================================================ diff --git a/libs/uibase/src/CMakeLists.txt b/libs/uibase/src/CMakeLists.txt index fff3ac3..3015be3 100644 --- a/libs/uibase/src/CMakeLists.txt +++ b/libs/uibase/src/CMakeLists.txt @@ -187,6 +187,10 @@ target_link_libraries(uibase PUBLIC Qt6::Widgets Qt6::Network Qt6::QuickWidgets PRIVATE spdlog::spdlog_header_only Qt6::Qml Qt6::Quick) +if(NOT WIN32 AND TARGET mo2::nak_ffi) + target_link_libraries(uibase PRIVATE mo2::nak_ffi) +endif() + # installation install(TARGETS uibase EXPORT uibaseTargets LIBRARY DESTINATION lib diff --git a/libs/uibase/src/utility.cpp b/libs/uibase/src/utility.cpp index 33f227d..e6811af 100644 --- a/libs/uibase/src/utility.cpp +++ b/libs/uibase/src/utility.cpp @@ -21,6 +21,9 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA #include #include #include +#ifndef _WIN32 +#include +#endif #include #include #include @@ -943,80 +946,39 @@ QIcon iconForExecutable(const QString& filePath) cache.insert(cacheKey, icon); return icon; #else - // Try to extract icon resources from PE executables via icoutils/wrestool. - // If this fails, return a generic executable icon. + // Extract icon from PE executable via NaK (pelite-based, no external tools). QIcon icon; - const QString wrestool = QStandardPaths::findExecutable("wrestool"); - if (!wrestool.isEmpty()) { - const QString cacheRoot = - QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + - "/exe_icons"; - QDir().mkpath(cacheRoot); - - const QString outDirPath = cacheRoot + "/" + QString::number(qHash(cacheKey), 16); - QDir outDir(outDirPath); - if (!outDir.exists()) { - QDir().mkpath(outDirPath); - } - - auto findBestIcon = [&](const QString& dirPath) -> QIcon { - QString bestFile; - int bestArea = -1; - - QDirIterator it(dirPath, - QStringList() << "*.png" - << "*.ico" - << "*.bmp", - QDir::Files, QDirIterator::Subdirectories); - while (it.hasNext()) { - const QString path = it.next(); - QImageReader reader(path); - const QSize size = reader.size(); - const int area = size.width() * size.height(); - if (area > bestArea) { - bestArea = area; - bestFile = path; - } else if (bestArea < 0) { - // Unknown image size (e.g. multi-size ico); keep first viable file. - bestFile = path; - } - } - - if (!bestFile.isEmpty()) { - QIcon candidate(bestFile); - if (!candidate.pixmap(24, 24).isNull() || !candidate.pixmap(32, 32).isNull()) { - return candidate; - } - } - - return QIcon(); - }; - - // Reuse previous extraction if present. - icon = findBestIcon(outDirPath); - - if (icon.isNull()) { - const QList tries = { - {"-x", "--type=group_icon", "-o", outDirPath, fi.absoluteFilePath()}, - {"-x", "--type=14", "-o", outDirPath, fi.absoluteFilePath()}, - {"-x", "-t14", "-o", outDirPath, fi.absoluteFilePath()}, - }; - - for (const auto& args : tries) { - QProcess p; - p.start(wrestool, args); - if (!p.waitForFinished(4000)) { - p.kill(); - continue; - } - if (p.exitStatus() == QProcess::NormalExit && p.exitCode() == 0) { - icon = findBestIcon(outDirPath); - if (!icon.isNull()) { - break; + { + const QByteArray pathUtf8 = fi.absoluteFilePath().toUtf8(); + NakIconData icoData = nak_extract_exe_icon(pathUtf8.constData()); + if (icoData.data && icoData.len > 0) { + QByteArray ba(reinterpret_cast(icoData.data), + static_cast(icoData.len)); + QBuffer buf(&ba); + buf.open(QIODevice::ReadOnly); + QImageReader reader(&buf, "ico"); + + // ICO files contain multiple sizes/depths — read all and add to QIcon + // so Qt picks the best one for each requested size. + if (reader.canRead()) { + const int imageCount = reader.imageCount(); + if (imageCount > 1) { + for (int i = 0; i < imageCount; ++i) { + reader.jumpToImage(i); + QImage img = reader.read(); + if (!img.isNull()) { + icon.addPixmap(QPixmap::fromImage(img)); + } + } + } else { + QImage img = reader.read(); + if (!img.isNull()) { + icon = QIcon(QPixmap::fromImage(img)); } } } + nak_icon_data_free(icoData); } } -- cgit v1.3.1