/* * ImageInfo.java * * Version 1.9 * * A Java class to determine image width, height and color depth for * a number of image file formats. * * Written by Marco Schmidt * * Contributed to the Public Domain. */ package com.flagstone.transform.util.image; import java.io.DataInput; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.Vector; /** * Get file format, image resolution, number of bits per pixel and optionally * number of images, comments and physical resolution from JPEG, GIF, BMP, PCX, * PNG, IFF, RAS, PBM, PGM, PPM and PSD files (or input streams). * <p> * Use the class like this: * * <pre> * ImageInfo ii = new ImageInfo(); * ii.setInput(in); // in can be InputStream or RandomAccessFile * ii.setDetermineImageNumber(true); // default is false * ii.setCollectComments(true); // default is false * if (!ii.check()) { * System.err.println("Not a supported image file format."); * return; * } * System.out.println(ii.getFormatName() + ", " + ii.getMimeType() * + ", " * + ii.getWidth() + " x " + ii.getHeight() * + " pixels, " * + ii.getBitsPerPixel() + " bits per pixel, " * + ii.getNumberOfImages() * + " image(s), " + ii.getNumberOfComments() * + " comment(s)."); * // there are other properties, check out the API documentation * </pre> * * You can also use this class as a command line program. Call it with a number * of image file names and URLs as parameters: * * <pre> * java ImageInfo *.jpg *.png *.gif http://somesite.tld/image.jpg * </pre> * * or call it without parameters and pipe data to it: * * <pre> * java ImageInfo < image.jpg * </pre> * <p> * Known limitations: * <ul> * <li>When the determination of the number of images is turned off, GIF bits * per pixel are only read from the global header. For some GIFs, local palettes * change this to a typically larger value. To be certain to get the correct * color depth, call setDetermineImageNumber(true) before calling check(). The * complete scan over the GIF file will take additional time.</li> * <li>Transparency information is not included in the bits per pixel count. * Actually, it was my decision not to include those bits, so it's a feature! * ;-)</li> * </ul> * <p> * Requirements: * <ul> * <li>Java 1.1 or higher</li> * </ul> * <p> * The latest version can be found at <a * href="http://schmidt.devlib.org/image-info/" * >http://schmidt.devlib.org/image-info/</a>. * <p> * Written by Marco Schmidt. * <p> * This class is contributed to the Public Domain. Use it at your own risk. * <p> * <a name="history">History</a>: * <ul> * <li><strong>2001-08-24</strong> Initial version.</li> * <li><strong>2001-10-13</strong> Added support for the file formats BMP and * PCX.</li> * <li><strong>2001-10-16</strong> Fixed bug in read(int[], int, int) that * returned * <li><strong>2002-01-22</strong> Added support for file formats Amiga IFF and * Sun Raster (RAS).</li> * <li><strong>2002-01-24</strong> Added support for file formats Portable * Bitmap / Graymap / Pixmap (PBM, PGM, PPM) and Adobe Photoshop (PSD). Added * new method getMimeType() to return the MIME type associated with a particular * file format.</li> * <li><strong>2002-03-15</strong> Added support to recognize number of images * in file. Only works with GIF. Use setDetermineImageNumber with * <code>true</code> as argument to identify animated GIFs ( * {@link #getNumberOfImages()} will return a value larger than <code>1</code>). * </li> * <li><strong>2002-04-10</strong> Fixed a bug in the feature 'determine number * of images in animated GIF' introduced with version 1.1. Thanks to Marcelo P. * Lima for sending in the bug report. Released as 1.1.1.</li> * <li><strong>2002-04-18</strong> Added {@link #setCollectComments(boolean)}. * That new method lets the user specify whether textual comments are to be * stored in an internal list when encountered in an input image file / stream. * Added two methods to return the physical width and height of the image in * dpi: {@link #getPhysicalWidthDpi()} and {@link #getPhysicalHeightDpi()}. If * the physical resolution could not be retrieved, these methods return * <code>-1</code>.</li> * <li><strong>2002-04-23</strong> Added support for the new properties physical * resolution and comments for some formats. Released as 1.2.</li> * <li><strong>2002-06-17</strong> Added support for SWF, sent in by Michael * Aird. Changed checkJpeg() so that other APP markers than APP0 will not lead * to a failure anymore. Released as 1.3.</li> * <li><strong>2003-07-28</strong> Bug fix - skip method now takes return values * into consideration. Less bytes than necessary may have been skipped, leading * to flaws in the retrieved information in some cases. Thanks to Bernard * Bernstein for pointing that out. Released as 1.4.</li> * <li><strong>2004-02-29</strong> Added support for recognizing progressive * JPEG and interlaced PNG and GIF. A new method {@link #isProgressive()} * returns whether ImageInfo has found that the storage type is progressive (or * interlaced). Thanks to Joe Germuska for suggesting the feature. Bug fix: BMP * physical resolution is now correctly determined. Released as 1.5.</li> * <li><strong>2004-11-30</strong> Bug fix: recognizing progressive GIFs * (interlaced in GIF terminology) did not work (thanks to Franz Jeitler for * pointing this out). Now it should work, but only if the number of images is * determined. This is because information on interlacing is stored in a local * image header. In theory, different images could be stored interlaced and * non-interlaced in one file. However, I think that's unlikely. Right now, the * last image in the GIF file that is examined by ImageInfo is used for the * "progressive" status.</li> * <li><strong>2005-01-02</strong> Some code clean up (unused methods and * variables commented out, missing javadoc comments, etc.). Thanks to George * Sexton for a long list. Removed usage of Boolean.toString because it's a Java * 1.4+ feature (thanks to Gregor Dupont). Changed delimiter character in * compact output from semicolon to tabulator (for better integration with * cut(1) and other Unix tools). Added some points to the <a * href="http://schmidt.devlib.org/image-info/index.html#knownissues">'Known * issues' section of the website</a>. Released as 1.6.</li> * <li><strong>2005-07-26</strong> Removed code to identify Flash (SWF) files. * Has repeatedly led to problems and support 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 others which didn't work for some people. I give up on SWF. * Please do not contact me about it anymore. Set package of ImageInfo class to * org.devlib.schmidt.imageinfo (a package was repeatedly requested by some * users). Released as 1.7.</li> * <li><strong>2006-02-23</strong> Removed Flash helper methods which weren't * used elsewhere. Updated skip method which tries "read" whenever "skip(Bytes)" * returns a result of 0. The old method didn't work with certain input stream * types on truncated data streams. Thanks to Martin Leidig for reporting this * and sending in code. Released as 1.8.</li> * </li> * <li><strong>2006-11-13</strong> Removed check that made ImageInfo report JPEG * APPx markers smaller than 14 bytes as files in unknown format. Such JPEGs * seem to be generated by Google's Picasa application. First reported with fix * by Karl von Randow. Released as 1.9.</li> * </ul> * * @author Marco Schmidt */ @SuppressWarnings("PMD") public final class ImageInfo { /** * Return value of {@link #getFormat()} for JPEG streams. ImageInfo can * extract physical resolution and comments from JPEGs (only from APP0 * headers). Only one image can be stored in a file. It is determined * whether the JPEG stream is progressive (see {@link #isProgressive()}). */ public static final int FORMAT_JPEG = 0; /** * Return value of {@link #getFormat()} for GIF streams. ImageInfo can * extract comments from GIFs and count the number of images (GIFs with more * than one image are animations). It is determined whether the GIF stream * is interlaced (see {@link #isProgressive()}). */ public static final int FORMAT_GIF = 1; /** * Return value of {@link #getFormat()} for PNG streams. PNG only supports * one image per file. Both physical resolution and comments can be stored * with PNG, but ImageInfo is currently not able to extract those. It is * determined whether the PNG stream is interlaced (see * {@link #isProgressive()}). */ public static final int FORMAT_PNG = 2; /** * Return value of {@link #getFormat()} for BMP streams. BMP only supports * one image per file. BMP does not allow for comments. The physical * resolution can be stored. */ public static final int FORMAT_BMP = 3; /** * Return value of {@link #getFormat()} for PCX streams. PCX does not allow * for comments or more than one image per file. However, the physical * resolution can be stored. */ public static final int FORMAT_PCX = 4; /** * Return value of {@link #getFormat()} for IFF streams. */ public static final int FORMAT_IFF = 5; /** * Return value of {@link #getFormat()} for RAS streams. Sun Raster allows * for one image per file only and is not able to store physical resolution * or comments. */ public static final int FORMAT_RAS = 6; /** Return value of {@link #getFormat()} for PBM streams. */ public static final int FORMAT_PBM = 7; /** Return value of {@link #getFormat()} for PGM streams. */ public static final int FORMAT_PGM = 8; /** Return value of {@link #getFormat()} for PPM streams. */ public static final int FORMAT_PPM = 9; /** Return value of {@link #getFormat()} for PSD streams. */ public static final int FORMAT_PSD = 10; /* * public static final int COLOR_TYPE_UNKNOWN = -1; public static final int * COLOR_TYPE_TRUECOLOR_RGB = 0; public static final int COLOR_TYPE_PALETTED * = 1; public static final int COLOR_TYPE_GRAYSCALE= 2; public static final * int COLOR_TYPE_BLACK_AND_WHITE = 3; */ /** * The names of all supported file formats. The FORMAT_xyz int constants can * be used as index values for this array. */ private static final String[] FORMAT_NAMES = {"JPEG", "GIF", "PNG", "BMP", "PCX", "IFF", "RAS", "PBM", "PGM", "PPM", "PSD" }; /** * The names of the MIME types for all supported file formats. The * FORMAT_xyz int constants can be used as index values for this array. */ private static final String[] MIME_TYPE_STRINGS = {"image/jpeg", "image/gif", "image/png", "image/bmp", "image/pcx", "image/iff", "image/ras", "image/x-portable-bitmap", "image/x-portable-graymap", "image/x-portable-pixmap", "image/psd" }; private ImageEncoding imageFormat; private int width; private int height; private int bitsPerPixel; // private int colorType = COLOR_TYPE_UNKNOWN; private boolean progressive; private int format; private InputStream in; private DataInput din; private boolean collectComments = true; private Vector<String> comments; // private boolean determineNumberOfImages; private int numberOfImages; private int physicalHeightDpi; private int physicalWidthDpi; private void addComment(final String s) { if (comments == null) { comments = new Vector<String>(); } comments.addElement(s); } /** * Call this method after you have provided an input stream or file using * {@link #setInput(InputStream)} or {@link #setInput(DataInput)}. If true * is returned, the file format was known and information on the file's * content can be retrieved using the various getXyz methods. * * @return if information could be retrieved from input */ public boolean check() { format = -1; width = -1; height = -1; bitsPerPixel = -1; numberOfImages = 1; physicalHeightDpi = -1; physicalWidthDpi = -1; comments = null; try { final int b1 = read() & 0xff; final int b2 = read() & 0xff; if ((b1 == 0x47) && (b2 == 0x49)) { return false; //checkGif(); } else if ((b1 == 0x89) && (b2 == 0x50)) { return checkPng(); } else if ((b1 == 0xff) && (b2 == 0xd8)) { return checkJpeg(); } else if ((b1 == 0x42) && (b2 == 0x4d)) { return checkBmp(); /* } else if ((b1 == 0x0a) && (b2 < 0x06)) { return false; //checkPcx(); } else if ((b1 == 0x46) && (b2 == 0x4f)) { return false; //checkIff(); } else if ((b1 == 0x59) && (b2 == 0xa6)) { return false; //checkRas(); } else if ((b1 == 0x50) && (b2 >= 0x31) && (b2 <= 0x36)) { return false; //checkPnm(b2 - '0'); } else if ((b1 == 0x38) && (b2 == 0x42)) { return false; //checkPsd(); */ } else { return false; } } catch (final IOException ioe) { return false; } } private boolean checkBmp() throws IOException { final byte[] a = new byte[44]; if (read(a) != a.length) { return false; } width = getIntLittleEndian(a, 16); height = getIntLittleEndian(a, 20); if ((width < 1) || (height < 1)) { return false; } bitsPerPixel = getShortLittleEndian(a, 26); if ((bitsPerPixel != 1) && (bitsPerPixel != 4) && (bitsPerPixel != 8) && (bitsPerPixel != 16) && (bitsPerPixel != 24) && (bitsPerPixel != 32)) { return false; } final int x = (int) (getIntLittleEndian(a, 36) * 0.0254); if (x > 0) { setPhysicalWidthDpi(x); } final int y = (int) (getIntLittleEndian(a, 40) * 0.0254); if (y > 0) { setPhysicalHeightDpi(y); } format = FORMAT_BMP; imageFormat = ImageEncoding.BMP; return true; } // private boolean checkGif() throws IOException { // final byte[] gifmagic87a = { 0x46, 0x38, 0x37, 0x61 }; // final byte[] gifmagic89a = { 0x46, 0x38, 0x39, 0x61 }; // final byte[] a = new byte[11]; // 4 from the GIF signature + 7 from // // the global // // header // if (read(a) != 11) { // return false; // } // if ((!equals(a, 0, gifmagic89a, 0, 4)) // && (!equals(a, 0, gifmagic87a, 0, 4))) { // return false; // } // format = FORMAT_GIF; // imageFormat = ImageEncoding.GIF; // width = getShortLittleEndian(a, 4); // height = getShortLittleEndian(a, 6); // int flags = a[8] & 0xff; // bitsPerPixel = ((flags >> 4) & 0x07) + 1; // // progressive = (flags & 0x02) != 0; // if (!determineNumberOfImages) { // return true; // } // // skip global color palette // if ((flags & 0x80) != 0) { // final int tableSize = (1 << ((flags & 7) + 1)) * 3; // skip(tableSize); // } // numberOfImages = 0; // int blockType; // int n; // do { // blockType = read(); // switch (blockType) { // case (0x2c): // image separator // if (read(a, 0, 9) != 9) { // return false; // } // flags = a[8] & 0xff; // progressive = (flags & 0x40) != 0; // /* // * int locWidth = getShortLittleEndian(a, 4); int locHeight = // * getShortLittleEndian(a, 6); System.out.println("LOCAL: " + // * locWidth + " x " + locHeight); // */ // final int localBitsPerPixel = (flags & 0x07) + 1; // if (localBitsPerPixel > bitsPerPixel) { // bitsPerPixel = localBitsPerPixel; // } // if ((flags & 0x80) != 0) { // skip((1 << localBitsPerPixel) * 3); // } // skip(1); // initial code length // do { // n = read(); // if (n > 0) { // skip(n); // } else if (n == -1) { // return false; // } // } while (n > 0); // numberOfImages++; // break; // case (0x21): // extension // final int extensionType = read(); // if (collectComments && (extensionType == 0xfe)) { // final StringBuilder sb = new StringBuilder(); // do { // n = read(); // if (n == -1) { // return false; // } // if (n > 0) { // for (int i = 0; i < n; i++) { // final int ch = read(); // if (ch == -1) { // return false; // } // sb.append((char) ch); // } // } // } while (n > 0); // } else { // do { // n = read(); // if (n > 0) { // skip(n); // } else if (n == -1) { // return false; // } // } while (n > 0); // } // break; // case (0x3b): // end of file // break; // default: // return false; // } // } while (blockType != 0x3b); // return true; // } // private boolean checkIff() throws IOException { // final byte[] a = new byte[10]; // // read remaining 2 bytes of file id, 4 bytes file size // // and 4 bytes IFF subformat // if (read(a, 0, 10) != 10) { // return false; // } // final byte[] iffrm = { 0x52, 0x4d }; // if (!equals(a, 0, iffrm, 0, 2)) { // return false; // } // final int type = getIntBigEndian(a, 6); // if ((type != 0x494c424d) && // type must be ILBM... // (type != 0x50424d20)) { // ...or PBM // return false; // } // // loop chunks to find BMHD chunk // do { // if (read(a, 0, 8) != 8) { // return false; // } // final int chunkId = getIntBigEndian(a, 0); // int size = getIntBigEndian(a, 4); // if ((size & 1) == 1) { // size++; // } // if (chunkId == 0x424d4844) { // BMHD chunk // if (read(a, 0, 9) != 9) { // return false; // } // format = FORMAT_IFF; // imageFormat = ImageEncoding.IFF; // width = getShortBigEndian(a, 0); // height = getShortBigEndian(a, 2); // bitsPerPixel = a[8] & 0xff; // return ((width > 0) && (height > 0) // && (bitsPerPixel > 0) && (bitsPerPixel < 33)); // } else { // skip(size); // } // } while (true); // } private boolean checkJpeg() throws IOException { final byte[] data = new byte[12]; while (true) { if (read(data, 0, 4) != 4) { return false; } final int marker = getShortBigEndian(data, 0); int size = getShortBigEndian(data, 2); if ((marker & 0xff00) != 0xff00) { return false; // not a valid marker } if (marker == 0xffe0) { // APPx if (size < 14) { // not an APPx header as we know it, skip skip(size - 2); continue; } if (read(data, 0, 12) != 12) { return false; } final byte[] appoId = {0x4a, 0x46, 0x49, 0x46, 0x00 }; if (equals(appoId, 0, data, 0, 5)) { // System.out.println("data 7=" + data[7]); if (data[7] == 1) { setPhysicalWidthDpi(getShortBigEndian(data, 8)); setPhysicalHeightDpi(getShortBigEndian(data, 10)); } else if (data[7] == 2) { final int x = getShortBigEndian(data, 8); final int y = getShortBigEndian(data, 10); setPhysicalWidthDpi((int) (x * 2.54f)); setPhysicalHeightDpi((int) (y * 2.54f)); } } skip(size - 14); } else if (collectComments && (size > 2) && (marker == 0xfffe)) { // comment size -= 2; final byte[] chars = new byte[size]; if (read(chars, 0, size) != size) { return false; } String comment = new String(chars, "iso-8859-1"); comment = comment.trim(); addComment(comment); } else if ((marker >= 0xffc0) && (marker <= 0xffcf) && (marker != 0xffc4) && (marker != 0xffc8)) { if (read(data, 0, 6) != 6) { return false; } format = FORMAT_JPEG; imageFormat = ImageEncoding.JPEG; bitsPerPixel = (data[0] & 0xff) * (data[5] & 0xff); progressive = (marker == 0xffc2) || (marker == 0xffc6) || (marker == 0xffca) || (marker == 0xffce); width = getShortBigEndian(data, 3); height = getShortBigEndian(data, 1); return true; } else { skip(size - 2); } } } // private boolean checkPcx() throws IOException { // final byte[] a = new byte[64]; // if (read(a) != a.length) { // return false; // } // if (a[0] != 1) { // encoding, 1=RLE is only valid value // return false; // } // // width / height // final int x1 = getShortLittleEndian(a, 2); // final int y1 = getShortLittleEndian(a, 4); // final int x2 = getShortLittleEndian(a, 6); // final int y2 = getShortLittleEndian(a, 8); // if ((x1 < 0) || (x2 < x1) || (y1 < 0) || (y2 < y1)) { // return false; // } // width = x2 - x1 + 1; // height = y2 - y1 + 1; // // color depth // final int bits = a[1]; // final int planes = a[63]; // if ((planes == 1) // && ((bits == 1) || (bits == 2) // || (bits == 4) || (bits == 8))) { // // paletted // bitsPerPixel = bits; // } else if ((planes == 3) && (bits == 8)) { // // RGB truecolor // bitsPerPixel = 24; // } else { // return false; // } // setPhysicalWidthDpi(getShortLittleEndian(a, 10)); // setPhysicalHeightDpi(getShortLittleEndian(a, 10)); // format = FORMAT_PCX; // imageFormat = ImageEncoding.PCX; // return true; // } private boolean checkPng() throws IOException { final byte[] pngmagic = {0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a }; final byte[] a = new byte[27]; if (read(a) != 27) { return false; } if (!equals(a, 0, pngmagic, 0, 6)) { return false; } format = FORMAT_PNG; imageFormat = ImageEncoding.PNG; width = getIntBigEndian(a, 14); height = getIntBigEndian(a, 18); bitsPerPixel = a[22] & 0xff; final int colorType = a[23] & 0xff; if ((colorType == 2) || (colorType == 6)) { bitsPerPixel *= 3; } progressive = (a[26] & 0xff) != 0; return true; } // private boolean checkPnm(final int id) throws IOException { // if ((id < 1) || (id > 6)) { // return false; // } // final int[] pnmformats = { FORMAT_PBM, FORMAT_PGM, FORMAT_PPM }; // format = pnmformats[(id - 1) % 3]; // boolean hasPixelResolution = false; // String s; // while (true) { // s = readLine(); // if (s != null) { // s = s.trim(); // } // if ((s == null) || (s.length() < 1)) { // continue; // } // if (s.charAt(0) == '#') { // comment // if (collectComments && (s.length() > 1)) { // addComment(s.substring(1)); // } // continue; // } // if (!hasPixelResolution) { // split "343 966" into width=343, // // height=966 // int spaceIndex = s.indexOf(' '); // if (spaceIndex == -1) { // return false; // } // final String widthString = s.substring(0, spaceIndex); // spaceIndex = s.lastIndexOf(' '); // if (spaceIndex == -1) { // return false; // } // final String heightString = s.substring(spaceIndex + 1); // try { // width = Integer.parseInt(widthString); // height = Integer.parseInt(heightString); // } catch (final NumberFormatException nfe) { // return false; // } // if ((width < 1) || (height < 1)) { // return false; // } // if (format == FORMAT_PBM) { // bitsPerPixel = 1; // return true; // } // hasPixelResolution = true; // } else { // int maxSample; // try { // maxSample = Integer.parseInt(s); // } catch (final NumberFormatException nfe) { // return false; // } // if (maxSample < 0) { // return false; // } // for (int i = 0; i < 25; i++) { // if (maxSample < (1 << (i + 1))) { // bitsPerPixel = i + 1; // if (format == FORMAT_PPM) { // bitsPerPixel *= 3; // } // return true; // } // } // return false; // } // } // } // private boolean checkPsd() throws IOException { // final byte[] a = new byte[24]; // if (read(a) != a.length) { // return false; // } // final byte[] psdmagic = { 0x50, 0x53 }; // if (!equals(a, 0, psdmagic, 0, 2)) { // return false; // } // format = FORMAT_PSD; // imageFormat = ImageEncoding.PSD; // width = getIntBigEndian(a, 16); // height = getIntBigEndian(a, 12); // final int channels = getShortBigEndian(a, 10); // final int depth = getShortBigEndian(a, 20); // bitsPerPixel = channels * depth; // return ((width > 0) && (height > 0) // && (bitsPerPixel > 0) && (bitsPerPixel <= 64)); // } // private boolean checkRas() throws IOException { // final byte[] a = new byte[14]; // if (read(a) != a.length) { // return false; // } // final byte[] rasmagic = { 0x6a, (byte) 0x95 }; // if (!equals(a, 0, rasmagic, 0, 2)) { // return false; // } // format = FORMAT_RAS; // imageFormat = ImageEncoding.RAS; // width = getIntBigEndian(a, 2); // height = getIntBigEndian(a, 6); // bitsPerPixel = getIntBigEndian(a, 10); // return ((width > 0) && (height > 0) // && (bitsPerPixel > 0) && (bitsPerPixel <= 24)); // } /** * Run over String list, return false iff at least one of the arguments * equals <code>-c</code>. * * @param args * string list to check */ private static boolean determineVerbosity(final String[] args) { if ((args != null) && (args.length > 0)) { for (final String arg : args) { if ("-c".equals(arg)) { return false; } } } return true; } private static boolean equals(final byte[] a1, final int offs1, final byte[] a2, final int offs2, final int num) { int index1 = offs1; int index2 = offs2; int count = num; while (count-- > 0) { if (a1[index1++] != a2[index2++]) { return false; } } return true; } /** * If {@link #check()} was successful, returns the image's number of bits * per pixel. Does not include transparency information like the alpha * channel. * * @return number of bits per image pixel */ public int getBitsPerPixel() { return bitsPerPixel; } /** * Returns the index'th comment retrieved from the file. * * @param index * int index of comment to return to the number of comments * retrieved * @see #getNumberOfComments */ public String getComment(final int index) { if ((comments == null) || (index < 0) || (index >= comments.size())) { throw new IllegalArgumentException("Not a valid comment index: " + index); } return comments.elementAt(index); } /** * If {@link #check()} was successful, returns the image format as one of * the FORMAT_xyz constants from this class. Use {@link #getFormatName()} to * get a textual description of the file format. * * @return file format as a FORMAT_xyz constant */ public int getFormat() { return format; } public ImageEncoding getImageFormat() { return imageFormat; } /** * If {@link #check()} was successful, returns the image format's name. Use * {@link #getFormat()} to get a unique number. * * @return file format name */ public String getFormatName() { if ((format >= 0) && (format < FORMAT_NAMES.length)) { return FORMAT_NAMES[format]; } else { return "?"; } } /** * If {@link #check()} was successful, returns one the image's vertical * resolution in pixels. * * @return image height in pixels */ public int getHeight() { return height; } private static int getIntBigEndian(final byte[] a, final int offs) { return (a[offs] & 0xff) << 24 | (a[offs + 1] & 0xff) << 16 | (a[offs + 2] & 0xff) << 8 | a[offs + 3] & 0xff; } private static int getIntLittleEndian(final byte[] a, final int offs) { return (a[offs + 3] & 0xff) << 24 | (a[offs + 2] & 0xff) << 16 | (a[offs + 1] & 0xff) << 8 | a[offs] & 0xff; } /** * If {@link #check()} was successful, returns a String with the MIME type * of the format. * * @return MIME type, e.g. <code>image/jpeg</code> */ public String getMimeType() { if ((format >= 0) && (format < MIME_TYPE_STRINGS.length)) { if ((format == FORMAT_JPEG) && progressive) { return "image/pjpeg"; } return MIME_TYPE_STRINGS[format]; } else { return null; } } /** * If {@link #check()} was successful and * {@link #setCollectComments(boolean)} was called with <code>true</code> as * argument, returns the number of comments retrieved from the input image * stream / file. Any number >= 0 and smaller than this number of * comments is then a valid argument for the {@link #getComment(int)} * method. * * @return number of comments retrieved from input image */ public int getNumberOfComments() { if (comments == null) { return 0; } else { return comments.size(); } } /** * Returns the number of images in the examined file. Assumes that * <code>setDetermineImageNumber(true);</code> was called before a * successful call to {@link #check()}. This value can currently be only * different from <code>1</code> for GIF images. * * @return number of images in file */ public int getNumberOfImages() { return numberOfImages; } /** * Returns the physical height of this image in dots per inch (dpi). Assumes * that {@link #check()} was successful. Returns <code>-1</code> on failure. * * @return physical height (in dpi) * @see #getPhysicalWidthDpi() * @see #getPhysicalHeightInch() */ public int getPhysicalHeightDpi() { return physicalHeightDpi; } /** * If {@link #check()} was successful, returns the physical width of this * image in dpi (dots per inch) or -1 if no value could be found. * * @return physical height (in dpi) * @see #getPhysicalHeightDpi() * @see #getPhysicalWidthDpi() * @see #getPhysicalWidthInch() */ public float getPhysicalHeightInch() { final int h = getHeight(); final int ph = getPhysicalHeightDpi(); if ((h > 0) && (ph > 0)) { return ((float) h) / ((float) ph); } else { return -1.0f; } } /** * If {@link #check()} was successful, returns the physical width of this * image in dpi (dots per inch) or -1 if no value could be found. * * @return physical width (in dpi) * @see #getPhysicalHeightDpi() * @see #getPhysicalWidthInch() * @see #getPhysicalHeightInch() */ public int getPhysicalWidthDpi() { return physicalWidthDpi; } /** * Returns the physical width of an image in inches, or <code>-1.0f</code> * if width information is not available. Assumes that {@link #check} has * been called successfully. * * @return physical width in inches or <code>-1.0f</code> on failure * @see #getPhysicalWidthDpi * @see #getPhysicalHeightInch */ public float getPhysicalWidthInch() { final int w = getWidth(); final int pw = getPhysicalWidthDpi(); if ((w > 0) && (pw > 0)) { return ((float) w) / ((float) pw); } else { return -1.0f; } } private static int getShortBigEndian(final byte[] a, final int offs) { return (a[offs] & 0xff) << 8 | (a[offs + 1] & 0xff); } private static int getShortLittleEndian(final byte[] a, final int offs) { return (a[offs] & 0xff) | (a[offs + 1] & 0xff) << 8; } /** * If {@link #check()} was successful, returns one the image's horizontal * resolution in pixels. * * @return image width in pixels */ public int getWidth() { return width; } /** * Returns whether the image is stored in a progressive (also called: * interlaced) way. * * @return true for progressive/interlaced, false otherwise */ public boolean isProgressive() { return progressive; } /** * To use this class as a command line application, give it either some file * names as parameters (information on them will be printed to standard * output, one line per file) or call it with no parameters. It will then * check data given to it via standard input. * * @param args * the program arguments which must be file names */ public static void main(final String[] args) { final ImageInfo imageInfo = new ImageInfo(); // imageInfo.setDetermineImageNumber(true); final boolean verbose = determineVerbosity(args); if (args.length == 0) { run(null, System.in, imageInfo, verbose); } else { int index = 0; while (index < args.length) { InputStream in = null; try { final String name = args[index++]; //System.out.print(name + ";"); if (name.startsWith("http://")) { in = new URL(name).openConnection().getInputStream(); } else { in = new FileInputStream(name); } run(name, in, imageInfo, verbose); in.close(); } catch (final IOException e) { //System.out.println(e); try { if (in != null) { in.close(); } } catch (final IOException ee) { ee.printStackTrace(); } } } } } // private static void print(final String sourceName, final ImageInfo ii, // final boolean verbose) { // if (verbose) { // printVerbose(sourceName, ii); // } else { // printCompact(sourceName, ii); // } // } // private static void printCompact(final String sourceName, // final ImageInfo imageInfo) { // final String SEP = "\t"; // System.out.println(sourceName + SEP + imageInfo.getFormatName() + SEP // + imageInfo.getMimeType() + SEP + imageInfo.getWidth() + SEP // + imageInfo.getHeight() + SEP + imageInfo.getBitsPerPixel() // + SEP + imageInfo.getNumberOfImages() + SEP // + imageInfo.getPhysicalWidthDpi() + SEP // + imageInfo.getPhysicalHeightDpi() + SEP // + imageInfo.getPhysicalWidthInch() + SEP // + imageInfo.getPhysicalHeightInch() + SEP // + imageInfo.isProgressive()); // } // private static void printLine(final int indentLevels, final String text, // final float value, final float minValidValue) { // if (value < minValidValue) { // return; // } // printLine(indentLevels, text, Float.toString(value)); // } // private static void printLine(final int indentLevels, final String text, // final int value, final int minValidValue) { // if (value >= minValidValue) { // printLine(indentLevels, text, Integer.toString(value)); // } // } // private static void printLine(int indentLevels, final String text, // final String value) { // if ((value == null) || (value.length() == 0)) { // return; // } // while (indentLevels-- > 0) { // System.out.print("\t"); // } // if ((text != null) && (text.length() > 0)) { // System.out.print(text); // System.out.print(" "); // } // System.out.println(value); // } // private static void printVerbose(final String // sourceName, final ImageInfo ii) { // printLine(0, null, sourceName); // printLine(1, "File format: ", ii.getFormatName()); // printLine(1, "MIME type: ", ii.getMimeType()); // printLine(1, "Width (pixels): ", ii.getWidth(), 1); // printLine(1, "Height (pixels): ", ii.getHeight(), 1); // printLine(1, "Bits per pixel: ", ii.getBitsPerPixel(), 1); // printLine(1, "Progressive: ", ii.isProgressive() ? "yes" : "no"); // printLine(1, "Number of images: ", ii.getNumberOfImages(), 1); // printLine(1, "Physical width (dpi): ", ii.getPhysicalWidthDpi(), 1); // printLine(1, "Physical height (dpi): ", ii.getPhysicalHeightDpi(), 1); // printLine(1, "Physical width (inches): ", ii.getPhysicalWidthInch(), // 1.0f); // printLine(1, "Physical height (inches): ", ii.getPhysicalHeightInch(), // 1.0f); // final int numComments = ii.getNumberOfComments(); // printLine(1, "Number of textual comments: ", numComments, 1); // if (numComments > 0) { // for (int i = 0; i < numComments; i++) { // printLine(2, null, ii.getComment(i)); // } // } // } private int read() throws IOException { if (in != null) { return in.read(); } else { return din.readByte(); } } private int read(final byte[] a) throws IOException { if (in != null) { return in.read(a); } else { din.readFully(a); return a.length; } } private int read(final byte[] a, final int offset, final int num) throws IOException { if (in != null) { return in.read(a, offset, num); } else { din.readFully(a, offset, num); return num; } } // private String readLine() throws IOException { // return readLine(new StringBuilder()); // } // private String readLine(final StringBuilder sb) throws IOException { // boolean finished; // do { // final int value = read(); // finished = ((value == -1) || (value == 10)); // if (!finished) { // sb.append((char) value); // } // } while (!finished); // return sb.toString(); // } private static void run(final String sourceName, final InputStream in, final ImageInfo imageInfo, final boolean verbose) { imageInfo.setInput(in); // imageInfo.setDetermineImageNumber(true); imageInfo.setCollectComments(verbose); // if (imageInfo.check()) { // print(sourceName, imageInfo, verbose); // } } /** * Specify whether textual comments are supposed to be extracted from input. * Default is <code>false</code>. If enabled, comments will be added to an * internal list. * * @param newValue * if <code>true</code>, this class will read comments * @see #getNumberOfComments * @see #getComment */ public void setCollectComments(final boolean newValue) { collectComments = newValue; } /** * Specify whether the number of images in a file is to be determined - * default is <code>false</code>. This is a special option because some file * formats require running over the entire file to find out the number of * images, a rather time-consuming task. Not all file formats support more * than one image. If this method is called with <code>true</code> as * argument, the actual number of images can be queried via * {@link #getNumberOfImages()} after a successful call to {@link #check()}. * * @param newValue * will the number of images be determined? * @see #getNumberOfImages */ // public void setDetermineImageNumber(final boolean newValue) { // determineNumberOfImages = newValue; // } /** * Set the input stream to the argument stream (or file). Note that * {@link java.io.RandomAccessFile} implements {@link java.io.DataInput}. * * @param dataInput * the input stream to read from */ public void setInput(final DataInput dataInput) { din = dataInput; in = null; } /** * Set the input stream to the argument stream (or file). * * @param inputStream * the input stream to read from */ public void setInput(final InputStream inputStream) { in = inputStream; din = null; } private void setPhysicalHeightDpi(final int newValue) { physicalWidthDpi = newValue; } private void setPhysicalWidthDpi(final int newValue) { physicalHeightDpi = newValue; } private void skip(final int num) throws IOException { int count = num; while (count > 0) { long result; if (in != null) { result = in.skip(num); } else { result = din.skipBytes(num); } if (result > 0) { count -= result; } else { if (in != null) { result = in.read(); } else { result = din.readByte(); } if (result == -1) { throw new IOException("Premature end of input."); } else { count--; } } } } }