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