Skip to content

Commit 78ef173

Browse files
Improve Windows app logo quality (#949)
* wip * Improve Windows icon extraction logic Switches from ExtractIconExW to PrivateExtractIconsW for better icon quality and size selection. Refactors icon extraction and conversion routines to prioritize higher resolution icons and optimize PNG output, improving reliability and visual fidelity. --------- Co-authored-by: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com>
1 parent 8e7e5d9 commit 78ef173

File tree

2 files changed

+168
-46
lines changed

2 files changed

+168
-46
lines changed

crates/scap-targets/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ windows = { workspace = true, features = [
2424
"Graphics_Capture",
2525
"Win32_Foundation",
2626
"Win32_System_Threading",
27+
"Win32_System_WinRT",
28+
"Win32_System_WinRT_Graphics",
29+
"Win32_System_WinRT_Graphics_Capture",
2730
"Win32_UI_WindowsAndMessaging",
2831
"Win32_UI_Shell",
2932
"Win32_UI_HiDpi",

crates/scap-targets/src/platform/win.rs

Lines changed: 165 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ use windows::{
1616
MONITORINFOEXW, MonitorFromPoint, MonitorFromWindow, ReleaseDC, SelectObject,
1717
},
1818
},
19-
Storage::FileSystem::{GetFileVersionInfoSizeW, GetFileVersionInfoW, VerQueryValueW},
19+
Storage::FileSystem::{
20+
FILE_FLAGS_AND_ATTRIBUTES, GetFileVersionInfoSizeW, GetFileVersionInfoW, VerQueryValueW,
21+
},
2022
System::{
2123
Threading::{
2224
GetCurrentProcessId, OpenProcess, PROCESS_NAME_FORMAT,
@@ -29,15 +31,19 @@ use windows::{
2931
GetDpiForMonitor, GetDpiForWindow, GetProcessDpiAwareness, MDT_EFFECTIVE_DPI,
3032
PROCESS_PER_MONITOR_DPI_AWARE,
3133
},
32-
Shell::ExtractIconExW,
34+
Shell::{
35+
ExtractIconExW, SHFILEINFOW, SHGFI_ICON, SHGFI_LARGEICON, SHGFI_SMALLICON,
36+
SHGetFileInfoW,
37+
},
3338
WindowsAndMessaging::{
3439
DI_FLAGS, DestroyIcon, DrawIconEx, EnumChildWindows, EnumWindows, GCLP_HICON,
3540
GW_HWNDNEXT, GWL_EXSTYLE, GWL_STYLE, GetClassLongPtrW, GetClassNameW,
3641
GetClientRect, GetCursorPos, GetDesktopWindow, GetIconInfo,
3742
GetLayeredWindowAttributes, GetWindow, GetWindowLongPtrW, GetWindowLongW,
3843
GetWindowRect, GetWindowTextLengthW, GetWindowTextW, GetWindowThreadProcessId,
39-
HICON, ICONINFO, IsIconic, IsWindowVisible, SendMessageW, WM_GETICON, WS_CHILD,
40-
WS_EX_LAYERED, WS_EX_TOOLWINDOW, WS_EX_TOPMOST, WS_EX_TRANSPARENT, WindowFromPoint,
44+
HICON, ICONINFO, IsIconic, IsWindowVisible, PrivateExtractIconsW, SendMessageW,
45+
WM_GETICON, WS_CHILD, WS_EX_LAYERED, WS_EX_TOOLWINDOW, WS_EX_TOPMOST,
46+
WS_EX_TRANSPARENT, WindowFromPoint,
4147
},
4248
},
4349
},
@@ -525,9 +531,23 @@ impl WindowImpl {
525531
pub fn app_icon(&self) -> Option<Vec<u8>> {
526532
unsafe {
527533
// Target size for acceptable icon quality - early termination threshold
528-
const GOOD_SIZE_THRESHOLD: i32 = 64;
534+
const GOOD_SIZE_THRESHOLD: i32 = 256;
535+
536+
// Method 1: Try shell icon extraction for highest quality
537+
if let Some(exe_path) = self.get_executable_path() {
538+
if let Some(icon_data) = self.extract_shell_icon_high_res(&exe_path, 512) {
539+
return Some(icon_data);
540+
}
541+
}
529542

530-
// Method 1: Try to get the window's large icon first
543+
// Method 2: Try executable file extraction with multiple icon sizes
544+
if let Some(exe_path) = self.get_executable_path() {
545+
if let Some(icon_data) = self.extract_executable_icons_high_res(&exe_path) {
546+
return Some(icon_data);
547+
}
548+
}
549+
550+
// Method 3: Try to get the window's large icon
531551
let large_icon = SendMessageW(
532552
self.0,
533553
WM_GETICON,
@@ -544,7 +564,7 @@ impl WindowImpl {
544564
}
545565
}
546566

547-
// Method 2: Try executable file extraction (only first icon, most likely to be main app icon)
567+
// Method 4: Try executable file extraction (fallback to original method)
548568
if let Some(exe_path) = self.get_executable_path() {
549569
let wide_path: Vec<u16> =
550570
exe_path.encode_utf16().chain(std::iter::once(0)).collect();
@@ -587,7 +607,7 @@ impl WindowImpl {
587607
}
588608
}
589609

590-
// Method 3: Try small window icon as fallback
610+
// Method 5: Try small window icon as fallback
591611
let small_icon = SendMessageW(
592612
self.0,
593613
WM_GETICON,
@@ -601,7 +621,7 @@ impl WindowImpl {
601621
}
602622
}
603623

604-
// Method 4: Try class icon as last resort
624+
// Method 6: Try class icon as last resort
605625
let class_icon = GetClassLongPtrW(self.0, GCLP_HICON) as isize;
606626
if class_icon != 0 {
607627
if let Some(result) = self.hicon_to_png_bytes_optimized(HICON(class_icon as _)) {
@@ -613,6 +633,107 @@ impl WindowImpl {
613633
}
614634
}
615635

636+
fn extract_shell_icon_high_res(&self, exe_path: &str, target_size: i32) -> Option<Vec<u8>> {
637+
unsafe {
638+
let wide_path: Vec<u16> = exe_path.encode_utf16().chain(std::iter::once(0)).collect();
639+
640+
// Try different shell icon sizes
641+
let icon_flags = [
642+
SHGFI_ICON | SHGFI_LARGEICON, // Large system icon
643+
SHGFI_ICON | SHGFI_SMALLICON, // Small system icon as fallback
644+
];
645+
646+
for flags in icon_flags {
647+
let mut file_info = SHFILEINFOW::default();
648+
let result = SHGetFileInfoW(
649+
windows::core::PCWSTR(wide_path.as_ptr()),
650+
FILE_FLAGS_AND_ATTRIBUTES(0),
651+
Some(&mut file_info),
652+
std::mem::size_of::<SHFILEINFOW>() as u32,
653+
flags,
654+
);
655+
656+
if result != 0 && !file_info.hIcon.is_invalid() {
657+
if let Some(result) = self.hicon_to_png_bytes_optimized(file_info.hIcon) {
658+
let _ = DestroyIcon(file_info.hIcon);
659+
if result.1 >= target_size / 2 {
660+
// Accept if at least half target size
661+
return Some(result.0);
662+
}
663+
}
664+
let _ = DestroyIcon(file_info.hIcon);
665+
}
666+
}
667+
668+
None
669+
}
670+
}
671+
672+
fn extract_executable_icons_high_res(&self, exe_path: &str) -> Option<Vec<u8>> {
673+
unsafe {
674+
let wide_path: Vec<u16> = exe_path.encode_utf16().chain(std::iter::once(0)).collect();
675+
676+
let mut path_buffer = [0u16; 260];
677+
let copy_len = wide_path.len().min(path_buffer.len());
678+
path_buffer[..copy_len].copy_from_slice(&wide_path[..copy_len]);
679+
680+
let icon_count = ExtractIconExW(PCWSTR(wide_path.as_ptr()), -1, None, None, 0);
681+
682+
let total_icons = if icon_count > 0 {
683+
icon_count as usize
684+
} else {
685+
1
686+
};
687+
688+
let max_icons_to_try = total_icons.min(8);
689+
let size_candidates: [i32; 12] = [512, 400, 256, 192, 128, 96, 72, 64, 48, 32, 24, 16];
690+
691+
let mut best_icon: Option<Vec<u8>> = None;
692+
let mut best_size: i32 = 0;
693+
694+
for &size in &size_candidates {
695+
for index in 0..max_icons_to_try {
696+
let mut icon_slot = [HICON::default(); 1];
697+
698+
let extracted = PrivateExtractIconsW(
699+
&path_buffer,
700+
index as i32,
701+
size,
702+
size,
703+
Some(&mut icon_slot),
704+
None,
705+
0,
706+
);
707+
708+
if extracted == 0 {
709+
continue;
710+
}
711+
712+
let icon_handle = icon_slot[0];
713+
if icon_handle.is_invalid() {
714+
continue;
715+
}
716+
717+
let icon_result = self.hicon_to_png_bytes_optimized(icon_handle);
718+
let _ = DestroyIcon(icon_handle);
719+
720+
if let Some((png_data, realized_size)) = icon_result {
721+
if realized_size > best_size {
722+
best_size = realized_size;
723+
best_icon = Some(png_data);
724+
725+
if best_size >= 256 {
726+
return best_icon;
727+
}
728+
}
729+
}
730+
}
731+
}
732+
733+
best_icon
734+
}
735+
}
736+
616737
fn get_executable_path(&self) -> Option<String> {
617738
unsafe {
618739
let mut process_id = 0u32;
@@ -674,50 +795,57 @@ impl WindowImpl {
674795

675796
fn hicon_to_png_bytes_optimized(&self, icon: HICON) -> Option<(Vec<u8>, i32)> {
676797
unsafe {
677-
// Get icon info to determine actual size
678798
let mut icon_info = ICONINFO::default();
679799
if !GetIconInfo(icon, &mut icon_info).is_ok() {
680800
return None;
681801
}
682802

683-
// Get device context
684803
let screen_dc = GetDC(Some(HWND::default()));
685804
let mem_dc = CreateCompatibleDC(Some(screen_dc));
686805

687-
// Get the native icon size to prioritize it
688806
let native_size = self.get_icon_size(icon);
689-
690-
// Determine the best size to try based on native size
691-
let target_sizes = if let Some((width, height)) = native_size {
807+
let target_sizes: Vec<i32> = if let Some((width, height)) = native_size {
692808
let native_dim = width.max(height);
693-
if native_dim >= 256 {
694-
vec![native_dim, 256, 128] // High-res icon
695-
} else if native_dim >= 64 {
696-
vec![native_dim, 64, 32] // Medium-res icon
697-
} else if native_dim >= 32 {
698-
vec![native_dim, 32, 16] // Standard icon
809+
if native_dim > 0 {
810+
let mut sizes = Vec::with_capacity(10);
811+
sizes.push(native_dim);
812+
for &candidate in &[256, 192, 128, 96, 64, 48, 32, 24, 16] {
813+
if candidate > 0 && candidate < native_dim {
814+
sizes.push(candidate);
815+
}
816+
}
817+
if sizes.is_empty() {
818+
vec![native_dim]
819+
} else {
820+
sizes
821+
}
699822
} else {
700-
vec![32, 16] // Small icon, try standard sizes
823+
vec![256, 192, 128, 96, 64, 48, 32, 24, 16]
701824
}
702825
} else {
703-
// No native size info, try reasonable defaults
704-
vec![128, 64, 32, 16]
826+
vec![512, 256, 192, 128, 96, 64, 48, 32, 24, 16]
705827
};
706828

707-
// Try each target size, return the first successful one
708-
for &size in &target_sizes {
709-
if let Some(result) = self.try_convert_icon_to_png(icon, size, screen_dc, mem_dc) {
710-
// Cleanup
829+
let mut deduped = Vec::new();
830+
for size in target_sizes.into_iter() {
831+
if !deduped.contains(&size) {
832+
deduped.push(size);
833+
}
834+
}
835+
836+
for size in deduped.into_iter().filter(|size| *size > 0) {
837+
if let Some((png_data, realized_size)) =
838+
self.try_convert_icon_to_png(icon, size, screen_dc, mem_dc)
839+
{
711840
let _ = DeleteDC(mem_dc);
712841
let _ = ReleaseDC(Some(HWND::default()), screen_dc);
713842
let _ = DeleteObject(icon_info.hbmColor.into());
714843
let _ = DeleteObject(icon_info.hbmMask.into());
715844

716-
return Some((result, size));
845+
return Some((png_data, realized_size));
717846
}
718847
}
719848

720-
// Cleanup
721849
let _ = DeleteDC(mem_dc);
722850
let _ = ReleaseDC(Some(HWND::default()), screen_dc);
723851
let _ = DeleteObject(icon_info.hbmColor.into());
@@ -733,19 +861,18 @@ impl WindowImpl {
733861
size: i32,
734862
screen_dc: HDC,
735863
mem_dc: HDC,
736-
) -> Option<Vec<u8>> {
864+
) -> Option<(Vec<u8>, i32)> {
737865
unsafe {
738866
let width = size;
739867
let height = size;
740868

741-
// Create bitmap info for this size
742869
let mut bitmap_info = BITMAPINFO {
743870
bmiHeader: BITMAPINFOHEADER {
744871
biSize: mem::size_of::<BITMAPINFOHEADER>() as u32,
745872
biWidth: width,
746-
biHeight: -height, // Top-down DIB
873+
biHeight: -height,
747874
biPlanes: 1,
748-
biBitCount: 32, // 32 bits per pixel (BGRA)
875+
biBitCount: 32,
749876
biCompression: BI_RGB.0,
750877
biSizeImage: 0,
751878
biXPelsPerMeter: 0,
@@ -756,15 +883,13 @@ impl WindowImpl {
756883
bmiColors: [Default::default(); 1],
757884
};
758885

759-
// Create a bitmap
760886
let bitmap = CreateCompatibleBitmap(screen_dc, width, height);
761887
if bitmap.is_invalid() {
762888
return None;
763889
}
764890

765891
let old_bitmap = SelectObject(mem_dc, bitmap.into());
766892

767-
// Fill with transparent background
768893
let brush = CreateSolidBrush(windows::Win32::Foundation::COLORREF(0));
769894
let rect = RECT {
770895
left: 0,
@@ -775,7 +900,6 @@ impl WindowImpl {
775900
let _ = FillRect(mem_dc, &rect, brush);
776901
let _ = DeleteObject(brush.into());
777902

778-
// Draw the icon onto the bitmap with proper scaling
779903
let draw_result = DrawIconEx(
780904
mem_dc,
781905
0,
@@ -785,13 +909,12 @@ impl WindowImpl {
785909
height,
786910
0,
787911
Some(HBRUSH::default()),
788-
DI_FLAGS(0x0003), // DI_NORMAL
912+
DI_FLAGS(0x0003),
789913
);
790914

791-
let mut result = None;
915+
let mut result: Option<(Vec<u8>, i32)> = None;
792916

793917
if draw_result.is_ok() {
794-
// Get bitmap bits
795918
let mut buffer = vec![0u8; (width * height * 4) as usize];
796919
let get_bits_result = GetDIBits(
797920
mem_dc,
@@ -804,16 +927,13 @@ impl WindowImpl {
804927
);
805928

806929
if get_bits_result > 0 {
807-
// Check if we have any non-transparent pixels
808930
let has_content = buffer.chunks_exact(4).any(|chunk| chunk[3] != 0);
809931

810932
if has_content {
811-
// Convert BGRA to RGBA
812933
for chunk in buffer.chunks_exact_mut(4) {
813-
chunk.swap(0, 2); // Swap B and R
934+
chunk.swap(0, 2);
814935
}
815936

816-
// Create PNG using the image crate
817937
if let Some(img) =
818938
image::RgbaImage::from_raw(width as u32, height as u32, buffer)
819939
{
@@ -825,14 +945,13 @@ impl WindowImpl {
825945
)
826946
.is_ok()
827947
{
828-
result = Some(png_data);
948+
result = Some((png_data, width));
829949
}
830950
}
831951
}
832952
}
833953
}
834954

835-
// Cleanup for this iteration
836955
let _ = SelectObject(mem_dc, old_bitmap);
837956
let _ = DeleteObject(bitmap.into());
838957

0 commit comments

Comments
 (0)