This module implements the template {{User:Isaacl/Colour convert}}.

OverviewEdit

Module name: Module:Sandbox/isaacl/ColourSpace


local me = { }

local bc = require('Module:BaseConvert')
local Tuple = require('Module:Sandbox/isaacl/ColourSpace/Tuple')
local Formats = require('Module:Sandbox/isaacl/ColourSpace/Formats')

local infoFor = {
    sRGB = {
        colourSpace = 'sRGB',
        formatType = 'float',
        defaultConversion = 'sRGB24bit',
        parseInput = function(args)
            local digitPattern = '^([%.%d]+)%%?$'
            local red = string.match(args[1], digitPattern)
            local green = string.match(args[2], digitPattern)
            local blue = string.match(args[3], digitPattern)
            return { red, green, blue }
        end,
        isInputFormat = function(args)
            local sRGBPattern = '^[%.%d]+%%$'
            if ( args[3] ~= nil and
                string.match(args[1], sRGBPattern) and
                string.match(args[2], sRGBPattern) and
                string.match(args[3], sRGBPattern) ) then
                return true
            end
            return false
        end,  -- end of isInputFormat function
        display = function(self, separator)
            local red   = self[1] .. '%'
            local green = self[2] .. '%'
            local blue  = self [3] .. '%'
            return Tuple.display({ red, green, blue }, separator)
        end,
        mapParametersFrom = {
            sRGB24bit =
                function( colourValue )
                    local red = colourValue[1] / 255 * 100
                    local green = colourValue[2] / 255 * 100
                    local blue = colourValue[3] / 255 * 100
                    return { red, green, blue }
                end,
        },  -- end of mapping functions

    },  -- info for sRGB

    sRGB24bit = {
        colourSpace = 'sRGB',
        formatType = '24bit',
        defaultConversion = 'sRGB',
        isInputFormat = function(args)
            local digitPattern = '^%d+$'
            if ( args[3] ~= nil and
                 string.match(args[1], digitPattern) and
                 string.match(args[2], digitPattern) and
                 string.match(args[3], digitPattern)
                 -- for some reason, tonumber() is required for range checking to work
                 and (tonumber(args[1]) <= 255)
                 and (tonumber(args[2]) <= 255)
                 and (tonumber(args[3]) <= 255)
            ) then
                 return true
            end
            return false
        end,
        display = function(self, separator)
            return Tuple.display(self, separator)
        end,
        mapParametersFrom = {
            sRGB = function(colourValue)
                local red   = math.floor(colourValue[1] * 255 / 100 + 0.5)
                local green = math.floor(colourValue[2] * 255 / 100 + 0.5)
                local blue  = math.floor(colourValue[3] * 255 / 100 + 0.5)
                return { red, green, blue }
            end,
            sRGB24bitHexString = function(colourValue)
                return colourValue
            end,
        },  -- end of mapping functions

    },  -- info for sRGB24bit

    sRGB24bitHexString = {
        colourSpace = 'sRGB',
        formatType = '24bit',
        defaultConversion = 'sRGB24bit',
        parseInput = function(args)
            local red
            local green
            local blue
            local hexString = args[1]
            local hexCharPattern = '^#?(%x%x)(%x%x)(%x%x)$'
            local fDoubleChar = false
            if ( #hexString == 3 or #hexString == 4 ) then
                hexCharPattern = '^#?(%x)(%x)(%x)$'
                fDoubleChar = true
            end
            red, green, blue = string.match(hexString, hexCharPattern)
            if ( fDoubleChar ) then
                red = red .. red;
                green = green .. green;
                blue = blue .. blue;
            end
    
            red = bc.convert({n = red, base = 10, from = 16})
            green = bc.convert({n = green, base = 10, from = 16})
            blue = bc.convert({n = blue, base = 10, from = 16})
            return { red, green, blue }
        end,
        isInputFormat = function(args)
            if (   string.match(args[1], '^#%x%x%x$')
                or string.match(args[1], '^#%x%x%x%x%x%x$' ) ) then
                return true
            end
            return false
        end,
        display = function(self, separator)
            local red   = string.format('&#35;%02X', self[1])
            local green = string.format('%02X', self[2])
            local blue  = string.format('%02X', self[3])
            return Tuple.display({ red, green, blue }, '')
        end,
        mapParametersFrom = {
            sRGB24bit = function( colourValue )
                return colourValue
            end,
        },  -- end of mapping functions

    },  -- info for sRGB24bitHexString

}  -- data for formats

function me.buildColourTuple(args, parameters)
    local result = Tuple.clone(args)
    result.format = parameters.format
    result.colourSpace = parameters.colourSpace
    result.defaultConversion = parameters.defaultConversion
    result.fValid = true
    result.display = function(self, separator)
        return parameters.displayFunc(self, separator)
    end
    return result
end -- function buildColourTuple

local options = {
    separator = ', ',
    displayPrefix = '',
    displaySuffix = '',
}

local formatTypeFor = { }

local checkInputFormatFor = { }

me.create = { }

local createFromParsedInput = { }

me.mapTo = { }

local colourSpaceFor = { }

local commonFormatForColourSpace = {
    sRGB = {
        andFormatType = {
            float = 'sRGB',
            ['24bit'] = 'sRGB24bit',
        },
    },
}

local function createInvalidColourValue(errorMsg)
    local invalidColourValue = {
        -1, -1, -1,
        fValid = false,
        errorMessage = errorMsg,
        display = function(self, separator)
            return 'InvalidValue ' .. self.errorMessage
        end,
    }
    return invalidColourValue
end

me.configureFormatInfo = function(infoFor)
    for format, info in pairs(infoFor) do
        -- If basic information for the format has not been defined
        -- already, configure it
        if ( me.create[format] == nil ) then
            createFromParsedInput[format] = function(parsedArgs)
                return me.buildColourTuple(parsedArgs, {
                      format = format,
                      colourSpace = info.colourSpace,
                      defaultConversion = info.defaultConversion,
                      displayFunc = info.display,
                })
            end  -- function createFromParsedInput[format]
            me.create[format] = function (args)
                local parsedArgs
                if ( info.parseInput ~= nil ) then
                    parsedArgs = info.parseInput(args)
                else
                    parsedArgs = args 
                end
                if ( parsedArgs == nil ) then
                    return createInvalidColourValue('badInputValues')
                end

                return createFromParsedInput[format](parsedArgs)
            end  -- function me.create[format]

            formatTypeFor[format] = info.formatType
            colourSpaceFor[format] = info.colourSpace

            if ( info.isInputFormat ~= nil ) then
                checkInputFormatFor[format] = info.isInputFormat
            end
        end  -- if me.create[format] == nil, configure basic info for format

        -- Define mapping functions from other formats to the
        -- current format being configured.
        for startFormat, mapper in pairs(info.mapParametersFrom) do
            if ( me.mapTo[format] == nil ) then
                me.mapTo[format] = { from = { } }
            end
            me.mapTo[format].from[startFormat] =
                function(parameters)
                    local copy = Tuple.clone(parameters)
                    local mappedParameters = mapper(copy)

                    if ( mappedParameters == nil ) then
                        return createInvalidColourValue('conversionError '
                            .. parameters:display()
                            )
                    end
                    
                    return createFromParsedInput[format]( mappedParameters )
                end
        end  -- loop over info.mapParametersFrom
    end  -- loop over infoFor table
end

me.configureFormatInfo(infoFor)

for idx=1, #Formats do
    local formatInfo = require('Module:Sandbox/isaacl/ColourSpace/Formats/' .. Formats[idx])
    me.configureFormatInfo(formatInfo.infoFor)
end

function me.loadFormatInfo(format)
    -- try to load the required module for the format
    local formatInfo = require('Module:Sandbox/isaacl/ColourSpace/Formats/'
        .. format)
    if ( formatInfo ~= nil ) then
        me.configureFormatInfo(formatInfo.infoFor)
        return format
    end
    return nil
end

function me.determineInputFormat(frame)
    local args = frame.args
    local fromFormat = frame.args["from"]
    if (fromFormat ~= nil) then
        if ( me.create[fromFormat] ~= nil ) then
            return fromFormat
        else
            -- try to load the required module for the format
            return me.loadFormatInfo(fromFormat)
        end
    end

    for format, isInputFormat in pairs(checkInputFormatFor) do
        if ( isInputFormat(args) ) then
            return format
        end
    end

    -- unable to deduce format
    return nil
end  -- function determineInputFormat()

local function determineOutputFormat(frame, startValue)
    local toFormat = frame.args["to"]
    if (toFormat ~= nil) then
        if ( me.create[toFormat] ~= nil ) then
            return toFormat
        else
            -- try to load the required module for the format
            return me.loadFormatInfo(toFormat)
        end
    end
    -- use default conversion
    return startValue.defaultConversion
end  -- function determineOutputFormat()

local function convertBetweenFormats(colourValue, listOfFormats)
    local convertedValue = colourValue
    for idx, nextFormat in ipairs(listOfFormats) do
        if (convertedValue.format ~= nextFormat) then
            if ( me.mapTo[nextFormat].from[convertedValue.format] == nil ) then
                return createInvalidColourValue('noConversionAvailable from '
                    .. convertedValue.format .. ' to ' .. nextFormat)
            end
            convertedValue = me.mapTo[nextFormat].from[convertedValue.format](convertedValue)
            if (not convertedValue.fValid) then
                -- error in conversion; return immediately with the invalidValue
                return convertedValue
            end
        end
    end  -- loop over list of formats to convert between
    return convertedValue
end  -- function convertBetweenFormats

function me.convertColour(frame)
    if ( frame.args[1] == nil ) then
        return ''
    end

    if ( frame.args.separator ~= nil ) then
        options.separator = frame.args.separator
    end

    local startFormat = me.determineInputFormat(frame)
    if ( startFormat == nil ) then
        return 'badInputFormat'
    end

    local startValue = me.create[startFormat](frame.args)

    if ( not startValue.fValid ) then
        return startValue:display()
    end

    local endFormat = determineOutputFormat(frame, startValue)

    if ( endFormat == nil ) then
        return 'badOutputFormat'
    end

    if ( startFormat == endFormat ) then
        return startValue:display(options.separator)
    end

    local result = { }

    -- If a direct conversion exists, use it
    if (me.mapTo[endFormat].from[startFormat] ~= nil) then
        result = me.mapTo[endFormat].from[startFormat](startValue)
        return result:display(options.separator)
    end

    local listOfFormats = { }

    -- If the start and end formats are in the same colour space:
    -- first, convert to the common format for the starting colour space and format type
    -- second, convert to the common format for the ending colour space and format type
    -- third, convert to the ending format type

    if (colourSpaceFor[startFormat] == colourSpaceFor[endFormat]) then
        table.insert(listOfFormats,
            commonFormatForColourSpace[colourSpaceFor[startFormat]].andFormatType[formatTypeFor[startFormat]] )
        table.insert(listOfFormats,
            commonFormatForColourSpace[colourSpaceFor[endFormat]].andFormatType[formatTypeFor[endFormat]] )
        table.insert(listOfFormats, endFormat)
    
        result = convertBetweenFormats(startValue, listOfFormats)
    else
        -- if the start and end formats are in different colour spaces:
        -- first, convert to the common format for the starting colour space and format type
        -- second, convert to the common floating point format for the starting colour space
        -- third, convert to the common floating point format for the ending colour space
        -- fourth, convert to the common format for the ending colour space and format type
        -- fifth, convert to the ending format type
        table.insert(listOfFormats,
            commonFormatForColourSpace[colourSpaceFor[startFormat]].andFormatType[formatTypeFor[startFormat]] )
        table.insert(listOfFormats,
            commonFormatForColourSpace[colourSpaceFor[startFormat]].andFormatType.float )
        table.insert(listOfFormats,
            commonFormatForColourSpace[colourSpaceFor[endFormat]].andFormatType.float )
        table.insert(listOfFormats,
            commonFormatForColourSpace[colourSpaceFor[endFormat]].andFormatType[formatTypeFor[endFormat]] )
        table.insert(listOfFormats, endFormat)
    
        result = convertBetweenFormats(startValue, listOfFormats)
    end

    return result:display(options.separator)
end  -- function convertColour()

function me.convertColour_fromTemplate(frame)
    return me.convertColour(frame:getParent())
end  -- function templateConvertColour()

return me