001: // JpegHeaders.java
002: // $Id: JpegHeaders.java,v 1.16 2003/07/23 12:08:02 ylafon Exp $
003: // (c) COPYRIGHT MIT, INRIA and Keio, 1999.
004: // Please first read the full copyright statement in file COPYRIGHT.html
005: //
006: // Modified by Norman Walsh (ndw@nwalsh.com) to support extraction of EXIF
007: // camera data.
008:
009: package org.w3c.tools.jpeg;
010:
011: import java.io.BufferedInputStream;
012: import java.io.File;
013: import java.io.FileInputStream;
014: import java.io.FileNotFoundException;
015: import java.io.IOException;
016: import java.io.InputStream;
017: import java.io.PrintStream;
018: import java.io.StringReader;
019: import java.io.UnsupportedEncodingException;
020:
021: import java.util.Vector;
022: import java.util.Hashtable;
023:
024: /**
025: * @version $Revision: 1.16 $
026: * @author Benoît Mahé (bmahe@w3.org)
027: * jpeg reading code adapted from rdjpgcom from The Independent JPEG Group
028: */
029: public class JpegHeaders implements Jpeg {
030:
031: protected File jpegfile = null;
032: protected InputStream in = null;
033:
034: protected Vector vcom = null;
035: protected Vector vacom[] = new Vector[16];
036:
037: protected String comments[] = null;
038: protected byte appcomments[][] = null;
039:
040: // Additional EXIF data
041: protected Exif exif = null;
042: protected int compression = -1;
043: protected int bitsPerPixel = -1;
044: protected int height = -1;
045: protected int width = -1;
046: protected int numComponents = -1;
047:
048: /**
049: * Get the comments extracted from the jpeg stream
050: * @return an array of Strings
051: */
052: public String[] getComments() {
053: if (comments == null) {
054: comments = new String[vcom.size()];
055: vcom.copyInto(comments);
056: }
057: return comments;
058: }
059:
060: /**
061: * Get the application specific values extracted from the jpeg stream
062: * @return an array of Strings
063: */
064: public String[] getStringAPPComments(int marker) {
065: // out of bound, no comment
066: if ((marker < M_APP0) || (marker > M_APP15)) {
067: return null;
068: }
069: int idx = marker - M_APP0;
070: int asize = vacom[idx].size();
071: if (appcomments == null) {
072: appcomments = new byte[asize][];
073: vacom[idx].copyInto(appcomments);
074: }
075: String strappcomments[] = new String[asize];
076: for (int i = 0; i < asize; i++) {
077: try {
078: strappcomments[i] = new String(appcomments[i],
079: "ISO-8859-1");
080: } catch (UnsupportedEncodingException ex) {
081: }
082: ;
083: }
084: return strappcomments;
085: }
086:
087: /**
088: * An old default, it gets only the M_APP12
089: */
090: public String[] getStringAppComments() {
091: return getStringAPPComments(M_APP12);
092: }
093:
094: /**
095: * A get XMP in APP1
096: */
097: public String getXMP() throws IOException, JpegException
098:
099: {
100: String magicstr = "W5M0MpCehiHzreSzNTczkc9d";
101: char magic[] = magicstr.toCharArray();
102: String magicendstr = "<?xpacket";
103: char magicend[] = magicendstr.toCharArray();
104: char c;
105: int length;
106: int h = 0, i = 0, j = 0, k;
107: char buf[] = new char[256];
108:
109: /* get the APP1 marker */
110: String app1markers[] = getStringAPPComments(M_APP1);
111: String app1marker = new String();
112: boolean found = false;
113: for (h = 0; h < app1markers.length; h++) {
114: if (found == false
115: && app1markers[h].indexOf(magicstr) != -1) {
116: found = true;
117: // System.out.println("magic found");
118: app1marker = app1marker.concat(app1markers[h]);
119: } else if (found == true) {
120: app1marker = app1marker.concat(app1markers[h]);
121: }
122: }
123:
124: StringReader app1reader = new StringReader(app1marker);
125: StringBuffer sbuf = new StringBuffer();
126:
127: /* Get the marker parameter length count */
128: length = read2bytes(app1reader);
129: /* Length includes itself, so must be at least 2 */
130: if (length < 2)
131: throw new JpegException("Erroneous JPEG marker length");
132: length -= 2;
133:
134: /* initialize a new reader to start from the beginning */
135: app1reader = new StringReader(app1marker);
136: /* Read until end of block or until magic string found */
137: while (length > 0 && j < magic.length) {
138: buf[i] = (char) (app1reader.read());
139: if (buf[i] == -1)
140: throw new JpegException("Premature EOF in JPEG file");
141: if (buf[i] == magic[j]) {
142: j++;
143: } else {
144: j = 0;
145: }
146: i = (i + 1) % 100;
147: length--;
148: }
149:
150: if (j == magic.length) {
151: /* Copy from buffer everything since beginning of the PI */
152: k = i;
153: do {
154: i = (i + 100 - 1) % 100;
155: } while (buf[i] != '<' || buf[(i + 1) % 100] != '?'
156: || buf[(i + 2) % 100] != 'x'
157: || buf[(i + 3) % 100] != 'p');
158:
159: for (; i != k; i = (i + 1) % 100)
160: sbuf.append(buf[i]);
161:
162: /* Continue copying until end of XMP packet */
163: j = 0;
164: while (length > 0 && j < magicend.length) {
165: c = (char) (app1reader.read());
166: if (c == -1)
167: throw new JpegException(
168: "Premature EOF in JPEG file");
169: if (c == magicend[j])
170: j++;
171: else
172: j = 0;
173: sbuf.append(c);
174: length--;
175: }
176: /* Copy until end of PI */
177: while (length > 0) {
178: c = (char) (app1reader.read());
179: if (c == -1)
180: throw new JpegException(
181: "Premature EOF in JPEG file");
182: sbuf.append(c);
183: length--;
184: if (c == '>')
185: break;
186: }
187: }
188: /* Skip rest, if any */
189: while (length > 0) {
190: app1reader.read();
191: length--;
192: }
193: return (sbuf.toString());
194: }
195:
196: public byte[][] getByteArrayAPPComment() {
197: return null;
198: }
199:
200: /**
201: * The old way of extracting comments in M_APP12 markers
202: * @deprecated use getStringAppComments instead
203: */
204: public String[] getAppComments() {
205: return getStringAppComments();
206: }
207:
208: protected int scanHeaders() throws IOException, JpegException {
209: int marker;
210: vcom = new Vector(1);
211: vacom = new Vector[16];
212: for (int i = 0; i < 16; i++) {
213: vacom[i] = new Vector(1);
214: }
215:
216: if (firstMarker() != M_SOI)
217: throw new JpegException("Expected SOI marker first");
218:
219: while (true) {
220: marker = nextMarker();
221: switch (marker) {
222: case M_SOF0: /* Baseline */
223: case M_SOF1: /* Extended sequential, Huffman */
224: case M_SOF2: /* Progressive, Huffman */
225: case M_SOF3: /* Lossless, Huffman */
226: case M_SOF5: /* Differential sequential, Huffman */
227: case M_SOF6: /* Differential progressive, Huffman */
228: case M_SOF7: /* Differential lossless, Huffman */
229: case M_SOF9: /* Extended sequential, arithmetic */
230: case M_SOF10: /* Progressive, arithmetic */
231: case M_SOF11: /* Lossless, arithmetic */
232: case M_SOF13: /* Differential sequential, arithmetic */
233: case M_SOF14: /* Differential progressive, arithmetic */
234: case M_SOF15: /* Differential lossless, arithmetic */
235: // Remember the kind of compression we saw
236: compression = marker;
237: // Get the intrinsic properties fo the image
238: readImageInfo();
239: break;
240: case M_SOS: /* stop before hitting compressed data */
241: skipVariable();
242: // Update the EXIF
243: updateExif();
244: return marker;
245: case M_EOI: /* in case it's a tables-only JPEG stream */
246: // Update the EXIF
247: updateExif();
248: return marker;
249: case M_COM:
250: // Always ISO-8859-1? Is this a bug or is there something about
251: // the comment field that I don't understand...
252: vcom.addElement(new String(processComment(),
253: "ISO-8859-1"));
254: break;
255: case M_APP0:
256: case M_APP1:
257: case M_APP2:
258: case M_APP3:
259: case M_APP4:
260: case M_APP5:
261: case M_APP6:
262: case M_APP7:
263: case M_APP8:
264: case M_APP9:
265: case M_APP10:
266: case M_APP11:
267: case M_APP12:
268: case M_APP13:
269: case M_APP14:
270: case M_APP15:
271: // Some digital camera makers put useful textual
272: // information into APP1 andAPP12 markers, so we print
273: // those out too when in -verbose mode.
274:
275: byte data[] = processComment();
276: vacom[marker - M_APP0].addElement(data);
277:
278: // This is where the EXIF data is stored, grab it and parse it!
279: if (marker == M_APP1) { // APP1 == EXIF
280: if (exif != null) {
281: exif.parseExif(data);
282: }
283: }
284:
285: break;
286: default: // Anything else just gets skipped
287: skipVariable(); // we assume it has a parameter count...
288: break;
289: }
290: }
291: }
292:
293: /** Update the EXIF to include the intrinsic values */
294: protected void updateExif() {
295: if (exif == null) {
296: return;
297: }
298:
299: if (compression >= 0) {
300: switch (compression) {
301: case -1:
302: // nop;
303: break;
304: case M_SOF0:
305: exif.setCompression("Baseline");
306: break;
307: case M_SOF1:
308: exif.setCompression("Extended sequential");
309: break;
310: case M_SOF2:
311: exif.setCompression("Progressive");
312: break;
313: case M_SOF3:
314: exif.setCompression("Lossless");
315: break;
316: case M_SOF5:
317: exif.setCompression("Differential sequential");
318: break;
319: case M_SOF6:
320: exif.setCompression("Differential progressive");
321: break;
322: case M_SOF7:
323: exif.setCompression("Differential lossless");
324: break;
325: case M_SOF9:
326: exif
327: .setCompression("Extended sequential, arithmetic coding");
328: break;
329: case M_SOF10:
330: exif.setCompression("Progressive, arithmetic coding");
331: break;
332: case M_SOF11:
333: exif.setCompression("Lossless, arithmetic coding");
334: break;
335: case M_SOF13:
336: exif
337: .setCompression("Differential sequential, arithmetic coding");
338: break;
339: case M_SOF14:
340: exif
341: .setCompression("Differential progressive, arithmetic coding");
342: break;
343: case M_SOF15:
344: exif
345: .setCompression("Differential lossless, arithmetic coding");
346: break;
347: default:
348: exif.setCompression("Unknown" + compression);
349: }
350: }
351:
352: if (bitsPerPixel >= 0) {
353: exif.setBPP(bitsPerPixel);
354: }
355:
356: if (height >= 0) {
357: exif.setHeight(height);
358: }
359:
360: if (width >= 0) {
361: exif.setWidth(width);
362: }
363:
364: if (numComponents >= 0) {
365: exif.setNumCC(numComponents);
366: }
367: }
368:
369: protected byte[] processComment() throws IOException, JpegException {
370: int length;
371:
372: /* Get the marker parameter length count */
373: length = read2bytes();
374: /* Length includes itself, so must be at least 2 */
375: if (length < 2)
376: throw new JpegException("Erroneous JPEG marker length");
377: length -= 2;
378:
379: StringBuffer buffer = new StringBuffer(length);
380: byte comment[] = new byte[length];
381: int got, pos;
382: pos = 0;
383: while (length > 0) {
384: got = in.read(comment, pos, length);
385: if (got < 0)
386: throw new JpegException(
387: "EOF while reading jpeg comment");
388: pos += got;
389: length -= got;
390: }
391: return comment;
392: }
393:
394: protected int read2bytes() throws IOException, JpegException {
395: int c1, c2;
396: c1 = in.read();
397: if (c1 == -1)
398: throw new JpegException("Premature EOF in JPEG file");
399: c2 = in.read();
400: if (c2 == -1)
401: throw new JpegException("Premature EOF in JPEG file");
402: return (((int) c1) << 8) + ((int) c2);
403: }
404:
405: protected int read2bytes(StringReader sr) throws IOException,
406: JpegException {
407: int c1, c2;
408: c1 = sr.read();
409: if (c1 == -1)
410: throw new JpegException("Premature EOF in JPEG file");
411: c2 = sr.read();
412: if (c2 == -1)
413: throw new JpegException("Premature EOF in JPEG file");
414: return (((int) c1) << 8) + ((int) c2);
415: }
416:
417: /**
418: * skip the body after a marker
419: */
420: protected void skipVariable() throws IOException, JpegException {
421: long len = (long) read2bytes() - 2;
422:
423: if (len < 0)
424: throw new JpegException("Erroneous JPEG marker length");
425: while (len > 0) {
426: long saved = in.skip(len);
427: if (saved < 0)
428: throw new IOException("Error while reading jpeg stream");
429: len -= saved;
430: }
431: }
432:
433: /**
434: * read the image info then the section
435: */
436: protected void readImageInfo() throws IOException, JpegException {
437: long len = (long) read2bytes() - 2;
438:
439: if (len < 0)
440: throw new JpegException("Erroneous JPEG marker length");
441:
442: bitsPerPixel = in.read();
443: len--;
444: height = read2bytes();
445: len -= 2;
446: width = read2bytes();
447: len -= 2;
448: numComponents = in.read();
449: len--;
450:
451: while (len > 0) {
452: long saved = in.skip(len);
453: if (saved < 0)
454: throw new IOException("Error while reading jpeg stream");
455: len -= saved;
456: }
457: }
458:
459: protected int firstMarker() throws IOException, JpegException {
460: int c1, c2;
461: c1 = in.read();
462: c2 = in.read();
463: if (c1 != 0xFF || c2 != M_SOI)
464: throw new JpegException("Not a JPEG file");
465: return c2;
466: }
467:
468: protected int nextMarker() throws IOException {
469: int discarded_bytes = 0;
470: int c;
471:
472: /* Find 0xFF byte; count and skip any non-FFs. */
473: c = in.read();
474: while (c != 0xFF)
475: c = in.read();
476:
477: /* Get marker code byte, swallowing any duplicate FF bytes. Extra FFs
478: * are legal as pad bytes, so don't count them in discarded_bytes.
479: */
480: do {
481: c = in.read();
482: } while (c == 0xFF);
483:
484: return c;
485: }
486:
487: /**
488: * get the headers out of a file, ignoring EXIF
489: */
490: public JpegHeaders(File jpegfile) throws FileNotFoundException,
491: JpegException, IOException {
492: parseJpeg(jpegfile, null);
493: }
494:
495: /**
496: * get the headers out of a file, including EXIF
497: */
498: public JpegHeaders(File jpegfile, Exif exif)
499: throws FileNotFoundException, JpegException, IOException {
500: parseJpeg(jpegfile, exif);
501: }
502:
503: /**
504: * get the headers out of a stream, ignoring EXIF
505: */
506: public JpegHeaders(InputStream in) throws JpegException,
507: IOException {
508: this .in = in;
509: scanHeaders();
510: }
511:
512: /**
513: * get the headers out of a stream, including EXIF
514: */
515: public JpegHeaders(InputStream in, Exif exif) throws JpegException,
516: IOException {
517: this .exif = exif;
518: this .in = in;
519: scanHeaders();
520: }
521:
522: protected void parseJpeg(File jpegfile, Exif exif)
523: throws FileNotFoundException, JpegException, IOException {
524: this .exif = exif;
525: this .jpegfile = jpegfile;
526: this .in = new BufferedInputStream(new FileInputStream(jpegfile));
527: try {
528: scanHeaders();
529: } finally {
530: try {
531: in.close();
532: } catch (Exception ex) {
533: }
534: ;
535: }
536: }
537:
538: public static void main(String args[]) {
539: try {
540: JpegHeaders headers = new JpegHeaders(new File(args[0]));
541: String comments[] = headers.getComments();
542: if (comments != null) {
543: for (int i = 0; i < comments.length; i++)
544: System.out.println(comments[i]);
545: }
546: System.out.println(headers.getXMP());
547: } catch (Exception ex) {
548: ex.printStackTrace();
549: }
550: }
551:
552: }
|