2 * Copyright (C) 2012 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.gallery3d.exif;
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;
26 * This class provides a way to replace the Exif header of a JPEG image.
28 * Below is an example of writing EXIF data into a file
30 * public static void writeExif(byte[] jpeg, ExifData exif, String path) {
31 * OutputStream os = null;
33 * os = new FileOutputStream(path);
34 * ExifOutputStream eos = new ExifOutputStream(os);
36 * // Set the exif header
37 * eos.setExifData(exif);
39 * // Write the original jpeg out, the header will be add into the file.
41 * } catch (FileNotFoundException e) {
42 * e.printStackTrace();
43 * } catch (IOException e) {
44 * e.printStackTrace();
49 * } catch (IOException e) {
50 * e.printStackTrace();
57 public class ExifOutputStream extends FilterOutputStream {
58 private static final String TAG = "ExifOutputStream";
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;
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;
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);
77 public ExifOutputStream(OutputStream ou) {
82 * Sets the ExifData to be written into the JPEG file. Should be called before writing image
85 public void setExifData(ExifData exifData) {
90 * Gets the Exif header to be written into the JPEF file.
92 public ExifData getExifData() {
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);
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.
109 public void write(byte[] buffer, int offset, int length) throws IOException {
110 while((mByteToSkip > 0 || mByteToCopy > 0 || mState != STATE_JPEG_DATA)
112 if (mByteToSkip > 0) {
113 int byteToProcess = length > mByteToSkip ? mByteToSkip : length;
114 length -= byteToProcess;
115 mByteToSkip -= byteToProcess;
116 offset += byteToProcess;
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;
125 if (length == 0) return;
128 int byteRead = requestByteToBuffer(2, buffer, offset, length);
131 if (mBuffer.position() < 2) return;
133 assert(mBuffer.getShort() == JpegHeader.SOI);
134 out.write(mBuffer.array(), 0 ,2);
135 mState = STATE_FRAME_HEADER;
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);
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);
152 if (mBuffer.position() < 4) return;
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;
162 out.write(mBuffer.array(), 0, 4);
163 mState = STATE_JPEG_DATA;
169 out.write(buffer, offset, length);
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.
178 public void write(int oneByte) throws IOException {
179 byte[] buf = new byte[] {(byte) (0xff & oneByte)};
184 * Equivalent to calling write(buffer, 0, buffer.length).
187 public void write(byte[] buffer) throws IOException {
188 write(buffer, 0, buffer.length);
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);
204 dataOutputStream.writeShort(TIFF_LITTLE_ENDIAN);
206 dataOutputStream.setByteOrder(mExifData.getByteOrder());
207 dataOutputStream.writeShort(TIFF_HEADER);
208 dataOutputStream.writeInt(8);
209 writeAllTags(dataOutputStream);
210 writeThumbnail(dataOutputStream);
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));
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);
230 IfdData gpsIfd = mExifData.getIfdData(IfdId.TYPE_IFD_GPS);
231 if (gpsIfd != null) {
232 writeIfd(gpsIfd, dataOutputStream);
234 IfdData ifd1 = mExifData.getIfdData(IfdId.TYPE_IFD_1);
236 writeIfd(mExifData.getIfdData(IfdId.TYPE_IFD_1), dataOutputStream);
240 private void writeIfd(IfdData ifd, OrderedDataOutputStream dataOutputStream)
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());
251 writeTagValue(tag, dataOutputStream);
252 for (int i = 0, n = 4 - tag.getDataSize(); i < n; i++) {
253 dataOutputStream.write(0);
257 dataOutputStream.writeInt(ifd.getOffsetToNextIfd());
258 for (ExifTag tag: tags) {
259 if (tag.getDataSize() > 4) {
260 writeTagValue(tag, dataOutputStream);
265 private void writeTagValue(ExifTag tag, OrderedDataOutputStream dataOutputStream)
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);
274 dataOutputStream.write(buf);
275 dataOutputStream.write(0);
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));
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));
290 case ExifTag.TYPE_UNDEFINED:
291 case ExifTag.TYPE_UNSIGNED_BYTE:
292 buf = new byte[tag.getComponentCount()];
294 dataOutputStream.write(buf);
296 case ExifTag.TYPE_UNSIGNED_SHORT:
297 for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
298 dataOutputStream.writeShort((short) tag.getValueAt(i));
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();
316 private void createRequiredIfdAndTag() {
317 // IFD0 is required for all file
318 IfdData ifd0 = mExifData.getIfdData(IfdId.TYPE_IFD_0);
320 ifd0 = new IfdData(IfdId.TYPE_IFD_0);
321 mExifData.addIfdData(ifd0);
323 ExifTag exifOffsetTag = new ExifTag(ExifTag.TAG_EXIF_IFD,
324 ExifTag.TYPE_UNSIGNED_LONG, 1, IfdId.TYPE_IFD_0);
325 ifd0.setTag(exifOffsetTag);
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);
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);
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);
350 IfdData ifd1 = mExifData.getIfdData(IfdId.TYPE_IFD_1);
353 if (mExifData.hasCompressedThumbnail()) {
355 ifd1 = new IfdData(IfdId.TYPE_IFD_1);
356 mExifData.addIfdData(ifd1);
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()){
367 ifd1 = new IfdData(IfdId.TYPE_IFD_1);
368 mExifData.addIfdData(ifd1);
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;
379 lengthTag.setValue(lengths);
380 ifd1.setTag(offsetTag);
381 ifd1.setTag(lengthTag);
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);
391 IfdData exifIfd = mExifData.getIfdData(IfdId.TYPE_IFD_EXIF);
392 offset = calculateOffsetOfIfd(exifIfd, offset);
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);
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);
406 IfdData ifd1 = mExifData.getIfdData(IfdId.TYPE_IFD_1);
408 ifd0.setOffsetToNextIfd(offset);
409 offset = calculateOffsetOfIfd(ifd1, offset);
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++) {
421 offset += mExifData.getStrip(i).length;
423 ifd1.getTag(ExifTag.TAG_STRIP_OFFSETS).setValue(offsets);