680 lines
28 KiB
Java
680 lines
28 KiB
Java
![]() |
/*
|
||
|
* Copyright 2014 The Android Open Source Project
|
||
|
*
|
||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
* you may not use this file except in compliance with the License.
|
||
|
* You may obtain a copy of the License at
|
||
|
*
|
||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||
|
*
|
||
|
* Unless required by applicable law or agreed to in writing, software
|
||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
* See the License for the specific language governing permissions and
|
||
|
* limitations under the License.
|
||
|
*/
|
||
|
|
||
|
package android.hardware.camera2;
|
||
|
|
||
|
import android.annotation.IntRange;
|
||
|
import android.annotation.NonNull;
|
||
|
import android.graphics.Bitmap;
|
||
|
import android.graphics.Color;
|
||
|
import android.graphics.ImageFormat;
|
||
|
import android.hardware.camera2.impl.CameraMetadataNative;
|
||
|
import android.location.Location;
|
||
|
import android.media.ExifInterface;
|
||
|
import android.media.Image;
|
||
|
import android.os.SystemClock;
|
||
|
import android.util.Log;
|
||
|
import android.util.Size;
|
||
|
|
||
|
import java.io.IOException;
|
||
|
import java.io.InputStream;
|
||
|
import java.io.OutputStream;
|
||
|
import java.nio.ByteBuffer;
|
||
|
import java.text.DateFormat;
|
||
|
import java.text.SimpleDateFormat;
|
||
|
import java.util.Calendar;
|
||
|
import java.util.Locale;
|
||
|
import java.util.TimeZone;
|
||
|
|
||
|
/**
|
||
|
* The {@link DngCreator} class provides functions to write raw pixel data as a DNG file.
|
||
|
*
|
||
|
* <p>
|
||
|
* This class is designed to be used with the {@link android.graphics.ImageFormat#RAW_SENSOR}
|
||
|
* buffers available from {@link android.hardware.camera2.CameraDevice}, or with Bayer-type raw
|
||
|
* pixel data that is otherwise generated by an application. The DNG metadata tags will be
|
||
|
* generated from a {@link android.hardware.camera2.CaptureResult} object or set directly.
|
||
|
* </p>
|
||
|
*
|
||
|
* <p>
|
||
|
* The DNG file format is a cross-platform file format that is used to store pixel data from
|
||
|
* camera sensors with minimal pre-processing applied. DNG files allow for pixel data to be
|
||
|
* defined in a user-defined colorspace, and have associated metadata that allow for this
|
||
|
* pixel data to be converted to the standard CIE XYZ colorspace during post-processing.
|
||
|
* </p>
|
||
|
*
|
||
|
* <p>
|
||
|
* For more information on the DNG file format and associated metadata, please refer to the
|
||
|
* <a href=
|
||
|
* "https://wwwimages2.adobe.com/content/dam/Adobe/en/products/photoshop/pdfs/dng_spec_1.4.0.0.pdf">
|
||
|
* Adobe DNG 1.4.0.0 specification</a>.
|
||
|
* </p>
|
||
|
*/
|
||
|
public final class DngCreator implements AutoCloseable {
|
||
|
|
||
|
private static final String TAG = "DngCreator";
|
||
|
/**
|
||
|
* Create a new DNG object.
|
||
|
*
|
||
|
* <p>
|
||
|
* It is not necessary to call any set methods to write a well-formatted DNG file.
|
||
|
* </p>
|
||
|
* <p>
|
||
|
* DNG metadata tags will be generated from the corresponding parameters in the
|
||
|
* {@link android.hardware.camera2.CaptureResult} object.
|
||
|
* </p>
|
||
|
* <p>
|
||
|
* For best quality DNG files, it is strongly recommended that lens shading map output is
|
||
|
* enabled if supported. See {@link CaptureRequest#STATISTICS_LENS_SHADING_MAP_MODE}.
|
||
|
* </p>
|
||
|
* @param characteristics an object containing the static
|
||
|
* {@link android.hardware.camera2.CameraCharacteristics}.
|
||
|
* @param metadata a metadata object to generate tags from.
|
||
|
*/
|
||
|
public DngCreator(@NonNull CameraCharacteristics characteristics,
|
||
|
@NonNull CaptureResult metadata) {
|
||
|
if (characteristics == null || metadata == null) {
|
||
|
throw new IllegalArgumentException("Null argument to DngCreator constructor");
|
||
|
}
|
||
|
|
||
|
// Find current time in milliseconds since 1970
|
||
|
long currentTime = System.currentTimeMillis();
|
||
|
// Assume that sensor timestamp has that timebase to start
|
||
|
long timeOffset = 0;
|
||
|
|
||
|
int timestampSource = characteristics.get(
|
||
|
CameraCharacteristics.SENSOR_INFO_TIMESTAMP_SOURCE);
|
||
|
|
||
|
if (timestampSource == CameraCharacteristics.SENSOR_INFO_TIMESTAMP_SOURCE_REALTIME) {
|
||
|
// This means the same timebase as SystemClock.elapsedRealtime(),
|
||
|
// which is CLOCK_BOOTTIME
|
||
|
timeOffset = currentTime - SystemClock.elapsedRealtime();
|
||
|
} else if (timestampSource == CameraCharacteristics.SENSOR_INFO_TIMESTAMP_SOURCE_UNKNOWN) {
|
||
|
// This means the same timebase as System.currentTimeMillis(),
|
||
|
// which is CLOCK_MONOTONIC
|
||
|
timeOffset = currentTime - SystemClock.uptimeMillis();
|
||
|
} else {
|
||
|
// Unexpected time source - treat as CLOCK_MONOTONIC
|
||
|
Log.w(TAG, "Sensor timestamp source is unexpected: " + timestampSource);
|
||
|
timeOffset = currentTime - SystemClock.uptimeMillis();
|
||
|
}
|
||
|
|
||
|
// Find capture time (nanos since boot)
|
||
|
Long timestamp = metadata.get(CaptureResult.SENSOR_TIMESTAMP);
|
||
|
long captureTime = currentTime;
|
||
|
if (timestamp != null) {
|
||
|
captureTime = timestamp / 1000000 + timeOffset;
|
||
|
}
|
||
|
|
||
|
// Create this fresh each time since the time zone may change while a long-running application
|
||
|
// is active.
|
||
|
final DateFormat dateTimeStampFormat =
|
||
|
new SimpleDateFormat(TIFF_DATETIME_FORMAT, Locale.US);
|
||
|
dateTimeStampFormat.setTimeZone(TimeZone.getDefault());
|
||
|
|
||
|
// Format for metadata
|
||
|
String formattedCaptureTime = dateTimeStampFormat.format(captureTime);
|
||
|
|
||
|
nativeInit(characteristics.getNativeCopy(), metadata.getNativeCopy(),
|
||
|
formattedCaptureTime);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Set the orientation value to write.
|
||
|
*
|
||
|
* <p>
|
||
|
* This will be written as the TIFF "Orientation" tag {@code (0x0112)}.
|
||
|
* Calling this will override any prior settings for this tag.
|
||
|
* </p>
|
||
|
*
|
||
|
* @param orientation the orientation value to set, one of:
|
||
|
* <ul>
|
||
|
* <li>{@link android.media.ExifInterface#ORIENTATION_NORMAL}</li>
|
||
|
* <li>{@link android.media.ExifInterface#ORIENTATION_FLIP_HORIZONTAL}</li>
|
||
|
* <li>{@link android.media.ExifInterface#ORIENTATION_ROTATE_180}</li>
|
||
|
* <li>{@link android.media.ExifInterface#ORIENTATION_FLIP_VERTICAL}</li>
|
||
|
* <li>{@link android.media.ExifInterface#ORIENTATION_TRANSPOSE}</li>
|
||
|
* <li>{@link android.media.ExifInterface#ORIENTATION_ROTATE_90}</li>
|
||
|
* <li>{@link android.media.ExifInterface#ORIENTATION_TRANSVERSE}</li>
|
||
|
* <li>{@link android.media.ExifInterface#ORIENTATION_ROTATE_270}</li>
|
||
|
* </ul>
|
||
|
* @return this {@link #DngCreator} object.
|
||
|
*/
|
||
|
@NonNull
|
||
|
public DngCreator setOrientation(int orientation) {
|
||
|
if (orientation < ExifInterface.ORIENTATION_UNDEFINED ||
|
||
|
orientation > ExifInterface.ORIENTATION_ROTATE_270) {
|
||
|
throw new IllegalArgumentException("Orientation " + orientation +
|
||
|
" is not a valid EXIF orientation value");
|
||
|
}
|
||
|
// ExifInterface and TIFF/EP spec differ on definition of
|
||
|
// "Unknown" orientation; other values map directly
|
||
|
if (orientation == ExifInterface.ORIENTATION_UNDEFINED) {
|
||
|
orientation = TAG_ORIENTATION_UNKNOWN;
|
||
|
}
|
||
|
nativeSetOrientation(orientation);
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Set the thumbnail image.
|
||
|
*
|
||
|
* <p>
|
||
|
* Pixel data will be converted to a Baseline TIFF RGB image, with 8 bits per color channel.
|
||
|
* The alpha channel will be discarded. Thumbnail images with a dimension larger than
|
||
|
* {@link #MAX_THUMBNAIL_DIMENSION} will be rejected.
|
||
|
* </p>
|
||
|
*
|
||
|
* @param pixels a {@link android.graphics.Bitmap} of pixel data.
|
||
|
* @return this {@link #DngCreator} object.
|
||
|
* @throws java.lang.IllegalArgumentException if the given thumbnail image has a dimension
|
||
|
* larger than {@link #MAX_THUMBNAIL_DIMENSION}.
|
||
|
*/
|
||
|
@NonNull
|
||
|
public DngCreator setThumbnail(@NonNull Bitmap pixels) {
|
||
|
if (pixels == null) {
|
||
|
throw new IllegalArgumentException("Null argument to setThumbnail");
|
||
|
}
|
||
|
|
||
|
int width = pixels.getWidth();
|
||
|
int height = pixels.getHeight();
|
||
|
|
||
|
if (width > MAX_THUMBNAIL_DIMENSION || height > MAX_THUMBNAIL_DIMENSION) {
|
||
|
throw new IllegalArgumentException("Thumbnail dimensions width,height (" + width +
|
||
|
"," + height + ") too large, dimensions must be smaller than " +
|
||
|
MAX_THUMBNAIL_DIMENSION);
|
||
|
}
|
||
|
|
||
|
ByteBuffer rgbBuffer = convertToRGB(pixels);
|
||
|
nativeSetThumbnail(rgbBuffer, width, height);
|
||
|
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Set the thumbnail image.
|
||
|
*
|
||
|
* <p>
|
||
|
* Pixel data is interpreted as a {@link android.graphics.ImageFormat#YUV_420_888} image.
|
||
|
* Thumbnail images with a dimension larger than {@link #MAX_THUMBNAIL_DIMENSION} will be
|
||
|
* rejected.
|
||
|
* </p>
|
||
|
*
|
||
|
* @param pixels an {@link android.media.Image} object with the format
|
||
|
* {@link android.graphics.ImageFormat#YUV_420_888}.
|
||
|
* @return this {@link #DngCreator} object.
|
||
|
* @throws java.lang.IllegalArgumentException if the given thumbnail image has a dimension
|
||
|
* larger than {@link #MAX_THUMBNAIL_DIMENSION}.
|
||
|
*/
|
||
|
@NonNull
|
||
|
public DngCreator setThumbnail(@NonNull Image pixels) {
|
||
|
if (pixels == null) {
|
||
|
throw new IllegalArgumentException("Null argument to setThumbnail");
|
||
|
}
|
||
|
|
||
|
int format = pixels.getFormat();
|
||
|
if (format != ImageFormat.YUV_420_888) {
|
||
|
throw new IllegalArgumentException("Unsupported Image format " + format);
|
||
|
}
|
||
|
|
||
|
int width = pixels.getWidth();
|
||
|
int height = pixels.getHeight();
|
||
|
|
||
|
if (width > MAX_THUMBNAIL_DIMENSION || height > MAX_THUMBNAIL_DIMENSION) {
|
||
|
throw new IllegalArgumentException("Thumbnail dimensions width,height (" + width +
|
||
|
"," + height + ") too large, dimensions must be smaller than " +
|
||
|
MAX_THUMBNAIL_DIMENSION);
|
||
|
}
|
||
|
|
||
|
ByteBuffer rgbBuffer = convertToRGB(pixels);
|
||
|
nativeSetThumbnail(rgbBuffer, width, height);
|
||
|
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Set image location metadata.
|
||
|
*
|
||
|
* <p>
|
||
|
* The given location object must contain at least a valid time, latitude, and longitude
|
||
|
* (equivalent to the values returned by {@link android.location.Location#getTime()},
|
||
|
* {@link android.location.Location#getLatitude()}, and
|
||
|
* {@link android.location.Location#getLongitude()} methods).
|
||
|
* </p>
|
||
|
*
|
||
|
* @param location an {@link android.location.Location} object to set.
|
||
|
* @return this {@link #DngCreator} object.
|
||
|
*
|
||
|
* @throws java.lang.IllegalArgumentException if the given location object doesn't
|
||
|
* contain enough information to set location metadata.
|
||
|
*/
|
||
|
@NonNull
|
||
|
public DngCreator setLocation(@NonNull Location location) {
|
||
|
if (location == null) {
|
||
|
throw new IllegalArgumentException("Null location passed to setLocation");
|
||
|
}
|
||
|
double latitude = location.getLatitude();
|
||
|
double longitude = location.getLongitude();
|
||
|
long time = location.getTime();
|
||
|
|
||
|
int[] latTag = toExifLatLong(latitude);
|
||
|
int[] longTag = toExifLatLong(longitude);
|
||
|
String latRef = latitude >= 0 ? GPS_LAT_REF_NORTH : GPS_LAT_REF_SOUTH;
|
||
|
String longRef = longitude >= 0 ? GPS_LONG_REF_EAST : GPS_LONG_REF_WEST;
|
||
|
|
||
|
String dateTag = sExifGPSDateStamp.format(time);
|
||
|
mGPSTimeStampCalendar.setTimeInMillis(time);
|
||
|
int[] timeTag = new int[] { mGPSTimeStampCalendar.get(Calendar.HOUR_OF_DAY), 1,
|
||
|
mGPSTimeStampCalendar.get(Calendar.MINUTE), 1,
|
||
|
mGPSTimeStampCalendar.get(Calendar.SECOND), 1 };
|
||
|
nativeSetGpsTags(latTag, latRef, longTag, longRef, dateTag, timeTag);
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Set the user description string to write.
|
||
|
*
|
||
|
* <p>
|
||
|
* This is equivalent to setting the TIFF "ImageDescription" tag {@code (0x010E)}.
|
||
|
* </p>
|
||
|
*
|
||
|
* @param description the user description string.
|
||
|
* @return this {@link #DngCreator} object.
|
||
|
*/
|
||
|
@NonNull
|
||
|
public DngCreator setDescription(@NonNull String description) {
|
||
|
if (description == null) {
|
||
|
throw new IllegalArgumentException("Null description passed to setDescription.");
|
||
|
}
|
||
|
nativeSetDescription(description);
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Write the {@link android.graphics.ImageFormat#RAW_SENSOR} pixel data to a DNG file with
|
||
|
* the currently configured metadata.
|
||
|
*
|
||
|
* <p>
|
||
|
* Raw pixel data must have 16 bits per pixel, and the input must contain at least
|
||
|
* {@code offset + 2 * width * height)} bytes. The width and height of
|
||
|
* the input are taken from the width and height set in the {@link DngCreator} metadata tags,
|
||
|
* and will typically be equal to the width and height of
|
||
|
* {@link CameraCharacteristics#SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE}. Prior to
|
||
|
* API level 23, this was always the same as
|
||
|
* {@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE}.
|
||
|
* The pixel layout in the input is determined from the reported color filter arrangement (CFA)
|
||
|
* set in {@link CameraCharacteristics#SENSOR_INFO_COLOR_FILTER_ARRANGEMENT}. If insufficient
|
||
|
* metadata is available to write a well-formatted DNG file, an
|
||
|
* {@link java.lang.IllegalStateException} will be thrown.
|
||
|
* </p>
|
||
|
*
|
||
|
* @param dngOutput an {@link java.io.OutputStream} to write the DNG file to.
|
||
|
* @param size the {@link Size} of the image to write, in pixels.
|
||
|
* @param pixels an {@link java.io.InputStream} of pixel data to write.
|
||
|
* @param offset the offset of the raw image in bytes. This indicates how many bytes will
|
||
|
* be skipped in the input before any pixel data is read.
|
||
|
*
|
||
|
* @throws IOException if an error was encountered in the input or output stream.
|
||
|
* @throws java.lang.IllegalStateException if not enough metadata information has been
|
||
|
* set to write a well-formatted DNG file.
|
||
|
* @throws java.lang.IllegalArgumentException if the size passed in does not match the
|
||
|
*/
|
||
|
public void writeInputStream(@NonNull OutputStream dngOutput, @NonNull Size size,
|
||
|
@NonNull InputStream pixels, @IntRange(from=0) long offset) throws IOException {
|
||
|
if (dngOutput == null) {
|
||
|
throw new IllegalArgumentException("Null dngOutput passed to writeInputStream");
|
||
|
} else if (size == null) {
|
||
|
throw new IllegalArgumentException("Null size passed to writeInputStream");
|
||
|
} else if (pixels == null) {
|
||
|
throw new IllegalArgumentException("Null pixels passed to writeInputStream");
|
||
|
} else if (offset < 0) {
|
||
|
throw new IllegalArgumentException("Negative offset passed to writeInputStream");
|
||
|
}
|
||
|
|
||
|
int width = size.getWidth();
|
||
|
int height = size.getHeight();
|
||
|
if (width <= 0 || height <= 0) {
|
||
|
throw new IllegalArgumentException("Size with invalid width, height: (" + width + "," +
|
||
|
height + ") passed to writeInputStream");
|
||
|
}
|
||
|
nativeWriteInputStream(dngOutput, pixels, width, height, offset);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Write the {@link android.graphics.ImageFormat#RAW_SENSOR} pixel data to a DNG file with
|
||
|
* the currently configured metadata.
|
||
|
*
|
||
|
* <p>
|
||
|
* Raw pixel data must have 16 bits per pixel, and the input must contain at least
|
||
|
* {@code offset + 2 * width * height)} bytes. The width and height of
|
||
|
* the input are taken from the width and height set in the {@link DngCreator} metadata tags,
|
||
|
* and will typically be equal to the width and height of
|
||
|
* {@link CameraCharacteristics#SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE}. Prior to
|
||
|
* API level 23, this was always the same as
|
||
|
* {@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE}.
|
||
|
* The pixel layout in the input is determined from the reported color filter arrangement (CFA)
|
||
|
* set in {@link CameraCharacteristics#SENSOR_INFO_COLOR_FILTER_ARRANGEMENT}. If insufficient
|
||
|
* metadata is available to write a well-formatted DNG file, an
|
||
|
* {@link java.lang.IllegalStateException} will be thrown.
|
||
|
* </p>
|
||
|
*
|
||
|
* <p>
|
||
|
* Any mark or limit set on this {@link ByteBuffer} is ignored, and will be cleared by this
|
||
|
* method.
|
||
|
* </p>
|
||
|
*
|
||
|
* @param dngOutput an {@link java.io.OutputStream} to write the DNG file to.
|
||
|
* @param size the {@link Size} of the image to write, in pixels.
|
||
|
* @param pixels an {@link java.nio.ByteBuffer} of pixel data to write.
|
||
|
* @param offset the offset of the raw image in bytes. This indicates how many bytes will
|
||
|
* be skipped in the input before any pixel data is read.
|
||
|
*
|
||
|
* @throws IOException if an error was encountered in the input or output stream.
|
||
|
* @throws java.lang.IllegalStateException if not enough metadata information has been
|
||
|
* set to write a well-formatted DNG file.
|
||
|
*/
|
||
|
public void writeByteBuffer(@NonNull OutputStream dngOutput, @NonNull Size size,
|
||
|
@NonNull ByteBuffer pixels, @IntRange(from=0) long offset)
|
||
|
throws IOException {
|
||
|
if (dngOutput == null) {
|
||
|
throw new IllegalArgumentException("Null dngOutput passed to writeByteBuffer");
|
||
|
} else if (size == null) {
|
||
|
throw new IllegalArgumentException("Null size passed to writeByteBuffer");
|
||
|
} else if (pixels == null) {
|
||
|
throw new IllegalArgumentException("Null pixels passed to writeByteBuffer");
|
||
|
} else if (offset < 0) {
|
||
|
throw new IllegalArgumentException("Negative offset passed to writeByteBuffer");
|
||
|
}
|
||
|
|
||
|
int width = size.getWidth();
|
||
|
int height = size.getHeight();
|
||
|
|
||
|
writeByteBuffer(width, height, pixels, dngOutput, DEFAULT_PIXEL_STRIDE,
|
||
|
width * DEFAULT_PIXEL_STRIDE, offset);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Write the pixel data to a DNG file with the currently configured metadata.
|
||
|
*
|
||
|
* <p>
|
||
|
* For this method to succeed, the {@link android.media.Image} input must contain
|
||
|
* {@link android.graphics.ImageFormat#RAW_SENSOR} pixel data, otherwise an
|
||
|
* {@link java.lang.IllegalArgumentException} will be thrown.
|
||
|
* </p>
|
||
|
*
|
||
|
* @param dngOutput an {@link java.io.OutputStream} to write the DNG file to.
|
||
|
* @param pixels an {@link android.media.Image} to write.
|
||
|
*
|
||
|
* @throws java.io.IOException if an error was encountered in the output stream.
|
||
|
* @throws java.lang.IllegalArgumentException if an image with an unsupported format was used.
|
||
|
* @throws java.lang.IllegalStateException if not enough metadata information has been
|
||
|
* set to write a well-formatted DNG file.
|
||
|
*/
|
||
|
public void writeImage(@NonNull OutputStream dngOutput, @NonNull Image pixels)
|
||
|
throws IOException {
|
||
|
if (dngOutput == null) {
|
||
|
throw new IllegalArgumentException("Null dngOutput to writeImage");
|
||
|
} else if (pixels == null) {
|
||
|
throw new IllegalArgumentException("Null pixels to writeImage");
|
||
|
}
|
||
|
|
||
|
int format = pixels.getFormat();
|
||
|
if (format != ImageFormat.RAW_SENSOR) {
|
||
|
throw new IllegalArgumentException("Unsupported image format " + format);
|
||
|
}
|
||
|
|
||
|
Image.Plane[] planes = pixels.getPlanes();
|
||
|
if (planes == null || planes.length <= 0) {
|
||
|
throw new IllegalArgumentException("Image with no planes passed to writeImage");
|
||
|
}
|
||
|
|
||
|
ByteBuffer buf = planes[0].getBuffer();
|
||
|
writeByteBuffer(pixels.getWidth(), pixels.getHeight(), buf, dngOutput,
|
||
|
planes[0].getPixelStride(), planes[0].getRowStride(), 0);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void close() {
|
||
|
nativeDestroy();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Max width or height dimension for thumbnails.
|
||
|
*/
|
||
|
public static final int MAX_THUMBNAIL_DIMENSION = 256; // max pixel dimension for TIFF/EP
|
||
|
|
||
|
@Override
|
||
|
protected void finalize() throws Throwable {
|
||
|
try {
|
||
|
close();
|
||
|
} finally {
|
||
|
super.finalize();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static final String GPS_LAT_REF_NORTH = "N";
|
||
|
private static final String GPS_LAT_REF_SOUTH = "S";
|
||
|
private static final String GPS_LONG_REF_EAST = "E";
|
||
|
private static final String GPS_LONG_REF_WEST = "W";
|
||
|
|
||
|
private static final String GPS_DATE_FORMAT_STR = "yyyy:MM:dd";
|
||
|
private static final String TIFF_DATETIME_FORMAT = "yyyy:MM:dd HH:mm:ss";
|
||
|
private static final DateFormat sExifGPSDateStamp =
|
||
|
new SimpleDateFormat(GPS_DATE_FORMAT_STR, Locale.US);
|
||
|
private final Calendar mGPSTimeStampCalendar = Calendar
|
||
|
.getInstance(TimeZone.getTimeZone("UTC"));
|
||
|
|
||
|
static {
|
||
|
sExifGPSDateStamp.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||
|
}
|
||
|
|
||
|
private static final int DEFAULT_PIXEL_STRIDE = 2; // bytes per sample
|
||
|
private static final int BYTES_PER_RGB_PIX = 3; // bytes per pixel
|
||
|
|
||
|
// TIFF tag values needed to map between public API and TIFF spec
|
||
|
private static final int TAG_ORIENTATION_UNKNOWN = 9;
|
||
|
|
||
|
/**
|
||
|
* Offset, rowStride, and pixelStride are given in bytes. Height and width are given in pixels.
|
||
|
*/
|
||
|
private void writeByteBuffer(int width, int height, ByteBuffer pixels, OutputStream dngOutput,
|
||
|
int pixelStride, int rowStride, long offset) throws IOException {
|
||
|
if (width <= 0 || height <= 0) {
|
||
|
throw new IllegalArgumentException("Image with invalid width, height: (" + width + "," +
|
||
|
height + ") passed to write");
|
||
|
}
|
||
|
long capacity = pixels.capacity();
|
||
|
long totalSize = ((long) rowStride) * height + offset;
|
||
|
if (capacity < totalSize) {
|
||
|
throw new IllegalArgumentException("Image size " + capacity +
|
||
|
" is too small (must be larger than " + totalSize + ")");
|
||
|
}
|
||
|
int minRowStride = pixelStride * width;
|
||
|
if (minRowStride > rowStride) {
|
||
|
throw new IllegalArgumentException("Invalid image pixel stride, row byte width " +
|
||
|
minRowStride + " is too large, expecting " + rowStride);
|
||
|
}
|
||
|
pixels.clear(); // Reset mark and limit
|
||
|
nativeWriteImage(dngOutput, width, height, pixels, rowStride, pixelStride, offset,
|
||
|
pixels.isDirect());
|
||
|
pixels.clear();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Convert a single YUV pixel to RGB.
|
||
|
*/
|
||
|
private static void yuvToRgb(byte[] yuvData, int outOffset, /*out*/byte[] rgbOut) {
|
||
|
final int COLOR_MAX = 255;
|
||
|
|
||
|
float y = yuvData[0] & 0xFF; // Y channel
|
||
|
float cb = yuvData[1] & 0xFF; // U channel
|
||
|
float cr = yuvData[2] & 0xFF; // V channel
|
||
|
|
||
|
// convert YUV -> RGB (from JFIF's "Conversion to and from RGB" section)
|
||
|
float r = y + 1.402f * (cr - 128);
|
||
|
float g = y - 0.34414f * (cb - 128) - 0.71414f * (cr - 128);
|
||
|
float b = y + 1.772f * (cb - 128);
|
||
|
|
||
|
// clamp to [0,255]
|
||
|
rgbOut[outOffset] = (byte) Math.max(0, Math.min(COLOR_MAX, r));
|
||
|
rgbOut[outOffset + 1] = (byte) Math.max(0, Math.min(COLOR_MAX, g));
|
||
|
rgbOut[outOffset + 2] = (byte) Math.max(0, Math.min(COLOR_MAX, b));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Convert a single {@link Color} pixel to RGB.
|
||
|
*/
|
||
|
private static void colorToRgb(int color, int outOffset, /*out*/byte[] rgbOut) {
|
||
|
rgbOut[outOffset] = (byte) Color.red(color);
|
||
|
rgbOut[outOffset + 1] = (byte) Color.green(color);
|
||
|
rgbOut[outOffset + 2] = (byte) Color.blue(color);
|
||
|
// Discards Alpha
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Generate a direct RGB {@link ByteBuffer} from a YUV420_888 {@link Image}.
|
||
|
*/
|
||
|
private static ByteBuffer convertToRGB(Image yuvImage) {
|
||
|
// TODO: Optimize this with renderscript intrinsic.
|
||
|
int width = yuvImage.getWidth();
|
||
|
int height = yuvImage.getHeight();
|
||
|
ByteBuffer buf = ByteBuffer.allocateDirect(BYTES_PER_RGB_PIX * width * height);
|
||
|
|
||
|
Image.Plane yPlane = yuvImage.getPlanes()[0];
|
||
|
Image.Plane uPlane = yuvImage.getPlanes()[1];
|
||
|
Image.Plane vPlane = yuvImage.getPlanes()[2];
|
||
|
|
||
|
ByteBuffer yBuf = yPlane.getBuffer();
|
||
|
ByteBuffer uBuf = uPlane.getBuffer();
|
||
|
ByteBuffer vBuf = vPlane.getBuffer();
|
||
|
|
||
|
yBuf.rewind();
|
||
|
uBuf.rewind();
|
||
|
vBuf.rewind();
|
||
|
|
||
|
int yRowStride = yPlane.getRowStride();
|
||
|
int vRowStride = vPlane.getRowStride();
|
||
|
int uRowStride = uPlane.getRowStride();
|
||
|
|
||
|
int yPixStride = yPlane.getPixelStride();
|
||
|
int vPixStride = vPlane.getPixelStride();
|
||
|
int uPixStride = uPlane.getPixelStride();
|
||
|
|
||
|
byte[] yuvPixel = { 0, 0, 0 };
|
||
|
byte[] yFullRow = new byte[yPixStride * (width - 1) + 1];
|
||
|
byte[] uFullRow = new byte[uPixStride * (width / 2 - 1) + 1];
|
||
|
byte[] vFullRow = new byte[vPixStride * (width / 2 - 1) + 1];
|
||
|
byte[] finalRow = new byte[BYTES_PER_RGB_PIX * width];
|
||
|
for (int i = 0; i < height; i++) {
|
||
|
int halfH = i / 2;
|
||
|
yBuf.position(yRowStride * i);
|
||
|
yBuf.get(yFullRow);
|
||
|
uBuf.position(uRowStride * halfH);
|
||
|
uBuf.get(uFullRow);
|
||
|
vBuf.position(vRowStride * halfH);
|
||
|
vBuf.get(vFullRow);
|
||
|
for (int j = 0; j < width; j++) {
|
||
|
int halfW = j / 2;
|
||
|
yuvPixel[0] = yFullRow[yPixStride * j];
|
||
|
yuvPixel[1] = uFullRow[uPixStride * halfW];
|
||
|
yuvPixel[2] = vFullRow[vPixStride * halfW];
|
||
|
yuvToRgb(yuvPixel, j * BYTES_PER_RGB_PIX, /*out*/finalRow);
|
||
|
}
|
||
|
buf.put(finalRow);
|
||
|
}
|
||
|
|
||
|
yBuf.rewind();
|
||
|
uBuf.rewind();
|
||
|
vBuf.rewind();
|
||
|
buf.rewind();
|
||
|
return buf;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Generate a direct RGB {@link ByteBuffer} from a {@link Bitmap}.
|
||
|
*/
|
||
|
private static ByteBuffer convertToRGB(Bitmap argbBitmap) {
|
||
|
// TODO: Optimize this.
|
||
|
int width = argbBitmap.getWidth();
|
||
|
int height = argbBitmap.getHeight();
|
||
|
ByteBuffer buf = ByteBuffer.allocateDirect(BYTES_PER_RGB_PIX * width * height);
|
||
|
|
||
|
int[] pixelRow = new int[width];
|
||
|
byte[] finalRow = new byte[BYTES_PER_RGB_PIX * width];
|
||
|
for (int i = 0; i < height; i++) {
|
||
|
argbBitmap.getPixels(pixelRow, /*offset*/0, /*stride*/width, /*x*/0, /*y*/i,
|
||
|
/*width*/width, /*height*/1);
|
||
|
for (int j = 0; j < width; j++) {
|
||
|
colorToRgb(pixelRow[j], j * BYTES_PER_RGB_PIX, /*out*/finalRow);
|
||
|
}
|
||
|
buf.put(finalRow);
|
||
|
}
|
||
|
|
||
|
buf.rewind();
|
||
|
return buf;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Convert coordinate to EXIF GPS tag format.
|
||
|
*/
|
||
|
private static int[] toExifLatLong(double value) {
|
||
|
// convert to the format dd/1 mm/1 ssss/100
|
||
|
value = Math.abs(value);
|
||
|
int degrees = (int) value;
|
||
|
value = (value - degrees) * 60;
|
||
|
int minutes = (int) value;
|
||
|
value = (value - minutes) * 6000;
|
||
|
int seconds = (int) value;
|
||
|
return new int[] { degrees, 1, minutes, 1, seconds, 100 };
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* This field is used by native code, do not access or modify.
|
||
|
*/
|
||
|
private long mNativeContext;
|
||
|
|
||
|
private static native void nativeClassInit();
|
||
|
|
||
|
private synchronized native void nativeInit(CameraMetadataNative nativeCharacteristics,
|
||
|
CameraMetadataNative nativeResult,
|
||
|
String captureTime);
|
||
|
|
||
|
private synchronized native void nativeDestroy();
|
||
|
|
||
|
private synchronized native void nativeSetOrientation(int orientation);
|
||
|
|
||
|
private synchronized native void nativeSetDescription(String description);
|
||
|
|
||
|
private synchronized native void nativeSetGpsTags(int[] latTag, String latRef, int[] longTag,
|
||
|
String longRef, String dateTag,
|
||
|
int[] timeTag);
|
||
|
|
||
|
private synchronized native void nativeSetThumbnail(ByteBuffer buffer, int width, int height);
|
||
|
|
||
|
private synchronized native void nativeWriteImage(OutputStream out, int width, int height,
|
||
|
ByteBuffer rawBuffer, int rowStride,
|
||
|
int pixStride, long offset, boolean isDirect)
|
||
|
throws IOException;
|
||
|
|
||
|
private synchronized native void nativeWriteInputStream(OutputStream out, InputStream rawStream,
|
||
|
int width, int height, long offset)
|
||
|
throws IOException;
|
||
|
|
||
|
static {
|
||
|
nativeClassInit();
|
||
|
}
|
||
|
}
|