Camera2: Add DNG validation to DngCreatorTest

- Rename JNI library since it's more than just for NDK now
- Link against static version of dng_sdk with validate active
- Duplicate much of SDK dng_validate executable for validation
- Statically include dng_sdk dependencies as well

Bug: 23727371
Change-Id: Ifec2bae0f4b4d7acf3b17ee39c252e1046a9b7b9
diff --git a/tests/camera/Android.mk b/tests/camera/Android.mk
index 85eaf5b..4065bfb 100644
--- a/tests/camera/Android.mk
+++ b/tests/camera/Android.mk
@@ -28,7 +28,9 @@
 	mockito-target \
 	android-ex-camera2
 
-LOCAL_JNI_SHARED_LIBRARIES := libctscamera2ndk_jni libnativehelper_compat_libc++
+LOCAL_JNI_SHARED_LIBRARIES := \
+	libctscamera2_jni \
+	libnativehelper_compat_libc++
 
 LOCAL_SRC_FILES := $(call all-java-files-under, src) $(call all-renderscript-files-under, src)
 
diff --git a/tests/camera/libcamera2ndkjni/Android.mk b/tests/camera/libctscamera2jni/Android.mk
similarity index 61%
rename from tests/camera/libcamera2ndkjni/Android.mk
rename to tests/camera/libctscamera2jni/Android.mk
index 89690a2..eac42f5 100644
--- a/tests/camera/libcamera2ndkjni/Android.mk
+++ b/tests/camera/libctscamera2jni/Android.mk
@@ -16,12 +16,13 @@
 
 include $(CLEAR_VARS)
 
-LOCAL_MODULE    := libctscamera2ndk_jni
+LOCAL_MODULE    := libctscamera2_jni
 
 LOCAL_MODULE_TAGS := optional
 
 LOCAL_SRC_FILES := \
-	native-camera-jni.cpp
+	native-camera-jni.cpp \
+	dng-validate-jni.cpp
 
 LOCAL_C_INCLUDES := \
 	$(JNI_H_INCLUDE) \
@@ -29,12 +30,24 @@
 	frameworks/av/include/camera/ndk \
 	frameworks/av/include/ndk \
 
+# Flags needed by DNG SDK
+LOCAL_CFLAGS := -DUNIX_ENV=1 -DqDNGBigEndian=0 -DqDNGThreadSafe=1 -DqDNGUseLibJPEG=1 -DqDNGUseXMP=0 -DqDNGValidate=1 -DqDNGValidateTarget=1 -DqAndroid=1 -fexceptions -Wsign-compare -Wno-reorder -Wframe-larger-than=20000
+
+# Flags to avoid warnings from DNG SDK
+LOCAL_CFLAGS += -Wno-unused-parameter
+
+LOCAL_STATIC_LIBRARIES := libdng_sdk_validate libjpeg_static libz
 LOCAL_SHARED_LIBRARIES := libandroid \
     libnativehelper_compat_libc++ \
     liblog \
     libcamera2ndk \
     libmediandk
 
+# NDK build, shared C++ runtime
+#LOCAL_SDK_VERSION := current
+#LOCAL_NDK_STL_VARIANT := c++_shared
+
+# Temporary workaround until camera2 NDK is active. See b/27102995.
 LOCAL_CXX_STL := libc++_static
 
 include $(BUILD_SHARED_LIBRARY)
diff --git a/tests/camera/libctscamera2jni/dng-validate-jni.cpp b/tests/camera/libctscamera2jni/dng-validate-jni.cpp
new file mode 100644
index 0000000..186cf3f
--- /dev/null
+++ b/tests/camera/libctscamera2jni/dng-validate-jni.cpp
@@ -0,0 +1,469 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+//#define LOG_NDEBUG 0
+#define LOG_TAG "DngValidateCamera"
+#include <log/log.h>
+#include <jni.h>
+
+#include <string>
+#include <sstream>
+#include <iostream>
+
+/**
+ * Use DNG SDK to validate captured DNG file.
+ *
+ * This code is largely based on the dng_validate.cpp implementation included
+ * with the DNG SDK. The portions of this file that are from the DNG SDK are
+ * covered by the the DNG SDK license in /external/dng_sdk/LICENSE
+ */
+
+#include "dng_color_space.h"
+#include "dng_date_time.h"
+#include "dng_exceptions.h"
+#include "dng_file_stream.h"
+#include "dng_globals.h"
+#include "dng_host.h"
+#include "dng_ifd.h"
+#include "dng_image_writer.h"
+#include "dng_info.h"
+#include "dng_linearization_info.h"
+#include "dng_mosaic_info.h"
+#include "dng_negative.h"
+#include "dng_preview.h"
+#include "dng_render.h"
+#include "dng_simple_image.h"
+#include "dng_tag_codes.h"
+#include "dng_tag_types.h"
+#include "dng_tag_values.h"
+
+// Version of DNG validate referenced for this implementation
+#define kDNGValidateVersion "1.4"
+
+static bool gFourColorBayer = false;
+
+static int32 gMosaicPlane = -1;
+
+static uint32 gPreferredSize = 0;
+static uint32 gMinimumSize   = 0;
+static uint32 gMaximumSize   = 0;
+
+static uint32 gProxyDNGSize = 0;
+
+static const dng_color_space *gFinalSpace = &dng_space_sRGB::Get();
+
+static uint32 gFinalPixelType = ttByte;
+
+static dng_string gDumpStage1;
+static dng_string gDumpStage2;
+static dng_string gDumpStage3;
+static dng_string gDumpTIF;
+static dng_string gDumpDNG;
+
+/**
+ * Validate DNG file in provided buffer.
+ *
+ * Returns dng_error_none (0) on success, otherwise one of the
+ * dng_error_code enum values is returned.
+ *
+ * Warnings and errors found during validation are printed to stderr
+ */
+static dng_error_code dng_validate(const void* data, uint32_t count) {
+
+    ALOGI("Validating DNG buffer");
+
+    try {
+        dng_stream stream(data, count);
+
+        dng_host host;
+
+        host.SetPreferredSize(gPreferredSize);
+        host.SetMinimumSize(gMinimumSize);
+        host.SetMaximumSize(gMaximumSize);
+
+        host.ValidateSizes();
+
+        if (host.MinimumSize()) {
+            host.SetForPreview(true);
+            gDumpDNG.Clear();
+        }
+
+        if (gDumpDNG.NotEmpty()) {
+            host.SetSaveDNGVersion(dngVersion_SaveDefault);
+            host.SetSaveLinearDNG(false);
+            host.SetKeepOriginalFile(false);
+        }
+
+        // Read into the negative.
+
+        AutoPtr<dng_negative> negative;
+        {
+            dng_info info;
+            info.Parse(host, stream);
+            info.PostParse(host);
+            if (!info.IsValidDNG()) {
+                return dng_error_bad_format;
+            }
+
+            negative.Reset(host.Make_dng_negative());
+            negative->Parse(host, stream, info);
+            negative->PostParse(host, stream, info);
+
+            {
+                dng_timer timer("Raw image read time");
+                negative->ReadStage1Image(host, stream, info);
+            }
+
+            if (info.fMaskIndex != -1) {
+                dng_timer timer("Transparency mask read time");
+                negative->ReadTransparencyMask(host, stream, info);
+            }
+
+            negative->ValidateRawImageDigest(host);
+        }
+
+        // Option to write stage 1 image.
+
+        if (gDumpStage1.NotEmpty()) {
+            dng_file_stream stream2 (gDumpStage1.Get(), true);
+            const dng_image &stage1 = *negative->Stage1Image();
+            dng_image_writer writer;
+
+            writer.WriteTIFF(host,
+                    stream2,
+                    stage1,
+                    stage1.Planes() >= 3 ? piRGB
+                    : piBlackIsZero);
+
+            gDumpStage1.Clear();
+        }
+
+        // Metadata.
+
+        negative->SynchronizeMetadata();
+
+        // Four color Bayer option.
+
+        if (gFourColorBayer) {
+            negative->SetFourColorBayer();
+        }
+
+        // Build stage 2 image.
+
+        {
+            dng_timer timer("Linearization time");
+            negative->BuildStage2Image(host);
+        }
+
+        if (gDumpStage2.NotEmpty()) {
+            dng_file_stream stream2(gDumpStage2.Get(), true);
+            const dng_image &stage2 = *negative->Stage2Image();
+            dng_image_writer writer;
+
+            writer.WriteTIFF (host,
+                    stream2,
+                    stage2,
+                    stage2.Planes() >= 3 ? piRGB
+                    : piBlackIsZero);
+
+            gDumpStage2.Clear();
+        }
+
+        // Build stage 3 image.
+
+        {
+            dng_timer timer("Interpolate time");
+            negative->BuildStage3Image(host,
+                    gMosaicPlane);
+        }
+
+        // Convert to proxy, if requested.
+
+        if (gProxyDNGSize) {
+            dng_timer timer("ConvertToProxy time");
+            dng_image_writer writer;
+
+            negative->ConvertToProxy(host,
+                    writer,
+                    gProxyDNGSize);
+        }
+
+        // Flatten transparency, if required.
+
+        if (negative->NeedFlattenTransparency(host)) {
+            dng_timer timer("FlattenTransparency time");
+            negative->FlattenTransparency(host);
+        }
+
+        if (gDumpStage3.NotEmpty()) {
+            dng_file_stream stream2(gDumpStage3.Get(), true);
+            const dng_image &stage3 = *negative->Stage3Image();
+            dng_image_writer writer;
+
+            writer.WriteTIFF (host,
+                    stream2,
+                    stage3,
+                    stage3.Planes () >= 3 ? piRGB
+                    : piBlackIsZero);
+
+            gDumpStage3.Clear();
+        }
+
+        // Output DNG file if requested.
+
+        if (gDumpDNG.NotEmpty()) {
+            // Build the preview list.
+            dng_preview_list previewList;
+            dng_date_time_info dateTimeInfo;
+            CurrentDateTimeAndZone(dateTimeInfo);
+
+            for (uint32 previewIndex = 0; previewIndex < 2; previewIndex++) {
+
+                // Skip preview if writing a compresssed main image to save space
+                // in this example code.
+                if (negative->RawJPEGImage() != NULL && previewIndex > 0) {
+                    break;
+                }
+
+                // Report timing.
+                dng_timer timer(previewIndex == 0 ? "Build thumbnail time"
+                        : "Build preview time");
+
+                // Render a preview sized image.
+                AutoPtr<dng_image> previewImage;
+
+                {
+                    dng_render render (host, *negative);
+                    render.SetFinalSpace (negative->IsMonochrome() ?
+                            dng_space_GrayGamma22::Get() : dng_space_sRGB::Get());
+                    render.SetFinalPixelType (ttByte);
+                    render.SetMaximumSize (previewIndex == 0 ? 256 : 1024);
+
+                    previewImage.Reset (render.Render());
+                }
+
+                // Don't write the preview if it is same size as thumbnail.
+
+                if (previewIndex > 0 &&
+                        Max_uint32(previewImage->Bounds().W(),
+                                previewImage->Bounds().H()) <= 256) {
+                    break;
+                }
+
+                // If we have compressed JPEG data, create a compressed thumbnail.  Otherwise
+                // save a uncompressed thumbnail.
+                bool useCompressedPreview = (negative->RawJPEGImage() != NULL) ||
+                        (previewIndex > 0);
+
+                AutoPtr<dng_preview> preview (useCompressedPreview ?
+                        (dng_preview *) new dng_jpeg_preview :
+                        (dng_preview *) new dng_image_preview);
+
+                // Setup up preview info.
+
+                preview->fInfo.fApplicationName.Set("dng_validate");
+                preview->fInfo.fApplicationVersion.Set(kDNGValidateVersion);
+
+                preview->fInfo.fSettingsName.Set("Default");
+
+                preview->fInfo.fColorSpace = previewImage->Planes() == 1 ?
+                        previewColorSpace_GrayGamma22 :
+                        previewColorSpace_sRGB;
+
+                preview->fInfo.fDateTime = dateTimeInfo.Encode_ISO_8601();
+
+                if (!useCompressedPreview) {
+                    dng_image_preview *imagePreview = static_cast<dng_image_preview *>(preview.Get());
+                    imagePreview->fImage.Reset(previewImage.Release());
+                } else {
+                    dng_jpeg_preview *jpegPreview = static_cast<dng_jpeg_preview *>(preview.Get());
+                    int32 quality = (previewIndex == 0 ? 8 : 5);
+                    dng_image_writer writer;
+                    writer.EncodeJPEGPreview (host,
+                            *previewImage,
+                            *jpegPreview,
+                            quality);
+                }
+                previewList.Append (preview);
+            }
+
+            // Write DNG file.
+
+            dng_file_stream stream2(gDumpDNG.Get(), true);
+
+            {
+                dng_timer timer("Write DNG time");
+                dng_image_writer writer;
+
+                writer.WriteDNG(host,
+                        stream2,
+                        *negative.Get(),
+                        &previewList,
+                        dngVersion_Current,
+                        false);
+            }
+
+            gDumpDNG.Clear();
+        }
+
+        // Output TIF file if requested.
+        if (gDumpTIF.NotEmpty()) {
+
+            // Render final image.
+
+            dng_render render(host, *negative);
+
+            render.SetFinalSpace(*gFinalSpace   );
+            render.SetFinalPixelType(gFinalPixelType);
+
+            if (host.MinimumSize()) {
+                dng_point stage3Size = negative->Stage3Image()->Size();
+                render.SetMaximumSize (Max_uint32(stage3Size.v,
+                                stage3Size.h));
+            }
+
+            AutoPtr<dng_image> finalImage;
+
+            {
+                dng_timer timer("Render time");
+                finalImage.Reset(render.Render());
+            }
+
+            finalImage->Rotate(negative->Orientation());
+
+            // Now that Camera Raw supports non-raw formats, we should
+            // not keep any Camera Raw settings in the XMP around when
+            // writing rendered files.
+#if qDNGUseXMP
+            if (negative->GetXMP()) {
+                negative->GetXMP()->RemoveProperties(XMP_NS_CRS);
+                negative->GetXMP()->RemoveProperties(XMP_NS_CRSS);
+            }
+#endif
+
+            // Write TIF file.
+            dng_file_stream stream2(gDumpTIF.Get(), true);
+
+            {
+                dng_timer timer("Write TIFF time");
+                dng_image_writer writer;
+
+                writer.WriteTIFF(host,
+                        stream2,
+                        *finalImage.Get(),
+                        finalImage->Planes() >= 3 ? piRGB
+                        : piBlackIsZero,
+                        ccUncompressed,
+                        negative.Get(),
+                        &render.FinalSpace());
+            }
+            gDumpTIF.Clear();
+        }
+    } catch (const dng_exception &except) {
+        return except.ErrorCode();
+    } catch (...) {
+        return dng_error_unknown;
+    }
+
+    ALOGI("DNG validation complete");
+
+    return dng_error_none;
+}
+
+extern "C" jboolean
+Java_android_hardware_camera2_cts_DngCreatorTest_validateDngNative(
+    JNIEnv* env, jclass /*clazz*/, jbyteArray dngBuffer) {
+
+    jbyte* buffer = env->GetByteArrayElements(dngBuffer, NULL);
+    jsize bufferCount = env->GetArrayLength(dngBuffer);
+    if (buffer == nullptr) {
+        ALOGE("Unable to map DNG buffer to native");
+        return JNI_FALSE;
+    }
+
+    // DNG parsing warnings/errors fprintfs are spread throughout the DNG SDK,
+    // guarded by the qDNGValidate define flag. To avoid modifying the SDK,
+    // redirect stderr to a pipe to capture output locally.
+
+    int pipeFds[2];
+    int err;
+
+    err = pipe(pipeFds);
+    if (err != 0) {
+        ALOGE("Error redirecting dng_validate output: %d", errno);
+        env->ReleaseByteArrayElements(dngBuffer, buffer, 0);
+        return JNI_FALSE;
+    }
+
+    int stderrFd = dup(fileno(stderr));
+    dup2(pipeFds[1], fileno(stderr));
+    close(pipeFds[1]);
+
+    // Actually run the validation
+    dng_error_code dng_err = dng_validate(buffer, bufferCount);
+
+    env->ReleaseByteArrayElements(dngBuffer, buffer, 0);
+
+    // Restore stderr and read out pipe
+    dup2(stderrFd, fileno(stderr));
+
+    std::stringstream errorStream;
+    const size_t BUF_SIZE = 256;
+    char readBuf[BUF_SIZE];
+
+    ssize_t count = 0;
+    while((count = read(pipeFds[0], readBuf, BUF_SIZE)) > 0) {
+        errorStream.write(readBuf, count);
+    }
+    if (count < 0) {
+        ALOGE("Error reading from dng_validate output pipe: %d", errno);
+        return JNI_FALSE;
+    }
+    close(pipeFds[1]);
+
+    std::string line;
+    int lineCount = 0;
+    ALOGI("Output from DNG validation:");
+    // dng_validate doesn't actually propagate all errors/warnings to the
+    // return error code, so look for an error pattern in output to detect
+    // problems. Also make sure the output is long enough since some non-error
+    // content should always be printed.
+    while(std::getline(errorStream, line, '\n')) {
+        lineCount++;
+        if ( (line.size() > 3) &&
+                (line[0] == line[1]) &&
+                (line[1] == line[2]) &&
+                (line[2] == '*') ) {
+            // Found a warning or error, so need to fail the test
+            if (dng_err == dng_error_none) {
+                dng_err = dng_error_bad_format;
+            }
+            ALOGE("**|%s", line.c_str());
+        } else {
+            ALOGI("  |%s", line.c_str());
+        }
+    }
+    // If no output is produced, assume something went wrong
+    if (lineCount < 3) {
+        ALOGE("Validation output less than expected!");
+        dng_err = dng_error_unknown;
+    }
+    if (dng_err != dng_error_none) {
+        ALOGE("DNG validation failed!");
+    }
+
+    return (dng_err == dng_error_none) ? JNI_TRUE : JNI_FALSE;
+}
diff --git a/tests/camera/libcamera2ndkjni/native-camera-jni.cpp b/tests/camera/libctscamera2jni/native-camera-jni.cpp
similarity index 100%
rename from tests/camera/libcamera2ndkjni/native-camera-jni.cpp
rename to tests/camera/libctscamera2jni/native-camera-jni.cpp
diff --git a/tests/camera/src/android/hardware/camera2/cts/DngCreatorTest.java b/tests/camera/src/android/hardware/camera2/cts/DngCreatorTest.java
index 6488cc6..769c810 100644
--- a/tests/camera/src/android/hardware/camera2/cts/DngCreatorTest.java
+++ b/tests/camera/src/android/hardware/camera2/cts/DngCreatorTest.java
@@ -81,6 +81,11 @@
     private static final Calendar GPS_CALENDAR =
             Calendar.getInstance(TimeZone.getTimeZone("GMT+0"));
 
+    /** Load DNG validation jni on initialization */
+    static {
+        System.loadLibrary("ctscamera2_jni");
+    }
+
     static {
         GPS_CALENDAR.set(2015, 0, 27, 2, 12, 01);
     }
@@ -166,6 +171,8 @@
                     fileStream.close();
                     Log.v(TAG, "Test DNG file for camera " + deviceId + " saved to " + dngFilePath);
                 }
+                assertTrue("Generated DNG file does not pass validation",
+                        validateDngNative(outputStream.toByteArray()));
             } finally {
                 closeDevice(deviceId);
                 closeImageReader(captureReader);
@@ -266,6 +273,9 @@
                     Log.v(TAG, "Test DNG file for camera " + deviceId + " saved to " + filePath);
                 }
 
+                assertTrue("Generated DNG file does not pass validation",
+                        validateDngNative(outputStream.toByteArray()));
+
                 ExifInterface exifInterface = new ExifInterface(filePath);
                 // Verify GPS data.
                 float[] latLong = new float[2];
@@ -740,4 +750,12 @@
 
         return ret;
     }
+
+    /**
+     * Use the DNG SDK to validate a DNG file stored in the buffer.
+     *
+     * Returns false if the DNG has validation errors. Validation warnings/errors
+     * will be printed to logcat.
+     */
+    private static native boolean validateDngNative(byte[] dngBuffer);
 }
diff --git a/tests/camera/src/android/hardware/camera2/cts/NativeCameraDeviceTest.java b/tests/camera/src/android/hardware/camera2/cts/NativeCameraDeviceTest.java
index ad460c8..e2a61a6 100644
--- a/tests/camera/src/android/hardware/camera2/cts/NativeCameraDeviceTest.java
+++ b/tests/camera/src/android/hardware/camera2/cts/NativeCameraDeviceTest.java
@@ -31,7 +31,7 @@
     /** Load jni on initialization */
     static {
         Log.i("NativeCameraDeviceTest", "before loadlibrary");
-        System.loadLibrary("ctscamera2ndk_jni");
+        System.loadLibrary("ctscamera2_jni");
         Log.i("NativeCameraDeviceTest", "after loadlibrary");
     }
 
diff --git a/tests/camera/src/android/hardware/camera2/cts/NativeCameraManagerTest.java b/tests/camera/src/android/hardware/camera2/cts/NativeCameraManagerTest.java
index 6d0d73c..08e0363 100644
--- a/tests/camera/src/android/hardware/camera2/cts/NativeCameraManagerTest.java
+++ b/tests/camera/src/android/hardware/camera2/cts/NativeCameraManagerTest.java
@@ -29,7 +29,7 @@
     /** Load jni on initialization */
     static {
         Log.i("NativeCameraManagerTest", "before loadlibrary");
-        System.loadLibrary("ctscamera2ndk_jni");
+        System.loadLibrary("ctscamera2_jni");
         Log.i("NativeCameraManagerTest", "after loadlibrary");
     }
 
diff --git a/tests/camera/src/android/hardware/camera2/cts/NativeImageReaderTest.java b/tests/camera/src/android/hardware/camera2/cts/NativeImageReaderTest.java
index 24fc04f..7e4508a 100644
--- a/tests/camera/src/android/hardware/camera2/cts/NativeImageReaderTest.java
+++ b/tests/camera/src/android/hardware/camera2/cts/NativeImageReaderTest.java
@@ -29,7 +29,7 @@
     /** Load jni on initialization */
     static {
         Log.i("NativeImageReaderTest", "before loadlibrary");
-        System.loadLibrary("ctscamera2ndk_jni");
+        System.loadLibrary("ctscamera2_jni");
         Log.i("NativeImageReaderTest", "after loadlibrary");
     }
 
diff --git a/tests/camera/src/android/hardware/camera2/cts/NativeStillCaptureTest.java b/tests/camera/src/android/hardware/camera2/cts/NativeStillCaptureTest.java
index 50fa715..14b5a22 100644
--- a/tests/camera/src/android/hardware/camera2/cts/NativeStillCaptureTest.java
+++ b/tests/camera/src/android/hardware/camera2/cts/NativeStillCaptureTest.java
@@ -31,7 +31,7 @@
     /** Load jni on initialization */
     static {
         Log.i("NativeStillCaptureTest", "before loadlibrary");
-        System.loadLibrary("ctscamera2ndk_jni");
+        System.loadLibrary("ctscamera2_jni");
         Log.i("NativeStillCaptureTest", "after loadlibrary");
     }