0001: /*
0002: * ImageInfo.java
0003: *
0004: * Version 1.9
0005: *
0006: * A Java class to determine image width, height and color depth for
0007: * a number of image file formats.
0008: *
0009: * Written by Marco Schmidt
0010: *
0011: * Contributed to the Public Domain.
0012: *
0013: * Tweaked slightly by Daniel Henninger
0014: */
0015:
0016: package org.jivesoftware.openfire.gateway.avatars;
0017:
0018: import java.io.DataInput;
0019: import java.io.FileInputStream;
0020: import java.io.InputStream;
0021: import java.io.IOException;
0022: import java.net.URL;
0023: import java.util.Vector;
0024:
0025: /**
0026: * Get file format, image resolution, number of bits per pixel and optionally
0027: * number of images, comments and physical resolution from
0028: * JPEG, GIF, BMP, PCX, PNG, IFF, RAS, PBM, PGM, PPM and PSD files
0029: * (or input streams).
0030: * <p>
0031: * Use the class like this:
0032: * <pre>
0033: * ImageInfo ii = new ImageInfo();
0034: * ii.setInput(in); // in can be InputStream or RandomAccessFile
0035: * ii.setDetermineImageNumber(true); // default is false
0036: * ii.setCollectComments(true); // default is false
0037: * if (!ii.check()) {
0038: * System.err.println("Not a supported image file format.");
0039: * return;
0040: * }
0041: * System.out.println(ii.getFormatName() + ", " + ii.getMimeType() +
0042: * ", " + ii.getWidth() + " x " + ii.getHeight() + " pixels, " +
0043: * ii.getBitsPerPixel() + " bits per pixel, " + ii.getNumberOfImages() +
0044: * " image(s), " + ii.getNumberOfComments() + " comment(s).");
0045: * // there are other properties, check out the API documentation
0046: * </pre>
0047: * You can also use this class as a command line program.
0048: * Call it with a number of image file names and URLs as parameters:
0049: * <pre>
0050: * java ImageInfo *.jpg *.png *.gif http://somesite.tld/image.jpg
0051: * </pre>
0052: * or call it without parameters and pipe data to it:
0053: * <pre>
0054: * java ImageInfo < image.jpg
0055: * </pre>
0056: * <p>
0057: * Known limitations:
0058: * <ul>
0059: * <li>When the determination of the number of images is turned off, GIF bits
0060: * per pixel are only read from the global header.
0061: * For some GIFs, local palettes change this to a typically larger
0062: * value. To be certain to get the correct color depth, call
0063: * setDetermineImageNumber(true) before calling check().
0064: * The complete scan over the GIF file will take additional time.</li>
0065: * <li>Transparency information is not included in the bits per pixel count.
0066: * Actually, it was my decision not to include those bits, so it's a feature! ;-)</li>
0067: * </ul>
0068: * <p>
0069: * Requirements:
0070: * <ul>
0071: * <li>Java 1.1 or higher</li>
0072: * </ul>
0073: * <p>
0074: * The latest version can be found at <a href="http://schmidt.devlib.org/image-info/">http://schmidt.devlib.org/image-info/</a>.
0075: * <p>
0076: * Written by Marco Schmidt.
0077: * <p>
0078: * This class is contributed to the Public Domain.
0079: * Use it at your own risk.
0080: * <p>
0081: * <a name="history">History</a>:
0082: * <ul>
0083: * <li><strong>2001-08-24</strong> Initial version.</li>
0084: * <li><strong>2001-10-13</strong> Added support for the file formats BMP and PCX.</li>
0085: * <li><strong>2001-10-16</strong> Fixed bug in read(int[], int, int) that returned
0086: * <li><strong>2002-01-22</strong> Added support for file formats Amiga IFF and Sun Raster (RAS).</li>
0087: * <li><strong>2002-01-24</strong> Added support for file formats Portable Bitmap / Graymap / Pixmap (PBM, PGM, PPM) and Adobe Photoshop (PSD).
0088: * Added new method getMimeType() to return the MIME type associated with a particular file format.</li>
0089: * <li><strong>2002-03-15</strong> Added support to recognize number of images in file. Only works with GIF.
0090: * Use {@link #setDetermineImageNumber} with <code>true</code> as argument to identify animated GIFs
0091: * ({@link #getNumberOfImages()} will return a value larger than <code>1</code>).</li>
0092: * <li><strong>2002-04-10</strong> Fixed a bug in the feature 'determine number of images in animated GIF' introduced with version 1.1.
0093: * Thanks to Marcelo P. Lima for sending in the bug report.
0094: * Released as 1.1.1.</li>
0095: * <li><strong>2002-04-18</strong> Added {@link #setCollectComments(boolean)}.
0096: * That new method lets the user specify whether textual comments are to be
0097: * stored in an internal list when encountered in an input image file / stream.
0098: * Added two methods to return the physical width and height of the image in dpi:
0099: * {@link #getPhysicalWidthDpi()} and {@link #getPhysicalHeightDpi()}.
0100: * If the physical resolution could not be retrieved, these methods return <code>-1</code>.
0101: * </li>
0102: * <li><strong>2002-04-23</strong> Added support for the new properties physical resolution and
0103: * comments for some formats. Released as 1.2.</li>
0104: * <li><strong>2002-06-17</strong> Added support for SWF, sent in by Michael Aird.
0105: * Changed checkJpeg() so that other APP markers than APP0 will not lead to a failure anymore.
0106: * Released as 1.3.</li>
0107: * <li><strong>2003-07-28</strong> Bug fix - skip method now takes return values into consideration.
0108: * Less bytes than necessary may have been skipped, leading to flaws in the retrieved information in some cases.
0109: * Thanks to Bernard Bernstein for pointing that out.
0110: * Released as 1.4.</li>
0111: * <li><strong>2004-02-29</strong> Added support for recognizing progressive JPEG and
0112: * interlaced PNG and GIF. A new method {@link #isProgressive()} returns whether ImageInfo
0113: * has found that the storage type is progressive (or interlaced).
0114: * Thanks to Joe Germuska for suggesting the feature.
0115: * Bug fix: BMP physical resolution is now correctly determined.
0116: * Released as 1.5.</li>
0117: * <li><strong>2004-11-30</strong> Bug fix: recognizing progressive GIFs
0118: * (interlaced in GIF terminology) did not work (thanks to Franz Jeitler for
0119: * pointing this out). Now it should work, but only if the number of images is determined.
0120: * This is because information on interlacing is stored in a local image header.
0121: * In theory, different images could be stored interlaced and non-interlaced in one
0122: * file. However, I think that's unlikely. Right now, the last image in the GIF file
0123: * that is examined by ImageInfo is used for the "progressive" status.</li>
0124: * <li><strong>2005-01-02</strong> Some code clean up (unused methods and variables
0125: * commented out, missing javadoc comments, etc.). Thanks to George Sexton for a long list.
0126: * Removed usage of Boolean.toString because
0127: * it's a Java 1.4+ feature (thanks to Gregor Dupont).
0128: * Changed delimiter character in compact output from semicolon to tabulator
0129: * (for better integration with cut(1) and other Unix tools).
0130: * Added some points to the <a href="http://schmidt.devlib.org/image-info/index.html#knownissues">'Known
0131: * issues' section of the website</a>.
0132: * Released as 1.6.</li>
0133: * <li><strong>2005-07-26</strong> Removed code to identify Flash (SWF) files.
0134: * Has repeatedly led to problems and support requests, and I don't know the
0135: * format and don't have the time and interest to fix it myself.
0136: * I repeatedly included fixes by others which didn't work for some people.
0137: * I give up on SWF. Please do not contact me about it anymore.
0138: * Set package of ImageInfo class to org.devlib.schmidt.imageinfo (a package
0139: * was repeatedly requested by some users).
0140: * Released as 1.7.</li>
0141: * <li><strong>2006-02-23</strong> Removed Flash helper methods which weren't used elsewhere.
0142: * Updated skip method which tries "read" whenever "skip(Bytes)" returns a result of 0.
0143: * The old method didn't work with certain input stream types on truncated data streams.
0144: * Thanks to Martin Leidig for reporting this and sending in code.
0145: * Released as 1.8.</li>
0146: * </li>
0147: * <li><strong>2006-11-13</strong> Removed check that made ImageInfo report JPEG APPx
0148: * markers smaller than 14 bytes as files in unknown format. Such JPEGs seem to be
0149: * generated by Google's Picasa application. First reported with fix by
0150: * Karl von Randow. Released as 1.9.</li>
0151: * </ul>
0152: * @author Marco Schmidt
0153: */
0154: public class ImageInfo {
0155: /**
0156: * Return value of {@link #getFormat()} for JPEG streams.
0157: * ImageInfo can extract physical resolution and comments
0158: * from JPEGs (only from APP0 headers).
0159: * Only one image can be stored in a file.
0160: * It is determined whether the JPEG stream is progressive
0161: * (see {@link #isProgressive()}).
0162: */
0163: public static final int FORMAT_JPEG = 0;
0164:
0165: /**
0166: * Return value of {@link #getFormat()} for GIF streams.
0167: * ImageInfo can extract comments from GIFs and count the number
0168: * of images (GIFs with more than one image are animations).
0169: * It is determined whether the GIF stream is interlaced (see {@link #isProgressive()}).
0170: */
0171: public static final int FORMAT_GIF = 1;
0172:
0173: /**
0174: * Return value of {@link #getFormat()} for PNG streams.
0175: * PNG only supports one image per file.
0176: * Both physical resolution and comments can be stored with PNG,
0177: * but ImageInfo is currently not able to extract those.
0178: * It is determined whether the PNG stream is interlaced (see {@link #isProgressive()}).
0179: */
0180: public static final int FORMAT_PNG = 2;
0181:
0182: /**
0183: * Return value of {@link #getFormat()} for BMP streams.
0184: * BMP only supports one image per file.
0185: * BMP does not allow for comments.
0186: * The physical resolution can be stored.
0187: */
0188: public static final int FORMAT_BMP = 3;
0189:
0190: /**
0191: * Return value of {@link #getFormat()} for PCX streams.
0192: * PCX does not allow for comments or more than one image per file.
0193: * However, the physical resolution can be stored.
0194: */
0195: public static final int FORMAT_PCX = 4;
0196:
0197: /**
0198: * Return value of {@link #getFormat()} for IFF streams.
0199: */
0200: public static final int FORMAT_IFF = 5;
0201:
0202: /**
0203: * Return value of {@link #getFormat()} for RAS streams.
0204: * Sun Raster allows for one image per file only and is not able to
0205: * store physical resolution or comments.
0206: */
0207: public static final int FORMAT_RAS = 6;
0208:
0209: /** Return value of {@link #getFormat()} for PBM streams. */
0210: public static final int FORMAT_PBM = 7;
0211:
0212: /** Return value of {@link #getFormat()} for PGM streams. */
0213: public static final int FORMAT_PGM = 8;
0214:
0215: /** Return value of {@link #getFormat()} for PPM streams. */
0216: public static final int FORMAT_PPM = 9;
0217:
0218: /** Return value of {@link #getFormat()} for PSD streams. */
0219: public static final int FORMAT_PSD = 10;
0220:
0221: /* public static final int COLOR_TYPE_UNKNOWN = -1;
0222: public static final int COLOR_TYPE_TRUECOLOR_RGB = 0;
0223: public static final int COLOR_TYPE_PALETTED = 1;
0224: public static final int COLOR_TYPE_GRAYSCALE= 2;
0225: public static final int COLOR_TYPE_BLACK_AND_WHITE = 3;*/
0226:
0227: /**
0228: * The names of all supported file formats.
0229: * The FORMAT_xyz int constants can be used as index values for
0230: * this array.
0231: */
0232: private static final String[] FORMAT_NAMES = { "JPEG", "GIF",
0233: "PNG", "BMP", "PCX", "IFF", "RAS", "PBM", "PGM", "PPM",
0234: "PSD" };
0235:
0236: /**
0237: * The names of the MIME types for all supported file formats.
0238: * The FORMAT_xyz int constants can be used as index values for
0239: * this array.
0240: */
0241: private static final String[] MIME_TYPE_STRINGS = { "image/jpeg",
0242: "image/gif", "image/png", "image/bmp", "image/pcx",
0243: "image/iff", "image/ras", "image/x-portable-bitmap",
0244: "image/x-portable-graymap", "image/x-portable-pixmap",
0245: "image/psd" };
0246:
0247: private int width;
0248: private int height;
0249: private int bitsPerPixel;
0250: //private int colorType = COLOR_TYPE_UNKNOWN;
0251: private boolean progressive;
0252: private int format;
0253: private InputStream in;
0254: private DataInput din;
0255: private boolean collectComments = true;
0256: private Vector<String> comments;
0257: private boolean determineNumberOfImages;
0258: private int numberOfImages;
0259: private int physicalHeightDpi;
0260: private int physicalWidthDpi;
0261:
0262: private void addComment(String s) {
0263: if (comments == null) {
0264: comments = new Vector<String>();
0265: }
0266: comments.addElement(s);
0267: }
0268:
0269: /**
0270: * Call this method after you have provided an input stream or file
0271: * using {@link #setInput(InputStream)} or {@link #setInput(DataInput)}.
0272: * If true is returned, the file format was known and information
0273: * on the file's content can be retrieved using the various getXyz methods.
0274: * @return if information could be retrieved from input
0275: */
0276: public boolean check() {
0277: format = -1;
0278: width = -1;
0279: height = -1;
0280: bitsPerPixel = -1;
0281: numberOfImages = 1;
0282: physicalHeightDpi = -1;
0283: physicalWidthDpi = -1;
0284: comments = null;
0285: try {
0286: int b1 = read() & 0xff;
0287: int b2 = read() & 0xff;
0288: if (b1 == 0x47 && b2 == 0x49) {
0289: return checkGif();
0290: } else if (b1 == 0x89 && b2 == 0x50) {
0291: return checkPng();
0292: } else if (b1 == 0xff && b2 == 0xd8) {
0293: return checkJpeg();
0294: } else if (b1 == 0x42 && b2 == 0x4d) {
0295: return checkBmp();
0296: } else if (b1 == 0x0a && b2 < 0x06) {
0297: return checkPcx();
0298: } else if (b1 == 0x46 && b2 == 0x4f) {
0299: return checkIff();
0300: } else if (b1 == 0x59 && b2 == 0xa6) {
0301: return checkRas();
0302: } else if (b1 == 0x50 && b2 >= 0x31 && b2 <= 0x36) {
0303: return checkPnm(b2 - '0');
0304: } else if (b1 == 0x38 && b2 == 0x42) {
0305: return checkPsd();
0306: } else {
0307: return false;
0308: }
0309: } catch (IOException ioe) {
0310: return false;
0311: }
0312: }
0313:
0314: private boolean checkBmp() throws IOException {
0315: byte[] a = new byte[44];
0316: if (read(a) != a.length) {
0317: return false;
0318: }
0319: width = getIntLittleEndian(a, 16);
0320: height = getIntLittleEndian(a, 20);
0321: if (width < 1 || height < 1) {
0322: return false;
0323: }
0324: bitsPerPixel = getShortLittleEndian(a, 26);
0325: if (bitsPerPixel != 1 && bitsPerPixel != 4 && bitsPerPixel != 8
0326: && bitsPerPixel != 16 && bitsPerPixel != 24
0327: && bitsPerPixel != 32) {
0328: return false;
0329: }
0330: int x = (int) (getIntLittleEndian(a, 36) * 0.0254);
0331: if (x > 0) {
0332: setPhysicalWidthDpi(x);
0333: }
0334: int y = (int) (getIntLittleEndian(a, 40) * 0.0254);
0335: if (y > 0) {
0336: setPhysicalHeightDpi(y);
0337: }
0338: format = FORMAT_BMP;
0339: return true;
0340: }
0341:
0342: private boolean checkGif() throws IOException {
0343: final byte[] GIF_MAGIC_87A = { 0x46, 0x38, 0x37, 0x61 };
0344: final byte[] GIF_MAGIC_89A = { 0x46, 0x38, 0x39, 0x61 };
0345: byte[] a = new byte[11]; // 4 from the GIF signature + 7 from the global header
0346: if (read(a) != 11) {
0347: return false;
0348: }
0349: if ((!equals(a, 0, GIF_MAGIC_89A, 0, 4))
0350: && (!equals(a, 0, GIF_MAGIC_87A, 0, 4))) {
0351: return false;
0352: }
0353: format = FORMAT_GIF;
0354: width = getShortLittleEndian(a, 4);
0355: height = getShortLittleEndian(a, 6);
0356: int flags = a[8] & 0xff;
0357: bitsPerPixel = ((flags >> 4) & 0x07) + 1;
0358: //progressive = (flags & 0x02) != 0;
0359: if (!determineNumberOfImages) {
0360: return true;
0361: }
0362: // skip global color palette
0363: if ((flags & 0x80) != 0) {
0364: int tableSize = (1 << ((flags & 7) + 1)) * 3;
0365: skip(tableSize);
0366: }
0367: numberOfImages = 0;
0368: int blockType;
0369: do {
0370: blockType = read();
0371: switch (blockType) {
0372: case (0x2c): // image separator
0373: {
0374: if (read(a, 0, 9) != 9) {
0375: return false;
0376: }
0377: flags = a[8] & 0xff;
0378: progressive = (flags & 0x40) != 0;
0379: /*int locWidth = getShortLittleEndian(a, 4);
0380: int locHeight = getShortLittleEndian(a, 6);
0381: System.out.println("LOCAL: " + locWidth + " x " + locHeight);*/
0382: int localBitsPerPixel = (flags & 0x07) + 1;
0383: if (localBitsPerPixel > bitsPerPixel) {
0384: bitsPerPixel = localBitsPerPixel;
0385: }
0386: if ((flags & 0x80) != 0) {
0387: skip((1 << localBitsPerPixel) * 3);
0388: }
0389: skip(1); // initial code length
0390: int n;
0391: do {
0392: n = read();
0393: if (n > 0) {
0394: skip(n);
0395: } else if (n == -1) {
0396: return false;
0397: }
0398: } while (n > 0);
0399: numberOfImages++;
0400: break;
0401: }
0402: case (0x21): // extension
0403: {
0404: int extensionType = read();
0405: if (collectComments && extensionType == 0xfe) {
0406: StringBuffer sb = new StringBuffer();
0407: int n;
0408: do {
0409: n = read();
0410: if (n == -1) {
0411: return false;
0412: }
0413: if (n > 0) {
0414: for (int i = 0; i < n; i++) {
0415: int ch = read();
0416: if (ch == -1) {
0417: return false;
0418: }
0419: sb.append((char) ch);
0420: }
0421: }
0422: } while (n > 0);
0423: } else {
0424: int n;
0425: do {
0426: n = read();
0427: if (n > 0) {
0428: skip(n);
0429: } else if (n == -1) {
0430: return false;
0431: }
0432: } while (n > 0);
0433: }
0434: break;
0435: }
0436: case (0x3b): // end of file
0437: {
0438: break;
0439: }
0440: default: {
0441: return false;
0442: }
0443: }
0444: } while (blockType != 0x3b);
0445: return true;
0446: }
0447:
0448: private boolean checkIff() throws IOException {
0449: byte[] a = new byte[10];
0450: // read remaining 2 bytes of file id, 4 bytes file size
0451: // and 4 bytes IFF subformat
0452: if (read(a, 0, 10) != 10) {
0453: return false;
0454: }
0455: final byte[] IFF_RM = { 0x52, 0x4d };
0456: if (!equals(a, 0, IFF_RM, 0, 2)) {
0457: return false;
0458: }
0459: int type = getIntBigEndian(a, 6);
0460: if (type != 0x494c424d && // type must be ILBM...
0461: type != 0x50424d20) { // ...or PBM
0462: return false;
0463: }
0464: // loop chunks to find BMHD chunk
0465: do {
0466: if (read(a, 0, 8) != 8) {
0467: return false;
0468: }
0469: int chunkId = getIntBigEndian(a, 0);
0470: int size = getIntBigEndian(a, 4);
0471: if ((size & 1) == 1) {
0472: size++;
0473: }
0474: if (chunkId == 0x424d4844) { // BMHD chunk
0475: if (read(a, 0, 9) != 9) {
0476: return false;
0477: }
0478: format = FORMAT_IFF;
0479: width = getShortBigEndian(a, 0);
0480: height = getShortBigEndian(a, 2);
0481: bitsPerPixel = a[8] & 0xff;
0482: return (width > 0 && height > 0 && bitsPerPixel > 0 && bitsPerPixel < 33);
0483: } else {
0484: skip(size);
0485: }
0486: } while (true);
0487: }
0488:
0489: private boolean checkJpeg() throws IOException {
0490: byte[] data = new byte[12];
0491: while (true) {
0492: if (read(data, 0, 4) != 4) {
0493: return false;
0494: }
0495: int marker = getShortBigEndian(data, 0);
0496: int size = getShortBigEndian(data, 2);
0497: if ((marker & 0xff00) != 0xff00) {
0498: return false; // not a valid marker
0499: }
0500: if (marker == 0xffe0) { // APPx
0501: if (size < 14) {
0502: // not an APPx header as we know it, skip
0503: skip(size - 2);
0504: continue;
0505: }
0506: if (read(data, 0, 12) != 12) {
0507: return false;
0508: }
0509: final byte[] APP0_ID = { 0x4a, 0x46, 0x49, 0x46, 0x00 };
0510: if (equals(APP0_ID, 0, data, 0, 5)) {
0511: //System.out.println("data 7=" + data[7]);
0512: if (data[7] == 1) {
0513: setPhysicalWidthDpi(getShortBigEndian(data, 8));
0514: setPhysicalHeightDpi(getShortBigEndian(data, 10));
0515: } else if (data[7] == 2) {
0516: int x = getShortBigEndian(data, 8);
0517: int y = getShortBigEndian(data, 10);
0518: setPhysicalWidthDpi((int) (x * 2.54f));
0519: setPhysicalHeightDpi((int) (y * 2.54f));
0520: }
0521: }
0522: skip(size - 14);
0523: } else if (collectComments && size > 2 && marker == 0xfffe) { // comment
0524: size -= 2;
0525: byte[] chars = new byte[size];
0526: if (read(chars, 0, size) != size) {
0527: return false;
0528: }
0529: String comment = new String(chars, "iso-8859-1");
0530: comment = comment.trim();
0531: addComment(comment);
0532: } else if (marker >= 0xffc0 && marker <= 0xffcf
0533: && marker != 0xffc4 && marker != 0xffc8) {
0534: if (read(data, 0, 6) != 6) {
0535: return false;
0536: }
0537: format = FORMAT_JPEG;
0538: bitsPerPixel = (data[0] & 0xff) * (data[5] & 0xff);
0539: progressive = marker == 0xffc2 || marker == 0xffc6
0540: || marker == 0xffca || marker == 0xffce;
0541: width = getShortBigEndian(data, 3);
0542: height = getShortBigEndian(data, 1);
0543: return true;
0544: } else {
0545: skip(size - 2);
0546: }
0547: }
0548: }
0549:
0550: private boolean checkPcx() throws IOException {
0551: byte[] a = new byte[64];
0552: if (read(a) != a.length) {
0553: return false;
0554: }
0555: if (a[0] != 1) { // encoding, 1=RLE is only valid value
0556: return false;
0557: }
0558: // width / height
0559: int x1 = getShortLittleEndian(a, 2);
0560: int y1 = getShortLittleEndian(a, 4);
0561: int x2 = getShortLittleEndian(a, 6);
0562: int y2 = getShortLittleEndian(a, 8);
0563: if (x1 < 0 || x2 < x1 || y1 < 0 || y2 < y1) {
0564: return false;
0565: }
0566: width = x2 - x1 + 1;
0567: height = y2 - y1 + 1;
0568: // color depth
0569: int bits = a[1];
0570: int planes = a[63];
0571: if (planes == 1
0572: && (bits == 1 || bits == 2 || bits == 4 || bits == 8)) {
0573: // paletted
0574: bitsPerPixel = bits;
0575: } else if (planes == 3 && bits == 8) {
0576: // RGB truecolor
0577: bitsPerPixel = 24;
0578: } else {
0579: return false;
0580: }
0581: setPhysicalWidthDpi(getShortLittleEndian(a, 10));
0582: setPhysicalHeightDpi(getShortLittleEndian(a, 10));
0583: format = FORMAT_PCX;
0584: return true;
0585: }
0586:
0587: private boolean checkPng() throws IOException {
0588: final byte[] PNG_MAGIC = { 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a };
0589: byte[] a = new byte[27];
0590: if (read(a) != 27) {
0591: return false;
0592: }
0593: if (!equals(a, 0, PNG_MAGIC, 0, 6)) {
0594: return false;
0595: }
0596: format = FORMAT_PNG;
0597: width = getIntBigEndian(a, 14);
0598: height = getIntBigEndian(a, 18);
0599: bitsPerPixel = a[22] & 0xff;
0600: int colorType = a[23] & 0xff;
0601: if (colorType == 2 || colorType == 6) {
0602: bitsPerPixel *= 3;
0603: }
0604: progressive = (a[26] & 0xff) != 0;
0605: return true;
0606: }
0607:
0608: private boolean checkPnm(int id) throws IOException {
0609: if (id < 1 || id > 6) {
0610: return false;
0611: }
0612: final int[] PNM_FORMATS = { FORMAT_PBM, FORMAT_PGM, FORMAT_PPM };
0613: format = PNM_FORMATS[(id - 1) % 3];
0614: boolean hasPixelResolution = false;
0615: String s;
0616: while (true) {
0617: s = readLine();
0618: if (s != null) {
0619: s = s.trim();
0620: }
0621: if (s == null || s.length() < 1) {
0622: continue;
0623: }
0624: if (s.charAt(0) == '#') { // comment
0625: if (collectComments && s.length() > 1) {
0626: addComment(s.substring(1));
0627: }
0628: continue;
0629: }
0630: if (!hasPixelResolution) { // split "343 966" into width=343, height=966
0631: int spaceIndex = s.indexOf(' ');
0632: if (spaceIndex == -1) {
0633: return false;
0634: }
0635: String widthString = s.substring(0, spaceIndex);
0636: spaceIndex = s.lastIndexOf(' ');
0637: if (spaceIndex == -1) {
0638: return false;
0639: }
0640: String heightString = s.substring(spaceIndex + 1);
0641: try {
0642: width = Integer.parseInt(widthString);
0643: height = Integer.parseInt(heightString);
0644: } catch (NumberFormatException nfe) {
0645: return false;
0646: }
0647: if (width < 1 || height < 1) {
0648: return false;
0649: }
0650: if (format == FORMAT_PBM) {
0651: bitsPerPixel = 1;
0652: return true;
0653: }
0654: hasPixelResolution = true;
0655: } else {
0656: int maxSample;
0657: try {
0658: maxSample = Integer.parseInt(s);
0659: } catch (NumberFormatException nfe) {
0660: return false;
0661: }
0662: if (maxSample < 0) {
0663: return false;
0664: }
0665: for (int i = 0; i < 25; i++) {
0666: if (maxSample < (1 << (i + 1))) {
0667: bitsPerPixel = i + 1;
0668: if (format == FORMAT_PPM) {
0669: bitsPerPixel *= 3;
0670: }
0671: return true;
0672: }
0673: }
0674: return false;
0675: }
0676: }
0677: }
0678:
0679: private boolean checkPsd() throws IOException {
0680: byte[] a = new byte[24];
0681: if (read(a) != a.length) {
0682: return false;
0683: }
0684: final byte[] PSD_MAGIC = { 0x50, 0x53 };
0685: if (!equals(a, 0, PSD_MAGIC, 0, 2)) {
0686: return false;
0687: }
0688: format = FORMAT_PSD;
0689: width = getIntBigEndian(a, 16);
0690: height = getIntBigEndian(a, 12);
0691: int channels = getShortBigEndian(a, 10);
0692: int depth = getShortBigEndian(a, 20);
0693: bitsPerPixel = channels * depth;
0694: return (width > 0 && height > 0 && bitsPerPixel > 0 && bitsPerPixel <= 64);
0695: }
0696:
0697: private boolean checkRas() throws IOException {
0698: byte[] a = new byte[14];
0699: if (read(a) != a.length) {
0700: return false;
0701: }
0702: final byte[] RAS_MAGIC = { 0x6a, (byte) 0x95 };
0703: if (!equals(a, 0, RAS_MAGIC, 0, 2)) {
0704: return false;
0705: }
0706: format = FORMAT_RAS;
0707: width = getIntBigEndian(a, 2);
0708: height = getIntBigEndian(a, 6);
0709: bitsPerPixel = getIntBigEndian(a, 10);
0710: return (width > 0 && height > 0 && bitsPerPixel > 0 && bitsPerPixel <= 24);
0711: }
0712:
0713: /**
0714: * Run over String list, return false if at least one of the arguments
0715: * equals <code>-c</code>.
0716: * @param args string list to check
0717: * @return false if at least one argument equals <code>-c</code>
0718: */
0719: private static boolean determineVerbosity(String[] args) {
0720: if (args != null && args.length > 0) {
0721: for (String arg : args) {
0722: if ("-c".equals(arg)) {
0723: return false;
0724: }
0725: }
0726: }
0727: return true;
0728: }
0729:
0730: private static boolean equals(byte[] a1, int offs1, byte[] a2,
0731: int offs2, int num) {
0732: while (num-- > 0) {
0733: if (a1[offs1++] != a2[offs2++]) {
0734: return false;
0735: }
0736: }
0737: return true;
0738: }
0739:
0740: /**
0741: * If {@link #check()} was successful, returns the image's number of bits per pixel.
0742: * Does not include transparency information like the alpha channel.
0743: * @return number of bits per image pixel
0744: */
0745: public int getBitsPerPixel() {
0746: return bitsPerPixel;
0747: }
0748:
0749: /**
0750: * Returns the index'th comment retrieved from the file.
0751: * @param index int index of comment to return
0752: * @throws IllegalArgumentException if index is smaller than 0 or larger than or equal
0753: * to the number of comments retrieved
0754: * @see #getNumberOfComments
0755: * @return referred comment from file
0756: */
0757: public String getComment(int index) {
0758: if (comments == null || index < 0 || index >= comments.size()) {
0759: throw new IllegalArgumentException(
0760: "Not a valid comment index: " + index);
0761: }
0762: return comments.elementAt(index);
0763: }
0764:
0765: /**
0766: * If {@link #check()} was successful, returns the image format as one
0767: * of the FORMAT_xyz constants from this class.
0768: * Use {@link #getFormatName()} to get a textual description of the file format.
0769: * @return file format as a FORMAT_xyz constant
0770: */
0771: public int getFormat() {
0772: return format;
0773: }
0774:
0775: /**
0776: * If {@link #check()} was successful, returns the image format's name.
0777: * Use {@link #getFormat()} to get a unique number.
0778: * @return file format name
0779: */
0780: public String getFormatName() {
0781: if (format >= 0 && format < FORMAT_NAMES.length) {
0782: return FORMAT_NAMES[format];
0783: } else {
0784: return "?";
0785: }
0786: }
0787:
0788: /**
0789: * If {@link #check()} was successful, returns one the image's vertical
0790: * resolution in pixels.
0791: * @return image height in pixels
0792: */
0793: public int getHeight() {
0794: return height;
0795: }
0796:
0797: private static int getIntBigEndian(byte[] a, int offs) {
0798: return (a[offs] & 0xff) << 24 | (a[offs + 1] & 0xff) << 16
0799: | (a[offs + 2] & 0xff) << 8 | a[offs + 3] & 0xff;
0800: }
0801:
0802: private static int getIntLittleEndian(byte[] a, int offs) {
0803: return (a[offs + 3] & 0xff) << 24 | (a[offs + 2] & 0xff) << 16
0804: | (a[offs + 1] & 0xff) << 8 | a[offs] & 0xff;
0805: }
0806:
0807: /**
0808: * If {@link #check()} was successful, returns a String with the
0809: * MIME type of the format.
0810: * @return MIME type, e.g. <code>image/jpeg</code>
0811: */
0812: public String getMimeType() {
0813: if (format >= 0 && format < MIME_TYPE_STRINGS.length) {
0814: if (format == FORMAT_JPEG && progressive) {
0815: return "image/pjpeg";
0816: }
0817: return MIME_TYPE_STRINGS[format];
0818: } else {
0819: return null;
0820: }
0821: }
0822:
0823: /**
0824: * If {@link #check()} was successful and {@link #setCollectComments(boolean)} was called with
0825: * <code>true</code> as argument, returns the number of comments retrieved
0826: * from the input image stream / file.
0827: * Any number >= 0 and smaller than this number of comments is then a
0828: * valid argument for the {@link #getComment(int)} method.
0829: * @return number of comments retrieved from input image
0830: */
0831: public int getNumberOfComments() {
0832: if (comments == null) {
0833: return 0;
0834: } else {
0835: return comments.size();
0836: }
0837: }
0838:
0839: /**
0840: * Returns the number of images in the examined file.
0841: * Assumes that <code>setDetermineImageNumber(true);</code> was called before
0842: * a successful call to {@link #check()}.
0843: * This value can currently be only different from <code>1</code> for GIF images.
0844: * @return number of images in file
0845: */
0846: public int getNumberOfImages() {
0847: return numberOfImages;
0848: }
0849:
0850: /**
0851: * Returns the physical height of this image in dots per inch (dpi).
0852: * Assumes that {@link #check()} was successful.
0853: * Returns <code>-1</code> on failure.
0854: * @return physical height (in dpi)
0855: * @see #getPhysicalWidthDpi()
0856: * @see #getPhysicalHeightInch()
0857: */
0858: public int getPhysicalHeightDpi() {
0859: return physicalHeightDpi;
0860: }
0861:
0862: /**
0863: * If {@link #check()} was successful, returns the physical width of this image in dpi (dots per inch)
0864: * or -1 if no value could be found.
0865: * @return physical height (in dpi)
0866: * @see #getPhysicalHeightDpi()
0867: * @see #getPhysicalWidthDpi()
0868: * @see #getPhysicalWidthInch()
0869: */
0870: public float getPhysicalHeightInch() {
0871: int h = getHeight();
0872: int ph = getPhysicalHeightDpi();
0873: if (h > 0 && ph > 0) {
0874: return ((float) h) / ((float) ph);
0875: } else {
0876: return -1.0f;
0877: }
0878: }
0879:
0880: /**
0881: * If {@link #check()} was successful, returns the physical width of this image in dpi (dots per inch)
0882: * or -1 if no value could be found.
0883: * @return physical width (in dpi)
0884: * @see #getPhysicalHeightDpi()
0885: * @see #getPhysicalWidthInch()
0886: * @see #getPhysicalHeightInch()
0887: */
0888: public int getPhysicalWidthDpi() {
0889: return physicalWidthDpi;
0890: }
0891:
0892: /**
0893: * Returns the physical width of an image in inches, or
0894: * <code>-1.0f</code> if width information is not available.
0895: * Assumes that {@link #check} has been called successfully.
0896: * @return physical width in inches or <code>-1.0f</code> on failure
0897: * @see #getPhysicalWidthDpi
0898: * @see #getPhysicalHeightInch
0899: */
0900: public float getPhysicalWidthInch() {
0901: int w = getWidth();
0902: int pw = getPhysicalWidthDpi();
0903: if (w > 0 && pw > 0) {
0904: return ((float) w) / ((float) pw);
0905: } else {
0906: return -1.0f;
0907: }
0908: }
0909:
0910: private static int getShortBigEndian(byte[] a, int offs) {
0911: return (a[offs] & 0xff) << 8 | (a[offs + 1] & 0xff);
0912: }
0913:
0914: private static int getShortLittleEndian(byte[] a, int offs) {
0915: return (a[offs] & 0xff) | (a[offs + 1] & 0xff) << 8;
0916: }
0917:
0918: /**
0919: * If {@link #check()} was successful, returns one the image's horizontal
0920: * resolution in pixels.
0921: * @return image width in pixels
0922: */
0923: public int getWidth() {
0924: return width;
0925: }
0926:
0927: /**
0928: * Returns whether the image is stored in a progressive (also called: interlaced) way.
0929: * @return true for progressive/interlaced, false otherwise
0930: */
0931: public boolean isProgressive() {
0932: return progressive;
0933: }
0934:
0935: /**
0936: * To use this class as a command line application, give it either
0937: * some file names as parameters (information on them will be
0938: * printed to standard output, one line per file) or call
0939: * it with no parameters. It will then check data given to it
0940: * via standard input.
0941: * @param args the program arguments which must be file names
0942: */
0943: public static void main(String[] args) {
0944: ImageInfo imageInfo = new ImageInfo();
0945: imageInfo.setDetermineImageNumber(true);
0946: boolean verbose = determineVerbosity(args);
0947: if (args.length == 0) {
0948: run(null, System.in, imageInfo, verbose);
0949: } else {
0950: int index = 0;
0951: while (index < args.length) {
0952: InputStream in = null;
0953: try {
0954: String name = args[index++];
0955: System.out.print(name + ";");
0956: if (name.startsWith("http://")) {
0957: in = new URL(name).openConnection()
0958: .getInputStream();
0959: } else {
0960: in = new FileInputStream(name);
0961: }
0962: run(name, in, imageInfo, verbose);
0963: in.close();
0964: } catch (IOException e) {
0965: System.out.println(e);
0966: try {
0967: if (in != null) {
0968: in.close();
0969: }
0970: } catch (IOException ee) {
0971: // Ignore
0972: }
0973: }
0974: }
0975: }
0976: }
0977:
0978: private static void print(String sourceName, ImageInfo ii,
0979: boolean verbose) {
0980: if (verbose) {
0981: printVerbose(sourceName, ii);
0982: } else {
0983: printCompact(sourceName, ii);
0984: }
0985: }
0986:
0987: private static void printCompact(String sourceName,
0988: ImageInfo imageInfo) {
0989: final String SEP = "\t";
0990: System.out.println(sourceName + SEP + imageInfo.getFormatName()
0991: + SEP + imageInfo.getMimeType() + SEP
0992: + imageInfo.getWidth() + SEP + imageInfo.getHeight()
0993: + SEP + imageInfo.getBitsPerPixel() + SEP
0994: + imageInfo.getNumberOfImages() + SEP
0995: + imageInfo.getPhysicalWidthDpi() + SEP
0996: + imageInfo.getPhysicalHeightDpi() + SEP
0997: + imageInfo.getPhysicalWidthInch() + SEP
0998: + imageInfo.getPhysicalHeightInch() + SEP
0999: + imageInfo.isProgressive());
1000: }
1001:
1002: private static void printLine(int indentLevels, String text,
1003: float value, float minValidValue) {
1004: if (value < minValidValue) {
1005: return;
1006: }
1007: printLine(indentLevels, text, Float.toString(value));
1008: }
1009:
1010: private static void printLine(int indentLevels, String text,
1011: int value, int minValidValue) {
1012: if (value >= minValidValue) {
1013: printLine(indentLevels, text, Integer.toString(value));
1014: }
1015: }
1016:
1017: private static void printLine(int indentLevels, String text,
1018: String value) {
1019: if (value == null || value.length() == 0) {
1020: return;
1021: }
1022: while (indentLevels-- > 0) {
1023: System.out.print("\t");
1024: }
1025: if (text != null && text.length() > 0) {
1026: System.out.print(text);
1027: System.out.print(" ");
1028: }
1029: System.out.println(value);
1030: }
1031:
1032: private static void printVerbose(String sourceName, ImageInfo ii) {
1033: printLine(0, null, sourceName);
1034: printLine(1, "File format: ", ii.getFormatName());
1035: printLine(1, "MIME type: ", ii.getMimeType());
1036: printLine(1, "Width (pixels): ", ii.getWidth(), 1);
1037: printLine(1, "Height (pixels): ", ii.getHeight(), 1);
1038: printLine(1, "Bits per pixel: ", ii.getBitsPerPixel(), 1);
1039: printLine(1, "Progressive: ", ii.isProgressive() ? "yes" : "no");
1040: printLine(1, "Number of images: ", ii.getNumberOfImages(), 1);
1041: printLine(1, "Physical width (dpi): ",
1042: ii.getPhysicalWidthDpi(), 1);
1043: printLine(1, "Physical height (dpi): ", ii
1044: .getPhysicalHeightDpi(), 1);
1045: printLine(1, "Physical width (inches): ", ii
1046: .getPhysicalWidthInch(), 1.0f);
1047: printLine(1, "Physical height (inches): ", ii
1048: .getPhysicalHeightInch(), 1.0f);
1049: int numComments = ii.getNumberOfComments();
1050: printLine(1, "Number of textual comments: ", numComments, 1);
1051: if (numComments > 0) {
1052: for (int i = 0; i < numComments; i++) {
1053: printLine(2, null, ii.getComment(i));
1054: }
1055: }
1056: }
1057:
1058: private int read() throws IOException {
1059: if (in != null) {
1060: return in.read();
1061: } else {
1062: return din.readByte();
1063: }
1064: }
1065:
1066: private int read(byte[] a) throws IOException {
1067: if (in != null) {
1068: return in.read(a);
1069: } else {
1070: din.readFully(a);
1071: return a.length;
1072: }
1073: }
1074:
1075: private int read(byte[] a, int offset, int num) throws IOException {
1076: if (in != null) {
1077: return in.read(a, offset, num);
1078: } else {
1079: din.readFully(a, offset, num);
1080: return num;
1081: }
1082: }
1083:
1084: private String readLine() throws IOException {
1085: return readLine(new StringBuffer());
1086: }
1087:
1088: private String readLine(StringBuffer sb) throws IOException {
1089: boolean finished;
1090: do {
1091: int value = read();
1092: finished = (value == -1 || value == 10);
1093: if (!finished) {
1094: sb.append((char) value);
1095: }
1096: } while (!finished);
1097: return sb.toString();
1098: }
1099:
1100: private static void run(String sourceName, InputStream in,
1101: ImageInfo imageInfo, boolean verbose) {
1102: imageInfo.setInput(in);
1103: imageInfo.setDetermineImageNumber(true);
1104: imageInfo.setCollectComments(verbose);
1105: if (imageInfo.check()) {
1106: print(sourceName, imageInfo, verbose);
1107: }
1108: }
1109:
1110: /**
1111: * Specify whether textual comments are supposed to be extracted from input.
1112: * Default is <code>false</code>.
1113: * If enabled, comments will be added to an internal list.
1114: * @param newValue if <code>true</code>, this class will read comments
1115: * @see #getNumberOfComments
1116: * @see #getComment
1117: */
1118: public void setCollectComments(boolean newValue) {
1119: collectComments = newValue;
1120: }
1121:
1122: /**
1123: * Specify whether the number of images in a file is to be
1124: * determined - default is <code>false</code>.
1125: * This is a special option because some file formats require running over
1126: * the entire file to find out the number of images, a rather time-consuming
1127: * task.
1128: * Not all file formats support more than one image.
1129: * If this method is called with <code>true</code> as argument,
1130: * the actual number of images can be queried via
1131: * {@link #getNumberOfImages()} after a successful call to
1132: * {@link #check()}.
1133: * @param newValue will the number of images be determined?
1134: * @see #getNumberOfImages
1135: */
1136: public void setDetermineImageNumber(boolean newValue) {
1137: determineNumberOfImages = newValue;
1138: }
1139:
1140: /**
1141: * Set the input stream to the argument stream (or file).
1142: * Note that {@link java.io.RandomAccessFile} implements
1143: * {@link java.io.DataInput}.
1144: * @param dataInput the input stream to read from
1145: */
1146: public void setInput(DataInput dataInput) {
1147: din = dataInput;
1148: in = null;
1149: }
1150:
1151: /**
1152: * Set the input stream to the argument stream (or file).
1153: * @param inputStream the input stream to read from
1154: */
1155: public void setInput(InputStream inputStream) {
1156: in = inputStream;
1157: din = null;
1158: }
1159:
1160: private void setPhysicalHeightDpi(int newValue) {
1161: physicalWidthDpi = newValue;
1162: }
1163:
1164: private void setPhysicalWidthDpi(int newValue) {
1165: physicalHeightDpi = newValue;
1166: }
1167:
1168: private void skip(int num) throws IOException {
1169: while (num > 0) {
1170: long result;
1171: if (in != null) {
1172: result = in.skip(num);
1173: } else {
1174: result = din.skipBytes(num);
1175: }
1176: if (result > 0) {
1177: num -= result;
1178: } else {
1179: if (in != null) {
1180: result = in.read();
1181: } else {
1182: result = din.readByte();
1183: }
1184: if (result == -1) {
1185: throw new IOException("Premature end of input.");
1186: } else {
1187: num--;
1188: }
1189: }
1190: }
1191: }
1192: }
|