diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-03-12 13:48:25 -0500 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-03-12 13:48:25 -0500 |
| commit | 84a43a2e6afbdfab92c610d47aa75e3876ffacd6 (patch) | |
| tree | ea624ec287181905441fbf1247c645976bdb8fc9 /libs/nak/src | |
| parent | 3ba283d9350783c038cc2844450f4938ad125f08 (diff) | |
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 <noreply@anthropic.com>
Diffstat (limited to 'libs/nak/src')
| -rw-r--r-- | libs/nak/src/icons.rs | 195 | ||||
| -rw-r--r-- | libs/nak/src/lib.rs | 1 |
2 files changed, 196 insertions, 0 deletions
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<Vec<u8>> {
+ 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<Vec<u8>, Box<dyn std::error::Error>> {
+ 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<Vec<u8>, Box<dyn std::error::Error>> {
+ 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<Vec<u8>, Box<dyn std::error::Error>> {
+ 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<u8>)> = 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<Vec<u8>, Box<dyn std::error::Error>> {
+ 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<IconEntry> = 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; |
