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.images;
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("Not a supported image file format.");
036 * return;
037 * }
038 * System.out.println(ii.getFormatName() + ", " + ii.getMimeType() + ", " + ii.getWidth() + " x " + ii.getHeight() + " pixels, "
039 * + ii.getBitsPerPixel() + " bits per pixel, " + ii.getNumberOfImages() + " image(s), "
040 * + ii.getNumberOfComments() + " comment(s).");
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 < 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<String></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 >= 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 }