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