/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
 *
 * Copyright (C) 2016-2024 Matthias Klumpp <matthias@tenstral.net>
 * Copyright (C) 2014-2016 Richard Hughes <richard@hughsie.com>
 *
 * Licensed under the GNU Lesser General Public License Version 2.1
 *
 * This library is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 2.1 of the license, or
 * (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this library.  If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * SECTION:asc-image
 * @short_description: Basic image rendering functions.
 * @include: appstream-compose.h
 */

#include "config.h"
#include "asc-image.h"

#include <gio/gio.h>
#include <math.h>

#include "asc-globals.h"

struct _AscImage {
	GObject parent_instance;
};

typedef struct {
	GdkPixbuf *pix;
	guint width;
	guint height;
} AscImagePrivate;

G_DEFINE_TYPE_WITH_PRIVATE (AscImage, asc_image, G_TYPE_OBJECT)
#define GET_PRIVATE(o) (asc_image_get_instance_private (o))

/**
 * asc_image_error_quark:
 *
 * Return value: An error quark.
 **/
GQuark
asc_image_error_quark (void)
{
	static GQuark quark = 0;
	if (!quark)
		quark = g_quark_from_static_string ("AscImageError");
	return quark;
}

/**
 * asc_image_format_to_string:
 * @format: the %AscImageFormat.
 *
 * Converts the enumerated value to an text representation.
 *
 * Returns: string version of @format
 **/
const gchar *
asc_image_format_to_string (AscImageFormat format)
{
	if (format == ASC_IMAGE_FORMAT_PNG)
		return "png";
	if (format == ASC_IMAGE_FORMAT_JPEG)
		return "jpeg";
	if (format == ASC_IMAGE_FORMAT_GIF)
		return "gif";
	if (format == ASC_IMAGE_FORMAT_SVG)
		return "svg";
	if (format == ASC_IMAGE_FORMAT_SVGZ)
		return "svgz";
	if (format == ASC_IMAGE_FORMAT_WEBP)
		return "webp";
	if (format == ASC_IMAGE_FORMAT_AVIF)
		return "avif";
	if (format == ASC_IMAGE_FORMAT_XPM)
		return "xpm";
	return NULL;
}

/**
 * asc_image_format_from_string:
 * @str: the string.
 *
 * Converts the text representation to an enumerated value.
 *
 * Returns: a #AscImageFormat or %ASC_IMAGE_FORMAT_UNKNOWN for unknown
 **/
AscImageFormat
asc_image_format_from_string (const gchar *str)
{
	if (g_strcmp0 (str, "png") == 0)
		return ASC_IMAGE_FORMAT_PNG;
	if (g_strcmp0 (str, "jpeg") == 0)
		return ASC_IMAGE_FORMAT_JPEG;
	if (g_strcmp0 (str, "gif") == 0)
		return ASC_IMAGE_FORMAT_GIF;
	if (g_strcmp0 (str, "svg") == 0)
		return ASC_IMAGE_FORMAT_SVG;
	if (g_strcmp0 (str, "svgz") == 0)
		return ASC_IMAGE_FORMAT_SVGZ;
	if (g_strcmp0 (str, "webp") == 0)
		return ASC_IMAGE_FORMAT_WEBP;
	if (g_strcmp0 (str, "avif") == 0)
		return ASC_IMAGE_FORMAT_AVIF;
	if (g_strcmp0 (str, "xpm") == 0)
		return ASC_IMAGE_FORMAT_XPM;
	return ASC_IMAGE_FORMAT_UNKNOWN;
}

/**
 * asc_image_format_from_filename:
 * @fname: the filename.
 *
 * Returns the image format type based on the given file's filename.
 *
 * Returns: a #AscImageFormat or %ASC_IMAGE_FORMAT_UNKNOWN for unknown
 **/
AscImageFormat
asc_image_format_from_filename (const gchar *fname)
{
	g_autofree gchar *fname_low = g_ascii_strdown (fname, -1);

	if (g_str_has_suffix (fname_low, ".png"))
		return ASC_IMAGE_FORMAT_PNG;
	if (g_str_has_suffix (fname_low, ".jpeg") || g_str_has_suffix (fname_low, ".jpg"))
		return ASC_IMAGE_FORMAT_JPEG;
	if (g_str_has_suffix (fname_low, ".gif"))
		return ASC_IMAGE_FORMAT_GIF;
	if (g_str_has_suffix (fname_low, ".svg"))
		return ASC_IMAGE_FORMAT_SVG;
	if (g_str_has_suffix (fname_low, ".svgz"))
		return ASC_IMAGE_FORMAT_SVGZ;
	if (g_str_has_suffix (fname_low, ".webp"))
		return ASC_IMAGE_FORMAT_WEBP;
	if (g_str_has_suffix (fname_low, ".avif"))
		return ASC_IMAGE_FORMAT_AVIF;
	if (g_str_has_suffix (fname_low, ".xpm"))
		return ASC_IMAGE_FORMAT_XPM;
	return ASC_IMAGE_FORMAT_UNKNOWN;
}

static void
asc_image_finalize (GObject *object)
{
	AscImage *image = ASC_IMAGE (object);
	AscImagePrivate *priv = GET_PRIVATE (image);

	if (priv->pix != NULL)
		g_object_unref (priv->pix);

	G_OBJECT_CLASS (asc_image_parent_class)->finalize (object);
}

static void
asc_image_init (AscImage *image)
{
}

static void
asc_image_class_init (AscImageClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);
	object_class->finalize = asc_image_finalize;
}

/**
 * asc_image_new:
 *
 * Creates a new #AscImage.
 **/
AscImage *
asc_image_new (void)
{
	AscImage *image;
	image = g_object_new (ASC_TYPE_IMAGE, NULL);
	return ASC_IMAGE (image);
}

/**
 * asc_optimize_png:
 * @fname: Filename of the PNG image to optimize.
 * @error: A #GError or %NULL
 *
 * Optimizes a PNG graphic for size with optipng, if its binary
 * is available and this feature is enabled.
 **/
gboolean
asc_optimize_png (const gchar *fname, GError **error)
{
	gint exit_status;
	gboolean r;
	const gchar *optipng_path;
	g_autofree gchar *opng_stdout = NULL;
	g_autofree gchar *opng_stderr = NULL;
	g_autofree const gchar **argv = NULL;
	GError *tmp_error = NULL;

	if (!asc_globals_get_use_optipng ())
		return TRUE;

	optipng_path = asc_globals_get_optipng_binary ();
	if (optipng_path == NULL) {
		g_set_error (error,
			     ASC_IMAGE_ERROR,
			     ASC_IMAGE_ERROR_FAILED,
			     "optipng not found in $PATH");
		return FALSE;
	}

	argv = g_new0 (const gchar *, 2 + 1);
	argv[0] = optipng_path;
	argv[1] = fname;

	/* NOTE: Maybe add an option to run optipng with stronger optimization? (>= -o4) */
	r = g_spawn_sync (NULL, /* working directory */
			  (gchar **) argv,
			  NULL, /* envp */
			  G_SPAWN_LEAVE_DESCRIPTORS_OPEN,
			  NULL, /* child setup */
			  NULL, /* user data */
			  &opng_stdout,
			  &opng_stderr,
			  &exit_status,
			  &tmp_error);
	if (!r) {
		g_propagate_prefixed_error (error, tmp_error, "Failed to spawn optipng.");
		return FALSE;
	}

	if (exit_status != 0) {
		/* FIXME: Maybe emit this as proper error, instead of just logging it? */
		g_warning ("Optipng on '%s' failed with error code %i: %s%s",
			   fname,
			   exit_status,
			   opng_stderr ? opng_stderr : "",
			   opng_stdout ? opng_stdout : "");
	}

	return TRUE;
}

/**
 * asc_image_supported_format_names:
 *
 * Get a set of image format names we can currently read
 * (via GdkPixbuf).
 *
 * Returns: (transfer full): A hash set of format names.
 **/
GHashTable *
asc_image_supported_format_names (void)
{
	g_autoptr(GSList) fm_list = NULL;
	GHashTable *res;

	res = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
	fm_list = gdk_pixbuf_get_formats ();

	if (fm_list == NULL)
		return res;

	for (GSList *l = fm_list; l != NULL; l = l->next)
		g_hash_table_add (res, gdk_pixbuf_format_get_name (l->data));

	return res;
}

/**
 * asc_image_load_pixbuf:
 **/
static gboolean
asc_image_load_pixbuf (AscImage *image,
		       GdkPixbuf *pixbuf,
		       guint dest_size,
		       guint src_size_min,
		       AscImageLoadFlags flags,
		       GError **error)
{
	guint pixbuf_height;
	guint pixbuf_width;
	guint tmp_height;
	guint tmp_width;
	g_autoptr(GdkPixbuf) pixbuf_tmp = NULL;
	g_autoptr(GdkPixbuf) pixbuf_new = NULL;

	/* check size */
	if (gdk_pixbuf_get_width (pixbuf) < (gint) src_size_min &&
	    gdk_pixbuf_get_height (pixbuf) < (gint) src_size_min) {
		g_set_error (error,
			     ASC_IMAGE_ERROR,
			     ASC_IMAGE_ERROR_FAILED,
			     "Image was too small %ix%i",
			     gdk_pixbuf_get_width (pixbuf),
			     gdk_pixbuf_get_height (pixbuf));
		return FALSE;
	}

	/* don't do anything to an icon with the perfect size */
	pixbuf_width = (guint) gdk_pixbuf_get_width (pixbuf);
	pixbuf_height = (guint) gdk_pixbuf_get_height (pixbuf);
	if (pixbuf_width == dest_size && pixbuf_height == dest_size) {
		asc_image_set_pixbuf (image, pixbuf);
		return TRUE;
	}

	/* this makes icons look blurry, but allows the software center to look
	 * good as icons are properly aligned in the UI layout */
	if (as_flags_contains (flags, ASC_IMAGE_LOAD_FLAG_ALWAYS_RESIZE)) {
		pixbuf_new = gdk_pixbuf_scale_simple (pixbuf,
						      (gint) dest_size,
						      (gint) dest_size,
						      GDK_INTERP_HYPER);
		asc_image_set_pixbuf (image, pixbuf_new);
		return TRUE;
	}

	/* never scale up, just pad */
	if (pixbuf_width < dest_size && pixbuf_height < dest_size) {
		g_debug ("icon padded to %ux%u as size %ux%u",
			 dest_size,
			 dest_size,
			 pixbuf_width,
			 pixbuf_height);
		pixbuf_new = gdk_pixbuf_new (GDK_COLORSPACE_RGB,
					     TRUE,
					     8,
					     (gint) dest_size,
					     (gint) dest_size);
		gdk_pixbuf_fill (pixbuf_new, 0x00000000);
		gdk_pixbuf_copy_area (pixbuf,
				      0,
				      0, /* of src */
				      (gint) pixbuf_width,
				      (gint) pixbuf_height,
				      pixbuf_new,
				      (gint) (dest_size - pixbuf_width) / 2,
				      (gint) (dest_size - pixbuf_height) / 2);
		asc_image_set_pixbuf (image, pixbuf_new);
		return TRUE;
	}

	/* is the aspect ratio perfectly square */
	if (pixbuf_width == pixbuf_height) {
		pixbuf_new = gdk_pixbuf_scale_simple (pixbuf,
						      (gint) dest_size,
						      (gint) dest_size,
						      GDK_INTERP_HYPER);
		asc_image_set_pixbuf (image, pixbuf_new);
		return TRUE;
	}

	/* create new square pixbuf with alpha padding */
	pixbuf_new = gdk_pixbuf_new (GDK_COLORSPACE_RGB,
				     TRUE,
				     8,
				     (gint) dest_size,
				     (gint) dest_size);
	gdk_pixbuf_fill (pixbuf_new, 0x00000000);
	if (pixbuf_width > pixbuf_height) {
		tmp_width = dest_size;
		tmp_height = dest_size * pixbuf_height / pixbuf_width;
	} else {
		tmp_width = dest_size * pixbuf_width / pixbuf_height;
		tmp_height = dest_size;
	}
	pixbuf_tmp = gdk_pixbuf_scale_simple (pixbuf,
					      (gint) tmp_width,
					      (gint) tmp_height,
					      GDK_INTERP_HYPER);
	if (as_flags_contains (flags, ASC_IMAGE_LOAD_FLAG_SHARPEN))
		asc_pixbuf_sharpen (pixbuf_tmp, 1, -0.5);
	gdk_pixbuf_copy_area (pixbuf_tmp,
			      0,
			      0, /* of src */
			      (gint) tmp_width,
			      (gint) tmp_height,
			      pixbuf_new,
			      (gint) (dest_size - tmp_width) / 2,
			      (gint) (dest_size - tmp_height) / 2);
	asc_image_set_pixbuf (image, pixbuf_new);
	return TRUE;
}

/**
 * asc_image_new_from_file:
 * @fname: Name of the file to load.
 * @dest_size: The size of the constructed pixbuf, or 0 for the native size
 * @flags: a #AscImageLoadFlags, e.g. %ASC_IMAGE_LOAD_FLAG_NONE
 * @error: A #GError or %NULL
 *
 * Creates a new #AscImage from a file on the filesystem.
 **/
AscImage *
asc_image_new_from_file (const gchar *fname,
			 guint dest_size,
			 AscImageLoadFlags flags,
			 GError **error)
{
	gboolean ret;
	g_autoptr(AscImage) image = asc_image_new ();

	ret = asc_image_load_filename (image, fname, dest_size, 0, flags, error);
	if (!ret)
		return NULL;
	return g_steal_pointer (&image);
}

/**
 * asc_image_new_from_data:
 * @data: Data to load.
 * @len: Length of the data to load.
 * @dest_size: The size of the constructed pixbuf, or 0 for the native size
 * @flags: a #AscImageLoadFlags, e.g. %ASC_IMAGE_LOAD_FLAG_NONE
 * @compressed: %TRUE if passed data is gzip-compressed
 * @error: A #GError or %NULL
 *
 * Creates a new #AscImage from data in memory.
 **/
AscImage *
asc_image_new_from_data (const void *data,
			 gssize len,
			 guint dest_size,
			 gboolean compressed,
			 AscImageLoadFlags flags,
			 GError **error)
{
	gboolean ret;
	g_autoptr(GInputStream) istream = NULL;
	g_autoptr(GInputStream) dstream = NULL;
	g_autoptr(GConverter) conv = NULL;
	g_autoptr(GdkPixbuf) pix = NULL;
	g_autoptr(AscImage) image = asc_image_new ();

	istream = g_memory_input_stream_new_from_data (data, len, NULL);
	if (compressed) {
		conv = G_CONVERTER (g_zlib_decompressor_new (G_ZLIB_COMPRESSOR_FORMAT_GZIP));
		dstream = g_converter_input_stream_new (istream, conv);
	} else {
		dstream = g_object_ref (istream);
	}

	if (dest_size == 0) {
		/* use the native size and don't perform any scaling */
		pix = gdk_pixbuf_new_from_stream (dstream, NULL, error);
		if (pix == NULL)
			return NULL;

		asc_image_set_pixbuf (image, pix);
		return g_steal_pointer (&image);
	}

	/* load & scale */
	if (as_flags_contains (flags, ASC_IMAGE_LOAD_FLAG_ALWAYS_RESIZE)) {
		pix = gdk_pixbuf_new_from_stream_at_scale (dstream,
							   dest_size,
							   dest_size,
							   TRUE,
							   NULL,
							   error);
		if (pix == NULL)
			return NULL;
	} else {
		/* just load, we will do resizing later */
		pix = gdk_pixbuf_new_from_stream (dstream, NULL, error);
		if (pix == NULL)
			return NULL;
	}
	ret = asc_image_load_pixbuf (image, pix, dest_size, 0, flags, error);
	if (!ret)
		return NULL;

	return g_steal_pointer (&image);
}

/**
 * asc_image_pixbuf_new_from_gz:
 *
 * Wrapper to allow GdkPixbuf to load SVG images from SVGZ files as well.
 */
static GdkPixbuf *
asc_image_pixbuf_new_from_gz (const gchar *filename, gint width, gint height, GError **error)
{
	g_autoptr(GFile) file = NULL;
	g_autoptr(GInputStream) file_stream = NULL;
	g_autoptr(GInputStream) stream_data = NULL;
	g_autoptr(GConverter) conv = NULL;
	g_autoptr(GFileInfo) info = NULL;
	const gchar *content_type = NULL;

	file = g_file_new_for_path (filename);
	if (!g_file_query_exists (file, NULL)) {
		g_set_error_literal (error,
				     ASC_IMAGE_ERROR,
				     ASC_IMAGE_ERROR_FAILED,
				     "Image file does not exist");
		return NULL;
	}
	info = g_file_query_info (file,
				  G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
				  G_FILE_QUERY_INFO_NONE,
				  NULL,
				  NULL);
	if (info != NULL)
		content_type = g_file_info_get_attribute_string (
		    info,
		    G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE);

	file_stream = G_INPUT_STREAM (g_file_read (file, NULL, error));
	if (file_stream == NULL)
		return NULL;

	if ((g_strcmp0 (content_type, "application/gzip") == 0) ||
	    (g_strcmp0 (content_type, "application/x-gzip") == 0)) {
		/* decompress the GZip stream */
		conv = G_CONVERTER (g_zlib_decompressor_new (G_ZLIB_COMPRESSOR_FORMAT_GZIP));
		stream_data = g_converter_input_stream_new (file_stream, conv);
	} else {
		stream_data = g_object_ref (file_stream);
	}

	if (width != 0 || height != 0)
		return gdk_pixbuf_new_from_stream_at_scale (stream_data,
							    width,
							    height,
							    TRUE,
							    NULL,
							    error);
	else
		return gdk_pixbuf_new_from_stream (stream_data, NULL, error);
}

/**
 * asc_image_load_filename:
 * @image: a #AscImage instance.
 * @filename: filename to read from
 * @dest_size: The size of the constructed pixbuf, or 0 for the native size
 * @src_size_min: The smallest source size allowed, or 0 for none
 * @flags: a #AscImageLoadFlags, e.g. %ASC_IMAGE_LOAD_FLAG_NONE
 * @error: A #GError or %NULL.
 *
 * Reads an image from a file.
 *
 * Returns: %TRUE for success
 **/
gboolean
asc_image_load_filename (AscImage *image,
			 const gchar *filename,
			 guint dest_size,
			 guint src_size_min,
			 AscImageLoadFlags flags,
			 GError **error)
{
	g_autoptr(GdkPixbuf) pixbuf_src = NULL;
	gboolean is_svg = FALSE;

	g_return_val_if_fail (ASC_IS_IMAGE (image), FALSE);

	is_svg = g_str_has_suffix (filename, ".svg") || g_str_has_suffix (filename, ".svgz");
#ifndef HAVE_SVG_SUPPORT
	if (is_svg) {
		g_warning ("Unable to load SVG graphic: AppStream built without SVG support.");
		g_set_error_literal (error,
				     ASC_IMAGE_ERROR,
				     ASC_IMAGE_ERROR_UNSUPPORTED,
				     "AppStream was built without SVG support. This is an issue "
				     "with your AppStream distribution. "
				     "Please rebuild AppStream with SVG support enabled or contact "
				     "your distributor to enable it for you.");
		return FALSE;
	}
#endif

	/* only support allowed types, unless support for any image is explicitly requested */
	if (!as_flags_contains (flags, ASC_IMAGE_LOAD_FLAG_ALLOW_UNSUPPORTED)) {
		GdkPixbufFormat *fmt;
		g_autofree gchar *name = NULL;
		fmt = gdk_pixbuf_get_file_info (filename, NULL, NULL);
		if (fmt == NULL) {
			g_set_error_literal (error,
					     ASC_IMAGE_ERROR,
					     ASC_IMAGE_ERROR_FAILED,
					     "Image format was not recognized");
			return FALSE;
		}
		name = gdk_pixbuf_format_get_name (fmt);
		if (asc_image_format_from_string (name) == ASC_IMAGE_FORMAT_UNKNOWN) {
			g_set_error (error,
				     ASC_IMAGE_ERROR,
				     ASC_IMAGE_ERROR_UNSUPPORTED,
				     "Image format %s is not supported",
				     name);
			return FALSE;
		}
	}

	/* load the image of the native size */
	if (dest_size == 0) {
		g_autoptr(GdkPixbuf) pixbuf = NULL;
		pixbuf = asc_image_pixbuf_new_from_gz (filename, 0, 0, error);
		if (pixbuf == NULL)
			return FALSE;
		asc_image_set_pixbuf (image, pixbuf);
		return TRUE;
	}

	/* open file in native size */
	if (is_svg) {
		pixbuf_src = asc_image_pixbuf_new_from_gz (filename,
							   (gint) dest_size,
							   (gint) dest_size,
							   error);
	} else {
		pixbuf_src = asc_image_pixbuf_new_from_gz (filename, 0, 0, error);
	}
	if (pixbuf_src == NULL)
		return FALSE;

	/* create from pixbuf & resize */
	return asc_image_load_pixbuf (image, pixbuf_src, dest_size, src_size_min, flags, error);
}

/**
 * asc_image_get_pixbuf:
 * @image: a #AscImage instance.
 *
 * Gets the image pixbuf if set.
 *
 * Returns: (transfer none): the #GdkPixbuf, or %NULL
 **/
GdkPixbuf *
asc_image_get_pixbuf (AscImage *image)
{
	AscImagePrivate *priv = GET_PRIVATE (image);
	g_return_val_if_fail (ASC_IS_IMAGE (image), NULL);
	return priv->pix;
}

/**
 * asc_image_set_pixbuf:
 * @image: a #AscImage instance.
 * @pixbuf: the #GdkPixbuf, or %NULL
 *
 * Sets the image pixbuf.
 **/
void
asc_image_set_pixbuf (AscImage *image, GdkPixbuf *pixbuf)
{
	AscImagePrivate *priv = GET_PRIVATE (image);
	g_return_if_fail (ASC_IS_IMAGE (image));

	g_set_object (&priv->pix, pixbuf);
	if (pixbuf == NULL)
		return;
	priv->width = (guint) gdk_pixbuf_get_width (pixbuf);
	priv->height = (guint) gdk_pixbuf_get_height (pixbuf);
}

/**
 * asc_image_get_width:
 * @image: an #AscImage instance.
 *
 * Gets the image width.
 **/
guint
asc_image_get_width (AscImage *image)
{
	AscImagePrivate *priv = GET_PRIVATE (image);
	return priv->width;
}

/**
 * asc_image_get_height:
 * @image: an #AscImage instance.
 *
 * Gets the image height.
 **/
guint
asc_image_get_height (AscImage *image)
{
	AscImagePrivate *priv = GET_PRIVATE (image);
	return priv->height;
}

/**
 * asc_image_scale:
 * @image: an #AscImage instance.
 * @new_width: The new width.
 * @new_height: the new height.
 *
 * Scale the image to the given size.
 **/
void
asc_image_scale (AscImage *image, guint new_width, guint new_height)
{
	AscImagePrivate *priv = GET_PRIVATE (image);
	g_autoptr(GdkPixbuf) res_pix = NULL;
	if (priv->pix == NULL)
		return;

	res_pix = gdk_pixbuf_scale_simple (priv->pix, new_width, new_height, GDK_INTERP_BILINEAR);
	if (res_pix == NULL)
		g_error ("Unable to allocate enough memory for image scaling.");

	/* set our current image to the scaled version */
	asc_image_set_pixbuf (image, res_pix);
}

/**
 * asc_image_scale_to_width:
 * @image: an #AscImage instance.
 * @new_width: The new width.
 *
 * Scale the image to the given width, preserving
 * its aspect ratio.
 **/
void
asc_image_scale_to_width (AscImage *image, guint new_width)
{
	double scale;
	guint new_height;
	scale = (double) new_width / (double) asc_image_get_width (image);
	new_height = floor (asc_image_get_height (image) * scale);

	asc_image_scale (image, new_width, new_height);
}

/**
 * asc_image_scale_to_height:
 * @image: an #AscImage instance.
 * @new_height: the new height.
 *
 * Scale the image to the given height, preserving
 * its aspect ratio.
 **/
void
asc_image_scale_to_height (AscImage *image, guint new_height)
{
	double scale;
	guint new_width;
	scale = (double) new_height / (double) asc_image_get_height (image);
	new_width = floor (asc_image_get_width (image) * scale);

	asc_image_scale (image, new_width, new_height);
}

/**
 * asc_image_scale_to_fit:
 * @image: an #AscImage instance.
 * @size: the maximum edge length.
 *
 * Scale the image to fir in a square with the given edge length,
 * and keep its aspect ratio.
 **/
void
asc_image_scale_to_fit (AscImage *image, guint size)
{
	if (asc_image_get_height (image) > asc_image_get_width (image))
		asc_image_scale_to_height (image, size);
	else
		asc_image_scale_to_width (image, size);
}

/**
 * asc_image_save_pixbuf:
 * @image: a #AscImage instance.
 * @width: target width, or 0 for default
 * @height: target height, or 0 for default
 * @flags: some #AscImageSaveFlags values, e.g. %ASC_IMAGE_SAVE_FLAG_PAD_16_9
 *
 * Resamples a pixbuf to a specific size.
 *
 * Returns: (transfer full): A #GdkPixbuf of the specified size
 **/
GdkPixbuf *
asc_image_save_pixbuf (AscImage *image, guint width, guint height, AscImageSaveFlags flags)
{
	AscImagePrivate *priv = GET_PRIVATE (image);
	GdkPixbuf *pixbuf = NULL;
	guint tmp_height;
	guint tmp_width;
	guint pixbuf_height;
	guint pixbuf_width;
	g_autoptr(GdkPixbuf) pixbuf_tmp = NULL;

	g_return_val_if_fail (ASC_IS_IMAGE (image), NULL);

	/* never set */
	if (priv->pix == NULL)
		return NULL;

	/* 0 means 'default' */
	if (width == 0)
		width = (guint) gdk_pixbuf_get_width (priv->pix);
	if (height == 0)
		height = (guint) gdk_pixbuf_get_height (priv->pix);

	/* don't do anything to an image with the correct size */
	pixbuf_width = (guint) gdk_pixbuf_get_width (priv->pix);
	pixbuf_height = (guint) gdk_pixbuf_get_height (priv->pix);
	if (width == pixbuf_width && height == pixbuf_height)
		return g_object_ref (priv->pix);

	/* is the aspect ratio of the source perfectly 16:9 */
	if (flags == ASC_IMAGE_SAVE_FLAG_NONE || (pixbuf_width / 16) * 9 == pixbuf_height) {
		pixbuf = gdk_pixbuf_scale_simple (priv->pix,
						  (gint) width,
						  (gint) height,
						  GDK_INTERP_HYPER);
		if (as_flags_contains (flags, ASC_IMAGE_SAVE_FLAG_SHARPEN))
			asc_pixbuf_sharpen (pixbuf, 1, -0.5);
		if (as_flags_contains (flags, ASC_IMAGE_SAVE_FLAG_BLUR))
			asc_pixbuf_blur (pixbuf, 5, 3);
		return pixbuf;
	}

	/* create new 16:9 pixbuf with alpha padding */
	pixbuf = gdk_pixbuf_new (GDK_COLORSPACE_RGB, TRUE, 8, (gint) width, (gint) height);
	gdk_pixbuf_fill (pixbuf, 0x00000000);
	/* check the ratio to see which property needs to be fitted and which needs
	 * to be reduced */
	if (pixbuf_width * 9 > pixbuf_height * 16) {
		tmp_width = width;
		tmp_height = width * pixbuf_height / pixbuf_width;
	} else {
		tmp_width = height * pixbuf_width / pixbuf_height;
		tmp_height = height;
	}
	pixbuf_tmp = gdk_pixbuf_scale_simple (priv->pix,
					      (gint) tmp_width,
					      (gint) tmp_height,
					      GDK_INTERP_HYPER);
	if (as_flags_contains (flags, ASC_IMAGE_SAVE_FLAG_SHARPEN))
		asc_pixbuf_sharpen (pixbuf_tmp, 1, -0.5);
	if (as_flags_contains (flags, ASC_IMAGE_SAVE_FLAG_BLUR))
		asc_pixbuf_blur (pixbuf_tmp, 5, 3);
	gdk_pixbuf_copy_area (pixbuf_tmp,
			      0,
			      0, /* of src */
			      (gint) tmp_width,
			      (gint) tmp_height,
			      pixbuf,
			      (gint) (width - tmp_width) / 2,
			      (gint) (height - tmp_height) / 2);
	return pixbuf;
}

/**
 * asc_image_save_filename:
 * @image: a #AscImage instance.
 * @filename: filename to write to
 * @width: target width, or 0 for default
 * @height: target height, or 0 for default
 * @flags: some #AscImageSaveFlags values, e.g. %ASC_IMAGE_SAVE_FLAG_PAD_16_9
 * @error: A #GError or %NULL.
 *
 * Saves the image to a file.
 *
 * Returns: %TRUE for success
 **/
gboolean
asc_image_save_filename (AscImage *image,
			 const gchar *filename,
			 guint width,
			 guint height,
			 AscImageSaveFlags flags,
			 GError **error)
{
	g_autoptr(GdkPixbuf) pixbuf = NULL;

	/* save source file */
	pixbuf = asc_image_save_pixbuf (image, width, height, flags);
	if (!gdk_pixbuf_save (pixbuf, filename, "png", error, NULL))
		return FALSE;

	if (!as_flags_contains (flags, ASC_IMAGE_SAVE_FLAG_OPTIMIZE))
		return TRUE;
	return asc_optimize_png (filename, error);
}

static void
asc_pixbuf_blur_private (GdkPixbuf *src, GdkPixbuf *dest, gint radius, guchar *div_kernel_size)
{
	gint width, height, src_rowstride, dest_rowstride, n_channels;
	guchar *p_src, *p_dest, *c1, *c2;
	gint x, y, i, i1, i2, width_minus_1, height_minus_1, radius_plus_1;
	gint r, g, b, a;
	guchar *p_dest_row, *p_dest_col;

	width = gdk_pixbuf_get_width (src);
	height = gdk_pixbuf_get_height (src);
	n_channels = gdk_pixbuf_get_n_channels (src);
	radius_plus_1 = radius + 1;

	/* horizontal blur */
	p_src = gdk_pixbuf_get_pixels (src);
	p_dest = gdk_pixbuf_get_pixels (dest);
	src_rowstride = gdk_pixbuf_get_rowstride (src);
	dest_rowstride = gdk_pixbuf_get_rowstride (dest);
	width_minus_1 = width - 1;
	for (y = 0; y < height; y++) {

		/* calc the initial sums of the kernel */
		r = g = b = a = 0;
		for (i = -radius; i <= radius; i++) {
			c1 = p_src + (CLAMP (i, 0, width_minus_1) * n_channels);
			r += c1[0];
			g += c1[1];
			b += c1[2];
		}

		p_dest_row = p_dest;
		for (x = 0; x < width; x++) {
			/* set as the mean of the kernel */
			p_dest_row[0] = div_kernel_size[r];
			p_dest_row[1] = div_kernel_size[g];
			p_dest_row[2] = div_kernel_size[b];
			p_dest_row += n_channels;

			/* the pixel to add to the kernel */
			i1 = x + radius_plus_1;
			if (i1 > width_minus_1)
				i1 = width_minus_1;
			c1 = p_src + (i1 * n_channels);

			/* the pixel to remove from the kernel */
			i2 = x - radius;
			if (i2 < 0)
				i2 = 0;
			c2 = p_src + (i2 * n_channels);

			/* calc the new sums of the kernel */
			r += c1[0] - c2[0];
			g += c1[1] - c2[1];
			b += c1[2] - c2[2];
		}

		p_src += src_rowstride;
		p_dest += dest_rowstride;
	}

	/* vertical blur */
	p_src = gdk_pixbuf_get_pixels (dest);
	p_dest = gdk_pixbuf_get_pixels (src);
	src_rowstride = gdk_pixbuf_get_rowstride (dest);
	dest_rowstride = gdk_pixbuf_get_rowstride (src);
	height_minus_1 = height - 1;
	for (x = 0; x < width; x++) {

		/* calc the initial sums of the kernel */
		r = g = b = a = 0;
		for (i = -radius; i <= radius; i++) {
			c1 = p_src + (CLAMP (i, 0, height_minus_1) * src_rowstride);
			r += c1[0];
			g += c1[1];
			b += c1[2];
		}

		p_dest_col = p_dest;
		for (y = 0; y < height; y++) {
			/* set as the mean of the kernel */

			p_dest_col[0] = div_kernel_size[r];
			p_dest_col[1] = div_kernel_size[g];
			p_dest_col[2] = div_kernel_size[b];
			p_dest_col += dest_rowstride;

			/* the pixel to add to the kernel */
			i1 = y + radius_plus_1;
			if (i1 > height_minus_1)
				i1 = height_minus_1;
			c1 = p_src + (i1 * src_rowstride);

			/* the pixel to remove from the kernel */
			i2 = y - radius;
			if (i2 < 0)
				i2 = 0;
			c2 = p_src + (i2 * src_rowstride);

			/* calc the new sums of the kernel */
			r += c1[0] - c2[0];
			g += c1[1] - c2[1];
			b += c1[2] - c2[2];
		}

		p_src += n_channels;
		p_dest += n_channels;
	}
}

/**
 * as_pixbuf_blur:
 * @src: the GdkPixbuf.
 * @radius: the pixel radius for the gaussian blur, typical values are 1..3
 * @iterations: Amount to blur the image, typical values are 1..5
 *
 * Blurs an image. Warning, this method is s..l..o..w... for large images.
 *
 * Since: 0.14.0
 **/
void
asc_pixbuf_blur (GdkPixbuf *src, gint radius, gint iterations)
{
	gint kernel_size;
	gint i;
	g_autofree guchar *div_kernel_size = NULL;
	g_autoptr(GdkPixbuf) tmp = NULL;

	tmp = gdk_pixbuf_new (gdk_pixbuf_get_colorspace (src),
			      gdk_pixbuf_get_has_alpha (src),
			      gdk_pixbuf_get_bits_per_sample (src),
			      gdk_pixbuf_get_width (src),
			      gdk_pixbuf_get_height (src));
	kernel_size = 2 * radius + 1;
	div_kernel_size = g_new (guchar, 256 * kernel_size);
	for (i = 0; i < 256 * kernel_size; i++)
		div_kernel_size[i] = (guchar) (i / kernel_size);

	while (iterations-- > 0)
		asc_pixbuf_blur_private (src, tmp, radius, div_kernel_size);
}

#define interpolate_value(original, reference, distance) \
	(CLAMP (((distance) * (reference)) + ((1.0 - (distance)) * (original)), 0, 255))

/**
 * as_pixbuf_sharpen:
 * @src: the GdkPixbuf.
 * @radius: the pixel radius for the unsharp mask, typical values are 1..3
 * @amount: Amount to sharpen the image, typical values are -0.1 to -0.9
 *
 * Sharpens an image. Warning, this method is s..l..o..w... for large images.
 **/
void
asc_pixbuf_sharpen (GdkPixbuf *src, gint radius, gdouble amount)
{
	gint width, height, rowstride, n_channels;
	gint x, y;
	guchar *p_blurred;
	guchar *p_blurred_row;
	guchar *p_src;
	guchar *p_src_row;
	g_autoptr(GdkPixbuf) blurred = NULL;

	blurred = gdk_pixbuf_copy (src);
	asc_pixbuf_blur (blurred, radius, 3);

	width = gdk_pixbuf_get_width (src);
	height = gdk_pixbuf_get_height (src);
	rowstride = gdk_pixbuf_get_rowstride (src);
	n_channels = gdk_pixbuf_get_n_channels (src);

	p_src = gdk_pixbuf_get_pixels (src);
	p_blurred = gdk_pixbuf_get_pixels (blurred);

	for (y = 0; y < height; y++) {
		p_src_row = p_src;
		p_blurred_row = p_blurred;
		for (x = 0; x < width; x++) {
			p_src_row[0] = (guchar) interpolate_value (p_src_row[0],
								   p_blurred_row[0],
								   amount);
			p_src_row[1] = (guchar) interpolate_value (p_src_row[1],
								   p_blurred_row[1],
								   amount);
			p_src_row[2] = (guchar) interpolate_value (p_src_row[2],
								   p_blurred_row[2],
								   amount);
			p_src_row += n_channels;
			p_blurred_row += n_channels;
		}
		p_src += rowstride;
		p_blurred += rowstride;
	}
}
