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.annotation.IntRange;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.graphics.Bitmap;
23 import android.graphics.Color;
24 import android.graphics.ImageFormat;
25 import android.hardware.camera2.impl.CameraMetadataNative;
26 import android.location.Location;
27 import android.media.ExifInterface;
28 import android.media.Image;
29 import android.os.SystemClock;
30 import android.util.Log;
31 import android.util.Size;
32 
33 import java.io.IOException;
34 import java.io.InputStream;
35 import java.io.OutputStream;
36 import java.nio.ByteBuffer;
37 import java.text.DateFormat;
38 import java.text.SimpleDateFormat;
39 import java.util.Calendar;
40 import 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  */
66 public 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      */
DngCreator(@onNull CameraCharacteristics characteristics, @NonNull CaptureResult metadata)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
setOrientation(int orientation)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
setThumbnail(@onNull Bitmap pixels)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
setThumbnail(@onNull Image pixels)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
setLocation(@onNull Location location)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
setDescription(@onNull String description)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      */
writeInputStream(@onNull OutputStream dngOutput, @NonNull Size size, @NonNull InputStream pixels, @IntRange(from=0) long offset)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      */
writeByteBuffer(@onNull OutputStream dngOutput, @NonNull Size size, @NonNull ByteBuffer pixels, @IntRange(from=0) long offset)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      */
writeImage(@onNull OutputStream dngOutput, @NonNull Image pixels)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
close()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
finalize()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      */
writeByteBuffer(int width, int height, ByteBuffer pixels, OutputStream dngOutput, int pixelStride, int rowStride, long offset)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      */
yuvToRgb(byte[] yuvData, int outOffset, byte[] rgbOut)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      */
colorToRgb(int color, int outOffset, byte[] rgbOut)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      */
convertToRGB(Image yuvImage)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      */
convertToRGB(Bitmap argbBitmap)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      */
toExifLatLong(double value)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 
nativeClassInit()648     private static native void nativeClassInit();
649 
nativeInit(CameraMetadataNative nativeCharacteristics, CameraMetadataNative nativeResult, String captureTime)650     private synchronized native void nativeInit(CameraMetadataNative nativeCharacteristics,
651                                                 CameraMetadataNative nativeResult,
652                                                 String captureTime);
653 
nativeDestroy()654     private synchronized native void nativeDestroy();
655 
nativeSetOrientation(int orientation)656     private synchronized native void nativeSetOrientation(int orientation);
657 
nativeSetDescription(String description)658     private synchronized native void nativeSetDescription(String description);
659 
nativeSetGpsTags(int[] latTag, String latRef, int[] longTag, String longRef, String dateTag, int[] timeTag)660     private synchronized native void nativeSetGpsTags(int[] latTag, String latRef, int[] longTag,
661                                                       String longRef, String dateTag,
662                                                       int[] timeTag);
663 
nativeSetThumbnail(ByteBuffer buffer, int width, int height)664     private synchronized native void nativeSetThumbnail(ByteBuffer buffer, int width, int height);
665 
nativeWriteImage(OutputStream out, int width, int height, ByteBuffer rawBuffer, int rowStride, int pixStride, long offset, boolean isDirect)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 
nativeWriteInputStream(OutputStream out, InputStream rawStream, int width, int height, long offset)671     private synchronized native void nativeWriteInputStream(OutputStream out, InputStream rawStream,
672                                                             int width, int height, long offset)
673                                                             throws IOException;
674 
675     static {
nativeClassInit()676         nativeClassInit();
677     }
678 }
679