Kotlin SDK

Official Kotlin SDK for TruthMark invisible watermarking with coroutine support.

Installation

Gradle (Kotlin DSL)
dependencies {
    implementation("com.truthmark:sdk:1.0.0")
}
Maven
<dependency>
    <groupId>com.truthmark</groupId>
    <artifactId>sdk</artifactId>
    <version>1.0.0</version>
</dependency>

Quick Example

import com.truthmark.sdk.TruthMarkClient
import com.truthmark.sdk.TruthMarkConfig

suspend fun main() {
    val client = TruthMarkClient(TruthMarkConfig(
        apiKey = "tm_live_your_key",
        baseUrl = "https://truthmark-api.onrender.com"
    ))

    // Encode
    val result = client.encode("image.png", "Copyright 2025")
    println("Download: ${result.downloadUrl}")
    println("PSNR: ${String.format("%.1f", result.metadata.psnr)} dB")

    // Decode
    val decoded = client.decode("watermarked.png")
    if (decoded.found) {
        println("Message: ${decoded.message}")
        println("Confidence: ${String.format("%.1f", decoded.confidence * 100)}%")
    }
}

API Reference

TruthMarkClient(config: TruthMarkConfig)

Create a new client.

data class TruthMarkConfig(
    val apiKey: String,
    val baseUrl: String = "https://truthmark-api.onrender.com",
    val timeout: Long = 30_000
)

suspend fun encode(imagePath: String, message: String): EncodeResult

Embed an invisible watermark. Coroutine-safe.

suspend fun decode(imagePath: String): DecodeResult

Extract watermark. Coroutine-safe.

Error Handling

import com.truthmark.sdk.exceptions.*

try {
    val result = client.encode("image.png", "message")
} catch (e: TruthMarkAuthException) {
    println("Invalid API key")
} catch (e: TruthMarkRateLimitException) {
    println("Rate limited. Retry after: ${e.retryAfter}s")
} catch (e: TruthMarkException) {
    println("Error (${e.statusCode}): ${e.message}")
}

Advanced Examples

Android ViewModel

class WatermarkViewModel(
    private val client: TruthMarkClient
) : ViewModel() {

    private val _state = MutableStateFlow<UiState>(UiState.Idle)
    val state: StateFlow<UiState> = _state

    fun watermarkImage(uri: Uri, message: String) {
        viewModelScope.launch {
            _state.value = UiState.Loading
            try {
                val result = client.encode(uri.path!!, message)
                _state.value = UiState.Success(result.downloadUrl)
            } catch (e: TruthMarkException) {
                _state.value = UiState.Error(e.message ?: "Unknown error")
            }
        }
    }

    sealed class UiState {
        object Idle : UiState()
        object Loading : UiState()
        data class Success(val url: String) : UiState()
        data class Error(val message: String) : UiState()
    }
}

Ktor Server Route

fun Application.configureRoutes() {
    val client = TruthMarkClient(TruthMarkConfig(
        apiKey = environment.config.property("truthmark.apiKey").getString()
    ))

    routing {
        post("/watermark") {
            val multipart = call.receiveMultipart()
            var filePath: String? = null
            var message: String? = null

            multipart.forEachPart { part ->
                when (part) {
                    is PartData.FileItem -> {
                        val temp = File.createTempFile("upload", ".png")
                        part.streamProvider().use { input ->
                            temp.outputStream().use { output -> input.copyTo(output) }
                        }
                        filePath = temp.absolutePath
                    }
                    is PartData.FormItem -> message = part.value
                    else -> {}
                }
                part.dispose()
            }

            val result = client.encode(filePath!!, message!!)
            call.respond(result)
        }
    }
}

Concurrent Batch with Coroutines

suspend fun batchWatermark(directory: String, message: String) =
    coroutineScope {
        val files = File(directory).listFiles { f ->
            f.extension in listOf("png", "jpg", "jpeg")
        } ?: return@coroutineScope emptyList()

        val semaphore = Semaphore(5)
        files.map { file ->
            async {
                semaphore.withPermit {
                    try {
                        val result = client.encode(file.absolutePath, message)
                        println("✓ ${file.name}: PSNR=${result.metadata.psnr}dB")
                        result
                    } catch (e: TruthMarkException) {
                        println("✗ ${file.name}: ${e.message}")
                        null
                    }
                }
            }
        }.awaitAll().filterNotNull()
    }

Best Practices

Use coroutines for all API calls

All SDK methods are suspend functions — use viewModelScope or lifecycleScope on Android.

Use Semaphore for batch operations

Limit concurrency with kotlinx.coroutines.sync.Semaphore to avoid rate limits.

Don't store keys in strings.xml

Use BuildConfig fields or encrypted SharedPreferences for API keys on Android.

Need Help?

Check out the API reference or reach out to support.