/* save to spng
 *
 * 2/12/11
 * 	- wrap a class around the spng writer
 * 16/7/12
 * 	- compression should be 0-9, not 1-10
 * 20/6/18 [felixbuenemann]
 * 	- support spng8 palette write with palette, colours, Q, dither
 * 24/6/20
 * 	- add @bitdepth, deprecate @colours
 * 11/11/21
 * 	- use libspng for save
 * 15/7/22 [lovell]
 * 	- default filter to none
 * 17/11/22
 * 	- add exif save
 */

/*

	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 DEBUG_VERBOSE
#define DEBUG
 */

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

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

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

#include "pforeign.h"
#include "quantise.h"

#ifdef HAVE_SPNG

#include <spng.h>

typedef struct _VipsForeignSaveSpng {
	VipsForeignSave parent_object;

	int compression;
	gboolean interlace;
	VipsForeignPngFilter filter;
	gboolean palette;
	int Q;
	double dither;
	int bitdepth;
	int effort;

	/* Set by subclasses.
	 */
	VipsTarget *target;

	/* Write state.
	 */
	spng_ctx *ctx;
	GSList *text_chunks;
	VipsImage *memory;
	size_t sizeof_line;
	VipsPel *line;

	/* Deprecated.
	 */
	int colours;

} VipsForeignSaveSpng;

typedef VipsForeignSaveClass VipsForeignSaveSpngClass;

G_DEFINE_ABSTRACT_TYPE(VipsForeignSaveSpng, vips_foreign_save_spng,
	VIPS_TYPE_FOREIGN_SAVE);

static void
vips_foreign_save_spng_dispose(GObject *gobject)
{
	VipsForeignSaveSpng *spng = (VipsForeignSaveSpng *) gobject;

	GSList *p;

	VIPS_UNREF(spng->target);
	VIPS_UNREF(spng->memory);
	VIPS_FREEF(spng_ctx_free, spng->ctx);

	for (p = spng->text_chunks; p; p = p->next) {
		struct spng_text *text = (struct spng_text *) p->data;

		VIPS_FREE(text->text);
		VIPS_FREE(text);
	}
	VIPS_FREEF(g_slist_free, spng->text_chunks);

	VIPS_FREE(spng->line);

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

static int
vips_foreign_save_spng_text(VipsForeignSaveSpng *spng,
	const char *keyword, const char *value)
{
	struct spng_text *text = VIPS_NEW(NULL, struct spng_text);

	g_strlcpy(text->keyword, keyword, sizeof(text->keyword));
	/* FIXME ... is this right?
	 */
	text->type = SPNG_TEXT;
	text->length = strlen(value);
	text->text = g_strdup(value);

	spng->text_chunks = g_slist_prepend(spng->text_chunks, text);

	return 0;
}

static void *
vips_foreign_save_spng_comment(VipsImage *image,
	const char *field, GValue *value, void *user_data)
{
	VipsForeignSaveSpng *spng = (VipsForeignSaveSpng *) user_data;

	if (vips_isprefix("png-comment-", field)) {
		const char *value;
		int i;
		char key[256];

		if (vips_image_get_string(image, field, &value))
			return image;

		if (strlen(field) > 256 ||
			sscanf(field, "png-comment-%d-%80s", &i, key) != 2) {
			vips_error("vips2png",
				"%s", _("bad png comment key"));
			return image;
		}

		vips_foreign_save_spng_text(spng, key, value);
	}

	return NULL;
}

static int
vips_foreign_save_spng_profile(VipsForeignSaveSpng *spng, VipsImage *in)
{
	VipsForeignSave *save = (VipsForeignSave *) spng;

	struct spng_iccp iccp;

	/* A profile supplied as an argument overrides an embedded
	 * profile.
	 */
	if (save->profile) {
		VipsBlob *blob;

		if (vips_profile_load(save->profile, &blob, NULL))
			return -1;

		if (blob) {
			size_t length;
			const void *data = vips_blob_get(blob, &length);
			char *basename = g_path_get_basename(save->profile);

#ifdef DEBUG
			printf("write_vips: attaching %zd bytes of ICC profile\n", length);
#endif /*DEBUG*/

			g_strlcpy(iccp.profile_name, basename,
				sizeof(iccp.profile_name));
			iccp.profile_len = length;
			iccp.profile = (void *) data;
			spng_set_iccp(spng->ctx, &iccp);

			vips_area_unref((VipsArea *) blob);
			g_free(basename);
		}
	}
	else if (vips_image_get_typeof(in, VIPS_META_ICC_NAME)) {
		const void *data;
		size_t length;

		if (vips_image_get_blob(in, VIPS_META_ICC_NAME, &data, &length))
			return -1;

#ifdef DEBUG
		printf("write_vips: attaching %zd bytes of ICC profile\n",
			length);
#endif /*DEBUG*/

		g_strlcpy(iccp.profile_name, "icc", sizeof(iccp.profile_name));
		iccp.profile_len = length;
		iccp.profile = (void *) data;

		spng_set_iccp(spng->ctx, &iccp);
	}

	return 0;
}

static int
vips_foreign_save_spng_metadata(VipsForeignSaveSpng *spng, VipsImage *in)
{
	uint32_t n_text;
	struct spng_text *text_chunk_array;
	struct spng_exif exif;
	int i;
	GSList *p;

	if (vips_image_get_typeof(in, VIPS_META_XMP_NAME)) {
		const void *data;
		size_t length;
		char *str;

		if (vips_image_get_blob(in, VIPS_META_XMP_NAME, &data, &length))
			return -1;

		/* The blob form of the XMP metadata is missing the
		 * terminating \0 bytes, we have to paste it back,
		 * unfortunately. See pngload.
		 */
		str = g_malloc(length + 1);
		g_strlcpy(str, data, length + 1);
		vips_foreign_save_spng_text(spng, "XML:com.adobe.xmp", str);
		g_free(str);
	}

	if (vips_image_get_typeof(in, VIPS_META_EXIF_NAME)) {
		if (vips_image_get_blob(in, VIPS_META_EXIF_NAME,
				(const void **) &exif.data, &exif.length))
			return -1;

		/* libspng does not want the JFIF "Exif\0\0" prefix.
		 */
		if (exif.length >= 6 &&
			vips_isprefix("Exif", exif.data)) {
			exif.data += 6;
			exif.length -= 6;
		}
		spng_set_exif(spng->ctx, &exif);
	}

	if (vips_image_map(in, vips_foreign_save_spng_comment, spng))
		return -1;

	n_text = g_slist_length(spng->text_chunks);
	text_chunk_array = VIPS_ARRAY(NULL, n_text, struct spng_text);
	for (i = 0, p = spng->text_chunks; p; p = p->next, i++) {
		struct spng_text *text = (struct spng_text *) p->data;

		text_chunk_array[i] = *text;
	}
#ifdef DEBUG
	printf("attaching %u text items\n", n_text);
#endif /*DEBUG*/
	spng_set_text(spng->ctx, text_chunk_array, n_text);
	VIPS_FREE(text_chunk_array);

	return 0;
}

/* Pack a line of 1/2/4 bit index values.
 */
static void
vips_foreign_save_spng_pack(VipsForeignSaveSpng *spng,
	VipsPel *q, VipsPel *p, size_t n)
{
	int pixel_mask = 8 / spng->bitdepth - 1;
	int shift = spng->palette ? 0 : 8 - spng->bitdepth;

	VipsPel bits;
	size_t x;

	bits = 0;
	for (x = 0; x < n; x++) {
		bits <<= spng->bitdepth;
		bits |= p[x] >> shift;

		if ((x & pixel_mask) == pixel_mask)
			*q++ = bits;
	}

	/* Any left-over bits? Need to be left-aligned.
	 */
	if ((x & pixel_mask) != 0) {
		/* The number of bits we've collected and must
		 * left-align and flush.
		 */
		int collected_bits = (x & pixel_mask) << (spng->bitdepth - 1);

		*q++ = bits << (8 - collected_bits);
	}
}

static int
vips_foreign_save_spng_write_fn(spng_ctx *ctx, void *user,
	void *data, size_t n)
{
	VipsForeignSaveSpng *spng = (VipsForeignSaveSpng *) user;

	if (vips_target_write(spng->target, data, n))
		return SPNG_IO_ERROR;

	return 0;
}

static int
vips_foreign_save_spng_write_block(VipsRegion *region, VipsRect *area,
	void *user)
{
	VipsForeignSaveSpng *spng = (VipsForeignSaveSpng *) user;
	VipsObjectClass *class = VIPS_OBJECT_GET_CLASS(spng);

	int y;
	int error;

	/* The area to write is always a set of complete scanlines.
	 */
	g_assert(area->left == 0);
	g_assert(area->width == region->im->Xsize);
	g_assert(area->top + area->height <= region->im->Ysize);

	for (y = 0; y < area->height; y++) {
		VipsPel *line;
		size_t sizeof_line;

		line = VIPS_REGION_ADDR(region, 0, area->top + y);
		sizeof_line = VIPS_REGION_SIZEOF_LINE(region);

		if (spng->bitdepth < 8) {
			vips_foreign_save_spng_pack(spng, spng->line, line, sizeof_line);
			line = spng->line;
			sizeof_line = spng->sizeof_line;
		}

		if ((error = spng_encode_row(spng->ctx, line, sizeof_line)))
			break;
	}

	/* You can get SPNG_EOI for the final scanline.
	 */
	if (error &&
		error != SPNG_EOI) {
		vips_error(class->nickname, "%s", spng_strerror(error));
		return -1;
	}

	return 0;
}

static int
vips_foreign_save_spng_write(VipsForeignSaveSpng *spng, VipsImage *in)
{
	VipsObjectClass *class = VIPS_OBJECT_GET_CLASS(spng);

	int error;
	struct spng_ihdr ihdr;
	struct spng_phys phys;
	int fmt;
	enum spng_encode_flags encode_flags;

	spng->ctx = spng_ctx_new(SPNG_CTX_ENCODER);

	if ((error = spng_set_png_stream(spng->ctx,
			 vips_foreign_save_spng_write_fn, spng))) {
		vips_error(class->nickname, "%s", spng_strerror(error));
		return -1;
	}

	ihdr.width = in->Xsize;
	ihdr.height = in->Ysize;
	ihdr.bit_depth = spng->bitdepth;

	switch (in->Bands) {
	case 1:
		ihdr.color_type = SPNG_COLOR_TYPE_GRAYSCALE;
		break;

	case 2:
		ihdr.color_type = SPNG_COLOR_TYPE_GRAYSCALE_ALPHA;
		break;

	case 3:
		ihdr.color_type = SPNG_COLOR_TYPE_TRUECOLOR;
		break;

	case 4:
		ihdr.color_type = SPNG_COLOR_TYPE_TRUECOLOR_ALPHA;
		break;

	default:
		vips_error(class->nickname, "%s", _("bad bands"));
		return -1;
	}

#ifdef HAVE_QUANTIZATION
	/* Enable image quantisation to paletted 8bpp PNG if palette is set.
	 */
	if (spng->palette)
		ihdr.color_type = SPNG_COLOR_TYPE_INDEXED;
#else
	if (spng->palette)
		g_warning("ignoring palette (no quantisation support)");
#endif /*HAVE_QUANTIZATION*/

	ihdr.compression_method = 0;
	ihdr.filter_method = 0;
	ihdr.interlace_method = spng->interlace ? 1 : 0;
	if ((error = spng_set_ihdr(spng->ctx, &ihdr))) {
		vips_error(class->nickname, "%s", spng_strerror(error));
		return -1;
	}

	spng_set_option(spng->ctx,
		SPNG_IMG_COMPRESSION_LEVEL, spng->compression);
	spng_set_option(spng->ctx,
		SPNG_TEXT_COMPRESSION_LEVEL, spng->compression);
	spng_set_option(spng->ctx,
		SPNG_FILTER_CHOICE, spng->filter);

	/* Set resolution. spng uses pixels per meter.
	 */
	phys.unit_specifier = 1;
	phys.ppu_x = rint(in->Xres * 1000.0);
	phys.ppu_y = rint(in->Xres * 1000.0);
	spng_set_phys(spng->ctx, &phys);

	/* Metadata.
	 */
	if (vips_foreign_save_spng_profile(spng, in) ||
		vips_foreign_save_spng_metadata(spng, in))
		return -1;

#ifdef HAVE_QUANTIZATION
	if (spng->palette) {
		struct spng_plte plte = { 0 };
		struct spng_trns trns = { 0 };

		VipsImage *im_index;
		VipsImage *im_palette;
		int palette_count;
		int i;

		if (vips__quantise_image(in, &im_index, &im_palette,
				1 << spng->bitdepth,
				spng->Q,
				spng->dither,
				spng->effort,
				FALSE))
			return -1;

		/* PNG is 8-bit index only.
		 */
		palette_count = im_palette->Xsize;
		g_assert(palette_count <= 256);

		for (i = 0; i < palette_count; i++) {
			VipsPel *p = (VipsPel *)
				VIPS_IMAGE_ADDR(im_palette, i, 0);
			struct spng_plte_entry *entry =
				&plte.entries[plte.n_entries];

			entry->red = p[0];
			entry->green = p[1];
			entry->blue = p[2];
			plte.n_entries += 1;

			trns.type3_alpha[i] = p[3];
			if (p[3] != 255) {
				trns.n_type3_entries = i + 1;
			}
		}

#ifdef DEBUG
		printf("attaching %d entry palette\n", plte.n_entries);
		if (trns.n_type3_entries)
			printf("attaching %d transparency values\n",
				trns.n_type3_entries);
#endif /*DEBUG*/

		VIPS_UNREF(im_palette);

		spng_set_plte(spng->ctx, &plte);
		if (trns.n_type3_entries)
			spng_set_trns(spng->ctx, &trns);

		in = spng->memory = im_index;
	}
#endif /*HAVE_QUANTIZATION*/

	/* Low-bitdepth write needs an extra buffer for packing pixels.
	 */
	if (spng->bitdepth < 8) {
		spng->sizeof_line =
			1 + VIPS_IMAGE_SIZEOF_LINE(in) / (8 / spng->bitdepth);

		if (!(spng->line =
					vips_malloc(NULL, VIPS_IMAGE_SIZEOF_LINE(in))))
			return -1;
	}

	/* SPNG_FMT_PNG is a special value that matches the format in ihdr
	 */
	fmt = SPNG_FMT_PNG;
	encode_flags = SPNG_ENCODE_PROGRESSIVE | SPNG_ENCODE_FINALIZE;
	if ((error = spng_encode_image(spng->ctx,
			 NULL, -1, fmt, encode_flags))) {
		vips_error(class->nickname, "%s", spng_strerror(error));
		return -1;
	}

	if (spng->interlace) {
		/* Force the input into memory, if it's not there already.
		 */
		if (!spng->memory) {
			if (!(spng->memory = vips_image_copy_memory(in)))
				return -1;
			in = spng->memory;
		}

		do {
			struct spng_row_info row_info;
			VipsPel *line;
			size_t sizeof_line;

			if ((error =
						spng_get_row_info(spng->ctx, &row_info)))
				break;

			line = VIPS_IMAGE_ADDR(in, 0, row_info.row_num);
			sizeof_line = VIPS_IMAGE_SIZEOF_LINE(in);

			if (spng->bitdepth < 8) {
				vips_foreign_save_spng_pack(spng,
					spng->line, line, sizeof_line);
				line = spng->line;
				sizeof_line = spng->sizeof_line;
			}

			error = spng_encode_row(spng->ctx, line, sizeof_line);
		} while (!error);

		if (error != SPNG_EOI) {
			vips_error(class->nickname,
				"%s", spng_strerror(error));
			return -1;
		}
	}
	else {
		if (vips_sink_disc(in, vips_foreign_save_spng_write_block, spng))
			return -1;
	}

	if (vips_target_end(spng->target))
		return -1;

	return 0;
}

static int
vips_foreign_save_spng_build(VipsObject *object)
{
	VipsForeignSave *save = (VipsForeignSave *) object;
	VipsForeignSaveSpng *spng = (VipsForeignSaveSpng *) object;

	VipsImage *in;
	VipsImage *x;

	if (VIPS_OBJECT_CLASS(vips_foreign_save_spng_parent_class)->build(object))
		return -1;

	in = save->ready;
	g_object_ref(in);

	/* If no output bitdepth has been specified, use input Type to pick.
	 */
	if (!vips_object_argument_isset(object, "bitdepth"))
		spng->bitdepth =
			in->Type == VIPS_INTERPRETATION_RGB16 ||
				in->Type == VIPS_INTERPRETATION_GREY16
			? 16
			: 8;

	/* Deprecated "colours" arg just sets bitdepth large enough to hold
	 * that many colours.
	 */
	if (vips_object_argument_isset(object, "colours"))
		spng->bitdepth = ceil(log2(spng->colours));

	/* The bitdepth param can change the interpretation.
	 */
	VipsInterpretation interpretation;
	if (in->Bands > 2) {
	   if (spng->bitdepth > 8)
		   interpretation = VIPS_INTERPRETATION_RGB16;
	   else
		   interpretation = VIPS_INTERPRETATION_sRGB;
	}
	else {
	   if (spng->bitdepth > 8)
		   interpretation = VIPS_INTERPRETATION_GREY16;
	   else
		   interpretation = VIPS_INTERPRETATION_B_W;
	}
	if (vips_colourspace(in, &x, interpretation, NULL)) {
		g_object_unref(in);
		return -1;
	}
	g_object_unref(in);
	in = x;

	/* If this is a RGB or RGBA image and a low bit depth has been
	 * requested, enable palettisation.
	 */
	if (in->Bands > 2 &&
		spng->bitdepth < 8)
		spng->palette = TRUE;

	/* Disable palettization for >8 bit save.
	 */
	if (spng->bitdepth > 8)
		spng->palette = FALSE;

	if (vips_foreign_save_spng_write(spng, in)) {
		g_object_unref(in);
		return -1;
	}

	g_object_unref(in);

	return 0;
}

#define UC VIPS_FORMAT_UCHAR
#define US VIPS_FORMAT_USHORT

/* Except for 8-bit inputs, we send everything else to 16. We decide on png8
 * vs. png16 based on Type in_build(), see above.
 */
static VipsBandFormat bandfmt_spng[10] = {
	/* Band format:  UC  C   US  S   UI  I   F   X   D   DX */
	/* Promotion: */ UC, UC, US, US, US, US, US, US, US, US
};

static void
vips_foreign_save_spng_class_init(VipsForeignSaveSpngClass *class)
{
	GObjectClass *gobject_class = G_OBJECT_CLASS(class);
	VipsObjectClass *object_class = (VipsObjectClass *) class;
	VipsForeignClass *foreign_class = (VipsForeignClass *) class;
	VipsForeignSaveClass *save_class = (VipsForeignSaveClass *) class;

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

	object_class->nickname = "spngsave_base";
	object_class->description = _("save spng");
	object_class->build = vips_foreign_save_spng_build;

	foreign_class->suffs = vips__png_suffs;

	save_class->saveable =
		VIPS_FOREIGN_SAVEABLE_MONO |
		VIPS_FOREIGN_SAVEABLE_RGB |
		VIPS_FOREIGN_SAVEABLE_ALPHA;
	save_class->format_table = bandfmt_spng;

	VIPS_ARG_INT(class, "compression", 6,
		_("Compression"),
		_("Compression factor"),
		VIPS_ARGUMENT_OPTIONAL_INPUT,
		G_STRUCT_OFFSET(VipsForeignSaveSpng, compression),
		0, 9, 6);

	VIPS_ARG_BOOL(class, "interlace", 7,
		_("Interlace"),
		_("Interlace image"),
		VIPS_ARGUMENT_OPTIONAL_INPUT,
		G_STRUCT_OFFSET(VipsForeignSaveSpng, interlace),
		FALSE);

	VIPS_ARG_FLAGS(class, "filter", 12,
		_("Filter"),
		_("libspng row filter flag(s)"),
		VIPS_ARGUMENT_OPTIONAL_INPUT,
		G_STRUCT_OFFSET(VipsForeignSaveSpng, filter),
		VIPS_TYPE_FOREIGN_PNG_FILTER,
		VIPS_FOREIGN_PNG_FILTER_NONE);

	VIPS_ARG_BOOL(class, "palette", 13,
		_("Palette"),
		_("Quantise to 8bpp palette"),
		VIPS_ARGUMENT_OPTIONAL_INPUT,
		G_STRUCT_OFFSET(VipsForeignSaveSpng, palette),
		FALSE);

	VIPS_ARG_INT(class, "Q", 15,
		_("Quality"),
		_("Quantisation quality"),
		VIPS_ARGUMENT_OPTIONAL_INPUT,
		G_STRUCT_OFFSET(VipsForeignSaveSpng, Q),
		0, 100, 100);

	VIPS_ARG_DOUBLE(class, "dither", 16,
		_("Dithering"),
		_("Amount of dithering"),
		VIPS_ARGUMENT_OPTIONAL_INPUT,
		G_STRUCT_OFFSET(VipsForeignSaveSpng, dither),
		0.0, 1.0, 1.0);

	VIPS_ARG_INT(class, "bitdepth", 17,
		_("Bit depth"),
		_("Write as a 1, 2, 4, 8 or 16 bit image"),
		VIPS_ARGUMENT_OPTIONAL_INPUT,
		G_STRUCT_OFFSET(VipsForeignSaveSpng, bitdepth),
		1, 16, 8);

	VIPS_ARG_INT(class, "effort", 18,
		_("Effort"),
		_("Quantisation CPU effort"),
		VIPS_ARGUMENT_OPTIONAL_INPUT,
		G_STRUCT_OFFSET(VipsForeignSaveSpng, effort),
		1, 10, 7);

	VIPS_ARG_INT(class, "colours", 14,
		_("Colours"),
		_("Max number of palette colours"),
		VIPS_ARGUMENT_OPTIONAL_INPUT | VIPS_ARGUMENT_DEPRECATED,
		G_STRUCT_OFFSET(VipsForeignSaveSpng, colours),
		2, 256, 256);
}

static void
vips_foreign_save_spng_init(VipsForeignSaveSpng *spng)
{
	spng->compression = 6;
	spng->filter = VIPS_FOREIGN_PNG_FILTER_NONE;
	spng->Q = 100;
	spng->dither = 1.0;
	spng->effort = 7;
}

typedef struct _VipsForeignSaveSpngTarget {
	VipsForeignSaveSpng parent_object;

	VipsTarget *target;
} VipsForeignSaveSpngTarget;

typedef VipsForeignSaveSpngClass VipsForeignSaveSpngTargetClass;

G_DEFINE_TYPE(VipsForeignSaveSpngTarget, vips_foreign_save_spng_target,
	vips_foreign_save_spng_get_type());

static int
vips_foreign_save_spng_target_build(VipsObject *object)
{
	VipsForeignSaveSpng *spng = (VipsForeignSaveSpng *) object;
	VipsForeignSaveSpngTarget *target =
		(VipsForeignSaveSpngTarget *) object;

	if (target->target) {
		spng->target = target->target;
		g_object_ref(spng->target);
	}

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

static void
vips_foreign_save_spng_target_class_init(VipsForeignSaveSpngTargetClass *class)
{
	GObjectClass *gobject_class = G_OBJECT_CLASS(class);
	VipsObjectClass *object_class = (VipsObjectClass *) class;

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

	object_class->nickname = "pngsave_target";
	object_class->description = _("save image to target as PNG");
	object_class->build = vips_foreign_save_spng_target_build;

	VIPS_ARG_OBJECT(class, "target", 1,
		_("Target"),
		_("Target to save to"),
		VIPS_ARGUMENT_REQUIRED_INPUT,
		G_STRUCT_OFFSET(VipsForeignSaveSpngTarget, target),
		VIPS_TYPE_TARGET);
}

static void
vips_foreign_save_spng_target_init(VipsForeignSaveSpngTarget *target)
{
}

typedef struct _VipsForeignSaveSpngFile {
	VipsForeignSaveSpng parent_object;

	char *filename;
} VipsForeignSaveSpngFile;

typedef VipsForeignSaveSpngClass VipsForeignSaveSpngFileClass;

G_DEFINE_TYPE(VipsForeignSaveSpngFile, vips_foreign_save_spng_file,
	vips_foreign_save_spng_get_type());

static int
vips_foreign_save_spng_file_build(VipsObject *object)
{
	VipsForeignSaveSpng *spng = (VipsForeignSaveSpng *) object;
	VipsForeignSaveSpngFile *file = (VipsForeignSaveSpngFile *) object;

	if (file->filename &&
		!(spng->target = vips_target_new_to_file(file->filename)))
		return -1;

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

static void
vips_foreign_save_spng_file_class_init(VipsForeignSaveSpngFileClass *class)
{
	GObjectClass *gobject_class = G_OBJECT_CLASS(class);
	VipsObjectClass *object_class = (VipsObjectClass *) class;

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

	object_class->nickname = "pngsave";
	object_class->description = _("save image to file as PNG");
	object_class->build = vips_foreign_save_spng_file_build;

	VIPS_ARG_STRING(class, "filename", 1,
		_("Filename"),
		_("Filename to save to"),
		VIPS_ARGUMENT_REQUIRED_INPUT,
		G_STRUCT_OFFSET(VipsForeignSaveSpngFile, filename),
		NULL);
}

static void
vips_foreign_save_spng_file_init(VipsForeignSaveSpngFile *file)
{
}

typedef struct _VipsForeignSaveSpngBuffer {
	VipsForeignSaveSpng parent_object;

	VipsArea *buf;
} VipsForeignSaveSpngBuffer;

typedef VipsForeignSaveSpngClass VipsForeignSaveSpngBufferClass;

G_DEFINE_TYPE(VipsForeignSaveSpngBuffer, vips_foreign_save_spng_buffer,
	vips_foreign_save_spng_get_type());

static int
vips_foreign_save_spng_buffer_build(VipsObject *object)
{
	VipsForeignSaveSpng *spng = (VipsForeignSaveSpng *) object;
	VipsForeignSaveSpngBuffer *buffer =
		(VipsForeignSaveSpngBuffer *) object;

	VipsBlob *blob;

	if (!(spng->target = vips_target_new_to_memory()))
		return -1;

	if (VIPS_OBJECT_CLASS(vips_foreign_save_spng_buffer_parent_class)
			->build(object))
		return -1;

	g_object_get(spng->target, "blob", &blob, NULL);
	g_object_set(buffer, "buffer", blob, NULL);
	vips_area_unref(VIPS_AREA(blob));

	return 0;
}

static void
vips_foreign_save_spng_buffer_class_init(VipsForeignSaveSpngBufferClass *class)
{
	GObjectClass *gobject_class = G_OBJECT_CLASS(class);
	VipsObjectClass *object_class = (VipsObjectClass *) class;

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

	object_class->nickname = "pngsave_buffer";
	object_class->description = _("save image to buffer as PNG");
	object_class->build = vips_foreign_save_spng_buffer_build;

	VIPS_ARG_BOXED(class, "buffer", 1,
		_("Buffer"),
		_("Buffer to save to"),
		VIPS_ARGUMENT_REQUIRED_OUTPUT,
		G_STRUCT_OFFSET(VipsForeignSaveSpngBuffer, buf),
		VIPS_TYPE_BLOB);
}

static void
vips_foreign_save_spng_buffer_init(VipsForeignSaveSpngBuffer *buffer)
{
}

#endif /*HAVE_SPNG*/
