summaryrefslogtreecommitdiffstats
path: root/app/core/gimpscanconvert.c
diff options
context:
space:
mode:
Diffstat (limited to 'app/core/gimpscanconvert.c')
-rw-r--r--app/core/gimpscanconvert.c637
1 files changed, 637 insertions, 0 deletions
diff --git a/app/core/gimpscanconvert.c b/app/core/gimpscanconvert.c
new file mode 100644
index 0000000..dc763bc
--- /dev/null
+++ b/app/core/gimpscanconvert.c
@@ -0,0 +1,637 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995-1999 Spencer Kimball and Peter Mattis
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <string.h>
+
+#include <gdk-pixbuf/gdk-pixbuf.h>
+#include <gegl.h>
+
+#include <cairo.h>
+
+#include "libgimpbase/gimpbase.h"
+#include "libgimpmath/gimpmath.h"
+
+#include "core-types.h"
+
+#include "gimpboundary.h"
+#include "gimpbezierdesc.h"
+#include "gimpscanconvert.h"
+
+
+struct _GimpScanConvert
+{
+ gdouble ratio_xy;
+
+ gboolean clip;
+ gint clip_x;
+ gint clip_y;
+ gint clip_w;
+ gint clip_h;
+
+ /* stroking options */
+ gboolean do_stroke;
+ gdouble width;
+ GimpJoinStyle join;
+ GimpCapStyle cap;
+ gdouble miter;
+ gdouble dash_offset;
+ GArray *dash_info;
+
+ GArray *path_data;
+};
+
+
+/* public functions */
+
+/**
+ * gimp_scan_convert_new:
+ *
+ * Create a new scan conversion context.
+ *
+ * Return value: a newly allocated #GimpScanConvert context.
+ */
+GimpScanConvert *
+gimp_scan_convert_new (void)
+{
+ GimpScanConvert *sc = g_slice_new0 (GimpScanConvert);
+
+ sc->path_data = g_array_new (FALSE, FALSE, sizeof (cairo_path_data_t));
+ sc->ratio_xy = 1.0;
+
+ return sc;
+}
+
+GimpScanConvert *
+gimp_scan_convert_new_from_boundary (const GimpBoundSeg *bound_segs,
+ gint n_bound_segs,
+ gint offset_x,
+ gint offset_y)
+{
+ g_return_val_if_fail (bound_segs == NULL || n_bound_segs != 0, NULL);
+
+ if (bound_segs)
+ {
+ GimpBoundSeg *stroke_segs;
+ gint n_stroke_segs;
+
+ stroke_segs = gimp_boundary_sort (bound_segs, n_bound_segs,
+ &n_stroke_segs);
+
+ if (stroke_segs)
+ {
+ GimpBezierDesc *bezier;
+
+ bezier = gimp_bezier_desc_new_from_bound_segs (stroke_segs,
+ n_bound_segs,
+ n_stroke_segs);
+
+ g_free (stroke_segs);
+
+ if (bezier)
+ {
+ GimpScanConvert *scan_convert;
+
+ scan_convert = gimp_scan_convert_new ();
+
+ gimp_bezier_desc_translate (bezier, offset_x, offset_y);
+ gimp_scan_convert_add_bezier (scan_convert, bezier);
+
+ gimp_bezier_desc_free (bezier);
+
+ return scan_convert;
+ }
+ }
+ }
+
+ return NULL;
+}
+
+/**
+ * gimp_scan_convert_free:
+ * @sc: a #GimpScanConvert context
+ *
+ * Frees the resources allocated for @sc.
+ */
+void
+gimp_scan_convert_free (GimpScanConvert *sc)
+{
+ g_return_if_fail (sc != NULL);
+
+ if (sc->path_data)
+ g_array_free (sc->path_data, TRUE);
+
+ if (sc->dash_info)
+ g_array_free (sc->dash_info, TRUE);
+
+ g_slice_free (GimpScanConvert, sc);
+}
+
+/**
+ * gimp_scan_convert_set_pixel_ratio:
+ * @sc: a #GimpScanConvert context
+ * @ratio_xy: the aspect ratio of the major coordinate axes
+ *
+ * Sets the pixel aspect ratio.
+ */
+void
+gimp_scan_convert_set_pixel_ratio (GimpScanConvert *sc,
+ gdouble ratio_xy)
+{
+ g_return_if_fail (sc != NULL);
+
+ /* we only need the relative resolution */
+ sc->ratio_xy = ratio_xy;
+}
+
+/**
+ * gimp_scan_convert_set_clip_rectangle
+ * @sc: a #GimpScanConvert context
+ * @x: horizontal offset of clip rectangle
+ * @y: vertical offset of clip rectangle
+ * @width: width of clip rectangle
+ * @height: height of clip rectangle
+ *
+ * Sets a clip rectangle on @sc. Subsequent render operations will be
+ * restricted to this area.
+ */
+void
+gimp_scan_convert_set_clip_rectangle (GimpScanConvert *sc,
+ gint x,
+ gint y,
+ gint width,
+ gint height)
+{
+ g_return_if_fail (sc != NULL);
+
+ sc->clip = TRUE;
+ sc->clip_x = x;
+ sc->clip_y = y;
+ sc->clip_w = width;
+ sc->clip_h = height;
+}
+
+/**
+ * gimp_scan_convert_add_polyline:
+ * @sc: a #GimpScanConvert context
+ * @n_points: number of points to add
+ * @points: array of points to add
+ * @closed: whether to close the polyline and make it a polygon
+ *
+ * Add a polyline with @n_points @points that may be open or closed.
+ *
+ * Please note that you should use gimp_scan_convert_stroke() if you
+ * specify open polygons.
+ */
+void
+gimp_scan_convert_add_polyline (GimpScanConvert *sc,
+ guint n_points,
+ const GimpVector2 *points,
+ gboolean closed)
+{
+ GimpVector2 prev = { 0.0, 0.0, };
+ cairo_path_data_t pd;
+ gint i;
+
+ g_return_if_fail (sc != NULL);
+ g_return_if_fail (points != NULL);
+ g_return_if_fail (n_points > 0);
+
+ for (i = 0; i < n_points; i++)
+ {
+ /* compress multiple identical coordinates */
+ if (i == 0 ||
+ prev.x != points[i].x ||
+ prev.y != points[i].y)
+ {
+ pd.header.type = (i == 0) ? CAIRO_PATH_MOVE_TO : CAIRO_PATH_LINE_TO;
+ pd.header.length = 2;
+ sc->path_data = g_array_append_val (sc->path_data, pd);
+
+ pd.point.x = points[i].x;
+ pd.point.y = points[i].y;
+ sc->path_data = g_array_append_val (sc->path_data, pd);
+ prev = points[i];
+ }
+ }
+
+ /* close the polyline when needed */
+ if (closed)
+ {
+ pd.header.type = CAIRO_PATH_CLOSE_PATH;
+ pd.header.length = 1;
+ sc->path_data = g_array_append_val (sc->path_data, pd);
+ }
+}
+
+/**
+ * gimp_scan_convert_add_polyline:
+ * @sc: a #GimpScanConvert context
+ * @bezier: a #GimpBezierDesc
+ *
+ * Adds a @bezier path to @sc.
+ *
+ * Please note that you should use gimp_scan_convert_stroke() if you
+ * specify open paths.
+ **/
+void
+gimp_scan_convert_add_bezier (GimpScanConvert *sc,
+ const GimpBezierDesc *bezier)
+{
+ g_return_if_fail (sc != NULL);
+ g_return_if_fail (bezier != NULL);
+
+ sc->path_data = g_array_append_vals (sc->path_data,
+ bezier->data, bezier->num_data);
+}
+
+/**
+ * gimp_scan_convert_stroke:
+ * @sc: a #GimpScanConvert context
+ * @width: line width in pixels
+ * @join: how lines should be joined
+ * @cap: how to render the end of lines
+ * @miter: convert a mitered join to a bevelled join if the miter would
+ * extend to a distance of more than @miter times @width from
+ * the actual join point
+ * @dash_offset: offset to apply on the dash pattern
+ * @dash_info: dash pattern or %NULL for a solid line
+ *
+ * Stroke the content of a GimpScanConvert. The next
+ * gimp_scan_convert_render() will result in the outline of the
+ * polygon defined with the commands above.
+ *
+ * You cannot add additional polygons after this command.
+ *
+ * Note that if you have nonstandard resolution, "width" gives the
+ * width (in pixels) for a vertical stroke, i.e. use the X resolution
+ * to calculate the width of a stroke when operating with real world
+ * units.
+ */
+void
+gimp_scan_convert_stroke (GimpScanConvert *sc,
+ gdouble width,
+ GimpJoinStyle join,
+ GimpCapStyle cap,
+ gdouble miter,
+ gdouble dash_offset,
+ GArray *dash_info)
+{
+ sc->do_stroke = TRUE;
+ sc->width = width;
+ sc->join = join;
+ sc->cap = cap;
+ sc->miter = miter;
+
+ if (sc->dash_info)
+ {
+ g_array_free (sc->dash_info, TRUE);
+ sc->dash_info = NULL;
+ }
+
+ if (dash_info && dash_info->len >= 2)
+ {
+ gint n_dashes;
+ gdouble *dashes;
+ gint i;
+
+ dash_offset = dash_offset * MAX (width, 1.0);
+
+ n_dashes = dash_info->len;
+ dashes = g_new (gdouble, dash_info->len);
+
+ for (i = 0; i < dash_info->len ; i++)
+ dashes[i] = MAX (width, 1.0) * g_array_index (dash_info, gdouble, i);
+
+ /* correct 0.0 in the first element (starts with a gap) */
+
+ if (dashes[0] == 0.0)
+ {
+ gdouble first;
+
+ first = dashes[1];
+
+ /* shift the pattern to really starts with a dash and
+ * use the offset to skip into it.
+ */
+ for (i = 0; i < dash_info->len - 2; i++)
+ {
+ dashes[i] = dashes[i+2];
+ dash_offset += dashes[i];
+ }
+
+ if (dash_info->len % 2 == 1)
+ {
+ dashes[dash_info->len - 2] = first;
+ n_dashes --;
+ }
+ else if (dash_info->len > 2)
+ {
+ dashes [dash_info->len - 3] += first;
+ n_dashes -= 2;
+ }
+ }
+
+ /* correct odd number of dash specifiers */
+
+ if (n_dashes % 2 == 1)
+ {
+ gdouble last;
+
+ last = dashes[n_dashes - 1];
+ dashes[0] += last;
+ dash_offset += last;
+ n_dashes --;
+ }
+
+ if (n_dashes >= 2)
+ {
+ sc->dash_info = g_array_sized_new (FALSE, FALSE,
+ sizeof (gdouble), n_dashes);
+ sc->dash_info = g_array_append_vals (sc->dash_info, dashes, n_dashes);
+ sc->dash_offset = dash_offset;
+ }
+
+ g_free (dashes);
+ }
+}
+
+
+/**
+ * gimp_scan_convert_render:
+ * @sc: a #GimpScanConvert context
+ * @buffer: the #GeglBuffer to render to
+ * @off_x: horizontal offset into the @buffer
+ * @off_y: vertical offset into the @buffer
+ * @antialias: whether to apply antialiasiing
+ *
+ * This is a wrapper around gimp_scan_convert_render_full() that replaces the
+ * content of the @buffer with a rendered form of the path passed in.
+ *
+ * You cannot add additional polygons after this command.
+ */
+void
+gimp_scan_convert_render (GimpScanConvert *sc,
+ GeglBuffer *buffer,
+ gint off_x,
+ gint off_y,
+ gboolean antialias)
+{
+ gimp_scan_convert_render_full (sc, buffer, off_x, off_y,
+ TRUE, antialias, 1.0);
+}
+
+/**
+ * gimp_scan_convert_render_value:
+ * @sc: a #GimpScanConvert context
+ * @buffer: the #GeglBuffer to render to
+ * @off_x: horizontal offset into the @buffer
+ * @off_y: vertical offset into the @buffer
+ * @value: value to use for covered pixels
+ *
+ * This is a wrapper around gimp_scan_convert_render_full() that
+ * doesn't do antialiasing but gives control over the value that
+ * should be used for pixels covered by the scan conversion. Uncovered
+ * pixels are set to zero.
+ *
+ * You cannot add additional polygons after this command.
+ */
+void
+gimp_scan_convert_render_value (GimpScanConvert *sc,
+ GeglBuffer *buffer,
+ gint off_x,
+ gint off_y,
+ gdouble value)
+{
+ gimp_scan_convert_render_full (sc, buffer, off_x, off_y,
+ TRUE, FALSE, value);
+}
+
+/**
+ * gimp_scan_convert_compose:
+ * @sc: a #GimpScanConvert context
+ * @buffer: the #GeglBuffer to render to
+ * @off_x: horizontal offset into the @buffer
+ * @off_y: vertical offset into the @buffer
+ *
+ * This is a wrapper around of gimp_scan_convert_render_full() that composes
+ * the (aliased) scan conversion on top of the content of the @buffer.
+ *
+ * You cannot add additional polygons after this command.
+ */
+void
+gimp_scan_convert_compose (GimpScanConvert *sc,
+ GeglBuffer *buffer,
+ gint off_x,
+ gint off_y)
+{
+ gimp_scan_convert_render_full (sc, buffer, off_x, off_y,
+ FALSE, FALSE, 1.0);
+}
+
+/**
+ * gimp_scan_convert_compose_value:
+ * @sc: a #GimpScanConvert context
+ * @buffer: the #GeglBuffer to render to
+ * @off_x: horizontal offset into the @buffer
+ * @off_y: vertical offset into the @buffer
+ * @value: value to use for covered pixels
+ *
+ * This is a wrapper around gimp_scan_convert_render_full() that
+ * composes the (aliased) scan conversion with value @value on top of the
+ * content of the @buffer.
+ *
+ * You cannot add additional polygons after this command.
+ */
+void
+gimp_scan_convert_compose_value (GimpScanConvert *sc,
+ GeglBuffer *buffer,
+ gint off_x,
+ gint off_y,
+ gdouble value)
+{
+ gimp_scan_convert_render_full (sc, buffer, off_x, off_y,
+ FALSE, FALSE, value);
+}
+
+/**
+ * gimp_scan_convert_render_full:
+ * @sc: a #GimpScanConvert context
+ * @buffer: the #GeglBuffer to render to
+ * @off_x: horizontal offset into the @buffer
+ * @off_y: vertical offset into the @buffer
+ * @replace: if true the original content of the @buffer gets estroyed
+ * @antialias: if true the rendering happens antialiased
+ * @value: value to use for covered pixels
+ *
+ * This function renders the area described by the path to the
+ * @buffer, taking the offset @off_x and @off_y in the buffer into
+ * account. The rendering can happen antialiased and be rendered on
+ * top of existing content or replacing it completely. The @value
+ * specifies the opacity value to be used for the objects in the @sc.
+ *
+ * You cannot add additional polygons after this command.
+ */
+void
+gimp_scan_convert_render_full (GimpScanConvert *sc,
+ GeglBuffer *buffer,
+ gint off_x,
+ gint off_y,
+ gboolean replace,
+ gboolean antialias,
+ gdouble value)
+{
+ const Babl *format;
+ GeglBufferIterator *iter;
+ GeglRectangle *roi;
+ cairo_t *cr;
+ cairo_surface_t *surface;
+ cairo_path_t path;
+ gint bpp;
+ gint x, y;
+ gint width, height;
+
+ g_return_if_fail (sc != NULL);
+ g_return_if_fail (GEGL_IS_BUFFER (buffer));
+
+ x = gegl_buffer_get_x (buffer);
+ y = gegl_buffer_get_y (buffer);
+ width = gegl_buffer_get_width (buffer);
+ height = gegl_buffer_get_height (buffer);
+
+ if (sc->clip && ! gimp_rectangle_intersect (x, y, width, height,
+ sc->clip_x, sc->clip_y,
+ sc->clip_w, sc->clip_h,
+ &x, &y, &width, &height))
+ return;
+
+ path.status = CAIRO_STATUS_SUCCESS;
+ path.data = (cairo_path_data_t *) sc->path_data->data;
+ path.num_data = sc->path_data->len;
+
+ format = babl_format ("Y u8");
+ bpp = babl_format_get_bytes_per_pixel (format);
+
+ iter = gegl_buffer_iterator_new (buffer, NULL, 0, format,
+ GEGL_ACCESS_READWRITE, GEGL_ABYSS_NONE, 1);
+ roi = &iter->items[0].roi;
+
+ while (gegl_buffer_iterator_next (iter))
+ {
+ guchar *data = iter->items[0].data;
+ guchar *tmp_buf = NULL;
+ const gint stride = cairo_format_stride_for_width (CAIRO_FORMAT_A8,
+ roi->width);
+
+ /* cairo rowstrides are always multiples of 4, whereas
+ * maskPR.rowstride can be anything, so to be able to create an
+ * image surface, we maybe have to create our own temporary
+ * buffer
+ */
+ if (roi->width * bpp != stride)
+ {
+ tmp_buf = g_alloca (stride * roi->height);
+
+ if (! replace)
+ {
+ const guchar *src = data;
+ guchar *dest = tmp_buf;
+ gint i;
+
+ for (i = 0; i < roi->height; i++)
+ {
+ memcpy (dest, src, roi->width * bpp);
+
+ src += roi->width * bpp;
+ dest += stride;
+ }
+ }
+ }
+
+ surface = cairo_image_surface_create_for_data (tmp_buf ?
+ tmp_buf : data,
+ CAIRO_FORMAT_A8,
+ roi->width, roi->height,
+ stride);
+
+ cairo_surface_set_device_offset (surface,
+ -off_x - roi->x,
+ -off_y - roi->y);
+ cr = cairo_create (surface);
+ cairo_set_operator (cr, CAIRO_OPERATOR_SOURCE);
+
+ if (replace)
+ {
+ cairo_set_source_rgba (cr, 0, 0, 0, 0);
+ cairo_paint (cr);
+ }
+
+ cairo_set_source_rgba (cr, 0, 0, 0, value);
+ cairo_append_path (cr, &path);
+
+ cairo_set_antialias (cr, antialias ?
+ CAIRO_ANTIALIAS_GRAY : CAIRO_ANTIALIAS_NONE);
+ cairo_set_miter_limit (cr, sc->miter);
+
+ if (sc->do_stroke)
+ {
+ cairo_set_line_cap (cr,
+ sc->cap == GIMP_CAP_BUTT ? CAIRO_LINE_CAP_BUTT :
+ sc->cap == GIMP_CAP_ROUND ? CAIRO_LINE_CAP_ROUND :
+ CAIRO_LINE_CAP_SQUARE);
+ cairo_set_line_join (cr,
+ sc->join == GIMP_JOIN_MITER ? CAIRO_LINE_JOIN_MITER :
+ sc->join == GIMP_JOIN_ROUND ? CAIRO_LINE_JOIN_ROUND :
+ CAIRO_LINE_JOIN_BEVEL);
+
+ cairo_set_line_width (cr, sc->width);
+
+ if (sc->dash_info)
+ cairo_set_dash (cr,
+ (double *) sc->dash_info->data,
+ sc->dash_info->len,
+ sc->dash_offset);
+
+ cairo_scale (cr, 1.0, sc->ratio_xy);
+ cairo_stroke (cr);
+ }
+ else
+ {
+ cairo_set_fill_rule (cr, CAIRO_FILL_RULE_EVEN_ODD);
+ cairo_fill (cr);
+ }
+
+ cairo_destroy (cr);
+ cairo_surface_destroy (surface);
+
+ if (tmp_buf)
+ {
+ const guchar *src = tmp_buf;
+ guchar *dest = data;
+ gint i;
+
+ for (i = 0; i < roi->height; i++)
+ {
+ memcpy (dest, src, roi->width * bpp);
+
+ src += stride;
+ dest += roi->width * bpp;
+ }
+ }
+ }
+}