/* 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 * . */ #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); }