Skip to content

Palette from Image — Java 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 Java implementation — the same logic the interactive tool runs, in a shareable, citable form.

// Palette from Image — median-cut quantization over RGBA pixels.
//
// Language: Java (17+), 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.

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public final class PaletteExtractor {

  public record Swatch(int r, int g, int b, int population) {}

  private record Pixel(byte r, byte g, byte b) {}

  // Down-sample so large images quantize in bounded time (TS: MAX_SAMPLES).
  private static final int MAX_SAMPLES = 16384;

  public static List<Swatch> extractPalette(byte[] rgba) { return extractPalette(rgba, 8); }

  public static List<Swatch> extractPalette(byte[] rgba, int maxColors) {
    int total = rgba.length / 4;
    if (total == 0) return List.of();

    List<Pixel> pixels = new ArrayList<>();
    int stride = Math.max(1, total / MAX_SAMPLES);
    for (int i = 0; i < total; i += stride) {
      int o = i * 4;
      if (rgba[o + 3] == 0) continue;  // fully transparent — skip
      pixels.add(new Pixel(rgba[o], rgba[o + 1], rgba[o + 2]));
    }
    if (pixels.isEmpty()) return List.of();

    List<List<Pixel>> buckets = new ArrayList<>(List.of(pixels));
    while (buckets.size() < maxColors) {
      // Widest-range bucket with more than one distinct value splits.
      int bestIdx = -1, bestRange = 1, bestChannel = 0;  // range 1 never splits
      for (int i = 0; i < buckets.size(); i++) {
        int[] rc = channelRange(buckets.get(i));
        if (rc[0] > bestRange) { bestRange = rc[0]; bestIdx = i; bestChannel = rc[1]; }
      }
      if (bestIdx < 0) break;  // every bucket is uniform — done

      List<Pixel> bucket = new ArrayList<>(buckets.get(bestIdx));
      final int ch = bestChannel;
      // List.sort IS stable — the TS lib relies on that too.
      bucket.sort(Comparator.comparingInt(p -> switch (ch) {
        case 0 -> p.r() & 0xFF;
        case 1 -> p.g() & 0xFF;
        default -> p.b() & 0xFF;
      }));
      int mid = bucket.size() / 2;
      buckets.set(bestIdx, bucket.subList(0, mid));           // replaces in place:
      buckets.add(bucket.subList(mid, bucket.size()));        // no index shuffle
    }

    List<Swatch> out = new ArrayList<>();
    for (List<Pixel> b : buckets) {
      long r = 0, g = 0, bl = 0;
      for (Pixel p : b) { r += p.r() & 0xFF; g += p.g() & 0xFF; bl += p.b() & 0xFF; }
      out.add(new Swatch((int) Math.round((double) r / b.size()),
          (int) Math.round((double) g / b.size()),
          (int) Math.round((double) bl / b.size()), b.size()));
    }
    out.sort(Comparator.comparingInt(Swatch::population).reversed());
    return out;
  }

  private static int[] channelRange(List<Pixel> bucket) {
    int rMin = 255, rMax = 0, gMin = 255, gMax = 0, bMin = 255, bMax = 0;
    for (Pixel p : bucket) {
      int r = p.r() & 0xFF, g = p.g() & 0xFF, b = p.b() & 0xFF;
      rMin = Math.min(rMin, r); rMax = Math.max(rMax, r);
      gMin = Math.min(gMin, g); gMax = Math.max(gMax, g);
      bMin = Math.min(bMin, b); bMax = Math.max(bMax, b);
    }
    int r = rMax - rMin, g = gMax - gMin, b = bMax - bMin;
    if (r >= g && r >= b) return new int[] { r, 0 };
    if (g >= b) return new int[] { g, 1 };
    return new int[] { b, 2 };
  }

  public static void main(String[] args) {
    // Smoke: 2 red + 1 blue pixels → red swatch wins on population.
    byte[] rgba = { (byte) 200, 30, 30, (byte) 255, (byte) 202, 28, 32, (byte) 255,
                    30, 30, (byte) 200, (byte) 255 };
    for (Swatch s : extractPalette(rgba, 2))
      System.out.printf("#%02X%02X%02X ×%d%n", 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 →