--- /dev/null
+// Exif.java\r
+// $Id: Exif.java,v 1.2 2010/06/15 17:53:10 smhuang Exp $\r
+// Copyright (c) 2003 Norman Walsh\r
+// Please first read the full copyright statement in file COPYRIGHT\r
+\r
+package org.w3c.tools.jpeg;\r
+\r
+import java.util.Hashtable;\r
+\r
+/**\r
+ * An API for accessing EXIF encoded information in a JPEG file.\r
+ *\r
+ * <p>JPEG images are stored in a tagged format reminiscent of TIFF files.\r
+ * Each component of the image is identified with a tag number and a size.\r
+ * This allows applications that read JPEG files to skip over information\r
+ * that they don't understand.</p>\r
+ *\r
+ * <p>Additional resources:</p>\r
+ * <ul>\r
+ * <li>Official standards, http://www.exif.org/specifications.html</li>\r
+ * <li>TsuruZoh Tachibanaya's excellent description,\r
+ * http://www.ba.wakwak.com/~tsuruzoh/Computer/Digicams/exif-e.html\r
+ * </li>\r
+ * <li>Matthias Wandel's JHead, http://www.sentex.net/~mwandel/jhead\r
+ * </ul>\r
+ *\r
+ * <p>This class treats a byte array as a JPEG image and parses the tags\r
+ * searching for EXIF data. EXIF data is also tagged. Based on information\r
+ * provided by the caller, this class builds a hash of EXIF data and\r
+ * makes it available to the caller.</p>\r
+ *\r
+ * <p>Most simple EXIF values are tagged with both their identity and their\r
+ * format. For example, ExposureTime (0x829A) is a rational number and\r
+ * this class can extract that value. However, some fields are of "unknown"\r
+ * format. If you decode one of these, you can add a special purpose decode\r
+ * for that field by associating the name of the decoder class with the\r
+ * field name. For example, my Nikon CoolPix 950 includes a MakerNote (0x927C)\r
+ * field that's tagged "unknown" format. Using information from TsuruZoh's\r
+ * page, I've built a decoder for that field and added it as an example.</p>\r
+ *\r
+ * <p>In addition to the tagged data, JPEG images have five intrinisic\r
+ * properties: height, width, the compression algorithm used, the number of \r
+ * bits used to store each pixel value, and the number of color components.\r
+ * This class allows the caller to unify those intrinsic components with the\r
+ * tagged data.</p>\r
+ *\r
+ * <p>In an effort to be flexible without requiring users to change the\r
+ * source, a fair bit of setup is needed to use this class.</p>\r
+ *\r
+ * <p>The caller must:</p>\r
+ *\r
+ * <ol>\r
+ * <li>Construct an Exif object, <code>exif = new Exif()</code>.</li>\r
+ * <li>Associate names with each of the tags of interest,\r
+ * <code>exif.addTag(<em>nnn</em>, "<em>name</em>")</code>.</li>\r
+ * <li>Associate names with the intrinsic values:\r
+ * <ul>\r
+ * <li><code>exif.addHeight("<em>name</em>")</code>\r
+ * <li><code>exif.addWidth("<em>name</em>")</code>\r
+ * <li><code>exif.addCompression("<em>name</em>")</code>\r
+ * <li><code>exif.addNumberOfColorComponents("<em>name</em>")</code>\r
+ * <li><code>exif.addBitsPerPixel("<em>name</em>")</code>\r
+ * </ul>\r
+ * </li>\r
+ * <li>Finally, the caller may associate a decoder with specific fields:\r
+ * <code>exif.addDecoder("<em>name</em>", "<em>java.class.name</em>")</code>.\r
+ * </li>\r
+ * </ol>\r
+ *\r
+ * <p>Having setup the exif object, it can be passed to JpegHeaders to be \r
+ * filled in when the JPEG file is parsed.</p>\r
+ *\r
+ * <p>The caller must also explicitly set the intrinsic values since they do\r
+ * not come from the EXIF data.</p>\r
+ *\r
+ * <p>After parsing the JPEG, call <code>exif.getTags()</code> to get back the\r
+ * has of name/value pairs.</p>\r
+ *\r
+ * @version $Revision: 1.2 $\r
+ * @author Norman Walsh\r
+ * @see ExifData\r
+ * @see TagDecoder\r
+ * @see JpegHeaders\r
+ */\r
+public class Exif {\r
+ private static final int TAG_EXIF_OFFSET = 0x8769;\r
+ private static final int TAG_INTEROP_OFFSET = 0xa005;\r
+\r
+ private Hashtable tags = new Hashtable();\r
+ private Hashtable exif = new Hashtable();\r
+ private Hashtable decoder = new Hashtable();\r
+ private ExifData data = null;\r
+ private boolean intelOrder = false;\r
+\r
+ private String tagHeight = null;\r
+ private String tagWidth = null;\r
+ private String tagComp = null;\r
+ private String tagBPP = null;\r
+ private String tagNumCC = null;\r
+\r
+ public void parseExif(byte[] exifData) {\r
+ data = new ExifData(exifData);\r
+ if (!data.isExifData()) {\r
+ return;\r
+ }\r
+\r
+ int firstOffset = data.get32u(10);\r
+ processExifDir(6+firstOffset, 6);\r
+ }\r
+\r
+ public void setHeight(int height) {\r
+ if (tagHeight != null) {\r
+ exif.put(tagHeight, ""+height);\r
+ }\r
+ }\r
+\r
+ public void setWidth(int width) {\r
+ if (tagWidth != null) {\r
+ exif.put(tagWidth, ""+width);\r
+ }\r
+ }\r
+\r
+ public void setCompression(String comp) {\r
+ if (tagComp != null) {\r
+ exif.put(tagComp, comp);\r
+ }\r
+ }\r
+\r
+ public void setBPP(int bitsPP) {\r
+ if (tagBPP != null) {\r
+ exif.put(tagBPP, ""+bitsPP);\r
+ }\r
+ }\r
+\r
+ public void setNumCC(int numCC) {\r
+ if (tagNumCC != null) {\r
+ exif.put(tagNumCC, ""+numCC);\r
+ }\r
+ }\r
+\r
+ public void addHeight(String name) {\r
+ tagHeight = name;\r
+ }\r
+\r
+ public void addWidth(String name) {\r
+ tagWidth = name;\r
+ }\r
+\r
+ public void addCompression(String name) {\r
+ tagComp = name;\r
+ }\r
+\r
+ public void addBitsPerPixel(String name) {\r
+ tagBPP = name;\r
+ }\r
+ public void addNumberOfColorComponents(String name) {\r
+ tagNumCC = name;\r
+ }\r
+\r
+ public void addTag(int tag, String tagName) {\r
+ tags.put(new Integer(tag), tagName);\r
+ }\r
+\r
+ public void addDecoder(String name, String className) {\r
+ decoder.put(name, className);\r
+ }\r
+\r
+ public Hashtable getTags() {\r
+ return exif;\r
+ }\r
+\r
+ protected void processExifDir(int dirStart,\r
+ int offsetBase) {\r
+\r
+ int numEntries = data.get16u(dirStart);\r
+ //System.err.println("EXIF: numEntries: " + numEntries);\r
+\r
+ for (int de = 0; de < numEntries; de++) {\r
+ int dirOffset = dirStart + 2 + (12 * de);\r
+\r
+ int tag = data.get16u(dirOffset);\r
+ int format = data.get16u(dirOffset+2);\r
+ int components = data.get32u(dirOffset+4);\r
+\r
+ //System.err.println("EXIF: entry: 0x" + Integer.toHexString(tag)\r
+ // + " " + format\r
+ // + " " + components);\r
+\r
+ if (format < 0 || format > data.NUM_FORMATS) {\r
+ System.err.println("Bad number of formats in EXIF dir: " +\r
+ format);\r
+ return;\r
+ }\r
+\r
+ int byteCount = components * ExifData.bytesPerFormat[format];\r
+ int valueOffset = dirOffset + 8;\r
+\r
+ if (byteCount > 4) {\r
+ int offsetVal = data.get32u(dirOffset+8);\r
+ valueOffset = offsetBase + offsetVal;\r
+ }\r
+\r
+ //System.err.println("valueOffset: " + valueOffset + \r
+ // " byteCount: " + byteCount);\r
+\r
+ Integer iTag = new Integer(tag);\r
+\r
+ if (tag == TAG_EXIF_OFFSET || tag == TAG_INTEROP_OFFSET) {\r
+ int subdirOffset = data.get32u(valueOffset);\r
+\r
+ //System.err.println("offset: " + subdirOffset+\r
+ // ":"+offsetBase+subdirOffset);\r
+\r
+ processExifDir(offsetBase+subdirOffset, offsetBase);\r
+ } else {\r
+ String tagName = "BugBugBug";\r
+ boolean usedTag = false;\r
+\r
+ if (tags.containsKey(iTag)) {\r
+ tagName = (String) tags.get(iTag);\r
+ usedTag = true;\r
+ } else {\r
+ tagName = ":unknown0x" + \r
+ Integer.toHexString(iTag.intValue());\r
+ }\r
+\r
+ if (decoder.containsKey(tagName)) {\r
+ String className = (String) decoder.get(tagName);\r
+ TagDecoder decoder = null;\r
+\r
+ try {\r
+ decoder = (TagDecoder) \r
+ Class.forName(className).newInstance();\r
+ //Added by Jeff Huang\r
+ //TODO: FIXIT\r
+ } catch (ClassNotFoundException cnfe) {\r
+ System.err.println("Class not found: " + className);\r
+ } catch (InstantiationException cnfe) {\r
+ System.err.println("Failed to instantiate " +\r
+ className);\r
+ } catch (IllegalAccessException cnfe) {\r
+ System.err.println("Illegal access instantiating " +\r
+ className);\r
+ } catch (ClassCastException cnfe) {\r
+ System.err.println("Class " + className + \r
+ " is not a TagDecoder");\r
+ }\r
+\r
+ if (decoder != null) {\r
+ decoder.decode(exif, data, format, valueOffset, \r
+ byteCount);\r
+ }\r
+ } else {\r
+ switch (format) {\r
+ case ExifData.FMT_UNDEFINED:\r
+ assignUndefined(tagName, valueOffset, byteCount);\r
+ break;\r
+ case ExifData.FMT_STRING:\r
+ assignString(tagName, valueOffset, byteCount);\r
+ break;\r
+ case ExifData.FMT_SBYTE:\r
+ assignSByte(tagName, valueOffset);\r
+ break;\r
+ case ExifData.FMT_BYTE:\r
+ assignByte(tagName, valueOffset);\r
+ break;\r
+ case ExifData.FMT_USHORT:\r
+ assignUShort(tagName, valueOffset);\r
+ break;\r
+ case ExifData.FMT_SSHORT:\r
+ assignSShort(tagName, valueOffset);\r
+ break;\r
+ case ExifData.FMT_ULONG:\r
+ assignULong(tagName, valueOffset);\r
+ break;\r
+ case ExifData.FMT_SLONG:\r
+ assignSLong(tagName, valueOffset);\r
+ break;\r
+ case ExifData.FMT_URATIONAL:\r
+ case ExifData.FMT_SRATIONAL:\r
+ assignRational(tagName, valueOffset);\r
+ break;\r
+ default:\r
+ //System.err.println("Unknown format " + format + \r
+ // " for " + tagName);\r
+ }\r
+ }\r
+ }\r
+ }\r
+ }\r
+\r
+ protected void assignUndefined(String tagName, int offset, int length) {\r
+ String result = data.getUndefined(offset, length);\r
+ if (!"".equals(result)) {\r
+ exif.put(tagName, result);\r
+ }\r
+ }\r
+\r
+ protected void assignString(String tagName, int offset, int length) {\r
+ String result = data.getString(offset, length);\r
+ if (!"".equals(result)) {\r
+ exif.put(tagName, result);\r
+ }\r
+ }\r
+\r
+ protected void assignSByte(String tagName, int offset) {\r
+ int result = (int) data.convertAnyValue(ExifData.FMT_SBYTE, offset);\r
+ //System.err.println("\t" + tagName + ": " + result);\r
+ exif.put(tagName, ""+result);\r
+ }\r
+\r
+ protected void assignByte(String tagName, int offset) {\r
+ int result = (int) data.convertAnyValue(ExifData.FMT_BYTE, offset);\r
+ //System.err.println("\t" + tagName + ": " + result);\r
+ exif.put(tagName, ""+result);\r
+ }\r
+\r
+ protected void assignUShort(String tagName, int offset) {\r
+ int result = (int) data.convertAnyValue(ExifData.FMT_USHORT, offset);\r
+ //System.err.println("\t" + tagName + ": " + result);\r
+ exif.put(tagName, ""+result);\r
+ }\r
+\r
+ protected void assignSShort(String tagName, int offset) {\r
+ int result = (int) data.convertAnyValue(ExifData.FMT_SSHORT, offset);\r
+ //System.err.println("\t" + tagName + ": " + result);\r
+ exif.put(tagName, ""+result);\r
+ }\r
+\r
+ protected void assignULong(String tagName, int offset) {\r
+ int result = (int) data.convertAnyValue(ExifData.FMT_ULONG, offset);\r
+ //System.err.println("\t" + tagName + ": " + result);\r
+ exif.put(tagName, ""+result);\r
+ }\r
+\r
+ protected void assignSLong(String tagName, int offset) {\r
+ int result = (int) data.convertAnyValue(ExifData.FMT_SLONG, offset);\r
+ //System.err.println("\t" + tagName + ": " + result);\r
+ exif.put(tagName, ""+result);\r
+ }\r
+\r
+ protected void assignRational(String tagName, int offset) {\r
+ int num = data.get32s(offset);\r
+ int den = data.get32s(offset+4);\r
+ String result = "";\r
+\r
+ // This is a bit silly, I really ought to find a real GCD algorithm\r
+ if (num % 10 == 0 && den % 10 == 0) {\r
+ num = num / 10;\r
+ den = den / 10;\r
+ }\r
+\r
+ if (num % 5 == 0 && den % 5 == 0) {\r
+ num = num / 5;\r
+ den = den / 5;\r
+ }\r
+\r
+ if (num % 3 == 0 && den % 3 == 0) {\r
+ num = num / 3;\r
+ den = den / 3;\r
+ }\r
+\r
+ if (num % 2 == 0 && den % 2 == 0) {\r
+ num = num / 2;\r
+ den = den / 2;\r
+ }\r
+\r
+ if (den == 0) {\r
+ result = "0";\r
+ } else if (den == 1) {\r
+ result = "" + num; // "" + int sure looks ugly...\r
+ } else {\r
+ result = "" + num + "/" + den;\r
+ }\r
+\r
+ //System.err.println("\t" + tagName + ": " + result);\r
+ exif.put(tagName, ""+result);\r
+ }\r
+}\r