0

I wrote some code to create ico files from any png, jpg, etc. images. The icons seem to be getting created correctly, and looks almost like the original image, when opened in Paint3d. Here is how it looks:

enter image description here

But when setting the image as a thumbnail to a folder, it looks weird and shiny.

Here is how it looks in windows file explorer:

enter image description here

enter image description here

 

Firstly, I would like to know if this is an issue in Windows itself, or is it code related? If this is Windows related, the code doesn't matter. If not, here it is:

 

I picked up a couple of code snippets from across the internet, so probably some non-optimized code, but here is the meat of my code:

//imagePaths => all images which I am converting to ico files
imagePaths.ForEach(imgPath => {
    //create a temp png at this path after changing the original img to a squared img
    var tempPNGpath = Path.Combine(icoDirPath, imgName.Replace(ext, ".png"));
    var icoPath = tempPNGpath.Replace(".png", ".ico");

    using (FileStream fs1 = File.OpenWrite(tempPNGpath)) {
        Bitmap b = ((Bitmap)Image.FromFile(imgPath));
        b = b.CopyToSquareCanvas(Color.Transparent);
        b.Save(fs1, ImageFormat.Png);

        fs1.Flush();
        fs1.Close();

        ConvertToIco(b, icoPath, 256);
    }
    File.Delete(tempPNGpath);
});


public static void ConvertToIco(Image img, string file, int size) {
    Icon icon;
    using (var msImg = new MemoryStream())
        using (var msIco = new MemoryStream()) {
            img.Save(msImg, ImageFormat.Png);
            using (var bw = new BinaryWriter(msIco)) {
                bw.Write((short)0);           //0-1 reserved
                bw.Write((short)1);           //2-3 image type, 1 = icon, 2 = cursor
                bw.Write((short)1);           //4-5 number of images
                bw.Write((byte)size);         //6 image width
                bw.Write((byte)size);         //7 image height
                bw.Write((byte)0);            //8 number of colors
                bw.Write((byte)0);            //9 reserved
                bw.Write((short)0);           //10-11 color planes
                bw.Write((short)32);          //12-13 bits per pixel
                bw.Write((int)msImg.Length);  //14-17 size of image data
                bw.Write(22);                 //18-21 offset of image data
                bw.Write(msImg.ToArray());    // write image data
                bw.Flush();
                bw.Seek(0, SeekOrigin.Begin);
                icon = new Icon(msIco);
            }
        }
    using (var fs = new FileStream(file, FileMode.Create, FileAccess.Write))
        icon.Save(fs);
}

In the Extension class, the method goes:

public static Bitmap CopyToSquareCanvas(this Bitmap sourceBitmap, Color canvasBackground) {
    int maxSide = sourceBitmap.Width > sourceBitmap.Height ? sourceBitmap.Width : sourceBitmap.Height;

    Bitmap bitmapResult = new Bitmap(maxSide, maxSide, PixelFormat.Format32bppArgb);
    using (Graphics graphicsResult = Graphics.FromImage(bitmapResult)) {
        graphicsResult.Clear(canvasBackground);

        int xOffset = (maxSide - sourceBitmap.Width) / 2;
        int yOffset = (maxSide - sourceBitmap.Height) / 2;

        graphicsResult.DrawImage(sourceBitmap, new Rectangle(xOffset, yOffset, sourceBitmap.Width, sourceBitmap.Height));
    }

    return bitmapResult;
}
Shraa1
  • 149
  • 9
  • 1
    You're not actually resizing it to 256x256 though are you? So the actual image inside the ico file is much larger, and what you're seeing is just the result of different downscaling/caching methods used to display it. – Nyerguds Jul 27 '20 at 02:13
  • 1
    Oh, and, side note: you should dispose the bitmaps when you're done with them; they're `IDisposable` too. Preferably, put them in `using` statements. And the result of `CopyToSquareCanvas` is a _new_ bitmap, so it should have a new `using` statement. – Nyerguds Jul 27 '20 at 02:14
  • Yeah I noticed that only high resolution images are causing this problem. The downscaling is happening on the OS level, right? No problems with C# ? – Shraa1 Jul 27 '20 at 03:46
  • I've updated my code slightly since posting the question, and I've moved the code into `using` statements – Shraa1 Jul 27 '20 at 03:48
  • 1
    The downscaling is happening on OS level, yes, but only because you're not doing it. You should be downscaling it to 256x256 in this code. Making an icon that pretends to be 256x256 but contains a larger image is technically a corrupt file, plain and simple, since the ico header does not match the image contents. You're lucky this worked at all. To more accurately control how the downscaled versions will look [you can put multiple images into the ico file](https://stackoverflow.com/q/54801185/395685). – Nyerguds Jul 27 '20 at 11:42

1 Answers1

1

The differences in scaling are the result of the fact you're not doing the scaling yourself.

The icon format technically only supports images up to 256x256. You have code to make a square image out of the given input, but you never resize it to 256x256, meaning you end up with an icon file in which the header says the image is 256x256, but which is really a lot larger. This is against the format specs, so you are creating a technically corrupted ico file. The strange differences you're seeing are a result of different downscaling methods the OS is using in different situations to remedy this situation.

So the solution is simple: resize the image to 256x256 before putting it into the icon.

If you want more control over any smaller display sizes for the icon, you can add code to resize it to a number of classic used formats, like 16x16, 32x32, 64x64 and 128x128, and put them all in an icon file together. I have written an answer to another question that details the process of putting multiple images into a single icon:

A: Combine System.Drawing.Bitmap[] -> Icon

There are quite a few other oddities in your code, though:

  • I see no reason to save your in-between image as png file. That whole fs1 stream serves no purpose at all. You never use or load the temp file; you just keep using the b variable, which does not need anything written to disk.
  • There is no point in first making the icon in a MemoryStream, then loading that as Icon class through its file loading function, and then saving that to a file. You can just write the contents of that stream straight to a file, or, heck, use a FileStream right away.
  • As I noted in the comments, Bitmap is a disposable class, so any bitmap objects you create should be put in using statements as well.

The adapted loading code, with the temp png writing removed, and the using statements and resizes added:

public static void WriteImagesToIcons(List<String> imagePaths, String icoDirPath)
{
    // Change this to whatever you prefer.
    InterpolationMode scalingMode = InterpolationMode.HighQualityBicubic;
    //imagePaths => all images which I am converting to ico files
    imagePaths.ForEach(imgPath =>
    {
        // The correct way of replacing an extension
        String icoPath = Path.Combine(icoDirPath, Path.GetFileNameWithoutExtension(imgPath) + ".ico");
        using (Bitmap orig = new Bitmap(imgPath))
        using (Bitmap squared = orig.CopyToSquareCanvas(Color.Transparent))
        using (Bitmap resize16 = squared.Resize(16, 16, scalingMode))
        using (Bitmap resize32 = squared.Resize(32, 32, scalingMode))
        using (Bitmap resize48 = squared.Resize(48, 48, scalingMode))
        using (Bitmap resize64 = squared.Resize(64, 64, scalingMode))
        using (Bitmap resize96 = squared.Resize(96, 96, scalingMode))
        using (Bitmap resize128 = squared.Resize(128, 128, scalingMode))
        using (Bitmap resize192 = squared.Resize(192, 192, scalingMode))
        using (Bitmap resize256 = squared.Resize(256, 256, scalingMode))
        {
            Image[] includedSizes = new Image[]
                { resize16, resize32, resize48, resize64, resize96, resize128, resize192, resize256 };
            ConvertImagesToIco(includedSizes, icoPath);
        }
    });
}

The CopyToSquareCanvas remains the same, so I didn't copy it here. The Resize function is fairly simple: just use Graphics.DrawImage to paint the picture on a different-sized canvas, after setting the desired interpolation mode.

public static Bitmap Resize(this Bitmap source, Int32 width, Int32 height, InterpolationMode scalingMode)
{
    Bitmap result = new Bitmap(width, height, PixelFormat.Format32bppArgb);
    using (Graphics g = Graphics.FromImage(result))
    {
        // Set desired interpolation mode here
        g.InterpolationMode = scalingMode;
        g.PixelOffsetMode = PixelOffsetMode.Half;
        g.DrawImage(source, new Rectangle(0, 0, width, height), new Rectangle(0, 0, source.Width, source.Height), GraphicsUnit.Pixel);
    }
    return result;
}

And, finally, the above-linked Bitmap[] to Icon function, slightly tweaked to write to a FileStream directly instead of loading the result into an Icon object:

public static void ConvertImagesToIco(Image[] images, String outputPath)
{
    if (images == null)
        throw new ArgumentNullException("images");
    Int32 imgCount = images.Length;
    if (imgCount == 0)
        throw new ArgumentException("No images given!", "images");
    if (imgCount > 0xFFFF)
        throw new ArgumentException("Too many images!", "images");
    using (FileStream fs = new FileStream(outputPath, FileMode.Create, FileAccess.Write))
    using (BinaryWriter iconWriter = new BinaryWriter(fs))
    {
        Byte[][] frameBytes = new Byte[imgCount][];
        // 0-1 reserved, 0
        iconWriter.Write((Int16)0);
        // 2-3 image type, 1 = icon, 2 = cursor
        iconWriter.Write((Int16)1);
        // 4-5 number of images
        iconWriter.Write((Int16)imgCount);
        // Calculate header size for first image data offset.
        Int32 offset = 6 + (16 * imgCount);
        for (Int32 i = 0; i < imgCount; ++i)
        {
            // Get image data
            Image curFrame = images[i];
            if (curFrame.Width > 256 || curFrame.Height > 256)
                throw new ArgumentException("Image too large!", "images");
            // for these three, 0 is interpreted as 256,
            // so the cast reducing 256 to 0 is no problem.
            Byte width = (Byte)curFrame.Width;
            Byte height = (Byte)curFrame.Height;
            Byte colors = (Byte)curFrame.Palette.Entries.Length;
            Int32 bpp;
            Byte[] frameData;
            using (MemoryStream pngMs = new MemoryStream())
            {
                curFrame.Save(pngMs, ImageFormat.Png);
                frameData = pngMs.ToArray();
            }
            // Get the colour depth to save in the icon info. This needs to be
            // fetched explicitly, since png does not support certain types
            // like 16bpp, so it will convert to the nearest valid on save.
            Byte colDepth = frameData[24];
            Byte colType = frameData[25];
            // I think .Net saving only supports colour types 2, 3 and 6 anyway.
            switch (colType)
            {
                case 2: bpp = 3 * colDepth; break; // RGB
                case 6: bpp = 4 * colDepth; break; // ARGB
                default: bpp = colDepth; break; // Indexed & greyscale
            }
            frameBytes[i] = frameData;
            Int32 imageLen = frameData.Length;
            // Write image entry
            // 0 image width. 
            iconWriter.Write(width);
            // 1 image height.
            iconWriter.Write(height);
            // 2 number of colors.
            iconWriter.Write(colors);
            // 3 reserved
            iconWriter.Write((Byte)0);
            // 4-5 color planes
            iconWriter.Write((Int16)0);
            // 6-7 bits per pixel
            iconWriter.Write((Int16)bpp);
            // 8-11 size of image data
            iconWriter.Write(imageLen);
            // 12-15 offset of image data
            iconWriter.Write(offset);
            offset += imageLen;
        }
        for (Int32 i = 0; i < imgCount; i++)
        {
            // Write image data
            // png data must contain the whole png data file
            iconWriter.Write(frameBytes[i]);
        }
        iconWriter.Flush();
    }
}
Nyerguds
  • 5,360
  • 1
  • 31
  • 63
  • For more info on resizing images I suggest you read this: https://photosauce.net/blog/post/image-scaling-with-gdi-part-3-drawimage-and-the-settings-that-affect-it – Nyerguds Jul 29 '20 at 11:29
  • iconWriter.Write(width); and iconWriter.Write(height); and iconWriter.Write(colors); - will be better replace to iconWriter.Write((Byte)0); - that's enough - just reserving space. – Garric Feb 07 '23 at 23:42
  • @Garric No it's not. Did you not read _anything_ before the last code block? This code is capable of writing _any_ of the supported image formats and sizes, not just high-colour 256x256. That's why there is the specific code to determine the bpp value, too. And even if it would work without that when using embedded png, I wrote the code to obey the format specs. – Nyerguds Feb 08 '23 at 10:19
  • 256 icon will not be created if you exclude "using (Bitmap resize256 = squared.Resize(256, 256, scalingMode))" from the code regardless of "iconWriter.Write((Byte)0)". The final code of the icon is created by another program. Remove 256 from the code, put (Byte)0 and the 256th icon will be gone, no matter how much you want it to be. – Garric Feb 08 '23 at 13:43
  • In any case, your code is great. I didn't know how to do it. I just added your "ConvertImagesToIco" function to my old powershell code. Everything worked. My icon set is created by looping through the array 256,64,48,40,32,24,20,16, and creates sliced bitmaps. Sliced bitmaps are a sufficient condition for the final program to create the correct Icon. The handler of your code will determine their dimension by itself. – Garric Feb 08 '23 at 14:09
  • I haven't checked what happens to the resulting colors in your code, but if you haven't changed the colors of the bitmap itself, then 8 and 4 bit color icon parts may not be created at all. You can check their presence in the IcoFx program. – Garric Feb 08 '23 at 14:27
  • You realise that a width, height and colour amount of "256" _automatically_ turn into 0 when you cast it to `byte`, right? But if you force it to 0 it'll also be 0 for the other sizes, which is wrong. – Nyerguds Feb 08 '23 at 18:20
  • May be. I'll check that each bitmap is correctly selected by Windows to display the icon in dependence of display mode of the window a little later, when I'm will working on another program that I've been wanting to remake for a long time. It just requires different, auto generated icons for different window modes. It will be something that looks like overlay icons. Anyway. Your code is very cool. I was looking for exactly what this code is. Thank you. – Garric Feb 08 '23 at 18:40