/* convert between colourspaces
 *
 * 6/11/12
 * 	- add RGB16 as a destination
 * 12/1/14
 * 	- add B_W as a source / dest
 * 	- add GREY16 as a source / dest
 * 	- add RGB16 as a source
 * 19/1/14
 * 	- auto-decode RAD images
 * 3/2/14
 * 	- add "source_space", overrides source space guess
 * 8/5/14
 * 	- oops, don't treat RGB16 as sRGB
 * 9/9/14
 * 	- mono <-> rgb converters were not handling extra bands, thanks James
 * 4/2/15
 * 	- much faster RGB16->sRGB path
 * 17/4/15
 * 	- better conversion to greyscale, see
 * 	  https://github.com/lovell/sharp/issues/193
 * 27/12/18
 * 	- add CMYK conversions
 */

/*

	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
 */

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

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

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

#include "pcolour.h"

static int
vips_scRGB2RGB16(VipsImage *in, VipsImage **out, ...)
{
	return vips_scRGB2sRGB(in, out, "depth", 16, NULL);
}

static int
vips_scRGB2BW16(VipsImage *in, VipsImage **out, ...)
{
	return vips_scRGB2BW(in, out, "depth", 16, NULL);
}

/* Do these two with a simple cast ... since we're just cast shifting, we can
 * short-circuit the extra band processing.
 */

static int
vips_RGB162sRGB(VipsImage *in, VipsImage **out, ...)
{
	if (vips_cast(in, out, VIPS_FORMAT_UCHAR,
			"shift", TRUE,
			NULL))
		return -1;
	(*out)->Type = VIPS_INTERPRETATION_sRGB;

	return 0;
}

static int
vips_sRGB2RGB16(VipsImage *in, VipsImage **out, ...)
{
	if (vips_cast(in, out, VIPS_FORMAT_USHORT,
			"shift", TRUE,
			NULL))
		return -1;
	(*out)->Type = VIPS_INTERPRETATION_RGB16;

	return 0;
}

/* Process the first @n bands with @fn, detach and reattach remaining bands.
 *
 * Also used by CMYK2XYZ and XYZ2CMYK.
 */
int
vips__colourspace_process_n(const char *domain,
	VipsImage *in, VipsImage **out, int n, VipsColourTransformFn fn)
{
	if (in->Bands > n) {
		VipsImage *scope = vips_image_new();
		VipsImage **t = (VipsImage **)
			vips_object_local_array(VIPS_OBJECT(scope), 4);

		if (vips_extract_band(in, &t[0], 0,
				"n", n,
				NULL) ||
			vips_extract_band(in, &t[1], n,
				"n", in->Bands - n,
				NULL) ||
			fn(t[0], &t[2], NULL) ||
			vips_cast(t[1], &t[3], t[2]->BandFmt,
				NULL) ||
			vips_bandjoin2(t[2], t[3], out, NULL)) {
			g_object_unref(scope);
			return -1;
		}

		g_object_unref(scope);
	}
	else if (in->Bands == n) {
		if (fn(in, out, NULL))
			return -1;
	}
	else {
		vips_error(domain, "%s", _("too few bands for operation"));
		return -1;
	}

	return 0;
}

static int
vips_BW2sRGB_op(VipsImage *in, VipsImage **out, ...)
{
	VipsImage *t[3];

	t[0] = in;
	t[1] = in;
	t[2] = in;
	if (vips_bandjoin(t, out, 3, NULL))
		return -1;

	return 0;
}

static int
vips_BW2sRGB(VipsImage *in, VipsImage **out, ...)
{
	if (vips__colourspace_process_n("BW2sRGB",
			in, out, 1, vips_BW2sRGB_op))
		return -1;
	(*out)->Type = VIPS_INTERPRETATION_sRGB;

	return 0;
}

static int
vips_GREY162RGB16(VipsImage *in, VipsImage **out, ...)
{
	if (vips__colourspace_process_n("GREY162RGB16",
			in, out, 1, vips_BW2sRGB_op))
		return -1;
	(*out)->Type = VIPS_INTERPRETATION_RGB16;

	return 0;
}

/* Maximum number of steps we allow in a route. 10 steps should be enough
 * for anyone.
 */
#define MAX_STEPS (10)

/* A route between two colour spaces.
 */
typedef struct _VipsColourRoute {
	VipsInterpretation from;
	VipsInterpretation to;
	VipsColourTransformFn route[MAX_STEPS + 1];
} VipsColourRoute;

/* Some defines to save typing. These are the colour spaces we support
 * conversions between.
 */
#define XYZ VIPS_INTERPRETATION_XYZ
#define LAB VIPS_INTERPRETATION_LAB
#define LABQ VIPS_INTERPRETATION_LABQ
#define LCH VIPS_INTERPRETATION_LCH
#define CMC VIPS_INTERPRETATION_CMC
#define LABS VIPS_INTERPRETATION_LABS
#define CMYK VIPS_INTERPRETATION_CMYK
#define scRGB VIPS_INTERPRETATION_scRGB
#define sRGB VIPS_INTERPRETATION_sRGB
#define HSV VIPS_INTERPRETATION_HSV
#define RGB16 VIPS_INTERPRETATION_RGB16
#define GREY16 VIPS_INTERPRETATION_GREY16
#define YXY VIPS_INTERPRETATION_YXY
#define BW VIPS_INTERPRETATION_B_W

/* All the routes we know about.
 */
static VipsColourRoute vips_colour_routes[] = {
	{ XYZ, LAB, { vips_XYZ2Lab, NULL } },
	{ XYZ, LABQ, { vips_XYZ2Lab, vips_Lab2LabQ, NULL } },
	{ XYZ, LCH, { vips_XYZ2Lab, vips_Lab2LCh, NULL } },
	{ XYZ, CMC, { vips_XYZ2Lab, vips_Lab2LCh, vips_LCh2CMC, NULL } },
	{ XYZ, LABS, { vips_XYZ2Lab, vips_Lab2LabS, NULL } },
	{ XYZ, CMYK, { vips_XYZ2CMYK, NULL } },
	{ XYZ, scRGB, { vips_XYZ2scRGB, NULL } },
	{ XYZ, sRGB, { vips_XYZ2scRGB, vips_scRGB2sRGB, NULL } },
	{ XYZ, HSV, { vips_XYZ2scRGB, vips_scRGB2sRGB, vips_sRGB2HSV, NULL } },
	{ XYZ, BW, { vips_XYZ2scRGB, vips_scRGB2BW, NULL } },
	{ XYZ, RGB16, { vips_XYZ2scRGB, vips_scRGB2RGB16, NULL } },
	{ XYZ, GREY16, { vips_XYZ2scRGB, vips_scRGB2BW16, NULL } },
	{ XYZ, YXY, { vips_XYZ2Yxy, NULL } },

	{ LAB, XYZ, { vips_Lab2XYZ, NULL } },
	{ LAB, LABQ, { vips_Lab2LabQ, NULL } },
	{ LAB, LCH, { vips_Lab2LCh, NULL } },
	{ LAB, CMC, { vips_Lab2LCh, vips_LCh2CMC, NULL } },
	{ LAB, LABS, { vips_Lab2LabS, NULL } },
	{ LAB, CMYK, { vips_Lab2XYZ, vips_XYZ2CMYK, NULL } },
	{ LAB, scRGB, { vips_Lab2XYZ, vips_XYZ2scRGB, NULL } },
	{ LAB, sRGB, { vips_Lab2XYZ, vips_XYZ2scRGB, vips_scRGB2sRGB, NULL } },
	{ LAB, HSV, { vips_Lab2XYZ, vips_XYZ2scRGB, vips_scRGB2sRGB, vips_sRGB2HSV, NULL } },
	{ LAB, BW, { vips_Lab2XYZ, vips_XYZ2scRGB, vips_scRGB2BW, NULL } },
	{ LAB, RGB16, { vips_Lab2XYZ, vips_XYZ2scRGB, vips_scRGB2RGB16, NULL } },
	{ LAB, GREY16, { vips_Lab2XYZ, vips_XYZ2scRGB, vips_scRGB2BW16, NULL } },
	{ LAB, YXY, { vips_Lab2XYZ, vips_XYZ2Yxy, NULL } },

	{ LABQ, XYZ, { vips_LabQ2Lab, vips_Lab2XYZ, NULL } },
	{ LABQ, LAB, { vips_LabQ2Lab, NULL } },
	{ LABQ, LCH, { vips_LabQ2Lab, vips_Lab2LCh, NULL } },
	{ LABQ, CMC, { vips_LabQ2Lab, vips_Lab2LCh, vips_LCh2CMC, NULL } },
	{ LABQ, LABS, { vips_LabQ2LabS, NULL } },
	{ LABQ, CMYK, { vips_LabQ2Lab, vips_Lab2XYZ, vips_XYZ2CMYK, NULL } },
	{ LABQ, scRGB, { vips_LabQ2Lab, vips_Lab2XYZ, vips_XYZ2scRGB, NULL } },
	{ LABQ, sRGB, { vips_LabQ2sRGB, NULL } },
	{ LABQ, HSV, { vips_LabQ2sRGB, vips_sRGB2HSV, NULL } },
	{ LABQ, BW, { vips_LabQ2Lab, vips_Lab2XYZ, vips_XYZ2scRGB, vips_scRGB2BW, NULL } },
	{ LABQ, RGB16, { vips_LabQ2Lab, vips_Lab2XYZ, vips_XYZ2scRGB, vips_scRGB2RGB16, NULL } },
	{ LABQ, GREY16, { vips_LabQ2Lab, vips_Lab2XYZ, vips_XYZ2scRGB, vips_scRGB2BW16, NULL } },
	{ LABQ, YXY, { vips_LabQ2Lab, vips_Lab2XYZ, vips_XYZ2Yxy, NULL } },

	{ LCH, XYZ, { vips_LCh2Lab, vips_Lab2XYZ, NULL } },
	{ LCH, LAB, { vips_LCh2Lab, NULL } },
	{ LCH, LABQ, { vips_LCh2Lab, vips_Lab2LabQ, NULL } },
	{ LCH, CMC, { vips_LCh2CMC, NULL } },
	{ LCH, LABS, { vips_LCh2Lab, vips_Lab2LabS, NULL } },
	{ LCH, CMYK, { vips_LCh2Lab, vips_Lab2XYZ, vips_XYZ2CMYK, NULL } },
	{ LCH, scRGB, { vips_LCh2Lab, vips_Lab2XYZ, vips_XYZ2scRGB, NULL } },
	{ LCH, sRGB, { vips_LCh2Lab, vips_Lab2XYZ, vips_XYZ2scRGB, vips_scRGB2sRGB, NULL } },
	{ LCH, HSV, { vips_LCh2Lab, vips_Lab2XYZ, vips_XYZ2scRGB, vips_scRGB2sRGB, vips_sRGB2HSV, NULL } },
	{ LCH, BW, { vips_LCh2Lab, vips_Lab2XYZ, vips_XYZ2scRGB, vips_scRGB2BW, NULL } },
	{ LCH, RGB16, { vips_LCh2Lab, vips_Lab2XYZ, vips_XYZ2scRGB, vips_scRGB2RGB16, NULL } },
	{ LCH, GREY16, { vips_LCh2Lab, vips_Lab2XYZ, vips_XYZ2scRGB, vips_scRGB2BW16, NULL } },
	{ LCH, YXY, { vips_LCh2Lab, vips_Lab2XYZ, vips_XYZ2Yxy, NULL } },

	{ CMC, XYZ, { vips_CMC2LCh, vips_LCh2Lab, vips_Lab2XYZ, NULL } },
	{ CMC, LAB, { vips_CMC2LCh, vips_LCh2Lab, NULL } },
	{ CMC, LABQ, { vips_CMC2LCh, vips_LCh2Lab, vips_Lab2LabQ, NULL } },
	{ CMC, LCH, { vips_CMC2LCh, NULL } },
	{ CMC, LABS, { vips_CMC2LCh, vips_LCh2Lab, vips_Lab2LabS, NULL } },
	{ CMC, CMYK, { vips_CMC2LCh, vips_LCh2Lab, vips_Lab2XYZ, vips_XYZ2CMYK, NULL } },
	{ CMC, scRGB, { vips_CMC2LCh, vips_LCh2Lab, vips_Lab2XYZ, vips_XYZ2scRGB, NULL } },
	{ CMC, sRGB, { vips_CMC2LCh, vips_LCh2Lab, vips_Lab2XYZ, vips_XYZ2scRGB, vips_scRGB2sRGB, NULL } },
	{ CMC, HSV, { vips_CMC2LCh, vips_LCh2Lab, vips_Lab2XYZ, vips_XYZ2scRGB, vips_scRGB2sRGB, vips_sRGB2HSV, NULL } },
	{ CMC, BW, { vips_CMC2LCh, vips_LCh2Lab, vips_Lab2XYZ, vips_XYZ2scRGB, vips_scRGB2BW, NULL } },
	{ CMC, RGB16, { vips_CMC2LCh, vips_LCh2Lab, vips_Lab2XYZ, vips_XYZ2scRGB, vips_scRGB2RGB16, NULL } },
	{ CMC, GREY16, { vips_CMC2LCh, vips_LCh2Lab, vips_Lab2XYZ, vips_XYZ2scRGB, vips_scRGB2BW16, NULL } },
	{ CMC, YXY, { vips_CMC2LCh, vips_LCh2Lab, vips_Lab2XYZ, vips_XYZ2Yxy, NULL } },

	{ LABS, XYZ, { vips_LabS2Lab, vips_Lab2XYZ, NULL } },
	{ LABS, LAB, { vips_LabS2Lab, NULL } },
	{ LABS, LABQ, { vips_LabS2LabQ, NULL } },
	{ LABS, LCH, { vips_LabS2Lab, vips_Lab2LCh, NULL } },
	{ LABS, CMC, { vips_LabS2Lab, vips_Lab2LCh, vips_LCh2CMC, NULL } },
	{ LABS, CMYK, { vips_LabS2Lab, vips_Lab2XYZ, vips_XYZ2CMYK, NULL } },
	{ LABS, scRGB, { vips_LabS2Lab, vips_Lab2XYZ, vips_XYZ2scRGB, NULL } },
	{ LABS, sRGB, { vips_LabS2Lab, vips_Lab2XYZ, vips_XYZ2scRGB, vips_scRGB2sRGB, NULL } },
	{ LABS, HSV, { vips_LabS2Lab, vips_Lab2XYZ, vips_XYZ2scRGB, vips_scRGB2sRGB, vips_sRGB2HSV, NULL } },
	{ LABS, BW, { vips_LabS2Lab, vips_Lab2XYZ, vips_XYZ2scRGB, vips_scRGB2BW, NULL } },
	{ LABS, RGB16, { vips_LabS2Lab, vips_Lab2XYZ, vips_XYZ2scRGB, vips_scRGB2RGB16, NULL } },
	{ LABS, GREY16, { vips_LabS2Lab, vips_Lab2XYZ, vips_XYZ2scRGB, vips_scRGB2BW16, NULL } },
	{ LABS, YXY, { vips_LabS2Lab, vips_Lab2XYZ, vips_XYZ2Yxy, NULL } },

	{ scRGB, XYZ, { vips_scRGB2XYZ, NULL } },
	{ scRGB, LAB, { vips_scRGB2XYZ, vips_XYZ2Lab, NULL } },
	{ scRGB, LABQ, { vips_scRGB2XYZ, vips_XYZ2Lab, vips_Lab2LabQ, NULL } },
	{ scRGB, LCH, { vips_scRGB2XYZ, vips_XYZ2Lab, vips_Lab2LCh, NULL } },
	{ scRGB, CMC, { vips_scRGB2XYZ, vips_XYZ2Lab, vips_Lab2LCh, vips_LCh2CMC, NULL } },
	{ scRGB, CMYK, { vips_scRGB2XYZ, vips_XYZ2CMYK, NULL } },
	{ scRGB, sRGB, { vips_scRGB2sRGB, NULL } },
	{ scRGB, HSV, { vips_scRGB2sRGB, vips_sRGB2HSV, NULL } },
	{ scRGB, BW, { vips_scRGB2BW, NULL } },
	{ scRGB, LABS, { vips_scRGB2XYZ, vips_XYZ2Lab, vips_Lab2LabS, NULL } },
	{ scRGB, RGB16, { vips_scRGB2RGB16, NULL } },
	{ scRGB, GREY16, { vips_scRGB2BW16, NULL } },
	{ scRGB, YXY, { vips_scRGB2XYZ, vips_XYZ2Yxy, NULL } },

	{ CMYK, XYZ, { vips_CMYK2XYZ, NULL } },
	{ CMYK, LAB, { vips_CMYK2XYZ, vips_XYZ2Lab, NULL } },
	{ CMYK, LABQ, { vips_CMYK2XYZ, vips_XYZ2Lab, vips_Lab2LabQ, NULL } },
	{ CMYK, LCH, { vips_CMYK2XYZ, vips_XYZ2Lab, vips_Lab2LCh, NULL } },
	{ CMYK, CMC, { vips_CMYK2XYZ, vips_XYZ2Lab, vips_Lab2LCh, vips_LCh2CMC, NULL } },
	{ CMYK, scRGB, { vips_CMYK2XYZ, vips_XYZ2scRGB, NULL } },
	{ CMYK, sRGB, { vips_CMYK2XYZ, vips_XYZ2scRGB, vips_scRGB2sRGB, NULL } },
	{ CMYK, HSV, { vips_CMYK2XYZ, vips_XYZ2scRGB, vips_scRGB2sRGB, vips_sRGB2HSV, NULL } },
	{ CMYK, BW, { vips_CMYK2XYZ, vips_XYZ2scRGB, vips_scRGB2BW, NULL } },
	{ CMYK, LABS, { vips_CMYK2XYZ, vips_XYZ2Lab, vips_Lab2LabS, NULL } },
	{ CMYK, RGB16, { vips_CMYK2XYZ, vips_XYZ2scRGB, vips_scRGB2RGB16, NULL } },
	{ CMYK, GREY16, { vips_CMYK2XYZ, vips_XYZ2scRGB, vips_scRGB2BW16, NULL } },
	{ CMYK, YXY, { vips_CMYK2XYZ, vips_XYZ2Yxy, NULL } },

	{ sRGB, XYZ, { vips_sRGB2scRGB, vips_scRGB2XYZ, NULL } },
	{ sRGB, LAB, { vips_sRGB2scRGB, vips_scRGB2XYZ, vips_XYZ2Lab, NULL } },
	{ sRGB, LABQ, { vips_sRGB2scRGB, vips_scRGB2XYZ, vips_XYZ2Lab, vips_Lab2LabQ, NULL } },
	{ sRGB, LCH, { vips_sRGB2scRGB, vips_scRGB2XYZ, vips_XYZ2Lab, vips_Lab2LCh, NULL } },
	{ sRGB, CMC, { vips_sRGB2scRGB, vips_scRGB2XYZ, vips_XYZ2Lab, vips_Lab2LCh, vips_LCh2CMC, NULL } },
	{ sRGB, CMYK, { vips_sRGB2scRGB, vips_scRGB2XYZ, vips_XYZ2CMYK, NULL } },
	{ sRGB, scRGB, { vips_sRGB2scRGB, NULL } },
	{ sRGB, HSV, { vips_sRGB2HSV, NULL } },
	{ sRGB, BW, { vips_sRGB2scRGB, vips_scRGB2BW, NULL } },
	{ sRGB, LABS, { vips_sRGB2scRGB, vips_scRGB2XYZ, vips_XYZ2Lab, vips_Lab2LabS, NULL } },
	{ sRGB, RGB16, { vips_sRGB2RGB16, NULL } },
	{ sRGB, GREY16, { vips_sRGB2scRGB, vips_scRGB2BW16, NULL } },
	{ sRGB, YXY, { vips_sRGB2scRGB, vips_scRGB2XYZ, vips_XYZ2Yxy, NULL } },

	{ HSV, XYZ, { vips_HSV2sRGB, vips_sRGB2scRGB, vips_scRGB2XYZ, NULL } },
	{ HSV, LAB, { vips_HSV2sRGB, vips_sRGB2scRGB, vips_scRGB2XYZ, vips_XYZ2Lab, NULL } },
	{ HSV, LABQ, { vips_HSV2sRGB, vips_sRGB2scRGB, vips_scRGB2XYZ, vips_XYZ2Lab, vips_Lab2LabQ, NULL } },
	{ HSV, LCH, { vips_HSV2sRGB, vips_sRGB2scRGB, vips_scRGB2XYZ, vips_XYZ2Lab, vips_Lab2LCh, NULL } },
	{ HSV, CMC, { vips_HSV2sRGB, vips_sRGB2scRGB, vips_scRGB2XYZ, vips_XYZ2Lab, vips_Lab2LCh, vips_LCh2CMC, NULL } },
	{ HSV, CMYK, { vips_HSV2sRGB, vips_sRGB2scRGB, vips_scRGB2XYZ, vips_XYZ2CMYK, NULL } },
	{ HSV, scRGB, { vips_HSV2sRGB, vips_sRGB2scRGB, NULL } },
	{ HSV, sRGB, { vips_HSV2sRGB, NULL } },
	{ HSV, BW, { vips_HSV2sRGB, vips_sRGB2scRGB, vips_scRGB2BW, NULL } },
	{ HSV, LABS, { vips_HSV2sRGB, vips_sRGB2scRGB, vips_scRGB2XYZ, vips_XYZ2Lab, vips_Lab2LabS, NULL } },
	{ HSV, RGB16, { vips_HSV2sRGB, vips_sRGB2RGB16, NULL } },
	{ HSV, GREY16, { vips_HSV2sRGB, vips_sRGB2scRGB, vips_scRGB2BW16, NULL } },
	{ HSV, YXY, { vips_HSV2sRGB, vips_sRGB2scRGB, vips_scRGB2XYZ, vips_XYZ2Yxy, NULL } },

	{ RGB16, XYZ, { vips_sRGB2scRGB, vips_scRGB2XYZ, NULL } },
	{ RGB16, LAB, { vips_sRGB2scRGB, vips_scRGB2XYZ, vips_XYZ2Lab, NULL } },
	{ RGB16, LABQ, { vips_sRGB2scRGB, vips_scRGB2XYZ, vips_XYZ2Lab, vips_Lab2LabQ, NULL } },
	{ RGB16, LCH, { vips_sRGB2scRGB, vips_scRGB2XYZ, vips_XYZ2Lab, vips_Lab2LCh, NULL } },
	{ RGB16, CMC, { vips_sRGB2scRGB, vips_scRGB2XYZ, vips_XYZ2Lab, vips_Lab2LCh, vips_LCh2CMC, NULL } },
	{ RGB16, CMYK, { vips_sRGB2scRGB, vips_scRGB2XYZ, vips_XYZ2CMYK, NULL } },
	{ RGB16, scRGB, { vips_sRGB2scRGB, NULL } },
	{ RGB16, sRGB, { vips_RGB162sRGB, NULL } },
	{ RGB16, HSV, { vips_RGB162sRGB, vips_sRGB2HSV, NULL } },
	{ RGB16, BW, { vips_sRGB2scRGB, vips_scRGB2BW, NULL } },
	{ RGB16, LABS, { vips_sRGB2scRGB, vips_scRGB2XYZ, vips_XYZ2Lab, vips_Lab2LabS, NULL } },
	{ RGB16, GREY16, { vips_sRGB2scRGB, vips_scRGB2BW16, NULL } },
	{ RGB16, YXY, { vips_sRGB2scRGB, vips_scRGB2XYZ, vips_XYZ2Yxy, NULL } },

	{ GREY16, XYZ, { vips_GREY162RGB16, vips_sRGB2scRGB, vips_scRGB2XYZ, NULL } },
	{ GREY16, LAB, { vips_GREY162RGB16, vips_sRGB2scRGB, vips_scRGB2XYZ, vips_XYZ2Lab, NULL } },
	{ GREY16, LABQ, { vips_GREY162RGB16, vips_sRGB2scRGB, vips_scRGB2XYZ, vips_XYZ2Lab, vips_Lab2LabQ, NULL } },
	{ GREY16, LCH, { vips_GREY162RGB16, vips_sRGB2scRGB, vips_scRGB2XYZ, vips_XYZ2Lab, vips_Lab2LCh, NULL } },
	{ GREY16, CMC, { vips_GREY162RGB16, vips_sRGB2scRGB, vips_scRGB2XYZ, vips_XYZ2Lab, vips_Lab2LCh, vips_LCh2CMC, NULL } },
	{ GREY16, CMYK, { vips_GREY162RGB16, vips_sRGB2scRGB, vips_scRGB2XYZ, vips_XYZ2CMYK, NULL } },
	{ GREY16, scRGB, { vips_GREY162RGB16, vips_sRGB2scRGB, NULL } },
	{ GREY16, sRGB, { vips_GREY162RGB16, vips_RGB162sRGB, NULL } },
	{ GREY16, HSV, { vips_GREY162RGB16, vips_RGB162sRGB, vips_sRGB2HSV, NULL } },
	{ GREY16, BW, { vips_GREY162RGB16, vips_sRGB2scRGB, vips_scRGB2BW, NULL } },
	{ GREY16, LABS, { vips_GREY162RGB16, vips_sRGB2scRGB, vips_scRGB2XYZ, vips_XYZ2Lab, vips_Lab2LabS, NULL } },
	{ GREY16, RGB16, { vips_GREY162RGB16, NULL } },
	{ GREY16, YXY, { vips_GREY162RGB16, vips_sRGB2scRGB, vips_scRGB2XYZ, vips_XYZ2Yxy, NULL } },

	{ BW, XYZ, { vips_BW2sRGB, vips_sRGB2scRGB, vips_scRGB2XYZ, NULL } },
	{ BW, LAB, { vips_BW2sRGB, vips_sRGB2scRGB, vips_scRGB2XYZ, vips_XYZ2Lab, NULL } },
	{ BW, LABQ, { vips_BW2sRGB, vips_sRGB2scRGB, vips_scRGB2XYZ, vips_XYZ2Lab, vips_Lab2LabQ, NULL } },
	{ BW, LCH, { vips_BW2sRGB, vips_sRGB2scRGB, vips_scRGB2XYZ, vips_XYZ2Lab, vips_Lab2LCh, NULL } },
	{ BW, CMC, { vips_BW2sRGB, vips_sRGB2scRGB, vips_scRGB2XYZ, vips_XYZ2Lab, vips_Lab2LCh, vips_LCh2CMC, NULL } },
	{ BW, CMYK, { vips_BW2sRGB, vips_sRGB2scRGB, vips_scRGB2XYZ, vips_XYZ2CMYK, NULL } },
	{ BW, scRGB, { vips_BW2sRGB, vips_sRGB2scRGB, NULL } },
	{ BW, sRGB, { vips_BW2sRGB, NULL } },
	{ BW, HSV, { vips_BW2sRGB, vips_sRGB2HSV, NULL } },
	{ BW, LABS, { vips_BW2sRGB, vips_sRGB2scRGB, vips_scRGB2XYZ, vips_XYZ2Lab, vips_Lab2LabS, NULL } },
	{ BW, RGB16, { vips_BW2sRGB, vips_sRGB2RGB16, NULL } },
	{ BW, GREY16, { vips_BW2sRGB, vips_sRGB2scRGB, vips_scRGB2BW16, NULL } },
	{ BW, YXY, { vips_BW2sRGB, vips_sRGB2scRGB, vips_scRGB2XYZ, vips_XYZ2Yxy, NULL } },

	{ YXY, XYZ, { vips_Yxy2XYZ, NULL } },
	{ YXY, LAB, { vips_Yxy2XYZ, vips_XYZ2Lab, NULL } },
	{ YXY, LABQ, { vips_Yxy2XYZ, vips_XYZ2Lab, vips_Lab2LabQ, NULL } },
	{ YXY, LCH, { vips_Yxy2XYZ, vips_XYZ2Lab, vips_Lab2LCh, NULL } },
	{ YXY, CMC, { vips_Yxy2XYZ, vips_XYZ2Lab, vips_Lab2LCh, vips_LCh2CMC, NULL } },
	{ YXY, LABS, { vips_Yxy2XYZ, vips_XYZ2Lab, vips_Lab2LabS, NULL } },
	{ YXY, CMYK, { vips_Yxy2XYZ, vips_XYZ2CMYK, NULL } },
	{ YXY, scRGB, { vips_Yxy2XYZ, vips_XYZ2scRGB, NULL } },
	{ YXY, sRGB, { vips_Yxy2XYZ, vips_XYZ2scRGB, vips_scRGB2sRGB, NULL } },
	{ YXY, HSV, { vips_Yxy2XYZ, vips_XYZ2scRGB, vips_scRGB2sRGB, vips_sRGB2HSV, NULL } },
	{ YXY, BW, { vips_Yxy2XYZ, vips_XYZ2scRGB, vips_scRGB2BW, NULL } },
	{ YXY, RGB16, { vips_Yxy2XYZ, vips_XYZ2scRGB, vips_scRGB2RGB16, NULL } },
	{ YXY, GREY16, { vips_Yxy2XYZ, vips_XYZ2scRGB, vips_scRGB2BW16, NULL } }

};

/* Is an image in a supported colourspace.
 */

/**
 * vips_colourspace_issupported: (method)
 * @image: input image
 *
 * Test if @image is in a colourspace that vips_colourspace() can process.
 *
 * Returns: %TRUE if @image is in a supported colourspace.
 */
gboolean
vips_colourspace_issupported(const VipsImage *image)
{
	VipsInterpretation interpretation;
	int i;

	/* Treat RGB as sRGB. If you want some other treatment,
	 * you'll need to use the icc funcs.
	 */
	interpretation = vips_image_guess_interpretation(image);
	if (interpretation == VIPS_INTERPRETATION_RGB)
		interpretation = VIPS_INTERPRETATION_sRGB;

	for (i = 0; i < VIPS_NUMBER(vips_colour_routes); i++)
		if (vips_colour_routes[i].from == interpretation)
			return TRUE;

	return FALSE;
}

typedef struct _VipsColourspace {
	VipsOperation parent_instance;

	VipsImage *in;
	VipsImage *out;
	VipsInterpretation space;
	VipsInterpretation source_space;
} VipsColourspace;

typedef VipsOperationClass VipsColourspaceClass;

G_DEFINE_TYPE(VipsColourspace, vips_colourspace, VIPS_TYPE_OPERATION);

static int
vips_colourspace_build(VipsObject *object)
{
	VipsColourspace *colourspace = (VipsColourspace *) object;

	int i, j;
	VipsImage *x;
	VipsImage **t = (VipsImage **)
		vips_object_local_array(object, 1);
	VipsImage **pipe = (VipsImage **)
		vips_object_local_array(object, MAX_STEPS);

	VipsInterpretation interpretation;

	/* Verify that all input args have been set.
	 */
	if (VIPS_OBJECT_CLASS(vips_colourspace_parent_class)->build(object))
		return -1;

	x = colourspace->in;

	/* Unpack radiance-coded images. We can't use interpretation for this,
	 * since rad images can be scRGB or XYZ.
	 */
	if (x->Coding == VIPS_CODING_RAD) {
		if (vips_rad2float(x, &t[0], NULL))
			return -1;
		x = t[0];
	}

	if (vips_object_argument_isset(object, "source_space"))
		interpretation = colourspace->source_space;
	else
		interpretation = vips_image_guess_interpretation(x);

	/* Treat RGB as sRGB. If you want some other treatment,
	 * you'll need to use the icc funcs.
	 */
	if (interpretation == VIPS_INTERPRETATION_RGB)
		interpretation = VIPS_INTERPRETATION_sRGB;

	/* No conversion necessary.
	 */
	if (interpretation == colourspace->space) {
		g_object_set(colourspace, "out", vips_image_new(), NULL);

		return vips_image_write(colourspace->in, colourspace->out);
	}

	for (i = 0; i < VIPS_NUMBER(vips_colour_routes); i++)
		if (vips_colour_routes[i].from == interpretation &&
			vips_colour_routes[i].to == colourspace->space)
			break;
	if (i == VIPS_NUMBER(vips_colour_routes)) {
		vips_error("vips_colourspace",
			_("no known route from '%s' to '%s'"),
			vips_enum_nick(VIPS_TYPE_INTERPRETATION,
				interpretation),
			vips_enum_nick(VIPS_TYPE_INTERPRETATION,
				colourspace->space));
		return -1;
	}

	for (j = 0; vips_colour_routes[i].route[j]; j++) {
		if (vips_colour_routes[i].route[j](x, &pipe[j], NULL))
			return -1;
		x = pipe[j];
	}

	g_object_set(colourspace, "out", vips_image_new(), NULL);
	if (vips_image_write(x, colourspace->out))
		return -1;

	return 0;
}

static void
vips_colourspace_class_init(VipsColourspaceClass *class)
{
	GObjectClass *gobject_class = G_OBJECT_CLASS(class);
	VipsObjectClass *vobject_class = VIPS_OBJECT_CLASS(class);
	VipsOperationClass *operation_class = VIPS_OPERATION_CLASS(class);

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

	vobject_class->nickname = "colourspace";
	vobject_class->description = _("convert to a new colorspace");
	vobject_class->build = vips_colourspace_build;

	operation_class->flags = VIPS_OPERATION_SEQUENTIAL;

	VIPS_ARG_IMAGE(class, "in", 1,
		_("Input"),
		_("Input image"),
		VIPS_ARGUMENT_REQUIRED_INPUT,
		G_STRUCT_OFFSET(VipsColourspace, in));

	VIPS_ARG_IMAGE(class, "out", 2,
		_("Output"),
		_("Output image"),
		VIPS_ARGUMENT_REQUIRED_OUTPUT,
		G_STRUCT_OFFSET(VipsColourspace, out));

	VIPS_ARG_ENUM(class, "space", 6,
		_("Space"),
		_("Destination color space"),
		VIPS_ARGUMENT_REQUIRED_INPUT,
		G_STRUCT_OFFSET(VipsColourspace, space),
		VIPS_TYPE_INTERPRETATION, VIPS_INTERPRETATION_sRGB);

	VIPS_ARG_ENUM(class, "source_space", 6,
		_("Source space"),
		_("Source color space"),
		VIPS_ARGUMENT_OPTIONAL_INPUT,
		G_STRUCT_OFFSET(VipsColourspace, source_space),
		VIPS_TYPE_INTERPRETATION, VIPS_INTERPRETATION_sRGB);
}

static void
vips_colourspace_init(VipsColourspace *colourspace)
{
	colourspace->source_space = VIPS_INTERPRETATION_sRGB;
}

/**
 * vips_colourspace: (method)
 * @in: input image
 * @out: (out): output image
 * @space: convert to this colour space
 * @...: %NULL-terminated list of optional named arguments
 *
 * Optional arguments:
 *
 * * @source_space: input colour space
 *
 * This operation looks at the interpretation field of @in (or uses
 * @source_space, if set) and runs
 * a set of colourspace conversion functions to move it to @space.
 *
 * For example, given an image tagged as #VIPS_INTERPRETATION_YXY, running
 * vips_colourspace() with @space set to #VIPS_INTERPRETATION_LAB will
 * convert with vips_Yxy2XYZ() and vips_XYZ2Lab().
 *
 * See also: vips_colourspace_issupported(),
 * vips_image_guess_interpretation().
 *
 * Returns: 0 on success, -1 on error.
 */
int
vips_colourspace(VipsImage *in, VipsImage **out,
	VipsInterpretation space, ...)
{
	va_list ap;
	int result;

	va_start(ap, space);
	result = vips_call_split("colourspace", ap, in, out, space);
	va_end(ap);

	return result;
}
