Skip to content

Instantly share code, notes, and snippets.



Created Jul 23, 2020
What would you like to do?
Kscript program to convert a sequence of images to a webp animation, using img2webp
//usr/bin/env echo '
/**** BOOTSTRAP kscript ****\'>/dev/null
command -v kscript >/dev/null 2>&1 || curl -L "" | bash 1>&2
exec kscript $0 "$@"
\*** IMPORTANT: Any code including imports and annotations must come after this line ***/
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.arguments.multiple
import com.github.ajalt.clikt.parameters.options.default
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.options.validate
import com.github.ajalt.clikt.parameters.types.choice
import com.github.ajalt.clikt.parameters.types.path
import java.nio.file.Path
import kotlin.math.roundToInt
import kotlin.system.exitProcess
val helpMessage = """This script creates a webp animation from an image sequence. You need to have img2webp
available on your path for this to work.
See for more info."""
inner class C2Webp : CliktCommand(help = helpMessage, printHelpOnEmptyArgs = true) {
private val sourceFiles: List<Path> by argument().path(mustExist = true).multiple()
private val outputFile: Path by argument().path()
private val loopCount: Int? by option("-loop").int().validate { it >= 0 }
private val fps: Int by option("-fps").int().default(10).validate { it > 0 }
private val quality: Int? by option("-q").int().validate { (0..100).contains(it) }
private val minSize: Boolean by option("-min_size").flag()
private val compression: String? by option("-c", "--compression").choice("lossy", "lossless", "mixed")
private val verbose: Boolean by option("-v", "--verbose").flag()
override fun run() {
if (!isInPath("img2webp")) {
"\n❌ You need img2webp on the PATH. Install it following the instructions at " +
if (sourceFiles.isEmpty()) {
System.err.println("\n❌ You need to specify at least one source file(s)")
if (verbose) {
println("\nℹ️ Input files:")
sourceFiles.forEach {
println("\nℹ️ Expected duration: ${sourceFiles.count() / fps * 1000} ms (approx.) @ ${fps}fps")
if (verbose) println("\nℹ️ Preparing img2webp command...")
val command = mutableListOf("img2webp")
if (loopCount != null) {
command += "-loop"
command += loopCount.toString()
if (minSize) command += "-min_size"
if (compression == "mixed") command += "-mixed"
if (verbose) command += "-v"
val frameDuration = (1 / fps.toFloat() * 1000).roundToInt()
val perFileArgs = mutableListOf("-d", frameDuration.toString())
if (quality != null) {
perFileArgs += "-q"
perFileArgs += quality.toString()
if (compression != null && compression != "mixed") perFileArgs += "-$compression"
for (sourceFile in sourceFiles) {
command += sourceFile.toAbsolutePath().toString()
command += perFileArgs
command += "-o"
command += outputFile.toAbsolutePath().toString()
println("\nℹ️ Creating $outputFile...")
val exitCode = ProcessBuilder(command).inheritIO()
if (exitCode == 0) {
println("\nℹ️ All done!\n")
} else {
System.err.println("\n❌ Something went wrong, check the output above")
if (!verbose) println("\tTip: use the -v flag to get verbose output and troubleshoot")
fun isInPath(executableName: String): Boolean {
val builder = ProcessBuilder(if (isWindows()) "where" else "which", executableName)
return try {
val errCode = builder.start().waitFor()
errCode == 0
} catch (ex: IOException) {
System.err.println("Error while looking for $executableName in PATH: ${ex.message}")
} catch (ex: InterruptedException) {
System.err.println("Error while looking for $executableName in PATH: ${ex.message}")
fun isWindows(): Boolean = System.getProperty("").contains("windows", ignoreCase = true)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.