diff --git a/CMakeLists.txt b/CMakeLists.txt
index bca4bdb..b3c6980 100755
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -180,7 +180,12 @@ if (${CMAKE_SYSTEM_PROCESSOR} MATCHES "arm" OR ${CMAKE_SYSTEM_PROCESSOR} MATCHES
# TODO: arm msvc?
else()
if (${CMAKE_SYSTEM_PROCESSOR} MATCHES "aarch64")
- add_compile_options(-mcpu=native)
+ # Check CLIP_NATIVE here to add -mcpu=native flag
+ # Without this check, Android NDK complains that '-mcpu=native'
+ # is not supported even after setting -DCLIP_NATIVE=Off
+ if (CLIP_NATIVE)
+ add_compile_options(-mcpu=native)
+ endif()
endif()
# TODO: armv6,7,8 version specific flags
endif()
diff --git a/clip.cpp b/clip.cpp
index a3710de..5ceb677 100644
--- a/clip.cpp
+++ b/clip.cpp
@@ -536,9 +536,13 @@ struct clip_ctx * clip_model_load(const char * fname, const int verbosity = 1) {
int idx_mean = get_key_idx(ctx, KEY_IMAGE_MEAN);
int idx_std = get_key_idx(ctx, KEY_IMAGE_STD);
+
+ // `gguf_get_arr_data(ctx, idx_mean) + i` is needed here, to access all elements from
+ // entities with ids `idx_mean` and `idx_std`.
+ // See issue https://github.com/monatis/clip.cpp/issues/99
for (int i = 0; i < 3; ++i) {
- new_clip->image_mean[i] = *((float *)gguf_get_arr_data(ctx, idx_mean));
- new_clip->image_std[i] = *((float *)gguf_get_arr_data(ctx, idx_std));
+ new_clip->image_mean[i] = *((float *)gguf_get_arr_data(ctx, idx_mean) + i);
+ new_clip->image_std[i] = *((float *)gguf_get_arr_data(ctx, idx_std) + i);
}
if (verbosity >= 2) {
@@ -795,6 +799,35 @@ bool clip_image_preprocess(const clip_ctx * ctx, const clip_image_u8 * img, clip
return true;
}
+bool clip_image_preprocess_no_resize(const clip_ctx * ctx, const clip_image_u8 * img, clip_image_f32 * res) {
+ if (!ctx->has_vision_encoder) {
+ printf("This gguf file seems to have no vision encoder\n");
+ return false;
+ }
+ const int nx2 = ctx->vision_model.hparams.image_size;
+ const int ny2 = ctx->vision_model.hparams.image_size;
+
+ res->nx = nx2;
+ res->ny = ny2;
+ res->size = 3 * nx2 * ny2;
+ res->data = new float[res->size]();
+
+ const auto & m3 = ctx->image_mean; // {0.48145466f, 0.4578275f, 0.40821073f};
+ const auto & s3 = ctx->image_std; // {0.26862954f, 0.26130258f, 0.27577711f};
+
+ for (int y = 0; y < ny2; y++) {
+ for (int x = 0; x < nx2; x++) {
+ for (int c = 0; c < 3; c++) {
+ const int i = 3 * (y * nx2 + x) + c;
+ res->data[i] = ((float(img->data[i]) / 255.0f) - m3[c]) / s3[c];
+ }
+ }
+ }
+
+ return true;
+
+}
+
// Structure to hold the image data as an input to function to be executed for thread
typedef struct {
const clip_image_u8 * input;
diff --git a/clip.h b/clip.h
index 183b22d..0ebefe0 100644
--- a/clip.h
+++ b/clip.h
@@ -87,6 +87,8 @@ void clip_image_f32_free(struct clip_image_f32 * res);
bool clip_image_load_from_file(const char * fname, struct clip_image_u8 * img);
bool clip_image_preprocess(const struct clip_ctx * ctx, const struct clip_image_u8 * img, struct clip_image_f32 * res);
+bool clip_image_preprocess_no_resize(const struct clip_ctx * ctx, const struct clip_image_u8 * img, struct clip_image_f32 * res);
+
bool clip_text_encode(const struct clip_ctx * ctx, const int n_threads, const struct clip_tokens * tokens, float * vec,
const bool normalize);
bool clip_image_encode(const struct clip_ctx * ctx, const int n_threads, struct clip_image_f32 * img, float * vec,
diff --git a/examples/clip.android/.gitignore b/examples/clip.android/.gitignore
new file mode 100644
index 0000000..10cfdbf
--- /dev/null
+++ b/examples/clip.android/.gitignore
@@ -0,0 +1,10 @@
+*.iml
+.gradle
+/local.properties
+/.idea
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
diff --git a/examples/clip.android/README.md b/examples/clip.android/README.md
new file mode 100644
index 0000000..76d9de9
--- /dev/null
+++ b/examples/clip.android/README.md
@@ -0,0 +1,50 @@
+# clip.cpp - Android App
+
+An Android app demonstrating the usage of `clip.cpp` library. It uses JNI and a Java wrapper class to interface with the functions provided in `clip.h`.
+
+## Setup
+
+### Build the app
+
+1. Open the current directory (`clip.cpp/examples/clip.android`) in Android Studio. An automatic Gradle build should start, if not click on the `Build` menu and select `Make Project`.
+
+2. Connect the test-device to the computer and make sure that the device is recognized by the computer.
+
+3. Download one of the GGUF models from the [HuggingFace repository](https://huggingface.co/my). For instance, if we download the `CLIP-ViT-B-32-laion2B-s34B-b79K_ggml-model-f16.gguf` model, we need to push it to the test-device's file-system using `adb push`,
+
+```commandline
+adb push CLIP-ViT-B-32-laion2B-s34B-b79K_ggml-model-f16.gguf /data/local/tmp/clip_model_fp16.gguf
+```
+
+4. In `MainActivityViewModel.kt`, ensure that the `modelPath` variable points to the correct model path on the test-device. For instance, if the model is pushed to `/data/local/tmp/clip_model_fp16.gguf`, then the `modelPath` variable should be set to `/data/local/tmp/clip_model_fp16.gguf`. Moreover, you can configure `NUM_THREADS` and `VERBOSITY` variables as well.
+
+```kotlin
+private val MODEL_PATH = "/data/local/tmp/clip_model_fp16.gguf"
+private val NUM_THREADS = 4
+private val VERBOSITY = 1
+```
+
+5. Run the app on the test-device by clicking on the `Run` button (Shift + F10) in Android Studio.
+
+### Run tests
+
+This Android project also includes an instrumented test which would require an Android device (emulator or physical device).
+
+1. Open the current directory (`clip.cpp/examples/clip.android`) in Android Studio. An automatic Gradle build should start, if not click on the `Build` menu and select `Make Project`.
+
+2. Connect the test-device to the computer and make sure that the device is recognized by the computer.
+
+3. Download one of the GGUF models from the [HuggingFace repository](https://huggingface.co/my). For instance, if we download the `CLIP-ViT-B-32-laion2B-s34B-b79K_ggml-model-f16.gguf` model, we need to push it to the test-device's file-system using `adb push`,
+
+```commandline
+adb push CLIP-ViT-B-32-laion2B-s34B-b79K_ggml-model-f16.gguf /data/local/tmp/clip_model.gguf
+```
+
+4. Get two images from the internet and push them to the test-device's file-system using `adb push`,
+
+```commandline
+adb push image1.png /data/local/tmp/sample.png
+adb push image2.png /data/local/tmp/sample_2.png
+```
+
+5. Navigate to `clip.cpp/examples/clip.android/clip/src/androidTest/java/android/example/clip/CLIPAndroidInstrumentedTest.kt`, right-click on the file, select `Run 'CLIPAndroidInstrumentedTest'` from the context menu.
diff --git a/examples/clip.android/app/.gitignore b/examples/clip.android/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/examples/clip.android/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/examples/clip.android/app/build.gradle.kts b/examples/clip.android/app/build.gradle.kts
new file mode 100644
index 0000000..5e7d3fc
--- /dev/null
+++ b/examples/clip.android/app/build.gradle.kts
@@ -0,0 +1,71 @@
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.jetbrains.kotlin.android)
+}
+
+android {
+ namespace = "android.example.clip"
+ compileSdk = 34
+
+ defaultConfig {
+ applicationId = "android.example.clip"
+ minSdk = 26
+ targetSdk = 34
+ versionCode = 1
+ versionName = "1.0"
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ vectorDrawables {
+ useSupportLibrary = true
+ }
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+ buildFeatures {
+ compose = true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = "1.5.1"
+ }
+ packaging {
+ resources {
+ excludes += "/META-INF/{AL2.0,LGPL2.1}"
+ }
+ }
+}
+
+dependencies {
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation(libs.androidx.activity.compose)
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.androidx.ui)
+ implementation(libs.androidx.ui.graphics)
+ implementation(libs.androidx.ui.tooling.preview)
+ implementation(libs.androidx.material3)
+ implementation("androidx.exifinterface:exifinterface:1.3.7" )
+ implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.5")
+ implementation(project(":clip"))
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+ androidTestImplementation(platform(libs.androidx.compose.bom))
+ androidTestImplementation(libs.androidx.ui.test.junit4)
+ debugImplementation(libs.androidx.ui.tooling)
+ debugImplementation(libs.androidx.ui.test.manifest)
+}
\ No newline at end of file
diff --git a/examples/clip.android/app/proguard-rules.pro b/examples/clip.android/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/examples/clip.android/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/examples/clip.android/app/src/main/AndroidManifest.xml b/examples/clip.android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..3bb958e
--- /dev/null
+++ b/examples/clip.android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/clip.android/app/src/main/java/android/example/clip/MainActivity.kt b/examples/clip.android/app/src/main/java/android/example/clip/MainActivity.kt
new file mode 100644
index 0000000..02c78c8
--- /dev/null
+++ b/examples/clip.android/app/src/main/java/android/example/clip/MainActivity.kt
@@ -0,0 +1,314 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2024 Shubham Panchal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package android.example.clip
+
+import android.example.clip.ui.components.AppProgressDialog
+import android.example.clip.ui.components.hideProgressDialog
+import android.example.clip.ui.components.setProgressDialogText
+import android.example.clip.ui.components.showProgressDialog
+import android.example.clip.ui.theme.ClipcppTheme
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory.*
+import android.graphics.Matrix
+import androidx.exifinterface.media.ExifInterface
+import android.net.Uri
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.activity.result.PickVisualMediaRequest
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material.icons.filled.Info
+import androidx.compose.material3.Button
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.window.Dialog
+import androidx.lifecycle.viewmodel.compose.viewModel
+
+class MainActivity : ComponentActivity() {
+
+ @OptIn(ExperimentalMaterial3Api::class)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ setContent {
+
+ val viewModel = viewModel()
+
+ ClipcppTheme {
+ Scaffold(
+ modifier = Modifier.fillMaxSize(),
+ topBar = {
+ TopAppBar(
+ title = { Text(text = "clip.android") },
+ actions = {
+ Row {
+ IconButton(onClick = {
+ viewModel.showModelInfo()
+ }) {
+ Icon(
+ imageVector = Icons.Default.Info,
+ contentDescription = "Model Info"
+ )
+ }
+ }
+ }
+ )
+ }
+ ) { innerPadding ->
+ Column(modifier = Modifier.padding(innerPadding)) {
+ SelectImagePanel(viewModel)
+ EnterDescriptionPanel(viewModel)
+ }
+ LoadModelProgressDialog(viewModel)
+ RunningInferenceProgressDialog(viewModel)
+ ModelInfoDialog(viewModel)
+ }
+ }
+ }
+ }
+
+ @Composable
+ private fun ColumnScope.SelectImagePanel(viewModel: MainActivityViewModel) {
+ var selectedImage by remember { viewModel.selectedImageState }
+ val pickMediaLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.PickVisualMedia()
+ ) {
+ if (it != null) {
+ val bitmap = getFixedBitmap(it)
+ selectedImage = bitmap
+ }
+ }
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color.LightGray)
+ .weight(1f),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ if (selectedImage == null) {
+ Button(modifier = Modifier.padding(vertical = 40.dp), onClick = {
+ pickMediaLauncher.launch(
+ PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
+ )
+ }) {
+ Icon(imageVector = Icons.Default.Add, contentDescription = "Select an image")
+ Text(text = "Select an image")
+ }
+ } else {
+ Image(
+ bitmap = selectedImage!!.asImageBitmap(),
+ contentDescription = "Selected image",
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ }
+ }
+
+ @Composable
+ private fun ColumnScope.EnterDescriptionPanel(viewModel: MainActivityViewModel) {
+ var description by remember{ viewModel.descriptionState }
+ val similarityScore by remember{ viewModel.similarityScoreState }
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .weight(1f)
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(24.dp),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ if (similarityScore == null) {
+ TextField(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 16.dp),
+ label = { Text(text = "Enter a description") },
+ value = description,
+ onValueChange = { description = it }
+ )
+ Button(
+ enabled = description.isNotEmpty(),
+ onClick = { viewModel.compare() }
+ ) {
+ Text(text = "Compare")
+ }
+ }
+ else {
+ Text(
+ text = "Similarity score: $similarityScore",
+ modifier = Modifier.padding(vertical = 16.dp),
+ fontSize = 24.sp
+ )
+ Button(onClick = { viewModel.reset() }) {
+ Text(text = "Compare again")
+ }
+ }
+ }
+ }
+ }
+
+ @Composable
+ private fun LoadModelProgressDialog(viewModel: MainActivityViewModel) {
+ val isLoadingModel by remember{ viewModel.isLoadingModelState }
+ if (isLoadingModel) {
+ showProgressDialog()
+ setProgressDialogText("Loading model...")
+ }
+ else {
+ hideProgressDialog()
+ }
+ AppProgressDialog()
+ }
+
+ @Composable
+ private fun RunningInferenceProgressDialog(viewModel: MainActivityViewModel) {
+ val isInferenceRunning by remember{ viewModel.isInferenceRunning }
+ if (isInferenceRunning) {
+ showProgressDialog()
+ setProgressDialogText("Running inference...")
+ }
+ else {
+ hideProgressDialog()
+ }
+ AppProgressDialog()
+ }
+
+ @Composable
+ private fun ModelInfoDialog(viewModel: MainActivityViewModel) {
+ var showDialog by remember{ viewModel.isShowingModelInfoDialogState }
+ if (showDialog && viewModel.visionHyperParameters != null && viewModel.textHyperParameters != null) {
+ Dialog(
+ onDismissRequest = { showDialog = false }
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp)
+ .background(Color.White, RoundedCornerShape(8.dp))
+ .padding(16.dp)
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = "Model Info",
+ modifier = Modifier.weight(1f),
+ style = MaterialTheme.typography.headlineLarge
+ )
+ IconButton(
+ onClick = { showDialog = false }
+ ) {
+ Icon(
+ imageVector = Icons.Default.Close,
+ contentDescription = "Close Model Info Dialog"
+ )
+ }
+ }
+ Spacer(modifier = Modifier.height(8.dp))
+
+ Text(text = "Vision Hyper-parameters", style = MaterialTheme.typography.bodyLarge)
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(text = "imageSize = ${viewModel.visionHyperParameters?.imageSize}", style = MaterialTheme.typography.labelSmall)
+ Text(text = "hiddenSize = ${viewModel.visionHyperParameters?.hiddenSize}", style = MaterialTheme.typography.labelSmall)
+ Text(text = "patchSize = ${viewModel.visionHyperParameters?.patchSize}", style = MaterialTheme.typography.labelSmall)
+ Text(text = "projectionDim = ${viewModel.visionHyperParameters?.projectionDim}", style = MaterialTheme.typography.labelSmall)
+ Text(text = "num layers = ${viewModel.visionHyperParameters?.nLayer}", style = MaterialTheme.typography.labelSmall)
+ Text(text = "num intermediate = ${viewModel.visionHyperParameters?.nIntermediate}", style = MaterialTheme.typography.labelSmall)
+ Text(text = "num heads = ${viewModel.visionHyperParameters?.nHead}", style = MaterialTheme.typography.labelSmall)
+ Spacer(modifier = Modifier.height(8.dp))
+
+ Text(text = "Text Hyper-parameters", style = MaterialTheme.typography.bodyLarge)
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(text = "num positions = ${viewModel.textHyperParameters?.numPositions}", style = MaterialTheme.typography.labelSmall)
+ Text(text = "hiddenSize = ${viewModel.textHyperParameters?.hiddenSize}", style = MaterialTheme.typography.labelSmall)
+ Text(text = "num vocab = ${viewModel.textHyperParameters?.nVocab}", style = MaterialTheme.typography.labelSmall)
+ Text(text = "projectionDim = ${viewModel.textHyperParameters?.projectionDim}", style = MaterialTheme.typography.labelSmall)
+ Text(text = "num layers = ${viewModel.textHyperParameters?.nLayer}", style = MaterialTheme.typography.labelSmall)
+ Text(text = "num intermediate = ${viewModel.textHyperParameters?.nIntermediate}", style = MaterialTheme.typography.labelSmall)
+ Text(text = "num heads = ${viewModel.textHyperParameters?.nHead}", style = MaterialTheme.typography.labelSmall)
+ }
+ }
+ }
+ }
+
+ private fun getFixedBitmap(imageFileUri: Uri): Bitmap {
+ var imageBitmap = decodeStream(contentResolver.openInputStream(imageFileUri))
+ val exifInterface = ExifInterface(contentResolver.openInputStream(imageFileUri)!!)
+ imageBitmap = when (exifInterface.getAttributeInt(
+ ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED
+ )) {
+ ExifInterface.ORIENTATION_ROTATE_90 -> rotateBitmap(imageBitmap, 90f)
+ ExifInterface.ORIENTATION_ROTATE_180 -> rotateBitmap(imageBitmap, 180f)
+ ExifInterface.ORIENTATION_ROTATE_270 -> rotateBitmap(imageBitmap, 270f)
+ else -> imageBitmap
+ }
+ return imageBitmap
+ }
+
+ private fun rotateBitmap(source: Bitmap, degrees: Float): Bitmap {
+ val matrix = Matrix()
+ matrix.postRotate(degrees)
+ return Bitmap.createBitmap(source, 0, 0, source.width, source.height, matrix, false)
+ }
+
+}
+
diff --git a/examples/clip.android/app/src/main/java/android/example/clip/MainActivityViewModel.kt b/examples/clip.android/app/src/main/java/android/example/clip/MainActivityViewModel.kt
new file mode 100644
index 0000000..e5ff4a6
--- /dev/null
+++ b/examples/clip.android/app/src/main/java/android/example/clip/MainActivityViewModel.kt
@@ -0,0 +1,127 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2024 Shubham Panchal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package android.example.clip
+
+import android.clip.cpp.CLIPAndroid
+import android.graphics.Bitmap
+import androidx.compose.runtime.mutableStateOf
+import androidx.lifecycle.ViewModel
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.nio.ByteBuffer
+
+class MainActivityViewModel: ViewModel() {
+
+ val selectedImageState = mutableStateOf(null)
+ val descriptionState = mutableStateOf("")
+ val isLoadingModelState = mutableStateOf(true)
+ val isInferenceRunning = mutableStateOf(false)
+ val isShowingModelInfoDialogState = mutableStateOf(false)
+ val similarityScoreState = mutableStateOf(null)
+ private val clipAndroid = CLIPAndroid()
+ var visionHyperParameters: CLIPAndroid.CLIPVisionHyperParameters? = null
+ var textHyperParameters: CLIPAndroid.CLIPTextHyperParameters? = null
+
+ private val MODEL_PATH = "/data/local/tmp/clip_model_fp16.gguf"
+ private val NUM_THREADS = 4
+ private val VERBOSITY = 1
+
+ init {
+ CoroutineScope(Dispatchers.IO).launch {
+ mainScope { isLoadingModelState.value = true }
+ clipAndroid.load(MODEL_PATH, VERBOSITY)
+ visionHyperParameters = clipAndroid.visionHyperParameters
+ textHyperParameters = clipAndroid.textHyperParameters
+ mainScope { isLoadingModelState.value = false }
+ }
+ }
+
+ fun compare() {
+ if (selectedImageState.value != null && descriptionState.value.isNotEmpty()) {
+ CoroutineScope(Dispatchers.Default).launch {
+ mainScope { isInferenceRunning.value = true }
+ val textEmbedding = clipAndroid.encodeText(
+ descriptionState.value,
+ NUM_THREADS,
+ textHyperParameters?.projectionDim ?: 512,
+ true
+ )
+ val imageBuffer = bitmapToByteBuffer(selectedImageState.value!!)
+ val imageEmbedding = clipAndroid.encodeImage(
+ imageBuffer,
+ selectedImageState.value!!.width,
+ selectedImageState.value!!.height,
+ NUM_THREADS,
+ visionHyperParameters?.projectionDim ?: 512,
+ true
+ )
+ mainScope {
+ similarityScoreState.value = clipAndroid.getSimilarityScore(textEmbedding, imageEmbedding)
+ isInferenceRunning.value = false
+ }
+ }
+ }
+ }
+
+ fun showModelInfo() {
+ isShowingModelInfoDialogState.value = true
+ }
+
+ fun reset() {
+ selectedImageState.value = null
+ descriptionState.value = ""
+ similarityScoreState.value = null
+ isInferenceRunning.value = false
+ }
+
+ override fun onCleared() {
+ super.onCleared()
+ clipAndroid.close()
+ }
+
+ private suspend fun mainScope(action: () -> Unit) {
+ withContext(Dispatchers.Main) {
+ action()
+ }
+ }
+
+ private fun bitmapToByteBuffer(bitmap: Bitmap): ByteBuffer {
+ val width = bitmap.width
+ val height = bitmap.height
+ val imageBuffer = ByteBuffer.allocateDirect(width * height * 3)
+ for (y in 0 until height) {
+ for (x in 0 until width) {
+ val pixel = bitmap.getPixel(x, y)
+ imageBuffer.put((pixel shr 16 and 0xFF).toByte())
+ imageBuffer.put((pixel shr 8 and 0xFF).toByte())
+ imageBuffer.put((pixel and 0xFF).toByte())
+ }
+ }
+ return imageBuffer
+ }
+
+}
\ No newline at end of file
diff --git a/examples/clip.android/app/src/main/java/android/example/clip/ui/components/AppProgressDialog.kt b/examples/clip.android/app/src/main/java/android/example/clip/ui/components/AppProgressDialog.kt
new file mode 100644
index 0000000..80411ee
--- /dev/null
+++ b/examples/clip.android/app/src/main/java/android/example/clip/ui/components/AppProgressDialog.kt
@@ -0,0 +1,65 @@
+package android.example.clip.ui.components
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.LinearProgressIndicator
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+
+private val progressDialogVisibleState = mutableStateOf(false)
+private val progressDialogText = mutableStateOf("")
+
+@Composable
+fun AppProgressDialog() {
+ val isVisible by remember { progressDialogVisibleState }
+ if (isVisible) {
+ Dialog(onDismissRequest = { /* Progress dialogs are non-cancellable */ }) {
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier =
+ Modifier.fillMaxWidth()
+ .background(Color.White, shape = RoundedCornerShape(8.dp))
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier.padding(vertical = 24.dp)
+ ) {
+ LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
+ Spacer(modifier = Modifier.padding(4.dp))
+ Text(
+ text = progressDialogText.value,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp)
+ )
+ }
+ }
+ }
+ }
+}
+
+fun setProgressDialogText(message: String) {
+ progressDialogText.value = message
+}
+
+fun showProgressDialog() {
+ progressDialogVisibleState.value = true
+ progressDialogText.value = ""
+}
+
+fun hideProgressDialog() {
+ progressDialogVisibleState.value = false
+}
\ No newline at end of file
diff --git a/examples/clip.android/app/src/main/java/android/example/clip/ui/theme/Color.kt b/examples/clip.android/app/src/main/java/android/example/clip/ui/theme/Color.kt
new file mode 100644
index 0000000..3b494c2
--- /dev/null
+++ b/examples/clip.android/app/src/main/java/android/example/clip/ui/theme/Color.kt
@@ -0,0 +1,11 @@
+package android.example.clip.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val Purple80 = Color(0xFFD0BCFF)
+val PurpleGrey80 = Color(0xFFCCC2DC)
+val Pink80 = Color(0xFFEFB8C8)
+
+val Purple40 = Color(0xFF6650a4)
+val PurpleGrey40 = Color(0xFF625b71)
+val Pink40 = Color(0xFF7D5260)
\ No newline at end of file
diff --git a/examples/clip.android/app/src/main/java/android/example/clip/ui/theme/Theme.kt b/examples/clip.android/app/src/main/java/android/example/clip/ui/theme/Theme.kt
new file mode 100644
index 0000000..ad416a8
--- /dev/null
+++ b/examples/clip.android/app/src/main/java/android/example/clip/ui/theme/Theme.kt
@@ -0,0 +1,58 @@
+package android.example.clip.ui.theme
+
+import android.app.Activity
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
+
+private val DarkColorScheme = darkColorScheme(
+ primary = Purple80,
+ secondary = PurpleGrey80,
+ tertiary = Pink80
+)
+
+private val LightColorScheme = lightColorScheme(
+ primary = Purple40,
+ secondary = PurpleGrey40,
+ tertiary = Pink40
+
+ /* Other default colors to override
+ background = Color(0xFFFFFBFE),
+ surface = Color(0xFFFFFBFE),
+ onPrimary = Color.White,
+ onSecondary = Color.White,
+ onTertiary = Color.White,
+ onBackground = Color(0xFF1C1B1F),
+ onSurface = Color(0xFF1C1B1F),
+ */
+)
+
+@Composable
+fun ClipcppTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ // Dynamic color is available on Android 12+
+ dynamicColor: Boolean = true,
+ content: @Composable () -> Unit
+) {
+ val colorScheme = when {
+ dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+ val context = LocalContext.current
+ if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+ }
+
+ darkTheme -> DarkColorScheme
+ else -> LightColorScheme
+ }
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = Typography,
+ content = content
+ )
+}
\ No newline at end of file
diff --git a/examples/clip.android/app/src/main/java/android/example/clip/ui/theme/Type.kt b/examples/clip.android/app/src/main/java/android/example/clip/ui/theme/Type.kt
new file mode 100644
index 0000000..eb7d0f3
--- /dev/null
+++ b/examples/clip.android/app/src/main/java/android/example/clip/ui/theme/Type.kt
@@ -0,0 +1,34 @@
+package android.example.clip.ui.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+// Set of Material typography styles to start with
+val Typography = Typography(
+ bodyLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.5.sp
+ )
+ /* Other default text styles to override
+ titleLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 22.sp,
+ lineHeight = 28.sp,
+ letterSpacing = 0.sp
+ ),
+ labelSmall = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Medium,
+ fontSize = 11.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp
+ )
+ */
+)
\ No newline at end of file
diff --git a/examples/clip.android/app/src/main/res/drawable/ic_launcher_background.xml b/examples/clip.android/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..07d5da9
--- /dev/null
+++ b/examples/clip.android/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/clip.android/app/src/main/res/drawable/ic_launcher_foreground.xml b/examples/clip.android/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/examples/clip.android/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/clip.android/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/examples/clip.android/app/src/main/res/mipmap-anydpi/ic_launcher.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/examples/clip.android/app/src/main/res/mipmap-anydpi/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/clip.android/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/examples/clip.android/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/examples/clip.android/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/clip.android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/examples/clip.android/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..c209e78
Binary files /dev/null and b/examples/clip.android/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/examples/clip.android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/examples/clip.android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..b2dfe3d
Binary files /dev/null and b/examples/clip.android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/examples/clip.android/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/examples/clip.android/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4f0f1d6
Binary files /dev/null and b/examples/clip.android/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/examples/clip.android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/examples/clip.android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62b611d
Binary files /dev/null and b/examples/clip.android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/examples/clip.android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/examples/clip.android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..948a307
Binary files /dev/null and b/examples/clip.android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/examples/clip.android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/examples/clip.android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..1b9a695
Binary files /dev/null and b/examples/clip.android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/examples/clip.android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/examples/clip.android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
Binary files /dev/null and b/examples/clip.android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/examples/clip.android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/examples/clip.android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9287f50
Binary files /dev/null and b/examples/clip.android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/examples/clip.android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/examples/clip.android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
Binary files /dev/null and b/examples/clip.android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/examples/clip.android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/examples/clip.android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9126ae3
Binary files /dev/null and b/examples/clip.android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/examples/clip.android/app/src/main/res/values/colors.xml b/examples/clip.android/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..f8c6127
--- /dev/null
+++ b/examples/clip.android/app/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
\ No newline at end of file
diff --git a/examples/clip.android/app/src/main/res/values/strings.xml b/examples/clip.android/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..02fb3c1
--- /dev/null
+++ b/examples/clip.android/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ clip.cpp
+
\ No newline at end of file
diff --git a/examples/clip.android/app/src/main/res/values/themes.xml b/examples/clip.android/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..91da76d
--- /dev/null
+++ b/examples/clip.android/app/src/main/res/values/themes.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/clip.android/app/src/main/res/xml/backup_rules.xml b/examples/clip.android/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..fa0f996
--- /dev/null
+++ b/examples/clip.android/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/examples/clip.android/app/src/main/res/xml/data_extraction_rules.xml b/examples/clip.android/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..9ee9997
--- /dev/null
+++ b/examples/clip.android/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/clip.android/build.gradle.kts b/examples/clip.android/build.gradle.kts
new file mode 100644
index 0000000..e3f8a07
--- /dev/null
+++ b/examples/clip.android/build.gradle.kts
@@ -0,0 +1,6 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.jetbrains.kotlin.android) apply false
+ alias(libs.plugins.android.library) apply false
+}
\ No newline at end of file
diff --git a/examples/clip.android/clip/.gitignore b/examples/clip.android/clip/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/examples/clip.android/clip/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/examples/clip.android/clip/build.gradle.kts b/examples/clip.android/clip/build.gradle.kts
new file mode 100644
index 0000000..37e4f61
--- /dev/null
+++ b/examples/clip.android/clip/build.gradle.kts
@@ -0,0 +1,56 @@
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.jetbrains.kotlin.android)
+}
+
+android {
+ namespace = "android.clip.cpp"
+ compileSdk = 34
+ defaultConfig {
+ minSdk = 26
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ externalNativeBuild {
+ // Note: to enable symbols in the stack-trace, set -DCMAKE_BUILD_TYPE=Debug
+ // below, else raw addresses would only be visible making
+ // debugging difficult
+ cmake {
+ arguments += listOf(
+ "-DCMAKE_BUILD_TYPE=Release",
+ "-DCLIP_NATIVE=OFF"
+ )
+ }
+ }
+ }
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+ externalNativeBuild {
+ cmake {
+ path("src/main/cpp/CMakeLists.txt")
+ version = "3.22.1"
+ }
+ }
+}
+
+dependencies {
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.appcompat)
+ implementation(libs.material)
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+}
\ No newline at end of file
diff --git a/examples/clip.android/clip/consumer-rules.pro b/examples/clip.android/clip/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/examples/clip.android/clip/proguard-rules.pro b/examples/clip.android/clip/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/examples/clip.android/clip/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/examples/clip.android/clip/src/androidTest/java/android/example/clip/CLIPAndroidInstrumentedTest.kt b/examples/clip.android/clip/src/androidTest/java/android/example/clip/CLIPAndroidInstrumentedTest.kt
new file mode 100644
index 0000000..6ced354
--- /dev/null
+++ b/examples/clip.android/clip/src/androidTest/java/android/example/clip/CLIPAndroidInstrumentedTest.kt
@@ -0,0 +1,186 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2024 Shubham Panchal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package android.example.clip
+
+import android.clip.cpp.CLIPAndroid
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.After
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+import org.junit.Before
+import java.nio.ByteBuffer
+
+/**
+ * An instrumented test to validate the functionality of CLIPAndroid.
+ * This test requires a GGUF CLIP model and sample images to be present on the device.
+ * See `modelPath` and `imagePaths` for the paths to the model and images respectively.
+ *
+ * @author Shubham Panchal (github.com/shubham0204)
+ */
+@RunWith(AndroidJUnit4::class)
+class CLIPAndroidInstrumentedTest {
+
+ private val clipAndroid = CLIPAndroid()
+ private val modelPath = "/data/local/tmp/clip_model.gguf"
+ private val imagePaths = listOf(
+ "/data/local/tmp/sample.png",
+ "/data/local/tmp/sample_2.png"
+ )
+
+ @Before
+ fun setup() {
+ clipAndroid.load(modelPath, 1)
+ }
+
+ @Test
+ fun getVisionHyperParameters_works() {
+ val visionHyperParameters = clipAndroid.visionHyperParameters
+ assertNotNull(visionHyperParameters)
+ }
+
+ @Test
+ fun getTextHyperParameters_works() {
+ val textHyperParameters = clipAndroid.textHyperParameters
+ assertNotNull(textHyperParameters)
+ }
+
+ @Test
+ fun imageEncode_works() {
+ val imageBitmap = BitmapFactory.decodeFile(imagePaths[0])
+ val width = imageBitmap.width
+ val height = imageBitmap.height
+ val imageBuffer = bitmapToByteBuffer(imageBitmap)
+ val numThreads = 4
+ val vectorDims = clipAndroid.visionHyperParameters.projectionDim
+ val normalize = true
+ val imageEmbedding = clipAndroid.encodeImage(imageBuffer, width, height, numThreads, vectorDims, normalize)
+ assertNotNull(imageEmbedding)
+ }
+
+ @Test
+ fun imageEncodeNoResize_works() {
+ val imageBitmap = BitmapFactory.decodeFile(imagePaths[0])
+ val resizedBitmap = Bitmap.createScaledBitmap(
+ imageBitmap,
+ clipAndroid.visionHyperParameters.imageSize,
+ clipAndroid.visionHyperParameters.imageSize,
+ true
+ )
+ val width = resizedBitmap.width
+ val height = resizedBitmap.height
+ val imageBuffer = bitmapToByteBuffer(resizedBitmap)
+ val numThreads = 4
+ val vectorDims = clipAndroid.visionHyperParameters.projectionDim
+ val normalize = true
+ val imageEmbedding = clipAndroid.encodeImageNoResize(imageBuffer, width, height, numThreads, vectorDims, normalize)
+ assertNotNull(imageEmbedding)
+ }
+
+ @Test
+ fun zeroShotClassify_works() {
+ val labels = listOf(
+ "cat", "dog", "mountains"
+ ).toTypedArray()
+ val imageBitmap = BitmapFactory.decodeFile(imagePaths[0])
+ val width = imageBitmap.width
+ val height = imageBitmap.height
+ val imageBuffer = bitmapToByteBuffer(imageBitmap)
+ val numThreads = 4
+
+ val scores = clipAndroid.zeroShotClassify(imageBuffer, width, height, numThreads, labels)
+ assertEquals(labels.size * 2, scores.size)
+ }
+
+ @Test
+ fun batchImageEncode_works() {
+ val widths = IntArray(imagePaths.size)
+ val heights = IntArray(imagePaths.size)
+ val imageBuffers = imagePaths.mapIndexed { index, imagePath ->
+ val imageBitmap = BitmapFactory.decodeFile(imagePath)
+ widths[index] = imageBitmap.width
+ heights[index] = imageBitmap.height
+ bitmapToByteBuffer(imageBitmap)
+ }
+ val numThreads = 4
+ val vectorDims = clipAndroid.visionHyperParameters.projectionDim
+ val normalize = true
+ val imageEmbeddings = clipAndroid.encodeImage(imageBuffers.toTypedArray(), widths, heights, numThreads, vectorDims, normalize)
+ assertNotNull(imageEmbeddings)
+ assertEquals(2, imageEmbeddings.size)
+ }
+
+ @Test
+ fun getSimilarityScore_equalDims_works() {
+ val vec1 = floatArrayOf(0.1f, 0.2f, 0.3f)
+ val vec2 = floatArrayOf(0.2f, 0.3f, 0.4f)
+ val similarityScore = clipAndroid.getSimilarityScore(vec1, vec2)
+ assertEquals(0.20000002f, similarityScore)
+ }
+
+ @Test
+ fun getSimilarityScore_unequalDims_throws() {
+ val vec1 = floatArrayOf(0.1f, 0.2f, 0.3f)
+ val vec2 = floatArrayOf(0.2f, 0.3f, 0.4f, 0.5f)
+ assertThrows(IllegalArgumentException::class.java) {
+ clipAndroid.getSimilarityScore(vec1, vec2)
+ }
+ }
+
+ @Test
+ fun textEncode_works() {
+ val text = "a photo of a tiny dog"
+ val numThreads = 4
+ val vectorDims = clipAndroid.textHyperParameters.projectionDim
+ val normalize = true
+ val textEmbedding = clipAndroid.encodeText(text, numThreads, vectorDims, normalize)
+ assertNotNull(textEmbedding)
+ }
+
+ @After
+ fun clean() {
+ clipAndroid.close()
+ }
+
+ private fun bitmapToByteBuffer(bitmap: Bitmap): ByteBuffer {
+ val width = bitmap.width
+ val height = bitmap.height
+ val imageBuffer = ByteBuffer.allocateDirect(width * height * 3)
+ for (y in 0 until height) {
+ for (x in 0 until width) {
+ val pixel = bitmap.getPixel(x, y)
+ imageBuffer.put((pixel shr 16 and 0xFF).toByte())
+ imageBuffer.put((pixel shr 8 and 0xFF).toByte())
+ imageBuffer.put((pixel and 0xFF).toByte())
+ }
+ }
+ return imageBuffer
+ }
+
+}
\ No newline at end of file
diff --git a/examples/clip.android/clip/src/main/AndroidManifest.xml b/examples/clip.android/clip/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..a5918e6
--- /dev/null
+++ b/examples/clip.android/clip/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/examples/clip.android/clip/src/main/cpp/CMakeLists.txt b/examples/clip.android/clip/src/main/cpp/CMakeLists.txt
new file mode 100644
index 0000000..4842fbb
--- /dev/null
+++ b/examples/clip.android/clip/src/main/cpp/CMakeLists.txt
@@ -0,0 +1,18 @@
+cmake_minimum_required(VERSION 3.22.1)
+
+project("clip-android")
+
+# Add local clip.cpp CMake project
+add_subdirectory(../../../../../../ build-clip)
+
+add_library(
+ ${CMAKE_PROJECT_NAME}
+ SHARED
+ clip_android.cpp)
+
+target_link_libraries(${CMAKE_PROJECT_NAME}
+ # List libraries link to the target library
+ clip
+ ggml
+ android
+ log)
\ No newline at end of file
diff --git a/examples/clip.android/clip/src/main/cpp/clip_android.cpp b/examples/clip.android/clip/src/main/cpp/clip_android.cpp
new file mode 100644
index 0000000..b358845
--- /dev/null
+++ b/examples/clip.android/clip/src/main/cpp/clip_android.cpp
@@ -0,0 +1,353 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2024 Shubham Panchal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#include
+#include
+#include "clip.h"
+
+#define TAG "clip-android.cpp"
+#define LOGi(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
+#define LOGe(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
+
+extern "C" JNIEXPORT jlong JNICALL
+Java_android_clip_cpp_CLIPAndroid_clipModelLoad__Ljava_lang_String_2I(
+ JNIEnv *env,
+ jobject,
+ jstring file_path,
+ jint verbosity
+) {
+ const char* file_path_chars = env -> GetStringUTFChars(file_path, nullptr);
+ LOGi("Loading the model from %s", file_path_chars);
+ const clip_ctx* ctx = clip_model_load(file_path_chars, verbosity);
+
+ if (!ctx) {
+ LOGe("Failed to load the model from %s", file_path_chars);
+ return 0;
+ }
+
+ env -> ReleaseStringUTFChars(file_path, file_path_chars);
+ return reinterpret_cast(ctx);
+}
+
+extern "C" JNIEXPORT void JNICALL
+Java_android_clip_cpp_CLIPAndroid_clipModelRelease__J(
+ JNIEnv *env,
+ jobject,
+ jlong ctx_ptr
+) {
+ auto* ctx = reinterpret_cast(ctx_ptr);
+ clip_free(ctx);
+}
+
+extern "C" JNIEXPORT jobject JNICALL
+Java_android_clip_cpp_CLIPAndroid_clipGetVisionHyperParameters__J(
+ JNIEnv *env,
+ jobject,
+ jlong ctx_ptr
+) {
+ auto* ctx = reinterpret_cast(ctx_ptr);
+ clip_vision_hparams* vision_params = clip_get_vision_hparams(ctx);
+
+ // Get the class CLIPVisionHyperParameters and and its constructor
+ // Create a new object of the class with the constructor
+ jclass cls = env -> FindClass("android/clip/cpp/CLIPAndroid$CLIPVisionHyperParameters");
+ jmethodID constructor = env -> GetMethodID(cls, "", "(IIIIIII)V");
+ jvalue args[7];
+ args[0].i = vision_params -> image_size;
+ args[1].i = vision_params -> patch_size;
+ args[2].i = vision_params -> hidden_size;
+ args[3].i = vision_params -> projection_dim;
+ args[4].i = vision_params -> n_intermediate;
+ args[5].i = vision_params -> n_head;
+ args[6].i = vision_params -> n_layer;
+ jobject object = env -> NewObjectA(cls, constructor, args);
+
+ return object;
+}
+
+extern "C" JNIEXPORT jobject JNICALL
+Java_android_clip_cpp_CLIPAndroid_clipGetTextHyperParameters__J(
+ JNIEnv *env,
+ jobject,
+ jlong ctx_ptr
+) {
+ auto* ctx = reinterpret_cast(ctx_ptr);
+ clip_text_hparams* text_params = clip_get_text_hparams(ctx);
+
+ // Get the class CLIPTextHyperParameters and and its constructor
+ // Create a new object of the class with the constructor
+ jclass cls = env -> FindClass("android/clip/cpp/CLIPAndroid$CLIPTextHyperParameters");
+ jmethodID constructor = env -> GetMethodID(cls, "", "(IIIIIII)V");
+ jvalue args[7];
+ args[0].i = text_params -> n_vocab;
+ args[1].i = text_params -> num_positions;
+ args[2].i = text_params -> hidden_size;
+ args[3].i = text_params -> projection_dim;
+ args[4].i = text_params -> n_intermediate;
+ args[5].i = text_params -> n_head;
+ args[6].i = text_params -> n_layer;
+ jobject object = env -> NewObjectA(cls, constructor, args);
+
+ return object;
+}
+
+extern "C" JNIEXPORT jfloatArray JNICALL
+Java_android_clip_cpp_CLIPAndroid_clipTextEncode__JLjava_lang_String_2IIZ(
+ JNIEnv *env,
+ jobject,
+ jlong ctx_ptr,
+ jstring text,
+ jint n_threads,
+ jint vector_dims,
+ jboolean normalize
+) {
+ LOGi("Vector dims: %d", vector_dims);
+ LOGi("Normalize: %d", normalize);
+ LOGi("Number of threads: %d", n_threads);
+
+ auto* ctx = reinterpret_cast(ctx_ptr);
+
+ const char* text_chars = env -> GetStringUTFChars(text, nullptr);
+ LOGi("Text: %s", text_chars);
+
+ auto* tokens = new clip_tokens();
+ clip_tokenize(ctx, text_chars, tokens);
+ env -> ReleaseStringUTFChars(text, text_chars);
+ float text_embedding[vector_dims];
+ clip_text_encode(ctx, n_threads, tokens, text_embedding, normalize);
+
+ jfloatArray result = env -> NewFloatArray(vector_dims);
+ env -> SetFloatArrayRegion(result, 0, vector_dims, text_embedding);
+ delete tokens;
+
+ return result;
+}
+
+extern "C" JNIEXPORT jfloatArray JNICALL
+Java_android_clip_cpp_CLIPAndroid_clipImageEncode__JLjava_nio_ByteBuffer_2IIIIZ(
+ JNIEnv *env,
+ jobject,
+ jlong ctx_ptr,
+ jobject img_buffer,
+ jint width,
+ jint height,
+ jint n_threads,
+ jint vector_dims,
+ jboolean normalize
+) {
+ LOGi("Vector dims: %d", vector_dims);
+ LOGi("Normalize: %d", normalize);
+ LOGi("Image size: %d x %d", width, height);
+
+ auto* ctx = reinterpret_cast(ctx_ptr);
+ auto* img = clip_image_u8_make();
+ img -> nx = width;
+ img -> ny = height;
+ img -> data = reinterpret_cast(env -> GetDirectBufferAddress(img_buffer));
+ img -> size = width * height * 3;
+
+ auto* img_f32 = clip_image_f32_make();
+ img_f32 -> nx = width;
+ img_f32 -> ny = height;
+ img_f32 -> data = new float[width * height * 3];
+ img_f32 -> size = width * height * 3;
+ clip_image_preprocess(ctx, img, img_f32);
+
+ float image_embedding[vector_dims];
+ clip_image_encode(ctx, n_threads, img_f32, image_embedding, normalize);
+ jfloatArray result = env -> NewFloatArray(vector_dims);
+ env -> SetFloatArrayRegion(result, 0, vector_dims, image_embedding);
+
+ return result;
+}
+
+extern "C" JNIEXPORT jfloatArray JNICALL
+Java_android_clip_cpp_CLIPAndroid_clipImageEncodeNoResize__JLjava_nio_ByteBuffer_2IIIIZ(
+ JNIEnv *env,
+ jobject,
+ jlong ctx_ptr,
+ jobject img_buffer,
+ jint width,
+ jint height,
+ jint n_threads,
+ jint vector_dims,
+ jboolean normalize
+) {
+ LOGi("Vector dims: %d", vector_dims);
+ LOGi("Normalize: %d", normalize);
+ LOGi("Image size: %d x %d", width, height);
+
+ auto* ctx = reinterpret_cast(ctx_ptr);
+ auto* img = clip_image_u8_make();
+ img -> nx = width;
+ img -> ny = height;
+ img -> data = reinterpret_cast(env -> GetDirectBufferAddress(img_buffer));
+ img -> size = width * height * 3;
+
+ auto* img_f32 = clip_image_f32_make();
+ img_f32 -> nx = width;
+ img_f32 -> ny = height;
+ img_f32 -> data = new float[width * height * 3];
+ img_f32 -> size = width * height * 3;
+ clip_image_preprocess(ctx, img, img_f32);
+
+ float image_embedding[vector_dims];
+ clip_image_encode(ctx, n_threads, img_f32, image_embedding, normalize);
+ jfloatArray result = env -> NewFloatArray(vector_dims);
+ env -> SetFloatArrayRegion(result, 0, vector_dims, image_embedding);
+
+ return result;
+}
+
+extern "C"
+JNIEXPORT jfloatArray JNICALL
+Java_android_clip_cpp_CLIPAndroid_clipZeroShotClassify(
+ JNIEnv *env,
+ jobject thiz,
+ jlong context_ptr,
+ jint num_threads,
+ jobject image_buffer,
+ jint width,
+ jint height,
+ jobjectArray labels) {
+ auto* ctx = reinterpret_cast(context_ptr);
+
+ auto* img = clip_image_u8_make();
+ img -> nx = width;
+ img -> ny = height;
+ img -> data = reinterpret_cast(env -> GetDirectBufferAddress(image_buffer));
+ img -> size = width * height * 3;
+
+ int n_labels = env -> GetArrayLength(labels);
+ const char* labels_cstr[n_labels];
+ for (int i = 0; i < env -> GetArrayLength(labels); i++) {
+ auto label = (jstring) env -> GetObjectArrayElement(labels, i);
+ labels_cstr[i] = env -> GetStringUTFChars(label, nullptr);
+ }
+
+ float sorted_scores[n_labels];
+ int sorted_indices[n_labels];
+ clip_zero_shot_label_image(ctx, num_threads, img, labels_cstr, n_labels, sorted_scores, sorted_indices);
+
+ float sorted_indices_fp[n_labels];
+ for (int i = 0; i < n_labels; i++) {
+ sorted_indices_fp[i] = (float)sorted_indices[i];
+ }
+ jfloatArray scores_indices = env -> NewFloatArray(2 * n_labels);
+ env -> SetFloatArrayRegion(scores_indices, 0, n_labels, sorted_scores);
+ env -> SetFloatArrayRegion(scores_indices, n_labels, n_labels, sorted_indices_fp);
+
+ return scores_indices;
+}
+
+extern "C" JNIEXPORT jfloatArray JNICALL
+Java_android_clip_cpp_CLIPAndroid_clipBatchImageEncode__J_3Ljava_nio_ByteBuffer_2_3I_3IIIZ(
+ JNIEnv *env,
+ jobject,
+ jlong ctx_ptr,
+ jobjectArray arr_img_buffer,
+ jintArray widths,
+ jintArray heights,
+ jint n_threads,
+ jint vector_dims,
+ jboolean normalize
+) {
+ auto* ctx = reinterpret_cast(ctx_ptr);
+
+ auto batch_size = env -> GetArrayLength(arr_img_buffer);
+ auto* widths_buf = env -> GetIntArrayElements(widths, nullptr);
+ auto* heights_buf = env -> GetIntArrayElements(heights, nullptr);
+
+ LOGi("Batch size: %d", batch_size);
+ LOGi("Vector dims: %d", vector_dims);
+ LOGi("Normalize: %d", normalize);
+
+ auto imgs_u8 = new clip_image_u8_batch();
+ imgs_u8 -> data = new clip_image_u8[batch_size];
+ imgs_u8 -> size = batch_size;
+
+ auto* imgs_f32 = new clip_image_f32_batch();
+ imgs_f32 -> data = new clip_image_f32[batch_size];
+ imgs_f32 -> size = batch_size;
+ for (int i = 0; i < batch_size; i++) {
+ LOGi("Image %d: %d x %d", i, widths_buf[i], heights_buf[i]);
+ auto* img = clip_image_u8_make();
+ img -> nx = widths_buf[i];
+ img -> ny = heights_buf[i];
+ img -> data = reinterpret_cast(
+ env -> GetDirectBufferAddress(
+ env -> GetObjectArrayElement(arr_img_buffer, i)
+ )
+ );
+ img -> size = widths_buf[i] * heights_buf[i] * 3;
+ imgs_u8 -> data[i] = *img;
+
+ auto* img_f32 = clip_image_f32_make();
+ img_f32 -> nx = widths_buf[i];
+ img_f32 -> ny = heights_buf[i];
+ img_f32 -> data = new float[widths_buf[i] * heights_buf[i] * 3];
+ img_f32 -> size = widths_buf[i] * heights_buf[i] * 3;
+ imgs_f32 -> data[i] = *img_f32;
+ }
+
+ clip_image_batch_preprocess(ctx, n_threads, imgs_u8, imgs_f32);
+
+ env -> ReleaseIntArrayElements(widths, widths_buf, 0);
+ env -> ReleaseIntArrayElements(heights, heights_buf, 0);
+
+ float image_embedding[vector_dims * batch_size];
+ clip_image_batch_encode(ctx, n_threads, imgs_f32, image_embedding, normalize);
+
+ jfloatArray vecs = env -> NewFloatArray(batch_size * vector_dims);
+ env -> SetFloatArrayRegion(vecs, 0, batch_size * vector_dims, image_embedding);
+
+ return vecs;
+}
+
+
+
+extern "C" JNIEXPORT jfloat JNICALL
+Java_android_clip_cpp_CLIPAndroid_clipSimilarityScore___3F_3F(
+ JNIEnv *env,
+ jobject,
+ jfloatArray vec1,
+ jfloatArray vec2
+) {
+ auto vec1_len = env -> GetArrayLength(vec1);
+ auto vec2_len = env -> GetArrayLength(vec2);
+
+ auto vec1_buf = env -> GetFloatArrayElements(vec1, nullptr);
+ auto vec2_buf = env -> GetFloatArrayElements(vec2, nullptr);
+
+ float score = clip_similarity_score(
+ reinterpret_cast(vec1_buf),
+ reinterpret_cast(vec2_buf),
+ vec1_len
+ );
+
+ env -> ReleaseFloatArrayElements(vec1, vec1_buf, 0);
+ env -> ReleaseFloatArrayElements(vec2, vec2_buf, 0);
+
+ return score;
+}
diff --git a/examples/clip.android/clip/src/main/java/android/clip/cpp/CLIPAndroid.java b/examples/clip.android/clip/src/main/java/android/clip/cpp/CLIPAndroid.java
new file mode 100644
index 0000000..d3df0e0
--- /dev/null
+++ b/examples/clip.android/clip/src/main/java/android/clip/cpp/CLIPAndroid.java
@@ -0,0 +1,254 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2024 Shubham Panchal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package android.clip.cpp;
+
+import java.nio.ByteBuffer;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * CLIPAndroid provides an interface to the functions present in the clip.cpp library using JNI
+ * for Android applications. It allows loading the CLIP model, encoding images and text, and
+ * calculating the similarity score between two vectors.
+ *
+ * @author Shubham Panchal (github.com/shubham0204)
+ */
+public class CLIPAndroid {
+
+ private long contextPtr; // holds clip_ctx* pointer
+
+ static {
+ System.loadLibrary("clip-android");
+ }
+
+ /**
+ * Load the CLIP model from the specified file path.
+ *
+ * @param filePath Absolute path to the CLIP model file (.gguf file)
+ * @param verbosity Verbosity level of the model loading process. 0: silent, 1: progress, 2: debug
+ */
+ public void load(String filePath, int verbosity) {
+ if (!Paths.get(filePath).toFile().exists()) {
+ throw new IllegalArgumentException("File not found: " + filePath);
+ }
+ long ptr = clipModelLoad(filePath, verbosity);
+ if (ptr == 0) {
+ throw new RuntimeException("Failed to load the model from " + filePath);
+ } else {
+ contextPtr = ptr;
+ }
+ }
+
+ /**
+ * Get the vision hyper-parameters of the loaded CLIP model.
+ *
+ * @return An instance of CLIPVisionHyperParameters
+ */
+ public CLIPVisionHyperParameters getVisionHyperParameters() {
+ return clipGetVisionHyperParameters(contextPtr);
+ }
+
+ /**
+ * Get the text hyper-parameters of the loaded CLIP model.
+ *
+ * @return An instance of CLIPTextHyperParameters
+ */
+ public CLIPTextHyperParameters getTextHyperParameters() {
+ return clipGetTextHyperParameters(contextPtr);
+ }
+
+ /**
+ * Encode an image into a vector using the CLIP model
+ *
+ * @param image a `ByteBuffer` containing the image data in RGBRGB... format where each component is a byte
+ * and the image is of size `width` x `height`
+ * @param width the width of the image
+ * @param height the height of the image
+ * @param numThreads the number of threads to use for encoding
+ * @param vectorDims the dimension of the output vector from CLIPVisionHyperParameters.projectionDim
+ * @param normalize whether to normalize the output vector
+ * @return a float array (embedding) of size `vectorDims` containing the encoded image
+ */
+ public float[] encodeImage(ByteBuffer image, int width, int height, int numThreads, int vectorDims, boolean normalize) {
+ return clipImageEncode(contextPtr, image, width, height, numThreads, vectorDims, normalize);
+ }
+
+ /**
+ * Encode an image into a vector using the CLIP model, without resizing the image
+ * This is useful when the image is already of the required size.
+ * The required size can be obtained from CLIPVisionHyperParameters.imageSize
+ *
+ * @param image a `ByteBuffer` containing the image data in RGBRGB... format where each component is a byte
+ * and the image is of size `width` x `height`
+ * @param width the width of the image
+ * @param height the height of the image
+ * @param numThreads the number of threads to use for encoding
+ * @param vectorDims the dimension of the output vector from CLIPVisionHyperParameters.projectionDim
+ * @param normalize whether to normalize the output vector
+ * @return a float array (embedding) of size `vectorDims` containing the encoded image
+ */
+ public float[] encodeImageNoResize(ByteBuffer image, int width, int height, int numThreads, int vectorDims, boolean normalize) {
+ return clipImageEncodeNoResize(contextPtr, image, width, height, numThreads, vectorDims, normalize);
+ }
+
+ /**
+ * Encode a batch of images into vectors using the CLIP model
+ *
+ * @param images an array of `ByteBuffer` containing the image data in RGBRGB... format where each component is a byte
+ * @param widths an array containing the width of each image in `images`
+ * @param heights an array containing the height of each image in `images`
+ * @param numThreads the number of threads to use for encoding
+ * @param vectorDims the dimension of the output vector from CLIPVisionHyperParameters.projectionDim
+ * @param normalize whether to normalize the output vector
+ * @return a list of float arrays (embeddings) of size `vectorDims` containing the encoded images
+ */
+ public List encodeImage(ByteBuffer[] images, int[] widths, int[] heights, int numThreads, int vectorDims, boolean normalize) {
+ if (images.length != widths.length || images.length != heights.length) {
+ throw new IllegalArgumentException("images, widths, and heights must have the same length. Got "
+ + images.length + ", " + widths.length + ", " + heights.length);
+ }
+ float[] vectors = clipBatchImageEncode(contextPtr, images, widths, heights, numThreads, vectorDims, normalize);
+ ArrayList vectorsList = new ArrayList<>();
+ for (int i = 0; i < vectors.length / vectorDims; i++) {
+ float[] vec = new float[vectorDims];
+ System.arraycopy(vectors, i * vectorDims, vec, 0, vectorDims);
+ vectorsList.add(vec);
+ }
+ return vectorsList;
+ }
+
+ /**
+ * Encode a text into a vector using the CLIP model
+ *
+ * @param text text to encode
+ * @param numThreads number of threads to use for encoding
+ * @param vectorDims the dimension of the output vector from CLIPTextHyperParameters.projectionDim
+ * @param normalize whether to normalize the output vector
+ * @return a float array (embedding) of size `vectorDims` containing the encoded text
+ */
+ public float[] encodeText(String text, int numThreads, int vectorDims, boolean normalize) {
+ return clipTextEncode(contextPtr, text, numThreads, vectorDims, normalize);
+ }
+
+ /**
+ * Perform zero-shot image classification using the CLIP model
+ *
+ * @param image a `ByteBuffer` containing the image data in RGBRGB... format where each component is a byte
+ * @param width width of the image
+ * @param height height of the image
+ * @param numThreads number of threads to use
+ * @param labels Comma-separated labels (all in lower-case) to classify the image into
+ * @return an array [s_1, s_2, ..., s_n, i_1, i_2, ..., i_n] where (s_1, ..., s_n) are the sorted scores (in descending order)
+ * and (i_1, ..., i_n) are the corresponding indices of the labels
+ */
+ public float[] zeroShotClassify(ByteBuffer image, int width, int height, int numThreads, String[] labels) {
+ return clipZeroShotClassify(contextPtr, numThreads, image, width, height, labels);
+ }
+
+ /**
+ * Calculate the similarity score between two vectors
+ *
+ * @param vec1 first vector
+ * @param vec2 second vector
+ * @return similarity score (cosine similarity) between the two vectors
+ * @throws IllegalArgumentException if the vectors have different lengths
+ */
+ public float getSimilarityScore(float[] vec1, float[] vec2) {
+ if (vec1.length != vec2.length) {
+ throw new IllegalArgumentException("Vectors must have the same length. Got " + vec1.length + ", " + vec2.length);
+ }
+ return clipSimilarityScore(vec1, vec2);
+ }
+
+ /**
+ * Releases the resources acquired by the CLIP model
+ */
+ public void close() {
+ clipModelRelease(contextPtr);
+ }
+
+ private native long clipModelLoad(String filePath, int verbosity);
+
+ private native void clipModelRelease(long model);
+
+ private native CLIPVisionHyperParameters clipGetVisionHyperParameters(long contextPtr);
+
+ private native CLIPTextHyperParameters clipGetTextHyperParameters(long contextPtr);
+
+ private native float[] clipImageEncode(long contextPtr, ByteBuffer imageBuffer, int width, int height, int numThreads, int vectorDims, boolean normalize);
+
+ private native float[] clipImageEncodeNoResize(long contextPtr, ByteBuffer imageBuffer, int width, int height, int numThreads, int vectorDims, boolean normalize);
+
+ private native float[] clipBatchImageEncode(long contextPtr, ByteBuffer[] imageBuffers, int[] widths, int[] heights, int numThreads, int vectorDims, boolean normalize);
+
+ private native float[] clipTextEncode(long contextPtr, String text, int numThreads, int vectorDims, boolean normalize);
+
+ private native float[] clipZeroShotClassify(long contextPtr, int numThreads, ByteBuffer imageBuffer, int width, int height, String[] labels);
+
+ private native float clipSimilarityScore(float[] vec1, float[] vec2);
+
+ public static class CLIPVisionHyperParameters {
+ public final int imageSize;
+ public final int patchSize;
+ public final int hiddenSize;
+ public final int projectionDim;
+ public final int nIntermediate;
+ public final int nHead;
+ public final int nLayer;
+
+ public CLIPVisionHyperParameters(int imageSize, int patchSize, int hiddenSize, int projectionDim, int nIntermediate, int nHead, int nLayer) {
+ this.imageSize = imageSize;
+ this.patchSize = patchSize;
+ this.hiddenSize = hiddenSize;
+ this.projectionDim = projectionDim;
+ this.nIntermediate = nIntermediate;
+ this.nHead = nHead;
+ this.nLayer = nLayer;
+ }
+ }
+
+ public static class CLIPTextHyperParameters {
+ public final int nVocab;
+ public final int numPositions;
+ public final int hiddenSize;
+ public final int projectionDim;
+ public final int nIntermediate;
+ public final int nHead;
+ public final int nLayer;
+
+ public CLIPTextHyperParameters(int nVocab, int numPositions, int hiddenSize, int projectionDim, int nIntermediate, int nHead, int nLayer) {
+ this.nVocab = nVocab;
+ this.numPositions = numPositions;
+ this.hiddenSize = hiddenSize;
+ this.projectionDim = projectionDim;
+ this.nIntermediate = nIntermediate;
+ this.nHead = nHead;
+ this.nLayer = nLayer;
+ }
+ }
+
+}
diff --git a/examples/clip.android/gradle.properties b/examples/clip.android/gradle.properties
new file mode 100644
index 0000000..20e2a01
--- /dev/null
+++ b/examples/clip.android/gradle.properties
@@ -0,0 +1,23 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. For more details, visit
+# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
\ No newline at end of file
diff --git a/examples/clip.android/gradle/libs.versions.toml b/examples/clip.android/gradle/libs.versions.toml
new file mode 100644
index 0000000..c7915d6
--- /dev/null
+++ b/examples/clip.android/gradle/libs.versions.toml
@@ -0,0 +1,36 @@
+[versions]
+agp = "8.5.0"
+kotlin = "1.9.0"
+coreKtx = "1.13.1"
+junit = "4.13.2"
+junitVersion = "1.2.1"
+espressoCore = "3.6.1"
+lifecycleRuntimeKtx = "2.8.4"
+activityCompose = "1.9.1"
+composeBom = "2024.04.01"
+appcompat = "1.7.0"
+material = "1.12.0"
+
+[libraries]
+androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+junit = { group = "junit", name = "junit", version.ref = "junit" }
+androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
+androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
+androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
+androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
+androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
+androidx-ui = { group = "androidx.compose.ui", name = "ui" }
+androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
+androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
+androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
+androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
+androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
+androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
+androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
+material = { group = "com.google.android.material", name = "material", version.ref = "material" }
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+android-library = { id = "com.android.library", version.ref = "agp" }
+
diff --git a/examples/clip.android/gradle/wrapper/gradle-wrapper.jar b/examples/clip.android/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..e708b1c
Binary files /dev/null and b/examples/clip.android/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/examples/clip.android/gradle/wrapper/gradle-wrapper.properties b/examples/clip.android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..165ff33
--- /dev/null
+++ b/examples/clip.android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Thu Sep 12 07:25:44 IST 2024
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/examples/clip.android/gradlew b/examples/clip.android/gradlew
new file mode 100644
index 0000000..4f906e0
--- /dev/null
+++ b/examples/clip.android/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# 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
+#
+# https://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.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/examples/clip.android/gradlew.bat b/examples/clip.android/gradlew.bat
new file mode 100644
index 0000000..107acd3
--- /dev/null
+++ b/examples/clip.android/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/examples/clip.android/settings.gradle.kts b/examples/clip.android/settings.gradle.kts
new file mode 100644
index 0000000..93d2c15
--- /dev/null
+++ b/examples/clip.android/settings.gradle.kts
@@ -0,0 +1,24 @@
+pluginManagement {
+ repositories {
+ google {
+ content {
+ includeGroupByRegex("com\\.android.*")
+ includeGroupByRegex("com\\.google.*")
+ includeGroupByRegex("androidx.*")
+ }
+ }
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "clip.cpp"
+include(":app")
+include(":clip")