summaryrefslogtreecommitdiff
path: root/lib/gears/color.lua
blob: f0197c13b457b79c1d0d4a294c7f9548dd395c88 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
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:<stops>"`
-- Alternatively, the pattern can be specified as a table:
--    { type = "linear", from = { x0, y0 }, to = { x1, y1 },
--      stops = { <stops> } }
-- `x0,y0` and `x1,y1` are the start and stop point of the pattern.
-- For the explanation of `<stops>`, 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:<stops>"`
-- Alternatively, the pattern can be specified as a table:
--    { type = "radial", from = { x0, y0, r0 }, to = { x1, y1, r1 },
--      stops = { <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 `<stops>`, 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