From 3295e9f33d330678384c84031c6779693e742453 Mon Sep 17 00:00:00 2001 From: worron Date: Sat, 22 Jun 2019 00:30:10 +0300 Subject: [PATCH] imagebox: Better svg support (#2779) Use rsvg api to render svg image at requested size. --- lib/wibox/widget/imagebox.lua | 173 +++++++++++++++++++++------------- 1 file changed, 110 insertions(+), 63 deletions(-) diff --git a/lib/wibox/widget/imagebox.lua b/lib/wibox/widget/imagebox.lua index 861deed49..0a5444319 100644 --- a/lib/wibox/widget/imagebox.lua +++ b/lib/wibox/widget/imagebox.lua @@ -6,20 +6,53 @@ -- @widgetmod wibox.widget.imagebox --------------------------------------------------------------------------- +local lgi = require("lgi") +local cairo = lgi.cairo + local base = require("wibox.widget.base") local surface = require("gears.surface") local gtable = require("gears.table") +local gdebug = require("gears.debug") local setmetatable = setmetatable local type = type -local print = print +local math = math + local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1) +-- Safe load for optional Rsvg module +local Rsvg = nil +do + local success, err = pcall(function() Rsvg = lgi.Rsvg end) + if not success then + gdebug.print_warning(debug.traceback("Could not load Rsvg: " .. tostring(err))) + end +end + local imagebox = { mt = {} } +local rsvg_handle_cache = setmetatable({}, { __mode = 'v' }) + +---Load rsvg handle form image file +---@tparam string file Path to svg file. +---@return Rsvg handle +local function load_rsvg_handle(file) + if not Rsvg then return end + + local cache = rsvg_handle_cache[file] + if cache then + return cache + end + + local handle, err = Rsvg.Handle.new_from_file(file) + if not err then + rsvg_handle_cache[file] = handle + return handle + end +end + -- Draw an imagebox with the given cairo context in the given geometry. function imagebox:draw(_, cr, width, height) - if not self._private.image then return end - if width == 0 or height == 0 then return end + if width == 0 or height == 0 or not self._private.default then return end -- Set the clip if self._private.clip_shape then @@ -28,86 +61,101 @@ function imagebox:draw(_, cr, width, height) if not self._private.resize_forbidden then -- Let's scale the image so that it fits into (width, height) - local w = self._private.image:get_width() - local h = self._private.image:get_height() - local aspect = width / w - local aspect_h = height / h - if aspect > aspect_h then aspect = aspect_h end - + local w, h = self._private.default.width, self._private.default.height + local aspect = math.min(width / w, height / h) cr:scale(aspect, aspect) end - cr:set_source_surface(self._private.image, 0, 0) - cr:paint() + if self._private.handle then + self._private.handle:render_cairo(cr) + else + cr:set_source_surface(self._private.image, 0, 0) + cr:paint() + end end -- Fit the imagebox into the given geometry function imagebox:fit(_, width, height) - if not self._private.image then - return 0, 0 - end + if not self._private.default then return 0, 0 end + local w, h = self._private.default.width, self._private.default.height - local w = self._private.image:get_width() - local h = self._private.image:get_height() - - if w > width then - h = h * width / w - w = width - end - if h > height then - w = w * height / h - h = height - end - - if h == 0 or w == 0 then - return 0, 0 - end - - if not self._private.resize_forbidden then - local aspect = width / w - local aspect_h = height / h - - -- Use the smaller one of the two aspect ratios. - if aspect > aspect_h then aspect = aspect_h end - - w, h = w * aspect, h * aspect + if not self._private.resize_forbidden or w > width or h > height then + local aspect = math.min(width / w, height / h) + return w * aspect, h * aspect end return w, h end +---Apply cairo surface for given imagebox widget +local function set_surface(ib, surf) + local is_surd_valid = surf.width > 0 and surf.height > 0 + if not is_surd_valid then return end + + ib._private.default = { width = surf.width, height = surf.height } + ib._private.handle = nil + ib._private.image = surf + return true +end + +---Apply RsvgHandle for given imagebox widget +local function set_handle(ib, handle) + local dim = handle:get_dimensions() + local is_handle_valid = dim.width > 0 and dim.height > 0 + if not is_handle_valid then return end + + ib._private.default = { width = dim.width, height = dim.height } + ib._private.handle = handle + ib._private.image = nil + return true +end + +---Try to load some image object from file then apply it to imagebox. +---@tparam table ib Imagebox +---@tparam string file Image file name +---@tparam function image_loader Function to load image object from file +---@tparam function image_setter Function to set image object to imagebox +---@treturn boolean True if image was successfully applied +local function load_and_apply(ib, file, image_loader, image_setter) + local image_applied + local object = image_loader(file) + if object then + image_applied = image_setter(ib, object) + end + return image_applied +end + --- Set an imagebox' image -- @property image --- @param image Either a string or a cairo image surface. A string is --- interpreted as the path to a png image file. +-- @param image This can can be string, cairo image surface, rsvg handle object or nil. A string is +-- interpreted as the path to an image file. Nil will deny previously set image. -- @return true on success, false if the image cannot be used - function imagebox:set_image(image) + local setup_succeed + if type(image) == "string" then - image = surface.load(image) - if not image then - print(debug.traceback()) - return false + -- try to load rsvg handle from file + setup_succeed = load_and_apply(self, image, load_rsvg_handle, set_handle) + + if not setup_succeed then + -- rsvg handle failed, try to load cairo surface with pixbuf + setup_succeed = load_and_apply(self, image, surface.load, set_surface) end + elseif Rsvg and Rsvg.Handle:is_type_of(image) then + -- try to apply given rsvg handle + setup_succeed = set_handle(self, image) + elseif cairo.Surface:is_type_of(image) then + -- try to apply given cairo surface + setup_succeed = set_surface(self, image) + elseif not image then + -- nil as argument mean full imagebox reset + setup_succeed = true + self._private.handle = nil + self._private.image = nil + self._private.default = nil end - image = image and surface.load(image) - - if image then - local w = image.width - local h = image.height - if w <= 0 or h <= 0 then - return false - end - end - - if self._private.image == image then - -- The image could have been modified, so better redraw - self:emit_signal("widget::redraw_needed") - return true - end - - self._private.image = image + if not setup_succeed then return false end self:emit_signal("widget::redraw_needed") self:emit_signal("widget::layout_changed") @@ -143,7 +191,6 @@ end -- @property resize -- @param allowed If false, the image will be clipped, else it will be resized -- to fit into the available space. - function imagebox:set_resize(allowed) self._private.resize_forbidden = not allowed self:emit_signal("widget::redraw_needed")