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