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