Jump to content

Sky Slate Blueberry Blackcurrant Watermelon Strawberry Orange Banana Apple Emerald Chocolate
Photo

GDI+ Save Icon to PNG Trouble...


  • Please log in to reply
5 replies to this topic
MiamiGuy
  • Members
  • 61 posts
  • Last active: Nov 25 2011 07:44 AM
  • Joined: 03 Nov 2006
Hi,
I've been trying for days to figure out a way of extracting the large 255x255 resolution Icons found in a vista resource file, and saving it to a PNG but havent been quite successful.

I originally used the Gdi_BitmapfromHicon function after extracting and getting handle to the icon, and then saving the bitmap, but it looses any alpha chanel info. the image might have, leaving it with a very nasty-looking black outline around it.
Posted Image

I looked on the MSDN and other sites for alternatives, and came across an article that explained that the DrawIconex function could be used to draw the mask of the icon to one DC, the actual Image to another, and use BitBlt to combine both, using 2 different raster methods 3 times in a row... (or at least thats what I understood, Im probably wrong, which is why I'm here I guess. :D )

The below code is what came up with, but it fails to work, since the image comes out with a black background.

If anyone could tell me what I'm missing or doing incorrectly, or has any suggestions, please take a look and see if something comes to mind. Any help would be great.

#Include, Gdip.ahk

   ; DrawIconEx diFlags ;
DI_MASK := 0x0001
DI_IMAGE := 0x0002
DI_NORMAL := 0x0003


   ; BitBlt dwRop (Raster) ;
SRCAND := 0x8800C6
SRCCOPY := 0xCC0020
SRCINVERT := 0x660046


   ; Location of file containing icon etc. ;
Width := 255
Height := 255
Filename := "C:\Windows\system32\cabview.dll"
IconNumber := 1
IconSize := 255



   ; Start gdi+ ;
If !pToken := Gdip_Startup()
{
   MsgBox, 48, gdiplus error!, Gdiplus failed to start. Please ensure you have gdiplus on your system
   ExitApp
}



   ; Get icon handle by extracting it ;
hIcon := ExtractIcon(Filename, IconNumber, IconSize)



   ; Create CmpDC, DIB, and select them ;
hdcMask := CreateCompatibleDC()
hbmMask := CreateDIBSection(Width, Height, hdcMask, 1)
obmMask := SelectObject(hdcMask, hbmMask)



   ; Draw icon into DC using DI_MASK ;
   ; this gives a b/w image of icon  ;
   DllCall("DrawIconEx"
       , "uint", hdcMask
       , "int",  0
       , "int",  0
       , "uint", hIcon
       , "int",  0
       , "int",  0
       , "uint", 0
       , "uint", 0
       , "uint", DI_MASK)

   ; Clean up for MaskDC ;
SelectObject(hdcMask, obmMask)
DeleteDC(hdcMask)



   ; Create CmpDC, DIB, and select them ;
hdcImage := CreateCompatibleDC()
hbmImage := CreateDIBSection(Width, Height, hdcImage)
obmImage := SelectObject(hdcImage, hbmImage)



   ; Draw icon again into another DC using DI_NORMAL ;
   ; this gives the image of icon with black background ;
   DllCall("DrawIconEx"
       , "uint", hdcImage
       , "int",  0
       , "int",  0
       , "uint", hIcon
       , "int",  0
       , "int",  0
       , "uint", 0
       , "uint", 0
       , "uint", DI_IMAGE)

   ; Clean up for ImageDC ;
SelectObject(hdcImage, obmImage)
DeleteDC(hdcImage)



   ; Create the DestinationDC, DIB, and select them ;
hdcDest := CreateCompatibleDC()
hbmDest := CreateDIBSection(Width, Height, hdcDest)
obmDest := SelectObject(hdcDest, hbmDest)



   ; BitBlt Image, Mask, and Image again into Destination DC ;
   ; This "should" make the black background transparent using ;
   ; these raster drawing, but not working... ;
   BitBlt(hdcDest, 0, 0, width, height, hdcImage, 0, 0, SRCINVERT)
   BitBlt(hdcDest, 0, 0, width, height, hdcMask,  0, 0, SRCAND)
   BitBlt(hdcDest, 0, 0, width, height, hdcImage, 0, 0, SRCINVERT)



   ; Clean up for DestinationDC ;
SelectObject(hdcDest, obmDest)
DeleteDC(hdcDest)



   ; Create a bitmap from the Destination bm handle ;
pBitmap := Gdip_CreateBitmapFromHBITMAP(hbmDest)



   ; Save Bitmap to file ;
Gdip_SaveBitmapToFile(pBitmap, "File.png")



   ; Clean up all bm created ;
DeleteObject(hbmMask)
DeleteObject(hbmImage)
DeleteObject(hbmDest)
Gdip_DisposeImage(pBitmap)



   ;Shut down GDI ;
Gdip_Shutdown(pToken)

Return




F8::
   Reload
Return



   ; ExtractIcon functions courtesy of LEXIKOS ;
ExtractIcon(Filename, IconNumber, IconSize=0)
{
    static SmallIconSize, LargeIconSize
    if (!SmallIconSize) {
        SysGet, SmallIconSize, 49  ; 49, 50  SM_CXSMICON, SM_CYSMICON
        SysGet, LargeIconSize, 11  ; 11, 12  SM_CXICON, SM_CYICON
    }


    VarSetCapacity(phicon, 4, 0)
    h_icon = 0

    ; If possible, use PrivateExtractIcons, which supports any size of icon.
    if A_OSVersion in WIN_VISTA,WIN_2003,WIN_XP,WIN_2000
    {
        VarSetCapacity(piconid, 4, 0)
       
        ; MSDN: "... this function is deprecated ..." (oh well)
        ret := DllCall("PrivateExtractIcons"
            , "str", Filename
            , "int", IconNumber-1   ; zero-based index of the first icon to extract
            , "int", IconSize
            , "int", IconSize
            , "str", phicon         ; pointer to an array of icon handles...
            , "str", piconid        ; piconid - won't be used
            , "uint", 1             ; nIcons - number of icons to extract
            , "uint", 0, "uint")    ; flags
       
        if (ret && ret != 0xFFFFFFFF)
            h_icon := NumGet(phicon)
    }
    else
    {   ; Use ExtractIconEx, which only returns 16x16 or 32x32 icons.
        VarSetCapacity(phiconSmall, 4, 0)
       
        ; Extract the icon from an executable, DLL or icon file.
        if DllCall("shell32.dll\ExtractIconExA"
            , "str", Filename
            , "int", IconNumber-1   ; zero-based index of the first icon to extract
            , "str", phicon         ; pointer to an array of icon handles...
            , "str", phiconSmall
            , "uint", 1)
        {
            ; Use the best-fit size; clean up the other.
            if (IconSize <= SmallIconSize) {
                DllCall("DestroyIcon", "uint", NumGet(phicon))
                h_icon := NumGet(phiconSmall)
            } else {
                DllCall("DestroyIcon", "uint", NumGet(phiconSmall))
                h_icon := NumGet(phicon)
            }
        }
    }

    return h_icon
}

GetIconSize(h_icon, ByRef width, ByRef height)
{
    VarSetCapacity(ii, 20, 0)
   
    if (DllCall("GetIconInfo", "UInt", h_icon, "UInt", &ii))
    {
        hbmColor := NumGet(ii, 16)
        hbmMask  := NumGet(ii, 12)
       
        ret := GetBitmapSize(hbmColor, width, height)
       
        DllCall("DeleteObject", "UInt", hbmColor)
        DllCall("DeleteObject", "UInt", hbmMask)
       
        return ret
    }
    return false
}

GetBitmapSize(h_bitmap, ByRef width, ByRef height, ByRef bpp="")
{
    VarSetCapacity(bm, 24, 0) ; BITMAP
    if (!DllCall("GetObject", "UInt", h_bitmap, "Int", 24, "UInt", &bm))
        return false
    width  := NumGet(bm, 4, "int")
    height := NumGet(bm, 8, "int")
    bpp    := NumGet(bm,18, "ushort")
    return true
}


tic
  • Members
  • 1934 posts
  • Last active: May 30 2018 08:13 PM
  • Joined: 22 Apr 2007
This has really frustrated me....

Hopefully someone else can help. Perhaps Lex...

You of course need:

Gdip.ahk

Here is what I have tested:

#SingleInstance, Force
#NoEnv
SetBatchLines, -1

If !pToken := Gdip_Startup()
{
	MsgBox, 48, gdiplus error!, Gdiplus failed to start. Please ensure you have gdiplus on your system
	ExitApp
}
OnExit, Exit

sFile := "shell32.dll"
IconNumber := 23
Width := Height := 256
if !hIcon := DllCall("LoadImage", "UInt", DllCall("GetModuleHandle", "Str", sFile), "UInt", IconNumber, "UInt", 1, "Int", Width, "Int", Height, "UInt", 0)
{
	MsgBox, 48, icon error!, Could not load icon
	ExitApp
}

Gui, 1: -Caption +E0x80000 +LastFound +AlwaysOnTop +ToolWindow +OwnDialogs
Gui, 1: Show, NA
hwnd1 := WinExist()

; Created like this so I could test if modifying pBits made any difference....
hbm := CreateDIBSection(Width, Height, "", 32, pBits)
hdc := CreateCompatibleDC()
obm := SelectObject(hdc, hbm)

; Manually setting bytes to make pixels transparent does not work!
; offset := 0
; Loop, %Height%
; {
	; Loop, %Width%
	; {
		; Turning background red shows that when blending then shadow from icon is still present
		; Set to 0x00000000 to make transparent
		; NumPut(0xffff0000, pBits+offset)
		; offset += 4
	; }
; }

if !DllCall("DrawIconEx", "UInt", hdc, "Int", 0, "Int", 0, "UInt", hIcon, "UInt", IconSize, "UInt", IconSize, "UInt", 0, "UInt", 0, "UInt", 3)
{
	MsgBox, 48, drawing error!, Could not draw icon
	ExitApp
}

UpdateLayeredWindow(hwnd1, hdc, 0, 0, Width, Height)

pBitmap := Gdip_CreateBitmapFromHBITMAP(hbm)
Gdip_SaveBitmapToFile(pBitmap, "test.png")

SelectObject(hdc, obm)
DeleteObject(hbm)
DeleteDC(hdc)

Gdip_DisposeImage(pBitmap)
return

;################################################################################

Exit:
Gdip_Shutdown(pToken)
ExitApp
return

As you can see the window looks fine and has all transparencies correct, but the bitmap saved to disk does not have them. When drawing onto the gdi+ bitmap it doesnt like blending the pixels. I tried to loop through the pixels setting them each to transparent, but this made no difference (I have included it and commented it out). I tried every possible method I could think of to get the information from the 1st bitmap onto the 2nd to no avail. i tried directly using a gdi+ bitmap and then using DrawIconEx with the hdc of its graphics, but this resulted in the same. I have seen samples where they say they get round this by using lockbits (so a similar method to the commented out part but obviously with a gdi+ bitmap. i tried similar methods but couldnt get it to work, but their example is related to office so I am unsure of some of the things they talk about

Take a look:

<!-- m -->http://blogs.msdn.co... ... mages.aspx<!-- m -->

I would be very happy if someone had greater success than me....

occasional ahker
  • Guests
  • Last active:
  • Joined: --
has anyone made any progress on this? This would be really nice to have.

Zaelia
  • Members
  • 754 posts
  • Last active: Jan 17 2015 02:38 AM
  • Joined: 31 Oct 2008
It's hard :( It's seems that an handle hbitmap can't have alpha channel, so we can have only one transcolor with a mask, and no semi-transparency color... They are a way to have a pBitmap and not use DrawIcon function for don't use handle ?

I have this problem too, with embeded ressource, because when I load it it's a handle bitmap :(

Lexikos
  • Administrators
  • 9844 posts
  • AutoHotkey Foundation
  • Last active:
  • Joined: 17 Oct 2006

It's seems that an handle hbitmap can't have alpha channel,

Things aren't always as they seem. In this case, the bitmap referred to by hbm does in fact have an alpha channel, but GDI+ is discarding it.


I thought of three different methods - the test function below can be used to demonstrate any of the three.
TEST_GetBitmapFromDIB(hbm, method=3)
{
    ; Retrieve DIBSECTION from HBITMAP.
    VarSetCapacity(dib, 84)
    if !DllCall("GetObject", "UInt", hbm, "Int", 84, "UInt", &dib)
        return
    
    width  := NumGet(dib, 4)  ; BITMAP.bmWidth
    height := NumGet(dib, 8)  ; BITMAP.bmHeight
    stride := NumGet(dib,12)  ; BITMAP.bmWidthBytes
    bits   := NumGet(dib,20)  ; BITMAP.bmBits
    
    if Method = 1
    {
        ; Method #1: Create Bitmap from BITMAPINFO and raw bitmap data.
        DllCall("gdiplus\GdipCreateBitmapFromGdiDib", "UInt", &dib + 24, "UInt", bits, "UInt*", pBitmap)
        return pBitmap
    }
    
    ; The following methods will only work correctly if the source and destination
    ; pixel formats are equivalent.  If the stride (byte-length of each scan line)
    ; is out or the source bitmap is bottom-up, the result will be incorrect.
    if (stride != width*4)
        return

    if Method = 2
    {
        ; Method #2: Create Bitmap and copy in raw data using LockBits.
        pBitmap := Gdip_CreateBitmap(width, height)
        VarSetCapacity(rect, 16, 0), NumPut(height, NumPut(width, rect, 8))
        VarSetCapacity(bmd, 24) ; GDI+ BitmapData
        DllCall("gdiplus\GdipBitmapLockBits", "UInt", pBitmap, "UInt", &rect, "UInt", 3, "UInt", 0x26200A, "UInt", &bmd)
        DllCall("RtlMoveMemory", "UInt", NumGet(bmd,16), "UInt", bits, "UInt", width*height*4)
        DllCall("gdiplus\GdipBitmapUnlockBits", "UInt", pBitmap, "UInt", &bmd)
    }
    else
    {
        ; Method #3: Create Bitmap from raw bitmap data.
        DllCall("gdiplus\GdipCreateBitmapFromScan0", "Int", width, "Int", height, "Int", stride, "Int", 0x26200A, "UInt", bits, "UInt*", pBitmap)
    }

    return pBitmap
}
Method #1 passes a BITMAPINFO structure and raw bitmap data to a GDI+ function. Both of these are contained within the DIBSECTION structure retrieved from the bitmap via GetObject. Raw bitmap data is in GDI format, as specified within the BITMAPINFO structure.

Methods #2 & #3 create a GDI+ Bitmap using raw bitmap data in the specified GDI+ pixel format. I wrote #2 first because I had forgotten about method #3.

GdipCreateBitmapFromGdiDib (method #1) and GdipCreateBitmapFromHBITMAP appear to create an RGB - not ARGB - bitmap, so the alpha channel is discarded or ignored. GdipCreateBitmapFromHICON appears to return an ARGB bitmap, but all semi-transparent pixels seem to become opaque.

Since methods #2 and #3 directly pass in the raw bitmap data and pixel format, the alpha channel is preserved. However, the bitmap data must be top-down and already in the correct pixel format. If you use CreateDIBSection to create the source GDI bitmap, specify a negative Height to create a top-down bitmap.

According to MSDN, BITMAPINFO.biHeight is negative for top-down bitmaps and positive for bottom-up bitmaps. When I tested, it was always positive even if a negative value was originally passed to CreateDIBSection (and the bitmap was definitely top-down). Consequently, it seemed that method #1 assumes the bitmap is bottom-up, so if it is top-down the resulting GDI+ Bitmap is upside-down.
BITMAPINFO_biHeight := NumGet(dib,32,"int")
You can determine the pixel format of a bitmap as follows:
DllCall("gdiplus\GdipGetImagePixelFormat", "UInt", pBitmap, "UInt*", fmt)
MsgBox % fmt
; PixelFormat32bppRGB=139273
; PixelFormat32bppARGB=2498570

tic, I recommend implementing method #3. Since the bitmap data must be in the correct format, I suggest hard-coding the format (0x26200A) and naming the function to imply it only supports (top-down) 32-bit bitmaps. If you want to support other formats, it might be possible to create a top-down 32-bit bitmap, blit the original bitmap into it and use the resulting bitmap data to create the GDI+ Bitmap. (This would only be useful for 32-bit bottom-up bitmaps since GdipCreateBitmapFromHBITMAP can be used if there is no alpha channel.)

tic
  • Members
  • 1934 posts
  • Last active: May 30 2018 08:13 PM
  • Joined: 22 Apr 2007
Cool....it works perfectly. I hadnt thought to use that even though it was being used in Gdip_BitmapFromBRA

One quick question if you already know the answer then it will save me a bit of research....

When using:

DllCall("PrivateExtractIcons", "Str", sFile, "Int", IconNumber-1, "Int", iconSize, "Int", IconSize, "UInt*", hIcon, "UInt*", 0, "UInt", 1, "UInt", 0)

Then what is the best way to find the sizes to use for IconSize. I see when using it, then for the same icon number I can retrieve a completely different icon depending on the iconsize used. I am altering the function:

Gdip_CreateBitmapFromFile(sFile, IconNumber=1, IconSize=256)

so that a bitmap can be taken directly from an exe or dll in the same way as an image format, so it would be useful to know the best way to get a list of these sizes, so that the function would have an optional parameter to return a list of avaiable sizes for a specified icon number

I saw one of Maj's scripts did this, but it was very hard to follow due to obfuscation, and it appeared to just be using trial and error to get the icon sizes

As always I would like to say thank you to Lex, as without him the gdi+ library would never have been started