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 image4j
has 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);