DngCreator.java revision fdb2112bbc140aab869aac047454cd2f67809df1
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.graphics.Bitmap;
20import android.graphics.Color;
21import android.graphics.ImageFormat;
22import android.hardware.camera2.impl.CameraMetadataNative;
23import android.location.Location;
24import android.media.ExifInterface;
25import android.media.Image;
26import android.os.SystemClock;
27import android.util.Size;
28
29import java.io.IOException;
30import java.io.InputStream;
31import java.io.OutputStream;
32import java.nio.ByteBuffer;
33import java.text.DateFormat;
34import java.text.SimpleDateFormat;
35import java.util.Calendar;
36import 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 */
62public 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 - 1) + 1];
534        byte[] uFullRow = new byte[uPixStride * (width / 2 - 1) + 1];
535        byte[] vFullRow = new byte[vPixStride * (width / 2 - 1) + 1];
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