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
  • 4,916
  • 1
  • 29
  • 58
  • 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