1 /*
2  * Copyright (C) 2012 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.messaging.util.exif;
18 
19 import android.util.Log;
20 import com.android.messaging.util.LogUtil;
21 
22 import java.io.BufferedOutputStream;
23 import java.io.FilterOutputStream;
24 import java.io.IOException;
25 import java.io.OutputStream;
26 import java.nio.ByteBuffer;
27 import java.nio.ByteOrder;
28 import java.util.ArrayList;
29 
30 /**
31  * This class provides a way to replace the Exif header of a JPEG image.
32  * <p>
33  * Below is an example of writing EXIF data into a file
34  *
35  * <pre>
36  * public static void writeExif(byte[] jpeg, ExifData exif, String path) {
37  *     OutputStream os = null;
38  *     try {
39  *         os = new FileOutputStream(path);
40  *         ExifOutputStream eos = new ExifOutputStream(os);
41  *         // Set the exif header
42  *         eos.setExifData(exif);
43  *         // Write the original jpeg out, the header will be add into the file.
44  *         eos.write(jpeg);
45  *     } catch (FileNotFoundException e) {
46  *         e.printStackTrace();
47  *     } catch (IOException e) {
48  *         e.printStackTrace();
49  *     } finally {
50  *         if (os != null) {
51  *             try {
52  *                 os.close();
53  *             } catch (IOException e) {
54  *                 e.printStackTrace();
55  *             }
56  *         }
57  *     }
58  * }
59  * </pre>
60  */
61 class ExifOutputStream extends FilterOutputStream {
62     private static final String TAG = LogUtil.BUGLE_TAG;
63     private static final boolean DEBUG = false;
64     private static final int STREAMBUFFER_SIZE = 0x00010000; // 64Kb
65 
66     private static final int STATE_SOI = 0;
67     private static final int STATE_FRAME_HEADER = 1;
68     private static final int STATE_JPEG_DATA = 2;
69 
70     private static final int EXIF_HEADER = 0x45786966;
71     private static final short TIFF_HEADER = 0x002A;
72     private static final short TIFF_BIG_ENDIAN = 0x4d4d;
73     private static final short TIFF_LITTLE_ENDIAN = 0x4949;
74     private static final short TAG_SIZE = 12;
75     private static final short TIFF_HEADER_SIZE = 8;
76     private static final int MAX_EXIF_SIZE = 65535;
77 
78     private ExifData mExifData;
79     private int mState = STATE_SOI;
80     private int mByteToSkip;
81     private int mByteToCopy;
82     private final byte[] mSingleByteArray = new byte[1];
83     private final ByteBuffer mBuffer = ByteBuffer.allocate(4);
84     private final ExifInterface mInterface;
85 
ExifOutputStream(OutputStream ou, ExifInterface iRef)86     protected ExifOutputStream(OutputStream ou, ExifInterface iRef) {
87         super(new BufferedOutputStream(ou, STREAMBUFFER_SIZE));
88         mInterface = iRef;
89     }
90 
91     /**
92      * Sets the ExifData to be written into the JPEG file. Should be called
93      * before writing image data.
94      */
setExifData(ExifData exifData)95     protected void setExifData(ExifData exifData) {
96         mExifData = exifData;
97     }
98 
99     /**
100      * Gets the Exif header to be written into the JPEF file.
101      */
getExifData()102     protected ExifData getExifData() {
103         return mExifData;
104     }
105 
requestByteToBuffer(int requestByteCount, byte[] buffer , int offset, int length)106     private int requestByteToBuffer(int requestByteCount, byte[] buffer
107             , int offset, int length) {
108         int byteNeeded = requestByteCount - mBuffer.position();
109         int byteToRead = length > byteNeeded ? byteNeeded : length;
110         mBuffer.put(buffer, offset, byteToRead);
111         return byteToRead;
112     }
113 
114     /**
115      * Writes the image out. The input data should be a valid JPEG format. After
116      * writing, it's Exif header will be replaced by the given header.
117      */
118     @Override
write(byte[] buffer, int offset, int length)119     public void write(byte[] buffer, int offset, int length) throws IOException {
120         while ((mByteToSkip > 0 || mByteToCopy > 0 || mState != STATE_JPEG_DATA)
121                 && length > 0) {
122             if (mByteToSkip > 0) {
123                 int byteToProcess = length > mByteToSkip ? mByteToSkip : length;
124                 length -= byteToProcess;
125                 mByteToSkip -= byteToProcess;
126                 offset += byteToProcess;
127             }
128             if (mByteToCopy > 0) {
129                 int byteToProcess = length > mByteToCopy ? mByteToCopy : length;
130                 out.write(buffer, offset, byteToProcess);
131                 length -= byteToProcess;
132                 mByteToCopy -= byteToProcess;
133                 offset += byteToProcess;
134             }
135             if (length == 0) {
136                 return;
137             }
138             switch (mState) {
139                 case STATE_SOI:
140                     int byteRead = requestByteToBuffer(2, buffer, offset, length);
141                     offset += byteRead;
142                     length -= byteRead;
143                     if (mBuffer.position() < 2) {
144                         return;
145                     }
146                     mBuffer.rewind();
147                     if (mBuffer.getShort() != JpegHeader.SOI) {
148                         throw new IOException("Not a valid jpeg image, cannot write exif");
149                     }
150                     out.write(mBuffer.array(), 0, 2);
151                     mState = STATE_FRAME_HEADER;
152                     mBuffer.rewind();
153                     writeExifData();
154                     break;
155                 case STATE_FRAME_HEADER:
156                     // We ignore the APP1 segment and copy all other segments
157                     // until SOF tag.
158                     byteRead = requestByteToBuffer(4, buffer, offset, length);
159                     offset += byteRead;
160                     length -= byteRead;
161                     // Check if this image data doesn't contain SOF.
162                     if (mBuffer.position() == 2) {
163                         short tag = mBuffer.getShort();
164                         if (tag == JpegHeader.EOI) {
165                             out.write(mBuffer.array(), 0, 2);
166                             mBuffer.rewind();
167                         }
168                     }
169                     if (mBuffer.position() < 4) {
170                         return;
171                     }
172                     mBuffer.rewind();
173                     short marker = mBuffer.getShort();
174                     if (marker == JpegHeader.APP1) {
175                         mByteToSkip = (mBuffer.getShort() & 0x0000ffff) - 2;
176                         mState = STATE_JPEG_DATA;
177                     } else if (!JpegHeader.isSofMarker(marker)) {
178                         out.write(mBuffer.array(), 0, 4);
179                         mByteToCopy = (mBuffer.getShort() & 0x0000ffff) - 2;
180                     } else {
181                         out.write(mBuffer.array(), 0, 4);
182                         mState = STATE_JPEG_DATA;
183                     }
184                     mBuffer.rewind();
185             }
186         }
187         if (length > 0) {
188             out.write(buffer, offset, length);
189         }
190     }
191 
192     /**
193      * Writes the one bytes out. The input data should be a valid JPEG format.
194      * After writing, it's Exif header will be replaced by the given header.
195      */
196     @Override
write(int oneByte)197     public void write(int oneByte) throws IOException {
198         mSingleByteArray[0] = (byte) (0xff & oneByte);
199         write(mSingleByteArray);
200     }
201 
202     /**
203      * Equivalent to calling write(buffer, 0, buffer.length).
204      */
205     @Override
write(byte[] buffer)206     public void write(byte[] buffer) throws IOException {
207         write(buffer, 0, buffer.length);
208     }
209 
writeExifData()210     private void writeExifData() throws IOException {
211         if (mExifData == null) {
212             return;
213         }
214         if (DEBUG) {
215             Log.v(TAG, "Writing exif data...");
216         }
217         ArrayList<ExifTag> nullTags = stripNullValueTags(mExifData);
218         createRequiredIfdAndTag();
219         int exifSize = calculateAllOffset();
220         if (exifSize + 8 > MAX_EXIF_SIZE) {
221             throw new IOException("Exif header is too large (>64Kb)");
222         }
223         OrderedDataOutputStream dataOutputStream = new OrderedDataOutputStream(out);
224         dataOutputStream.setByteOrder(ByteOrder.BIG_ENDIAN);
225         dataOutputStream.writeShort(JpegHeader.APP1);
226         dataOutputStream.writeShort((short) (exifSize + 8));
227         dataOutputStream.writeInt(EXIF_HEADER);
228         dataOutputStream.writeShort((short) 0x0000);
229         if (mExifData.getByteOrder() == ByteOrder.BIG_ENDIAN) {
230             dataOutputStream.writeShort(TIFF_BIG_ENDIAN);
231         } else {
232             dataOutputStream.writeShort(TIFF_LITTLE_ENDIAN);
233         }
234         dataOutputStream.setByteOrder(mExifData.getByteOrder());
235         dataOutputStream.writeShort(TIFF_HEADER);
236         dataOutputStream.writeInt(8);
237         writeAllTags(dataOutputStream);
238         writeThumbnail(dataOutputStream);
239         for (ExifTag t : nullTags) {
240             mExifData.addTag(t);
241         }
242     }
243 
stripNullValueTags(ExifData data)244     private ArrayList<ExifTag> stripNullValueTags(ExifData data) {
245         ArrayList<ExifTag> nullTags = new ArrayList<ExifTag>();
246         if (data.getAllTags() == null) {
247             return nullTags;
248         }
249         for (ExifTag t : data.getAllTags()) {
250             if (t.getValue() == null && !ExifInterface.isOffsetTag(t.getTagId())) {
251                 data.removeTag(t.getTagId(), t.getIfd());
252                 nullTags.add(t);
253             }
254         }
255         return nullTags;
256     }
257 
writeThumbnail(OrderedDataOutputStream dataOutputStream)258     private void writeThumbnail(OrderedDataOutputStream dataOutputStream) throws IOException {
259         if (mExifData.hasCompressedThumbnail()) {
260             dataOutputStream.write(mExifData.getCompressedThumbnail());
261         } else if (mExifData.hasUncompressedStrip()) {
262             for (int i = 0; i < mExifData.getStripCount(); i++) {
263                 dataOutputStream.write(mExifData.getStrip(i));
264             }
265         }
266     }
267 
writeAllTags(OrderedDataOutputStream dataOutputStream)268     private void writeAllTags(OrderedDataOutputStream dataOutputStream) throws IOException {
269         writeIfd(mExifData.getIfdData(IfdId.TYPE_IFD_0), dataOutputStream);
270         writeIfd(mExifData.getIfdData(IfdId.TYPE_IFD_EXIF), dataOutputStream);
271         IfdData interoperabilityIfd = mExifData.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY);
272         if (interoperabilityIfd != null) {
273             writeIfd(interoperabilityIfd, dataOutputStream);
274         }
275         IfdData gpsIfd = mExifData.getIfdData(IfdId.TYPE_IFD_GPS);
276         if (gpsIfd != null) {
277             writeIfd(gpsIfd, dataOutputStream);
278         }
279         IfdData ifd1 = mExifData.getIfdData(IfdId.TYPE_IFD_1);
280         if (ifd1 != null) {
281             writeIfd(mExifData.getIfdData(IfdId.TYPE_IFD_1), dataOutputStream);
282         }
283     }
284 
writeIfd(IfdData ifd, OrderedDataOutputStream dataOutputStream)285     private void writeIfd(IfdData ifd, OrderedDataOutputStream dataOutputStream)
286             throws IOException {
287         ExifTag[] tags = ifd.getAllTags();
288         dataOutputStream.writeShort((short) tags.length);
289         for (ExifTag tag : tags) {
290             dataOutputStream.writeShort(tag.getTagId());
291             dataOutputStream.writeShort(tag.getDataType());
292             dataOutputStream.writeInt(tag.getComponentCount());
293             if (DEBUG) {
294                 Log.v(TAG, "\n" + tag.toString());
295             }
296             if (tag.getDataSize() > 4) {
297                 dataOutputStream.writeInt(tag.getOffset());
298             } else {
299                 ExifOutputStream.writeTagValue(tag, dataOutputStream);
300                 for (int i = 0, n = 4 - tag.getDataSize(); i < n; i++) {
301                     dataOutputStream.write(0);
302                 }
303             }
304         }
305         dataOutputStream.writeInt(ifd.getOffsetToNextIfd());
306         for (ExifTag tag : tags) {
307             if (tag.getDataSize() > 4) {
308                 ExifOutputStream.writeTagValue(tag, dataOutputStream);
309             }
310         }
311     }
312 
calculateOffsetOfIfd(IfdData ifd, int offset)313     private int calculateOffsetOfIfd(IfdData ifd, int offset) {
314         offset += 2 + ifd.getTagCount() * TAG_SIZE + 4;
315         ExifTag[] tags = ifd.getAllTags();
316         for (ExifTag tag : tags) {
317             if (tag.getDataSize() > 4) {
318                 tag.setOffset(offset);
319                 offset += tag.getDataSize();
320             }
321         }
322         return offset;
323     }
324 
createRequiredIfdAndTag()325     private void createRequiredIfdAndTag() throws IOException {
326         // IFD0 is required for all file
327         IfdData ifd0 = mExifData.getIfdData(IfdId.TYPE_IFD_0);
328         if (ifd0 == null) {
329             ifd0 = new IfdData(IfdId.TYPE_IFD_0);
330             mExifData.addIfdData(ifd0);
331         }
332         ExifTag exifOffsetTag = mInterface.buildUninitializedTag(ExifInterface.TAG_EXIF_IFD);
333         if (exifOffsetTag == null) {
334             throw new IOException("No definition for crucial exif tag: "
335                     + ExifInterface.TAG_EXIF_IFD);
336         }
337         ifd0.setTag(exifOffsetTag);
338 
339         // Exif IFD is required for all files.
340         IfdData exifIfd = mExifData.getIfdData(IfdId.TYPE_IFD_EXIF);
341         if (exifIfd == null) {
342             exifIfd = new IfdData(IfdId.TYPE_IFD_EXIF);
343             mExifData.addIfdData(exifIfd);
344         }
345 
346         // GPS IFD
347         IfdData gpsIfd = mExifData.getIfdData(IfdId.TYPE_IFD_GPS);
348         if (gpsIfd != null) {
349             ExifTag gpsOffsetTag = mInterface.buildUninitializedTag(ExifInterface.TAG_GPS_IFD);
350             if (gpsOffsetTag == null) {
351                 throw new IOException("No definition for crucial exif tag: "
352                         + ExifInterface.TAG_GPS_IFD);
353             }
354             ifd0.setTag(gpsOffsetTag);
355         }
356 
357         // Interoperability IFD
358         IfdData interIfd = mExifData.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY);
359         if (interIfd != null) {
360             ExifTag interOffsetTag = mInterface
361                     .buildUninitializedTag(ExifInterface.TAG_INTEROPERABILITY_IFD);
362             if (interOffsetTag == null) {
363                 throw new IOException("No definition for crucial exif tag: "
364                         + ExifInterface.TAG_INTEROPERABILITY_IFD);
365             }
366             exifIfd.setTag(interOffsetTag);
367         }
368 
369         IfdData ifd1 = mExifData.getIfdData(IfdId.TYPE_IFD_1);
370 
371         // thumbnail
372         if (mExifData.hasCompressedThumbnail()) {
373 
374             if (ifd1 == null) {
375                 ifd1 = new IfdData(IfdId.TYPE_IFD_1);
376                 mExifData.addIfdData(ifd1);
377             }
378 
379             ExifTag offsetTag = mInterface
380                     .buildUninitializedTag(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT);
381             if (offsetTag == null) {
382                 throw new IOException("No definition for crucial exif tag: "
383                         + ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT);
384             }
385 
386             ifd1.setTag(offsetTag);
387             ExifTag lengthTag = mInterface
388                     .buildUninitializedTag(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
389             if (lengthTag == null) {
390                 throw new IOException("No definition for crucial exif tag: "
391                         + ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
392             }
393 
394             lengthTag.setValue(mExifData.getCompressedThumbnail().length);
395             ifd1.setTag(lengthTag);
396 
397             // Get rid of tags for uncompressed if they exist.
398             ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS));
399             ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_BYTE_COUNTS));
400         } else if (mExifData.hasUncompressedStrip()) {
401             if (ifd1 == null) {
402                 ifd1 = new IfdData(IfdId.TYPE_IFD_1);
403                 mExifData.addIfdData(ifd1);
404             }
405             int stripCount = mExifData.getStripCount();
406             ExifTag offsetTag = mInterface.buildUninitializedTag(ExifInterface.TAG_STRIP_OFFSETS);
407             if (offsetTag == null) {
408                 throw new IOException("No definition for crucial exif tag: "
409                         + ExifInterface.TAG_STRIP_OFFSETS);
410             }
411             ExifTag lengthTag = mInterface
412                     .buildUninitializedTag(ExifInterface.TAG_STRIP_BYTE_COUNTS);
413             if (lengthTag == null) {
414                 throw new IOException("No definition for crucial exif tag: "
415                         + ExifInterface.TAG_STRIP_BYTE_COUNTS);
416             }
417             long[] lengths = new long[stripCount];
418             for (int i = 0; i < mExifData.getStripCount(); i++) {
419                 lengths[i] = mExifData.getStrip(i).length;
420             }
421             lengthTag.setValue(lengths);
422             ifd1.setTag(offsetTag);
423             ifd1.setTag(lengthTag);
424             // Get rid of tags for compressed if they exist.
425             ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT));
426             ifd1.removeTag(ExifInterface
427                     .getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH));
428         } else if (ifd1 != null) {
429             // Get rid of offset and length tags if there is no thumbnail.
430             ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS));
431             ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_BYTE_COUNTS));
432             ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT));
433             ifd1.removeTag(ExifInterface
434                     .getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH));
435         }
436     }
437 
calculateAllOffset()438     private int calculateAllOffset() {
439         int offset = TIFF_HEADER_SIZE;
440         IfdData ifd0 = mExifData.getIfdData(IfdId.TYPE_IFD_0);
441         offset = calculateOffsetOfIfd(ifd0, offset);
442         ifd0.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_EXIF_IFD)).setValue(offset);
443 
444         IfdData exifIfd = mExifData.getIfdData(IfdId.TYPE_IFD_EXIF);
445         offset = calculateOffsetOfIfd(exifIfd, offset);
446 
447         IfdData interIfd = mExifData.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY);
448         if (interIfd != null) {
449             exifIfd.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_INTEROPERABILITY_IFD))
450                     .setValue(offset);
451             offset = calculateOffsetOfIfd(interIfd, offset);
452         }
453 
454         IfdData gpsIfd = mExifData.getIfdData(IfdId.TYPE_IFD_GPS);
455         if (gpsIfd != null) {
456             ifd0.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_GPS_IFD)).setValue(offset);
457             offset = calculateOffsetOfIfd(gpsIfd, offset);
458         }
459 
460         IfdData ifd1 = mExifData.getIfdData(IfdId.TYPE_IFD_1);
461         if (ifd1 != null) {
462             ifd0.setOffsetToNextIfd(offset);
463             offset = calculateOffsetOfIfd(ifd1, offset);
464         }
465 
466         // thumbnail
467         if (mExifData.hasCompressedThumbnail()) {
468             ifd1.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT))
469                     .setValue(offset);
470             offset += mExifData.getCompressedThumbnail().length;
471         } else if (mExifData.hasUncompressedStrip()) {
472             int stripCount = mExifData.getStripCount();
473             long[] offsets = new long[stripCount];
474             for (int i = 0; i < mExifData.getStripCount(); i++) {
475                 offsets[i] = offset;
476                 offset += mExifData.getStrip(i).length;
477             }
478             ifd1.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS)).setValue(
479                     offsets);
480         }
481         return offset;
482     }
483 
writeTagValue(ExifTag tag, OrderedDataOutputStream dataOutputStream)484     static void writeTagValue(ExifTag tag, OrderedDataOutputStream dataOutputStream)
485             throws IOException {
486         switch (tag.getDataType()) {
487             case ExifTag.TYPE_ASCII:
488                 byte buf[] = tag.getStringByte();
489                 if (buf.length == tag.getComponentCount()) {
490                     buf[buf.length - 1] = 0;
491                     dataOutputStream.write(buf);
492                 } else {
493                     dataOutputStream.write(buf);
494                     dataOutputStream.write(0);
495                 }
496                 break;
497             case ExifTag.TYPE_LONG:
498             case ExifTag.TYPE_UNSIGNED_LONG:
499                 for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
500                     dataOutputStream.writeInt((int) tag.getValueAt(i));
501                 }
502                 break;
503             case ExifTag.TYPE_RATIONAL:
504             case ExifTag.TYPE_UNSIGNED_RATIONAL:
505                 for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
506                     dataOutputStream.writeRational(tag.getRational(i));
507                 }
508                 break;
509             case ExifTag.TYPE_UNDEFINED:
510             case ExifTag.TYPE_UNSIGNED_BYTE:
511                 buf = new byte[tag.getComponentCount()];
512                 tag.getBytes(buf);
513                 dataOutputStream.write(buf);
514                 break;
515             case ExifTag.TYPE_UNSIGNED_SHORT:
516                 for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
517                     dataOutputStream.writeShort((short) tag.getValueAt(i));
518                 }
519                 break;
520         }
521     }
522 }
523