Skip to content

Instantly share code, notes, and snippets.

@rock3r
Created July 23, 2020 10:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rock3r/4ae3931cda60291e01a76874d418fdfc to your computer and use it in GitHub Desktop.
Save rock3r/4ae3931cda60291e01a76874d418fdfc to your computer and use it in GitHub Desktop.
Kscript program to convert a sequence of images to a webp animation, using img2webp
#!/bin/bash
//usr/bin/env echo '
/**** BOOTSTRAP kscript ****\'>/dev/null
command -v kscript >/dev/null 2>&1 || curl -L "https://git.io/fpF1K" | bash 1>&2
exec kscript $0 "$@"
\*** IMPORTANT: Any code including imports and annotations must come after this line ***/
@file:DependsOn("com.github.ajalt:clikt:2.8.0")
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.int
import com.github.ajalt.clikt.parameters.types.path
import java.io.IOException
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 https://developers.google.com/speed/webp/docs/img2webp 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")
.default("lossless")
private val verbose: Boolean by option("-v", "--verbose").flag()
override fun run() {
if (!isInPath("img2webp")) {
System.err.println(
"\n❌ You need img2webp on the PATH. Install it following the instructions at " +
"https://developers.google.com/speed/webp/docs/precompiled"
)
exitProcess(1)
}
if (sourceFiles.isEmpty()) {
System.err.println("\n❌ You need to specify at least one source file(s)")
exitProcess(2)
}
if (verbose) {
println("\nℹ️ Input files:")
sourceFiles.forEach {
println("\t$it")
}
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()
.start()
.waitFor()
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")
exitProcess(3)
}
}
}
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}")
false
} catch (ex: InterruptedException) {
System.err.println("Error while looking for $executableName in PATH: ${ex.message}")
false
}
}
fun isWindows(): Boolean = System.getProperty("os.name").contains("windows", ignoreCase = true)
C2Webp().main(args)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment