Palette from Image — Ruby 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 Ruby implementation — the same logic the interactive tool runs, in a shareable, citable form.
# Palette from Image — median-cut quantization over RGBA pixels.
#
# Language: Ruby (3.1+), 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.
Swatch = Struct.new(:r, :g, :b, :population)
# Down-sample so large images quantize in bounded time (TS: MAX_SAMPLES).
MAX_SAMPLES = 16_384
def extract_palette(rgba, max_colors = 8)
total = rgba.length / 4
return [] if total.zero?
pixels = []
stride = [1, total / MAX_SAMPLES].max
(0...total).step(stride) do |i|
o = i * 4
next if rgba[o + 3].zero? # fully transparent — skip
pixels << [rgba[o], rgba[o + 1], rgba[o + 2]]
end
return [] if pixels.empty?
buckets = [pixels]
while buckets.length < max_colors
# Widest-range bucket with more than one distinct value splits.
best_idx = -1
best = [1, 0] # range 1 (exact duplicates) never splits
buckets.each_with_index do |bucket, i|
range, channel = channel_range(bucket)
if range > best[0]
best = [range, channel]
best_idx = i
end
end
break if best_idx.negative? # every bucket is uniform — done
bucket = buckets.delete_at(best_idx)
channel = best[1]
# sort_by IS stable in Ruby — the TS lib relies on stable sorts too.
sorted = bucket.sort_by { |px| px[channel] }
mid = sorted.length / 2
buckets << sorted[0, mid]
buckets << sorted[mid..] || []
end
buckets.map { |b|
n = b.length.to_f
Swatch.new((b.sum { |px| px[0] } / n).round,
(b.sum { |px| px[1] } / n).round,
(b.sum { |px| px[2] } / n).round,
b.length)
}.sort_by { |s| -s.population }
end
def channel_range(bucket)
r_min = g_min = b_min = 255
r_max = g_max = b_max = 0
bucket.each do |(r, g, b)|
r_min = [r_min, r].min; r_max = [r_max, r].max
g_min = [g_min, g].min; g_max = [g_max, g].max
b_min = [b_min, b].min; b_max = [b_max, b].max
end
r = r_max - r_min; g = g_max - g_min; b = b_max - b_min
return [r, 0] if r >= g && r >= b
return [g, 1] if g >= b
[b, 2]
end
if __FILE__ == $PROGRAM_NAME
# Smoke: 2 red + 1 blue pixels → red swatch wins on population.
rgba = [200, 30, 30, 255, 202, 28, 32, 255, 30, 30, 200, 255]
extract_palette(rgba, 2).each do |s|
format('#%02X%02X%02X ×%d', s.r, s.g, s.b, s.population).then { puts _1 }
end
end
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 →