/* Read a virtual microscope slide using OpenSlide.
 *
 * Benjamin Gilbert
 *
 * Copyright (c) 2011-2015 Carnegie Mellon University
 *
 * 26/11/11
 *	- initial version
 * 27/11/11
 *	- fix black background in transparent areas
 *	- no need to set *stop on fill_region() error return
 *	- add OpenSlide properties to image metadata
 *	- consolidate setup into one function
 *	- support reading arbitrary layers
 *	- use VIPS_ARRAY()
 *	- add helper to copy a line of pixels
 *	- support reading associated images
 * 7/12/11
 *	- redirect OpenSlide error logging to vips_error()
 * 8/12/11
 *	- add more exposition to documentation
 * 9/12/11
 * 	- unpack to a tile cache
 * 11/12/11
 * 	- move argb->rgba into conversion
 * 	- turn into a set of read fns ready to be called from a class
 * 28/2/12
 * 	- convert "layer" to "level" where externally visible
 * 9/4/12
 * 	- move argb2rgba back in here, we don't have a use for coded pixels
 * 	- small cleanups
 * 11/4/12
 * 	- fail if both level and associated image are specified
 * 20/9/12
 *	- update openslide_open error handling for 3.3.0 semantics
 *	- switch from deprecated _layer_ functions
 * 11/10/12
 * 	- look for tile-width and tile-height properties
 * 	- use threaded tile cache
 * 6/8/13
 * 	- always output solid (not transparent) pixels
 * 25/1/14
 * 	- use openslide_detect_vendor() on >= 3.4.0
 * 30/7/14
 * 	- add autocrop toggle
 * 9/8/14
 * 	- do argb -> rgba for associated as well
 * 27/1/15
 * 	- unpremultiplication speedups for fully opaque/transparent pixels
 * 18/1/17
 * 	- reorganise to support invalidate on read error
 * 27/1/18
 * 	- option to attach associated images as metadata
 * 22/6/20 adamu
 * 	- set libvips xres/yres from openslide mpp-x/mpp-y
 * 13/2/21 kleisauke
 * 	- include GObject part from openslideload.c
 * 2/10/22
 *	- add "rgb" option
 * 1/10/23
 *	- add openslide4 icc profile support
 */

/*

	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

 */

/*
#define VIPS_DEBUG
 */

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

#ifdef HAVE_OPENSLIDE

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

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

#include "pforeign.h"

#include <openslide.h>

typedef struct {
	/* Params.
	 */
	char *filename;
	VipsImage *out;
	int32_t level;
	gboolean autocrop;
	char *associated;
	gboolean attach_associated;
	gboolean rgb;

	openslide_t *osr;

	/* Crop to image bounds if @autocrop is set.
	 */
	VipsRect bounds;

	/* Only valid if associated == NULL.
	 */
	double downsample;
	uint32_t bg;

	/* Try to get these from openslide properties.
	 */
	int tile_width;
	int tile_height;
} ReadSlide;

static int
vips__openslide_isslide(const char *filename)
{
#ifdef HAVE_OPENSLIDE_3_4
	const char *vendor;
	int ok;

	vendor = openslide_detect_vendor(filename);

	/* Generic tiled tiff images can be opened by openslide as well.
	 * Only offer to load this file if it's not a generic tiff since
	 * we want vips_tiffload() to handle these.
	 */
	ok = (vendor &&
		strcmp(vendor, "generic-tiff") != 0);

	VIPS_DEBUG_MSG("vips__openslide_isslide: %s - %d\n", filename, ok);

	return ok;
#else
	openslide_t *osr;
	int ok;

	ok = 0;
	osr = openslide_open(filename);

	if (osr) {
		const char *vendor;

		/* Generic tiled tiff images can be opened by openslide as
		 * well. Only offer to load this file if it's not a generic
		 * tiff since we want vips_tiffload() to handle these.
		 */
		vendor = openslide_get_property_value(osr,
			OPENSLIDE_PROPERTY_NAME_VENDOR);

		/* vendor will be NULL if osr is in error state.
		 */
		if (vendor &&
			strcmp(vendor, "generic-tiff") != 0)
			ok = 1;

		openslide_close(osr);
	}

	VIPS_DEBUG_MSG("vips__openslide_isslide: %s - %d\n", filename, ok);

	return ok;
#endif
}

static void
readslide_destroy_cb(VipsImage *image, ReadSlide *rslide)
{
	VIPS_FREEF(openslide_close, rslide->osr);
	VIPS_FREE(rslide->associated);
	VIPS_FREE(rslide->filename);
	VIPS_FREE(rslide);
}

static int
check_associated_image(openslide_t *osr, const char *name)
{
	const char *const *associated;

	for (associated = openslide_get_associated_image_names(osr);
		 *associated != NULL; associated++)
		if (strcmp(*associated, name) == 0)
			return 0;

	vips_error("openslide2vips",
		"%s", _("invalid associated image name"));

	return -1;
}

static gboolean
get_bounds(openslide_t *osr, VipsRect *rect)
{
	static const char *openslide_names[] = {
		"openslide.bounds-x",
		"openslide.bounds-y",
		"openslide.bounds-width",
		"openslide.bounds-height"
	};
	static int vips_offsets[] = {
		G_STRUCT_OFFSET(VipsRect, left),
		G_STRUCT_OFFSET(VipsRect, top),
		G_STRUCT_OFFSET(VipsRect, width),
		G_STRUCT_OFFSET(VipsRect, height)
	};

	const char *value;
	int i;

	for (i = 0; i < 4; i++) {
		if (!(value = openslide_get_property_value(osr,
				  openslide_names[i])))
			return FALSE;
		G_STRUCT_MEMBER(int, rect, vips_offsets[i]) =
			atoi(value);
	}

	return TRUE;
}

static ReadSlide *
readslide_new(const char *filename, VipsImage *out,
	int level, gboolean autocrop,
	const char *associated, gboolean attach_associated, gboolean rgb)
{
	ReadSlide *rslide;

	if (level &&
		associated) {
		vips_error("openslide2vips",
			"%s", _("specify only one of level and associated image"));
		return NULL;
	}

	if (attach_associated &&
		associated) {
		vips_error("openslide2vips", "%s",
			_("specify only one of attach_assicated and associated image"));
		return NULL;
	}

	rslide = VIPS_NEW(NULL, ReadSlide);
	memset(rslide, 0, sizeof(*rslide));
	g_signal_connect(out, "close", G_CALLBACK(readslide_destroy_cb),
		rslide);

	rslide->filename = g_strdup(filename);
	rslide->out = out;
	rslide->level = level;
	rslide->autocrop = autocrop;
	rslide->associated = g_strdup(associated);
	rslide->attach_associated = attach_associated;
	rslide->rgb = rgb;

	/* Non-crazy defaults, override in _parse() if we can.
	 */
	rslide->tile_width = 256;
	rslide->tile_height = 256;

	return rslide;
}

/* Convert from ARGB to RGBA and undo premultiplication.
 *
 * We throw away transparency. Formats like Mirax use transparent + bg
 * colour for areas with no useful pixels. But if we output
 * transparent pixels and then convert to RGB for jpeg write later, we
 * would have to pass the bg colour down the pipe somehow. The
 * structure of dzsave makes this tricky.
 *
 * We could output plain RGB instead, but that would break
 * compatibility with older vipses.
 */
static void
argb2rgba(uint32_t *restrict buf, int64_t n, uint32_t bg)
{
	const uint32_t pbg = GUINT32_TO_BE((bg << 8) | 255);

	int64_t i;

	for (i = 0; i < n; i++) {
		uint32_t *restrict p = buf + i;
		uint32_t x = *p;
		uint8_t a = x >> 24;
		VipsPel *restrict out = (VipsPel *) p;

		if (a == 255)
			*p = GUINT32_TO_BE((x << 8) | 255);
		else if (a == 0)
			/* Use background color.
			 */
			*p = pbg;
		else {
			/* Undo premultiplication.
			 */
			out[0] = 255 * ((x >> 16) & 255) / a;
			out[1] = 255 * ((x >> 8) & 255) / a;
			out[2] = 255 * (x & 255) / a;
			out[3] = 255;
		}
	}
}

/* Convert from ARGB to RGB. In RGB mode, assume a is always 255.
 */
static void
argb2rgb(uint32_t *restrict buf, VipsPel *restrict q, int64_t n)
{
	int64_t i;

	for (i = 0; i < n; i++) {
		uint32_t x = buf[i];

		q[0] = ((x >> 16) & 0xff);
		q[1] = ((x >> 8) & 0xff);
		q[2] = (x & 0xff);
		q += 3;
	}
}

static VipsImage *
vips__openslide_get_associated(ReadSlide *rslide, const char *associated_name)
{
	VipsImage *associated;
	int64_t w, h;
	const char *error;

	associated = vips_image_new_memory();
	openslide_get_associated_image_dimensions(rslide->osr,
		associated_name, &w, &h);

	/* Always 4 bands, since this is the image that gets the ARGB from
	 * cairo.
	 */
	vips_image_init_fields(associated, w, h, 4,
		VIPS_FORMAT_UCHAR,
		VIPS_CODING_NONE, VIPS_INTERPRETATION_sRGB, 1.0, 1.0);

#ifdef HAVE_OPENSLIDE_ICC
	int64_t len;
	if ((len = openslide_get_associated_image_icc_profile_size(rslide->osr,
			associated_name)) > 0) {
		void *data;

		if (!(data = VIPS_MALLOC(NULL, len))) {
			g_object_unref(associated);
			return NULL;
		}
		openslide_read_associated_image_icc_profile(rslide->osr,
				associated_name, data);
		error = openslide_get_error(rslide->osr);
		if (error) {
			g_free(data);
			g_object_unref(associated);
			vips_error("openslide2vips", _( "opening slide: %s" ), error);
			return NULL;
		}

		vips_image_set_blob(associated, VIPS_META_ICC_NAME,
			(VipsCallbackFn) g_free, data, len);
	}
#endif /*HAVE_OPENSLIDE_ICC*/

	if (vips_image_pipelinev(associated,
			VIPS_DEMAND_STYLE_THINSTRIP, NULL) ||
		vips_image_write_prepare(associated)) {
		g_object_unref(associated);
		return NULL;
	}

	openslide_read_associated_image(rslide->osr,
		associated_name,
		(uint32_t *) VIPS_IMAGE_ADDR(associated, 0, 0));
	error = openslide_get_error(rslide->osr);
	if (error) {
		vips_error("openslide2vips",
			_("reading associated image: %s"), error);
		g_object_unref(associated);
		return NULL;
	}

	/* In RGB mode we make a second RGB image and repack to that.
	 */
	if (rslide->rgb) {
		VipsImage *rgb;

		rgb = vips_image_new_memory();
		vips_object_local(rgb, associated);

		vips_image_init_fields(rgb, w, h, 3,
			VIPS_FORMAT_UCHAR,
			VIPS_CODING_NONE, VIPS_INTERPRETATION_sRGB, 1.0, 1.0);
		if (vips_image_pipelinev(rgb,
				VIPS_DEMAND_STYLE_THINSTRIP, NULL) ||
			vips_image_write_prepare(rgb)) {
			g_object_unref(rgb);
			return NULL;
		}

		argb2rgb((uint32_t *) VIPS_IMAGE_ADDR(associated, 0, 0),
			VIPS_IMAGE_ADDR(rgb, 0, 0), w * h);

		associated = rgb;
	}
	else
		/* We can do this in place.
		 */
		argb2rgba((uint32_t *) VIPS_IMAGE_ADDR(associated, 0, 0),
			w * h, rslide->bg);

	return associated;
}

static int
readslide_attach_associated(ReadSlide *rslide, VipsImage *image)
{
	const char *const *associated_name;

	for (associated_name =
			 openslide_get_associated_image_names(rslide->osr);
		 *associated_name != NULL; associated_name++) {
		VipsImage *associated;
		char buf[256];

		if (!(associated = vips__openslide_get_associated(rslide,
				  *associated_name)))
			return -1;

		vips_snprintf(buf, 256,
			"openslide.associated.%s", *associated_name);
		vips_image_set_image(image, buf, associated);

		g_object_unref(associated);
	}

	return 0;
}

/* Read out a resolution field, converting to pixels per mm.
 */
static double
readslice_parse_res(ReadSlide *rslide, const char *name)
{
	const char *value = openslide_get_property_value(rslide->osr, name);
	double mpp = g_ascii_strtod(value, NULL);

	return mpp == 0 ? 1.0 : 1000.0 / mpp;
}

static int
readslide_parse(ReadSlide *rslide, VipsImage *image)
{
	int64_t w, h;
	const char *error;
	const char *background;
	const char *const *properties;
	char *associated_names;
	double xres;
	double yres;

	rslide->osr = openslide_open(rslide->filename);
	if (!rslide->osr) {
		vips_error("openslide2vips",
			"%s", _("unsupported slide format"));
		return -1;
	}
	error = openslide_get_error(rslide->osr);
	if (error) {
		vips_error("openslide2vips",
			_("opening slide: %s"), error);
		return -1;
	}

	if (rslide->level < 0 ||
		rslide->level >= openslide_get_level_count(rslide->osr)) {
		vips_error("openslide2vips",
			"%s", _("invalid slide level"));
		return -1;
	}

	if (rslide->associated &&
		check_associated_image(rslide->osr, rslide->associated))
		return -1;

	if (rslide->associated) {
		openslide_get_associated_image_dimensions(rslide->osr,
			rslide->associated, &w, &h);
		vips_image_set_string(image, "slide-associated-image",
			rslide->associated);
		if (vips_image_pipelinev(image,
				VIPS_DEMAND_STYLE_THINSTRIP, NULL))
			return -1;
	}
	else {
		char buf[256];
		const char *value;

		openslide_get_level_dimensions(rslide->osr,
			rslide->level, &w, &h);
		rslide->downsample = openslide_get_level_downsample(
			rslide->osr, rslide->level);
		vips_image_set_int(image, "slide-level", rslide->level);
		if (vips_image_pipelinev(image,
				VIPS_DEMAND_STYLE_SMALLTILE, NULL))
			return -1;

		/* Try to get tile width/height. An undocumented, experimental
		 * feature.
		 */
		vips_snprintf(buf, 256,
			"openslide.level[%d].tile-width", rslide->level);
		if ((value = openslide_get_property_value(rslide->osr, buf)))
			rslide->tile_width = atoi(value);
		vips_snprintf(buf, 256,
			"openslide.level[%d].tile-height", rslide->level);
		if ((value = openslide_get_property_value(rslide->osr, buf)))
			rslide->tile_height = atoi(value);
		if (value)
			VIPS_DEBUG_MSG("readslide_new: found tile-size\n");

		/* Some images have a bounds in the header. Crop to
		 * that if autocrop is set.
		 */
		if (rslide->autocrop)
			if (!get_bounds(rslide->osr, &rslide->bounds))
				rslide->autocrop = FALSE;
		if (rslide->autocrop) {
			VipsRect whole;

			rslide->bounds.left /= rslide->downsample;
			rslide->bounds.top /= rslide->downsample;
			rslide->bounds.width /= rslide->downsample;
			rslide->bounds.height /= rslide->downsample;

			/* Clip against image size.
			 */
			whole.left = 0;
			whole.top = 0;
			whole.width = w;
			whole.height = h;
			vips_rect_intersectrect(&rslide->bounds, &whole,
				&rslide->bounds);

			/* If we've clipped to nothing, ignore bounds.
			 */
			if (vips_rect_isempty(&rslide->bounds))
				rslide->autocrop = FALSE;
		}
		if (rslide->autocrop) {
			w = rslide->bounds.width;
			h = rslide->bounds.height;
		}

		/* Attach all associated images.
		 */
		if (rslide->attach_associated &&
			readslide_attach_associated(rslide, image))
			return -1;
	}

	rslide->bg = 0xffffff;
	if ((background = openslide_get_property_value(rslide->osr,
			 OPENSLIDE_PROPERTY_NAME_BACKGROUND_COLOR)))
		rslide->bg = strtoul(background, NULL, 16);

	if (w <= 0 ||
		h <= 0 ||
		rslide->downsample < 0) {
		vips_error("openslide2vips", _("getting dimensions: %s"),
			openslide_get_error(rslide->osr));
		return -1;
	}
	if (w > INT_MAX ||
		h > INT_MAX) {
		vips_error("openslide2vips",
			"%s", _("image dimensions overflow int"));
		return -1;
	}

	if (!rslide->autocrop) {
		rslide->bounds.left = 0;
		rslide->bounds.top = 0;
		rslide->bounds.width = w;
		rslide->bounds.height = h;
	}

	/* Try to get resolution from openslide properties.
	 */
	xres = 1.0;
	yres = 1.0;

	for (properties = openslide_get_property_names(rslide->osr);
		 *properties != NULL; properties++) {
		const char *name = *properties;
		const char *value =
			openslide_get_property_value(rslide->osr, name);

		/* Can be NULL for some openslides with some images.
		 */
		if (value) {
			vips_image_set_string(image, name, value);

			if (strcmp(*properties, "openslide.mpp-x") == 0)
				xres = readslice_parse_res(rslide, name);
			if (strcmp(*properties, "openslide.mpp-y") == 0)
				yres = readslice_parse_res(rslide, name);
		}
	}

	associated_names = g_strjoinv(", ",
		(char **) openslide_get_associated_image_names(rslide->osr));
	vips_image_set_string(image,
		"slide-associated-images", associated_names);
	VIPS_FREE(associated_names);

	vips_image_init_fields(image, w, h, rslide->rgb ? 3 : 4,
		VIPS_FORMAT_UCHAR, VIPS_CODING_NONE,
		VIPS_INTERPRETATION_sRGB, xres, yres);

#ifdef HAVE_OPENSLIDE_ICC
	int64_t len;
	if ((len = openslide_get_icc_profile_size(rslide->osr)) > 0) {
		void *data;

		if (!(data = VIPS_MALLOC(NULL, len)))
			return -1;
		openslide_read_icc_profile(rslide->osr, data);
		error = openslide_get_error(rslide->osr);
		if (error) {
			g_free(data);
			vips_error("openslide2vips", _( "opening slide: %s" ), error);
			return -1;
		}

		vips_image_set_blob(image, VIPS_META_ICC_NAME,
			(VipsCallbackFn) g_free, data, len);
	}
#endif /*HAVE_OPENSLIDE_ICC*/

	return 0;
}

static int
vips__openslide_read_header(const char *filename, VipsImage *out,
	int level, gboolean autocrop,
	char *associated, gboolean attach_associated, gboolean rgb)
{
	ReadSlide *rslide;

	if (!(rslide = readslide_new(filename,
			  out, level, autocrop, associated, attach_associated, rgb)) ||
		readslide_parse(rslide, out))
		return -1;

	return 0;
}

/* Allocate a tile buffer. Have one of these for each thread so we can unpack
 * to vips in parallel.
 */
static void *
vips__openslide_start(VipsImage *out, void *a, void *b)
{
	ReadSlide *rslide = (ReadSlide *) a;

	uint32_t *tile_buffer;

	if (!(tile_buffer = VIPS_MALLOC(NULL,
			  (size_t) rslide->tile_width * rslide->tile_height * 4)))
		return NULL;

	return (void *) tile_buffer;
}

static int
vips__openslide_generate(VipsRegion *out,
	void *_seq, void *_rslide, void *unused, gboolean *stop)
{
	uint32_t *tile_buffer = (uint32_t *) _seq;
	ReadSlide *rslide = _rslide;
	uint32_t bg = rslide->bg;
	VipsRect *r = &out->valid;
	int n = r->width * r->height;

	uint32_t *buf;
	const char *error;

	VIPS_DEBUG_MSG("vips__openslide_generate: %dx%d @ %dx%d\n",
		r->width, r->height, r->left, r->top);

	/* We're inside a cache, so requests should always be
	 * tile_width by tile_height pixels and on a tile boundary.
	 */
	g_assert((r->left % rslide->tile_width) == 0);
	g_assert((r->top % rslide->tile_height) == 0);
	g_assert(r->width <= rslide->tile_width);
	g_assert(r->height <= rslide->tile_height);

	/* The memory on the region should be contiguous.
	 */
	g_assert(VIPS_REGION_LSKIP(out) == r->width * out->im->Bands);

	/* In RGB mode we need to read to the tile buffer.
	 */
	if (rslide->rgb) {
		g_assert(tile_buffer);
		g_assert(rslide->tile_width >= r->width);
		g_assert(rslide->tile_height >= r->height);

		buf = tile_buffer;
	}
	else
		buf = (uint32_t *) VIPS_REGION_ADDR(out, r->left, r->top);

	openslide_read_region(rslide->osr,
		buf,
		(r->left + rslide->bounds.left) * rslide->downsample,
		(r->top + rslide->bounds.top) * rslide->downsample,
		rslide->level,
		r->width, r->height);

	/* openslide errors are terminal. To support
	 * @fail we'd have to close the openslide_t and reopen, perhaps
	 * somehow marking this tile as unreadable.
	 *
	 * See
	 * https://github.com/libvips/libvips/commit/bb0a6643f94e69294e36d2b253f9bdd60c8c40ed#commitcomment-19838911
	 */
	error = openslide_get_error(rslide->osr);
	if (error) {
		vips_error("openslide2vips",
			_("reading region: %s"), error);
		return -1;
	}

	if (rslide->rgb)
		argb2rgb(tile_buffer,
			VIPS_REGION_ADDR(out, r->left, r->top), n);
	else
		argb2rgba(buf, n, bg);

	return 0;
}

static int
vips__openslide_stop(void *_seq, void *a, void *b)
{
	uint32_t *tile_buffer = (uint32_t *) _seq;

	VIPS_FREE(tile_buffer);

	return 0;
}

static int
vips__openslide_read(const char *filename, VipsImage *out,
	int level, gboolean autocrop, gboolean attach_associated,
	gboolean rgb)
{
	ReadSlide *rslide;
	VipsImage *raw;
	VipsImage *t;

	VIPS_DEBUG_MSG("vips__openslide_read: %s %d\n",
		filename, level);

	if (!(rslide = readslide_new(filename, out, level, autocrop,
			  NULL, attach_associated, rgb)))
		return -1;

	raw = vips_image_new();
	vips_object_local(out, raw);

	if (readslide_parse(rslide, raw) ||
		vips_image_generate(raw,
			vips__openslide_start,
			vips__openslide_generate,
			vips__openslide_stop, rslide, NULL))
		return -1;

	/* Copy to out, adding a cache. Enough tiles for two complete rows,
	 * plus 50%. We need at least two rows, or we'll constantly reload
	 * tiles if they cross a tile boundary.
	 */
	if (vips_tilecache(raw, &t,
			"tile_width", rslide->tile_width,
			"tile_height", rslide->tile_height,
			"max_tiles", (int) (2.5 * (1 + raw->Xsize / rslide->tile_width)),
			"threaded", TRUE,
			NULL))
		return -1;
	if (vips_image_write(t, out)) {
		g_object_unref(t);
		return -1;
	}
	g_object_unref(t);

	return 0;
}

static int
vips__openslide_read_associated(const char *filename, VipsImage *out,
	const char *associated_name, gboolean rgb)
{
	ReadSlide *rslide;
	VipsImage *associated;

	VIPS_DEBUG_MSG("vips__openslide_read_associated: %s %s\n",
		filename, associated_name);

	if (!(rslide = readslide_new(filename,
			  out, 0, FALSE, associated_name, FALSE, rgb)))
		return -1;
	rslide->osr = openslide_open(rslide->filename);
	if (!(associated = vips__openslide_get_associated(rslide, associated_name)))
		return -1;

	if (vips_image_write(associated, out)) {
		VIPS_UNREF(associated);
		return -1;
	}
	VIPS_UNREF(associated);

	return 0;
}

typedef struct _VipsForeignLoadOpenslide {
	VipsForeignLoad parent_object;

	/* Source to load from (set by subclasses).
	 */
	VipsSource *source;

	/* Filename from source.
	 */
	const char *filename;

	/* Load this level.
	 */
	int level;

	/* Crop to image bounds.
	 */
	gboolean autocrop;

	/* Load just this associated image.
	 */
	char *associated;

	/* Attach all associated images as metadata items.
	 */
	gboolean attach_associated;

	/* Read as RGB, not RGBA.
	 */
	gboolean rgb;

} VipsForeignLoadOpenslide;

typedef VipsForeignLoadClass VipsForeignLoadOpenslideClass;

G_DEFINE_ABSTRACT_TYPE(VipsForeignLoadOpenslide, vips_foreign_load_openslide,
	VIPS_TYPE_FOREIGN_LOAD);

static void
vips_foreign_load_openslide_dispose(GObject *gobject)
{
	VipsForeignLoadOpenslide *openslide =
		(VipsForeignLoadOpenslide *) gobject;

	VIPS_UNREF(openslide->source);

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

static int
vips_foreign_load_openslide_build(VipsObject *object)
{
	VipsObjectClass *class = VIPS_OBJECT_GET_CLASS(object);
	VipsForeignLoadOpenslide *openslide =
		(VipsForeignLoadOpenslide *) object;

	/* We can only open source which have an associated filename, since
	 * the openslide library works in terms of filenames.
	 */
	if (openslide->source) {
		VipsConnection *connection =
			VIPS_CONNECTION(openslide->source);

		const char *filename;

		if (!vips_source_is_file(openslide->source) ||
			!(filename = vips_connection_filename(connection))) {
			vips_error(class->nickname, "%s",
				_("no filename available"));
			return -1;
		}

		openslide->filename = filename;
	}

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

	return 0;
}

static VipsForeignFlags
vips_foreign_load_openslide_get_flags_source(VipsSource *source)
{
	/* We can't tell from just the source, we need to know what part of
	 * the file the user wants. But it'll usually be partial.
	 */
	return VIPS_FOREIGN_PARTIAL;
}

static VipsForeignFlags
vips_foreign_load_openslide_get_flags(VipsForeignLoad *load)
{
	VipsForeignLoadOpenslide *openslide = (VipsForeignLoadOpenslide *) load;
	VipsForeignFlags flags;

	flags = 0;
	if (!openslide->associated)
		flags |= VIPS_FOREIGN_PARTIAL;

	return flags;
}

static VipsForeignFlags
vips_foreign_load_openslide_get_flags_filename(const char *filename)
{
	VipsSource *source;
	VipsForeignFlags flags;

	if (!(source = vips_source_new_from_file(filename)))
		return 0;
	flags = vips_foreign_load_openslide_get_flags_source(source);
	VIPS_UNREF(source);

	return flags;
}

static int
vips_foreign_load_openslide_header(VipsForeignLoad *load)
{
	VipsForeignLoadOpenslide *openslide = (VipsForeignLoadOpenslide *) load;

	if (vips__openslide_read_header(openslide->filename, load->out,
			openslide->level, openslide->autocrop,
			openslide->associated, openslide->attach_associated,
			openslide->rgb))
		return -1;

	VIPS_SETSTR(load->out->filename, openslide->filename);

	return 0;
}

static int
vips_foreign_load_openslide_load(VipsForeignLoad *load)
{
	VipsForeignLoadOpenslide *openslide = (VipsForeignLoadOpenslide *) load;

	if (!openslide->associated) {
		if (vips__openslide_read(openslide->filename, load->real,
				openslide->level, openslide->autocrop,
				openslide->attach_associated,
				openslide->rgb))
			return -1;
	}
	else {
		if (vips__openslide_read_associated(openslide->filename,
				load->real, openslide->associated, openslide->rgb))
			return -1;
	}

	return 0;
}

static void
vips_foreign_load_openslide_class_init(VipsForeignLoadOpenslideClass *class)
{
	GObjectClass *gobject_class = G_OBJECT_CLASS(class);
	VipsObjectClass *object_class = (VipsObjectClass *) class;
	VipsOperationClass *operation_class = VIPS_OPERATION_CLASS(class);
	VipsForeignClass *foreign_class = (VipsForeignClass *) class;
	VipsForeignLoadClass *load_class = (VipsForeignLoadClass *) class;

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

	object_class->nickname = "openslideload_base";
	object_class->description = _("load OpenSlide base class");
	object_class->build = vips_foreign_load_openslide_build;

	/* We need to be ahead of the tiff sniffer since many OpenSlide
	 * formats are tiff derivatives. If we see a tiff which would be
	 * better handled by the vips tiff loader we are careful to say no.
	 *
	 * We need to be ahead of JPEG, since MRXS images are also
	 * JPEGs.
	 */
	foreign_class->priority = 100;

	/* libopenslide does not try to recover from errors, so it's not safe
	 * to cache.
	 */
	operation_class->flags |= VIPS_OPERATION_NOCACHE;

	/* openslide has not been fuzzed and is largly unmaintained, so should
	 * not be used with untrusted input unless you are very careful.
	 */
	operation_class->flags |= VIPS_OPERATION_UNTRUSTED;

	load_class->get_flags_filename =
		vips_foreign_load_openslide_get_flags_filename;
	load_class->get_flags = vips_foreign_load_openslide_get_flags;
	load_class->header = vips_foreign_load_openslide_header;
	load_class->load = vips_foreign_load_openslide_load;

	VIPS_ARG_INT(class, "level", 20,
		_("Level"),
		_("Load this level from the file"),
		VIPS_ARGUMENT_OPTIONAL_INPUT,
		G_STRUCT_OFFSET(VipsForeignLoadOpenslide, level),
		0, 100000, 0);

	VIPS_ARG_BOOL(class, "autocrop", 21,
		_("Autocrop"),
		_("Crop to image bounds"),
		VIPS_ARGUMENT_OPTIONAL_INPUT,
		G_STRUCT_OFFSET(VipsForeignLoadOpenslide, autocrop),
		FALSE);

	VIPS_ARG_STRING(class, "associated", 22,
		_("Associated"),
		_("Load this associated image"),
		VIPS_ARGUMENT_OPTIONAL_INPUT,
		G_STRUCT_OFFSET(VipsForeignLoadOpenslide, associated),
		NULL);

	VIPS_ARG_BOOL(class, "attach_associated", 23,
		_("Attach associated"),
		_("Attach all associated images"),
		VIPS_ARGUMENT_OPTIONAL_INPUT,
		G_STRUCT_OFFSET(VipsForeignLoadOpenslide, attach_associated),
		FALSE);

	VIPS_ARG_BOOL(class, "rgb", 24,
		_("RGB"),
		_("Output RGB (not RGBA)"),
		VIPS_ARGUMENT_OPTIONAL_INPUT,
		G_STRUCT_OFFSET(VipsForeignLoadOpenslide, rgb),
		FALSE);
}

static void
vips_foreign_load_openslide_init(VipsForeignLoadOpenslide *openslide)
{
}

typedef struct _VipsForeignLoadOpenslideFile {
	VipsForeignLoadOpenslide parent_object;

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

} VipsForeignLoadOpenslideFile;

typedef VipsForeignLoadOpenslideClass VipsForeignLoadOpenslideFileClass;

G_DEFINE_TYPE(VipsForeignLoadOpenslideFile, vips_foreign_load_openslide_file,
	vips_foreign_load_openslide_get_type());

static int
vips_foreign_load_openslide_file_build(VipsObject *object)
{
	VipsForeignLoadOpenslide *openslide =
		(VipsForeignLoadOpenslide *) object;
	VipsForeignLoadOpenslideFile *file =
		(VipsForeignLoadOpenslideFile *) object;

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

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

	return 0;
}

static const char *vips_foreign_openslide_suffs[] = {
	".svs",					 /* Aperio */
	".vms", ".vmu", ".ndpi", /* Hamamatsu */
	".scn",					 /* Leica */
	".mrxs",				 /* MIRAX */
	".svslide",				 /* Sakura */
	".tif",					 /* Trestle */
	".bif",					 /* Ventana */
	NULL
};

static void
vips_foreign_load_openslide_file_class_init(
	VipsForeignLoadOpenslideFileClass *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 = "openslideload";
	object_class->description = _("load file with OpenSlide");
	object_class->build = vips_foreign_load_openslide_file_build;

	foreign_class->suffs = vips_foreign_openslide_suffs;

	load_class->is_a = vips__openslide_isslide;

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

static void
vips_foreign_load_openslide_file_init(VipsForeignLoadOpenslideFile *openslide)
{
}

typedef struct _VipsForeignLoadOpenslideSource {
	VipsForeignLoadOpenslide parent_object;

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

} VipsForeignLoadOpenslideSource;

typedef VipsForeignLoadOpenslideClass VipsForeignLoadOpenslideSourceClass;

G_DEFINE_TYPE(VipsForeignLoadOpenslideSource,
	vips_foreign_load_openslide_source,
	vips_foreign_load_openslide_get_type());

static int
vips_foreign_load_openslide_source_build(VipsObject *object)
{
	VipsForeignLoadOpenslide *openslide =
		(VipsForeignLoadOpenslide *) object;
	VipsForeignLoadOpenslideSource *source =
		(VipsForeignLoadOpenslideSource *) object;

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

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

	return 0;
}

static gboolean
vips_foreign_load_openslide_source_is_a_source(VipsSource *source)
{
	VipsConnection *connection = VIPS_CONNECTION(source);

	const char *filename;

	return vips_source_is_file(source) &&
		(filename = vips_connection_filename(connection)) &&
		vips__openslide_isslide(filename);
}

static void
vips_foreign_load_openslide_source_class_init(
	VipsForeignLoadOpenslideSourceClass *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 = "openslideload_source";
	object_class->description = _("load source with OpenSlide");
	object_class->build = vips_foreign_load_openslide_source_build;

	load_class->is_a_source =
		vips_foreign_load_openslide_source_is_a_source;

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

static void
vips_foreign_load_openslide_source_init(
	VipsForeignLoadOpenslideSource *openslide)
{
}

#endif /*HAVE_OPENSLIDE*/

/* The C API wrappers are defined in foreign.c.
 */
