无法从ico文件中读取图片,因为宽度和高度为0。

3

我正在尝试读取一个包含两个位深度为8的ico图片的ico文件。我了解ICONDIRENTRY格式 (https://msdn.microsoft.com/en-us/library/ms997538.aspx),这段代码大部分都可以工作,除了一些特定的ico文件。以下是我的代码-

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;
    }

}

问题出现在以下区域
iis = ImageIO.createImageInputStream(new ByteArrayInputStream(entry.getImageData()));

此后,我得到的位数是一个大整数而不是实际的8位。因此,在创建这么大的数组时会抛出以下异常—
"java.lang.OutOfMemoryError: Requested array size exceeds VM limit"
无法处理的ico文件为https://www.dropbox.com/s/euh52s0vc2s2ryf/Tool.ico?dl=0

1
构造如colorCount = new Byte(in.readByte()).shortValue();的目的是什么,与直接使用colorCount = in.readByte();相比有何不同?此外,“java.lang.OutOfMemoryError: Requested array size exceeds VM limit”只会在实际数组分配时发生,在getImageData()中没有这种情况。请使用错误的堆栈跟踪。 - Holger
iis = ImageIO.createImageInputStream(new ByteArrayInputStream(entry.getImageData())); 然后我们通过biBitCount = entry.getBitCount()获取位数。 而下面的数组初始化会抛出错误(因为位数错误地变成了一个大数字而不是8)- int nColors = (int)(Math.pow(2, biBitCount)); colors = new RGBQuad[nColors]; - Somesh Dhal
1
表达式 new RGBQuad[nColors]ImageIO.createImageInputStream(new ByteArrayInputStream(entry.getImageData())) 完全不相关。它们是同一个程序的一部分,但这也适用于您代码中的所有其他行。不要选择任意一行并将其称为“有问题的区域”。 - Holger
我并没有随意选择这一行代码。ImageIO.createImageInputStream(new ByteArrayInputStream(entry.getImageData())) 返回的是流,我们从中获取 bitcount 的值,最后计算 ncolors,它负责计算数组的大小。这是一个传统的代码,长期以来几乎可以用于所有 ICO 文件,除了其中的一些文件。因此,我对出现的问题感到有些困惑。 - Somesh Dhal
1
是的,这行代码创建了一个流,你从中读取值。而这个流是由之前构建的数组构造的,并且填充了来自另一个流的数据,偏移量由所有先前的读操作确定。问题出现的数字是所有这些先前操作的结果。你只是选择了其中一个。除此之外,entry.getBitCount() 似乎没有超出范围。问问自己为什么会有所不同(以及为什么在 readBitCountFromImageData 中进行冗余工作)。顺便说一下,(int)(Math.pow(2, biBitCount)) 等同于 1 << biBitCount... - Holger
这是所有的旧代码,已经写了一段时间了。我需要再花点时间来查看代码,然后再回来。 - Somesh Dhal
1个回答

1
对于这个图标,图像数据不遵循tagBITMAPINFOHEADER结构。相反,它们是嵌入的PNG图像,您可以通过第一个单词来识别它们,该单词不是常规大小(当遵循tagBITMAPINFOHEADER结构时),而是PNG图像的魔术词。
您可以通过将IconImage构造函数的开头更改为来验证这一点
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;
        }
…

魔术词是…PNG,第一个字节为0x89,但当您将其作为小端int值阅读时,顺序已被反转,因此它是(('G'<<24)|('N'<<16)|('P'<<8)|0x89)
我让您重构代码以使用通用接口处理两种情况...

谢谢解释,现在有意义了。但有一个问题,是否有一种工具可以确定ico文件是否包含嵌入的png图像? - Somesh Dhal
抱歉,我不知道,过去几年里我没有处理ICO文件的经验。 - Holger

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接