gtkcairoblur.c 8.92 KB
Newer Older
1 2
/* GTK - The GIMP Toolkit
 *
3
 * Copyright (C) 2014 Red Hat
4
 *
5 6 7 8
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
9
 *
10 11
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13
 * Library General Public License for more details.
14
 *
15 16
 * You should have received a copy of the GNU Library General Public
 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
17
 *
18 19 20
 * Written by:
 *     Jasper St. Pierre <jstpierre@mecheye.net>
 *     Owen Taylor <otaylor@redhat.com>
21 22 23 24 25
 */

#include "gtkcairoblurprivate.h"

#include <math.h>
26
#include <string.h>
27

28 29 30 31 32 33 34 35 36 37 38
/*
 * Gets the size for a single box blur.
 *
 * Much of this, the 3 * sqrt(2 * pi) / 4, is the known value for
 * approximating a Gaussian using box blurs.  This yields quite a good
 * approximation for a Gaussian.  For more details, see:
 * http://www.w3.org/TR/SVG11/filters.html#feGaussianBlurElement
 * https://bugzilla.mozilla.org/show_bug.cgi?id=590039#c19
 */
#define GAUSSIAN_SCALE_FACTOR ((3.0 * sqrt(2 * G_PI) / 4))

39 40 41 42 43 44 45 46 47 48 49 50 51 52
#define get_box_filter_size(radius) ((int)(GAUSSIAN_SCALE_FACTOR * (radius)))

/* Sadly, clang is picky about get_box_filter_size(2) not being a
 * constant expression, thus we have to use precomputed values.
 */
#define BOX_FILTER_SIZE_2 3
#define BOX_FILTER_SIZE_3 5
#define BOX_FILTER_SIZE_4 7
#define BOX_FILTER_SIZE_5 9
#define BOX_FILTER_SIZE_6 11
#define BOX_FILTER_SIZE_7 13
#define BOX_FILTER_SIZE_8 15
#define BOX_FILTER_SIZE_9 16
#define BOX_FILTER_SIZE_10 18
53

54 55 56 57 58 59 60 61 62
/* This applies a single box blur pass to a horizontal range of pixels;
 * since the box blur has the same weight for all pixels, we can
 * implement an efficient sliding window algorithm where we add
 * in pixels coming into the window from the right and remove
 * them when they leave the windw to the left.
 *
 * d is the filter width; for even d shift indicates how the blurred
 * result is aligned with the original - does ' x ' go to ' yy' (shift=1)
 * or 'yy ' (shift=-1)
63
 */
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
static void
blur_xspan (guchar *row,
            guchar *tmp_buffer,
            int     row_width,
            int     d,
            int     shift)
{
  int offset;
  int sum = 0;
  int i;

  if (d % 2 == 1)
    offset = d / 2;
  else
    offset = (d - shift) / 2;

  /* All the conditionals in here look slow, but the branches will
   * be well predicted and there are enough different possibilities
   * that trying to write this as a series of unconditional loops
   * is hard and not an obvious win. The main slow down here seems
Matthias Clasen's avatar
Matthias Clasen committed
84
   * to be the integer division per pixel; one possible optimization
85 86 87 88 89
   * would be to accumulate into two 16-bit integer buffers and
   * only divide down after all three passes. (SSE parallel implementation
   * of the divide step is possible.)
   */

90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
#define BLUR_ROW_KERNEL(D)                                      \
  for (i = -(D) + offset; i < row_width + offset; i++)		\
    {                                                           \
      if (i >= 0 && i < row_width)                              \
        sum += row[i];                                          \
                                                                \
      if (i >= offset)						\
	{							\
	  if (i >= (D))						\
	    sum -= row[i - (D)];				\
                                                                \
	  tmp_buffer[i - offset] = (sum + (D) / 2) / (D);	\
	}							\
    }								\
  break;
105

106 107 108 109
  /* We unroll the values for d for radius 2-10 to avoid a generic
   * divide operation (not radius 1, because its a no-op) */
  switch (d)
    {
110 111 112 113 114 115 116 117 118
    case BOX_FILTER_SIZE_2: BLUR_ROW_KERNEL (BOX_FILTER_SIZE_2);
    case BOX_FILTER_SIZE_3: BLUR_ROW_KERNEL (BOX_FILTER_SIZE_3);
    case BOX_FILTER_SIZE_4: BLUR_ROW_KERNEL (BOX_FILTER_SIZE_4);
    case BOX_FILTER_SIZE_5: BLUR_ROW_KERNEL (BOX_FILTER_SIZE_5);
    case BOX_FILTER_SIZE_6: BLUR_ROW_KERNEL (BOX_FILTER_SIZE_6);
    case BOX_FILTER_SIZE_7: BLUR_ROW_KERNEL (BOX_FILTER_SIZE_7);
    case BOX_FILTER_SIZE_8: BLUR_ROW_KERNEL (BOX_FILTER_SIZE_8);
    case BOX_FILTER_SIZE_9: BLUR_ROW_KERNEL (BOX_FILTER_SIZE_9);
    case BOX_FILTER_SIZE_10: BLUR_ROW_KERNEL (BOX_FILTER_SIZE_10);
119
    default: BLUR_ROW_KERNEL (d);
120 121 122 123 124 125 126 127 128 129 130
    }

  memcpy (row, tmp_buffer, row_width);
}

static void
blur_rows (guchar *dst_buffer,
           guchar *tmp_buffer,
           int     buffer_width,
           int     buffer_height,
           int     d)
131
{
132 133 134 135 136 137 138 139 140 141
  int i;

  for (i = 0; i < buffer_height; i++)
    {
      guchar *row = dst_buffer + i * buffer_width;

      /* We want to produce a symmetric blur that spreads a pixel
       * equally far to the left and right. If d is odd that happens
       * naturally, but for d even, we approximate by using a blur
       * on either side and then a centered blur of size d + 1.
Matthias Clasen's avatar
Matthias Clasen committed
142
       * (technique also from the SVG specification)
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158
       */
      if (d % 2 == 1)
        {
          blur_xspan (row, tmp_buffer, buffer_width, d, 0);
          blur_xspan (row, tmp_buffer, buffer_width, d, 0);
          blur_xspan (row, tmp_buffer, buffer_width, d, 0);
        }
      else
        {
          blur_xspan (row, tmp_buffer, buffer_width, d, 1);
          blur_xspan (row, tmp_buffer, buffer_width, d, -1);
          blur_xspan (row, tmp_buffer, buffer_width, d + 1, 0);
        }
    }
}

Matthias Clasen's avatar
Matthias Clasen committed
159
/* Swaps width and height.
160 161 162 163 164 165
 */
static void
flip_buffer (guchar *dst_buffer,
             guchar *src_buffer,
             int     width,
             int     height)
166
{
167
  /* Working in blocks increases cache efficiency, compared to reading
Matthias Clasen's avatar
Matthias Clasen committed
168 169
   * or writing an entire column at once
   */
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185
#define BLOCK_SIZE 16

  int i0, j0;

  for (i0 = 0; i0 < width; i0 += BLOCK_SIZE)
    for (j0 = 0; j0 < height; j0 += BLOCK_SIZE)
      {
        int max_j = MIN(j0 + BLOCK_SIZE, height);
        int max_i = MIN(i0 + BLOCK_SIZE, width);
        int i, j;

        for (i = i0; i < max_i; i++)
          for (j = j0; j < max_j; j++)
            dst_buffer[i * height + j] = src_buffer[j * width + i];
      }
#undef BLOCK_SIZE
186 187
}

188
static void
189 190 191 192 193
_boxblur (guchar      *buffer,
          int          width,
          int          height,
          int          radius,
          GtkBlurFlags flags)
194
{
195
  guchar *flipped_buffer;
196
  int d = get_box_filter_size (radius);
197

198
  flipped_buffer = g_malloc (width * height);
199

200 201 202 203
  if (flags & GTK_BLUR_Y)
    {
      /* Step 1: swap rows and columns */
      flip_buffer (flipped_buffer, buffer, width, height);
204

205 206
      /* Step 2: blur rows (really columns) */
      blur_rows (flipped_buffer, buffer, height, width, d);
207

208 209 210
      /* Step 3: swap rows and columns */
      flip_buffer (buffer, flipped_buffer, height, width);
    }
211

212 213 214 215 216
  if (flags & GTK_BLUR_X)
    {
      /* Step 4: blur rows */
      blur_rows (buffer, flipped_buffer, width, height, d);
    }
217

218
  g_free (flipped_buffer);
219 220 221 222 223 224 225 226 227 228 229
}

/*
 * _gtk_cairo_blur_surface:
 * @surface: a cairo image surface.
 * @radius: the blur radius.
 *
 * Blurs the cairo image surface at the given radius.
 */
void
_gtk_cairo_blur_surface (cairo_surface_t* surface,
230 231
                         double           radius_d,
                         GtkBlurFlags     flags)
232
{
233
  int radius = radius_d;
234 235 236

  g_return_if_fail (surface != NULL);
  g_return_if_fail (cairo_surface_get_type (surface) == CAIRO_SURFACE_TYPE_IMAGE);
237
  g_return_if_fail (cairo_image_surface_get_format (surface) == CAIRO_FORMAT_A8);
238

239 240 241
  /* The code doesn't actually do any blurring for radius 1, as it
   * ends up with box filter size 1 */
  if (radius <= 1)
242 243
    return;

244 245 246
  if ((flags & (GTK_BLUR_X|GTK_BLUR_Y)) == 0)
    return;

Matthias Clasen's avatar
Matthias Clasen committed
247
  /* Before we mess with the surface, execute any pending drawing. */
248 249
  cairo_surface_flush (surface);

250
  _boxblur (cairo_image_surface_get_data (surface),
Benjamin Otte's avatar
Benjamin Otte committed
251
            cairo_image_surface_get_stride (surface),
252
            cairo_image_surface_get_height (surface),
253
            radius, flags);
254

Matthias Clasen's avatar
Matthias Clasen committed
255
  /* Inform cairo we altered the surface contents. */
256 257
  cairo_surface_mark_dirty (surface);
}
258

Matthias Clasen's avatar
Matthias Clasen committed
259
/*
260 261 262 263 264 265 266 267
 * _gtk_cairo_blur_compute_pixels:
 * @radius: the radius to compute the pixels for
 *
 * Computes the number of pixels necessary to extend an image in one
 * direction to hold the image with shadow.
 *
 * This is just the number of pixels added by the blur radius, shadow
 * offset and spread are not included.
268
 *
269 270 271 272 273 274 275 276 277 278 279
 * Much of this, the 3 * sqrt(2 * pi) / 4, is the known value for
 * approximating a Gaussian using box blurs.  This yields quite a good
 * approximation for a Gaussian.  Then we multiply this by 1.5 since our
 * code wants the radius of the entire triple-box-blur kernel instead of
 * the diameter of an individual box blur.  For more details, see:
 * http://www.w3.org/TR/SVG11/filters.html#feGaussianBlurElement
 * https://bugzilla.mozilla.org/show_bug.cgi?id=590039#c19
 */
int
_gtk_cairo_blur_compute_pixels (double radius)
{
280
  return floor (radius * GAUSSIAN_SCALE_FACTOR * 1.5 + 0.5);
281
}