Writing ico files java

4.5k views Asked by At

Recently i have become interested in creating .ico file or windows icon files in java. This is the current code i use. I have gotten the file format specs from here http://en.wikipedia.org/wiki/ICO_%28file_format%29

    BufferedImage img = new BufferedImage(16, 16, BufferedImage.TYPE_INT_RGB);
    Graphics g = img.getGraphics();
    g.setColor(Color.GREEN);
    g.fillRect(0, 0, 16, 16);
    byte[] imgBytes = getImgBytes(img);
    int fileSize = imgBytes.length + 22;
    ByteBuffer bytes = ByteBuffer.allocate(fileSize);
    bytes.order(ByteOrder.LITTLE_ENDIAN);
    bytes.putShort((short) 0);//Reserved must be 0
    bytes.putShort((short) 1);//Image type
    bytes.putShort((short) 1);//Number of image in file
    bytes.put((byte) img.getWidth());//image width
    bytes.put((byte) img.getHeight());//image height
    bytes.put((byte) 0);//number of colors in color palette
    bytes.put((byte) 0);//reserved must be 0
    bytes.putShort((short) 0);//color planes
    bytes.putShort((short) 0);//bits per pixel
    bytes.putInt(imgBytes.length);//image size
    bytes.putInt(22);//image offset
    bytes.put(imgBytes);
    byte[] result = bytes.array();
    FileOutputStream fos = new FileOutputStream("C://Users//Owner//Desktop//picture.ico");
    fos.write(result);
    fos.close();
    fos.flush();

private static byte[] getImgBytes(BufferedImage img) throws IOException
{
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    ImageIO.write(img, "png", bos);
    return bos.toByteArray();
}

The problem is that windows doesn't seem to be able to open the image, giving an error when i try to open the image using Windows Photo Gallery. However when i try to open the image using gimp the image opens fine. What am i doing wrong. I feel like i am messing up something in the file header. Edit: Even stranger on the desktop the picture looks right, just not when i try to open it.

On my desktop the image looks like this enter image description here

When i try to open it in Windows Photo Gallery it displays this error

enter image description here

After having failed with the png attempt i tried it with bitmap image instead, here is my new code

import java.awt.AWTException;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.HeadlessException;
import java.awt.Rectangle;
import java.awt.Robot;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;

import javax.imageio.ImageIO;

public class IconWriter
{
    public static void main(String[] args) throws HeadlessException, AWTException, IOException
    {
        BufferedImage img = new BufferedImage(16, 16, BufferedImage.TYPE_INT_RGB);
        Graphics g = img.getGraphics();
        g.setColor(Color.GREEN);
        g.fillRect(0, 0, 16, 16);
        byte[] imgBytes = getImgBytes(img);
        int fileSize = imgBytes.length + 22;
        ByteBuffer bytes = ByteBuffer.allocate(fileSize);
        bytes.order(ByteOrder.LITTLE_ENDIAN);
        bytes.putShort((short) 0);//Reserved must be 0
        bytes.putShort((short) 1);//Image type
        bytes.putShort((short) 1);//Number of images in file
        bytes.put((byte) img.getWidth());//image width
        bytes.put((byte) img.getHeight());//image height
        bytes.put((byte) 0);//number of colors in color palette
        bytes.put((byte) 0);//reserved must be 0
        bytes.putShort((short) 0);//color planes
        bytes.putShort((short) 0);//bits per pixel
        bytes.putInt(imgBytes.length);//image size
        bytes.putInt(22);//image offset
        bytes.put(imgBytes);
        byte[] result = bytes.array();
        FileOutputStream fos = new FileOutputStream("C://Users//Owner//Desktop//hi.ico");
        fos.write(result);
        fos.close();
        fos.flush();
    }

    private static byte[] getImgBytes(BufferedImage img) throws IOException
    {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ImageIO.write(img, "bmp", bos);
        byte[] bytes = bos.toByteArray();
        return Arrays.copyOfRange(bytes, 14, bytes.length);
    }
}

now when i try to open my image in photo gallery the image looks like this i have no idea why it isn't working now and especially why the weird lines are appearing, although i suspect it has to with the color planes attribute in the ico image header. enter image description here

5

There are 5 answers

0
bvdb On BEST ANSWER

Actually, the problem you are having is mentioned in the specs (at wikipedia). Quote:

Images with less than 32 bits of color depth[6] follow a particular format: the image is encoded as a single image consisting of a color mask (the "XOR mask") together with an opacity mask (the "AND mask").

That's very complicated.

Creating a 32-bit image -> fails

So, the quote above might make you think: "Oh, I just have to make the image 32-bit instead of 24-bit", as a workaround. Unfortunately that won't work. Well, actually there exists a 32-bit BMP format. But the last 8 bits are not really used, because BMP files do not really support transparency.

So, you could get tempted to use a different image type: INT_ARGB_PRE which uses a 32-bit color depth. But as soon as you try to save it with the ImageIO class, you will notice that nothing happens. The content of the stream will be null.

BufferedImage img = new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB_PRE);
ImageIO.write(img, "bmp", bos);

Alternative solution: image4j

ImageIO cannot handle 32-bit images, but there are other libraries that can do the trick. The image4J libs can save 32-bit bmp files. But my guess is that for some reason you do not want to use this library. (Using image4J would make most of your code above pointless, because image4jhas built-in ICO creation support).

Second option: creating a shifted 24-bit image -> works

So, let's take a second look at what wikipedia says about < 32-bit BMP data.

The height for the image in the ICONDIRENTRY structure of the ICO/CUR file takes on that of the intended image dimensions (after the masks are composited), whereas the height in the BMP header takes on that of the two mask images combined (before they are composited). Therefore, the masks must each be of the same dimensions, and the height specified in the BMP header must be exactly twice the height specified in the ICONDIRENTRY structure.

So, the second solution is to create an image that is twice the original size. And you actually only have to replace your getImageBytes function for that, with the one below. As mentioned above the ICONDIRENTRY header specified in the other part of your code keeps the original image height.

  private static byte[] getImgBytes(BufferedImage img) throws IOException
  {
    // create a new image, with 2x the original height.
    BufferedImage img2 = new BufferedImage(img.getWidth(), img.getHeight()*2, BufferedImage.TYPE_INT_RGB);

    // copy paste the pixels, but move them half the height.
    Raster sourceRaster = img.getRaster();
    WritableRaster destinationRaster = img2.getRaster();
    destinationRaster.setRect(0, img.getHeight(), sourceRaster);

    // save the new image to BMP format. 
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    ImageIO.write(img2, "bmp", bos);

    // strip the first 14 bytes (contains the bitmap-file-header)
    // the next 40 bytes contains the DIB header which we still need.
    // the pixel data follows until the end of the file.
    byte[] bytes = bos.toByteArray();
    return Arrays.copyOfRange(bytes, 14, bytes.length);
  }

I propose to use the headers as follows:

ByteBuffer bytes = ByteBuffer.allocate(fileSize);
bytes.order(ByteOrder.LITTLE_ENDIAN);

bytes.putShort((short) 0);
bytes.putShort((short) 1);
bytes.putShort((short) 1);
bytes.put((byte) img.getWidth());
bytes.put((byte) img.getHeight()); //no need to multiply
bytes.put((byte) img.getColorModel().getNumColorComponents()); //the pallet size
bytes.put((byte) 0);
bytes.putShort((short) 1); //should be 1
bytes.putShort((short) img.getColorModel().getPixelSize()); //bits per pixel
bytes.putInt(imgBytes.length);
bytes.putInt(22);
bytes.put(imgBytes);
0
assylias On

Have you tried:

bytes.putShort((short) img.getColorModel().getPixelSize()); //bits per pixel

as seen in image4j.BMPEncoder#createInfoHeader, which is called by image4j.ICOEncoder#write?

If there are other issues, most of the relevant code for you seems to be in those two methods.

1
404 Not Found On

I believe bytes.putShort((short) 0);//bits per pixel should be changed to have the value 32, instead of 0.

If you're getting that little picture you edited in after changing the value to 32, then I'm going to say that, on second thought, it's probably actually 16.

2
Holger On

Strange…but: make the BMP picture twice as high as the desired icon. Keep the declared icon size in the ICO header as before, only the picture should be higher. Then keep the area (0,0)-(16,16) black (its defining the transparency but I don’t know how it is encoded, all black for opaque works). Draw the desired contents in the BufferedImage in the area (0,16)-(16,32). In other words, add the half of the height to all pixel coordinates.

Beware that the Windows Desktop might cache icons and refuse to update them on the desktop. If in doubt open the desktop folder through another Explorer window and perform “Update” there.

import java.awt.Color;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;

import javax.imageio.ImageIO;

public class IconWriter
{
    public static void main(String[] args) throws IOException
    {
      // note the double height
        BufferedImage img = new BufferedImage(16, 32, BufferedImage.TYPE_INT_RGB);
        Graphics g = img.getGraphics();
        g.setColor(Color.GREEN);
        g.fillRect(0, 16, 16, 16);// added 16 to y coordinate
        byte[] imgBytes = getImgBytes(img);
        int fileSize = imgBytes.length + 22;
        ByteBuffer bytes = ByteBuffer.allocate(fileSize);
        bytes.order(ByteOrder.LITTLE_ENDIAN);
        bytes.putShort((short) 0);//Reserved must be 0
        bytes.putShort((short) 1);//Image type
        bytes.putShort((short) 1);//Number of images in file
        bytes.put((byte) img.getWidth());//image width
        bytes.put((byte) (img.getHeight()>>1));//image height, half the BMP height
        bytes.put((byte) 0);//number of colors in color palette
        bytes.put((byte) 0);//reserved must be 0
        bytes.putShort((short) 0);//color planes
        bytes.putShort((short) 0);//bits per pixel
        bytes.putInt(imgBytes.length);//image size
        bytes.putInt(22);//image offset
        bytes.put(imgBytes);
        byte[] result = bytes.array();
        FileOutputStream fos = new FileOutputStream(System.getProperty("user.home")+"\\Desktop\\hi.ico");
        fos.write(result);
        fos.close();
        fos.flush();
    }

    private static byte[] getImgBytes(BufferedImage img) throws IOException
    {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ImageIO.write(img, "bmp", bos);
        byte[] bytes = bos.toByteArray();
        return Arrays.copyOfRange(bytes, 14, bytes.length);
    }
}
0
Mahesh Shivaji Mane. On

Please try below, ImageIo will support only png,jpg,jpeg,gif,bmp formats.

To write icons, please add below dependancy.

<!-- https://mvnrepository.com/artifact/net.sf.image4j/image4j -->
<dependency>
    <groupId>net.sf.image4j</groupId>
    <artifactId>image4j</artifactId>
    <version>0.7zensight1</version>
</dependency>

Use ICODecoder.write(image, file);