Skip to content

Commit

Permalink
gears.surface: Add crop (#3882)
Browse files Browse the repository at this point in the history
* added surface crop function

* added busted tests

* added devShell Flake

* Revert "added devShell Flake"

This reverts commit 0acb3d4.

* 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 <paul.schneider2@student.uni-halle.de>
Co-authored-by: Lucas Schwiderski <4508454+sclu1034@users.noreply.github.com>
  • Loading branch information
4 people authored Dec 31, 2023
1 parent 2f8b334 commit 00ba63b
Show file tree
Hide file tree
Showing 2 changed files with 237 additions and 0 deletions.
77 changes: 77 additions & 0 deletions lib/gears/surface.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)')
Expand Down Expand Up @@ -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
160 changes: 160 additions & 0 deletions spec/gears/surface_spec.lua
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 00ba63b

Please sign in to comment.