Jump to content

Module:Color

Permanently protected module
From Wikipedia, the free encyclopedia
This is an old revision of this page, as edited by Ftrebien (talk | contribs) at 22:48, 10 January 2022 (Handle dirty input). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.

local p = {}

local function hexToRgb(color)
	cleanColor = color:gsub("#", "#"):match('^[%s#]*(.-)[%s;]*$')
	if (#cleanColor == 6) then
		return {
			r = tonumber(string.sub(cleanColor, 1, 2), 16),
			g = tonumber(string.sub(cleanColor, 3, 4), 16),
			b = tonumber(string.sub(cleanColor, 5, 6), 16)
		}
	elseif (#cleanColor == 3) then
		return {
			r = 17 * tonumber(string.sub(cleanColor, 1, 1), 16),
			g = 17 * tonumber(string.sub(cleanColor, 2, 2), 16),
			b = 17 * tonumber(string.sub(cleanColor, 3, 3), 16)
		}
	end
	error("Invalid hexadecimal color " .. cleanColor, 1)
end

local function rgbToHsl(r, g, b)
	if (r > 255 or g > 255 or b > 255 or r < 0 or g < 0 or b < 0) then
		error("Color level out of bounds")
	end
	channelMax = math.max(r, g, b)
	channelMin = math.min(r, g, b)
	range = channelMax - channelMin
	if (range == 0) then
		h = 0
	elseif (channelMax == r) then
		h = 60 * ((g - b) / range)
	elseif (channelMax == g) then
		h = 60 * (2 + (b - r) / range)
	else
		h = 60 * (4 + (r - g) / range)
	end
	if (h < 0) then
		h = 360 + h
	end
	L = channelMax + channelMin
	if (L == 0 or L == 510) then
		s = 0
	else
		s = 100 * range / math.min(L, 510 - L)
	end
	return { h = h, s = s, l = L * 50 / 255 }
end

local function rgbToHsv(r, g, b)
	if (r > 255 or g > 255 or b > 255 or r < 0 or g < 0 or b < 0) then
		error("Color level out of bounds")
	end
	channelMax = math.max(r, g, b)
	channelMin = math.min(r, g, b)
	range = channelMax - channelMin
	if (range == 0) then
		h = 0
	elseif (channelMax == r) then
		h = 60 * ((g - b) / range)
	elseif (channelMax == g) then
		h = 60 * (2 + (b - r) / range)
	else
		h = 60 * (4 + (r - g) / range)
	end
	if (h < 0) then
		h = 360 + h
	end
	if (channelMax == 0) then
		s = 0
	else
		s = 100 * range / channelMax
	end
	return { h = h, s = s, v = channelMax * 100 / 255 }
end

-- c in [0, 255], condition tweaked for no discontinuity
-- http://entropymine.com/imageworsener/srgbformula/
local function inverseCompanding(c)
	if (c > 10.314300250662591) then
		return math.pow((c + 14.025) / 269.025, 2.4)
	else
		return c / 3294.6
	end
end

local function srgbToCielchuvD65o2deg(r, g, b)
	if (r > 255 or g > 255 or b > 255 or r < 0 or g < 0 or b < 0) then
		error("Color level out of bounds")
	end
	R = inverseCompanding(r)
	G = inverseCompanding(g)
	B = inverseCompanding(b)
	-- https://github.com/w3c/csswg-drafts/issues/5922
	X = 0.1804807884018343 * B + 0.357584339383878 * G + 0.41239079926595934 * R
	Y = 0.07219231536073371 * B + 0.21263900587151027 * R + 0.715168678767756 * G
	Z = 0.01933081871559182 * R + 0.11919477979462598 * G + 0.9505321522496607 * B
	if (Y > 0.00885645167903563082) then
		L = 116 * math.pow(Y, 1/3) - 16
	else
		L = Y * 903.2962962962962962963
	end
	if (r == g and g == b) then
		C = 0
		h = 0
	else
		d = X + 3 * Z + 15 * Y
		if (d == 0) then
			C = 0
			h = 0
		else
			-- 0.19783... and 0.4631... computed with extra precision from (X,Y,Z) when (R,G,B) = (1,1,1),
			-- in which case (u,v) ≈ (0,0)
			u = 13 * L * (4 * X / d - 0.19783000664283678994)
			v = 13 * L * (9 * Y / d - 0.46831999493879099801)
			h = math.atan2(v, u) * 57.2957795130823208768
			if (h < 0) then
				h = h + 360
			elseif (h == 0) then
				h = 0 -- ensure zero is positive
			end
			C = math.sqrt(u * u + v * v)
			if (C == 0) then
				C = 0
				h = 0
			end
		end
	end
	return { L = L, C = C, h = h }
end

local function formatAngle(value, p)
	local vs = string.format("%." .. p .. "f", value)
	local zeros
	if (p > 0) then
		zeros = "." .. string.rep("0", p)
	else
		zeros = ""
	end
	if (vs == "360" .. zeros) then
		vs = "0" .. zeros -- handle rounding to 360
	end
	return vs .. "°"
end

function p.hexToRgbTriplet(frame)
	local args = frame.args or frame:getParent().args
	local hex = args[1]
	if (hex) then
		local rgb = hexToRgb(hex)
		return rgb.r .. ', ' .. rgb.g .. ', ' .. rgb.b
	else
		return ""
	end
end

function p.hexToHsl(frame)
	local args = frame.args or frame:getParent().args
	local hex = args[1]
	local p = tonumber(args.precision) or 0
	if (hex) then
		local rgb = hexToRgb(hex)
		local hsl = rgbToHsl(rgb.r, rgb.g, rgb.b)
		return formatAngle(hsl.h, p) .. string.format(", %." .. p .. "f%%, %." .. p .. "f%%", hsl.s, hsl.l)
	else
		return ""
	end
end

function p.hexToHsv(frame)
	local args = frame.args or frame:getParent().args
	local hex = args[1]
	local p = tonumber(args.precision) or 0
	if (hex) then
		local rgb = hexToRgb(hex)
		local hsv = rgbToHsv(rgb.r, rgb.g, rgb.b)
		return formatAngle(hsv.h, p) .. string.format(", %." .. p .. "f%%, %." .. p .. "f%%", hsv.s, hsv.v)
	else
		return ""
	end
end

function p.hexToCielch(frame)
	local args = frame.args or frame:getParent().args
	local hex = args[1]
	local p = tonumber(args.precision) or 0
	if (hex) then
		local rgb = hexToRgb(hex)
		local LCh = srgbToCielchuvD65o2deg(rgb.r, rgb.g, rgb.b)
		return string.format("%." .. p .. "f, %." .. p .. "f, ", LCh.L, LCh.C) .. formatAngle(LCh.h, p)
	else
		return ""
	end
end

return p