OSDN Git Service

DNG: Implement remaining interface methods.
authorRuben Brunk <rubenbrunk@google.com>
Thu, 29 May 2014 01:38:42 +0000 (18:38 -0700)
committerRuben Brunk <rubenbrunk@google.com>
Thu, 10 Jul 2014 23:17:35 +0000 (23:17 +0000)
Bug: 15112503
Bug: 16206973
Bug: 15748139

- Adds remaining write methods.
- Adds GPS and description convenience methods.
- Unhides convenience methods.

Change-Id: I9535b21261027f6c06a041c1621de8f865a0ad32

api/current.txt
core/java/android/hardware/camera2/DngCreator.java
core/jni/android_hardware_camera2_DngCreator.cpp

index 9586d8e..ddd9d68 100644 (file)
@@ -12822,7 +12822,15 @@ package android.hardware.camera2 {
   public final class DngCreator implements java.lang.AutoCloseable {
     ctor public DngCreator(android.hardware.camera2.CameraCharacteristics, android.hardware.camera2.CaptureResult);
     method public void close();
+    method public android.hardware.camera2.DngCreator setDescription(java.lang.String);
+    method public android.hardware.camera2.DngCreator setLocation(android.location.Location);
+    method public android.hardware.camera2.DngCreator setOrientation(int);
+    method public android.hardware.camera2.DngCreator setThumbnail(android.graphics.Bitmap);
+    method public android.hardware.camera2.DngCreator setThumbnail(android.media.Image);
+    method public void writeByteBuffer(java.io.OutputStream, android.util.Size, java.nio.ByteBuffer, long) throws java.io.IOException;
     method public void writeImage(java.io.OutputStream, android.media.Image) throws java.io.IOException;
+    method public void writeInputStream(java.io.OutputStream, android.util.Size, java.io.InputStream, long) throws java.io.IOException;
+    field public static final int MAX_THUMBNAIL_DIMENSION = 256; // 0x100
   }
 
   public final class TotalCaptureResult extends android.hardware.camera2.CaptureResult {
index 3e3303c..6fc99ac 100644 (file)
@@ -17,6 +17,7 @@
 package android.hardware.camera2;
 
 import android.graphics.Bitmap;
+import android.graphics.Color;
 import android.graphics.ImageFormat;
 import android.hardware.camera2.impl.CameraMetadataNative;
 import android.location.Location;
@@ -31,6 +32,7 @@ import java.io.OutputStream;
 import java.nio.ByteBuffer;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
+import java.util.Calendar;
 import java.util.TimeZone;
 
 /**
@@ -68,17 +70,19 @@ public final class DngCreator implements AutoCloseable {
      * </p>
      * <p>
      * DNG metadata tags will be generated from the corresponding parameters in the
-     * {@link android.hardware.camera2.CaptureResult} object.  This removes or overrides
-     * all previous tags set.
+     * {@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(CameraCharacteristics characteristics, CaptureResult metadata) {
         if (characteristics == null || metadata == null) {
-            throw new NullPointerException("Null argument to DngCreator constructor");
+            throw new IllegalArgumentException("Null argument to DngCreator constructor");
         }
 
         // Find current time
@@ -121,10 +125,8 @@ public final class DngCreator implements AutoCloseable {
      *                      <li>{@link android.media.ExifInterface#ORIENTATION_ROTATE_270}</li>
      *                    </ul>
      * @return this {@link #DngCreator} object.
-     * @hide
      */
     public DngCreator setOrientation(int orientation) {
-
         if (orientation < ExifInterface.ORIENTATION_UNDEFINED ||
                 orientation > ExifInterface.ORIENTATION_ROTATE_270) {
             throw new IllegalArgumentException("Orientation " + orientation +
@@ -139,32 +141,32 @@ public final class DngCreator implements AutoCloseable {
      *
      * <p>
      * Pixel data will be converted to a Baseline TIFF RGB image, with 8 bits per color channel.
-     * The alpha channel will be discarded.
-     * </p>
-     *
-     * <p>
-     * The given bitmap should not be altered while this object is in use.
+     * 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.
-     * @hide
+     * @throws java.lang.IllegalArgumentException if the given thumbnail image has a dimension
+     *      larger than {@link #MAX_THUMBNAIL_DIMENSION}.
      */
     public DngCreator setThumbnail(Bitmap pixels) {
         if (pixels == null) {
-            throw new NullPointerException("Null argument to setThumbnail");
+            throw new IllegalArgumentException("Null argument to setThumbnail");
         }
 
-        Bitmap.Config config = pixels.getConfig();
+        int width = pixels.getWidth();
+        int height = pixels.getHeight();
 
-        if (config != Bitmap.Config.ARGB_8888) {
-            pixels = pixels.copy(Bitmap.Config.ARGB_8888, false);
-            if (pixels == null) {
-                throw new IllegalArgumentException("Unsupported Bitmap format " + config);
-            }
-            nativeSetThumbnailBitmap(pixels);
+        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;
     }
 
@@ -173,37 +175,41 @@ public final class DngCreator implements AutoCloseable {
      *
      * <p>
      * Pixel data is interpreted as a {@link android.graphics.ImageFormat#YUV_420_888} image.
-     * </p>
-     *
-     * <p>
-     * The given image should not be altered while this object is in use.
+     * 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.
-     * @hide
+     * @throws java.lang.IllegalArgumentException if the given thumbnail image has a dimension
+     *      larger than {@link #MAX_THUMBNAIL_DIMENSION}.
      */
     public DngCreator setThumbnail(Image pixels) {
         if (pixels == null) {
-            throw new NullPointerException("Null argument to setThumbnail");
+            throw new IllegalArgumentException("Null argument to setThumbnail");
         }
 
         int format = pixels.getFormat();
         if (format != ImageFormat.YUV_420_888) {
-            throw new IllegalArgumentException("Unsupported image format " + format);
+            throw new IllegalArgumentException("Unsupported Image format " + format);
         }
 
-        Image.Plane[] planes = pixels.getPlanes();
-        nativeSetThumbnailImage(pixels.getWidth(), pixels.getHeight(), planes[0].getBuffer(),
-                planes[0].getRowStride(), planes[0].getPixelStride(), planes[1].getBuffer(),
-                planes[1].getRowStride(), planes[1].getPixelStride(), planes[1].getBuffer(),
-                planes[1].getRowStride(), planes[1].getPixelStride());
+        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.
      *
@@ -219,10 +225,26 @@ public final class DngCreator implements AutoCloseable {
      *
      * @throws java.lang.IllegalArgumentException if the given location object doesn't
      *          contain enough information to set location metadata.
-     * @hide
      */
     public DngCreator setLocation(Location location) {
-        /*TODO*/
+        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;
     }
 
@@ -235,10 +257,12 @@ public final class DngCreator implements AutoCloseable {
      *
      * @param description the user description string.
      * @return this {@link #DngCreator} object.
-     * @hide
      */
     public DngCreator setDescription(String description) {
-        /*TODO*/
+        if (description == null) {
+            throw new IllegalArgumentException("Null description passed to setDescription.");
+        }
+        nativeSetDescription(description);
         return this;
     }
 
@@ -268,14 +292,26 @@ public final class DngCreator implements AutoCloseable {
      * @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
-     * @hide
      */
     public void writeInputStream(OutputStream dngOutput, Size size, InputStream pixels, long offset)
             throws IOException {
-        if (dngOutput == null || pixels == null) {
-            throw new NullPointerException("Null argument to writeImage");
+        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");
         }
-        nativeWriteInputStream(dngOutput, pixels, offset);
+
+        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);
     }
 
     /**
@@ -294,6 +330,11 @@ public final class DngCreator implements AutoCloseable {
      * {@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.
@@ -303,14 +344,24 @@ public final class DngCreator implements AutoCloseable {
      * @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.
-     * @hide
      */
     public void writeByteBuffer(OutputStream dngOutput, Size size, ByteBuffer pixels, long offset)
             throws IOException {
-        if (dngOutput == null || pixels == null) {
-            throw new NullPointerException("Null argument to writeImage");
+        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");
         }
-        nativeWriteByteBuffer(dngOutput, pixels, offset);
+
+        int width = size.getWidth();
+        int height = size.getHeight();
+
+        writeByteBuffer(width, height, pixels, dngOutput, DEFAULT_PIXEL_STRIDE,
+                width * DEFAULT_PIXEL_STRIDE, offset);
     }
 
     /**
@@ -331,8 +382,10 @@ public final class DngCreator implements AutoCloseable {
      *          set to write a well-formatted DNG file.
      */
     public void writeImage(OutputStream dngOutput, Image pixels) throws IOException {
-        if (dngOutput == null || pixels == null) {
-            throw new NullPointerException("Null argument to writeImage");
+        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();
@@ -341,8 +394,13 @@ public final class DngCreator implements AutoCloseable {
         }
 
         Image.Plane[] planes = pixels.getPlanes();
-        nativeWriteImage(dngOutput, pixels.getWidth(), pixels.getHeight(), planes[0].getBuffer(),
-                planes[0].getRowStride(), planes[0].getPixelStride());
+        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
@@ -350,6 +408,11 @@ public final class DngCreator implements AutoCloseable {
         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 {
@@ -359,13 +422,181 @@ public final class DngCreator implements AutoCloseable {
         }
     }
 
+    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 kk:mm:ss";
+    private static final DateFormat sExifGPSDateStamp = new SimpleDateFormat(GPS_DATE_FORMAT_STR);
     private static final DateFormat sDateTimeStampFormat =
             new SimpleDateFormat(TIFF_DATETIME_FORMAT);
+    private final Calendar mGPSTimeStampCalendar = Calendar
+            .getInstance(TimeZone.getTimeZone("UTC"));
 
     static {
         sDateTimeStampFormat.setTimeZone(TimeZone.getDefault());
+        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; // byts per pixel
+
+    /**
+     * 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 = 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];
+        byte[] uFullRow = new byte[uPixStride * width / 2];
+        byte[] vFullRow = new byte[vPixStride * width / 2];
+        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.
      */
@@ -381,24 +612,22 @@ public final class DngCreator implements AutoCloseable {
 
     private synchronized native void nativeSetOrientation(int orientation);
 
-    private synchronized native void nativeSetThumbnailBitmap(Bitmap bitmap);
+    private synchronized native void nativeSetDescription(String description);
 
-    private synchronized native void nativeSetThumbnailImage(int width, int height,
-                                                             ByteBuffer yBuffer, int yRowStride,
-                                                             int yPixStride, ByteBuffer uBuffer,
-                                                             int uRowStride, int uPixStride,
-                                                             ByteBuffer vBuffer, int vRowStride,
-                                                             int vPixStride);
+    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) throws IOException;
-
-    private synchronized native void nativeWriteByteBuffer(OutputStream out, ByteBuffer rawBuffer,
-                                                           long offset) throws IOException;
+                                                      int pixStride, long offset, boolean isDirect)
+                                                      throws IOException;
 
     private synchronized native void nativeWriteInputStream(OutputStream out, InputStream rawStream,
-                                                            long offset) throws IOException;
+                                                            int width, int height, long offset)
+                                                            throws IOException;
 
     static {
         nativeClassInit();
index 33100bf..3a3328f 100644 (file)
 #include <img_utils/TiffIfd.h>
 #include <img_utils/TiffWriter.h>
 #include <img_utils/Output.h>
+#include <img_utils/Input.h>
+#include <img_utils/StripSource.h>
 
 #include <utils/Log.h>
 #include <utils/Errors.h>
 #include <utils/StrongPointer.h>
 #include <utils/RefBase.h>
+#include <utils/Vector.h>
 #include <cutils/properties.h>
 
 #include <string.h>
 using namespace android;
 using namespace img_utils;
 
-#define BAIL_IF_INVALID(expr, jnienv, tagId) \
+#define BAIL_IF_INVALID(expr, jnienv, tagId, writer) \
     if ((expr) != OK) { \
         jniThrowExceptionFmt(jnienv, "java/lang/IllegalArgumentException", \
-                "Invalid metadata for tag %x", tagId); \
+                "Invalid metadata for tag %s (%x)", (writer)->getTagName(tagId), (tagId)); \
         return; \
     }
 
-#define BAIL_IF_EMPTY(entry, jnienv, tagId) \
+#define BAIL_IF_EMPTY(entry, jnienv, tagId, writer) \
     if (entry.count == 0) { \
         jniThrowExceptionFmt(jnienv, "java/lang/IllegalArgumentException", \
-                "Missing metadata fields for tag %x", tagId); \
+                "Missing metadata fields for tag %s (%x)", (writer)->getTagName(tagId), (tagId)); \
         return; \
     }
 
@@ -66,15 +69,98 @@ static struct {
     jmethodID mWriteMethod;
 } gOutputStreamClassInfo;
 
+static struct {
+    jmethodID mReadMethod;
+    jmethodID mSkipMethod;
+} gInputStreamClassInfo;
+
+static struct {
+    jmethodID mGetMethod;
+} gInputByteBufferClassInfo;
+
 enum {
     BITS_PER_SAMPLE = 16,
     BYTES_PER_SAMPLE = 2,
-    TIFF_IFD_0 = 0
+    BYTES_PER_RGB_PIXEL = 3,
+    BITS_PER_RGB_SAMPLE = 8,
+    BYTES_PER_RGB_SAMPLE = 1,
+    SAMPLES_PER_RGB_PIXEL = 3,
+    SAMPLES_PER_RAW_PIXEL = 1,
+    TIFF_IFD_0 = 0,
+    TIFF_IFD_SUB1 = 1,
+    TIFF_IFD_GPSINFO = 2,
 };
 
 // ----------------------------------------------------------------------------
 
-// This class is not intended to be used across JNI calls.
+/**
+ * Container class for the persistent native context.
+ */
+
+class NativeContext : public LightRefBase<NativeContext> {
+
+public:
+    NativeContext();
+    virtual ~NativeContext();
+
+    TiffWriter* getWriter();
+
+    uint32_t getThumbnailWidth();
+    uint32_t getThumbnailHeight();
+    const uint8_t* getThumbnail();
+
+    bool setThumbnail(const uint8_t* buffer, uint32_t width, uint32_t height);
+
+private:
+    Vector<uint8_t> mCurrentThumbnail;
+    TiffWriter mWriter;
+    uint32_t mThumbnailWidth;
+    uint32_t mThumbnailHeight;
+};
+
+NativeContext::NativeContext() : mThumbnailWidth(0), mThumbnailHeight(0) {}
+
+NativeContext::~NativeContext() {}
+
+TiffWriter* NativeContext::getWriter() {
+    return &mWriter;
+}
+
+uint32_t NativeContext::getThumbnailWidth() {
+    return mThumbnailWidth;
+}
+
+uint32_t NativeContext::getThumbnailHeight() {
+    return mThumbnailHeight;
+}
+
+const uint8_t* NativeContext::getThumbnail() {
+    return mCurrentThumbnail.array();
+}
+
+bool NativeContext::setThumbnail(const uint8_t* buffer, uint32_t width, uint32_t height) {
+    mThumbnailWidth = width;
+    mThumbnailHeight = height;
+
+    size_t size = BYTES_PER_RGB_PIXEL * width * height;
+    if (mCurrentThumbnail.resize(size) < 0) {
+        ALOGE("%s: Could not resize thumbnail buffer.", __FUNCTION__);
+        return false;
+    }
+
+    uint8_t* thumb = mCurrentThumbnail.editArray();
+    memcpy(thumb, buffer, size);
+    return true;
+}
+
+// End of NativeContext
+// ----------------------------------------------------------------------------
+
+/**
+ * Wrapper class for a Java OutputStream.
+ *
+ * This class is not intended to be used across JNI calls.
+ */
 class JniOutputStream : public Output, public LightRefBase<JniOutputStream> {
 public:
     JniOutputStream(JNIEnv* env, jobject outStream);
@@ -82,11 +168,13 @@ public:
     virtual ~JniOutputStream();
 
     status_t open();
+
     status_t write(const uint8_t* buf, size_t offset, size_t count);
+
     status_t close();
 private:
     enum {
-        BYTE_ARRAY_LENGTH = 1024
+        BYTE_ARRAY_LENGTH = 4096
     };
     jobject mOutputStream;
     JNIEnv* mEnv;
@@ -138,27 +226,465 @@ status_t JniOutputStream::close() {
     return OK;
 }
 
+// End of JniOutputStream
 // ----------------------------------------------------------------------------
 
+/**
+ * Wrapper class for a Java InputStream.
+ *
+ * This class is not intended to be used across JNI calls.
+ */
+class JniInputStream : public Input, public LightRefBase<JniInputStream> {
+public:
+    JniInputStream(JNIEnv* env, jobject inStream);
+
+    status_t open();
+
+    status_t close();
+
+    ssize_t read(uint8_t* buf, size_t offset, size_t count);
+
+    ssize_t skip(size_t count);
+
+    virtual ~JniInputStream();
+private:
+    enum {
+        BYTE_ARRAY_LENGTH = 4096
+    };
+    jobject mInStream;
+    JNIEnv* mEnv;
+    jbyteArray mByteArray;
+
+};
+
+JniInputStream::JniInputStream(JNIEnv* env, jobject inStream) : mInStream(inStream), mEnv(env) {
+    mByteArray = env->NewByteArray(BYTE_ARRAY_LENGTH);
+    if (mByteArray == NULL) {
+        jniThrowException(env, "java/lang/OutOfMemoryError", "Could not allocate byte array.");
+    }
+}
+
+JniInputStream::~JniInputStream() {
+    mEnv->DeleteLocalRef(mByteArray);
+}
+
+ssize_t JniInputStream::read(uint8_t* buf, size_t offset, size_t count) {
+
+    jint realCount = BYTE_ARRAY_LENGTH;
+    if (count < BYTE_ARRAY_LENGTH) {
+        realCount = count;
+    }
+    jint actual = mEnv->CallIntMethod(mInStream, gInputStreamClassInfo.mReadMethod, mByteArray, 0,
+            realCount);
+
+    if (actual < 0) {
+        return NOT_ENOUGH_DATA;
+    }
+
+    if (mEnv->ExceptionCheck()) {
+        return BAD_VALUE;
+    }
+
+    mEnv->GetByteArrayRegion(mByteArray, 0, actual, reinterpret_cast<jbyte*>(buf + offset));
+    if (mEnv->ExceptionCheck()) {
+        return BAD_VALUE;
+    }
+    return actual;
+}
+
+ssize_t JniInputStream::skip(size_t count) {
+    jlong actual = mEnv->CallLongMethod(mInStream, gInputStreamClassInfo.mSkipMethod,
+            static_cast<jlong>(count));
+
+    if (mEnv->ExceptionCheck()) {
+        return BAD_VALUE;
+    }
+    if (actual < 0) {
+        return NOT_ENOUGH_DATA;
+    }
+    return actual;
+}
+
+status_t JniInputStream::open() {
+    // Do nothing
+    return OK;
+}
+
+status_t JniInputStream::close() {
+    // Do nothing
+    return OK;
+}
+
+// End of JniInputStream
+// ----------------------------------------------------------------------------
+
+/**
+ * Wrapper class for a non-direct Java ByteBuffer.
+ *
+ * This class is not intended to be used across JNI calls.
+ */
+class JniInputByteBuffer : public Input, public LightRefBase<JniInputByteBuffer> {
+public:
+    JniInputByteBuffer(JNIEnv* env, jobject inBuf);
+
+    status_t open();
+
+    status_t close();
+
+    ssize_t read(uint8_t* buf, size_t offset, size_t count);
+
+    virtual ~JniInputByteBuffer();
+private:
+    enum {
+        BYTE_ARRAY_LENGTH = 4096
+    };
+    jobject mInBuf;
+    JNIEnv* mEnv;
+    jbyteArray mByteArray;
+};
+
+JniInputByteBuffer::JniInputByteBuffer(JNIEnv* env, jobject inBuf) : mInBuf(inBuf), mEnv(env) {
+    mByteArray = env->NewByteArray(BYTE_ARRAY_LENGTH);
+    if (mByteArray == NULL) {
+        jniThrowException(env, "java/lang/OutOfMemoryError", "Could not allocate byte array.");
+    }
+}
+
+JniInputByteBuffer::~JniInputByteBuffer() {
+    mEnv->DeleteLocalRef(mByteArray);
+}
+
+ssize_t JniInputByteBuffer::read(uint8_t* buf, size_t offset, size_t count) {
+    jint realCount = BYTE_ARRAY_LENGTH;
+    if (count < BYTE_ARRAY_LENGTH) {
+        realCount = count;
+    }
+
+    mEnv->CallObjectMethod(mInBuf, gInputByteBufferClassInfo.mGetMethod, mByteArray, 0,
+            realCount);
+
+    if (mEnv->ExceptionCheck()) {
+        return BAD_VALUE;
+    }
+
+    mEnv->GetByteArrayRegion(mByteArray, 0, realCount, reinterpret_cast<jbyte*>(buf + offset));
+    if (mEnv->ExceptionCheck()) {
+        return BAD_VALUE;
+    }
+    return realCount;
+}
+
+status_t JniInputByteBuffer::open() {
+    // Do nothing
+    return OK;
+}
+
+status_t JniInputByteBuffer::close() {
+    // Do nothing
+    return OK;
+}
+
+// End of JniInputByteBuffer
+// ----------------------------------------------------------------------------
+
+/**
+ * StripSource subclass for Input types.
+ *
+ * This class is not intended to be used across JNI calls.
+ */
+
+class InputStripSource : public StripSource, public LightRefBase<InputStripSource> {
+public:
+    InputStripSource(JNIEnv* env, Input& input, uint32_t ifd, uint32_t width, uint32_t height,
+            uint32_t pixStride, uint32_t rowStride, uint64_t offset, uint32_t bytesPerSample,
+            uint32_t samplesPerPixel);
+
+    virtual ~InputStripSource();
+
+    virtual status_t writeToStream(Output& stream, uint32_t count);
+
+    virtual uint32_t getIfd() const;
+protected:
+    uint32_t mIfd;
+    Input* mInput;
+    uint32_t mWidth;
+    uint32_t mHeight;
+    uint32_t mPixStride;
+    uint32_t mRowStride;
+    uint64_t mOffset;
+    JNIEnv* mEnv;
+    uint32_t mBytesPerSample;
+    uint32_t mSamplesPerPixel;
+};
+
+InputStripSource::InputStripSource(JNIEnv* env, Input& input, uint32_t ifd, uint32_t width,
+        uint32_t height, uint32_t pixStride, uint32_t rowStride, uint64_t offset,
+        uint32_t bytesPerSample, uint32_t samplesPerPixel) : mIfd(ifd), mInput(&input),
+        mWidth(width), mHeight(height), mPixStride(pixStride), mRowStride(rowStride),
+        mOffset(offset), mEnv(env), mBytesPerSample(bytesPerSample),
+        mSamplesPerPixel(samplesPerPixel) {}
+
+InputStripSource::~InputStripSource() {}
+
+status_t InputStripSource::writeToStream(Output& stream, uint32_t count) {
+    status_t err = OK;
+    uint32_t fullSize = mRowStride * mHeight;
+    jlong offset = mOffset;
+
+    if (fullSize != count) {
+        ALOGE("%s: Amount to write %u doesn't match image size %u", __FUNCTION__, count,
+                fullSize);
+        jniThrowException(mEnv, "java/lang/IllegalStateException", "Not enough data to write");
+        return BAD_VALUE;
+    }
+
+    // Skip offset
+    while (offset > 0) {
+        ssize_t skipped = mInput->skip(offset);
+        if (skipped <= 0) {
+            if (skipped == NOT_ENOUGH_DATA || skipped == 0) {
+                jniThrowExceptionFmt(mEnv, "java/io/IOException",
+                        "Early EOF encountered in skip, not enough pixel data for image of size %u",
+                        fullSize);
+                skipped = NOT_ENOUGH_DATA;
+            } else {
+                if (!mEnv->ExceptionCheck()) {
+                    jniThrowException(mEnv, "java/io/IOException",
+                            "Error encountered while skip bytes in input stream.");
+                }
+            }
+
+            return skipped;
+        }
+        offset -= skipped;
+    }
+
+    Vector<uint8_t> row;
+    if (row.resize(mRowStride) < 0) {
+        jniThrowException(mEnv, "java/lang/OutOfMemoryError", "Could not allocate row vector.");
+        return BAD_VALUE;
+    }
+
+    uint8_t* rowBytes = row.editArray();
+
+    for (uint32_t i = 0; i < mHeight; ++i) {
+        size_t rowFillAmt = 0;
+        size_t rowSize = mPixStride;
+
+        while (rowFillAmt < mRowStride) {
+            ssize_t bytesRead = mInput->read(rowBytes, rowFillAmt, rowSize);
+            if (bytesRead <= 0) {
+                if (bytesRead == NOT_ENOUGH_DATA || bytesRead == 0) {
+                    jniThrowExceptionFmt(mEnv, "java/io/IOException",
+                            "Early EOF encountered, not enough pixel data for image of size %u",
+                            fullSize);
+                    bytesRead = NOT_ENOUGH_DATA;
+                } else {
+                    if (!mEnv->ExceptionCheck()) {
+                        jniThrowException(mEnv, "java/io/IOException",
+                                "Error encountered while reading");
+                    }
+                }
+                return bytesRead;
+            }
+            rowFillAmt += bytesRead;
+            rowSize -= bytesRead;
+        }
+
+        if (mPixStride == mBytesPerSample * mSamplesPerPixel) {
+            ALOGV("%s: Using stream per-row write for strip.", __FUNCTION__);
+
+            if (stream.write(rowBytes, 0, mBytesPerSample * mSamplesPerPixel * mWidth) != OK ||
+                    mEnv->ExceptionCheck()) {
+                if (!mEnv->ExceptionCheck()) {
+                    jniThrowException(mEnv, "java/io/IOException", "Failed to write pixel data");
+                }
+                return BAD_VALUE;
+            }
+        } else {
+            ALOGV("%s: Using stream per-pixel write for strip.", __FUNCTION__);
+            jniThrowException(mEnv, "java/lang/IllegalStateException",
+                    "Per-pixel strides are not supported for RAW16 -- pixels must be contiguous");
+            return BAD_VALUE;
+
+            // TODO: Add support for non-contiguous pixels if needed.
+        }
+    }
+    return OK;
+}
+
+uint32_t InputStripSource::getIfd() const {
+    return mIfd;
+}
+
+// End of InputStripSource
+// ----------------------------------------------------------------------------
+
+/**
+ * StripSource subclass for direct buffer types.
+ *
+ * This class is not intended to be used across JNI calls.
+ */
+
+class DirectStripSource : public StripSource, public LightRefBase<DirectStripSource> {
+public:
+    DirectStripSource(JNIEnv* env, const uint8_t* pixelBytes, uint32_t ifd, uint32_t width,
+            uint32_t height, uint32_t pixStride, uint32_t rowStride, uint64_t offset,
+            uint32_t bytesPerSample, uint32_t samplesPerPixel);
+
+    virtual ~DirectStripSource();
+
+    virtual status_t writeToStream(Output& stream, uint32_t count);
+
+    virtual uint32_t getIfd() const;
+protected:
+    uint32_t mIfd;
+    const uint8_t* mPixelBytes;
+    uint32_t mWidth;
+    uint32_t mHeight;
+    uint32_t mPixStride;
+    uint32_t mRowStride;
+    uint16_t mOffset;
+    JNIEnv* mEnv;
+    uint32_t mBytesPerSample;
+    uint32_t mSamplesPerPixel;
+};
+
+DirectStripSource::DirectStripSource(JNIEnv* env, const uint8_t* pixelBytes, uint32_t ifd,
+            uint32_t width, uint32_t height, uint32_t pixStride, uint32_t rowStride,
+            uint64_t offset, uint32_t bytesPerSample, uint32_t samplesPerPixel) : mIfd(ifd),
+            mPixelBytes(pixelBytes), mWidth(width), mHeight(height), mPixStride(pixStride),
+            mRowStride(rowStride), mOffset(offset), mEnv(env), mBytesPerSample(bytesPerSample),
+            mSamplesPerPixel(samplesPerPixel) {}
+
+DirectStripSource::~DirectStripSource() {}
+
+status_t DirectStripSource::writeToStream(Output& stream, uint32_t count) {
+    uint32_t fullSize = mRowStride * mHeight;
+
+    if (fullSize != count) {
+        ALOGE("%s: Amount to write %u doesn't match image size %u", __FUNCTION__, count,
+                fullSize);
+        jniThrowException(mEnv, "java/lang/IllegalStateException", "Not enough data to write");
+        return BAD_VALUE;
+    }
+
+    if (mPixStride == mBytesPerSample * mSamplesPerPixel
+            && mRowStride == mWidth * mBytesPerSample * mSamplesPerPixel) {
+        ALOGV("%s: Using direct single-pass write for strip.", __FUNCTION__);
+
+        if (stream.write(mPixelBytes, mOffset, fullSize) != OK || mEnv->ExceptionCheck()) {
+            if (!mEnv->ExceptionCheck()) {
+                jniThrowException(mEnv, "java/io/IOException", "Failed to write pixel data");
+            }
+            return BAD_VALUE;
+        }
+    } else if (mPixStride == mBytesPerSample * mSamplesPerPixel) {
+        ALOGV("%s: Using direct per-row write for strip.", __FUNCTION__);
+
+        for (size_t i = 0; i < mHeight; ++i) {
+            if (stream.write(mPixelBytes, mOffset + i * mRowStride, mPixStride * mWidth) != OK ||
+                        mEnv->ExceptionCheck()) {
+                if (!mEnv->ExceptionCheck()) {
+                    jniThrowException(mEnv, "java/io/IOException", "Failed to write pixel data");
+                }
+                return BAD_VALUE;
+            }
+        }
+    } else {
+        ALOGV("%s: Using direct per-pixel write for strip.", __FUNCTION__);
+
+        jniThrowException(mEnv, "java/lang/IllegalStateException",
+                "Per-pixel strides are not supported for RAW16 -- pixels must be contiguous");
+        return BAD_VALUE;
+
+        // TODO: Add support for non-contiguous pixels if needed.
+    }
+    return OK;
+
+}
+
+uint32_t DirectStripSource::getIfd() const {
+    return mIfd;
+}
+
+// End of DirectStripSource
+// ----------------------------------------------------------------------------
+
+static bool validateDngHeader(JNIEnv* env, TiffWriter* writer, jint width, jint height) {
+    bool hasThumbnail = writer->hasIfd(TIFF_IFD_SUB1);
+
+    // TODO: handle lens shading map, etc. conversions for other raw buffer sizes.
+    uint32_t metadataWidth = *(writer->getEntry(TAG_IMAGEWIDTH, (hasThumbnail) ? TIFF_IFD_SUB1 : TIFF_IFD_0)->getData<uint32_t>());
+    uint32_t metadataHeight = *(writer->getEntry(TAG_IMAGELENGTH, (hasThumbnail) ? TIFF_IFD_SUB1 : TIFF_IFD_0)->getData<uint32_t>());
+
+    if (width < 0 || metadataWidth != static_cast<uint32_t>(width)) {
+        jniThrowExceptionFmt(env, "java/lang/IllegalArgumentException", \
+                        "Metadata width %d doesn't match image width %d", metadataWidth, width);
+        return false;
+    }
+
+    if (height < 0 || metadataHeight != static_cast<uint32_t>(height)) {
+        jniThrowExceptionFmt(env, "java/lang/IllegalArgumentException", \
+                        "Metadata height %d doesn't match image height %d", metadataHeight, height);
+        return false;
+    }
+
+    return true;
+}
+
+static status_t moveEntries(TiffWriter* writer, uint32_t ifdFrom, uint32_t ifdTo,
+        const Vector<uint16_t>& entries) {
+    for (size_t i = 0; i < entries.size(); ++i) {
+        uint16_t tagId = entries[i];
+        sp<TiffEntry> entry = writer->getEntry(tagId, ifdFrom);
+        if (entry == NULL) {
+            ALOGE("%s: moveEntries failed, entry %u not found in IFD %u", __FUNCTION__, tagId,
+                    ifdFrom);
+            return BAD_VALUE;
+        }
+        if (writer->addEntry(entry, ifdTo) != OK) {
+            ALOGE("%s: moveEntries failed, could not add entry %u to IFD %u", __FUNCTION__, tagId,
+                    ifdFrom);
+            return BAD_VALUE;
+        }
+        writer->removeEntry(tagId, ifdFrom);
+    }
+    return OK;
+}
+
+// ----------------------------------------------------------------------------
 extern "C" {
 
-static TiffWriter* DngCreator_getCreator(JNIEnv* env, jobject thiz) {
+static NativeContext* DngCreator_getNativeContext(JNIEnv* env, jobject thiz) {
     ALOGV("%s:", __FUNCTION__);
-    return reinterpret_cast<TiffWriter*>(env->GetLongField(thiz,
+    return reinterpret_cast<NativeContext*>(env->GetLongField(thiz,
             gDngCreatorClassInfo.mNativeContext));
 }
 
-static void DngCreator_setCreator(JNIEnv* env, jobject thiz, sp<TiffWriter> writer) {
+static void DngCreator_setNativeContext(JNIEnv* env, jobject thiz, sp<NativeContext> context) {
     ALOGV("%s:", __FUNCTION__);
-    TiffWriter* current = DngCreator_getCreator(env, thiz);
-    if (writer != NULL) {
-        writer->incStrong((void*) DngCreator_setCreator);
+    NativeContext* current = DngCreator_getNativeContext(env, thiz);
+
+    if (context != NULL) {
+        context->incStrong((void*) DngCreator_setNativeContext);
     }
+
     if (current) {
-        current->decStrong((void*) DngCreator_setCreator);
+        current->decStrong((void*) DngCreator_setNativeContext);
     }
+
     env->SetLongField(thiz, gDngCreatorClassInfo.mNativeContext,
-            reinterpret_cast<jlong>(writer.get()));
+            reinterpret_cast<jlong>(context.get()));
+}
+
+static TiffWriter* DngCreator_getCreator(JNIEnv* env, jobject thiz) {
+    ALOGV("%s:", __FUNCTION__);
+    NativeContext* current = DngCreator_getNativeContext(env, thiz);
+    if (current) {
+        return current->getWriter();
+    }
+    return NULL;
 }
 
 static void DngCreator_nativeClassInit(JNIEnv* env, jclass clazz) {
@@ -174,6 +700,19 @@ static void DngCreator_nativeClassInit(JNIEnv* env, jclass clazz) {
     LOG_ALWAYS_FATAL_IF(outputStreamClazz == NULL, "Can't find java/io/OutputStream class");
     gOutputStreamClassInfo.mWriteMethod = env->GetMethodID(outputStreamClazz, "write", "([BII)V");
     LOG_ALWAYS_FATAL_IF(gOutputStreamClassInfo.mWriteMethod == NULL, "Can't find write method");
+
+    jclass inputStreamClazz = env->FindClass("java/io/InputStream");
+    LOG_ALWAYS_FATAL_IF(inputStreamClazz == NULL, "Can't find java/io/InputStream class");
+    gInputStreamClassInfo.mReadMethod = env->GetMethodID(inputStreamClazz, "read", "([BII)I");
+    LOG_ALWAYS_FATAL_IF(gInputStreamClassInfo.mReadMethod == NULL, "Can't find read method");
+    gInputStreamClassInfo.mSkipMethod = env->GetMethodID(inputStreamClazz, "skip", "(J)J");
+    LOG_ALWAYS_FATAL_IF(gInputStreamClassInfo.mSkipMethod == NULL, "Can't find skip method");
+
+    jclass inputBufferClazz = env->FindClass("java/nio/ByteBuffer");
+    LOG_ALWAYS_FATAL_IF(inputBufferClazz == NULL, "Can't find java/nio/ByteBuffer class");
+    gInputByteBufferClassInfo.mGetMethod = env->GetMethodID(inputBufferClazz, "get",
+            "([BII)Ljava/nio/ByteBuffer;");
+    LOG_ALWAYS_FATAL_IF(gInputByteBufferClassInfo.mGetMethod == NULL, "Can't find get method");
 }
 
 static void DngCreator_init(JNIEnv* env, jobject thiz, jobject characteristicsPtr,
@@ -192,7 +731,8 @@ static void DngCreator_init(JNIEnv* env, jobject thiz, jobject characteristicsPt
         return;
     }
 
-    sp<TiffWriter> writer = new TiffWriter();
+    sp<NativeContext> nativeContext = new NativeContext();
+    TiffWriter* writer = nativeContext->getWriter();
 
     writer->addIfd(TIFF_IFD_0);
 
@@ -208,96 +748,99 @@ static void DngCreator_init(JNIEnv* env, jobject thiz, jobject characteristicsPt
 
     // TODO: Greensplit.
     // TODO: Add remaining non-essential tags
+
+    // Setup main image tags
+
     {
         // Set orientation
         uint16_t orientation = 1; // Normal
         BAIL_IF_INVALID(writer->addEntry(TAG_ORIENTATION, 1, &orientation, TIFF_IFD_0), env,
-                TAG_ORIENTATION);
+                TAG_ORIENTATION, writer);
     }
 
     {
         // Set subfiletype
         uint32_t subfileType = 0; // Main image
         BAIL_IF_INVALID(writer->addEntry(TAG_NEWSUBFILETYPE, 1, &subfileType, TIFF_IFD_0), env,
-                TAG_NEWSUBFILETYPE);
+                TAG_NEWSUBFILETYPE, writer);
     }
 
     {
         // Set bits per sample
         uint16_t bits = static_cast<uint16_t>(bitsPerSample);
         BAIL_IF_INVALID(writer->addEntry(TAG_BITSPERSAMPLE, 1, &bits, TIFF_IFD_0), env,
-                TAG_BITSPERSAMPLE);
+                TAG_BITSPERSAMPLE, writer);
     }
 
     {
         // Set compression
         uint16_t compression = 1; // None
         BAIL_IF_INVALID(writer->addEntry(TAG_COMPRESSION, 1, &compression, TIFF_IFD_0), env,
-                TAG_COMPRESSION);
+                TAG_COMPRESSION, writer);
     }
 
     {
         // Set dimensions
         camera_metadata_entry entry =
                 characteristics.find(ANDROID_SENSOR_INFO_ACTIVE_ARRAY_SIZE);
-        BAIL_IF_EMPTY(entry, env, TAG_IMAGEWIDTH);
+        BAIL_IF_EMPTY(entry, env, TAG_IMAGEWIDTH, writer);
         uint32_t width = static_cast<uint32_t>(entry.data.i32[2]);
         uint32_t height = static_cast<uint32_t>(entry.data.i32[3]);
         BAIL_IF_INVALID(writer->addEntry(TAG_IMAGEWIDTH, 1, &width, TIFF_IFD_0), env,
-                TAG_IMAGEWIDTH);
+                TAG_IMAGEWIDTH, writer);
         BAIL_IF_INVALID(writer->addEntry(TAG_IMAGELENGTH, 1, &height, TIFF_IFD_0), env,
-                TAG_IMAGELENGTH);
+                TAG_IMAGELENGTH, writer);
         imageWidth = width;
         imageHeight = height;
     }
 
     {
         // Set photometric interpretation
-        uint16_t interpretation = 32803;
+        uint16_t interpretation = 32803; // CFA
         BAIL_IF_INVALID(writer->addEntry(TAG_PHOTOMETRICINTERPRETATION, 1, &interpretation,
-                TIFF_IFD_0), env, TAG_PHOTOMETRICINTERPRETATION);
+                TIFF_IFD_0), env, TAG_PHOTOMETRICINTERPRETATION, writer);
     }
 
     {
         // Set blacklevel tags
         camera_metadata_entry entry =
                 characteristics.find(ANDROID_SENSOR_BLACK_LEVEL_PATTERN);
-        BAIL_IF_EMPTY(entry, env, TAG_BLACKLEVEL);
+        BAIL_IF_EMPTY(entry, env, TAG_BLACKLEVEL, writer);
         const uint32_t* blackLevel = reinterpret_cast<const uint32_t*>(entry.data.i32);
         BAIL_IF_INVALID(writer->addEntry(TAG_BLACKLEVEL, entry.count, blackLevel, TIFF_IFD_0), env,
-                TAG_BLACKLEVEL);
+                TAG_BLACKLEVEL, writer);
 
         uint16_t repeatDim[2] = {2, 2};
         BAIL_IF_INVALID(writer->addEntry(TAG_BLACKLEVELREPEATDIM, 2, repeatDim, TIFF_IFD_0), env,
-                TAG_BLACKLEVELREPEATDIM);
+                TAG_BLACKLEVELREPEATDIM, writer);
     }
 
     {
         // Set samples per pixel
         uint16_t samples = static_cast<uint16_t>(samplesPerPixel);
         BAIL_IF_INVALID(writer->addEntry(TAG_SAMPLESPERPIXEL, 1, &samples, TIFF_IFD_0),
-                env, TAG_SAMPLESPERPIXEL);
+                env, TAG_SAMPLESPERPIXEL, writer);
     }
 
     {
         // Set planar configuration
         uint16_t config = 1; // Chunky
         BAIL_IF_INVALID(writer->addEntry(TAG_PLANARCONFIGURATION, 1, &config, TIFF_IFD_0),
-                env, TAG_PLANARCONFIGURATION);
+                env, TAG_PLANARCONFIGURATION, writer);
     }
 
     {
         // Set CFA pattern dimensions
         uint16_t repeatDim[2] = {2, 2};
         BAIL_IF_INVALID(writer->addEntry(TAG_CFAREPEATPATTERNDIM, 2, repeatDim, TIFF_IFD_0),
-                env, TAG_CFAREPEATPATTERNDIM);
+                env, TAG_CFAREPEATPATTERNDIM, writer);
     }
 
     {
         // Set CFA pattern
         camera_metadata_entry entry =
                         characteristics.find(ANDROID_SENSOR_INFO_COLOR_FILTER_ARRANGEMENT);
-        BAIL_IF_EMPTY(entry, env, TAG_CFAPATTERN);
+        BAIL_IF_EMPTY(entry, env, TAG_CFAPATTERN, writer);
         camera_metadata_enum_android_sensor_info_color_filter_arrangement_t cfa =
                 static_cast<camera_metadata_enum_android_sensor_info_color_filter_arrangement_t>(
                 entry.data.u8[0]);
@@ -305,28 +848,28 @@ static void DngCreator_init(JNIEnv* env, jobject thiz, jobject characteristicsPt
             case ANDROID_SENSOR_INFO_COLOR_FILTER_ARRANGEMENT_RGGB: {
                 uint8_t cfa[4] = {0, 1, 1, 2};
                 BAIL_IF_INVALID(writer->addEntry(TAG_CFAPATTERN, 4, cfa, TIFF_IFD_0),
-                                                env, TAG_CFAPATTERN);
+                                                env, TAG_CFAPATTERN, writer);
                 opcodeCfaLayout = OpcodeListBuilder::CFA_RGGB;
                 break;
             }
             case ANDROID_SENSOR_INFO_COLOR_FILTER_ARRANGEMENT_GRBG: {
                 uint8_t cfa[4] = {1, 0, 2, 1};
                 BAIL_IF_INVALID(writer->addEntry(TAG_CFAPATTERN, 4, cfa, TIFF_IFD_0),
-                                                env, TAG_CFAPATTERN);
+                                                env, TAG_CFAPATTERN, writer);
                 opcodeCfaLayout = OpcodeListBuilder::CFA_GRBG;
                 break;
             }
             case ANDROID_SENSOR_INFO_COLOR_FILTER_ARRANGEMENT_GBRG: {
                 uint8_t cfa[4] = {1, 2, 0, 1};
                 BAIL_IF_INVALID(writer->addEntry(TAG_CFAPATTERN, 4, cfa, TIFF_IFD_0),
-                                                env, TAG_CFAPATTERN);
+                                                env, TAG_CFAPATTERN, writer);
                 opcodeCfaLayout = OpcodeListBuilder::CFA_GBRG;
                 break;
             }
             case ANDROID_SENSOR_INFO_COLOR_FILTER_ARRANGEMENT_BGGR: {
                 uint8_t cfa[4] = {2, 1, 1, 0};
                 BAIL_IF_INVALID(writer->addEntry(TAG_CFAPATTERN, 4, cfa, TIFF_IFD_0),
-                                env, TAG_CFAPATTERN);
+                                env, TAG_CFAPATTERN, writer);
                 opcodeCfaLayout = OpcodeListBuilder::CFA_BGGR;
                 break;
             }
@@ -342,21 +885,21 @@ static void DngCreator_init(JNIEnv* env, jobject thiz, jobject characteristicsPt
         // Set CFA plane color
         uint8_t cfaPlaneColor[3] = {0, 1, 2};
         BAIL_IF_INVALID(writer->addEntry(TAG_CFAPLANECOLOR, 3, cfaPlaneColor, TIFF_IFD_0),
-                env, TAG_CFAPLANECOLOR);
+                env, TAG_CFAPLANECOLOR, writer);
     }
 
     {
         // Set CFA layout
         uint16_t cfaLayout = 1;
         BAIL_IF_INVALID(writer->addEntry(TAG_CFALAYOUT, 1, &cfaLayout, TIFF_IFD_0),
-                env, TAG_CFALAYOUT);
+                env, TAG_CFALAYOUT, writer);
     }
 
     {
         // image description
         uint8_t imageDescription = '\0'; // empty
         BAIL_IF_INVALID(writer->addEntry(TAG_IMAGEDESCRIPTION, 1, &imageDescription, TIFF_IFD_0),
-                env, TAG_IMAGEDESCRIPTION);
+                env, TAG_IMAGEDESCRIPTION, writer);
     }
 
     {
@@ -368,7 +911,7 @@ static void DngCreator_init(JNIEnv* env, jobject thiz, jobject characteristicsPt
         uint32_t count = static_cast<uint32_t>(strlen(manufacturer)) + 1;
 
         BAIL_IF_INVALID(writer->addEntry(TAG_MAKE, count, reinterpret_cast<uint8_t*>(manufacturer),
-                TIFF_IFD_0), env, TAG_MAKE);
+                TIFF_IFD_0), env, TAG_MAKE, writer);
     }
 
     {
@@ -380,23 +923,23 @@ static void DngCreator_init(JNIEnv* env, jobject thiz, jobject characteristicsPt
         uint32_t count = static_cast<uint32_t>(strlen(model)) + 1;
 
         BAIL_IF_INVALID(writer->addEntry(TAG_MODEL, count, reinterpret_cast<uint8_t*>(model),
-                TIFF_IFD_0), env, TAG_MODEL);
+                TIFF_IFD_0), env, TAG_MODEL, writer);
     }
 
     {
         // x resolution
         uint32_t xres[] = { 72, 1 }; // default 72 ppi
         BAIL_IF_INVALID(writer->addEntry(TAG_XRESOLUTION, 1, xres, TIFF_IFD_0),
-                env, TAG_XRESOLUTION);
+                env, TAG_XRESOLUTION, writer);
 
         // y resolution
         uint32_t yres[] = { 72, 1 }; // default 72 ppi
         BAIL_IF_INVALID(writer->addEntry(TAG_YRESOLUTION, 1, yres, TIFF_IFD_0),
-                env, TAG_YRESOLUTION);
+                env, TAG_YRESOLUTION, writer);
 
         uint16_t unit = 2; // inches
         BAIL_IF_INVALID(writer->addEntry(TAG_RESOLUTIONUNIT, 1, &unit, TIFF_IFD_0),
-                env, TAG_RESOLUTIONUNIT);
+                env, TAG_RESOLUTIONUNIT, writer);
     }
 
     {
@@ -405,7 +948,7 @@ static void DngCreator_init(JNIEnv* env, jobject thiz, jobject characteristicsPt
         property_get("ro.build.fingerprint", software, "");
         uint32_t count = static_cast<uint32_t>(strlen(software)) + 1;
         BAIL_IF_INVALID(writer->addEntry(TAG_SOFTWARE, count, reinterpret_cast<uint8_t*>(software),
-                TIFF_IFD_0), env, TAG_SOFTWARE);
+                TIFF_IFD_0), env, TAG_SOFTWARE, writer);
     }
 
     {
@@ -420,12 +963,22 @@ static void DngCreator_init(JNIEnv* env, jobject thiz, jobject characteristicsPt
             return;
         }
 
-        BAIL_IF_INVALID(writer->addEntry(TAG_DATETIME, DATETIME_COUNT,
-                reinterpret_cast<const uint8_t*>(captureTime), TIFF_IFD_0), env, TAG_DATETIMEORIGINAL);
+        if (writer->addEntry(TAG_DATETIME, DATETIME_COUNT,
+                reinterpret_cast<const uint8_t*>(captureTime), TIFF_IFD_0) != OK) {
+            env->ReleaseStringUTFChars(formattedCaptureTime, captureTime);
+            jniThrowExceptionFmt(env, "java/lang/IllegalArgumentException",
+                    "Invalid metadata for tag %x", TAG_DATETIME);
+            return;
+        }
 
         // datetime original
-        BAIL_IF_INVALID(writer->addEntry(TAG_DATETIMEORIGINAL, DATETIME_COUNT,
-                reinterpret_cast<const uint8_t*>(captureTime), TIFF_IFD_0), env, TAG_DATETIMEORIGINAL);
+        if (writer->addEntry(TAG_DATETIMEORIGINAL, DATETIME_COUNT,
+                reinterpret_cast<const uint8_t*>(captureTime), TIFF_IFD_0) != OK) {
+            env->ReleaseStringUTFChars(formattedCaptureTime, captureTime);
+            jniThrowExceptionFmt(env, "java/lang/IllegalArgumentException",
+                    "Invalid metadata for tag %x", TAG_DATETIMEORIGINAL);
+            return;
+        }
         env->ReleaseStringUTFChars(formattedCaptureTime, captureTime);
     }
 
@@ -433,21 +986,21 @@ static void DngCreator_init(JNIEnv* env, jobject thiz, jobject characteristicsPt
         // TIFF/EP standard id
         uint8_t standardId[] = { 1, 0, 0, 0 };
         BAIL_IF_INVALID(writer->addEntry(TAG_TIFFEPSTANDARDID, 4, standardId,
-                TIFF_IFD_0), env, TAG_TIFFEPSTANDARDID);
+                TIFF_IFD_0), env, TAG_TIFFEPSTANDARDID, writer);
     }
 
     {
         // copyright
         uint8_t copyright = '\0'; // empty
         BAIL_IF_INVALID(writer->addEntry(TAG_COPYRIGHT, 1, &copyright,
-                TIFF_IFD_0), env, TAG_COPYRIGHT);
+                TIFF_IFD_0), env, TAG_COPYRIGHT, writer);
     }
 
     {
         // exposure time
         camera_metadata_entry entry =
             results.find(ANDROID_SENSOR_EXPOSURE_TIME);
-        BAIL_IF_EMPTY(entry, env, TAG_EXPOSURETIME);
+        BAIL_IF_EMPTY(entry, env, TAG_EXPOSURETIME, writer);
 
         int64_t exposureTime = *(entry.data.i64);
 
@@ -473,7 +1026,7 @@ static void DngCreator_init(JNIEnv* env, jobject thiz, jobject characteristicsPt
 
         uint32_t exposure[] = { static_cast<uint32_t>(exposureTime), denominator };
         BAIL_IF_INVALID(writer->addEntry(TAG_EXPOSURETIME, 1, exposure,
-                TIFF_IFD_0), env, TAG_EXPOSURETIME);
+                TIFF_IFD_0), env, TAG_EXPOSURETIME, writer);
 
     }
 
@@ -481,7 +1034,7 @@ static void DngCreator_init(JNIEnv* env, jobject thiz, jobject characteristicsPt
         // ISO speed ratings
         camera_metadata_entry entry =
             results.find(ANDROID_SENSOR_SENSITIVITY);
-        BAIL_IF_EMPTY(entry, env, TAG_ISOSPEEDRATINGS);
+        BAIL_IF_EMPTY(entry, env, TAG_ISOSPEEDRATINGS, writer);
 
         int32_t tempIso = *(entry.data.i32);
         if (tempIso < 0) {
@@ -497,57 +1050,57 @@ static void DngCreator_init(JNIEnv* env, jobject thiz, jobject characteristicsPt
 
         uint16_t iso = static_cast<uint16_t>(tempIso);
         BAIL_IF_INVALID(writer->addEntry(TAG_ISOSPEEDRATINGS, 1, &iso,
-                TIFF_IFD_0), env, TAG_ISOSPEEDRATINGS);
+                TIFF_IFD_0), env, TAG_ISOSPEEDRATINGS, writer);
     }
 
     {
         // focal length
         camera_metadata_entry entry =
             results.find(ANDROID_LENS_FOCAL_LENGTH);
-        BAIL_IF_EMPTY(entry, env, TAG_FOCALLENGTH);
+        BAIL_IF_EMPTY(entry, env, TAG_FOCALLENGTH, writer);
 
         uint32_t focalLength[] = { static_cast<uint32_t>(*(entry.data.f) * 100), 100 };
         BAIL_IF_INVALID(writer->addEntry(TAG_FOCALLENGTH, 1, focalLength,
-                TIFF_IFD_0), env, TAG_FOCALLENGTH);
+                TIFF_IFD_0), env, TAG_FOCALLENGTH, writer);
     }
 
     {
         // f number
         camera_metadata_entry entry =
             results.find(ANDROID_LENS_APERTURE);
-        BAIL_IF_EMPTY(entry, env, TAG_FNUMBER);
+        BAIL_IF_EMPTY(entry, env, TAG_FNUMBER, writer);
 
         uint32_t fnum[] = { static_cast<uint32_t>(*(entry.data.f) * 100), 100 };
         BAIL_IF_INVALID(writer->addEntry(TAG_FNUMBER, 1, fnum,
-                TIFF_IFD_0), env, TAG_FNUMBER);
+                TIFF_IFD_0), env, TAG_FNUMBER, writer);
     }
 
     {
         // Set DNG version information
         uint8_t version[4] = {1, 4, 0, 0};
         BAIL_IF_INVALID(writer->addEntry(TAG_DNGVERSION, 4, version, TIFF_IFD_0),
-                env, TAG_DNGVERSION);
+                env, TAG_DNGVERSION, writer);
 
         uint8_t backwardVersion[4] = {1, 1, 0, 0};
         BAIL_IF_INVALID(writer->addEntry(TAG_DNGBACKWARDVERSION, 4, backwardVersion, TIFF_IFD_0),
-                env, TAG_DNGBACKWARDVERSION);
+                env, TAG_DNGBACKWARDVERSION, writer);
     }
 
     {
         // Set whitelevel
         camera_metadata_entry entry =
                 characteristics.find(ANDROID_SENSOR_INFO_WHITE_LEVEL);
-        BAIL_IF_EMPTY(entry, env, TAG_WHITELEVEL);
+        BAIL_IF_EMPTY(entry, env, TAG_WHITELEVEL, writer);
         uint32_t whiteLevel = static_cast<uint32_t>(entry.data.i32[0]);
         BAIL_IF_INVALID(writer->addEntry(TAG_WHITELEVEL, 1, &whiteLevel, TIFF_IFD_0), env,
-                TAG_WHITELEVEL);
+                TAG_WHITELEVEL, writer);
     }
 
     {
         // Set default scale
         uint32_t defaultScale[4] = {1, 1, 1, 1};
         BAIL_IF_INVALID(writer->addEntry(TAG_DEFAULTSCALE, 2, defaultScale, TIFF_IFD_0),
-                env, TAG_DEFAULTSCALE);
+                env, TAG_DEFAULTSCALE, writer);
     }
 
     bool singleIlluminant = false;
@@ -555,7 +1108,7 @@ static void DngCreator_init(JNIEnv* env, jobject thiz, jobject characteristicsPt
         // Set calibration illuminants
         camera_metadata_entry entry1 =
             characteristics.find(ANDROID_SENSOR_REFERENCE_ILLUMINANT1);
-        BAIL_IF_EMPTY(entry1, env, TAG_CALIBRATIONILLUMINANT1);
+        BAIL_IF_EMPTY(entry1, env, TAG_CALIBRATIONILLUMINANT1, writer);
         camera_metadata_entry entry2 =
             characteristics.find(ANDROID_SENSOR_REFERENCE_ILLUMINANT2);
         if (entry2.count == 0) {
@@ -564,12 +1117,12 @@ static void DngCreator_init(JNIEnv* env, jobject thiz, jobject characteristicsPt
         uint16_t ref1 = entry1.data.u8[0];
 
         BAIL_IF_INVALID(writer->addEntry(TAG_CALIBRATIONILLUMINANT1, 1, &ref1,
-                TIFF_IFD_0), env, TAG_CALIBRATIONILLUMINANT1);
+                TIFF_IFD_0), env, TAG_CALIBRATIONILLUMINANT1, writer);
 
         if (!singleIlluminant) {
             uint16_t ref2 = entry2.data.u8[0];
             BAIL_IF_INVALID(writer->addEntry(TAG_CALIBRATIONILLUMINANT2, 1, &ref2,
-                    TIFF_IFD_0), env, TAG_CALIBRATIONILLUMINANT2);
+                    TIFF_IFD_0), env, TAG_CALIBRATIONILLUMINANT2, writer);
         }
     }
 
@@ -577,7 +1130,7 @@ static void DngCreator_init(JNIEnv* env, jobject thiz, jobject characteristicsPt
         // Set color transforms
         camera_metadata_entry entry1 =
             characteristics.find(ANDROID_SENSOR_COLOR_TRANSFORM1);
-        BAIL_IF_EMPTY(entry1, env, TAG_COLORMATRIX1);
+        BAIL_IF_EMPTY(entry1, env, TAG_COLORMATRIX1, writer);
 
         int32_t colorTransform1[entry1.count * 2];
 
@@ -587,12 +1140,12 @@ static void DngCreator_init(JNIEnv* env, jobject thiz, jobject characteristicsPt
             colorTransform1[ctr++] = entry1.data.r[i].denominator;
         }
 
-        BAIL_IF_INVALID(writer->addEntry(TAG_COLORMATRIX1, entry1.count, colorTransform1, TIFF_IFD_0),
-                env, TAG_COLORMATRIX1);
+        BAIL_IF_INVALID(writer->addEntry(TAG_COLORMATRIX1, entry1.count, colorTransform1,
+                TIFF_IFD_0), env, TAG_COLORMATRIX1, writer);
 
         if (!singleIlluminant) {
             camera_metadata_entry entry2 = characteristics.find(ANDROID_SENSOR_COLOR_TRANSFORM2);
-            BAIL_IF_EMPTY(entry2, env, TAG_COLORMATRIX2);
+            BAIL_IF_EMPTY(entry2, env, TAG_COLORMATRIX2, writer);
             int32_t colorTransform2[entry2.count * 2];
 
             ctr = 0;
@@ -601,8 +1154,8 @@ static void DngCreator_init(JNIEnv* env, jobject thiz, jobject characteristicsPt
                 colorTransform2[ctr++] = entry2.data.r[i].denominator;
             }
 
-            BAIL_IF_INVALID(writer->addEntry(TAG_COLORMATRIX2, entry2.count, colorTransform2, TIFF_IFD_0),
-                    env, TAG_COLORMATRIX2);
+            BAIL_IF_INVALID(writer->addEntry(TAG_COLORMATRIX2, entry2.count, colorTransform2,
+                    TIFF_IFD_0), env, TAG_COLORMATRIX2, writer);
         }
     }
 
@@ -610,7 +1163,7 @@ static void DngCreator_init(JNIEnv* env, jobject thiz, jobject characteristicsPt
         // Set calibration transforms
         camera_metadata_entry entry1 =
             characteristics.find(ANDROID_SENSOR_CALIBRATION_TRANSFORM1);
-        BAIL_IF_EMPTY(entry1, env, TAG_CAMERACALIBRATION1);
+        BAIL_IF_EMPTY(entry1, env, TAG_CAMERACALIBRATION1, writer);
 
         int32_t calibrationTransform1[entry1.count * 2];
 
@@ -621,12 +1174,12 @@ static void DngCreator_init(JNIEnv* env, jobject thiz, jobject characteristicsPt
         }
 
         BAIL_IF_INVALID(writer->addEntry(TAG_CAMERACALIBRATION1, entry1.count, calibrationTransform1,
-                TIFF_IFD_0), env, TAG_CAMERACALIBRATION1);
+                TIFF_IFD_0), env, TAG_CAMERACALIBRATION1, writer);
 
         if (!singleIlluminant) {
             camera_metadata_entry entry2 =
                 characteristics.find(ANDROID_SENSOR_CALIBRATION_TRANSFORM2);
-            BAIL_IF_EMPTY(entry2, env, TAG_CAMERACALIBRATION2);
+            BAIL_IF_EMPTY(entry2, env, TAG_CAMERACALIBRATION2, writer);
             int32_t calibrationTransform2[entry2.count * 2];
 
             ctr = 0;
@@ -636,7 +1189,7 @@ static void DngCreator_init(JNIEnv* env, jobject thiz, jobject characteristicsPt
             }
 
             BAIL_IF_INVALID(writer->addEntry(TAG_CAMERACALIBRATION2, entry2.count, calibrationTransform1,
-                    TIFF_IFD_0),  env, TAG_CAMERACALIBRATION2);
+                    TIFF_IFD_0),  env, TAG_CAMERACALIBRATION2, writer);
         }
     }
 
@@ -644,7 +1197,7 @@ static void DngCreator_init(JNIEnv* env, jobject thiz, jobject characteristicsPt
         // Set forward transforms
         camera_metadata_entry entry1 =
             characteristics.find(ANDROID_SENSOR_FORWARD_MATRIX1);
-        BAIL_IF_EMPTY(entry1, env, TAG_FORWARDMATRIX1);
+        BAIL_IF_EMPTY(entry1, env, TAG_FORWARDMATRIX1, writer);
 
         int32_t forwardTransform1[entry1.count * 2];
 
@@ -655,12 +1208,12 @@ static void DngCreator_init(JNIEnv* env, jobject thiz, jobject characteristicsPt
         }
 
         BAIL_IF_INVALID(writer->addEntry(TAG_FORWARDMATRIX1, entry1.count, forwardTransform1,
-                TIFF_IFD_0), env, TAG_FORWARDMATRIX1);
+                TIFF_IFD_0), env, TAG_FORWARDMATRIX1, writer);
 
         if (!singleIlluminant) {
             camera_metadata_entry entry2 =
                 characteristics.find(ANDROID_SENSOR_FORWARD_MATRIX2);
-            BAIL_IF_EMPTY(entry2, env, TAG_FORWARDMATRIX2);
+            BAIL_IF_EMPTY(entry2, env, TAG_FORWARDMATRIX2, writer);
             int32_t forwardTransform2[entry2.count * 2];
 
             ctr = 0;
@@ -670,7 +1223,7 @@ static void DngCreator_init(JNIEnv* env, jobject thiz, jobject characteristicsPt
             }
 
             BAIL_IF_INVALID(writer->addEntry(TAG_FORWARDMATRIX2, entry2.count, forwardTransform2,
-                    TIFF_IFD_0),  env, TAG_FORWARDMATRIX2);
+                    TIFF_IFD_0),  env, TAG_FORWARDMATRIX2, writer);
         }
     }
 
@@ -678,7 +1231,7 @@ static void DngCreator_init(JNIEnv* env, jobject thiz, jobject characteristicsPt
         // Set camera neutral
         camera_metadata_entry entry =
             results.find(ANDROID_SENSOR_NEUTRAL_COLOR_POINT);
-        BAIL_IF_EMPTY(entry, env, TAG_ASSHOTNEUTRAL);
+        BAIL_IF_EMPTY(entry, env, TAG_ASSHOTNEUTRAL, writer);
         uint32_t cameraNeutral[entry.count * 2];
 
         size_t ctr = 0;
@@ -690,23 +1243,18 @@ static void DngCreator_init(JNIEnv* env, jobject thiz, jobject characteristicsPt
         }
 
         BAIL_IF_INVALID(writer->addEntry(TAG_ASSHOTNEUTRAL, entry.count, cameraNeutral,
-                TIFF_IFD_0), env, TAG_ASSHOTNEUTRAL);
+                TIFF_IFD_0), env, TAG_ASSHOTNEUTRAL, writer);
     }
 
     {
         // Setup data strips
         // TODO: Switch to tiled implementation.
-        uint32_t offset = 0;
-        BAIL_IF_INVALID(writer->addEntry(TAG_STRIPOFFSETS, 1, &offset, TIFF_IFD_0), env,
-                TAG_STRIPOFFSETS);
-
-        BAIL_IF_INVALID(writer->addEntry(TAG_ROWSPERSTRIP, 1, &imageHeight, TIFF_IFD_0), env,
-                TAG_ROWSPERSTRIP);
-
-        uint32_t byteCount = imageWidth * imageHeight * bitsPerSample * samplesPerPixel /
-                bitsPerByte;
-        BAIL_IF_INVALID(writer->addEntry(TAG_STRIPBYTECOUNTS, 1, &byteCount, TIFF_IFD_0), env,
-                TAG_STRIPBYTECOUNTS);
+        if (writer->addStrip(TIFF_IFD_0) != OK) {
+            ALOGE("%s: Could not setup strip tags.", __FUNCTION__);
+            jniThrowException(env, "java/lang/IllegalStateException",
+                    "Failed to setup strip tags.");
+            return;
+        }
     }
 
     {
@@ -717,9 +1265,9 @@ static void DngCreator_init(JNIEnv* env, jobject thiz, jobject characteristicsPt
             uint32_t defaultCropOrigin[] = {margin, margin};
             uint32_t defaultCropSize[] = {imageWidth - margin, imageHeight - margin};
             BAIL_IF_INVALID(writer->addEntry(TAG_DEFAULTCROPORIGIN, 2, defaultCropOrigin,
-                    TIFF_IFD_0), env, TAG_DEFAULTCROPORIGIN);
+                    TIFF_IFD_0), env, TAG_DEFAULTCROPORIGIN, writer);
             BAIL_IF_INVALID(writer->addEntry(TAG_DEFAULTCROPSIZE, 2, defaultCropSize,
-                    TIFF_IFD_0), env, TAG_DEFAULTCROPSIZE);
+                    TIFF_IFD_0), env, TAG_DEFAULTCROPSIZE, writer);
         }
     }
 
@@ -742,21 +1290,26 @@ static void DngCreator_init(JNIEnv* env, jobject thiz, jobject characteristicsPt
 
         BAIL_IF_INVALID(writer->addEntry(TAG_UNIQUECAMERAMODEL, cameraModel.size() + 1,
                 reinterpret_cast<const uint8_t*>(cameraModel.string()), TIFF_IFD_0), env,
-                TAG_UNIQUECAMERAMODEL);
+                TAG_UNIQUECAMERAMODEL, writer);
     }
 
     {
         // Setup opcode List 2
         camera_metadata_entry entry1 =
                 characteristics.find(ANDROID_LENS_INFO_SHADING_MAP_SIZE);
-        BAIL_IF_EMPTY(entry1, env, TAG_OPCODELIST2);
-        uint32_t lsmWidth = static_cast<uint32_t>(entry1.data.i32[0]);
-        uint32_t lsmHeight = static_cast<uint32_t>(entry1.data.i32[1]);
+
+        uint32_t lsmWidth = 0;
+        uint32_t lsmHeight = 0;
+
+        if (entry1.count != 0) {
+            lsmWidth = static_cast<uint32_t>(entry1.data.i32[0]);
+            lsmHeight = static_cast<uint32_t>(entry1.data.i32[1]);
+        }
 
         camera_metadata_entry entry2 =
                 results.find(ANDROID_STATISTICS_LENS_SHADING_MAP);
-        BAIL_IF_EMPTY(entry2, env, TAG_OPCODELIST2);
-        if (entry2.count == lsmWidth * lsmHeight * 4) {
+
+        if (entry2.count > 0 && entry2.count == lsmWidth * lsmHeight * 4) {
 
             OpcodeListBuilder builder;
             status_t err = builder.addGainMapsForMetadata(lsmWidth,
@@ -773,7 +1326,7 @@ static void DngCreator_init(JNIEnv* env, jobject thiz, jobject characteristicsPt
                 err = builder.buildOpList(opcodeListBuf);
                 if (err == OK) {
                     BAIL_IF_INVALID(writer->addEntry(TAG_OPCODELIST2, listSize, opcodeListBuf,
-                            TIFF_IFD_0), env, TAG_OPCODELIST2);
+                            TIFF_IFD_0), env, TAG_OPCODELIST2, writer);
                 } else {
                     ALOGE("%s: Could not build Lens shading map opcode.", __FUNCTION__);
                     jniThrowRuntimeException(env, "failed to construct lens shading map opcode.");
@@ -783,138 +1336,505 @@ static void DngCreator_init(JNIEnv* env, jobject thiz, jobject characteristicsPt
                 jniThrowRuntimeException(env, "failed to add lens shading map.");
             }
         } else {
-            ALOGW("%s: Lens shading map not present in results, skipping...", __FUNCTION__);
+            ALOGW("%s: No lens shading map found in result metadata. Image quality may be reduced.",
+                    __FUNCTION__);
         }
     }
 
-    DngCreator_setCreator(env, thiz, writer);
+    DngCreator_setNativeContext(env, thiz, nativeContext);
 }
 
 static void DngCreator_destroy(JNIEnv* env, jobject thiz) {
     ALOGV("%s:", __FUNCTION__);
-    DngCreator_setCreator(env, thiz, NULL);
+    DngCreator_setNativeContext(env, thiz, NULL);
 }
 
-static void DngCreator_nativeSetOrientation(JNIEnv* env, jobject thiz) {
+static void DngCreator_nativeSetOrientation(JNIEnv* env, jobject thiz, jint orient) {
     ALOGV("%s:", __FUNCTION__);
-    jniThrowRuntimeException(env, "nativeSetOrientation is not implemented");
-}
 
-static void DngCreator_nativeSetThumbnailBitmap(JNIEnv* env, jobject thiz, jobject bitmap) {
-    ALOGV("%s:", __FUNCTION__);
-    jniThrowRuntimeException(env, "nativeSetThumbnailBitmap is not implemented");
+    TiffWriter* writer = DngCreator_getCreator(env, thiz);
+    if (writer == NULL) {
+        ALOGE("%s: Failed to initialize DngCreator", __FUNCTION__);
+        jniThrowException(env, "java/lang/AssertionError",
+                "setOrientation called with uninitialized DngCreator");
+        return;
+    }
+
+    uint16_t orientation = static_cast<uint16_t>(orient);
+    BAIL_IF_INVALID(writer->addEntry(TAG_ORIENTATION, 1, &orientation, TIFF_IFD_0), env,
+                TAG_ORIENTATION, writer);
+
+    // Set main image orientation also if in a separate IFD
+    if (writer->hasIfd(TIFF_IFD_SUB1)) {
+        BAIL_IF_INVALID(writer->addEntry(TAG_ORIENTATION, 1, &orientation, TIFF_IFD_SUB1), env,
+                    TAG_ORIENTATION, writer);
+    }
 }
 
-static void DngCreator_nativeSetThumbnailImage(JNIEnv* env, jobject thiz, jint width, jint height,
-        jobject yBuffer, jint yRowStride, jint yPixStride, jobject uBuffer, jint uRowStride,
-        jint uPixStride, jobject vBuffer, jint vRowStride, jint vPixStride) {
+static void DngCreator_nativeSetDescription(JNIEnv* env, jobject thiz, jstring description) {
     ALOGV("%s:", __FUNCTION__);
-    jniThrowRuntimeException(env, "nativeSetThumbnailImage is not implemented");
+
+    TiffWriter* writer = DngCreator_getCreator(env, thiz);
+    if (writer == NULL) {
+        ALOGE("%s: Failed to initialize DngCreator", __FUNCTION__);
+        jniThrowException(env, "java/lang/AssertionError",
+                "setDescription called with uninitialized DngCreator");
+        return;
+    }
+
+    const char* desc = env->GetStringUTFChars(description, NULL);
+    size_t len = strlen(desc) + 1;
+
+    if (writer->addEntry(TAG_IMAGEDESCRIPTION, len,
+            reinterpret_cast<const uint8_t*>(desc), TIFF_IFD_0) != OK) {
+        jniThrowExceptionFmt(env, "java/lang/IllegalArgumentException",
+                "Invalid metadata for tag %x", TAG_IMAGEDESCRIPTION);
+    }
+
+    env->ReleaseStringUTFChars(description, desc);
 }
 
-static void DngCreator_nativeWriteImage(JNIEnv* env, jobject thiz, jobject outStream, jint width,
-        jint height, jobject inBuffer, jint rowStride, jint pixStride) {
+static void DngCreator_nativeSetGpsTags(JNIEnv* env, jobject thiz, jintArray latTag, jstring latRef,
+        jintArray longTag, jstring longRef, jstring dateTag, jintArray timeTag) {
     ALOGV("%s:", __FUNCTION__);
 
-    sp<JniOutputStream> out = new JniOutputStream(env, outStream);
-    if(env->ExceptionCheck()) {
-        ALOGE("%s: Could not allocate buffers for output stream", __FUNCTION__);
+    TiffWriter* writer = DngCreator_getCreator(env, thiz);
+    if (writer == NULL) {
+        ALOGE("%s: Failed to initialize DngCreator", __FUNCTION__);
+        jniThrowException(env, "java/lang/AssertionError",
+                "setGpsTags called with uninitialized DngCreator");
         return;
     }
 
-    uint8_t* pixelBytes = reinterpret_cast<uint8_t*>(env->GetDirectBufferAddress(inBuffer));
-    if (pixelBytes == NULL) {
-        ALOGE("%s: Could not get native byte buffer", __FUNCTION__);
-        jniThrowException(env, "java/lang/IllegalArgumentException", "Invalid bytebuffer");
+    if (!writer->hasIfd(TIFF_IFD_GPSINFO)) {
+        if (writer->addSubIfd(TIFF_IFD_0, TIFF_IFD_GPSINFO, TiffWriter::GPSINFO) != OK) {
+            ALOGE("%s: Failed to add GpsInfo IFD %u to IFD %u", __FUNCTION__, TIFF_IFD_GPSINFO,
+                    TIFF_IFD_0);
+            jniThrowException(env, "java/lang/IllegalStateException", "Failed to add GPSINFO");
+            return;
+        }
+    }
+
+    const jsize GPS_VALUE_LENGTH = 6;
+    jsize latLen = env->GetArrayLength(latTag);
+    jsize longLen = env->GetArrayLength(longTag);
+    jsize timeLen = env->GetArrayLength(timeTag);
+    if (latLen != GPS_VALUE_LENGTH) {
+        jniThrowException(env, "java/lang/IllegalArgumentException",
+                "invalid latitude tag length");
+        return;
+    } else if (longLen != GPS_VALUE_LENGTH) {
+        jniThrowException(env, "java/lang/IllegalArgumentException",
+                "invalid longitude tag length");
         return;
+    } else if (timeLen != GPS_VALUE_LENGTH) {
+        jniThrowException(env, "java/lang/IllegalArgumentException",
+                "invalid time tag length");
+        return;
+    }
+
+    uint32_t latitude[GPS_VALUE_LENGTH];
+    uint32_t longitude[GPS_VALUE_LENGTH];
+    uint32_t timestamp[GPS_VALUE_LENGTH];
+
+    env->GetIntArrayRegion(latTag, 0, static_cast<jsize>(GPS_VALUE_LENGTH),
+            reinterpret_cast<jint*>(&latitude));
+    env->GetIntArrayRegion(longTag, 0, static_cast<jsize>(GPS_VALUE_LENGTH),
+            reinterpret_cast<jint*>(&longitude));
+    env->GetIntArrayRegion(timeTag, 0, static_cast<jsize>(GPS_VALUE_LENGTH),
+            reinterpret_cast<jint*>(&timestamp));
+
+    const jsize GPS_REF_LENGTH = 2;
+    const jsize GPS_DATE_LENGTH = 11;
+    uint8_t latitudeRef[GPS_REF_LENGTH];
+    uint8_t longitudeRef[GPS_REF_LENGTH];
+    uint8_t date[GPS_DATE_LENGTH];
+
+    env->GetStringUTFRegion(latRef, 0, 1, reinterpret_cast<char*>(&latitudeRef));
+    latitudeRef[GPS_REF_LENGTH - 1] = '\0';
+    env->GetStringUTFRegion(longRef, 0, 1, reinterpret_cast<char*>(&longitudeRef));
+    longitudeRef[GPS_REF_LENGTH - 1] = '\0';
+
+    env->GetStringUTFRegion(dateTag, 0, GPS_DATE_LENGTH - 1, reinterpret_cast<char*>(&date));
+    date[GPS_DATE_LENGTH - 1] = '\0';
+
+    {
+        uint8_t version[] = {2, 3, 0, 0};
+        BAIL_IF_INVALID(writer->addEntry(TAG_GPSVERSIONID, 4, version,
+                TIFF_IFD_GPSINFO), env, TAG_GPSVERSIONID, writer);
+    }
+
+    {
+        BAIL_IF_INVALID(writer->addEntry(TAG_GPSLATITUDEREF, GPS_REF_LENGTH, latitudeRef,
+                TIFF_IFD_GPSINFO), env, TAG_GPSLATITUDEREF, writer);
+    }
+
+    {
+        BAIL_IF_INVALID(writer->addEntry(TAG_GPSLONGITUDEREF, GPS_REF_LENGTH, longitudeRef,
+                TIFF_IFD_GPSINFO), env, TAG_GPSLONGITUDEREF, writer);
     }
 
+    {
+        BAIL_IF_INVALID(writer->addEntry(TAG_GPSLATITUDE, 3, latitude,
+                TIFF_IFD_GPSINFO), env, TAG_GPSLATITUDE, writer);
+    }
+
+    {
+        BAIL_IF_INVALID(writer->addEntry(TAG_GPSLONGITUDE, 3, longitude,
+                TIFF_IFD_GPSINFO), env, TAG_GPSLONGITUDE, writer);
+    }
+
+    {
+        BAIL_IF_INVALID(writer->addEntry(TAG_GPSTIMESTAMP, 3, timestamp,
+                TIFF_IFD_GPSINFO), env, TAG_GPSTIMESTAMP, writer);
+    }
+
+    {
+        BAIL_IF_INVALID(writer->addEntry(TAG_GPSDATESTAMP, GPS_DATE_LENGTH, date,
+                TIFF_IFD_GPSINFO), env, TAG_GPSDATESTAMP, writer);
+    }
+}
+
+static void DngCreator_nativeSetThumbnail(JNIEnv* env, jobject thiz, jobject buffer, jint width,
+        jint height) {
+    ALOGV("%s:", __FUNCTION__);
+
+    NativeContext* context = DngCreator_getNativeContext(env, thiz);
     TiffWriter* writer = DngCreator_getCreator(env, thiz);
-    if (writer == NULL) {
+    if (writer == NULL || context == NULL) {
         ALOGE("%s: Failed to initialize DngCreator", __FUNCTION__);
         jniThrowException(env, "java/lang/AssertionError",
-                "Write called with uninitialized DngCreator");
+                "setThumbnail called with uninitialized DngCreator");
         return;
     }
-    // TODO: handle lens shading map, etc. conversions for other raw buffer sizes.
-    uint32_t metadataWidth = *(writer->getEntry(TAG_IMAGEWIDTH, TIFF_IFD_0)->getData<uint32_t>());
-    uint32_t metadataHeight = *(writer->getEntry(TAG_IMAGELENGTH, TIFF_IFD_0)->getData<uint32_t>());
-    if (metadataWidth != width) {
-        jniThrowExceptionFmt(env, "java/lang/IllegalStateException", \
-                        "Metadata width %d doesn't match image width %d", metadataWidth, width);
+
+    size_t fullSize = width * height * BYTES_PER_RGB_PIXEL;
+    jlong capacity = env->GetDirectBufferCapacity(buffer);
+    if (capacity != fullSize) {
+        jniThrowExceptionFmt(env, "java/lang/AssertionError",
+                "Invalid size %d for thumbnail, expected size was %d",
+                capacity, fullSize);
         return;
     }
 
-    if (metadataHeight != height) {
-        jniThrowExceptionFmt(env, "java/lang/IllegalStateException", \
-                        "Metadata height %d doesn't match image height %d", metadataHeight, height);
+    uint8_t* pixelBytes = reinterpret_cast<uint8_t*>(env->GetDirectBufferAddress(buffer));
+    if (pixelBytes == NULL) {
+        ALOGE("%s: Could not get native ByteBuffer", __FUNCTION__);
+        jniThrowException(env, "java/lang/IllegalArgumentException", "Invalid ByteBuffer");
         return;
     }
 
-    uint32_t stripOffset = writer->getTotalSize();
+    if (!writer->hasIfd(TIFF_IFD_SUB1)) {
+        if (writer->addSubIfd(TIFF_IFD_0, TIFF_IFD_SUB1) != OK) {
+            ALOGE("%s: Failed to add SubIFD %u to IFD %u", __FUNCTION__, TIFF_IFD_SUB1,
+                    TIFF_IFD_0);
+            jniThrowException(env, "java/lang/IllegalStateException", "Failed to add SubIFD");
+            return;
+        }
 
-    BAIL_IF_INVALID(writer->addEntry(TAG_STRIPOFFSETS, 1, &stripOffset, TIFF_IFD_0), env,
-                    TAG_STRIPOFFSETS);
+        Vector<uint16_t> tagsToMove;
+        tagsToMove.add(TAG_ORIENTATION);
+        tagsToMove.add(TAG_NEWSUBFILETYPE);
+        tagsToMove.add(TAG_BITSPERSAMPLE);
+        tagsToMove.add(TAG_COMPRESSION);
+        tagsToMove.add(TAG_IMAGEWIDTH);
+        tagsToMove.add(TAG_IMAGELENGTH);
+        tagsToMove.add(TAG_PHOTOMETRICINTERPRETATION);
+        tagsToMove.add(TAG_BLACKLEVEL);
+        tagsToMove.add(TAG_BLACKLEVELREPEATDIM);
+        tagsToMove.add(TAG_SAMPLESPERPIXEL);
+        tagsToMove.add(TAG_PLANARCONFIGURATION);
+        tagsToMove.add(TAG_CFAREPEATPATTERNDIM);
+        tagsToMove.add(TAG_CFAPATTERN);
+        tagsToMove.add(TAG_CFAPLANECOLOR);
+        tagsToMove.add(TAG_CFALAYOUT);
+        tagsToMove.add(TAG_XRESOLUTION);
+        tagsToMove.add(TAG_YRESOLUTION);
+        tagsToMove.add(TAG_RESOLUTIONUNIT);
+        tagsToMove.add(TAG_WHITELEVEL);
+        tagsToMove.add(TAG_DEFAULTSCALE);
+        tagsToMove.add(TAG_ROWSPERSTRIP);
+        tagsToMove.add(TAG_STRIPBYTECOUNTS);
+        tagsToMove.add(TAG_STRIPOFFSETS);
+        tagsToMove.add(TAG_DEFAULTCROPORIGIN);
+        tagsToMove.add(TAG_DEFAULTCROPSIZE);
+        tagsToMove.add(TAG_OPCODELIST2);
+
+        if (moveEntries(writer, TIFF_IFD_0, TIFF_IFD_SUB1, tagsToMove) != OK) {
+            jniThrowException(env, "java/lang/IllegalStateException", "Failed to move entries");
+            return;
+        }
 
-    if (writer->write(out.get()) != OK) {
-        if (!env->ExceptionCheck()) {
-            jniThrowException(env, "java/io/IOException", "Failed to write metadata");
+        // Make sure both IFDs get the same orientation tag
+        sp<TiffEntry> orientEntry = writer->getEntry(TAG_ORIENTATION, TIFF_IFD_SUB1);
+        if (orientEntry != NULL) {
+            writer->addEntry(orientEntry, TIFF_IFD_0);
         }
+    }
+
+    // Setup thumbnail tags
+
+    {
+        // Set photometric interpretation
+        uint16_t interpretation = 2; // RGB
+        BAIL_IF_INVALID(writer->addEntry(TAG_PHOTOMETRICINTERPRETATION, 1, &interpretation,
+                TIFF_IFD_0), env, TAG_PHOTOMETRICINTERPRETATION, writer);
+    }
+
+    {
+        // Set planar configuration
+        uint16_t config = 1; // Chunky
+        BAIL_IF_INVALID(writer->addEntry(TAG_PLANARCONFIGURATION, 1, &config, TIFF_IFD_0),
+                env, TAG_PLANARCONFIGURATION, writer);
+    }
+
+    {
+        // Set samples per pixel
+        uint16_t samples = SAMPLES_PER_RGB_PIXEL;
+        BAIL_IF_INVALID(writer->addEntry(TAG_SAMPLESPERPIXEL, 1, &samples, TIFF_IFD_0),
+                env, TAG_SAMPLESPERPIXEL, writer);
+    }
+
+    {
+        // Set bits per sample
+        uint16_t bits = BITS_PER_RGB_SAMPLE;
+        BAIL_IF_INVALID(writer->addEntry(TAG_BITSPERSAMPLE, 1, &bits, TIFF_IFD_0), env,
+                TAG_BITSPERSAMPLE, writer);
+    }
+
+    {
+        // Set subfiletype
+        uint32_t subfileType = 1; // Thumbnail image
+        BAIL_IF_INVALID(writer->addEntry(TAG_NEWSUBFILETYPE, 1, &subfileType, TIFF_IFD_0), env,
+                TAG_NEWSUBFILETYPE, writer);
+    }
+
+    {
+        // Set compression
+        uint16_t compression = 1; // None
+        BAIL_IF_INVALID(writer->addEntry(TAG_COMPRESSION, 1, &compression, TIFF_IFD_0), env,
+                TAG_COMPRESSION, writer);
+    }
+
+    {
+        // Set dimensions
+        uint32_t uWidth = static_cast<uint32_t>(width);
+        uint32_t uHeight = static_cast<uint32_t>(height);
+        BAIL_IF_INVALID(writer->addEntry(TAG_IMAGEWIDTH, 1, &uWidth, TIFF_IFD_0), env,
+                TAG_IMAGEWIDTH, writer);
+        BAIL_IF_INVALID(writer->addEntry(TAG_IMAGELENGTH, 1, &uHeight, TIFF_IFD_0), env,
+                TAG_IMAGELENGTH, writer);
+    }
+
+    {
+        // x resolution
+        uint32_t xres[] = { 72, 1 }; // default 72 ppi
+        BAIL_IF_INVALID(writer->addEntry(TAG_XRESOLUTION, 1, xres, TIFF_IFD_0),
+                env, TAG_XRESOLUTION, writer);
+
+        // y resolution
+        uint32_t yres[] = { 72, 1 }; // default 72 ppi
+        BAIL_IF_INVALID(writer->addEntry(TAG_YRESOLUTION, 1, yres, TIFF_IFD_0),
+                env, TAG_YRESOLUTION, writer);
+
+        uint16_t unit = 2; // inches
+        BAIL_IF_INVALID(writer->addEntry(TAG_RESOLUTIONUNIT, 1, &unit, TIFF_IFD_0),
+                env, TAG_RESOLUTIONUNIT, writer);
+    }
+
+    {
+        // Setup data strips
+        if (writer->addStrip(TIFF_IFD_0) != OK) {
+            ALOGE("%s: Could not setup thumbnail strip tags.", __FUNCTION__);
+            jniThrowException(env, "java/lang/IllegalStateException",
+                    "Failed to setup thumbnail strip tags.");
+            return;
+        }
+        if (writer->addStrip(TIFF_IFD_SUB1) != OK) {
+            ALOGE("%s: Could not main image strip tags.", __FUNCTION__);
+            jniThrowException(env, "java/lang/IllegalStateException",
+                    "Failed to setup main image strip tags.");
+            return;
+        }
+    }
+
+    if (!context->setThumbnail(pixelBytes, width, height)) {
+        jniThrowException(env, "java/lang/IllegalStateException",
+                "Failed to set thumbnail.");
         return;
     }
+}
 
-    size_t fullSize = rowStride * height;
-    jlong capacity = env->GetDirectBufferCapacity(inBuffer);
-    if (capacity < 0 || fullSize > capacity) {
-        jniThrowExceptionFmt(env, "java/lang/IllegalStateException",
-                "Invalid size %d for Image, size given in metadata is %d at current stride",
-                capacity, fullSize);
+// TODO: Refactor out common preamble for the two nativeWrite methods.
+static void DngCreator_nativeWriteImage(JNIEnv* env, jobject thiz, jobject outStream, jint width,
+        jint height, jobject inBuffer, jint rowStride, jint pixStride, jlong offset,
+        jboolean isDirect) {
+    ALOGV("%s:", __FUNCTION__);
+    ALOGV("%s: nativeWriteImage called with: width=%d, height=%d, rowStride=%d, pixStride=%d,"
+              " offset=%lld", __FUNCTION__, width, height, rowStride, pixStride, offset);
+    uint32_t rStride = static_cast<uint32_t>(rowStride);
+    uint32_t pStride = static_cast<uint32_t>(pixStride);
+    uint32_t uWidth = static_cast<uint32_t>(width);
+    uint32_t uHeight = static_cast<uint32_t>(height);
+    uint64_t uOffset = static_cast<uint64_t>(offset);
+
+    sp<JniOutputStream> out = new JniOutputStream(env, outStream);
+    if(env->ExceptionCheck()) {
+        ALOGE("%s: Could not allocate buffers for output stream", __FUNCTION__);
         return;
     }
 
-    if (pixStride == BYTES_PER_SAMPLE && rowStride == width * BYTES_PER_SAMPLE) {
-        if (out->write(pixelBytes, 0, fullSize) != OK || env->ExceptionCheck()) {
-            if (!env->ExceptionCheck()) {
-                jniThrowException(env, "java/io/IOException", "Failed to write pixel data");
-            }
+    TiffWriter* writer = DngCreator_getCreator(env, thiz);
+    NativeContext* context = DngCreator_getNativeContext(env, thiz);
+    if (writer == NULL || context == NULL) {
+        ALOGE("%s: Failed to initialize DngCreator", __FUNCTION__);
+        jniThrowException(env, "java/lang/AssertionError",
+                "Write called with uninitialized DngCreator");
+        return;
+    }
+
+    // Validate DNG header
+    if (!validateDngHeader(env, writer, width, height)) {
+        return;
+    }
+
+    sp<JniInputByteBuffer> inBuf;
+    Vector<StripSource*> sources;
+    sp<DirectStripSource> thumbnailSource;
+    uint32_t targetIfd = TIFF_IFD_0;
+
+    bool hasThumbnail = writer->hasIfd(TIFF_IFD_SUB1);
+
+    if (hasThumbnail) {
+        ALOGV("%s: Adding thumbnail strip sources.", __FUNCTION__);
+        uint32_t bytesPerPixel = SAMPLES_PER_RGB_PIXEL * BYTES_PER_RGB_SAMPLE;
+        uint32_t thumbWidth = context->getThumbnailWidth();
+        thumbnailSource = new DirectStripSource(env, context->getThumbnail(), TIFF_IFD_0,
+                thumbWidth, context->getThumbnailHeight(), bytesPerPixel,
+                bytesPerPixel * thumbWidth, /*offset*/0, BYTES_PER_RGB_SAMPLE,
+                SAMPLES_PER_RGB_PIXEL);
+        sources.add(thumbnailSource.get());
+        targetIfd = TIFF_IFD_SUB1;
+    }
+
+    if (isDirect) {
+        size_t fullSize = rStride * uHeight;
+        jlong capacity = env->GetDirectBufferCapacity(inBuffer);
+        if (capacity < 0 || fullSize + uOffset > static_cast<uint64_t>(capacity)) {
+            jniThrowExceptionFmt(env, "java/lang/IllegalStateException",
+                    "Invalid size %d for Image, size given in metadata is %d at current stride",
+                    capacity, fullSize);
             return;
         }
-    } else if (pixStride == BYTES_PER_SAMPLE) {
-        for (size_t i = 0; i < height; ++i) {
-            if (out->write(pixelBytes, i * rowStride, pixStride * width) != OK ||
-                        env->ExceptionCheck()) {
-                if (!env->ExceptionCheck()) {
-                    jniThrowException(env, "java/io/IOException", "Failed to write pixel data");
-                }
-                return;
+
+        uint8_t* pixelBytes = reinterpret_cast<uint8_t*>(env->GetDirectBufferAddress(inBuffer));
+        if (pixelBytes == NULL) {
+            ALOGE("%s: Could not get native ByteBuffer", __FUNCTION__);
+            jniThrowException(env, "java/lang/IllegalArgumentException", "Invalid ByteBuffer");
+            return;
+        }
+
+        ALOGV("%s: Using direct-type strip source.", __FUNCTION__);
+        DirectStripSource stripSource(env, pixelBytes, targetIfd, uWidth, uHeight, pStride,
+                rStride, uOffset, BYTES_PER_SAMPLE, SAMPLES_PER_RAW_PIXEL);
+        sources.add(&stripSource);
+
+        status_t ret = OK;
+        if ((ret = writer->write(out.get(), sources.editArray(), sources.size())) != OK) {
+            ALOGE("%s: write failed with error %d.", __FUNCTION__, ret);
+            if (!env->ExceptionCheck()) {
+                jniThrowExceptionFmt(env, "java/io/IOException",
+                        "Encountered error %d while writing file.", ret);
             }
+            return;
         }
     } else {
-        for (size_t i = 0; i < height; ++i) {
-            for (size_t j = 0; j < width; ++j) {
-                if (out->write(pixelBytes, i * rowStride + j * pixStride,
-                        BYTES_PER_SAMPLE) != OK || !env->ExceptionCheck()) {
-                    if (env->ExceptionCheck()) {
-                        jniThrowException(env, "java/io/IOException", "Failed to write pixel data");
-                    }
-                    return;
-                }
+        inBuf = new JniInputByteBuffer(env, inBuffer);
+
+        ALOGV("%s: Using input-type strip source.", __FUNCTION__);
+        InputStripSource stripSource(env, *inBuf, targetIfd, uWidth, uHeight, pStride,
+                 rStride, uOffset, BYTES_PER_SAMPLE, SAMPLES_PER_RAW_PIXEL);
+        sources.add(&stripSource);
+
+        status_t ret = OK;
+        if ((ret = writer->write(out.get(), sources.editArray(), sources.size())) != OK) {
+            ALOGE("%s: write failed with error %d.", __FUNCTION__, ret);
+            if (!env->ExceptionCheck()) {
+                jniThrowExceptionFmt(env, "java/io/IOException",
+                        "Encountered error %d while writing file.", ret);
             }
+            return;
         }
     }
-
-}
-
-static void DngCreator_nativeWriteByteBuffer(JNIEnv* env, jobject thiz, jobject outStream,
-        jobject rawBuffer, jlong offset) {
-    ALOGV("%s:", __FUNCTION__);
-    jniThrowRuntimeException(env, "nativeWriteByteBuffer is not implemented.");
 }
 
 static void DngCreator_nativeWriteInputStream(JNIEnv* env, jobject thiz, jobject outStream,
-        jobject inStream, jlong offset) {
+        jobject inStream, jint width, jint height, jlong offset) {
     ALOGV("%s:", __FUNCTION__);
-    jniThrowRuntimeException(env, "nativeWriteInputStream is not implemented.");
+
+    uint32_t rowStride = width * BYTES_PER_SAMPLE;
+    uint32_t pixStride = BYTES_PER_SAMPLE;
+    uint32_t uWidth = static_cast<uint32_t>(width);
+    uint32_t uHeight = static_cast<uint32_t>(height);
+    uint64_t uOffset = static_cast<uint32_t>(offset);
+
+    ALOGV("%s: nativeWriteInputStream called with: width=%d, height=%d, rowStride=%u,"
+              "pixStride=%u, offset=%lld", __FUNCTION__, width, height, rowStride, pixStride,
+              offset);
+
+    sp<JniOutputStream> out = new JniOutputStream(env, outStream);
+    if(env->ExceptionCheck()) {
+        ALOGE("%s: Could not allocate buffers for output stream", __FUNCTION__);
+        return;
+    }
+
+    TiffWriter* writer = DngCreator_getCreator(env, thiz);
+    NativeContext* context = DngCreator_getNativeContext(env, thiz);
+    if (writer == NULL || context == NULL) {
+        ALOGE("%s: Failed to initialize DngCreator", __FUNCTION__);
+        jniThrowException(env, "java/lang/AssertionError",
+                "Write called with uninitialized DngCreator");
+        return;
+    }
+
+    // Validate DNG header
+    if (!validateDngHeader(env, writer, width, height)) {
+        return;
+    }
+
+    sp<DirectStripSource> thumbnailSource;
+    uint32_t targetIfd = TIFF_IFD_0;
+    bool hasThumbnail = writer->hasIfd(TIFF_IFD_SUB1);
+    Vector<StripSource*> sources;
+
+    if (hasThumbnail) {
+        ALOGV("%s: Adding thumbnail strip sources.", __FUNCTION__);
+        uint32_t bytesPerPixel = SAMPLES_PER_RGB_PIXEL * BYTES_PER_RGB_SAMPLE;
+        uint32_t width = context->getThumbnailWidth();
+        thumbnailSource = new DirectStripSource(env, context->getThumbnail(), TIFF_IFD_0,
+                width, context->getThumbnailHeight(), bytesPerPixel,
+                bytesPerPixel * width, /*offset*/0, BYTES_PER_RGB_SAMPLE,
+                SAMPLES_PER_RGB_PIXEL);
+        sources.add(thumbnailSource.get());
+        targetIfd = TIFF_IFD_SUB1;
+    }
+
+    sp<JniInputStream> in = new JniInputStream(env, inStream);
+
+    ALOGV("%s: Using input-type strip source.", __FUNCTION__);
+    InputStripSource stripSource(env, *in, targetIfd, uWidth, uHeight, pixStride,
+             rowStride, uOffset, BYTES_PER_SAMPLE, SAMPLES_PER_RAW_PIXEL);
+    sources.add(&stripSource);
+
+    status_t ret = OK;
+    if ((ret = writer->write(out.get(), sources.editArray(), sources.size())) != OK) {
+        ALOGE("%s: write failed with error %d.", __FUNCTION__, ret);
+        if (!env->ExceptionCheck()) {
+            jniThrowExceptionFmt(env, "java/io/IOException",
+                    "Encountered error %d while writing file.", ret);
+        }
+        return;
+    }
 }
 
 } /*extern "C" */
@@ -926,16 +1846,13 @@ static JNINativeMethod gDngCreatorMethods[] = {
             (void*) DngCreator_init},
     {"nativeDestroy",           "()V",      (void*) DngCreator_destroy},
     {"nativeSetOrientation",    "(I)V",     (void*) DngCreator_nativeSetOrientation},
-    {"nativeSetThumbnailBitmap","(Landroid/graphics/Bitmap;)V",
-            (void*) DngCreator_nativeSetThumbnailBitmap},
-    {"nativeSetThumbnailImage",
-            "(IILjava/nio/ByteBuffer;IILjava/nio/ByteBuffer;IILjava/nio/ByteBuffer;II)V",
-            (void*) DngCreator_nativeSetThumbnailImage},
-    {"nativeWriteImage",        "(Ljava/io/OutputStream;IILjava/nio/ByteBuffer;II)V",
+    {"nativeSetDescription",    "(Ljava/lang/String;)V", (void*) DngCreator_nativeSetDescription},
+    {"nativeSetGpsTags",    "([ILjava/lang/String;[ILjava/lang/String;Ljava/lang/String;[I)V",
+            (void*) DngCreator_nativeSetGpsTags},
+    {"nativeSetThumbnail","(Ljava/nio/ByteBuffer;II)V", (void*) DngCreator_nativeSetThumbnail},
+    {"nativeWriteImage",        "(Ljava/io/OutputStream;IILjava/nio/ByteBuffer;IIJZ)V",
             (void*) DngCreator_nativeWriteImage},
-    {"nativeWriteByteBuffer",    "(Ljava/io/OutputStream;Ljava/nio/ByteBuffer;J)V",
-            (void*) DngCreator_nativeWriteByteBuffer},
-    {"nativeWriteInputStream",    "(Ljava/io/OutputStream;Ljava/io/InputStream;J)V",
+    {"nativeWriteInputStream",    "(Ljava/io/OutputStream;Ljava/io/InputStream;IIJ)V",
             (void*) DngCreator_nativeWriteInputStream},
 };