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