Reputation: 149
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:
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:
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;
}
Upvotes: 0
Views: 613
Reputation: 5629
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:
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.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.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();
}
}
Upvotes: 1