001 /* 002 * ImageInfo.java 003 * 004 * Version 1.9 005 * 006 * A Java class to determine image width, height and color depth for 007 * a number of image file formats. 008 * 009 * Written by Marco Schmidt 010 * 011 * Contributed to the Public Domain. 012 */ 013 014 package org.jboss.dna.sequencer.image; 015 016 import java.io.DataInput; 017 import java.io.FileInputStream; 018 import java.io.IOException; 019 import java.io.InputStream; 020 import java.net.URL; 021 import java.util.Vector; 022 023 /** 024 * Get file format, image resolution, number of bits per pixel and optionally number of images, comments and physical resolution 025 * from JPEG, GIF, BMP, PCX, PNG, IFF, RAS, PBM, PGM, PPM and PSD files (or input streams). 026 * <p> 027 * Use the class like this: 028 * 029 * <pre> 030 * ImageMetadata ii = new ImageMetadata(); 031 * ii.setInput(in); // in can be InputStream or RandomAccessFile 032 * ii.setDetermineImageNumber(true); // default is false 033 * ii.setCollectComments(true); // default is false 034 * if (!ii.check()) { 035 * System.err.println("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 }