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