PIKApp/libpika/pikabrush.c

313 lines
10 KiB
C
Raw Permalink Normal View History

2023-10-30 23:56:43 +01:00
/* LIBPIKA - The PIKA Library
* Copyright (C) 1995-1997 Peter Mattis and Spencer Kimball
*
* pikabrush.c
* Copyright (C) 2023 Jehan
*
* This library is free software: you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see
* <https://www.gnu.org/licenses/>.
*/
#include "config.h"
#include "pika.h"
#include "pikabrush.h"
struct _PikaBrush
{
PikaResource parent_instance;
/* Native size buffers of the brush contents. */
GeglBuffer *buffer;
GeglBuffer *mask;
};
G_DEFINE_TYPE (PikaBrush, pika_brush, PIKA_TYPE_RESOURCE);
static void pika_brush_finalize (GObject *object);
static void pika_brush_get_data (PikaBrush *brush);
static GeglBuffer * pika_brush_scale (GeglBuffer *buffer,
gint max_width,
gint max_height);
static const Babl * pika_brush_data_get_format (gint bpp,
gboolean with_alpha);
static void pika_brush_class_init (PikaBrushClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->finalize = pika_brush_finalize;
}
static void pika_brush_init (PikaBrush *brush)
{
brush->buffer = NULL;
brush->mask = NULL;
}
static void
pika_brush_finalize (GObject *object)
{
PikaBrush *brush = PIKA_BRUSH (object);
g_clear_object (&brush->buffer);
g_clear_object (&brush->mask);
G_OBJECT_CLASS (pika_brush_parent_class)->finalize (object);
}
/**
* pika_brush_get_buffer:
* @brush: a [class@Brush].
* @max_width: a maximum width for the returned buffer.
* @max_height: a maximum height for the returned buffer.
* @format: an optional Babl format.
*
* Gets pixel data of the brush within the bounding box specified by @max_width
* and @max_height. The data will be scaled down so that it fits within this
* size without changing its ratio. If the brush is smaller than this size to
* begin with, it will not be scaled up.
*
* If @max_width or @max_height are %NULL, the buffer is returned in the brush's
* native size.
*
* When the brush is parametric or a raster mask, only the mask (as returned by
* [method@Gimp.Brush.get_mask]) will be set. The returned buffer will be NULL.
*
* Make sure you called [func@Gegl.init] before calling any function using
* `GEGL`.
*
* Returns: (transfer full): a [class@Gegl.Buffer] of %NULL if the brush is parametric
* or mask only.
*/
GeglBuffer *
pika_brush_get_buffer (PikaBrush *brush,
gint max_width,
gint max_height,
const Babl *format)
{
pika_brush_get_data (brush);
if (brush->buffer == NULL)
return NULL;
if (max_width == 0 || max_height == 0 ||
(gegl_buffer_get_width (brush->buffer) <= max_width &&
gegl_buffer_get_height (brush->buffer) <= max_height))
return gegl_buffer_dup (brush->buffer);
return pika_brush_scale (brush->buffer, max_width, max_height);
}
/**
* pika_brush_get_mask:
* @brush: a [class@Brush].
* @max_width: a maximum width for the returned buffer.
* @max_height: a maximum height for the returned buffer.
* @format: an optional Babl format.
*
* Gets mask data of the brush within the bounding box specified by @max_width
* and @max_height. The data will be scaled down so that it fits within this
* size without changing its ratio. If the brush is smaller than this size to
* begin with, it will not be scaled up.
*
* If @max_width or @max_height are %NULL, the buffer is returned in the brush's
* native size.
*
* Make sure you called [func@Gegl.init] before calling any function using
* `GEGL`.
*
* Returns: (transfer full): a [class@Gegl.Buffer] representing the @brush mask.
*/
GeglBuffer *
pika_brush_get_mask (PikaBrush *brush,
gint max_width,
gint max_height,
const Babl *format)
{
pika_brush_get_data (brush);
g_return_val_if_fail (brush->mask != NULL, NULL);
if (max_width == 0 || max_height == 0 ||
(gegl_buffer_get_width (brush->mask) <= max_width &&
gegl_buffer_get_height (brush->mask) <= max_height))
return gegl_buffer_dup (brush->mask);
return pika_brush_scale (brush->mask, max_width, max_height);
}
static void
pika_brush_get_data (PikaBrush *brush)
{
gint width;
gint height;
gint mask_bpp;
GBytes *mask_bytes;
gint color_bpp;
GBytes *color_bytes;
const guchar *mask;
gsize mask_size;
const guchar *color;
gsize color_size;
/* We check the mask because the buffer might be NULL.
*
* This check assumes that the brush contents doesn't change, which is not a
* perfect assumption. We could maybe add a PDB call which would return
* the new brush data only if it changed since last call (which can be
* verified with some kind of internal runtime version to pass from one call
* to another). TODO
*/
if (brush->mask != NULL)
return;
g_clear_object (&brush->buffer);
g_clear_object (&brush->mask);
_pika_brush_get_pixels (brush, &width, &height,
&mask_bpp, &mask_bytes,
&color_bpp, &color_bytes);
mask = g_bytes_unref_to_data (mask_bytes, &mask_size);
color = g_bytes_unref_to_data (color_bytes, &color_size);
brush->mask = gegl_buffer_linear_new_from_data ((const gpointer) mask,
pika_brush_data_get_format (mask_bpp, FALSE),
GEGL_RECTANGLE (0, 0, width, height),
0, g_free, NULL);
if (color_bpp > 0)
{
GeglBufferIterator *gi;
GeglBuffer *buffer;
buffer = gegl_buffer_linear_new_from_data ((const gpointer) color,
pika_brush_data_get_format (color_bpp, FALSE),
GEGL_RECTANGLE (0, 0, width, height),
0, g_free, NULL);
brush->buffer = gegl_buffer_new (GEGL_RECTANGLE (0, 0, width, height),
pika_brush_data_get_format (color_bpp, TRUE));
gi = gegl_buffer_iterator_new (brush->buffer, NULL, 0, NULL,
GEGL_ACCESS_WRITE, GEGL_ABYSS_NONE, 3);
gegl_buffer_iterator_add (gi, buffer, GEGL_RECTANGLE (0, 0, width, height),
0, NULL, GEGL_ACCESS_READ, GEGL_ABYSS_NONE);
gegl_buffer_iterator_add (gi, brush->mask, GEGL_RECTANGLE (0, 0, width, height),
0, NULL, GEGL_ACCESS_READ, GEGL_ABYSS_NONE);
while (gegl_buffer_iterator_next (gi))
{
guint8 *out = gi->items[0].data;
guint8 *color = gi->items[1].data;
guint8 *alpha = gi->items[2].data;
for (gint i = 0; i < gi->length; i++)
{
gint c;
for (c = 0; c < color_bpp; c++)
out[c] = color[c];
out[c] = alpha[0];
out += color_bpp + 1;
color += color_bpp;
alpha += 1;
}
}
g_object_unref (buffer);
}
}
static GeglBuffer *
pika_brush_scale (GeglBuffer *buffer,
gint max_width,
gint max_height)
{
GeglBuffer *scaled = NULL;
GeglNode *graph;
GeglNode *source;
GeglNode *op;
GeglNode *sink;
gdouble width;
gdouble height;
gdouble scale;
height = (gdouble) max_height;
width = (gdouble) gegl_buffer_get_width (buffer) / gegl_buffer_get_height (buffer) * height;
if (width > (gdouble) max_width)
{
width = (gdouble) max_width;
height = (gdouble) gegl_buffer_get_height (buffer) / gegl_buffer_get_width (buffer) * width;
}
scale = width / gegl_buffer_get_width (buffer);
graph = gegl_node_new ();
source = gegl_node_new_child (graph,
"operation", "gegl:buffer-source",
"buffer", buffer,
NULL);
op = gegl_node_new_child (graph,
"operation", "gegl:scale-ratio",
"origin-x", 0.0,
"origin-y", 0.0,
"sampler", PIKA_INTERPOLATION_LINEAR,
"abyss-policy", GEGL_ABYSS_CLAMP,
"x", scale,
"y", scale,
NULL);
sink = gegl_node_new_child (graph,
"operation", "gegl:buffer-sink",
"buffer", &scaled,
"format", gegl_buffer_get_format (buffer),
NULL);
gegl_node_link_many (source, op, sink, NULL);
gegl_node_process (sink);
g_object_unref (graph);
return scaled;
}
static const Babl *
pika_brush_data_get_format (gint bpp,
gboolean with_alpha)
{
/* It's an ugly way to determine the proper format but pika_brush_get_pixels()
* doesn't give more info.
*/
switch (bpp)
{
case 1:
if (! with_alpha)
return babl_format ("Y' u8");
/* fallthrough */
case 2:
return babl_format ("Y'A u8");
case 3:
if (! with_alpha)
return babl_format ("R'G'B' u8");
/* fallthrough */
case 4:
return babl_format ("R'G'B'A u8");
default:
g_return_val_if_reached (NULL);
}
g_return_val_if_reached (NULL);
}