1 /* 2 * Copyright 2014 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 android.hardware.camera2; 18 19 import android.annotation.IntRange; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.graphics.Bitmap; 23 import android.graphics.Color; 24 import android.graphics.ImageFormat; 25 import android.hardware.camera2.impl.CameraMetadataNative; 26 import android.location.Location; 27 import android.media.ExifInterface; 28 import android.media.Image; 29 import android.os.SystemClock; 30 import android.util.Log; 31 import android.util.Size; 32 33 import java.io.IOException; 34 import java.io.InputStream; 35 import java.io.OutputStream; 36 import java.nio.ByteBuffer; 37 import java.text.DateFormat; 38 import java.text.SimpleDateFormat; 39 import java.util.Calendar; 40 import java.util.TimeZone; 41 42 /** 43 * The {@link DngCreator} class provides functions to write raw pixel data as a DNG file. 44 * 45 * <p> 46 * This class is designed to be used with the {@link android.graphics.ImageFormat#RAW_SENSOR} 47 * buffers available from {@link android.hardware.camera2.CameraDevice}, or with Bayer-type raw 48 * pixel data that is otherwise generated by an application. The DNG metadata tags will be 49 * generated from a {@link android.hardware.camera2.CaptureResult} object or set directly. 50 * </p> 51 * 52 * <p> 53 * The DNG file format is a cross-platform file format that is used to store pixel data from 54 * camera sensors with minimal pre-processing applied. DNG files allow for pixel data to be 55 * defined in a user-defined colorspace, and have associated metadata that allow for this 56 * pixel data to be converted to the standard CIE XYZ colorspace during post-processing. 57 * </p> 58 * 59 * <p> 60 * For more information on the DNG file format and associated metadata, please refer to the 61 * <a href= 62 * "https://wwwimages2.adobe.com/content/dam/Adobe/en/products/photoshop/pdfs/dng_spec_1.4.0.0.pdf"> 63 * Adobe DNG 1.4.0.0 specification</a>. 64 * </p> 65 */ 66 public final class DngCreator implements AutoCloseable { 67 68 private static final String TAG = "DngCreator"; 69 /** 70 * Create a new DNG object. 71 * 72 * <p> 73 * It is not necessary to call any set methods to write a well-formatted DNG file. 74 * </p> 75 * <p> 76 * DNG metadata tags will be generated from the corresponding parameters in the 77 * {@link android.hardware.camera2.CaptureResult} object. 78 * </p> 79 * <p> 80 * For best quality DNG files, it is strongly recommended that lens shading map output is 81 * enabled if supported. See {@link CaptureRequest#STATISTICS_LENS_SHADING_MAP_MODE}. 82 * </p> 83 * @param characteristics an object containing the static 84 * {@link android.hardware.camera2.CameraCharacteristics}. 85 * @param metadata a metadata object to generate tags from. 86 */ 87 public DngCreator(@NonNull CameraCharacteristics characteristics, 88 @NonNull CaptureResult metadata) { 89 if (characteristics == null || metadata == null) { 90 throw new IllegalArgumentException("Null argument to DngCreator constructor"); 91 } 92 93 // Find current time in milliseconds since 1970 94 long currentTime = System.currentTimeMillis(); 95 // Assume that sensor timestamp has that timebase to start 96 long timeOffset = 0; 97 98 int timestampSource = characteristics.get( 99 CameraCharacteristics.SENSOR_INFO_TIMESTAMP_SOURCE); 100 101 if (timestampSource == CameraCharacteristics.SENSOR_INFO_TIMESTAMP_SOURCE_REALTIME) { 102 // This means the same timebase as SystemClock.elapsedRealtime(), 103 // which is CLOCK_BOOTTIME 104 timeOffset = currentTime - SystemClock.elapsedRealtime(); 105 } else if (timestampSource == CameraCharacteristics.SENSOR_INFO_TIMESTAMP_SOURCE_UNKNOWN) { 106 // This means the same timebase as System.currentTimeMillis(), 107 // which is CLOCK_MONOTONIC 108 timeOffset = currentTime - SystemClock.uptimeMillis(); 109 } else { 110 // Unexpected time source - treat as CLOCK_MONOTONIC 111 Log.w(TAG, "Sensor timestamp source is unexpected: " + timestampSource); 112 timeOffset = currentTime - SystemClock.uptimeMillis(); 113 } 114 115 // Find capture time (nanos since boot) 116 Long timestamp = metadata.get(CaptureResult.SENSOR_TIMESTAMP); 117 long captureTime = currentTime; 118 if (timestamp != null) { 119 captureTime = timestamp / 1000000 + timeOffset; 120 } 121 122 // Create this fresh each time since the time zone may change while a long-running application 123 // is active. 124 final DateFormat dateTimeStampFormat = 125 new SimpleDateFormat(TIFF_DATETIME_FORMAT); 126 dateTimeStampFormat.setTimeZone(TimeZone.getDefault()); 127 128 // Format for metadata 129 String formattedCaptureTime = dateTimeStampFormat.format(captureTime); 130 131 nativeInit(characteristics.getNativeCopy(), metadata.getNativeCopy(), 132 formattedCaptureTime); 133 } 134 135 /** 136 * Set the orientation value to write. 137 * 138 * <p> 139 * This will be written as the TIFF "Orientation" tag {@code (0x0112)}. 140 * Calling this will override any prior settings for this tag. 141 * </p> 142 * 143 * @param orientation the orientation value to set, one of: 144 * <ul> 145 * <li>{@link android.media.ExifInterface#ORIENTATION_NORMAL}</li> 146 * <li>{@link android.media.ExifInterface#ORIENTATION_FLIP_HORIZONTAL}</li> 147 * <li>{@link android.media.ExifInterface#ORIENTATION_ROTATE_180}</li> 148 * <li>{@link android.media.ExifInterface#ORIENTATION_FLIP_VERTICAL}</li> 149 * <li>{@link android.media.ExifInterface#ORIENTATION_TRANSPOSE}</li> 150 * <li>{@link android.media.ExifInterface#ORIENTATION_ROTATE_90}</li> 151 * <li>{@link android.media.ExifInterface#ORIENTATION_TRANSVERSE}</li> 152 * <li>{@link android.media.ExifInterface#ORIENTATION_ROTATE_270}</li> 153 * </ul> 154 * @return this {@link #DngCreator} object. 155 */ 156 @NonNull 157 public DngCreator setOrientation(int orientation) { 158 if (orientation < ExifInterface.ORIENTATION_UNDEFINED || 159 orientation > ExifInterface.ORIENTATION_ROTATE_270) { 160 throw new IllegalArgumentException("Orientation " + orientation + 161 " is not a valid EXIF orientation value"); 162 } 163 // ExifInterface and TIFF/EP spec differ on definition of 164 // "Unknown" orientation; other values map directly 165 if (orientation == ExifInterface.ORIENTATION_UNDEFINED) { 166 orientation = TAG_ORIENTATION_UNKNOWN; 167 } 168 nativeSetOrientation(orientation); 169 return this; 170 } 171 172 /** 173 * Set the thumbnail image. 174 * 175 * <p> 176 * Pixel data will be converted to a Baseline TIFF RGB image, with 8 bits per color channel. 177 * The alpha channel will be discarded. Thumbnail images with a dimension larger than 178 * {@link #MAX_THUMBNAIL_DIMENSION} will be rejected. 179 * </p> 180 * 181 * @param pixels a {@link android.graphics.Bitmap} of pixel data. 182 * @return this {@link #DngCreator} object. 183 * @throws java.lang.IllegalArgumentException if the given thumbnail image has a dimension 184 * larger than {@link #MAX_THUMBNAIL_DIMENSION}. 185 */ 186 @NonNull 187 public DngCreator setThumbnail(@NonNull Bitmap pixels) { 188 if (pixels == null) { 189 throw new IllegalArgumentException("Null argument to setThumbnail"); 190 } 191 192 int width = pixels.getWidth(); 193 int height = pixels.getHeight(); 194 195 if (width > MAX_THUMBNAIL_DIMENSION || height > MAX_THUMBNAIL_DIMENSION) { 196 throw new IllegalArgumentException("Thumbnail dimensions width,height (" + width + 197 "," + height + ") too large, dimensions must be smaller than " + 198 MAX_THUMBNAIL_DIMENSION); 199 } 200 201 ByteBuffer rgbBuffer = convertToRGB(pixels); 202 nativeSetThumbnail(rgbBuffer, width, height); 203 204 return this; 205 } 206 207 /** 208 * Set the thumbnail image. 209 * 210 * <p> 211 * Pixel data is interpreted as a {@link android.graphics.ImageFormat#YUV_420_888} image. 212 * Thumbnail images with a dimension larger than {@link #MAX_THUMBNAIL_DIMENSION} will be 213 * rejected. 214 * </p> 215 * 216 * @param pixels an {@link android.media.Image} object with the format 217 * {@link android.graphics.ImageFormat#YUV_420_888}. 218 * @return this {@link #DngCreator} object. 219 * @throws java.lang.IllegalArgumentException if the given thumbnail image has a dimension 220 * larger than {@link #MAX_THUMBNAIL_DIMENSION}. 221 */ 222 @NonNull 223 public DngCreator setThumbnail(@NonNull Image pixels) { 224 if (pixels == null) { 225 throw new IllegalArgumentException("Null argument to setThumbnail"); 226 } 227 228 int format = pixels.getFormat(); 229 if (format != ImageFormat.YUV_420_888) { 230 throw new IllegalArgumentException("Unsupported Image format " + format); 231 } 232 233 int width = pixels.getWidth(); 234 int height = pixels.getHeight(); 235 236 if (width > MAX_THUMBNAIL_DIMENSION || height > MAX_THUMBNAIL_DIMENSION) { 237 throw new IllegalArgumentException("Thumbnail dimensions width,height (" + width + 238 "," + height + ") too large, dimensions must be smaller than " + 239 MAX_THUMBNAIL_DIMENSION); 240 } 241 242 ByteBuffer rgbBuffer = convertToRGB(pixels); 243 nativeSetThumbnail(rgbBuffer, width, height); 244 245 return this; 246 } 247 248 /** 249 * Set image location metadata. 250 * 251 * <p> 252 * The given location object must contain at least a valid time, latitude, and longitude 253 * (equivalent to the values returned by {@link android.location.Location#getTime()}, 254 * {@link android.location.Location#getLatitude()}, and 255 * {@link android.location.Location#getLongitude()} methods). 256 * </p> 257 * 258 * @param location an {@link android.location.Location} object to set. 259 * @return this {@link #DngCreator} object. 260 * 261 * @throws java.lang.IllegalArgumentException if the given location object doesn't 262 * contain enough information to set location metadata. 263 */ 264 @NonNull 265 public DngCreator setLocation(@NonNull Location location) { 266 if (location == null) { 267 throw new IllegalArgumentException("Null location passed to setLocation"); 268 } 269 double latitude = location.getLatitude(); 270 double longitude = location.getLongitude(); 271 long time = location.getTime(); 272 273 int[] latTag = toExifLatLong(latitude); 274 int[] longTag = toExifLatLong(longitude); 275 String latRef = latitude >= 0 ? GPS_LAT_REF_NORTH : GPS_LAT_REF_SOUTH; 276 String longRef = longitude >= 0 ? GPS_LONG_REF_EAST : GPS_LONG_REF_WEST; 277 278 String dateTag = sExifGPSDateStamp.format(time); 279 mGPSTimeStampCalendar.setTimeInMillis(time); 280 int[] timeTag = new int[] { mGPSTimeStampCalendar.get(Calendar.HOUR_OF_DAY), 1, 281 mGPSTimeStampCalendar.get(Calendar.MINUTE), 1, 282 mGPSTimeStampCalendar.get(Calendar.SECOND), 1 }; 283 nativeSetGpsTags(latTag, latRef, longTag, longRef, dateTag, timeTag); 284 return this; 285 } 286 287 /** 288 * Set the user description string to write. 289 * 290 * <p> 291 * This is equivalent to setting the TIFF "ImageDescription" tag {@code (0x010E)}. 292 * </p> 293 * 294 * @param description the user description string. 295 * @return this {@link #DngCreator} object. 296 */ 297 @NonNull 298 public DngCreator setDescription(@NonNull String description) { 299 if (description == null) { 300 throw new IllegalArgumentException("Null description passed to setDescription."); 301 } 302 nativeSetDescription(description); 303 return this; 304 } 305 306 /** 307 * Write the {@link android.graphics.ImageFormat#RAW_SENSOR} pixel data to a DNG file with 308 * the currently configured metadata. 309 * 310 * <p> 311 * Raw pixel data must have 16 bits per pixel, and the input must contain at least 312 * {@code offset + 2 * width * height)} bytes. The width and height of 313 * the input are taken from the width and height set in the {@link DngCreator} metadata tags, 314 * and will typically be equal to the width and height of 315 * {@link CameraCharacteristics#SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE}. Prior to 316 * API level 23, this was always the same as 317 * {@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE}. 318 * The pixel layout in the input is determined from the reported color filter arrangement (CFA) 319 * set in {@link CameraCharacteristics#SENSOR_INFO_COLOR_FILTER_ARRANGEMENT}. If insufficient 320 * metadata is available to write a well-formatted DNG file, an 321 * {@link java.lang.IllegalStateException} will be thrown. 322 * </p> 323 * 324 * @param dngOutput an {@link java.io.OutputStream} to write the DNG file to. 325 * @param size the {@link Size} of the image to write, in pixels. 326 * @param pixels an {@link java.io.InputStream} of pixel data to write. 327 * @param offset the offset of the raw image in bytes. This indicates how many bytes will 328 * be skipped in the input before any pixel data is read. 329 * 330 * @throws IOException if an error was encountered in the input or output stream. 331 * @throws java.lang.IllegalStateException if not enough metadata information has been 332 * set to write a well-formatted DNG file. 333 * @throws java.lang.IllegalArgumentException if the size passed in does not match the 334 */ 335 public void writeInputStream(@NonNull OutputStream dngOutput, @NonNull Size size, 336 @NonNull InputStream pixels, @IntRange(from=0) long offset) throws IOException { 337 if (dngOutput == null) { 338 throw new IllegalArgumentException("Null dngOutput passed to writeInputStream"); 339 } else if (size == null) { 340 throw new IllegalArgumentException("Null size passed to writeInputStream"); 341 } else if (pixels == null) { 342 throw new IllegalArgumentException("Null pixels passed to writeInputStream"); 343 } else if (offset < 0) { 344 throw new IllegalArgumentException("Negative offset passed to writeInputStream"); 345 } 346 347 int width = size.getWidth(); 348 int height = size.getHeight(); 349 if (width <= 0 || height <= 0) { 350 throw new IllegalArgumentException("Size with invalid width, height: (" + width + "," + 351 height + ") passed to writeInputStream"); 352 } 353 nativeWriteInputStream(dngOutput, pixels, width, height, offset); 354 } 355 356 /** 357 * Write the {@link android.graphics.ImageFormat#RAW_SENSOR} pixel data to a DNG file with 358 * the currently configured metadata. 359 * 360 * <p> 361 * Raw pixel data must have 16 bits per pixel, and the input must contain at least 362 * {@code offset + 2 * width * height)} bytes. The width and height of 363 * the input are taken from the width and height set in the {@link DngCreator} metadata tags, 364 * and will typically be equal to the width and height of 365 * {@link CameraCharacteristics#SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE}. Prior to 366 * API level 23, this was always the same as 367 * {@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE}. 368 * The pixel layout in the input is determined from the reported color filter arrangement (CFA) 369 * set in {@link CameraCharacteristics#SENSOR_INFO_COLOR_FILTER_ARRANGEMENT}. If insufficient 370 * metadata is available to write a well-formatted DNG file, an 371 * {@link java.lang.IllegalStateException} will be thrown. 372 * </p> 373 * 374 * <p> 375 * Any mark or limit set on this {@link ByteBuffer} is ignored, and will be cleared by this 376 * method. 377 * </p> 378 * 379 * @param dngOutput an {@link java.io.OutputStream} to write the DNG file to. 380 * @param size the {@link Size} of the image to write, in pixels. 381 * @param pixels an {@link java.nio.ByteBuffer} of pixel data to write. 382 * @param offset the offset of the raw image in bytes. This indicates how many bytes will 383 * be skipped in the input before any pixel data is read. 384 * 385 * @throws IOException if an error was encountered in the input or output stream. 386 * @throws java.lang.IllegalStateException if not enough metadata information has been 387 * set to write a well-formatted DNG file. 388 */ 389 public void writeByteBuffer(@NonNull OutputStream dngOutput, @NonNull Size size, 390 @NonNull ByteBuffer pixels, @IntRange(from=0) long offset) 391 throws IOException { 392 if (dngOutput == null) { 393 throw new IllegalArgumentException("Null dngOutput passed to writeByteBuffer"); 394 } else if (size == null) { 395 throw new IllegalArgumentException("Null size passed to writeByteBuffer"); 396 } else if (pixels == null) { 397 throw new IllegalArgumentException("Null pixels passed to writeByteBuffer"); 398 } else if (offset < 0) { 399 throw new IllegalArgumentException("Negative offset passed to writeByteBuffer"); 400 } 401 402 int width = size.getWidth(); 403 int height = size.getHeight(); 404 405 writeByteBuffer(width, height, pixels, dngOutput, DEFAULT_PIXEL_STRIDE, 406 width * DEFAULT_PIXEL_STRIDE, offset); 407 } 408 409 /** 410 * Write the pixel data to a DNG file with the currently configured metadata. 411 * 412 * <p> 413 * For this method to succeed, the {@link android.media.Image} input must contain 414 * {@link android.graphics.ImageFormat#RAW_SENSOR} pixel data, otherwise an 415 * {@link java.lang.IllegalArgumentException} will be thrown. 416 * </p> 417 * 418 * @param dngOutput an {@link java.io.OutputStream} to write the DNG file to. 419 * @param pixels an {@link android.media.Image} to write. 420 * 421 * @throws java.io.IOException if an error was encountered in the output stream. 422 * @throws java.lang.IllegalArgumentException if an image with an unsupported format was used. 423 * @throws java.lang.IllegalStateException if not enough metadata information has been 424 * set to write a well-formatted DNG file. 425 */ 426 public void writeImage(@NonNull OutputStream dngOutput, @NonNull Image pixels) 427 throws IOException { 428 if (dngOutput == null) { 429 throw new IllegalArgumentException("Null dngOutput to writeImage"); 430 } else if (pixels == null) { 431 throw new IllegalArgumentException("Null pixels to writeImage"); 432 } 433 434 int format = pixels.getFormat(); 435 if (format != ImageFormat.RAW_SENSOR) { 436 throw new IllegalArgumentException("Unsupported image format " + format); 437 } 438 439 Image.Plane[] planes = pixels.getPlanes(); 440 if (planes == null || planes.length <= 0) { 441 throw new IllegalArgumentException("Image with no planes passed to writeImage"); 442 } 443 444 ByteBuffer buf = planes[0].getBuffer(); 445 writeByteBuffer(pixels.getWidth(), pixels.getHeight(), buf, dngOutput, 446 planes[0].getPixelStride(), planes[0].getRowStride(), 0); 447 } 448 449 @Override 450 public void close() { 451 nativeDestroy(); 452 } 453 454 /** 455 * Max width or height dimension for thumbnails. 456 */ 457 public static final int MAX_THUMBNAIL_DIMENSION = 256; // max pixel dimension for TIFF/EP 458 459 @Override 460 protected void finalize() throws Throwable { 461 try { 462 close(); 463 } finally { 464 super.finalize(); 465 } 466 } 467 468 private static final String GPS_LAT_REF_NORTH = "N"; 469 private static final String GPS_LAT_REF_SOUTH = "S"; 470 private static final String GPS_LONG_REF_EAST = "E"; 471 private static final String GPS_LONG_REF_WEST = "W"; 472 473 private static final String GPS_DATE_FORMAT_STR = "yyyy:MM:dd"; 474 private static final String TIFF_DATETIME_FORMAT = "yyyy:MM:dd HH:mm:ss"; 475 private static final DateFormat sExifGPSDateStamp = new SimpleDateFormat(GPS_DATE_FORMAT_STR); 476 private final Calendar mGPSTimeStampCalendar = Calendar 477 .getInstance(TimeZone.getTimeZone("UTC")); 478 479 static { 480 sExifGPSDateStamp.setTimeZone(TimeZone.getTimeZone("UTC")); 481 } 482 483 private static final int DEFAULT_PIXEL_STRIDE = 2; // bytes per sample 484 private static final int BYTES_PER_RGB_PIX = 3; // byts per pixel 485 486 // TIFF tag values needed to map between public API and TIFF spec 487 private static final int TAG_ORIENTATION_UNKNOWN = 9; 488 489 /** 490 * Offset, rowStride, and pixelStride are given in bytes. Height and width are given in pixels. 491 */ 492 private void writeByteBuffer(int width, int height, ByteBuffer pixels, OutputStream dngOutput, 493 int pixelStride, int rowStride, long offset) throws IOException { 494 if (width <= 0 || height <= 0) { 495 throw new IllegalArgumentException("Image with invalid width, height: (" + width + "," + 496 height + ") passed to write"); 497 } 498 long capacity = pixels.capacity(); 499 long totalSize = ((long) rowStride) * height + offset; 500 if (capacity < totalSize) { 501 throw new IllegalArgumentException("Image size " + capacity + 502 " is too small (must be larger than " + totalSize + ")"); 503 } 504 int minRowStride = pixelStride * width; 505 if (minRowStride > rowStride) { 506 throw new IllegalArgumentException("Invalid image pixel stride, row byte width " + 507 minRowStride + " is too large, expecting " + rowStride); 508 } 509 pixels.clear(); // Reset mark and limit 510 nativeWriteImage(dngOutput, width, height, pixels, rowStride, pixelStride, offset, 511 pixels.isDirect()); 512 pixels.clear(); 513 } 514 515 /** 516 * Convert a single YUV pixel to RGB. 517 */ 518 private static void yuvToRgb(byte[] yuvData, int outOffset, /*out*/byte[] rgbOut) { 519 final int COLOR_MAX = 255; 520 521 float y = yuvData[0] & 0xFF; // Y channel 522 float cb = yuvData[1] & 0xFF; // U channel 523 float cr = yuvData[2] & 0xFF; // V channel 524 525 // convert YUV -> RGB (from JFIF's "Conversion to and from RGB" section) 526 float r = y + 1.402f * (cr - 128); 527 float g = y - 0.34414f * (cb - 128) - 0.71414f * (cr - 128); 528 float b = y + 1.772f * (cb - 128); 529 530 // clamp to [0,255] 531 rgbOut[outOffset] = (byte) Math.max(0, Math.min(COLOR_MAX, r)); 532 rgbOut[outOffset + 1] = (byte) Math.max(0, Math.min(COLOR_MAX, g)); 533 rgbOut[outOffset + 2] = (byte) Math.max(0, Math.min(COLOR_MAX, b)); 534 } 535 536 /** 537 * Convert a single {@link Color} pixel to RGB. 538 */ 539 private static void colorToRgb(int color, int outOffset, /*out*/byte[] rgbOut) { 540 rgbOut[outOffset] = (byte) Color.red(color); 541 rgbOut[outOffset + 1] = (byte) Color.green(color); 542 rgbOut[outOffset + 2] = (byte) Color.blue(color); 543 // Discards Alpha 544 } 545 546 /** 547 * Generate a direct RGB {@link ByteBuffer} from a YUV420_888 {@link Image}. 548 */ 549 private static ByteBuffer convertToRGB(Image yuvImage) { 550 // TODO: Optimize this with renderscript intrinsic. 551 int width = yuvImage.getWidth(); 552 int height = yuvImage.getHeight(); 553 ByteBuffer buf = ByteBuffer.allocateDirect(BYTES_PER_RGB_PIX * width * height); 554 555 Image.Plane yPlane = yuvImage.getPlanes()[0]; 556 Image.Plane uPlane = yuvImage.getPlanes()[1]; 557 Image.Plane vPlane = yuvImage.getPlanes()[2]; 558 559 ByteBuffer yBuf = yPlane.getBuffer(); 560 ByteBuffer uBuf = uPlane.getBuffer(); 561 ByteBuffer vBuf = vPlane.getBuffer(); 562 563 yBuf.rewind(); 564 uBuf.rewind(); 565 vBuf.rewind(); 566 567 int yRowStride = yPlane.getRowStride(); 568 int vRowStride = vPlane.getRowStride(); 569 int uRowStride = uPlane.getRowStride(); 570 571 int yPixStride = yPlane.getPixelStride(); 572 int vPixStride = vPlane.getPixelStride(); 573 int uPixStride = uPlane.getPixelStride(); 574 575 byte[] yuvPixel = { 0, 0, 0 }; 576 byte[] yFullRow = new byte[yPixStride * (width - 1) + 1]; 577 byte[] uFullRow = new byte[uPixStride * (width / 2 - 1) + 1]; 578 byte[] vFullRow = new byte[vPixStride * (width / 2 - 1) + 1]; 579 byte[] finalRow = new byte[BYTES_PER_RGB_PIX * width]; 580 for (int i = 0; i < height; i++) { 581 int halfH = i / 2; 582 yBuf.position(yRowStride * i); 583 yBuf.get(yFullRow); 584 uBuf.position(uRowStride * halfH); 585 uBuf.get(uFullRow); 586 vBuf.position(vRowStride * halfH); 587 vBuf.get(vFullRow); 588 for (int j = 0; j < width; j++) { 589 int halfW = j / 2; 590 yuvPixel[0] = yFullRow[yPixStride * j]; 591 yuvPixel[1] = uFullRow[uPixStride * halfW]; 592 yuvPixel[2] = vFullRow[vPixStride * halfW]; 593 yuvToRgb(yuvPixel, j * BYTES_PER_RGB_PIX, /*out*/finalRow); 594 } 595 buf.put(finalRow); 596 } 597 598 yBuf.rewind(); 599 uBuf.rewind(); 600 vBuf.rewind(); 601 buf.rewind(); 602 return buf; 603 } 604 605 /** 606 * Generate a direct RGB {@link ByteBuffer} from a {@link Bitmap}. 607 */ 608 private static ByteBuffer convertToRGB(Bitmap argbBitmap) { 609 // TODO: Optimize this. 610 int width = argbBitmap.getWidth(); 611 int height = argbBitmap.getHeight(); 612 ByteBuffer buf = ByteBuffer.allocateDirect(BYTES_PER_RGB_PIX * width * height); 613 614 int[] pixelRow = new int[width]; 615 byte[] finalRow = new byte[BYTES_PER_RGB_PIX * width]; 616 for (int i = 0; i < height; i++) { 617 argbBitmap.getPixels(pixelRow, /*offset*/0, /*stride*/width, /*x*/0, /*y*/i, 618 /*width*/width, /*height*/1); 619 for (int j = 0; j < width; j++) { 620 colorToRgb(pixelRow[j], j * BYTES_PER_RGB_PIX, /*out*/finalRow); 621 } 622 buf.put(finalRow); 623 } 624 625 buf.rewind(); 626 return buf; 627 } 628 629 /** 630 * Convert coordinate to EXIF GPS tag format. 631 */ 632 private static int[] toExifLatLong(double value) { 633 // convert to the format dd/1 mm/1 ssss/100 634 value = Math.abs(value); 635 int degrees = (int) value; 636 value = (value - degrees) * 60; 637 int minutes = (int) value; 638 value = (value - minutes) * 6000; 639 int seconds = (int) value; 640 return new int[] { degrees, 1, minutes, 1, seconds, 100 }; 641 } 642 643 /** 644 * This field is used by native code, do not access or modify. 645 */ 646 private long mNativeContext; 647 648 private static native void nativeClassInit(); 649 650 private synchronized native void nativeInit(CameraMetadataNative nativeCharacteristics, 651 CameraMetadataNative nativeResult, 652 String captureTime); 653 654 private synchronized native void nativeDestroy(); 655 656 private synchronized native void nativeSetOrientation(int orientation); 657 658 private synchronized native void nativeSetDescription(String description); 659 660 private synchronized native void nativeSetGpsTags(int[] latTag, String latRef, int[] longTag, 661 String longRef, String dateTag, 662 int[] timeTag); 663 664 private synchronized native void nativeSetThumbnail(ByteBuffer buffer, int width, int height); 665 666 private synchronized native void nativeWriteImage(OutputStream out, int width, int height, 667 ByteBuffer rawBuffer, int rowStride, 668 int pixStride, long offset, boolean isDirect) 669 throws IOException; 670 671 private synchronized native void nativeWriteInputStream(OutputStream out, InputStream rawStream, 672 int width, int height, long offset) 673 throws IOException; 674 675 static { 676 nativeClassInit(); 677 } 678 } 679