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