652 lines
17 KiB
C++
652 lines
17 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 <gegl.h>
|
|
#include <gdk-pixbuf/gdk-pixbuf.h>
|
|
|
|
#include "libpikamath/pikamath.h"
|
|
|
|
extern "C"
|
|
{
|
|
|
|
#include "paint-types.h"
|
|
|
|
#include "core/pikatempbuf.h"
|
|
|
|
#include "pikabrushcore.h"
|
|
#include "pikabrushcore-loops.h"
|
|
|
|
} /* extern "C" */
|
|
|
|
#include "pikabrushcore-kernels.h"
|
|
|
|
|
|
#define PIXELS_PER_THREAD \
|
|
(/* each thread costs as much as */ 64.0 * 64.0 /* pixels */)
|
|
|
|
#define EPSILON 1e-6
|
|
|
|
|
|
static void
|
|
clear_edges (PikaTempBuf *buf,
|
|
gint top,
|
|
gint bottom,
|
|
gint left,
|
|
gint right)
|
|
{
|
|
guchar *data;
|
|
const Babl *format = pika_temp_buf_get_format (buf);
|
|
gint bpp = babl_format_get_bytes_per_pixel (format);
|
|
gint width = pika_temp_buf_get_width (buf);
|
|
gint height = pika_temp_buf_get_height (buf);
|
|
|
|
if (top + bottom >= height || left + right >= width)
|
|
{
|
|
pika_temp_buf_data_clear (buf);
|
|
|
|
return;
|
|
}
|
|
|
|
data = pika_temp_buf_get_data (buf);
|
|
|
|
memset (data, 0, (top * width + left) * bpp);
|
|
|
|
if (left + right)
|
|
{
|
|
gint stride = width * bpp;
|
|
gint size = (left + right) * bpp;
|
|
gint y;
|
|
|
|
data = pika_temp_buf_get_data (buf) +
|
|
((top + 1) * width - right) * bpp;
|
|
|
|
for (y = top; y < height - bottom - 1; y++)
|
|
{
|
|
memset (data, 0, size);
|
|
|
|
data += stride;
|
|
}
|
|
}
|
|
|
|
data = pika_temp_buf_get_data (buf) +
|
|
((height - bottom) * width - right) * bpp;
|
|
|
|
memset (data, 0, (bottom * width + right) * bpp);
|
|
}
|
|
|
|
template <class T>
|
|
static inline void
|
|
rotate_pointers (T **p,
|
|
gint n)
|
|
{
|
|
T *tmp;
|
|
gint i;
|
|
|
|
tmp = p[0];
|
|
|
|
for (i = 0; i < n - 1; i++)
|
|
p[i] = p[i + 1];
|
|
|
|
p[i] = tmp;
|
|
}
|
|
|
|
template <class T>
|
|
static void
|
|
pika_brush_core_subsample_mask_impl (const PikaTempBuf *mask,
|
|
PikaTempBuf *dest,
|
|
gint dest_offset_x,
|
|
gint dest_offset_y,
|
|
gint index1,
|
|
gint index2)
|
|
{
|
|
using value_type = typename Subsample<T>::value_type;
|
|
using kernel_type = typename Subsample<T>::kernel_type;
|
|
using accum_type = typename Subsample<T>::accum_type;
|
|
|
|
Subsample<T> subsample;
|
|
const kernel_type *kernel = subsample.kernel[index2][index1];
|
|
gint mask_width = pika_temp_buf_get_width (mask);
|
|
gint mask_height = pika_temp_buf_get_height (mask);
|
|
gint dest_width = pika_temp_buf_get_width (dest);
|
|
gint dest_height = pika_temp_buf_get_height (dest);
|
|
|
|
gegl_parallel_distribute_range (
|
|
mask_height, PIXELS_PER_THREAD / mask_width,
|
|
[=] (gint y, gint height)
|
|
{
|
|
const value_type *m;
|
|
value_type *d;
|
|
const kernel_type *k;
|
|
gint y0;
|
|
gint i, j;
|
|
gint r, s;
|
|
gint offs;
|
|
accum_type *accum[KERNEL_HEIGHT];
|
|
|
|
/* Allocate and initialize the accum buffer */
|
|
for (i = 0; i < KERNEL_HEIGHT ; i++)
|
|
accum[i] = gegl_scratch_new0 (accum_type, dest_width + 1);
|
|
|
|
y0 = MAX (y - (KERNEL_HEIGHT - 1), 0);
|
|
|
|
m = (const value_type *) pika_temp_buf_get_data (mask) +
|
|
y0 * mask_width;
|
|
|
|
for (i = y0; i < y; i++)
|
|
{
|
|
for (j = 0; j < mask_width; j++)
|
|
{
|
|
k = kernel + KERNEL_WIDTH * (y - i);
|
|
for (r = y - i; r < KERNEL_HEIGHT; r++)
|
|
{
|
|
offs = j + dest_offset_x;
|
|
s = KERNEL_WIDTH;
|
|
while (s--)
|
|
accum[r][offs++] += *m * *k++;
|
|
}
|
|
m++;
|
|
}
|
|
|
|
rotate_pointers (accum, KERNEL_HEIGHT);
|
|
}
|
|
|
|
for (i = y; i < y + height; i++)
|
|
{
|
|
for (j = 0; j < mask_width; j++)
|
|
{
|
|
k = kernel;
|
|
for (r = 0; r < KERNEL_HEIGHT; r++)
|
|
{
|
|
offs = j + dest_offset_x;
|
|
s = KERNEL_WIDTH;
|
|
while (s--)
|
|
accum[r][offs++] += *m * *k++;
|
|
}
|
|
m++;
|
|
}
|
|
|
|
/* store the accum buffer into the destination mask */
|
|
d = (value_type *) pika_temp_buf_get_data (dest) +
|
|
(i + dest_offset_y) * dest_width;
|
|
for (j = 0; j < dest_width; j++)
|
|
*d++ = subsample.round (accum[0][j]);
|
|
|
|
rotate_pointers (accum, KERNEL_HEIGHT);
|
|
|
|
memset (accum[KERNEL_HEIGHT - 1], 0,
|
|
sizeof (accum_type) * dest_width);
|
|
}
|
|
|
|
if (y + height == mask_height)
|
|
{
|
|
/* store the rest of the accum buffer into the dest mask */
|
|
while (i + dest_offset_y < dest_height)
|
|
{
|
|
d = (value_type *) pika_temp_buf_get_data (dest) +
|
|
(i + dest_offset_y) * dest_width;
|
|
for (j = 0; j < dest_width; j++)
|
|
*d++ = subsample.round (accum[0][j]);
|
|
|
|
rotate_pointers (accum, KERNEL_HEIGHT);
|
|
i++;
|
|
}
|
|
}
|
|
|
|
for (i = KERNEL_HEIGHT - 1; i >= 0; i--)
|
|
gegl_scratch_free (accum[i]);
|
|
});
|
|
}
|
|
|
|
const PikaTempBuf *
|
|
pika_brush_core_subsample_mask (PikaBrushCore *core,
|
|
const PikaTempBuf *mask,
|
|
gdouble x,
|
|
gdouble y)
|
|
{
|
|
PikaTempBuf *dest;
|
|
const Babl *mask_format;
|
|
gdouble left;
|
|
gint index1;
|
|
gint index2;
|
|
gint dest_offset_x = 0;
|
|
gint dest_offset_y = 0;
|
|
gint mask_width = pika_temp_buf_get_width (mask);
|
|
gint mask_height = pika_temp_buf_get_height (mask);
|
|
|
|
left = x - floor (x);
|
|
index1 = (gint) (left * (gdouble) (KERNEL_SUBSAMPLE + 1));
|
|
|
|
left = y - floor (y);
|
|
index2 = (gint) (left * (gdouble) (KERNEL_SUBSAMPLE + 1));
|
|
|
|
if ((mask_width % 2) == 0)
|
|
{
|
|
index1 += KERNEL_SUBSAMPLE >> 1;
|
|
|
|
if (index1 > KERNEL_SUBSAMPLE)
|
|
{
|
|
index1 -= KERNEL_SUBSAMPLE + 1;
|
|
dest_offset_x = 1;
|
|
}
|
|
}
|
|
|
|
if ((mask_height % 2) == 0)
|
|
{
|
|
index2 += KERNEL_SUBSAMPLE >> 1;
|
|
|
|
if (index2 > KERNEL_SUBSAMPLE)
|
|
{
|
|
index2 -= KERNEL_SUBSAMPLE + 1;
|
|
dest_offset_y = 1;
|
|
}
|
|
}
|
|
|
|
if (mask == core->last_subsample_brush_mask &&
|
|
! core->subsample_cache_invalid)
|
|
{
|
|
if (core->subsample_brushes[index2][index1])
|
|
return core->subsample_brushes[index2][index1];
|
|
}
|
|
else
|
|
{
|
|
gint i, j;
|
|
|
|
for (i = 0; i < KERNEL_SUBSAMPLE + 1; i++)
|
|
for (j = 0; j < KERNEL_SUBSAMPLE + 1; j++)
|
|
g_clear_pointer (&core->subsample_brushes[i][j], pika_temp_buf_unref);
|
|
|
|
core->last_subsample_brush_mask = mask;
|
|
core->subsample_cache_invalid = FALSE;
|
|
}
|
|
|
|
mask_format = pika_temp_buf_get_format (mask);
|
|
|
|
dest = pika_temp_buf_new (mask_width + 2,
|
|
mask_height + 2,
|
|
mask_format);
|
|
clear_edges (dest, dest_offset_y, 0, 0, 0);
|
|
|
|
core->subsample_brushes[index2][index1] = dest;
|
|
|
|
if (mask_format == babl_format ("Y u8"))
|
|
{
|
|
pika_brush_core_subsample_mask_impl<guchar> (mask, dest,
|
|
dest_offset_x, dest_offset_y,
|
|
index1, index2);
|
|
}
|
|
else if (mask_format == babl_format ("Y float"))
|
|
{
|
|
pika_brush_core_subsample_mask_impl<gfloat> (mask, dest,
|
|
dest_offset_x, dest_offset_y,
|
|
index1, index2);
|
|
}
|
|
else
|
|
{
|
|
g_warn_if_reached ();
|
|
}
|
|
|
|
return dest;
|
|
}
|
|
|
|
/* The simple pressure profile
|
|
*
|
|
* It is: I'(I) = MIN (2 * pressure * I, 1)
|
|
*/
|
|
class SimplePressure
|
|
{
|
|
gfloat scale;
|
|
|
|
public:
|
|
SimplePressure (gdouble pressure)
|
|
{
|
|
scale = 2.0 * pressure;
|
|
}
|
|
|
|
guchar
|
|
operator () (guchar x) const
|
|
{
|
|
gint v = RINT (scale * x);
|
|
|
|
return MIN (v, 255);
|
|
}
|
|
|
|
gfloat
|
|
operator () (gfloat x) const
|
|
{
|
|
gfloat v = scale * x;
|
|
|
|
return MIN (v, 1.0f);
|
|
}
|
|
|
|
template <class T>
|
|
T
|
|
operator () (T x) const = delete;
|
|
};
|
|
|
|
/* The fancy pressure profile
|
|
*
|
|
* It is: I'(I) = tanh (20 * (pressure - 0.5) * I) : pressure > 0.5
|
|
* I'(I) = 1 - tanh (20 * (0.5 - pressure) * (1 - I)) : pressure < 0.5
|
|
*
|
|
* It looks like:
|
|
*
|
|
* low pressure medium pressure high pressure
|
|
*
|
|
* | / --
|
|
* | / /
|
|
* / / |
|
|
* -- / |
|
|
*/
|
|
class FancyPressure
|
|
{
|
|
gfloat map[257];
|
|
|
|
public:
|
|
FancyPressure (gdouble pressure)
|
|
{
|
|
gdouble ds, s, c;
|
|
gint i;
|
|
|
|
ds = (pressure - 0.5) * (20.0 / 256.0);
|
|
s = 0;
|
|
c = 1.0;
|
|
|
|
if (ds > 0)
|
|
{
|
|
for (i = 0; i < 256; i++)
|
|
{
|
|
map[i] = s / c;
|
|
s += c * ds;
|
|
c += s * ds;
|
|
}
|
|
|
|
for (i = 0; i < 256; i++)
|
|
map[i] = map[i] / map[255];
|
|
}
|
|
else
|
|
{
|
|
ds = -ds;
|
|
|
|
for (i = 255; i >= 0; i--)
|
|
{
|
|
map[i] = s / c;
|
|
s += c * ds;
|
|
c += s * ds;
|
|
}
|
|
|
|
for (i = 255; i >= 0; i--)
|
|
map[i] = 1.0f - map[i] / map[0];
|
|
}
|
|
|
|
map[256] = map[255];
|
|
}
|
|
|
|
guchar
|
|
operator () (guchar x) const
|
|
{
|
|
return RINT (255.0f * map[x]);
|
|
}
|
|
|
|
gfloat
|
|
operator () (gfloat x) const
|
|
{
|
|
gint i;
|
|
gfloat f;
|
|
|
|
x *= 255.0f;
|
|
|
|
i = floorf (x);
|
|
f = x - i;
|
|
|
|
return map[i] + (map[i + 1] - map[i]) * f;
|
|
}
|
|
|
|
template <class T>
|
|
T
|
|
operator () (T x) const = delete;
|
|
};
|
|
|
|
template <class T>
|
|
class CachedPressure
|
|
{
|
|
T map[T (~0) + 1];
|
|
|
|
public:
|
|
template <class Pressure>
|
|
CachedPressure (Pressure pressure)
|
|
{
|
|
gint i;
|
|
|
|
for (i = 0; i < (gint) G_N_ELEMENTS (map); i++)
|
|
map[i] = pressure (T (i));
|
|
}
|
|
|
|
T
|
|
operator () (T x) const
|
|
{
|
|
return map[x];
|
|
}
|
|
|
|
template <class U>
|
|
U
|
|
operator () (U x) const = delete;
|
|
};
|
|
|
|
template <class T,
|
|
class Pressure>
|
|
void
|
|
pika_brush_core_pressurize_mask_impl (const PikaTempBuf *mask,
|
|
PikaTempBuf *dest,
|
|
Pressure pressure)
|
|
{
|
|
gegl_parallel_distribute_range (
|
|
pika_temp_buf_get_width (mask) * pika_temp_buf_get_height (mask),
|
|
PIXELS_PER_THREAD,
|
|
[=] (gint offset, gint size)
|
|
{
|
|
const T *m;
|
|
T *d;
|
|
gint i;
|
|
|
|
m = (const T *) pika_temp_buf_get_data (mask) + offset;
|
|
d = ( T *) pika_temp_buf_get_data (dest) + offset;
|
|
|
|
for (i = 0; i < size; i++)
|
|
*d++ = pressure (*m++);
|
|
});
|
|
}
|
|
|
|
/* #define FANCY_PRESSURE */
|
|
|
|
const PikaTempBuf *
|
|
pika_brush_core_pressurize_mask (PikaBrushCore *core,
|
|
const PikaTempBuf *brush_mask,
|
|
gdouble x,
|
|
gdouble y,
|
|
gdouble pressure)
|
|
{
|
|
const PikaTempBuf *subsample_mask;
|
|
const Babl *subsample_mask_format;
|
|
|
|
/* Get the raw subsampled mask */
|
|
subsample_mask = pika_brush_core_subsample_mask (core,
|
|
brush_mask,
|
|
x, y);
|
|
|
|
/* Special case pressure = 0.5 */
|
|
if (fabs (pressure - 0.5) <= EPSILON)
|
|
return subsample_mask;
|
|
|
|
g_clear_pointer (&core->pressure_brush, pika_temp_buf_unref);
|
|
|
|
subsample_mask_format = pika_temp_buf_get_format (subsample_mask);
|
|
|
|
core->pressure_brush =
|
|
pika_temp_buf_new (pika_temp_buf_get_width (brush_mask) + 2,
|
|
pika_temp_buf_get_height (brush_mask) + 2,
|
|
subsample_mask_format);
|
|
|
|
#ifdef FANCY_PRESSURE
|
|
using Pressure = FancyPressure;
|
|
#else
|
|
using Pressure = SimplePressure;
|
|
#endif
|
|
|
|
if (subsample_mask_format == babl_format ("Y u8"))
|
|
{
|
|
pika_brush_core_pressurize_mask_impl<guchar> (subsample_mask,
|
|
core->pressure_brush,
|
|
CachedPressure<guchar> (
|
|
Pressure (pressure)));
|
|
}
|
|
else if (subsample_mask_format == babl_format ("Y float"))
|
|
{
|
|
pika_brush_core_pressurize_mask_impl<gfloat> (subsample_mask,
|
|
core->pressure_brush,
|
|
Pressure (pressure));
|
|
}
|
|
else
|
|
{
|
|
g_warn_if_reached ();
|
|
}
|
|
|
|
return core->pressure_brush;
|
|
}
|
|
|
|
template <class T>
|
|
static void
|
|
pika_brush_core_solidify_mask_impl (const PikaTempBuf *mask,
|
|
PikaTempBuf *dest,
|
|
gint dest_offset_x,
|
|
gint dest_offset_y)
|
|
{
|
|
gint mask_width = pika_temp_buf_get_width (mask);
|
|
gint mask_height = pika_temp_buf_get_height (mask);
|
|
gint dest_width = pika_temp_buf_get_width (dest);
|
|
|
|
gegl_parallel_distribute_area (
|
|
GEGL_RECTANGLE (0, 0, mask_width, mask_height),
|
|
PIXELS_PER_THREAD,
|
|
[=] (const GeglRectangle *area)
|
|
{
|
|
const T *m;
|
|
gfloat *d;
|
|
gint i, j;
|
|
|
|
m = (const T *) pika_temp_buf_get_data (mask) +
|
|
area->y * mask_width + area->x;
|
|
d = ((gfloat *) pika_temp_buf_get_data (dest) +
|
|
((dest_offset_y + 1 + area->y) * dest_width +
|
|
(dest_offset_x + 1 + area->x)));
|
|
|
|
for (i = 0; i < area->height; i++)
|
|
{
|
|
for (j = 0; j < area->width; j++)
|
|
*d++ = (*m++) ? 1.0 : 0.0;
|
|
|
|
m += mask_width - area->width;
|
|
d += dest_width - area->width;
|
|
}
|
|
});
|
|
}
|
|
|
|
const PikaTempBuf *
|
|
pika_brush_core_solidify_mask (PikaBrushCore *core,
|
|
const PikaTempBuf *brush_mask,
|
|
gdouble x,
|
|
gdouble y)
|
|
{
|
|
PikaTempBuf *dest;
|
|
const Babl *brush_mask_format;
|
|
gint dest_offset_x = 0;
|
|
gint dest_offset_y = 0;
|
|
gint brush_mask_width = pika_temp_buf_get_width (brush_mask);
|
|
gint brush_mask_height = pika_temp_buf_get_height (brush_mask);
|
|
|
|
if ((brush_mask_width % 2) == 0)
|
|
{
|
|
if (x < 0.0)
|
|
x = fmod (x, brush_mask_width) + brush_mask_width;
|
|
|
|
if ((x - floor (x)) >= 0.5)
|
|
dest_offset_x++;
|
|
}
|
|
|
|
if ((brush_mask_height % 2) == 0)
|
|
{
|
|
if (y < 0.0)
|
|
y = fmod (y, brush_mask_height) + brush_mask_height;
|
|
|
|
if ((y - floor (y)) >= 0.5)
|
|
dest_offset_y++;
|
|
}
|
|
|
|
if (! core->solid_cache_invalid &&
|
|
brush_mask == core->last_solid_brush_mask)
|
|
{
|
|
if (core->solid_brushes[dest_offset_y][dest_offset_x])
|
|
return core->solid_brushes[dest_offset_y][dest_offset_x];
|
|
}
|
|
else
|
|
{
|
|
gint i, j;
|
|
|
|
for (i = 0; i < BRUSH_CORE_SOLID_SUBSAMPLE; i++)
|
|
for (j = 0; j < BRUSH_CORE_SOLID_SUBSAMPLE; j++)
|
|
g_clear_pointer (&core->solid_brushes[i][j], pika_temp_buf_unref);
|
|
|
|
core->last_solid_brush_mask = brush_mask;
|
|
core->solid_cache_invalid = FALSE;
|
|
}
|
|
|
|
brush_mask_format = pika_temp_buf_get_format (brush_mask);
|
|
|
|
dest = pika_temp_buf_new (brush_mask_width + 2,
|
|
brush_mask_height + 2,
|
|
babl_format ("Y float"));
|
|
clear_edges (dest,
|
|
1 + dest_offset_y, 1 - dest_offset_y,
|
|
1 + dest_offset_x, 1 - dest_offset_x);
|
|
|
|
core->solid_brushes[dest_offset_y][dest_offset_x] = dest;
|
|
|
|
if (brush_mask_format == babl_format ("Y u8"))
|
|
{
|
|
pika_brush_core_solidify_mask_impl<guchar> (brush_mask, dest,
|
|
dest_offset_x, dest_offset_y);
|
|
}
|
|
else if (brush_mask_format == babl_format ("Y float"))
|
|
{
|
|
pika_brush_core_solidify_mask_impl<gfloat> (brush_mask, dest,
|
|
dest_offset_x, dest_offset_y);
|
|
}
|
|
else
|
|
{
|
|
g_warn_if_reached ();
|
|
}
|
|
|
|
return dest;
|
|
}
|