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