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