Skip to content

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 →