5
votes

As the title of the question says, I'm trying to extract a specific icon layer from file then save it as ico file with transparency (as the source icon have).

There are many questions related to icon extraction, but this is specific to the following code that I'm applying with the usage of SHDefExtractIcon function.

The problem I have is that the colors of the generated .ico file are wrong, it generated a kind of half and horrible transparency, on the other side, the generated .png file is perfectlly saved.

This is the resulting PNG file:

enter image description here

This is the resulting ICO file:

enter image description here

Is this a limitation of Windows API, or I'm doing something wrong?.

C#:

[DllImport("Shell32.dll", SetLastError = false)]
public static extern int SHDefExtractIcon(string iconFile, int iconIndex, uint flags, ref IntPtr hiconLarge, ref IntPtr hiconSmall, uint iconSize);

IntPtr hiconLarge = default(IntPtr);
SHDefExtractIcon("C:\\file.exe", 0, 0, hiconLarge, null, 256);
// ToDO: Handle HRESULT.

Icon ico = Icon.FromHandle(hiconLarge);
Bitmap bmp = ico.ToBitmap();

// Save as .png with transparency. success.
bmp.Save("C:\\ico.png", ImageFormat.Png);

// 1st intent: Save as .ico with transparency. failure. 
//' Transparency is ok but it generates a false icon, it's .png with modified extension to .ico.
bmp.Save("C:\\ico1.ico", ImageFormat.Icon);

// 2nd intent: Save as .ico with transparency. failure. Wrong transparency.
using (MemoryStream ms = new MemoryStream()) {
    ico.Save(ms);
    using (FileStream fs = new FileStream("C:\\ico2.ico", FileMode.CreateNew)) {
        ms.WriteTo(fs);
    }
    // ToDO: Destroy hiconLarge here with DestroyIcon function.
}

VB.NET:

Imports System.Drawing.Imaging
Imports System.Runtime.InteropServices

<DllImport("Shell32.dll", SetLastError:=False)>
Public Shared Function SHDefExtractIcon(ByVal iconFile As String,
                                        ByVal iconIndex As Integer,
                                        ByVal flags As UInteger,
                                        ByRef hiconLarge As IntPtr,
                                        ByRef hiconSmall As IntPtr,
                                        ByVal iconSize As UInteger
) As Integer
End Function


    Dim hiconLarge As IntPtr
    SHDefExtractIcon("C:\file.exe", 0, 0, hiconLarge, Nothing, 256)
    ' ToDO: Handle HRESULT.

    Dim ico As Icon = Icon.FromHandle(hiconLarge)
    Dim bmp As Bitmap = ico.ToBitmap()

    ' Save as .png with transparency. success.
    bmp.Save("C:\ico.png", ImageFormat.Png)

    ' 1st intent: Save as .ico with transparency. failure. 
    ' Transparency is ok but it generates a false icon, it's .png with modified extension to .ico.
    bmp.Save("C:\ico1.ico", ImageFormat.Icon)

    ' 2nd intent: Save as .ico with transparency. failure. Wrong transparency.
    Using ms As New MemoryStream
        ico.Save(ms)
        Using fs As New FileStream("C:\ico2.ico", FileMode.CreateNew)
            ms.WriteTo(fs)
        End Using
    End Using

    ' ToDO: Destroy hiconLarge here with DestroyIcon function.
2
Assuming that the PNG really is saving correctly, why is it that you suspect that the SHDefExtractIcon API call isn't working? Also, could you elaborate on what you mean by 'failure' (a screenshot would be helpful). (Side Node: If Image.Save can't find a codec to use for its save, it uses png by default.)theB
Works fine for me. Get rid of the memorystream and just use ico.Save(fs) the Icon type knows how to save themselves to file.Ňɏssa Pøngjǣrdenlarp
Maybe it's just me, but I think the resulting .ico file looks awesome as-is.вʀaᴎᴅᴏƞ вєнᴎєƞ
Just a note, using ToBitmap doesn't preserve transparency, more than likely your issue you are facing.Trevor

2 Answers

5
votes

You need a tool

The reason no one could reproduce the problem is that it requires an elaborate, artistic icon. A simple icon will make the trip from ico to disk to image just fine and preserve transparency. We also do not know what you did to get the poor quality image - it is a JPG here so somewhere there is an Icon to JPG transform.

So, I went looking for the image shown. The Plixia crapware Google found for me seems not to be the same, so I downloaded JDownloader which did have an icon matching an image link in comments:

enter image description here

Not only is the transparency lost, it looks bad all way around and the Image view of it in Explorer is just as bad. We all know an ICO file is a container which can hold a number of images, but as it turns out, Icons are complex critters and there is no high quality encoder for detailed/complex images.

IconLib

I found a fair amount of C/C++ code to manage/wrangle icons, but there is also a NET lib to do this for you. There are a (very) few posts here on this topic, but they all seem link to dead links or commercial products. However, IconLib from CodeProject seems to do what you want. Looking briefly at the source code to create/draw the icon, it looks very much like that C/C++ code.

It can be used to do the extraction(s) and save icons to a collection. I ignored that part since the question seems interested only in SHDefExtractIcon. It might be a little out of date and you may want to recompile it for NET 4.xxx; but it does seem to write out a high quality icon. Code to start with an icon from SHDefExtractIcon, save it to file and show the file version:

Dim file As String = "C:\Temp\electro crapware\JDownloader2.exe"

Dim hiconLarge As IntPtr
NativeMethods.SHDefExtractIcon(file, 0, 0, hiconLarge, Nothing, 256)

Dim ico As Icon = Icon.FromHandle(hiconLarge)
Dim bmp As Bitmap = ico.ToBitmap()
pbIcoA.Image = bmp

' png version
bmp.Save("C:\Temp\electro.png", ImageFormat.Png)
    
'' simple NET ico save: Doesnt Work! (== low quality image)
'Using fs As New FileStream("C:\Temp\electro.ico", FileMode.OpenOrCreate)
'    ico.Save(fs)
'End Using
    
'' Imports System.Drawing.IconLib
Dim myIcons As New MultiIcon()
Dim sIco As SingleIcon = myIcons.Add("electro")

' create Icon from the PInvoked ico.ToBitmap()
' note that enum only goes "up" to Vista
sIco.CreateFrom(bmp, IconOutputFormat.Vista)
sIco.Save("C:\Temp\electro2.ico")

' now display image from disk ico
Dim bmpX As Bitmap
Using icob As New Icon("C:\Temp\electro2.ico")
    bmpX = icob.ToBitmap
End Using
pbIcoB.Image = bmpX

' ToDo: Dispose

Results:

enter image description hereenter image description here

You could probably crib the SingleIcon.CreateFrom and Save methods to create your own versions and or maybe use it in place of PInvoke. I would have loved to test it on the snake/worm image because that background looks like it suffers from more than a loss of transparency.


Resources

Icons on MSDN

IconLib on CodeProject

2
votes

I was trying to reproduce your question but without your original exe or icon file, it's difficult to do so. I did have this issue in the past though. There are some instances where saving the icon in Visual Basic or C# (especially on older editions such as 2008 or 2010) ended with a 256 color icon. I ended up resolving this by going for an external image management library such as FreeImage which I plug into my project to replace the MS one.

FreeImage is 100% open source and available here: http://freeimage.sourceforge.net/ (see the instructions on how to reference it into your project below).

Here is an example of how to use it in VB.net:

<DllImport("Shell32.dll", SetLastError:=False)>
Public Shared Function SHDefExtractIcon(ByVal iconFile As String,
                                        ByVal iconIndex As Integer,
                                        ByVal flags As UInteger,
                                        ByRef hiconLarge As IntPtr,
                                        ByRef hiconSmall As IntPtr,
                                        ByVal iconSize As UInteger
) As Integer
End Function


Dim hiconLarge As IntPtr
SHDefExtractIcon("C:\file.exe", 0, 0, hiconLarge, Nothing, 256)

Dim ico As Icon = Icon.FromHandle(hiconLarge)
Dim bmp As Bitmap = ico.ToBitmap()
Dim fiBmp As FreeImageAPI.FreeImageBitmap = New FreeImageAPI.FreeImageBitmap(bmp)

' At this point you can either save a simple one size icon file or you can 
' take advantage of the FreeImage engine to save a multi-sized icon:

fiBmp.Save("C:\ico1.ico", FreeImageAPI.FREE_IMAGE_FORMAT.FIF_ICO)

' To add more layers to your Icon:
fiBmp.Rescale(128, 128, FreeImageAPI.FREE_IMAGE_FILTER.FILTER_LANCZOS3)
fiBmp.SaveAdd("C:\ico1.ico")

fiBmp.Rescale(64, 64, FreeImageAPI.FREE_IMAGE_FILTER.FILTER_LANCZOS3)
fiBmp.SaveAdd("C:\ico1.ico")

fiBmp.Rescale(48, 48, FreeImageAPI.FREE_IMAGE_FILTER.FILTER_LANCZOS3)
fiBmp.SaveAdd("C:\ico1.ico")    

'etc etc etc

This is how you plug FreeImage into your project.

  • Download the source code from the project site and compile it. Be sure to compile in 64bit if your project is 64bit and vice versa for 32.
  • Find FreeImage.dll and move it to your C# or vb.net project folder.
  • This dll cannot be referenced directly by .Net projects in Visual Studio. You will have to also build FreeImageNET.dll which you include in the same folder as FreeImage.dll and reference in Visual Studio. FreeImageNET.dll's source can be fround under Wrapper/FreeImage.NET/cs. FreeImageNET.dll can be compiled under the anycpu architecture (64+32bit) or 64/32bit indivually.