How to convert HICON to png?

hello everyone.

I want to extract the icon in the application and convert it to png format.

At present, I can get HICON through the SHGetFileInfoW method of winapi

And get ICONINFO from HICON through GetIconInfo, but I don't know how to continue to convert to png format.

Please give me some advice, thank you!

Do you read your HICON from an .ico file? I don't know Windows, sorry. But if that is the case, you can use the image crate for converting a .ico file to .png, as it supports both formats.

Hello, I didn't extract the HICON from .ico file, but extracted the HICON from exe or dll through SHGetFileInfoW

anybody konws how to do it?

Probably using DrawIcon with a drawing context that lets you get the image data.

You can use GetIconInfo() to get the HBITMAP for the icon, and potentially it's mask (iirc only if it's paletted)

You can then use GetDIBits() to get the raw bits and metadata of the HBITMAP(s).

With that raw data, you can probably most easily use the image crate's flat module with a bit of work to get an image you can then easily save as PNG as in the module example.

Here's a complete example of using GetIconInfo() and GetDIBits() to extract the image data as an RgbaImage that can be saved as a PNG file:

/*
[dependencies]
image = "0.24.5"

[dependencies.windows]
version = "0.46.0"
features = [
    "Win32_Foundation",
    "Win32_Graphics_Gdi",
    "Win32_UI_WindowsAndMessaging",
]
*/

use image::RgbaImage;
use std::{
    mem::{self, MaybeUninit},
    ptr::addr_of_mut,
};
use windows::Win32::{
    Foundation::HWND,
    Graphics::Gdi::{
        DeleteObject, GetDC, GetDIBits, GetObjectW, ReleaseDC, BITMAP, BITMAPINFOHEADER, BI_RGB,
        DIB_RGB_COLORS, HDC,
    },
    UI::WindowsAndMessaging::{GetIconInfo, HICON},
};

unsafe fn icon_to_image(icon: HICON) -> RgbaImage {
    let bitmap_size_i32 = i32::try_from(mem::size_of::<BITMAP>()).unwrap();
    let biheader_size_u32 = u32::try_from(mem::size_of::<BITMAPINFOHEADER>()).unwrap();

    let mut info = MaybeUninit::uninit();
    GetIconInfo(icon, info.as_mut_ptr()).unwrap();
    let info = info.assume_init_ref();
    DeleteObject(info.hbmMask).unwrap();

    let mut bitmap: MaybeUninit<BITMAP> = MaybeUninit::uninit();
    let result = GetObjectW(
        info.hbmColor,
        bitmap_size_i32,
        Some(bitmap.as_mut_ptr().cast()),
    );
    assert!(result == bitmap_size_i32);
    let bitmap = bitmap.assume_init_ref();

    let width_u32 = u32::try_from(bitmap.bmWidth).unwrap();
    let height_u32 = u32::try_from(bitmap.bmHeight).unwrap();
    let width_usize = usize::try_from(bitmap.bmWidth).unwrap();
    let height_usize = usize::try_from(bitmap.bmHeight).unwrap();
    let buf_size = width_usize
        .checked_mul(height_usize)
        .and_then(|size| size.checked_mul(4))
        .unwrap();
    let mut buf: Vec<u8> = Vec::with_capacity(buf_size);

    let dc = GetDC(HWND(0));
    assert!(dc != HDC(0));

    let mut bitmap_info = BITMAPINFOHEADER {
        biSize: biheader_size_u32,
        biWidth: bitmap.bmWidth,
        biHeight: -bitmap.bmHeight,
        biPlanes: 1,
        biBitCount: 32,
        biCompression: BI_RGB,
        biSizeImage: 0,
        biXPelsPerMeter: 0,
        biYPelsPerMeter: 0,
        biClrUsed: 0,
        biClrImportant: 0,
    };
    let result = GetDIBits(
        dc,
        info.hbmColor,
        0,
        height_u32,
        Some(buf.as_mut_ptr().cast()),
        addr_of_mut!(bitmap_info).cast(),
        DIB_RGB_COLORS,
    );
    assert!(result == bitmap.bmHeight);
    buf.set_len(buf.capacity());

    let result = ReleaseDC(HWND(0), dc);
    assert!(result == 1);
    DeleteObject(info.hbmColor).unwrap();

    for chunk in buf.chunks_exact_mut(4) {
        let [b, _, r, _] = chunk else { unreachable!() };
        mem::swap(b, r);
    }

    RgbaImage::from_vec(width_u32, height_u32, buf).unwrap()
}

// testing:

use windows::{
    core::PCWSTR,
    Win32::{
        Foundation::HINSTANCE,
        UI::WindowsAndMessaging::{DestroyIcon, LoadImageW, IMAGE_ICON, LR_DEFAULTCOLOR, OIC_NOTE},
    },
};

fn main() {
    unsafe {
        let icon = LoadImageW(
            HINSTANCE(0),
            PCWSTR::from_raw(OIC_NOTE as *const u16),
            IMAGE_ICON,
            0,
            0,
            LR_DEFAULTCOLOR,
        )
        .unwrap();
        let icon = HICON(icon.0);
        let image = icon_to_image(icon);
        DestroyIcon(icon).unwrap();
        image.save("example.png").unwrap();
    }
}

(I do assume here that the hbmColor bitmap of an HICON is always a DDB compatible with the screen; I do not see any way for it to be a DIB, or to be compatible with some other DC.)

1 Like

Thank you for your suggestion. I successfully saved the image, but the image has been displaced. Do you know how to solve this problem?
It seems that the left and right parts of the picture have exchanged
1680144072583

That's interpreting the header as part of the image data (see the garbage on the start of the first line), pushing everything to the right and wrapping to the next line.

That implies you're somehow passing the wrong pointer to image for it to interpret as an image? From what I can tell, you should be able to pass the lpvBits output directly, but I might be missing something...

1 Like

Hmm, I just noticed that it might be illegal to use a Vec<u8> pointer for lpvBits:

If the lpvBits parameter is a valid pointer, the first six members of the BITMAPINFOHEADER structure must be initialized to specify the size and format of the DIB. The scan lines must be aligned on a DWORD except for RLE compressed bitmaps.

And indeed, the image you're getting appears to be preceded by 13 bytes of garbage. Could you see if this modified version has the same issue?

unsafe fn icon_to_image(icon: HICON) -> RgbaImage {
    let bitmap_size_i32 = i32::try_from(mem::size_of::<BITMAP>()).unwrap();
    let biheader_size_u32 = u32::try_from(mem::size_of::<BITMAPINFOHEADER>()).unwrap();

    let mut info = MaybeUninit::uninit();
    GetIconInfo(icon, info.as_mut_ptr()).unwrap();
    let info = info.assume_init_ref();
    DeleteObject(info.hbmMask).unwrap();

    let mut bitmap: MaybeUninit<BITMAP> = MaybeUninit::uninit();
    let result = GetObjectW(
        info.hbmColor,
        bitmap_size_i32,
        Some(bitmap.as_mut_ptr().cast()),
    );
    assert!(result == bitmap_size_i32);
    let bitmap = bitmap.assume_init_ref();

    let width_u32 = u32::try_from(bitmap.bmWidth).unwrap();
    let height_u32 = u32::try_from(bitmap.bmHeight).unwrap();
    let width_usize = usize::try_from(bitmap.bmWidth).unwrap();
    let height_usize = usize::try_from(bitmap.bmHeight).unwrap();
    let buf_size = width_usize.checked_mul(height_usize).unwrap();
    let mut buf: Vec<u32> = Vec::with_capacity(buf_size);

    let dc = GetDC(HWND(0));
    assert!(dc != HDC(0));

    let mut bitmap_info = BITMAPINFOHEADER {
        biSize: biheader_size_u32,
        biWidth: bitmap.bmWidth,
        biHeight: -bitmap.bmHeight,
        biPlanes: 1,
        biBitCount: 32,
        biCompression: BI_RGB,
        biSizeImage: 0,
        biXPelsPerMeter: 0,
        biYPelsPerMeter: 0,
        biClrUsed: 0,
        biClrImportant: 0,
    };
    let result = GetDIBits(
        dc,
        info.hbmColor,
        0,
        height_u32,
        Some(buf.as_mut_ptr().cast()),
        addr_of_mut!(bitmap_info).cast(),
        DIB_RGB_COLORS,
    );
    assert!(result == bitmap.bmHeight);
    buf.set_len(buf.capacity());

    let result = ReleaseDC(HWND(0), dc);
    assert!(result == 1);
    DeleteObject(info.hbmColor).unwrap();

    RgbaImage::from_fn(width_u32, height_u32, |x, y| {
        let x_usize = usize::try_from(x).unwrap();
        let y_usize = usize::try_from(y).unwrap();
        let idx = y_usize * width_usize + x_usize;
        let [b, g, r, a] = buf[idx].to_le_bytes();
        [r, g, b, a].into()
    })
}

Unaligned access would mean either worse performance or access violations, it wouldn't explain leading garbage. But yes, they should be properly aligned.

My best guess is GetDIBits is also putting the header into lpbits, which is very silly.

My thought was that maybe some sufficiently-weird arithmetic with the pointer value could cause it to round down the source address for the copy to before the start of the image data.

The odd part is, this works perfectly fine for the built-in icon I tested it with above, and the same method works for taking a screenshot on Windows. So it either has something to do with the buffer, or with the icon. (Perhaps it has something to do with it being extracted from an executable?)

Thank you very much for your help. I have successfully implemented relevant functions. After replacing GetDIBits with GetBitmapBits, I successfully extracted the normal icon!

Thank you very much for your help. I have successfully implemented the relevant functions.

/**
use std::{
    ffi::OsStr,
    os::windows::ffi::OsStrExt,
    mem,fs
};

use winapi::{
        shared::{
            minwindef::{DWORD,LPVOID},
            windef::{HBITMAP,HICON},
        },
        um::{
            wingdi::{BITMAP,GetBitmapBits,GetObjectW,BITMAPINFOHEADER,DeleteObject},
            winuser::{ICONINFO,GetIconInfo,DestroyIcon},
            shellapi::{SHGetFileInfoW,SHFILEINFOW},
            winnt::VOID,
        }
};
use image::RgbaImage;
use core::mem::MaybeUninit;
**/
unsafe fn convert_icon_to_image(icon:HICON) -> RgbaImage{

    let bitmap_size_i32 = i32::try_from(mem::size_of::<BITMAP>()).unwrap();
    let biheader_size_u32 = u32::try_from(mem::size_of::<BITMAPINFOHEADER>()).unwrap();
    let mut info = ICONINFO{
        fIcon: 0,
        xHotspot: 0,
        yHotspot: 0,
        hbmMask: std::mem::size_of::<HBITMAP>() as HBITMAP,
        hbmColor: std::mem::size_of::<HBITMAP>() as HBITMAP,
    };
    GetIconInfo(icon, &mut info);
    DeleteObject(info.hbmMask as *mut VOID);
    let mut bitmap: MaybeUninit<BITMAP> = MaybeUninit::uninit();

    let result = GetObjectW(
        info.hbmColor as *mut VOID,
        bitmap_size_i32,
       bitmap.as_mut_ptr() as *mut VOID);

    assert!(result == bitmap_size_i32);
    let bitmap = bitmap.assume_init_ref();
    
    let width_u32 = u32::try_from(bitmap.bmWidth).unwrap();
    let height_u32 = u32::try_from(bitmap.bmHeight).unwrap();
    let width_usize = usize::try_from(bitmap.bmWidth).unwrap();
    let height_usize = usize::try_from(bitmap.bmHeight).unwrap();
    let buf_size = width_usize
        .checked_mul(height_usize)
        .and_then(|size| size.checked_mul(4))
        .unwrap();
    let mut buf: Vec<u8> = Vec::with_capacity(buf_size);

    let dc:windows::Win32::Graphics::Gdi::HDC = 
    windows::Win32::Graphics::Gdi::GetDC(windows::Win32::Foundation::HWND(0));
    assert!(dc != windows::Win32::Graphics::Gdi::HDC(0));

    let _bitmap_info = BITMAPINFOHEADER {
        biSize: biheader_size_u32,
        biWidth: bitmap.bmWidth,
        biHeight: -bitmap.bmHeight.abs(),
        biPlanes: 1,
        biBitCount: 32,
        biCompression:  winapi::um::wingdi::BI_RGB,
        biSizeImage: 0,
        biXPelsPerMeter: 0,
        biYPelsPerMeter: 0,
        biClrUsed: 0,
        biClrImportant: 0,
    };

    let mut bmp: Vec<u8> =vec![0; buf_size]; 
    let _mr_right = GetBitmapBits(
            info.hbmColor,  
            buf_size as i32, 
            bmp.as_mut_ptr() as LPVOID, 
    );
    buf.set_len(bmp.capacity());
    let result =   windows::Win32::Graphics::Gdi::ReleaseDC(windows::Win32::Foundation::HWND(0), dc);
    assert!(result == 1);
    DeleteObject(info.hbmColor as *mut VOID);

    for chunk in bmp.chunks_exact_mut(4) {
        let [b, _, r, _] = chunk else { unreachable!() };
        mem::swap(b, r);
    }
    RgbaImage::from_vec(width_u32, height_u32, bmp).unwrap()
}

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.