1# utilities_blur.py 2# 3# Copyright 2018-2021 Romain F. T. 4# 5# This program is free software: you can redistribute it and/or modify 6# it under the terms of the GNU General Public License as published by 7# the Free Software Foundation, either version 3 of the License, or 8# (at your option) any later version. 9# 10# This program is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# 15# You should have received a copy of the GNU General Public License 16# along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18import cairo, threading 19# from datetime import datetime # Not actually needed, just to measure perfs 20 21class BlurType(int): 22 INVALID = -1 23 AUTO = 0 24 PX_BOX = 1 25 PX_BOX_MULTI = 2 26 CAIRO_REPAINTS = 3 27 TILES = 4 28 29class BlurDirection(int): 30 INVALID = -1 31 BOTH = 0 32 HORIZONTAL = 1 33 VERTICAL = 2 34 35################################################################################ 36 37def utilities_blur_surface(surface, radius, blur_type, blur_direction): 38 """This is the 'official' method to access the blur algorithms. 39 The third argument is an integer corresponding to the BlurType enumeration. 40 The 4th one is an integer corresponding to the BlurDirection enumeration.""" 41 radius = int(radius) 42 if radius < 1: 43 return surface 44 blurred_surface = None 45 # time0 = datetime.now() 46 # print('blurring begins, using algo ', blur_type, '-', blur_direction) 47 48 if blur_type == BlurType.INVALID: 49 return surface 50 elif blur_type == BlurType.AUTO: 51 blur_type = BlurType.PX_BOX 52 # XXX c'est nul ça mdr, mais bon c'est peu utilisé 53 54 if blur_type == BlurType.PX_BOX: 55 blurred_surface = _generic_px_box_blur(surface, radius, blur_direction) 56 elif blur_type == BlurType.PX_BOX_MULTI: 57 blurred_surface = _generic_multi_threaded_blur(surface, radius, blur_direction) 58 elif blur_type == BlurType.CAIRO_REPAINTS: 59 blurred_surface = _generic_cairo_blur(surface, radius, blur_direction) 60 elif blur_type == BlurType.TILES: 61 blurred_surface = _generic_tiled_blur(surface, radius, blur_direction) 62 63 # time1 = datetime.now() 64 # print('blurring ended, total time:', time1 - time0) 65 return blurred_surface 66 67################################################################################ 68# BlurType.PX_BOX ############################################################## 69 70def _generic_px_box_blur(surface, radius, blur_direction): 71 w = surface.get_width() 72 h = surface.get_height() 73 channels = 4 # ARGB 74 if radius > w - 1 or radius > h - 1: 75 return surface 76 77 # this code a modified version of this https://github.com/elementary/granite/blob/14e3aaa216b61f7e63762214c0b36ee97fa7c52b/lib/Drawing/BufferSurface.vala#L230 78 # the main differences (aside of the language) is the poor attempt to use 79 # multithreading (i'm quite sure the access to buffers are not safe at all). 80 # The 2 phases of the algo have been separated to allow directional blur. 81 original = cairo.ImageSurface(cairo.Format.ARGB32, w, h) 82 cairo_context = cairo.Context(original) 83 # cairo_context.set_operator(cairo.Operator.SOURCE) 84 cairo_context.set_source_surface(surface, 0, 0) 85 cairo_context.paint() # XXX is this copy useful? 86 original.flush() 87 pixels = original.get_data() 88 89 buffer0 = [None] * (w * h * channels) 90 vmin = [None] * max(w, h) 91 vmax = [None] * max(w, h) 92 div = 2 * radius + 1 93 dv = [None] * (256 * div) 94 for i in range(0, len(dv)): 95 dv[i] = int(i / div) 96 97 iterations = 1 98 while iterations > 0: 99 iterations = iterations - 1 100 if blur_direction == BlurDirection.VERTICAL: 101 for i in range(0, len(pixels)): 102 buffer0[i] = pixels[i] 103 else: 104 _box_blur_1st_phase(w, h, channels, radius, pixels, buffer0, vmin, vmax, dv) 105 # print('end of the 1st phase…', datetime.now() - time0) 106 if blur_direction == BlurDirection.HORIZONTAL: 107 for i in range(0, len(buffer0)): 108 pixels[i] = buffer0[i] 109 else: 110 _box_blur_2nd_phase(w, h, channels, radius, pixels, buffer0, vmin, vmax, dv) 111 return original 112 113# this code a modified version of a naïve approach to box blur, copied from 114# here: https://github.com/elementary/granite/blob/14e3aaa216b61f7e63762214c0b36ee97fa7c52b/lib/Drawing/BufferSurface.vala#L230 115# The 2 phases of the algo have been separated to allow directional blur. 116 117def _box_blur_1st_phase(w, h, channels, radius, pixels, buff0, vmin, vmax, dv): 118 """Horizontal blurring""" 119 for x in range(0, w): 120 vmin[x] = min(x + radius + 1, w - 1) 121 vmax[x] = max(x - radius, 0) 122 for y in range(0, h): 123 cur_pixel = y * w * channels 124 asum = radius * pixels[cur_pixel + 0] 125 rsum = radius * pixels[cur_pixel + 1] 126 gsum = radius * pixels[cur_pixel + 2] 127 bsum = radius * pixels[cur_pixel + 3] 128 for i in range(0, radius+1): 129 asum += pixels[cur_pixel + 0] 130 rsum += pixels[cur_pixel + 1] 131 gsum += pixels[cur_pixel + 2] 132 bsum += pixels[cur_pixel + 3] 133 cur_pixel += channels 134 cur_pixel = y * w * channels 135 for x in range(0, w): 136 p1 = (y * w + vmin[x]) * channels 137 p2 = (y * w + vmax[x]) * channels 138 buff0[cur_pixel + 0] = dv[asum] 139 buff0[cur_pixel + 1] = dv[rsum] 140 buff0[cur_pixel + 2] = dv[gsum] 141 buff0[cur_pixel + 3] = dv[bsum] 142 asum += pixels[p1 + 0] - pixels[p2 + 0] 143 rsum += pixels[p1 + 1] - pixels[p2 + 1] 144 gsum += pixels[p1 + 2] - pixels[p2 + 2] 145 bsum += pixels[p1 + 3] - pixels[p2 + 3] 146 cur_pixel += channels 147 148def _box_blur_2nd_phase(w, h, channels, radius, pixels, buff0, vmin, vmax, dv): 149 """Vertical blurring""" 150 for y in range(0, h): 151 vmin[y] = min(y + radius + 1, h - 1) * w 152 vmax[y] = max (y - radius, 0) * w 153 for x in range(0, w): 154 cur_pixel = x * channels 155 asum = radius * buff0[cur_pixel + 0] 156 rsum = radius * buff0[cur_pixel + 1] 157 gsum = radius * buff0[cur_pixel + 2] 158 bsum = radius * buff0[cur_pixel + 3] 159 for i in range(0, radius+1): 160 asum += buff0[cur_pixel + 0] 161 rsum += buff0[cur_pixel + 1] 162 gsum += buff0[cur_pixel + 2] 163 bsum += buff0[cur_pixel + 3] 164 cur_pixel += w * channels 165 cur_pixel = x * channels 166 for y in range(0, h): 167 p1 = (x + vmin[y]) * channels 168 p2 = (x + vmax[y]) * channels 169 pixels[cur_pixel + 0] = dv[asum] 170 pixels[cur_pixel + 1] = dv[rsum] 171 pixels[cur_pixel + 2] = dv[gsum] 172 pixels[cur_pixel + 3] = dv[bsum] 173 asum += buff0[p1 + 0] - buff0[p2 + 0] 174 rsum += buff0[p1 + 1] - buff0[p2 + 1] 175 gsum += buff0[p1 + 2] - buff0[p2 + 2] 176 bsum += buff0[p1 + 3] - buff0[p2 + 3] 177 cur_pixel += w * channels 178 179################################################################################ 180# BlurType.PX_BOX_MULTI ######################################################## 181 182def _generic_multi_threaded_blur(surface, radius, blur_direction): 183 """Experimental multi-threaded blur. The parameter `blur_direction` will be 184 ignored (it always blurs in both directions).""" 185 w = surface.get_width() 186 h = surface.get_height() 187 channels = 4 # ARGB 188 if radius > w - 1 or radius > h - 1: 189 return 190 191 original = cairo.ImageSurface(cairo.Format.ARGB32, w, h) 192 cairo_context = cairo.Context(original) 193 # cairo_context.set_operator(cairo.Operator.SOURCE) 194 cairo_context.set_source_surface(surface, 0, 0) 195 cairo_context.paint() 196 original.flush() 197 pixels = original.get_data() 198 199 buffer0 = [None] * (w * h * channels) 200 vmin = [None] * max(w, h) 201 vmax = [None] * max(w, h) 202 div = 2 * radius + 1 203 dv = [None] * (256 * div) 204 for i in range(0, len(dv)): 205 dv[i] = int(i / div) 206 207 full_buff = _box_blur_1st_phase_multi(w, h, channels, radius, pixels, vmin, vmax, dv) 208 # print('end of the 1st phase…', datetime.now() - time0) 209 _box_blur_2nd_phase(w, h, channels, radius, pixels, full_buff, vmin, vmax, dv) 210 211 return original 212 213# this code a modified version of a naïve approach to box blur, copied from 214# here: https://github.com/elementary/granite/blob/14e3aaa216b61f7e63762214c0b36ee97fa7c52b/lib/Drawing/BufferSurface.vala#L230 215# the main differences (aside of the language) is the poor attempt to use 216# multithreading (i'm quite sure the access to buffers are not safe at all) 217# during the first phase. Multithreading of the second phase has not even been 218# tried because this multithreaded version is slower than _box_blur_1st_phase 219 220def _box_blur_1st_phase_multi(w, h, channels, radius, pixels, vmin, vmax, dv): 221 NB_THREADS = 4 222 t1 = [None] * NB_THREADS 223 full_buffer = [] 224 buffers = [] 225 y_end = 0 226 for x in range(0, w): 227 vmin[x] = min(x + radius + 1, w - 1) 228 vmax[x] = max(x - radius, 0) 229 for t in range(0, NB_THREADS): 230 y_start = y_end 231 if t == NB_THREADS - 1: 232 y_end = h 233 else: 234 y_end = y_start + int(h / NB_THREADS) 235 buff_size = w * (y_end - y_start) * channels 236 buff = [0] * buff_size 237 buffers.append(buff) 238 t1[t] = threading.Thread(target=_blur_rows3, args=(x, y_start, y_end, \ 239 w, channels, radius, pixels, buff, vmin, vmax, dv)) 240 for t in range(0, NB_THREADS): 241 t1[t].start() 242 for t in range(0, NB_THREADS): 243 t1[t].join() 244 full_buffer += buffers[t] 245 return full_buffer 246 247def _blur_rows3(x, y0, y1, w, channels, radius, pixels, buff0, vmin, vmax, dv): 248 # print('row thread with', y0, y1, '(begin)') 249 diff = y0 * w * channels 250 for y in range(y0, y1): 251 cur_pixel = y * w * channels 252 asum = radius * pixels[cur_pixel + 0] 253 rsum = radius * pixels[cur_pixel + 1] 254 gsum = radius * pixels[cur_pixel + 2] 255 bsum = radius * pixels[cur_pixel + 3] 256 for i in range(0, radius+1): 257 asum += pixels[cur_pixel + 0] 258 rsum += pixels[cur_pixel + 1] 259 gsum += pixels[cur_pixel + 2] 260 bsum += pixels[cur_pixel + 3] 261 cur_pixel += channels 262 cur_pixel = y * w * channels - diff 263 for x in range(0, w): 264 p1 = (y * w + vmin[x]) * channels 265 p2 = (y * w + vmax[x]) * channels 266 buff0[cur_pixel + 0] = dv[asum] 267 buff0[cur_pixel + 1] = dv[rsum] 268 buff0[cur_pixel + 2] = dv[gsum] 269 buff0[cur_pixel + 3] = dv[bsum] 270 asum += pixels[p1 + 0] - pixels[p2 + 0] 271 rsum += pixels[p1 + 1] - pixels[p2 + 1] 272 gsum += pixels[p1 + 2] - pixels[p2 + 2] 273 bsum += pixels[p1 + 3] - pixels[p2 + 3] 274 cur_pixel += channels 275 # print('row thread with', y0, y1, '(end)') 276 277################################################################################ 278# BlurType.CAIRO_REPAINTS ###################################################### 279 280def _generic_cairo_blur(surface, radius, blur_direction): 281 if blur_direction == BlurDirection.HORIZONTAL: 282 surface = _cairo_directional_blur(surface, radius, False) 283 elif blur_direction == BlurDirection.VERTICAL: 284 surface = _cairo_directional_blur(surface, radius, True) 285 else: 286 # XXX with some big radius, it's visible that it's just a sequence of 287 # 2 directional blurs instead of an actual algorithm 288 surface = _cairo_directional_blur(surface, radius, True) 289 surface = _cairo_directional_blur(surface, radius, False) 290 return surface 291 292# Weird attempt to produce a blurred image using cairo. I mean ok, the image is 293# blurred, and with amazing performances, but the quality is not convincing and 294# the result when the area has (semi-)transparency really sucks. 295 296def _cairo_directional_blur(surface, radius, is_vertical): 297 cairo_context = cairo.Context(surface) 298 if radius < 10: 299 step = 1 300 alpha = min(0.9, step / radius) 301 elif radius < 15: 302 step = 1 303 alpha = min(0.9, (0.5 + step) / radius) 304 else: 305 step = int(radius / 6) # why 6? mystery 306 # cette optimisation donne de légers glitchs aux grands radius, qui ne 307 # sont de toutes manières pas beaux car on voit en partie à travers 308 alpha = min(0.9, (1 + step) / radius) 309 for i in range(-1 * radius, radius, step): 310 if is_vertical: 311 cairo_context.set_source_surface(surface, 0, i) 312 else: 313 cairo_context.set_source_surface(surface, i, 0) 314 cairo_context.paint_with_alpha(alpha) 315 surface.flush() 316 return surface 317 318################################################################################ 319# BlurType.TILES ############################################################### 320 321def _generic_tiled_blur(surface, radius, blur_direction): 322 if blur_direction == BlurDirection.HORIZONTAL: 323 tile_width = radius 324 tile_height = 1 325 elif blur_direction == BlurDirection.VERTICAL: 326 tile_width = 1 327 tile_height = radius 328 else: 329 tile_width = radius 330 tile_height = radius 331 return _get_tiled_surface(surface, tile_width, tile_height) 332 333def _get_tiled_surface(surface, tile_width, tile_height): 334 w = surface.get_width() 335 h = surface.get_height() 336 channels = 4 # ARGB 337 pixels = surface.get_data() 338 pixel_max = w * h * channels 339 340 for x in range(0, w, tile_width): 341 for y in range(0, h, tile_height): 342 current_pixel = (x + (w * y)) * channels 343 if current_pixel >= pixel_max: 344 continue 345 tile_b = pixels[current_pixel + 0] 346 tile_g = pixels[current_pixel + 1] 347 tile_r = pixels[current_pixel + 2] 348 tile_a = pixels[current_pixel + 3] 349 for tx in range(0, tile_width): 350 for ty in range(0, tile_height): 351 current_pixel = ((x + tx) + (w * (y + ty))) * channels 352 if current_pixel >= pixel_max: 353 continue 354 if current_pixel >= (w * (y + ty + 1)) * channels: 355 # the current tile is out of the surface, this guard 356 # clause avoids corrupting the next tile 357 continue 358 pixels[current_pixel + 0] = tile_b 359 pixels[current_pixel + 1] = tile_g 360 pixels[current_pixel + 2] = tile_r 361 pixels[current_pixel + 3] = tile_a 362 # end of one tile 363 # end of the "for each tile" 364 return surface 365 366################################################################################ 367 368