/* 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 <vips/intl.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:
 * @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 );
}
