/* 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 . */ #include "config.h" #include #include #include #include "libpikabase/pikabase.h" #include "libpikacolor/pikacolor.h" #include "libpikaconfig/pikaconfig.h" #include "core-types.h" #include "config/pikadialogconfig.h" #include "gegl/pika-gegl-apply-operation.h" #include "gegl/pika-gegl-mask.h" #include "gegl/pika-gegl-mask-combine.h" #include "gegl/pika-gegl-utils.h" #include "operations/layer-modes/pika-layer-modes.h" #include "pika.h" #include "pikachannel.h" #include "pikadrawable.h" #include "pikadrawable-bucket-fill.h" #include "pikafilloptions.h" #include "pikastrokeoptions.h" #include "pikaimage.h" #include "pikalineart.h" #include "pikapickable.h" #include "pikapickable-contiguous-region.h" #include "pika-intl.h" /* public functions */ void pika_drawable_bucket_fill (PikaDrawable *drawable, PikaFillOptions *options, gboolean fill_transparent, PikaSelectCriterion fill_criterion, gdouble threshold, gboolean sample_merged, gboolean diagonal_neighbors, gdouble seed_x, gdouble seed_y) { PikaImage *image; GeglBuffer *buffer; gdouble mask_x; gdouble mask_y; gint width, height; g_return_if_fail (PIKA_IS_DRAWABLE (drawable)); g_return_if_fail (pika_item_is_attached (PIKA_ITEM (drawable))); g_return_if_fail (PIKA_IS_FILL_OPTIONS (options)); image = pika_item_get_image (PIKA_ITEM (drawable)); pika_set_busy (image->pika); buffer = pika_drawable_get_bucket_fill_buffer (drawable, options, fill_transparent, fill_criterion, threshold, FALSE, sample_merged, diagonal_neighbors, seed_x, seed_y, NULL, &mask_x, &mask_y, &width, &height); if (buffer) { /* Apply it to the image */ pika_drawable_apply_buffer (drawable, buffer, GEGL_RECTANGLE (0, 0, width, height), TRUE, C_("undo-type", "Bucket Fill"), pika_context_get_opacity (PIKA_CONTEXT (options)), pika_context_get_paint_mode (PIKA_CONTEXT (options)), PIKA_LAYER_COLOR_SPACE_AUTO, PIKA_LAYER_COLOR_SPACE_AUTO, pika_layer_mode_get_paint_composite_mode (pika_context_get_paint_mode (PIKA_CONTEXT (options))), NULL, (gint) mask_x, mask_y); g_object_unref (buffer); pika_drawable_update (drawable, mask_x, mask_y, width, height); } pika_unset_busy (image->pika); } /** * pika_drawable_get_bucket_fill_buffer: * @drawable: the #PikaDrawable to edit. * @options: * @fill_transparent: * @fill_criterion: * @threshold: * @show_all: * @sample_merged: * @diagonal_neighbors: * @seed_x: X coordinate to start the fill. * @seed_y: Y coordinate to start the fill. * @mask_buffer: mask of the fill in-progress when in an interactive * filling process. Set to NULL if you need a one-time * fill. * @mask_x: returned x bound of @mask_buffer. * @mask_y: returned x bound of @mask_buffer. * @mask_width: returned width bound of @mask_buffer. * @mask_height: returned height bound of @mask_buffer. * * Creates the fill buffer for a bucket fill operation on @drawable, * without actually applying it (if you want to apply it directly as a * one-time operation, use pika_drawable_bucket_fill() instead). If * @mask_buffer is not NULL, the intermediate fill mask will also be * returned. This fill mask can later be reused in successive calls to * pika_drawable_get_bucket_fill_buffer() for interactive filling. * * Returns: a fill buffer which can be directly applied to @drawable, or * used in a drawable filter as preview. */ GeglBuffer * pika_drawable_get_bucket_fill_buffer (PikaDrawable *drawable, PikaFillOptions *options, gboolean fill_transparent, PikaSelectCriterion fill_criterion, gdouble threshold, gboolean show_all, gboolean sample_merged, gboolean diagonal_neighbors, gdouble seed_x, gdouble seed_y, GeglBuffer **mask_buffer, gdouble *mask_x, gdouble *mask_y, gint *mask_width, gint *mask_height) { PikaImage *image; PikaPickable *pickable; GeglBuffer *buffer; GeglBuffer *new_mask; gboolean antialias; gint x, y, width, height; gint mask_offset_x = 0; gint mask_offset_y = 0; gint sel_x, sel_y, sel_width, sel_height; g_return_val_if_fail (PIKA_IS_DRAWABLE (drawable), NULL); g_return_val_if_fail (pika_item_is_attached (PIKA_ITEM (drawable)), NULL); g_return_val_if_fail (PIKA_IS_FILL_OPTIONS (options), NULL); image = pika_item_get_image (PIKA_ITEM (drawable)); if (! pika_item_mask_intersect (PIKA_ITEM (drawable), &sel_x, &sel_y, &sel_width, &sel_height)) return NULL; if (mask_buffer && *mask_buffer && threshold == 0.0) { gfloat pixel; gegl_buffer_sample (*mask_buffer, seed_x, seed_y, NULL, &pixel, babl_format ("Y float"), GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE); if (pixel != 0.0) /* Already selected. This seed won't change the selection. */ return NULL; } pika_set_busy (image->pika); if (sample_merged) { if (! show_all) pickable = PIKA_PICKABLE (image); else pickable = PIKA_PICKABLE (pika_image_get_projection (image)); } else { pickable = PIKA_PICKABLE (drawable); } antialias = pika_fill_options_get_antialias (options); /* Do a seed bucket fill...To do this, calculate a new * contiguous region. */ new_mask = pika_pickable_contiguous_region_by_seed (pickable, antialias, threshold, fill_transparent, fill_criterion, diagonal_neighbors, (gint) seed_x, (gint) seed_y); if (mask_buffer && *mask_buffer) { pika_gegl_mask_combine_buffer (new_mask, *mask_buffer, PIKA_CHANNEL_OP_ADD, 0, 0); g_object_unref (*mask_buffer); } if (mask_buffer) *mask_buffer = new_mask; pika_gegl_mask_bounds (new_mask, &x, &y, &width, &height); width -= x; height -= y; /* If there is a selection, intersect the region bounds * with the selection bounds, to avoid processing areas * that are going to be masked out anyway. The actual * intersection of the fill region with the mask data * happens when combining the fill buffer, in * pika_drawable_apply_buffer(). */ if (! pika_channel_is_empty (pika_image_get_mask (image))) { gint off_x = 0; gint off_y = 0; if (sample_merged) pika_item_get_offset (PIKA_ITEM (drawable), &off_x, &off_y); if (! pika_rectangle_intersect (x, y, width, height, sel_x + off_x, sel_y + off_y, sel_width, sel_height, &x, &y, &width, &height)) { /* The fill region and the selection are disjoint; bail. */ if (! mask_buffer) g_object_unref (new_mask); pika_unset_busy (image->pika); return NULL; } } /* make sure we handle the mask correctly if it was sample-merged */ if (sample_merged) { PikaItem *item = PIKA_ITEM (drawable); gint off_x, off_y; /* Limit the channel bounds to the drawable's extents */ pika_item_get_offset (item, &off_x, &off_y); pika_rectangle_intersect (x, y, width, height, off_x, off_y, pika_item_get_width (item), pika_item_get_height (item), &x, &y, &width, &height); mask_offset_x = x; mask_offset_y = y; /* translate mask bounds to drawable coords */ x -= off_x; y -= off_y; } else { mask_offset_x = x; mask_offset_y = y; } buffer = pika_fill_options_create_buffer (options, drawable, GEGL_RECTANGLE (0, 0, width, height), -x, -y); pika_gegl_apply_opacity (buffer, NULL, NULL, buffer, new_mask, -mask_offset_x, -mask_offset_y, 1.0); if (mask_x) *mask_x = x; if (mask_y) *mask_y = y; if (mask_width) *mask_width = width; if (mask_height) *mask_height = height; if (! mask_buffer) g_object_unref (new_mask); pika_unset_busy (image->pika); return buffer; } /** * pika_drawable_get_line_art_fill_buffer: * @drawable: the #PikaDrawable to edit. * @line_art: the #PikaLineArt computed as fill source. * @options: the #PikaFillOptions. * @sample_merged: * @fill_color_as_line_art: do we add pixels in @drawable filled with * fill color to the line art? * @fill_color_threshold: threshold value to determine fill color. * @seed_x: X coordinate to start the fill. * @seed_y: Y coordinate to start the fill. * @mask_buffer: mask of the fill in-progress when in an interactive * filling process. Set to NULL if you need a one-time * fill. * @mask_x: returned x bound of @mask_buffer. * @mask_y: returned y bound of @mask_buffer. * @mask_width: returned width bound of @mask_buffer. * @mask_height: returned height bound of @mask_buffer. * * Creates the fill buffer for a bucket fill operation on @drawable * based on @line_art and @options, without actually applying it. * If @mask_buffer is not NULL, the intermediate fill mask will also be * returned. This fill mask can later be reused in successive calls to * pika_drawable_get_line_art_fill_buffer() for interactive filling. * * The @fill_color_as_line_art option is a special feature where we * consider pixels in @drawable already in the fill color as part of the * line art. This is a post-process, i.e. that this is not taken into * account while @line_art is computed, making this a fast addition * processing allowing to close some area manually. * * Returns: a fill buffer which can be directly applied to @drawable, or * used in a drawable filter as preview. */ GeglBuffer * pika_drawable_get_line_art_fill_buffer (PikaDrawable *drawable, PikaLineArt *line_art, PikaFillOptions *options, gboolean sample_merged, gboolean fill_color_as_line_art, gdouble fill_color_threshold, gboolean line_art_stroke, PikaStrokeOptions *stroke_options, gdouble seed_x, gdouble seed_y, GeglBuffer **mask_buffer, gdouble *mask_x, gdouble *mask_y, gint *mask_width, gint *mask_height) { PikaImage *image; GeglBuffer *buffer; GeglBuffer *new_mask; GeglBuffer *rendered_mask; GeglBuffer *fill_buffer = NULL; PikaRGB fill_color; gint fill_offset_x = 0; gint fill_offset_y = 0; gint x, y, width, height; gint mask_offset_x = 0; gint mask_offset_y = 0; gint sel_x, sel_y, sel_width, sel_height; gdouble feather_radius; g_return_val_if_fail (PIKA_IS_DRAWABLE (drawable), NULL); g_return_val_if_fail (pika_item_is_attached (PIKA_ITEM (drawable)), NULL); g_return_val_if_fail (PIKA_IS_FILL_OPTIONS (options), NULL); image = pika_item_get_image (PIKA_ITEM (drawable)); if (! pika_item_mask_intersect (PIKA_ITEM (drawable), &sel_x, &sel_y, &sel_width, &sel_height)) return NULL; if (mask_buffer && *mask_buffer) { gfloat pixel; gegl_buffer_sample (*mask_buffer, seed_x, seed_y, NULL, &pixel, babl_format ("Y float"), GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE); if (pixel != 0.0) /* Already selected. This seed won't change the selection. */ return NULL; } pika_set_busy (image->pika); /* Do a seed bucket fill...To do this, calculate a new * contiguous region. */ if (fill_color_as_line_art) { PikaPickable *pickable = pika_line_art_get_input (line_art); /* This cannot be a pattern fill. */ g_return_val_if_fail (pika_fill_options_get_style (options) != PIKA_FILL_STYLE_PATTERN, NULL); /* Meaningful only in above/below layer cases. */ g_return_val_if_fail (PIKA_IS_DRAWABLE (pickable), NULL); if (pika_fill_options_get_style (options) == PIKA_FILL_STYLE_FG_COLOR) pika_context_get_foreground (PIKA_CONTEXT (options), &fill_color); else if (pika_fill_options_get_style (options) == PIKA_FILL_STYLE_BG_COLOR) pika_context_get_background (PIKA_CONTEXT (options), &fill_color); fill_buffer = pika_drawable_get_buffer (drawable); fill_offset_x = pika_item_get_offset_x (PIKA_ITEM (drawable)) - pika_item_get_offset_x (PIKA_ITEM (pickable)); fill_offset_y = pika_item_get_offset_y (PIKA_ITEM (drawable)) - pika_item_get_offset_y (PIKA_ITEM (pickable)); } new_mask = pika_pickable_contiguous_region_by_line_art (NULL, line_art, fill_buffer, &fill_color, fill_color_threshold, fill_offset_x, fill_offset_y, (gint) seed_x, (gint) seed_y); if (mask_buffer && *mask_buffer) { pika_gegl_mask_combine_buffer (new_mask, *mask_buffer, PIKA_CHANNEL_OP_ADD, 0, 0); g_object_unref (*mask_buffer); } if (mask_buffer) *mask_buffer = new_mask; rendered_mask = pika_gegl_buffer_dup (new_mask); if (pika_fill_options_get_feather (options, &feather_radius)) { /* Feathering for the line art algorithm is not applied during * mask creation because we just want to apply it on the borders * of the mask at the end (since the mask can evolve, we don't * want to actually touch the mask, but only the intermediate * rendered results). */ pika_gegl_apply_feather (rendered_mask, NULL, NULL, rendered_mask, NULL, feather_radius, feather_radius, TRUE); } if (line_art_stroke) { /* Similarly to feathering, stroke only happens on the rendered * result, not on the returned mask. */ PikaChannel *channel; PikaChannel *stroked; GList *drawables; PikaContext *context = pika_get_user_context (image->pika); GError *error = NULL; const PikaRGB white = {1.0, 1.0, 1.0, 1.0}; context = pika_config_duplicate (PIKA_CONFIG (context)); /* As we are stroking a mask, we need to set color to white. */ pika_context_set_foreground (PIKA_CONTEXT (context), &white); channel = pika_channel_new_from_buffer (image, new_mask, NULL, NULL); stroked = pika_channel_new_from_buffer (image, rendered_mask, NULL, NULL); pika_image_add_hidden_item (image, PIKA_ITEM (channel)); pika_image_add_hidden_item (image, PIKA_ITEM (stroked)); drawables = g_list_prepend (NULL, stroked); if (! pika_item_stroke (PIKA_ITEM (channel), drawables, context, stroke_options, NULL, FALSE, NULL, &error)) { g_warning ("%s: stroking failed with: %s\n", G_STRFUNC, error ? error->message : "no error message"); g_clear_error (&error); } g_list_free (drawables); pika_pickable_flush (PIKA_PICKABLE (stroked)); g_object_unref (rendered_mask); rendered_mask = pika_drawable_get_buffer (PIKA_DRAWABLE (stroked)); g_object_ref (rendered_mask); pika_image_remove_hidden_item (image, PIKA_ITEM (channel)); g_object_unref (channel); pika_image_remove_hidden_item (image, PIKA_ITEM (stroked)); g_object_unref (stroked); g_object_unref (context); } pika_gegl_mask_bounds (rendered_mask, &x, &y, &width, &height); width -= x; height -= y; /* If there is a selection, intersect the region bounds * with the selection bounds, to avoid processing areas * that are going to be masked out anyway. The actual * intersection of the fill region with the mask data * happens when combining the fill buffer, in * pika_drawable_apply_buffer(). */ if (! pika_channel_is_empty (pika_image_get_mask (image))) { gint off_x = 0; gint off_y = 0; if (sample_merged) pika_item_get_offset (PIKA_ITEM (drawable), &off_x, &off_y); if (! pika_rectangle_intersect (x, y, width, height, sel_x + off_x, sel_y + off_y, sel_width, sel_height, &x, &y, &width, &height)) { if (! mask_buffer) g_object_unref (new_mask); /* The fill region and the selection are disjoint; bail. */ pika_unset_busy (image->pika); return NULL; } } /* make sure we handle the mask correctly if it was sample-merged */ if (sample_merged) { PikaItem *item = PIKA_ITEM (drawable); gint off_x, off_y; /* Limit the channel bounds to the drawable's extents */ pika_item_get_offset (item, &off_x, &off_y); pika_rectangle_intersect (x, y, width, height, off_x, off_y, pika_item_get_width (item), pika_item_get_height (item), &x, &y, &width, &height); mask_offset_x = x; mask_offset_y = y; /* translate mask bounds to drawable coords */ x -= off_x; y -= off_y; } else { mask_offset_x = x; mask_offset_y = y; } buffer = pika_fill_options_create_buffer (options, drawable, GEGL_RECTANGLE (0, 0, width, height), -x, -y); pika_gegl_apply_opacity (buffer, NULL, NULL, buffer, rendered_mask, -mask_offset_x, -mask_offset_y, 1.0); if (mask_x) *mask_x = x; if (mask_y) *mask_y = y; if (mask_width) *mask_width = width; if (mask_height) *mask_height = height; if (! mask_buffer) g_object_unref (new_mask); g_object_unref (rendered_mask); pika_unset_busy (image->pika); return buffer; }