Gimp/libgimp/gimpcurve.c

749 lines
20 KiB
C
Raw Permalink Normal View History

/* LIBGIMP - The GIMP Library
* Copyright (C) 1995-1997 Peter Mattis and Spencer Kimball
*
* gimpcurve.c
* Copyright (C) 2026 Alx Sa
*
* This library is free software: you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see
* <https://www.gnu.org/licenses/>.
*/
#include "config.h"
#include "gimp.h"
#include "gimpcurve.h"
enum
{
PROP_0,
PROP_CURVE_TYPE,
PROP_N_SAMPLES,
N_PROPS
};
static GParamSpec *obj_props[N_PROPS] = { NULL, };
enum
{
POINTS_CHANGED,
SAMPLES_CHANGED,
LAST_SIGNAL
};
static guint gimp_curve_signals[LAST_SIGNAL] = { 0 };
typedef struct
{
gdouble x;
gdouble y;
GimpCurvePointType type;
} GimpCurvePoint;
struct _GimpCurve
{
GObject parent_instance;
GimpCurveType curve_type;
gint n_points;
GimpCurvePoint *points;
gint n_samples;
gdouble *samples;
gboolean identity; /* whether the curve is an identity mapping */
};
static void gimp_curve_finalize (GObject *object);
static void gimp_curve_set_property (GObject *object,
guint property_id,
const GValue *value,
GParamSpec *pspec);
static void gimp_curve_get_property (GObject *object,
guint property_id,
GValue *value,
GParamSpec *pspec);
static void gimp_curve_build_samples (GimpCurve *curve);
G_DEFINE_TYPE (GimpCurve, gimp_curve, G_TYPE_OBJECT);
static void gimp_curve_class_init (GimpCurveClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
/**
* GimpCurve::points-changed:
* @curve: the curve emitting the signal
*
* Emitted when any points are added, deleted, or edited in @curve.
*
* Since: 3.2
*/
gimp_curve_signals[POINTS_CHANGED] =
g_signal_new ("points-changed",
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_FIRST,
0,
NULL, NULL, NULL,
G_TYPE_NONE, 0);
/**
* GimpCurve::samples-changed:
* @curve: the curve emitting the signal
*
* Emitted when any sample is edited, or if the number of samples
* changed, in @curve.
*
* Since: 3.2
*/
gimp_curve_signals[SAMPLES_CHANGED] =
g_signal_new ("samples-changed",
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_FIRST,
0,
NULL, NULL, NULL,
G_TYPE_NONE, 0);
object_class->finalize = gimp_curve_finalize;
object_class->set_property = gimp_curve_set_property;
object_class->get_property = gimp_curve_get_property;
/**
* GimpCurve:curve-type:
*
* The curve type.
*
* Since: 3.2
*/
obj_props[PROP_CURVE_TYPE] =
g_param_spec_enum ("curve-type",
"Curve Type",
"The curve type",
GIMP_TYPE_CURVE_TYPE,
GIMP_CURVE_SMOOTH,
GIMP_CONFIG_PARAM_FLAGS |
G_PARAM_EXPLICIT_NOTIFY);
/**
* GimpCurve:n-samples:
*
* The number of samples this [enum@Gimp.CurveType.FREE] curve is
* split into.
*
* Since: 3.2
*/
obj_props[PROP_N_SAMPLES] =
g_param_spec_int ("n-samples",
"Number of Samples",
"The number of samples",
256, 256, 256,
GIMP_CONFIG_PARAM_FLAGS |
G_PARAM_EXPLICIT_NOTIFY);
g_object_class_install_properties (object_class, N_PROPS, obj_props);
}
static void
gimp_curve_init (GimpCurve *curve)
{
curve->n_points = 0;
curve->points = NULL;
curve->n_samples = 256;
curve->samples = NULL;
curve->identity = FALSE;
}
static void
gimp_curve_finalize (GObject *object)
{
GimpCurve *curve = GIMP_CURVE (object);
g_clear_pointer (&curve->points, g_free);
curve->n_points = 0;
g_clear_pointer (&curve->samples, g_free);
curve->n_samples = 0;
}
static void
gimp_curve_set_property (GObject *object,
guint property_id,
const GValue *value,
GParamSpec *pspec)
{
GimpCurve *curve = GIMP_CURVE (object);
switch (property_id)
{
case PROP_CURVE_TYPE:
gimp_curve_set_curve_type (curve, g_value_get_enum (value));
break;
case PROP_N_SAMPLES:
if (curve->curve_type == GIMP_CURVE_FREE)
gimp_curve_set_n_samples (curve, g_value_get_int (value));
else
curve->n_samples = g_value_get_int (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static void
gimp_curve_get_property (GObject *object,
guint property_id,
GValue *value,
GParamSpec *pspec)
{
GimpCurve *curve = GIMP_CURVE (object);
switch (property_id)
{
case PROP_CURVE_TYPE:
g_value_set_enum (value, curve->curve_type);
break;
case PROP_N_SAMPLES:
g_value_set_int (value, curve->n_samples);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
/* Public Functions */
/**
* gimp_curve_new:
*
* Creates a new #GimpCurve object, of type
* [enum@Gimp.CurveType.SMOOTH], with 0 points initially.
*
* Returns: (transfer full): a new curve.
*
* Since: 3.2
*/
GimpCurve *
gimp_curve_new (void)
{
return g_object_new (GIMP_TYPE_CURVE, NULL);
}
/**
* gimp_curve_get_curve_type:
* @curve: the #GimpCurve.
*
* Returns: the @curve type.
*
* Since: 3.2
*/
GimpCurveType
gimp_curve_get_curve_type (GimpCurve *curve)
{
g_return_val_if_fail (GIMP_IS_CURVE (curve), GIMP_CURVE_SMOOTH);
return curve->curve_type;
}
/**
* gimp_curve_set_curve_type:
* @curve: the #GimpCurve.
* @curve_type: the new curve type.
*
* Sets the curve type of @curve, as follows:
*
* - Nothing happens if the curve type is unchanged.
* - If you change to [enum@Gimp.CurveType.SMOOTH], it will create a
* non-specified number of points and will approximate their position
* along the freehand curve. All default points will be
* [enum@Gimp.CurvePointType.SMOOTH].
* - If you change to [enum@Gimp.CurveType.FREE], all existing points
* will be cleared.
*
* Since: 3.2
*/
void
gimp_curve_set_curve_type (GimpCurve *curve,
GimpCurveType curve_type)
{
g_return_if_fail (GIMP_IS_CURVE (curve));
if (curve->curve_type != curve_type)
{
g_object_freeze_notify (G_OBJECT (curve));
if (curve_type == GIMP_CURVE_SMOOTH)
{
gint i;
curve->curve_type = curve_type;
g_free (curve->points);
/* pick some points from the curve and make them control
* points
*/
curve->n_points = 9;
curve->points = g_new0 (GimpCurvePoint, 9);
for (i = 0; i < curve->n_points; i++)
{
gint sample = i * (curve->n_samples - 1) / (curve->n_points - 1);
curve->points[i].x = (gdouble) sample /
(gdouble) (curve->n_samples - 1);
curve->points[i].y = curve->samples[sample];
curve->points[i].type = GIMP_CURVE_POINT_SMOOTH;
}
g_signal_emit (G_OBJECT (curve), gimp_curve_signals[POINTS_CHANGED], 0);
}
else
{
gimp_curve_clear_points (curve);
curve->curve_type = curve_type;
gimp_curve_build_samples (curve);
}
g_object_notify_by_pspec (G_OBJECT (curve), obj_props[PROP_CURVE_TYPE]);
g_object_thaw_notify (G_OBJECT (curve));
}
}
/**
* gimp_curve_get_n_points:
* @curve: the #GimpCurve.
*
* Gets the number of points in a [enum@Gimp.CurveType.SMOOTH] curve. Note that it will always
* be 0 for a [enum@Gimp.CurveType.FREE] curve.
*
* This can later be used e.g. in [method@Gimp.Curve.get_point] as points
* are numbered from 0 (included) to the returned number (excluded).
*
* Note that the #GimpCurve API is not thread-safe. So be careful that
* the information on the number of points is still valid when you use
* it (you may have added or removed points in particular).
*
* Returns: the number of points in a smooth curve.
*
* Since: 3.2
*/
gint
gimp_curve_get_n_points (GimpCurve *curve)
{
g_return_val_if_fail (GIMP_IS_CURVE (curve), 0);
g_return_val_if_fail (curve->curve_type == GIMP_CURVE_SMOOTH, 0);
return curve->n_points;
}
/**
* gimp_curve_add_point:
* @curve: the #GimpCurve.
* @x: the point abscissa on a `[0.0, 1.0]` range.
* @y: the point ordinate on a `[0.0, 1.0]` range.
*
* Add a new point in a [enum@Gimp.CurveType.SMOOTH] @curve, with
* coordinates `(x, y)`. Any value outside the `[0.0, 1.0]` range will
* be silently clamped.
*
* The returned identifier can later be used e.g. in
* [method@Gimp.Curve.get_point] or other functions taking a @point number
* as argument.
*
* Calling this may change identifiers for other points and the total
* number of points in this @curve. Any such information you currently
* hold should be considered invalid once the curve is changed.
*
* Returns: a point identifier to be used in other functions.
*
* Since: 3.2
*/
gint
gimp_curve_add_point (GimpCurve *curve,
gdouble x,
gdouble y)
{
GimpCurvePoint *points;
gint point;
g_return_val_if_fail (GIMP_IS_CURVE (curve), -1);
g_return_val_if_fail (curve->curve_type == GIMP_CURVE_SMOOTH, -1);
x = CLAMP (x, 0.0, 1.0);
y = CLAMP (y, 0.0, 1.0);
for (point = 0; point < curve->n_points; point++)
{
if (curve->points[point].x > x)
break;
}
points = g_new0 (GimpCurvePoint, curve->n_points + 1);
memcpy (points, curve->points,
point * sizeof (GimpCurvePoint));
memcpy (points + point + 1, curve->points + point,
(curve->n_points - point) * sizeof (GimpCurvePoint));
points[point].x = x;
points[point].y = y;
points[point].type = GIMP_CURVE_POINT_SMOOTH;
g_free (curve->points);
curve->n_points++;
curve->points = points;
g_signal_emit (G_OBJECT (curve), gimp_curve_signals[POINTS_CHANGED], 0);
return point;
}
/**
* gimp_curve_get_point:
* @curve: the #GimpCurve.
* @point: a point identifier.
* @x: (out) (nullable): the point abscissa on a `[0.0, 1.0]` range.
* @y: (out) (nullable): the point ordinate on a `[0.0, 1.0]` range.
*
* Gets the @point coordinates for a [enum@Gimp.CurveType.SMOOTH] @curve.
*
* The @point identifier must be between 0 and the value returned by
* [method@Gimp.Curve.get_n_points].
*
* You may also use a point identifier as returned by
* [method@Gimp.Curve.add_point], which will correspond to the same
* point, unless you modified the @curve since (e.g. by calling
* `gimp_curve_add_point` again, or by deleting or modifying a point).
*
* Since: 3.2
*/
void
gimp_curve_get_point (GimpCurve *curve,
gint point,
gdouble *x,
gdouble *y)
{
g_return_if_fail (GIMP_IS_CURVE (curve));
g_return_if_fail (curve->curve_type == GIMP_CURVE_SMOOTH);
g_return_if_fail (point >= 0 && point < curve->n_points);
if (x) *x = curve->points[point].x;
if (y) *y = curve->points[point].y;
}
/**
* gimp_curve_set_point_type:
* @curve: the #GimpCurve.
* @point: a point identifier.
* @type: a point type.
*
* Sets the @point type in a [enum@Gimp.CurveType.SMOOTH] @curve.
*
* Since: 3.2
*/
void
gimp_curve_set_point_type (GimpCurve *curve,
gint point,
GimpCurvePointType type)
{
g_return_if_fail (GIMP_IS_CURVE (curve));
g_return_if_fail (curve->curve_type == GIMP_CURVE_SMOOTH);
g_return_if_fail (point >= 0 && point < curve->n_points);
curve->points[point].type = type;
g_signal_emit (G_OBJECT (curve), gimp_curve_signals[POINTS_CHANGED], 0);
}
/**
* gimp_curve_get_point_type:
* @curve: the #GimpCurve.
* @point: a point identifier.
*
* Returns: the @point type of a [enum@Gimp.CurveType.SMOOTH] @curve.
*
* Since: 3.2
*/
GimpCurvePointType
gimp_curve_get_point_type (GimpCurve *curve,
gint point)
{
g_return_val_if_fail (GIMP_IS_CURVE (curve), GIMP_CURVE_POINT_SMOOTH);
g_return_val_if_fail (curve->curve_type == GIMP_CURVE_SMOOTH, GIMP_CURVE_POINT_SMOOTH);
g_return_val_if_fail (point >= 0 && point < curve->n_points, GIMP_CURVE_POINT_SMOOTH);
return curve->points[point].type;
}
/**
* gimp_curve_delete_point:
* @curve: the #GimpCurve.
* @point: a point identifier.
*
* Deletes a specific @point from a [enum@Gimp.CurveType.SMOOTH] @curve.
*
* The @point identifier must be between 0 and the value returned by
* [method@Gimp.Curve.get_n_points].
*
* You may also use a point identifier as returned by
* [method@Gimp.Curve.add_point], which will correspond to the same
* point, unless you modified the @curve since (e.g. by calling
* `gimp_curve_add_point` again, or by deleting or modifying a point).
*
* Since: 3.2
*/
void
gimp_curve_delete_point (GimpCurve *curve,
gint point)
{
GimpCurvePoint *points;
g_return_if_fail (GIMP_IS_CURVE (curve));
g_return_if_fail (curve->curve_type == GIMP_CURVE_SMOOTH);
g_return_if_fail (point >= 0 && point < curve->n_points);
points = g_new0 (GimpCurvePoint, curve->n_points - 1);
memcpy (points, curve->points,
point * sizeof (GimpCurvePoint));
memcpy (points + point, curve->points + point + 1,
(curve->n_points - point - 1) * sizeof (GimpCurvePoint));
g_free (curve->points);
curve->n_points--;
curve->points = points;
g_signal_emit (G_OBJECT (curve), gimp_curve_signals[POINTS_CHANGED], 0);
}
/**
* gimp_curve_set_point:
* @curve: the #GimpCurve.
* @point: a point identifier.
* @x: the point abscissa on a `[0.0, 1.0]` range.
* @y: the point ordinate on a `[0.0, 1.0]` range.
*
* Sets the @point coordinates in a [enum@Gimp.CurveType.SMOOTH] @curve.
* Any value outside the `[0.0, 1.0]` range will be silently clamped.
*
* Since: 3.2
*/
void
gimp_curve_set_point (GimpCurve *curve,
gint point,
gdouble x,
gdouble y)
{
g_return_if_fail (GIMP_IS_CURVE (curve));
g_return_if_fail (curve->curve_type == GIMP_CURVE_SMOOTH);
g_return_if_fail (point >= 0 && point < curve->n_points);
curve->points[point].x = CLAMP (x, 0.0, 1.0);
curve->points[point].y = CLAMP (y, 0.0, 1.0);
if (point > 0)
curve->points[point].x = MAX (x, curve->points[point - 1].x);
if (point < curve->n_points - 1)
curve->points[point].x = MIN (x, curve->points[point + 1].x);
g_signal_emit (G_OBJECT (curve), gimp_curve_signals[POINTS_CHANGED], 0);
}
/**
* gimp_curve_clear_points:
* @curve: the #GimpCurve.
*
* Deletes all points from a [enum@Gimp.CurveType.SMOOTH] @curve.
*
* A subsequent call to [method@Gimp.Curve.get_n_points] will return 0.
*
* Since: 3.2
*/
void
gimp_curve_clear_points (GimpCurve *curve)
{
g_return_if_fail (GIMP_IS_CURVE (curve));
g_return_if_fail (curve->curve_type == GIMP_CURVE_SMOOTH);
if (curve->points)
{
g_clear_pointer (&curve->points, g_free);
curve->n_points = 0;
g_signal_emit (G_OBJECT (curve), gimp_curve_signals[POINTS_CHANGED], 0);
}
}
/**
* gimp_curve_get_n_samples:
* @curve: the #GimpCurve.
*
* Gets the number of samples in a [enum@Gimp.CurveType.FREE] curve.
*
* Returns: the number of samples in a freehand curve.
*
* Since: 3.2
*/
gint
gimp_curve_get_n_samples (GimpCurve *curve)
{
g_return_val_if_fail (GIMP_IS_CURVE (curve), 0);
g_return_val_if_fail (curve->curve_type == GIMP_CURVE_FREE, 0);
return curve->n_samples;
}
/**
* gimp_curve_set_n_samples:
* @curve: the #GimpCurve.
* @n_samples: the number of samples.
*
* Sets the number of sample in a [enum@Gimp.CurveType.FREE] @curve.
*
* Samples will be positioned on the curve abscissa at regular interval.
* The more samples, the more your curve will have details. Currently,
* the value of @n_samples is limited and must be between `2^8` and `2^12`.
*
* Note that changing the number of samples will reset the curve to an
* identity curve.
*
* Since: 3.2
*/
void
gimp_curve_set_n_samples (GimpCurve *curve,
gint n_samples)
{
g_return_if_fail (GIMP_IS_CURVE (curve));
g_return_if_fail (curve->curve_type == GIMP_CURVE_FREE);
g_return_if_fail (n_samples >= 256);
g_return_if_fail (n_samples <= 4096);
if (n_samples != curve->n_samples)
{
g_object_freeze_notify (G_OBJECT (curve));
curve->n_samples = n_samples;
gimp_curve_build_samples (curve);
g_object_thaw_notify (G_OBJECT (curve));
g_object_notify_by_pspec (G_OBJECT (curve), obj_props[PROP_N_SAMPLES]);
g_signal_emit (G_OBJECT (curve), gimp_curve_signals[SAMPLES_CHANGED], 0);
}
}
/**
* gimp_curve_get_sample:
* @curve: the #GimpCurve.
* @x: an abscissa on a `[0.0, 1.0]` range.
*
* Gets the ordinate @y value corresponding to the passed @x abscissa
* value, in a [enum@Gimp.CurveType.FREE] @curve.
*
* Note that while the @y coordinate will be stored exactly, the @x
* coordinate will be rounded to the closest curve sample on the
* abscissa. The more sample was set with
* [method@Gimp.Curve.set_n_samples], the more precise the rounding will
* be.
*
* Since: 3.2
*/
gdouble
gimp_curve_get_sample (GimpCurve *curve,
gdouble x)
{
g_return_val_if_fail (GIMP_IS_CURVE (curve), 0);
g_return_val_if_fail (curve->curve_type == GIMP_CURVE_FREE, 0);
g_return_val_if_fail (x >= 0 && x <= 1.0, 0);
return curve->samples[ROUND (x * (gdouble) (curve->n_samples - 1))];
}
/**
* gimp_curve_set_sample:
* @curve: the #GimpCurve.
* @x: the point abscissa on a `[0.0, 1.0]` range.
* @y: the point ordinate on a `[0.0, 1.0]` range.
*
* Sets a sample in a [enum@Gimp.CurveType.FREE] @curve, with
* coordinates `(x, y)`.
*
* Note that while the @y coordinate will be stored exactly, the @x
* coordinate will be rounded to the closest curve sample on the
* abscissa. The more sample was set with
* [method@Gimp.Curve.set_n_samples], the more precise the rounding will
* be.
*
* Since: 3.2
*/
void
gimp_curve_set_sample (GimpCurve *curve,
gdouble x,
gdouble y)
{
g_return_if_fail (GIMP_IS_CURVE (curve));
g_return_if_fail (curve->curve_type == GIMP_CURVE_FREE);
g_return_if_fail (x >= 0 && x <= 1.0);
g_return_if_fail (y >= 0 && y <= 1.0);
curve->samples[ROUND (x * (gdouble) (curve->n_samples - 1))] = y;
g_signal_emit (G_OBJECT (curve), gimp_curve_signals[SAMPLES_CHANGED], 0);
}
/**
* gimp_curve_is_identity:
* @curve: a #GimpCurve object
*
* If this function returns %TRUE, then the curve maps each value to
* itself. If it returns %FALSE, then this assumption can not be made.
*
* Returns: %TRUE if the curve is an identity mapping, %FALSE otherwise.
*
* Since: 3.2
**/
gboolean
gimp_curve_is_identity (GimpCurve *curve)
{
g_return_val_if_fail (GIMP_IS_CURVE (curve), FALSE);
return curve->identity;
}
/* Private functions */
static void
gimp_curve_build_samples (GimpCurve *curve)
{
curve->samples = g_renew (gdouble, curve->samples, curve->n_samples);
for (gint i = 0; i < curve->n_samples; i++)
curve->samples[i] = (gdouble) i / (gdouble) (curve->n_samples - 1);
if (curve->curve_type == GIMP_CURVE_FREE)
curve->identity = TRUE;
}