3

I am trying to read an ico file which contains two ico images of bitcount = 8. I am aware of the ICONDIRENTRY format (https://msdn.microsoft.com/en-us/library/ms997538.aspx) and this code mostly works apart from few specific ico files.Below is my code-

import javax.imageio.ImageIO;
import javax.imageio.stream.ImageInputStream;
import java.io.FileInputStream;
import java.nio.ByteOrder;
import java.util.ArrayList;

/**
 * Created by dsomesh8 on 5/25/2018.
 */
public class Program {
    private static ArrayList<IconDirEntry> iconDirEntries;


    private static final byte SEED = -67;
    private static final byte SEED2 = 107;
    private static final String HEADER = "@OB@";
    private static final char[] DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
    private static ImageInputStream in;


    public  static void main(String[] args) {
        FileInputStream fis = null;
        try {
            //C:\Users\dsomesh8\Downloads\Logs\test\Tool.ico
            //C:\Users\dsomesh8\Downloads\icons\zen.ico
            String filePath = "C:\\Users\\dsomesh8\\Downloads\\Logs\\test\\Tool.ico";
            //String filePath="C:\\Users\\tdivya\\Downloads\\test.ico";
            fis = new FileInputStream(filePath);
            in = ImageIO.createImageInputStream(fis);
            ArrayList<IconDirEntry> list=decodeIcon(in);
            IconImage nweIcon=new IconImage(list.get(0));
            //iconDirEntries = new ArrayList<IconDirEntry>();
            //boolean res = ;
        } catch (java.io.FileNotFoundException fnfe) {
            //WebLogger.debug("Input icon file " + filePath + " is missing");
        } catch (java.io.IOException ioe) {
            //WebLogger.debug("IO Exception reading the icon file " + filePath);
        }
    }

    private static ArrayList<IconDirEntry> decodeIcon(ImageInputStream in)
    {
        try
        {
            in.setByteOrder(ByteOrder.LITTLE_ENDIAN);

            in.readShort();             // idReserved field

            if(in.readShort() != 1)    // idType field
                return null;

            int imgCount = in.readShort();  //No of icon entries

            iconDirEntries = new ArrayList<IconDirEntry>();
            System.out.println(imgCount);
            for(int i = 0; i < imgCount; i++)
            {

                IconDirEntry dirEntry = new IconDirEntry(in);
                System.out.println(dirEntry.toString());
                iconDirEntries.add(dirEntry);
            }



        }
        catch(java.io.IOException ioe)
        {
            // WebLogger.debug("IOException reading the reserved field of the icon");
            return null;
        }
        return  iconDirEntries;
    }
}


import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;

import javax.imageio.ImageIO;
import javax.imageio.stream.ImageInputStream;


/*
typdef struct
{
   BITMAPINFOHEADER   icHeader;   // DIB header
   RGBQUAD         icColors[1];   // Color table
   BYTE            icXOR[1];      // DIB bits for XOR mask
   BYTE            icAND[1];      // DIB bits for AND mask
} ICONIMAGE, *LPICONIMAGE;

typedef struct tagBITMAPINFOHEADER{
  DWORD  biSize;
  LONG   biWidth;
  LONG   biHeight;
  WORD   biPlanes;
  WORD   biBitCount;
  DWORD  biCompression;
  DWORD  biSizeImage;
  LONG   biXPelsPerMeter;
  LONG   biYPelsPerMeter;
  DWORD  biClrUsed;
  DWORD  biClrImportant;
} BITMAPINFOHEADER, *PBITMAPINFOHEADER;

typedef struct tagRGBQUAD {
  BYTE    rgbBlue;
  BYTE    rgbGreen;
  BYTE    rgbRed;
  BYTE    rgbReserved;
} RGBQUAD;
*/


public class IconImage
{
    private int biSize;
    private int biWidth;
    private int biHeight;
    private int biPlanes;
    private int biBitCount;
    private int biCompression;
    private int biSizeImage;
    private int biXPelsPerMeter;
    private int biYPelsPerMeter;
    private int biClrUsed;
    private int biClrImportant;

    private byte[] rgbQuad;
    private byte[] icXOR;
    private byte[] icAND;

    private RGBQuad[] colors;
    private byte[] andMask;
    private byte[] xorMask;

    private IconDirEntry entry;
    private ImageInputStream iis;

    public IconImage(IconDirEntry entry)
    {
        this.entry = entry;

        try
        {
            iis = ImageIO.createImageInputStream(new ByteArrayInputStream(entry.getImageData()));
            iis.setByteOrder(java.nio.ByteOrder.LITTLE_ENDIAN);

            biSize = iis.readInt();
            biWidth = iis.readInt();
            biHeight = iis.readInt();
            biPlanes = iis.readShort();
            biBitCount = iis.readShort();

            biCompression = iis.readInt();
            biSizeImage = iis.readInt();
            biXPelsPerMeter = iis.readInt();
            biYPelsPerMeter = iis.readInt();
            biClrUsed = iis.readInt();
            biClrImportant = iis.readInt();

            if(entry.getBitCount() <= 8)
            {
                int nColors = (int)(Math.pow(2, biBitCount));
                colors = new RGBQuad[nColors]; //color table specifying colors uses in the image

                for(int i = 0; i < colors.length; i++)
                {
                    colors[i] = new RGBQuad(iis);
                }

                int bitsPerPixel = biBitCount;
                int pixelsPerByte = 8/bitsPerPixel;
                int nPixels = biWidth*biHeight/2; //biHeight is twice of actual height
                int nBytes = nPixels/pixelsPerByte;

                xorMask = new byte[nBytes];
                for(int i = 0; i < nBytes; i++)
                {
                    xorMask[i] = (byte)iis.readUnsignedByte();
                }

                int paddedWidth = 0;
                if(biWidth <= 32)
                    paddedWidth = 32;
                else
                {
                    int rem = biWidth%32;
                    if(rem == 0)
                        paddedWidth = biWidth;
                    else
                        paddedWidth = (biWidth/32 + 1)*32; //Round off to the next multiple of 32
                }

                int len = paddedWidth*(biHeight/2)/8;
                //AND mask is a monochrome DIB, with a color depth of 1 bpp
                andMask = new byte[len];
                for(int i = 0; i < len; i++)
                {
                    andMask[i] = (byte)iis.readUnsignedByte();
                }
            }

        }
        catch(Exception ioe)
        {
            System.out.println("Exception while reading image details for icon entry");
        }

    }

    public int[] getPixelValues()
    {
        int nRows = entry.getHeight();
        int nCols = entry.getWidth();
        int bpp = entry.getBitCount()/8; //Bytes per pixel
        int[] pixelValues = new int[nRows*nCols];

        for(int row = 0; row < nRows; row++)
        {

            byte[] rowData = new byte[nCols*bpp];
            try
            {
                iis.readFully(rowData);
            }
            catch(Exception e)
            {
                System.out.println("Exception reading the image data for this entry!!!");
            }

            int curRow = nRows - row; //Moving upwards starting from the last row
            int pos = (curRow - 1)*nCols; //Index of first pixel at current row

            int iByte = 0; //Iterator for each byte

            for(int col = 0; col < nCols; col++)
            {
                int pixelValue = 0;

                pixelValue = (rowData[iByte++] & 0xFF);

                if(bpp > 1)
                    pixelValue += ((rowData[iByte++] & 0xFF) << 8);

                if(bpp > 2)
                    pixelValue += ((rowData[iByte++] & 0xFF) << 16);

                if(bpp > 3)
                    pixelValue += ((rowData[iByte++] & 0xFF) << 24);
                else
                {
                    //if (pixelValue == 0)
                    pixelValue += ((255 & 0xFF) << 24);
                }

                pixelValues[pos] = pixelValue;
                pos++;
            }
        }

        return pixelValues;
    }

    public BufferedImage getIconGraphics()
    {
        BufferedImage buffImg = new BufferedImage(entry.getWidth(), entry.getHeight(), BufferedImage.TYPE_INT_ARGB);
        final Color TRANSPARENT = new Color(0, 0, 0, 0);

        Graphics2D g = buffImg.createGraphics();
        for(int y = biHeight/2 - 1; y >= 0; y--)
        {
            for(int x = 0; x < biWidth; x++)
            {
                if(isTransparent(x, y))
                    g.setColor(TRANSPARENT);
                else
                    g.setColor(getRGB(x, y));

                g.fillRect(x, entry.getHeight() - y - 1, 1, 1);
            }

        }

        return buffImg;
    }

    private boolean isTransparent(int x, int y)
    {
        int paddedWidth = 0;
        if(biWidth <= 32)
            paddedWidth = 32;
        else
        {
            int rem = biWidth%32;
            if(rem == 0)
                paddedWidth = biWidth;
            else
                paddedWidth = (biWidth/32 + 1)*32; //Round off to the next multiple of 32
        }

        int pixelIndex = (paddedWidth*y) + x;
        int andByteIndex = pixelIndex/8;
        int andByte = andMask[andByteIndex];
        int pos = x%8; //position of bit in the byte, for pixel x,y
        int nRightShift = 8 - (pos + 1); //Right shift needed to get the bit to LSB; increment of 1 since x starts from 0
        int pixelBit = andByte >> nRightShift;
        int andMask = pixelBit & 1;
        return (andMask == 1);
    }

    private Color getRGB(int x, int y)
    {
        int pixelIndex = (biWidth*y) + x;
        int bitsPerPixel = biBitCount;
        int pixelsPerByte = 8/bitsPerPixel;
        int xorByteIndex = pixelIndex/pixelsPerByte;

        int shift = ((pixelsPerByte - (x%pixelsPerByte) - 1)*biBitCount);
        int colIdx = (xorMask[xorByteIndex] >> shift) & ((1 << biBitCount) - 1);

        int b = colors[colIdx].getBlue();
        int g = colors[colIdx].getGreen();
        int r = colors[colIdx].getRed();

        return new Color(r, g, b);
    }

}


import com.sun.imageio.plugins.common.ReaderUtil;

import java.io.ByteArrayInputStream;
import java.io.IOException;

import javax.imageio.ImageIO;
import javax.imageio.stream.ImageInputStream;



/*
typedef struct
{
    BYTE        bWidth;          // Width, in pixels, of the image
    BYTE        bHeight;         // Height, in pixels, of the image
    BYTE        bColorCount;     // Number of colors in image (0 if >=8bpp)
    BYTE        bReserved;       // Reserved ( must be 0)
    WORD        wPlanes;         // Color Planes
    WORD        wBitCount;       // Bits per pixel
    DWORD       dwBytesInRes;    // How many bytes in this resource?
    DWORD       dwImageOffset;   // Where in the file is this image?
} ICONDIRENTRY, *LPICONDIRENTRY;
*/

public class IconDirEntry
{
    private short width;
    private short height;
    private short colorCount;
    private short reserved;

    private int planes;
    private int bitCount;
    private int bytesInResource;
    private int imageOffset;

    private byte[] imgData;

    public IconDirEntry(ImageInputStream in)
    {
        try
        {
            //System.out.println("canDecodeInput-"+canDecodeInput(in));
           // bitCount = readBitCountFromImageData(imgData);
            width = (short)in.readUnsignedByte();

            height = (short)in.readUnsignedByte();
            colorCount = new Byte(in.readByte()).shortValue();
            reserved = new Byte(in.readByte()).shortValue();

            planes = in.readShort();
            bitCount = in.readShort();
            bytesInResource = in.readInt();
            imageOffset = in.readInt();

            /*
            System.out.println("val : " + width);
            System.out.println("val : " + height);
            System.out.println("val : " + colorCount);
            System.out.println("val : " + reserved);

            System.out.println("val : " + planes);
            System.out.println("val : " + bitCount);
            System.out.println("val : " + bytesInResource);
            System.out.println("val : " + imageOffset);
            System.out.println("\n");
            */

            in.mark();

            long curPos = in.getStreamPosition();
            int nBytesToSkip = imageOffset - (int)curPos;
            in.skipBytes(nBytesToSkip);

            imgData = new byte[bytesInResource];
            try
            {
                in.read(imgData);
            }
            finally
            {
                in.reset();
            }

            // Certain icons will not specify the bitCount at the icon entry level.
            // For such cases, read the bitCount from the image data
            if(bitCount == 0 && imageOffset > 0)
                bitCount = readBitCountFromImageData(imgData);

        }
        catch(Exception e)
        {
            System.out.println("Exception reading icon entry");
        }
    }


    /*
     * Image data structure:
            typdef struct
            {
               BITMAPINFOHEADER   icHeader;   // DIB header
               RGBQUAD         icColors[1];   // Color table
               BYTE            icXOR[1];      // DIB bits for XOR mask
               BYTE            icAND[1];      // DIB bits for AND mask
            } ICONIMAGE, *LPICONIMAGE;

            typedef struct tagBITMAPINFOHEADER{
              DWORD  biSize;
              LONG   biWidth;
              LONG   biHeight;
              WORD   biPlanes;
              WORD   biBitCount;
              DWORD  biCompression;
              DWORD  biSizeImage;
              LONG   biXPelsPerMeter;
              LONG   biYPelsPerMeter;
              DWORD  biClrUsed;
              DWORD  biClrImportant;
            } BITMAPINFOHEADER, *PBITMAPINFOHEADER;
    *
    * Read biBitCount
    */
    private int readBitCountFromImageData(byte[] imgData) throws IOException
    {
        ImageInputStream iis = ImageIO.createImageInputStream(new ByteArrayInputStream(imgData));
        iis.setByteOrder(java.nio.ByteOrder.LITTLE_ENDIAN);

        // These many number of bytes can actually be skipped. Reading for code clarity.
        iis.readInt();   // biSize
        iis.readInt();   // biWidth
        iis.readInt();   // biHeight
        iis.readShort(); // biPlanes

        int biBitCount = iis.readShort();

        return biBitCount;
    }


    public short getWidth()
    {
        return width;
    }

    public short getHeight()
    {
        return height;
    }

    public int getBitCount()
    {
        return bitCount;
    }

    public byte[] getImageData()
    {
        return imgData;
    }

}

The problematic area is
iis = ImageIO.createImageInputStream(new ByteArrayInputStream(entry.getImageData()));

post this I am getting the bit count as a big integer number though the actual bit count is 8.So while creating an array of that big size throws the following exception-

"java.lang.OutOfMemoryError: Requested array size exceeds VM limit"

The ico file for which it is failing is https://www.dropbox.com/s/euh52s0vc2s2ryf/Tool.ico?dl=0

UVM
  • 9,776
  • 6
  • 41
  • 66
Somesh Dhal
  • 336
  • 2
  • 15
  • 1
    What is the purpose of constructs like `colorCount = new Byte(in.readByte()).shortValue();`, compared to a straight-forward `colorCount = in.readByte();`? Besides that, a “"java.lang.OutOfMemoryError: Requested array size exceeds VM limit” can only happen at an actual array allocation and there is none in `getImageData()`. Use the error’s stack trace. – Holger May 25 '18 at 11:22
  • iis = ImageIO.createImageInputStream(new ByteArrayInputStream(entry.getImageData())); Post this we get the bit count by biBitCount = entry.getBitCount(); And the following array initialization throws error(Because bitcount wrongly comes as a large number istead of 8)- int nColors = (int)(Math.pow(2, biBitCount)); colors = new RGBQuad[nColors]; – Somesh Dhal May 25 '18 at 11:37
  • 1
    The expression `new RGBQuad[nColors]` is entirely unrelated to the other expression `ImageIO.createImageInputStream(new ByteArrayInputStream(entry.getImageData()))`. It’s part of the same program, but that applies to all other lines of your code as well. Don’t pick an arbitrary line and call it the “problematic area”. – Holger May 25 '18 at 11:42
  • I did not pick it as a random line.ImageIO.createImageInputStream(new ByteArrayInputStream(entry.getImageData())).which returns the stream from which we get the value of bitcount and finally we calculate ncolors which is resposible to calculate the size of array.This is legacy code has been working from long time for almost ico files barring a couple of them.So,I am bit clueless here about what is going wrong. – Somesh Dhal May 25 '18 at 11:50
  • 1
    Yes that line creates the stream you’re reading the value from. And that stream is constructed from the array you’ve constructed before that and filled with data from another stream from an offset determined by all previous read operations. The problematic number is the result of *all* these previous operations. You just picked one of them. Besides that, `entry.getBitCount()` does not seem to be out of range. Ask yourself why this is different (and why you are doing redundant work in `readBitCountFromImageData`). By the way, `(int)(Math.pow(2, biBitCount))` is `1 << biBitCount`… – Holger May 25 '18 at 11:58
  • This is all legacy code and has been written from some time.I will take some more time to look into the code and would come back. – Somesh Dhal May 25 '18 at 12:19

1 Answers1

1

For this icon, the image data does not follow the tagBITMAPINFOHEADER structure. Instead, they are embedded PNG images, which you can recognize at the first word which isn’t a regular size (which it would when following the tagBITMAPINFOHEADER structure), but the magic word of PNG images.

You can verify this by changing the beginning of IconImage’s constructor to

public IconImage(IconDirEntry entry)
{
    this.entry = entry;
    try
    {
        final ByteArrayInputStream bais = new ByteArrayInputStream(entry.getImageData());
        bais.mark(4);
        iis = ImageIO.createImageInputStream(bais);
        iis.setByteOrder(java.nio.ByteOrder.LITTLE_ENDIAN);
        biSize = iis.readInt();
        if(biSize == 0x474e5089) { //PNG instead of tagBITMAPINFOHEADER)
            bais.reset();
            BufferedImage bi = ImageIO.read(bais);
            System.out.println("read embedded PNG "+bi.getWidth()+" x "+bi.getHeight());
            return;
        }
…

The magic word is …PNG the first byte being 0x89, but the order has been reversed as you read it as a little endian int value, so its (('G'<<24)|('N'<<16)|('P'<<8)|0x89).

I leave it up to you to restructure your code to handle both cases with a common interface…

Holger
  • 285,553
  • 42
  • 434
  • 765
  • Thanks for the explanation.Makes sense now.One question though.Is there an way to figure out if the ico file contains embedded pngs by using some tool? – Somesh Dhal May 25 '18 at 14:53
  • I don't know, I didn't have to deal with ICO files in the last years. Sorry. – Holger May 26 '18 at 10:54