// Copyright (C) 2004-2025 Artifex Software, Inc.
//
// This file is part of MuPDF.
//
// MuPDF is free software: you can redistribute it and/or modify it under the
// terms of the GNU Affero General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// MuPDF 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 Affero General Public License for more
// details.
//
// You should have received a copy of the GNU Affero General Public License
// along with MuPDF. If not, see <https://www.gnu.org/licenses/agpl-3.0.en.html>
//
// Alternative licensing terms are available from the licensor.
// For commercial licensing, see <https://www.artifex.com/> or contact
// Artifex Software, Inc., 39 Mesa Street, Suite 108A, San Francisco,
// CA 94129, USA, for further information.

#include "mupdf/fitz.h"
#include "mupdf/pdf.h"

#include <string.h>

static fz_colorspace *pdf_load_colorspace_imp(fz_context *ctx, pdf_obj *obj, pdf_cycle_list *cycle_up);

/* ICCBased */
static fz_colorspace *
load_icc_based(fz_context *ctx, pdf_obj *dict, int allow_alt, pdf_cycle_list *cycle_up)
{
	int n = pdf_dict_get_int(ctx, dict, PDF_NAME(N));
	fz_colorspace *alt = NULL;
	fz_colorspace *cs = NULL;
	pdf_obj *obj;

	fz_var(alt);
	fz_var(cs);

	/* Look at Alternate to detect type (especially Lab). */
	if (allow_alt)
	{
		obj = pdf_dict_get(ctx, dict, PDF_NAME(Alternate));
		if (obj)
		{
			fz_try(ctx)
				alt = pdf_load_colorspace_imp(ctx, obj, cycle_up);
			fz_catch(ctx)
			{
				fz_rethrow_if(ctx, FZ_ERROR_TRYLATER);
				fz_rethrow_if(ctx, FZ_ERROR_SYSTEM);
				fz_report_error(ctx);
				fz_warn(ctx, "ignoring broken ICC Alternate colorspace");
			}
		}
	}

#if FZ_ENABLE_ICC
	{
		fz_buffer *buf = NULL;
		fz_var(buf);
		fz_try(ctx)
		{
			buf = pdf_load_stream(ctx, dict);
			cs = fz_new_icc_colorspace(ctx, alt ? alt->type : FZ_COLORSPACE_NONE, 0, NULL, buf);
			if (cs->n > n)
			{
				fz_warn(ctx, "ICC colorspace N=%d does not match profile N=%d (ignoring profile)", n, cs->n);
				fz_drop_colorspace(ctx, cs);
				cs = NULL;
			}
			else if (cs->n < n)
			{
				fz_warn(ctx, "ICC colorspace N=%d does not match profile N=%d (using profile)", n, cs->n);
			}
		}
		fz_always(ctx)
			fz_drop_buffer(ctx, buf);
		fz_catch(ctx)
		{
			if (fz_caught(ctx) == FZ_ERROR_TRYLATER || fz_caught(ctx) == FZ_ERROR_SYSTEM)
			{
				fz_drop_colorspace(ctx, alt);
				fz_rethrow(ctx);
			}
			fz_report_error(ctx);
			fz_warn(ctx, "ignoring broken ICC profile");
		}
	}
#endif

	if (!cs)
		cs = alt;
	else
		fz_drop_colorspace(ctx, alt);

	if (!cs)
	{
		if (n == 1) cs = fz_keep_colorspace(ctx, fz_device_gray(ctx));
		else if (n == 3) cs = fz_keep_colorspace(ctx, fz_device_rgb(ctx));
		else if (n == 4) cs = fz_keep_colorspace(ctx, fz_device_cmyk(ctx));
		else fz_throw(ctx, FZ_ERROR_SYNTAX, "invalid ICC colorspace");
	}

	return cs;
}

static void
devicen_eval(fz_context *ctx, void *tint, const float *sv, int sn, float *dv, int dn)
{
	pdf_eval_function(ctx, tint, sv, sn, dv, dn);
}

static void
devicen_drop(fz_context *ctx, void *tint)
{
	pdf_drop_function(ctx, tint);
}

static fz_colorspace *
load_devicen(fz_context *ctx, pdf_obj *array, int is_devn, pdf_cycle_list *cycle_up)
{
	fz_colorspace *base = NULL;
	fz_colorspace *cs = NULL;
	pdf_obj *nameobj = pdf_array_get(ctx, array, 1);
	pdf_obj *baseobj = pdf_array_get(ctx, array, 2);
	pdf_obj *tintobj = pdf_array_get(ctx, array, 3);
	char name[100];
	int i, n;

	fz_var(cs);

	if (pdf_is_array(ctx, nameobj))
	{
		n = pdf_array_len(ctx, nameobj);
		if (n < 1)
			fz_throw(ctx, FZ_ERROR_SYNTAX, "too few components in DeviceN colorspace");
		if (n > FZ_MAX_COLORS)
			fz_throw(ctx, FZ_ERROR_SYNTAX, "too many components in DeviceN colorspace");
	}
	else
	{
		n = 1;
	}

	base = pdf_load_colorspace_imp(ctx, baseobj, cycle_up);
	fz_try(ctx)
	{
		if (is_devn)
		{
			fz_snprintf(name, sizeof name, "DeviceN(%d,%s", n, base->name);
			for (i = 0; i < n; i++) {
				fz_strlcat(name, ",", sizeof name);
				fz_strlcat(name, pdf_array_get_name(ctx, nameobj, i), sizeof name);
			}
			fz_strlcat(name, ")", sizeof name);
		}
		else
		{
			fz_snprintf(name, sizeof name, "Separation(%s,%s)", base->name, pdf_to_name(ctx, nameobj));
		}

		cs = fz_new_colorspace(ctx, FZ_COLORSPACE_SEPARATION, 0, n, name);
		cs->u.separation.eval = devicen_eval;
		cs->u.separation.drop = devicen_drop;
		cs->u.separation.base = fz_keep_colorspace(ctx, base);
		cs->u.separation.tint = pdf_load_function(ctx, tintobj, n, cs->u.separation.base->n);
		if (pdf_is_array(ctx, nameobj))
			for (i = 0; i < n; i++)
				fz_colorspace_name_colorant(ctx, cs, i, pdf_array_get_name(ctx, nameobj, i));
		else
			fz_colorspace_name_colorant(ctx, cs, 0, pdf_to_name(ctx, nameobj));
	}
	fz_always(ctx)
	{
		fz_drop_colorspace(ctx, base);
	}
	fz_catch(ctx)
	{
		fz_drop_colorspace(ctx, cs);
		fz_rethrow(ctx);
	}

	return cs;
}

int
pdf_is_tint_colorspace(fz_context *ctx, fz_colorspace *cs)
{
	return cs && cs->type == FZ_COLORSPACE_SEPARATION;
}

/* Indexed */

static fz_colorspace *
load_indexed(fz_context *ctx, pdf_obj *array, pdf_cycle_list *cycle_up)
{
	pdf_obj *baseobj = pdf_array_get(ctx, array, 1);
	pdf_obj *highobj = pdf_array_get(ctx, array, 2);
	pdf_obj *lookupobj = pdf_array_get(ctx, array, 3);
	fz_colorspace *base = NULL;
	fz_colorspace *cs;
	size_t i, n;
	int high;
	unsigned char *lookup = NULL;

	fz_var(base);
	fz_var(lookup);

	fz_try(ctx)
	{
		base = pdf_load_colorspace_imp(ctx, baseobj, cycle_up);

		high = pdf_to_int(ctx, highobj);
		high = fz_clampi(high, 0, 255);
		n = (size_t)base->n * (high + 1);
		lookup = Memento_label(fz_malloc(ctx, n), "cs_lookup");

		if (pdf_is_string(ctx, lookupobj))
		{
			size_t sn = fz_minz(n, pdf_to_str_len(ctx, lookupobj));
			unsigned char *buf = (unsigned char *) pdf_to_str_buf(ctx, lookupobj);
			for (i = 0; i < sn; ++i)
				lookup[i] = buf[i];
			for (; i < n; ++i)
				lookup[i] = 0;
		}
		else if (pdf_is_indirect(ctx, lookupobj))
		{
			fz_stream *file = NULL;

			fz_var(file);

			fz_try(ctx)
			{
				file = pdf_open_stream(ctx, lookupobj);
				i = fz_read(ctx, file, lookup, n);
				if (i < n)
					memset(lookup+i, 0, n-i);
			}
			fz_always(ctx)
			{
				fz_drop_stream(ctx, file);
			}
			fz_catch(ctx)
			{
				fz_rethrow(ctx);
			}
		}
		else
		{
			fz_throw(ctx, FZ_ERROR_SYNTAX, "cannot parse colorspace lookup table");
		}

		cs = fz_new_indexed_colorspace(ctx, base, high, lookup);
	}
	fz_always(ctx)
		fz_drop_colorspace(ctx, base);
	fz_catch(ctx)
	{
		fz_free(ctx, lookup);
		fz_rethrow(ctx);
	}

	return cs;
}

static void
pdf_load_cal_common(fz_context *ctx, pdf_obj *dict, float *wp, float *bp, float *gamma)
{
	pdf_obj *obj;
	int i;

	obj = pdf_dict_get(ctx, dict, PDF_NAME(WhitePoint));
	if (pdf_array_len(ctx, obj) != 3)
		fz_throw(ctx, FZ_ERROR_SYNTAX, "WhitePoint must be a 3-element array");

	for (i = 0; i < 3; i++)
	{
		wp[i] = pdf_array_get_real(ctx, obj, i);
		if (wp[i] < 0)
			fz_throw(ctx, FZ_ERROR_SYNTAX, "WhitePoint numbers must be positive");
	}
	if (wp[1] != 1)
		fz_throw(ctx, FZ_ERROR_SYNTAX, "WhitePoint Yw must be 1.0");

	obj = pdf_dict_get(ctx, dict, PDF_NAME(BlackPoint));
	if (pdf_array_len(ctx, obj) == 3)
	{
		for (i = 0; i < 3; i++)
		{
			bp[i] = pdf_array_get_real(ctx, obj, i);
			if (bp[i] < 0)
				fz_throw(ctx, FZ_ERROR_SYNTAX, "BlackPoint numbers must be positive");
		}
	}

	obj = pdf_dict_get(ctx, dict, PDF_NAME(Gamma));
	if (pdf_is_number(ctx, obj))
	{
		gamma[0] = pdf_to_real(ctx, obj);
		gamma[1] = gamma[2];
		if (gamma[0] <= 0)
			fz_throw(ctx, FZ_ERROR_SYNTAX, "Gamma must be greater than zero");
	}
	else if (pdf_array_len(ctx, obj) == 3)
	{
		for (i = 0; i < 3; i++)
		{
			gamma[i] = pdf_array_get_real(ctx, obj, i);
			if (gamma[i] <= 0)
				fz_throw(ctx, FZ_ERROR_SYNTAX, "Gamma must be greater than zero");
		}
	}
}

static fz_colorspace *
pdf_load_cal_gray(fz_context *ctx, pdf_obj *dict)
{
	float wp[3];
	float bp[3] = { 0, 0, 0 };
	float gamma[3] = { 1, 1, 1 };

	if (dict == NULL)
		return fz_keep_colorspace(ctx, fz_device_gray(ctx));

	fz_try(ctx)
		pdf_load_cal_common(ctx, dict, wp, bp, gamma);
	fz_catch(ctx)
		return fz_keep_colorspace(ctx, fz_device_gray(ctx));
	return fz_new_cal_gray_colorspace(ctx, wp, bp, gamma[0]);
}

static fz_colorspace *
pdf_load_cal_rgb(fz_context *ctx, pdf_obj *dict)
{
	pdf_obj *obj;
	float matrix[9] = { 1, 0, 0, 0, 1, 0, 0, 0, 1 };
	float wp[3];
	float bp[3] = { 0, 0, 0 };
	float gamma[3] = { 1, 1, 1 };
	int i;

	if (dict == NULL)
		return fz_keep_colorspace(ctx, fz_device_rgb(ctx));

	fz_try(ctx)
	{
		pdf_load_cal_common(ctx, dict, wp, bp, gamma);
		obj = pdf_dict_get(ctx, dict, PDF_NAME(Matrix));
		if (pdf_array_len(ctx, obj) == 9)
		{
			for (i = 0; i < 9; i++)
				matrix[i] = pdf_array_get_real(ctx, obj, i);
		}
	}
	fz_catch(ctx)
		return fz_keep_colorspace(ctx, fz_device_rgb(ctx));
	return fz_new_cal_rgb_colorspace(ctx, wp, bp, gamma, matrix);
}

/* Parse and create colorspace from PDF object */

static fz_colorspace *
pdf_load_colorspace_imp(fz_context *ctx, pdf_obj *obj, pdf_cycle_list *cycle_up)
{
	fz_colorspace *cs = NULL;
	pdf_cycle_list cycle;

	if (pdf_cycle(ctx, &cycle, cycle_up, obj))
		fz_throw(ctx, FZ_ERROR_SYNTAX, "recursive colorspace");

	if (pdf_is_name(ctx, obj))
	{
		if (pdf_name_eq(ctx, obj, PDF_NAME(Pattern)))
			return fz_keep_colorspace(ctx, fz_device_gray(ctx));
		else if (pdf_name_eq(ctx, obj, PDF_NAME(G)))
			return fz_keep_colorspace(ctx, fz_device_gray(ctx));
		else if (pdf_name_eq(ctx, obj, PDF_NAME(RGB)))
			return fz_keep_colorspace(ctx, fz_device_rgb(ctx));
		else if (pdf_name_eq(ctx, obj, PDF_NAME(CMYK)))
			return fz_keep_colorspace(ctx, fz_device_cmyk(ctx));
		else if (pdf_name_eq(ctx, obj, PDF_NAME(DeviceGray)))
			return fz_keep_colorspace(ctx, fz_device_gray(ctx));
		else if (pdf_name_eq(ctx, obj, PDF_NAME(DeviceRGB)))
			return fz_keep_colorspace(ctx, fz_device_rgb(ctx));
		else if (pdf_name_eq(ctx, obj, PDF_NAME(DeviceCMYK)))
			return fz_keep_colorspace(ctx, fz_device_cmyk(ctx));
		else
			fz_throw(ctx, FZ_ERROR_SYNTAX, "unknown colorspace: %s", pdf_to_name(ctx, obj));
	}

	else if (pdf_is_array(ctx, obj))
	{
		pdf_obj *name = pdf_array_get(ctx, obj, 0);

		if (pdf_is_name(ctx, name))
		{
			/* load base colorspace instead */
			if (pdf_name_eq(ctx, name, PDF_NAME(G)))
				return fz_keep_colorspace(ctx, fz_device_gray(ctx));
			else if (pdf_name_eq(ctx, name, PDF_NAME(RGB)))
				return fz_keep_colorspace(ctx, fz_device_rgb(ctx));
			else if (pdf_name_eq(ctx, name, PDF_NAME(CMYK)))
				return fz_keep_colorspace(ctx, fz_device_cmyk(ctx));
			else if (pdf_name_eq(ctx, name, PDF_NAME(DeviceGray)))
				return fz_keep_colorspace(ctx, fz_device_gray(ctx));
			else if (pdf_name_eq(ctx, name, PDF_NAME(DeviceRGB)))
				return fz_keep_colorspace(ctx, fz_device_rgb(ctx));
			else if (pdf_name_eq(ctx, name, PDF_NAME(DeviceCMYK)))
				return fz_keep_colorspace(ctx, fz_device_cmyk(ctx));
			else if (pdf_name_eq(ctx, name, PDF_NAME(CalCMYK)))
				return fz_keep_colorspace(ctx, fz_device_cmyk(ctx));
			else if (pdf_name_eq(ctx, name, PDF_NAME(Lab)))
				return fz_keep_colorspace(ctx, fz_device_lab(ctx));

			else
			{
				if ((cs = pdf_find_item(ctx, fz_drop_colorspace_imp, obj)) != NULL)
					return cs;

				if (pdf_name_eq(ctx, name, PDF_NAME(ICCBased)))
					cs = load_icc_based(ctx, pdf_array_get(ctx, obj, 1), 1, &cycle);

				else if (pdf_name_eq(ctx, name, PDF_NAME(CalGray)))
					cs = pdf_load_cal_gray(ctx, pdf_array_get(ctx, obj, 1));
				else if (pdf_name_eq(ctx, name, PDF_NAME(CalRGB)))
					cs = pdf_load_cal_rgb(ctx, pdf_array_get(ctx, obj, 1));

				else if (pdf_name_eq(ctx, name, PDF_NAME(Indexed)))
					cs = load_indexed(ctx, obj, &cycle);
				else if (pdf_name_eq(ctx, name, PDF_NAME(I)))
					cs = load_indexed(ctx, obj, &cycle);

				else if (pdf_name_eq(ctx, name, PDF_NAME(Separation)))
					cs = load_devicen(ctx, obj, 0, &cycle);

				else if (pdf_name_eq(ctx, name, PDF_NAME(DeviceN)))
					cs = load_devicen(ctx, obj, 1, &cycle);

				else if (pdf_name_eq(ctx, name, PDF_NAME(Pattern)))
				{
					pdf_obj *pobj = pdf_array_get(ctx, obj, 1);
					if (!pobj)
						return fz_keep_colorspace(ctx, fz_device_gray(ctx));
					cs = pdf_load_colorspace_imp(ctx, pobj, &cycle);
				}
				else
					fz_throw(ctx, FZ_ERROR_SYNTAX, "unknown colorspace %s", pdf_to_name(ctx, name));

				pdf_store_item(ctx, obj, cs, 1000);
				return cs;
			}
		}
	}

	/* We have seen files where /DefaultRGB is specified as 1 0 R,
	 * and 1 0 obj << /Length 3144 /Alternate /DeviceRGB /N 3 >>
	 * stream ...iccprofile... endstream endobj.
	 * This *should* be [ /ICCBased 1 0 R ], but Acrobat seems to
	 * handle it, so do our best. */
	else if (pdf_is_dict(ctx, obj))
	{
		if ((cs = pdf_find_item(ctx, fz_drop_colorspace_imp, obj)) != NULL)
			return cs;
		cs = load_icc_based(ctx, obj, 1, &cycle);
		pdf_store_item(ctx, obj, cs, 1000);
		return cs;
	}

	fz_throw(ctx, FZ_ERROR_SYNTAX, "could not parse color space (%d 0 R)", pdf_to_num(ctx, obj));
}

fz_colorspace *
pdf_load_colorspace(fz_context *ctx, pdf_obj *obj)
{
	return pdf_load_colorspace_imp(ctx, obj, NULL);
}

#if FZ_ENABLE_ICC

static fz_colorspace *
pdf_load_output_intent(fz_context *ctx, pdf_document *doc)
{
	pdf_obj *root = pdf_dict_get(ctx, pdf_trailer(ctx, doc), PDF_NAME(Root));
	pdf_obj *intents = pdf_dict_get(ctx, root, PDF_NAME(OutputIntents));
	pdf_obj *intent_dict;
	pdf_obj *dest_profile;
	fz_colorspace *cs = NULL;

	/* An array of intents */
	if (!intents)
		return NULL;

	/* For now, always just use the first intent. I have never even seen a file
	 * with multiple intents but it could happen */
	intent_dict = pdf_array_get(ctx, intents, 0);
	if (!intent_dict)
		return NULL;
	dest_profile = pdf_dict_get(ctx, intent_dict, PDF_NAME(DestOutputProfile));
	if (!dest_profile)
		return NULL;

	fz_var(cs);

	fz_try(ctx)
		cs = load_icc_based(ctx, dest_profile, 0, NULL);
	fz_catch(ctx)
	{
		fz_rethrow_if(ctx, FZ_ERROR_TRYLATER);
		fz_rethrow_if(ctx, FZ_ERROR_SYSTEM);
		fz_report_error(ctx);
		fz_warn(ctx, "Attempt to read Output Intent failed");
	}
	return cs;
}

fz_colorspace *
pdf_document_output_intent(fz_context *ctx, pdf_document *doc)
{
	if (!doc->oi)
		doc->oi = pdf_load_output_intent(ctx, doc);
	return doc->oi;
}

#else

fz_colorspace *
pdf_document_output_intent(fz_context *ctx, pdf_document *doc)
{
	return NULL;
}

#endif

static pdf_obj *
pdf_add_indexed_colorspace(fz_context *ctx, pdf_document *doc, fz_colorspace *cs)
{
	fz_colorspace *basecs;
	unsigned char *lookup = NULL;
	int high = 0;
	int basen;
	pdf_obj *obj;

	basecs = cs->u.indexed.base;
	high = cs->u.indexed.high;
	lookup = cs->u.indexed.lookup;
	basen = basecs->n;

	if (fz_colorspace_is_indexed(ctx, basecs))
		fz_throw(ctx, FZ_ERROR_FORMAT, "indexed colorspaces must not have an indexed colorspace as base");

	obj = pdf_add_new_array(ctx, doc, 4);
	fz_try(ctx)
	{
		pdf_array_push(ctx, obj, PDF_NAME(Indexed));
		pdf_array_push(ctx, obj, pdf_add_colorspace(ctx, doc, basecs));
		pdf_array_push_int(ctx, obj, high);
		pdf_array_push_string(ctx, obj, (char *) lookup, (size_t)basen * (high + 1));
	}
	fz_catch(ctx)
	{
		pdf_drop_obj(ctx, obj);
		fz_rethrow(ctx);
	}
	return obj;
}

static pdf_obj *
pdf_add_icc_colorspace(fz_context *ctx, pdf_document *doc, fz_colorspace *cs)
{
#if FZ_ENABLE_ICC
	pdf_obj *obj = NULL;
	pdf_obj *stm = NULL;
	pdf_obj *base;
	pdf_obj *ref;
	pdf_colorspace_resource_key key;

	fz_var(obj);
	fz_var(stm);

	obj = pdf_find_colorspace_resource(ctx, doc, cs, &key);
	if (obj)
		return obj;

	switch (fz_colorspace_type(ctx, cs))
	{
	default:
		fz_throw(ctx, FZ_ERROR_UNSUPPORTED, "only Gray, RGB, and CMYK ICC colorspaces supported");
	case FZ_COLORSPACE_GRAY:
		base = PDF_NAME(DeviceGray);
		break;
	case FZ_COLORSPACE_RGB:
		base = PDF_NAME(DeviceRGB);
		break;
	case FZ_COLORSPACE_CMYK:
		base = PDF_NAME(DeviceCMYK);
		break;
	case FZ_COLORSPACE_LAB:
		base = PDF_NAME(Lab);
		break;
	}

	fz_try(ctx)
	{
		stm = pdf_add_stream(ctx, doc, cs->u.icc.buffer, NULL, 0);
		pdf_dict_put_int(ctx, stm, PDF_NAME(N), cs->n);
		pdf_dict_put(ctx, stm, PDF_NAME(Alternate), base);

		obj = pdf_add_new_array(ctx, doc, 2);
		pdf_array_push(ctx, obj, PDF_NAME(ICCBased));
		pdf_array_push(ctx, obj, stm);

		ref = pdf_insert_colorspace_resource(ctx, doc, &key, obj);
	}
	fz_always(ctx)
	{
		pdf_drop_obj(ctx, obj);
		pdf_drop_obj(ctx, stm);
	}
	fz_catch(ctx)
		fz_rethrow(ctx);
	return ref;
#else
	fz_throw(ctx, FZ_ERROR_UNSUPPORTED, "ICC support disabled");
#endif
}

pdf_obj *
pdf_add_colorspace(fz_context *ctx, pdf_document *doc, fz_colorspace *cs)
{
	if (fz_colorspace_is_indexed(ctx, cs))
		return pdf_add_indexed_colorspace(ctx, doc, cs);

	if (fz_colorspace_is_icc(ctx, cs))
		return pdf_add_icc_colorspace(ctx, doc, cs);

	switch (fz_colorspace_type(ctx, cs))
	{
	case FZ_COLORSPACE_NONE:
	case FZ_COLORSPACE_GRAY:
		return PDF_NAME(DeviceGray);
	case FZ_COLORSPACE_RGB:
		return PDF_NAME(DeviceRGB);
	case FZ_COLORSPACE_CMYK:
		return PDF_NAME(DeviceCMYK);
	case FZ_COLORSPACE_LAB:
		return PDF_NAME(Lab);
	default:
		fz_throw(ctx, FZ_ERROR_ARGUMENT, "only Gray, RGB, and CMYK colorspaces supported");
		break;
	}
}
