/* load a GIF with libnsgif
 *
 * 6/10/18
 * 	- from gifload.c
 * 3/3/22 tlsa
 * 	- update libnsgif API
 * 9/5/22
 * 	- attach GIF palette as metadata
 * 26/11/22 kleisauke
 * 	- avoid minimise after mapping -- not reliable on Win32
 * 25/1/23 kleisauke
 * 	- set interlaced=1 for interlaced images
 * 13/3/23 MathemanFlo
 * 	- add bits per sample metadata
 */

/*

	This file is part of VIPS.

	VIPS 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 of the License, or
	(at your option) any later version.

	This program 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 program; if not, write to the Free Software
	Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
	02110-1301  USA

 */

/*

	These files are distributed with VIPS - http://www.vips.ecs.soton.ac.uk

 */

/*
#define VERBOSE
#define VIPS_DEBUG
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif /*HAVE_CONFIG_H*/
#include <glib/gi18n-lib.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <ctype.h>

#include <vips/vips.h>
#include <vips/buf.h>
#include <vips/internal.h>
#include <vips/debug.h>

/* TODO:
 *
 * - libnsgif does not seem to support comment metadata
 *
 * - it always loads the entire source file into memory
 *
 * Notes:
 *
 * - hard to detect mono images -- local_colour_table in libnsgif is only set
 *   when we decode a frame, so we can't tell just from init whether any
 *   frames have colour info
 */

#ifdef HAVE_NSGIF

#include <libnsgif/nsgif.h>

#define VIPS_TYPE_FOREIGN_LOAD_GIF (vips_foreign_load_nsgif_get_type())
#define VIPS_FOREIGN_LOAD_GIF(obj) \
	(G_TYPE_CHECK_INSTANCE_CAST((obj), \
		VIPS_TYPE_FOREIGN_LOAD_GIF, VipsForeignLoadNsgif))
#define VIPS_FOREIGN_LOAD_GIF_CLASS(klass) \
	(G_TYPE_CHECK_CLASS_CAST((klass), \
		VIPS_TYPE_FOREIGN_LOAD_GIF, VipsForeignLoadNsgifClass))
#define VIPS_IS_FOREIGN_LOAD_GIF(obj) \
	(G_TYPE_CHECK_INSTANCE_TYPE((obj), VIPS_TYPE_FOREIGN_LOAD_GIF))
#define VIPS_IS_FOREIGN_LOAD_GIF_CLASS(klass) \
	(G_TYPE_CHECK_CLASS_TYPE((klass), VIPS_TYPE_FOREIGN_LOAD_GIF))
#define VIPS_FOREIGN_LOAD_GIF_GET_CLASS(obj) \
	(G_TYPE_INSTANCE_GET_CLASS((obj), \
		VIPS_TYPE_FOREIGN_LOAD_GIF, VipsForeignLoadNsgifClass))

typedef struct _VipsForeignLoadNsgif {
	VipsForeignLoad parent_object;

	/* Load this page (frame number).
	 */
	int page;

	/* Load this many pages.
	 */
	int n;

	/* Load from this source (set by subclasses).
	 */
	VipsSource *source;

	/* The animation created by libnsgif.
	 */
	nsgif_t *anim;

	/* The data/size pair we pass to libnsgif.
	 */
	unsigned char *data;
	size_t size;

	/* Information about the current GIF.
	 */
	const nsgif_info_t *info;

	/* Delays between frames (in milliseconds). Array of length
	 * @info->frame_count.
	 */
	int *delay;

	/* A single centisecond value for compatibility.
	 */
	int gif_delay;

	/* n, ready for libnsgif.
	 */
	int gif_n;

	/* If the GIF contains any frames with transparent elements.
	 */
	gboolean has_transparency;

	/* If the GIF has any interlaced frames.
	 */
	gboolean interlaced;

	/* If the GIF has any local palettes.
	 */
	gboolean local_palette;

	/* The current frame bitmap and the frame number for it.
	 */
	nsgif_bitmap_t *bitmap;
	int frame_number;

} VipsForeignLoadNsgif;

typedef VipsForeignLoadClass VipsForeignLoadNsgifClass;

G_DEFINE_ABSTRACT_TYPE(VipsForeignLoadNsgif, vips_foreign_load_nsgif,
	VIPS_TYPE_FOREIGN_LOAD);

static void
vips_foreign_load_nsgif_error(VipsForeignLoadNsgif *gif, nsgif_error result)
{
	VipsObjectClass *class = VIPS_OBJECT_GET_CLASS(gif);

	vips_error(class->nickname, "%s", nsgif_strerror(result));
}

static void
vips_foreign_load_nsgif_dispose(GObject *gobject)
{
	VipsForeignLoadNsgif *gif = (VipsForeignLoadNsgif *) gobject;

	VIPS_DEBUG_MSG("vips_foreign_load_nsgif_dispose:\n");

	VIPS_FREEF(nsgif_destroy, gif->anim);
	VIPS_UNREF(gif->source);
	VIPS_FREE(gif->delay);

	G_OBJECT_CLASS(vips_foreign_load_nsgif_parent_class)->dispose(gobject);
}

static VipsForeignFlags
vips_foreign_load_nsgif_get_flags_filename(const char *filename)
{
	return VIPS_FOREIGN_SEQUENTIAL;
}

static VipsForeignFlags
vips_foreign_load_nsgif_get_flags(VipsForeignLoad *load)
{
	return VIPS_FOREIGN_SEQUENTIAL;
}

static gboolean
vips_foreign_load_nsgif_is_a_source(VipsSource *source)
{
	const unsigned char *data;

	if ((data = vips_source_sniff(source, 4)) &&
		data[0] == 'G' &&
		data[1] == 'I' &&
		data[2] == 'F' &&
		data[3] == '8')
		return TRUE;

	return FALSE;
}

#ifdef VERBOSE

static void
print_frame(const nsgif_frame_info_t *frame_info)
{
	if (frame_info == NULL)
		return;

	printf("frame_info:\n");
	printf("  display = %d\n", frame_info->display);
	printf("  local_palette = %d\n", frame_info->local_palette);
	printf("  transparency = %d\n", frame_info->transparency);
	printf("  interlaced = %d\n", frame_info->interlaced);
	printf("  disposal = %d (%s)\n",
		frame_info->disposal,
		nsgif_str_disposal(frame_info->disposal));
	printf("  delay = %d\n", frame_info->delay);
	printf("  rect.x0 = %u\n", frame_info->rect.x0);
	printf("  rect.y0 = %u\n", frame_info->rect.y0);
	printf("  rect.x1 = %u\n", frame_info->rect.x1);
	printf("  rect.y1 = %u\n", frame_info->rect.y1);
}

static void
print_animation(nsgif_t *anim, const nsgif_info_t *info)
{
	int i;
	const uint8_t *bg = (uint8_t *) &info->background;

	printf("animation:\n");
	printf("  width = %d\n", info->width);
	printf("  height = %d\n", info->height);
	printf("  frame_count = %d\n", info->frame_count);
	printf("  global_palette = %d\n", info->global_palette);
	printf("  loop_max = %d\n", info->loop_max);
	printf("  background = %d %d %d %d\n",
		bg[0], bg[1], bg[2], bg[3]);

	for (i = 0; i < info->frame_count; i++) {
		printf("%d ", i);
		print_frame(nsgif_get_frame_info(anim, i));
	}
}

#endif /*VERBOSE*/

static int
vips_foreign_load_nsgif_set_header(VipsForeignLoadNsgif *gif,
	VipsImage *image)
{
	double array[3];
	const uint8_t *bg;
	size_t entries;
	uint32_t table[NSGIF_MAX_COLOURS];
	int colours;

	VIPS_DEBUG_MSG("vips_foreign_load_nsgif_set_header:\n");

	vips_image_init_fields(image,
		gif->info->width, gif->info->height * gif->gif_n,
		gif->has_transparency ? 4 : 3,
		VIPS_FORMAT_UCHAR, VIPS_CODING_NONE,
		VIPS_INTERPRETATION_sRGB, 1.0, 1.0);
	vips_image_pipelinev(image, VIPS_DEMAND_STYLE_FATSTRIP, NULL);

	/* Only set page-height if we have more than one page, or this could
	 * accidentally turn into an animated image later.
	 */
	if (gif->gif_n > 1)
		vips_image_set_int(image, VIPS_META_PAGE_HEIGHT, gif->info->height);
	vips_image_set_int(image, VIPS_META_N_PAGES,
		gif->info->frame_count);
	vips_image_set_int(image, "loop", gif->info->loop_max);

	vips_image_set_array_int(image, "delay",
		gif->delay, gif->info->frame_count);

	bg = (uint8_t *) &gif->info->background;
	array[0] = bg[0];
	array[1] = bg[1];
	array[2] = bg[2];

	vips_image_set_array_double(image, "background", array, 3);

	VIPS_SETSTR(image->filename,
		vips_connection_filename(VIPS_CONNECTION(gif->source)));

	/* DEPRECATED "gif-loop"
	 *
	 * Not the correct behavior as loop=1 became gif-loop=0
	 * but we want to keep the old behavior untouched!
	 */
	vips_image_set_int(image, "gif-loop",
		gif->info->loop_max == 0
			? 0
			: gif->info->loop_max - 1);

	/* The deprecated gif-delay field is in centiseconds.
	 */
	vips_image_set_int(image, "gif-delay", gif->gif_delay);

	/* If there are no local palettes, we can attach the global palette as
	 * metadata.
	 */
	if (!gif->local_palette) {
		nsgif_global_palette(gif->anim, table, &entries);
		vips_image_set_array_int(image, "gif-palette",
			(const int *) table, entries);

		colours = entries;
	}
	else {
		int i;

		colours = 0;

		if (gif->info->global_palette) {
			nsgif_global_palette(gif->anim, table, &entries);
			colours = entries;
		}

		for (i = 0; i < gif->info->frame_count; i++) {
			if (nsgif_local_palette(gif->anim, i, table,
					&entries))
				colours = VIPS_MAX(colours, entries);
		}
	}

	vips_image_set_int(image, VIPS_META_BITS_PER_SAMPLE,
		ceil(log2(colours)));

	/* Deprecated "palette-bit-depth" use "bits-per-sample" instead.
	 */
	vips_image_set_int(image, "palette-bit-depth",
		ceil(log2(colours)));

	vips_image_set_int(image, VIPS_META_PALETTE, 1);

	/* Let our caller know if the GIF is interlaced.
	 */
	if (gif->interlaced)
		vips_image_set_int(image, "interlaced", 1);

	return 0;
}

/* Scan the GIF as quickly as we can and extract transparency, bands, pages,
 * etc.
 *
 * Don't flag any errors unless we have to: we want to work for corrupt or
 * malformed GIFs.
 */
static int
vips_foreign_load_nsgif_header(VipsForeignLoad *load)
{
	VipsObjectClass *class = VIPS_OBJECT_GET_CLASS(load);
	VipsForeignLoadNsgif *gif = (VipsForeignLoadNsgif *) load;

	const void *data;
	size_t size;
	nsgif_error result;
	int i;

	VIPS_DEBUG_MSG("vips_foreign_load_nsgif_header:\n");

	/* Map the whole source into memory.
	 */
	if (!(data = vips_source_map(gif->source, &size)))
		return -1;

	/* Treat errors from _scan() as warnings. If libnsgif really can't do
	 * something it'll fail gracefully later when we try to read out
	 * frame data.
	 */
	result = nsgif_data_scan(gif->anim, size, (void *) data);
	VIPS_DEBUG_MSG("nsgif_data_scan() = %s\n", nsgif_strerror(result));
	switch (result) {
	case NSGIF_ERR_END_OF_DATA:
		if (load->fail_on >= VIPS_FAIL_ON_TRUNCATED) {
			vips_foreign_load_nsgif_error(gif, result);
			return -1;
		}
		else
			g_warning("%s", nsgif_strerror(result));
		break;

	case NSGIF_OK:
		break;

	default:
		if (load->fail_on >= VIPS_FAIL_ON_WARNING) {
			vips_foreign_load_nsgif_error(gif, result);
			return -1;
		}
		else
			g_warning("%s", nsgif_strerror(result));
		break;
	}

	/* Tell libnsgif that that's all the data we have. This will let us
	 * read out any truncated final frames.
	 */
	nsgif_data_complete(gif->anim);

	gif->info = nsgif_get_info(gif->anim);
#ifdef VERBOSE
	print_animation(gif->anim, gif->info);
#endif /*VERBOSE*/
	if (!gif->info->frame_count) {
		vips_error(class->nickname, "%s", _("no frames in GIF"));
		return -1;
	}

	/* Update our global struct based on the information in the
	 * individual frames.
	 */
	for (i = 0; i < gif->info->frame_count; i++) {
		const nsgif_frame_info_t *frame_info;

		if ((frame_info = nsgif_get_frame_info(gif->anim, i))) {
			if (frame_info->transparency)
				gif->has_transparency = TRUE;
			if (frame_info->interlaced)
				gif->interlaced = TRUE;
			if (frame_info->local_palette)
				gif->local_palette = TRUE;
		}
	}

	if (gif->n == -1)
		gif->gif_n = gif->info->frame_count - gif->page;
	else
		gif->gif_n = gif->n;

	if (gif->page < 0 ||
		gif->gif_n <= 0 ||
		gif->page + gif->gif_n > gif->info->frame_count) {
		vips_error(class->nickname, "%s", _("bad page number"));
		return -1;
	}

	/* In ms, frame_delay in cs.
	 */
	VIPS_FREE(gif->delay);
	if (!(gif->delay = VIPS_ARRAY(NULL,
			  gif->info->frame_count, int)))
		return -1;
	for (i = 0; i < gif->info->frame_count; i++) {
		const nsgif_frame_info_t *frame_info;

		frame_info = nsgif_get_frame_info(gif->anim, i);
		if (frame_info == NULL) {
			vips_error(class->nickname, "%s", _("bad frame"));
			return -1;
		}
		gif->delay[i] = 10 * frame_info->delay;
	}

	gif->gif_delay = gif->delay[0] / 10;

	vips_foreign_load_nsgif_set_header(gif, load->out);

	return 0;
}

static int
vips_foreign_load_nsgif_generate(VipsRegion *out_region,
	void *seq, void *a, void *b, gboolean *stop)
{
	VipsRect *r = &out_region->valid;
	VipsForeignLoadNsgif *gif = (VipsForeignLoadNsgif *) a;

	int y;

#ifdef VERBOSE
	VIPS_DEBUG_MSG("vips_foreign_load_nsgif_generate: "
				   "top = %d, height = %d\n",
		r->top, r->height);
#endif /*VERBOSE*/

	for (y = 0; y < r->height; y++) {
		/* The page for this output line, and the line number in page.
		 */
		int page = (r->top + y) / gif->info->height + gif->page;
		int line = (r->top + y) % gif->info->height;

		nsgif_error result;
		VipsPel *p, *q;

		g_assert(line >= 0 && line < gif->info->height);
		g_assert(page >= 0 && page < gif->info->frame_count);

		if (gif->frame_number != page) {
			result = nsgif_frame_decode(gif->anim,
				page, &gif->bitmap);
			VIPS_DEBUG_MSG("  nsgif_frame_decode(%d) = %d\n",
				page, result);
			if (result != NSGIF_OK) {
				vips_foreign_load_nsgif_error(gif, result);
				return -1;
			}

#ifdef VERBOSE
			print_frame(nsgif_get_frame_info(gif->anim, page));
#endif /*VERBOSE*/

			gif->frame_number = page;
		}

		p = (VipsPel *) gif->bitmap + (size_t) line * gif->info->width * sizeof(int);
		q = VIPS_REGION_ADDR(out_region, 0, r->top + y);
		if (gif->has_transparency)
			memcpy(q, p, VIPS_REGION_SIZEOF_LINE(out_region));
		else {
			int i;

			for (i = 0; i < r->width; i++) {
				q[0] = p[0];
				q[1] = p[1];
				q[2] = p[2];

				q += 3;
				p += 4;
			}
		}
	}

	return 0;
}

int
vips_foreign_load_nsgif_tile_height(VipsForeignLoadNsgif *gif)
{
	int height = gif->info->height;

	int i;

	/* First, check the perfect size.
	 */
	if (height % 16 == 0)
		return 16;

	/* Next, check larger and smaller sizes.
	 */
	for (i = 1; i < 16; i++) {
		if (height % (16 + i) == 0)
			return 16 + i;
		if (height % (16 - i) == 0)
			return 16 - i;
	}

	return 1;
}

static int
vips_foreign_load_nsgif_load(VipsForeignLoad *load)
{
	VipsForeignLoadNsgif *gif = (VipsForeignLoadNsgif *) load;
	VipsImage **t = (VipsImage **)
		vips_object_local_array(VIPS_OBJECT(load), 2);

	VIPS_DEBUG_MSG("vips_foreign_load_nsgif_load:\n");

	/* Make the output pipeline.
	 */
	t[0] = vips_image_new();
	if (vips_foreign_load_nsgif_set_header(gif, t[0]))
		return -1;

	/* Strips 8 pixels high to avoid too many tiny regions.
	 */
	if (vips_image_generate(t[0],
			NULL, vips_foreign_load_nsgif_generate, NULL, gif, NULL) ||
		vips_sequential(t[0], &t[1],
			"tile_height",
			vips_foreign_load_nsgif_tile_height(gif),
			NULL) ||
		vips_image_write(t[1], load->real))
		return -1;

	return 0;
}

static void
vips_foreign_load_nsgif_class_init(VipsForeignLoadNsgifClass *class)
{
	GObjectClass *gobject_class = G_OBJECT_CLASS(class);
	VipsObjectClass *object_class = (VipsObjectClass *) class;
	VipsForeignClass *foreign_class = (VipsForeignClass *) class;
	VipsForeignLoadClass *load_class = (VipsForeignLoadClass *) class;

	gobject_class->dispose = vips_foreign_load_nsgif_dispose;
	gobject_class->set_property = vips_object_set_property;
	gobject_class->get_property = vips_object_get_property;

	object_class->nickname = "gifload_base";
	object_class->description = _("load GIF with libnsgif");

	/* High priority, so that we handle vipsheader etc.
	 */
	foreign_class->priority = 50;

	load_class->get_flags_filename =
		vips_foreign_load_nsgif_get_flags_filename;
	load_class->get_flags = vips_foreign_load_nsgif_get_flags;
	load_class->header = vips_foreign_load_nsgif_header;
	load_class->load = vips_foreign_load_nsgif_load;

	VIPS_ARG_INT(class, "page", 10,
		_("Page"),
		_("First page to load"),
		VIPS_ARGUMENT_OPTIONAL_INPUT,
		G_STRUCT_OFFSET(VipsForeignLoadNsgif, page),
		0, 100000, 0);

	VIPS_ARG_INT(class, "n", 6,
		_("n"),
		_("Number of pages to load, -1 for all"),
		VIPS_ARGUMENT_OPTIONAL_INPUT,
		G_STRUCT_OFFSET(VipsForeignLoadNsgif, n),
		-1, 100000, 1);
}

static void *
vips_foreign_load_nsgif_bitmap_create(int width, int height)
{
	/* GIF has a limit of 64k per axis -- double-check this.
	 *
	 * gifsave also enforces a pixel count limit, apply the same
	 * constraint here.
	 */
	if (width <= 0 ||
		width > 65535 ||
		height <= 0 ||
		height > 65535 ||
		(guint64) width * height > INT_MAX / 4) {
		vips_error("gifload",
			"%s", _("bad image dimensions"));
		return NULL;
	}

	return g_malloc0((gsize) width * height * 4);
}

static unsigned char *
vips_foreign_load_nsgif_bitmap_get_buffer(void *bitmap)
{
	g_assert(bitmap);

	return bitmap;
}

static void
vips_foreign_load_nsgif_bitmap_destroy(void *bitmap)
{
	g_assert(bitmap);
	g_free(bitmap);
}

static nsgif_bitmap_cb_vt vips_foreign_load_nsgif_bitmap_callbacks = {
	vips_foreign_load_nsgif_bitmap_create,
	vips_foreign_load_nsgif_bitmap_destroy,
	vips_foreign_load_nsgif_bitmap_get_buffer,
};

static void
vips_foreign_load_nsgif_init(VipsForeignLoadNsgif *gif)
{
	nsgif_error result = nsgif_create(
		&vips_foreign_load_nsgif_bitmap_callbacks,
		NSGIF_BITMAP_FMT_R8G8B8A8,
		&gif->anim);
	if (result != NSGIF_OK) {
		VipsObjectClass *class = VIPS_OBJECT_GET_CLASS(gif);

		vips_error(class->nickname, "%s",
			nsgif_strerror(result));
		return;
	}

	gif->n = 1;
	gif->gif_n = 1;
	gif->frame_number = -1;
	gif->bitmap = NULL;
}

typedef struct _VipsForeignLoadNsgifFile {
	VipsForeignLoadNsgif parent_object;

	/* Filename for load.
	 */
	char *filename;

} VipsForeignLoadNsgifFile;

typedef VipsForeignLoadNsgifClass VipsForeignLoadNsgifFileClass;

G_DEFINE_TYPE(VipsForeignLoadNsgifFile, vips_foreign_load_nsgif_file,
	vips_foreign_load_nsgif_get_type());

static int
vips_foreign_load_gif_file_build(VipsObject *object)
{
	VipsForeignLoadNsgif *gif = (VipsForeignLoadNsgif *) object;
	VipsForeignLoadNsgifFile *file = (VipsForeignLoadNsgifFile *) object;

	if (file->filename &&
		!(gif->source = vips_source_new_from_file(file->filename)))
		return -1;

	return VIPS_OBJECT_CLASS(vips_foreign_load_nsgif_file_parent_class)
		->build(object);
}

static const char *vips_foreign_nsgif_suffs[] = {
	".gif",
	NULL
};

static gboolean
vips_foreign_load_nsgif_file_is_a(const char *filename)
{
	VipsSource *source;
	gboolean result;

	if (!(source = vips_source_new_from_file(filename)))
		return FALSE;
	result = vips_foreign_load_nsgif_is_a_source(source);
	VIPS_UNREF(source);

	return result;
}

static void
vips_foreign_load_nsgif_file_class_init(
	VipsForeignLoadNsgifFileClass *class)
{
	GObjectClass *gobject_class = G_OBJECT_CLASS(class);
	VipsObjectClass *object_class = (VipsObjectClass *) class;
	VipsForeignClass *foreign_class = (VipsForeignClass *) class;
	VipsForeignLoadClass *load_class = (VipsForeignLoadClass *) class;

	gobject_class->set_property = vips_object_set_property;
	gobject_class->get_property = vips_object_get_property;

	object_class->nickname = "gifload";
	object_class->description = _("load GIF with libnsgif");
	object_class->build = vips_foreign_load_gif_file_build;

	foreign_class->suffs = vips_foreign_nsgif_suffs;

	load_class->is_a = vips_foreign_load_nsgif_file_is_a;

	VIPS_ARG_STRING(class, "filename", 1,
		_("Filename"),
		_("Filename to load from"),
		VIPS_ARGUMENT_REQUIRED_INPUT,
		G_STRUCT_OFFSET(VipsForeignLoadNsgifFile, filename),
		NULL);
}

static void
vips_foreign_load_nsgif_file_init(VipsForeignLoadNsgifFile *file)
{
}

typedef struct _VipsForeignLoadNsgifBuffer {
	VipsForeignLoadNsgif parent_object;

	/* Load from a buffer.
	 */
	VipsArea *blob;

} VipsForeignLoadNsgifBuffer;

typedef VipsForeignLoadNsgifClass VipsForeignLoadNsgifBufferClass;

G_DEFINE_TYPE(VipsForeignLoadNsgifBuffer, vips_foreign_load_nsgif_buffer,
	vips_foreign_load_nsgif_get_type());

static int
vips_foreign_load_nsgif_buffer_build(VipsObject *object)
{
	VipsForeignLoadNsgif *gif = (VipsForeignLoadNsgif *) object;
	VipsForeignLoadNsgifBuffer *buffer =
		(VipsForeignLoadNsgifBuffer *) object;

	if (buffer->blob &&
		!(gif->source = vips_source_new_from_memory(
			  buffer->blob->data,
			  buffer->blob->length)))
		return -1;

	return VIPS_OBJECT_CLASS(vips_foreign_load_nsgif_buffer_parent_class)
		->build(object);
}

static gboolean
vips_foreign_load_nsgif_buffer_is_a_buffer(const void *buf, size_t len)
{
	VipsSource *source;
	gboolean result;

	if (!(source = vips_source_new_from_memory(buf, len)))
		return FALSE;
	result = vips_foreign_load_nsgif_is_a_source(source);
	VIPS_UNREF(source);

	return result;
}

static void
vips_foreign_load_nsgif_buffer_class_init(
	VipsForeignLoadNsgifBufferClass *class)
{
	GObjectClass *gobject_class = G_OBJECT_CLASS(class);
	VipsObjectClass *object_class = (VipsObjectClass *) class;
	VipsForeignLoadClass *load_class = (VipsForeignLoadClass *) class;

	gobject_class->set_property = vips_object_set_property;
	gobject_class->get_property = vips_object_get_property;

	object_class->nickname = "gifload_buffer";
	object_class->description = _("load GIF with libnsgif");
	object_class->build = vips_foreign_load_nsgif_buffer_build;

	load_class->is_a_buffer = vips_foreign_load_nsgif_buffer_is_a_buffer;

	VIPS_ARG_BOXED(class, "buffer", 1,
		_("Buffer"),
		_("Buffer to load from"),
		VIPS_ARGUMENT_REQUIRED_INPUT,
		G_STRUCT_OFFSET(VipsForeignLoadNsgifBuffer, blob),
		VIPS_TYPE_BLOB);
}

static void
vips_foreign_load_nsgif_buffer_init(VipsForeignLoadNsgifBuffer *buffer)
{
}

typedef struct _VipsForeignLoadNsgifSource {
	VipsForeignLoadNsgif parent_object;

	/* Load from a source.
	 */
	VipsSource *source;

} VipsForeignLoadNsgifSource;

typedef VipsForeignLoadClass VipsForeignLoadNsgifSourceClass;

G_DEFINE_TYPE(VipsForeignLoadNsgifSource, vips_foreign_load_nsgif_source,
	vips_foreign_load_nsgif_get_type());

static int
vips_foreign_load_nsgif_source_build(VipsObject *object)
{
	VipsForeignLoadNsgif *gif = (VipsForeignLoadNsgif *) object;
	VipsForeignLoadNsgifSource *source =
		(VipsForeignLoadNsgifSource *) object;

	if (source->source) {
		gif->source = source->source;
		g_object_ref(gif->source);
	}

	return VIPS_OBJECT_CLASS(vips_foreign_load_nsgif_source_parent_class)
		->build(object);
}

static void
vips_foreign_load_nsgif_source_class_init(
	VipsForeignLoadNsgifSourceClass *class)
{
	GObjectClass *gobject_class = G_OBJECT_CLASS(class);
	VipsObjectClass *object_class = (VipsObjectClass *) class;
	VipsOperationClass *operation_class = VIPS_OPERATION_CLASS(class);
	VipsForeignLoadClass *load_class = (VipsForeignLoadClass *) class;

	gobject_class->set_property = vips_object_set_property;
	gobject_class->get_property = vips_object_get_property;

	object_class->nickname = "gifload_source";
	object_class->description = _("load gif from source");
	object_class->build = vips_foreign_load_nsgif_source_build;

	operation_class->flags |= VIPS_OPERATION_NOCACHE;

	load_class->is_a_source = vips_foreign_load_nsgif_is_a_source;

	VIPS_ARG_OBJECT(class, "source", 1,
		_("Source"),
		_("Source to load from"),
		VIPS_ARGUMENT_REQUIRED_INPUT,
		G_STRUCT_OFFSET(VipsForeignLoadNsgifSource, source),
		VIPS_TYPE_SOURCE);
}

static void
vips_foreign_load_nsgif_source_init(VipsForeignLoadNsgifSource *source)
{
}

#endif /*HAVE_NSGIF*/

/**
 * vips_gifload:
 * @filename: file to load
 * @out: (out): output image
 * @...: `NULL`-terminated list of optional named arguments
 *
 * Read a GIF file into a libvips image.
 *
 * Use @page to select a page to render, numbering from zero.
 *
 * Use @n to select the number of pages to render. The default is 1. Pages are
 * rendered in a vertical column. Set to -1 to mean "until the end of the
 * document". Use [method@Image.grid] to change page layout.
 *
 * Use @fail_on to set the type of error that will cause load to fail. By
 * default, loaders are permissive, that is, [enum@Vips.FailOn.NONE].
 *
 * The output image is RGBA for GIFs containing transparent elements, RGB
 * otherwise.
 *
 * ::: tip "Optional arguments"
 *     * @page: `gint`, page (frame) to read
 *     * @n: `gint`, load this many pages
 *     * @fail_on: [enum@FailOn], types of read error to fail on
 *
 * ::: seealso
 *     [ctor@Image.new_from_file].
 *
 * Returns: 0 on success, -1 on error.
 */
int
vips_gifload(const char *filename, VipsImage **out, ...)
{
	va_list ap;
	int result;

	va_start(ap, out);
	result = vips_call_split("gifload", ap, filename, out);
	va_end(ap);

	return result;
}

/**
 * vips_gifload_buffer:
 * @buf: (array length=len) (element-type guint8): memory area to load
 * @len: (type gsize): size of memory area
 * @out: (out): image to write
 * @...: `NULL`-terminated list of optional named arguments
 *
 * Exactly as [ctor@Image.gifload], but read from a memory buffer.
 *
 * You must not free the buffer while @out is active. The
 * [signal@Object::postclose] signal on @out is a good place to free.
 *
 * ::: tip "Optional arguments"
 *     * @page: `gint`, page (frame) to read
 *     * @n: `gint`, load this many pages
 *     * @fail_on: [enum@FailOn], types of read error to fail on
 *
 * ::: seealso
 *     [ctor@Image.gifload].
 *
 * Returns: 0 on success, -1 on error.
 */
int
vips_gifload_buffer(void *buf, size_t len, VipsImage **out, ...)
{
	va_list ap;
	VipsBlob *blob;
	int result;

	/* We don't take a copy of the data or free it.
	 */
	blob = vips_blob_new(NULL, buf, len);

	va_start(ap, out);
	result = vips_call_split("gifload_buffer", ap, blob, out);
	va_end(ap);

	vips_area_unref(VIPS_AREA(blob));

	return result;
}

/**
 * vips_gifload_source:
 * @source: source to load
 * @out: (out): image to write
 * @...: `NULL`-terminated list of optional named arguments
 *
 * Exactly as [ctor@Image.gifload], but read from a source.
 *
 * ::: tip "Optional arguments"
 *     * @page: `gint`, page (frame) to read
 *     * @n: `gint`, load this many pages
 *     * @fail_on: [enum@FailOn], types of read error to fail on
 *
 * ::: seealso
 *     [ctor@Image.gifload].
 *
 * Returns: 0 on success, -1 on error.
 */
int
vips_gifload_source(VipsSource *source, VipsImage **out, ...)
{
	va_list ap;
	int result;

	va_start(ap, out);
	result = vips_call_split("gifload_source", ap, source, out);
	va_end(ap);

	return result;
}
