imgFilter.c

///
// @module Lil
// @license MPL-2.0
// @author eiko
#include <math.h>

#include "inc.h"
#define ENTRY(id) static LUAFUNC(imgFilter_ ## id)

/// @section filters

/// By default inverts rgb
// @function Img:invert
// @param stringOrInt If string inverts channels specified in string like rgba, if int, xors with it directly
ENTRY(invert){
	const lil_Image* img = lil_getImage(L, 1);
	lil_Colour b = { .c = 0x00ffffff };
	int typ = lua_type(L, 2);
	uintmax_t flags;
	switch(typ){
		case LUA_TNONE:
			break;
		case LUA_TNUMBER:
			b.c = lua_tointeger(L, 2);
			break;
		case LUA_TSTRING:
			flags = lil_getStringFlags(L, 2, "rgba", "rgb");
			b.r = isByte1(flags, 0) * 0xff;
			b.g = isByte1(flags, 1) * 0xff;
			b.b = isByte1(flags, 2) * 0xff;
			b.a = isByte1(flags, 3) * 0xff;
			break;
		default:
			luaL_argerror(L, 2, "Expected nothing, number, or string");
			break;
	}
	#if INTPTR_MAX == INT64_MAX
	uint64_t cc = b.c | (uint64_t)b.c << 32;
	#pragma omp parallel for
	for(size_t o = 0; o < (img->w * img->h) / 2; o++)
		((uint64_t*)img->d)[o] ^= cc;
	if(img->w & img->h & 1)
		img->d[(img->w * img->h) - 1].c ^= b.c;
	#else
	#pragma omp parallel for
	for(size_t o = 0; o < img->w * img->h; o++)
		img->d[o].c ^= b.c;
	#endif
	lua_pushvalue(L, 1);
	return 1;
}

///
// @function Img:grey
ENTRY(grey){
	const lil_Image* img = lil_getImage(L, 1);
	#pragma omp parallel for
	for(size_t o = 0; o < img->w * img->h; o++)
		img->d[o].r = img->d[o].g = img->d[o].b = (img->d[o].r + img->d[o].g + img->d[o].b) / 3;
	lua_pushvalue(L, 1);
	return 1;
}

///
// @function Img:bw
// @float[opt=0.5] threshold
ENTRY(bw){
	const lil_Image* img = lil_getImage(L, 1);
	const unsigned char threshold = clamp1(1.0 - luaL_optnumber(L, 2, 0.5)) * 0xff;
	#pragma omp parallel for
	for(size_t o = 0; o < img->w * img->h; o++) // TODO use better weighing
		img->d[o].r = img->d[o].g = img->d[o].b = (((img->d[o].r + img->d[o].g + img->d[o].b) / 3) >= threshold) * 0xff;
	lua_pushvalue(L, 1);
	return 1;
}

///
// @function Img:contrast
// @float amount -1 to 1
ENTRY(contrast){
	const lil_Image* img = lil_getImage(L, 1);
	const lil_Number c = clamp(luaL_checknumber(L, 2), -1, 1) * 255;
	const lil_Number f = (259. * (c + 255.)) / (255. * (259. - c));
	#pragma omp parallel for
	for(size_t o = 0; o < img->w * img->h; o++){
		for(int i = 0; i < 3; i++)
			img->d[o].arr[i] = clamp255(f * (img->d[o].arr[i] - 128) + 128);
	}
	lua_pushvalue(L, 1);
	return 1;
}

///
// @function Img:brightness
// @float amount -1 to 1
ENTRY(brightness){
	const lil_Image* img = lil_getImage(L, 1);
	const lil_Number b = clamp(luaL_checknumber(L, 2), -1, 1) * 255;
	#pragma omp parallel for
	for(size_t o = 0; o < img->w * img->h; o++){
		for(int i = 0; i < 3; i ++)
			img->d[o].arr[i] = clamp255(img->d[o].arr[i] + b);
	}
	lua_pushvalue(L, 1);
	return 1;
}

///
// @function Img:gamma
// @float amount 0+
ENTRY(gamma){
	const lil_Image* img = lil_getImage(L, 1);
	const lil_Number v = clamp(luaL_checknumber(L, 2), 0, 100);
	const lil_Number g = 1. / v;
	#pragma omp parallel for
	for(size_t o = 0; o < img->w * img->h; o++){
		for(int i = 0; i < 3; i ++)
			img->d[o].arr[i] = clamp255(pow(((lil_Number)img->d[o].arr[i] / 255.), g) * 255);
	}
	lua_pushvalue(L, 1);
	return 1;
}

static int doMap(lua_State* L, const lil_Image* img, size_t start, size_t end){
	for(int y = start; y < end; y++){
		for(int x = 0; x < img->w; x++){
			lil_Number fc[4];
			const size_t o = XY(img, x, y);
			lua_pushvalue(L, -1);
			lil_Colour* col = &img->d[o];
			#pragma omp simd
			for(int i = 0; i < 4; i++)
				fc[i] = (lil_Number)col->arr[i] / (lil_Number)0xff;
			lua_pushnumber(L, fc[0]);
			lua_pushnumber(L, fc[1]);
			lua_pushnumber(L, fc[2]);
			lua_pushnumber(L, fc[3]);
			lua_pushnumber(L, x);
			lua_pushnumber(L, y);
			if(lua_pcall(L, 6, 4, 0))
				return 1;
			fc[0] = luaL_checknumber(L, -4);
			fc[1] = luaL_checknumber(L, -3);
			fc[2] = luaL_checknumber(L, -2);
			fc[3] = luaL_checknumber(L, -1);
			#pragma omp simd
			for(int i = 0; i < 4; i++)
				col->arr[i] = clamp1(fc[i]) * (lil_Number)0xff;
			lua_pop(L, 4);
		}
	}
	return 0;
}
/// Runs a function over every pixel in the image. If passed a string and compiled with OpenMP will create separate Lua states in threads and run it in parallel with no access to variables outside the function. More limited but might be faster with very heavy functions and large images. In case of errors in the middle of processing, the image will not be reset. Call img:clone if you plan on recovering errors
// @function Img:map
// @param strOrFunc
// @usage img:map(function(r, g, b, a, x, y) -- Green "nightvision" effect
// 	return 0, 1 - g, 0, 1
// end)
// @usage img:map(function() -- Turns the image into noise
// 	local x = math.random(0, img.w - 1)
// 	local y = math.random(0, img.h - 1)
// 	return img:getPixel(x, y)
// end)
// @usage img:map([[return function(r, g, b, a)
// 	return math.pow(r, 2), math.pow(g, 2), math.pow(b, 2), a
// end]])
ENTRY(map){
	const lil_Image* img = lil_getImage(L, 1);
	switch(lua_type(L, 2)){
		case LUA_TFUNCTION:
			if(doMap(L, img, 0, img->h))
				lua_error(L);
			break;
		case LUA_TSTRING: (void)0;
			const char* str = lua_tostring(L, 2);
			#ifdef _OPENMP
			volatile bool err = false; // First error gets copied and rest ignored
			volatile char* errStr = NULL;
			#pragma omp parallel
			{
				size_t tnum = omp_get_thread_num();
				size_t numt = omp_get_num_threads();
				size_t start = (img->h / numt) * tnum;
				size_t end = start + (img->h / numt);
				if(tnum == numt - 1) // Last thread handles the excess
					end += img->h % numt;
				lua_State* l = luaL_newstate();
				luaL_openlibs(l);
				if(luaL_dostring(l, str)){
					#pragma omp critical
					if(!err){
						const char* es = lua_tostring(l, -1);
						errStr = malloc(strlen(es) + 1);
						strcpy((char*)errStr, es);
						err = true;
					}
					lua_close(l);
				} else {
					if(doMap(l, img, start, end)){
						#pragma omp critical
						if(!err){
							const char* es = lua_tostring(l, -1);
							errStr = malloc(strlen(es) + 1);
							strcpy((char*)errStr, es);
							err = true;
						}
					}
					lua_close(l);
				}
			}
			if(err){
				lua_pushstring(L, (char*)errStr);
				free((char*)errStr);
				lua_error(L);
			}
			#else
			if(luaL_dostring(L, str))
				lua_error(L);
			if(doMap(L, img, 0, img->h))
				lua_error(L);
			#endif
			break;
		default:
			luaL_argerror(L, 2, "Expected function or string");
			break;
	}
	lua_pushvalue(L, 1);
	return 1;
}

/// Flips the image horizontally and/or vertically, by default just vertically
// @function Img:flip
// @string Flag string like vh
// @see Img:mirror
ENTRY(flip){
	const lil_Image* img = lil_getImage(L, 1);
	uintmax_t flags = lil_getStringFlags(L, 2, "vh", "v");
	if(isByte1(flags, 1)){
		const int xinv = img->w-1;
		#pragma omp parallel for
		for(int y = 0; y < img->h; y++){
			for(int x = 0; x < img->w / 2; x++){
				const size_t o = XY(img, x,  y);
				const size_t j = XY(img, xinv - x, y);
				lil_Colour temp = img->d[o];
				img->d[o] = img->d[j];
				img->d[j] = temp;
			}
		}
	}
	if(isByte1(flags, 0)){
		const int yinv = img->h-1;
		#pragma omp parallel for
		for(int y = 0; y < img->h / 2; y++){
			for(int x = 0; x < img->w; x++){
				const size_t o = XY(img, x,  y);
				const size_t j = XY(img, x, yinv - y);
				lil_Colour temp = img->d[o];
				img->d[o] = img->d[j];
				img->d[j] = temp;
			}
		}
	}
	lua_pushvalue(L, 1);
	return 1;
}

static void applyKernel(const lil_Number* kernel, const int kw, const int kh, const lil_Number bias, const bool alpha, const lil_Image* img, lil_Image* imgNew){
	#pragma omp parallel for
	for(int py = 0; py < img->h; py++){
		for(int px = 0; px < img->w; px++){
			lua_Number sum[4] = { 0, 0, 0, 0 };
			for(int ky = 0; ky < kh; ky++){
				const int y = clamp(py + (ky-(kh/2)), 0, img->h-1);
				for(int kx = 0; kx < kw; kx++){
					const lua_Number k = kernel[(ky*kw)+kx];
					const int x = clamp(px + (kx-(kw/2)), 0, img->w-1);
					const size_t o = XY(img, x, y);
					for(int i = 0; i < 4; i++)
						sum[i] += img->d[o].arr[i] * k;
				}
			}
			const size_t o = XY(img, px, py);
			for(int i = 0; i < 3; i++)
				imgNew->d[o].arr[i] = clamp255(sum[i] + bias);
			if(alpha)
				imgNew->d[o].a = clamp255(sum[3] + bias);
			else
				imgNew->d[o].a = img->d[o].a;
		}
	}
}

/// Convolution kernel
// @function Img:kernel
// @tab kernel Matrix of any size
// @tab[opt={}] opts
// @float[opt=sum(kernel)] opts.divisor Value that each kernel entry is divided by, generally to normalise. By default it's the sum of the kernel
// @float[opt=0] opts.bias Value added to each pixel
// @bool[opt=false] opts.alpha Whether to apply the operation to the alpha channel
ENTRY(kernel){
	lil_Image** imgp = lil_getImagePointer(L, 1);
	const lil_Image* img = *imgp;
	lil_Mat kernel = lil_mat_get(L, 2);
	lil_Number divisor = lil_getOptFloat(L, 3, "divisor", NAN);
	luaL_argcheck(L, divisor != 0, 3, "Divisor can't be zero");
	const lil_Number bias = lil_getOptFloat(L, 3, "bias", 0) * 255;
	const bool alpha = lil_getOptBool(L, 3, "alpha", false);
	if(isnan(divisor)){
		divisor = 0;
		for(int i = 0; i < kernel.w * kernel.h; i++)
			divisor += kernel.d[i];
	}
	for(int i = 0; i < kernel.w * kernel.h; i++)
		kernel.d[i] /= divisor;
	lil_Image* imgNew = lil_newImage(L, img->w, img->h);
	if(!imgNew)
		return 0;
	applyKernel(kernel.d, kernel.w, kernel.h, bias, alpha, img, imgNew);
	*imgp = imgNew;
	lil_mat_free(&kernel);
	lil_freeImage(img);
	lua_pushvalue(L, 1);
	return 1;
}

static int deltaColour(lil_Colour a, lil_Colour b){
	int s = 0;
	for(int i = 0; i < 3; i++)
		s += abs((a.arr[i] * a.a) - (b.arr[i] * b.a));
	return s;
}

///
// @function Img:blur
// @int x
// @int[opt=x] y
// @tab[opt={}] opts
// @string[opt="box"] opts.method box snn gaussian
// @float[opt=1] opts.sigma
ENTRY(blur){
	lil_Image** imgp = lil_getImagePointer(L, 1);
	const lil_Image* img = *imgp;
	int kw = luaL_checknumber(L, 2);
	int kh = luaL_optnumber(L, 3, kw);
	kw++;
	kh++;
	luaL_argcheck(L, kw >= 2, 2, "Invalid");
	luaL_argcheck(L, kh >= 2, 3, "Invalid");
	const lua_Number mulf = 1. / (kw * kh);
	lil_Image* imgNew = lil_newImage(L, img->w, img->h);
	if(!imgNew)
		return 0;
	const char* const methods[] = { "box", "snn", "gaussian", NULL };
	const int method = lil_getOptEnum(L, 4, "method", methods, "box");
	const lil_Number sigma = lil_getOptFloat(L, 4, "sigma", 1.);
	lil_Number* kernel = NULL;
	switch(method){
		case 0:
			#pragma omp parallel for
			for(int py = 0; py < img->h; py++){
				for(int px = 0; px < img->w; px++){
					int sum[4] = { 0, 0, 0, 0 };
					for(int ky = 0; ky < kh; ky++){
						const int y = clamp(py + (ky-(kh/2)), 0, img->h-1);
						for(int kx = 0; kx < kw; kx++){
							const int x = clamp(px + (kx-(kw/2)), 0, img->w-1);
							for(int i = 0; i < 4; i++)
								sum[i] += img->d[XY(img, x, y)].arr[i];
						}
					}
					for(int i = 0; i < 4; i++)
						imgNew->d[XY(img, px, py)].arr[i] = sum[i] * mulf;
				}
			}
			break;
		case 1:
			#pragma omp parallel for
			for(int py = 0; py < img->h; py++){
				for(int px = 0; px < img->w; px++){
					lil_Number sum[4] = { 0, 0, 0, 0 };
					const lil_Colour pix = img->d[XY(img, px, py)];
					for(int ky = 0; ky < kh; ky++){
						const int ya = clamp(py + (ky-(kh/2)), 0, img->h-1);
						const int yb = clamp(py - (ky-(kh/2)), 0, img->h-1);
						for(int kx = 0; kx < kw; kx++){
							const int xa = clamp(px + (kx-(kw/2)), 0, img->w-1);
							const int xb = clamp(px - (kx-(kw/2)), 0, img->w-1);
							const lil_Colour pixa = img->d[XY(img, xa, ya)];
							const lil_Colour pixb = img->d[XY(img, xb, yb)];
							if(deltaColour(pix, pixa) < deltaColour(pix, pixb)){
								for(int i = 0; i < 4; i++)
									sum[i] += pixa.arr[i];
							} else {
								for(int i = 0; i < 4; i++)
									sum[i] += pixb.arr[i];
							}
						}
					}
					for(int i = 0; i < 4; i++)
						imgNew->d[XY(img, px, py)].arr[i] = sum[i] * mulf;
				}
			}
			break;
		case 2:
			kernel = malloc(sizeof(lil_Number) * kw * kh);
			lil_Number sum = 0;
			#define mean (kw/2)
			for(int y = 0; y < kh; y++){
				for(int x = 0; x < kw; x++){
					kernel[(y*kw)+x] = exp(-0.5 * (pow((x-mean)/sigma, 2.0) + pow((y-mean)/sigma,2.0)) ) / (2 * PI * sigma * sigma);
					sum += kernel[(y*kw)+x];
				}
			}
			for(int y = 0; y < kh; y++)
				for(int x = 0; x < kw; x++)
					kernel[(y*kw)+x] /= sum;
			applyKernel(kernel, kw, kh, 0, true, img, imgNew);
			#undef mean
			break;
	}
	*imgp = imgNew;
	lil_freeImage(img);
	lua_pushvalue(L, 1);
	return 1;
}

static int colourDifference_Lab(lil_Colour c1, lil_Colour c2){
	lil_ColourLab Lab1 = lil_rgb2Lab(c1);
	lil_ColourLab Lab2 = lil_rgb2Lab(c2);
	float a1 = c1.a / 255.f;
	float a2 = c2.a / 255.f;
	float Ld = Lab1.L*a1 - Lab2.L*a2;
	float ad = Lab1.a*a1 - Lab2.a*a2;
	float bd = Lab1.b*a1 - Lab2.b*a2;
	float Ad = (a1 - a2) * 100.f;
	return Ld * Ld + ad * ad + bd * bd + Ad * Ad;
}
static int colourDifference_rgb(lil_Colour c1, lil_Colour c2){
	int rd = (c1.r*c1.a >> 8) - (c2.r*c2.a >> 8);
	int gd = (c1.g*c1.a >> 8) - (c2.g*c2.a >> 8);
	int bd = (c1.b*c1.a >> 8) - (c2.b*c2.a >> 8);
	int ba = c1.a - c2.a;
	return rd * rd + gd * gd + bd * bd + ba * ba;
}
static lil_Colour findNearestColour(lil_Colour target, int (*colourDifference)(lil_Colour, lil_Colour), const lil_Colour* list, unsigned length){
	lil_Colour closest = list[0];
	int bestDifference = INT_MAX;
	for(int i = 0; i < length; i++){
		lil_Colour attempt = list[i];
		long difference = colourDifference(attempt, target);
		if(difference < bestDifference){
			closest = attempt;
			bestDifference = difference;
		}
	}
	return closest;
}
static void applyError(lil_Colour* col, int r, int g, int b, int a, int fac){
	col->r = clamp255(col->r + (r * fac / 16));
	col->g = clamp255(col->g + (g * fac / 16));
	col->b = clamp255(col->b + (b * fac / 16));
	col->a = clamp255(col->a + (a * fac / 16));
}
static void img_palette(lil_Image* img, const lil_Colour* colours, int numCol){
	#pragma omp parallel for
	for(int y = 0; y < img->h; y++){
		for(int x = 0; x < img->w; x++){
			img->d[XY(img, x, y)] = findNearestColour(img->d[XY(img, x, y)], colourDifference_rgb, colours, numCol);
		}
	}
}
static void img_palette_fs(lil_Image* img, int (*colourDifference)(lil_Colour, lil_Colour), const lil_Colour* colours, int numCol){
	for(int y = 0; y < img->h; y++){
		for(int x = y%2 ? 0 : img->w-1; x < img->w && x >= 0; x += (((y%2) * 2) - 1)){
			lil_Colour orig = img->d[XY(img, x, y)];
			lil_Colour new = findNearestColour(orig, colourDifference, colours, numCol);
			int r = (orig.r - new.r);
			int g = (orig.g - new.g);
			int b = (orig.b - new.b);
			int a = (orig.a - new.a);
			if(y%2 && x + 1 < img->w)
				applyError(&(img->d[XY(img, x+1, y)]), r, g, b, a, 7);
			else if(!(y%2) && x - 1 >= 0)
				applyError(&(img->d[XY(img, x-1, y)]), r, g, b, a, 7);
			if(y+1 < img->h-1){
				applyError(&(img->d[XY(img, x, y+1)]), r, g, b, a, 5);
				if(x-1 >= 0)
					applyError(&(img->d[XY(img, x-1, y+1)]), r, g, b, a, 3);
				if(x+1 < img->w-1)
					applyError(&(img->d[XY(img, x+1, y+1)]), r, g, b, a, 1);
			}
			img->d[XY(img, x, y)] = new;
		}
	}
}

///
// @function Img:palette
// @tab colours
// @tab[opt={}] opts
// @string[opt="none"] opts.dither none fs (Floyd–Steinberg) fslab (same but in Lab colour space which gives better results but is much slower)
ENTRY(palette){
	lil_Image* img = lil_getImage(L, 1);
	lil_Colour* colours = NULL;
	luaL_checktype(L, 2, LUA_TTABLE);
	const int numCol = lua_objlen(L, 2);
	luaL_argcheck(L, numCol > 0, 2, "Need colours");
	int top = lua_gettop(L);
	for(int i = 0; i < numCol; i++){ // Going over each colour first since lil_getColour calls lua_error if it's invalid which wouldn't free the colours pointer
		lua_pushinteger(L, i+1);
		lua_gettable(L, 2);
		lil_getColour(L, top + i + 1, NULL);
	}
	const char* const methods[] = { "none", "fs", "fslab", NULL };
	const int dither = lil_getOptEnum(L, 3, "dither", methods, "none");
	colours = malloc(sizeof(lil_Colour) * numCol);
	for(int i = 0; i < numCol; i++)
		colours[i] = lil_getColour(L, top + i + 1, NULL);
	switch(dither){
		case 0:
			img_palette(img, colours, numCol);
			break;
		case 1:
			img_palette_fs(img, colourDifference_rgb, colours, numCol);
			break;
		case 2:
			img_palette_fs(img, colourDifference_Lab, colours, numCol);
			break;
	}
	lua_pushvalue(L, 1);
	return 1;
}

/// Trim edges. If the image is blank does nothing, else trims image and returns the image and where the crop starts
// @function Img:trim
// @float[opt=0] threshold A value of 0 matches exactly and a value of 1 makes no sense
// @colour[opt=transparent] colour
// @return img, cropX, cropY
// @return[2] img
ENTRY(trim){
	lil_Image** imgp = lil_getImagePointer(L, 1);
	const lil_Image* img = *imgp;
	lil_Colour defCol = { .c = 0 };
	float fthres = clamp1(luaL_optnumber(L, 2, 0));
	if(fthres == 1.f)
		goto end;
	int thres = fthres * colourDifference_rgb(defCol, (lil_Colour){ .c = 0xffffffff });
	lil_Colour col = lil_getColour(L, 3, &defCol);
	int minx = img->w-1, miny = -1, maxx = -1, maxy = -1;
	for(int y = 0; y < img->h; y++){
		for(int x = 0; x < img->w; x++){
			lil_Colour p = img->d[XY(img, x, y)];
			long diff = colourDifference_rgb(p, col);
			if(diff > thres){
				maxy = y;
				if(miny < 0)
					miny = y;
				if(minx > x)
					minx = x;
				if(maxx < x)
					maxx = x;
			}
		}
	}
	int w = (maxx - minx) + 1;
	int h = (maxy - miny) + 1;
	if(w <= 0 || h <= 0){
		end:
		lua_pushvalue(L, 1);
		return 1;
	}
	lil_Image* imgNew = lil_newImage(L, w, h);
	if(!imgNew)
		return 0;
	for(int py = 0; py < h; py++){
		const size_t os = XY(img, minx, miny + py);
		const size_t od = XY(imgNew, 0, py);
		memcpy(&imgNew->d[od], &img->d[os], w * 4);
	}
	*imgp = imgNew;
	lil_freeImage(img);
	lua_pushvalue(L, 1);
	lil_setImageSize(L, imgNew);
	lua_pushinteger(L, minx);
	lua_pushinteger(L, miny);
	return 3;
}

/// Expands the sides of the image. 1 arg: all sides, 2 args: horizontal, vertical, 4 args: north, west, south, east
// @function Img:expand
// @int a
// @int[opt] b
// @int[opt] c
// @int[opt] d
// @colour[opt=transparent] colour
ENTRY(expand){
	lil_Image** imgp = lil_getImagePointer(L, 1);
	const lil_Image* img = *imgp;
	int x = 0, y = 0, b, c, d, optTop = 0;
	int a = luaL_checkinteger(L, 2);
	luaL_argcheck(L, a >= 0, 2, "Need to expand by 0 or more");
	int w = img->w, h = img->h;
	if(lua_type(L, 3) == LUA_TNUMBER){
		b = lua_tointeger(L, 3);
		luaL_argcheck(L, b >= 0, 3, "Need to expand by 0 or more");
		if(lua_type(L, 4) == LUA_TNUMBER){
			if(lua_type(L, 5) == LUA_TNUMBER){
				c = lua_tointeger(L, 4);
				luaL_argcheck(L, c >= 0, 4, "Need to expand by 0 or more");
				d = lua_tointeger(L, 5);
				luaL_argcheck(L, d >= 0, 5, "Need to expand by 0 or more");
				x = d;
				y = a;
				h += a;
				w += b;
				h += c;
				w += d;
				optTop = 6;
			} else {
				luaL_error(L, "Need 1, 2, or 4 numbers");
			}
		} else {
			x = a;
			y = b;
			w += a * 2;
			h += b * 2;
			optTop = 4;
		}
	} else{
		x = y = a;
		w += a * 2;
		h += a * 2;
		optTop = 3;
	}
	lil_Colour colDef = { .c = 0 };
	lil_Colour col = lil_getColour(L, optTop, &colDef);
	lil_Image* imgNew = lil_newImage(L, w, h);
	if(!imgNew)
		return 0;
	lil_fillImage(imgNew, col);
	for(int py = 0; py < img->h; py++){
		const size_t os = XY(img,    0, 0 + py);
		const size_t od = XY(imgNew, x, y + py);
		memcpy(&imgNew->d[od], &img->d[os], img->w * 4);
	}
	*imgp = imgNew;
	lil_freeImage(img);
	lua_pushvalue(L, 1);
	lil_setImageSize(L, imgNew);
	return 1;
	return 1;
}

///
// @function Img:hue
// @float hue 0 - 360
ENTRY(hue){
	lil_Image* img = lil_getImage(L, 1);
	lil_Number h = fmod(luaL_checknumber(L, 2), 360);
	#pragma omp parallel for
	for(size_t o = 0; o < img->w * img->h; o++){
		lil_ColourHsv hsv = lil_rgb2hsv(img->d[o]);
		hsv.h = fmod(hsv.h + h, 360);
		img->d[o].rgb = lil_hsv2rgb(hsv).rgb;
	}
	lua_pushvalue(L, 1);
	return 1;
}

/// Changes the saturation. Slower but better than Img:grey
// @function Img:saturation
// @float saturation 0 removes colour, 2 doubles colour, 0.5 halves colour, -1 inverts colour
ENTRY(saturation){
	lil_Image* img = lil_getImage(L, 1);
	lil_Number s = luaL_checknumber(L, 2);
	#pragma omp parallel for
	for(size_t o = 0; o < img->w * img->h; o++){
		lil_Colour c = img->d[o];
		lil_Number p = sqrt(
				(((lil_Number)c.r * (lil_Number)c.r) * (lil_Number).299) +
				(((lil_Number)c.g * (lil_Number)c.g) * (lil_Number).587) +
				(((lil_Number)c.b * (lil_Number)c.b) * (lil_Number).114)
		);
		img->d[o].r = clamp255(p + (c.r - p) * s);
		img->d[o].g = clamp255(p + (c.g - p) * s);
		img->d[o].b = clamp255(p + (c.b - p) * s);
	}
	lua_pushvalue(L, 1);
	return 1;
}

/// Applies an affine matrix
// @function Img:affine
// @tab mat 3x2 Matrix
// @string[opt="bilinear"|"box"] opts.sample
// @bool[opt=false] opts.expand Whether to expand the image to prevent clipping. Note: high shearing will create very large images
ENTRY(affine){
	lil_Image** imgp = lil_getImagePointer(L, 1);
	const lil_Image* img = *imgp;
	lil_Mat omat = lil_mat_get(L, 2);
	luaL_argcheck(L, omat.w == 3 && omat.h == 2, 2, "Need a 3x2 matrix");
	const enum lil_SampleMode sm = lil_getOptSampleMode(L, 3, 1, 1);
	lil_Mat mat = { 3, 3 }; lil_mat_init(L, &mat);
	memcpy(mat.d, omat.d, sizeof(*mat.d) * 3 * 3);
	lil_Image* imgNew = lil_newImage(L, img->w, img->h);
	if(!imgNew) return 0;
	#pragma omp parallel
	{
		lil_Mat res = { 3, 3 }; lil_mat_init(L, &res);
		lil_Mat cor = { 1, 3 }; lil_mat_init(L, &cor);
		cor.d[2] = 1;
		#pragma omp for
		for(int y = 0; y < imgNew->h; y++){
			for(int x = 0; x < imgNew->w; x++){
				lil_Number xx = x;
				lil_Number yy = y;
				cor.d[0] = (xx / (lil_Number)imgNew->w * 2.) - 1.;
				cor.d[1] = (yy / (lil_Number)imgNew->h * 2.) - 1.;
				lil_mat_dot(&mat, &cor, &res);
				lil_Number px = ((res.d[0] + 1.) * 0.5) * (lil_Number)img->w;
				lil_Number py = ((res.d[3] + 1.) * 0.5) * (lil_Number)img->h;
				if(px >= -1 && px < img->w && py >= -1 && py < img->h)
					imgNew->d[XY(imgNew, x, y)] = lil_sampleImage(sm, img, px, py, 1, 1);
			}
		}
		lil_mat_free(&res); lil_mat_free(&cor);
	}
	*imgp = imgNew;
	lil_mat_free(&omat);
	lil_mat_free(&mat);
	lil_freeImage(img);
	lua_pushvalue(L, 1);
	lil_setImageSize(L, imgNew);
	return 1;
}

#undef ENTRY
#define ENTRY(id) { #id, LUAFUNCD(imgFilter_ ## id) }

LUAREG(imgFilterLib) = {
	ENTRY(invert),
	ENTRY(grey),
	ENTRY(bw),
	ENTRY(contrast),
	ENTRY(brightness),
	ENTRY(gamma),
	ENTRY(map),
	ENTRY(flip),
	ENTRY(kernel),
	ENTRY(blur),
	ENTRY(palette),
	ENTRY(trim),
	ENTRY(expand),
	ENTRY(hue),
	ENTRY(saturation),
	ENTRY(affine),
	{ NULL, NULL }
};

#undef ENTRY
generated by LDoc 1.4.6 Last updated 2023-04-13 13:58:34