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 →