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