/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
 * vi:set noexpandtab tabstop=8 shiftwidth=8:
 *
 * Copyright (C) 2013-2016 Richard Hughes <richard@hughsie.com>
 * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com>
 * Copyright (C) 2014-2018 Kalev Lember <klember@redhat.com>
 * Copyright (C) 2021 Endless OS Foundation LLC
 *
 * Authors:
 *  - Richard Hughes <richard@hughsie.com>
 *  - Kalev Lember <klember@redhat.com>
 *  - Philip Withnall <pwithnall@endlessos.org>
 *
 * SPDX-License-Identifier: GPL-2.0+
 */

/**
 * SECTION:gs-key-colors
 * @short_description: Helper functions for calculating key colors
 *
 * Key colors are RGB colors which represent an app, and they are derived from
 * the app’s icon, or manually specified as an override.
 *
 * Use gs_calculate_key_colors() to calculate the key colors from an app’s icon.
 *
 * Since: 40
 */

#include "config.h"

#include <glib.h>
#include <gdk/gdk.h>
#include <gdk-pixbuf/gdk-pixbuf.h>

#include "gs-key-colors.h"

/* Hard-code the number of clusters to split the icon color space into. This
 * gives the maximum number of key colors returned for an icon. This number has
 * been chosen by examining 1000 icons to subjectively see how many key colors
 * each has. The number of key colors ranged from 1 to 6, but the mode was
 * definitely 3. */
const guint n_clusters = 3;

/* Discard pixels with less than this level of alpha. Almost all icons have a
 * transparent background/border at 100% transparency, and a blending fringe
 * with some intermediate level of transparency which should be ignored for
 * choosing key colors. A number of icons have partially-transparent colored
 * sections in the main body of the icon, which should be used if they’re above
 * this threshold. About 1% of icons have no completely opaque pixels, so we
 * can’t discard non-opaque pixels entirely. */
const guint minimum_alpha = 0.5 * 255;

typedef struct {
	guint8 red;
	guint8 green;
	guint8 blue;
} Pixel8;

typedef struct {
	Pixel8 color;
	union {
		guint8 alpha;
		guint8 cluster;
	};
} ClusterPixel8;

typedef struct {
	guint red;
	guint green;
	guint blue;
	guint n_members;
} CentroidAccumulator;

static inline ClusterPixel8 *
get_pixel (guint8 *pixels,
           guint   x,
           guint   y,
           guint   rowstride,
           guint   n_channels)
{
	return (ClusterPixel8 *) (pixels + y * rowstride + x * n_channels);
}

static inline guint
color_distance (const Pixel8 *a,
                const Pixel8 *b)
{
	/* Take the absolute value rather than the square root to save some
	 * time, as the caller is comparing distances.
	 *
	 * The arithmetic here can’t overflow, as the R/G/B components have a
	 * maximum value of 255 but the arithmetic is done in (at least) 32-bit
	 * variables.*/
	gint dr = b->red - a->red;
	gint dg = b->green - a->green;
	gint db = b->blue - a->blue;

	return abs (dr * dr + dg * dg + db * db);
}

/* NOTE: This has to return stable results when more than one cluster is
 * equidistant from the @pixel, or the k_means() function may not terminate. */
static inline gsize
nearest_cluster (const Pixel8 *pixel,
                 const Pixel8 *cluster_centres,
                 gsize         n_cluster_centres)
{
	gsize nearest_cluster = 0;
	guint nearest_cluster_distance = color_distance (&cluster_centres[0], pixel);

	for (gsize i = 1; i < n_cluster_centres; i++) {
		guint distance = color_distance (&cluster_centres[i], pixel);
		if (distance < nearest_cluster_distance) {
			nearest_cluster = i;
			nearest_cluster_distance = distance;
		}
	}

	return nearest_cluster;
}

/* Extract the key colors from @pb by clustering the pixels in RGB space.
 * Clustering is done using k-means, with initialisation using a
 * Random Partition.
 *
 * This approach can be thought of as plotting every pixel in @pb in a
 * three-dimensional color space, with red, green and blue axes (alpha is
 * clipped to 0 (pixel is ignored) or 1 (pixel is used)). The key colors for
 * the image are the ones where a large number of pixels are plotted in a group
 * in the color space — either a lot of pixels with an identical color
 * (repeated use of exactly the same color in the image) or a lot of pixels in
 * a rough group (use of a lot of similar shades of the same color in the
 * image).
 *
 * By transforming to a color space, information about the X and Y positions of
 * each color is ignored, so a thin outline in the image of a single color
 * will appear in the color space as a cluster, just as a contiguous block of
 * one color would.
 *
 * The k-means clustering algorithm is then used to find these clusters. k-means
 * is used, rather than (say) principal component analysis, because it
 * inherently calculates the centroid for each cluster. In a color space, the
 * centroid is itself a color, which can then be used as the key color to
 * return.
 *
 * The number of clusters is limited to @n_clusters, as a subjective survey of
 * 1000 icons found that they commonly used this number of key colors.
 *
 * Various other shortcuts have been taken which make this approach quite
 * specific to key color extraction from icons, with the aim of making it
 * faster. That’s fine — it doesn’t matter if the results this function produces
 * are optimal, only that they’re good enough. */
static void
k_means (GArray    *colors,
         GdkPixbuf *pb)
{
	gint rowstride, n_channels;
	gint width, height;
	guint8 *raw_pixels;
	ClusterPixel8 *pixels;
	const ClusterPixel8 *pixels_end;
	Pixel8 cluster_centres[n_clusters];
	CentroidAccumulator cluster_accumulators[n_clusters];
	guint n_assignments_changed;
	guint n_iterations = 0;
	guint assignments_termination_limit;

	n_channels = gdk_pixbuf_get_n_channels (pb);
	rowstride = gdk_pixbuf_get_rowstride (pb);
	raw_pixels = gdk_pixbuf_get_pixels (pb);
	width = gdk_pixbuf_get_width (pb);
	height = gdk_pixbuf_get_height (pb);

	/* The pointer arithmetic over pixels can be simplified if we can assume
	 * there are no gaps in the @raw_pixels data. Since the caller is
	 * downsizing the #GdkPixbuf, this is a reasonable assumption. */
	g_assert (rowstride == width * n_channels);
	g_assert (n_channels == 4);

	pixels = (ClusterPixel8 *) raw_pixels;
	pixels_end = &pixels[height * width];

	/* Initialise the clusters using the Random Partition method: randomly
	 * assign a starting cluster to each pixel.
	 *
	 * The Forgy method (choosing random pixels as the starting cluster
	 * centroids) is not appropriate as the checks required to make sure
	 * they aren’t transparent or duplicated colors mean that the
	 * initialisation step may never complete. Consider the case of an icon
	 * which is a block of solid color. */
	for (ClusterPixel8 *p = pixels; p < pixels_end; p++) {
		if (p->alpha < minimum_alpha)
			p->cluster = G_N_ELEMENTS (cluster_centres);
		else
			p->cluster = g_random_int_range (0, G_N_ELEMENTS (cluster_centres));
	}

	/* Iterate until every cluster is relatively settled. This is determined
	 * by the number of pixels whose assignment to a cluster changes in
	 * each iteration — if the number of pixels is less than 1% of the image
	 * then subsequent iterations are not going to significantly affect the
	 * results.
	 *
	 * As we’re choosing key colors, finding the optimal result is not
	 * needed. We just need to find one which is good enough, quickly.
	 *
	 * A second termination condition is set on the number of iterations, to
	 * avoid a potential infinite loop. This termination condition is never
	 * normally expected to be hit — typically an icon will require 5–10
	 * iterations to terminate based on @n_assignments_changed. */
	assignments_termination_limit = width * height * 0.01;
	n_iterations = 0;
	do {
		/* Update step. Re-calculate the centroid of each cluster from
		 * the colors which are in it. */
		memset (cluster_accumulators, 0, sizeof (cluster_accumulators));

		for (const ClusterPixel8 *p = pixels; p < pixels_end; p++) {
			if (p->cluster >= G_N_ELEMENTS (cluster_centres))
				continue;

			cluster_accumulators[p->cluster].red += p->color.red;
			cluster_accumulators[p->cluster].green += p->color.green;
			cluster_accumulators[p->cluster].blue += p->color.blue;
			cluster_accumulators[p->cluster].n_members++;
		}

		for (gsize i = 0; i < G_N_ELEMENTS (cluster_centres); i++) {
			if (cluster_accumulators[i].n_members == 0)
				continue;

			cluster_centres[i].red = cluster_accumulators[i].red / cluster_accumulators[i].n_members;
			cluster_centres[i].green = cluster_accumulators[i].green / cluster_accumulators[i].n_members;
			cluster_centres[i].blue = cluster_accumulators[i].blue / cluster_accumulators[i].n_members;
		}

		/* Update assignments of colors to clusters. */
		n_assignments_changed = 0;
		for (ClusterPixel8 *p = pixels; p < pixels_end; p++) {
			gsize new_cluster;

			if (p->cluster >= G_N_ELEMENTS (cluster_centres))
				continue;

			new_cluster = nearest_cluster (&p->color, cluster_centres, G_N_ELEMENTS (cluster_centres));
			if (new_cluster != p->cluster)
				n_assignments_changed++;
			p->cluster = new_cluster;
		}

		n_iterations++;
	} while (n_assignments_changed > assignments_termination_limit && n_iterations < 50);

	/* Output the cluster centres: these are the icon’s key colors. */
	for (gsize i = 0; i < G_N_ELEMENTS (cluster_centres); i++) {
		GdkRGBA color;

		if (cluster_accumulators[i].n_members == 0)
			continue;

		color.red = (gdouble) cluster_centres[i].red / 255.0;
		color.green = (gdouble) cluster_centres[i].green / 255.0;
		color.blue = (gdouble) cluster_centres[i].blue / 255.0;
		color.alpha = 1.0;
		g_array_append_val (colors, color);
	}
}

/**
 * gs_calculate_key_colors:
 * @pixbuf: an app icon to calculate key colors from
 *
 * Calculate the set of key colors present in @pixbuf. These are the colors
 * which stand out the most, and they are subjective. This function does not
 * guarantee to return perfect results, but should return workable results for
 * most icons.
 *
 * @pixbuf will be scaled down to 32×32 pixels, so if it can be provided at
 * that resolution by the caller, this function will return faster.
 *
 * Returns: (transfer full) (element-type GdkRGBA): key colors for @pixbuf
 * Since: 40
 */
GArray *
gs_calculate_key_colors (GdkPixbuf *pixbuf)
{
	g_autoptr(GdkPixbuf) pb_small = NULL;
	g_autoptr(GArray) colors = g_array_new (FALSE, FALSE, sizeof (GdkRGBA));

	/* people almost always use BILINEAR scaling with pixbufs, but we can
	 * use NEAREST here since we only care about the rough colour data, not
	 * whether the edges in the image are smooth and visually appealing;
	 * NEAREST is twice as fast as BILINEAR */
	pb_small = gdk_pixbuf_scale_simple (pixbuf, 32, 32, GDK_INTERP_NEAREST);

	/* require an alpha channel for storing temporary values; most images
	 * have one already, about 2% don’t */
	if (gdk_pixbuf_get_n_channels (pixbuf) != 4) {
		g_autoptr(GdkPixbuf) temp = g_steal_pointer (&pb_small);
		pb_small = gdk_pixbuf_add_alpha (temp, FALSE, 0, 0, 0);
	}

	/* get a list of key colors */
	k_means (colors, pb_small);

	return g_steal_pointer (&colors);
}
