Palette from Image — Kotlin source
Extract the dominant colors from any image as a reusable palette — median-cut quantization with population shares, hex and rgb, copyable — runs entirely in your browser.
This is the Kotlin implementation — the same logic the interactive tool runs, in a shareable, citable form.
// Palette from Image — median-cut quantization over RGBA pixels.
//
// Language: Kotlin (1.9+), standard library only
// CosmoDev polyglot showcase port of the `palette-from-image` tool.
// Ported from src/lib/palette-extract.ts — display source, part of CosmoDev's
// polyglot tool pages.
//
// Deterministic: stable sorts only, widest-channel median split, buckets
// average into swatches. Transparent pixels are skipped.
data class Swatch(val r: Int, val g: Int, val b: Int, val population: Int)
private class Pixel(val r: Int, val g: Int, val b: Int)
// Down-sample so large images quantize in bounded time (TS: MAX_SAMPLES).
private const val MAX_SAMPLES = 16384
fun extractPalette(rgba: ByteArray, maxColors: Int = 8): List<Swatch> {
val total = rgba.size / 4
if (total == 0) return emptyList()
val pixels = ArrayList<Pixel>()
val stride = maxOf(1, total / MAX_SAMPLES)
var i = 0
while (i < total) {
val o = i * 4
if (rgba[o + 3].toInt() != 0) // skip fully transparent
pixels.add(Pixel(rgba[o].toInt() and 0xFF, rgba[o + 1].toInt() and 0xFF, rgba[o + 2].toInt() and 0xFF))
i += stride
}
if (pixels.isEmpty()) return emptyList()
val buckets = ArrayList<MutableList<Pixel>>(listOf(pixels))
while (buckets.size < maxColors) {
// Widest-range bucket with more than one distinct value splits.
var bestIdx = -1; var bestRange = 1; var bestChannel = 0 // range 1 never splits
for (k in buckets.indices) {
val (range, channel) = channelRange(buckets[k])
if (range > bestRange) { bestRange = range; bestIdx = k; bestChannel = channel }
}
if (bestIdx < 0) break // every bucket is uniform — done
val bucket = buckets.removeAt(bestIdx)
val ch = bestChannel
val sorted = bucket.sortedBy { // sortedBy IS stable — like the TS lib
when (ch) { 0 -> it.r; 1 -> it.g; else -> it.b }
}
val mid = sorted.size / 2
buckets.add(ArrayList(sorted.subList(0, mid)))
buckets.add(ArrayList(sorted.subList(mid, sorted.size)))
}
return buckets.map { b ->
Swatch(
Math.round(b.sumOf { it.r }.toDouble() / b.size).toInt(),
Math.round(b.sumOf { it.g }.toDouble() / b.size).toInt(),
Math.round(b.sumOf { it.b }.toDouble() / b.size).toInt(),
b.size,
)
}.sortedByDescending { it.population }
}
private fun channelRange(bucket: List<Pixel>): Pair<Int, Int> {
var rMin = 255; var rMax = 0; var gMin = 255; var gMax = 0; var bMin = 255; var bMax = 0
for (p in bucket) {
rMin = minOf(rMin, p.r); rMax = maxOf(rMax, p.r)
gMin = minOf(gMin, p.g); gMax = maxOf(gMax, p.g)
bMin = minOf(bMin, p.b); bMax = maxOf(bMax, p.b)
}
val r = rMax - rMin; val g = gMax - gMin; val b = bMax - bMin
return if (r >= g && r >= b) r to 0 else if (g >= b) g to 1 else b to 2
}
fun main() {
// Smoke: 2 red + 1 blue pixels → red swatch wins on population.
val rgba = byteArrayOf(200.toByte(), 30, 30, 255.toByte(),
202.toByte(), 28, 32, 255.toByte(), 30, 30, 200.toByte(), 255.toByte())
for (s in extractPalette(rgba, 2))
println("#%02X%02X%02X ×%d".format(s.r, s.g, s.b, s.population))
}
Also available in 9 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →