I am currently trying to write my own implementation of an ICO file reader.
And yes, I do know that it is possible using already existing libraries, this is not an option though.
Essentially, I got my reader to work up until the point of image data deserialization. Icon Directory Entries seem to be loaded correctly, but the image data in the .ico
file I am trying to read "doesn't work".
First of all, here's the files related to this question:
ico
file: http://www.mediafire.com/file/sj579gdv0g60t3w/test.icobmp
in it: http://www.mediafire.com/file/jr4xkbqvzpelozo/debug.bmp (corrupt file)The method in question that does not properly work is this:
private static BufferedImage readImage(InputStream stream, IconDirEntry entry) throws IOException {
System.out.println("deserializing "+entry);
byte[] bytes = new byte[entry.data];
stream.read(bytes);
if (!isPNG(bytes)) {
byte[] bmp = makeBitmapFileHeader(bytes);
bytes = PrimArrays.concat(bmp, bytes);
}
return ImageIO.read(new ByteArrayInputStream(bytes));
}
Basically, my approach is checking whether the image data bytes are a PNG and if not, concatenating them with a new header.
This way I can still use ImageIO
to read the bitmap instead of having to make my own bitmap deserializer.
When running the code, the icon directory entries seem to be loaded correctly and are printed into the console:
IconDirEntry{dims=16x16, palette=0, planes=0, bpPixel=32, data=1128, offset=86}
IconDirEntry{dims=32x32, palette=0, planes=0, bpPixel=32, data=4264, offset=1214}
IconDirEntry{dims=48x48, palette=0, planes=0, bpPixel=32, data=9640, offset=5478}
IconDirEntry{dims=64x64, palette=0, planes=0, bpPixel=32, data=16936, offset=15118}
IconDirEntry{dims=128x128, palette=0, planes=0, bpPixel=32, data=67624, offset=32054}
Through all kinds of debugging I have verified that the header does have length of 14 bytes as it should, that the first bytes of the image data array look like a BMP info block etc.
I have also verified that the EOF is reached exactly when all the bytes amounts specified in the icon directory entries are loaded.
Also, the offset in the first IconDirEntry
seems to be correct, since an ico
header has a size of 6 bytes and each of the 5 ICONDIRENTRY
blocks have a size of 16 bytes. (6 + 5*16 = 86)
Everything from net.grian
packages has been tested and is proven to work, the error can not possibly lie there.
Despite everything seemingly working, I am getting the following exception when loading the very first image:
java.io.EOFException
at javax.imageio.stream.ImageInputStreamImpl.readFully(ImageInputStreamImpl.java:353)
at javax.imageio.stream.ImageInputStreamImpl.readFully(ImageInputStreamImpl.java:405)
at com.sun.imageio.plugins.bmp.BMPImageReader.read32Bit(BMPImageReader.java:1353)
at com.sun.imageio.plugins.bmp.BMPImageReader.read(BMPImageReader.java:890)
at javax.imageio.ImageIO.read(ImageIO.java:1448)
at javax.imageio.ImageIO.read(ImageIO.java:1352)
at me.headaxe.imtu.io.DeserializerICO.readImage(DeserializerICO.java:103)
at me.headaxe.imtu.io.DeserializerICO.fromStream(DeserializerICO.java:54)
at me.headaxe.imtu.io.DeserializerICO.fromStream(DeserializerICO.java:27)
at me.headaxe.imtu.io.DeserializerICO.fromStream(DeserializerICO.java:18)
All I really need is a hint as to why the image data can not be loaded correctly despite the rest of my code working flawlessly.
(the raw bytes consisting of the "artificial" bmp header + the image data from the ico file are provided for download above)
When I read exactly 960 more bytes than necessary when loading the first bmp in the ico file, I am no longer getting an EOFException
, instead the first bitmap gets loaded and looks like this:
Due to that offset the second bitmap will obviously fail to load with an
IIOException
since it is malformed.
Direct comparison between the 16x
ico and the malformed bmp:
Printing all byte arrays before trying to deserialize them as Bitmaps yields the following results:
This suggests that the amount of bytes being buffered is correct, since they all begin with the iconic
40
, which is the length if the BITMAPINFOHEADER
structure.
Used a different ->ico
converter this time, effect stays the same in the sense that buffering more than the data
field specifies produces an actual bmp
file:
Bytes are:
[40, 0, 0, 0, 32, 0, 0, 0, 64, 0, 0, 0, 1, 0, 32, 0, 0, 0, 0, 0, 0, 16, 0, 0, 35, 46, 0, 0, 35, 46, 0 ...
Since the 32
should be the image width and the 64
should be the image height, I am seriously confused since the ico is a 32x32
one.
In the second test, the .ico
file is 4286 bytes large.
The first and only ICONDIRENTRY
has an offset of 22
and data of 4264
.
Despite that being perfectly correct and the correct amount of bytes being allocated, reading the bmp fails.
private static BufferedImage readImage(InputStream stream, IconDirEntry entry) throws IOException {
byte[] bytes = new byte[entry.data];
stream.read(bytes);
if (!isPNG(bytes)) {
setLittleInt(bytes, 8, entry.height);
byte[] bmp = makeBitmapFileHeader(bytes);
bytes = PrimArrays.concat(bmp, bytes);
}
if (DEBUG_FILE.exists() && entry.width == 32)
new SerializerByteArray().toFile(bytes, DEBUG_FILE);
return ImageIO.read(new ByteArrayInputStream(bytes));
}
All that was required to make it work is line #5.
Apparently you can't rely on bitmaps stored in .ico
files not to be corrupted garbage, so manually correcting the byte array fixes the "problem".
I have no words for this, I am completely dumbstruck. Well, if anyone has an explanation for why such a thing would possibly fix it, please comment.
If anyone wonders, this is setLittleInt
:
private static void setLittleInt(byte[] bytes, int index, int value) {
byte[] ins = IOMath.toBytes(value);
bytes[index+3] = ins[0];
bytes[index+2] = ins[1];
bytes[index+1] = ins[2];
bytes[index] = ins[3];
}