658 lines
18 KiB
C++
658 lines
18 KiB
C++
/* PIKA - Photo and Image Kooker Application
|
|
* a rebranding of The GNU Image Manipulation Program (created with heckimp)
|
|
* A derived work which may be trivial. However, any changes may be (C)2023 by Aldercone Studio
|
|
*
|
|
* Original copyright, applying to most contents (license remains unchanged):
|
|
* 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 "libpikabase/pikabase.h"
|
|
#include "libpikamath/pikamath.h"
|
|
|
|
extern "C"
|
|
{
|
|
|
|
#include "pika-gegl-types.h"
|
|
|
|
#include "pika-babl.h"
|
|
#include "pika-gegl-loops.h"
|
|
#include "pika-gegl-mask-combine.h"
|
|
|
|
|
|
#define EPSILON 1e-6
|
|
|
|
#define PIXELS_PER_THREAD \
|
|
(/* each thread costs as much as */ 64.0 * 64.0 /* pixels */)
|
|
|
|
|
|
gboolean
|
|
pika_gegl_mask_combine_rect (GeglBuffer *mask,
|
|
PikaChannelOps 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 PIKA_CHANNEL_OP_REPLACE:
|
|
case PIKA_CHANNEL_OP_ADD:
|
|
value = 1.0f;
|
|
break;
|
|
|
|
case PIKA_CHANNEL_OP_SUBTRACT:
|
|
value = 0.0f;
|
|
break;
|
|
|
|
case PIKA_CHANNEL_OP_INTERSECT:
|
|
return TRUE;
|
|
}
|
|
|
|
gegl_buffer_set_color_from_pixel (mask, &rect, &value,
|
|
babl_format ("Y float"));
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
gboolean
|
|
pika_gegl_mask_combine_ellipse (GeglBuffer *mask,
|
|
PikaChannelOps op,
|
|
gint x,
|
|
gint y,
|
|
gint w,
|
|
gint h,
|
|
gboolean antialias)
|
|
{
|
|
return pika_gegl_mask_combine_ellipse_rect (mask, op, x, y, w, h,
|
|
w / 2.0, h / 2.0, antialias);
|
|
}
|
|
|
|
gboolean
|
|
pika_gegl_mask_combine_ellipse_rect (GeglBuffer *mask,
|
|
PikaChannelOps 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 pika_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 = pika_babl_format_change_component_type (
|
|
format, PIKA_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 PIKA_CHANNEL_OP_REPLACE:
|
|
case PIKA_CHANNEL_OP_INTERSECT:
|
|
memset (dest, 0, bpp * n);
|
|
break;
|
|
|
|
case PIKA_CHANNEL_OP_ADD:
|
|
case PIKA_CHANNEL_OP_SUBTRACT:
|
|
break;
|
|
}
|
|
|
|
return (gpointer) ((guint8 *) dest + bpp * n);
|
|
};
|
|
|
|
auto fill1 = [=] (gpointer dest,
|
|
gint n)
|
|
{
|
|
switch (op)
|
|
{
|
|
case PIKA_CHANNEL_OP_REPLACE:
|
|
case PIKA_CHANNEL_OP_ADD:
|
|
gegl_memset_pattern (dest, one, bpp, n);
|
|
break;
|
|
|
|
case PIKA_CHANNEL_OP_SUBTRACT:
|
|
memset (dest, 0, bpp * n);
|
|
break;
|
|
|
|
case PIKA_CHANNEL_OP_INTERSECT:
|
|
break;
|
|
}
|
|
|
|
return (gpointer) ((guint8 *) dest + bpp * n);
|
|
};
|
|
|
|
auto set = [=] (gpointer dest,
|
|
gfloat value)
|
|
{
|
|
gfloat *p = (gfloat *) dest;
|
|
|
|
switch (op)
|
|
{
|
|
case PIKA_CHANNEL_OP_REPLACE:
|
|
*p = value;
|
|
break;
|
|
|
|
case PIKA_CHANNEL_OP_ADD:
|
|
*p = MIN (*p + value, 1.0);
|
|
break;
|
|
|
|
case PIKA_CHANNEL_OP_SUBTRACT:
|
|
*p = MAX (*p - value, 0.0);
|
|
break;
|
|
|
|
case PIKA_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 == PIKA_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
|
|
pika_gegl_mask_combine_buffer (GeglBuffer *mask,
|
|
GeglBuffer *add_on,
|
|
PikaChannelOps 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 == PIKA_CHANNEL_OP_REPLACE &&
|
|
(pika_babl_is_bounded (pika_babl_format_get_precision (add_on_format)) ||
|
|
pika_babl_is_bounded (pika_babl_format_get_precision (mask_format))))
|
|
{
|
|
/* See below: this additional hack is only needed for the
|
|
* pika-channel-combine-masks procedure, it's the only place that
|
|
* allows to combine arbitrary channels with each other.
|
|
*/
|
|
gegl_buffer_set_format (
|
|
add_on,
|
|
pika_babl_format_change_trc (
|
|
add_on_format, pika_babl_format_get_trc (mask_format)));
|
|
|
|
pika_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 = pika_babl_format_change_component_type (
|
|
mask_format, PIKA_COMPONENT_TYPE_FLOAT);
|
|
|
|
add_on_format = pika_babl_format_change_component_type (
|
|
add_on_format, PIKA_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 == PIKA_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 PIKA_CHANNEL_OP_REPLACE:
|
|
process ([] (const gfloat *mask,
|
|
const gfloat *add_on)
|
|
{
|
|
return *add_on;
|
|
});
|
|
break;
|
|
|
|
case PIKA_CHANNEL_OP_ADD:
|
|
process ([] (const gfloat *mask,
|
|
const gfloat *add_on)
|
|
{
|
|
return *mask + *add_on;
|
|
});
|
|
break;
|
|
|
|
case PIKA_CHANNEL_OP_SUBTRACT:
|
|
process ([] (const gfloat *mask,
|
|
const gfloat *add_on)
|
|
{
|
|
return *mask - *add_on;
|
|
});
|
|
break;
|
|
|
|
case PIKA_CHANNEL_OP_INTERSECT:
|
|
process ([] (const gfloat *mask,
|
|
const gfloat *add_on)
|
|
{
|
|
return MIN (*mask, *add_on);
|
|
});
|
|
break;
|
|
}
|
|
});
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
} /* extern "C" */
|