001    /*
002     * ImageInfo.java
003     *
004     * Version 1.9
005     *
006     * A Java class to determine image width, height and color depth for
007     * a number of image file formats.
008     *
009     * Written by Marco Schmidt 
010     *
011     * Contributed to the Public Domain.
012     */
013    
014    package org.jboss.dna.sequencer.image;
015    
016    import java.io.DataInput;
017    import java.io.FileInputStream;
018    import java.io.IOException;
019    import java.io.InputStream;
020    import java.net.URL;
021    import java.util.Vector;
022    
023    /**
024     * Get file format, image resolution, number of bits per pixel and optionally number of images, comments and physical resolution
025     * from JPEG, GIF, BMP, PCX, PNG, IFF, RAS, PBM, PGM, PPM and PSD files (or input streams).
026     * <p>
027     * Use the class like this:
028     * 
029     * <pre>
030     * ImageMetadata ii = new ImageMetadata();
031     * ii.setInput(in); // in can be InputStream or RandomAccessFile
032     * ii.setDetermineImageNumber(true); // default is false
033     * ii.setCollectComments(true); // default is false
034     * if (!ii.check()) {
035     *     System.err.println(&quot;Not a supported image file format.&quot;);
036     *     return;
037     * }
038     * System.out.println(ii.getFormatName() + &quot;, &quot; + ii.getMimeType() + &quot;, &quot; + ii.getWidth() + &quot; x &quot; + ii.getHeight() + &quot; pixels, &quot;
039     *                    + ii.getBitsPerPixel() + &quot; bits per pixel, &quot; + ii.getNumberOfImages() + &quot; image(s), &quot;
040     *                    + ii.getNumberOfComments() + &quot; comment(s).&quot;);
041     * // there are other properties, check out the API documentation
042     * </pre>
043     * 
044     * You can also use this class as a command line program. Call it with a number of image file names and URLs as parameters:
045     * 
046     * <pre>
047     *   java ImageMetadata *.jpg *.png *.gif http://somesite.tld/image.jpg
048     * </pre>
049     * 
050     * or call it without parameters and pipe data to it:
051     * 
052     * <pre>
053     *   java ImageMetadata &lt; image.jpg  
054     * </pre>
055     * 
056     * <p>
057     * Known limitations:
058     * <ul>
059     * <li>When the determination of the number of images is turned off, GIF bits per pixel are only read from the global header. For
060     * some GIFs, local palettes change this to a typically larger value. To be certain to get the correct color depth, call
061     * setDetermineImageNumber(true) before calling check(). The complete scan over the GIF file will take additional time.</li>
062     * <li>Transparency information is not included in the bits per pixel count. Actually, it was my decision not to include those
063     * bits, so it's a feature! ;-)</li>
064     * </ul>
065     * <p>
066     * Requirements:
067     * <ul>
068     * <li>Java 1.1 or higher</li>
069     * </ul>
070     * <p>
071     * The latest version can be found at <a href="http://schmidt.devlib.org/image-info/">http://schmidt.devlib.org/image-info/</a>.
072     * <p>
073     * Written by Marco Schmidt.
074     * <p>
075     * This class is contributed to the Public Domain. Use it at your own risk.
076     * <p>
077     * <a name="history">History</a>:
078     * <ul>
079     * <li><strong>2001-08-24</strong> Initial version.</li>
080     * <li><strong>2001-10-13</strong> Added support for the file formats BMP and PCX.</li>
081     * <li><strong>2001-10-16</strong> Fixed bug in read(int[], int, int) that returned
082     * <li><strong>2002-01-22</strong> Added support for file formats Amiga IFF and Sun Raster (RAS).</li>
083     * <li><strong>2002-01-24</strong> Added support for file formats Portable Bitmap / Graymap / Pixmap (PBM, PGM, PPM) and Adobe
084     * Photoshop (PSD). Added new method getMimeType() to return the MIME type associated with a particular file format.</li>
085     * <li><strong>2002-03-15</strong> Added support to recognize number of images in file. Only works with GIF. Use
086     * {@link #setDetermineImageNumber} with <code>true</code> as argument to identify animated GIFs ({@link #getNumberOfImages()}
087     * will return a value larger than <code>1</code>).</li>
088     * <li><strong>2002-04-10</strong> Fixed a bug in the feature 'determine number of images in animated GIF' introduced with
089     * version 1.1. Thanks to Marcelo P. Lima for sending in the bug report. Released as 1.1.1.</li>
090     * <li><strong>2002-04-18</strong> Added {@link #setCollectComments(boolean)}. That new method lets the user specify whether
091     * textual comments are to be stored in an internal list when encountered in an input image file / stream. Added two methods to
092     * return the physical width and height of the image in dpi: {@link #getPhysicalWidthDpi()} and {@link #getPhysicalHeightDpi()}.
093     * If the physical resolution could not be retrieved, these methods return <code>-1</code>. </li>
094     * <li><strong>2002-04-23</strong> Added support for the new properties physical resolution and comments for some formats.
095     * Released as 1.2.</li>
096     * <li><strong>2002-06-17</strong> Added support for SWF, sent in by Michael Aird. Changed checkJpeg() so that other APP markers
097     * than APP0 will not lead to a failure anymore. Released as 1.3.</li>
098     * <li><strong>2003-07-28</strong> Bug fix - skip method now takes return values into consideration. Less bytes than necessary
099     * may have been skipped, leading to flaws in the retrieved information in some cases. Thanks to Bernard Bernstein for pointing
100     * that out. Released as 1.4.</li>
101     * <li><strong>2004-02-29</strong> Added support for recognizing progressive JPEG and interlaced PNG and GIF. A new method
102     * {@link #isProgressive()} returns whether ImageMetadata has found that the storage type is progressive (or interlaced). Thanks
103     * to Joe Germuska for suggesting the feature. Bug fix: BMP physical resolution is now correctly determined. Released as 1.5.</li>
104     * <li><strong>2004-11-30</strong> Bug fix: recognizing progressive GIFs (interlaced in GIF terminology) did not work (thanks to
105     * Franz Jeitler for pointing this out). Now it should work, but only if the number of images is determined. This is because
106     * information on interlacing is stored in a local image header. In theory, different images could be stored interlaced and
107     * non-interlaced in one file. However, I think that's unlikely. Right now, the last image in the GIF file that is examined by
108     * ImageMetadata is used for the "progressive" status.</li>
109     * <li><strong>2005-01-02</strong> Some code clean up (unused methods and variables commented out, missing javadoc comments,
110     * etc.). Thanks to George Sexton for a long list. Removed usage of Boolean.toString because it's a Java 1.4+ feature (thanks to
111     * Gregor Dupont). Changed delimiter character in compact output from semicolon to tabulator (for better integration with cut(1)
112     * and other Unix tools). Added some points to the <a href="http://schmidt.devlib.org/image-info/index.html#knownissues">'Known
113     * issues' section of the website</a>. Released as 1.6.</li>
114     * <li><strong>2005-07-26</strong> Removed code to identify Flash (SWF) files. Has repeatedly led to problems and support
115     * 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
116     * others which didn't work for some people. I give up on SWF. Please do not contact me about it anymore. Set package of
117     * ImageMetadata class to org.devlib.schmidt.imageinfo (a package was repeatedly requested by some users). Released as 1.7.</li>
118     * <li><strong>2006-02-23</strong> Removed Flash helper methods which weren't used elsewhere. Updated skip method which tries
119     * "read" whenever "skip(Bytes)" returns a result of 0. The old method didn't work with certain input stream types on truncated
120     * data streams. Thanks to Martin Leidig for reporting this and sending in code. Released as 1.8.</li>
121     * </li>
122     * <li><strong>2006-11-13</strong> Removed check that made ImageMetadata report JPEG APPx markers smaller than 14 bytes as files
123     * in unknown format. Such JPEGs seem to be generated by Google's Picasa application. First reported with fix by Karl von Randow.
124     * Released as 1.9.</li>
125     * <li><strong>2008-04-10</strong> Changed comment vector to be <code>Vector&lt;String&gt;</code>, and removed any
126     * unnecessary casting. Also removed the unnecessary else statements where the previous block ended in a return. Also renamed to
127     * <code>ImageMetadata</code>.
128     * </ul>
129     * 
130     * @author Marco Schmidt
131     */
132    public class ImageMetadata {
133    
134        /**
135         * Return value of {@link #getFormat()} for JPEG streams. ImageMetadata can extract physical resolution and comments from
136         * JPEGs (only from APP0 headers). Only one image can be stored in a file. It is determined whether the JPEG stream is
137         * progressive (see {@link #isProgressive()}).
138         */
139        public static final int FORMAT_JPEG = 0;
140    
141        /**
142         * Return value of {@link #getFormat()} for GIF streams. ImageMetadata can extract comments from GIFs and count the number of
143         * images (GIFs with more than one image are animations). It is determined whether the GIF stream is interlaced (see
144         * {@link #isProgressive()}).
145         */
146        public static final int FORMAT_GIF = 1;
147    
148        /**
149         * Return value of {@link #getFormat()} for PNG streams. PNG only supports one image per file. Both physical resolution and
150         * comments can be stored with PNG, but ImageMetadata is currently not able to extract those. It is determined whether the PNG
151         * stream is interlaced (see {@link #isProgressive()}).
152         */
153        public static final int FORMAT_PNG = 2;
154    
155        /**
156         * Return value of {@link #getFormat()} for BMP streams. BMP only supports one image per file. BMP does not allow for
157         * comments. The physical resolution can be stored.
158         */
159        public static final int FORMAT_BMP = 3;
160    
161        /**
162         * Return value of {@link #getFormat()} for PCX streams. PCX does not allow for comments or more than one image per file.
163         * However, the physical resolution can be stored.
164         */
165        public static final int FORMAT_PCX = 4;
166    
167        /**
168         * Return value of {@link #getFormat()} for IFF streams.
169         */
170        public static final int FORMAT_IFF = 5;
171    
172        /**
173         * Return value of {@link #getFormat()} for RAS streams. Sun Raster allows for one image per file only and is not able to
174         * store physical resolution or comments.
175         */
176        public static final int FORMAT_RAS = 6;
177    
178        /** Return value of {@link #getFormat()} for PBM streams. */
179        public static final int FORMAT_PBM = 7;
180    
181        /** Return value of {@link #getFormat()} for PGM streams. */
182        public static final int FORMAT_PGM = 8;
183    
184        /** Return value of {@link #getFormat()} for PPM streams. */
185        public static final int FORMAT_PPM = 9;
186    
187        /** Return value of {@link #getFormat()} for PSD streams. */
188        public static final int FORMAT_PSD = 10;
189    
190        /*
191         * public static final int COLOR_TYPE_UNKNOWN = -1; public static final int COLOR_TYPE_TRUECOLOR_RGB = 0; public static final
192         * int COLOR_TYPE_PALETTED = 1; public static final int COLOR_TYPE_GRAYSCALE= 2; public static final int
193         * COLOR_TYPE_BLACK_AND_WHITE = 3;
194         */
195    
196        /**
197         * The names of all supported file formats. The FORMAT_xyz int constants can be used as index values for this array.
198         */
199        private static final String[] FORMAT_NAMES = {"JPEG", "GIF", "PNG", "BMP", "PCX", "IFF", "RAS", "PBM", "PGM", "PPM", "PSD"};
200    
201        /**
202         * The names of the MIME types for all supported file formats. The FORMAT_xyz int constants can be used as index values for
203         * this array.
204         */
205        private static final String[] MIME_TYPE_STRINGS = {"image/jpeg", "image/gif", "image/png", "image/bmp", "image/pcx",
206            "image/iff", "image/ras", "image/x-portable-bitmap", "image/x-portable-graymap", "image/x-portable-pixmap", "image/psd"};
207    
208        private int width;
209        private int height;
210        private int bitsPerPixel;
211        // private int colorType = COLOR_TYPE_UNKNOWN;
212        private boolean progressive;
213        private int format;
214        private InputStream in;
215        private DataInput din;
216        private boolean collectComments = true;
217        private Vector<String> comments;
218        private boolean determineNumberOfImages;
219        private int numberOfImages;
220        private int physicalHeightDpi;
221        private int physicalWidthDpi;
222    
223        private void addComment( String s ) {
224            if (comments == null) {
225                comments = new Vector<String>();
226            }
227            comments.addElement(s);
228        }
229    
230        /**
231         * Call this method after you have provided an input stream or file using {@link #setInput(InputStream)} or
232         * {@link #setInput(DataInput)}. If true is returned, the file format was known and information on the file's content can be
233         * retrieved using the various getXyz methods.
234         * 
235         * @return if information could be retrieved from input
236         */
237        public boolean check() {
238            format = -1;
239            width = -1;
240            height = -1;
241            bitsPerPixel = -1;
242            numberOfImages = 1;
243            physicalHeightDpi = -1;
244            physicalWidthDpi = -1;
245            comments = null;
246            try {
247                int b1 = read() & 0xff;
248                int b2 = read() & 0xff;
249                if (b1 == 0x47 && b2 == 0x49) {
250                    return checkGif();
251                } else if (b1 == 0x89 && b2 == 0x50) {
252                    return checkPng();
253                } else if (b1 == 0xff && b2 == 0xd8) {
254                    return checkJpeg();
255                } else if (b1 == 0x42 && b2 == 0x4d) {
256                    return checkBmp();
257                } else if (b1 == 0x0a && b2 < 0x06) {
258                    return checkPcx();
259                } else if (b1 == 0x46 && b2 == 0x4f) {
260                    return checkIff();
261                } else if (b1 == 0x59 && b2 == 0xa6) {
262                    return checkRas();
263                } else if (b1 == 0x50 && b2 >= 0x31 && b2 <= 0x36) {
264                    return checkPnm(b2 - '0');
265                } else if (b1 == 0x38 && b2 == 0x42) {
266                    return checkPsd();
267                } else {
268                    return false;
269                }
270            } catch (IOException ioe) {
271                return false;
272            }
273        }
274    
275        private boolean checkBmp() throws IOException {
276            byte[] a = new byte[44];
277            if (read(a) != a.length) {
278                return false;
279            }
280            width = getIntLittleEndian(a, 16);
281            height = getIntLittleEndian(a, 20);
282            if (width < 1 || height < 1) {
283                return false;
284            }
285            bitsPerPixel = getShortLittleEndian(a, 26);
286            if (bitsPerPixel != 1 && bitsPerPixel != 4 && bitsPerPixel != 8 && bitsPerPixel != 16 && bitsPerPixel != 24
287                && bitsPerPixel != 32) {
288                return false;
289            }
290            int x = (int)(getIntLittleEndian(a, 36) * 0.0254);
291            if (x > 0) {
292                setPhysicalWidthDpi(x);
293            }
294            int y = (int)(getIntLittleEndian(a, 40) * 0.0254);
295            if (y > 0) {
296                setPhysicalHeightDpi(y);
297            }
298            format = FORMAT_BMP;
299            return true;
300        }
301    
302        private boolean checkGif() throws IOException {
303            final byte[] GIF_MAGIC_87A = {0x46, 0x38, 0x37, 0x61};
304            final byte[] GIF_MAGIC_89A = {0x46, 0x38, 0x39, 0x61};
305            byte[] a = new byte[11]; // 4 from the GIF signature + 7 from the global header
306            if (read(a) != 11) {
307                return false;
308            }
309            if ((!equals(a, 0, GIF_MAGIC_89A, 0, 4)) && (!equals(a, 0, GIF_MAGIC_87A, 0, 4))) {
310                return false;
311            }
312            format = FORMAT_GIF;
313            width = getShortLittleEndian(a, 4);
314            height = getShortLittleEndian(a, 6);
315            int flags = a[8] & 0xff;
316            bitsPerPixel = ((flags >> 4) & 0x07) + 1;
317            // progressive = (flags & 0x02) != 0;
318            if (!determineNumberOfImages) {
319                return true;
320            }
321            // skip global color palette
322            if ((flags & 0x80) != 0) {
323                int tableSize = (1 << ((flags & 7) + 1)) * 3;
324                skip(tableSize);
325            }
326            numberOfImages = 0;
327            int blockType;
328            do {
329                blockType = read();
330                switch (blockType) {
331                    case (0x2c): // image separator
332                    {
333                        if (read(a, 0, 9) != 9) {
334                            return false;
335                        }
336                        flags = a[8] & 0xff;
337                        progressive = (flags & 0x40) != 0;
338                        /*
339                         * int locWidth = getShortLittleEndian(a, 4); int locHeight = getShortLittleEndian(a, 6);
340                         * System.out.println("LOCAL: " + locWidth + " x " + locHeight);
341                         */
342                        int localBitsPerPixel = (flags & 0x07) + 1;
343                        if (localBitsPerPixel > bitsPerPixel) {
344                            bitsPerPixel = localBitsPerPixel;
345                        }
346                        if ((flags & 0x80) != 0) {
347                            skip((1 << localBitsPerPixel) * 3);
348                        }
349                        skip(1); // initial code length
350                        int n;
351                        do {
352                            n = read();
353                            if (n > 0) {
354                                skip(n);
355                            } else if (n == -1) {
356                                return false;
357                            }
358                        } while (n > 0);
359                        numberOfImages++;
360                        break;
361                    }
362                    case (0x21): // extension
363                    {
364                        int extensionType = read();
365                        if (collectComments && extensionType == 0xfe) {
366                            StringBuffer sb = new StringBuffer();
367                            int n;
368                            do {
369                                n = read();
370                                if (n == -1) {
371                                    return false;
372                                }
373                                if (n > 0) {
374                                    for (int i = 0; i < n; i++) {
375                                        int ch = read();
376                                        if (ch == -1) {
377                                            return false;
378                                        }
379                                        sb.append((char)ch);
380                                    }
381                                }
382                            } while (n > 0);
383                        } else {
384                            int n;
385                            do {
386                                n = read();
387                                if (n > 0) {
388                                    skip(n);
389                                } else if (n == -1) {
390                                    return false;
391                                }
392                            } while (n > 0);
393                        }
394                        break;
395                    }
396                    case (0x3b): // end of file
397                    {
398                        break;
399                    }
400                    default: {
401                        return false;
402                    }
403                }
404            } while (blockType != 0x3b);
405            return true;
406        }
407    
408        private boolean checkIff() throws IOException {
409            byte[] a = new byte[10];
410            // read remaining 2 bytes of file id, 4 bytes file size
411            // and 4 bytes IFF subformat
412            if (read(a, 0, 10) != 10) {
413                return false;
414            }
415            final byte[] IFF_RM = {0x52, 0x4d};
416            if (!equals(a, 0, IFF_RM, 0, 2)) {
417                return false;
418            }
419            int type = getIntBigEndian(a, 6);
420            if (type != 0x494c424d && // type must be ILBM...
421                type != 0x50424d20) { // ...or PBM
422                return false;
423            }
424            // loop chunks to find BMHD chunk
425            do {
426                if (read(a, 0, 8) != 8) {
427                    return false;
428                }
429                int chunkId = getIntBigEndian(a, 0);
430                int size = getIntBigEndian(a, 4);
431                if ((size & 1) == 1) {
432                    size++;
433                }
434                if (chunkId == 0x424d4844) { // BMHD chunk
435                    if (read(a, 0, 9) != 9) {
436                        return false;
437                    }
438                    format = FORMAT_IFF;
439                    width = getShortBigEndian(a, 0);
440                    height = getShortBigEndian(a, 2);
441                    bitsPerPixel = a[8] & 0xff;
442                    return (width > 0 && height > 0 && bitsPerPixel > 0 && bitsPerPixel < 33);
443                }
444                skip(size);
445            } while (true);
446        }
447    
448        private boolean checkJpeg() throws IOException {
449            byte[] data = new byte[12];
450            while (true) {
451                if (read(data, 0, 4) != 4) {
452                    return false;
453                }
454                int marker = getShortBigEndian(data, 0);
455                int size = getShortBigEndian(data, 2);
456                if ((marker & 0xff00) != 0xff00) {
457                    return false; // not a valid marker
458                }
459                if (marker == 0xffe0) { // APPx
460                    if (size < 14) {
461                        // not an APPx header as we know it, skip
462                        skip(size - 2);
463                        continue;
464                    }
465                    if (read(data, 0, 12) != 12) {
466                        return false;
467                    }
468                    final byte[] APP0_ID = {0x4a, 0x46, 0x49, 0x46, 0x00};
469                    if (equals(APP0_ID, 0, data, 0, 5)) {
470                        // System.out.println("data 7=" + data[7]);
471                        if (data[7] == 1) {
472                            setPhysicalWidthDpi(getShortBigEndian(data, 8));
473                            setPhysicalHeightDpi(getShortBigEndian(data, 10));
474                        } else if (data[7] == 2) {
475                            int x = getShortBigEndian(data, 8);
476                            int y = getShortBigEndian(data, 10);
477                            setPhysicalWidthDpi((int)(x * 2.54f));
478                            setPhysicalHeightDpi((int)(y * 2.54f));
479                        }
480                    }
481                    skip(size - 14);
482                } else if (collectComments && size > 2 && marker == 0xfffe) { // comment
483                    size -= 2;
484                    byte[] chars = new byte[size];
485                    if (read(chars, 0, size) != size) {
486                        return false;
487                    }
488                    String comment = new String(chars, "iso-8859-1");
489                    comment = comment.trim();
490                    addComment(comment);
491                } else if (marker >= 0xffc0 && marker <= 0xffcf && marker != 0xffc4 && marker != 0xffc8) {
492                    if (read(data, 0, 6) != 6) {
493                        return false;
494                    }
495                    format = FORMAT_JPEG;
496                    bitsPerPixel = (data[0] & 0xff) * (data[5] & 0xff);
497                    progressive = marker == 0xffc2 || marker == 0xffc6 || marker == 0xffca || marker == 0xffce;
498                    width = getShortBigEndian(data, 3);
499                    height = getShortBigEndian(data, 1);
500                    return true;
501                } else {
502                    skip(size - 2);
503                }
504            }
505        }
506    
507        private boolean checkPcx() throws IOException {
508            byte[] a = new byte[64];
509            if (read(a) != a.length) {
510                return false;
511            }
512            if (a[0] != 1) { // encoding, 1=RLE is only valid value
513                return false;
514            }
515            // width / height
516            int x1 = getShortLittleEndian(a, 2);
517            int y1 = getShortLittleEndian(a, 4);
518            int x2 = getShortLittleEndian(a, 6);
519            int y2 = getShortLittleEndian(a, 8);
520            if (x1 < 0 || x2 < x1 || y1 < 0 || y2 < y1) {
521                return false;
522            }
523            width = x2 - x1 + 1;
524            height = y2 - y1 + 1;
525            // color depth
526            int bits = a[1];
527            int planes = a[63];
528            if (planes == 1 && (bits == 1 || bits == 2 || bits == 4 || bits == 8)) {
529                // paletted
530                bitsPerPixel = bits;
531            } else if (planes == 3 && bits == 8) {
532                // RGB truecolor
533                bitsPerPixel = 24;
534            } else {
535                return false;
536            }
537            setPhysicalWidthDpi(getShortLittleEndian(a, 10));
538            setPhysicalHeightDpi(getShortLittleEndian(a, 10));
539            format = FORMAT_PCX;
540            return true;
541        }
542    
543        private boolean checkPng() throws IOException {
544            final byte[] PNG_MAGIC = {0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a};
545            byte[] a = new byte[27];
546            if (read(a) != 27) {
547                return false;
548            }
549            if (!equals(a, 0, PNG_MAGIC, 0, 6)) {
550                return false;
551            }
552            format = FORMAT_PNG;
553            width = getIntBigEndian(a, 14);
554            height = getIntBigEndian(a, 18);
555            bitsPerPixel = a[22] & 0xff;
556            int colorType = a[23] & 0xff;
557            if (colorType == 2 || colorType == 6) {
558                bitsPerPixel *= 3;
559            }
560            progressive = (a[26] & 0xff) != 0;
561            return true;
562        }
563    
564        private boolean checkPnm( int id ) throws IOException {
565            if (id < 1 || id > 6) {
566                return false;
567            }
568            final int[] PNM_FORMATS = {FORMAT_PBM, FORMAT_PGM, FORMAT_PPM};
569            format = PNM_FORMATS[(id - 1) % 3];
570            boolean hasPixelResolution = false;
571            String s;
572            while (true) {
573                s = readLine();
574                if (s != null) {
575                    s = s.trim();
576                }
577                if (s == null || s.length() < 1) {
578                    continue;
579                }
580                if (s.charAt(0) == '#') { // comment
581                    if (collectComments && s.length() > 1) {
582                        addComment(s.substring(1));
583                    }
584                    continue;
585                }
586                if (!hasPixelResolution) { // split "343 966" into width=343, height=966
587                    int spaceIndex = s.indexOf(' ');
588                    if (spaceIndex == -1) {
589                        return false;
590                    }
591                    String widthString = s.substring(0, spaceIndex);
592                    spaceIndex = s.lastIndexOf(' ');
593                    if (spaceIndex == -1) {
594                        return false;
595                    }
596                    String heightString = s.substring(spaceIndex + 1);
597                    try {
598                        width = Integer.parseInt(widthString);
599                        height = Integer.parseInt(heightString);
600                    } catch (NumberFormatException nfe) {
601                        return false;
602                    }
603                    if (width < 1 || height < 1) {
604                        return false;
605                    }
606                    if (format == FORMAT_PBM) {
607                        bitsPerPixel = 1;
608                        return true;
609                    }
610                    hasPixelResolution = true;
611                } else {
612                    int maxSample;
613                    try {
614                        maxSample = Integer.parseInt(s);
615                    } catch (NumberFormatException nfe) {
616                        return false;
617                    }
618                    if (maxSample < 0) {
619                        return false;
620                    }
621                    for (int i = 0; i < 25; i++) {
622                        if (maxSample < (1 << (i + 1))) {
623                            bitsPerPixel = i + 1;
624                            if (format == FORMAT_PPM) {
625                                bitsPerPixel *= 3;
626                            }
627                            return true;
628                        }
629                    }
630                    return false;
631                }
632            }
633        }
634    
635        private boolean checkPsd() throws IOException {
636            byte[] a = new byte[24];
637            if (read(a) != a.length) {
638                return false;
639            }
640            final byte[] PSD_MAGIC = {0x50, 0x53};
641            if (!equals(a, 0, PSD_MAGIC, 0, 2)) {
642                return false;
643            }
644            format = FORMAT_PSD;
645            width = getIntBigEndian(a, 16);
646            height = getIntBigEndian(a, 12);
647            int channels = getShortBigEndian(a, 10);
648            int depth = getShortBigEndian(a, 20);
649            bitsPerPixel = channels * depth;
650            return (width > 0 && height > 0 && bitsPerPixel > 0 && bitsPerPixel <= 64);
651        }
652    
653        private boolean checkRas() throws IOException {
654            byte[] a = new byte[14];
655            if (read(a) != a.length) {
656                return false;
657            }
658            final byte[] RAS_MAGIC = {0x6a, (byte)0x95};
659            if (!equals(a, 0, RAS_MAGIC, 0, 2)) {
660                return false;
661            }
662            format = FORMAT_RAS;
663            width = getIntBigEndian(a, 2);
664            height = getIntBigEndian(a, 6);
665            bitsPerPixel = getIntBigEndian(a, 10);
666            return (width > 0 && height > 0 && bitsPerPixel > 0 && bitsPerPixel <= 24);
667        }
668    
669        /**
670         * Run over String list, return false if and only if at least one of the arguments equals <code>-c</code>.
671         * 
672         * @param args string list to check
673         * @return <code>true</code> none of the supplied parameters is <code>-c</code>
674         */
675        private static boolean determineVerbosity( String[] args ) {
676            if (args != null && args.length > 0) {
677                for (int i = 0; i < args.length; i++) {
678                    if ("-c".equals(args[i])) {
679                        return false;
680                    }
681                }
682            }
683            return true;
684        }
685    
686        private static boolean equals( byte[] a1,
687                                       int offs1,
688                                       byte[] a2,
689                                       int offs2,
690                                       int num ) {
691            while (num-- > 0) {
692                if (a1[offs1++] != a2[offs2++]) {
693                    return false;
694                }
695            }
696            return true;
697        }
698    
699        /**
700         * If {@link #check()} was successful, returns the image's number of bits per pixel. Does not include transparency information
701         * like the alpha channel.
702         * 
703         * @return number of bits per image pixel
704         */
705        public int getBitsPerPixel() {
706            return bitsPerPixel;
707        }
708    
709        /**
710         * Returns the index'th comment retrieved from the file.
711         * 
712         * @param index int index of comment to return
713         * @return the comment at the supplied index
714         * @throws IllegalArgumentException if index is smaller than 0 or larger than or equal to the number of comments retrieved
715         * @see #getNumberOfComments
716         */
717        public String getComment( int index ) {
718            if (comments == null || index < 0 || index >= comments.size()) {
719                throw new IllegalArgumentException("Not a valid comment index: " + index);
720            }
721            return comments.elementAt(index);
722        }
723    
724        /**
725         * If {@link #check()} was successful, returns the image format as one of the FORMAT_xyz constants from this class. Use
726         * {@link #getFormatName()} to get a textual description of the file format.
727         * 
728         * @return file format as a FORMAT_xyz constant
729         */
730        public int getFormat() {
731            return format;
732        }
733    
734        /**
735         * If {@link #check()} was successful, returns the image format's name. Use {@link #getFormat()} to get a unique number.
736         * 
737         * @return file format name
738         */
739        public String getFormatName() {
740            if (format >= 0 && format < FORMAT_NAMES.length) {
741                return FORMAT_NAMES[format];
742            }
743            return "?";
744        }
745    
746        /**
747         * If {@link #check()} was successful, returns one the image's vertical resolution in pixels.
748         * 
749         * @return image height in pixels
750         */
751        public int getHeight() {
752            return height;
753        }
754    
755        private static int getIntBigEndian( byte[] a,
756                                            int offs ) {
757            return (a[offs] & 0xff) << 24 | (a[offs + 1] & 0xff) << 16 | (a[offs + 2] & 0xff) << 8 | a[offs + 3] & 0xff;
758        }
759    
760        private static int getIntLittleEndian( byte[] a,
761                                               int offs ) {
762            return (a[offs + 3] & 0xff) << 24 | (a[offs + 2] & 0xff) << 16 | (a[offs + 1] & 0xff) << 8 | a[offs] & 0xff;
763        }
764    
765        /**
766         * If {@link #check()} was successful, returns a String with the MIME type of the format.
767         * 
768         * @return MIME type, e.g. <code>image/jpeg</code>
769         */
770        public String getMimeType() {
771            if (format >= 0 && format < MIME_TYPE_STRINGS.length) {
772                if (format == FORMAT_JPEG && progressive) {
773                    return "image/pjpeg";
774                }
775                return MIME_TYPE_STRINGS[format];
776            }
777            return null;
778        }
779    
780        /**
781         * If {@link #check()} was successful and {@link #setCollectComments(boolean)} was called with <code>true</code> as
782         * argument, returns the number of comments retrieved from the input image stream / file. Any number &gt;= 0 and smaller than
783         * this number of comments is then a valid argument for the {@link #getComment(int)} method.
784         * 
785         * @return number of comments retrieved from input image
786         */
787        public int getNumberOfComments() {
788            if (comments == null) {
789                return 0;
790            }
791            return comments.size();
792        }
793    
794        /**
795         * Returns the number of images in the examined file. Assumes that <code>setDetermineImageNumber(true);</code> was called
796         * before a successful call to {@link #check()}. This value can currently be only different from <code>1</code> for GIF
797         * images.
798         * 
799         * @return number of images in file
800         */
801        public int getNumberOfImages() {
802            return numberOfImages;
803        }
804    
805        /**
806         * Returns the physical height of this image in dots per inch (dpi). Assumes that {@link #check()} was successful. Returns
807         * <code>-1</code> on failure.
808         * 
809         * @return physical height (in dpi)
810         * @see #getPhysicalWidthDpi()
811         * @see #getPhysicalHeightInch()
812         */
813        public int getPhysicalHeightDpi() {
814            return physicalHeightDpi;
815        }
816    
817        /**
818         * If {@link #check()} was successful, returns the physical width of this image in dpi (dots per inch) or -1 if no value could
819         * be found.
820         * 
821         * @return physical height (in dpi)
822         * @see #getPhysicalHeightDpi()
823         * @see #getPhysicalWidthDpi()
824         * @see #getPhysicalWidthInch()
825         */
826        public float getPhysicalHeightInch() {
827            int h = getHeight();
828            int ph = getPhysicalHeightDpi();
829            if (h > 0 && ph > 0) {
830                return ((float)h) / ((float)ph);
831            }
832            return -1.0f;
833        }
834    
835        /**
836         * If {@link #check()} was successful, returns the physical width of this image in dpi (dots per inch) or -1 if no value could
837         * be found.
838         * 
839         * @return physical width (in dpi)
840         * @see #getPhysicalHeightDpi()
841         * @see #getPhysicalWidthInch()
842         * @see #getPhysicalHeightInch()
843         */
844        public int getPhysicalWidthDpi() {
845            return physicalWidthDpi;
846        }
847    
848        /**
849         * Returns the physical width of an image in inches, or <code>-1.0f</code> if width information is not available. Assumes
850         * that {@link #check} has been called successfully.
851         * 
852         * @return physical width in inches or <code>-1.0f</code> on failure
853         * @see #getPhysicalWidthDpi
854         * @see #getPhysicalHeightInch
855         */
856        public float getPhysicalWidthInch() {
857            int w = getWidth();
858            int pw = getPhysicalWidthDpi();
859            if (w > 0 && pw > 0) {
860                return ((float)w) / ((float)pw);
861            }
862            return -1.0f;
863        }
864    
865        private static int getShortBigEndian( byte[] a,
866                                              int offs ) {
867            return (a[offs] & 0xff) << 8 | (a[offs + 1] & 0xff);
868        }
869    
870        private static int getShortLittleEndian( byte[] a,
871                                                 int offs ) {
872            return (a[offs] & 0xff) | (a[offs + 1] & 0xff) << 8;
873        }
874    
875        /**
876         * If {@link #check()} was successful, returns one the image's horizontal resolution in pixels.
877         * 
878         * @return image width in pixels
879         */
880        public int getWidth() {
881            return width;
882        }
883    
884        /**
885         * Returns whether the image is stored in a progressive (also called: interlaced) way.
886         * 
887         * @return true for progressive/interlaced, false otherwise
888         */
889        public boolean isProgressive() {
890            return progressive;
891        }
892    
893        /**
894         * To use this class as a command line application, give it either some file names as parameters (information on them will be
895         * printed to standard output, one line per file) or call it with no parameters. It will then check data given to it via
896         * standard input.
897         * 
898         * @param args the program arguments which must be file names
899         */
900        public static void main( String[] args ) {
901            ImageMetadata imageMetadata = new ImageMetadata();
902            imageMetadata.setDetermineImageNumber(true);
903            boolean verbose = determineVerbosity(args);
904            if (args.length == 0) {
905                run(null, System.in, imageMetadata, verbose);
906            } else {
907                int index = 0;
908                while (index < args.length) {
909                    InputStream in = null;
910                    try {
911                        String name = args[index++];
912                        System.out.print(name + ";");
913                        if (name.startsWith("http://")) {
914                            in = new URL(name).openConnection().getInputStream();
915                        } else {
916                            in = new FileInputStream(name);
917                        }
918                        run(name, in, imageMetadata, verbose);
919                        in.close();
920                    } catch (IOException e) {
921                        System.out.println(e);
922                        try {
923                            if (in != null) {
924                                in.close();
925                            }
926                        } catch (IOException ee) {
927                        }
928                    }
929                }
930            }
931        }
932    
933        private static void print( String sourceName,
934                                   ImageMetadata ii,
935                                   boolean verbose ) {
936            if (verbose) {
937                printVerbose(sourceName, ii);
938            } else {
939                printCompact(sourceName, ii);
940            }
941        }
942    
943        private static void printCompact( String sourceName,
944                                          ImageMetadata imageMetadata ) {
945            final String SEP = "\t";
946            System.out.println(sourceName + SEP + imageMetadata.getFormatName() + SEP + imageMetadata.getMimeType() + SEP
947                               + imageMetadata.getWidth() + SEP + imageMetadata.getHeight() + SEP + imageMetadata.getBitsPerPixel()
948                               + SEP + imageMetadata.getNumberOfImages() + SEP + imageMetadata.getPhysicalWidthDpi() + SEP
949                               + imageMetadata.getPhysicalHeightDpi() + SEP + imageMetadata.getPhysicalWidthInch() + SEP
950                               + imageMetadata.getPhysicalHeightInch() + SEP + imageMetadata.isProgressive());
951        }
952    
953        private static void printLine( int indentLevels,
954                                       String text,
955                                       float value,
956                                       float minValidValue ) {
957            if (value < minValidValue) {
958                return;
959            }
960            printLine(indentLevels, text, Float.toString(value));
961        }
962    
963        private static void printLine( int indentLevels,
964                                       String text,
965                                       int value,
966                                       int minValidValue ) {
967            if (value >= minValidValue) {
968                printLine(indentLevels, text, Integer.toString(value));
969            }
970        }
971    
972        private static void printLine( int indentLevels,
973                                       String text,
974                                       String value ) {
975            if (value == null || value.length() == 0) {
976                return;
977            }
978            while (indentLevels-- > 0) {
979                System.out.print("\t");
980            }
981            if (text != null && text.length() > 0) {
982                System.out.print(text);
983                System.out.print(" ");
984            }
985            System.out.println(value);
986        }
987    
988        private static void printVerbose( String sourceName,
989                                          ImageMetadata ii ) {
990            printLine(0, null, sourceName);
991            printLine(1, "File format: ", ii.getFormatName());
992            printLine(1, "MIME type: ", ii.getMimeType());
993            printLine(1, "Width (pixels): ", ii.getWidth(), 1);
994            printLine(1, "Height (pixels): ", ii.getHeight(), 1);
995            printLine(1, "Bits per pixel: ", ii.getBitsPerPixel(), 1);
996            printLine(1, "Progressive: ", ii.isProgressive() ? "yes" : "no");
997            printLine(1, "Number of images: ", ii.getNumberOfImages(), 1);
998            printLine(1, "Physical width (dpi): ", ii.getPhysicalWidthDpi(), 1);
999            printLine(1, "Physical height (dpi): ", ii.getPhysicalHeightDpi(), 1);
1000            printLine(1, "Physical width (inches): ", ii.getPhysicalWidthInch(), 1.0f);
1001            printLine(1, "Physical height (inches): ", ii.getPhysicalHeightInch(), 1.0f);
1002            int numComments = ii.getNumberOfComments();
1003            printLine(1, "Number of textual comments: ", numComments, 1);
1004            if (numComments > 0) {
1005                for (int i = 0; i < numComments; i++) {
1006                    printLine(2, null, ii.getComment(i));
1007                }
1008            }
1009        }
1010    
1011        private int read() throws IOException {
1012            if (in != null) {
1013                return in.read();
1014            }
1015            return din.readByte();
1016        }
1017    
1018        private int read( byte[] a ) throws IOException {
1019            if (in != null) {
1020                return in.read(a);
1021            }
1022            din.readFully(a);
1023            return a.length;
1024        }
1025    
1026        private int read( byte[] a,
1027                          int offset,
1028                          int num ) throws IOException {
1029            if (in != null) {
1030                return in.read(a, offset, num);
1031            }
1032            din.readFully(a, offset, num);
1033            return num;
1034        }
1035    
1036        private String readLine() throws IOException {
1037            return readLine(new StringBuffer());
1038        }
1039    
1040        private String readLine( StringBuffer sb ) throws IOException {
1041            boolean finished;
1042            do {
1043                int value = read();
1044                finished = (value == -1 || value == 10);
1045                if (!finished) {
1046                    sb.append((char)value);
1047                }
1048            } while (!finished);
1049            return sb.toString();
1050        }
1051    
1052        private static void run( String sourceName,
1053                                 InputStream in,
1054                                 ImageMetadata imageMetadata,
1055                                 boolean verbose ) {
1056            imageMetadata.setInput(in);
1057            imageMetadata.setDetermineImageNumber(true);
1058            imageMetadata.setCollectComments(verbose);
1059            if (imageMetadata.check()) {
1060                print(sourceName, imageMetadata, verbose);
1061            }
1062        }
1063    
1064        /**
1065         * Specify whether textual comments are supposed to be extracted from input. Default is <code>false</code>. If enabled,
1066         * comments will be added to an internal list.
1067         * 
1068         * @param newValue if <code>true</code>, this class will read comments
1069         * @see #getNumberOfComments
1070         * @see #getComment
1071         */
1072        public void setCollectComments( boolean newValue ) {
1073            collectComments = newValue;
1074        }
1075    
1076        /**
1077         * Specify whether the number of images in a file is to be determined - default is <code>false</code>. This is a special
1078         * option because some file formats require running over the entire file to find out the number of images, a rather
1079         * time-consuming task. Not all file formats support more than one image. If this method is called with <code>true</code> as
1080         * argument, the actual number of images can be queried via {@link #getNumberOfImages()} after a successful call to
1081         * {@link #check()}.
1082         * 
1083         * @param newValue will the number of images be determined?
1084         * @see #getNumberOfImages
1085         */
1086        public void setDetermineImageNumber( boolean newValue ) {
1087            determineNumberOfImages = newValue;
1088        }
1089    
1090        /**
1091         * Set the input stream to the argument stream (or file). Note that {@link java.io.RandomAccessFile} implements
1092         * {@link java.io.DataInput}.
1093         * 
1094         * @param dataInput the input stream to read from
1095         */
1096        public void setInput( DataInput dataInput ) {
1097            din = dataInput;
1098            in = null;
1099        }
1100    
1101        /**
1102         * Set the input stream to the argument stream (or file).
1103         * 
1104         * @param inputStream the input stream to read from
1105         */
1106        public void setInput( InputStream inputStream ) {
1107            in = inputStream;
1108            din = null;
1109        }
1110    
1111        private void setPhysicalHeightDpi( int newValue ) {
1112            physicalWidthDpi = newValue;
1113        }
1114    
1115        private void setPhysicalWidthDpi( int newValue ) {
1116            physicalHeightDpi = newValue;
1117        }
1118    
1119        private void skip( int num ) throws IOException {
1120            while (num > 0) {
1121                long result;
1122                if (in != null) {
1123                    result = in.skip(num);
1124                } else {
1125                    result = din.skipBytes(num);
1126                }
1127                if (result > 0) {
1128                    num -= result;
1129                } else {
1130                    if (in != null) {
1131                        result = in.read();
1132                    } else {
1133                        result = din.readByte();
1134                    }
1135                    if (result == -1) {
1136                        throw new IOException("Premature end of input.");
1137                    }
1138                    num--;
1139                }
1140            }
1141        }
1142    }