From 22d656903563f75678f3634964731ccf93355dfd Mon Sep 17 00:00:00 2001 From: ache Date: Mon, 13 Mar 2017 23:17:19 +0100 Subject: Init commit --- lib/gears/color.lua | 346 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 346 insertions(+) create mode 100644 lib/gears/color.lua (limited to 'lib/gears/color.lua') diff --git a/lib/gears/color.lua b/lib/gears/color.lua new file mode 100644 index 0000000..f0197c1 --- /dev/null +++ b/lib/gears/color.lua @@ -0,0 +1,346 @@ +--------------------------------------------------------------------------- +-- @author Uli Schlachter +-- @copyright 2010 Uli Schlachter +-- @module gears.color +--------------------------------------------------------------------------- + +local setmetatable = setmetatable +local string = string +local table = table +local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1) +local tonumber = tonumber +local ipairs = ipairs +local pairs = pairs +local type = type +local lgi = require("lgi") +local cairo = lgi.cairo +local Pango = lgi.Pango +local surface = require("gears.surface") + +local color = { mt = {} } +local pattern_cache + +--- Create a pattern from a given string. +-- This function can create solid, linear, radial and png patterns. In general, +-- patterns are specified as strings formatted as "type:arguments". "arguments" +-- is specific to the pattern being used. For example, one can use +-- "radial:50,50,10:55,55,30:0,#ff0000:0.5,#00ff00:1,#0000ff". +-- Alternatively, patterns can be specified via tables. In this case, the +-- table's 'type' member specifies the type. For example: +-- { +-- type = "radial", +-- from = { 50, 50, 10 }, +-- to = { 55, 55, 30 }, +-- stops = { { 0, "#ff0000" }, { 0.5, "#00ff00" }, { 1, "#0000ff" } } +-- } +-- Any argument that cannot be understood is passed to @{create_solid_pattern}. +-- +-- Please note that you MUST NOT modify the returned pattern, for example by +-- calling :set_matrix() on it, because this function uses a cache and your +-- changes could thus have unintended side effects. Use @{create_pattern_uncached} +-- if you need to modify the returned pattern. +-- @see create_pattern_uncached, create_solid_pattern, create_png_pattern, +-- create_linear_pattern, create_radial_pattern +-- @tparam string col The string describing the pattern. +-- @return a cairo pattern object +-- @function gears.color + +--- Parse a HTML-color. +-- This function can parse colors like `#rrggbb` and `#rrggbbaa` and also `red`. +-- Max 4 chars per channel. +-- +-- @param col The color to parse +-- @treturn table 4 values representing color in RGBA format (each of them in +-- [0, 1] range) or nil if input is incorrect. +-- @usage -- This will return 0, 1, 0, 1 +-- gears.color.parse_color("#00ff00ff") +function color.parse_color(col) + local rgb = {} + if string.match(col, "^#%x+$") then + local hex_str = col:sub(2, #col) + local channels + if #hex_str % 3 == 0 then + channels = 3 + elseif #hex_str % 4 == 0 then + channels = 4 + else + return nil + end + local chars_per_channel = #hex_str / channels + if chars_per_channel > 4 then + return nil + end + local dividor = (0x10 ^ chars_per_channel) - 1 + for idx=1,#hex_str,chars_per_channel do + local channel_val = tonumber(hex_str:sub(idx,idx+chars_per_channel-1), 16) + table.insert(rgb, channel_val / dividor) + end + if channels == 3 then + table.insert(rgb, 1) + end + else + local c = Pango.Color() + if not c:parse(col) then + return nil + end + rgb = { + c.red / 0xffff, + c.green / 0xffff, + c.blue / 0xffff, + 1.0 + } + end + assert(#rgb == 4, col) + return unpack(rgb) +end + +--- Find all numbers in a string +-- +-- @tparam string s The string to parse +-- @return Each number found as a separate value +local function parse_numbers(s) + local res = {} + for k in string.gmatch(s, "-?[0-9]+[.]?[0-9]*") do + table.insert(res, tonumber(k)) + end + return unpack(res) +end + +--- Create a solid pattern +-- +-- @param col The color for the pattern +-- @return A cairo pattern object +function color.create_solid_pattern(col) + if col == nil then + col = "#000000" + elseif type(col) == "table" then + col = col.color + end + return cairo.Pattern.create_rgba(color.parse_color(col)) +end + +--- Create an image pattern from a png file +-- +-- @param file The filename of the file +-- @return a cairo pattern object +function color.create_png_pattern(file) + if type(file) == "table" then + file = file.file + end + local image = surface.load(file) + local pattern = cairo.Pattern.create_for_surface(image) + pattern:set_extend(cairo.Extend.REPEAT) + return pattern +end + +--- Add stops to the given pattern. +-- @param p The cairo pattern to add stops to +-- @param iterator An iterator that returns strings. Each of those strings +-- should be in the form place,color where place is in [0, 1]. +local function add_iterator_stops(p, iterator) + for k in iterator do + local sub = string.gmatch(k, "[^,]+") + local point, clr = sub(), sub() + p:add_color_stop_rgba(point, color.parse_color(clr)) + end +end + +--- Add a list of stops to a given pattern +local function add_stops_table(pat, arg) + for _, stop in ipairs(arg) do + pat:add_color_stop_rgba(stop[1], color.parse_color(stop[2])) + end +end + +--- Create a pattern from a string +local function string_pattern(creator, arg) + local iterator = string.gmatch(arg, "[^:]+") + -- Create a table where each entry is a number from the original string + local args = { parse_numbers(iterator()) } + local to = { parse_numbers(iterator()) } + -- Now merge those two tables + for _, v in pairs(to) do + table.insert(args, v) + end + -- And call our creator function with the values + local p = creator(unpack(args)) + + add_iterator_stops(p, iterator) + return p +end + +--- Create a linear pattern object. +-- The pattern is created from a string. This string should have the following +-- form: `"x0, y0:x1, y1:"` +-- Alternatively, the pattern can be specified as a table: +-- { type = "linear", from = { x0, y0 }, to = { x1, y1 }, +-- stops = { } } +-- `x0,y0` and `x1,y1` are the start and stop point of the pattern. +-- For the explanation of ``, see `color.create_pattern`. +-- @tparam string|table arg The argument describing the pattern. +-- @return a cairo pattern object +function color.create_linear_pattern(arg) + local pat + + if type(arg) == "string" then + return string_pattern(cairo.Pattern.create_linear, arg) + elseif type(arg) ~= "table" then + error("Wrong argument type: " .. type(arg)) + end + + pat = cairo.Pattern.create_linear(arg.from[1], arg.from[2], arg.to[1], arg.to[2]) + add_stops_table(pat, arg.stops) + return pat +end + +--- Create a radial pattern object. +-- The pattern is created from a string. This string should have the following +-- form: `"x0, y0, r0:x1, y1, r1:"` +-- Alternatively, the pattern can be specified as a table: +-- { type = "radial", from = { x0, y0, r0 }, to = { x1, y1, r1 }, +-- stops = { } } +-- `x0,y0` and `x1,y1` are the start and stop point of the pattern. +-- `r0` and `r1` are the radii of the start / stop circle. +-- For the explanation of ``, see `color.create_pattern`. +-- @tparam string|table arg The argument describing the pattern +-- @return a cairo pattern object +function color.create_radial_pattern(arg) + local pat + + if type(arg) == "string" then + return string_pattern(cairo.Pattern.create_radial, arg) + elseif type(arg) ~= "table" then + error("Wrong argument type: " .. type(arg)) + end + + pat = cairo.Pattern.create_radial(arg.from[1], arg.from[2], arg.from[3], + arg.to[1], arg.to[2], arg.to[3]) + add_stops_table(pat, arg.stops) + return pat +end + +--- Mapping of all supported color types. New entries can be added. +color.types = { + solid = color.create_solid_pattern, + png = color.create_png_pattern, + linear = color.create_linear_pattern, + radial = color.create_radial_pattern +} + +--- Create a pattern from a given string. +-- For full documentation of this function, please refer to +-- `color.create_pattern`. The difference between `color.create_pattern` +-- and this function is that this function does not insert the generated +-- objects into the pattern cache. Thus, you are allowed to modify the +-- returned object. +-- @see create_pattern +-- @param col The string describing the pattern. +-- @return a cairo pattern object +function color.create_pattern_uncached(col) + -- If it already is a cairo pattern, just leave it as that + if cairo.Pattern:is_type_of(col) then + return col + end + col = col or "#000000" + if type(col) == "string" then + local t = string.match(col, "[^:]+") + if color.types[t] then + local pos = string.len(t) + local arg = string.sub(col, pos + 2) + return color.types[t](arg) + end + elseif type(col) == "table" then + local t = col.type + if color.types[t] then + return color.types[t](col) + end + end + return color.create_solid_pattern(col) +end + +--- Create a pattern from a given string, same as `gears.color`. +-- @see gears.color +function color.create_pattern(col) + if cairo.Pattern:is_type_of(col) then + return col + end + return pattern_cache:get(col or "#000000") +end + +--- Check if a pattern is opaque. +-- A pattern is transparent if the background on which it gets drawn (with +-- operator OVER) doesn't influence the visual result. +-- @param col An argument that `create_pattern` accepts. +-- @return The pattern if it is surely opaque, else nil +function color.create_opaque_pattern(col) + local pattern = color.create_pattern(col) + local kind = pattern:get_type() + + if kind == "SOLID" then + local _, _, _, _, alpha = pattern:get_rgba() + if alpha ~= 1 then + return + end + return pattern + elseif kind == "SURFACE" then + local status, surf = pattern:get_surface() + if status ~= "SUCCESS" or surf.content ~= "COLOR" then + -- The surface has an alpha channel which *might* be non-opaque + return + end + + -- Only the "NONE" extend mode is forbidden, everything else doesn't + -- introduce transparent parts + if pattern:get_extend() == "NONE" then + return + end + + return pattern + elseif kind == "LINEAR" then + local _, stops = pattern:get_color_stop_count() + + -- No color stops or extend NONE -> pattern *might* contain transparency + if stops == 0 or pattern:get_extend() == "NONE" then + return + end + + -- Now check if any of the color stops contain transparency + for i = 0, stops - 1 do + local _, _, _, _, _, alpha = pattern:get_color_stop_rgba(i) + if alpha ~= 1 then + return + end + end + return pattern + end + + -- Unknown type, e.g. mesh or raster source or unsupported type (radial + -- gradients can do weird self-intersections) +end + +--- Fill non-transparent area of an image with a given color. +-- @param image Image or path to it. +-- @param new_color New color. +-- @return Recolored image. +function color.recolor_image(image, new_color) + if type(image) == 'string' then + image = surface.duplicate_surface(image) + end + local cr = cairo.Context.create(image) + cr:set_source(color.create_pattern(new_color)) + cr:mask(cairo.Pattern.create_for_surface(image), 0, 0) + return image +end + +function color.mt.__call(_, ...) + return color.create_pattern(...) +end + +pattern_cache = require("gears.cache").new(color.create_pattern_uncached) + +--- No color +color.transparent = color.create_pattern("#00000000") + +return setmetatable(color, color.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 -- cgit v1.2.3-70-g09d2