• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.graphics.Bitmap;
22 import android.graphics.Color;
23 import android.graphics.ImageFormat;
24 import android.hardware.camera2.impl.CameraMetadataNative;
25 import android.location.Location;
26 import android.media.ExifInterface;
27 import android.media.Image;
28 import android.os.SystemClock;
29 import android.util.Log;
30 import android.util.Size;
31 
32 import java.io.IOException;
33 import java.io.InputStream;
34 import java.io.OutputStream;
35 import java.nio.ByteBuffer;
36 import java.text.DateFormat;
37 import java.text.SimpleDateFormat;
38 import java.util.Calendar;
39 import java.util.Locale;
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, Locale.US);
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 =
476             new SimpleDateFormat(GPS_DATE_FORMAT_STR, Locale.US);
477     private final Calendar mGPSTimeStampCalendar = Calendar
478             .getInstance(TimeZone.getTimeZone("UTC"));
479 
480     static {
481         sExifGPSDateStamp.setTimeZone(TimeZone.getTimeZone("UTC"));
482     }
483 
484     private static final int DEFAULT_PIXEL_STRIDE = 2; // bytes per sample
485     private static final int BYTES_PER_RGB_PIX = 3; // byts per pixel
486 
487     // TIFF tag values needed to map between public API and TIFF spec
488     private static final int TAG_ORIENTATION_UNKNOWN = 9;
489 
490     /**
491      * Offset, rowStride, and pixelStride are given in bytes.  Height and width are given in pixels.
492      */
writeByteBuffer(int width, int height, ByteBuffer pixels, OutputStream dngOutput, int pixelStride, int rowStride, long offset)493     private void writeByteBuffer(int width, int height, ByteBuffer pixels, OutputStream dngOutput,
494                                  int pixelStride, int rowStride, long offset)  throws IOException {
495         if (width <= 0 || height <= 0) {
496             throw new IllegalArgumentException("Image with invalid width, height: (" + width + "," +
497                     height + ") passed to write");
498         }
499         long capacity = pixels.capacity();
500         long totalSize = ((long) rowStride) * height + offset;
501         if (capacity < totalSize) {
502             throw new IllegalArgumentException("Image size " + capacity +
503                     " is too small (must be larger than " + totalSize + ")");
504         }
505         int minRowStride = pixelStride * width;
506         if (minRowStride > rowStride) {
507             throw new IllegalArgumentException("Invalid image pixel stride, row byte width " +
508                     minRowStride + " is too large, expecting " + rowStride);
509         }
510         pixels.clear(); // Reset mark and limit
511         nativeWriteImage(dngOutput, width, height, pixels, rowStride, pixelStride, offset,
512                 pixels.isDirect());
513         pixels.clear();
514     }
515 
516     /**
517      * Convert a single YUV pixel to RGB.
518      */
yuvToRgb(byte[] yuvData, int outOffset, byte[] rgbOut)519     private static void yuvToRgb(byte[] yuvData, int outOffset, /*out*/byte[] rgbOut) {
520         final int COLOR_MAX = 255;
521 
522         float y = yuvData[0] & 0xFF;  // Y channel
523         float cb = yuvData[1] & 0xFF; // U channel
524         float cr = yuvData[2] & 0xFF; // V channel
525 
526         // convert YUV -> RGB (from JFIF's "Conversion to and from RGB" section)
527         float r = y + 1.402f * (cr - 128);
528         float g = y - 0.34414f * (cb - 128) - 0.71414f * (cr - 128);
529         float b = y + 1.772f * (cb - 128);
530 
531         // clamp to [0,255]
532         rgbOut[outOffset] = (byte) Math.max(0, Math.min(COLOR_MAX, r));
533         rgbOut[outOffset + 1] = (byte) Math.max(0, Math.min(COLOR_MAX, g));
534         rgbOut[outOffset + 2] = (byte) Math.max(0, Math.min(COLOR_MAX, b));
535     }
536 
537     /**
538      * Convert a single {@link Color} pixel to RGB.
539      */
colorToRgb(int color, int outOffset, byte[] rgbOut)540     private static void colorToRgb(int color, int outOffset, /*out*/byte[] rgbOut) {
541         rgbOut[outOffset] = (byte) Color.red(color);
542         rgbOut[outOffset + 1] = (byte) Color.green(color);
543         rgbOut[outOffset + 2] = (byte) Color.blue(color);
544         // Discards Alpha
545     }
546 
547     /**
548      * Generate a direct RGB {@link ByteBuffer} from a YUV420_888 {@link Image}.
549      */
convertToRGB(Image yuvImage)550     private static ByteBuffer convertToRGB(Image yuvImage) {
551         // TODO: Optimize this with renderscript intrinsic.
552         int width = yuvImage.getWidth();
553         int height = yuvImage.getHeight();
554         ByteBuffer buf = ByteBuffer.allocateDirect(BYTES_PER_RGB_PIX * width * height);
555 
556         Image.Plane yPlane = yuvImage.getPlanes()[0];
557         Image.Plane uPlane = yuvImage.getPlanes()[1];
558         Image.Plane vPlane = yuvImage.getPlanes()[2];
559 
560         ByteBuffer yBuf = yPlane.getBuffer();
561         ByteBuffer uBuf = uPlane.getBuffer();
562         ByteBuffer vBuf = vPlane.getBuffer();
563 
564         yBuf.rewind();
565         uBuf.rewind();
566         vBuf.rewind();
567 
568         int yRowStride = yPlane.getRowStride();
569         int vRowStride = vPlane.getRowStride();
570         int uRowStride = uPlane.getRowStride();
571 
572         int yPixStride = yPlane.getPixelStride();
573         int vPixStride = vPlane.getPixelStride();
574         int uPixStride = uPlane.getPixelStride();
575 
576         byte[] yuvPixel = { 0, 0, 0 };
577         byte[] yFullRow = new byte[yPixStride * (width - 1) + 1];
578         byte[] uFullRow = new byte[uPixStride * (width / 2 - 1) + 1];
579         byte[] vFullRow = new byte[vPixStride * (width / 2 - 1) + 1];
580         byte[] finalRow = new byte[BYTES_PER_RGB_PIX * width];
581         for (int i = 0; i < height; i++) {
582             int halfH = i / 2;
583             yBuf.position(yRowStride * i);
584             yBuf.get(yFullRow);
585             uBuf.position(uRowStride * halfH);
586             uBuf.get(uFullRow);
587             vBuf.position(vRowStride * halfH);
588             vBuf.get(vFullRow);
589             for (int j = 0; j < width; j++) {
590                 int halfW = j / 2;
591                 yuvPixel[0] = yFullRow[yPixStride * j];
592                 yuvPixel[1] = uFullRow[uPixStride * halfW];
593                 yuvPixel[2] = vFullRow[vPixStride * halfW];
594                 yuvToRgb(yuvPixel, j * BYTES_PER_RGB_PIX, /*out*/finalRow);
595             }
596             buf.put(finalRow);
597         }
598 
599         yBuf.rewind();
600         uBuf.rewind();
601         vBuf.rewind();
602         buf.rewind();
603         return buf;
604     }
605 
606     /**
607      * Generate a direct RGB {@link ByteBuffer} from a {@link Bitmap}.
608      */
convertToRGB(Bitmap argbBitmap)609     private static ByteBuffer convertToRGB(Bitmap argbBitmap) {
610         // TODO: Optimize this.
611         int width = argbBitmap.getWidth();
612         int height = argbBitmap.getHeight();
613         ByteBuffer buf = ByteBuffer.allocateDirect(BYTES_PER_RGB_PIX * width * height);
614 
615         int[] pixelRow = new int[width];
616         byte[] finalRow = new byte[BYTES_PER_RGB_PIX * width];
617         for (int i = 0; i < height; i++) {
618             argbBitmap.getPixels(pixelRow, /*offset*/0, /*stride*/width, /*x*/0, /*y*/i,
619                     /*width*/width, /*height*/1);
620             for (int j = 0; j < width; j++) {
621                 colorToRgb(pixelRow[j], j * BYTES_PER_RGB_PIX, /*out*/finalRow);
622             }
623             buf.put(finalRow);
624         }
625 
626         buf.rewind();
627         return buf;
628     }
629 
630     /**
631      * Convert coordinate to EXIF GPS tag format.
632      */
toExifLatLong(double value)633     private static int[] toExifLatLong(double value) {
634         // convert to the format dd/1 mm/1 ssss/100
635         value = Math.abs(value);
636         int degrees = (int) value;
637         value = (value - degrees) * 60;
638         int minutes = (int) value;
639         value = (value - minutes) * 6000;
640         int seconds = (int) value;
641         return new int[] { degrees, 1, minutes, 1, seconds, 100 };
642     }
643 
644     /**
645      * This field is used by native code, do not access or modify.
646      */
647     private long mNativeContext;
648 
nativeClassInit()649     private static native void nativeClassInit();
650 
nativeInit(CameraMetadataNative nativeCharacteristics, CameraMetadataNative nativeResult, String captureTime)651     private synchronized native void nativeInit(CameraMetadataNative nativeCharacteristics,
652                                                 CameraMetadataNative nativeResult,
653                                                 String captureTime);
654 
nativeDestroy()655     private synchronized native void nativeDestroy();
656 
nativeSetOrientation(int orientation)657     private synchronized native void nativeSetOrientation(int orientation);
658 
nativeSetDescription(String description)659     private synchronized native void nativeSetDescription(String description);
660 
nativeSetGpsTags(int[] latTag, String latRef, int[] longTag, String longRef, String dateTag, int[] timeTag)661     private synchronized native void nativeSetGpsTags(int[] latTag, String latRef, int[] longTag,
662                                                       String longRef, String dateTag,
663                                                       int[] timeTag);
664 
nativeSetThumbnail(ByteBuffer buffer, int width, int height)665     private synchronized native void nativeSetThumbnail(ByteBuffer buffer, int width, int height);
666 
nativeWriteImage(OutputStream out, int width, int height, ByteBuffer rawBuffer, int rowStride, int pixStride, long offset, boolean isDirect)667     private synchronized native void nativeWriteImage(OutputStream out, int width, int height,
668                                                       ByteBuffer rawBuffer, int rowStride,
669                                                       int pixStride, long offset, boolean isDirect)
670                                                       throws IOException;
671 
nativeWriteInputStream(OutputStream out, InputStream rawStream, int width, int height, long offset)672     private synchronized native void nativeWriteInputStream(OutputStream out, InputStream rawStream,
673                                                             int width, int height, long offset)
674                                                             throws IOException;
675 
676     static {
nativeClassInit()677         nativeClassInit();
678     }
679 }
680