/* load SVG with librsvg
 *
 * 7/2/16
 * 	- from svgload.c
 * 1/8/16 felixbuenemann
 * 	- add svgz support
 * 18/1/17
 * 	- invalidate operation on read error
 * 8/7/17
 * 	- fix DPI mixup, thanks Fosk
 * 9/9/17
 * 	- limit max tile width to 30k pixels to prevent overflow in render
 * 17/9/17 lovell
 * 	- handle scaling of svg files missing width and height attributes
 * 22/3/18 lovell
 * 	- svgload was missing is_a
 * 28/6/19
 * 	- add "unlimited"
 * 	- requires us to use the gio API to librsvg
 * 11/9/19
 * 	- rework as a sequential loader to reduce overcomputation
 * 11/6/21
 * 	- switch to rsvg_handle_render_document()
 * 	- librsvg can no longer render very large images :(
 * 14/10/21
 * 	- allow utf-8 headers for svg detection
 * 28/4/22
 * 	- support rsvg_handle_get_intrinsic_size_in_pixels()
 * 5/6/22
 * 	- allow random access
 * 20/5/25
 * 	- support high bitdepth rendering (128-bit)
 */

/*

	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 <string.h>
#include <errno.h>
#include <ctype.h>

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

#if defined(HAVE_RSVG)

#include <cairo.h>
#include <librsvg/rsvg.h>

/* Render SVGs with tiles this size. They need to be pretty big to limit
 * overcomputation.
 */
#define TILE_SIZE (2000)

/* The <svg tag must appear within this many bytes of the start of the file.
 */
#define SVG_HEADER_SIZE (1000)

/* A handy #define for we-will-handle-svgz.
 */
#if LIBRSVG_CHECK_FEATURE(SVGZ) && defined(HAVE_ZLIB)
#define HANDLE_SVGZ
#endif

#ifdef HANDLE_SVGZ
#include <zlib.h>
#endif

typedef struct _VipsForeignLoadSvg {
	VipsForeignLoad parent_object;

	/* Render at this DPI.
	 */
	double dpi;

	/* Scale by this factor.
	 */
	double scale;

	/* The total scale factor we render with.
	 */
	double total_scale;

	/* Allow SVGs of any size.
	 */
	gboolean unlimited;

	/* Enables scRGB 128-bit output (32-bit per channel).
	 */
	gboolean high_bitdepth;

	/* Custom CSS.
	 */
	const char *stylesheet;

	RsvgHandle *page;

} VipsForeignLoadSvg;

typedef VipsForeignLoadClass VipsForeignLoadSvgClass;

G_DEFINE_ABSTRACT_TYPE(VipsForeignLoadSvg, vips_foreign_load_svg,
	VIPS_TYPE_FOREIGN_LOAD);

#ifdef HANDLE_SVGZ
static void *
vips_foreign_load_svg_zalloc(void *opaque, unsigned items, unsigned size)
{
	return g_malloc0_n(items, size);
}

static void
vips_foreign_load_svg_zfree(void *opaque, void *ptr)
{
	return g_free(ptr);
}
#endif /*HANDLE_SVGZ*/

/* Find a utf-8 substring within the first len_bytes (not characters).
 *
 *   - case-insensitive
 *   - needle must be zero-terminated, but haystack need not be
 *   - haystack can be null-terminated
 *   - if haystack is shorter than len bytes, that'll end the search
 *   - if we hit invalid utf-8, we return NULL
 */
static const char *
vips_utf8_strcasestr(const char *haystack_start, const char *needle_start,
	int len_bytes)
{
	int needle_len = g_utf8_strlen(needle_start, -1);
	int needle_len_bytes = strlen(needle_start);

	const char *haystack;

	for (haystack = haystack_start;
		 haystack - haystack_start <= len_bytes - needle_len_bytes;
		 haystack = g_utf8_find_next_char(haystack, NULL)) {
		const char *needle_char;
		const char *haystack_char;
		int i;

		haystack_char = haystack;
		needle_char = needle_start;
		for (i = 0; i < needle_len; i++) {
			/* Haystack isn't necessarily null-terminated and
			 * might end half-way through a utf-8 character, so we
			 * need to be careful not to run off the end.
			 */
			gunichar a =
				g_utf8_get_char_validated(haystack_char,
					haystack_start + len_bytes - haystack);
			gunichar b =
				g_utf8_get_char_validated(needle_char, -1);

			/* Invalid utf8?
			 *
			 * gunichar is a uint32, so we can't compare < 0, we
			 * have to look for -1 and -2 (the two possible error
			 * values).
			 */
			if (a == (gunichar) -1 ||
				a == (gunichar) -2 ||
				b == (gunichar) -1 ||
				b == (gunichar) -2)
				return NULL;

#if !GLIB_CHECK_VERSION(2, 63, 0)
			/* Disallow codepoint U+0000 as it's a nul byte.
			 * This is redundant with GLib >= 2.63.0, see:
			 * https://gitlab.gnome.org/GNOME/glib/-/merge_requests/967
			 */
			if (a == (gunichar) 0)
				return NULL;
#endif

			/* Mismatch.
			 */
			if (g_unichar_tolower(a) != g_unichar_tolower(b))
				break;

			haystack_char =
				g_utf8_find_next_char(haystack_char,
					haystack_start + len_bytes);

			/* End of haystack. There can't be a complete needle
			 * anywhere.
			 */
			if (haystack_char == NULL)
				return NULL;

			/* needle_char will never be NULL.
			 */
			needle_char =
				g_utf8_find_next_char(needle_char, NULL);
		}

		if (i == needle_len)
			/* Walked the whole of needle, so we must have found a
			 * complete match.
			 */
			return haystack;
	}

	/* Walked the whole of haystack without finding a match.
	 */
	return NULL;
}

/* This is used by both the file and buffer subclasses.
 */
static gboolean
vips_foreign_load_svg_is_a(const void *buf, size_t len)
{
	char *str;

#ifdef HANDLE_SVGZ
	/* If the buffer looks like a zip, deflate to here and then search
	 * that for <svg.
	 */
	char obuf[SVG_HEADER_SIZE];
#endif /*HANDLE_SVGZ*/

	/* Start with str pointing at the argument buffer, swap to it pointing
	 * into obuf if we see zip data.
	 */
	str = (char *) buf;

#ifdef HANDLE_SVGZ
	/* Check for SVGZ gzip signature and inflate.
	 *
	 * Minimum gzip size is 18 bytes, starting with 1F 8B.
	 */
	if (len >= 18 &&
		str[0] == '\037' &&
		str[1] == '\213') {
		z_stream zs;
		size_t opos;
		int err;

		zs.zalloc = (alloc_func) vips_foreign_load_svg_zalloc;
		zs.zfree = (free_func) vips_foreign_load_svg_zfree;
		zs.opaque = Z_NULL;
		zs.next_in = (unsigned char *) str;
		zs.avail_in = len;

		/* There isn't really an error return from is_a_buffer()
		 */
		if (inflateInit2(&zs, 15 | 32) != Z_OK)
			return FALSE;

		opos = 0;
		do {
			zs.avail_out = sizeof(obuf) - opos;
			zs.next_out = (unsigned char *) obuf + opos;
			err = inflate(&zs, Z_NO_FLUSH);

			/* Always update opos after inflate(), even if stream ended.
			 */
			opos = sizeof(obuf) - zs.avail_out;

			if (err == Z_STREAM_END)
				break;
			if (err != Z_OK) {
				inflateEnd(&zs);
				return FALSE;
			}
		} while (opos < sizeof(obuf) &&
			zs.avail_in > 0);

		inflateEnd(&zs);

		str = obuf;
		len = opos;
	}
#endif /*HANDLE_SVGZ*/

	/* SVG documents are very freeform. They normally look like:
	 *
	 * <?xml version="1.0" encoding="UTF-8"?>
	 * <svg xmlns="http://www.w3.org/2000/svg" ...
	 *
	 * But there can be a doctype in there too. And case and whitespace can
	 * vary a lot. And the <?xml can be missing. And you can have a comment
	 * before the <svg line. And it can be utf-8, so non ASCII characters.
	 *
	 * All we do is look for "<svg", any case, within the first
	 * SVG_HEADER_SIZE bytes, where the bytestream up to the "<svg" is
	 * valid utf-8.
	 *
	 * We could rsvg_handle_new_from_data() on the buffer, but that can be
	 * horribly slow for large documents.
	 */
	if (vips_utf8_strcasestr(str, "<svg", len))
		return TRUE;

	return FALSE;
}

static void
vips_foreign_load_svg_dispose(GObject *gobject)
{
	VipsForeignLoadSvg *svg = (VipsForeignLoadSvg *) gobject;

	VIPS_UNREF(svg->page);

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

static int
vips_foreign_load_svg_build(VipsObject *object)
{
	VipsForeignLoadSvg *svg G_GNUC_UNUSED = (VipsForeignLoadSvg *) object;

#ifdef DEBUG
	printf("vips_foreign_load_svg_build:\n");
#endif /*DEBUG*/

#ifndef HAVE_CAIRO_FORMAT_RGBA128F
	if (svg->high_bitdepth) {
		g_warning("ignoring high_bitdepth");
		svg->high_bitdepth = FALSE;
	}
#endif /*HAVE_CAIRO_FORMAT_RGBA128F*/

	svg->total_scale = svg->scale * svg->dpi / 72.0;

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

static VipsForeignFlags
vips_foreign_load_svg_get_flags_filename(const char *filename)
{
	/* We can render any part of the page on demand.
	 */
	return VIPS_FOREIGN_PARTIAL;
}

static VipsForeignFlags
vips_foreign_load_svg_get_flags(VipsForeignLoad *load)
{
	return VIPS_FOREIGN_PARTIAL;
}

#if LIBRSVG_CHECK_VERSION(2, 52, 0)
/* Derived from `CssLength::to_user` in librsvg.
 * https://gitlab.gnome.org/GNOME/librsvg/-/blob/2.60.0/rsvg/src/length.rs#L403
 */
static double
svg_css_length_to_pixels(RsvgLength length, double dpi)
{
	double value = length.length;

	/* The following implies that our default font size is 12, which
	 * matches the default in librsvg.
	 */
	double font_size = 12.0;

	switch (length.unit) {
	case RSVG_UNIT_PX:
		/* Already a pixel value.
		 */
		break;
	case RSVG_UNIT_EM:
		value *= font_size;
		break;
	case RSVG_UNIT_EX:
		value *= font_size / 2.0;
		break;
	case RSVG_UNIT_IN:
		value *= dpi;
		break;
	case RSVG_UNIT_CM:
		/* 2.54 cm in an inch.
		 */
		value = dpi * value / 2.54;
		break;
	case RSVG_UNIT_MM:
		/* 25.4 mm in an inch.
		 */
		value = dpi * value / 25.4;
		break;
	case RSVG_UNIT_PT:
		/* 72 points in an inch.
		 */
		value = dpi * value / 72;
		break;
	case RSVG_UNIT_PC:
		/* 6 picas in an inch.
		 */
		value = dpi * value / 6;
		break;
	default:
		/* Probably RSVG_UNIT_PERCENT. We can't know what the
		 * pixel value is without more information.
		 */
		value = 0;
	}

	return value;
}
#endif

static int
vips_foreign_load_svg_get_natural_size(VipsForeignLoadSvg *svg,
	double *out_width, double *out_height)
{
	VipsObjectClass *class = VIPS_OBJECT_GET_CLASS(svg);

	double width;
	double height;

#if LIBRSVG_CHECK_VERSION(2, 52, 0)

	if (!rsvg_handle_get_intrinsic_size_in_pixels(svg->page,
			&width, &height)) {
		RsvgRectangle viewbox;

		/* Try the intrinsic dimensions first.
		 */
		gboolean has_width, has_height;
		RsvgLength iwidth, iheight;
		gboolean has_viewbox;

		rsvg_handle_get_intrinsic_dimensions(svg->page,
			&has_width, &iwidth,
			&has_height, &iheight,
			&has_viewbox, &viewbox);

#if LIBRSVG_CHECK_VERSION(2, 54, 0)
		/* After librsvg 2.54.0, the `has_width` and `has_height`
		 * arguments always returns `TRUE`, since with SVG2 all
		 * documents *have* a default width and height of `100%`.
		 */
		width = svg_css_length_to_pixels(iwidth, svg->dpi);
		height = svg_css_length_to_pixels(iheight, svg->dpi);

		has_width = width > 0.0;
		has_height = height > 0.0;

		if (has_width && has_height) {
			/* Success! Taking the viewbox into account is not
			 * needed.
			 */
		}
		else if (has_width && has_viewbox) {
			height = width * viewbox.height / viewbox.width;
		}
		else if (has_height && has_viewbox) {
			width = height * viewbox.width / viewbox.height;
		}
		else if (has_viewbox) {
			width = viewbox.width;
			height = viewbox.height;
		}
#else  /*!LIBRSVG_CHECK_VERSION(2, 54, 0)*/
		if (has_width && has_height) {
			/* We can use these values directly.
			 */
			width = svg_css_length_to_pixels(iwidth, svg->dpi);
			height = svg_css_length_to_pixels(iheight, svg->dpi);
		}
		else if (has_width && has_viewbox) {
			width = svg_css_length_to_pixels(iwidth, svg->dpi);
			height = width * viewbox.height / viewbox.width;
		}
		else if (has_height && has_viewbox) {
			height = svg_css_length_to_pixels(iheight, svg->dpi);
			width = height * viewbox.width / viewbox.height;
		}
		else if (has_viewbox) {
			width = viewbox.width;
			height = viewbox.height;
		}
#endif /*!LIBRSVG_CHECK_VERSION(2, 54, 0)*/

		if (width <= 0.0 ||
			height <= 0.0) {
			/* We haven't found a usable set of sizes, so try
			 * working out the visible area.
			 */
			rsvg_handle_get_geometry_for_element(svg->page, NULL,
				&viewbox, NULL, NULL);
			width = viewbox.x + viewbox.width;
			height = viewbox.y + viewbox.height;
		}
	}

#else /*!LIBRSVG_CHECK_VERSION(2, 52, 0)*/

	{
		RsvgDimensionData dimensions;

		rsvg_handle_get_dimensions(svg->page, &dimensions);
		width = dimensions.width;
		height = dimensions.height;
	}

#endif /*LIBRSVG_CHECK_VERSION(2, 52, 0)*/

	/* width or height below 0.5 can't be rounded to 1.
	 */
	if (width < 0.5 ||
		height < 0.5) {
		vips_error(class->nickname, "%s", _("bad dimensions"));
		return -1;
	}

	*out_width = width;
	*out_height = height;

	return 0;
}

static int
vips_foreign_load_svg_get_scaled_size(VipsForeignLoadSvg *svg,
	int *out_width, int *out_height)
{
	double width;
	double height;

	/* Set target DPI to scale non-pixel units correctly.
	 */
	rsvg_handle_set_dpi(svg->page, svg->dpi);
	if (vips_foreign_load_svg_get_natural_size(svg, &width, &height))
		return -1;

	width *= svg->total_scale;
	height *= svg->total_scale;

	*out_width = VIPS_ROUND_UINT(width);
	*out_height = VIPS_ROUND_UINT(height);

	return 0;
}

static int
vips_foreign_load_svg_parse(VipsForeignLoadSvg *svg, VipsImage *out)
{
	VipsObjectClass *class = VIPS_OBJECT_GET_CLASS(svg);

	int width;
	int height;
	double res;

	if (vips_foreign_load_svg_get_scaled_size(svg, &width, &height))
		return -1;
	if (width <= 0 ||
		height <= 0) {
		vips_error(class->nickname, "%s", _("zero-sized image"));
		return -1;
	}

	/* We need pixels/mm for vips.
	 */
	res = svg->dpi / 25.4;

	vips_image_init_fields(out,
		width, height, 4,
		svg->high_bitdepth ? VIPS_FORMAT_FLOAT : VIPS_FORMAT_UCHAR,
		VIPS_CODING_NONE,
		svg->high_bitdepth ?
			VIPS_INTERPRETATION_scRGB : VIPS_INTERPRETATION_sRGB,
		res, res);

	/* We use a tilecache, so it's smalltile.
	 */
	if (vips_image_pipelinev(out, VIPS_DEMAND_STYLE_SMALLTILE, NULL))
		return -1;

	return 0;
}

static int
vips_foreign_load_svg_header(VipsForeignLoad *load)
{
	VipsForeignLoadSvg *svg = (VipsForeignLoadSvg *) load;

	return vips_foreign_load_svg_parse(svg, load->out);
}

static int
vips_foreign_load_svg_generate(VipsRegion *out_region,
	void *seq, void *a, void *b, gboolean *stop)
{
	const VipsForeignLoadSvg *svg = (VipsForeignLoadSvg *) a;
	const VipsObjectClass *class = VIPS_OBJECT_GET_CLASS(svg);
	const VipsRect *r = &out_region->valid;

	cairo_surface_t *surface;
	cairo_t *cr;
	int y;

#ifdef DEBUG
	printf("vips_foreign_load_svg_generate: %p \n     "
		   "left = %d, top = %d, width = %d, height = %d\n",
		svg,
		r->left, r->top, r->width, r->height);
#endif /*DEBUG*/

	/* rsvg won't always paint the background.
	 */
	vips_region_black(out_region);

#ifdef HAVE_CAIRO_FORMAT_RGBA128F
	cairo_format_t format =
		svg->high_bitdepth ? CAIRO_FORMAT_RGBA128F : CAIRO_FORMAT_ARGB32;
#else
	cairo_format_t format = CAIRO_FORMAT_ARGB32;
#endif /*HAVE_CAIRO_FORMAT_RGBA128F*/

	surface = cairo_image_surface_create_for_data(
		VIPS_REGION_ADDR(out_region, r->left, r->top),
		format,
		r->width, r->height,
		VIPS_REGION_LSKIP(out_region));
	cr = cairo_create(surface);
	cairo_surface_destroy(surface);

#if LIBRSVG_CHECK_VERSION(2, 48, 0)
	if (svg->stylesheet &&
		g_utf8_validate(svg->stylesheet, -1, NULL)) {
		GError *error = NULL;
		if (!rsvg_handle_set_stylesheet(svg->page,
				(const guint8 *) svg->stylesheet,
				g_utf8_strlen(svg->stylesheet, -1), &error)) {
			cairo_destroy(cr);
			vips_operation_invalidate(VIPS_OPERATION(svg));
			vips_error(class->nickname, "Invalid custom CSS");
			vips_g_error(&error);
			return -1;
		}
	}
#endif

	/* rsvg is single-threaded, but we don't need to lock since we're
	 * running inside a non-threaded tilecache.
	 */
#if LIBRSVG_CHECK_VERSION(2, 46, 0)

	{
		RsvgRectangle viewport;
		GError *error = NULL;

		viewport.x = 0;
		viewport.y = 0;
		viewport.width = out_region->im->Xsize;
		viewport.height = out_region->im->Ysize;

		/* No need to scale -- we always set the viewport to the
		 * whole image, and set the region to draw on the surface.
		 */
		cairo_translate(cr, -r->left, -r->top);

		if (!rsvg_handle_render_document(svg->page, cr, &viewport, &error)) {
			cairo_destroy(cr);
			vips_operation_invalidate(VIPS_OPERATION(svg));
			vips_error(class->nickname,
				"%s", _("SVG rendering failed"));
			vips_g_error(&error);
			return -1;
		}

		cairo_destroy(cr);
	}

#else /*!LIBRSVG_CHECK_VERSION(2, 46, 0)*/

	cairo_scale(cr, svg->total_scale, svg->total_scale);
	cairo_translate(cr, -r->left / svg->total_scale,
		-r->top / svg->total_scale);

	if (!rsvg_handle_render_cairo(svg->page, cr)) {
		cairo_destroy(cr);
		vips_operation_invalidate(VIPS_OPERATION(svg));
		vips_error(class->nickname,
			"%s", _("SVG rendering failed"));
		return -1;
	}

	cairo_destroy(cr);

#endif /*LIBRSVG_CHECK_VERSION(2, 46, 0)*/
	if (svg->high_bitdepth) {
		/* Assuming the surface is RGBA128F and the data is premultiplied.
		   Loop through each row and unpremultiply the float data.
		*/
		for (y = 0; y < r->height; y++)
			vips__premultiplied_rgb1282scrgba(
				(float *) VIPS_REGION_ADDR(out_region, r->left, r->top + y),
				r->width);
	}
	else {
		/* Cairo makes pre-multipled BRGA -- we must byteswap and unpremultiply.
		 */
		for (y = 0; y < r->height; y++)
			vips__premultiplied_bgra2rgba(
				(guint32 *) VIPS_REGION_ADDR(out_region, r->left, r->top + y),
				r->width);
	}

	return 0;
}

static int
vips_foreign_load_svg_load(VipsForeignLoad *load)
{
	VipsForeignLoadSvg *svg = (VipsForeignLoadSvg *) load;
	VipsImage **t = (VipsImage **)
		vips_object_local_array((VipsObject *) load, 3);

	/* Enough tiles for two complete rows.
	 */
	t[0] = vips_image_new();
	if (vips_foreign_load_svg_parse(svg, t[0]) ||
		vips_image_generate(t[0], NULL,
			vips_foreign_load_svg_generate, NULL, svg, NULL) ||
		vips_tilecache(t[0], &t[1],
			"tile_width", TILE_SIZE,
			"tile_height", TILE_SIZE,
			"max_tiles", 2 * (1 + t[0]->Xsize / TILE_SIZE),
			NULL) ||
		vips_image_write(t[1], load->real))
		return -1;

	return 0;
}

static void
vips_foreign_load_svg_class_init(VipsForeignLoadSvgClass *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_svg_dispose;
	gobject_class->set_property = vips_object_set_property;
	gobject_class->get_property = vips_object_get_property;

	object_class->nickname = "svgload_base";
	object_class->description = _("load SVG with rsvg");
	object_class->build = vips_foreign_load_svg_build;

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

	/* is_a() is not that quick ... lower the priority.
	 */
	foreign_class->priority = -5;

	load_class->get_flags_filename =
		vips_foreign_load_svg_get_flags_filename;
	load_class->get_flags = vips_foreign_load_svg_get_flags;
	load_class->load = vips_foreign_load_svg_load;

	VIPS_ARG_DOUBLE(class, "dpi", 21,
		_("DPI"),
		_("Render at this DPI"),
		VIPS_ARGUMENT_OPTIONAL_INPUT,
		G_STRUCT_OFFSET(VipsForeignLoadSvg, dpi),
		0.001, 100000.0, 72.0);

	VIPS_ARG_DOUBLE(class, "scale", 22,
		_("Scale"),
		_("Scale output by this factor"),
		VIPS_ARGUMENT_OPTIONAL_INPUT,
		G_STRUCT_OFFSET(VipsForeignLoadSvg, scale),
		0.0, 100000.0, 1.0);

#ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
	VIPS_ARG_BOOL(class, "unlimited", 23,
		_("Unlimited"),
		_("Allow SVG of any size"),
		VIPS_ARGUMENT_OPTIONAL_INPUT,
		G_STRUCT_OFFSET(VipsForeignLoadSvg, unlimited),
		FALSE);
#endif

	VIPS_ARG_STRING(class, "stylesheet", 24,
		_("Stylesheet"),
		_("Custom CSS"),
		VIPS_ARGUMENT_OPTIONAL_INPUT,
		G_STRUCT_OFFSET(VipsForeignLoadSvg, stylesheet),
		NULL);

	VIPS_ARG_BOOL(class, "high_bitdepth", 25,
		_("High bitdepth"),
		_("Enable scRGB 128-bit output (32-bit per channel)"),
		VIPS_ARGUMENT_OPTIONAL_INPUT,
		G_STRUCT_OFFSET(VipsForeignLoadSvg, high_bitdepth),
		FALSE);
}

static void
vips_foreign_load_svg_init(VipsForeignLoadSvg *svg)
{
	svg->dpi = 72.0;
	svg->scale = 1.0;
}

typedef struct _VipsForeignLoadSvgSource {
	VipsForeignLoadSvg parent_object;

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

} VipsForeignLoadSvgSource;

typedef VipsForeignLoadClass VipsForeignLoadSvgSourceClass;

G_DEFINE_TYPE(VipsForeignLoadSvgSource, vips_foreign_load_svg_source,
	vips_foreign_load_svg_get_type());

gboolean
vips_foreign_load_svg_source_is_a_source(VipsSource *source)
{
	unsigned char *data;
	gint64 bytes_read;

	if ((bytes_read = vips_source_sniff_at_most(source,
			 &data, SVG_HEADER_SIZE)) <= 0)
		return FALSE;

	return vips_foreign_load_svg_is_a(data, bytes_read);
}

static int
vips_foreign_load_svg_source_header(VipsForeignLoad *load)
{
	VipsForeignLoadSvg *svg = (VipsForeignLoadSvg *) load;
	VipsForeignLoadSvgSource *source =
		(VipsForeignLoadSvgSource *) load;
	RsvgHandleFlags flags = svg->unlimited ? RSVG_HANDLE_FLAG_UNLIMITED : 0;

	GError *error = NULL;

	GInputStream *gstream;

	if (vips_source_rewind(source->source))
		return -1;

	gstream = vips_g_input_stream_new_from_source(source->source);
	if (!(svg->page = rsvg_handle_new_from_stream_sync(
			  gstream, NULL, flags, NULL, &error))) {
		g_object_unref(gstream);
		vips_g_error(&error);
		return -1;
	}
	g_object_unref(gstream);

	return vips_foreign_load_svg_header(load);
}

static int
vips_foreign_load_svg_source_load(VipsForeignLoad *load)
{
	VipsForeignLoadSvgSource *source = (VipsForeignLoadSvgSource *) load;

	if (vips_source_rewind(source->source) ||
		vips_foreign_load_svg_load(load) ||
		vips_source_decode(source->source))
		return -1;

	return 0;
}

static void
vips_foreign_load_svg_source_class_init(VipsForeignLoadSvgSourceClass *class)
{
	GObjectClass *gobject_class = G_OBJECT_CLASS(class);
	VipsObjectClass *object_class = (VipsObjectClass *) class;
	VipsOperationClass *operation_class = VIPS_OPERATION_CLASS(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 = "svgload_source";
	object_class->description = _("load svg from source");

	operation_class->flags |= VIPS_OPERATION_NOCACHE;

	load_class->is_a_source = vips_foreign_load_svg_source_is_a_source;
	load_class->header = vips_foreign_load_svg_source_header;
	load_class->load = vips_foreign_load_svg_source_load;

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

static void
vips_foreign_load_svg_source_init(VipsForeignLoadSvgSource *source)
{
}

typedef struct _VipsForeignLoadSvgFile {
	VipsForeignLoadSvg parent_object;

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

} VipsForeignLoadSvgFile;

typedef VipsForeignLoadSvgClass VipsForeignLoadSvgFileClass;

G_DEFINE_TYPE(VipsForeignLoadSvgFile, vips_foreign_load_svg_file,
	vips_foreign_load_svg_get_type());

static gboolean
vips_foreign_load_svg_file_is_a(const char *filename)
{
	unsigned char buf[SVG_HEADER_SIZE];
	guint64 bytes;

	return (bytes = vips__get_bytes(filename, buf,
				SVG_HEADER_SIZE)) > 0 &&
		vips_foreign_load_svg_is_a(buf, bytes);
}

static int
vips_foreign_load_svg_file_header(VipsForeignLoad *load)
{
	VipsForeignLoadSvg *svg = (VipsForeignLoadSvg *) load;
	VipsForeignLoadSvgFile *file = (VipsForeignLoadSvgFile *) load;
	RsvgHandleFlags flags = svg->unlimited ? RSVG_HANDLE_FLAG_UNLIMITED : 0;

	GError *error = NULL;

	GFile *gfile;

	gfile = g_file_new_for_path(file->filename);
	if (!(svg->page = rsvg_handle_new_from_gfile_sync(
			  gfile, flags, NULL, &error))) {
		g_object_unref(gfile);
		vips_g_error(&error);
		return -1;
	}
	g_object_unref(gfile);

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

	return vips_foreign_load_svg_header(load);
}

static const char *vips_foreign_svg_suffs[] = {
	".svg",
/* librsvg supports svgz directly, no need to check for zlib here.
 */
#if LIBRSVG_CHECK_FEATURE(SVGZ)
	".svgz",
	".svg.gz",
#endif
	NULL
};

static void
vips_foreign_load_svg_file_class_init(
	VipsForeignLoadSvgFileClass *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 = "svgload";

	foreign_class->suffs = vips_foreign_svg_suffs;

	load_class->is_a = vips_foreign_load_svg_file_is_a;
	load_class->header = vips_foreign_load_svg_file_header;

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

static void
vips_foreign_load_svg_file_init(VipsForeignLoadSvgFile *file)
{
}

typedef struct _VipsForeignLoadSvgBuffer {
	VipsForeignLoadSvg parent_object;

	/* Load from a buffer.
	 */
	VipsArea *buf;

} VipsForeignLoadSvgBuffer;

typedef VipsForeignLoadSvgClass VipsForeignLoadSvgBufferClass;

G_DEFINE_TYPE(VipsForeignLoadSvgBuffer, vips_foreign_load_svg_buffer,
	vips_foreign_load_svg_get_type());

static int
vips_foreign_load_svg_buffer_header(VipsForeignLoad *load)
{
	VipsForeignLoadSvg *svg = (VipsForeignLoadSvg *) load;
	VipsForeignLoadSvgBuffer *buffer =
		(VipsForeignLoadSvgBuffer *) load;
	RsvgHandleFlags flags = svg->unlimited ? RSVG_HANDLE_FLAG_UNLIMITED : 0;

	GError *error = NULL;

	GInputStream *gstream;

	gstream = g_memory_input_stream_new_from_data(
		buffer->buf->data, buffer->buf->length, NULL);
	if (!(svg->page = rsvg_handle_new_from_stream_sync(
			  gstream, NULL, flags, NULL, &error))) {
		g_object_unref(gstream);
		vips_g_error(&error);
		return -1;
	}
	g_object_unref(gstream);

	return vips_foreign_load_svg_header(load);
}

static void
vips_foreign_load_svg_buffer_class_init(
	VipsForeignLoadSvgBufferClass *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 = "svgload_buffer";

	load_class->is_a_buffer = vips_foreign_load_svg_is_a;
	load_class->header = vips_foreign_load_svg_buffer_header;

	VIPS_ARG_BOXED(class, "buffer", 1,
		_("Buffer"),
		_("Buffer to load from"),
		VIPS_ARGUMENT_REQUIRED_INPUT,
		G_STRUCT_OFFSET(VipsForeignLoadSvgBuffer, buf),
		VIPS_TYPE_BLOB);
}

static void
vips_foreign_load_svg_buffer_init(VipsForeignLoadSvgBuffer *buffer)
{
}

#endif /*HAVE_RSVG*/

/**
 * vips_svgload:
 * @filename: file to load
 * @out: (out): output image
 * @...: `NULL`-terminated list of optional named arguments
 *
 * Render a SVG file into a VIPS image.
 *
 * Rendering uses the librsvg library and should be fast.
 *
 * Use @dpi to set the rendering resolution. The default is 72. Additionally,
 * you can scale by setting @scale. If you set both, they combine.
 *
 * This function only reads the image header and does not render any pixel
 * data. Rendering occurs when pixels are accessed.
 *
 * SVGs larger than 10MB are normally blocked for security. Set @unlimited to
 * allow SVGs of any size.
 *
 * A UTF-8 string containing custom CSS can be provided via @stylesheet.
 * During the CSS cascade, the specified stylesheet will be applied with a
 * User Origin. This feature requires librsvg 2.48.0 or later.
 *
 * If @high_bitdepth is set and the version of cairo supports it
 * (e.g. cairo >= 1.17.2), enable 128-bit scRGB output (32-bit per channel).
 *
 * ::: tip "Optional arguments"
 *     * @dpi: `gdouble`, render at this DPI
 *     * @scale: `gdouble`, scale render by this factor
 *     * @unlimited: `gboolean`, allow SVGs of any size
 *     * @stylesheet: `gchararray`, custom CSS
 *     * @high_bitdepth: `gboolean`, enable scRGB 128-bit output
 *
 * ::: seealso
 *     [ctor@Image.new_from_file].
 *
 * Returns: 0 on success, -1 on error.
 */
int
vips_svgload(const char *filename, VipsImage **out, ...)
{
	va_list ap;
	int result;

	va_start(ap, out);
	result = vips_call_split("svgload", ap, filename, out);
	va_end(ap);

	return result;
}

/**
 * vips_svgload_buffer:
 * @buf: (array length=len) (element-type guint8): memory area to load
 * @len: (type gsize): size of memory area
 * @out: (out): image to write
 * @...: `NULL`-terminated list of optional named arguments
 *
 * Read a SVG-formatted memory block into a VIPS image. Exactly as
 * [ctor@Image.svgload], but read from a memory buffer.
 *
 * You must not free the buffer while @out is active. The
 * [signal@Object::postclose] signal on @out is a good place to free.
 *
 * ::: tip "Optional arguments"
 *     * @dpi: `gdouble`, render at this DPI
 *     * @scale: `gdouble`, scale render by this factor
 *     * @unlimited: `gboolean`, allow SVGs of any size
 *     * @stylesheet: `gchararray`, custom CSS
 *     * @high_bitdepth: `gboolean`, enable scRGB 128-bit output
 *
 * ::: seealso
 *     [ctor@Image.svgload].
 *
 * Returns: 0 on success, -1 on error.
 */
int
vips_svgload_buffer(void *buf, size_t len, VipsImage **out, ...)
{
	va_list ap;
	VipsBlob *blob;
	int result;

	/* We don't take a copy of the data or free it.
	 */
	blob = vips_blob_new(NULL, buf, len);

	va_start(ap, out);
	result = vips_call_split("svgload_buffer", ap, blob, out);
	va_end(ap);

	vips_area_unref(VIPS_AREA(blob));

	return result;
}

/**
 * vips_svgload_string:
 * @str: string to load
 * @out: (out): image to write
 * @...: `NULL`-terminated list of optional named arguments
 *
 * Exactly as [ctor@Image.svgload], but read from a string. This function
 * takes a copy of the string.
 *
 * ::: tip "Optional arguments"
 *     * @dpi: `gdouble`, render at this DPI
 *     * @scale: `gdouble`, scale render by this factor
 *     * @unlimited: `gboolean`, allow SVGs of any size
 *     * @stylesheet: `gchararray`, custom CSS
 *     * @high_bitdepth: `gboolean`, enable scRGB 128-bit output
 *
 * ::: seealso
 *     [ctor@Image.svgload].
 *
 * Returns: 0 on success, -1 on error.
 */
int
vips_svgload_string(const char *str, VipsImage **out, ...)
{
	va_list ap;
	VipsBlob *blob;
	int result;

	/* Copy the string.
	 */
	blob = vips_blob_copy((const void *) str, strlen(str));

	va_start(ap, out);
	result = vips_call_split("svgload_buffer", ap, blob, out);
	va_end(ap);

	vips_area_unref(VIPS_AREA(blob));

	return result;
}

/**
 * vips_svgload_source:
 * @source: source to load from
 * @out: (out): image to write
 * @...: `NULL`-terminated list of optional named arguments
 *
 * Exactly as [ctor@Image.svgload], but read from a source.
 *
 * ::: seealso
 *     [ctor@Image.svgload].
 *
 * Returns: 0 on success, -1 on error.
 */
int
vips_svgload_source(VipsSource *source, VipsImage **out, ...)
{
	va_list ap;
	int result;

	va_start(ap, out);
	result = vips_call_split("svgload_source", ap, source, out);
	va_end(ap);

	return result;
}
