diff --git a/library/gradle.properties b/library/gradle.properties index e157deb..ff893b0 100644 --- a/library/gradle.properties +++ b/library/gradle.properties @@ -1,3 +1,3 @@ # C2PA Native Library Version # Update this to use a different release from https://github.com/contentauth/c2pa-rs/releases -c2paVersion=v0.85.2 +c2paVersion=v0.90.0 diff --git a/library/src/androidTest/kotlin/org/contentauth/c2pa/AndroidBuilderTests.kt b/library/src/androidTest/kotlin/org/contentauth/c2pa/AndroidBuilderTests.kt index 71f620c..68794a7 100644 --- a/library/src/androidTest/kotlin/org/contentauth/c2pa/AndroidBuilderTests.kt +++ b/library/src/androidTest/kotlin/org/contentauth/c2pa/AndroidBuilderTests.kt @@ -194,6 +194,12 @@ class AndroidBuilderTests : BuilderTests() { assertTrue(result.success, "Context Builder Callback Error Paths test failed: ${result.message}") } + @Test + fun runTestReaderCrJson() = runBlocking { + val result = testReaderCrJson() + assertTrue(result.success, "Reader crJSON test failed: ${result.message}") + } + @Test fun runTestBuilderFromArchive() = runBlocking { val result = testBuilderFromArchive() diff --git a/library/src/main/jni/c2pa_jni.c b/library/src/main/jni/c2pa_jni.c index 3a3e014..ce87047 100644 --- a/library/src/main/jni/c2pa_jni.c +++ b/library/src/main/jni/c2pa_jni.c @@ -770,6 +770,26 @@ JNIEXPORT jstring JNICALL Java_org_contentauth_c2pa_Reader_toDetailedJsonNative( return result; } +JNIEXPORT jstring JNICALL Java_org_contentauth_c2pa_Reader_crjsonNative(JNIEnv *env, jobject obj, jlong readerPtr) { + if (readerPtr == 0) { + (*env)->ThrowNew(env, (*env)->FindClass(env, "java/lang/IllegalStateException"), + "Reader is not initialized"); + return NULL; + } + + struct C2paReader *reader = (struct C2paReader*)(uintptr_t)readerPtr; + char *json = c2pa_reader_crjson(reader); + + if (json == NULL) { + throw_c2pa_exception(env, "Failed to generate crJSON from reader"); + return NULL; + } + + jstring result = cstring_to_jstring(env, json); + c2pa_free(json); + return result; +} + JNIEXPORT jstring JNICALL Java_org_contentauth_c2pa_Reader_remoteUrlNative(JNIEnv *env, jobject obj, jlong readerPtr) { if (readerPtr == 0) { (*env)->ThrowNew(env, (*env)->FindClass(env, "java/lang/IllegalStateException"), @@ -853,13 +873,25 @@ JNIEXPORT jlong JNICALL Java_org_contentauth_c2pa_Builder_nativeFromArchive(JNIE } struct C2paStream *stream = (struct C2paStream*)(uintptr_t)streamPtr; - struct C2paBuilder *builder = c2pa_builder_from_archive(stream); - + + // Create a builder from a default context, then attach the archive. The + // context can be released once the builder has been created from it. + struct C2paContext *ctx = c2pa_context_new(); + struct C2paBuilder *builder = NULL; + if (ctx != NULL) { + struct C2paBuilder *base = c2pa_builder_from_context(ctx); + if (base != NULL) { + // with_archive consumes `base` and returns a new builder. + builder = c2pa_builder_with_archive(base, stream); + } + c2pa_free(ctx); + } + if (builder == NULL) { throw_c2pa_exception(env, "Failed to create builder from archive"); return 0; } - + return (jlong)(uintptr_t)builder; } diff --git a/library/src/main/kotlin/org/contentauth/c2pa/Reader.kt b/library/src/main/kotlin/org/contentauth/c2pa/Reader.kt index b9ead79..b241d9c 100644 --- a/library/src/main/kotlin/org/contentauth/c2pa/Reader.kt +++ b/library/src/main/kotlin/org/contentauth/c2pa/Reader.kt @@ -296,6 +296,27 @@ class Reader internal constructor(private var ptr: Long) : Closeable { return json } + /** + * Returns the manifest store as a crJSON string. + * + * crJSON is the Content Credentials JSON export format defined by the crJSON specification. + * Use [json] for the standard representation or [detailedJson] for the verbose one. + * + * @return The manifest as a crJSON string + * @throws C2PAError.Api if the manifest cannot be serialized + * + * @see json + * @see detailedJson + */ + @Throws(C2PAError::class) + fun crJSON(): String { + val json = crjsonNative(ptr) + if (json == null) { + throw C2PAError.Api(C2PA.getError() ?: "Failed to convert to crJSON") + } + return json + } + /** * Returns the remote URL where the manifest is hosted, if available. * @@ -395,6 +416,7 @@ class Reader internal constructor(private var ptr: Long) : Closeable { private external fun withFragmentNative(handle: Long, format: String, streamHandle: Long, fragmentHandle: Long): Long private external fun toJsonNative(handle: Long): String? private external fun toDetailedJsonNative(handle: Long): String? + private external fun crjsonNative(handle: Long): String? private external fun remoteUrlNative(handle: Long): String? private external fun isEmbeddedNative(handle: Long): Boolean private external fun resourceToStreamNative(handle: Long, uri: String, streamHandle: Long): Long diff --git a/test-app/app/src/main/kotlin/org/contentauth/c2pa/testapp/TestScreen.kt b/test-app/app/src/main/kotlin/org/contentauth/c2pa/testapp/TestScreen.kt index 60ba1d0..56babd9 100644 --- a/test-app/app/src/main/kotlin/org/contentauth/c2pa/testapp/TestScreen.kt +++ b/test-app/app/src/main/kotlin/org/contentauth/c2pa/testapp/TestScreen.kt @@ -205,6 +205,7 @@ private suspend fun runAllTests(context: Context): List = withContex results.add(builderTests.testContextHttpResolverRemoteFetch()) results.add(builderTests.testContextHttpResolverOkHttpFetch()) results.add(builderTests.testContextBuilderCallbackErrorPaths()) + results.add(builderTests.testReaderCrJson()) results.add(builderTests.testBuilderFromArchive()) results.add(builderTests.testReaderWithManifestData()) results.add(builderTests.testJsonRoundTrip()) diff --git a/test-shared/src/main/kotlin/org/contentauth/c2pa/test/shared/BuilderTests.kt b/test-shared/src/main/kotlin/org/contentauth/c2pa/test/shared/BuilderTests.kt index 293b307..4b7df51 100644 --- a/test-shared/src/main/kotlin/org/contentauth/c2pa/test/shared/BuilderTests.kt +++ b/test-shared/src/main/kotlin/org/contentauth/c2pa/test/shared/BuilderTests.kt @@ -1239,6 +1239,47 @@ abstract class BuilderTests : TestBase() { } } + suspend fun testReaderCrJson(): TestResult = withContext(Dispatchers.IO) { + runTest("Reader crJSON") { + try { + val certPem = loadResourceAsString("es256_certs") + val keyPem = loadResourceAsString("es256_private") + val sourceImageData = loadResourceAsBytes("pexels_asadphoto_457882") + + val signedData = Builder.fromJson(TEST_MANIFEST_JSON).use { builder -> + ByteArrayStream(sourceImageData).use { source -> + ByteArrayStream().use { dest -> + Signer.fromInfo(SignerInfo(SigningAlgorithm.ES256, certPem, keyPem)).use { signer -> + builder.sign("image/jpeg", source, dest, signer) + } + dest.getData() + } + } + } + + val crjson = ByteArrayStream(signedData).use { signedStream -> + Reader.fromStream("image/jpeg", signedStream).use { reader -> + reader.crJSON() + } + } + + val success = crjson.isNotEmpty() + TestResult( + "Reader crJSON", + success, + if (success) { + "crJSON returned ${crjson.length} chars" + } else { + "crJSON was empty" + }, + "crJSON length: ${crjson.length}", + ) + } catch (e: C2PAError) { + TestResult("Reader crJSON", false, "crJSON flow threw", e.toString()) + } + } + } + suspend fun testBuilderFromArchive(): TestResult = withContext(Dispatchers.IO) { runTest("Builder from Archive") { val manifestJson = TEST_MANIFEST_JSON