001: // Exif.java
002: // $Id: Exif.java,v 1.1 2003/07/23 12:08:01 ylafon Exp $
003: // Copyright (c) 2003 Norman Walsh
004: // Please first read the full copyright statement in file COPYRIGHT
005:
006: package org.w3c.tools.jpeg;
007:
008: import java.util.Hashtable;
009:
010: /**
011: * An API for accessing EXIF encoded information in a JPEG file.
012: *
013: * <p>JPEG images are stored in a tagged format reminiscent of TIFF files.
014: * Each component of the image is identified with a tag number and a size.
015: * This allows applications that read JPEG files to skip over information
016: * that they don't understand.</p>
017: *
018: * <p>Additional resources:</p>
019: * <ul>
020: * <li>Official standards, http://www.exif.org/specifications.html</li>
021: * <li>TsuruZoh Tachibanaya's excellent description,
022: * http://www.ba.wakwak.com/~tsuruzoh/Computer/Digicams/exif-e.html
023: * </li>
024: * <li>Matthias Wandel's JHead, http://www.sentex.net/~mwandel/jhead
025: * </ul>
026: *
027: * <p>This class treats a byte array as a JPEG image and parses the tags
028: * searching for EXIF data. EXIF data is also tagged. Based on information
029: * provided by the caller, this class builds a hash of EXIF data and
030: * makes it available to the caller.</p>
031: *
032: * <p>Most simple EXIF values are tagged with both their identity and their
033: * format. For example, ExposureTime (0x829A) is a rational number and
034: * this class can extract that value. However, some fields are of "unknown"
035: * format. If you decode one of these, you can add a special purpose decode
036: * for that field by associating the name of the decoder class with the
037: * field name. For example, my Nikon CoolPix 950 includes a MakerNote (0x927C)
038: * field that's tagged "unknown" format. Using information from TsuruZoh's
039: * page, I've built a decoder for that field and added it as an example.</p>
040: *
041: * <p>In addition to the tagged data, JPEG images have five intrinisic
042: * properties: height, width, the compression algorithm used, the number of
043: * bits used to store each pixel value, and the number of color components.
044: * This class allows the caller to unify those intrinsic components with the
045: * tagged data.</p>
046: *
047: * <p>In an effort to be flexible without requiring users to change the
048: * source, a fair bit of setup is needed to use this class.</p>
049: *
050: * <p>The caller must:</p>
051: *
052: * <ol>
053: * <li>Construct an Exif object, <code>exif = new Exif()</code>.</li>
054: * <li>Associate names with each of the tags of interest,
055: * <code>exif.addTag(<em>nnn</em>, "<em>name</em>")</code>.</li>
056: * <li>Associate names with the intrinsic values:
057: * <ul>
058: * <li><code>exif.addHeight("<em>name</em>")</code>
059: * <li><code>exif.addWidth("<em>name</em>")</code>
060: * <li><code>exif.addCompression("<em>name</em>")</code>
061: * <li><code>exif.addNumberOfColorComponents("<em>name</em>")</code>
062: * <li><code>exif.addBitsPerPixel("<em>name</em>")</code>
063: * </ul>
064: * </li>
065: * <li>Finally, the caller may associate a decoder with specific fields:
066: * <code>exif.addDecoder("<em>name</em>", "<em>java.class.name</em>")</code>.
067: * </li>
068: * </ol>
069: *
070: * <p>Having setup the exif object, it can be passed to JpegHeaders to be
071: * filled in when the JPEG file is parsed.</p>
072: *
073: * <p>The caller must also explicitly set the intrinsic values since they do
074: * not come from the EXIF data.</p>
075: *
076: * <p>After parsing the JPEG, call <code>exif.getTags()</code> to get back the
077: * has of name/value pairs.</p>
078: *
079: * @version $Revision: 1.1 $
080: * @author Norman Walsh
081: * @see ExifData
082: * @see TagDecoder
083: * @see JpegHeaders
084: */
085: public class Exif {
086: private static final int TAG_EXIF_OFFSET = 0x8769;
087: private static final int TAG_INTEROP_OFFSET = 0xa005;
088:
089: private Hashtable tags = new Hashtable();
090: private Hashtable exif = new Hashtable();
091: private Hashtable decoder = new Hashtable();
092: private ExifData data = null;
093: private boolean intelOrder = false;
094:
095: private String tagHeight = null;
096: private String tagWidth = null;
097: private String tagComp = null;
098: private String tagBPP = null;
099: private String tagNumCC = null;
100:
101: public void parseExif(byte[] exifData) {
102: data = new ExifData(exifData);
103: if (!data.isExifData()) {
104: return;
105: }
106:
107: int firstOffset = data.get32u(10);
108: processExifDir(6 + firstOffset, 6);
109: }
110:
111: public void setHeight(int height) {
112: if (tagHeight != null) {
113: exif.put(tagHeight, "" + height);
114: }
115: }
116:
117: public void setWidth(int width) {
118: if (tagWidth != null) {
119: exif.put(tagWidth, "" + width);
120: }
121: }
122:
123: public void setCompression(String comp) {
124: if (tagComp != null) {
125: exif.put(tagComp, comp);
126: }
127: }
128:
129: public void setBPP(int bitsPP) {
130: if (tagBPP != null) {
131: exif.put(tagBPP, "" + bitsPP);
132: }
133: }
134:
135: public void setNumCC(int numCC) {
136: if (tagNumCC != null) {
137: exif.put(tagNumCC, "" + numCC);
138: }
139: }
140:
141: public void addHeight(String name) {
142: tagHeight = name;
143: }
144:
145: public void addWidth(String name) {
146: tagWidth = name;
147: }
148:
149: public void addCompression(String name) {
150: tagComp = name;
151: }
152:
153: public void addBitsPerPixel(String name) {
154: tagBPP = name;
155: }
156:
157: public void addNumberOfColorComponents(String name) {
158: tagNumCC = name;
159: }
160:
161: public void addTag(int tag, String tagName) {
162: tags.put(new Integer(tag), tagName);
163: }
164:
165: public void addDecoder(String name, String className) {
166: decoder.put(name, className);
167: }
168:
169: public Hashtable getTags() {
170: return exif;
171: }
172:
173: protected void processExifDir(int dirStart, int offsetBase) {
174:
175: int numEntries = data.get16u(dirStart);
176: //System.err.println("EXIF: numEntries: " + numEntries);
177:
178: for (int de = 0; de < numEntries; de++) {
179: int dirOffset = dirStart + 2 + (12 * de);
180:
181: int tag = data.get16u(dirOffset);
182: int format = data.get16u(dirOffset + 2);
183: int components = data.get32u(dirOffset + 4);
184:
185: //System.err.println("EXIF: entry: 0x" + Integer.toHexString(tag)
186: // + " " + format
187: // + " " + components);
188:
189: if (format < 0 || format > data.NUM_FORMATS) {
190: System.err
191: .println("Bad number of formats in EXIF dir: "
192: + format);
193: return;
194: }
195:
196: int byteCount = components
197: * ExifData.bytesPerFormat[format];
198: int valueOffset = dirOffset + 8;
199:
200: if (byteCount > 4) {
201: int offsetVal = data.get32u(dirOffset + 8);
202: valueOffset = offsetBase + offsetVal;
203: }
204:
205: //System.err.println("valueOffset: " + valueOffset +
206: // " byteCount: " + byteCount);
207:
208: Integer iTag = new Integer(tag);
209:
210: if (tag == TAG_EXIF_OFFSET || tag == TAG_INTEROP_OFFSET) {
211: int subdirOffset = data.get32u(valueOffset);
212:
213: //System.err.println("offset: " + subdirOffset+
214: // ":"+offsetBase+subdirOffset);
215:
216: processExifDir(offsetBase + subdirOffset, offsetBase);
217: } else {
218: String tagName = "BugBugBug";
219: boolean usedTag = false;
220:
221: if (tags.containsKey(iTag)) {
222: tagName = (String) tags.get(iTag);
223: usedTag = true;
224: } else {
225: tagName = ":unknown0x"
226: + Integer.toHexString(iTag.intValue());
227: }
228:
229: if (decoder.containsKey(tagName)) {
230: String className = (String) decoder.get(tagName);
231: TagDecoder decoder = null;
232:
233: try {
234: decoder = (TagDecoder) Class.forName(className)
235: .newInstance();
236: } catch (ClassNotFoundException cnfe) {
237: System.err.println("Class not found: "
238: + className);
239: } catch (InstantiationException cnfe) {
240: System.err.println("Failed to instantiate "
241: + className);
242: } catch (IllegalAccessException cnfe) {
243: System.err
244: .println("Illegal access instantiating "
245: + className);
246: } catch (ClassCastException cnfe) {
247: System.err.println("Class " + className
248: + " is not a TagDecoder");
249: }
250:
251: if (decoder != null) {
252: decoder.decode(exif, data, format, valueOffset,
253: byteCount);
254: }
255: } else {
256: switch (format) {
257: case ExifData.FMT_UNDEFINED:
258: assignUndefined(tagName, valueOffset, byteCount);
259: break;
260: case ExifData.FMT_STRING:
261: assignString(tagName, valueOffset, byteCount);
262: break;
263: case ExifData.FMT_SBYTE:
264: assignSByte(tagName, valueOffset);
265: break;
266: case ExifData.FMT_BYTE:
267: assignByte(tagName, valueOffset);
268: break;
269: case ExifData.FMT_USHORT:
270: assignUShort(tagName, valueOffset);
271: break;
272: case ExifData.FMT_SSHORT:
273: assignSShort(tagName, valueOffset);
274: break;
275: case ExifData.FMT_ULONG:
276: assignULong(tagName, valueOffset);
277: break;
278: case ExifData.FMT_SLONG:
279: assignSLong(tagName, valueOffset);
280: break;
281: case ExifData.FMT_URATIONAL:
282: case ExifData.FMT_SRATIONAL:
283: assignRational(tagName, valueOffset);
284: break;
285: default:
286: //System.err.println("Unknown format " + format +
287: // " for " + tagName);
288: }
289: }
290: }
291: }
292: }
293:
294: protected void assignUndefined(String tagName, int offset,
295: int length) {
296: String result = data.getUndefined(offset, length);
297: if (!"".equals(result)) {
298: exif.put(tagName, result);
299: }
300: }
301:
302: protected void assignString(String tagName, int offset, int length) {
303: String result = data.getString(offset, length);
304: if (!"".equals(result)) {
305: exif.put(tagName, result);
306: }
307: }
308:
309: protected void assignSByte(String tagName, int offset) {
310: int result = (int) data.convertAnyValue(ExifData.FMT_SBYTE,
311: offset);
312: //System.err.println("\t" + tagName + ": " + result);
313: exif.put(tagName, "" + result);
314: }
315:
316: protected void assignByte(String tagName, int offset) {
317: int result = (int) data.convertAnyValue(ExifData.FMT_BYTE,
318: offset);
319: //System.err.println("\t" + tagName + ": " + result);
320: exif.put(tagName, "" + result);
321: }
322:
323: protected void assignUShort(String tagName, int offset) {
324: int result = (int) data.convertAnyValue(ExifData.FMT_USHORT,
325: offset);
326: //System.err.println("\t" + tagName + ": " + result);
327: exif.put(tagName, "" + result);
328: }
329:
330: protected void assignSShort(String tagName, int offset) {
331: int result = (int) data.convertAnyValue(ExifData.FMT_SSHORT,
332: offset);
333: //System.err.println("\t" + tagName + ": " + result);
334: exif.put(tagName, "" + result);
335: }
336:
337: protected void assignULong(String tagName, int offset) {
338: int result = (int) data.convertAnyValue(ExifData.FMT_ULONG,
339: offset);
340: //System.err.println("\t" + tagName + ": " + result);
341: exif.put(tagName, "" + result);
342: }
343:
344: protected void assignSLong(String tagName, int offset) {
345: int result = (int) data.convertAnyValue(ExifData.FMT_SLONG,
346: offset);
347: //System.err.println("\t" + tagName + ": " + result);
348: exif.put(tagName, "" + result);
349: }
350:
351: protected void assignRational(String tagName, int offset) {
352: int num = data.get32s(offset);
353: int den = data.get32s(offset + 4);
354: String result = "";
355:
356: // This is a bit silly, I really ought to find a real GCD algorithm
357: if (num % 10 == 0 && den % 10 == 0) {
358: num = num / 10;
359: den = den / 10;
360: }
361:
362: if (num % 5 == 0 && den % 5 == 0) {
363: num = num / 5;
364: den = den / 5;
365: }
366:
367: if (num % 3 == 0 && den % 3 == 0) {
368: num = num / 3;
369: den = den / 3;
370: }
371:
372: if (num % 2 == 0 && den % 2 == 0) {
373: num = num / 2;
374: den = den / 2;
375: }
376:
377: if (den == 0) {
378: result = "0";
379: } else if (den == 1) {
380: result = "" + num; // "" + int sure looks ugly...
381: } else {
382: result = "" + num + "/" + den;
383: }
384:
385: //System.err.println("\t" + tagName + ": " + result);
386: exif.put(tagName, "" + result);
387: }
388: }
|