View Javadoc

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