summaryrefslogtreecommitdiffstats
path: root/app/paint/gimpink.c
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--app/paint/gimpink.c785
1 files changed, 785 insertions, 0 deletions
diff --git a/app/paint/gimpink.c b/app/paint/gimpink.c
new file mode 100644
index 0000000..50f72b8
--- /dev/null
+++ b/app/paint/gimpink.c
@@ -0,0 +1,785 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <string.h>
+
+#include <gdk-pixbuf/gdk-pixbuf.h>
+#include <gegl.h>
+
+#include "libgimpmath/gimpmath.h"
+
+#include "paint-types.h"
+
+#include "operations/layer-modes/gimp-layer-modes.h"
+
+#include "gegl/gimp-gegl-utils.h"
+
+#include "core/gimp-palettes.h"
+#include "core/gimpdrawable.h"
+#include "core/gimpimage.h"
+#include "core/gimpimage-undo.h"
+#include "core/gimppickable.h"
+#include "core/gimpsymmetry.h"
+#include "core/gimptempbuf.h"
+
+#include "gimpinkoptions.h"
+#include "gimpink.h"
+#include "gimpink-blob.h"
+#include "gimpinkundo.h"
+
+#include "gimp-intl.h"
+
+
+#define SUBSAMPLE 8
+
+
+/* local function prototypes */
+
+static void gimp_ink_finalize (GObject *object);
+
+static void gimp_ink_paint (GimpPaintCore *paint_core,
+ GimpDrawable *drawable,
+ GimpPaintOptions *paint_options,
+ GimpSymmetry *sym,
+ GimpPaintState paint_state,
+ guint32 time);
+static GeglBuffer * gimp_ink_get_paint_buffer (GimpPaintCore *paint_core,
+ GimpDrawable *drawable,
+ GimpPaintOptions *paint_options,
+ GimpLayerMode paint_mode,
+ const GimpCoords *coords,
+ gint *paint_buffer_x,
+ gint *paint_buffer_y,
+ gint *paint_width,
+ gint *paint_height);
+static GimpUndo * gimp_ink_push_undo (GimpPaintCore *core,
+ GimpImage *image,
+ const gchar *undo_desc);
+
+static void gimp_ink_motion (GimpPaintCore *paint_core,
+ GimpDrawable *drawable,
+ GimpPaintOptions *paint_options,
+ GimpSymmetry *sym,
+ guint32 time);
+
+static GimpBlob * ink_pen_ellipse (GimpInkOptions *options,
+ gdouble x_center,
+ gdouble y_center,
+ gdouble pressure,
+ gdouble xtilt,
+ gdouble ytilt,
+ gdouble velocity,
+ const GimpMatrix3 *transform);
+
+static void render_blob (GeglBuffer *buffer,
+ GeglRectangle *rect,
+ GimpBlob *blob);
+
+
+G_DEFINE_TYPE (GimpInk, gimp_ink, GIMP_TYPE_PAINT_CORE)
+
+#define parent_class gimp_ink_parent_class
+
+
+void
+gimp_ink_register (Gimp *gimp,
+ GimpPaintRegisterCallback callback)
+{
+ (* callback) (gimp,
+ GIMP_TYPE_INK,
+ GIMP_TYPE_INK_OPTIONS,
+ "gimp-ink",
+ _("Ink"),
+ "gimp-tool-ink");
+}
+
+static void
+gimp_ink_class_init (GimpInkClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GimpPaintCoreClass *paint_core_class = GIMP_PAINT_CORE_CLASS (klass);
+
+ object_class->finalize = gimp_ink_finalize;
+
+ paint_core_class->paint = gimp_ink_paint;
+ paint_core_class->get_paint_buffer = gimp_ink_get_paint_buffer;
+ paint_core_class->push_undo = gimp_ink_push_undo;
+}
+
+static void
+gimp_ink_init (GimpInk *ink)
+{
+}
+
+static void
+gimp_ink_finalize (GObject *object)
+{
+ GimpInk *ink = GIMP_INK (object);
+
+ if (ink->start_blobs)
+ {
+ g_list_free_full (ink->start_blobs, g_free);
+ ink->start_blobs = NULL;
+ }
+
+ if (ink->last_blobs)
+ {
+ g_list_free_full (ink->last_blobs, g_free);
+ ink->last_blobs = NULL;
+ }
+
+ G_OBJECT_CLASS (parent_class)->finalize (object);
+}
+
+static void
+gimp_ink_paint (GimpPaintCore *paint_core,
+ GimpDrawable *drawable,
+ GimpPaintOptions *paint_options,
+ GimpSymmetry *sym,
+ GimpPaintState paint_state,
+ guint32 time)
+{
+ GimpInk *ink = GIMP_INK (paint_core);
+ GimpCoords *cur_coords;
+ GimpCoords last_coords;
+
+ gimp_paint_core_get_last_coords (paint_core, &last_coords);
+ cur_coords = gimp_symmetry_get_origin (sym);
+
+ switch (paint_state)
+ {
+ case GIMP_PAINT_STATE_INIT:
+ {
+ GimpContext *context = GIMP_CONTEXT (paint_options);
+ GimpRGB foreground;
+
+ gimp_symmetry_set_stateful (sym, TRUE);
+ gimp_context_get_foreground (context, &foreground);
+ gimp_palettes_add_color_history (context->gimp,
+ &foreground);
+
+ if (cur_coords->x == last_coords.x &&
+ cur_coords->y == last_coords.y)
+ {
+ if (ink->start_blobs)
+ {
+ g_list_free_full (ink->start_blobs, g_free);
+ ink->start_blobs = NULL;
+ }
+
+ if (ink->last_blobs)
+ {
+ g_list_free_full (ink->last_blobs, g_free);
+ ink->last_blobs = NULL;
+ }
+ }
+ else if (ink->last_blobs)
+ {
+ GimpBlob *last_blob;
+ GList *iter;
+ gint i;
+
+ if (ink->start_blobs)
+ {
+ g_list_free_full (ink->start_blobs, g_free);
+ ink->start_blobs = NULL;
+ }
+
+ /* save the start blobs of each stroke for undo otherwise */
+ for (iter = ink->last_blobs, i = 0; iter; iter = g_list_next (iter), i++)
+ {
+ last_blob = g_list_nth_data (ink->last_blobs, i);
+
+ ink->start_blobs = g_list_prepend (ink->start_blobs,
+ gimp_blob_duplicate (last_blob));
+ }
+ ink->start_blobs = g_list_reverse (ink->start_blobs);
+ }
+ }
+ break;
+
+ case GIMP_PAINT_STATE_MOTION:
+ gimp_ink_motion (paint_core, drawable, paint_options, sym, time);
+ break;
+
+ case GIMP_PAINT_STATE_FINISH:
+ gimp_symmetry_set_stateful (sym, FALSE);
+ break;
+ }
+}
+
+static GeglBuffer *
+gimp_ink_get_paint_buffer (GimpPaintCore *paint_core,
+ GimpDrawable *drawable,
+ GimpPaintOptions *paint_options,
+ GimpLayerMode paint_mode,
+ const GimpCoords *coords,
+ gint *paint_buffer_x,
+ gint *paint_buffer_y,
+ gint *paint_width,
+ gint *paint_height)
+{
+ GimpInk *ink = GIMP_INK (paint_core);
+ gint x, y;
+ gint width, height;
+ gint dwidth, dheight;
+ gint x1, y1, x2, y2;
+
+ gimp_blob_bounds (ink->cur_blob, &x, &y, &width, &height);
+
+ dwidth = gimp_item_get_width (GIMP_ITEM (drawable));
+ dheight = gimp_item_get_height (GIMP_ITEM (drawable));
+
+ x1 = CLAMP (x / SUBSAMPLE - 1, 0, dwidth);
+ y1 = CLAMP (y / SUBSAMPLE - 1, 0, dheight);
+ x2 = CLAMP ((x + width) / SUBSAMPLE + 2, 0, dwidth);
+ y2 = CLAMP ((y + height) / SUBSAMPLE + 2, 0, dheight);
+
+ if (paint_width)
+ *paint_width = width / SUBSAMPLE + 3;
+ if (paint_height)
+ *paint_height = height / SUBSAMPLE + 3;
+
+ /* configure the canvas buffer */
+ if ((x2 - x1) && (y2 - y1))
+ {
+ GimpTempBuf *temp_buf;
+ const Babl *format;
+ GimpLayerCompositeMode composite_mode;
+
+ composite_mode = gimp_layer_mode_get_paint_composite_mode (paint_mode);
+
+ format = gimp_layer_mode_get_format (paint_mode,
+ GIMP_LAYER_COLOR_SPACE_AUTO,
+ GIMP_LAYER_COLOR_SPACE_AUTO,
+ composite_mode,
+ gimp_drawable_get_format (drawable));
+
+ temp_buf = gimp_temp_buf_new ((x2 - x1), (y2 - y1),
+ format);
+
+ *paint_buffer_x = x1;
+ *paint_buffer_y = y1;
+
+ if (paint_core->paint_buffer)
+ g_object_unref (paint_core->paint_buffer);
+
+ paint_core->paint_buffer = gimp_temp_buf_create_buffer (temp_buf);
+
+ gimp_temp_buf_unref (temp_buf);
+
+ return paint_core->paint_buffer;
+ }
+
+ return NULL;
+}
+
+static GimpUndo *
+gimp_ink_push_undo (GimpPaintCore *core,
+ GimpImage *image,
+ const gchar *undo_desc)
+{
+ return gimp_image_undo_push (image, GIMP_TYPE_INK_UNDO,
+ GIMP_UNDO_INK, undo_desc,
+ 0,
+ "paint-core", core,
+ NULL);
+}
+
+static void
+gimp_ink_motion (GimpPaintCore *paint_core,
+ GimpDrawable *drawable,
+ GimpPaintOptions *paint_options,
+ GimpSymmetry *sym,
+ guint32 time)
+{
+ GimpInk *ink = GIMP_INK (paint_core);
+ GimpInkOptions *options = GIMP_INK_OPTIONS (paint_options);
+ GimpContext *context = GIMP_CONTEXT (paint_options);
+ GList *blob_unions = NULL;
+ GList *blobs_to_render = NULL;
+ GeglBuffer *paint_buffer;
+ gint paint_buffer_x;
+ gint paint_buffer_y;
+ GimpLayerMode paint_mode;
+ GimpRGB foreground;
+ GeglColor *color;
+ GimpBlob *last_blob;
+ GimpCoords *coords;
+ gint n_strokes;
+ gint i;
+
+ n_strokes = gimp_symmetry_get_size (sym);
+
+ if (ink->last_blobs &&
+ g_list_length (ink->last_blobs) != n_strokes)
+ {
+ g_list_free_full (ink->last_blobs, g_free);
+ ink->last_blobs = NULL;
+ }
+
+ if (! ink->last_blobs)
+ {
+ if (ink->start_blobs)
+ {
+ g_list_free_full (ink->start_blobs, g_free);
+ ink->start_blobs = NULL;
+ }
+
+ for (i = 0; i < n_strokes; i++)
+ {
+ GimpMatrix3 transform;
+
+ coords = gimp_symmetry_get_coords (sym, i);
+
+ gimp_symmetry_get_matrix (sym, i, &transform);
+
+ last_blob = ink_pen_ellipse (options,
+ coords->x,
+ coords->y,
+ coords->pressure,
+ coords->xtilt,
+ coords->ytilt,
+ 100,
+ &transform);
+
+ ink->last_blobs = g_list_prepend (ink->last_blobs,
+ last_blob);
+ ink->start_blobs = g_list_prepend (ink->start_blobs,
+ gimp_blob_duplicate (last_blob));
+ blobs_to_render = g_list_prepend (blobs_to_render, last_blob);
+ }
+ ink->start_blobs = g_list_reverse (ink->start_blobs);
+ ink->last_blobs = g_list_reverse (ink->last_blobs);
+ blobs_to_render = g_list_reverse (blobs_to_render);
+ }
+ else
+ {
+ for (i = 0; i < n_strokes; i++)
+ {
+ GimpBlob *blob;
+ GimpBlob *blob_union = NULL;
+ GimpMatrix3 transform;
+
+ coords = gimp_symmetry_get_coords (sym, i);
+
+ gimp_symmetry_get_matrix (sym, i, &transform);
+
+ blob = ink_pen_ellipse (options,
+ coords->x,
+ coords->y,
+ coords->pressure,
+ coords->xtilt,
+ coords->ytilt,
+ coords->velocity * 100,
+ &transform);
+
+ last_blob = g_list_nth_data (ink->last_blobs, i);
+ blob_union = gimp_blob_convex_union (last_blob, blob);
+
+ g_free (last_blob);
+ g_list_nth (ink->last_blobs, i)->data = blob;
+
+ blobs_to_render = g_list_prepend (blobs_to_render, blob_union);
+ blob_unions = g_list_prepend (blob_unions, blob_union);
+ }
+ blobs_to_render = g_list_reverse (blobs_to_render);
+ }
+
+ paint_mode = gimp_context_get_paint_mode (context);
+
+ gimp_context_get_foreground (context, &foreground);
+ gimp_pickable_srgb_to_image_color (GIMP_PICKABLE (drawable),
+ &foreground, &foreground);
+ color = gimp_gegl_color_new (&foreground);
+
+ for (i = 0; i < n_strokes; i++)
+ {
+ GimpBlob *blob_to_render = g_list_nth_data (blobs_to_render, i);
+
+ coords = gimp_symmetry_get_coords (sym, i);
+
+ ink->cur_blob = blob_to_render;
+ paint_buffer = gimp_paint_core_get_paint_buffer (paint_core, drawable,
+ paint_options,
+ paint_mode,
+ coords,
+ &paint_buffer_x,
+ &paint_buffer_y,
+ NULL, NULL);
+ ink->cur_blob = NULL;
+
+ if (! paint_buffer)
+ continue;
+
+ gegl_buffer_set_color (paint_buffer, NULL, color);
+
+ /* draw the blob directly to the canvas_buffer */
+ render_blob (paint_core->canvas_buffer,
+ GEGL_RECTANGLE (paint_core->paint_buffer_x,
+ paint_core->paint_buffer_y,
+ gegl_buffer_get_width (paint_core->paint_buffer),
+ gegl_buffer_get_height (paint_core->paint_buffer)),
+ blob_to_render);
+
+ /* draw the paint_area using the just rendered canvas_buffer as mask */
+ gimp_paint_core_paste (paint_core,
+ NULL,
+ paint_core->paint_buffer_x,
+ paint_core->paint_buffer_y,
+ drawable,
+ GIMP_OPACITY_OPAQUE,
+ gimp_context_get_opacity (context),
+ paint_mode,
+ GIMP_PAINT_CONSTANT);
+
+ }
+
+ g_object_unref (color);
+
+ g_list_free_full (blob_unions, g_free);
+}
+
+static GimpBlob *
+ink_pen_ellipse (GimpInkOptions *options,
+ gdouble x_center,
+ gdouble y_center,
+ gdouble pressure,
+ gdouble xtilt,
+ gdouble ytilt,
+ gdouble velocity,
+ const GimpMatrix3 *transform)
+{
+ GimpBlobFunc blob_function;
+ gdouble size;
+ gdouble tsin, tcos;
+ gdouble aspect, radmin;
+ gdouble x,y;
+ gdouble tscale;
+ gdouble tscale_c;
+ gdouble tscale_s;
+
+ /* Adjust the size depending on pressure. */
+
+ size = options->size * (1.0 + options->size_sensitivity *
+ (2.0 * pressure - 1.0));
+
+ /* Adjust the size further depending on pointer velocity and
+ * velocity-sensitivity. These 'magic constants' are 'feels
+ * natural' tigert-approved. --ADM
+ */
+
+ if (velocity < 3.0)
+ velocity = 3.0;
+
+#ifdef VERBOSE
+ g_printerr ("%g (%g) -> ", size, velocity);
+#endif
+
+ size = (options->vel_sensitivity *
+ ((4.5 * size) / (1.0 + options->vel_sensitivity * (2.0 * velocity)))
+ + (1.0 - options->vel_sensitivity) * size);
+
+#ifdef VERBOSE
+ g_printerr ("%g\n", (gfloat) size);
+#endif
+
+ /* Clamp resulting size to sane limits */
+
+ if (size > options->size * (1.0 + options->size_sensitivity))
+ size = options->size * (1.0 + options->size_sensitivity);
+
+ if (size * SUBSAMPLE < 1.0)
+ size = 1.0 / SUBSAMPLE;
+
+ /* Add brush angle/aspect to tilt vectorially */
+
+ /* I'm not happy with the way the brush widget info is combined with
+ * tilt info from the brush. My personal feeling is that
+ * representing both as affine transforms would make the most
+ * sense. -RLL
+ */
+
+ tscale = options->tilt_sensitivity * 10.0;
+ tscale_c = tscale * cos (gimp_deg_to_rad (options->tilt_angle));
+ tscale_s = tscale * sin (gimp_deg_to_rad (options->tilt_angle));
+
+ x = (options->blob_aspect * cos (options->blob_angle) +
+ xtilt * tscale_c - ytilt * tscale_s);
+ y = (options->blob_aspect * sin (options->blob_angle) +
+ ytilt * tscale_c + xtilt * tscale_s);
+
+#ifdef VERBOSE
+ g_printerr ("angle %g aspect %g; %g %g; %g %g\n",
+ options->blob_angle, options->blob_aspect,
+ tscale_c, tscale_s, x, y);
+#endif
+
+ aspect = sqrt (SQR (x) + SQR (y));
+
+ if (aspect != 0)
+ {
+ tcos = x / aspect;
+ tsin = y / aspect;
+ }
+ else
+ {
+ tcos = cos (options->blob_angle);
+ tsin = sin (options->blob_angle);
+ }
+
+ gimp_matrix3_transform_point (transform,
+ tcos, tsin,
+ &tcos, &tsin);
+
+ aspect = CLAMP (aspect, 1.0, 10.0);
+
+ radmin = MAX (1.0, SUBSAMPLE * size / aspect);
+
+ switch (options->blob_type)
+ {
+ case GIMP_INK_BLOB_TYPE_CIRCLE:
+ blob_function = gimp_blob_ellipse;
+ break;
+
+ case GIMP_INK_BLOB_TYPE_SQUARE:
+ blob_function = gimp_blob_square;
+ break;
+
+ case GIMP_INK_BLOB_TYPE_DIAMOND:
+ blob_function = gimp_blob_diamond;
+ break;
+
+ default:
+ g_return_val_if_reached (NULL);
+ break;
+ }
+
+ return (* blob_function) (x_center * SUBSAMPLE,
+ y_center * SUBSAMPLE,
+ radmin * aspect * tcos,
+ radmin * aspect * tsin,
+ -radmin * tsin,
+ radmin * tcos);
+}
+
+
+/*********************************/
+/* Rendering functions */
+/*********************************/
+
+/* Some of this stuff should probably be combined with the
+ * code it was copied from in paint_core.c; but I wanted
+ * to learn this stuff, so I've kept it simple.
+ *
+ * The following only supports CONSTANT mode. Incremental
+ * would, I think, interact strangely with the way we
+ * do things. But it wouldn't be hard to implement at all.
+ */
+
+enum
+{
+ ROW_START,
+ ROW_STOP
+};
+
+/* The insertion sort here, for SUBSAMPLE = 8, tends to beat out
+ * qsort() by 4x with CFLAGS=-O2, 2x with CFLAGS=-g
+ */
+static void
+insert_sort (gint *data,
+ gint n)
+{
+ gint i, j, k;
+
+ for (i = 2; i < 2 * n; i += 2)
+ {
+ gint tmp1 = data[i];
+ gint tmp2 = data[i + 1];
+
+ j = 0;
+
+ while (data[j] < tmp1)
+ j += 2;
+
+ for (k = i; k > j; k -= 2)
+ {
+ data[k] = data[k - 2];
+ data[k + 1] = data[k - 1];
+ }
+
+ data[j] = tmp1;
+ data[j + 1] = tmp2;
+ }
+}
+
+static void
+fill_run (gfloat *dest,
+ gfloat alpha,
+ gint w)
+{
+ if (alpha == 1.0)
+ {
+ while (w--)
+ {
+ *dest = 1.0;
+ dest++;
+ }
+ }
+ else
+ {
+ while (w--)
+ {
+ *dest = MAX (*dest, alpha);
+ dest++;
+ }
+ }
+}
+
+static void
+render_blob_line (GimpBlob *blob,
+ gfloat *dest,
+ gint x,
+ gint y,
+ gint width)
+{
+ gint buf[4 * SUBSAMPLE];
+ gint *data = buf;
+ gint n = 0;
+ gint i, j;
+ gint current = 0; /* number of filled rows at this point
+ * in the scan line
+ */
+ gint last_x;
+
+ /* Sort start and ends for all lines */
+
+ j = y * SUBSAMPLE - blob->y;
+ for (i = 0; i < SUBSAMPLE; i++)
+ {
+ if (j >= blob->height)
+ break;
+
+ if ((j > 0) && (blob->data[j].left <= blob->data[j].right))
+ {
+ data[2 * n] = blob->data[j].left;
+ data[2 * n + 1] = ROW_START;
+ data[2 * SUBSAMPLE + 2 * n] = blob->data[j].right;
+ data[2 * SUBSAMPLE + 2 * n + 1] = ROW_STOP;
+ n++;
+ }
+ j++;
+ }
+
+ /* If we have less than SUBSAMPLE rows, compress */
+ if (n < SUBSAMPLE)
+ {
+ for (i = 0; i < 2 * n; i++)
+ data[2 * n + i] = data[2 * SUBSAMPLE + i];
+ }
+
+ /* Now count start and end separately */
+ n *= 2;
+
+ insert_sort (data, n);
+
+ /* Discard portions outside of tile */
+
+ while ((n > 0) && (data[0] < SUBSAMPLE*x))
+ {
+ if (data[1] == ROW_START)
+ current++;
+ else
+ current--;
+ data += 2;
+ n--;
+ }
+
+ while ((n > 0) && (data[2*(n-1)] >= SUBSAMPLE*(x+width)))
+ n--;
+
+ /* Render the row */
+
+ last_x = 0;
+ for (i = 0; i < n;)
+ {
+ gint cur_x = data[2 * i] / SUBSAMPLE - x;
+ gint pixel;
+
+ /* Fill in portion leading up to this pixel */
+ if (current && cur_x != last_x)
+ fill_run (dest + last_x, (gfloat) current / SUBSAMPLE, cur_x - last_x);
+
+ /* Compute the value for this pixel */
+ pixel = current * SUBSAMPLE;
+
+ while (i<n)
+ {
+ gint tmp_x = data[2 * i] / SUBSAMPLE;
+
+ if (tmp_x - x != cur_x)
+ break;
+
+ if (data[2 * i + 1] == ROW_START)
+ {
+ current++;
+ pixel += ((tmp_x + 1) * SUBSAMPLE) - data[2 * i];
+ }
+ else
+ {
+ current--;
+ pixel -= ((tmp_x + 1) * SUBSAMPLE) - data[2 * i];
+ }
+
+ i++;
+ }
+
+ dest[cur_x] = MAX (dest[cur_x], (gfloat) pixel / (SUBSAMPLE * SUBSAMPLE));
+
+ last_x = cur_x + 1;
+ }
+
+ if (current != 0)
+ fill_run (dest + last_x, (gfloat) current / SUBSAMPLE, width - last_x);
+}
+
+static void
+render_blob (GeglBuffer *buffer,
+ GeglRectangle *rect,
+ GimpBlob *blob)
+{
+ GeglBufferIterator *iter;
+ GeglRectangle *roi;
+
+ iter = gegl_buffer_iterator_new (buffer, rect, 0, babl_format ("Y float"),
+ GEGL_ACCESS_READWRITE, GEGL_ABYSS_NONE, 1);
+ roi = &iter->items[0].roi;
+
+ while (gegl_buffer_iterator_next (iter))
+ {
+ gfloat *d = iter->items[0].data;
+ gint h = roi->height;
+ gint y;
+
+ for (y = 0; y < h; y++, d += roi->width * 1)
+ {
+ render_blob_line (blob, d, roi->x, roi->y + y, roi->width);
+ }
+ }
+}