/* 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
 *
 * Copyright (C) 2017 Sébastien Fourey & David Tchumperlé
 * Copyright (C) 2018 Jehan
 *
 * 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 "libpikabase/pikabase.h"
#include "libpikamath/pikamath.h"
#include "core-types.h"
#include "gegl/pika-gegl-loops.h"
#include "gegl/pika-gegl-utils.h"
#include "pika-parallel.h"
#include "pika-priorities.h"
#include "pika-utils.h" /* PIKA_TIMER */
#include "pikaasync.h"
#include "pikacancelable.h"
#include "pikadrawable.h"
#include "pikaimage.h"
#include "pikalineart.h"
#include "pikapickable.h"
#include "pikaprojection.h"
#include "pikaviewable.h"
#include "pikawaitable.h"
#include "pika-intl.h"
enum
{
  COMPUTING_START,
  COMPUTING_END,
  LAST_SIGNAL,
};
enum
{
  PROP_0,
  PROP_SELECT_TRANSPARENT,
  PROP_MAX_GROW,
  PROP_THRESHOLD,
  PROP_AUTOMATIC_CLOSURE,
  PROP_SPLINE_MAX_LEN,
  PROP_SEGMENT_MAX_LEN,
};
typedef struct _PikaLineArtPrivate PikaLineArtPrivate;
struct _PikaLineArtPrivate
{
  gboolean      frozen;
  gboolean      compute_after_thaw;
  PikaAsync    *async;
  gint          idle_id;
  PikaPickable *input;
  GeglBuffer   *closed;
  gfloat       *distmap;
  /* Used in the closing step. */
  gboolean      select_transparent;
  gdouble       threshold;
  gboolean      automatic_closure;
  gint          spline_max_len;
  gint          segment_max_len;
  gboolean      max_len_bound;
  /* Used in the grow step. */
  gint          max_grow;
};
typedef struct
{
  GeglBuffer  *buffer;
  gboolean     select_transparent;
  gdouble      threshold;
  gboolean     automatic_closure;
  gint         spline_max_len;
  gint         segment_max_len;
} LineArtData;
typedef struct
{
  GeglBuffer *closed;
  gfloat     *distmap;
} LineArtResult;
static int DeltaX[4] = {+1, -1, 0, 0};
static int DeltaY[4] = {0, 0, +1, -1};
static const PikaVector2 Direction2Normal[4] =
{
    {  1.0f,  0.0f },
    { -1.0f,  0.0f },
    {  0.0f,  1.0f },
    {  0.0f, -1.0f }
};
typedef enum _Direction
{
  XPlusDirection  = 0,
  XMinusDirection = 1,
  YPlusDirection  = 2,
  YMinusDirection = 3
} Direction;
typedef PikaVector2 Pixel;
typedef struct _SplineCandidate
{
  Pixel p1;
  Pixel p2;
  float quality;
} SplineCandidate;
typedef struct _Edgel
{
  gint      x, y;
  Direction direction;
  gfloat    x_normal;
  gfloat    y_normal;
  gfloat    curvature;
  guint     next, previous;
} Edgel;
static void            pika_line_art_finalize                  (GObject               *object);
static void            pika_line_art_set_property              (GObject                *object,
                                                                guint                   property_id,
                                                                const GValue           *value,
                                                                GParamSpec             *pspec);
static void            pika_line_art_get_property              (GObject                *object,
                                                                guint                   property_id,
                                                                GValue                 *value,
                                                                GParamSpec             *pspec);
/* Functions for asynchronous computation. */
static void            pika_line_art_compute                   (PikaLineArt            *line_art);
static void            pika_line_art_compute_cb                (PikaAsync              *async,
                                                                PikaLineArt            *line_art);
static PikaAsync     * pika_line_art_prepare_async             (PikaLineArt            *line_art,
                                                                gint                    priority);
static void            pika_line_art_prepare_async_func        (PikaAsync              *async,
                                                                LineArtData            *data);
static LineArtData   * line_art_data_new                       (GeglBuffer             *buffer,
                                                                PikaLineArt            *line_art);
static void            line_art_data_free                      (LineArtData            *data);
static LineArtResult * line_art_result_new                     (GeglBuffer             *line_art,
                                                                gfloat                 *distmap);
static void            line_art_result_free                    (LineArtResult          *result);
static gboolean        pika_line_art_idle                      (PikaLineArt            *line_art);
static void            pika_line_art_input_invalidate_preview  (PikaViewable           *viewable,
                                                                PikaLineArt            *line_art);
/* All actual computation functions. */
static GeglBuffer    * pika_line_art_close                     (GeglBuffer             *buffer,
                                                                gboolean                select_transparent,
                                                                gdouble                 stroke_threshold,
                                                                gboolean                automatic_closure,
                                                                gint                    spline_max_length,
                                                                gint                    segment_max_length,
                                                                gint                    minimal_lineart_area,
                                                                gint                    normal_estimate_mask_size,
                                                                gfloat                  end_point_rate,
                                                                gfloat                  spline_max_angle,
                                                                gint                    end_point_connectivity,
                                                                gfloat                  spline_roundness,
                                                                gboolean                allow_self_intersections,
                                                                gint                    created_regions_significant_area,
                                                                gint                    created_regions_minimum_area,
                                                                gboolean                small_segments_from_spline_sources,
                                                                gfloat                **lineart_distmap,
                                                                PikaAsync              *async);
static void            pika_lineart_denoise                    (GeglBuffer             *buffer,
                                                                int                     size,
                                                                PikaAsync              *async);
static void            pika_lineart_compute_normals_curvatures (GeglBuffer             *mask,
                                                                gfloat                 *normals,
                                                                gfloat                 *curvatures,
                                                                gfloat                 *smoothed_curvatures,
                                                                int                     normal_estimate_mask_size,
                                                                PikaAsync              *async);
static gfloat        * pika_lineart_get_smooth_curvatures      (GArray                 *edgelset,
                                                                PikaAsync              *async);
static GArray        * pika_lineart_curvature_extremums        (gfloat                 *curvatures,
                                                                gfloat                 *smoothed_curvatures,
                                                                gint                    curvatures_width,
                                                                gint                    curvatures_height,
                                                                PikaAsync              *async);
static gint            pika_spline_candidate_cmp               (const SplineCandidate  *a,
                                                                const SplineCandidate  *b,
                                                                gpointer                user_data);
static GList         * pika_lineart_find_spline_candidates     (GArray                 *max_positions,
                                                                gfloat                 *normals,
                                                                gint                    width,
                                                                gint                    distance_threshold,
                                                                gfloat                  max_angle_deg,
                                                                PikaAsync              *async);
static GArray        * pika_lineart_discrete_spline            (Pixel                   p0,
                                                                PikaVector2             n0,
                                                                Pixel                   p1,
                                                                PikaVector2             n1);
static gint            pika_number_of_transitions               (GArray                 *pixels,
                                                                 GeglBuffer             *buffer);
static gboolean        pika_line_art_allow_closure              (GeglBuffer             *mask,
                                                                 GArray                 *pixels,
                                                                 GList                 **fill_pixels,
                                                                 int                     significant_size,
                                                                 int                     minimum_size);
static GArray        * pika_lineart_line_segment_until_hit      (const GeglBuffer       *buffer,
                                                                 Pixel                   start,
                                                                 PikaVector2             direction,
                                                                 int                     size);
static gfloat        * pika_lineart_estimate_strokes_radii      (GeglBuffer             *mask,
                                                                 PikaAsync              *async);
static void            pika_line_art_simple_fill                (GeglBuffer             *buffer,
                                                                 gint                    x,
                                                                 gint                    y,
                                                                 gint                   *counter);
/* Some callback-type functions. */
static guint           visited_hash_fun                         (Pixel                  *key);
static gboolean        visited_equal_fun                        (Pixel                  *e1,
                                                                 Pixel                  *e2);
static inline gboolean border_in_direction                      (GeglBuffer             *mask,
                                                                 Pixel                   p,
                                                                 int                     direction);
static inline PikaVector2 pair2normal                           (Pixel                   p,
                                                                 gfloat                 *normals,
                                                                 gint                    width);
/* Edgel */
static Edgel    * pika_edgel_new                  (int                x,
                                                   int                y,
                                                   Direction          direction);
static void       pika_edgel_init                 (Edgel             *edgel);
static void       pika_edgel_clear                (Edgel            **edgel);
static int        pika_edgel_cmp                  (const Edgel       *e1,
                                                   const Edgel       *e2);
static guint      edgel2index_hash_fun            (Edgel              *key);
static gboolean   edgel2index_equal_fun           (Edgel              *e1,
                                                   Edgel              *e2);
static glong      pika_edgel_track_mark           (GeglBuffer         *mask,
                                                   Edgel               edgel,
                                                   long                size_limit);
static glong      pika_edgel_region_area          (const GeglBuffer   *mask,
                                                   Edgel               start_edgel);
/* Edgel set */
static GArray   * pika_edgelset_new               (GeglBuffer         *buffer,
                                                   PikaAsync          *async);
static void       pika_edgelset_add               (GArray             *set,
                                                   int                 x,
                                                   int                 y,
                                                   Direction           direction,
                                                   GHashTable         *edgel2index);
static void       pika_edgelset_init_normals      (GArray             *set);
static void       pika_edgelset_smooth_normals    (GArray             *set,
                                                   int                 mask_size,
                                                   PikaAsync          *async);
static void       pika_edgelset_compute_curvature (GArray             *set,
                                                   PikaAsync          *async);
static void       pika_edgelset_build_graph       (GArray            *set,
                                                   GeglBuffer        *buffer,
                                                   GHashTable        *edgel2index,
                                                   PikaAsync         *async);
static void       pika_edgelset_next8             (const GeglBuffer  *buffer,
                                                   Edgel             *it,
                                                   Edgel             *n);
G_DEFINE_TYPE_WITH_CODE (PikaLineArt, pika_line_art, PIKA_TYPE_OBJECT,
                         G_ADD_PRIVATE (PikaLineArt))
#define parent_class pika_line_art_parent_class
static guint pika_line_art_signals[LAST_SIGNAL] = { 0 };
static void
pika_line_art_class_init (PikaLineArtClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  pika_line_art_signals[COMPUTING_START] =
    g_signal_new ("computing-start",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_FIRST,
                  G_STRUCT_OFFSET (PikaLineArtClass, computing_start),
                  NULL, NULL, NULL,
                  G_TYPE_NONE, 0);
  pika_line_art_signals[COMPUTING_END] =
    g_signal_new ("computing-end",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_FIRST,
                  G_STRUCT_OFFSET (PikaLineArtClass, computing_end),
                  NULL, NULL, NULL,
                  G_TYPE_NONE, 0);
  object_class->finalize     = pika_line_art_finalize;
  object_class->set_property = pika_line_art_set_property;
  object_class->get_property = pika_line_art_get_property;
  g_object_class_install_property (object_class, PROP_SELECT_TRANSPARENT,
                                   g_param_spec_boolean ("select-transparent",
                                                         _("Select transparent pixels instead of gray ones"),
                                                         _("Select transparent pixels instead of gray ones"),
                                                         TRUE,
                                                         G_PARAM_CONSTRUCT | PIKA_PARAM_READWRITE));
  g_object_class_install_property (object_class, PROP_THRESHOLD,
                                   g_param_spec_double ("threshold",
                                                        _("Line art detection threshold"),
                                                        _("Threshold to detect contour (higher values will include more pixels)"),
                                                        0.0, 1.0, 0.92,
                                                        G_PARAM_CONSTRUCT | PIKA_PARAM_READWRITE));
  g_object_class_install_property (object_class, PROP_MAX_GROW,
                                   g_param_spec_int ("max-grow",
                                                     _("Maximum growing size"),
                                                     _("Maximum number of pixels grown under the line art"),
                                                     1, 100, 3,
                                                     G_PARAM_CONSTRUCT | PIKA_PARAM_READWRITE));
  g_object_class_install_property (object_class, PROP_AUTOMATIC_CLOSURE,
                                   g_param_spec_boolean ("automatic-closure",
                                                         _("Whether or not we should perform the closing step"),
                                                         _("Whether or not we should perform the closing step"),
                                                         TRUE,
                                                         G_PARAM_CONSTRUCT | PIKA_PARAM_READWRITE));
  g_object_class_install_property (object_class, PROP_SPLINE_MAX_LEN,
                                   g_param_spec_int ("spline-max-length",
                                                     _("Maximum curved closing length"),
                                                     _("Maximum curved length (in pixels) to close the line art"),
                                                     0, 1000, 100,
                                                     G_PARAM_CONSTRUCT | PIKA_PARAM_READWRITE));
  g_object_class_install_property (object_class, PROP_SEGMENT_MAX_LEN,
                                   g_param_spec_int ("segment-max-length",
                                                     _("Maximum straight closing length"),
                                                     _("Maximum straight length (in pixels) to close the line art"),
                                                     0, 1000, 100,
                                                     G_PARAM_CONSTRUCT | PIKA_PARAM_READWRITE));
}
static void
pika_line_art_init (PikaLineArt *line_art)
{
  line_art->priv = pika_line_art_get_instance_private (line_art);
}
static void
pika_line_art_finalize (GObject *object)
{
  PikaLineArt *line_art = PIKA_LINE_ART (object);
  line_art->priv->frozen = FALSE;
  pika_line_art_set_input (line_art, NULL);
  G_OBJECT_CLASS (parent_class)->finalize (object);
}
static void
pika_line_art_set_property (GObject      *object,
                            guint         property_id,
                            const GValue *value,
                            GParamSpec   *pspec)
{
  PikaLineArt *line_art = PIKA_LINE_ART (object);
  switch (property_id)
    {
    case PROP_SELECT_TRANSPARENT:
      if (line_art->priv->select_transparent != g_value_get_boolean (value))
        {
          line_art->priv->select_transparent = g_value_get_boolean (value);
          pika_line_art_compute (line_art);
        }
      break;
    case PROP_MAX_GROW:
      line_art->priv->max_grow = g_value_get_int (value);
      break;
    case PROP_THRESHOLD:
      if (line_art->priv->threshold != g_value_get_double (value))
        {
          line_art->priv->threshold = g_value_get_double (value);
          pika_line_art_compute (line_art);
        }
      break;
    case PROP_AUTOMATIC_CLOSURE:
      if (line_art->priv->automatic_closure != g_value_get_boolean (value))
        {
          line_art->priv->automatic_closure = g_value_get_boolean (value);
          pika_line_art_compute (line_art);
        }
      break;
    case PROP_SPLINE_MAX_LEN:
      if (line_art->priv->spline_max_len != g_value_get_int (value))
        {
          line_art->priv->spline_max_len = g_value_get_int (value);
          if (line_art->priv->max_len_bound)
            line_art->priv->segment_max_len = line_art->priv->spline_max_len;
          pika_line_art_compute (line_art);
        }
      break;
    case PROP_SEGMENT_MAX_LEN:
      if (line_art->priv->segment_max_len != g_value_get_int (value))
        {
          line_art->priv->segment_max_len = g_value_get_int (value);
          if (line_art->priv->max_len_bound)
            line_art->priv->spline_max_len = line_art->priv->segment_max_len;
          pika_line_art_compute (line_art);
        }
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }
}
static void
pika_line_art_get_property (GObject    *object,
                            guint       property_id,
                            GValue     *value,
                            GParamSpec *pspec)
{
  PikaLineArt *line_art = PIKA_LINE_ART (object);
  switch (property_id)
    {
    case PROP_SELECT_TRANSPARENT:
      g_value_set_boolean (value, line_art->priv->select_transparent);
      break;
    case PROP_MAX_GROW:
      g_value_set_int (value, line_art->priv->max_grow);
      break;
    case PROP_THRESHOLD:
      g_value_set_double (value, line_art->priv->threshold);
      break;
    case PROP_AUTOMATIC_CLOSURE:
      g_value_set_boolean (value, line_art->priv->automatic_closure);
      break;
    case PROP_SPLINE_MAX_LEN:
      g_value_set_int (value, line_art->priv->spline_max_len);
      break;
    case PROP_SEGMENT_MAX_LEN:
      g_value_set_int (value, line_art->priv->segment_max_len);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }
}
/* Public functions */
PikaLineArt *
pika_line_art_new (void)
{
  return g_object_new (PIKA_TYPE_LINE_ART,
                       NULL);
}
void
pika_line_art_bind_gap_length (PikaLineArt *line_art,
                               gboolean     bound)
{
  line_art->priv->max_len_bound = bound;
}
void
pika_line_art_set_input (PikaLineArt  *line_art,
                         PikaPickable *pickable)
{
  g_return_if_fail (pickable == NULL || PIKA_IS_VIEWABLE (pickable));
  if (pickable != line_art->priv->input)
    {
      if (line_art->priv->input)
        g_signal_handlers_disconnect_by_data (line_art->priv->input, line_art);
      g_set_object (&line_art->priv->input, pickable);
      pika_line_art_compute (line_art);
      if (pickable)
        {
          g_signal_connect (pickable, "invalidate-preview",
                            G_CALLBACK (pika_line_art_input_invalidate_preview),
                            line_art);
        }
    }
}
PikaPickable *
pika_line_art_get_input (PikaLineArt *line_art)
{
  return line_art->priv->input;
}
void
pika_line_art_freeze (PikaLineArt *line_art)
{
  g_return_if_fail (! line_art->priv->frozen);
  line_art->priv->frozen             = TRUE;
  line_art->priv->compute_after_thaw = FALSE;
}
void
pika_line_art_thaw (PikaLineArt *line_art)
{
  g_return_if_fail (line_art->priv->frozen);
  line_art->priv->frozen = FALSE;
  if (line_art->priv->compute_after_thaw)
    {
      pika_line_art_compute (line_art);
      line_art->priv->compute_after_thaw = FALSE;
    }
}
gboolean
pika_line_art_is_frozen (PikaLineArt *line_art)
{
  return line_art->priv->frozen;
}
GeglBuffer *
pika_line_art_get (PikaLineArt  *line_art,
                   gfloat      **distmap)
{
  g_return_val_if_fail (line_art->priv->input, NULL);
  if (line_art->priv->async)
    {
      pika_waitable_wait (PIKA_WAITABLE (line_art->priv->async));
    }
  else if (! line_art->priv->closed)
    {
      pika_line_art_compute (line_art);
      if (line_art->priv->async)
        pika_waitable_wait (PIKA_WAITABLE (line_art->priv->async));
    }
  g_return_val_if_fail (line_art->priv->closed, NULL);
  if (distmap)
    *distmap = line_art->priv->distmap;
  return line_art->priv->closed;
}
/* Functions for asynchronous computation. */
static void
pika_line_art_compute (PikaLineArt *line_art)
{
  if (line_art->priv->frozen)
    {
      line_art->priv->compute_after_thaw = TRUE;
      return;
    }
  if (line_art->priv->async)
    {
      /* we cancel the async, but don't wait for it to finish, since
       * it might take a while to respond.  instead pika_line_art_compute_cb()
       * bails if the async has been canceled, to avoid accessing the line art.
       */
      g_signal_emit (line_art, pika_line_art_signals[COMPUTING_END], 0);
      pika_cancelable_cancel (PIKA_CANCELABLE (line_art->priv->async));
      g_clear_object (&line_art->priv->async);
    }
  if (line_art->priv->idle_id)
    {
      g_source_remove (line_art->priv->idle_id);
      line_art->priv->idle_id = 0;
    }
  g_clear_object (&line_art->priv->closed);
  g_clear_pointer (&line_art->priv->distmap, g_free);
  if (line_art->priv->input)
    {
      /* pika_line_art_prepare_async() will flush the pickable, which
       * may trigger this signal handler, and will leak a line art (as
       * line_art->priv->async has not been set yet).
       */
      g_signal_handlers_block_by_func (
        line_art->priv->input,
        G_CALLBACK (pika_line_art_input_invalidate_preview),
        line_art);
      line_art->priv->async = pika_line_art_prepare_async (line_art, +1);
      g_signal_emit (line_art, pika_line_art_signals[COMPUTING_START], 0);
      g_signal_handlers_unblock_by_func (
        line_art->priv->input,
        G_CALLBACK (pika_line_art_input_invalidate_preview),
        line_art);
      pika_async_add_callback_for_object (line_art->priv->async,
                                          (PikaAsyncCallback) pika_line_art_compute_cb,
                                          line_art, line_art);
    }
}
static void
pika_line_art_compute_cb (PikaAsync   *async,
                          PikaLineArt *line_art)
{
  if (pika_async_is_canceled (async))
    return;
  if (pika_async_is_finished (async))
    {
      LineArtResult *result;
      result = pika_async_get_result (async);
      line_art->priv->closed  = g_object_ref (result->closed);
      line_art->priv->distmap = result->distmap;
      result->distmap  = NULL;
      g_signal_emit (line_art, pika_line_art_signals[COMPUTING_END], 0);
    }
  g_clear_object (&line_art->priv->async);
}
static PikaAsync *
pika_line_art_prepare_async (PikaLineArt *line_art,
                             gint         priority)
{
  GeglBuffer  *buffer;
  PikaAsync   *async;
  LineArtData *data;
  g_return_val_if_fail (PIKA_IS_PICKABLE (line_art->priv->input), NULL);
  pika_pickable_flush (line_art->priv->input);
  buffer = pika_gegl_buffer_dup (
    pika_pickable_get_buffer (line_art->priv->input));
  data  = line_art_data_new (buffer, line_art);
  g_object_unref (buffer);
  async = pika_parallel_run_async_full (
    priority,
    (PikaRunAsyncFunc) pika_line_art_prepare_async_func,
    data, (GDestroyNotify) line_art_data_free);
  return async;
}
static void
pika_line_art_prepare_async_func (PikaAsync   *async,
                                  LineArtData *data)
{
  GeglBuffer *buffer;
  GeglBuffer *closed  = NULL;
  gfloat     *distmap = NULL;
  gint        buffer_x;
  gint        buffer_y;
  gboolean    has_alpha;
  gboolean    select_transparent = FALSE;
  has_alpha = babl_format_has_alpha (gegl_buffer_get_format (data->buffer));
  if (has_alpha)
    {
      if (data->select_transparent)
        {
          /*  don't select transparent regions if there are no fully
           *  transparent pixels.
           */
          GeglBufferIterator *gi;
          gi = gegl_buffer_iterator_new (data->buffer, NULL, 0,
                                         babl_format ("A u8"),
                                         GEGL_ACCESS_READ, GEGL_ABYSS_NONE, 3);
          while (gegl_buffer_iterator_next (gi))
            {
              guint8 *p = (guint8*) gi->items[0].data;
              gint    k;
              if (pika_async_is_canceled (async))
                {
                  gegl_buffer_iterator_stop (gi);
                  pika_async_abort (async);
                  line_art_data_free (data);
                  return;
                }
              for (k = 0; k < gi->length; k++)
                {
                  if (! *p)
                    {
                      select_transparent = TRUE;
                      break;
                    }
                  p++;
                }
              if (select_transparent)
                break;
            }
          if (select_transparent)
            gegl_buffer_iterator_stop (gi);
        }
    }
  buffer   = data->buffer;
  buffer_x = gegl_buffer_get_x (data->buffer);
  buffer_y = gegl_buffer_get_y (data->buffer);
  if (buffer_x != 0 || buffer_y != 0)
    {
      buffer = g_object_new (GEGL_TYPE_BUFFER,
                             "source",  buffer,
                             "shift-x", buffer_x,
                             "shift-y", buffer_y,
                             NULL);
    }
  /* For smart selection, we generate a binarized image with close
   * regions, then run a composite selection with no threshold on
   * this intermediate buffer.
   */
  PIKA_TIMER_START();
  closed = pika_line_art_close (buffer,
                                select_transparent,
                                data->threshold,
                                data->automatic_closure,
                                data->spline_max_len,
                                data->segment_max_len,
                                /*minimal_lineart_area,*/
                                5,
                                /*normal_estimate_mask_size,*/
                                5,
                                /*end_point_rate,*/
                                0.85,
                                /*spline_max_angle,*/
                                90.0,
                                /*end_point_connectivity,*/
                                2,
                                /*spline_roundness,*/
                                1.0,
                                /*allow_self_intersections,*/
                                TRUE,
                                /*created_regions_significant_area,*/
                                4,
                                /*created_regions_minimum_area,*/
                                100,
                                /*small_segments_from_spline_sources,*/
                                TRUE,
                                &distmap,
                                async);
  PIKA_TIMER_END("close line-art");
  if (buffer != data->buffer)
    g_object_unref (buffer);
  if (! pika_async_is_stopped (async))
    {
      if (buffer_x != 0 || buffer_y != 0)
        {
          buffer = g_object_new (GEGL_TYPE_BUFFER,
                                 "source",  closed,
                                 "shift-x", -buffer_x,
                                 "shift-y", -buffer_y,
                                 NULL);
          g_object_unref (closed);
          closed = buffer;
        }
      pika_async_finish_full (async,
                              line_art_result_new (closed, distmap),
                              (GDestroyNotify) line_art_result_free);
    }
  line_art_data_free (data);
}
static LineArtData *
line_art_data_new (GeglBuffer  *buffer,
                   PikaLineArt *line_art)
{
  LineArtData *data = g_slice_new (LineArtData);
  data->buffer             = g_object_ref (buffer);
  data->select_transparent = line_art->priv->select_transparent;
  data->threshold          = line_art->priv->threshold;
  data->automatic_closure  = line_art->priv->automatic_closure;
  data->spline_max_len     = line_art->priv->spline_max_len;
  data->segment_max_len    = line_art->priv->segment_max_len;
  return data;
}
static void
line_art_data_free (LineArtData *data)
{
  g_object_unref (data->buffer);
  g_slice_free (LineArtData, data);
}
static LineArtResult *
line_art_result_new (GeglBuffer *closed,
                     gfloat     *distmap)
{
  LineArtResult *data;
  data = g_slice_new (LineArtResult);
  data->closed  = closed;
  data->distmap = distmap;
  return data;
}
static void
line_art_result_free (LineArtResult *data)
{
  g_object_unref (data->closed);
  g_clear_pointer (&data->distmap, g_free);
  g_slice_free (LineArtResult, data);
}
static gboolean
pika_line_art_idle (PikaLineArt *line_art)
{
  line_art->priv->idle_id = 0;
  pika_line_art_compute (line_art);
  return G_SOURCE_REMOVE;
}
static void
pika_line_art_input_invalidate_preview (PikaViewable *viewable,
                                        PikaLineArt  *line_art)
{
  if (! line_art->priv->idle_id)
    {
      line_art->priv->idle_id = g_idle_add_full (
        PIKA_PRIORITY_VIEWABLE_IDLE,
        (GSourceFunc) pika_line_art_idle,
        line_art, NULL);
    }
}
/* All actual computation functions. */
/**
 * pika_line_art_close:
 * @buffer: the input #GeglBuffer.
 * @select_transparent: whether we binarize the alpha channel or the
 *                      luminosity.
 * @stroke_threshold: [0-1] threshold value for detecting stroke pixels
 *                    (higher values will detect more stroke pixels).
 * @automatic_closure: whether the closing step should be performed or
 *                     not. @spline_max_length and @segment_max_len are
 *                     used only if @automatic_closure is %TRUE.
 * @spline_max_length: the maximum length for creating splines between
 *                     end points.
 * @segment_max_length: the maximum length for creating segments
 *                      between end points. Unlike splines, segments
 *                      are straight lines.
 * @minimal_lineart_area: the minimum size in number pixels for area to
 *                        be considered as line art.
 * @normal_estimate_mask_size:
 * @end_point_rate: threshold to estimate if a curvature is an end-point
 *                  in [0-1] range value.
 * @spline_max_angle: the maximum angle between end point normals for
 *                    creating splines between them.
 * @end_point_connectivity:
 * @spline_roundness:
 * @allow_self_intersections: whether to allow created splines and
 *                            segments to intersect.
 * @created_regions_significant_area:
 * @created_regions_minimum_area:
 * @small_segments_from_spline_sources:
 * @closed_distmap: a distance map of the closed line art pixels.
 * @async: the #PikaAsync associated with the computation
 *
 * Creates a binarized version of the strokes of @buffer, detected either
 * with luminosity (light means background) or alpha values depending on
 * @select_transparent. This binary version of the strokes will have closed
 * regions allowing adequate selection of "nearly closed regions".
 * This algorithm is meant for digital painting (and in particular on the
 * sketch-only step), and therefore will likely produce unexpected results on
 * other types of input.
 *
 * The algorithm is the first step from the research paper "A Fast and
 * Efficient Semi-guided Algorithm for Flat Coloring Line-arts", by Sébastian
 * Fourey, David Tschumperlé, David Revoy.
 * https://hal.archives-ouvertes.fr/hal-01891876
 *
 * Returns: a new #GeglBuffer of format "Y u8" representing the
 *          binarized @line_art. If @lineart_distmap is not %NULL, a
 *          newly allocated float buffer is returned, which can be used
 *          for overflowing created masks later.
 */
static GeglBuffer *
pika_line_art_close (GeglBuffer  *buffer,
                     gboolean     select_transparent,
                     gdouble      stroke_threshold,
                     gboolean     automatic_closure,
                     gint         spline_max_length,
                     gint         segment_max_length,
                     gint         minimal_lineart_area,
                     gint         normal_estimate_mask_size,
                     gfloat       end_point_rate,
                     gfloat       spline_max_angle,
                     gint         end_point_connectivity,
                     gfloat       spline_roundness,
                     gboolean     allow_self_intersections,
                     gint         created_regions_significant_area,
                     gint         created_regions_minimum_area,
                     gboolean     small_segments_from_spline_sources,
                     gfloat     **closed_distmap,
                     PikaAsync   *async)
{
  const Babl         *gray_format;
  GeglBufferIterator *gi;
  GeglBuffer         *closed  = NULL;
  GeglBuffer         *strokes = NULL;
  guchar              max_value = 0;
  gint                width  = gegl_buffer_get_width (buffer);
  gint                height = gegl_buffer_get_height (buffer);
  gint                i;
  if (select_transparent)
    /* Keep alpha channel as gray levels */
    gray_format = babl_format ("A u8");
  else
    /* Keep luminance */
    gray_format = babl_format ("Y' u8");
  /* Transform the line art from any format to gray. */
  strokes = gegl_buffer_new (gegl_buffer_get_extent (buffer),
                             gray_format);
  pika_gegl_buffer_copy (buffer, NULL, GEGL_ABYSS_NONE, strokes, NULL);
  gegl_buffer_set_format (strokes, babl_format ("Y' u8"));
  if (! select_transparent)
    {
      /* Compute the biggest value */
      gi = gegl_buffer_iterator_new (strokes, NULL, 0, NULL,
                                     GEGL_ACCESS_READ, GEGL_ABYSS_NONE, 1);
      while (gegl_buffer_iterator_next (gi))
        {
          guchar *data = (guchar*) gi->items[0].data;
          gint    k;
          if (pika_async_is_canceled (async))
            {
              gegl_buffer_iterator_stop (gi);
              pika_async_abort (async);
              goto end1;
            }
          for (k = 0; k < gi->length; k++)
            {
              if (*data > max_value)
                max_value = *data;
              data++;
            }
        }
    }
  /* Make the image binary: 1 is stroke, 0 background */
  gi = gegl_buffer_iterator_new (strokes, NULL, 0, NULL,
                                 GEGL_ACCESS_READWRITE, GEGL_ABYSS_NONE, 1);
  while (gegl_buffer_iterator_next (gi))
    {
      guchar *data = (guchar*) gi->items[0].data;
      gint    k;
      if (pika_async_is_canceled (async))
        {
          gegl_buffer_iterator_stop (gi);
          pika_async_abort (async);
          goto end1;
        }
      for (k = 0; k < gi->length; k++)
        {
          if (! select_transparent)
            /* Negate the value. */
            *data = max_value - *data;
          /* Apply a threshold. */
          if (*data > (guchar) (255.0f * (1.0f - stroke_threshold)))
            *data = 1;
          else
            *data = 0;
          data++;
        }
    }
  /* Denoise (remove small connected components) */
  pika_lineart_denoise (strokes, minimal_lineart_area, async);
  if (pika_async_is_stopped (async))
    goto end1;
  closed = g_object_ref (strokes);
  if (automatic_closure &&
      (spline_max_length > 0 || segment_max_length > 0))
    {
      GArray     *keypoints           = NULL;
      GHashTable *visited             = NULL;
      gfloat     *radii               = NULL;
      gfloat     *normals             = NULL;
      gfloat     *curvatures          = NULL;
      gfloat     *smoothed_curvatures = NULL;
      gfloat      threshold;
      gfloat      clamped_threshold;
      GList      *fill_pixels         = NULL;
      GList      *iter;
      normals             = g_new0 (gfloat, width * height * 2);
      curvatures          = g_new0 (gfloat, width * height);
      smoothed_curvatures = g_new0 (gfloat, width * height);
      /* Estimate normals & curvature */
      pika_lineart_compute_normals_curvatures (strokes, normals, curvatures,
                                               smoothed_curvatures,
                                               normal_estimate_mask_size,
                                               async);
      if (pika_async_is_stopped (async))
        goto end2;
      radii = pika_lineart_estimate_strokes_radii (strokes, async);
      if (pika_async_is_stopped (async))
        goto end2;
      threshold = 1.0f - end_point_rate;
      clamped_threshold = MAX (0.25f, threshold);
      for (i = 0; i < width; i++)
        {
          gint j;
          if (pika_async_is_canceled (async))
            {
              pika_async_abort (async);
              goto end2;
            }
          for (j = 0; j < height; j++)
            {
              if (smoothed_curvatures[i + j * width] >= (threshold / MAX (1.0f, radii[i + j * width])) ||
                  curvatures[i + j * width] >= clamped_threshold)
                curvatures[i + j * width] = 1.0;
              else
                curvatures[i + j * width] = 0.0;
            }
        }
      g_clear_pointer (&radii, g_free);
      keypoints = pika_lineart_curvature_extremums (curvatures, smoothed_curvatures,
                                                    width, height, async);
      if (pika_async_is_stopped (async))
        goto end2;
      visited = g_hash_table_new_full ((GHashFunc) visited_hash_fun,
                                       (GEqualFunc) visited_equal_fun,
                                       (GDestroyNotify) g_free, NULL);
      if (spline_max_length > 0)
        {
          GList           *candidates;
          SplineCandidate *candidate;
          candidates = pika_lineart_find_spline_candidates (keypoints, normals, width,
                                                            spline_max_length,
                                                            spline_max_angle,
                                                            async);
          if (pika_async_is_stopped (async))
            goto end3;
          g_object_unref (closed);
          closed = pika_gegl_buffer_dup (strokes);
          /* Draw splines */
          while (candidates)
            {
              Pixel    *p1;
              Pixel    *p2;
              gboolean  inserted = FALSE;
              if (pika_async_is_canceled (async))
                {
                  pika_async_abort (async);
                  goto end3;
                }
              p1 = g_new (Pixel, 1);
              p2 = g_new (Pixel, 1);
              candidate = (SplineCandidate *) candidates->data;
              p1->x = candidate->p1.x;
              p1->y = candidate->p1.y;
              p2->x = candidate->p2.x;
              p2->y = candidate->p2.y;
              g_free (candidate);
              candidates = g_list_delete_link (candidates, candidates);
              if ((! g_hash_table_contains (visited, p1) ||
                   GPOINTER_TO_INT (g_hash_table_lookup (visited, p1)) < end_point_connectivity) &&
                  (! g_hash_table_contains (visited, p2) ||
                   GPOINTER_TO_INT (g_hash_table_lookup (visited, p2)) < end_point_connectivity))
                {
                  GArray      *discrete_curve;
                  PikaVector2  vect1 = pair2normal (*p1, normals, width);
                  PikaVector2  vect2 = pair2normal (*p2, normals, width);
                  gfloat       distance = pika_vector2_length_val (pika_vector2_sub_val (*p1, *p2));
                  gint         transitions;
                  pika_vector2_mul (&vect1, distance);
                  pika_vector2_mul (&vect1, spline_roundness);
                  pika_vector2_mul (&vect2, distance);
                  pika_vector2_mul (&vect2, spline_roundness);
                  discrete_curve = pika_lineart_discrete_spline (*p1, vect1, *p2, vect2);
                  transitions = allow_self_intersections ?
                    pika_number_of_transitions (discrete_curve, strokes) :
                    pika_number_of_transitions (discrete_curve, closed);
                  if (transitions == 2 &&
                      pika_line_art_allow_closure (closed, discrete_curve,
                                                   &fill_pixels,
                                                   created_regions_significant_area,
                                                   created_regions_minimum_area))
                    {
                      for (i = 0; i < discrete_curve->len; i++)
                        {
                          Pixel p = g_array_index (discrete_curve, Pixel, i);
                          if (p.x >= 0 && p.x < gegl_buffer_get_width (closed) &&
                              p.y >= 0 && p.y < gegl_buffer_get_height (closed))
                            {
                              guchar val = 2;
                              gegl_buffer_set (closed, GEGL_RECTANGLE ((gint) p.x, (gint) p.y, 1, 1), 0,
                                               NULL, &val, GEGL_AUTO_ROWSTRIDE);
                            }
                        }
                      g_hash_table_replace (visited, p1,
                                            GINT_TO_POINTER (GPOINTER_TO_INT (g_hash_table_lookup (visited, p1)) + 1));
                      g_hash_table_replace (visited, p2,
                                            GINT_TO_POINTER (GPOINTER_TO_INT (g_hash_table_lookup (visited, p2)) + 1));
                      inserted = TRUE;
                    }
                  g_array_free (discrete_curve, TRUE);
                }
              if (! inserted)
                {
                  g_free (p1);
                  g_free (p2);
                }
            }
 end3:
          g_list_free_full (candidates, g_free);
          if (pika_async_is_stopped (async))
            goto end2;
        }
      g_clear_object (&strokes);
      /* Draw straight line segments */
      if (segment_max_length > 0)
        {
          Pixel *point;
          point = (Pixel *) keypoints->data;
          for (i = 0; i < keypoints->len; i++)
            {
              Pixel    *p;
              gboolean  inserted = FALSE;
              if (pika_async_is_canceled (async))
                {
                  pika_async_abort (async);
                  goto end2;
                }
              p  = g_new (Pixel, 1);
              *p = *point;
              if (! g_hash_table_contains (visited, p) ||
                  (small_segments_from_spline_sources &&
                   GPOINTER_TO_INT (g_hash_table_lookup (visited, p)) < end_point_connectivity))
                {
                  GArray *segment = pika_lineart_line_segment_until_hit (closed, *point,
                                                                         pair2normal (*point, normals, width),
                                                                         segment_max_length);
                  if (segment->len &&
                      pika_line_art_allow_closure (closed, segment, &fill_pixels,
                                                   created_regions_significant_area,
                                                   created_regions_minimum_area))
                    {
                      gint j;
                      for (j = 0; j < segment->len; j++)
                        {
                          Pixel  p2 = g_array_index (segment, Pixel, j);
                          guchar val = 2;
                          gegl_buffer_set (closed, GEGL_RECTANGLE ((gint) p2.x, (gint) p2.y, 1, 1), 0,
                                           NULL, &val, GEGL_AUTO_ROWSTRIDE);
                        }
                      g_hash_table_replace (visited, p,
                                            GINT_TO_POINTER (GPOINTER_TO_INT (g_hash_table_lookup (visited, p)) + 1));
                      inserted = TRUE;
                    }
                  g_array_free (segment, TRUE);
                }
              if (! inserted)
                g_free (p);
              point++;
            }
        }
      for (iter = fill_pixels; iter; iter = iter->next)
        {
          Pixel *p        = iter->data;
          gint   fill_max = created_regions_significant_area - 1;
          if (pika_async_is_canceled (async))
            {
              pika_async_abort (async);
              goto end2;
            }
          /* XXX A best approach would be to generalize
           * pika_drawable_bucket_fill() to work on any buffer (the code
           * is already mostly there) rather than reimplementing a naive
           * bucket fill.
           * This is mostly a quick'n dirty first implementation which I
           * will improve later.
           */
          pika_line_art_simple_fill (closed, (gint) p->x, (gint) p->y, &fill_max);
        }
 end2:
      g_list_free_full (fill_pixels, g_free);
      g_free (normals);
      g_free (curvatures);
      g_free (smoothed_curvatures);
      g_clear_pointer (&radii, g_free);
      if (keypoints)
        g_array_free (keypoints, TRUE);
      g_clear_pointer (&visited, g_hash_table_destroy);
      if (pika_async_is_stopped (async))
        goto end1;
    }
  else
    {
      g_clear_object (&strokes);
    }
  if (closed_distmap)
    {
      GeglNode   *graph;
      GeglNode   *input;
      GeglNode   *op;
      /* Flooding needs a distance map for closed line art. */
      *closed_distmap = g_new (gfloat, width * height);
      graph = gegl_node_new ();
      input = gegl_node_new_child (graph,
                                   "operation", "gegl:buffer-source",
                                   "buffer", closed,
                                   NULL);
      op  = gegl_node_new_child (graph,
                                 "operation", "gegl:distance-transform",
                                 "metric",    GEGL_DISTANCE_METRIC_EUCLIDEAN,
                                 "normalize", FALSE,
                                 NULL);
      gegl_node_link (input, op);
      gegl_node_blit (op, 1.0, gegl_buffer_get_extent (closed),
                      NULL, *closed_distmap,
                      GEGL_AUTO_ROWSTRIDE, GEGL_BLIT_DEFAULT);
      g_object_unref (graph);
    }
 end1:
  g_clear_object (&strokes);
  if (pika_async_is_stopped (async))
    g_clear_object (&closed);
  return closed;
}
static void
pika_lineart_denoise (GeglBuffer *buffer,
                      int         minimum_area,
                      PikaAsync  *async)
{
  /* Keep connected regions with significant area. */
  GArray   *region;
  GQueue   *q       = g_queue_new ();
  gint      width   = gegl_buffer_get_width (buffer);
  gint      height  = gegl_buffer_get_height (buffer);
  gboolean *visited = g_new0 (gboolean, width * height);
  gint      x, y;
  region = g_array_sized_new (TRUE, TRUE, sizeof (Pixel *), minimum_area);
  for (y = 0; y < height; ++y)
    for (x = 0; x < width; ++x)
      {
        guchar has_stroke;
        if (pika_async_is_canceled (async))
          {
            pika_async_abort (async);
            goto end;
          }
        gegl_buffer_sample (buffer, x, y, NULL, &has_stroke, NULL,
                            GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
        if (has_stroke && ! visited[x + y * width])
          {
            Pixel *p = g_new (Pixel, 1);
            gint   regionSize = 0;
            p->x = x;
            p->y = y;
            g_queue_push_tail (q, p);
            visited[x + y * width] = TRUE;
            while (! g_queue_is_empty (q))
              {
                Pixel *p;
                gint   p2x;
                gint   p2y;
                if (pika_async_is_canceled (async))
                  {
                    pika_async_abort (async);
                    goto end;
                  }
                p = (Pixel *) g_queue_pop_head (q);
                p2x = p->x + 1;
                p2y = p->y;
                if (p2x >= 0 && p2x < width && p2y >= 0 && p2y < height)
                  {
                    gegl_buffer_sample (buffer, p2x, p2y, NULL, &has_stroke, NULL,
                                        GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
                    if (has_stroke && ! visited[p2x + p2y * width])
                      {
                        Pixel *p2 = g_new (Pixel, 1);
                        p2->x = p2x;
                        p2->y = p2y;
                        g_queue_push_tail (q, p2);
                        visited[p2x +p2y * width] = TRUE;
                      }
                  }
                p2x = p->x - 1;
                p2y = p->y;
                if (p2x >= 0 && p2x < width && p2y >= 0 && p2y < height)
                  {
                    gegl_buffer_sample (buffer, p2x, p2y, NULL, &has_stroke, NULL,
                                        GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
                    if (has_stroke && ! visited[p2x + p2y * width])
                      {
                        Pixel *p2 = g_new (Pixel, 1);
                        p2->x = p2x;
                        p2->y = p2y;
                        g_queue_push_tail (q, p2);
                        visited[p2x + p2y * width] = TRUE;
                      }
                  }
                p2x = p->x;
                p2y = p->y - 1;
                if (p2x >= 0 && p2x < width && p2y >= 0 && p2y < height)
                  {
                    gegl_buffer_sample (buffer, p2x, p2y, NULL, &has_stroke, NULL,
                                        GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
                    if (has_stroke && ! visited[p2x + p2y * width])
                      {
                        Pixel *p2 = g_new (Pixel, 1);
                        p2->x = p2x;
                        p2->y = p2y;
                        g_queue_push_tail (q, p2);
                        visited[p2x + p2y * width] = TRUE;
                      }
                  }
                p2x = p->x;
                p2y = p->y + 1;
                if (p2x >= 0 && p2x < width && p2y >= 0 && p2y < height)
                  {
                    gegl_buffer_sample (buffer, p2x, p2y, NULL, &has_stroke, NULL,
                                        GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
                    if (has_stroke && ! visited[p2x + p2y * width])
                      {
                        Pixel *p2 = g_new (Pixel, 1);
                        p2->x = p2x;
                        p2->y = p2y;
                        g_queue_push_tail (q, p2);
                        visited[p2x + p2y * width] = TRUE;
                      }
                  }
                p2x = p->x + 1;
                p2y = p->y + 1;
                if (p2x >= 0 && p2x < width && p2y >= 0 && p2y < height)
                  {
                    gegl_buffer_sample (buffer, p2x, p2y, NULL, &has_stroke, NULL,
                                        GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
                    if (has_stroke && ! visited[p2x + p2y * width])
                      {
                        Pixel *p2 = g_new (Pixel, 1);
                        p2->x = p2x;
                        p2->y = p2y;
                        g_queue_push_tail (q, p2);
                        visited[p2x + p2y * width] = TRUE;
                      }
                  }
                p2x = p->x - 1;
                p2y = p->y - 1;
                if (p2x >= 0 && p2x < width && p2y >= 0 && p2y < height)
                  {
                    gegl_buffer_sample (buffer, p2x, p2y, NULL, &has_stroke, NULL,
                                        GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
                    if (has_stroke && ! visited[p2x + p2y * width])
                      {
                        Pixel *p2 = g_new (Pixel, 1);
                        p2->x = p2x;
                        p2->y = p2y;
                        g_queue_push_tail (q, p2);
                        visited[p2x + p2y * width] = TRUE;
                      }
                  }
                p2x = p->x - 1;
                p2y = p->y + 1;
                if (p2x >= 0 && p2x < width && p2y >= 0 && p2y < height)
                  {
                    gegl_buffer_sample (buffer, p2x, p2y, NULL, &has_stroke, NULL,
                                        GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
                    if (has_stroke && ! visited[p2x + p2y * width])
                      {
                        Pixel *p2 = g_new (Pixel, 1);
                        p2->x = p2x;
                        p2->y = p2y;
                        g_queue_push_tail (q, p2);
                        visited[p2x + p2y * width] = TRUE;
                      }
                  }
                p2x = p->x + 1;
                p2y = p->y - 1;
                if (p2x >= 0 && p2x < width && p2y >= 0 && p2y < height)
                  {
                    gegl_buffer_sample (buffer, p2x, p2y, NULL, &has_stroke, NULL,
                                        GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
                    if (has_stroke && ! visited[p2x + p2y * width])
                      {
                        Pixel *p2 = g_new (Pixel, 1);
                        p2->x = p2x;
                        p2->y = p2y;
                        g_queue_push_tail (q, p2);
                        visited[p2x + p2y * width] = TRUE;
                      }
                  }
                ++regionSize;
                if (regionSize < minimum_area)
                  g_array_append_val (region, *p);
                g_free (p);
              }
            if (regionSize < minimum_area)
              {
                Pixel *pixel = (Pixel *) region->data;
                gint   i = 0;
                for (; i < region->len; i++)
                  {
                    guchar val = 0;
                    gegl_buffer_set (buffer, GEGL_RECTANGLE (pixel->x, pixel->y, 1, 1), 0,
                                     NULL, &val, GEGL_AUTO_ROWSTRIDE);
                    pixel++;
                  }
              }
            g_array_remove_range (region, 0, region->len);
          }
      }
 end:
  g_array_free (region, TRUE);
  g_queue_free_full (q, g_free);
  g_free (visited);
}
static void
pika_lineart_compute_normals_curvatures (GeglBuffer *mask,
                                         gfloat     *normals,
                                         gfloat     *curvatures,
                                         gfloat     *smoothed_curvatures,
                                         int         normal_estimate_mask_size,
                                         PikaAsync  *async)
{
  gfloat  *edgels_curvatures  = NULL;
  gfloat  *smoothed_curvature;
  GArray  *es                 = NULL;
  Edgel  **e;
  gint     width              = gegl_buffer_get_width (mask);
  es = pika_edgelset_new (mask, async);
  if (pika_async_is_stopped (async))
    goto end;
  e = (Edgel **) es->data;
  pika_edgelset_smooth_normals (es, normal_estimate_mask_size, async);
  if (pika_async_is_stopped (async))
    goto end;
  pika_edgelset_compute_curvature (es, async);
  if (pika_async_is_stopped (async))
    goto end;
  while (*e)
    {
      const float curvature = ((*e)->curvature > 0.0f) ? (*e)->curvature : 0.0f;
      const float w         = MAX (1e-8f, curvature * curvature);
      if (pika_async_is_canceled (async))
        {
          pika_async_abort (async);
          goto end;
        }
      normals[((*e)->x + (*e)->y * width) * 2] += w * (*e)->x_normal;
      normals[((*e)->x + (*e)->y * width) * 2 + 1] += w * (*e)->y_normal;
      curvatures[(*e)->x + (*e)->y * width] = MAX (curvature,
                                                   curvatures[(*e)->x + (*e)->y * width]);
      e++;
    }
  for (int y = 0; y < gegl_buffer_get_height (mask); ++y)
    {
      if (pika_async_is_canceled (async))
        {
          pika_async_abort (async);
          goto end;
        }
      for (int x = 0; x < gegl_buffer_get_width (mask); ++x)
        {
          const float _angle = atan2f (normals[(x + y * width) * 2 + 1],
                                       normals[(x + y * width) * 2]);
          normals[(x + y * width) * 2] = cosf (_angle);
          normals[(x + y * width) * 2 + 1] = sinf (_angle);
        }
    }
  /* Smooth curvatures on edgels, then take maximum on each pixel. */
  edgels_curvatures = pika_lineart_get_smooth_curvatures (es, async);
  if (pika_async_is_stopped (async))
    goto end;
  smoothed_curvature = edgels_curvatures;
  e = (Edgel **) es->data;
  while (*e)
    {
      gfloat *pixel_curvature = &smoothed_curvatures[(*e)->x + (*e)->y * width];
      if (*pixel_curvature < *smoothed_curvature)
        *pixel_curvature = *smoothed_curvature;
      ++smoothed_curvature;
      e++;
    }
 end:
  g_free (edgels_curvatures);
  if (es)
    g_array_free (es, TRUE);
}
static gfloat *
pika_lineart_get_smooth_curvatures (GArray    *edgelset,
                                    PikaAsync *async)
{
  Edgel  **e;
  gfloat  *smoothed_curvatures = g_new0 (gfloat, edgelset->len);
  gfloat   weights[9];
  gfloat   smoothed_curvature;
  gfloat   weights_sum;
  gint     idx = 0;
  weights[0] = 1.0f;
  for (int i = 1; i <= 8; ++i)
    weights[i] = expf (-(i * i) / 30.0f);
  e = (Edgel **) edgelset->data;
  while (*e)
    {
      Edgel *edgel_before = g_array_index (edgelset, Edgel*, (*e)->previous);
      Edgel *edgel_after  = g_array_index (edgelset, Edgel*, (*e)->next);
      int    n = 5;
      int    i = 1;
      if (pika_async_is_canceled (async))
        {
          pika_async_abort (async);
          g_free (smoothed_curvatures);
          return NULL;
        }
      smoothed_curvature = (*e)->curvature;
      weights_sum = weights[0];
      while (n-- && (edgel_after != edgel_before))
        {
          smoothed_curvature += weights[i] * edgel_before->curvature;
          smoothed_curvature += weights[i] * edgel_after->curvature;
          edgel_before = g_array_index (edgelset, Edgel*, edgel_before->previous);
          edgel_after  = g_array_index (edgelset, Edgel*, edgel_after->next);
          weights_sum += 2 * weights[i];
          i++;
        }
      smoothed_curvature /= weights_sum;
      smoothed_curvatures[idx++] = smoothed_curvature;
      e++;
    }
  return smoothed_curvatures;
}
/**
 * Keep one pixel per connected component of curvature extremums.
 */
static GArray *
pika_lineart_curvature_extremums (gfloat    *curvatures,
                                  gfloat    *smoothed_curvatures,
                                  gint       width,
                                  gint       height,
                                  PikaAsync *async)
{
  gboolean *visited = g_new0 (gboolean, width * height);
  GQueue   *q       = g_queue_new ();
  GArray   *max_positions;
  max_positions = g_array_new (FALSE, TRUE, sizeof (Pixel));
  for (int y = 0; y < height; ++y)
    {
      if (pika_async_is_canceled (async))
        {
          pika_async_abort (async);
          goto end;
        }
      for (int x = 0; x < width; ++x)
        {
          if ((curvatures[x + y * width] > 0.0) && ! visited[x + y * width])
            {
              Pixel  *p = g_new (Pixel, 1);
              Pixel   max_smoothed_curvature_pixel;
              Pixel   max_raw_curvature_pixel;
              gfloat  max_smoothed_curvature;
              gfloat  max_raw_curvature;
              max_smoothed_curvature_pixel = pika_vector2_new (-1.0, -1.0);
              max_smoothed_curvature       = 0.0f;
              max_raw_curvature_pixel = pika_vector2_new (x, y);
              max_raw_curvature       = curvatures[x + y * width];
              p->x = x;
              p->y = y;
              g_queue_push_tail (q, p);
              visited[x + y * width] = TRUE;
              while (! g_queue_is_empty (q))
                {
                  gfloat sc;
                  gfloat c;
                  gint   p2x;
                  gint   p2y;
                  if (pika_async_is_canceled (async))
                    {
                      pika_async_abort (async);
                      goto end;
                    }
                  p  = (Pixel *) g_queue_pop_head (q);
                  sc = smoothed_curvatures[(gint) p->x + (gint) p->y * width];
                  c  = curvatures[(gint) p->x + (gint) p->y * width];
                  curvatures[(gint) p->x + (gint) p->y * width] = 0.0f;
                  p2x = (gint) p->x + 1;
                  p2y = (gint) p->y;
                  if (p2x >= 0 && p2x < width    &&
                      p2y >= 0 && p2y < height   &&
                      curvatures[p2x + p2y * width] > 0.0 &&
                      ! visited[p2x + p2y * width])
                    {
                      Pixel *p2 = g_new (Pixel, 1);
                      p2->x = p2x;
                      p2->y = p2y;
                      g_queue_push_tail (q, p2);
                      visited[p2x + p2y * width] = TRUE;
                    }
                  p2x = p->x - 1;
                  p2y = p->y;
                  if (p2x >= 0 && p2x < width    &&
                      p2y >= 0 && p2y < height   &&
                      curvatures[p2x + p2y * width] > 0.0 &&
                      ! visited[p2x + p2y * width])
                    {
                      Pixel *p2 = g_new (Pixel, 1);
                      p2->x = p2x;
                      p2->y = p2y;
                      g_queue_push_tail (q, p2);
                      visited[p2x + p2y * width] = TRUE;
                    }
                  p2x = p->x;
                  p2y = p->y - 1;
                  if (p2x >= 0 && p2x < width    &&
                      p2y >= 0 && p2y < height   &&
                      curvatures[p2x + p2y * width] > 0.0 &&
                      ! visited[p2x + p2y * width])
                    {
                      Pixel *p2 = g_new (Pixel, 1);
                      p2->x = p2x;
                      p2->y = p2y;
                      g_queue_push_tail (q, p2);
                      visited[p2x + p2y * width] = TRUE;
                    }
                  p2x = p->x;
                  p2y = p->y + 1;
                  if (p2x >= 0 && p2x < width    &&
                      p2y >= 0 && p2y < height   &&
                      curvatures[p2x + p2y * width] > 0.0 &&
                      ! visited[p2x + p2y * width])
                    {
                      Pixel *p2 = g_new (Pixel, 1);
                      p2->x = p2x;
                      p2->y = p2y;
                      g_queue_push_tail (q, p2);
                      visited[p2x + p2y * width] = TRUE;
                    }
                  p2x = p->x + 1;
                  p2y = p->y + 1;
                  if (p2x >= 0 && p2x < width    &&
                      p2y >= 0 && p2y < height   &&
                      curvatures[p2x + p2y * width] > 0.0 &&
                      ! visited[p2x + p2y * width])
                    {
                      Pixel *p2 = g_new (Pixel, 1);
                      p2->x = p2x;
                      p2->y = p2y;
                      g_queue_push_tail (q, p2);
                      visited[p2x + p2y * width] = TRUE;
                    }
                  p2x = p->x - 1;
                  p2y = p->y - 1;
                  if (p2x >= 0 && p2x < width    &&
                      p2y >= 0 && p2y < height   &&
                      curvatures[p2x + p2y * width] > 0.0 &&
                      ! visited[p2x + p2y * width])
                    {
                      Pixel *p2 = g_new (Pixel, 1);
                      p2->x = p2x;
                      p2->y = p2y;
                      g_queue_push_tail (q, p2);
                      visited[p2x + p2y * width] = TRUE;
                    }
                  p2x = p->x - 1;
                  p2y = p->y + 1;
                  if (p2x >= 0 && p2x < width    &&
                      p2y >= 0 && p2y < height   &&
                      curvatures[p2x + p2y * width] > 0.0 &&
                      ! visited[p2x + p2y * width])
                    {
                      Pixel *p2 = g_new (Pixel, 1);
                      p2->x = p2x;
                      p2->y = p2y;
                      g_queue_push_tail (q, p2);
                      visited[p2x + p2y * width] = TRUE;
                    }
                  p2x = p->x + 1;
                  p2y = p->y - 1;
                  if (p2x >= 0 && p2x < width    &&
                      p2y >= 0 && p2y < height   &&
                      curvatures[p2x + p2y * width] > 0.0 &&
                      ! visited[p2x + p2y * width])
                    {
                      Pixel *p2 = g_new (Pixel, 1);
                      p2->x = p2x;
                      p2->y = p2y;
                      g_queue_push_tail (q, p2);
                      visited[p2x + p2y * width] = TRUE;
                    }
                  if (sc > max_smoothed_curvature)
                    {
                      max_smoothed_curvature_pixel = *p;
                      max_smoothed_curvature = sc;
                    }
                  if (c > max_raw_curvature)
                    {
                      max_raw_curvature_pixel = *p;
                      max_raw_curvature = c;
                    }
                  g_free (p);
                }
              if (max_smoothed_curvature > 0.0f)
                {
                  curvatures[(gint) max_smoothed_curvature_pixel.x + (gint) max_smoothed_curvature_pixel.y * width] = max_smoothed_curvature;
                  g_array_append_val (max_positions, max_smoothed_curvature_pixel);
                }
              else
                {
                  curvatures[(gint) max_raw_curvature_pixel.x + (gint) max_raw_curvature_pixel.y * width] = max_raw_curvature;
                  g_array_append_val (max_positions, max_raw_curvature_pixel);
                }
            }
        }
    }
 end:
  g_queue_free_full (q, g_free);
  g_free (visited);
  if (pika_async_is_stopped (async))
    {
      g_array_free (max_positions, TRUE);
      max_positions = NULL;
    }
  return max_positions;
}
static gint
pika_spline_candidate_cmp (const SplineCandidate *a,
                           const SplineCandidate *b,
                           gpointer               user_data)
{
  /* This comparison actually returns the opposite of common comparison
   * functions on purpose, as we want the first element on the list to
   * be the "bigger".
   */
  if (a->quality < b->quality)
    return 1;
  else if (a->quality > b->quality)
    return -1;
  else
    return 0;
}
static GList *
pika_lineart_find_spline_candidates (GArray    *max_positions,
                                     gfloat    *normals,
                                     gint       width,
                                     gint       distance_threshold,
                                     gfloat     max_angle_deg,
                                     PikaAsync *async)
{
  GList       *candidates = NULL;
  const float  CosMin     = cosf (M_PI * (max_angle_deg / 180.0));
  gint         i;
  for (i = 0; i < max_positions->len; i++)
    {
      Pixel p1 = g_array_index (max_positions, Pixel, i);
      gint  j;
      if (pika_async_is_canceled (async))
        {
          pika_async_abort (async);
          g_list_free_full (candidates, g_free);
          return NULL;
        }
      for (j = i + 1; j < max_positions->len; j++)
        {
          Pixel       p2 = g_array_index (max_positions, Pixel, j);
          const float distance = pika_vector2_length_val (pika_vector2_sub_val (p1, p2));
          if (distance <= distance_threshold)
            {
              PikaVector2 normalP1;
              PikaVector2 normalP2;
              PikaVector2 p1f;
              PikaVector2 p2f;
              PikaVector2 p1p2;
              float       cosN;
              float       qualityA;
              float       qualityB;
              float       qualityC;
              float       quality;
              normalP1 = pika_vector2_new (normals[((gint) p1.x + (gint) p1.y * width) * 2],
                                           normals[((gint) p1.x + (gint) p1.y * width) * 2 + 1]);
              normalP2 = pika_vector2_new (normals[((gint) p2.x + (gint) p2.y * width) * 2],
                                           normals[((gint) p2.x + (gint) p2.y * width) * 2 + 1]);
              p1f = pika_vector2_new (p1.x, p1.y);
              p2f = pika_vector2_new (p2.x, p2.y);
              p1p2 = pika_vector2_sub_val (p2f, p1f);
              cosN = pika_vector2_inner_product_val (normalP1, (pika_vector2_neg_val (normalP2)));
              qualityA = MAX (0.0f, 1 - distance / distance_threshold);
              qualityB = MAX (0.0f,
                              (float) (pika_vector2_inner_product_val (normalP1, p1p2) - pika_vector2_inner_product_val (normalP2, p1p2)) /
                              distance);
              qualityC = MAX (0.0f, cosN - CosMin);
              quality = qualityA * qualityB * qualityC;
              if (quality > 0)
                {
                  SplineCandidate *candidate = g_new (SplineCandidate, 1);
                  candidate->p1      = p1;
                  candidate->p2      = p2;
                  candidate->quality = quality;
                  candidates = g_list_insert_sorted_with_data (candidates, candidate,
                                                               (GCompareDataFunc) pika_spline_candidate_cmp,
                                                               NULL);
                }
            }
        }
    }
  return candidates;
}
static GArray *
pika_lineart_discrete_spline (Pixel       p0,
                              PikaVector2 n0,
                              Pixel       p1,
                              PikaVector2 n1)
{
  GArray       *points = g_array_new (FALSE, TRUE, sizeof (Pixel));
  const double  a0 = 2 * p0.x - 2 * p1.x + n0.x - n1.x;
  const double  b0 = -3 * p0.x + 3 * p1.x - 2 * n0.x + n1.x;
  const double  c0 = n0.x;
  const double  d0 = p0.x;
  const double  a1 = 2 * p0.y - 2 * p1.y + n0.y - n1.y;
  const double  b1 = -3 * p0.y + 3 * p1.y - 2 * n0.y + n1.y;
  const double  c1 = n0.y;
  const double  d1 = p0.y;
  double        t = 0.0;
  const double  dtMin = 1.0 / MAX (fabs (p0.x - p1.x), fabs (p0.y - p1.y));
  Pixel         point = pika_vector2_new ((gint) round (d0), (gint) round (d1));
  g_array_append_val (points, point);
  while (t <= 1.0)
    {
      const double t2 = t * t;
      const double t3 = t * t2;
      double       dx;
      double       dy;
      Pixel        p = pika_vector2_new ((gint) round (a0 * t3 + b0 * t2 + c0 * t + d0),
                                         (gint) round (a1 * t3 + b1 * t2 + c1 * t + d1));
      /* create pika_vector2_neq () ? */
      if (g_array_index (points, Pixel, points->len - 1).x != p.x ||
          g_array_index (points, Pixel, points->len - 1).y != p.y)
        {
          g_array_append_val (points, p);
        }
      dx = fabs (3 * a0 * t * t + 2 * b0 * t + c0) + 1e-8;
      dy = fabs (3 * a1 * t * t + 2 * b1 * t + c1) + 1e-8;
      t += MIN (dtMin, 0.75 / MAX (dx, dy));
    }
  if (g_array_index (points, Pixel, points->len - 1).x != p1.x ||
      g_array_index (points, Pixel, points->len - 1).y != p1.y)
    {
      g_array_append_val (points, p1);
    }
  return points;
}
static gint
pika_number_of_transitions (GArray     *pixels,
                            GeglBuffer *buffer)
{
  int result = 0;
  if (pixels->len > 0)
    {
      Pixel    it = g_array_index (pixels, Pixel, 0);
      guchar   value;
      gboolean previous;
      gint     i;
      gegl_buffer_sample (buffer, (gint) it.x, (gint) it.y, NULL, &value, NULL,
                          GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
      previous = (gboolean) value;
      /* Starts at the second element. */
      for (i = 1; i < pixels->len; i++)
        {
          it = g_array_index (pixels, Pixel, i);
          gegl_buffer_sample (buffer, (gint) it.x, (gint) it.y, NULL, &value, NULL,
                              GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
          result += ((gboolean) value != previous);
          previous = (gboolean) value;
        }
    }
  return result;
}
/**
 * pika_line_art_allow_closure:
 * @mask: the current state of line art closure.
 * @pixels: the pixels of a candidate closure (spline or segment).
 * @fill_pixels: #GList of insignificant pixels to bucket fill.
 * @significant_size: number of pixels for area to be considered
 *                    "significant".
 * @minimum_size: number of pixels for area to be allowed.
 *
 * Checks whether adding the set of points @pixels to @mask will create
 * 4-connected background regions whose size (i.e. number of pixels)
 * will be below @minimum_size. If it creates such small areas, the
 * function will refuse this candidate spline/segment, with the
 * exception of very small areas under @significant_size. These
 * micro-area are considered "insignificant" and accepted (because they
 * can be created in some conditions, for instance when created curves
 * cross or start from a same endpoint), and one pixel for each
 * micro-area will be added to @fill_pixels to be later filled along
 * with the candidate pixels.
 *
 * Returns: %TRUE if @pixels should be added to @mask, %FALSE otherwise.
 */
static gboolean
pika_line_art_allow_closure (GeglBuffer *mask,
                             GArray     *pixels,
                             GList     **fill_pixels,
                             int         significant_size,
                             int         minimum_size)
{
  /* A theorem from the paper is that a zone with more than
   * `2 * (@minimum_size - 1)` edgels (border pixels) will have more
   * than @minimum_size pixels.
   * Since we are following the edges of the area, we can therefore stop
   * earlier if we reach this number of edgels.
   */
  const glong max_edgel_count = 2 * minimum_size;
  Pixel      *p = (Pixel*) pixels->data;
  GList      *fp = NULL;
  gint        i;
  /* Mark pixels */
  for (i = 0; i < pixels->len; i++)
    {
      if (p->x >= 0 && p->x < gegl_buffer_get_width (mask) &&
          p->y >= 0 && p->y < gegl_buffer_get_height (mask))
        {
          guchar val;
          gegl_buffer_sample (mask, (gint) p->x, (gint) p->y, NULL, &val,
                              NULL, GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
          val = val ? 3 : 2;
          gegl_buffer_set (mask, GEGL_RECTANGLE ((gint) p->x, (gint) p->y, 1, 1), 0,
                           NULL, &val, GEGL_AUTO_ROWSTRIDE);
        }
      p++;
    }
  for (i = 0; i < pixels->len; i++)
    {
      Pixel p = g_array_index (pixels, Pixel, i);
      for (int direction = 0; direction < 4; ++direction)
        {
          if (p.x >= 0 && p.x < gegl_buffer_get_width (mask) &&
              p.y >= 0 && p.y < gegl_buffer_get_height (mask) &&
              border_in_direction (mask, p, direction))
            {
              Edgel  e;
              guchar val;
              glong  count;
              glong  area;
              gegl_buffer_sample (mask, (gint) p.x, (gint) p.y, NULL, &val,
                                  NULL, GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
              if ((gboolean) (val & (4 << direction)))
                continue;
              pika_edgel_init (&e);
              e.x = p.x;
              e.y = p.y;
              e.direction = direction;
              count = pika_edgel_track_mark (mask, e, max_edgel_count);
              if ((count != -1) && (count <= max_edgel_count))
                {
                  area = pika_edgel_region_area (mask, e);
                  if (area >= significant_size && area < minimum_size)
                    {
                      gint j;
                      /* Remove marks */
                      for (j = 0; j < pixels->len; j++)
                        {
                          Pixel p2 = g_array_index (pixels, Pixel, j);
                          if (p2.x >= 0 && p2.x < gegl_buffer_get_width (mask) &&
                              p2.y >= 0 && p2.y < gegl_buffer_get_height (mask))
                            {
                              guchar val;
                              gegl_buffer_sample (mask, (gint) p2.x, (gint) p2.y, NULL, &val,
                                                  NULL, GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
                              val &= 1;
                              gegl_buffer_set (mask, GEGL_RECTANGLE ((gint) p2.x, (gint) p2.y, 1, 1), 0,
                                               NULL, &val, GEGL_AUTO_ROWSTRIDE);
                            }
                        }
                      g_list_free_full (fp, g_free);
                      return FALSE;
                    }
                  else if (area > 0 && area < significant_size)
                    {
                      Pixel *np = g_new (Pixel, 1);
                      np->x = direction == XPlusDirection ? p.x + 1 : (direction == XMinusDirection ? p.x - 1 : p.x);
                      np->y = direction == YPlusDirection ? p.y + 1 : (direction == YMinusDirection ? p.y - 1 : p.y);
                      if (np->x >= 0 && np->x < gegl_buffer_get_width (mask) &&
                          np->y >= 0 && np->y < gegl_buffer_get_height (mask))
                        fp = g_list_prepend (fp, np);
                      else
                        g_free (np);
                    }
                }
            }
        }
    }
  *fill_pixels = g_list_concat (*fill_pixels, fp);
  /* Remove marks */
  for (i = 0; i < pixels->len; i++)
    {
      Pixel p = g_array_index (pixels, Pixel, i);
      if (p.x >= 0 && p.x < gegl_buffer_get_width (mask) &&
          p.y >= 0 && p.y < gegl_buffer_get_height (mask))
        {
          guchar val;
          gegl_buffer_sample (mask, (gint) p.x, (gint) p.y, NULL, &val,
                              NULL, GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
          val &= 1;
          gegl_buffer_set (mask, GEGL_RECTANGLE ((gint) p.x, (gint) p.y, 1, 1), 0,
                           NULL, &val, GEGL_AUTO_ROWSTRIDE);
        }
    }
  return TRUE;
}
static GArray *
pika_lineart_line_segment_until_hit (const GeglBuffer *mask,
                                     Pixel             start,
                                     PikaVector2       direction,
                                     int               size)
{
  GeglBuffer  *buffer = (GeglBuffer *) mask;
  gboolean     out = FALSE;
  GArray      *points = g_array_new (FALSE, TRUE, sizeof (Pixel));
  int          tmax;
  PikaVector2  p0 = pika_vector2_new (start.x, start.y);
  pika_vector2_mul (&direction, (gdouble) size);
  direction.x = round (direction.x);
  direction.y = round (direction.y);
  tmax = MAX (abs ((int) direction.x), abs ((int) direction.y));
  for (int t = 0; t <= tmax; ++t)
    {
      PikaVector2 v = pika_vector2_add_val (p0, pika_vector2_mul_val (direction, (float)t / tmax));
      Pixel       p;
      p.x = (gint) round (v.x);
      p.y = (gint) round (v.y);
      if (p.x >= 0 && p.x < gegl_buffer_get_width (buffer) &&
          p.y >= 0 && p.y < gegl_buffer_get_height (buffer))
        {
          guchar val;
          gegl_buffer_sample (buffer, p.x, p.y, NULL, &val,
                              NULL, GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
          if (out && val)
            {
              return points;
            }
          out = ! val;
        }
      else if (out)
        {
          return points;
        }
      else
        {
          g_array_free (points, TRUE);
          return g_array_new (FALSE, TRUE, sizeof (Pixel));
        }
      g_array_append_val (points, p);
    }
  g_array_free (points, TRUE);
  return g_array_new (FALSE, TRUE, sizeof (Pixel));
}
static gfloat *
pika_lineart_estimate_strokes_radii (GeglBuffer *mask,
                                     PikaAsync  *async)
{
  GeglBufferIterator *gi;
  gfloat             *dist;
  gfloat             *thickness;
  GeglNode           *graph;
  GeglNode           *input;
  GeglNode           *op;
  gint                width  = gegl_buffer_get_width (mask);
  gint                height = gegl_buffer_get_height (mask);
  /* Compute a distance map for the line art. */
  dist = g_new (gfloat, width * height);
  graph = gegl_node_new ();
  input = gegl_node_new_child (graph,
                               "operation", "gegl:buffer-source",
                               "buffer", mask,
                               NULL);
  op  = gegl_node_new_child (graph,
                             "operation", "gegl:distance-transform",
                             "metric",    GEGL_DISTANCE_METRIC_EUCLIDEAN,
                             "normalize", FALSE,
                             NULL);
  gegl_node_link (input, op);
  gegl_node_blit (op, 1.0, gegl_buffer_get_extent (mask),
                  NULL, dist, GEGL_AUTO_ROWSTRIDE, GEGL_BLIT_DEFAULT);
  g_object_unref (graph);
  thickness = g_new0 (gfloat, width * height);
  gi = gegl_buffer_iterator_new (mask, NULL, 0, NULL,
                                 GEGL_ACCESS_READ, GEGL_ABYSS_NONE, 1);
  while (gegl_buffer_iterator_next (gi))
    {
      guint8 *m = (guint8*) gi->items[0].data;
      gint    startx = gi->items[0].roi.x;
      gint    starty = gi->items[0].roi.y;
      gint    endy   = starty + gi->items[0].roi.height;
      gint    endx   = startx + gi->items[0].roi.width;
      gint    x;
      gint    y;
      if (pika_async_is_canceled (async))
        {
          gegl_buffer_iterator_stop (gi);
          pika_async_abort (async);
          goto end;
        }
      for (y = starty; y < endy; y++)
        for (x = startx; x < endx; x++)
          {
            if (*m && dist[x + y * width] == 1.0)
              {
                gint     dx = x;
                gint     dy = y;
                gfloat   d  = 1.0;
                gfloat   nd;
                gboolean neighbour_thicker = TRUE;
                while (neighbour_thicker)
                  {
                    gint px = dx - 1;
                    gint py = dy - 1;
                    gint nx = dx + 1;
                    gint ny = dy + 1;
                    neighbour_thicker = FALSE;
                    if (px >= 0)
                      {
                        if ((nd = dist[px + dy * width]) > d)
                          {
                            d = nd;
                            dx = px;
                            neighbour_thicker = TRUE;
                            continue;
                          }
                        if (py >= 0 && (nd = dist[px + py * width]) > d)
                          {
                            d = nd;
                            dx = px;
                            dy = py;
                            neighbour_thicker = TRUE;
                            continue;
                          }
                        if (ny < height && (nd = dist[px + ny * width]) > d)
                          {
                            d = nd;
                            dx = px;
                            dy = ny;
                            neighbour_thicker = TRUE;
                            continue;
                          }
                      }
                    if (nx < width)
                      {
                        if ((nd = dist[nx + dy * width]) > d)
                          {
                            d = nd;
                            dx = nx;
                            neighbour_thicker = TRUE;
                            continue;
                          }
                        if (py >= 0 && (nd = dist[nx + py * width]) > d)
                          {
                            d = nd;
                            dx = nx;
                            dy = py;
                            neighbour_thicker = TRUE;
                            continue;
                          }
                        if (ny < height && (nd = dist[nx + ny * width]) > d)
                          {
                            d = nd;
                            dx = nx;
                            dy = ny;
                            neighbour_thicker = TRUE;
                            continue;
                          }
                      }
                    if (py > 0 && (nd = dist[dx + py * width]) > d)
                      {
                        d = nd;
                        dy = py;
                        neighbour_thicker = TRUE;
                        continue;
                      }
                    if (ny < height && (nd = dist[dx + ny * width]) > d)
                      {
                        d = nd;
                        dy = ny;
                        neighbour_thicker = TRUE;
                        continue;
                      }
                  }
                thickness[(gint) x + (gint) y * width] = d;
              }
            m++;
          }
    }
 end:
  g_free (dist);
  if (pika_async_is_stopped (async))
    g_clear_pointer (&thickness, g_free);
  return thickness;
}
static void
pika_line_art_simple_fill (GeglBuffer *buffer,
                           gint        x,
                           gint        y,
                           gint       *counter)
{
  guchar val;
  if (x < 0 || x >= gegl_buffer_get_width (buffer)  ||
      y < 0 || y >= gegl_buffer_get_height (buffer) ||
      *counter <= 0)
    return;
  gegl_buffer_sample (buffer, x, y, NULL, &val,
                      NULL, GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
  if (! val)
    {
      val = 1;
      gegl_buffer_set (buffer, GEGL_RECTANGLE (x, y, 1, 1), 0,
                       NULL, &val, GEGL_AUTO_ROWSTRIDE);
      (*counter)--;
      pika_line_art_simple_fill (buffer, x + 1, y, counter);
      pika_line_art_simple_fill (buffer, x - 1, y, counter);
      pika_line_art_simple_fill (buffer, x, y + 1, counter);
      pika_line_art_simple_fill (buffer, x, y - 1, counter);
    }
}
static guint
visited_hash_fun (Pixel *key)
{
  /* Cantor pairing function. */
  return (key->x + key->y) * (key->x + key->y + 1) / 2 + key->y;
}
static gboolean
visited_equal_fun (Pixel *e1,
                   Pixel *e2)
{
  return (e1->x == e2->x && e1->y == e2->y);
}
static inline gboolean
border_in_direction (GeglBuffer *mask,
                     Pixel       p,
                     int         direction)
{
  gint px = (gint) p.x + DeltaX[direction];
  gint py = (gint) p.y + DeltaY[direction];
  if (px >= 0 && px < gegl_buffer_get_width (mask) &&
      py >= 0 && py < gegl_buffer_get_height (mask))
    {
      guchar val;
      gegl_buffer_sample (mask, px, py, NULL, &val,
                          NULL, GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
      return ! ((gboolean) val);
    }
  return TRUE;
}
static inline PikaVector2
pair2normal (Pixel   p,
             gfloat *normals,
             gint    width)
{
  return pika_vector2_new (normals[((gint) p.x + (gint) p.y * width) * 2],
                           normals[((gint) p.x + (gint) p.y * width) * 2 + 1]);
}
/* Edgel functions */
static Edgel *
pika_edgel_new (int       x,
                int       y,
                Direction direction)
{
  Edgel *edgel = g_new (Edgel, 1);
  edgel->x         = x;
  edgel->y         = y;
  edgel->direction = direction;
  pika_edgel_init (edgel);
  return edgel;
}
static void
pika_edgel_init (Edgel *edgel)
{
  edgel->x_normal  = 0;
  edgel->y_normal  = 0;
  edgel->curvature = 0;
  edgel->next      = edgel->previous = G_MAXUINT;
}
static void
pika_edgel_clear (Edgel **edgel)
{
  g_clear_pointer (edgel, g_free);
}
static int
pika_edgel_cmp (const Edgel* e1,
                const Edgel* e2)
{
  pika_assert (e1 && e2);
  if ((e1->x == e2->x) && (e1->y == e2->y) &&
      (e1->direction == e2->direction))
    return 0;
  else if ((e1->y < e2->y) || (e1->y == e2->y && e1->x < e2->x) ||
           (e1->y == e2->y && e1->x == e2->x && e1->direction < e2->direction))
    return -1;
  else
    return 1;
}
static guint
edgel2index_hash_fun (Edgel *key)
{
  /* Cantor pairing function.
   * Was not sure how to use the direction though. :-/
   */
  return (key->x + key->y) * (key->x + key->y + 1) / 2 + key->y * key->direction;
}
static gboolean
edgel2index_equal_fun (Edgel *e1,
                       Edgel *e2)
{
  return (e1->x == e2->x && e1->y == e2->y &&
          e1->direction == e2->direction);
}
/**
 * @mask;
 * @edgel:
 * @size_limit:
 *
 * Track a border, marking inner pixels with a bit corresponding to the
 * edgel traversed (4 << direction) for direction in {0,1,2,3}.
 * Stop tracking after @size_limit edgels have been visited.
 *
 * Returns: Number of visited edgels, or -1 if an already visited edgel
 *          has been encountered.
 */
static glong
pika_edgel_track_mark (GeglBuffer *mask,
                       Edgel       edgel,
                       long        size_limit)
{
  Edgel start = edgel;
  long  count = 1;
  do
    {
      guchar val;
      pika_edgelset_next8 (mask, &edgel, &edgel);
      gegl_buffer_sample (mask, edgel.x, edgel.y, NULL, &val,
                          NULL, GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
      if (val & 2)
        {
          /* Only mark pixels of the spline/segment */
          if (val & (4 << edgel.direction))
            return -1;
          /* Mark edgel in pixel (1 == In Mask, 2 == Spline/Segment) */
          val |= (4 << edgel.direction);
          gegl_buffer_set (mask, GEGL_RECTANGLE (edgel.x, edgel.y, 1, 1), 0,
                           NULL, &val, GEGL_AUTO_ROWSTRIDE);
        }
      if (pika_edgel_cmp (&edgel, &start) != 0)
        ++count;
    }
  while (pika_edgel_cmp (&edgel, &start) != 0 && count <= size_limit);
  return count;
}
/**
 * pika_edgel_region_area:
 * @mask: current state of closed line art buffer.
 * @start_edgel: edgel to follow.
 *
 * Follows a line border, starting from @start_edgel to compute the area
 * enclosed by this border.
 * Unfortunately this may return a negative area when the line does not
 * close a zone. In this case, there is an uncertainty on the size of
 * the created zone, and we should consider it a big size.
 *
 * Returns: the area enclosed by the followed line, or a negative value
 * if the zone is not closed (hence actual area unknown).
 */
static glong
pika_edgel_region_area (const GeglBuffer *mask,
                        Edgel             start_edgel)
{
  Edgel edgel = start_edgel;
  glong area = 0;
  do
    {
      if (edgel.direction == XPlusDirection)
        area -= edgel.x;
      else if (edgel.direction == XMinusDirection)
        area += edgel.x - 1;
      pika_edgelset_next8 (mask, &edgel, &edgel);
    }
  while (pika_edgel_cmp (&edgel, &start_edgel) != 0);
  return area;
}
/* Edgel sets */
static GArray *
pika_edgelset_new (GeglBuffer *buffer,
                   PikaAsync  *async)
{
  GeglBufferIterator *gi;
  GArray             *set;
  GHashTable         *edgel2index;
  gint                width  = gegl_buffer_get_width (buffer);
  gint                height = gegl_buffer_get_height (buffer);
  set = g_array_new (TRUE, TRUE, sizeof (Edgel *));
  g_array_set_clear_func (set, (GDestroyNotify) pika_edgel_clear);
  if (width <= 1 || height <= 1)
    return set;
  edgel2index = g_hash_table_new ((GHashFunc) edgel2index_hash_fun,
                                  (GEqualFunc) edgel2index_equal_fun);
  gi = gegl_buffer_iterator_new (buffer, GEGL_RECTANGLE (0, 0, width, height),
                                 0, NULL, GEGL_ACCESS_READ, GEGL_ABYSS_NONE, 5);
  gegl_buffer_iterator_add (gi, buffer, GEGL_RECTANGLE (0, -1, width, height),
                            0, NULL, GEGL_ACCESS_READ, GEGL_ABYSS_NONE);
  gegl_buffer_iterator_add (gi, buffer, GEGL_RECTANGLE (0, 1, width, height),
                            0, NULL, GEGL_ACCESS_READ, GEGL_ABYSS_NONE);
  gegl_buffer_iterator_add (gi, buffer, GEGL_RECTANGLE (-1, 0, width, height),
                            0, NULL, GEGL_ACCESS_READ, GEGL_ABYSS_NONE);
  gegl_buffer_iterator_add (gi, buffer, GEGL_RECTANGLE (1, 0, width, height),
                            0, NULL, GEGL_ACCESS_READ, GEGL_ABYSS_NONE);
  while (gegl_buffer_iterator_next (gi))
    {
      guint8 *p      = (guint8*) gi->items[0].data;
      guint8 *prevy  = (guint8*) gi->items[1].data;
      guint8 *nexty  = (guint8*) gi->items[2].data;
      guint8 *prevx  = (guint8*) gi->items[3].data;
      guint8 *nextx  = (guint8*) gi->items[4].data;
      gint    startx = gi->items[0].roi.x;
      gint    starty = gi->items[0].roi.y;
      gint    endy   = starty + gi->items[0].roi.height;
      gint    endx   = startx + gi->items[0].roi.width;
      gint    x;
      gint    y;
      if (pika_async_is_canceled (async))
        {
          gegl_buffer_iterator_stop (gi);
          pika_async_abort (async);
          goto end;
        }
      for (y = starty; y < endy; y++)
        for (x = startx; x < endx; x++)
          {
            if (*(p++))
              {
                if (! *prevy)
                  pika_edgelset_add (set, x, y, YMinusDirection, edgel2index);
                if (! *nexty)
                  pika_edgelset_add (set, x, y, YPlusDirection, edgel2index);
                if (! *prevx)
                  pika_edgelset_add (set, x, y, XMinusDirection, edgel2index);
                if (! *nextx)
                  pika_edgelset_add (set, x, y, XPlusDirection, edgel2index);
              }
            prevy++;
            nexty++;
            prevx++;
            nextx++;
          }
    }
  pika_edgelset_build_graph (set, buffer, edgel2index, async);
  if (pika_async_is_stopped (async))
    goto end;
  pika_edgelset_init_normals (set);
 end:
  g_hash_table_destroy (edgel2index);
  if (pika_async_is_stopped (async))
    {
      g_array_free (set, TRUE);
      set = NULL;
    }
  return set;
}
static void
pika_edgelset_add (GArray     *set,
                   int         x,
                   int         y,
                   Direction   direction,
                   GHashTable *edgel2index)
{
  Edgel         *edgel    = pika_edgel_new (x, y, direction);
  unsigned long  position = set->len;
  g_array_append_val (set, edgel);
  g_hash_table_insert (edgel2index, edgel, GUINT_TO_POINTER (position));
}
static void
pika_edgelset_init_normals (GArray *set)
{
  Edgel **e = (Edgel**) set->data;
  while (*e)
    {
      PikaVector2 n = Direction2Normal[(*e)->direction];
      (*e)->x_normal = n.x;
      (*e)->y_normal = n.y;
      e++;
    }
}
static void
pika_edgelset_smooth_normals (GArray    *set,
                              int        mask_size,
                              PikaAsync *async)
{
  const gfloat sigma = mask_size * 0.775;
  const gfloat den   = 2 * sigma * sigma;
  gfloat       weights[65];
  PikaVector2  smoothed_normal;
  gint         i;
  pika_assert (mask_size <= 65);
  weights[0] = 1.0f;
  for (int i = 1; i <= mask_size; ++i)
    weights[i] = expf (-(i * i) / den);
  for (i = 0; i < set->len; i++)
    {
      Edgel *it           = g_array_index (set, Edgel*, i);
      Edgel *edgel_before = g_array_index (set, Edgel*, it->previous);
      Edgel *edgel_after  = g_array_index (set, Edgel*, it->next);
      int    n = mask_size;
      int    i = 1;
      if (pika_async_is_canceled (async))
        {
          pika_async_abort (async);
          return;
        }
      smoothed_normal = Direction2Normal[it->direction];
      while (n-- && (edgel_after != edgel_before))
        {
          smoothed_normal = pika_vector2_add_val (smoothed_normal,
                                                  pika_vector2_mul_val (Direction2Normal[edgel_before->direction], weights[i]));
          smoothed_normal = pika_vector2_add_val (smoothed_normal,
                                                  pika_vector2_mul_val (Direction2Normal[edgel_after->direction], weights[i]));
          edgel_before = g_array_index (set, Edgel *, edgel_before->previous);
          edgel_after  = g_array_index (set, Edgel *, edgel_after->next);
          ++i;
        }
      pika_vector2_normalize (&smoothed_normal);
      it->x_normal = smoothed_normal.x;
      it->y_normal = smoothed_normal.y;
    }
}
static void
pika_edgelset_compute_curvature (GArray    *set,
                                 PikaAsync *async)
{
  gint i;
  for (i = 0; i < set->len; i++)
    {
      Edgel       *it       = g_array_index (set, Edgel*, i);
      Edgel       *previous = g_array_index (set, Edgel *, it->previous);
      Edgel       *next     = g_array_index (set, Edgel *, it->next);
      PikaVector2  n_prev   = pika_vector2_new (previous->x_normal, previous->y_normal);
      PikaVector2  n_next   = pika_vector2_new (next->x_normal, next->y_normal);
      PikaVector2  diff     = pika_vector2_mul_val (pika_vector2_sub_val (n_next, n_prev),
                                                    0.5);
      const float  c        = pika_vector2_length_val (diff);
      const float  crossp   = n_prev.x * n_next.y - n_prev.y * n_next.x;
      it->curvature = (crossp > 0.0f) ? c : -c;
      if (pika_async_is_canceled (async))
        {
          pika_async_abort (async);
          return;
        }
    }
}
static void
pika_edgelset_build_graph (GArray     *set,
                           GeglBuffer *buffer,
                           GHashTable *edgel2index,
                           PikaAsync  *async)
{
  Edgel edgel;
  gint  i;
  for (i = 0; i < set->len; i++)
    {
      Edgel *neighbor;
      Edgel *it = g_array_index (set, Edgel *, i);
      guint  neighbor_pos;
      if (pika_async_is_canceled (async))
        {
          pika_async_abort (async);
          return;
        }
      pika_edgelset_next8 (buffer, it, &edgel);
      pika_assert (g_hash_table_contains (edgel2index, &edgel));
      neighbor_pos = GPOINTER_TO_UINT (g_hash_table_lookup (edgel2index, &edgel));
      it->next = neighbor_pos;
      neighbor = g_array_index (set, Edgel *, neighbor_pos);
      neighbor->previous = i;
    }
}
static void
pika_edgelset_next8 (const GeglBuffer *buffer,
                     Edgel            *it,
                     Edgel            *n)
{
  guint8 pixels[9];
  n->x         = it->x;
  n->y         = it->y;
  n->direction = it->direction;
  gegl_buffer_get ((GeglBuffer *) buffer,
                   GEGL_RECTANGLE (n->x - 1, n->y - 1, 3, 3),
                   1.0, NULL, pixels, GEGL_AUTO_ROWSTRIDE,
                   GEGL_ABYSS_NONE);
  switch (n->direction)
    {
    case XPlusDirection:
      if (pixels[8])
        {
          ++(n->y);
          ++(n->x);
          n->direction = YMinusDirection;
        }
      else if (pixels[7])
        {
          ++(n->y);
        }
      else
        {
          n->direction = YPlusDirection;
        }
      break;
    case YMinusDirection:
      if (pixels[2])
        {
          ++(n->x);
          --(n->y);
          n->direction = XMinusDirection;
        }
      else if (pixels[5])
        {
          ++(n->x);
        }
      else
        {
          n->direction = XPlusDirection;
        }
      break;
    case XMinusDirection:
      if (pixels[0])
        {
          --(n->x);
          --(n->y);
          n->direction = YPlusDirection;
        }
      else if (pixels[1])
        {
          --(n->y);
        }
      else
        {
          n->direction = YMinusDirection;
        }
      break;
    case YPlusDirection:
      if (pixels[6])
        {
          --(n->x);
          ++(n->y);
          n->direction = XPlusDirection;
        }
      else if (pixels[3])
        {
          --(n->x);
        }
      else
        {
          n->direction = XMinusDirection;
        }
      break;
    default:
      g_return_if_reached ();
      break;
    }
}