Palette from Image — Swift 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 Swift implementation — the same logic the interactive tool runs, in a shareable, citable form.
// Palette from Image — median-cut quantization over RGBA pixels.
//
// Language: Swift (5.9+), Foundation for String(format:) 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.
import Foundation
struct Swatch: CustomStringConvertible {
let r: Int, g: Int, b: Int, population: Int
var description: String {
String(format: "#%02X%02X%02X ×%d", r, g, b, population)
}
}
struct Pixel {
let r: Int, g: Int, b: Int
}
// Down-sample so large images quantize in bounded time (TS: MAX_SAMPLES).
let MAX_SAMPLES = 16_384
func extractPalette(_ rgba: [UInt8], maxColors: Int = 8) -> [Swatch] {
let total = rgba.count / 4
if total == 0 { return [] }
var pixels: [Pixel] = []
let stride = max(1, total / MAX_SAMPLES)
var i = 0
while i < total {
let o = i * 4
if rgba[o + 3] != 0 { // skip fully transparent
pixels.append(Pixel(r: Int(rgba[o]), g: Int(rgba[o + 1]), b: Int(rgba[o + 2])))
}
i += stride
}
if pixels.isEmpty { return [] }
var buckets: [[Pixel]] = [pixels]
while buckets.count < maxColors {
// Widest-range bucket with more than one distinct value splits.
var bestIdx = -1, bestRange = 1, bestChannel = 0 // range 1 never splits
for (k, bucket) in buckets.enumerated() {
let (range, channel) = channelRange(bucket)
if range > bestRange { bestRange = range; bestIdx = k; bestChannel = channel }
}
if bestIdx < 0 { break } // every bucket is uniform — done
let bucket = buckets.remove(at: bestIdx)
let ch = bestChannel
// sort(by:) is NOT stable in Swift — sort a (index, pixel) pair to make
// the median split deterministic, matching the TS lib's stable sort.
let sorted = bucket.enumerated()
.sorted { a, b in
let av = ch == 0 ? a.element.r : (ch == 1 ? a.element.g : a.element.b)
let bv = ch == 0 ? b.element.r : (ch == 1 ? b.element.g : b.element.b)
return av == bv ? a.offset < b.offset : av < bv
}
.map(\.element)
let mid = sorted.count / 2
buckets.append(Array(sorted[..<mid]))
buckets.append(Array(sorted[mid...]))
}
return buckets.map { b in
let n = Double(b.count)
return Swatch(
r: Int((Double(b.reduce(0) { $0 + $1.r }) / n).rounded()),
g: Int((Double(b.reduce(0) { $0 + $1.g }) / n).rounded()),
b: Int((Double(b.reduce(0) { $0 + $1.b }) / n).rounded()),
population: b.count)
}
.sorted { $0.population > $1.population }
}
private func channelRange(_ bucket: [Pixel]) -> (range: Int, channel: Int) {
var rMin = 255, rMax = 0, gMin = 255, gMax = 0, bMin = 255, bMax = 0
for p in bucket {
rMin = min(rMin, p.r); rMax = max(rMax, p.r)
gMin = min(gMin, p.g); gMax = max(gMax, p.g)
bMin = min(bMin, p.b); bMax = max(bMax, p.b)
}
let r = rMax - rMin, g = gMax - gMin, b = bMax - bMin
if r >= g && r >= b { return (r, 0) }
if g >= b { return (g, 1) }
return (b, 2)
}
// Smoke: 2 red + 1 blue pixels → red swatch wins on population.
let rgba: [UInt8] = [200, 30, 30, 255, 202, 28, 32, 255, 30, 30, 200, 255]
for s in extractPalette(rgba, maxColors: 2) { print(s) }
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 →