summaryrefslogtreecommitdiffstats
path: root/app/gegl/gimp-gegl-mask-combine.cc
diff options
context:
space:
mode:
Diffstat (limited to 'app/gegl/gimp-gegl-mask-combine.cc')
-rw-r--r--app/gegl/gimp-gegl-mask-combine.cc653
1 files changed, 653 insertions, 0 deletions
diff --git a/app/gegl/gimp-gegl-mask-combine.cc b/app/gegl/gimp-gegl-mask-combine.cc
new file mode 100644
index 0000000..0e61c0e
--- /dev/null
+++ b/app/gegl/gimp-gegl-mask-combine.cc
@@ -0,0 +1,653 @@
+/* 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 <gio/gio.h>
+#include <gegl.h>
+
+#include "libgimpbase/gimpbase.h"
+#include "libgimpmath/gimpmath.h"
+
+extern "C"
+{
+
+#include "gimp-gegl-types.h"
+
+#include "gimp-babl.h"
+#include "gimp-gegl-loops.h"
+#include "gimp-gegl-mask-combine.h"
+
+
+#define EPSILON 1e-6
+
+#define PIXELS_PER_THREAD \
+ (/* each thread costs as much as */ 64.0 * 64.0 /* pixels */)
+
+
+gboolean
+gimp_gegl_mask_combine_rect (GeglBuffer *mask,
+ GimpChannelOps op,
+ gint x,
+ gint y,
+ gint w,
+ gint h)
+{
+ GeglRectangle rect;
+ gfloat value;
+
+ g_return_val_if_fail (GEGL_IS_BUFFER (mask), FALSE);
+
+ if (! gegl_rectangle_intersect (&rect,
+ GEGL_RECTANGLE (x, y, w, h),
+ gegl_buffer_get_abyss (mask)))
+ {
+ return FALSE;
+ }
+
+ switch (op)
+ {
+ case GIMP_CHANNEL_OP_REPLACE:
+ case GIMP_CHANNEL_OP_ADD:
+ value = 1.0f;
+ break;
+
+ case GIMP_CHANNEL_OP_SUBTRACT:
+ value = 0.0f;
+ break;
+
+ case GIMP_CHANNEL_OP_INTERSECT:
+ return TRUE;
+ }
+
+ gegl_buffer_set_color_from_pixel (mask, &rect, &value,
+ babl_format ("Y float"));
+
+ return TRUE;
+}
+
+gboolean
+gimp_gegl_mask_combine_ellipse (GeglBuffer *mask,
+ GimpChannelOps op,
+ gint x,
+ gint y,
+ gint w,
+ gint h,
+ gboolean antialias)
+{
+ return gimp_gegl_mask_combine_ellipse_rect (mask, op, x, y, w, h,
+ w / 2.0, h / 2.0, antialias);
+}
+
+gboolean
+gimp_gegl_mask_combine_ellipse_rect (GeglBuffer *mask,
+ GimpChannelOps op,
+ gint x,
+ gint y,
+ gint w,
+ gint h,
+ gdouble rx,
+ gdouble ry,
+ gboolean antialias)
+{
+ GeglRectangle rect;
+ const Babl *format;
+ gint bpp;
+ gfloat one_f = 1.0f;
+ gpointer one;
+ gdouble cx;
+ gdouble cy;
+ gint left;
+ gint right;
+ gint top;
+ gint bottom;
+
+ g_return_val_if_fail (GEGL_IS_BUFFER (mask), FALSE);
+
+ if (rx <= EPSILON || ry <= EPSILON)
+ return gimp_gegl_mask_combine_rect (mask, op, x, y, w, h);
+
+ left = x;
+ right = x + w;
+ top = y;
+ bottom = y + h;
+
+ cx = (left + right) / 2.0;
+ cy = (top + bottom) / 2.0;
+
+ rx = MIN (rx, w / 2.0);
+ ry = MIN (ry, h / 2.0);
+
+ if (! gegl_rectangle_intersect (&rect,
+ GEGL_RECTANGLE (x, y, w, h),
+ gegl_buffer_get_abyss (mask)))
+ {
+ return FALSE;
+ }
+
+ format = gegl_buffer_get_format (mask);
+
+ if (antialias)
+ {
+ format = gimp_babl_format_change_component_type (
+ format, GIMP_COMPONENT_TYPE_FLOAT);
+ }
+
+ bpp = babl_format_get_bytes_per_pixel (format);
+ one = g_alloca (bpp);
+
+ babl_process (babl_fish ("Y float", format), &one_f, one, 1);
+
+ /* coordinate-system transforms. (x, y) coordinates are in the image
+ * coordinate-system, and (u, v) coordinates are in a coordinate-system
+ * aligned with the center of one of the elliptic corners, with the positive
+ * directions pointing away from the rectangle. when converting from (x, y)
+ * to (u, v), we use the closest elliptic corner.
+ */
+ auto x_to_u = [=] (gdouble x)
+ {
+ if (x < cx)
+ return (left + rx) - x;
+ else
+ return x - (right - rx);
+ };
+
+ auto y_to_v = [=] (gdouble y)
+ {
+ if (y < cy)
+ return (top + ry) - y;
+ else
+ return y - (bottom - ry);
+ };
+
+ auto u_to_x_left = [=] (gdouble u)
+ {
+ return (left + rx) - u;
+ };
+
+ auto u_to_x_right = [=] (gdouble u)
+ {
+ return (right - rx) + u;
+ };
+
+ /* intersection of a horizontal line with the ellipse */
+ auto v_to_u = [=] (gdouble v)
+ {
+ if (v > 0.0)
+ return sqrt (MAX (SQR (rx) - SQR (rx * v / ry), 0.0));
+ else
+ return rx;
+ };
+
+ /* intersection of a vertical line with the ellipse */
+ auto u_to_v = [=] (gdouble u)
+ {
+ if (u > 0.0)
+ return sqrt (MAX (SQR (ry) - SQR (ry * u / rx), 0.0));
+ else
+ return ry;
+ };
+
+ /* signed, normalized distance of a point from the ellipse's circumference.
+ * the sign of the result determines if the point is inside (positive) or
+ * outside (negative) the ellipse. the result is normalized to the cross-
+ * section length of a pixel, in the direction of the closest point along the
+ * ellipse.
+ *
+ * we use the following method to approximate the distance: pass horizontal
+ * and vertical lines at the given point, P, and find their (positive) points
+ * of intersection with the ellipse, A and B. the segment AB is an
+ * approximation of the corresponding elliptic arc (see bug #147836). find
+ * the closest point, C, to P, along the segment AB. find the (positive)
+ * point of intersection, Q, of the line PC and the ellipse. Q is an
+ * approximation for the closest point to P along the ellipse, and the
+ * approximated distance is the distance from P to Q.
+ */
+ auto ellipse_distance = [=] (gdouble u,
+ gdouble v)
+ {
+ gdouble du;
+ gdouble dv;
+ gdouble t;
+ gdouble a, b, c;
+ gdouble d;
+
+ u = MAX (u, 0.0);
+ v = MAX (v, 0.0);
+
+ du = v_to_u (v) - u;
+ dv = u_to_v (u) - v;
+
+ t = SQR (du) / (SQR (du) + SQR (dv));
+
+ du *= 1.0 - t;
+ dv *= t;
+
+ v *= rx / ry;
+ dv *= rx / ry;
+
+ a = SQR (du) + SQR (dv);
+ b = u * du + v * dv;
+ c = SQR (u) + SQR (v) - SQR (rx);
+
+ if (a <= EPSILON)
+ return 0.0;
+
+ if (c < 0.0)
+ t = (-b + sqrt (MAX (SQR (b) - a * c, 0.0))) / a;
+ else
+ t = (-b - sqrt (MAX (SQR (b) - a * c, 0.0))) / a;
+
+ dv *= ry / rx;
+
+ d = sqrt (SQR (du * t) + SQR (dv * t));
+
+ if (c > 0.0)
+ d = -d;
+
+ d /= sqrt (SQR (MIN (du / dv, dv / du)) + 1.0);
+
+ return d;
+ };
+
+ /* anti-aliased value of a pixel */
+ auto pixel_value = [=] (gint x,
+ gint y)
+ {
+ gdouble u = x_to_u (x + 0.5);
+ gdouble v = y_to_v (y + 0.5);
+ gdouble d = ellipse_distance (u, v);
+
+ /* use the distance of the pixel's center from the ellipse to approximate
+ * the coverage
+ */
+ d = CLAMP (0.5 + d, 0.0, 1.0);
+
+ /* we're at the horizontal boundary of an elliptic corner */
+ if (u < 0.5)
+ d = d * (0.5 + u) + (0.5 - u);
+
+ /* we're at the vertical boundary of an elliptic corner */
+ if (v < 0.5)
+ d = d * (0.5 + v) + (0.5 - v);
+
+ /* opposite horizontal corners intersect the pixel */
+ if (x == (right - 1) - (x - left))
+ d = 2.0 * d - 1.0;
+
+ /* opposite vertical corners intersect the pixel */
+ if (y == (bottom - 1) - (y - top))
+ d = 2.0 * d - 1.0;
+
+ return d;
+ };
+
+ auto ellipse_range = [=] (gdouble y,
+ gdouble *x0,
+ gdouble *x1)
+ {
+ gdouble u = v_to_u (y_to_v (y));
+
+ *x0 = u_to_x_left (u);
+ *x1 = u_to_x_right (u);
+ };
+
+ auto fill0 = [=] (gpointer dest,
+ gint n)
+ {
+ switch (op)
+ {
+ case GIMP_CHANNEL_OP_REPLACE:
+ case GIMP_CHANNEL_OP_INTERSECT:
+ memset (dest, 0, bpp * n);
+ break;
+
+ case GIMP_CHANNEL_OP_ADD:
+ case GIMP_CHANNEL_OP_SUBTRACT:
+ break;
+ }
+
+ return (gpointer) ((guint8 *) dest + bpp * n);
+ };
+
+ auto fill1 = [=] (gpointer dest,
+ gint n)
+ {
+ switch (op)
+ {
+ case GIMP_CHANNEL_OP_REPLACE:
+ case GIMP_CHANNEL_OP_ADD:
+ gegl_memset_pattern (dest, one, bpp, n);
+ break;
+
+ case GIMP_CHANNEL_OP_SUBTRACT:
+ memset (dest, 0, bpp * n);
+ break;
+
+ case GIMP_CHANNEL_OP_INTERSECT:
+ break;
+ }
+
+ return (gpointer) ((guint8 *) dest + bpp * n);
+ };
+
+ auto set = [=] (gpointer dest,
+ gfloat value)
+ {
+ gfloat *p = (gfloat *) dest;
+
+ switch (op)
+ {
+ case GIMP_CHANNEL_OP_REPLACE:
+ *p = value;
+ break;
+
+ case GIMP_CHANNEL_OP_ADD:
+ *p = MIN (*p + value, 1.0);
+ break;
+
+ case GIMP_CHANNEL_OP_SUBTRACT:
+ *p = MAX (*p - value, 0.0);
+ break;
+
+ case GIMP_CHANNEL_OP_INTERSECT:
+ *p = MIN (*p, value);
+ break;
+ }
+
+ return (gpointer) (p + 1);
+ };
+
+ gegl_parallel_distribute_area (
+ &rect, PIXELS_PER_THREAD,
+ [=] (const GeglRectangle *area)
+ {
+ GeglBufferIterator *iter;
+
+ iter = gegl_buffer_iterator_new (
+ mask, area, 0, format,
+ op == GIMP_CHANNEL_OP_REPLACE ? GEGL_ACCESS_WRITE :
+ GEGL_ACCESS_READWRITE,
+ GEGL_ABYSS_NONE, 1);
+
+ while (gegl_buffer_iterator_next (iter))
+ {
+ const GeglRectangle *roi = &iter->items[0].roi;
+ gpointer d = iter->items[0].data;
+ gdouble tx0, ty0;
+ gdouble tx1, ty1;
+ gdouble x0;
+ gdouble x1;
+ gint y;
+
+ /* tile bounds */
+ tx0 = roi->x;
+ ty0 = roi->y;
+
+ tx1 = roi->x + roi->width;
+ ty1 = roi->y + roi->height;
+
+ if (! antialias)
+ {
+ tx0 += 0.5;
+ ty0 += 0.5;
+
+ tx1 -= 0.5;
+ ty1 -= 0.5;
+ }
+
+ /* if the tile is fully inside/outside the ellipse, fill it with 1/0,
+ * respectively, and skip the rest.
+ */
+ ellipse_range (ty0, &x0, &x1);
+
+ if (tx0 >= x0 && tx1 <= x1)
+ {
+ ellipse_range (ty1, &x0, &x1);
+
+ if (tx0 >= x0 && tx1 <= x1)
+ {
+ fill1 (d, iter->length);
+
+ continue;
+ }
+ }
+ else if (tx1 < x0 || tx0 > x1)
+ {
+ ellipse_range (ty1, &x0, &x1);
+
+ if (tx1 < x0 || tx0 > x1)
+ {
+ if ((ty0 - cy) * (ty1 - cy) >= 0.0)
+ {
+ fill0 (d, iter->length);
+
+ continue;
+ }
+ }
+ }
+
+ for (y = roi->y; y < roi->y + roi->height; y++)
+ {
+ gint a, b;
+
+ if (antialias)
+ {
+ gdouble v = y_to_v (y + 0.5);
+ gdouble u0 = v_to_u (v - 0.5);
+ gdouble u1 = v_to_u (v + 0.5);
+ gint x;
+
+ a = floor (u_to_x_left (u0)) - roi->x;
+ a = CLAMP (a, 0, roi->width);
+
+ b = ceil (u_to_x_left (u1)) - roi->x;
+ b = CLAMP (b, a, roi->width);
+
+ d = fill0 (d, a);
+
+ for (x = roi->x + a; x < roi->x + b; x++)
+ d = set (d, pixel_value (x, y));
+
+ a = floor (u_to_x_right (u1)) - roi->x;
+ a = CLAMP (a, b, roi->width);
+
+ d = fill1 (d, a - b);
+
+ b = ceil (u_to_x_right (u0)) - roi->x;
+ b = CLAMP (b, a, roi->width);
+
+ for (x = roi->x + a; x < roi->x + b; x++)
+ d = set (d, pixel_value (x, y));
+
+ d = fill0 (d, roi->width - b);
+ }
+ else
+ {
+ ellipse_range (y + 0.5, &x0, &x1);
+
+ a = ceil (x0 - 0.5) - roi->x;
+ a = CLAMP (a, 0, roi->width);
+
+ b = floor (x1 + 0.5) - roi->x;
+ b = CLAMP (b, 0, roi->width);
+
+ d = fill0 (d, a);
+ d = fill1 (d, b - a);
+ d = fill0 (d, roi->width - b);
+ }
+ }
+ }
+ });
+
+ return TRUE;
+}
+
+gboolean
+gimp_gegl_mask_combine_buffer (GeglBuffer *mask,
+ GeglBuffer *add_on,
+ GimpChannelOps op,
+ gint off_x,
+ gint off_y)
+{
+ GeglRectangle mask_rect;
+ GeglRectangle add_on_rect;
+ const Babl *mask_format;
+ const Babl *add_on_format;
+
+ g_return_val_if_fail (GEGL_IS_BUFFER (mask), FALSE);
+ g_return_val_if_fail (GEGL_IS_BUFFER (add_on), FALSE);
+
+ if (! gegl_rectangle_intersect (&mask_rect,
+ GEGL_RECTANGLE (
+ off_x + gegl_buffer_get_x (add_on),
+ off_y + gegl_buffer_get_y (add_on),
+ gegl_buffer_get_width (add_on),
+ gegl_buffer_get_height (add_on)),
+ gegl_buffer_get_abyss (mask)))
+ {
+ return FALSE;
+ }
+
+ add_on_rect = mask_rect;
+ add_on_rect.x -= off_x;
+ add_on_rect.y -= off_y;
+
+ mask_format = gegl_buffer_get_format (mask);
+ add_on_format = gegl_buffer_get_format (add_on);
+
+ if (op == GIMP_CHANNEL_OP_REPLACE &&
+ (gimp_babl_is_bounded (gimp_babl_format_get_precision (add_on_format)) ||
+ gimp_babl_is_bounded (gimp_babl_format_get_precision (mask_format))))
+ {
+ /* See below: this additional hack is only needed for the
+ * gimp-channel-combine-masks procedure, it's the only place that
+ * allows to combine arbitrary channels with each other.
+ */
+ gegl_buffer_set_format (
+ add_on,
+ gimp_babl_format_change_linear (
+ add_on_format, gimp_babl_format_get_linear (mask_format)));
+
+ gimp_gegl_buffer_copy (add_on, &add_on_rect, GEGL_ABYSS_NONE,
+ mask, &mask_rect);
+
+ gegl_buffer_set_format (add_on, NULL);
+
+ return TRUE;
+ }
+
+ /* This is a hack: all selections/layer masks/channels are always
+ * linear except for channels in 8-bit images. We don't want these
+ * "Y' u8" to be converted to "Y float" because that would cause a
+ * gamma canversion and give unexpected results for
+ * "add/subtract/etc channel from selection". Instead, use all
+ * channel values "as-is", which makes no differce except in the
+ * 8-bit case where we need it.
+ *
+ * See https://bugzilla.gnome.org/show_bug.cgi?id=791519
+ */
+ mask_format = gimp_babl_format_change_component_type (
+ mask_format, GIMP_COMPONENT_TYPE_FLOAT);
+
+ add_on_format = gimp_babl_format_change_component_type (
+ add_on_format, GIMP_COMPONENT_TYPE_FLOAT);
+
+ gegl_parallel_distribute_area (
+ &mask_rect, PIXELS_PER_THREAD,
+ [=] (const GeglRectangle *mask_area)
+ {
+ GeglBufferIterator *iter;
+ GeglRectangle add_on_area;
+
+ add_on_area = *mask_area;
+ add_on_area.x -= off_x;
+ add_on_area.y -= off_y;
+
+ iter = gegl_buffer_iterator_new (mask, mask_area, 0,
+ mask_format,
+ op == GIMP_CHANNEL_OP_REPLACE ?
+ GEGL_ACCESS_WRITE :
+ GEGL_ACCESS_READWRITE,
+ GEGL_ABYSS_NONE, 2);
+
+ gegl_buffer_iterator_add (iter, add_on, &add_on_area, 0,
+ add_on_format,
+ GEGL_ACCESS_READ, GEGL_ABYSS_NONE);
+
+ auto process = [=] (auto value)
+ {
+ while (gegl_buffer_iterator_next (iter))
+ {
+ gfloat *mask_data = (gfloat *) iter->items[0].data;
+ const gfloat *add_on_data = (const gfloat *) iter->items[1].data;
+ gint count = iter->length;
+
+ while (count--)
+ {
+ const gfloat val = value (mask_data, add_on_data);
+
+ *mask_data = CLAMP (val, 0.0f, 1.0f);
+
+ add_on_data++;
+ mask_data++;
+ }
+ }
+ };
+
+ switch (op)
+ {
+ case GIMP_CHANNEL_OP_REPLACE:
+ process ([] (const gfloat *mask,
+ const gfloat *add_on)
+ {
+ return *add_on;
+ });
+ break;
+
+ case GIMP_CHANNEL_OP_ADD:
+ process ([] (const gfloat *mask,
+ const gfloat *add_on)
+ {
+ return *mask + *add_on;
+ });
+ break;
+
+ case GIMP_CHANNEL_OP_SUBTRACT:
+ process ([] (const gfloat *mask,
+ const gfloat *add_on)
+ {
+ return *mask - *add_on;
+ });
+ break;
+
+ case GIMP_CHANNEL_OP_INTERSECT:
+ process ([] (const gfloat *mask,
+ const gfloat *add_on)
+ {
+ return MIN (*mask, *add_on);
+ });
+ break;
+ }
+ });
+
+ return TRUE;
+}
+
+} /* extern "C" */