Module talk:Color contrast
Add topic| This module does not require a rating on Wikipedia's content assessment scale. It is of interest to the following WikiProjects: | ||||||||
| ||||||||
| Text has been copied to or from this page; see the list below. The source pages now serve to provide attribution for the content in the destination pages and must not be deleted as long as the copies exist. For attribution and to access older versions of the copied text, please see the history links below. |
Making the lum function accessible from modules
[edit source]I've made the lum function accessible from modules by changing the code from:
function p.lum(frame) return color2lum(frame.args[1] or frame:getParent().args[1]) end
to:
function p.lum(frame) local args = frame.args[1] or frame:getParent().args[1] return p._lum(args) end function p._lum(args) return color2lum(args) end
The changes are on the sandbox and will implement them if there are no comments. --Gonnym (talk) 11:03, 6 January 2019 (UTC)
- @Gonnym: That looks like a very sensible and useful improvement. My only reservation is that passing the parameters as a table gives the person wanting to import the routine no idea of what values/types to supply to the routine without having to read through all of the code to determine them. In general I'd usually recommend either documenting a brief list of required values for the args table or passing the parameters as a list of obviously-named variables rather than a table. But that's just a minor point and shouldn't stop you from updating the main module. --RexxS (talk) 13:03, 6 January 2019 (UTC)
- @RexxS: I don't mind doing either if that helps. I just followed the current style used for the other three public functions. Do you want me to change _lum(args) to _lum(color)? --Gonnym (talk) 13:27, 6 January 2019 (UTC)
- @Gonnym: Sorry, my confusion: now I've looked harder at it, you're actually passing a string representing the colour, not a table (which is what args would most commonly indicate). I would suggest:I know we can put fuller information in the documentation, but I always suggest leaving small annotations in the code to help any re-users.
-- use {{#invoke:Color_contrast|somecolor}} directly or {{#invoke:Color_contrast}} from a wrapper template: function p.lum(frame) local color = frame.args[1] or frame:getParent().args[1] return p._lum(color) end -- This exports the function for use in other modules. -- The colour is passed as a string: function p._lum(color) return color2lum(color) end
- You could also write
p._lum = color2luminstead of the second function definition, but setting it out explicitly, as you have done, helps whoever is importing the module to see what parameters to supply. Cheers --RexxS (talk) 15:41, 6 January 2019 (UTC)- Sure, looks good. I'll update the sandbox version. --Gonnym (talk) 15:43, 6 January 2019 (UTC)
- @Gonnym: Sorry, my confusion: now I've looked harder at it, you're actually passing a string representing the colour, not a table (which is what args would most commonly indicate). I would suggest:
- @RexxS: I don't mind doing either if that helps. I just followed the current style used for the other three public functions. Do you want me to change _lum(args) to _lum(color)? --Gonnym (talk) 13:27, 6 January 2019 (UTC)
Possible typos in code?
[edit source]Observing the code, I might have spotted a couple of typos:
In p._greatercontrast function, lines 160 and 161
[edit source]I think
if mw.ustring.match(v3, '^[A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9]$') or
mw.ustring.match(v3, '^[A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9]$') then
should read
if mw.ustring.match(c3, '^[A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9]$') or
mw.ustring.match(c3, '^[A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9]$') then
instead. Here, v3 is expected to be a number, thus there is no point in applying it a regular expression. Besides, given the four lines above, it seems logical to use c3 variable here instead of v3.
In p._styleratio function, line 209
[edit source]I think
if( lum ~= '' ) then bg, lum_fg = v, lum end
should read
if( lum ~= '' ) then fg, lum_fg = v, lum end
instead, since at this point we have isolated a forecolor specification from input CSS.
My apologies if I would have misinterpreted your code.
Kindly, Olivier Ph. (talk) 12:11, 13 October 2020 (UTC)
Categorisation broken?
[edit source]Through Documentation subpage, Category:Modules handling colors is added as expected. However, the page does not appear in Category:Modules handling colors (3). -DePiep (talk) 15:12, 7 January 2021 (UTC)
- @DePiep: categorisation behaves as something of a black art. The page appears in Category:Modules handling colors now, but obviously didn't earlier today. Wikipedia:FAQ/Categorization #Why might a category list not be up to date? discusses some of the issues that may cause the lag. I usually try a null edit to "touch" the pages concerned and often find the page then appears in the category, but it's not always 100% successful. --RexxS (talk) 16:07, 7 January 2021 (UTC)
- I had just edited out the {{Sandbox other}} . So we have a cause (bug). Did not investigate problem further.-DePiep (talk) 16:11, 7 January 2021 (UTC)
- Yes I saw that. But because the category was being added via {{Sandbox other}} and now is added directly via Module:Color contrast/doc, that becomes effectively a different transclusion and the usual caveats about delays because of cached transclusions apply. --RexxS (talk) 16:56, 7 January 2021 (UTC)
- {{Sandbox other}} should be checked for Module ns then? But here it seems to work. (To be clear: before my edit the category was empty, and no delay was working, no recent edits). I'll leave it for now. -DePiep (talk) 17:18, 7 January 2021 (UTC)
- Yes I saw that. But because the category was being added via {{Sandbox other}} and now is added directly via Module:Color contrast/doc, that becomes effectively a different transclusion and the usual caveats about delays because of cached transclusions apply. --RexxS (talk) 16:56, 7 January 2021 (UTC)
- I had just edited out the {{Sandbox other}} . So we have a cause (bug). Did not investigate problem further.-DePiep (talk) 16:11, 7 January 2021 (UTC)
Template-protected edit request on 18 March 2025
[edit source]This edit request has been answered. Set the |answered= parameter to no to reactivate your request. |
Line 135:
| − | local c2 = args[2] or | + | local c2 = args[2] or 'white' |
Line 137:
| − | local c3 = args[3] or | + | local c3 = args[3] or 'black' |
Change the specification of the default colors from hex triplets '#FFFFFF' and '#000000' to named HTML colors 'white' and 'black' respectively. This allows the default case to take advantage of the pre-computed table of relative luminances of named HTML colors at Module:Color contrast/colors checked by the color2lum() function instead of calculating the relative luminances of white and black every time (as the module as currently written does), as well as, at least for the default colors, avoiding a parser bug that causes output that begins with # to result in a numbered list. Kinsio (talk ★ contribs ★ rights) 18:55, 19 March 2025 (UTC)
Template-protected edit request on 20 July 2026
[edit source]This edit request has been answered. Set the |answered= parameter to no to reactivate your request. |
Please update the module to support additional CSS3/CSS4 color formats ("rgba", "hsla", 4-digit and 8-digit hex values) and incorporate several code fixes, bringing it up to speed with the improvements from "ptwiki".
Key changes
[edit source]- Updated regex patterns in "color2lum" to support "rgba()", "hsla()", space-separated values, and "#RGBA" (4-digit) / "#RRGGBBAA" (8-digit) hex formats.
- Fixed a variable check bug in "_greatercontrast" where "v3" was tested instead of "c3".
- Stripped newlines and normalized whitespace in the CSS output for "_greatercontrast" when the "css" flag is active.
- Cleaned up variable scoping in "_styleratio" to prevent shadowing loop index "k" and removed unused variables ("bg", "fg").
Replace the current code of the module with:
Code |
|---|
--
-- This module implements
-- {{Color contrast ratio}}
-- {{Greater color contrast ratio}}
-- {{ColorToLum}}
-- {{RGBColorToLum}}
--
local p = {}
local HTMLcolor = mw.loadData( 'Module:Color contrast/colors' )
local function sRGB (v)
if (v <= 0.03928) then
v = v / 12.92
else
v = math.pow((v+0.055)/1.055, 2.4)
end
return v
end
local function rgbdec2lum(R, G, B)
if ( 0 <= R and R < 256 and 0 <= G and G < 256 and 0 <= B and B < 256 ) then
return 0.2126 * sRGB(R/255) + 0.7152 * sRGB(G/255) + 0.0722 * sRGB(B/255)
else
return ''
end
end
local function hsl2lum(h, s, l)
if ( 0 <= h and h < 360 and 0 <= s and s <= 1 and 0 <= l and l <= 1 ) then
local c = (1 - math.abs(2*l - 1))*s
local x = c*(1 - math.abs( math.fmod(h/60, 2) - 1) )
local m = l - c/2
local r, g, b = m, m, m
if( 0 <= h and h < 60 ) then
r = r + c
g = g + x
elseif( 60 <= h and h < 120 ) then
r = r + x
g = g + c
elseif( 120 <= h and h < 180 ) then
g = g + c
b = b + x
elseif( 180 <= h and h < 240 ) then
g = g + x
b = b + c
elseif( 240 <= h and h < 300 ) then
r = r + x
b = b + c
elseif( 300 <= h and h < 360 ) then
r = r + c
b = b + x
end
return rgbdec2lum(255*r, 255*g, 255*b)
else
return ''
end
end
local function color2lum(c)
if (c == nil) then
return ''
end
-- html '#' entity
c = c:gsub("#", "#")
-- whitespace
c = c:match( '^%s*(.-)[%s;]*$' )
-- unstrip nowiki strip markers
c = mw.text.unstripNoWiki(c)
-- lowercase
c = c:lower()
-- first try to look it up
local L = HTMLcolor[c]
if (L ~= nil) then
return L
end
-- convert from hsl / hsla (accepts comma or space delimiters)
if mw.ustring.match(c,'^hsla?%([%s]*[0-9][0-9%.]*[%s]*[,%s][%s]*[0-9][0-9%.]*%%[%s]*[,%s][%s]*[0-9][0-9%.]*%%.*%)$') then
local h, s, l = mw.ustring.match(c,'^hsla?%([%s]*([0-9][0-9%.]*)[%s]*[,%s][%s]*([0-9][0-9%.]*)%%[%s]*[,%s][%s]*([0-9][0-9%.]*)%%.*%)$')
return hsl2lum(tonumber(h), tonumber(s)/100, tonumber(l)/100)
end
-- convert from rgb / rgba (numeric values 0 to 255)
if mw.ustring.match(c,'^rgba?%([%s]*[0-9][0-9]*[%s]*[,%s][%s]*[0-9][0-9]*[%s]*[,%s][%s]*[0-9][0-9]*.*%)$') then
local R, G, B = mw.ustring.match(c,'^rgba?%([%s]*([0-9][0-9]*)[%s]*[,%s][%s]*([0-9][0-9]*)[%s]*[,%s][%s]*([0-9][0-9]*).*%)$')
return rgbdec2lum(tonumber(R), tonumber(G), tonumber(B))
end
-- convert from rgb / rgba percent
if mw.ustring.match(c,'^rgba?%([%s]*[0-9][0-9%.]*%%[%s]*[,%s][%s]*[0-9][0-9%.]*%%[%s]*[,%s][%s]*[0-9][0-9%.]*%%.*%)$') then
local R, G, B = mw.ustring.match(c,'^rgba?%([%s]*([0-9][0-9%.]*)%%[%s]*[,%s][%s]*([0-9][0-9%.]*)%%[%s]*[,%s][%s]*([0-9][0-9%.]*)%%.*%)$')
return rgbdec2lum(255*tonumber(R)/100, 255*tonumber(G)/100, 255*tonumber(B)/100)
end
-- remove leading # (if there is one) and whitespace
c = mw.ustring.match(c, '^[%s#]*([a-f0-9]*)[%s]*$')
-- split into rgb
local cs = mw.text.split(c or '', '')
if( #cs == 6 or #cs == 8 ) then -- Accepts 6 digits (#RRGGBB) or 8 digits (#RRGGBBAA)
local R = 16*tonumber('0x' .. cs[1]) + tonumber('0x' .. cs[2])
local G = 16*tonumber('0x' .. cs[3]) + tonumber('0x' .. cs[4])
local B = 16*tonumber('0x' .. cs[5]) + tonumber('0x' .. cs[6])
return rgbdec2lum(R, G, B)
elseif ( #cs == 3 or #cs == 4 ) then -- Accepts 3 digits (#RGB) or 4 digits (#RGBA)
local R = 16*tonumber('0x' .. cs[1]) + tonumber('0x' .. cs[1])
local G = 16*tonumber('0x' .. cs[2]) + tonumber('0x' .. cs[2])
local B = 16*tonumber('0x' .. cs[3]) + tonumber('0x' .. cs[3])
return rgbdec2lum(R, G, B)
end
-- failure, return blank
return ''
end
-- This exports the function for use in other modules.
-- The colour is passed as a string.
function p._lum(color)
return color2lum(color)
end
function p._greatercontrast(args)
local bias = tonumber(args['bias'] or '0') or 0
local css = (args['css'] and args['css'] ~= '') and true or false
local v1 = color2lum(args[1] or '')
local c2 = args[2] or 'white'
local v2 = color2lum(c2)
local c3 = args[3] or 'black'
local v3 = color2lum(c3)
local ratio1 = -1;
local ratio2 = -1;
if (type(v1) == 'number' and type(v2) == 'number') then
ratio1 = (v2 + 0.05)/(v1 + 0.05)
ratio1 = (ratio1 < 1) and 1/ratio1 or ratio1
end
if (type(v1) == 'number' and type(v3) == 'number') then
ratio2 = (v3 + 0.05)/(v1 + 0.05)
ratio2 = (ratio2 < 1) and 1/ratio2 or ratio2
end
if css then
local c1 = args[1] or ''
if mw.ustring.match(c1, '^[A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9]$') or
mw.ustring.match(c1, '^[A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9]$') then
c1 = '#' .. c1
end
if mw.ustring.match(c2, '^[A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9]$') or
mw.ustring.match(c2, '^[A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9]$') then
c2 = '#' .. c2
end
if mw.ustring.match(c3, '^[A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9]$') or
mw.ustring.match(c3, '^[A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9]$') then
c3 = '#' .. c3
end
local css_output = 'background-color:' .. c1 .. '; color:' .. ((ratio1 > 0) and (ratio2 > 0) and ((ratio1 + bias > ratio2) and c2 or c3) or '') .. ';'
-- Remove newlines (\n, \r) and extra spaces before returning CSS
return css_output:gsub('[\r\n]', ''):gsub('%s+', ' ')
end
return (ratio1 > 0) and (ratio2 > 0) and ((ratio1 + bias > ratio2) and c2 or c3) or ''
end
function p._ratio(args)
local v1 = color2lum(args[1])
local v2 = color2lum(args[2])
if (type(v1) == 'number' and type(v2) == 'number') then
-- v1 should be the brighter of the two.
if v2 > v1 then
v1, v2 = v2, v1
end
return (v1 + 0.05)/(v2 + 0.05)
else
return args['error'] or '?'
end
end
function p._styleratio(args)
local style = (args[1] or ''):lower()
local lum_bg, lum_fg = 1, 0
if args[2] then
local lum = color2lum(args[2])
if lum ~= '' then lum_bg = lum end
end
if args[3] then
local lum = color2lum(args[3])
if lum ~= '' then lum_fg = lum end
end
local slist = mw.text.split(mw.ustring.gsub(mw.ustring.gsub(style or '', '&#[Xx]23;', '#'), '#', '#'), ';')
for k = 1,#slist do
local s = slist[k]
local prop, val = s:match( '^[%s]*([^:]-):([^:]-)[%s;]*$' )
prop = prop or ''
val = val or ''
if (prop:match('^[%s]*(background)[%s]*$') or prop:match('^[%s]*(background%-color)[%s]*$')) then
local lum = color2lum(val)
if( lum ~= '' ) then lum_bg = lum end
elseif (prop:match('^[%s]*(color)[%s]*$')) then
local lum = color2lum(val)
if( lum ~= '' ) then lum_fg = lum end
end
end
if lum_bg > lum_fg then
return (lum_bg + 0.05)/(lum_fg + 0.05)
else
return (lum_fg + 0.05)/(lum_bg + 0.05)
end
end
--[[
Use {{#invoke:Color contrast|somecolor}} directly or
{{#invoke:Color contrast}} from a wrapper template.
Parameters:
-- |1= — required; A color to check.
--]]
function p.lum(frame)
local color = frame.args[1] or frame:getParent().args[1]
return p._lum(color)
end
function p.ratio(frame)
local args = frame.args[1] and frame.args or frame:getParent().args
return p._ratio(args)
end
function p.styleratio(frame)
local args = frame.args[1] and frame.args or frame:getParent().args
return p._styleratio(args)
end
function p.greatercontrast(frame)
local args = frame.args[1] and frame.args or frame:getParent().args
return p._greatercontrast(args)
end
return p
|
--GKNishimoto (talk) 05:16, 20 July 2026 (UTC)
- I've gone ahead and done some cleanup on the code and everything generally looks good, but I do have a question. Since this adds support for specifying colours with an alpha, is it correct behaviour to just ignore that alpha property entirely? It feels like that should matter for the purpose of calculating the ratio, though I'm not familiar with the exact scope of this module. Aidan9382 (talk) 21:33, 26 July 2026 (UTC)
- Hi @Aidan9382,
- Thanks for reviewing and refining the code in the sandbox! The regex fix in '_greatercontrast' to support 4-digit and 8-digit hex values when using the 'css' flag is a great spot.
- To answer your question directly: Yes, ignoring the alpha component is the intended and correct behavior for this module.
- Under the WCAG 2.1 accessibility guidelines, computing the exact contrast ratio of a semi-transparent color requires blending it against whatever background is rendered behind it. Since this Lua module evaluates color values statically without access to the surrounding page layout or DOM context, it cannot reliably determine the underlying background layer.
- Stripping or ignoring the alpha channel allows the module to gracefully fall back to evaluating the base RGB/HSL contrast without throwing errors or returning blank values when editors pass modern CSS formats (such as '#RRGGBBAA' or 'rgba()').
- The code currently in the sandbox looks great and is ready to be applied. Thanks again for the help! -- GKNishimoto (talk) 21:42, 26 July 2026 (UTC)
- P.S. Thanks again! Once this goes live here, I'll be porting this updated version directly to ptwiki as well to keep both modules completely aligned.-- GKNishimoto (talk) 21:51, 26 July 2026 (UTC)
- In that case this seems fine,
Done. Aidan9382 (talk) 12:42, 27 July 2026 (UTC)
- This change broke the layout at Template:Basketballbox/testcases for transclusions using
|bg=. Please test the sandbox module with that template. – Jonesey95 (talk) 16:11, 27 July 2026 (UTC)- More specifically, it caused output in the form of
background-color:#eeeeee; color:#202122;1, breaking following css rules. LittlePuppers (talk) 16:16, 27 July 2026 (UTC)- Well, it seems I didn't aniticipate that scribunto would just implicitly concatenate multiple returns instead of taking just the first return, which gsub will cause. Thanks for that. I've gone ahead and fixed that in the sandbox. Aidan9382 (talk) 16:23, 27 July 2026 (UTC)
- Updated module sandbox to fix the CSS output formatting issue. Removed the
:gsub()whitespace/newline compression that was causing multiple return values and appending an unintended '1' to the style string, fully resolving the layout breaks on templates using|bg=(such as Basketballbox) while retaining all new CSS3/CSS4 color format features. - While Aidan9382's quick fix in the sandbox solved the immediate multiple-return bug by keeping a single
:gsub('[\r\n]', ''), completely removing the extra string manipulation—as we did in our version—is cleaner and fully avoids any potential edge cases with complex template markup or string concatenation in Scribunto. It keeps the CSS output pure, exact, and 100% robust. --GKNishimoto (talk) 17:46, 27 July 2026 (UTC)
- Updated module sandbox to fix the CSS output formatting issue. Removed the
- Well, it seems I didn't aniticipate that scribunto would just implicitly concatenate multiple returns instead of taking just the first return, which gsub will cause. Thanks for that. I've gone ahead and fixed that in the sandbox. Aidan9382 (talk) 16:23, 27 July 2026 (UTC)
- More specifically, it caused output in the form of
- This change broke the layout at Template:Basketballbox/testcases for transclusions using
- In that case this seems fine,