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 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 */ 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 */ 102 protected ExifData getExifData() { 103 return mExifData; 104 } 105 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 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 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 206 public void write(byte[] buffer) throws IOException { 207 write(buffer, 0, buffer.length); 208 } 209 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 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 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 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 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 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 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 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 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