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.graphics.Bitmap; 22 import android.graphics.Color; 23 import android.graphics.ImageFormat; 24 import android.hardware.camera2.impl.CameraMetadataNative; 25 import android.location.Location; 26 import android.media.ExifInterface; 27 import android.media.Image; 28 import android.os.SystemClock; 29 import android.util.Log; 30 import android.util.Size; 31 32 import java.io.IOException; 33 import java.io.InputStream; 34 import java.io.OutputStream; 35 import java.nio.ByteBuffer; 36 import java.text.DateFormat; 37 import java.text.SimpleDateFormat; 38 import java.util.Calendar; 39 import java.util.Locale; 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 */ DngCreator(@onNull CameraCharacteristics characteristics, @NonNull CaptureResult metadata)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, Locale.US); 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 setOrientation(int orientation)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 setThumbnail(@onNull Bitmap pixels)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 setThumbnail(@onNull Image pixels)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 setLocation(@onNull Location location)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 setDescription(@onNull String description)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 */ writeInputStream(@onNull OutputStream dngOutput, @NonNull Size size, @NonNull InputStream pixels, @IntRange(from=0) long offset)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 */ writeByteBuffer(@onNull OutputStream dngOutput, @NonNull Size size, @NonNull ByteBuffer pixels, @IntRange(from=0) long offset)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 */ writeImage(@onNull OutputStream dngOutput, @NonNull Image pixels)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 close()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 finalize()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 = 476 new SimpleDateFormat(GPS_DATE_FORMAT_STR, Locale.US); 477 private final Calendar mGPSTimeStampCalendar = Calendar 478 .getInstance(TimeZone.getTimeZone("UTC")); 479 480 static { 481 sExifGPSDateStamp.setTimeZone(TimeZone.getTimeZone("UTC")); 482 } 483 484 private static final int DEFAULT_PIXEL_STRIDE = 2; // bytes per sample 485 private static final int BYTES_PER_RGB_PIX = 3; // byts per pixel 486 487 // TIFF tag values needed to map between public API and TIFF spec 488 private static final int TAG_ORIENTATION_UNKNOWN = 9; 489 490 /** 491 * Offset, rowStride, and pixelStride are given in bytes. Height and width are given in pixels. 492 */ writeByteBuffer(int width, int height, ByteBuffer pixels, OutputStream dngOutput, int pixelStride, int rowStride, long offset)493 private void writeByteBuffer(int width, int height, ByteBuffer pixels, OutputStream dngOutput, 494 int pixelStride, int rowStride, long offset) throws IOException { 495 if (width <= 0 || height <= 0) { 496 throw new IllegalArgumentException("Image with invalid width, height: (" + width + "," + 497 height + ") passed to write"); 498 } 499 long capacity = pixels.capacity(); 500 long totalSize = ((long) rowStride) * height + offset; 501 if (capacity < totalSize) { 502 throw new IllegalArgumentException("Image size " + capacity + 503 " is too small (must be larger than " + totalSize + ")"); 504 } 505 int minRowStride = pixelStride * width; 506 if (minRowStride > rowStride) { 507 throw new IllegalArgumentException("Invalid image pixel stride, row byte width " + 508 minRowStride + " is too large, expecting " + rowStride); 509 } 510 pixels.clear(); // Reset mark and limit 511 nativeWriteImage(dngOutput, width, height, pixels, rowStride, pixelStride, offset, 512 pixels.isDirect()); 513 pixels.clear(); 514 } 515 516 /** 517 * Convert a single YUV pixel to RGB. 518 */ yuvToRgb(byte[] yuvData, int outOffset, byte[] rgbOut)519 private static void yuvToRgb(byte[] yuvData, int outOffset, /*out*/byte[] rgbOut) { 520 final int COLOR_MAX = 255; 521 522 float y = yuvData[0] & 0xFF; // Y channel 523 float cb = yuvData[1] & 0xFF; // U channel 524 float cr = yuvData[2] & 0xFF; // V channel 525 526 // convert YUV -> RGB (from JFIF's "Conversion to and from RGB" section) 527 float r = y + 1.402f * (cr - 128); 528 float g = y - 0.34414f * (cb - 128) - 0.71414f * (cr - 128); 529 float b = y + 1.772f * (cb - 128); 530 531 // clamp to [0,255] 532 rgbOut[outOffset] = (byte) Math.max(0, Math.min(COLOR_MAX, r)); 533 rgbOut[outOffset + 1] = (byte) Math.max(0, Math.min(COLOR_MAX, g)); 534 rgbOut[outOffset + 2] = (byte) Math.max(0, Math.min(COLOR_MAX, b)); 535 } 536 537 /** 538 * Convert a single {@link Color} pixel to RGB. 539 */ colorToRgb(int color, int outOffset, byte[] rgbOut)540 private static void colorToRgb(int color, int outOffset, /*out*/byte[] rgbOut) { 541 rgbOut[outOffset] = (byte) Color.red(color); 542 rgbOut[outOffset + 1] = (byte) Color.green(color); 543 rgbOut[outOffset + 2] = (byte) Color.blue(color); 544 // Discards Alpha 545 } 546 547 /** 548 * Generate a direct RGB {@link ByteBuffer} from a YUV420_888 {@link Image}. 549 */ convertToRGB(Image yuvImage)550 private static ByteBuffer convertToRGB(Image yuvImage) { 551 // TODO: Optimize this with renderscript intrinsic. 552 int width = yuvImage.getWidth(); 553 int height = yuvImage.getHeight(); 554 ByteBuffer buf = ByteBuffer.allocateDirect(BYTES_PER_RGB_PIX * width * height); 555 556 Image.Plane yPlane = yuvImage.getPlanes()[0]; 557 Image.Plane uPlane = yuvImage.getPlanes()[1]; 558 Image.Plane vPlane = yuvImage.getPlanes()[2]; 559 560 ByteBuffer yBuf = yPlane.getBuffer(); 561 ByteBuffer uBuf = uPlane.getBuffer(); 562 ByteBuffer vBuf = vPlane.getBuffer(); 563 564 yBuf.rewind(); 565 uBuf.rewind(); 566 vBuf.rewind(); 567 568 int yRowStride = yPlane.getRowStride(); 569 int vRowStride = vPlane.getRowStride(); 570 int uRowStride = uPlane.getRowStride(); 571 572 int yPixStride = yPlane.getPixelStride(); 573 int vPixStride = vPlane.getPixelStride(); 574 int uPixStride = uPlane.getPixelStride(); 575 576 byte[] yuvPixel = { 0, 0, 0 }; 577 byte[] yFullRow = new byte[yPixStride * (width - 1) + 1]; 578 byte[] uFullRow = new byte[uPixStride * (width / 2 - 1) + 1]; 579 byte[] vFullRow = new byte[vPixStride * (width / 2 - 1) + 1]; 580 byte[] finalRow = new byte[BYTES_PER_RGB_PIX * width]; 581 for (int i = 0; i < height; i++) { 582 int halfH = i / 2; 583 yBuf.position(yRowStride * i); 584 yBuf.get(yFullRow); 585 uBuf.position(uRowStride * halfH); 586 uBuf.get(uFullRow); 587 vBuf.position(vRowStride * halfH); 588 vBuf.get(vFullRow); 589 for (int j = 0; j < width; j++) { 590 int halfW = j / 2; 591 yuvPixel[0] = yFullRow[yPixStride * j]; 592 yuvPixel[1] = uFullRow[uPixStride * halfW]; 593 yuvPixel[2] = vFullRow[vPixStride * halfW]; 594 yuvToRgb(yuvPixel, j * BYTES_PER_RGB_PIX, /*out*/finalRow); 595 } 596 buf.put(finalRow); 597 } 598 599 yBuf.rewind(); 600 uBuf.rewind(); 601 vBuf.rewind(); 602 buf.rewind(); 603 return buf; 604 } 605 606 /** 607 * Generate a direct RGB {@link ByteBuffer} from a {@link Bitmap}. 608 */ convertToRGB(Bitmap argbBitmap)609 private static ByteBuffer convertToRGB(Bitmap argbBitmap) { 610 // TODO: Optimize this. 611 int width = argbBitmap.getWidth(); 612 int height = argbBitmap.getHeight(); 613 ByteBuffer buf = ByteBuffer.allocateDirect(BYTES_PER_RGB_PIX * width * height); 614 615 int[] pixelRow = new int[width]; 616 byte[] finalRow = new byte[BYTES_PER_RGB_PIX * width]; 617 for (int i = 0; i < height; i++) { 618 argbBitmap.getPixels(pixelRow, /*offset*/0, /*stride*/width, /*x*/0, /*y*/i, 619 /*width*/width, /*height*/1); 620 for (int j = 0; j < width; j++) { 621 colorToRgb(pixelRow[j], j * BYTES_PER_RGB_PIX, /*out*/finalRow); 622 } 623 buf.put(finalRow); 624 } 625 626 buf.rewind(); 627 return buf; 628 } 629 630 /** 631 * Convert coordinate to EXIF GPS tag format. 632 */ toExifLatLong(double value)633 private static int[] toExifLatLong(double value) { 634 // convert to the format dd/1 mm/1 ssss/100 635 value = Math.abs(value); 636 int degrees = (int) value; 637 value = (value - degrees) * 60; 638 int minutes = (int) value; 639 value = (value - minutes) * 6000; 640 int seconds = (int) value; 641 return new int[] { degrees, 1, minutes, 1, seconds, 100 }; 642 } 643 644 /** 645 * This field is used by native code, do not access or modify. 646 */ 647 private long mNativeContext; 648 nativeClassInit()649 private static native void nativeClassInit(); 650 nativeInit(CameraMetadataNative nativeCharacteristics, CameraMetadataNative nativeResult, String captureTime)651 private synchronized native void nativeInit(CameraMetadataNative nativeCharacteristics, 652 CameraMetadataNative nativeResult, 653 String captureTime); 654 nativeDestroy()655 private synchronized native void nativeDestroy(); 656 nativeSetOrientation(int orientation)657 private synchronized native void nativeSetOrientation(int orientation); 658 nativeSetDescription(String description)659 private synchronized native void nativeSetDescription(String description); 660 nativeSetGpsTags(int[] latTag, String latRef, int[] longTag, String longRef, String dateTag, int[] timeTag)661 private synchronized native void nativeSetGpsTags(int[] latTag, String latRef, int[] longTag, 662 String longRef, String dateTag, 663 int[] timeTag); 664 nativeSetThumbnail(ByteBuffer buffer, int width, int height)665 private synchronized native void nativeSetThumbnail(ByteBuffer buffer, int width, int height); 666 nativeWriteImage(OutputStream out, int width, int height, ByteBuffer rawBuffer, int rowStride, int pixStride, long offset, boolean isDirect)667 private synchronized native void nativeWriteImage(OutputStream out, int width, int height, 668 ByteBuffer rawBuffer, int rowStride, 669 int pixStride, long offset, boolean isDirect) 670 throws IOException; 671 nativeWriteInputStream(OutputStream out, InputStream rawStream, int width, int height, long offset)672 private synchronized native void nativeWriteInputStream(OutputStream out, InputStream rawStream, 673 int width, int height, long offset) 674 throws IOException; 675 676 static { nativeClassInit()677 nativeClassInit(); 678 } 679 } 680