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 */ 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); 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 = 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 */ writeByteBuffer(int width, int height, ByteBuffer pixels, OutputStream dngOutput, int pixelStride, int rowStride, long offset)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 */ yuvToRgb(byte[] yuvData, int outOffset, byte[] rgbOut)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 */ colorToRgb(int color, int outOffset, byte[] rgbOut)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 */ convertToRGB(Image yuvImage)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 */ convertToRGB(Bitmap argbBitmap)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 */ toExifLatLong(double value)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 nativeClassInit()648 private static native void nativeClassInit(); 649 nativeInit(CameraMetadataNative nativeCharacteristics, CameraMetadataNative nativeResult, String captureTime)650 private synchronized native void nativeInit(CameraMetadataNative nativeCharacteristics, 651 CameraMetadataNative nativeResult, 652 String captureTime); 653 nativeDestroy()654 private synchronized native void nativeDestroy(); 655 nativeSetOrientation(int orientation)656 private synchronized native void nativeSetOrientation(int orientation); 657 nativeSetDescription(String description)658 private synchronized native void nativeSetDescription(String description); 659 nativeSetGpsTags(int[] latTag, String latRef, int[] longTag, String longRef, String dateTag, int[] timeTag)660 private synchronized native void nativeSetGpsTags(int[] latTag, String latRef, int[] longTag, 661 String longRef, String dateTag, 662 int[] timeTag); 663 nativeSetThumbnail(ByteBuffer buffer, int width, int height)664 private synchronized native void nativeSetThumbnail(ByteBuffer buffer, int width, int height); 665 nativeWriteImage(OutputStream out, int width, int height, ByteBuffer rawBuffer, int rowStride, int pixStride, long offset, boolean isDirect)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 nativeWriteInputStream(OutputStream out, InputStream rawStream, int width, int height, long offset)671 private synchronized native void nativeWriteInputStream(OutputStream out, InputStream rawStream, 672 int width, int height, long offset) 673 throws IOException; 674 675 static { nativeClassInit()676 nativeClassInit(); 677 } 678 } 679