OSDN Git Service

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