changeset 0:db63d01a23c6

JNI calls and test case (finally!) seem to work.
author David Barts <n5jrn@me.com>
date Tue, 31 Mar 2020 13:24:48 -0700
parents
children 42277ce58ace
files .hgignore Makefile.mac build.xml setup.sh src/name/blackcap/exifwasher/Files.kt src/name/blackcap/exifwasher/Test.kt src/name/blackcap/exifwasher/default.properties src/name/blackcap/exifwasher/exiv2/ExifData.kt src/name/blackcap/exifwasher/exiv2/Exiv2Exception.kt src/name/blackcap/exifwasher/exiv2/Image.kt src/name/blackcap/exifwasher/exiv2/Initialize.kt src/name/blackcap/exifwasher/exiv2/native.cpp src/name/blackcap/exifwasher/exiv2/native.hpp
diffstat 13 files changed, 701 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore	Tue Mar 31 13:24:48 2020 -0700
@@ -0,0 +1,7 @@
+~$
+\.bak$
+\.class$
+\.dylib$
+\.o$
+^bundles/
+^work/
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Makefile.mac	Tue Mar 31 13:24:48 2020 -0700
@@ -0,0 +1,23 @@
+CXX = g++
+CXXFLAGS = -I$(JRE_HOME)/include -I$(JRE_HOME)/include/darwin \
+    -I$(EXIV2_HOME)/include -I$(EXIV2_HOME)/build
+NDIR = src/name/blackcap/exifwasher/exiv2
+BDIR = src/name/blackcap/exifwasher/binaries/mac
+
+.PHONY: all checkenv
+
+all: checkenv $(BDIR)/libjni.dylib $(BDIR)/libexiv2.dylib
+
+checkenv:
+	@if [ -z "$(JRE_HOME)" -o -z "$(EXIV2_HOME)" ]; then \
+		1>&2 echo "JRE_HOME or EXIV2_HOME not set"; \
+		exit 1; \
+	fi
+
+$(NDIR)/native.o: $(NDIR)/native.cpp
+
+$(BDIR)/libjni.dylib: $(NDIR)/native.o
+	$(CXX) -dynamiclib -o $@ $< -L$(EXIV2_HOME)/build/lib -lexiv2
+
+$(BDIR)/libexiv2.dylib: $(EXIV2_HOME)/build/lib/libexiv2.dylib
+	cp -fp $< $@
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/build.xml	Tue Mar 31 13:24:48 2020 -0700
@@ -0,0 +1,146 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project name="ExifWasher" default="help" basedir="." xmlns:fx="javafx:com.sun.javafx.tools.ant">
+  <!-- import all environment variables as env.* -->
+  <property environment="env"/>
+
+  <!-- ensure required environment variables are set -->
+  <macrodef name="env-require">
+    <attribute name="name"/>
+    <sequential>
+      <fail message="Environment variable @{name} not set!">
+        <condition>
+          <not><isset property="env.@{name}"/></not>
+        </condition>
+      </fail>
+    </sequential>
+  </macrodef>
+  <env-require name="JRE_HOME"/>
+  <env-require name="KOTLIN_HOME"/>
+
+  <!-- define the kotlin task -->
+  <property name="kotlin.lib" value="${env.KOTLIN_HOME}/libexec/lib"/>
+  <typedef resource="org/jetbrains/kotlin/ant/antlib.xml"
+           classpath="${kotlin.lib}/kotlin-ant.jar"/>
+
+  <!-- define the package-building tasks -->
+  <taskdef resource="com/sun/javafx/tools/ant/antlib.xml"
+           uri="javafx:com.sun.javafx.tools.ant"
+           classpath="${env.JRE_HOME}/lib/ant-javafx.jar"/>
+
+  <!-- cribbed from https://stackoverflow.com/questions/7129672/uppercase-lowercase-capitalize-an-ant-property -->
+  <scriptdef language="javascript" name="toLowerCase">
+    <attribute name="value" />
+    <attribute name="target" />
+    <![CDATA[
+      project.setProperty( attributes.get( "target" ),
+                           attributes.get( "value" ).toLowerCase() );
+    ]]>
+  </scriptdef>
+
+  <!-- Define the properties used by the build -->
+  <property name="app.name"      value="${ant.project.name}"/>
+  <property name="app.entry"     value="name.blackcap.exifwasher.MainKt"/>
+  <toLowerCase target="lc.app.name" value="${app.name}"/>
+  <property name="jar.name"      value="${basedir}/${lc.app.name}.jar"/>
+  <property name="work.jar"      value="${basedir}/work.jar"/>
+  <property name="lib.home"      value="${basedir}/lib"/>
+  <property name="src.home"      value="${basedir}/src"/>
+  <property name="nat.dir"       value="${src.home}/name/blackcap/exifwasher/exiv2"/>
+  <property name="bin.dir"       value="${src.home}/name/blackcap/exifwasher/binaries"/>
+
+  <!-- help message -->
+  <target name="help">
+    <echo>You can use the following targets:</echo>
+    <echo> </echo>
+    <echo>  help    : (default) Prints this message </echo>
+    <echo>  all     : Cleans, compiles, and stages application</echo>
+    <echo>  clean   : Deletes work directories</echo>
+    <echo>  compile : Compiles servlets into class files</echo>
+    <echo>  jar     : Make JAR file.</echo>
+    <echo> </echo>
+    <echo>For example, to clean, compile, and package all at once, run:</echo>
+    <echo>prompt> ant all </echo>
+  </target>
+
+  <!-- Define the CLASSPATH -->
+  <target name="classpath">
+    <path id="std.classpath">
+      <fileset dir="${lib.home}">
+        <include name="*.jar"/>
+      </fileset>
+    </path>
+    <path id="compile.classpath">
+      <path refid="std.classpath"/>
+      <pathelement location="${src.home}"/>
+    </path>
+    <path id="test.classpath">
+      <path refid="std.classpath"/>
+      <pathelement location="${work.home}"/>
+    </path>
+  </target>
+
+  <!-- do everything but install -->
+  <target name="all" depends="jar"
+          description="Clean work dirs, compile, make JAR."/>
+
+  <!-- compile *.kt to *.class -->
+  <target name="compile" depends="classpath"
+          description="Compile Java sources to ${work.home}">
+    <kotlinc src="${src.home}" output="${work.jar}"
+             classpathref="compile.classpath"/>
+  </target>
+
+  <!-- make .jar file -->
+  <target name="jar" depends="compile" description="Create JAR file.">
+    <jar destfile="${jar.name}">
+      <manifest>
+        <attribute name="Main-Class" value="${app.entry}"/>
+      </manifest>
+      <zipgroupfileset dir="${lib.home}" includes="*.jar"/>
+      <zipfileset src="${work.jar}"/>
+      <zipfileset dir="${src.home}" includes="**/*.properties,**/*.dll,**/*.so,**/*.dylib"/>
+    </jar>
+  </target>
+
+  <!-- for making bundled apps -->
+  <macrodef name="bundle">
+    <attribute name="type"/>
+    <element name="args"/>
+    <sequential>
+      <fx:deploy nativeBundles="@{type}" outdir="${basedir}" outfile="${app.name}"
+        signBundle="false">
+        <fx:application mainClass="${app.entry}" name="${app.name}" toolkit="swing"
+          version="1.0"/>
+        <fx:info description="ExifWasher" title="${app.name}"
+          vendor="David Barts &lt;n5jrn@me.com&gt;"
+          copyright="© MMXX, David W. Barts"/>
+        <fx:resources>
+          <fx:fileset dir="${basedir}" includes="${lc.app.name}.jar"/>
+        </fx:resources>
+        <fx:bundleArgument arg="runtime" value="${env.JRE_HOME}"/>
+        <args/>
+      </fx:deploy>
+      </sequential>
+  </macrodef>
+
+  <target name="macapp" depends="jar" description="Create MacOS app bundle.">
+    <bundle type="image">
+      <args>
+        <fx:bundleArgument arg="jvmOptions" value="-Xdock:name=${app.name}"/>
+      </args>
+    </bundle>
+  </target>
+
+  <target name="app" depends="jar" description="Create app bundle.">
+    <bundle type="image"/>
+  </target>
+
+  <target name="rpm" depends="jar" description="Create RPM package.">
+    <bundle type="rpm"/>
+  </target>
+
+  <target name="deb" depends="jar" description="Create Debian package.">
+    <bundle type="deb"/>
+  </target>
+
+</project>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/setup.sh	Tue Mar 31 13:24:48 2020 -0700
@@ -0,0 +1,11 @@
+#!/bin/bash
+
+export JRE_HOME="$(/usr/libexec/java_home)"
+export KOTLIN_HOME="/usr/local/Cellar/kotlin/1.3.71"
+export EXIV2_HOME="$HOME/temp/exiv2/exiv2-0.27.2-Source"
+
+export ANT_HOME="$HOME/java/apache-ant-1.10.1"
+if [[ "$PATH" != *$ANT_HOME/bin* ]]
+then
+    export PATH="$ANT_HOME/bin:$PATH"
+fi
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/name/blackcap/exifwasher/Files.kt	Tue Mar 31 13:24:48 2020 -0700
@@ -0,0 +1,85 @@
+/*
+ * For dealing with files.
+ */
+package name.blackcap.exifwasher
+
+import java.io.BufferedReader
+import java.io.File
+import java.io.FileInputStream
+import java.io.InputStreamReader
+import java.util.Properties
+import java.util.logging.FileHandler
+import java.util.logging.Level
+import java.util.logging.Logger
+import java.util.logging.SimpleFormatter
+
+/* OS Type */
+
+enum class OS {
+    MAC, UNIX, WINDOWS, OTHER;
+    companion object {
+        private val rawType = System.getProperty("os.name")?.toLowerCase()
+        val type = if (rawType == null) {
+                OTHER
+            } else if (rawType.contains("win")) {
+                WINDOWS
+            } else if (rawType.contains("mac")) {
+                MAC
+            } else if (rawType.contains("nix") || rawType.contains("nux") || rawType.contains("aix") || rawType.contains("sunos")) {
+                UNIX
+            } else {
+                OTHER
+            }
+    }
+}
+
+/* joins path name components to java.io.File */
+
+fun joinPath(base: String, vararg rest: String) = rest.fold(File(base), ::File)
+
+/* file names */
+
+private val SHORTNAME = "exifwasher"
+private val LONGNAME = "name.blackcap." + SHORTNAME
+private val HOME = System.getenv("HOME")
+private val APPDATA = System.getenv("APPDATA")
+val PF_DIR = when (OS.type) {
+    OS.MAC -> joinPath(HOME, "Library", "Application Support", LONGNAME)
+    OS.WINDOWS -> joinPath(APPDATA, "Roaming", LONGNAME)
+    else -> joinPath(HOME, "." + SHORTNAME)
+}
+val LF_DIR = when (OS.type) {
+    OS.MAC -> joinPath(HOME, "Library", "Application Support", LONGNAME)
+    OS.WINDOWS -> joinPath(APPDATA, "Local", LONGNAME)
+    else -> joinPath(HOME, "." + SHORTNAME)
+}
+val PROP_FILE = File(PF_DIR, SHORTNAME + ".properties")
+val LOG_FILE = File(LF_DIR, SHORTNAME + ".log")
+
+/* make some needed directories */
+
+private fun File.makeIfNeeded() = if (exists()) { true } else { mkdirs() }
+
+/* make some usable objects */
+
+val DPROPERTIES = Properties().apply {
+    OS::class.java.getResourceAsStream("default.properties").use { load(it) }
+}
+
+val PROPERTIES = Properties(DPROPERTIES).apply {
+    PF_DIR.makeIfNeeded()
+    PROP_FILE.createNewFile()
+    BufferedReader(InputStreamReader(FileInputStream(PROP_FILE), "UTF-8")).use  {
+        load(it)
+    }
+}
+
+val LOGGER = run {
+    LF_DIR.makeIfNeeded()
+    Logger.getLogger(LONGNAME).apply {
+        addHandler(FileHandler(LOG_FILE.toString()).apply {
+            formatter = SimpleFormatter() })
+        level = Level.CONFIG
+        useParentHandlers = false
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/name/blackcap/exifwasher/Test.kt	Tue Mar 31 13:24:48 2020 -0700
@@ -0,0 +1,24 @@
+/*
+ * A basic test of the library: try to use it to print out the EXIF
+ * data.
+ */
+package name.blackcap.exifwasher
+
+import name.blackcap.exifwasher.exiv2.*
+
+/* entry point */
+fun main(args: Array<String>) {
+    println("java.class.path = " + System.getProperty("java.class.path"))
+    if (args.size != 1) {
+        System.err.println("expecting a file name")
+        System.exit(1)
+    }
+    val image = Image(args[0])
+    val meta = image.metadata
+    val keys = meta.keys
+    keys.sort()
+    keys.forEach {
+        val v = meta[it]
+        println("${it}: ${v.type} = ${v.value}")
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/name/blackcap/exifwasher/default.properties	Tue Mar 31 13:24:48 2020 -0700
@@ -0,0 +1,1 @@
+# A placeholder, because we currently don't use properties.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/name/blackcap/exifwasher/exiv2/ExifData.kt	Tue Mar 31 13:24:48 2020 -0700
@@ -0,0 +1,30 @@
+/*
+ * Some image EXIF metadata.
+ */
+package name.blackcap.exifwasher.exiv2
+
+import kotlin.collections.Iterable
+import kotlin.collections.Iterator
+
+public class ExifData(ptr: Long) {
+    init {
+        Initialize.libraries()
+    }
+
+    private external fun _erase(key: String): Unit
+    private external fun _value(key: String): Value
+    private external fun _keys(): Array<String>
+
+    private val pointer = ptr
+
+    val keys: Array<String>
+    get() {
+        return _keys()
+    }
+
+    fun erase(key: String): Unit = _erase(key)
+
+    public data class Value(val type: String, val value: String)
+
+    operator fun get(key: String): Value = _value(key)
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/name/blackcap/exifwasher/exiv2/Exiv2Exception.kt	Tue Mar 31 13:24:48 2020 -0700
@@ -0,0 +1,6 @@
+/*
+ * Exception thrown when something goes wrong in exiv2.
+ */
+package name.blackcap.exifwasher.exiv2
+
+class Exiv2Exception(message: String): Exception(message) { }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/name/blackcap/exifwasher/exiv2/Image.kt	Tue Mar 31 13:24:48 2020 -0700
@@ -0,0 +1,29 @@
+/*
+ * Represents an image file we're examining and manipulating the metadata of.
+ */
+package name.blackcap.exifwasher.exiv2
+
+public class Image(path: String) {
+    init {
+        Initialize.libraries()
+    }
+
+    private external fun _ctor(path: String): Long
+    private external fun _writeMetadata()
+    private external fun _getMetadata(): Long
+    private external fun _dtor()
+
+    private val pointer: Long
+    init {
+        pointer = _ctor(path)
+    }
+
+    val metadata: ExifData
+    get() {
+        return ExifData(_getMetadata())
+    }
+
+    fun store() = _writeMetadata()
+
+    protected fun finalize() = _dtor()
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/name/blackcap/exifwasher/exiv2/Initialize.kt	Tue Mar 31 13:24:48 2020 -0700
@@ -0,0 +1,81 @@
+/*
+ * Logic common to initializing all external (JNI) classes goes here.
+ */
+package name.blackcap.exifwasher.exiv2
+
+import java.io.File
+import java.io.FileOutputStream
+import java.io.IOException
+import java.util.jar.JarEntry
+import java.util.jar.JarFile
+import name.blackcap.exifwasher.LF_DIR
+import name.blackcap.exifwasher.OS
+
+object Initialize {
+    private var initialized = false
+
+    public fun libraries() {
+        /* no-op if already initialized */
+        if (initialized) {
+            return
+        }
+
+        /* use the appropriate binary for the system we're on */
+        val subdir = when(OS.type) {
+            OS.UNIX -> "linux"
+            OS.MAC -> "mac"
+            OS.WINDOWS -> "windows"
+            OS.OTHER -> throw RuntimeException("unsupported OS!")
+        }
+        val ext = when(OS.type) {
+            OS.UNIX -> "so"
+            OS.MAC -> "dylib"
+            OS.WINDOWS -> "dll"
+            OS.OTHER -> throw RuntimeException("unsupported OS!")
+        }
+
+        /* extract to scratch files, if needed, then load */
+        val klass = Initialize::class.java
+        var myJar = JarFile(File(klass.getProtectionDomain().getCodeSource().getLocation().toURI()))
+        for (base in arrayOf("libexiv2", "libjni")) {
+            val eBase = "${base}.${ext}"
+            val sPath = "name/blackcap/exifwasher/binaries/${subdir}/${eBase}"
+            val source = myJar.getJarEntry(sPath)
+            if (source == null) {
+                die("${sPath} not found in jar")
+            }
+            val target = File(LF_DIR, eBase)
+            val tPath = target.toString()
+            try {
+                if (!target.exists() || target.lastModified() < source.getTime()) {
+                    myJar.getInputStream(source).use { sfp ->
+                        FileOutputStream(target).use { tfp ->
+                            sfp.copyTo(tfp)
+                        }
+                    }
+                    target.setExecutable(true)
+                    target.setLastModified(source.getTime())
+                }
+            } catch (e: IOException) {
+                val message = e.message ?: "I/O error"
+                die("unable to create ${tPath}: ${message}")
+            }
+            try {
+                println("loading: ${tPath}")  /* debug */
+                System.load(tPath)
+            } catch (e: UnsatisfiedLinkError) {
+                val message = e.message ?: "unsatisfied link"
+                die("unable to link ${tPath}: ${message}")
+            }
+        }
+
+        initialized = true
+        System.err.println("libraries loaded")  /* debug */
+    }
+
+    private fun die(message: String) {
+        /* should open a dialog */
+        System.err.println(message)
+        System.exit(1)
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/name/blackcap/exifwasher/exiv2/native.cpp	Tue Mar 31 13:24:48 2020 -0700
@@ -0,0 +1,167 @@
+#include <jni.h>
+#include <exiv2/exiv2.hpp>
+#include <iostream>
+#include <iomanip>
+#include <cassert>
+
+/* Functions for class name_blackcap_exifwasher_exiv2_Image */
+
+#ifndef _Included_name_blackcap_exifwasher_exiv2_Image
+#define _Included_name_blackcap_exifwasher_exiv2_Image
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*
+ * Utility function to get pointer field.
+ */
+jlong getPointer(JNIEnv *jEnv, jobject jThis) {
+    return jEnv->GetLongField(jThis, jEnv->GetFieldID(jEnv->GetObjectClass(jThis), "pointer", "J"));
+}
+
+/*
+ * Class:     name_blackcap_exifwasher_exiv2_Image
+ * Method:    _ctor
+ * Signature: (Ljava/lang/String;)J
+ */
+JNIEXPORT jlong JNICALL Java_name_blackcap_exifwasher_exiv2_Image__1ctor
+  (JNIEnv *jEnv, jobject jThis, jstring path) {
+      const char *cPath = jEnv->GetStringUTFChars(path, NULL);
+      jlong ret = 0;
+      try {
+          ret = reinterpret_cast<jlong> (Exiv2::ImageFactory::open(cPath).release());
+      } catch (...) {
+          jclass ex = jEnv->FindClass("name/blackcap/exifwasher/exiv2/Exiv2Exception");
+          const char *pfx = "unable to open ";
+          char *message = (char *) malloc(strlen(cPath) + strlen(pfx) + 1);
+          strcpy(message, pfx);
+          strcat(message, cPath);
+          jEnv->ThrowNew(ex, message);
+          free(message);
+      }
+      jEnv->ReleaseStringUTFChars(path, cPath);
+      return ret;
+  }
+
+/*
+ * Class:     name_blackcap_exifwasher_exiv2_Image
+ * Method:    _writeMetadata
+ * Signature: ()V
+ */
+JNIEXPORT void JNICALL Java_name_blackcap_exifwasher_exiv2_Image__1writeMetadata
+  (JNIEnv *jEnv, jobject jThis) {
+      Exiv2::Image *image = reinterpret_cast<Exiv2::Image *> (getPointer(jEnv, jThis));
+      try {
+          image->writeMetadata();
+      } catch (...) {
+          jclass ex = jEnv->FindClass("name/blackcap/exifwasher/exiv2/Exiv2Exception");
+          jEnv->ThrowNew(ex, "unable to write metadata");
+      }
+  }
+
+/*
+ * Class:     name_blackcap_exifwasher_exiv2_Image
+ * Method:    _getMetadata
+ * Signature: ()J
+ */
+JNIEXPORT jlong JNICALL Java_name_blackcap_exifwasher_exiv2_Image__1getMetadata
+  (JNIEnv *jEnv, jobject jThis) {
+      Exiv2::Image *image = reinterpret_cast<Exiv2::Image *> (getPointer(jEnv, jThis));
+      try {
+          image->readMetadata();
+      } catch (...) {
+          jclass ex = jEnv->FindClass("name/blackcap/exifwasher/exiv2/Exiv2Exception");
+          jEnv->ThrowNew(ex, "unable to read metadata");
+      }
+      return reinterpret_cast<jlong> (&(image->exifData()));
+  }
+
+/*
+ * Class:     name_blackcap_exifwasher_exiv2_Image
+ * Method:    _dtor
+ * Signature: ()V
+ */
+JNIEXPORT void JNICALL Java_name_blackcap_exifwasher_exiv2_Image__1dtor
+  (JNIEnv *jEnv, jobject jThis) {
+      Exiv2::Image *image = reinterpret_cast<Exiv2::Image *> (getPointer(jEnv, jThis));
+      delete image;
+  }
+
+#ifdef __cplusplus
+}
+#endif
+#endif
+/* Header for class name_blackcap_exifwasher_exiv2_ExifData */
+
+#ifndef _Included_name_blackcap_exifwasher_exiv2_ExifData
+#define _Included_name_blackcap_exifwasher_exiv2_ExifData
+#ifdef __cplusplus
+extern "C" {
+#endif
+/*
+ * Class:     name_blackcap_exifwasher_exiv2_ExifData
+ * Method:    _erase
+ * Signature: (Ljava/lang/String;)V
+ */
+JNIEXPORT void JNICALL Java_name_blackcap_exifwasher_exiv2_ExifData__1erase
+  (JNIEnv *jEnv, jobject jThis, jstring key) {
+      Exiv2::ExifData *data = reinterpret_cast<Exiv2::ExifData *> (getPointer(jEnv, jThis));
+      const char *cKey = jEnv->GetStringUTFChars(key, NULL);
+      Exiv2::ExifData::iterator found = data->findKey(Exiv2::ExifKey(std::string(cKey)));
+      try {
+          data->erase(found);
+      } catch (...) {
+          jclass ex = jEnv->FindClass("name/blackcap/exifwasher/exiv2/Exiv2Exception");
+          const char *pfx = "unable to delete ";
+          char *message = (char *) malloc(strlen(cKey) + strlen(pfx) + 1);
+          strcpy(message, pfx);
+          strcat(message, cKey);
+          jEnv->ThrowNew(ex, message);
+          free(message);
+      }
+      jEnv->ReleaseStringUTFChars(key, cKey);
+  }
+
+/*
+ * Class:     name_blackcap_exifwasher_exiv2_ExifData
+ * Method:    _value
+ * Signature: (Ljava/lang/String;)Lname/blackcap/exifwasher/exiv2/ExifData/Value;
+ */
+JNIEXPORT jobject JNICALL Java_name_blackcap_exifwasher_exiv2_ExifData__1value
+  (JNIEnv *jEnv, jobject jThis, jstring key) {
+      Exiv2::ExifData *data = reinterpret_cast<Exiv2::ExifData *> (getPointer(jEnv, jThis));
+      const char *cKey = jEnv->GetStringUTFChars(key, NULL);
+      Exiv2::ExifData::const_iterator found = data->findKey(Exiv2::ExifKey(std::string(cKey)));
+      jEnv->ReleaseStringUTFChars(key, cKey);
+      if (found == data->end()) {
+          return NULL;
+      }
+      jclass klass = jEnv->FindClass("name/blackcap/exifwasher/exiv2/ExifData$Value");
+      jstring type = jEnv->NewStringUTF(found->typeName());
+      jstring value = jEnv->NewStringUTF(found->toString().c_str());
+      jmethodID method = jEnv->GetMethodID(klass, "<init>", "(Ljava/lang/String;Ljava/lang/String;)V");
+      return jEnv->NewObject(klass, method, type, value);
+  }
+
+/*
+ * Class:     name_blackcap_exifwasher_exiv2_ExifData
+ * Method:    _keys
+ * Signature: ()[Ljava/lang/String;
+ */
+JNIEXPORT jobjectArray JNICALL Java_name_blackcap_exifwasher_exiv2_ExifData__1keys
+  (JNIEnv *jEnv, jobject jThis) {
+      Exiv2::ExifData *data = reinterpret_cast<Exiv2::ExifData *> (getPointer(jEnv, jThis));
+      jclass klass = jEnv->FindClass("java/lang/String");
+      jobjectArray ret = jEnv->NewObjectArray(data->count(), klass, NULL);
+      Exiv2::ExifData::const_iterator end = data->end();
+      jsize j = 0;
+      for (Exiv2::ExifData::const_iterator i = data->begin(); i != end; ++i) {
+          jEnv->SetObjectArrayElement(ret, j++, jEnv->NewStringUTF(i->key().c_str()));
+      }
+      return ret;
+  }
+
+#ifdef __cplusplus
+}
+#endif
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/name/blackcap/exifwasher/exiv2/native.hpp	Tue Mar 31 13:24:48 2020 -0700
@@ -0,0 +1,91 @@
+/* DO NOT EDIT THIS FILE - it is machine generated */
+#include <jni.h>
+/* Header for class name_blackcap_exifwasher_exiv2_Image */
+
+#ifndef _Included_name_blackcap_exifwasher_exiv2_Image
+#define _Included_name_blackcap_exifwasher_exiv2_Image
+#ifdef __cplusplus
+extern "C" {
+#endif
+/*
+ * Class:     name_blackcap_exifwasher_exiv2_Image
+ * Method:    _ctor
+ * Signature: (Ljava/lang/String;)J
+ */
+JNIEXPORT jlong JNICALL Java_name_blackcap_exifwasher_exiv2_Image__1ctor
+  (JNIEnv *, jobject, jstring);
+
+/*
+ * Class:     name_blackcap_exifwasher_exiv2_Image
+ * Method:    _writeMetadata
+ * Signature: ()V
+ */
+JNIEXPORT void JNICALL Java_name_blackcap_exifwasher_exiv2_Image__1writeMetadata
+  (JNIEnv *, jobject);
+
+/*
+ * Class:     name_blackcap_exifwasher_exiv2_Image
+ * Method:    _getMetadata
+ * Signature: ()J
+ */
+JNIEXPORT jlong JNICALL Java_name_blackcap_exifwasher_exiv2_Image__1getMetadata
+  (JNIEnv *, jobject);
+
+/*
+ * Class:     name_blackcap_exifwasher_exiv2_Image
+ * Method:    _dtor
+ * Signature: ()V
+ */
+JNIEXPORT void JNICALL Java_name_blackcap_exifwasher_exiv2_Image__1dtor
+  (JNIEnv *, jobject);
+
+#ifdef __cplusplus
+}
+#endif
+#endif
+/* Header for class name_blackcap_exifwasher_exiv2_ExifData */
+
+#ifndef _Included_name_blackcap_exifwasher_exiv2_ExifData
+#define _Included_name_blackcap_exifwasher_exiv2_ExifData
+#ifdef __cplusplus
+extern "C" {
+#endif
+/*
+ * Class:     name_blackcap_exifwasher_exiv2_ExifData
+ * Method:    _erase
+ * Signature: (Ljava/lang/String;)V
+ */
+JNIEXPORT void JNICALL Java_name_blackcap_exifwasher_exiv2_ExifData__1erase
+  (JNIEnv *, jobject, jstring);
+
+/*
+ * Class:     name_blackcap_exifwasher_exiv2_ExifData
+ * Method:    _value
+ * Signature: (Ljava/lang/String;)Lname/blackcap/exifwasher/exiv2/ExifData/Value;
+ */
+JNIEXPORT jobject JNICALL Java_name_blackcap_exifwasher_exiv2_ExifData__1value
+  (JNIEnv *, jobject, jstring);
+
+/*
+ * Class:     name_blackcap_exifwasher_exiv2_ExifData
+ * Method:    _keys
+ * Signature: ()[Ljava/lang/String;
+ */
+JNIEXPORT jobjectArray JNICALL Java_name_blackcap_exifwasher_exiv2_ExifData__1keys
+  (JNIEnv *, jobject);
+
+#ifdef __cplusplus
+}
+#endif
+#endif
+/* Header for class name_blackcap_exifwasher_exiv2_ExifData_Value */
+
+#ifndef _Included_name_blackcap_exifwasher_exiv2_ExifData_Value
+#define _Included_name_blackcap_exifwasher_exiv2_ExifData_Value
+#ifdef __cplusplus
+extern "C" {
+#endif
+#ifdef __cplusplus
+}
+#endif
+#endif