From 00ba63b44e6360fef9c15c869240c070137ea6d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emmanuel=20Lepage=20Vall=C3=A9e?= Date: Sun, 31 Dec 2023 15:15:50 -0800 Subject: [PATCH] gears.surface: Add crop (#3882) * added surface crop function * added busted tests * added devShell Flake * Revert "added devShell Flake" This reverts commit 0acb3d4e43365468ce8beb0e7d1d5b8f4ff0ef8d. * general purpose crop added * cleanup * merged functions, added tests * replaced gdebug.print_error with error * clearer error message for crop Co-authored-by: Lucas Schwiderski <4508454+sclu1034@users.noreply.github.com> * make error message shorter * surface.crop: Fix doc warnings. --------- Co-authored-by: Paul Schneider <74120050+paulhersch@users.noreply.github.com> Co-authored-by: Paul Schneider Co-authored-by: Lucas Schwiderski <4508454+sclu1034@users.noreply.github.com> --- lib/gears/surface.lua | 77 +++++++++++++++++ spec/gears/surface_spec.lua | 160 ++++++++++++++++++++++++++++++++++++ 2 files changed, 237 insertions(+) create mode 100644 spec/gears/surface_spec.lua diff --git a/lib/gears/surface.lua b/lib/gears/surface.lua index 21b4e15319..566d4b390c 100644 --- a/lib/gears/surface.lua +++ b/lib/gears/surface.lua @@ -14,6 +14,7 @@ local GdkPixbuf = require("lgi").GdkPixbuf local color, beautiful = nil, nil local gdebug = require("gears.debug") local hierarchy = require("wibox.hierarchy") +local ceil = math.ceil -- Keep this in sync with build-utils/lgi-check.c! local ver_major, ver_minor, ver_patch = string.match(require('lgi.version'), '(%d)%.(%d)%.(%d)') @@ -283,6 +284,82 @@ function surface.widget_to_surface(widget, width, height, format) return img, run_in_hierarchy(widget, cr, width, height) end +--- Crop a surface on its edges. +-- @tparam[opt=nil] table args +-- @tparam[opt=0] integer args.left Left cutoff, cannot be negative +-- @tparam[opt=0] integer args.right Right cutoff, cannot be negative +-- @tparam[opt=0] integer args.top Top cutoff, cannot be negative +-- @tparam[opt=0] integer args.bottom Bottom cutoff, cannot be negative +-- @tparam[opt=nil] number|nil args.ratio Ratio to crop the image to. If edge cutoffs and +-- ratio are given, the edge cutoffs are computed first. Using ratio will crop +-- the center out of an image, similar to what "zoomed-fill" does in wallpaper +-- setter programs. Cannot be negative +-- @tparam[opt=nil] surface args.surface The surface to crop +-- @return The cropped surface +-- @staticfct crop_surface +function surface.crop_surface(args) + args = args or {} + + if not args.surface then + error("No surface to crop_surface supplied") + return nil + end + + local surf = args.surface + local target_ratio = args.ratio + + local w, h = surface.get_size(surf) + local offset_w, offset_h = 0, 0 + + if (args.top or args.right or args.bottom or args.left) then + local left = args.left or 0 + local right = args.right or 0 + local top = args.top or 0 + local bottom = args.bottom or 0 + + if (top < 0 or right < 0 or bottom < 0 or left < 0) then + error("negative offsets are not supported for crop_surface") + end + + w = w - left - right + h = h - top - bottom + + -- the offset needs to be negative + offset_w = - left + offset_h = - top + + -- breaking stuff with cairo crashes awesome with no way to restart in place + -- so here are checks for user error + if w <= 0 or h <= 0 then + error("Area to remove cannot be larger than the image size") + return nil + end + end + + if target_ratio and target_ratio > 0 then + local prev_ratio = w/h + if prev_ratio ~= target_ratio then + if (prev_ratio < target_ratio) then + local old_h = h + h = ceil(w * (1/target_ratio)) + offset_h = offset_h - ceil((old_h - h)/2) + else + local old_w = w + w = ceil(h * target_ratio) + offset_w = offset_w - ceil((old_w - w)/2) + end + end + end + + local ret = cairo.ImageSurface(cairo.Format.ARGB32, w, h) + local cr = cairo.Context(ret) + cr:set_source_surface(surf, offset_w, offset_h) + cr.operator = cairo.Operator.SOURCE + cr:paint() + + return ret +end + return setmetatable(surface, surface.mt) -- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/spec/gears/surface_spec.lua b/spec/gears/surface_spec.lua new file mode 100644 index 0000000000..ecadf4fa78 --- /dev/null +++ b/spec/gears/surface_spec.lua @@ -0,0 +1,160 @@ +local surface = require("gears.surface") +local color = require("gears.color") +local lgi = require("lgi") +local cairo = lgi.cairo +local gdk = lgi.Gdk + +describe("gears.surface", function () + describe("crop_surface", function () + local function test_square() + local surf = cairo.ImageSurface(cairo.Format.ARGB32, 50, 50) + local ctx = cairo.Context(surf) + -- creates complicated pattern, to make sure each pixel has a + -- different color value + ctx:rectangle(0,0,50,50) + local pattern = cairo.Pattern.create_linear(0, 0, 50, 50) + pattern:add_color_stop_rgba(0, color.parse_color("#00000000")) + pattern:add_color_stop_rgba(1, color.parse_color("#FF00FF55")) + ctx:set_source(pattern) + ctx:fill() + local ctx2 = cairo.Context(surf) + ctx2:rectangle(0,0,50,50) + local pattern2 = cairo.Pattern.create_linear(0, 50, 50, 0) + pattern2:add_color_stop_rgba(0, color.parse_color("#00FF0088")) + pattern2:add_color_stop_rgba(1, color.parse_color("#00000000")) + ctx2:set_source(pattern2) + ctx2:fill() + + return surf + end + + -- if cutoff + ratio are tested only right and bottom cutoff can be + -- used because the test should not rebuild the math logic of the actual + -- function + ---@param args table + ---@param target_heigth integer + ---@param target_width integer + local function test(args, target_width, target_heigth) + args.surface = test_square() + local out = surface.crop_surface(args) + + -- check size + local w, h = surface.get_size(out) + assert.is_equal(w, target_width) + assert.is_equal(h, target_heigth) + + -- check if area in img and cropped img are the same + local calc_w_offset = math.ceil((50 - w - (args.right or 0))/2) + local calc_h_offset = math.ceil((50 - h - (args.bottom or 0))/2) + + local pbuf1_in = gdk.pixbuf_get_from_surface( + args.surface, + args.left or calc_w_offset, + args.top or calc_h_offset, + w, + h + ) + local pbuf1_out = gdk.pixbuf_get_from_surface(out, 0, 0, w, h) + assert.is_equal(pbuf1_in:get_pixels(), pbuf1_out:get_pixels()) + end + + it("keep size using offsets", function () + test({ + left = 0, + top = 0, + }, 50, 50) + end) + + it("keep size using ratio", function () + test({ + ratio = 1, + left = 0, + top = 0, + }, 50, 50) + end) + + it("crop to 50x25 with offsets", function () + test({ + top = 12, + bottom = 13, + left = 0, + }, 50, 25) + end) + + it("crop to 25x50 with offsets", function () + test({ + left = 12, + right = 13, + top = 0, + }, 25, 50) + end) + + it("crop all edges", function () + test({ + left = 7, + right = 13, + top = 9, + bottom = 16 + }, 30, 25) + end) + + it("use ratio to crop width", function () + test({ + ratio = 3/5, + }, 30, 50) + end) + + it("use ratio to crop height", function () + test({ + ratio = 5/3, + }, 50, 30) + end) + + it("use very large ratio", function () + test({ + ratio = 10000 + }, 50, 1) + end) + + it("use very small ratio", function () + test({ + ratio = 0.0001 + }, 1, 50) + end) + + it("use ratio and offset", function () + test({ + bottom = 20, + ratio = 1 + }, 30, 30) + end) + + it("use too large offset", function () + assert.has.errors(function () + test({ + left = 55, + top = 0, + }, 0, 0) + end) + end) + + it("use negative offset", function () + assert.has.errors(function () + test({ + left = -1, + top = 0 + }, 0, 0) + end) + end) + + it("use negative ratio", function () + assert.has.errors(function () + test({ + ratio = - 1, + }, 0, 0) + end) + end) + end) +end) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80