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