package sgdk.tool;

/*
 * ImageInfo.java
 *
 * Version 1.91
 *
 * A Java class to determine image width, height and color depth for
 * a number of image file formats.
 *
 * Written by Marco Schmidt
 * Modified by Stephane Dallongeville
 *
 * Contributed to the Public Domain.
 */

import java.io.DataInput;
import java.io.IOException;
import java.io.InputStream;
import java.util.Vector;

/**
 * Get file format, image resolution, number of bits per pixel and optionally
 * number of images, comments and physical resolution from
 * JPEG, GIF, BMP, PCX, PNG, IFF, RAS, PBM, PGM, PPM and PSD files
 * (or input streams).
 * <p>
 * Use the class like this:
 * 
 * <pre>
 * ImageInfo ii = new ImageInfo();
 * ii.setInput(in); // in can be InputStream or RandomAccessFile
 * ii.setDetermineImageNumber(true); // default is false
 * ii.setCollectComments(true); // default is false
 * if (!ii.check())
 * {
 *     System.err.println("Not a supported image file format.");
 *     return;
 * }
 * System.out.println(ii.getFormatName() + ", " + ii.getMimeType() + ", " + ii.getWidth() + " x " + ii.getHeight()
 *         + " pixels, " + ii.getBitsPerPixel() + " bits per pixel, " + ii.getNumberOfImages() + " image(s), "
 *         + ii.getNumberOfComments() + " comment(s).");
 * // there are other properties, check out the API documentation
 * </pre>
 * 
 * You can also use this class as a command line program.
 * Call it with a number of image file names and URLs as parameters:
 * 
 * <pre>
 *   java ImageInfo *.jpg *.png *.gif http://somesite.tld/image.jpg
 * </pre>
 * 
 * or call it without parameters and pipe data to it:
 * 
 * <pre>
 *   java ImageInfo &lt; image.jpg
 * </pre>
 * <p>
 * Known limitations:
 * <ul>
 * <li>When the determination of the number of images is turned off, GIF bits
 * per pixel are only read from the global header.
 * For some GIFs, local palettes change this to a typically larger
 * value. To be certain to get the correct color depth, call
 * setDetermineImageNumber(true) before calling check().
 * The complete scan over the GIF file will take additional time.</li>
 * <li>Transparency information is not included in the bits per pixel count.
 * Actually, it was my decision not to include those bits, so it's a feature! ;-)</li>
 * </ul>
 * <p>
 * Requirements:
 * <ul>
 * <li>Java 1.1 or higher</li>
 * </ul>
 * <p>
 * The latest version can be found at
 * <a href="http://schmidt.devlib.org/image-info/">http://schmidt.devlib.org/image-info/</a>.
 * <p>
 * Written by Marco Schmidt.
 * <p>
 * This class is contributed to the Public Domain.
 * Use it at your own risk.
 * <p>
 * <a name="history">History</a>:
 * <ul>
 * <li><strong>2001-08-24</strong> Initial version.</li>
 * <li><strong>2001-10-13</strong> Added support for the file formats BMP and PCX.</li>
 * <li><strong>2001-10-16</strong> Fixed bug in read(int[], int, int) that returned
 * <li><strong>2002-01-22</strong> Added support for file formats Amiga IFF and Sun Raster (RAS).
 * </li>
 * <li><strong>2002-01-24</strong> Added support for file formats Portable Bitmap / Graymap / Pixmap
 * (PBM, PGM, PPM) and Adobe Photoshop (PSD).
 * Added new method getMimeType() to return the MIME type associated with a particular file format.
 * </li>
 * <li><strong>2002-03-15</strong> Added support to recognize number of images in file. Only works
 * with GIF.
 * Use {@link #setDetermineImageNumber} with <code>true</code> as argument to identify animated GIFs
 * ({@link #getNumberOfImages()} will return a value larger than <code>1</code>).</li>
 * <li><strong>2002-04-10</strong> Fixed a bug in the feature 'determine number of images in
 * animated GIF' introduced with version 1.1.
 * Thanks to Marcelo P. Lima for sending in the bug report.
 * Released as 1.1.1.</li>
 * <li><strong>2002-04-18</strong> Added {@link #setCollectComments(boolean)}.
 * That new method lets the user specify whether textual comments are to be
 * stored in an internal list when encountered in an input image file / stream.
 * Added two methods to return the physical width and height of the image in dpi:
 * {@link #getPhysicalWidthDpi()} and {@link #getPhysicalHeightDpi()}.
 * If the physical resolution could not be retrieved, these methods return <code>-1</code>.
 * </li>
 * <li><strong>2002-04-23</strong> Added support for the new properties physical resolution and
 * comments for some formats. Released as 1.2.</li>
 * <li><strong>2002-06-17</strong> Added support for SWF, sent in by Michael Aird.
 * Changed checkJpeg() so that other APP markers than APP0 will not lead to a failure anymore.
 * Released as 1.3.</li>
 * <li><strong>2003-07-28</strong> Bug fix - skip method now takes return values into consideration.
 * Less bytes than necessary may have been skipped, leading to flaws in the retrieved information in
 * some cases.
 * Thanks to Bernard Bernstein for pointing that out.
 * Released as 1.4.</li>
 * <li><strong>2004-02-29</strong> Added support for recognizing progressive JPEG and
 * interlaced PNG and GIF. A new method {@link #isProgressive()} returns whether ImageInfo
 * has found that the storage type is progressive (or interlaced).
 * Thanks to Joe Germuska for suggesting the feature.
 * Bug fix: BMP physical resolution is now correctly determined.
 * Released as 1.5.</li>
 * <li><strong>2004-11-30</strong> Bug fix: recognizing progressive GIFs
 * (interlaced in GIF terminology) did not work (thanks to Franz Jeitler for
 * pointing this out). Now it should work, but only if the number of images is determined.
 * This is because information on interlacing is stored in a local image header.
 * In theory, different images could be stored interlaced and non-interlaced in one
 * file. However, I think that's unlikely. Right now, the last image in the GIF file
 * that is examined by ImageInfo is used for the "progressive" status.</li>
 * <li><strong>2005-01-02</strong> Some code clean up (unused methods and variables
 * commented out, missing javadoc comments, etc.). Thanks to George Sexton for a long list.
 * Removed usage of Boolean.toString because
 * it's a Java 1.4+ feature (thanks to Gregor Dupont).
 * Changed delimiter character in compact output from semicolon to tabulator
 * (for better integration with cut(1) and other Unix tools).
 * Added some points to the
 * <a href="http://schmidt.devlib.org/image-info/index.html#knownissues">'Known
 * issues' section of the website</a>.
 * Released as 1.6.</li>
 * <li><strong>2005-07-26</strong> Removed code to identify Flash (SWF) files.
 * Has repeatedly led to problems and support requests, and I don't know the
 * format and don't have the time and interest to fix it myself.
 * I repeatedly included fixes by others which didn't work for some people.
 * I give up on SWF. Please do not contact me about it anymore.
 * Set package of ImageInfo class to org.devlib.schmidt.imageinfo (a package
 * was repeatedly requested by some users).
 * Released as 1.7.</li>
 * <li><strong>2006-02-23</strong> Removed Flash helper methods which weren't used elsewhere.
 * Updated skip method which tries "read" whenever "skip(Bytes)" returns a result of 0.
 * The old method didn't work with certain input stream types on truncated data streams.
 * Thanks to Martin Leidig for reporting this and sending in code.
 * Released as 1.8.</li>
 * </li>
 * <li><strong>2006-11-13</strong> Removed check that made ImageInfo report JPEG APPx
 * markers smaller than 14 bytes as files in unknown format. Such JPEGs seem to be
 * generated by Google's Picasa application. First reported with fix by
 * Karl von Randow. Released as 1.9.</li>
 * </ul>
 * 
 * @author Marco Schmidt
 */
public class ImageInfo
{
    /**
     * Return value of {@link #getFormat()} for JPEG streams.
     * ImageInfo can extract physical resolution and comments
     * from JPEGs (only from APP0 headers).
     * Only one image can be stored in a file.
     * It is determined whether the JPEG stream is progressive
     * (see {@link #isProgressive()}).
     */
    public static final int FORMAT_JPEG = 0;

    /**
     * Return value of {@link #getFormat()} for GIF streams.
     * ImageInfo can extract comments from GIFs and count the number
     * of images (GIFs with more than one image are animations).
     * It is determined whether the GIF stream is interlaced (see {@link #isProgressive()}).
     */
    public static final int FORMAT_GIF = 1;

    /**
     * Return value of {@link #getFormat()} for PNG streams.
     * PNG only supports one image per file.
     * Both physical resolution and comments can be stored with PNG,
     * but ImageInfo is currently not able to extract those.
     * It is determined whether the PNG stream is interlaced (see {@link #isProgressive()}).
     */
    public static final int FORMAT_PNG = 2;

    /**
     * Return value of {@link #getFormat()} for BMP streams.
     * BMP only supports one image per file.
     * BMP does not allow for comments.
     * The physical resolution can be stored.
     */
    public static final int FORMAT_BMP = 3;

    /**
     * Return value of {@link #getFormat()} for PCX streams.
     * PCX does not allow for comments or more than one image per file.
     * However, the physical resolution can be stored.
     */
    public static final int FORMAT_PCX = 4;

    /**
     * Return value of {@link #getFormat()} for IFF streams.
     */
    public static final int FORMAT_IFF = 5;

    /**
     * Return value of {@link #getFormat()} for RAS streams.
     * Sun Raster allows for one image per file only and is not able to
     * store physical resolution or comments.
     */
    public static final int FORMAT_RAS = 6;

    /** Return value of {@link #getFormat()} for PBM streams. */
    public static final int FORMAT_PBM = 7;

    /** Return value of {@link #getFormat()} for PGM streams. */
    public static final int FORMAT_PGM = 8;

    /** Return value of {@link #getFormat()} for PPM streams. */
    public static final int FORMAT_PPM = 9;

    /** Return value of {@link #getFormat()} for PSD streams. */
    public static final int FORMAT_PSD = 10;

    /*
     * public static final int COLOR_TYPE_UNKNOWN = -1;
     * public static final int COLOR_TYPE_TRUECOLOR_RGB = 0;
     * public static final int COLOR_TYPE_PALETTED = 1;
     * public static final int COLOR_TYPE_GRAYSCALE= 2;
     * public static final int COLOR_TYPE_BLACK_AND_WHITE = 3;
     */

    /**
     * The names of all supported file formats.
     * The FORMAT_xyz int constants can be used as index values for
     * this array.
     */
    private static final String[] FORMAT_NAMES = {"JPEG", "GIF", "PNG", "BMP", "PCX", "IFF", "RAS", "PBM", "PGM", "PPM",
            "PSD"};

    /**
     * The names of the MIME types for all supported file formats.
     * The FORMAT_xyz int constants can be used as index values for
     * this array.
     */
    private static final String[] MIME_TYPE_STRINGS = {"image/jpeg", "image/gif", "image/png", "image/bmp", "image/pcx",
            "image/iff", "image/ras", "image/x-portable-bitmap", "image/x-portable-graymap", "image/x-portable-pixmap",
            "image/psd"};

    private int width;
    private int height;
    private int bitsPerPixel;
    // private int colorType = COLOR_TYPE_UNKNOWN;
    private boolean progressive;
    private int format;
    private InputStream in;
    private DataInput din;
    private boolean collectComments = true;
    private Vector<String> comments;
    private boolean determineNumberOfImages;
    private int numberOfImages;
    private int physicalHeightDpi;
    private int physicalWidthDpi;

    private void addComment(String s)
    {
        if (comments == null)
            comments = new Vector<>();

        comments.addElement(s);
    }

    /**
     * Call this method after you have provided an input stream or file
     * using {@link #setInput(InputStream)} or {@link #setInput(DataInput)}.
     * If true is returned, the file format was known and information
     * on the file's content can be retrieved using the various getXyz methods.
     * 
     * @return if information could be retrieved from input
     * @throws IOException
     */
    public boolean check() throws IOException
    {
        format = -1;
        width = -1;
        height = -1;
        bitsPerPixel = -1;
        numberOfImages = 1;
        physicalHeightDpi = -1;
        physicalWidthDpi = -1;
        comments = null;

        final int b1 = read() & 0xff;
        final int b2 = read() & 0xff;

        if (b1 == 0x47 && b2 == 0x49)
            return checkGif();
        else if (b1 == 0x89 && b2 == 0x50)
            return checkPng();
        else if (b1 == 0xff && b2 == 0xd8)
            return checkJpeg();
        else if (b1 == 0x42 && b2 == 0x4d)
            return checkBmp();
        else if (b1 == 0x0a && b2 < 0x06)
            return checkPcx();
        else if (b1 == 0x46 && b2 == 0x4f)
            return checkIff();
        else if (b1 == 0x59 && b2 == 0xa6)
            return checkRas();
        else if (b1 == 0x50 && b2 >= 0x31 && b2 <= 0x36)
            return checkPnm(b2 - '0');
        else if (b1 == 0x38 && b2 == 0x42)
            return checkPsd();

        return false;
    }

    private boolean checkBmp() throws IOException
    {
        byte[] a = new byte[44];
        if (read(a) != a.length)
            return false;

        width = getIntLittleEndian(a, 16);
        height = getIntLittleEndian(a, 20);
        if (width < 1 || height < 1)
            return false;

        bitsPerPixel = getShortLittleEndian(a, 26);
        if (bitsPerPixel != 1 && bitsPerPixel != 4 && bitsPerPixel != 8 && bitsPerPixel != 16 && bitsPerPixel != 24
                && bitsPerPixel != 32)
            return false;

        int x = (int) (getIntLittleEndian(a, 36) * 0.0254);
        if (x > 0)
            setPhysicalWidthDpi(x);

        int y = (int) (getIntLittleEndian(a, 40) * 0.0254);
        if (y > 0)
            setPhysicalHeightDpi(y);

        format = FORMAT_BMP;

        return true;
    }

    private boolean checkGif() throws IOException
    {
        final byte[] GIF_MAGIC_87A = {0x46, 0x38, 0x37, 0x61};
        final byte[] GIF_MAGIC_89A = {0x46, 0x38, 0x39, 0x61};

        byte[] a = new byte[11]; // 4 from the GIF signature + 7 from the global header
        if (read(a) != 11)
            return false;

        if ((!equals(a, 0, GIF_MAGIC_89A, 0, 4)) && (!equals(a, 0, GIF_MAGIC_87A, 0, 4)))
            return false;

        format = FORMAT_GIF;
        width = getShortLittleEndian(a, 4);
        height = getShortLittleEndian(a, 6);
        int flags = a[8] & 0xff;
        bitsPerPixel = ((flags >> 4) & 0x07) + 1;
        // progressive = (flags & 0x02) != 0;
        if (!determineNumberOfImages)
            return true;

        // skip global color palette
        if ((flags & 0x80) != 0)
        {
            int tableSize = (1 << ((flags & 7) + 1)) * 3;
            skip(tableSize);
        }

        numberOfImages = 0;
        int blockType;
        do
        {
            blockType = read();
            switch (blockType)
            {
                case (0x2c): // image separator
                {
                    if (read(a, 0, 9) != 9)
                        return false;

                    flags = a[8] & 0xff;
                    progressive = (flags & 0x40) != 0;

                    /*
                     * int locWidth = getShortLittleEndian(a, 4);
                     * int locHeight = getShortLittleEndian(a, 6);
                     * System.out.println("LOCAL: " + locWidth + " x " + locHeight);
                     */
                    int localBitsPerPixel = (flags & 0x07) + 1;
                    if (localBitsPerPixel > bitsPerPixel)
                        bitsPerPixel = localBitsPerPixel;

                    if ((flags & 0x80) != 0)
                        skip((1 << localBitsPerPixel) * 3);

                    skip(1); // initial code length

                    int n;
                    do
                    {
                        n = read();
                        if (n > 0)
                            skip(n);
                        else if (n == -1)
                            return false;
                    }
                    while (n > 0);

                    numberOfImages++;
                    break;
                }

                case (0x21): // extension
                {
                    int extensionType = read();

                    if (collectComments && extensionType == 0xfe)
                    {
                        StringBuffer sb = new StringBuffer();
                        int n;
                        do
                        {
                            n = read();
                            if (n == -1)
                                return false;

                            if (n > 0)
                            {
                                for (int i = 0; i < n; i++)
                                {
                                    int ch = read();
                                    if (ch == -1)
                                        return false;

                                    sb.append((char) ch);
                                }
                            }
                        }
                        while (n > 0);
                    }
                    else
                    {
                        int n;
                        do
                        {
                            n = read();
                            if (n > 0)
                            {
                                skip(n);
                            }
                            else if (n == -1)
                            {
                                return false;
                            }
                        }
                        while (n > 0);
                    }
                    break;
                }
                case (0x3b): // end of file
                {
                    break;
                }
                default:
                {
                    return false;
                }
            }
        }
        while (blockType != 0x3b);

        return true;
    }

    private boolean checkIff() throws IOException
    {
        byte[] a = new byte[10];

        // read remaining 2 bytes of file id, 4 bytes file size
        // and 4 bytes IFF subformat
        if (read(a, 0, 10) != 10)
            return false;

        final byte[] IFF_RM = {0x52, 0x4d};
        if (!equals(a, 0, IFF_RM, 0, 2))
            return false;

        int type = getIntBigEndian(a, 6);
        if (type != 0x494c424d && // type must be ILBM...
                type != 0x50424d20) // ...or PBM
            return false;

        // loop chunks to find BMHD chunk
        do
        {
            if (read(a, 0, 8) != 8)
                return false;

            int chunkId = getIntBigEndian(a, 0);
            int size = getIntBigEndian(a, 4);
            if ((size & 1) == 1)
                size++;

            if (chunkId == 0x424d4844)
            {
                // BMHD chunk
                if (read(a, 0, 9) != 9)
                    return false;

                format = FORMAT_IFF;
                width = getShortBigEndian(a, 0);
                height = getShortBigEndian(a, 2);
                bitsPerPixel = a[8] & 0xff;
                return (width > 0 && height > 0 && bitsPerPixel > 0 && bitsPerPixel < 33);
            }

            skip(size);
        }
        while (true);
    }

    private boolean checkJpeg() throws IOException
    {
        byte[] data = new byte[12];

        while (true)
        {
            if (read(data, 0, 4) != 4)
                return false;

            int marker = getShortBigEndian(data, 0);
            int size = getShortBigEndian(data, 2);

            if ((marker & 0xff00) != 0xff00)
                return false; // not a valid marker

            if (marker == 0xffe0)
            {
                // APPx
                if (size < 14)
                {
                    // not an APPx header as we know it, skip
                    skip(size - 2);
                    continue;
                }

                if (read(data, 0, 12) != 12)
                    return false;

                final byte[] APP0_ID = {0x4a, 0x46, 0x49, 0x46, 0x00};
                if (equals(APP0_ID, 0, data, 0, 5))
                {
                    // System.out.println("data 7=" + data[7]);
                    if (data[7] == 1)
                    {
                        setPhysicalWidthDpi(getShortBigEndian(data, 8));
                        setPhysicalHeightDpi(getShortBigEndian(data, 10));
                    }
                    else if (data[7] == 2)
                    {
                        int x = getShortBigEndian(data, 8);
                        int y = getShortBigEndian(data, 10);
                        setPhysicalWidthDpi((int) (x * 2.54f));
                        setPhysicalHeightDpi((int) (y * 2.54f));
                    }
                }

                skip(size - 14);
            }
            else if (collectComments && size > 2 && marker == 0xfffe)
            { // comment
                size -= 2;
                byte[] chars = new byte[size];
                if (read(chars, 0, size) != size)
                    return false;

                String comment = new String(chars, "iso-8859-1");
                comment = comment.trim();
                addComment(comment);
            }
            else if (marker >= 0xffc0 && marker <= 0xffcf && marker != 0xffc4 && marker != 0xffc8)
            {
                if (read(data, 0, 6) != 6)
                    return false;

                format = FORMAT_JPEG;
                bitsPerPixel = (data[0] & 0xff) * (data[5] & 0xff);
                progressive = marker == 0xffc2 || marker == 0xffc6 || marker == 0xffca || marker == 0xffce;
                width = getShortBigEndian(data, 3);
                height = getShortBigEndian(data, 1);

                return true;
            }
            else
                skip(size - 2);
        }
    }

    private boolean checkPcx() throws IOException
    {
        byte[] a = new byte[64];
        if (read(a) != a.length)
            return false;

        // encoding, 1=RLE is only valid value
        if (a[0] != 1)
            return false;

        // width / height
        int x1 = getShortLittleEndian(a, 2);
        int y1 = getShortLittleEndian(a, 4);
        int x2 = getShortLittleEndian(a, 6);
        int y2 = getShortLittleEndian(a, 8);

        if (x1 < 0 || x2 < x1 || y1 < 0 || y2 < y1)
            return false;

        width = x2 - x1 + 1;
        height = y2 - y1 + 1;

        // color depth
        int bits = a[1];
        int planes = a[63];

        // paletted
        if (planes == 1 && (bits == 1 || bits == 2 || bits == 4 || bits == 8))
            bitsPerPixel = bits;
        // RGB truecolor
        else if (planes == 3 && bits == 8)
            bitsPerPixel = 24;
        else
            return false;

        setPhysicalWidthDpi(getShortLittleEndian(a, 10));
        setPhysicalHeightDpi(getShortLittleEndian(a, 10));
        format = FORMAT_PCX;

        return true;
    }

    private boolean checkPng() throws IOException
    {
        final byte[] PNG_MAGIC = {0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a};

        byte[] a = new byte[27];
        if (read(a) != 27)
            return false;

        if (!equals(a, 0, PNG_MAGIC, 0, 6))
            return false;

        format = FORMAT_PNG;
        width = getIntBigEndian(a, 14);
        height = getIntBigEndian(a, 18);
        bitsPerPixel = a[22] & 0xff;
        int colorType = a[23] & 0xff;
        if (colorType == 2 || colorType == 6)
            bitsPerPixel *= 3;
        progressive = (a[26] & 0xff) != 0;

        return true;
    }

    private boolean checkPnm(int id) throws IOException
    {
        if (id < 1 || id > 6)
            return false;

        final int[] PNM_FORMATS = {FORMAT_PBM, FORMAT_PGM, FORMAT_PPM};
        format = PNM_FORMATS[(id - 1) % 3];
        boolean hasPixelResolution = false;
        String s;
        while (true)
        {
            s = readLine();
            if (s != null)
                s = s.trim();

            if (s == null || s.length() < 1)
                continue;

            if (s.charAt(0) == '#')
            { // comment
                if (collectComments && s.length() > 1)
                    addComment(s.substring(1));

                continue;
            }
            if (!hasPixelResolution)
            { // split "343 966" into width=343, height=966
                int spaceIndex = s.indexOf(' ');
                if (spaceIndex == -1)
                    return false;

                String widthString = s.substring(0, spaceIndex);
                spaceIndex = s.lastIndexOf(' ');
                if (spaceIndex == -1)
                    return false;

                String heightString = s.substring(spaceIndex + 1);
                try
                {
                    width = Integer.parseInt(widthString);
                    height = Integer.parseInt(heightString);
                }
                catch (NumberFormatException nfe)
                {
                    return false;
                }

                if (width < 1 || height < 1)
                    return false;

                if (format == FORMAT_PBM)
                {
                    bitsPerPixel = 1;
                    return true;
                }

                hasPixelResolution = true;
            }
            else
            {
                int maxSample;
                try
                {
                    maxSample = Integer.parseInt(s);
                }
                catch (NumberFormatException nfe)
                {
                    return false;
                }

                if (maxSample < 0)
                    return false;

                for (int i = 0; i < 25; i++)
                {
                    if (maxSample < (1 << (i + 1)))
                    {
                        bitsPerPixel = i + 1;
                        if (format == FORMAT_PPM)
                            bitsPerPixel *= 3;

                        return true;
                    }
                }

                return false;
            }
        }
    }

    private boolean checkPsd() throws IOException
    {
        byte[] a = new byte[24];
        if (read(a) != a.length)
            return false;

        final byte[] PSD_MAGIC = {0x50, 0x53};
        if (!equals(a, 0, PSD_MAGIC, 0, 2))
            return false;

        format = FORMAT_PSD;
        width = getIntBigEndian(a, 16);
        height = getIntBigEndian(a, 12);
        int channels = getShortBigEndian(a, 10);
        int depth = getShortBigEndian(a, 20);
        bitsPerPixel = channels * depth;

        return (width > 0 && height > 0 && bitsPerPixel > 0 && bitsPerPixel <= 64);
    }

    private boolean checkRas() throws IOException
    {
        byte[] a = new byte[14];
        if (read(a) != a.length)
            return false;

        final byte[] RAS_MAGIC = {0x6a, (byte) 0x95};
        if (!equals(a, 0, RAS_MAGIC, 0, 2))
            return false;

        format = FORMAT_RAS;
        width = getIntBigEndian(a, 2);
        height = getIntBigEndian(a, 6);
        bitsPerPixel = getIntBigEndian(a, 10);

        return (width > 0 && height > 0 && bitsPerPixel > 0 && bitsPerPixel <= 24);
    }

    private static boolean equals(byte[] a1, int offs1, byte[] a2, int offs2, int num)
    {
        while (num-- > 0)
        {
            if (a1[offs1++] != a2[offs2++])
                return false;
        }

        return true;
    }

    /**
     * If {@link #check()} was successful, returns the image's number of bits per pixel.
     * Does not include transparency information like the alpha channel.
     * 
     * @return number of bits per image pixel
     */
    public int getBitsPerPixel()
    {
        return bitsPerPixel;
    }

    /**
     * Returns the index'th comment retrieved from the file.
     * 
     * @param index
     *        int index of comment to return
     * @throws IllegalArgumentException
     *         if index is smaller than 0 or larger than or equal
     *         to the number of comments retrieved
     * @see #getNumberOfComments
     */
    public String getComment(int index)
    {
        if (comments == null || index < 0 || index >= comments.size())
            throw new IllegalArgumentException("Not a valid comment index: " + index);

        return comments.elementAt(index);
    }

    /**
     * If {@link #check()} was successful, returns the image format as one
     * of the FORMAT_xyz constants from this class.
     * Use {@link #getFormatName()} to get a textual description of the file format.
     * 
     * @return file format as a FORMAT_xyz constant
     */
    public int getFormat()
    {
        return format;
    }

    /**
     * If {@link #check()} was successful, returns the image format's name.
     * Use {@link #getFormat()} to get a unique number.
     * 
     * @return file format name
     */
    public String getFormatName()
    {
        if (format >= 0 && format < FORMAT_NAMES.length)
            return FORMAT_NAMES[format];

        return "?";
    }

    /**
     * If {@link #check()} was successful, returns one the image's vertical
     * resolution in pixels.
     * 
     * @return image height in pixels
     */
    public int getHeight()
    {
        return height;
    }

    private static int getIntBigEndian(byte[] a, int offs)
    {
        return (a[offs] & 0xff) << 24 | (a[offs + 1] & 0xff) << 16 | (a[offs + 2] & 0xff) << 8 | a[offs + 3] & 0xff;
    }

    private static int getIntLittleEndian(byte[] a, int offs)
    {
        return (a[offs + 3] & 0xff) << 24 | (a[offs + 2] & 0xff) << 16 | (a[offs + 1] & 0xff) << 8 | a[offs] & 0xff;
    }

    /**
     * If {@link #check()} was successful, returns a String with the
     * MIME type of the format.
     * 
     * @return MIME type, e.g. <code>image/jpeg</code>
     */
    public String getMimeType()
    {
        if (format >= 0 && format < MIME_TYPE_STRINGS.length)
        {
            if (format == FORMAT_JPEG && progressive)
                return "image/pjpeg";

            return MIME_TYPE_STRINGS[format];
        }

        return null;
    }

    /**
     * If {@link #check()} was successful and {@link #setCollectComments(boolean)} was called with
     * <code>true</code> as argument, returns the number of comments retrieved
     * from the input image stream / file.
     * Any number &gt;= 0 and smaller than this number of comments is then a
     * valid argument for the {@link #getComment(int)} method.
     * 
     * @return number of comments retrieved from input image
     */
    public int getNumberOfComments()
    {
        if (comments == null)
            return 0;

        return comments.size();
    }

    /**
     * Returns the number of images in the examined file.
     * Assumes that <code>setDetermineImageNumber(true);</code> was called before
     * a successful call to {@link #check()}.
     * This value can currently be only different from <code>1</code> for GIF images.
     * 
     * @return number of images in file
     */
    public int getNumberOfImages()
    {
        return numberOfImages;
    }

    /**
     * Returns the physical height of this image in dots per inch (dpi).
     * Assumes that {@link #check()} was successful.
     * Returns <code>-1</code> on failure.
     * 
     * @return physical height (in dpi)
     * @see #getPhysicalWidthDpi()
     * @see #getPhysicalHeightInch()
     */
    public int getPhysicalHeightDpi()
    {
        return physicalHeightDpi;
    }

    /**
     * If {@link #check()} was successful, returns the physical width of this image in dpi (dots per
     * inch)
     * or -1 if no value could be found.
     * 
     * @return physical height (in dpi)
     * @see #getPhysicalHeightDpi()
     * @see #getPhysicalWidthDpi()
     * @see #getPhysicalWidthInch()
     */
    public float getPhysicalHeightInch()
    {
        int h = getHeight();
        int ph = getPhysicalHeightDpi();
        if (h > 0 && ph > 0)
        {
            return ((float) h) / ((float) ph);
        }

        return -1.0f;
    }

    /**
     * If {@link #check()} was successful, returns the physical width of this image in dpi (dots per
     * inch)
     * or -1 if no value could be found.
     * 
     * @return physical width (in dpi)
     * @see #getPhysicalHeightDpi()
     * @see #getPhysicalWidthInch()
     * @see #getPhysicalHeightInch()
     */
    public int getPhysicalWidthDpi()
    {
        return physicalWidthDpi;
    }

    /**
     * Returns the physical width of an image in inches, or
     * <code>-1.0f</code> if width information is not available.
     * Assumes that {@link #check} has been called successfully.
     * 
     * @return physical width in inches or <code>-1.0f</code> on failure
     * @see #getPhysicalWidthDpi
     * @see #getPhysicalHeightInch
     */
    public float getPhysicalWidthInch()
    {
        int w = getWidth();
        int pw = getPhysicalWidthDpi();
        if (w > 0 && pw > 0)
            return ((float) w) / ((float) pw);

        return -1.0f;
    }

    private static int getShortBigEndian(byte[] a, int offs)
    {
        return (a[offs] & 0xff) << 8 | (a[offs + 1] & 0xff);
    }

    private static int getShortLittleEndian(byte[] a, int offs)
    {
        return (a[offs] & 0xff) | (a[offs + 1] & 0xff) << 8;
    }

    /**
     * If {@link #check()} was successful, returns one the image's horizontal
     * resolution in pixels.
     * 
     * @return image width in pixels
     */
    public int getWidth()
    {
        return width;
    }

    /**
     * Returns whether the image is stored in a progressive (also called: interlaced) way.
     * 
     * @return true for progressive/interlaced, false otherwise
     */
    public boolean isProgressive()
    {
        return progressive;
    }

    //// private static void print(String sourceName, ImageInfo ii, boolean verbose)
    //// {
    //// if (verbose)
    //// {
    //// printVerbose(sourceName, ii);
    //// }
    //// else
    //// {
    //// printCompact(sourceName, ii);
    //// }
    //// }
    //
    // private static void printCompact(String sourceName, ImageInfo imageInfo)
    // {
    // final String SEP = "\t";
    // System.out.println(sourceName + SEP + imageInfo.getFormatName() + SEP +
    //// imageInfo.getMimeType() + SEP
    // + imageInfo.getWidth() + SEP + imageInfo.getHeight() + SEP + imageInfo.getBitsPerPixel() +
    //// SEP
    // + imageInfo.getNumberOfImages() + SEP + imageInfo.getPhysicalWidthDpi() + SEP
    // + imageInfo.getPhysicalHeightDpi() + SEP + imageInfo.getPhysicalWidthInch() + SEP
    // + imageInfo.getPhysicalHeightInch() + SEP + imageInfo.isProgressive());
    // }
    //
    // private static void printLine(int indentLevels, String text, float value, float
    //// minValidValue)
    // {
    // if (value < minValidValue)
    // {
    // return;
    // }
    // printLine(indentLevels, text, Float.toString(value));
    // }
    //
    // private static void printLine(int indentLevels, String text, int value, int minValidValue)
    // {
    // if (value >= minValidValue)
    // {
    // printLine(indentLevels, text, Integer.toString(value));
    // }
    // }
    //
    // private static void printLine(int indentLevels, String text, String value)
    // {
    // if (value == null || value.length() == 0)
    // {
    // return;
    // }
    // while (indentLevels-- > 0)
    // {
    // System.out.print("\t");
    // }
    // if (text != null && text.length() > 0)
    // {
    // System.out.print(text);
    // System.out.print(" ");
    // }
    // System.out.println(value);
    // }
    //
    // private static void printVerbose(String sourceName, ImageInfo ii)
    // {
    // printLine(0, null, sourceName);
    // printLine(1, "File format: ", ii.getFormatName());
    // printLine(1, "MIME type: ", ii.getMimeType());
    // printLine(1, "Width (pixels): ", ii.getWidth(), 1);
    // printLine(1, "Height (pixels): ", ii.getHeight(), 1);
    // printLine(1, "Bits per pixel: ", ii.getBitsPerPixel(), 1);
    // printLine(1, "Progressive: ", ii.isProgressive() ? "yes" : "no");
    // printLine(1, "Number of images: ", ii.getNumberOfImages(), 1);
    // printLine(1, "Physical width (dpi): ", ii.getPhysicalWidthDpi(), 1);
    // printLine(1, "Physical height (dpi): ", ii.getPhysicalHeightDpi(), 1);
    // printLine(1, "Physical width (inches): ", ii.getPhysicalWidthInch(), 1.0f);
    // printLine(1, "Physical height (inches): ", ii.getPhysicalHeightInch(), 1.0f);
    // int numComments = ii.getNumberOfComments();
    // printLine(1, "Number of textual comments: ", numComments, 1);
    // if (numComments > 0)
    // {
    // for (int i = 0; i < numComments; i++)
    // {
    // printLine(2, null, ii.getComment(i));
    // }
    // }
    // }

    private int read() throws IOException
    {
        if (in != null)
            return in.read();

        return din.readByte();
    }

    private int read(byte[] a) throws IOException
    {
        if (in != null)
            return in.read(a);

        din.readFully(a);
        return a.length;
    }

    private int read(byte[] a, int offset, int num) throws IOException
    {
        if (in != null)
            return in.read(a, offset, num);

        din.readFully(a, offset, num);
        return num;
    }

    private String readLine() throws IOException
    {
        return readLine(new StringBuffer());
    }

    private String readLine(StringBuffer sb) throws IOException
    {
        boolean finished;

        do
        {
            int value = read();
            finished = (value == -1 || value == 10);
            if (!finished)
                sb.append((char) value);
        }
        while (!finished);

        return sb.toString();
    }

    /**
     * Specify whether textual comments are supposed to be extracted from input.
     * Default is <code>false</code>.
     * If enabled, comments will be added to an internal list.
     * 
     * @param newValue
     *        if <code>true</code>, this class will read comments
     * @see #getNumberOfComments
     * @see #getComment
     */
    public void setCollectComments(boolean newValue)
    {
        collectComments = newValue;
    }

    /**
     * Specify whether the number of images in a file is to be
     * determined - default is <code>false</code>.
     * This is a special option because some file formats require running over
     * the entire file to find out the number of images, a rather time-consuming
     * task.
     * Not all file formats support more than one image.
     * If this method is called with <code>true</code> as argument,
     * the actual number of images can be queried via
     * {@link #getNumberOfImages()} after a successful call to
     * {@link #check()}.
     * 
     * @param newValue
     *        will the number of images be determined?
     * @see #getNumberOfImages
     */
    public void setDetermineImageNumber(boolean newValue)
    {
        determineNumberOfImages = newValue;
    }

    /**
     * Set the input stream to the argument stream (or file).
     * Note that {@link java.io.RandomAccessFile} implements
     * {@link java.io.DataInput}.
     * 
     * @param dataInput
     *        the input stream to read from
     */
    public void setInput(DataInput dataInput)
    {
        din = dataInput;
        in = null;
    }

    /**
     * Set the input stream to the argument stream (or file).
     * 
     * @param inputStream
     *        the input stream to read from
     */
    public void setInput(InputStream inputStream)
    {
        in = inputStream;
        din = null;
    }

    private void setPhysicalHeightDpi(int newValue)
    {
        physicalWidthDpi = newValue;
    }

    private void setPhysicalWidthDpi(int newValue)
    {
        physicalHeightDpi = newValue;
    }

    private void skip(int num) throws IOException
    {
        while (num > 0)
        {
            long result;

            if (in != null)
                result = in.skip(num);
            else
                result = din.skipBytes(num);

            if (result > 0)
                num -= result;
            else
            {
                if (in != null)
                    result = in.read();
                else
                    result = din.readByte();

                if (result == -1)
                    throw new IOException("Premature end of input.");

                num--;
            }
        }
    }
}