diff options
Diffstat (limited to '')
121 files changed, 28364 insertions, 0 deletions
diff --git a/src/display/CMakeLists.txt b/src/display/CMakeLists.txt new file mode 100644 index 0000000..edbd2b4 --- /dev/null +++ b/src/display/CMakeLists.txt @@ -0,0 +1,130 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +set(display_SRC + cairo-utils.cpp + canvas-arena.cpp + canvas-axonomgrid.cpp + canvas-bpath.cpp + canvas-debug.cpp + canvas-grid.cpp + canvas-rotate.cpp + canvas-temporary-item-list.cpp + canvas-temporary-item.cpp + canvas-text.cpp + curve.cpp + drawing-context.cpp + drawing-group.cpp + drawing-image.cpp + drawing-item.cpp + drawing-pattern.cpp + drawing-shape.cpp + drawing-surface.cpp + drawing-text.cpp + drawing.cpp + gnome-canvas-acetate.cpp + grayscale.cpp + guideline.cpp + nr-3dutils.cpp + nr-filter-blend.cpp + nr-filter-colormatrix.cpp + nr-filter-component-transfer.cpp + nr-filter-composite.cpp + nr-filter-convolve-matrix.cpp + nr-filter-diffuselighting.cpp + nr-filter-displacement-map.cpp + nr-filter-flood.cpp + nr-filter-gaussian.cpp + nr-filter-image.cpp + nr-filter-merge.cpp + nr-filter-morphology.cpp + nr-filter-offset.cpp + nr-filter-primitive.cpp + # nr-filter-skeleton.cpp + nr-filter-slot.cpp + nr-filter-specularlighting.cpp + nr-filter-tile.cpp + nr-filter-turbulence.cpp + nr-filter-units.cpp + nr-filter.cpp + nr-light.cpp + nr-style.cpp + nr-svgfonts.cpp + snap-indicator.cpp + sodipodi-ctrl.cpp + sodipodi-ctrlrect.cpp + sp-canvas-util.cpp + sp-canvas.cpp + sp-ctrlcurve.cpp + sp-ctrlline.cpp + sp-ctrlquadr.cpp + + + # ------- + # Headers + cairo-templates.h + cairo-utils.h + canvas-arena.h + canvas-axonomgrid.h + canvas-bpath.h + canvas-debug.h + canvas-grid.h + canvas-rotate.h + canvas-temporary-item-list.h + canvas-temporary-item.h + canvas-text.h + curve.h + drawing-context.h + drawing-group.h + drawing-image.h + drawing-item.h + drawing-pattern.h + drawing-shape.h + drawing-surface.h + drawing-text.h + drawing.h + gnome-canvas-acetate.h + grayscale.h + guideline.h + nr-3dutils.h + nr-filter-blend.h + nr-filter-colormatrix.h + nr-filter-component-transfer.h + nr-filter-composite.h + nr-filter-convolve-matrix.h + nr-filter-diffuselighting.h + nr-filter-displacement-map.h + nr-filter-flood.h + nr-filter-gaussian.h + nr-filter-image.h + nr-filter-merge.h + nr-filter-morphology.h + nr-filter-offset.h + nr-filter-primitive.h + nr-filter-skeleton.h + nr-filter-slot.h + nr-filter-specularlighting.h + nr-filter-tile.h + nr-filter-turbulence.h + nr-filter-types.h + nr-filter-units.h + nr-filter-utils.h + nr-filter.h + nr-light-types.h + nr-light.h + nr-style.h + nr-svgfonts.h + rendermode.h + snap-indicator.h + sodipodi-ctrl.h + sodipodi-ctrlrect.h + sp-canvas-group.h + sp-canvas-item.h + sp-canvas-util.h + sp-canvas.h + sp-ctrlcurve.h + sp-ctrlline.h + sp-ctrlquadr.h +) + +# add_inkscape_lib(display_LIB "${display_SRC}") +add_inkscape_source("${display_SRC}") diff --git a/src/display/README b/src/display/README new file mode 100644 index 0000000..0a74759 --- /dev/null +++ b/src/display/README @@ -0,0 +1,17 @@ + + +This directory contains code for rendering graphics to the +canvas. This includes rendering of SVG elements (DrawingItem and +derived classes) as well as rendering of control items - +nodes/handles/lines (CanvasItem and derived classes). + +Currently we rely on the "Cairo" graphics library for rendering. + +To do: + +* Split into three directories: drawing, canvas, common. +* Remove SPCurve. + + + + diff --git a/src/display/cairo-templates.h b/src/display/cairo-templates.h new file mode 100644 index 0000000..9a3dd66 --- /dev/null +++ b/src/display/cairo-templates.h @@ -0,0 +1,700 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Cairo software blending templates. + *//* + * Authors: + * Krzysztof KosiĆski <tweenk.pl@gmail.com> + * + * Copyright (C) 2010 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_DISPLAY_CAIRO_TEMPLATES_H +#define SEEN_INKSCAPE_DISPLAY_CAIRO_TEMPLATES_H + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include <glib.h> + +#ifdef HAVE_OPENMP +#include <omp.h> +#include "preferences.h" +// single-threaded operation if the number of pixels is below this threshold +static const int OPENMP_THRESHOLD = 2048; +#endif + +#include <algorithm> +#include <cairo.h> +#include <cmath> +#include "display/nr-3dutils.h" +#include "display/cairo-utils.h" + +/** + * Blend two surfaces using the supplied functor. + * This template blends two Cairo image surfaces using a blending functor that takes + * two 32-bit ARGB pixel values and returns a modified 32-bit pixel value. + * Differences in input surface formats are handled transparently. In future, this template + * will also handle software fallback for GL surfaces. + */ +template <typename Blend> +void ink_cairo_surface_blend(cairo_surface_t *in1, cairo_surface_t *in2, cairo_surface_t *out, Blend blend) +{ + cairo_surface_flush(in1); + cairo_surface_flush(in2); + + // ASSUMPTIONS + // 1. Cairo ARGB32 surface strides are always divisible by 4 + // 2. We can only receive CAIRO_FORMAT_ARGB32 or CAIRO_FORMAT_A8 surfaces + // 3. Both surfaces are of the same size + // 4. Output surface is ARGB32 if at least one input is ARGB32 + + int w = cairo_image_surface_get_width(in2); + int h = cairo_image_surface_get_height(in2); + int stride1 = cairo_image_surface_get_stride(in1); + int stride2 = cairo_image_surface_get_stride(in2); + int strideout = cairo_image_surface_get_stride(out); + int bpp1 = cairo_image_surface_get_format(in1) == CAIRO_FORMAT_A8 ? 1 : 4; + int bpp2 = cairo_image_surface_get_format(in2) == CAIRO_FORMAT_A8 ? 1 : 4; + int bppout = std::max(bpp1, bpp2); + + // Check whether we can loop over pixels without taking stride into account. + bool fast_path = true; + fast_path &= (stride1 == w * bpp1); + fast_path &= (stride2 == w * bpp2); + fast_path &= (strideout == w * bppout); + + int limit = w * h; + + guint32 *const in1_data = reinterpret_cast<guint32*>(cairo_image_surface_get_data(in1)); + guint32 *const in2_data = reinterpret_cast<guint32*>(cairo_image_surface_get_data(in2)); + guint32 *const out_data = reinterpret_cast<guint32*>(cairo_image_surface_get_data(out)); + + // NOTE + // OpenMP probably doesn't help much here. + // It would be better to render more than 1 tile at a time. + #if HAVE_OPENMP + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int numOfThreads = prefs->getIntLimited("/options/threading/numthreads", omp_get_num_procs(), 1, 256); + if (numOfThreads){} // inform compiler we are using it. + #endif + + // The number of code paths here is evil. + if (bpp1 == 4) { + if (bpp2 == 4) { + if (fast_path) { + #if HAVE_OPENMP + #pragma omp parallel for if(limit > OPENMP_THRESHOLD) num_threads(numOfThreads) + #endif + for (int i = 0; i < limit; ++i) { + *(out_data + i) = blend(*(in1_data + i), *(in2_data + i)); + } + } else { + #if HAVE_OPENMP + #pragma omp parallel for if(limit > OPENMP_THRESHOLD) num_threads(numOfThreads) + #endif + for (int i = 0; i < h; ++i) { + guint32 *in1_p = in1_data + i * stride1/4; + guint32 *in2_p = in2_data + i * stride2/4; + guint32 *out_p = out_data + i * strideout/4; + for (int j = 0; j < w; ++j) { + *out_p = blend(*in1_p, *in2_p); + ++in1_p; ++in2_p; ++out_p; + } + } + } + } else { + // bpp2 == 1 + #if HAVE_OPENMP + #pragma omp parallel for if(limit > OPENMP_THRESHOLD) num_threads(numOfThreads) + #endif + for (int i = 0; i < h; ++i) { + guint32 *in1_p = in1_data + i * stride1/4; + guint8 *in2_p = reinterpret_cast<guint8*>(in2_data) + i * stride2; + guint32 *out_p = out_data + i * strideout/4; + for (int j = 0; j < w; ++j) { + guint32 in2_px = *in2_p; + in2_px <<= 24; + *out_p = blend(*in1_p, in2_px); + ++in1_p; ++in2_p; ++out_p; + } + } + } + } else { + if (bpp2 == 4) { + // bpp1 == 1 + #if HAVE_OPENMP + #pragma omp parallel for if(limit > OPENMP_THRESHOLD) num_threads(numOfThreads) + #endif + for (int i = 0; i < h; ++i) { + guint8 *in1_p = reinterpret_cast<guint8*>(in1_data) + i * stride1; + guint32 *in2_p = in2_data + i * stride2/4; + guint32 *out_p = out_data + i * strideout/4; + for (int j = 0; j < w; ++j) { + guint32 in1_px = *in1_p; + in1_px <<= 24; + *out_p = blend(in1_px, *in2_p); + ++in1_p; ++in2_p; ++out_p; + } + } + } else { + // bpp1 == 1 && bpp2 == 1 + if (fast_path) { + #if HAVE_OPENMP + #pragma omp parallel for if(limit > OPENMP_THRESHOLD) num_threads(numOfThreads) + #endif + for (int i = 0; i < limit; ++i) { + guint8 *in1_p = reinterpret_cast<guint8*>(in1_data) + i; + guint8 *in2_p = reinterpret_cast<guint8*>(in2_data) + i; + guint8 *out_p = reinterpret_cast<guint8*>(out_data) + i; + guint32 in1_px = *in1_p; in1_px <<= 24; + guint32 in2_px = *in2_p; in2_px <<= 24; + guint32 out_px = blend(in1_px, in2_px); + *out_p = out_px >> 24; + } + } else { + #if HAVE_OPENMP + #pragma omp parallel for if(limit > OPENMP_THRESHOLD) num_threads(numOfThreads) + #endif + for (int i = 0; i < h; ++i) { + guint8 *in1_p = reinterpret_cast<guint8*>(in1_data) + i * stride1; + guint8 *in2_p = reinterpret_cast<guint8*>(in2_data) + i * stride2; + guint8 *out_p = reinterpret_cast<guint8*>(out_data) + i * strideout; + for (int j = 0; j < w; ++j) { + guint32 in1_px = *in1_p; in1_px <<= 24; + guint32 in2_px = *in2_p; in2_px <<= 24; + guint32 out_px = blend(in1_px, in2_px); + *out_p = out_px >> 24; + ++in1_p; ++in2_p; ++out_p; + } + } + } + } + } + + cairo_surface_mark_dirty(out); +} + +template <typename Filter> +void ink_cairo_surface_filter(cairo_surface_t *in, cairo_surface_t *out, Filter filter) +{ + cairo_surface_flush(in); + + // ASSUMPTIONS + // 1. Cairo ARGB32 surface strides are always divisible by 4 + // 2. We can only receive CAIRO_FORMAT_ARGB32 or CAIRO_FORMAT_A8 surfaces + // 3. Surfaces have the same dimensions + // 4. Output surface is A8 if input is A8 + + int w = cairo_image_surface_get_width(in); + int h = cairo_image_surface_get_height(in); + int stridein = cairo_image_surface_get_stride(in); + int strideout = cairo_image_surface_get_stride(out); + int bppin = cairo_image_surface_get_format(in) == CAIRO_FORMAT_A8 ? 1 : 4; + int bppout = cairo_image_surface_get_format(out) == CAIRO_FORMAT_A8 ? 1 : 4; + int limit = w * h; + + // Check whether we can loop over pixels without taking stride into account. + bool fast_path = true; + fast_path &= (stridein == w * bppin); + fast_path &= (strideout == w * bppout); + + guint32 *const in_data = reinterpret_cast<guint32*>(cairo_image_surface_get_data(in)); + guint32 *const out_data = reinterpret_cast<guint32*>(cairo_image_surface_get_data(out)); + + #if HAVE_OPENMP + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int numOfThreads = prefs->getIntLimited("/options/threading/numthreads", omp_get_num_procs(), 1, 256); + if (numOfThreads){} // inform compiler we are using it. + #endif + + // this is provided just in case, to avoid problems with strict aliasing rules + if (in == out) { + if (bppin == 4) { + #if HAVE_OPENMP + #pragma omp parallel for if(limit > OPENMP_THRESHOLD) num_threads(numOfThreads) + #endif + for (int i = 0; i < limit; ++i) { + *(in_data + i) = filter(*(in_data + i)); + } + } else { + #if HAVE_OPENMP + #pragma omp parallel for if(limit > OPENMP_THRESHOLD) num_threads(numOfThreads) + #endif + for (int i = 0; i < limit; ++i) { + guint8 *in_p = reinterpret_cast<guint8*>(in_data) + i; + guint32 in_px = *in_p; in_px <<= 24; + guint32 out_px = filter(in_px); + *in_p = out_px >> 24; + } + } + cairo_surface_mark_dirty(out); + return; + } + + if (bppin == 4) { + if (bppout == 4) { + // bppin == 4, bppout == 4 + if (fast_path) { + #if HAVE_OPENMP + #pragma omp parallel for if(limit > OPENMP_THRESHOLD) num_threads(numOfThreads) + #endif + for (int i = 0; i < limit; ++i) { + *(out_data + i) = filter(*(in_data + i)); + } + } else { + #if HAVE_OPENMP + #pragma omp parallel for if(limit > OPENMP_THRESHOLD) num_threads(numOfThreads) + #endif + for (int i = 0; i < h; ++i) { + guint32 *in_p = in_data + i * stridein/4; + guint32 *out_p = out_data + i * strideout/4; + for (int j = 0; j < w; ++j) { + *out_p = filter(*in_p); + ++in_p; ++out_p; + } + } + } + } else { + // bppin == 4, bppout == 1 + // we use this path with COLORMATRIX_LUMINANCETOALPHA + #if HAVE_OPENMP + #pragma omp parallel for if(limit > OPENMP_THRESHOLD) num_threads(numOfThreads) + #endif + for (int i = 0; i < h; ++i) { + guint32 *in_p = in_data + i * stridein/4; + guint8 *out_p = reinterpret_cast<guint8*>(out_data) + i * strideout; + for (int j = 0; j < w; ++j) { + guint32 out_px = filter(*in_p); + *out_p = out_px >> 24; + ++in_p; ++out_p; + } + } + } + } else { + // bppin == 1, bppout == 1 + // Note: there is no path for bppin == 1, bppout == 4 because it is useless + if (fast_path) { + #if HAVE_OPENMP + #pragma omp parallel for if(limit > OPENMP_THRESHOLD) num_threads(numOfThreads) + #endif + for (int i = 0; i < limit; ++i) { + guint8 *in_p = reinterpret_cast<guint8*>(in_data) + i; + guint8 *out_p = reinterpret_cast<guint8*>(out_data) + i; + guint32 in_px = *in_p; in_px <<= 24; + guint32 out_px = filter(in_px); + *out_p = out_px >> 24; + } + } else { + #if HAVE_OPENMP + #pragma omp parallel for if(limit > OPENMP_THRESHOLD) num_threads(numOfThreads) + #endif + for (int i = 0; i < h; ++i) { + guint8 *in_p = reinterpret_cast<guint8*>(in_data) + i * stridein; + guint8 *out_p = reinterpret_cast<guint8*>(out_data) + i * strideout; + for (int j = 0; j < w; ++j) { + guint32 in_px = *in_p; in_px <<= 24; + guint32 out_px = filter(in_px); + *out_p = out_px >> 24; + ++in_p; ++out_p; + } + } + } + } + cairo_surface_mark_dirty(out); +} + + +/** + * Synthesize surface pixels based on their position. + * This template accepts a functor that gets called with the x and y coordinates of the pixels, + * given as integers. + * @param out Output surface + * @param out_area The region of the output surface that should be synthesized + * @param synth Synthesis functor + */ +template <typename Synth> +void ink_cairo_surface_synthesize(cairo_surface_t *out, cairo_rectangle_t const &out_area, Synth synth) +{ + // ASSUMPTIONS + // 1. Cairo ARGB32 surface strides are always divisible by 4 + // 2. We can only receive CAIRO_FORMAT_ARGB32 or CAIRO_FORMAT_A8 surfaces + + int w = out_area.width; + int h = out_area.height; + int strideout = cairo_image_surface_get_stride(out); + int bppout = cairo_image_surface_get_format(out) == CAIRO_FORMAT_A8 ? 1 : 4; + // NOTE: fast path is not used, because we would need 2 divisions to get pixel indices + + unsigned char *out_data = cairo_image_surface_get_data(out); + + #if HAVE_OPENMP + int limit = w * h; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int numOfThreads = prefs->getIntLimited("/options/threading/numthreads", omp_get_num_procs(), 1, 256); + if (numOfThreads){} // inform compiler we are using it. + #endif + + if (bppout == 4) { + #if HAVE_OPENMP + #pragma omp parallel for if(limit > OPENMP_THRESHOLD) num_threads(numOfThreads) + #endif + for (int i = out_area.y; i < h; ++i) { + guint32 *out_p = reinterpret_cast<guint32*>(out_data + i * strideout); + for (int j = out_area.x; j < w; ++j) { + *out_p = synth(j, i); + ++out_p; + } + } + } else { + // bppout == 1 + #if HAVE_OPENMP + #pragma omp parallel for if(limit > OPENMP_THRESHOLD) num_threads(numOfThreads) + #endif + for (int i = out_area.y; i < h; ++i) { + guint8 *out_p = out_data + i * strideout; + for (int j = out_area.x; j < w; ++j) { + guint32 out_px = synth(j, i); + *out_p = out_px >> 24; + ++out_p; + } + } + } + cairo_surface_mark_dirty(out); +} + +template <typename Synth> +void ink_cairo_surface_synthesize(cairo_surface_t *out, Synth synth) +{ + int w = cairo_image_surface_get_width(out); + int h = cairo_image_surface_get_height(out); + + cairo_rectangle_t area; + area.x = 0; + area.y = 0; + area.width = w; + area.height = h; + + ink_cairo_surface_synthesize(out, area, synth); +} + +struct SurfaceSynth { + SurfaceSynth(cairo_surface_t *surface) + : _px(cairo_image_surface_get_data(surface)) + , _w(cairo_image_surface_get_width(surface)) + , _h(cairo_image_surface_get_height(surface)) + , _stride(cairo_image_surface_get_stride(surface)) + , _alpha(cairo_surface_get_content(surface) == CAIRO_CONTENT_ALPHA) + { + cairo_surface_flush(surface); + } + + guint32 pixelAt(int x, int y) const { + if (_alpha) { + unsigned char *px = _px + y*_stride + x; + return *px << 24; + } else { + unsigned char *px = _px + y*_stride + x*4; + return *reinterpret_cast<guint32*>(px); + } + } + guint32 alphaAt(int x, int y) const { + if (_alpha) { + unsigned char *px = _px + y*_stride + x; + return *px; + } else { + unsigned char *px = _px + y*_stride + x*4; + guint32 p = *reinterpret_cast<guint32*>(px); + return (p & 0xff000000) >> 24; + } + } + + // retrieve a pixel value with bilinear interpolation + guint32 pixelAt(double x, double y) const { + if (_alpha) { + return alphaAt(x, y) << 24; + } + + double xf = floor(x), yf = floor(y); + int xi = xf, yi = yf; + guint32 xif = round((x - xf) * 255), yif = round((y - yf) * 255); + guint32 p00, p01, p10, p11; + + unsigned char *pxi = _px + yi*_stride + xi*4; + guint32 *pxu = reinterpret_cast<guint32*>(pxi); + guint32 *pxl = reinterpret_cast<guint32*>(pxi + _stride); + p00 = *pxu; p10 = *(pxu + 1); + p01 = *pxl; p11 = *(pxl + 1); + + guint32 comp[4]; + + for (unsigned i = 0; i < 4; ++i) { + guint32 shift = i*8; + guint32 mask = 0xff << shift; + guint32 c00 = (p00 & mask) >> shift; + guint32 c10 = (p10 & mask) >> shift; + guint32 c01 = (p01 & mask) >> shift; + guint32 c11 = (p11 & mask) >> shift; + + guint32 iu = (255-xif) * c00 + xif * c10; + guint32 il = (255-xif) * c01 + xif * c11; + comp[i] = (255-yif) * iu + yif * il; + comp[i] = (comp[i] + (255*255/2)) / (255*255); + } + + guint32 result = comp[0] | (comp[1] << 8) | (comp[2] << 16) | (comp[3] << 24); + return result; + } + + // retrieve an alpha value with bilinear interpolation + guint32 alphaAt(double x, double y) const { + double xf = floor(x), yf = floor(y); + int xi = xf, yi = yf; + guint32 xif = round((x - xf) * 255), yif = round((y - yf) * 255); + guint32 p00, p01, p10, p11; + if (_alpha) { + unsigned char *pxu = _px + yi*_stride + xi; + unsigned char *pxl = pxu + _stride; + p00 = *pxu; p10 = *(pxu + 1); + p01 = *pxl; p11 = *(pxl + 1); + } else { + unsigned char *pxi = _px + yi*_stride + xi*4; + guint32 *pxu = reinterpret_cast<guint32*>(pxi); + guint32 *pxl = reinterpret_cast<guint32*>(pxi + _stride); + p00 = (*pxu & 0xff000000) >> 24; p10 = (*(pxu + 1) & 0xff000000) >> 24; + p01 = (*pxl & 0xff000000) >> 24; p11 = (*(pxl + 1) & 0xff000000) >> 24; + } + guint32 iu = (255-xif) * p00 + xif * p10; + guint32 il = (255-xif) * p01 + xif * p11; + guint32 result = (255-yif) * iu + yif * il; + result = (result + (255*255/2)) / (255*255); + return result; + } + + // compute surface normal at given coordinates using 3x3 Sobel gradient filter + NR::Fvector surfaceNormalAt(int x, int y, double scale) const { + // Below there are some multiplies by zero. They will be optimized out. + // Do not remove them, because they improve readability. + // NOTE: fetching using alphaAt is slightly lazy. + NR::Fvector normal; + double fx = -scale/255.0, fy = -scale/255.0; + normal[Z_3D] = 1.0; + if (G_UNLIKELY(x == 0)) { + // leftmost column + if (G_UNLIKELY(y == 0)) { + // upper left corner + fx *= (2.0/3.0); + fy *= (2.0/3.0); + double p00 = alphaAt(x,y), p10 = alphaAt(x+1, y), + p01 = alphaAt(x,y+1), p11 = alphaAt(x+1, y+1); + normal[X_3D] = + -2.0 * p00 +2.0 * p10 + -1.0 * p01 +1.0 * p11; + normal[Y_3D] = + -2.0 * p00 -1.0 * p10 + +2.0 * p01 +1.0 * p11; + } else if (G_UNLIKELY(y == (_h - 1))) { + // lower left corner + fx *= (2.0/3.0); + fy *= (2.0/3.0); + double p00 = alphaAt(x,y-1), p10 = alphaAt(x+1, y-1), + p01 = alphaAt(x,y ), p11 = alphaAt(x+1, y); + normal[X_3D] = + -1.0 * p00 +1.0 * p10 + -2.0 * p01 +2.0 * p11; + normal[Y_3D] = + -2.0 * p00 -1.0 * p10 + +2.0 * p01 +1.0 * p11; + } else { + // leftmost column + fx *= (1.0/2.0); + fy *= (1.0/3.0); + double p00 = alphaAt(x, y-1), p10 = alphaAt(x+1, y-1), + p01 = alphaAt(x, y ), p11 = alphaAt(x+1, y ), + p02 = alphaAt(x, y+1), p12 = alphaAt(x+1, y+1); + normal[X_3D] = + -1.0 * p00 +1.0 * p10 + -2.0 * p01 +2.0 * p11 + -1.0 * p02 +1.0 * p12; + normal[Y_3D] = + -2.0 * p00 -1.0 * p10 + +0.0 * p01 +0.0 * p11 // this will be optimized out + +2.0 * p02 +1.0 * p12; + } + } else if (G_UNLIKELY(x == (_w - 1))) { + // rightmost column + if (G_UNLIKELY(y == 0)) { + // top right corner + fx *= (2.0/3.0); + fy *= (2.0/3.0); + double p00 = alphaAt(x-1,y), p10 = alphaAt(x, y), + p01 = alphaAt(x-1,y+1), p11 = alphaAt(x, y+1); + normal[X_3D] = + -2.0 * p00 +2.0 * p10 + -1.0 * p01 +1.0 * p11; + normal[Y_3D] = + -1.0 * p00 -2.0 * p10 + +1.0 * p01 +2.0 * p11; + } else if (G_UNLIKELY(y == (_h - 1))) { + // bottom right corner + fx *= (2.0/3.0); + fy *= (2.0/3.0); + double p00 = alphaAt(x-1,y-1), p10 = alphaAt(x, y-1), + p01 = alphaAt(x-1,y ), p11 = alphaAt(x, y); + normal[X_3D] = + -1.0 * p00 +1.0 * p10 + -2.0 * p01 +2.0 * p11; + normal[Y_3D] = + -1.0 * p00 -2.0 * p10 + +1.0 * p01 +2.0 * p11; + } else { + // rightmost column + fx *= (1.0/2.0); + fy *= (1.0/3.0); + double p00 = alphaAt(x-1, y-1), p10 = alphaAt(x, y-1), + p01 = alphaAt(x-1, y ), p11 = alphaAt(x, y ), + p02 = alphaAt(x-1, y+1), p12 = alphaAt(x, y+1); + normal[X_3D] = + -1.0 * p00 +1.0 * p10 + -2.0 * p01 +2.0 * p11 + -1.0 * p02 +1.0 * p12; + normal[Y_3D] = + -1.0 * p00 -2.0 * p10 + +0.0 * p01 +0.0 * p11 + +1.0 * p02 +2.0 * p12; + } + } else { + // interior + if (G_UNLIKELY(y == 0)) { + // top row + fx *= (1.0/3.0); + fy *= (1.0/2.0); + double p00 = alphaAt(x-1, y ), p10 = alphaAt(x, y ), p20 = alphaAt(x+1, y ), + p01 = alphaAt(x-1, y+1), p11 = alphaAt(x, y+1), p21 = alphaAt(x+1, y+1); + normal[X_3D] = + -2.0 * p00 +0.0 * p10 +2.0 * p20 + -1.0 * p01 +0.0 * p11 +1.0 * p21; + normal[Y_3D] = + -1.0 * p00 -2.0 * p10 -1.0 * p20 + +1.0 * p01 +2.0 * p11 +1.0 * p21; + } else if (G_UNLIKELY(y == (_h - 1))) { + // bottom row + fx *= (1.0/3.0); + fy *= (1.0/2.0); + double p00 = alphaAt(x-1, y-1), p10 = alphaAt(x, y-1), p20 = alphaAt(x+1, y-1), + p01 = alphaAt(x-1, y ), p11 = alphaAt(x, y ), p21 = alphaAt(x+1, y ); + normal[X_3D] = + -1.0 * p00 +0.0 * p10 +1.0 * p20 + -2.0 * p01 +0.0 * p11 +2.0 * p21; + normal[Y_3D] = + -1.0 * p00 -2.0 * p10 -1.0 * p20 + +1.0 * p01 +2.0 * p11 +1.0 * p21; + } else { + // interior pixels + // note: p11 is actually unused, so we don't fetch its value + fx *= (1.0/4.0); + fy *= (1.0/4.0); + double p00 = alphaAt(x-1, y-1), p10 = alphaAt(x, y-1), p20 = alphaAt(x+1, y-1), + p01 = alphaAt(x-1, y ), p11 = 0.0, p21 = alphaAt(x+1, y ), + p02 = alphaAt(x-1, y+1), p12 = alphaAt(x, y+1), p22 = alphaAt(x+1, y+1); + normal[X_3D] = + -1.0 * p00 +0.0 * p10 +1.0 * p20 + -2.0 * p01 +0.0 * p11 +2.0 * p21 + -1.0 * p02 +0.0 * p12 +1.0 * p22; + normal[Y_3D] = + -1.0 * p00 -2.0 * p10 -1.0 * p20 + +0.0 * p01 +0.0 * p11 +0.0 * p21 + +1.0 * p02 +2.0 * p12 +1.0 * p22; + } + } + normal[X_3D] *= fx; + normal[Y_3D] *= fy; + NR::normalize_vector(normal); + return normal; + } + + unsigned char *_px; + int _w, _h, _stride; + bool _alpha; +}; + +/* +// simple pixel accessor for image surface that handles different edge wrapping modes +class PixelAccessor { +public: + typedef PixelAccessor self; + enum EdgeMode { + EDGE_PAD, + EDGE_WRAP, + EDGE_ZERO + }; + + PixelAccessor(cairo_surface_t *s, EdgeMode e) + : _surface(s) + , _px(cairo_image_surface_get_data(s)) + , _x(0), _y(0) + , _w(cairo_image_surface_get_width(s)) + , _h(cairo_image_surface_get_height(s)) + , _stride(cairo_image_surface_get_stride(s)) + , _edge_mode(e) + , _alpha(cairo_image_surface_get_format(s) == CAIRO_FORMAT_A8) + {} + + guint32 pixelAt(int x, int y) { + // This is a lot of ifs for a single pixel access. However, branch prediction + // should help us a lot, as the result of ifs is always the same for a single image. + int real_x = x, real_y = y; + switch (_edge_mode) { + case EDGE_PAD: + real_x = CLAMP(x, 0, _w-1); + real_y = CLAMP(y, 0, _h-1); + break; + case EDGE_WRAP: + real_x %= _w; + real_y %= _h; + break; + case EDGE_ZERO: + default: + if (x < 0 || x >= _w || y < 0 || y >= _h) + return 0; + break; + } + if (_alpha) { + return *(_px + real_y*_stride + real_x) << 24; + } else { + guint32 *px = reinterpret_cast<guint32*>(_px +real_y*_stride + real_x*4); + return *px; + } + } +private: + cairo_surface_t *_surface; + guint8 *_px; + int _x, _y, _w, _h, _stride; + EdgeMode _edge_mode; + bool _alpha; +};*/ + +// Some helpers for pixel manipulation +G_GNUC_CONST inline gint32 +pxclamp(gint32 v, gint32 low, gint32 high) { + // NOTE: it is possible to write a "branchless" clamping operation. + // However, it will be slower than this function, because the code below + // is compiled to conditional moves. + if (v < low) return low; + if (v > high) return high; + return v; +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/cairo-utils.cpp b/src/display/cairo-utils.cpp new file mode 100644 index 0000000..f616af7 --- /dev/null +++ b/src/display/cairo-utils.cpp @@ -0,0 +1,1746 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Helper functions to use cairo with inkscape + * + * Copyright (C) 2007 bulia byak + * Copyright (C) 2008 Johan Engelen + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + */ + +#include "display/cairo-utils.h" + +#include <stdexcept> + +#include <glib/gstdio.h> +#include <glibmm/fileutils.h> +#include <gdk-pixbuf/gdk-pixbuf.h> + +#include <2geom/pathvector.h> +#include <2geom/curves.h> +#include <2geom/affine.h> +#include <2geom/point.h> +#include <2geom/path.h> +#include <2geom/transforms.h> +#include <2geom/sbasis-to-bezier.h> + +#include <boost/algorithm/string.hpp> +#include <boost/operators.hpp> +#include <boost/optional/optional.hpp> + +#include "color.h" +#include "cairo-templates.h" +#include "document.h" +#include "preferences.h" +#include "util/units.h" +#include "helper/pixbuf-ops.h" + + +/** + * Key for cairo_surface_t to keep track of current color interpolation value + * Only the address of the structure is used, it is never initialized. See: + * http://www.cairographics.org/manual/cairo-Types.html#cairo-user-data-key-t + */ +cairo_user_data_key_t ink_color_interpolation_key; +cairo_user_data_key_t ink_pixbuf_key; + +namespace Inkscape { + +CairoGroup::CairoGroup(cairo_t *_ct) : ct(_ct), pushed(false) {} +CairoGroup::~CairoGroup() { + if (pushed) { + cairo_pattern_t *p = cairo_pop_group(ct); + cairo_pattern_destroy(p); + } +} +void CairoGroup::push() { + cairo_push_group(ct); + pushed = true; +} +void CairoGroup::push_with_content(cairo_content_t content) { + cairo_push_group_with_content(ct, content); + pushed = true; +} +cairo_pattern_t *CairoGroup::pop() { + if (pushed) { + cairo_pattern_t *ret = cairo_pop_group(ct); + pushed = false; + return ret; + } else { + throw std::logic_error("Cairo group popped without pushing it first"); + } +} +Cairo::RefPtr<Cairo::Pattern> CairoGroup::popmm() { + if (pushed) { + cairo_pattern_t *ret = cairo_pop_group(ct); + Cairo::RefPtr<Cairo::Pattern> retmm(new Cairo::Pattern(ret, true)); + pushed = false; + return retmm; + } else { + throw std::logic_error("Cairo group popped without pushing it first"); + } +} +void CairoGroup::pop_to_source() { + if (pushed) { + cairo_pop_group_to_source(ct); + pushed = false; + } +} + +CairoContext::CairoContext(cairo_t *obj, bool ref) + : Cairo::Context(obj, ref) +{} + +void CairoContext::transform(Geom::Affine const &m) +{ + cairo_matrix_t cm; + cm.xx = m[0]; + cm.xy = m[2]; + cm.x0 = m[4]; + cm.yx = m[1]; + cm.yy = m[3]; + cm.y0 = m[5]; + cairo_transform(cobj(), &cm); +} + +void CairoContext::set_source_rgba32(guint32 color) +{ + double red = SP_RGBA32_R_F(color); + double gre = SP_RGBA32_G_F(color); + double blu = SP_RGBA32_B_F(color); + double alp = SP_RGBA32_A_F(color); + cairo_set_source_rgba(cobj(), red, gre, blu, alp); +} + +void CairoContext::append_path(Geom::PathVector const &pv) +{ + feed_pathvector_to_cairo(cobj(), pv); +} + +Cairo::RefPtr<CairoContext> CairoContext::create(Cairo::RefPtr<Cairo::Surface> const &target) +{ + cairo_t *ct = cairo_create(target->cobj()); + Cairo::RefPtr<CairoContext> ret(new CairoContext(ct, true)); + return ret; +} + + +/* The class below implement the following hack: + * + * The pixels formats of Cairo and GdkPixbuf are different. + * GdkPixbuf accesses pixels as bytes, alpha is not premultiplied, + * and successive bytes of a single pixel contain R, G, B and A components. + * Cairo accesses pixels as 32-bit ints, alpha is premultiplied, + * and each int contains as 0xAARRGGBB, accessed with bitwise operations. + * + * In other words, on a little endian system, a GdkPixbuf will contain: + * char *data = "rgbargbargba...." + * int *data = { 0xAABBGGRR, 0xAABBGGRR, 0xAABBGGRR, ... } + * while a Cairo image surface will contain: + * char *data = "bgrabgrabgra...." + * int *data = { 0xAARRGGBB, 0xAARRGGBB, 0xAARRGGBB, ... } + * + * It is possible to convert between these two formats (almost) losslessly. + * Some color information from partially transparent regions of the image + * is lost, but the result when displaying this image will remain the same. + * + * The class allows interoperation between GdkPixbuf + * and Cairo surfaces without creating a copy of the image. + * This is implemented by creating a GdkPixbuf and a Cairo image surface + * which share their data. Depending on what is needed at a given time, + * the pixels are converted in place to the Cairo or the GdkPixbuf format. + */ + +/** Create a pixbuf from a Cairo surface. + * The constructor takes ownership of the passed surface, + * so it should not be destroyed. */ +Pixbuf::Pixbuf(cairo_surface_t *s) + : _pixbuf(gdk_pixbuf_new_from_data( + cairo_image_surface_get_data(s), GDK_COLORSPACE_RGB, TRUE, 8, + cairo_image_surface_get_width(s), cairo_image_surface_get_height(s), + cairo_image_surface_get_stride(s), nullptr, nullptr)) + , _surface(s) + , _mod_time(0) + , _pixel_format(PF_CAIRO) + , _cairo_store(true) +{} + +/** Create a pixbuf from a GdkPixbuf. + * The constructor takes ownership of the passed GdkPixbuf reference, + * so it should not be unrefed. */ +Pixbuf::Pixbuf(GdkPixbuf *pb) + : _pixbuf(pb) + , _surface(nullptr) + , _mod_time(0) + , _pixel_format(PF_GDK) + , _cairo_store(false) +{ + _forceAlpha(); + _surface = cairo_image_surface_create_for_data( + gdk_pixbuf_get_pixels(_pixbuf), CAIRO_FORMAT_ARGB32, + gdk_pixbuf_get_width(_pixbuf), gdk_pixbuf_get_height(_pixbuf), gdk_pixbuf_get_rowstride(_pixbuf)); +} + +Pixbuf::Pixbuf(Inkscape::Pixbuf const &other) + : _pixbuf(gdk_pixbuf_copy(other._pixbuf)) + , _surface(cairo_image_surface_create_for_data( + gdk_pixbuf_get_pixels(_pixbuf), CAIRO_FORMAT_ARGB32, + gdk_pixbuf_get_width(_pixbuf), gdk_pixbuf_get_height(_pixbuf), gdk_pixbuf_get_rowstride(_pixbuf))) + , _mod_time(other._mod_time) + , _path(other._path) + , _pixel_format(other._pixel_format) + , _cairo_store(false) +{} + +Pixbuf::~Pixbuf() +{ + if (_cairo_store) { + g_object_unref(_pixbuf); + cairo_surface_destroy(_surface); + } else { + cairo_surface_destroy(_surface); + g_object_unref(_pixbuf); + } +} + +#if !GDK_PIXBUF_CHECK_VERSION(2, 41, 0) +/** + * Incremental file read introduced to workaround + * https://gitlab.gnome.org/GNOME/gdk-pixbuf/issues/70 + */ +static bool _workaround_issue_70__gdk_pixbuf_loader_write( // + GdkPixbufLoader *loader, guchar *decoded, gsize decoded_len, GError **error) +{ + bool success = true; + gsize bytes_left = decoded_len; + gsize secret_limit = 0xffff; + guchar *decoded_head = decoded; + while (bytes_left && success) { + gsize bytes = (bytes_left > secret_limit) ? secret_limit : bytes_left; + success = gdk_pixbuf_loader_write(loader, decoded_head, bytes, error); + decoded_head += bytes; + bytes_left -= bytes; + } + + return success; +} +#define gdk_pixbuf_loader_write _workaround_issue_70__gdk_pixbuf_loader_write +#endif + +Pixbuf *Pixbuf::create_from_data_uri(gchar const *uri_data, double svgdpi) +{ + Pixbuf *pixbuf = nullptr; + + bool data_is_image = false; + bool data_is_svg = false; + bool data_is_base64 = false; + + gchar const *data = uri_data; + + while (*data) { + if (strncmp(data,"base64",6) == 0) { + /* base64-encoding */ + data_is_base64 = true; + data_is_image = true; // Illustrator produces embedded images without MIME type, so we assume it's image no matter what + data += 6; + } + else if (strncmp(data,"image/png",9) == 0) { + /* PNG image */ + data_is_image = true; + data += 9; + } + else if (strncmp(data,"image/jpg",9) == 0) { + /* JPEG image */ + data_is_image = true; + data += 9; + } + else if (strncmp(data,"image/jpeg",10) == 0) { + /* JPEG image */ + data_is_image = true; + data += 10; + } + else if (strncmp(data,"image/jp2",9) == 0) { + /* JPEG2000 image */ + data_is_image = true; + data += 9; + } + else if (strncmp(data,"image/svg+xml",13) == 0) { + /* JPEG2000 image */ + data_is_svg = true; + data_is_image = true; + data += 13; + } + else { /* unrecognized option; skip it */ + while (*data) { + if (((*data) == ';') || ((*data) == ',')) { + break; + } + data++; + } + } + if ((*data) == ';') { + data++; + continue; + } + if ((*data) == ',') { + data++; + break; + } + } + + if ((*data) && data_is_image && !data_is_svg && data_is_base64) { + GdkPixbufLoader *loader = gdk_pixbuf_loader_new(); + + if (!loader) return nullptr; + + gsize decoded_len = 0; + guchar *decoded = g_base64_decode(data, &decoded_len); + + if (gdk_pixbuf_loader_write(loader, decoded, decoded_len, nullptr)) { + gdk_pixbuf_loader_close(loader, nullptr); + GdkPixbuf *buf = gdk_pixbuf_loader_get_pixbuf(loader); + if (buf) { + g_object_ref(buf); + pixbuf = new Pixbuf(buf); + + GdkPixbufFormat *fmt = gdk_pixbuf_loader_get_format(loader); + gchar *fmt_name = gdk_pixbuf_format_get_name(fmt); + pixbuf->_setMimeData(decoded, decoded_len, fmt_name); + g_free(fmt_name); + } else { + g_free(decoded); + } + } else { + g_free(decoded); + } + g_object_unref(loader); + } + + if ((*data) && data_is_image && data_is_svg && data_is_base64) { + gsize decoded_len = 0; + guchar *decoded = g_base64_decode(data, &decoded_len); + SPDocument *svgDoc = SPDocument::createNewDocFromMem (reinterpret_cast<gchar const *>(decoded), decoded_len, false); + // Check the document loaded properly + if (svgDoc == nullptr) { + return nullptr; + } + if (svgDoc->getRoot() == nullptr) + { + svgDoc->doUnref(); + return nullptr; + } + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double dpi = prefs->getDouble("/dialogs/import/defaultxdpi/value", 96.0); + if (svgdpi && svgdpi > 0) { + dpi = svgdpi; + } + std::cout << dpi << "dpi" << std::endl; + // Get the size of the document + Inkscape::Util::Quantity svgWidth = svgDoc->getWidth(); + Inkscape::Util::Quantity svgHeight = svgDoc->getHeight(); + const double svgWidth_px = svgWidth.value("px"); + const double svgHeight_px = svgHeight.value("px"); + + // Now get the resized values + const int scaledSvgWidth = round(svgWidth_px/(96.0/dpi)); + const int scaledSvgHeight = round(svgHeight_px/(96.0/dpi)); + + GdkPixbuf *buf = sp_generate_internal_bitmap(svgDoc, nullptr, 0, 0, svgWidth_px, svgHeight_px, scaledSvgWidth, scaledSvgHeight, dpi, dpi, (guint32) 0xffffff00, nullptr)->getPixbufRaw(); + // Tidy up + svgDoc->doUnref(); + if (buf == nullptr) { + std::cerr << "Pixbuf::create_from_data: failed to load contents: " << std::endl; + g_free(decoded); + return nullptr; + } else { + g_object_ref(buf); + pixbuf = new Pixbuf(buf); + pixbuf->_setMimeData(decoded, decoded_len, "svg+xml"); + } + } + + return pixbuf; +} + +Pixbuf *Pixbuf::create_from_file(std::string const &fn, double svgdpi) +{ + Pixbuf *pb = nullptr; + // test correctness of filename + if (!g_file_test(fn.c_str(), G_FILE_TEST_EXISTS)) { + return nullptr; + } + GStatBuf stdir; + int val = g_stat(fn.c_str(), &stdir); + if (val == 0 && stdir.st_mode & S_IFDIR){ + return nullptr; + } + // we need to load the entire file into memory, + // since we'll store it as MIME data + gchar *data = nullptr; + gsize len = 0; + GError *error = nullptr; + + if (g_file_get_contents(fn.c_str(), &data, &len, &error)) { + + if (error != nullptr) { + std::cerr << "Pixbuf::create_from_file: " << error->message << std::endl; + std::cerr << " (" << fn << ")" << std::endl; + return nullptr; + } + + pb = Pixbuf::create_from_buffer(std::move(data), len, svgdpi, fn); + + if (pb) { + pb->_mod_time = stdir.st_mtime; + } + } else { + std::cerr << "Pixbuf::create_from_file: failed to get contents: " << fn << std::endl; + return nullptr; + } + + return pb; +} + +Pixbuf *Pixbuf::create_from_buffer(std::string const &buffer, double svgdpi, std::string const &fn) +{ + auto datacopy = (gchar *)g_memdup(buffer.data(), buffer.size()); + return Pixbuf::create_from_buffer(std::move(datacopy), buffer.size(), svgdpi, fn); +} + +Pixbuf *Pixbuf::create_from_buffer(gchar *&&data, gsize len, double svgdpi, std::string const &fn) +{ + Pixbuf *pb = nullptr; + GError *error = nullptr; + { + GdkPixbuf *buf = nullptr; + GdkPixbufLoader *loader = nullptr; + std::string::size_type idx; + idx = fn.rfind('.'); + bool is_svg = false; + if(idx != std::string::npos) + { + if (boost::iequals(fn.substr(idx+1).c_str(), "svg")) { + + SPDocument *svgDoc = SPDocument::createNewDocFromMem (data, len, true); + + // Check the document loaded properly + if (svgDoc == nullptr) { + return nullptr; + } + if (svgDoc->getRoot() == nullptr) + { + svgDoc->doUnref(); + return nullptr; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double dpi = prefs->getDouble("/dialogs/import/defaultxdpi/value", 96.0); + if (svgdpi && svgdpi > 0) { + dpi = svgdpi; + } + + // Get the size of the document + Inkscape::Util::Quantity svgWidth = svgDoc->getWidth(); + Inkscape::Util::Quantity svgHeight = svgDoc->getHeight(); + const double svgWidth_px = svgWidth.value("px"); + const double svgHeight_px = svgHeight.value("px"); + + // Now get the resized values + const int scaledSvgWidth = round(svgWidth_px/(96.0/dpi)); + const int scaledSvgHeight = round(svgHeight_px/(96.0/dpi)); + + buf = sp_generate_internal_bitmap(svgDoc, nullptr, 0, 0, svgWidth_px, svgHeight_px, scaledSvgWidth, scaledSvgHeight, dpi, dpi, (guint32) 0xffffff00, nullptr)->getPixbufRaw(); + + // Tidy up + svgDoc->doUnref(); + if (buf == nullptr) { + return nullptr; + } + is_svg = true; + } + } + if (!is_svg) { + loader = gdk_pixbuf_loader_new(); + gdk_pixbuf_loader_write(loader, (guchar *) data, len, &error); + if (error != nullptr) { + std::cerr << "Pixbuf::create_from_file: " << error->message << std::endl; + std::cerr << " (" << fn << ")" << std::endl; + g_free(data); + g_object_unref(loader); + return nullptr; + } + + gdk_pixbuf_loader_close(loader, &error); + if (error != nullptr) { + std::cerr << "Pixbuf::create_from_file: " << error->message << std::endl; + std::cerr << " (" << fn << ")" << std::endl; + g_free(data); + g_object_unref(loader); + return nullptr; + } + + buf = gdk_pixbuf_loader_get_pixbuf(loader); + } + + if (buf) { + g_object_ref(buf); + pb = new Pixbuf(buf); + pb->_path = fn; + if (!is_svg) { + GdkPixbufFormat *fmt = gdk_pixbuf_loader_get_format(loader); + gchar *fmt_name = gdk_pixbuf_format_get_name(fmt); + pb->_setMimeData((guchar *) data, len, fmt_name); + g_free(fmt_name); + g_object_unref(loader); + } else { + pb->_setMimeData((guchar *) data, len, "svg"); + } + } else { + std::cerr << "Pixbuf::create_from_file: failed to load contents: " << fn << std::endl; + g_free(data); + } + + // TODO: we could also read DPI, ICC profile, gamma correction, and other information + // from the file. This can be done by using format-specific libraries e.g. libpng. + } + + return pb; +} + +/** + * Converts the pixbuf to GdkPixbuf pixel format. + * The returned pixbuf can be used e.g. in calls to gdk_pixbuf_save(). + */ +GdkPixbuf *Pixbuf::getPixbufRaw(bool convert_format) +{ + if (convert_format) { + ensurePixelFormat(PF_GDK); + } + return _pixbuf; +} + +/** + * Converts the pixbuf to Cairo pixel format and returns an image surface + * which can be used as a source. + * + * The returned surface is owned by the GdkPixbuf and should not be freed. + * Calling this function causes the pixbuf to be unsuitable for use + * with GTK drawing functions until ensurePixelFormat(Pixbuf::PIXEL_FORMAT_PIXBUF) is called. + */ +cairo_surface_t *Pixbuf::getSurfaceRaw(bool convert_format) +{ + if (convert_format) { + ensurePixelFormat(PF_CAIRO); + } + return _surface; +} + +/* Declaring this function in the header requires including <gdkmm/pixbuf.h>, + * which stupidly includes <glibmm.h> which in turn pulls in <glibmm/threads.h>. + * However, since glib 2.32, <glibmm/threads.h> has to be included before <glib.h> + * when compiling with G_DISABLE_DEPRECATED, as we do in non-release builds. + * This necessitates spamming a lot of files with #include <glibmm/threads.h> + * at the top. + * + * Since we don't really use gdkmm, do not define this function for now. */ + +/* +Glib::RefPtr<Gdk::Pixbuf> Pixbuf::getPixbuf(bool convert_format = true) +{ + g_object_ref(_pixbuf); + Glib::RefPtr<Gdk::Pixbuf> p(getPixbuf(convert_format)); + return p; +} +*/ + +Cairo::RefPtr<Cairo::Surface> Pixbuf::getSurface(bool convert_format) +{ + Cairo::RefPtr<Cairo::Surface> p(new Cairo::Surface(getSurfaceRaw(convert_format), false)); + return p; +} + +/** Retrieves the original compressed data for the surface, if any. + * The returned data belongs to the object and should not be freed. */ +guchar const *Pixbuf::getMimeData(gsize &len, std::string &mimetype) const +{ + static gchar const *mimetypes[] = { + CAIRO_MIME_TYPE_JPEG, CAIRO_MIME_TYPE_JP2, CAIRO_MIME_TYPE_PNG, nullptr }; + static guint mimetypes_len = g_strv_length(const_cast<gchar**>(mimetypes)); + + guchar const *data = nullptr; + + for (guint i = 0; i < mimetypes_len; ++i) { + unsigned long len_long = 0; + cairo_surface_get_mime_data(const_cast<cairo_surface_t*>(_surface), mimetypes[i], &data, &len_long); + if (data != nullptr) { + len = len_long; + mimetype = mimetypes[i]; + break; + } + } + + return data; +} + +int Pixbuf::width() const { + return gdk_pixbuf_get_width(const_cast<GdkPixbuf*>(_pixbuf)); +} +int Pixbuf::height() const { + return gdk_pixbuf_get_height(const_cast<GdkPixbuf*>(_pixbuf)); +} +int Pixbuf::rowstride() const { + return gdk_pixbuf_get_rowstride(const_cast<GdkPixbuf*>(_pixbuf)); +} +guchar const *Pixbuf::pixels() const { + return gdk_pixbuf_get_pixels(const_cast<GdkPixbuf*>(_pixbuf)); +} +guchar *Pixbuf::pixels() { + return gdk_pixbuf_get_pixels(_pixbuf); +} +void Pixbuf::markDirty() { + cairo_surface_mark_dirty(_surface); +} + +void Pixbuf::_forceAlpha() +{ + if (gdk_pixbuf_get_has_alpha(_pixbuf)) return; + + GdkPixbuf *old = _pixbuf; + _pixbuf = gdk_pixbuf_add_alpha(old, FALSE, 0, 0, 0); + g_object_unref(old); +} + +void Pixbuf::_setMimeData(guchar *data, gsize len, Glib::ustring const &format) +{ + gchar const *mimetype = nullptr; + + if (format == "jpeg") { + mimetype = CAIRO_MIME_TYPE_JPEG; + } else if (format == "jpeg2000") { + mimetype = CAIRO_MIME_TYPE_JP2; + } else if (format == "png") { + mimetype = CAIRO_MIME_TYPE_PNG; + } + + if (mimetype != nullptr) { + cairo_surface_set_mime_data(_surface, mimetype, data, len, g_free, data); + //g_message("Setting Cairo MIME data: %s", mimetype); + } else { + g_free(data); + //g_message("Not setting Cairo MIME data: unknown format %s", name.c_str()); + } +} + +void Pixbuf::ensurePixelFormat(PixelFormat fmt) +{ + if (_pixel_format == PF_GDK) { + if (fmt == PF_GDK) { + return; + } + if (fmt == PF_CAIRO) { + convert_pixels_pixbuf_to_argb32( + gdk_pixbuf_get_pixels(_pixbuf), + gdk_pixbuf_get_width(_pixbuf), + gdk_pixbuf_get_height(_pixbuf), + gdk_pixbuf_get_rowstride(_pixbuf)); + _pixel_format = fmt; + return; + } + g_assert_not_reached(); + } + if (_pixel_format == PF_CAIRO) { + if (fmt == PF_GDK) { + convert_pixels_argb32_to_pixbuf( + gdk_pixbuf_get_pixels(_pixbuf), + gdk_pixbuf_get_width(_pixbuf), + gdk_pixbuf_get_height(_pixbuf), + gdk_pixbuf_get_rowstride(_pixbuf)); + _pixel_format = fmt; + return; + } + if (fmt == PF_CAIRO) { + return; + } + g_assert_not_reached(); + } + g_assert_not_reached(); +} + +} // namespace Inkscape + +/* + * Can be called recursively. + * If optimize_stroke == false, the view Rect is not used. + */ +static void +feed_curve_to_cairo(cairo_t *cr, Geom::Curve const &c, Geom::Affine const & trans, Geom::Rect view, bool optimize_stroke) +{ + using Geom::X; + using Geom::Y; + + unsigned order = 0; + if (Geom::BezierCurve const* b = dynamic_cast<Geom::BezierCurve const*>(&c)) { + order = b->order(); + } + + // handle the three typical curve cases + switch (order) { + case 1: + { + Geom::Point end_tr = c.finalPoint() * trans; + if (!optimize_stroke) { + cairo_line_to(cr, end_tr[0], end_tr[1]); + } else { + Geom::Rect swept(c.initialPoint()*trans, end_tr); + if (swept.intersects(view)) { + cairo_line_to(cr, end_tr[0], end_tr[1]); + } else { + cairo_move_to(cr, end_tr[0], end_tr[1]); + } + } + } + break; + case 2: + { + Geom::QuadraticBezier const *quadratic_bezier = static_cast<Geom::QuadraticBezier const*>(&c); + std::vector<Geom::Point> points = quadratic_bezier->controlPoints(); + points[0] *= trans; + points[1] *= trans; + points[2] *= trans; + // degree-elevate to cubic Bezier, since Cairo doesn't do quadratic Beziers + Geom::Point b1 = points[0] + (2./3) * (points[1] - points[0]); + Geom::Point b2 = b1 + (1./3) * (points[2] - points[0]); + if (!optimize_stroke) { + cairo_curve_to(cr, b1[X], b1[Y], b2[X], b2[Y], points[2][X], points[2][Y]); + } else { + Geom::Rect swept(points[0], points[2]); + swept.expandTo(points[1]); + if (swept.intersects(view)) { + cairo_curve_to(cr, b1[X], b1[Y], b2[X], b2[Y], points[2][X], points[2][Y]); + } else { + cairo_move_to(cr, points[2][X], points[2][Y]); + } + } + } + break; + case 3: + { + Geom::CubicBezier const *cubic_bezier = static_cast<Geom::CubicBezier const*>(&c); + std::vector<Geom::Point> points = cubic_bezier->controlPoints(); + //points[0] *= trans; // don't do this one here for fun: it is only needed for optimized strokes + points[1] *= trans; + points[2] *= trans; + points[3] *= trans; + if (!optimize_stroke) { + cairo_curve_to(cr, points[1][X], points[1][Y], points[2][X], points[2][Y], points[3][X], points[3][Y]); + } else { + points[0] *= trans; // didn't transform this point yet + Geom::Rect swept(points[0], points[3]); + swept.expandTo(points[1]); + swept.expandTo(points[2]); + if (swept.intersects(view)) { + cairo_curve_to(cr, points[1][X], points[1][Y], points[2][X], points[2][Y], points[3][X], points[3][Y]); + } else { + cairo_move_to(cr, points[3][X], points[3][Y]); + } + } + } + break; + default: + { + if (Geom::EllipticalArc const *a = dynamic_cast<Geom::EllipticalArc const*>(&c)) { + if (a->isChord()) { + Geom::Point endPoint(a->finalPoint()); + cairo_line_to(cr, endPoint[0], endPoint[1]); + } else { + Geom::Affine xform = a->unitCircleTransform() * trans; + Geom::Point ang(a->initialAngle().radians(), a->finalAngle().radians()); + // Apply the transformation to the current context + cairo_matrix_t cm; + cm.xx = xform[0]; + cm.xy = xform[2]; + cm.x0 = xform[4]; + cm.yx = xform[1]; + cm.yy = xform[3]; + cm.y0 = xform[5]; + + cairo_save(cr); + cairo_transform(cr, &cm); + + // Draw the circle + if (a->sweep()) { + cairo_arc(cr, 0, 0, 1, ang[0], ang[1]); + } else { + cairo_arc_negative(cr, 0, 0, 1, ang[0], ang[1]); + } + // Revert the current context + cairo_restore(cr); + } + } else { + // handles sbasis as well as all other curve types + // this is very slow + Geom::Path sbasis_path = Geom::cubicbezierpath_from_sbasis(c.toSBasis(), 0.1); + + // recurse to convert the new path resulting from the sbasis to svgd + for (const auto & iter : sbasis_path) { + feed_curve_to_cairo(cr, iter, trans, view, optimize_stroke); + } + } + } + break; + } +} + + +/** Feeds path-creating calls to the cairo context translating them from the Path */ +static void +feed_path_to_cairo (cairo_t *ct, Geom::Path const &path) +{ + if (path.empty()) + return; + + cairo_move_to(ct, path.initialPoint()[0], path.initialPoint()[1] ); + + for (Geom::Path::const_iterator cit = path.begin(); cit != path.end_open(); ++cit) { + feed_curve_to_cairo(ct, *cit, Geom::identity(), Geom::Rect(), false); // optimize_stroke is false, so the view rect is not used + } + + if (path.closed()) { + cairo_close_path(ct); + } +} + +/** Feeds path-creating calls to the cairo context translating them from the Path, with the given transform and shift */ +static void +feed_path_to_cairo (cairo_t *ct, Geom::Path const &path, Geom::Affine trans, Geom::OptRect area, bool optimize_stroke, double stroke_width) +{ + if (!area) + return; + if (path.empty()) + return; + + // Transform all coordinates to coords within "area" + Geom::Point shift = area->min(); + Geom::Rect view = *area; + view.expandBy (stroke_width); + view = view * (Geom::Affine)Geom::Translate(-shift); + // Pass transformation to feed_curve, so that we don't need to create a whole new path. + Geom::Affine transshift(trans * Geom::Translate(-shift)); + + Geom::Point initial = path.initialPoint() * transshift; + cairo_move_to(ct, initial[0], initial[1] ); + + for(Geom::Path::const_iterator cit = path.begin(); cit != path.end_open(); ++cit) { + feed_curve_to_cairo(ct, *cit, transshift, view, optimize_stroke); + } + + if (path.closed()) { + if (!optimize_stroke) { + cairo_close_path(ct); + } else { + cairo_line_to(ct, initial[0], initial[1]); + /* We cannot use cairo_close_path(ct) here because some parts of the path may have been + clipped and not drawn (maybe the before last segment was outside view area), which + would result in closing the "subpath" after the last interruption, not the entire path. + + However, according to cairo documentation: + The behavior of cairo_close_path() is distinct from simply calling cairo_line_to() with the equivalent coordinate + in the case of stroking. When a closed sub-path is stroked, there are no caps on the ends of the sub-path. Instead, + there is a line join connecting the final and initial segments of the sub-path. + + The correct fix will be possible when cairo introduces methods for moving without + ending/starting subpaths, which we will use for skipping invisible segments; then we + will be able to use cairo_close_path here. This issue also affects ps/eps/pdf export, + see bug 168129 + */ + } + } +} + +/** Feeds path-creating calls to the cairo context translating them from the PathVector, with the given transform and shift + * One must have done cairo_new_path(ct); before calling this function. */ +void +feed_pathvector_to_cairo (cairo_t *ct, Geom::PathVector const &pathv, Geom::Affine trans, Geom::OptRect area, bool optimize_stroke, double stroke_width) +{ + if (!area) + return; + if (pathv.empty()) + return; + + for(const auto & it : pathv) { + feed_path_to_cairo(ct, it, trans, area, optimize_stroke, stroke_width); + } +} + +/** Feeds path-creating calls to the cairo context translating them from the PathVector + * One must have done cairo_new_path(ct); before calling this function. */ +void +feed_pathvector_to_cairo (cairo_t *ct, Geom::PathVector const &pathv) +{ + if (pathv.empty()) + return; + + for(const auto & it : pathv) { + feed_path_to_cairo(ct, it); + } +} + +SPColorInterpolation +get_cairo_surface_ci(cairo_surface_t *surface) { + void* data = cairo_surface_get_user_data( surface, &ink_color_interpolation_key ); + if( data != nullptr ) { + return (SPColorInterpolation)GPOINTER_TO_INT( data ); + } else { + return SP_CSS_COLOR_INTERPOLATION_AUTO; + } +} + +/** Set the color_interpolation_value for a Cairo surface. + * Transform the surface between sRGB and linearRGB if necessary. */ +void +set_cairo_surface_ci(cairo_surface_t *surface, SPColorInterpolation ci) { + + if( cairo_surface_get_content( surface ) != CAIRO_CONTENT_ALPHA ) { + + SPColorInterpolation ci_in = get_cairo_surface_ci( surface ); + + if( ci_in == SP_CSS_COLOR_INTERPOLATION_SRGB && + ci == SP_CSS_COLOR_INTERPOLATION_LINEARRGB ) { + ink_cairo_surface_srgb_to_linear( surface ); + } + if( ci_in == SP_CSS_COLOR_INTERPOLATION_LINEARRGB && + ci == SP_CSS_COLOR_INTERPOLATION_SRGB ) { + ink_cairo_surface_linear_to_srgb( surface ); + } + + cairo_surface_set_user_data(surface, &ink_color_interpolation_key, GINT_TO_POINTER (ci), nullptr); + } +} + +void +copy_cairo_surface_ci(cairo_surface_t *in, cairo_surface_t *out) { + cairo_surface_set_user_data(out, &ink_color_interpolation_key, cairo_surface_get_user_data(in, &ink_color_interpolation_key), nullptr); +} + +void +ink_cairo_set_source_rgba32(cairo_t *ct, guint32 rgba) +{ + cairo_set_source_rgba(ct, SP_RGBA32_R_F(rgba), SP_RGBA32_G_F(rgba), SP_RGBA32_B_F(rgba), SP_RGBA32_A_F(rgba)); +} + +void +ink_cairo_set_source_color(cairo_t *ct, SPColor const &c, double opacity) +{ + cairo_set_source_rgba(ct, c.v.c[0], c.v.c[1], c.v.c[2], opacity); +} + +void ink_matrix_to_2geom(Geom::Affine &m, cairo_matrix_t const &cm) +{ + m[0] = cm.xx; + m[2] = cm.xy; + m[4] = cm.x0; + m[1] = cm.yx; + m[3] = cm.yy; + m[5] = cm.y0; +} + +void ink_matrix_to_cairo(cairo_matrix_t &cm, Geom::Affine const &m) +{ + cm.xx = m[0]; + cm.xy = m[2]; + cm.x0 = m[4]; + cm.yx = m[1]; + cm.yy = m[3]; + cm.y0 = m[5]; +} + +void +ink_cairo_transform(cairo_t *ct, Geom::Affine const &m) +{ + cairo_matrix_t cm; + ink_matrix_to_cairo(cm, m); + cairo_transform(ct, &cm); +} + +void +ink_cairo_pattern_set_matrix(cairo_pattern_t *cp, Geom::Affine const &m) +{ + cairo_matrix_t cm; + ink_matrix_to_cairo(cm, m); + cairo_pattern_set_matrix(cp, &cm); +} + +/** + * Create an exact copy of a surface. + * Creates a surface that has the same type, content type, dimensions and contents + * as the specified surface. + */ +cairo_surface_t * +ink_cairo_surface_copy(cairo_surface_t *s) +{ + cairo_surface_t *ns = ink_cairo_surface_create_identical(s); + + if (cairo_surface_get_type(s) == CAIRO_SURFACE_TYPE_IMAGE) { + // use memory copy instead of using a Cairo context + cairo_surface_flush(s); + int stride = cairo_image_surface_get_stride(s); + int h = cairo_image_surface_get_height(s); + memcpy(cairo_image_surface_get_data(ns), cairo_image_surface_get_data(s), stride * h); + cairo_surface_mark_dirty(ns); + } else { + // generic implementation + cairo_t *ct = cairo_create(ns); + cairo_set_source_surface(ct, s, 0, 0); + cairo_set_operator(ct, CAIRO_OPERATOR_SOURCE); + cairo_paint(ct); + cairo_destroy(ct); + } + + return ns; +} + +/** + * Create a surface that differs only in pixel content. + * Creates a surface that has the same type, content type and dimensions + * as the specified surface. Pixel contents are not copied. + */ +cairo_surface_t * +ink_cairo_surface_create_identical(cairo_surface_t *s) +{ + cairo_surface_t *ns = ink_cairo_surface_create_same_size(s, cairo_surface_get_content(s)); + cairo_surface_set_user_data(ns, &ink_color_interpolation_key, cairo_surface_get_user_data(s, &ink_color_interpolation_key), nullptr); + return ns; +} + +cairo_surface_t * +ink_cairo_surface_create_same_size(cairo_surface_t *s, cairo_content_t c) +{ + // ink_cairo_surface_get_width()/height() returns value in pixels + // cairo_surface_create_similar() uses device units + double x_scale = 0; + double y_scale = 0; + cairo_surface_get_device_scale( s, &x_scale, &y_scale ); + + assert (x_scale > 0); + assert (y_scale > 0); + + cairo_surface_t *ns = + cairo_surface_create_similar(s, c, + ink_cairo_surface_get_width(s)/x_scale, + ink_cairo_surface_get_height(s)/y_scale); + return ns; +} + +/** + * Extract the alpha channel into a new surface. + * Creates a surface with a content type of CAIRO_CONTENT_ALPHA that contains + * the alpha values of pixels from @a s. + */ +cairo_surface_t * +ink_cairo_extract_alpha(cairo_surface_t *s) +{ + cairo_surface_t *alpha = ink_cairo_surface_create_same_size(s, CAIRO_CONTENT_ALPHA); + + cairo_t *ct = cairo_create(alpha); + cairo_set_source_surface(ct, s, 0, 0); + cairo_set_operator(ct, CAIRO_OPERATOR_SOURCE); + cairo_paint(ct); + cairo_destroy(ct); + + return alpha; +} + +cairo_surface_t * +ink_cairo_surface_create_output(cairo_surface_t *image, cairo_surface_t *bg) +{ + cairo_content_t imgt = cairo_surface_get_content(image); + cairo_content_t bgt = cairo_surface_get_content(bg); + cairo_surface_t *out = nullptr; + + if (bgt == CAIRO_CONTENT_ALPHA && imgt == CAIRO_CONTENT_ALPHA) { + out = ink_cairo_surface_create_identical(bg); + } else { + out = ink_cairo_surface_create_same_size(bg, CAIRO_CONTENT_COLOR_ALPHA); + } + + return out; +} + +void +ink_cairo_surface_blit(cairo_surface_t *src, cairo_surface_t *dest) +{ + if (cairo_surface_get_type(src) == CAIRO_SURFACE_TYPE_IMAGE && + cairo_surface_get_type(dest) == CAIRO_SURFACE_TYPE_IMAGE && + cairo_image_surface_get_format(src) == cairo_image_surface_get_format(dest) && + cairo_image_surface_get_height(src) == cairo_image_surface_get_height(dest) && + cairo_image_surface_get_width(src) == cairo_image_surface_get_width(dest) && + cairo_image_surface_get_stride(src) == cairo_image_surface_get_stride(dest)) + { + // use memory copy instead of using a Cairo context + cairo_surface_flush(src); + int stride = cairo_image_surface_get_stride(src); + int h = cairo_image_surface_get_height(src); + memcpy(cairo_image_surface_get_data(dest), cairo_image_surface_get_data(src), stride * h); + cairo_surface_mark_dirty(dest); + } else { + // generic implementation + cairo_t *ct = cairo_create(dest); + cairo_set_source_surface(ct, src, 0, 0); + cairo_set_operator(ct, CAIRO_OPERATOR_SOURCE); + cairo_paint(ct); + cairo_destroy(ct); + } +} + +/** + * Return width in pixels. + */ +int +ink_cairo_surface_get_width(cairo_surface_t *surface) +{ + // For now only image surface is handled. + // Later add others, e.g. cairo-gl + assert(cairo_surface_get_type(surface) == CAIRO_SURFACE_TYPE_IMAGE); + return cairo_image_surface_get_width(surface); +} + +/** + * Return height in pixels. + */ +int +ink_cairo_surface_get_height(cairo_surface_t *surface) +{ + assert(cairo_surface_get_type(surface) == CAIRO_SURFACE_TYPE_IMAGE); + return cairo_image_surface_get_height(surface); +} + +static int ink_cairo_surface_average_color_internal(cairo_surface_t *surface, double &rf, double &gf, double &bf, double &af) +{ + rf = gf = bf = af = 0.0; + cairo_surface_flush(surface); + int width = cairo_image_surface_get_width(surface); + int height = cairo_image_surface_get_height(surface); + int stride = cairo_image_surface_get_stride(surface); + unsigned char *data = cairo_image_surface_get_data(surface); + + /* TODO convert this to OpenMP somehow */ + for (int y = 0; y < height; ++y, data += stride) { + for (int x = 0; x < width; ++x) { + guint32 px = *reinterpret_cast<guint32*>(data + 4*x); + EXTRACT_ARGB32(px, a,r,g,b) + rf += r / 255.0; + gf += g / 255.0; + bf += b / 255.0; + af += a / 255.0; + } + } + return width * height; +} + +guint32 ink_cairo_surface_average_color(cairo_surface_t *surface) +{ + double rf,gf,bf,af; + ink_cairo_surface_average_color_premul(surface, rf,gf,bf,af); + guint32 r = round(rf * 255); + guint32 g = round(gf * 255); + guint32 b = round(bf * 255); + guint32 a = round(af * 255); + ASSEMBLE_ARGB32(px, a,r,g,b); + return px; +} + +void ink_cairo_surface_average_color(cairo_surface_t *surface, double &r, double &g, double &b, double &a) +{ + int count = ink_cairo_surface_average_color_internal(surface, r,g,b,a); + + r /= a; + g /= a; + b /= a; + a /= count; + + r = CLAMP(r, 0.0, 1.0); + g = CLAMP(g, 0.0, 1.0); + b = CLAMP(b, 0.0, 1.0); + a = CLAMP(a, 0.0, 1.0); +} + +void ink_cairo_surface_average_color_premul(cairo_surface_t *surface, double &r, double &g, double &b, double &a) +{ + int count = ink_cairo_surface_average_color_internal(surface, r,g,b,a); + + r /= count; + g /= count; + b /= count; + a /= count; + + r = CLAMP(r, 0.0, 1.0); + g = CLAMP(g, 0.0, 1.0); + b = CLAMP(b, 0.0, 1.0); + a = CLAMP(a, 0.0, 1.0); +} + +static guint32 srgb_to_linear( const guint32 c, const guint32 a ) { + + const guint32 c1 = unpremul_alpha( c, a ); + + double cc = c1/255.0; + + if( cc < 0.04045 ) { + cc /= 12.92; + } else { + cc = pow( (cc+0.055)/1.055, 2.4 ); + } + cc *= 255.0; + + const guint32 c2 = (int)cc; + + return premul_alpha( c2, a ); +} + +static guint32 linear_to_srgb( const guint32 c, const guint32 a ) { + + const guint32 c1 = unpremul_alpha( c, a ); + + double cc = c1/255.0; + + if( cc < 0.0031308 ) { + cc *= 12.92; + } else { + cc = pow( cc, 1.0/2.4 )*1.055-0.055; + } + cc *= 255.0; + + const guint32 c2 = (int)cc; + + return premul_alpha( c2, a ); +} + +struct SurfaceSrgbToLinear { + + guint32 operator()(guint32 in) { + EXTRACT_ARGB32(in, a,r,g,b) ; // Unneeded semi-colon for indenting + if( a != 0 ) { + r = srgb_to_linear( r, a ); + g = srgb_to_linear( g, a ); + b = srgb_to_linear( b, a ); + } + ASSEMBLE_ARGB32(out, a,r,g,b); + return out; + } +private: + /* None */ +}; + +int ink_cairo_surface_srgb_to_linear(cairo_surface_t *surface) +{ + cairo_surface_flush(surface); + int width = cairo_image_surface_get_width(surface); + int height = cairo_image_surface_get_height(surface); + // int stride = cairo_image_surface_get_stride(surface); + // unsigned char *data = cairo_image_surface_get_data(surface); + + ink_cairo_surface_filter( surface, surface, SurfaceSrgbToLinear() ); + + /* TODO convert this to OpenMP somehow */ + // for (int y = 0; y < height; ++y, data += stride) { + // for (int x = 0; x < width; ++x) { + // guint32 px = *reinterpret_cast<guint32*>(data + 4*x); + // EXTRACT_ARGB32(px, a,r,g,b) ; // Unneeded semi-colon for indenting + // if( a != 0 ) { + // r = srgb_to_linear( r, a ); + // g = srgb_to_linear( g, a ); + // b = srgb_to_linear( b, a ); + // } + // ASSEMBLE_ARGB32(px2, a,r,g,b); + // *reinterpret_cast<guint32*>(data + 4*x) = px2; + // } + // } + return width * height; +} + +struct SurfaceLinearToSrgb { + + guint32 operator()(guint32 in) { + EXTRACT_ARGB32(in, a,r,g,b) ; // Unneeded semi-colon for indenting + if( a != 0 ) { + r = linear_to_srgb( r, a ); + g = linear_to_srgb( g, a ); + b = linear_to_srgb( b, a ); + } + ASSEMBLE_ARGB32(out, a,r,g,b); + return out; + } +private: + /* None */ +}; + +SPBlendMode ink_cairo_operator_to_css_blend(cairo_operator_t cairo_operator) +{ + // All of the blend modes are implemented in Cairo as of 1.10. + // For a detailed description, see: + // http://cairographics.org/operators/ + auto res = SP_CSS_BLEND_NORMAL; + switch (cairo_operator) { + case CAIRO_OPERATOR_MULTIPLY: + res = SP_CSS_BLEND_MULTIPLY; + break; + case CAIRO_OPERATOR_SCREEN: + res = SP_CSS_BLEND_SCREEN; + break; + case CAIRO_OPERATOR_DARKEN: + res = SP_CSS_BLEND_DARKEN; + break; + case CAIRO_OPERATOR_LIGHTEN: + res = SP_CSS_BLEND_LIGHTEN; + break; + case CAIRO_OPERATOR_OVERLAY: + res = SP_CSS_BLEND_OVERLAY; + break; + case CAIRO_OPERATOR_COLOR_DODGE: + res = SP_CSS_BLEND_COLORDODGE; + break; + case CAIRO_OPERATOR_COLOR_BURN: + res = SP_CSS_BLEND_COLORBURN; + break; + case CAIRO_OPERATOR_HARD_LIGHT: + res = SP_CSS_BLEND_HARDLIGHT; + break; + case CAIRO_OPERATOR_SOFT_LIGHT: + res = SP_CSS_BLEND_SOFTLIGHT; + break; + case CAIRO_OPERATOR_DIFFERENCE: + res = SP_CSS_BLEND_DIFFERENCE; + break; + case CAIRO_OPERATOR_EXCLUSION: + res = SP_CSS_BLEND_EXCLUSION; + break; + case CAIRO_OPERATOR_HSL_HUE: + res = SP_CSS_BLEND_HUE; + break; + case CAIRO_OPERATOR_HSL_SATURATION: + res = SP_CSS_BLEND_SATURATION; + break; + case CAIRO_OPERATOR_HSL_COLOR: + res = SP_CSS_BLEND_COLOR; + break; + case CAIRO_OPERATOR_HSL_LUMINOSITY: + res = SP_CSS_BLEND_LUMINOSITY; + break; + case CAIRO_OPERATOR_OVER: + default: + res = SP_CSS_BLEND_NORMAL; + break; + } + return res; +} + +cairo_operator_t ink_css_blend_to_cairo_operator(SPBlendMode css_blend) +{ + // All of the blend modes are implemented in Cairo as of 1.10. + // For a detailed description, see: + // http://cairographics.org/operators/ + + cairo_operator_t res = CAIRO_OPERATOR_OVER; + switch (css_blend) { + case SP_CSS_BLEND_MULTIPLY: + res = CAIRO_OPERATOR_MULTIPLY; + break; + case SP_CSS_BLEND_SCREEN: + res = CAIRO_OPERATOR_SCREEN; + break; + case SP_CSS_BLEND_DARKEN: + res = CAIRO_OPERATOR_DARKEN; + break; + case SP_CSS_BLEND_LIGHTEN: + res = CAIRO_OPERATOR_LIGHTEN; + break; + case SP_CSS_BLEND_OVERLAY: + res = CAIRO_OPERATOR_OVERLAY; + break; + case SP_CSS_BLEND_COLORDODGE: + res = CAIRO_OPERATOR_COLOR_DODGE; + break; + case SP_CSS_BLEND_COLORBURN: + res = CAIRO_OPERATOR_COLOR_BURN; + break; + case SP_CSS_BLEND_HARDLIGHT: + res = CAIRO_OPERATOR_HARD_LIGHT; + break; + case SP_CSS_BLEND_SOFTLIGHT: + res = CAIRO_OPERATOR_SOFT_LIGHT; + break; + case SP_CSS_BLEND_DIFFERENCE: + res = CAIRO_OPERATOR_DIFFERENCE; + break; + case SP_CSS_BLEND_EXCLUSION: + res = CAIRO_OPERATOR_EXCLUSION; + break; + case SP_CSS_BLEND_HUE: + res = CAIRO_OPERATOR_HSL_HUE; + break; + case SP_CSS_BLEND_SATURATION: + res = CAIRO_OPERATOR_HSL_SATURATION; + break; + case SP_CSS_BLEND_COLOR: + res = CAIRO_OPERATOR_HSL_COLOR; + break; + case SP_CSS_BLEND_LUMINOSITY: + res = CAIRO_OPERATOR_HSL_LUMINOSITY; + break; + case SP_CSS_BLEND_NORMAL: + res = CAIRO_OPERATOR_OVER; + break; + default: + g_error("Invalid SPBlendMode %d", css_blend); + } + return res; +} + + + +int ink_cairo_surface_linear_to_srgb(cairo_surface_t *surface) +{ + cairo_surface_flush(surface); + int width = cairo_image_surface_get_width(surface); + int height = cairo_image_surface_get_height(surface); + // int stride = cairo_image_surface_get_stride(surface); + // unsigned char *data = cairo_image_surface_get_data(surface); + + ink_cairo_surface_filter( surface, surface, SurfaceLinearToSrgb() ); + + // /* TODO convert this to OpenMP somehow */ + // for (int y = 0; y < height; ++y, data += stride) { + // for (int x = 0; x < width; ++x) { + // guint32 px = *reinterpret_cast<guint32*>(data + 4*x); + // EXTRACT_ARGB32(px, a,r,g,b) ; // Unneeded semi-colon for indenting + // if( a != 0 ) { + // r = linear_to_srgb( r, a ); + // g = linear_to_srgb( g, a ); + // b = linear_to_srgb( b, a ); + // } + // ASSEMBLE_ARGB32(px2, a,r,g,b); + // *reinterpret_cast<guint32*>(data + 4*x) = px2; + // } + // } + return width * height; +} + +cairo_pattern_t * +ink_cairo_pattern_create_checkerboard(guint32 rgba) +{ + int const w = 6; + int const h = 6; + + double r = SP_RGBA32_R_F(rgba); + double g = SP_RGBA32_G_F(rgba); + double b = SP_RGBA32_B_F(rgba); + + float hsl[3]; + SPColor::rgb_to_hsl_floatv(hsl, r, g, b); + hsl[2] += hsl[2] < 0.08 ? 0.08 : -0.08; // 0.08 = 0.77-0.69, the original checkerboard colors. + + float rgb2[3]; + SPColor::hsl_to_rgb_floatv(rgb2, hsl[0], hsl[1], hsl[2]); + + cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 2*w, 2*h); + + cairo_t *ct = cairo_create(s); + cairo_set_operator(ct, CAIRO_OPERATOR_SOURCE); + cairo_set_source_rgb(ct, r, g, b); + cairo_paint(ct); + cairo_set_source_rgb(ct, rgb2[0], rgb2[1], rgb2[2]); + cairo_rectangle(ct, 0, 0, w, h); + cairo_rectangle(ct, w, h, w, h); + cairo_fill(ct); + cairo_destroy(ct); + + cairo_pattern_t *p = cairo_pattern_create_for_surface(s); + cairo_pattern_set_extend(p, CAIRO_EXTEND_REPEAT); + cairo_pattern_set_filter(p, CAIRO_FILTER_NEAREST); + + cairo_surface_destroy(s); + return p; +} + +/** + * Converts the Cairo surface to a GdkPixbuf pixel format, + * without allocating extra memory. + * + * This function is intended mainly for creating previews displayed by GTK. + * For loading images for display on the canvas, use the Inkscape::Pixbuf object. + * + * The returned GdkPixbuf takes ownership of the passed surface reference, + * so it should NOT be freed after calling this function. + */ +GdkPixbuf *ink_pixbuf_create_from_cairo_surface(cairo_surface_t *s) +{ + guchar *pixels = cairo_image_surface_get_data(s); + int w = cairo_image_surface_get_width(s); + int h = cairo_image_surface_get_height(s); + int rs = cairo_image_surface_get_stride(s); + + convert_pixels_argb32_to_pixbuf(pixels, w, h, rs); + + GdkPixbuf *pb = gdk_pixbuf_new_from_data( + pixels, GDK_COLORSPACE_RGB, TRUE, 8, + w, h, rs, ink_cairo_pixbuf_cleanup, s); + + return pb; +} + +/** + * Cleanup function for GdkPixbuf. + * This function should be passed as the GdkPixbufDestroyNotify parameter + * to gdk_pixbuf_new_from_data when creating a GdkPixbuf backed by + * a Cairo surface. + */ +void ink_cairo_pixbuf_cleanup(guchar * /*pixels*/, void *data) +{ + cairo_surface_t *surface = static_cast<cairo_surface_t*>(data); + cairo_surface_destroy(surface); +} + +/* The following two functions use "from" instead of "to", because when you write: + val1 = argb32_from_pixbuf(val1); + the name of the format is closer to the value in that format. */ + +guint32 argb32_from_pixbuf(guint32 c) +{ + guint32 o = 0; +#if G_BYTE_ORDER == G_LITTLE_ENDIAN + guint32 a = (c & 0xff000000) >> 24; +#else + guint32 a = (c & 0x000000ff); +#endif + if (a != 0) { + // extract color components +#if G_BYTE_ORDER == G_LITTLE_ENDIAN + guint32 r = (c & 0x000000ff); + guint32 g = (c & 0x0000ff00) >> 8; + guint32 b = (c & 0x00ff0000) >> 16; +#else + guint32 r = (c & 0xff000000) >> 24; + guint32 g = (c & 0x00ff0000) >> 16; + guint32 b = (c & 0x0000ff00) >> 8; +#endif + // premultiply + r = premul_alpha(r, a); + b = premul_alpha(b, a); + g = premul_alpha(g, a); + // combine into output + o = (a << 24) | (r << 16) | (g << 8) | (b); + } + return o; +} + +guint32 pixbuf_from_argb32(guint32 c) +{ + guint32 a = (c & 0xff000000) >> 24; + if (a == 0) return 0; + + // extract color components + guint32 r = (c & 0x00ff0000) >> 16; + guint32 g = (c & 0x0000ff00) >> 8; + guint32 b = (c & 0x000000ff); + // unpremultiply; adding a/2 gives correct rounding + // (taken from Cairo sources) + r = (r * 255 + a/2) / a; + b = (b * 255 + a/2) / a; + g = (g * 255 + a/2) / a; + // combine into output +#if G_BYTE_ORDER == G_LITTLE_ENDIAN + guint32 o = (r) | (g << 8) | (b << 16) | (a << 24); +#else + guint32 o = (r << 24) | (g << 16) | (b << 8) | (a); +#endif + return o; +} + +/** + * Convert pixel data from GdkPixbuf format to ARGB. + * This will convert pixel data from GdkPixbuf format to Cairo's native pixel format. + * This involves premultiplying alpha and shuffling around the channels. + * Pixbuf data must have an alpha channel, otherwise the results are undefined + * (usually a segfault). + */ +void +convert_pixels_pixbuf_to_argb32(guchar *data, int w, int h, int stride) +{ + for (size_t i = 0; i < h; ++i) { + guint32 *px = reinterpret_cast<guint32*>(data + i*stride); + for (int j = 0; j < w; ++j) { + *px = argb32_from_pixbuf(*px); + ++px; + } + } +} + +/** + * Convert pixel data from ARGB to GdkPixbuf format. + * This will convert pixel data from GdkPixbuf format to Cairo's native pixel format. + * This involves premultiplying alpha and shuffling around the channels. + */ +void +convert_pixels_argb32_to_pixbuf(guchar *data, int w, int h, int stride) +{ + for (size_t i = 0; i < h; ++i) { + guint32 *px = reinterpret_cast<guint32*>(data + i*stride); + for (int j = 0; j < w; ++j) { + *px = pixbuf_from_argb32(*px); + ++px; + } + } +} + +/** + * Converts GdkPixbuf's data to premultiplied ARGB. + * This function will convert a GdkPixbuf in place into Cairo's native pixel format. + * Note that this is a hack intended to save memory. When the pixbuf is in Cairo's format, + * using it with GTK will result in corrupted drawings. + */ +void +ink_pixbuf_ensure_argb32(GdkPixbuf *pb) +{ + gchar *pixel_format = reinterpret_cast<gchar*>(g_object_get_data(G_OBJECT(pb), "pixel_format")); + if (pixel_format != nullptr && strcmp(pixel_format, "argb32") == 0) { + // nothing to do + return; + } + + convert_pixels_pixbuf_to_argb32( + gdk_pixbuf_get_pixels(pb), + gdk_pixbuf_get_width(pb), + gdk_pixbuf_get_height(pb), + gdk_pixbuf_get_rowstride(pb)); + g_object_set_data_full(G_OBJECT(pb), "pixel_format", g_strdup("argb32"), g_free); +} + +/** + * Converts GdkPixbuf's data back to its native format. + * Once this is done, the pixbuf can be used with GTK again. + */ +void +ink_pixbuf_ensure_normal(GdkPixbuf *pb) +{ + gchar *pixel_format = reinterpret_cast<gchar*>(g_object_get_data(G_OBJECT(pb), "pixel_format")); + if (pixel_format == nullptr || strcmp(pixel_format, "pixbuf") == 0) { + // nothing to do + return; + } + + convert_pixels_argb32_to_pixbuf( + gdk_pixbuf_get_pixels(pb), + gdk_pixbuf_get_width(pb), + gdk_pixbuf_get_height(pb), + gdk_pixbuf_get_rowstride(pb)); + g_object_set_data_full(G_OBJECT(pb), "pixel_format", g_strdup("pixbuf"), g_free); +} + +guint32 argb32_from_rgba(guint32 in) +{ + guint32 r, g, b, a; + a = (in & 0x000000ff); + r = premul_alpha((in & 0xff000000) >> 24, a); + g = premul_alpha((in & 0x00ff0000) >> 16, a); + b = premul_alpha((in & 0x0000ff00) >> 8, a); + ASSEMBLE_ARGB32(px, a, r, g, b) + return px; +} + + +/** + * Converts a pixbuf to a PNG data structure. + * For 8-but RGBA png, this is like copying. + * + */ +const guchar* pixbuf_to_png(guchar const**rows, guchar* px, int num_rows, int num_cols, int stride, int color_type, int bit_depth) +{ + int n_fields = 1 + (color_type&2) + (color_type&4)/4; + const guchar* new_data = (const guchar*)malloc((n_fields * bit_depth * num_rows * num_cols)/8 + 64); + char* ptr = (char*) new_data; + int pad=0; //used when we write image data smaller than one byte (for instance in black and white images where 1px = 1bit) + for(int row = 0; row < num_rows; ++row){ + rows[row] = (const guchar*)ptr; + for(int col = 0; col < num_cols; ++col){ + guint32 *pixel = reinterpret_cast<guint32*>(px + row*stride)+col; +#if G_BYTE_ORDER == G_LITTLE_ENDIAN + //this part should probably be rewritten as (a tested, working) big endian, using htons() and ntohs() + guint64 a = (*pixel & 0xff000000) >> 24; + guint64 b = (*pixel & 0x00ff0000) >> 16; + guint64 g = (*pixel & 0x0000ff00) >> 8; + guint64 r = (*pixel & 0x000000ff); + + //one of possible rgb to greyscale formulas. This one is called "luminance, "luminosity" or "luma" + guint16 gray = (guint16)((guint32)((0.2126*(r<<24) + 0.7152*(g<<24) + 0.0722*(b<<24)))>>16); + + if (!pad) *((guint64*)ptr)=0; + if (color_type & 2) { //RGB + // for 8bit->16bit transition, I take the FF -> FFFF convention (multiplication by 0x101). + // If you prefer FF -> FF00 (multiplication by 0x100), remove the <<8, <<24, <<40 and <<56 + if (color_type & 4) { //RGBA + if (bit_depth==8) + *((guint32*)ptr) = *pixel; + else + *((guint64*)ptr) = (guint64)((a<<56)+(a<<48)+(b<<40)+(b<<32)+(g<<24)+(g<<16)+(r<<8)+(r)); + } + else{ //no alpha + if(bit_depth==8) + *((guint32*)ptr) = ((*pixel)<<8)>>8; + else + *((guint64*)ptr) = (guint64)((b<<40)+(b<<32)+(g<<24)+(g<<16)+(r<<8)+r); + } + } else { //Grayscale + int realpad = 8 - bit_depth - pad; // in PNG numbers are stored left to right, but in most significant bits first, so the first one processed is the ``big'' mask, etc. + if(bit_depth==16) + *(guint16*)ptr= ((gray & 0xff00)>>8) + ((gray &0x00ff)<<8); + else *((guint16*)ptr) += guint16(((gray >> (16-bit_depth))<<realpad) ); //note the "+=" + + if(color_type & 4) { //Alpha channel + if (bit_depth == 16) + *((guint32*)(ptr+2)) = a + (a<<8); + else + *((guint32*)(ptr)) += guint32((a << 8) >> (16 - bit_depth))<<(bit_depth + realpad); + } + } + +#else + // I don't have any access to a big-endian machine to test this. It should still work with default export settings. + guint64 r = (*pixel & 0xff000000) >> 24; + guint64 g = (*pixel & 0x00ff0000) >> 16; + guint64 b = (*pixel & 0x0000ff00) >> 8; + guint64 a = (*pixel & 0x000000ff); + guint32 gray = (guint32)(0.2126*(r<<24) + 0.7152*(g<<24) + 0.0722*(b<<24)); + if(color_type & 2){ + if(bit_depth==8)*ptr = *pixel; else *ptr = (guint64)((r<<56)+(g<<40)+(b<<24)+(a<<8)); + } else { + *((guint32*)ptr) += guint32(gray>>pad); + if(color_type & 4) *((guint32*)ptr) += guint32((a>>pad)>> bit_depth); + } +#endif + pad+=bit_depth*n_fields; + ptr+=(pad/8); + pad%=8; + } + if(pad){pad=0;ptr++;}//align bytes on rows + } + return new_data; +} + + + + + + + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/cairo-utils.h b/src/display/cairo-utils.h new file mode 100644 index 0000000..232e666 --- /dev/null +++ b/src/display/cairo-utils.h @@ -0,0 +1,267 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Cairo integration helpers. + *//* + * Authors: + * Krzysztof KosiĆski <tweenk.pl@gmail.com> + * + * Copyright (C) 2010 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_DISPLAY_CAIRO_UTILS_H +#define SEEN_INKSCAPE_DISPLAY_CAIRO_UTILS_H + +#include <2geom/forward.h> +#include <cairomm/cairomm.h> +#include "style.h" + +struct SPColor; +typedef struct _GdkPixbuf GdkPixbuf; + +void ink_cairo_pixbuf_cleanup(unsigned char *, void *); +void convert_pixbuf_argb32_to_normal(GdkPixbuf *pb); + +namespace Inkscape { + +/** + * RAII idiom for Cairo groups. + * Groups are temporary surfaces used when rendering e.g. masks and opacity. + * Use this class to ensure that each group push is matched with a pop. + */ +class CairoGroup { +public: + CairoGroup(cairo_t *_ct); + ~CairoGroup(); + void push(); + void push_with_content(cairo_content_t content); + cairo_pattern_t *pop(); + Cairo::RefPtr<Cairo::Pattern> popmm(); + void pop_to_source(); +private: + cairo_t *ct; + bool pushed; +}; + +/** RAII idiom for Cairo state saving. */ +class CairoSave { +public: + CairoSave(cairo_t *_ct, bool save=false) + : ct(_ct) + , saved(save) + { + if (save) { + cairo_save(ct); + } + } + void save() { + if (!saved) { + cairo_save(ct); + saved = true; + } + } + ~CairoSave() { + if (saved) + cairo_restore(ct); + } +private: + cairo_t *ct; + bool saved; +}; + +/** Cairo context with Inkscape-specific operations. */ +class CairoContext : public Cairo::Context { +public: + CairoContext(cairo_t *obj, bool ref = false); + + void transform(Geom::Affine const &m); + void set_source_rgba32(guint32 color); + void append_path(Geom::PathVector const &pv); + + static Cairo::RefPtr<CairoContext> create(Cairo::RefPtr<Cairo::Surface> const &target); +}; + +/** Class to hold image data for raster images. + * Allows easy interoperation with GdkPixbuf and Cairo. */ +class Pixbuf { +public: + enum PixelFormat { + PF_CAIRO = 1, + PF_GDK = 2, + PF_LAST + }; + + explicit Pixbuf(cairo_surface_t *s); + explicit Pixbuf(GdkPixbuf *pb); + Pixbuf(Inkscape::Pixbuf const &other); + ~Pixbuf(); + + GdkPixbuf *getPixbufRaw(bool convert_format = true); + //Glib::RefPtr<Gdk::Pixbuf> getPixbuf(bool convert_format = true); + + cairo_surface_t *getSurfaceRaw(bool convert_format = true); + Cairo::RefPtr<Cairo::Surface> getSurface(bool convert_format = true); + + int width() const; + int height() const; + int rowstride() const; + guchar const *pixels() const; + guchar *pixels(); + void markDirty(); + + bool hasMimeData() const; + guchar const *getMimeData(gsize &len, std::string &mimetype) const; + std::string const &originalPath() const { return _path; } + time_t modificationTime() const { return _mod_time; } + + PixelFormat pixelFormat() const { return _pixel_format; } + void ensurePixelFormat(PixelFormat fmt); + + static Pixbuf *create_from_data_uri(gchar const *uri, double svgdpi = 0); + static Pixbuf *create_from_file(std::string const &fn, double svgddpi = 0); + static Pixbuf *create_from_buffer(std::string const &, double svgddpi = 0, std::string const &fn = ""); + + private: + static Pixbuf *create_from_buffer(gchar *&&, gsize, double svgddpi = 0, std::string const &fn = ""); + + void _ensurePixelsARGB32(); + void _ensurePixelsPixbuf(); + void _forceAlpha(); + void _setMimeData(guchar *data, gsize len, Glib::ustring const &format); + + GdkPixbuf *_pixbuf; + cairo_surface_t *_surface; + time_t _mod_time; + std::string _path; + PixelFormat _pixel_format; + bool _cairo_store; +}; + +} // namespace Inkscape + +// TODO: these declarations may not be needed in the header +extern cairo_user_data_key_t ink_color_interpolation_key; +extern cairo_user_data_key_t ink_pixbuf_key; + +SPColorInterpolation get_cairo_surface_ci(cairo_surface_t *surface); +void set_cairo_surface_ci(cairo_surface_t *surface, SPColorInterpolation cif); +void copy_cairo_surface_ci(cairo_surface_t *in, cairo_surface_t *out); +void convert_cairo_surface_ci(cairo_surface_t *surface, SPColorInterpolation cif); + +void ink_cairo_set_source_color(cairo_t *ct, SPColor const &color, double opacity); +void ink_cairo_set_source_rgba32(cairo_t *ct, guint32 rgba); +void ink_cairo_transform(cairo_t *ct, Geom::Affine const &m); +void ink_cairo_pattern_set_matrix(cairo_pattern_t *cp, Geom::Affine const &m); + +void ink_matrix_to_2geom(Geom::Affine &, cairo_matrix_t const &); +void ink_matrix_to_cairo(cairo_matrix_t &, Geom::Affine const &); +cairo_operator_t ink_css_blend_to_cairo_operator(SPBlendMode blend_mode); +SPBlendMode ink_cairo_operator_to_css_blend(cairo_operator_t cairo_operator); +cairo_surface_t *ink_cairo_surface_copy(cairo_surface_t *s); +cairo_surface_t *ink_cairo_surface_create_identical(cairo_surface_t *s); +cairo_surface_t *ink_cairo_surface_create_same_size(cairo_surface_t *s, cairo_content_t c); +cairo_surface_t *ink_cairo_extract_alpha(cairo_surface_t *s); +cairo_surface_t *ink_cairo_surface_create_output(cairo_surface_t *image, cairo_surface_t *bg); +void ink_cairo_surface_blit(cairo_surface_t *src, cairo_surface_t *dest); +int ink_cairo_surface_get_width(cairo_surface_t *surface); +int ink_cairo_surface_get_height(cairo_surface_t *surface); +guint32 ink_cairo_surface_average_color(cairo_surface_t *surface); +void ink_cairo_surface_average_color(cairo_surface_t *surface, double &r, double &g, double &b, double &a); +void ink_cairo_surface_average_color_premul(cairo_surface_t *surface, double &r, double &g, double &b, double &a); + +double srgb_to_linear( const double c ); +int ink_cairo_surface_srgb_to_linear(cairo_surface_t *surface); +int ink_cairo_surface_linear_to_srgb(cairo_surface_t *surface); + +cairo_pattern_t *ink_cairo_pattern_create_checkerboard(guint32 rgba = 0xC4C4C4FF); + +GdkPixbuf *ink_pixbuf_create_from_cairo_surface(cairo_surface_t *s); +void convert_pixels_pixbuf_to_argb32(guchar *data, int w, int h, int rs); +void convert_pixels_argb32_to_pixbuf(guchar *data, int w, int h, int rs); + +G_GNUC_CONST guint32 argb32_from_pixbuf(guint32 in); +G_GNUC_CONST guint32 pixbuf_from_argb32(guint32 in); +const guchar* pixbuf_to_png(guchar const**rows, guchar* px, int nrows, int ncols, int stride, int color_type, int bit_depth); + +/** Convert a pixel in 0xRRGGBBAA format to Cairo ARGB32 format. */ +G_GNUC_CONST guint32 argb32_from_rgba(guint32 in); + + +G_GNUC_CONST inline guint32 +premul_alpha(const guint32 color, const guint32 alpha) +{ + const guint32 temp = alpha * color + 128; + return (temp + (temp >> 8)) >> 8; +} +G_GNUC_CONST inline guint32 +unpremul_alpha(const guint32 color, const guint32 alpha) +{ + // NOTE: you must check for alpha != 0 yourself. + return (255 * color + alpha/2) / alpha; +} + +// TODO: move those to 2Geom +void feed_pathvector_to_cairo (cairo_t *ct, Geom::PathVector const &pathv, Geom::Affine trans, Geom::OptRect area, bool optimize_stroke, double stroke_width); +void feed_pathvector_to_cairo (cairo_t *ct, Geom::PathVector const &pathv); + +#define EXTRACT_ARGB32(px,a,r,g,b) \ + guint32 a, r, g, b; \ + a = ((px) & 0xff000000) >> 24; \ + r = ((px) & 0x00ff0000) >> 16; \ + g = ((px) & 0x0000ff00) >> 8; \ + b = ((px) & 0x000000ff); + +#define ASSEMBLE_ARGB32(px,a,r,g,b) \ + guint32 px = (a << 24) | (r << 16) | (g << 8) | b; + +inline double srgb_to_linear( const double c ) { + if( c < 0.04045 ) { + return c / 12.92; + } else { + return pow( (c+0.055)/1.055, 2.4 ); + } +} + + +namespace Inkscape { + +namespace Display +{ + +inline void ExtractARGB32(guint32 px, guint32 &a, guint32 &r, guint32 &g, guint &b) +{ + a = ((px) & 0xff000000) >> 24; + r = ((px) & 0x00ff0000) >> 16; + g = ((px) & 0x0000ff00) >> 8; + b = ((px) & 0x000000ff); +} + +inline void ExtractRGB32(guint32 px, guint32 &r, guint32 &g, guint &b) +{ + r = ((px) & 0x00ff0000) >> 16; + g = ((px) & 0x0000ff00) >> 8; + b = ((px) & 0x000000ff); +} + +inline guint AssembleARGB32(guint32 a, guint32 r, guint32 g, guint32 b) +{ + return (a << 24) | (r << 16) | (g << 8) | b; +} + +} // namespace Display + +} // namespace Inkscape + +#endif // SEEN_INKSCAPE_DISPLAY_CAIRO_UTILS_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/canvas-arena.cpp b/src/display/canvas-arena.cpp new file mode 100644 index 0000000..1ab617f --- /dev/null +++ b/src/display/canvas-arena.cpp @@ -0,0 +1,385 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * RGBA display list system for inkscape + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2001-2002 Lauris Kaplinski + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm.h> + +#include "display/sp-canvas-util.h" +#include "helper/sp-marshal.h" +#include "display/canvas-arena.h" +#include "display/cairo-utils.h" +#include "display/drawing-context.h" +#include "display/drawing-item.h" +#include "display/drawing-group.h" +#include "display/drawing-surface.h" +#include "preferences.h" + +using namespace Inkscape; + +enum { + ARENA_EVENT, + LAST_SIGNAL +}; + +static void sp_canvas_arena_destroy(SPCanvasItem *object); + +static void sp_canvas_arena_item_deleted(SPCanvasArena *arena, Inkscape::DrawingItem *item); +static void sp_canvas_arena_update (SPCanvasItem *item, Geom::Affine const &affine, unsigned int flags); +static void sp_canvas_arena_render (SPCanvasItem *item, SPCanvasBuf *buf); +static double sp_canvas_arena_point (SPCanvasItem *item, Geom::Point p, SPCanvasItem **actual_item); +static void sp_canvas_arena_viewbox_changed (SPCanvasItem *item, Geom::IntRect const &new_area); +static gint sp_canvas_arena_event (SPCanvasItem *item, GdkEvent *event); + +static gint sp_canvas_arena_send_event (SPCanvasArena *arena, GdkEvent *event); + +static void sp_canvas_arena_request_update (SPCanvasArena *ca, DrawingItem *item); +static void sp_canvas_arena_request_render (SPCanvasArena *ca, Geom::IntRect const &area); + +static guint signals[LAST_SIGNAL] = {0}; + +struct CachePrefObserver : public Inkscape::Preferences::Observer { + CachePrefObserver(SPCanvasArena *arena) + : Inkscape::Preferences::Observer("/options/renderingcache") + , _arena(arena) + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + std::vector<Inkscape::Preferences::Entry> v = prefs->getAllEntries(observed_path); + for (const auto & i : v) { + notify(i); + } + prefs->addObserver(*this); + } + void notify(Preferences::Entry const &v) override { + Glib::ustring name = v.getEntryName(); + if (name == "size") { + _arena->drawing.setCacheBudget((1 << 20) * v.getIntLimited(64, 0, 4096)); + } + } + SPCanvasArena *_arena; +}; + +G_DEFINE_TYPE(SPCanvasArena, sp_canvas_arena, SP_TYPE_CANVAS_ITEM); + +static void +sp_canvas_arena_class_init (SPCanvasArenaClass *klass) +{ + SPCanvasItemClass *item_class = (SPCanvasItemClass *) klass; + + signals[ARENA_EVENT] = g_signal_new ("arena_event", + G_TYPE_FROM_CLASS(item_class), + G_SIGNAL_RUN_LAST, + ((glong)((guint8*)&(klass->arena_event) - (guint8*)klass)), + nullptr, nullptr, + sp_marshal_INT__POINTER_POINTER, + G_TYPE_INT, 2, G_TYPE_POINTER, G_TYPE_POINTER); + + item_class->destroy = sp_canvas_arena_destroy; + item_class->update = sp_canvas_arena_update; + item_class->render = sp_canvas_arena_render; + item_class->point = sp_canvas_arena_point; + item_class->event = sp_canvas_arena_event; + item_class->viewbox_changed = sp_canvas_arena_viewbox_changed; +} + +static void +sp_canvas_arena_init (SPCanvasArena *arena) +{ + arena->sticky = FALSE; + + new (&arena->drawing) Inkscape::Drawing(arena); + + Inkscape::DrawingGroup *root = new DrawingGroup(arena->drawing); + root->setPickChildren(true); + arena->drawing.setRoot(root); + + arena->observer = new CachePrefObserver(arena); + + arena->drawing.signal_request_update.connect( + sigc::bind<0>( + sigc::ptr_fun(&sp_canvas_arena_request_update), + arena)); + arena->drawing.signal_request_render.connect( + sigc::bind<0>( + sigc::ptr_fun(&sp_canvas_arena_request_render), + arena)); + arena->drawing.signal_item_deleted.connect( + sigc::bind<0>( + sigc::ptr_fun(&sp_canvas_arena_item_deleted), + arena)); + + arena->active = nullptr; +} + +static void sp_canvas_arena_destroy(SPCanvasItem *object) +{ + SPCanvasArena *arena = SP_CANVAS_ARENA(object); + + delete arena->observer; + arena->drawing.~Drawing(); + + if (SP_CANVAS_ITEM_CLASS(sp_canvas_arena_parent_class)->destroy) + SP_CANVAS_ITEM_CLASS(sp_canvas_arena_parent_class)->destroy(object); +} + +static void +sp_canvas_arena_update (SPCanvasItem *item, Geom::Affine const &affine, unsigned int flags) +{ + SPCanvasArena *arena = SP_CANVAS_ARENA (item); + + if (SP_CANVAS_ITEM_CLASS(sp_canvas_arena_parent_class)->update) + SP_CANVAS_ITEM_CLASS(sp_canvas_arena_parent_class)->update(item, affine, flags); + + arena->ctx.ctm = affine; + + unsigned reset = flags & SP_CANVAS_UPDATE_AFFINE ? DrawingItem::STATE_ALL : 0; + arena->drawing.update(Geom::IntRect::infinite(), DrawingItem::STATE_ALL, reset); + + Geom::OptIntRect b = arena->drawing.root()->visualBounds(); + if (b) { + item->x1 = b->left() - 1; + item->y1 = b->top() - 1; + item->x2 = b->right() + 1; + item->y2 = b->bottom() + 1; + } + + if (arena->cursor) { + /* Mess with enter/leave notifiers */ + DrawingItem *new_arena = arena->drawing.pick(arena->c, arena->drawing.delta, arena->sticky); + if (new_arena != arena->active) { + GdkEventCrossing ec; + ec.window = gtk_widget_get_window (GTK_WIDGET (item->canvas)); + ec.send_event = TRUE; + ec.subwindow = ec.window; + ec.time = GDK_CURRENT_TIME; + ec.x = arena->c[Geom::X]; + ec.y = arena->c[Geom::Y]; + /* fixme: */ + if (arena->active) { + ec.type = GDK_LEAVE_NOTIFY; + sp_canvas_arena_send_event (arena, (GdkEvent *) &ec); + } + arena->active = new_arena; + if (arena->active) { + ec.type = GDK_ENTER_NOTIFY; + sp_canvas_arena_send_event (arena, (GdkEvent *) &ec); + } + } + } +} + +static void +sp_canvas_arena_item_deleted(SPCanvasArena *arena, Inkscape::DrawingItem *item) +{ + if (arena->active == item) { + arena->active = nullptr; + } +} + +static void +sp_canvas_arena_render (SPCanvasItem *item, SPCanvasBuf *buf) +{ + // todo: handle NR_ARENA_ITEM_RENDER_NO_CACHE + SPCanvasArena *arena = SP_CANVAS_ARENA (item); + + Geom::OptIntRect r = buf->rect; + if (!r || r->hasZeroArea()) return; + + Inkscape::DrawingContext dc(buf->ct, r->min()); + + arena->drawing.update(Geom::IntRect::infinite()); + arena->drawing.render(dc, *r); +} + +static double +sp_canvas_arena_point (SPCanvasItem *item, Geom::Point p, SPCanvasItem **actual_item) +{ + SPCanvasArena *arena = SP_CANVAS_ARENA (item); + + arena->drawing.update(Geom::IntRect::infinite(), DrawingItem::STATE_PICK | DrawingItem::STATE_BBOX); + DrawingItem *picked = arena->drawing.pick(p, arena->drawing.delta, arena->sticky); + + arena->picked = picked; + + if (picked) { + *actual_item = item; + return 0.0; + } + + return 1e18; +} + +static void +sp_canvas_arena_viewbox_changed (SPCanvasItem *item, Geom::IntRect const &new_area) +{ + SPCanvasArena *arena = SP_CANVAS_ARENA(item); + // make the cache limit larger than screen to facilitate smooth scrolling + Geom::IntRect expanded = new_area; + Geom::IntPoint expansion(new_area.width()/2, new_area.height()/2); + expanded.expandBy(expansion); + arena->drawing.setCacheLimit(expanded); +} + +// What is this used for? It appears to allow one to get signals on entering/leaving elements which +// is intend to be used by Inkview to change the cursor when over an anchor. However arena->active +// appears to only be set to root. +static gint +sp_canvas_arena_event (SPCanvasItem *item, GdkEvent *event) +{ + Inkscape::DrawingItem *new_arena; + /* fixme: This sucks, we have to handle enter/leave notifiers */ + + SPCanvasArena *arena = SP_CANVAS_ARENA (item); + gint ret = FALSE; + + switch (event->type) { + case GDK_ENTER_NOTIFY: + if (!arena->cursor) { + if (arena->active) { + //g_warning ("Cursor entered to arena with already active item"); + } + arena->cursor = TRUE; + + /* TODO ... event -> arena transform? */ + arena->c = Geom::Point(event->crossing.x, event->crossing.y); + + /* fixme: Not sure abut this, but seems the right thing (Lauris) */ + arena->drawing.update(Geom::IntRect::infinite(), + DrawingItem::STATE_PICK | DrawingItem::STATE_BBOX, 0); + arena->active = arena->drawing.pick(arena->c, arena->drawing.delta, arena->sticky); + ret = sp_canvas_arena_send_event (arena, event); + } + break; + + case GDK_LEAVE_NOTIFY: + if (arena->cursor) { + ret = sp_canvas_arena_send_event (arena, event); + arena->active = nullptr; + arena->cursor = FALSE; + } + break; + + case GDK_MOTION_NOTIFY: + /* TODO ... event -> arena transform? */ + arena->c = Geom::Point(event->motion.x, event->motion.y); + + /* fixme: Not sure abut this, but seems the right thing (Lauris) */ + arena->drawing.update(Geom::IntRect::infinite(), + DrawingItem::STATE_PICK | DrawingItem::STATE_BBOX); + new_arena = arena->drawing.pick(arena->c, arena->drawing.delta, arena->sticky); + if (new_arena != arena->active) { + GdkEventCrossing ec; + ec.window = event->motion.window; + ec.send_event = event->motion.send_event; + ec.subwindow = event->motion.window; + ec.time = event->motion.time; + ec.x = event->motion.x; + ec.y = event->motion.y; + /* fixme: */ + if (arena->active) { + ec.type = GDK_LEAVE_NOTIFY; + ret = sp_canvas_arena_send_event (arena, (GdkEvent *) &ec); + } + arena->active = new_arena; + if (arena->active) { + ec.type = GDK_ENTER_NOTIFY; + ret = sp_canvas_arena_send_event (arena, (GdkEvent *) &ec); + } + } + ret = ret || sp_canvas_arena_send_event (arena, event); + break; + + case GDK_SCROLL: { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool wheelzooms = prefs->getBool("/options/wheelzooms/value"); + bool ctrl = (event->scroll.state & GDK_CONTROL_MASK); + if ((ctrl && !wheelzooms) || (!ctrl && wheelzooms)) { + /* Zoom is emitted by the canvas as well, ignore here */ + return FALSE; + } + ret = sp_canvas_arena_send_event (arena, event); + break; + } + + default: + /* Just send event */ + ret = sp_canvas_arena_send_event (arena, event); + break; + } + + return ret; +} + +static gint +sp_canvas_arena_send_event (SPCanvasArena *arena, GdkEvent *event) +{ + gint ret = FALSE; + + /* Send event to arena */ + g_signal_emit (G_OBJECT (arena), signals[ARENA_EVENT], 0, arena->active, event, &ret); + + return ret; +} + +static void +sp_canvas_arena_request_update (SPCanvasArena *ca, DrawingItem */*item*/) +{ + sp_canvas_item_request_update (SP_CANVAS_ITEM (ca)); +} + +static void sp_canvas_arena_request_render(SPCanvasArena *ca, Geom::IntRect const &area) +{ + SPCanvas *canvas = SP_CANVAS_ITEM(ca)->canvas; + canvas->requestRedraw(area.left(), area.top(), area.right(), area.bottom()); +} + +void +sp_canvas_arena_set_pick_delta (SPCanvasArena *ca, gdouble delta) +{ + g_return_if_fail (ca != nullptr); + g_return_if_fail (SP_IS_CANVAS_ARENA (ca)); + + /* fixme: repick? */ + ca->delta = delta; +} + +void +sp_canvas_arena_set_sticky (SPCanvasArena *ca, gboolean sticky) +{ + g_return_if_fail (ca != nullptr); + g_return_if_fail (SP_IS_CANVAS_ARENA (ca)); + + /* fixme: repick? */ + ca->sticky = sticky; +} + +void +sp_canvas_arena_render_surface (SPCanvasArena *ca, cairo_surface_t *surface, Geom::IntRect const &r) +{ + g_return_if_fail (ca != nullptr); + g_return_if_fail (SP_IS_CANVAS_ARENA (ca)); + + Inkscape::DrawingContext dc(surface, r.min()); + ca->drawing.update(Geom::IntRect::infinite()); + ca->drawing.render(dc, r); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/canvas-arena.h b/src/display/canvas-arena.h new file mode 100644 index 0000000..8d40964 --- /dev/null +++ b/src/display/canvas-arena.h @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_CANVAS_ARENA_H +#define SEEN_SP_CANVAS_ARENA_H + +/* + * RGBA display list system for inkscape + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 2001-2002 Lauris Kaplinski + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/rect.h> + +#include "display/drawing.h" +#include "display/drawing-item.h" +#include "display/sp-canvas.h" +#include "display/sp-canvas-item.h" + +#define SP_TYPE_CANVAS_ARENA (sp_canvas_arena_get_type ()) +#define SP_CANVAS_ARENA(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SP_TYPE_CANVAS_ARENA, SPCanvasArena)) +#define SP_CANVAS_ARENA_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SP_TYPE_CANVAS_ARENA, SPCanvasArenaClass)) +#define SP_IS_CANVAS_ARENA(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SP_TYPE_CANVAS_ARENA)) +#define SP_IS_CANVAS_ARENA_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), SP_TYPE_CANVAS_ARENA)) + +typedef struct _SPCanvasArena SPCanvasArena; +typedef struct _SPCanvasArenaClass SPCanvasArenaClass; +typedef struct _cairo_surface cairo_surface_t; +struct CachePrefObserver; + +namespace Inkscape { + +class Drawing; +class DrawingItem; + +} // namespace Inkscape + +struct _SPCanvasArena { + SPCanvasItem item; + + guint cursor : 1; + guint sticky : 1; + Geom::Point c; // what is this? + + Inkscape::Drawing drawing; + Inkscape::UpdateContext ctx; + + Inkscape::DrawingItem *active; + /* fixme: */ + Inkscape::DrawingItem *picked; + CachePrefObserver *observer; + double delta; +}; + +struct _SPCanvasArenaClass { + SPCanvasItemClass parent_class; + + gint (* arena_event) (SPCanvasArena *carena, Inkscape::DrawingItem *item, GdkEvent *event); +}; + +GType sp_canvas_arena_get_type (); + +void sp_canvas_arena_set_pick_delta (SPCanvasArena *ca, gdouble delta); +void sp_canvas_arena_set_sticky (SPCanvasArena *ca, gboolean sticky); + +void sp_canvas_arena_render_surface (SPCanvasArena *ca, cairo_surface_t *surface, Geom::IntRect const &area); + +#endif // SEEN_SP_CANVAS_ARENA_H diff --git a/src/display/canvas-axonomgrid.cpp b/src/display/canvas-axonomgrid.cpp new file mode 100644 index 0000000..428f659 --- /dev/null +++ b/src/display/canvas-axonomgrid.cpp @@ -0,0 +1,758 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Johan Engelen <j.b.c.engelen@alumnus.utwente.nl> + * + * Copyright (C) 2006-2012 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +/* + * Current limits are: one axis (y-axis) is always vertical. The other two + * axes are bound to a certain range of angles. The z-axis always has an angle + * smaller than 90 degrees (measured from horizontal, 0 degrees being a line extending + * to the right). The x-axis will always have an angle between 0 and 90 degrees. + */ + +#include <gtkmm/box.h> +#include <gtkmm/label.h> +#include <gtkmm/grid.h> + +#include <glibmm/i18n.h> + +#include "display/canvas-axonomgrid.h" + +#include "ui/widget/registered-widget.h" +#include "desktop.h" + +#include "display/cairo-utils.h" +#include "display/canvas-grid.h" +#include "display/sp-canvas-util.h" +#include "display/sp-canvas.h" +#include "document.h" +#include "inkscape.h" +#include "preferences.h" +#include "object/sp-namedview.h" +#include "object/sp-object.h" +#include "object/sp-root.h" +#include "svg/svg-color.h" +#include "2geom/line.h" +#include "2geom/angle.h" +#include "helper/mathfns.h" +#include "util/units.h" + +using Inkscape::Util::unit_table; + +enum Dim3 { X=0, Y, Z }; + +/** + * This function calls Cairo to render a line on a particular canvas buffer. + * Coordinates are interpreted as SCREENcoordinates + */ +static void +sp_caxonomgrid_drawline (SPCanvasBuf *buf, gint x0, gint y0, gint x1, gint y1, guint32 rgba) +{ + cairo_move_to(buf->ct, 0.5 + x0, 0.5 + y0); + cairo_line_to(buf->ct, 0.5 + x1, 0.5 + y1); + ink_cairo_set_source_rgba32(buf->ct, rgba); + cairo_stroke(buf->ct); +} + +static void +sp_grid_vline (SPCanvasBuf *buf, gint x, gint ys, gint ye, guint32 rgba) +{ + if ((x < buf->rect.left()) || (x >= buf->rect.right())) + return; + + cairo_move_to(buf->ct, 0.5 + x, 0.5 + ys); + cairo_line_to(buf->ct, 0.5 + x, 0.5 + ye); + ink_cairo_set_source_rgba32(buf->ct, rgba); + cairo_stroke(buf->ct); +} + +namespace Inkscape { + + +CanvasAxonomGrid::CanvasAxonomGrid (SPNamedView * nv, Inkscape::XML::Node * in_repr, SPDocument * in_doc) + : CanvasGrid(nv, in_repr, in_doc, GRID_AXONOMETRIC) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gridunit = unit_table.getUnit(prefs->getString("/options/grids/axonom/units")); + if (!gridunit) { + gridunit = unit_table.getUnit("px"); + } + origin[Geom::X] = Inkscape::Util::Quantity::convert(prefs->getDouble("/options/grids/axonom/origin_x", 0.0), gridunit, "px"); + origin[Geom::Y] = Inkscape::Util::Quantity::convert(prefs->getDouble("/options/grids/axonom/origin_y", 0.0), gridunit, "px"); + color = prefs->getInt("/options/grids/axonom/color", GRID_DEFAULT_COLOR); + empcolor = prefs->getInt("/options/grids/axonom/empcolor", GRID_DEFAULT_EMPCOLOR); + empspacing = prefs->getInt("/options/grids/axonom/empspacing", 5); + lengthy = Inkscape::Util::Quantity::convert(prefs->getDouble("/options/grids/axonom/spacing_y", 1.0), gridunit, "px"); + angle_deg[X] = prefs->getDouble("/options/grids/axonom/angle_x", 30.0); + angle_deg[Z] = prefs->getDouble("/options/grids/axonom/angle_z", 30.0); + angle_deg[Y] = 0; + + angle_rad[X] = Geom::rad_from_deg(angle_deg[X]); + tan_angle[X] = tan(angle_rad[X]); + angle_rad[Z] = Geom::rad_from_deg(angle_deg[Z]); + tan_angle[Z] = tan(angle_rad[Z]); + + snapper = new CanvasAxonomGridSnapper(this, &namedview->snap_manager, 0); + + if (repr) readRepr(); +} + +CanvasAxonomGrid::~CanvasAxonomGrid () +{ + if (snapper) delete snapper; +} + +static gboolean sp_nv_read_opacity(gchar const *str, guint32 *color) +{ + if (!str) { + return FALSE; + } + + gchar *u; + gdouble v = g_ascii_strtod(str, &u); + if (!u) { + return FALSE; + } + v = CLAMP(v, 0.0, 1.0); + + *color = (*color & 0xffffff00) | (guint32) floor(v * 255.9999); + + return TRUE; +} + + + +void +CanvasAxonomGrid::readRepr() +{ + SPRoot *root = doc->getRoot(); + double scale_x = 1.0; + double scale_y = 1.0; + if( root->viewBox_set ) { + scale_x = root->width.computed / root->viewBox.width(); + scale_y = root->height.computed / root->viewBox.height(); + if (Geom::are_near(scale_x / scale_y, 1.0, Geom::EPSILON)) { + // scaling is uniform, try to reduce numerical error + scale_x = (scale_x + scale_y)/2.0; + double scale_none = Inkscape::Util::Quantity::convert(1, doc->getDisplayUnit(), "px"); + if (Geom::are_near(scale_x / scale_none, 1.0, Geom::EPSILON)) + scale_x = scale_none; // objects are same size, reduce numerical error + scale_y = scale_x; + } + } + + gchar const *value; + + if ( (value = repr->attribute("originx")) ) { + + Inkscape::Util::Quantity q = unit_table.parseQuantity(value); + + if( q.unit->type == UNIT_TYPE_LINEAR ) { + // Legacy grid not in 'user units' + origin[Geom::X] = q.value("px"); + } else { + // Grid in 'user units' + origin[Geom::X] = q.quantity * scale_x; + } + } + + if ( (value = repr->attribute("originy")) ) { + + Inkscape::Util::Quantity q = unit_table.parseQuantity(value); + + if( q.unit->type == UNIT_TYPE_LINEAR ) { + // Legacy grid not in 'user units' + origin[Geom::Y] = q.value("px"); + } else { + // Grid in 'user units' + origin[Geom::Y] = q.quantity * scale_y; + } + } + + if ( (value = repr->attribute("spacingy")) ) { + + Inkscape::Util::Quantity q = unit_table.parseQuantity(value); + + if( q.unit->type == UNIT_TYPE_LINEAR ) { + // Legacy grid not in 'user units' + lengthy = q.value("px"); + } else { + // Grid in 'user units' + lengthy = q.quantity * scale_y; // We do not handle scale_x != scale_y + } + if (lengthy < 0.0500) lengthy = 0.0500; + } + + if ( (value = repr->attribute("gridanglex")) ) { + angle_deg[X] = g_ascii_strtod(value, nullptr); + if (angle_deg[X] < 0.) angle_deg[X] = 0.; + if (angle_deg[X] > 89.0) angle_deg[X] = 89.0; + angle_rad[X] = Geom::rad_from_deg(angle_deg[X]); + tan_angle[X] = tan(angle_rad[X]); + } + + if ( (value = repr->attribute("gridanglez")) ) { + angle_deg[Z] = g_ascii_strtod(value, nullptr); + if (angle_deg[Z] < 0.) angle_deg[Z] = 0.; + if (angle_deg[Z] > 89.0) angle_deg[Z] = 89.0; + angle_rad[Z] = Geom::rad_from_deg(angle_deg[Z]); + tan_angle[Z] = tan(angle_rad[Z]); + } + + if ( (value = repr->attribute("color")) ) { + color = (color & 0xff) | sp_svg_read_color(value, color); + } + + if ( (value = repr->attribute("empcolor")) ) { + empcolor = (empcolor & 0xff) | sp_svg_read_color(value, empcolor); + } + + if ( (value = repr->attribute("opacity")) ) { + sp_nv_read_opacity(value, &color); + } + if ( (value = repr->attribute("empopacity")) ) { + sp_nv_read_opacity(value, &empcolor); + } + + if ( (value = repr->attribute("empspacing")) ) { + empspacing = atoi(value); + } + + if ( (value = repr->attribute("visible")) ) { + visible = (strcmp(value,"false") != 0 && strcmp(value, "0") != 0); + } + + if ( (value = repr->attribute("enabled")) ) { + g_assert(snapper != nullptr); + snapper->setEnabled(strcmp(value,"false") != 0 && strcmp(value, "0") != 0); + } + + if ( (value = repr->attribute("snapvisiblegridlinesonly")) ) { + g_assert(snapper != nullptr); + snapper->setSnapVisibleOnly(strcmp(value,"false") != 0 && strcmp(value, "0") != 0); + } + + if ( (value = repr->attribute("units")) ) { + gridunit = unit_table.getUnit(value); // Display unit identifier in grid menu + } + + for (auto i:canvasitems) { + sp_canvas_item_request_update(i); + } + return; +} + +/** + * Called when XML node attribute changed; updates dialog widgets if change was not done by widgets themselves. + */ +void +CanvasAxonomGrid::onReprAttrChanged(Inkscape::XML::Node */*repr*/, gchar const */*key*/, gchar const */*oldval*/, gchar const */*newval*/, bool /*is_interactive*/) +{ + readRepr(); + + if ( ! (_wr.isUpdating()) ) + updateWidgets(); +} + +Gtk::Widget * +CanvasAxonomGrid::newSpecificWidget() +{ + _rumg = Gtk::manage( new Inkscape::UI::Widget::RegisteredUnitMenu( + _("Grid _units:"), "units", _wr, repr, doc) ); + _rsu_ox = Gtk::manage( new Inkscape::UI::Widget::RegisteredScalarUnit( + _("_Origin X:"), _("X coordinate of grid origin"), "originx", + *_rumg, _wr, repr, doc, Inkscape::UI::Widget::RSU_x) ); + _rsu_oy = Gtk::manage( new Inkscape::UI::Widget::RegisteredScalarUnit( + _("O_rigin Y:"), _("Y coordinate of grid origin"), "originy", + *_rumg, _wr, repr, doc, Inkscape::UI::Widget::RSU_y) ); + _rsu_sy = Gtk::manage( new Inkscape::UI::Widget::RegisteredScalarUnit( + _("Spacing _Y:"), _("Base length of z-axis"), "spacingy", + *_rumg, _wr, repr, doc, Inkscape::UI::Widget::RSU_y) ); + _rsu_ax = Gtk::manage( new Inkscape::UI::Widget::RegisteredScalar( + _("Angle X:"), _("Angle of x-axis"), "gridanglex", _wr, repr, doc ) ); + _rsu_az = Gtk::manage( new Inkscape::UI::Widget::RegisteredScalar( + _("Angle Z:"), _("Angle of z-axis"), "gridanglez", _wr, repr, doc ) ); + + _rcp_gcol = Gtk::manage( new Inkscape::UI::Widget::RegisteredColorPicker( + _("Minor grid line _color:"), _("Minor grid line color"), _("Color of the minor grid lines"), + "color", "opacity", _wr, repr, doc)); + _rcp_gmcol = Gtk::manage( new Inkscape::UI::Widget::RegisteredColorPicker( + _("Ma_jor grid line color:"), _("Major grid line color"), + _("Color of the major (highlighted) grid lines"), + "empcolor", "empopacity", _wr, repr, doc)); + + _rsi = Gtk::manage( new Inkscape::UI::Widget::RegisteredSuffixedInteger( + _("_Major grid line every:"), "", _("lines"), "empspacing", _wr, repr, doc ) ); + + _rumg->set_hexpand(); + _rsu_ox->set_hexpand(); + _rsu_oy->set_hexpand(); + _rsu_sy->set_hexpand(); + _rsu_ax->set_hexpand(); + _rsu_az->set_hexpand(); + _rcp_gcol->set_hexpand(); + _rcp_gmcol->set_hexpand(); + _rsi->set_hexpand(); + + // set widget values + _wr.setUpdating (true); + + _rsu_ox->setDigits(5); + _rsu_ox->setIncrements(0.1, 1.0); + + _rsu_oy->setDigits(5); + _rsu_oy->setIncrements(0.1, 1.0); + + _rsu_sy->setDigits(5); + _rsu_sy->setIncrements(0.1, 1.0); + + _rumg->setUnit (gridunit->abbr); + + gdouble val; + val = origin[Geom::X]; + val = Inkscape::Util::Quantity::convert(val, "px", gridunit); + _rsu_ox->setValue (val); + val = origin[Geom::Y]; + val = Inkscape::Util::Quantity::convert(val, "px", gridunit); + _rsu_oy->setValue (val); + val = lengthy; + double gridy = Inkscape::Util::Quantity::convert(val, "px", gridunit); + _rsu_sy->setValue (gridy); + + _rsu_ax->setValue(angle_deg[X]); + _rsu_az->setValue(angle_deg[Z]); + + _rcp_gcol->setRgba32 (color); + _rcp_gmcol->setRgba32 (empcolor); + _rsi->setValue (empspacing); + + _wr.setUpdating (false); + + _rsu_ox->setProgrammatically = false; + _rsu_oy->setProgrammatically = false; + + Gtk::Box *column = new Gtk::Box(Gtk::ORIENTATION_VERTICAL, 4); + column->pack_start(*_rumg, true, false); + column->pack_start(*_rsu_ox, true, false); + column->pack_start(*_rsu_oy, true, false); + column->pack_start(*_rsu_sy, true, false); + column->pack_start(*_rsu_ax, true, false); + column->pack_start(*_rsu_az, true, false); + column->pack_start(*_rcp_gcol, true, false); + column->pack_start(*_rcp_gmcol, true, false); + column->pack_start(*_rsi, true, false); + + return column; +} + + +/** + * Update dialog widgets from object's values. + */ +void +CanvasAxonomGrid::updateWidgets() +{ + if (_wr.isUpdating()) return; + + //no widgets (grid created with the document, not with the dialog) + if (!_rcb_visible) return; + + _wr.setUpdating (true); + + _rcb_visible->setActive(visible); + if (snapper != nullptr) { + _rcb_enabled->setActive(snapper->getEnabled()); + _rcb_snap_visible_only->setActive(snapper->getSnapVisibleOnly()); + } + + _rumg->setUnit (gridunit->abbr); + + gdouble val; + + val = origin[Geom::X]; + val = Inkscape::Util::Quantity::convert(val, "px", gridunit); + _rsu_ox->setValue (val); + + val = origin[Geom::Y]; + val = Inkscape::Util::Quantity::convert(val, "px", gridunit); + _rsu_oy->setValue (val); + + val = lengthy; + val = Inkscape::Util::Quantity::convert(val, "px", gridunit); + _rsu_sy->setValue (val); + + _rsu_ax->setValue(angle_deg[X]); + _rsu_az->setValue(angle_deg[Z]); + + _rsu_ox->setProgrammatically = false; + _rsu_oy->setProgrammatically = false; + _rsu_sy->setProgrammatically = false; + _rsu_ax->setProgrammatically = false; + _rsu_az->setProgrammatically = false; + + _rcp_gcol->setRgba32 (color); + _rcp_gmcol->setRgba32 (empcolor); + _rsi->setValue (empspacing); + + _wr.setUpdating (false); +} + + + +void +CanvasAxonomGrid::Update (Geom::Affine const &affine, unsigned int /*flags*/) +{ + ow = origin * affine; + sw = Geom::Point(fabs(affine[0]),fabs(affine[3])); + sw *= lengthy; + + scaled = false; + + for(int dim = 0; dim < 2; dim++) { + gint scaling_factor = empspacing; + + if (scaling_factor <= 1) + scaling_factor = 5; + + int watchdog = 0; + while ( (sw[dim] < 8.0) & (watchdog < 100) ) { + scaled = true; + sw[dim] *= scaling_factor; + // First pass, go up to the major line spacing, then + // keep increasing by two. + scaling_factor = 2; + watchdog++; + } + + } + + spacing_ylines = sw[Geom::X] /(tan_angle[X] + tan_angle[Z]); + lyw = sw[Geom::Y]; + lxw_x = Geom::are_near(tan_angle[X],0.) ? Geom::infinity() : sw[Geom::X] / tan_angle[X]; + lxw_z = Geom::are_near(tan_angle[Z],0.) ? Geom::infinity() : sw[Geom::X] / tan_angle[Z]; + + if (empspacing == 0) { + scaled = true; + } +} + +void +CanvasAxonomGrid::Render (SPCanvasBuf *buf) +{ + //set correct coloring, depending preference (when zoomed out, always major coloring or minor coloring) + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + guint32 _empcolor; + guint32 _color = color; + bool preference = prefs->getBool("/options/grids/no_emphasize_when_zoomedout", false); + if( scaled && preference ) { + _empcolor = color; + } else { + _empcolor = empcolor; + } + bool xrayactive = prefs->getBool("/desktop/xrayactive", false); + if (xrayactive) { //this allow good looking on xray zones + guint32 bg = namedview->pagecolor; + _color = SP_RGBA32_F_COMPOSE( + CLAMP(((1 - SP_RGBA32_A_F(_color)) * SP_RGBA32_R_F(bg)) + (SP_RGBA32_A_F(_color) * SP_RGBA32_R_F(_color)), 0.0, 1.0), + CLAMP(((1 - SP_RGBA32_A_F(_color)) * SP_RGBA32_G_F(bg)) + (SP_RGBA32_A_F(_color) * SP_RGBA32_G_F(_color)), 0.0, 1.0), + CLAMP(((1 - SP_RGBA32_A_F(_color)) * SP_RGBA32_B_F(bg)) + (SP_RGBA32_A_F(_color) * SP_RGBA32_B_F(_color)), 0.0, 1.0), + 1.0); + _empcolor = SP_RGBA32_F_COMPOSE( + CLAMP(((1 - SP_RGBA32_A_F(_empcolor)) * SP_RGBA32_R_F(bg)) + (SP_RGBA32_A_F(_empcolor) * SP_RGBA32_R_F(_empcolor)), 0.0, 1.0), + CLAMP(((1 - SP_RGBA32_A_F(_empcolor)) * SP_RGBA32_G_F(bg)) + (SP_RGBA32_A_F(_empcolor) * SP_RGBA32_G_F(_empcolor)), 0.0, 1.0), + CLAMP(((1 - SP_RGBA32_A_F(_empcolor)) * SP_RGBA32_B_F(bg)) + (SP_RGBA32_A_F(_empcolor) * SP_RGBA32_B_F(_empcolor)), 0.0, 1.0), + 1.0); + } + cairo_save(buf->ct); + cairo_translate(buf->ct, -buf->rect.left(), -buf->rect.top()); + cairo_set_line_width(buf->ct, 1.0); + cairo_set_line_cap(buf->ct, CAIRO_LINE_CAP_SQUARE); + + // gc = gridcoordinates (the coordinates calculated from the grids origin 'grid->ow'. + // sc = screencoordinates ( for example "buf->rect.left()" is in screencoordinates ) + // bc = buffer patch coordinates (x=0 on left side of page, y=0 on bottom of page) + + // tl = topleft ; br = bottomright + Geom::Point buf_tl_gc; + Geom::Point buf_br_gc; + buf_tl_gc[Geom::X] = buf->rect.left() - ow[Geom::X]; + buf_tl_gc[Geom::Y] = buf->rect.top() - ow[Geom::Y]; + buf_br_gc[Geom::X] = buf->rect.right() - ow[Geom::X]; + buf_br_gc[Geom::Y] = buf->rect.bottom() - ow[Geom::Y]; + + // render the three separate line groups representing the main-axes + + // x-axis always goes from topleft to bottomright. (0,0) - (1,1) + gdouble const xintercept_y_bc = (buf_tl_gc[Geom::X] * tan_angle[X]) - buf_tl_gc[Geom::Y] ; + gdouble const xstart_y_sc = ( xintercept_y_bc - floor(xintercept_y_bc/lyw)*lyw ) + buf->rect.top(); + gint const xlinestart = round( (xstart_y_sc - buf_tl_gc[Geom::X]*tan_angle[X] - ow[Geom::Y]) / lyw ); + gint xlinenum = xlinestart; + // lines starting on left side. + for (gdouble y = xstart_y_sc; y < buf->rect.bottom(); y += lyw, xlinenum++) { + gint const x0 = buf->rect.left(); + gint const y0 = round(y); + gint x1 = x0 + round( (buf->rect.bottom() - y) / tan_angle[X] ); + gint y1 = buf->rect.bottom(); + if ( Geom::are_near(tan_angle[X],0.) ) { + x1 = buf->rect.right(); + y1 = y0; + } + + if (!scaled && (xlinenum % empspacing) != 0) { + sp_caxonomgrid_drawline (buf, x0, y0, x1, y1, _color); + } else { + sp_caxonomgrid_drawline (buf, x0, y0, x1, y1, _empcolor); + } + } + // lines starting from top side + if (!Geom::are_near(tan_angle[X],0.)) + { + gdouble const xstart_x_sc = buf->rect.left() + (lxw_x - (xstart_y_sc - buf->rect.top()) / tan_angle[X]) ; + xlinenum = xlinestart-1; + for (gdouble x = xstart_x_sc; x < buf->rect.right(); x += lxw_x, xlinenum--) { + gint const y0 = buf->rect.top(); + gint const y1 = buf->rect.bottom(); + gint const x0 = round(x); + gint const x1 = x0 + round( (y1 - y0) / tan_angle[X] ); + + if (!scaled && (xlinenum % empspacing) != 0) { + sp_caxonomgrid_drawline (buf, x0, y0, x1, y1, _color); + } else { + sp_caxonomgrid_drawline (buf, x0, y0, x1, y1, _empcolor); + } + } + } + + // y-axis lines (vertical) + gdouble const ystart_x_sc = floor (buf_tl_gc[Geom::X] / spacing_ylines) * spacing_ylines + ow[Geom::X]; + gint const ylinestart = round((ystart_x_sc - ow[Geom::X]) / spacing_ylines); + gint ylinenum = ylinestart; + for (gdouble x = ystart_x_sc; x < buf->rect.right(); x += spacing_ylines, ylinenum++) { + gint const x0 = floor(x); // sp_grid_vline will add 0.5 again, so we'll pre-emptively use floor() + // instead of round() to avoid biasing the vertical lines to the right by half a pixel; see + // CanvasXYGrid::Render() for more details + + if (!scaled && (ylinenum % empspacing) != 0) { + sp_grid_vline (buf, x0, buf->rect.top(), buf->rect.bottom() - 1, _color); + } else { + sp_grid_vline (buf, x0, buf->rect.top(), buf->rect.bottom() - 1, _empcolor); + } + } + + // z-axis always goes from bottomleft to topright. (0,1) - (1,0) + gdouble const zintercept_y_bc = (buf_tl_gc[Geom::X] * -tan_angle[Z]) - buf_tl_gc[Geom::Y] ; + gdouble const zstart_y_sc = ( zintercept_y_bc - floor(zintercept_y_bc/lyw)*lyw ) + buf->rect.top(); + gint const zlinestart = round( (zstart_y_sc + buf_tl_gc[Geom::X]*tan_angle[Z] - ow[Geom::Y]) / lyw ); + gint zlinenum = zlinestart; + // lines starting from left side + gdouble next_y = zstart_y_sc; + for (gdouble y = zstart_y_sc; y < buf->rect.bottom(); y += lyw, zlinenum++, next_y = y) { + gint const x0 = buf->rect.left(); + gint const y0 = round(y); + gint x1 = x0 + round( (y - buf->rect.top() ) / tan_angle[Z] ); + gint y1 = buf->rect.top(); + if ( Geom::are_near(tan_angle[Z],0.) ) { + x1 = buf->rect.right(); + y1 = y0; + } + + if (!scaled && (zlinenum % empspacing) != 0) { + sp_caxonomgrid_drawline (buf, x0, y0, x1, y1, _color); + } else { + sp_caxonomgrid_drawline (buf, x0, y0, x1, y1, _empcolor); + } + } + // draw lines from bottom-up + if (!Geom::are_near(tan_angle[Z],0.)) + { + gdouble const zstart_x_sc = buf->rect.left() + (next_y - buf->rect.bottom()) / tan_angle[Z] ; + for (gdouble x = zstart_x_sc; x < buf->rect.right(); x += lxw_z, zlinenum++) { + gint const y0 = buf->rect.bottom(); + gint const y1 = buf->rect.top(); + gint const x0 = round(x); + gint const x1 = x0 + round(buf->rect.height() / tan_angle[Z] ); + + if (!scaled && (zlinenum % empspacing) != 0) { + sp_caxonomgrid_drawline (buf, x0, y0, x1, y1, _color); + } else { + sp_caxonomgrid_drawline (buf, x0, y0, x1, y1, _empcolor); + } + } + } + + cairo_restore(buf->ct); +} + +CanvasAxonomGridSnapper::CanvasAxonomGridSnapper(CanvasAxonomGrid *grid, SnapManager *sm, Geom::Coord const d) : LineSnapper(sm, d) +{ + this->grid = grid; +} + +/** + * \return Snap tolerance (desktop coordinates); depends on current zoom so that it's always the same in screen pixels + */ +Geom::Coord CanvasAxonomGridSnapper::getSnapperTolerance() const +{ + SPDesktop const *dt = _snapmanager->getDesktop(); + double const zoom = dt ? dt->current_zoom() : 1; + return _snapmanager->snapprefs.getGridTolerance() / zoom; +} + +bool CanvasAxonomGridSnapper::getSnapperAlwaysSnap() const +{ + return _snapmanager->snapprefs.getGridTolerance() == 10000; //TODO: Replace this threshold of 10000 by a constant; see also tolerance-slider.cpp +} + +LineSnapper::LineList +CanvasAxonomGridSnapper::_getSnapLines(Geom::Point const &p) const +{ + LineList s; + + if ( grid == nullptr ) { + return s; + } + + SPDesktop const *dt = _snapmanager->getDesktop(); + double ta_x = grid->tan_angle[X]; + double ta_z = grid->tan_angle[Z]; + + if (dt && dt->is_yaxisdown()) { + std::swap(ta_x, ta_z); + } + + double spacing_h; + double spacing_v; + + if (getSnapVisibleOnly()) { + // Only snapping to visible grid lines + spacing_h = grid->spacing_ylines; // this is the spacing of the visible grid lines measured in screen pixels + spacing_v = grid->lyw; // vertical + // convert screen pixels to px + // FIXME: after we switch to snapping dist in screen pixels, this will be unnecessary + if (dt) { + spacing_h /= dt->current_zoom(); + spacing_v /= dt->current_zoom(); + } + } else { + // Snapping to any grid line, whether it's visible or not + spacing_h = grid->lengthy /(grid->tan_angle[X] + grid->tan_angle[Z]); + spacing_v = grid->lengthy; + + } + + // In an axonometric grid, any point will be surrounded by 6 grid lines: + // - 2 vertical grid lines, one left and one right from the point + // - 2 angled z grid lines, one above and one below the point + // - 2 angled x grid lines, one above and one below the point + + // Calculate the x coordinate of the vertical grid lines + Geom::Coord x_max = Inkscape::Util::round_to_upper_multiple_plus(p[Geom::X], spacing_h, grid->origin[Geom::X]); + Geom::Coord x_min = Inkscape::Util::round_to_lower_multiple_plus(p[Geom::X], spacing_h, grid->origin[Geom::X]); + + // Calculate the y coordinate of the intersection of the angled grid lines with the y-axis + double y_proj_along_z = p[Geom::Y] - ta_z * (p[Geom::X] - grid->origin[Geom::X]); + double y_proj_along_x = p[Geom::Y] + ta_x * (p[Geom::X] - grid->origin[Geom::X]); + double y_proj_along_z_max = Inkscape::Util::round_to_upper_multiple_plus(y_proj_along_z, spacing_v, grid->origin[Geom::Y]); + double y_proj_along_z_min = Inkscape::Util::round_to_lower_multiple_plus(y_proj_along_z, spacing_v, grid->origin[Geom::Y]); + double y_proj_along_x_max = Inkscape::Util::round_to_upper_multiple_plus(y_proj_along_x, spacing_v, grid->origin[Geom::Y]); + double y_proj_along_x_min = Inkscape::Util::round_to_lower_multiple_plus(y_proj_along_x, spacing_v, grid->origin[Geom::Y]); + + // Calculate the versor for the angled grid lines + Geom::Point vers_x = Geom::Point(1, -ta_x); + Geom::Point vers_z = Geom::Point(1, ta_z); + + // Calculate the normal for the angled grid lines + Geom::Point norm_x = Geom::rot90(vers_x); + Geom::Point norm_z = Geom::rot90(vers_z); + + // The four angled grid lines form a parallelogram, enclosing the point + // One of the two vertical grid lines divides this parallelogram in two triangles + // We will now try to find out in which half (i.e. triangle) our point is, and return + // only the three grid lines defining that triangle + + // The vertical grid line is at the intersection of two angled grid lines. + // Now go find that intersection! + Geom::Point p_x(0, y_proj_along_x_max); + Geom::Line line_x(p_x, p_x + vers_x); + Geom::Point p_z(0, y_proj_along_z_max); + Geom::Line line_z(p_z, p_z + vers_z); + + Geom::OptCrossing inters = Geom::OptCrossing(); // empty by default + try + { + inters = Geom::intersection(line_x, line_z); + } + catch (Geom::InfiniteSolutions &e) + { + // We're probably dealing with parallel lines; this is useless! + return s; + } + + // Determine which half of the parallelogram to use + bool use_left_half = true; + bool use_right_half = true; + + if (inters) { + Geom::Point inters_pt = line_x.pointAt((*inters).ta); + use_left_half = (p[Geom::X] - grid->origin[Geom::X]) < inters_pt[Geom::X]; + use_right_half = !use_left_half; + } + + // Return the three grid lines which define the triangle that encloses our point + // If we didn't find an intersection above, all 6 grid lines will be returned + if (use_left_half) { + s.push_back(std::make_pair(norm_z, Geom::Point(grid->origin[Geom::X], y_proj_along_z_max))); + s.push_back(std::make_pair(norm_x, Geom::Point(grid->origin[Geom::X], y_proj_along_x_min))); + s.push_back(std::make_pair(Geom::Point(1, 0), Geom::Point(x_max, 0))); + } + + if (use_right_half) { + s.push_back(std::make_pair(norm_z, Geom::Point(grid->origin[Geom::X], y_proj_along_z_min))); + s.push_back(std::make_pair(norm_x, Geom::Point(grid->origin[Geom::X], y_proj_along_x_max))); + s.push_back(std::make_pair(Geom::Point(1, 0), Geom::Point(x_min, 0))); + } + + return s; +} + +void CanvasAxonomGridSnapper::_addSnappedLine(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, Geom::Point const &normal_to_line, Geom::Point const &point_on_line) const +{ + SnappedLine dummy = SnappedLine(snapped_point, snapped_distance, source, source_num, Inkscape::SNAPTARGET_GRID, getSnapperTolerance(), getSnapperAlwaysSnap(), normal_to_line, point_on_line); + isr.grid_lines.push_back(dummy); +} + +void CanvasAxonomGridSnapper::_addSnappedPoint(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, bool constrained_snap) const +{ + SnappedPoint dummy = SnappedPoint(snapped_point, source, source_num, Inkscape::SNAPTARGET_GRID, snapped_distance, getSnapperTolerance(), getSnapperAlwaysSnap(), constrained_snap, true); + isr.points.push_back(dummy); +} + +void CanvasAxonomGridSnapper::_addSnappedLinePerpendicularly(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, bool constrained_snap) const +{ + SnappedPoint dummy = SnappedPoint(snapped_point, source, source_num, Inkscape::SNAPTARGET_GRID_PERPENDICULAR, snapped_distance, getSnapperTolerance(), getSnapperAlwaysSnap(), constrained_snap, true); + isr.points.push_back(dummy); +} + +bool CanvasAxonomGridSnapper::ThisSnapperMightSnap() const +{ + return _snap_enabled && _snapmanager->snapprefs.isTargetSnappable(Inkscape::SNAPTARGET_GRID); +} + + +}; // namespace Inkscape + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/canvas-axonomgrid.h b/src/display/canvas-axonomgrid.h new file mode 100644 index 0000000..ce4a19e --- /dev/null +++ b/src/display/canvas-axonomgrid.h @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef CANVAS_AXONOMGRID_H +#define CANVAS_AXONOMGRID_H + +/* + * Authors: + * Johan Engelen <j.b.c.engelen@alumnus.utwente.nl> + * + * Copyright (C) 2006-2012 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "line-snapper.h" +#include "canvas-grid.h" + +struct SPCanvasBuf; +class SPNamedView; + +namespace Inkscape { +namespace XML { + class Node; +}; + +class CanvasAxonomGrid : public CanvasGrid { +public: + CanvasAxonomGrid(SPNamedView * nv, Inkscape::XML::Node * in_repr, SPDocument * in_doc); + ~CanvasAxonomGrid() override; + + void Update (Geom::Affine const &affine, unsigned int flags) override; + void Render (SPCanvasBuf *buf) override; + + void readRepr() override; + void onReprAttrChanged (Inkscape::XML::Node * repr, char const *key, char const *oldval, char const *newval, bool is_interactive) override; + + double lengthy; /**< The lengths of the primary y-axis */ + double angle_deg[3]; /**< Angle of each axis (note that angle[2] == 0) */ + double angle_rad[3]; /**< Angle of each axis (note that angle[2] == 0) */ + double tan_angle[3]; /**< tan(angle[.]) */ + + bool scaled; /**< Whether the grid is in scaled mode */ + +protected: + friend class CanvasAxonomGridSnapper; + + Geom::Point ow; /**< Transformed origin by the affine for the zoom */ + double lyw; /**< Transformed length y by the affine for the zoom */ + double lxw_x; + double lxw_z; + double spacing_ylines; + + Geom::Point sw; /**< the scaling factors of the affine transform */ + + Gtk::Widget * newSpecificWidget() override; + +private: + CanvasAxonomGrid(const CanvasAxonomGrid&) = delete; + CanvasAxonomGrid& operator=(const CanvasAxonomGrid&) = delete; + + void updateWidgets(); + + Inkscape::UI::Widget::RegisteredUnitMenu *_rumg; + Inkscape::UI::Widget::RegisteredScalarUnit *_rsu_ox; + Inkscape::UI::Widget::RegisteredScalarUnit *_rsu_oy; + Inkscape::UI::Widget::RegisteredScalarUnit *_rsu_sy; + Inkscape::UI::Widget::RegisteredScalar *_rsu_ax; + Inkscape::UI::Widget::RegisteredScalar *_rsu_az; + Inkscape::UI::Widget::RegisteredColorPicker *_rcp_gcol; + Inkscape::UI::Widget::RegisteredColorPicker *_rcp_gmcol; + Inkscape::UI::Widget::RegisteredSuffixedInteger *_rsi; +}; + + + +class CanvasAxonomGridSnapper : public LineSnapper +{ +public: + CanvasAxonomGridSnapper(CanvasAxonomGrid *grid, SnapManager *sm, Geom::Coord const d); + bool ThisSnapperMightSnap() const override; + + Geom::Coord getSnapperTolerance() const override; //returns the tolerance of the snapper in screen pixels (i.e. independent of zoom) + bool getSnapperAlwaysSnap() const override; //if true, then the snapper will always snap, regardless of its tolerance + +private: + LineList _getSnapLines(Geom::Point const &p) const override; + void _addSnappedLine(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, Geom::Point const &normal_to_line, const Geom::Point &point_on_line) const override; + void _addSnappedPoint(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, bool constrained_snap) const override; + void _addSnappedLinePerpendicularly(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, bool constrained_snap) const override; + + CanvasAxonomGrid *grid; +}; + + +}; //namespace Inkscape + + + +#endif + + diff --git a/src/display/canvas-bpath.cpp b/src/display/canvas-bpath.cpp new file mode 100644 index 0000000..327bb1c --- /dev/null +++ b/src/display/canvas-bpath.cpp @@ -0,0 +1,253 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Simple bezier bpath CanvasItem for inkscape + * + * Authors: + * Lauris Kaplinski <lauris@ximian.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2001 Lauris Kaplinski and Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + */ + +#include <sstream> +#include <cstring> +#include "desktop.h" + +#include "color.h" +#include "display/sp-canvas-group.h" +#include "display/sp-canvas-util.h" +#include "display/canvas-bpath.h" +#include "display/curve.h" +#include "display/cairo-utils.h" +#include "helper/geom.h" +#include "display/sp-canvas.h" + +static void sp_canvas_bpath_destroy(SPCanvasItem *object); + +static void sp_canvas_bpath_update (SPCanvasItem *item, Geom::Affine const &affine, unsigned int flags); +static void sp_canvas_bpath_render (SPCanvasItem *item, SPCanvasBuf *buf); +static double sp_canvas_bpath_point (SPCanvasItem *item, Geom::Point p, SPCanvasItem **actual_item); + +G_DEFINE_TYPE(SPCanvasBPath, sp_canvas_bpath, SP_TYPE_CANVAS_ITEM); + +static void sp_canvas_bpath_class_init(SPCanvasBPathClass *klass) +{ + SPCanvasItemClass *item_class = (SPCanvasItemClass *) klass; + + item_class->destroy = sp_canvas_bpath_destroy; + item_class->update = sp_canvas_bpath_update; + item_class->render = sp_canvas_bpath_render; + item_class->point = sp_canvas_bpath_point; +} + +static void +sp_canvas_bpath_init (SPCanvasBPath * bpath) +{ + bpath->fill_rgba = 0x00000000; + bpath->fill_rule = SP_WIND_RULE_EVENODD; + + bpath->stroke_rgba = 0x00000000; + bpath->stroke_width = 1.0; + bpath->stroke_linejoin = SP_STROKE_LINEJOIN_MITER; + bpath->stroke_linecap = SP_STROKE_LINECAP_BUTT; + bpath->stroke_miterlimit = 11.0; + bpath->phantom_line = false; +} + +static void sp_canvas_bpath_destroy(SPCanvasItem *object) +{ + SPCanvasBPath *cbp = SP_CANVAS_BPATH (object); + + if (cbp->curve) { + cbp->curve = cbp->curve->unref(); + } + + if (SP_CANVAS_ITEM_CLASS(sp_canvas_bpath_parent_class)->destroy) + (* SP_CANVAS_ITEM_CLASS(sp_canvas_bpath_parent_class)->destroy) (object); +} + +static void sp_canvas_bpath_update(SPCanvasItem *item, Geom::Affine const &affine, unsigned int flags) +{ + SPCanvasBPath *cbp = SP_CANVAS_BPATH(item); + + item->canvas->requestRedraw((int)item->x1 - 1, (int)item->y1 - 1, (int)item->x2 + 1 , (int)item->y2 + 1); + + if (reinterpret_cast<SPCanvasItemClass *>(sp_canvas_bpath_parent_class)->update) { + reinterpret_cast<SPCanvasItemClass *>(sp_canvas_bpath_parent_class)->update(item, affine, flags); + } + + sp_canvas_item_reset_bounds (item); + + if (!cbp->curve) return; + + cbp->affine = affine; + + Geom::OptRect bbox = bounds_exact_transformed(cbp->curve->get_pathvector(), affine); + + if (bbox) { + item->x1 = (int)floor(bbox->min()[Geom::X]) - 1; + item->y1 = (int)floor(bbox->min()[Geom::Y]) - 1; + item->x2 = (int)ceil(bbox->max()[Geom::X]) + 1; + item->y2 = (int)ceil(bbox->max()[Geom::Y]) + 1; + } else { + item->x1 = 0; + item->y1 = 0; + item->x2 = 0; + item->y2 = 0; + } + item->canvas->requestRedraw((int)item->x1, (int)item->y1, (int)item->x2, (int)item->y2); +} + +static void +sp_canvas_bpath_render (SPCanvasItem *item, SPCanvasBuf *buf) +{ + SPCanvasBPath *cbp = SP_CANVAS_BPATH (item); + + Geom::Rect area = buf->rect; + + if ( !cbp->curve || + ((cbp->stroke_rgba & 0xff) == 0 && (cbp->fill_rgba & 0xff) == 0 ) || + cbp->curve->get_segment_count() < 1) + return; + + if (!buf->ct) + return; + + bool dofill = ((cbp->fill_rgba & 0xff) != 0); + bool dostroke = ((cbp->stroke_rgba & 0xff) != 0); + + cairo_set_tolerance(buf->ct, 0.5); + cairo_new_path(buf->ct); + + feed_pathvector_to_cairo (buf->ct, cbp->curve->get_pathvector(), cbp->affine, area, + /* optimized_stroke = */ !dofill, 1); + + if (dofill) { + // RGB / BGR + ink_cairo_set_source_rgba32(buf->ct, cbp->fill_rgba); + cairo_set_fill_rule(buf->ct, cbp->fill_rule == SP_WIND_RULE_EVENODD? CAIRO_FILL_RULE_EVEN_ODD + : CAIRO_FILL_RULE_WINDING); + cairo_fill_preserve(buf->ct); + } + + if (dostroke && cbp->phantom_line) { + ink_cairo_set_source_rgba32(buf->ct, 0xffffff4d); + cairo_set_line_width(buf->ct, 2); + if (cbp->dashes[0] != 0 && cbp->dashes[1] != 0) { + cairo_set_dash (buf->ct, cbp->dashes, 2, 0); + } + cairo_stroke(buf->ct); + cairo_set_tolerance(buf->ct, 0.5); + cairo_new_path(buf->ct); + feed_pathvector_to_cairo (buf->ct, cbp->curve->get_pathvector(), cbp->affine, area, + /* optimized_stroke = */ !dofill, 1); + ink_cairo_set_source_rgba32(buf->ct, cbp->stroke_rgba); + cairo_set_line_width(buf->ct, 1); + cairo_stroke(buf->ct); + } else if (dostroke) { + ink_cairo_set_source_rgba32(buf->ct, cbp->stroke_rgba); + cairo_set_line_width(buf->ct, 1); + if (cbp->dashes[0] != 0 && cbp->dashes[1] != 0) { + cairo_set_dash (buf->ct, cbp->dashes, 2, 0); + } + cairo_stroke(buf->ct); + } else { + cairo_new_path(buf->ct); + } +} + +static double +sp_canvas_bpath_point (SPCanvasItem *item, Geom::Point p, SPCanvasItem **actual_item) +{ + SPCanvasBPath *cbp = SP_CANVAS_BPATH (item); + + if ( !cbp->curve || + ((cbp->stroke_rgba & 0xff) == 0 && (cbp->fill_rgba & 0xff) == 0 ) || + cbp->curve->get_segment_count() < 1) + return Geom::infinity(); + + double width = 0.5; + Geom::Rect viewbox = item->canvas->getViewbox(); + viewbox.expandBy (width); + double dist = Geom::infinity(); + pathv_matrix_point_bbox_wind_distance(cbp->curve->get_pathvector(), cbp->affine, p, nullptr, nullptr, &dist, 0.5, &viewbox); + + if (dist <= 1.0) { + *actual_item = item; + } + + return dist; +} + +SPCanvasItem * +sp_canvas_bpath_new (SPCanvasGroup *parent, SPCurve *curve, bool phantom_line) +{ + g_return_val_if_fail (parent != nullptr, NULL); + g_return_val_if_fail (SP_IS_CANVAS_GROUP (parent), NULL); + + SPCanvasItem *item = sp_canvas_item_new (parent, SP_TYPE_CANVAS_BPATH, nullptr); + + sp_canvas_bpath_set_bpath (SP_CANVAS_BPATH (item), curve, phantom_line); + + return item; +} + +void +sp_canvas_bpath_set_bpath (SPCanvasBPath *cbp, SPCurve *curve, bool phantom_line) +{ + g_return_if_fail (cbp != nullptr); + g_return_if_fail (SP_IS_CANVAS_BPATH (cbp)); + + cbp->phantom_line = phantom_line; + if (cbp->curve) { + cbp->curve = cbp->curve->unref(); + } + + if (curve) { + cbp->curve = curve->ref(); + } + + sp_canvas_item_request_update (SP_CANVAS_ITEM (cbp)); +} + +void +sp_canvas_bpath_set_fill (SPCanvasBPath *cbp, guint32 rgba, SPWindRule rule) +{ + g_return_if_fail (cbp != nullptr); + g_return_if_fail (SP_IS_CANVAS_BPATH (cbp)); + + cbp->fill_rgba = rgba; + cbp->fill_rule = rule; + + sp_canvas_item_request_update (SP_CANVAS_ITEM (cbp)); +} + +void +sp_canvas_bpath_set_stroke (SPCanvasBPath *cbp, guint32 rgba, gdouble width, SPStrokeJoinType join, SPStrokeCapType cap, double dash, double gap) +{ + g_return_if_fail (cbp != nullptr); + g_return_if_fail (SP_IS_CANVAS_BPATH (cbp)); + + cbp->stroke_rgba = rgba; + cbp->stroke_width = MAX (width, 0.1); + cbp->stroke_linejoin = join; + cbp->stroke_linecap = cap; + cbp->dashes[0] = dash; + cbp->dashes[1] = gap; + + sp_canvas_item_request_update (SP_CANVAS_ITEM (cbp)); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/canvas-bpath.h b/src/display/canvas-bpath.h new file mode 100644 index 0000000..272c95b --- /dev/null +++ b/src/display/canvas-bpath.h @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_CANVAS_BPATH_H +#define SEEN_SP_CANVAS_BPATH_H + +/* + * Simple bezier bpath CanvasItem for inkscape + * + * Authors: + * Lauris Kaplinski <lauris@ximian.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2001 Lauris Kaplinski and Ximian, Inc. + * Copyright (C) 2010 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + */ + +#include <cstdint> +#include <glib.h> + +#include "sp-canvas-item.h" + +struct SPCanvasBPath; +struct SPCanvasBPathClass; +struct SPCanvasGroup; +class SPCurve; + +#define SP_TYPE_CANVAS_BPATH (sp_canvas_bpath_get_type ()) +#define SP_CANVAS_BPATH(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SP_TYPE_CANVAS_BPATH, SPCanvasBPath)) +#define SP_CANVAS_BPATH_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SP_TYPE_CANVAS_BPATH, SPCanvasBPathClass)) +#define SP_IS_CANVAS_BPATH(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SP_TYPE_CANVAS_BPATH)) +#define SP_IS_CANVAS_BPATH_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), SP_TYPE_CANVAS_BPATH)) + +#define bpath_liv + +class Shape; + +/* stroke-linejoin */ + +enum SPStrokeJoinType : std::uint_least8_t { + SP_STROKE_LINEJOIN_MITER, + SP_STROKE_LINEJOIN_ROUND, + SP_STROKE_LINEJOIN_BEVEL +}; + +/* stroke-linecap */ + +enum SPStrokeCapType : std::uint_least8_t { + SP_STROKE_LINECAP_BUTT, + SP_STROKE_LINECAP_ROUND, + SP_STROKE_LINECAP_SQUARE +}; + + +/* fill-rule */ +/* clip-rule */ + +enum SPWindRule : std::uint_least8_t { + SP_WIND_RULE_NONZERO, + SP_WIND_RULE_INTERSECT, + SP_WIND_RULE_EVENODD, + SP_WIND_RULE_POSITIVE +}; + + +struct SPCanvasBPath { + SPCanvasItem item; + + /* Line def */ + SPCurve *curve; + Geom::Affine affine; + + /* Fill attributes */ + guint32 fill_rgba; + SPWindRule fill_rule; + + /* Line attributes */ + guint32 stroke_rgba; + gdouble stroke_width; + gdouble dashes[2]; + SPStrokeJoinType stroke_linejoin; + SPStrokeCapType stroke_linecap; + gdouble stroke_miterlimit; + bool phantom_line; + /* State */ + Shape *fill_shp; + Shape *stroke_shp; +}; + +struct SPCanvasBPathClass { + SPCanvasItemClass parent_class; +}; + +GType sp_canvas_bpath_get_type (); + +SPCanvasItem *sp_canvas_bpath_new (SPCanvasGroup *parent, SPCurve *curve, bool phantom_line = false); + +void sp_canvas_bpath_set_bpath (SPCanvasBPath *cbp, SPCurve *curve, bool phantom_line = false); +void sp_canvas_bpath_set_fill (SPCanvasBPath *cbp, guint32 rgba, SPWindRule rule); +void sp_canvas_bpath_set_stroke (SPCanvasBPath *cbp, guint32 rgba, gdouble width, SPStrokeJoinType join, SPStrokeCapType cap, double dash=0, double gap=0); + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/canvas-debug.cpp b/src/display/canvas-debug.cpp new file mode 100644 index 0000000..03db61f --- /dev/null +++ b/src/display/canvas-debug.cpp @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A simple surface for debugging the canvas. Shows how tiles are drawn. + * + * Author: + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 2017 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "canvas-debug.h" +#include "sp-canvas.h" +#include "cairo-utils.h" +#include "ui/event-debug.h" + +namespace { + +static void sp_canvas_debug_destroy(SPCanvasItem *item); +static void sp_canvas_debug_update (SPCanvasItem *item, Geom::Affine const &affine, unsigned int flags); +static void sp_canvas_debug_render (SPCanvasItem *item, SPCanvasBuf *buf); +static int sp_canvas_debug_event (SPCanvasItem *item, GdkEvent *event); + +} // namespace + +G_DEFINE_TYPE(SPCanvasDebug, sp_canvas_debug, SP_TYPE_CANVAS_ITEM); + +static void sp_canvas_debug_class_init (SPCanvasDebugClass *klass) +{ + klass->destroy = sp_canvas_debug_destroy; + klass->update = sp_canvas_debug_update; + klass->render = sp_canvas_debug_render; + klass->event = sp_canvas_debug_event; +} + +static void sp_canvas_debug_init (SPCanvasDebug *debug) +{ + debug->pickable = true; // So we can receive events. +} + +namespace { +static void sp_canvas_debug_destroy (SPCanvasItem *object) +{ + g_return_if_fail (object != nullptr); + g_return_if_fail (SP_IS_CANVAS_DEBUG (object)); + + if (SP_CANVAS_ITEM_CLASS(sp_canvas_debug_parent_class)->destroy) { + SP_CANVAS_ITEM_CLASS(sp_canvas_debug_parent_class)->destroy(object); + } +} + +static void sp_canvas_debug_update( SPCanvasItem *item, Geom::Affine const &/*affine*/, unsigned int /*flags*/ ) +{ + // We cover the entire canvas + item->x1 = -G_MAXINT; + item->y1 = -G_MAXINT; + item->x2 = G_MAXINT; + item->y2 = G_MAXINT; +} + +static void sp_canvas_debug_render( SPCanvasItem *item, SPCanvasBuf *buf) +{ + if (!buf->ct) { + return; + } + + cairo_set_line_width (buf->ct, 2); + + // Draw box around buffer (for debugging) + cairo_new_path (buf->ct); + cairo_move_to (buf->ct, 0, 0); + cairo_line_to (buf->ct, buf->rect.width(), 0); + cairo_line_to (buf->ct, buf->rect.width(), buf->rect.height()); + cairo_line_to (buf->ct, 0, buf->rect.height()); + cairo_close_path (buf->ct); + ink_cairo_set_source_rgba32 (buf->ct, 0xff7f7f7f); + cairo_stroke (buf->ct); +} + +static int sp_canvas_debug_event (SPCanvasItem *item, GdkEvent *event) +{ + ui_dump_event (event, Glib::ustring("sp_canvas_debug_event")); + return false; // We don't use any events... +} + +} // namespace + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/canvas-debug.h b/src/display/canvas-debug.h new file mode 100644 index 0000000..19ccd15 --- /dev/null +++ b/src/display/canvas-debug.h @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_CANVAS_DEBUG_H +#define SEEN_SP_CANVAS_DEBUG_H + +/* + * A simple surface for debugging the canvas. Shows how tiles are drawn. + * + * Author: + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 2017 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-canvas-item.h" + +class SPItem; + +#define SP_TYPE_CANVAS_DEBUG (sp_canvas_debug_get_type ()) +#define SP_CANVAS_DEBUG(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SP_TYPE_CANVAS_DEBUG, SPCanvasDebug)) +#define SP_IS_CANVAS_DEBUG(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SP_TYPE_CANVAS_DEBUG)) + +struct SPCanvasDebug : public SPCanvasItem { +}; + +GType sp_canvas_debug_get_type (); + +struct SPCanvasDebugClass : public SPCanvasItemClass{}; + +#endif // SEEN_SP_CANVAS_DEBUG_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/display/canvas-grid.cpp b/src/display/canvas-grid.cpp new file mode 100644 index 0000000..0148b46 --- /dev/null +++ b/src/display/canvas-grid.cpp @@ -0,0 +1,1143 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Cartesian grid item for the Inkscape canvas. + *//* + * Authors: + * see git history + * Copyright (C) Johan Engelen 2006-2007 <johan@shouraizou.nl> + * Copyright (C) Lauris Kaplinski 2000 + * Abhishek Sharma + * Jon A. Cruz <jon@joncruz.org> + * Copyright (C) Tavmong Bah 2017 <tavmjong@free.fr> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +/* As a general comment, I am not exactly proud of how things are done. + * (for example the 'enable' widget and readRepr things) + * It does seem to work however. I intend to clean up and sort things out later, but that can take forever... + * Don't be shy to correct things. + */ + +#include <gtkmm/box.h> +#include <gtkmm/label.h> +#include <gtkmm/grid.h> + +#include <glibmm/i18n.h> + +#include "desktop.h" +#include "sp-canvas-util.h" +#include "helper/mathfns.h" + +#include "display/cairo-utils.h" +#include "display/canvas-axonomgrid.h" +#include "display/canvas-grid.h" +#include "display/sp-canvas-group.h" +#include "document.h" +#include "util/units.h" +#include "inkscape.h" +#include "preferences.h" +#include "object/sp-namedview.h" +#include "object/sp-object.h" +#include "object/sp-root.h" +#include "svg/svg-color.h" +#include "svg/stringstream.h" +#include "helper/mathfns.h" +#include "xml/node-event-vector.h" +#include "verbs.h" +#include "display/sp-canvas.h" + +using Inkscape::DocumentUndo; +using Inkscape::Util::unit_table; + +namespace Inkscape { + +static gchar const *const grid_name[] = { + N_("Rectangular grid"), + N_("Axonometric grid") +}; +static gchar const *const grid_svgname[] = { + "xygrid", + "axonomgrid" +}; + + +// ########################################################## +// Grid CanvasItem +static void grid_canvasitem_destroy(SPCanvasItem *object); +static void grid_canvasitem_update (SPCanvasItem *item, Geom::Affine const &affine, unsigned int flags); +static void grid_canvasitem_render (SPCanvasItem *item, SPCanvasBuf *buf); + +G_DEFINE_TYPE(GridCanvasItem, grid_canvasitem, SP_TYPE_CANVAS_ITEM); + +static void grid_canvasitem_class_init(GridCanvasItemClass *klass) +{ + SPCanvasItemClass *item_class = (SPCanvasItemClass *) klass; + + item_class->destroy = grid_canvasitem_destroy; + item_class->update = grid_canvasitem_update; + item_class->render = grid_canvasitem_render; +} + +static void +grid_canvasitem_init (GridCanvasItem *griditem) +{ + griditem->grid = nullptr; +} + +static void grid_canvasitem_destroy(SPCanvasItem *object) +{ + g_return_if_fail (object != nullptr); + g_return_if_fail (INKSCAPE_IS_GRID_CANVASITEM (object)); + + if (SP_CANVAS_ITEM_CLASS(grid_canvasitem_parent_class)->destroy) + (* SP_CANVAS_ITEM_CLASS(grid_canvasitem_parent_class)->destroy) (object); +} + +/** +*/ +static void +grid_canvasitem_render (SPCanvasItem * item, SPCanvasBuf * buf) +{ + GridCanvasItem *gridcanvasitem = INKSCAPE_GRID_CANVASITEM (item); + + if ( gridcanvasitem->grid && gridcanvasitem->grid->isVisible() ) { + sp_canvas_prepare_buffer (buf); + gridcanvasitem->grid->Render(buf); + } +} + +static void +grid_canvasitem_update (SPCanvasItem *item, Geom::Affine const &affine, unsigned int flags) +{ + GridCanvasItem *gridcanvasitem = INKSCAPE_GRID_CANVASITEM (item); + + if (SP_CANVAS_ITEM_CLASS(grid_canvasitem_parent_class)->update) + SP_CANVAS_ITEM_CLASS(grid_canvasitem_parent_class)->update(item, affine, flags); + + if (gridcanvasitem->grid) { + gridcanvasitem->grid->Update(affine, flags); + + item->canvas->requestRedraw(-1000000, -1000000, + 1000000, 1000000); + + item->x1 = item->y1 = -1000000; + item->x2 = item->y2 = 1000000; + } +} + + + +// ########################################################## +// CanvasGrid + + static Inkscape::XML::NodeEventVector const _repr_events = { + nullptr, /* child_added */ + nullptr, /* child_removed */ + CanvasGrid::on_repr_attr_changed, + nullptr, /* content_changed */ + nullptr /* order_changed */ + }; + +CanvasGrid::CanvasGrid(SPNamedView * nv, Inkscape::XML::Node * in_repr, SPDocument *in_doc, GridType type) + : visible(true), gridtype(type), legacy(false), pixel(false) +{ + repr = in_repr; + doc = in_doc; + if (repr) { + repr->addListener (&_repr_events, this); + } + + namedview = nv; +} + +CanvasGrid::~CanvasGrid() +{ + if (repr) { + repr->removeListenerByData (this); + } + for (auto i:canvasitems) + sp_canvas_item_destroy(i); + canvasitems.clear(); +} + +const char * +CanvasGrid::getName() const +{ + return _(grid_name[gridtype]); +} + +const char * +CanvasGrid::getSVGName() const +{ + return grid_svgname[gridtype]; +} + +GridType +CanvasGrid::getGridType() const +{ + return gridtype; +} + + +char const * +CanvasGrid::getName(GridType type) +{ + return _(grid_name[type]); +} + +char const * +CanvasGrid::getSVGName(GridType type) +{ + return grid_svgname[type]; +} + +GridType +CanvasGrid::getGridTypeFromSVGName(char const *typestr) +{ + if (!typestr) return GRID_RECTANGULAR; + + gint t = 0; + for (t = GRID_MAXTYPENR; t >= 0; t--) { //this automatically defaults to grid0 which is rectangular grid + if (!strcmp(typestr, grid_svgname[t])) break; + } + return (GridType) t; +} + +GridType +CanvasGrid::getGridTypeFromName(char const *typestr) +{ + if (!typestr) return GRID_RECTANGULAR; + + gint t = 0; + for (t = GRID_MAXTYPENR; t >= 0; t--) { //this automatically defaults to grid0 which is rectangular grid + if (!strcmp(typestr, _(grid_name[t]))) break; + } + return (GridType) t; +} + + +/* +* writes an <inkscape:grid> child to repr. +*/ +void +CanvasGrid::writeNewGridToRepr(Inkscape::XML::Node * repr, SPDocument * doc, GridType gridtype) +{ + if (!repr) return; + if (gridtype > GRID_MAXTYPENR) return; + + // first create the child xml node, then hook it to repr. This order is important, to not set off listeners to repr before the new node is complete. + + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + Inkscape::XML::Node *newnode; + newnode = xml_doc->createElement("inkscape:grid"); + newnode->setAttribute("type", getSVGName(gridtype)); + + repr->appendChild(newnode); + Inkscape::GC::release(newnode); + + DocumentUndo::done(doc, SP_VERB_DIALOG_NAMEDVIEW, _("Create new grid")); +} + +/* +* Creates a new CanvasGrid object of type gridtype +*/ +CanvasGrid* +CanvasGrid::NewGrid(SPNamedView * nv, Inkscape::XML::Node * repr, SPDocument * doc, GridType gridtype) +{ + if (!repr) return nullptr; + if (!doc) { + g_error("CanvasGrid::NewGrid - doc==NULL"); + return nullptr; + } + + switch (gridtype) { + case GRID_RECTANGULAR: + return dynamic_cast<CanvasGrid*>(new CanvasXYGrid(nv, repr, doc)); + case GRID_AXONOMETRIC: + return dynamic_cast<CanvasGrid*>(new CanvasAxonomGrid(nv, repr, doc)); + } + + return nullptr; +} + + +/** +* creates a new grid canvasitem for the SPDesktop given as parameter. Keeps a link to this canvasitem in the canvasitems list. +*/ +GridCanvasItem * +CanvasGrid::createCanvasItem(SPDesktop * desktop) +{ + if (!desktop) return nullptr; +// Johan: I think for multiple desktops it is best if each has their own canvasitem, +// but share the same CanvasGrid object; that is what this function is for. + + // check if there is already a canvasitem on this desktop linking to this grid + for (auto i:canvasitems) { + if ( desktop->getGridGroup() == SP_CANVAS_GROUP(i->parent) ) { + return nullptr; + } + } + + GridCanvasItem * item = INKSCAPE_GRID_CANVASITEM( sp_canvas_item_new(desktop->getGridGroup(), INKSCAPE_TYPE_GRID_CANVASITEM, nullptr) ); + item->grid = this; + sp_canvas_item_show(SP_CANVAS_ITEM(item)); + + g_object_ref(item); // since we're keeping a link to this item, we need to bump up the ref count + canvasitems.push_back(item); + + return item; +} + +Gtk::Widget * +CanvasGrid::newWidget() +{ + Gtk::VBox * vbox = Gtk::manage( new Gtk::VBox() ); + Gtk::Label * namelabel = Gtk::manage(new Gtk::Label("", Gtk::ALIGN_CENTER) ); + + Glib::ustring str("<b>"); + str += getName(); + str += "</b>"; + namelabel->set_markup(str); + vbox->pack_start(*namelabel, false, false); + + _rcb_enabled = Gtk::manage( new Inkscape::UI::Widget::RegisteredCheckButton( + _("_Enabled"), + _("Makes the grid available for working with on the canvas."), + "enabled", _wr, false, repr, doc) ); + + _rcb_snap_visible_only = Gtk::manage( new Inkscape::UI::Widget::RegisteredCheckButton( + _("Snap to visible _grid lines only"), + _("When zoomed out, not all grid lines will be displayed. Only the visible ones will be snapped to"), + "snapvisiblegridlinesonly", _wr, false, repr, doc) ); + + _rcb_visible = Gtk::manage( new Inkscape::UI::Widget::RegisteredCheckButton( + _("_Visible"), + _("Determines whether the grid is displayed or not. Objects are still snapped to invisible grids."), + "visible", _wr, false, repr, doc) ); + + _as_alignment = Gtk::manage( new Inkscape::UI::Widget::AlignmentSelector() ); + _as_alignment->on_alignmentClicked().connect(sigc::mem_fun(*this, &CanvasGrid::align_clicked)); + + Gtk::Box *left = new Gtk::Box(Gtk::ORIENTATION_VERTICAL, 4); + left->pack_start(*_rcb_enabled, false, false); + left->pack_start(*_rcb_visible, false, false); + left->pack_start(*_rcb_snap_visible_only, false, false); + + if (getGridType() == GRID_RECTANGULAR) { + _rcb_dotted = Gtk::manage( new Inkscape::UI::Widget::RegisteredCheckButton( + _("_Show dots instead of lines"), _("If set, displays dots at gridpoints instead of gridlines"), + "dotted", _wr, false, repr, doc) ); + _rcb_dotted->setActive(render_dotted); + left->pack_start(*_rcb_dotted, false, false); + } + + left->pack_start(*Gtk::manage(new Gtk::Label(_("Align to page:"))), false, false); + left->pack_start(*_as_alignment, false, false); + + auto right = newSpecificWidget(); + right->set_hexpand(false); + + Gtk::Box *inner = new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 4); + inner->pack_start(*left, true, true); + inner->pack_start(*right, false, false); + vbox->pack_start(*inner, false, false); + vbox->set_border_width(4); + + std::list<Gtk::Widget*> slaves; + for (auto &item : left->get_children()) { + if (item != _rcb_enabled) { + slaves.push_back(item); + } + } + slaves.push_back(right); + _rcb_enabled->setSlaveWidgets(slaves); + + // set widget values + _wr.setUpdating (true); + _rcb_visible->setActive(visible); + if (snapper != nullptr) { + _rcb_enabled->setActive(snapper->getEnabled()); + _rcb_snap_visible_only->setActive(snapper->getSnapVisibleOnly()); + } + _wr.setUpdating (false); + return dynamic_cast<Gtk::Widget *> (vbox); +} + +void +CanvasGrid::on_repr_attr_changed(Inkscape::XML::Node *repr, gchar const *key, gchar const *oldval, gchar const *newval, bool is_interactive, void *data) +{ + if (!data) + return; + + (static_cast<CanvasGrid*>(data))->onReprAttrChanged(repr, key, oldval, newval, is_interactive); +} + +bool CanvasGrid::isEnabled() const +{ + if (snapper == nullptr) { + return false; + } + + return snapper->getEnabled(); +} + +// Used to shift origin when page size changed to fit drawing. +void CanvasGrid::setOrigin(Geom::Point const &origin_px) +{ + SPRoot *root = doc->getRoot(); + double scale_x = 1.0; + double scale_y = 1.0; + if( root->viewBox_set ) { + scale_x = root->viewBox.width() / root->width.computed; + scale_y = root->viewBox.height() / root->height.computed; + } + + // Write out in 'user-units' + Inkscape::SVGOStringStream os_x, os_y; + os_x << origin_px[Geom::X] * scale_x; + os_y << origin_px[Geom::Y] * scale_y; + repr->setAttribute("originx", os_x.str()); + repr->setAttribute("originy", os_y.str()); +} + +void CanvasGrid::align_clicked(int align) +{ + Geom::Point dimensions = doc->getDimensions(); + dimensions[Geom::X] *= align % 3 * 0.5; + dimensions[Geom::Y] *= align / 3 * 0.5; + dimensions *= doc->doc2dt(); + setOrigin(dimensions); +} + + + +// ########################################################## +// CanvasXYGrid + +CanvasXYGrid::CanvasXYGrid (SPNamedView * nv, Inkscape::XML::Node * in_repr, SPDocument * in_doc) + : CanvasGrid(nv, in_repr, in_doc, GRID_RECTANGULAR) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gridunit = unit_table.getUnit(prefs->getString("/options/grids/xy/units")); + if (!gridunit) { + gridunit = unit_table.getUnit("px"); + } + origin[Geom::X] = Inkscape::Util::Quantity::convert(prefs->getDouble("/options/grids/xy/origin_x", 0.0), gridunit, "px"); + origin[Geom::Y] = Inkscape::Util::Quantity::convert(prefs->getDouble("/options/grids/xy/origin_y", 0.0), gridunit, "px"); + color = prefs->getInt("/options/grids/xy/color", GRID_DEFAULT_COLOR); + empcolor = prefs->getInt("/options/grids/xy/empcolor", GRID_DEFAULT_EMPCOLOR); + empspacing = prefs->getInt("/options/grids/xy/empspacing", 5); + spacing[Geom::X] = Inkscape::Util::Quantity::convert(prefs->getDouble("/options/grids/xy/spacing_x", 0.0), gridunit, "px"); + spacing[Geom::Y] = Inkscape::Util::Quantity::convert(prefs->getDouble("/options/grids/xy/spacing_y", 0.0), gridunit, "px"); + render_dotted = prefs->getBool("/options/grids/xy/dotted", false); + + snapper = new CanvasXYGridSnapper(this, &namedview->snap_manager, 0); + + if (repr) readRepr(); +} + +CanvasXYGrid::~CanvasXYGrid () +{ + if (snapper) delete snapper; +} + +static gboolean sp_nv_read_opacity(gchar const *str, guint32 *color) +{ + if (!str) { + return FALSE; + } + + gchar *u; + gdouble v = g_ascii_strtod(str, &u); + if (!u) { + return FALSE; + } + v = CLAMP(v, 0.0, 1.0); + + *color = (*color & 0xffffff00) | (guint32) floor(v * 255.9999); + + return TRUE; +} + +/** If the passed int is invalid (<=0), then set the widget and the int + to use the given old value. + + @param oldVal Old value to use if the new one is invalid. + @param pTarget The int to validate. + @param widget Widget associated with the int. +*/ +static void validateInt(gint oldVal, + gint* pTarget) +{ + // Avoid nullness. + if ( pTarget == nullptr ) + return; + + // Invalid new value? + if ( *pTarget <= 0 ) { + // If the old value is somehow invalid as well, then default to 1. + if ( oldVal <= 0 ) + oldVal = 1; + + // Reset the int and associated widget to the old value. + *pTarget = oldVal; + } //if + +} //validateInt + +void +CanvasXYGrid::readRepr() +{ + SPRoot *root = doc->getRoot(); + double scale_x = 1.0; + double scale_y = 1.0; + if( root->viewBox_set ) { + scale_x = root->width.computed / root->viewBox.width(); + scale_y = root->height.computed / root->viewBox.height(); + if (Geom::are_near(scale_x / scale_y, 1.0, Geom::EPSILON)) { + // scaling is uniform, try to reduce numerical error + scale_x = (scale_x + scale_y)/2.0; + double scale_none = Inkscape::Util::Quantity::convert(1, doc->getDisplayUnit(), "px"); + if (Geom::are_near(scale_x / scale_none, 1.0, Geom::EPSILON)) + scale_x = scale_none; // objects are same size, reduce numerical error + scale_y = scale_x; + } + } + + gchar const *value; + + if ( (value = repr->attribute("originx")) ) { + + Inkscape::Util::Quantity q = unit_table.parseQuantity(value); + + if( q.unit->type == UNIT_TYPE_LINEAR ) { + // Legacy grid not in 'user units' + origin[Geom::X] = q.value("px"); + legacy = true; + if (q.unit->abbr == "px" ) { + pixel = true; + } + } else { + // Grid in 'user units' + origin[Geom::X] = q.quantity * scale_x; + } + } + + if ( (value = repr->attribute("originy")) ) { + + Inkscape::Util::Quantity q = unit_table.parseQuantity(value); + + if( q.unit->type == UNIT_TYPE_LINEAR ) { + // Legacy grid not in 'user units' + origin[Geom::Y] = q.value("px"); + legacy = true; + if (q.unit->abbr == "px" ) { + pixel = true; + } + } else { + // Grid in 'user units' + origin[Geom::Y] = q.quantity * scale_y; + } + } + + if ( (value = repr->attribute("spacingx")) ) { + + // Ensure a valid default value + if( spacing[Geom::X] <= 0.0 ) + spacing[Geom::X] = 1.0; + + Inkscape::Util::Quantity q = unit_table.parseQuantity(value); + // Ensure a valid new value + if( q.quantity > 0 ) { + if( q.unit->type == UNIT_TYPE_LINEAR ) { + // Legacy grid not in 'user units' + spacing[Geom::X] = q.value("px"); + legacy = true; + if (q.unit->abbr == "px" ) { + pixel = true; + } + } else { + // Grid in 'user units' + spacing[Geom::X] = q.quantity * scale_x; + } + } + } + + if ( (value = repr->attribute("spacingy")) ) { + + // Ensure a valid default value + if( spacing[Geom::Y] <= 0.0 ) + spacing[Geom::Y] = 1.0; + + Inkscape::Util::Quantity q = unit_table.parseQuantity(value); + // Ensure a valid new value + if( q.quantity > 0 ) { + if( q.unit->type == UNIT_TYPE_LINEAR ) { + // Legacy grid not in 'user units' + spacing[Geom::Y] = q.value("px"); + legacy = true; + if (q.unit->abbr == "px" ) { + pixel = true; + } + } else { + // Grid in 'user units' + spacing[Geom::Y] = q.quantity * scale_y; + } + } + } + + if ( (value = repr->attribute("color")) ) { + color = (color & 0xff) | sp_svg_read_color(value, color); + } + + if ( (value = repr->attribute("empcolor")) ) { + empcolor = (empcolor & 0xff) | sp_svg_read_color(value, empcolor); + } + + if ( (value = repr->attribute("opacity")) ) { + sp_nv_read_opacity(value, &color); + } + if ( (value = repr->attribute("empopacity")) ) { + sp_nv_read_opacity(value, &empcolor); + } + + if ( (value = repr->attribute("empspacing")) ) { + gint oldVal = empspacing; + empspacing = atoi(value); + validateInt( oldVal, &empspacing); + } + + if ( (value = repr->attribute("dotted")) ) { + render_dotted = (strcmp(value,"false") != 0 && strcmp(value, "0") != 0); + } + + if ( (value = repr->attribute("visible")) ) { + visible = (strcmp(value,"false") != 0 && strcmp(value, "0") != 0); + } + + if ( (value = repr->attribute("enabled")) ) { + g_assert(snapper != nullptr); + snapper->setEnabled(strcmp(value,"false") != 0 && strcmp(value, "0") != 0); + } + + if ( (value = repr->attribute("snapvisiblegridlinesonly")) ) { + g_assert(snapper != nullptr); + snapper->setSnapVisibleOnly(strcmp(value,"false") != 0 && strcmp(value, "0") != 0); + } + + if ( (value = repr->attribute("units")) ) { + gridunit = unit_table.getUnit(value); // Display unit identifier in grid menu + } + + for (auto i:canvasitems) { + sp_canvas_item_request_update(i); + } + + return; +} + +/** + * Called when XML node attribute changed; updates dialog widgets if change was not done by widgets themselves. + */ +void +CanvasXYGrid::onReprAttrChanged(Inkscape::XML::Node */*repr*/, gchar const */*key*/, gchar const */*oldval*/, gchar const */*newval*/, bool /*is_interactive*/) +{ + readRepr(); + updateWidgets(); +} + + +Gtk::Widget * +CanvasXYGrid::newSpecificWidget() +{ + _rumg = Gtk::manage( new Inkscape::UI::Widget::RegisteredUnitMenu( + _("Grid _units:"), "units", _wr, repr, doc) ); + _rsu_ox = Gtk::manage( new Inkscape::UI::Widget::RegisteredScalarUnit( + _("_Origin X:"), _("X coordinate of grid origin"), "originx", + *_rumg, _wr, repr, doc, Inkscape::UI::Widget::RSU_x) ); + _rsu_oy = Gtk::manage( new Inkscape::UI::Widget::RegisteredScalarUnit( + _("O_rigin Y:"), _("Y coordinate of grid origin"), "originy", + *_rumg, _wr, repr, doc, Inkscape::UI::Widget::RSU_y) ); + _rsu_sx = Gtk::manage( new Inkscape::UI::Widget::RegisteredScalarUnit( + _("Spacing _X:"), _("Distance between vertical grid lines"), "spacingx", + *_rumg, _wr, repr, doc, Inkscape::UI::Widget::RSU_x) ); + _rsu_sy = Gtk::manage( new Inkscape::UI::Widget::RegisteredScalarUnit( + _("Spacing _Y:"), _("Distance between horizontal grid lines"), "spacingy", + *_rumg, _wr, repr, doc, Inkscape::UI::Widget::RSU_y) ); + + _rcp_gcol = Gtk::manage( new Inkscape::UI::Widget::RegisteredColorPicker( + _("Minor grid line _color:"), _("Minor grid line color"), _("Color of the minor grid lines"), + "color", "opacity", _wr, repr, doc) ); + + _rcp_gmcol = Gtk::manage( new Inkscape::UI::Widget::RegisteredColorPicker( + _("Ma_jor grid line color:"), _("Major grid line color"), + _("Color of the major (highlighted) grid lines"), "empcolor", "empopacity", + _wr, repr, doc) ); + + _rsi = Gtk::manage( new Inkscape::UI::Widget::RegisteredSuffixedInteger( + _("_Major grid line every:"), "", _("lines"), "empspacing", _wr, repr, doc) ); + + _rumg->set_hexpand(); + _rsu_ox->set_hexpand(); + _rsu_oy->set_hexpand(); + _rsu_sx->set_hexpand(); + _rsu_sy->set_hexpand(); + _rcp_gcol->set_hexpand(); + _rcp_gmcol->set_hexpand(); + _rsi->set_hexpand(); + + // set widget values + _wr.setUpdating (true); + + _rsu_ox->setDigits(5); + _rsu_ox->setIncrements(0.1, 1.0); + + _rsu_oy->setDigits(5); + _rsu_oy->setIncrements(0.1, 1.0); + + _rsu_sx->setDigits(5); + _rsu_sx->setIncrements(0.1, 1.0); + + _rsu_sy->setDigits(5); + _rsu_sy->setIncrements(0.1, 1.0); + + _rumg->setUnit (gridunit->abbr); + + gdouble val; + val = origin[Geom::X]; + val = Inkscape::Util::Quantity::convert(val, "px", gridunit); + _rsu_ox->setValue (val); + val = origin[Geom::Y]; + val = Inkscape::Util::Quantity::convert(val, "px", gridunit); + _rsu_oy->setValue (val); + val = spacing[Geom::X]; + double gridx = Inkscape::Util::Quantity::convert(val, "px", gridunit); + _rsu_sx->setValue (gridx); + val = spacing[Geom::Y]; + double gridy = Inkscape::Util::Quantity::convert(val, "px", gridunit); + _rsu_sy->setValue (gridy); + + _rcp_gcol->setRgba32 (color); + _rcp_gmcol->setRgba32 (empcolor); + _rsi->setValue (empspacing); + + _wr.setUpdating (false); + + _rsu_ox->setProgrammatically = false; + _rsu_oy->setProgrammatically = false; + _rsu_sx->setProgrammatically = false; + _rsu_sy->setProgrammatically = false; + + Gtk::Box *column = new Gtk::Box(Gtk::ORIENTATION_VERTICAL, 4); + column->pack_start(*_rumg, true, false); + column->pack_start(*_rsu_ox, true, false); + column->pack_start(*_rsu_oy, true, false); + column->pack_start(*_rsu_sx, true, false); + column->pack_start(*_rsu_sy, true, false); + column->pack_start(*_rcp_gcol, true, false); + column->pack_start(*_rcp_gmcol, true, false); + column->pack_start(*_rsi, true, false); + + return column; +} + + +/** + * Update dialog widgets from object's values. + */ +void +CanvasXYGrid::updateWidgets() +{ + if (_wr.isUpdating()) return; + + //no widgets (grid created with the document, not with the dialog) + if (!_rcb_visible) return; + + _wr.setUpdating (true); + + _rcb_visible->setActive(visible); + if (snapper != nullptr) { + _rcb_enabled->setActive(snapper->getEnabled()); + _rcb_snap_visible_only->setActive(snapper->getSnapVisibleOnly()); + } + + _rumg->setUnit (gridunit->abbr); + + gdouble val; + + val = origin[Geom::X]; + val = Inkscape::Util::Quantity::convert(val, "px", gridunit); + _rsu_ox->setValue (val); + + val = origin[Geom::Y]; + val = Inkscape::Util::Quantity::convert(val, "px", gridunit); + _rsu_oy->setValue (val); + + val = spacing[Geom::X]; + val = Inkscape::Util::Quantity::convert(val, "px", gridunit); + _rsu_sx->setValue (val); + + val = spacing[Geom::Y]; + val = Inkscape::Util::Quantity::convert(val, "px", gridunit); + _rsu_sy->setValue (val); + + _rsu_ox->setProgrammatically = false; + _rsu_oy->setProgrammatically = false; + _rsu_sx->setProgrammatically = false; + _rsu_sy->setProgrammatically = false; + + _rcp_gcol->setRgba32 (color); + _rcp_gmcol->setRgba32 (empcolor); + _rsi->setValue (empspacing); + _rcb_dotted->setActive (render_dotted); + + _wr.setUpdating (false); +} + +// For correcting old SVG Inkscape files +void +CanvasXYGrid::Scale (Geom::Scale const &scale ) { + origin *= scale; + spacing *= scale; + + // Write out in 'user-units' + Inkscape::SVGOStringStream os_x, os_y, ss_x, ss_y; + os_x << origin[Geom::X]; + os_y << origin[Geom::Y]; + ss_x << spacing[Geom::X]; + ss_y << spacing[Geom::Y]; + repr->setAttribute("originx", os_x.str()); + repr->setAttribute("originy", os_y.str()); + repr->setAttribute("spacingx", ss_x.str()); + repr->setAttribute("spacingy", ss_y.str()); +} + +void +CanvasXYGrid::Update (Geom::Affine const &affine, unsigned int /*flags*/) +{ + ow = origin * affine; + sw[0] = Geom::Point(spacing[0], 0) * affine.withoutTranslation(); + sw[1] = Geom::Point(0, spacing[1]) * affine.withoutTranslation(); + + // Find suitable grid spacing for display + for(int dim = 0; dim < 2; dim++) { + gint scaling_factor = empspacing; + + if (scaling_factor <= 1) + scaling_factor = 5; + + scaled[dim] = false; + while (fabs(sw[dim].length()) < 8.0) { + scaled[dim] = true; + sw[dim] *= scaling_factor; + /* First pass, go up to the major line spacing, then + keep increasing by two. */ + scaling_factor = 2; + } + } +} + + +// Find intersections of line with rectangle. There should be zero or two. +// If line is degenerate with rectangle side, two corner points are returned. +static std::vector<Geom::Point> +intersect_line_rectangle( Geom::Line const &line, Geom::Rect const &rect ) +{ + std::vector<Geom::Point> intersections; + for (unsigned i = 0; i < 4; ++i) { + Geom::LineSegment side( rect.corner(i), rect.corner((i+1)%4) ); + try { + Geom::OptCrossing oc = Geom::intersection(line, side); + if (oc) { + intersections.push_back( line.pointAt((*oc).ta)); + } + } catch (Geom::InfiniteSolutions) { + intersections.clear(); + intersections.push_back( side.pointAt(0) ); + intersections.push_back( side.pointAt(1) ); + return intersections; + } + } + return intersections; +} + +// Find the signed distance of a point to a line. The distance is negative if +// the point lies to the left of the line considering the line's versor. +static double +signed_distance( Geom::Point const &point, Geom::Line const &line ) { + Geom::Coord t = line.nearestTime( point ); + Geom::Point p = line.pointAt(t); + double distance = Geom::distance( p, point ); + if ( Geom::cross( Geom::Line( p, point ).versor(), line.versor() ) < 0.0 ) { + distance = -distance; + } + return distance; +} + +void +CanvasXYGrid::Render (SPCanvasBuf *buf) +{ + + // no_emphasize_when_zoomedout determines color (minor or major) when only major grid lines/dots shown. + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + guint32 _empcolor; + guint32 _color = color; + bool no_emp_when_zoomed_out = prefs->getBool("/options/grids/no_emphasize_when_zoomedout", false); + if( (scaled[Geom::X] || scaled[Geom::Y]) && no_emp_when_zoomed_out ) { + _empcolor = color; + } else { + _empcolor = empcolor; + } + + bool xrayactive = prefs->getBool("/desktop/xrayactive", false); + if (xrayactive) { + guint32 bg = namedview->pagecolor; + _color = SP_RGBA32_F_COMPOSE( + CLAMP(((1 - SP_RGBA32_A_F(_color)) * SP_RGBA32_R_F(bg)) + (SP_RGBA32_A_F(_color) * SP_RGBA32_R_F(_color)), 0.0, 1.0), + CLAMP(((1 - SP_RGBA32_A_F(_color)) * SP_RGBA32_G_F(bg)) + (SP_RGBA32_A_F(_color) * SP_RGBA32_G_F(_color)), 0.0, 1.0), + CLAMP(((1 - SP_RGBA32_A_F(_color)) * SP_RGBA32_B_F(bg)) + (SP_RGBA32_A_F(_color) * SP_RGBA32_B_F(_color)), 0.0, 1.0), + 1.0); + _empcolor = SP_RGBA32_F_COMPOSE( + CLAMP(((1 - SP_RGBA32_A_F(_empcolor)) * SP_RGBA32_R_F(bg)) + (SP_RGBA32_A_F(_empcolor) * SP_RGBA32_R_F(_empcolor)), 0.0, 1.0), + CLAMP(((1 - SP_RGBA32_A_F(_empcolor)) * SP_RGBA32_G_F(bg)) + (SP_RGBA32_A_F(_empcolor) * SP_RGBA32_G_F(_empcolor)), 0.0, 1.0), + CLAMP(((1 - SP_RGBA32_A_F(_empcolor)) * SP_RGBA32_B_F(bg)) + (SP_RGBA32_A_F(_empcolor) * SP_RGBA32_B_F(_empcolor)), 0.0, 1.0), + 1.0); + } + cairo_save(buf->ct); + cairo_translate(buf->ct, -buf->rect.left(), -buf->rect.top()); + cairo_set_line_width(buf->ct, 1.0); + cairo_set_line_cap(buf->ct, CAIRO_LINE_CAP_SQUARE); + + // Adding a 2 px margin to the buffer rectangle to avoid missing intersections (in case of rounding errors, and due to adding 0.5 below) + Geom::IntRect buf_rect_with_margin = buf->rect; + buf_rect_with_margin.expandBy(2); + + for (unsigned dim = 0; dim < 2; ++dim) { + + // std::cout << "\n " << (dim==0?"Horizontal":"Vertical") << " ------------" << std::endl; + + // Construct an axis line through origin with direction normal to grid spacing. + Geom::Line axis = Geom::Line::from_origin_and_vector( ow, sw[dim] ); + Geom::Line orth = Geom::Line::from_origin_and_vector( ow, sw[(dim+1)%2] ); + + double spacing = sw[(dim+1)%2].length(); // Spacing between grid lines. + double dash = sw[dim].length(); // Total length of dash pattern. + + // std::cout << " axis: " << axis.origin() << ", " << axis.vector() << std::endl; + // std::cout << " spacing: " << spacing << std::endl; + // std::cout << " dash period: " << dash << std::endl; + + // Find the minimum and maximum distances of the buffer corners from axis. + double min = 1000000; + double max = -1000000; + for (unsigned c = 0; c < 4; ++c) { + + // We need signed distance... lib2geom offers only positive distance. + double distance = signed_distance( buf_rect_with_margin.corner(c), axis ); + + // Correct it for coordinate flips (inverts handedness). + if (Geom::cross( axis.vector(), orth.vector() ) > 0 ) { + distance = -distance; + } + + if (distance < min) + min = distance; + if (distance > max) + max = distance; + } + int start = floor( min/spacing ); + int stop = floor( max/spacing ); + + // std::cout << " rect: " << buf->rect << std::endl; + // std::cout << " min: " << min << " max: " << max << std::endl; + // std::cout << " start: " << start << " stop: " << stop << std::endl; + + // Loop over grid lines that intersected buf rectangle. + for (int j = start+1; j <= stop; ++j) { + + Geom::Line grid_line = Geom::make_parallel_line( ow + j * sw[(dim+1)%2], axis ); + + std::vector<Geom::Point> x = intersect_line_rectangle( grid_line, buf_rect_with_margin ); + + // If we have two intersections, grid line intersects buffer rectangle. + if (x.size() == 2 ) { + // Make sure lines are always drawn in the same direction (or dashes misplaced). + Geom::Line vector( x[0], x[1]); + if (Geom::dot( vector.vector(), axis.vector() ) < 0.0) { + std::swap(x[0], x[1]); + } + + // Set up line. Need to use floor()+0.5 such that Cairo will draw us lines with a width of a single pixel, without any aliasing. + // For this we need to position the lines at exactly half pixels, see https://www.cairographics.org/FAQ/#sharp_lines + // Must be consistent with the pixel alignment of the guide lines, see CanvasXYGrid::Render(), and the drawing of the rulers + cairo_move_to(buf->ct, floor(x[0][Geom::X]) + 0.5, floor(x[0][Geom::Y]) + 0.5); + cairo_line_to(buf->ct, floor(x[1][Geom::X]) + 0.5, floor(x[1][Geom::Y]) + 0.5); + + // Set dash pattern and color. + if (render_dotted) { + // alpha needs to be larger than in the line case to maintain a similar + // visual impact but setting it to the maximal value makes the dots + // dominant in some cases. Solution, increase the alpha by a factor of + // 4. This then allows some user adjustment. + guint32 _empdot = (_empcolor & 0xff) << 2; + if (_empdot > 0xff) + _empdot = 0xff; + _empdot += (_empcolor & 0xffffff00); + + guint32 _colordot = (_color & 0xff) << 2; + if (_colordot > 0xff) + _colordot = 0xff; + _colordot += (_color & 0xffffff00); + + // Dash pattern must use spacing from orthogonal direction. + // Offset is to center dash on orthogonal lines. + double offset = fmod( signed_distance( x[0], orth ), sw[dim].length()); + if (Geom::cross( axis.vector(), orth.vector() ) > 0 ) { + offset = -offset; + } + + double dashes[2]; + if (!scaled[dim] && (j % empspacing) != 0) { + // Minor lines + dashes[0] = 1; + dashes[1] = dash -1; + offset -= 0.5; + ink_cairo_set_source_rgba32(buf->ct, _colordot); + } else { + // Major lines + dashes[0] = 3; + dashes[1] = dash - 3; + offset -= 1.5; // Center dash on intersection. + ink_cairo_set_source_rgba32(buf->ct, _empdot); + } + + cairo_set_line_cap(buf->ct, CAIRO_LINE_CAP_BUTT); + cairo_set_dash(buf->ct, dashes, 2, -offset); + + } else { + + // Solid lines + + // Set color + if (!scaled[dim] && (j % empspacing) != 0) { + ink_cairo_set_source_rgba32(buf->ct, _color ); + } else { + ink_cairo_set_source_rgba32(buf->ct, _empcolor ); + } + } + + cairo_stroke(buf->ct); + + } else { + std::cerr << "CanvasXYGrid::render: Grid line doesn't intersect!" << std::endl; + } + } + } + + cairo_restore(buf->ct); +} + +CanvasXYGridSnapper::CanvasXYGridSnapper(CanvasXYGrid *grid, SnapManager *sm, Geom::Coord const d) : LineSnapper(sm, d) +{ + this->grid = grid; +} + +/** + * \return Snap tolerance (desktop coordinates); depends on current zoom so that it's always the same in screen pixels + */ +Geom::Coord CanvasXYGridSnapper::getSnapperTolerance() const +{ + SPDesktop const *dt = _snapmanager->getDesktop(); + double const zoom = dt ? dt->current_zoom() : 1; + return _snapmanager->snapprefs.getGridTolerance() / zoom; +} + +bool CanvasXYGridSnapper::getSnapperAlwaysSnap() const +{ + return _snapmanager->snapprefs.getGridTolerance() == 10000; //TODO: Replace this threshold of 10000 by a constant; see also tolerance-slider.cpp +} + +LineSnapper::LineList +CanvasXYGridSnapper::_getSnapLines(Geom::Point const &p) const +{ + LineList s; + + if ( grid == nullptr ) { + return s; + } + + for (unsigned int i = 0; i < 2; ++i) { + + double spacing; + + if (getSnapVisibleOnly()) { + // Only snapping to visible grid lines + spacing = grid->sw[i].length(); // this is the spacing of the visible grid lines measured in screen pixels + // convert screen pixels to px + // FIXME: after we switch to snapping dist in screen pixels, this will be unnecessary + SPDesktop const *dt = _snapmanager->getDesktop(); + if (dt) { + spacing /= dt->current_zoom(); + } + } else { + // Snapping to any grid line, whether it's visible or not + spacing = grid->spacing[i]; + } + + Geom::Coord rounded; + Geom::Point point_on_line; + Geom::Point cvec(0.,0.); + cvec[i] = 1.; + + rounded = Inkscape::Util::round_to_upper_multiple_plus(p[i], spacing, grid->origin[i]); + point_on_line = i ? Geom::Point(0, rounded) : Geom::Point(rounded, 0); + s.push_back(std::make_pair(cvec, point_on_line)); + + rounded = Inkscape::Util::round_to_lower_multiple_plus(p[i], spacing, grid->origin[i]); + point_on_line = i ? Geom::Point(0, rounded) : Geom::Point(rounded, 0); + s.push_back(std::make_pair(cvec, point_on_line)); + } + + return s; +} + +void CanvasXYGridSnapper::_addSnappedLine(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, Geom::Point const &normal_to_line, Geom::Point const &point_on_line) const +{ + SnappedLine dummy = SnappedLine(snapped_point, snapped_distance, source, source_num, Inkscape::SNAPTARGET_GRID, getSnapperTolerance(), getSnapperAlwaysSnap(), normal_to_line, point_on_line); + isr.grid_lines.push_back(dummy); +} + +void CanvasXYGridSnapper::_addSnappedPoint(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, bool constrained_snap) const +{ + SnappedPoint dummy = SnappedPoint(snapped_point, source, source_num, Inkscape::SNAPTARGET_GRID, snapped_distance, getSnapperTolerance(), getSnapperAlwaysSnap(), constrained_snap, true); + isr.points.push_back(dummy); +} + +void CanvasXYGridSnapper::_addSnappedLinePerpendicularly(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, bool constrained_snap) const +{ + SnappedPoint dummy = SnappedPoint(snapped_point, source, source_num, Inkscape::SNAPTARGET_GRID_PERPENDICULAR, snapped_distance, getSnapperTolerance(), getSnapperAlwaysSnap(), constrained_snap, true); + isr.points.push_back(dummy); +} + +/** + * \return true if this Snapper will snap at least one kind of point. + */ +bool CanvasXYGridSnapper::ThisSnapperMightSnap() const +{ + return _snap_enabled && _snapmanager->snapprefs.isTargetSnappable(Inkscape::SNAPTARGET_GRID); +} + +} // namespace Inkscape + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/canvas-grid.h b/src/display/canvas-grid.h new file mode 100644 index 0000000..73fe587 --- /dev/null +++ b/src/display/canvas-grid.h @@ -0,0 +1,216 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Cartesian grid item for the Inkscape canvas. + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Copyright (C) Johan Engelen 2006-2007 <johan@shouraizou.nl> + * Copyright (C) Lauris Kaplinski 2000 + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_CANVAS_GRID_H +#define INKSCAPE_CANVAS_GRID_H + +#include "sp-canvas-item.h" +#include "ui/widget/alignment-selector.h" +#include "ui/widget/registered-widget.h" +#include "ui/widget/registry.h" +#include "line-snapper.h" + +class SPDesktop; +class SPNamedView; +struct SPCanvasBuf; +class SPDocument; + +namespace Gtk { + class Widget; +} + +namespace Inkscape { +class Snapper; + +namespace XML { +class Node; +} + +namespace Util { +class Unit; +} + +enum GridType { + GRID_RECTANGULAR = 0, + GRID_AXONOMETRIC = 1 +}; +#define GRID_MAXTYPENR 1 + +#define INKSCAPE_TYPE_GRID_CANVASITEM (Inkscape::grid_canvasitem_get_type ()) +#define INKSCAPE_GRID_CANVASITEM(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), INKSCAPE_TYPE_GRID_CANVASITEM, GridCanvasItem)) +#define INKSCAPE_GRID_CANVASITEM_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), INKSCAPE_TYPE_GRID_CANVASITEM, GridCanvasItem)) +#define INKSCAPE_IS_GRID_CANVASITEM(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), INKSCAPE_TYPE_GRID_CANVASITEM)) +#define INKSCAPE_IS_GRID_CANVASITEM_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), INKSCAPE_TYPE_GRID_CANVASITEM)) + +#define GRID_DEFAULT_COLOR 0x3F3FFF20 +#define GRID_DEFAULT_EMPCOLOR 0x3F3FFF40 + +class CanvasGrid; + +/** All the variables that are tracked for a grid specific canvas item. */ +struct GridCanvasItem : public SPCanvasItem{ + CanvasGrid *grid; // the owning grid object +}; + +struct GridCanvasItemClass { + SPCanvasItemClass parent_class; +}; + +/* Standard Gtk function */ +GType grid_canvasitem_get_type (); + + + +class CanvasGrid { +public: + virtual ~CanvasGrid(); + + // TODO: see effect.h and effect.cpp from live_effects how to link enums to SVGname to typename properly. (johan) + const char * getName() const; + const char * getSVGName() const; + GridType getGridType() const; + static const char * getName(GridType type); + static const char * getSVGName(GridType type); + static GridType getGridTypeFromSVGName(const char * typestr); + static GridType getGridTypeFromName(const char * typestr); + + static CanvasGrid* NewGrid(SPNamedView * nv, Inkscape::XML::Node * repr, SPDocument *doc, GridType gridtype); + static void writeNewGridToRepr(Inkscape::XML::Node * repr, SPDocument * doc, GridType gridtype); + + GridCanvasItem * createCanvasItem(SPDesktop * desktop); + + virtual void Update (Geom::Affine const &affine, unsigned int flags) = 0; + virtual void Render (SPCanvasBuf *buf) = 0; + + virtual void readRepr() = 0; + virtual void onReprAttrChanged (Inkscape::XML::Node * /*repr*/, char const */*key*/, char const */*oldval*/, char const */*newval*/, bool /*is_interactive*/) = 0; + + Gtk::Widget * newWidget(); + + void setOrigin(Geom::Point const &origin_px); /**< writes new origin (specified in px units) to SVG */ + Geom::Point origin; /**< Origin of the grid */ + + guint32 color; /**< Color for normal lines */ + guint32 empcolor; /**< Color for emphasis lines */ + gint empspacing; /**< Spacing between emphasis lines */ + + Inkscape::Util::Unit const* gridunit; /**< points to Unit object in UnitTable (so don't delete it) */ + + Inkscape::XML::Node * repr; + SPDocument *doc; + + Inkscape::Snapper* snapper; + + static void on_repr_attr_changed (Inkscape::XML::Node * repr, const gchar *key, const gchar *oldval, const gchar *newval, bool is_interactive, void * data); + + bool isLegacy() const { return legacy; } + bool isPixel() const { return pixel; } + + bool isVisible() const { return (isEnabled() &&visible); }; + bool isEnabled() const; + + void align_clicked(int align); + +protected: + CanvasGrid(SPNamedView * nv, Inkscape::XML::Node * in_repr, SPDocument *in_doc, GridType type); + + virtual Gtk::Widget * newSpecificWidget() = 0; + + std::vector<SPCanvasItem*> canvasitems; // list of created canvasitems + + SPNamedView * namedview; + + Inkscape::UI::Widget::Registry _wr; + bool visible; + bool render_dotted; + + GridType gridtype; + + // For dealing with old Inkscape SVG files (pre 0.92) + bool legacy; + bool pixel; + + Inkscape::UI::Widget::RegisteredCheckButton *_rcb_enabled = nullptr; + Inkscape::UI::Widget::RegisteredCheckButton *_rcb_snap_visible_only = nullptr; + Inkscape::UI::Widget::RegisteredCheckButton *_rcb_visible = nullptr; + Inkscape::UI::Widget::RegisteredCheckButton *_rcb_dotted = nullptr; + Inkscape::UI::Widget::AlignmentSelector *_as_alignment = nullptr; + +private: + CanvasGrid(const CanvasGrid&) = delete; + CanvasGrid& operator=(const CanvasGrid&) = delete; +}; + + +class CanvasXYGrid : public CanvasGrid { +public: + CanvasXYGrid(SPNamedView * nv, Inkscape::XML::Node * in_repr, SPDocument * in_doc); + ~CanvasXYGrid() override; + + virtual void Scale (Geom::Scale const &scale); + void Update (Geom::Affine const &affine, unsigned int flags) override; + void Render (SPCanvasBuf *buf) override; + + void readRepr() override; + void onReprAttrChanged (Inkscape::XML::Node * repr, char const *key, char const *oldval, char const *newval, bool is_interactive) override; + + Geom::Point spacing; /**< Spacing between elements of the grid */ + bool scaled[2]; /**< Whether the grid is in scaled mode, which can + be different in the X or Y direction, hence two + variables */ + Geom::Point ow; /**< Transformed origin by the affine for the zoom */ + Geom::Point sw[2]; /**< Transformed spacing by the affine for the zoom */ + +protected: + Gtk::Widget * newSpecificWidget() override; + +private: + CanvasXYGrid(const CanvasXYGrid&) = delete; + CanvasXYGrid& operator=(const CanvasXYGrid&) = delete; + + void updateWidgets(); + + Inkscape::UI::Widget::RegisteredUnitMenu *_rumg = nullptr; + Inkscape::UI::Widget::RegisteredScalarUnit *_rsu_ox = nullptr; + Inkscape::UI::Widget::RegisteredScalarUnit *_rsu_oy = nullptr; + Inkscape::UI::Widget::RegisteredScalarUnit *_rsu_sx = nullptr; + Inkscape::UI::Widget::RegisteredScalarUnit *_rsu_sy = nullptr; + Inkscape::UI::Widget::RegisteredColorPicker *_rcp_gcol = nullptr; + Inkscape::UI::Widget::RegisteredColorPicker *_rcp_gmcol = nullptr; + Inkscape::UI::Widget::RegisteredSuffixedInteger *_rsi = nullptr; +}; + + + +class CanvasXYGridSnapper : public LineSnapper +{ +public: + CanvasXYGridSnapper(CanvasXYGrid *grid, SnapManager *sm, Geom::Coord const d); + bool ThisSnapperMightSnap() const override; + + Geom::Coord getSnapperTolerance() const override; //returns the tolerance of the snapper in screen pixels (i.e. independent of zoom) + bool getSnapperAlwaysSnap() const override; //if true, then the snapper will always snap, regardless of its tolerance + +private: + LineList _getSnapLines(Geom::Point const &p) const override; + void _addSnappedLine(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, Geom::Point const &normal_to_line, const Geom::Point &point_on_line) const override; + void _addSnappedPoint(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, bool constrained_snap) const override; + void _addSnappedLinePerpendicularly(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, bool constrained_snap) const override; + CanvasXYGrid *grid; +}; + +}; /* namespace Inkscape */ + + + + +#endif diff --git a/src/display/canvas-rotate.cpp b/src/display/canvas-rotate.cpp new file mode 100644 index 0000000..0a5fcb5 --- /dev/null +++ b/src/display/canvas-rotate.cpp @@ -0,0 +1,291 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Temporary surface for previewing rotated canvas. + * + * Author: + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 2017 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "inkscape.h" +#include "desktop.h" + +#include "canvas-rotate.h" +#include "sp-canvas.h" +#include "cairo-utils.h" +#include "ui/event-debug.h" + +#include "2geom/point.h" +#include "2geom/rect.h" + +#include <gtk/gtk.h> + +namespace { + +static void sp_canvas_rotate_destroy(SPCanvasItem *item); +static void sp_canvas_rotate_update (SPCanvasItem *item, Geom::Affine const &affine, unsigned int flags); +static void sp_canvas_rotate_render (SPCanvasItem *item, SPCanvasBuf *buf); +static int sp_canvas_rotate_event (SPCanvasItem *item, GdkEvent *event); + +} // namespace + +void sp_canvas_rotate_paint (SPCanvasRotate *canvas_rotate, cairo_surface_t *background); + +G_DEFINE_TYPE(SPCanvasRotate, sp_canvas_rotate, SP_TYPE_CANVAS_ITEM); + +static void sp_canvas_rotate_class_init (SPCanvasRotateClass *klass) +{ + klass->destroy = sp_canvas_rotate_destroy; + //klass->update = sp_canvas_rotate_update; + //klass->render = sp_canvas_rotate_render; + klass->event = sp_canvas_rotate_event; +} + +static void sp_canvas_rotate_init (SPCanvasRotate *rotate) +{ + rotate->pickable = true; // So we can receive events. + rotate->angle = 0.0; + rotate->start_angle = -1000; + rotate->surface_copy = nullptr; + rotate->surface_rotated = nullptr; +} + +namespace { +static void sp_canvas_rotate_destroy (SPCanvasItem *object) +{ + g_return_if_fail (object != nullptr); + g_return_if_fail (SP_IS_CANVAS_ROTATE (object)); + + if (SP_CANVAS_ITEM_CLASS(sp_canvas_rotate_parent_class)->destroy) { + SP_CANVAS_ITEM_CLASS(sp_canvas_rotate_parent_class)->destroy(object); + } +} + +// NOT USED... TOO SLOW +static void sp_canvas_rotate_update( SPCanvasItem *item, Geom::Affine const &/*affine*/, unsigned int /*flags*/ ) +{ + SPCanvasRotate *cr = SP_CANVAS_ROTATE(item); + + if (cr->surface_copy == nullptr) { + // std::cout << "sp_canvas_rotate_update: surface_copy is NULL" << std::endl; + return; + } + + // Destroy surface_rotated if it already exists. + if (cr->surface_rotated != nullptr) { + cairo_surface_destroy (cr->surface_rotated); + cr->surface_rotated = nullptr; + } + + // Create rotated surface + cr->surface_rotated = ink_cairo_surface_create_identical(cr->surface_copy); + double width = cairo_image_surface_get_width (cr->surface_rotated); + double height = cairo_image_surface_get_height (cr->surface_rotated); + cairo_t *context = cairo_create( cr->surface_rotated ); + cairo_set_operator( context, CAIRO_OPERATOR_SOURCE ); + cairo_translate( context, width/2.0, height/2.0 ); + cairo_rotate( context, Geom::rad_from_deg(-cr->angle) ); + cairo_translate( context, -width/2.0, -height/2.0 ); + cairo_set_source_surface( context, cr->surface_copy, 0, 0 ); + cairo_paint( context ); + cairo_destroy( context); + + // We cover the entire canvas + item->x1 = -G_MAXINT; + item->y1 = -G_MAXINT; + item->x2 = G_MAXINT; + item->y2 = G_MAXINT; + + item->canvas->requestRedraw(item->x1, item->y1, item->x2, item->y2); +} + +// NOT USED... TOO SLOW +static void sp_canvas_rotate_render( SPCanvasItem *item, SPCanvasBuf *buf) +{ + // std::cout << "sp_canvas_rotate_render:" << std::endl; + // std::cout << " buf->rect: " << buf->rect << std::endl; + // std::cout << " buf->canvas_rect: " << buf->canvas_rect << std::endl; + SPCanvasRotate *cr = SP_CANVAS_ROTATE(item); + auto ct = buf->ct; + + if (!ct) { + return; + } + + if (cr->surface_rotated == nullptr ) { + // std::cout << " surface_rotated is NULL" << std::endl; + return; + } + + // Draw rotated canvas + cairo_save (ct); + cairo_translate (ct, + buf->canvas_rect.left() - buf->rect.left(), + buf->canvas_rect.top() - buf->rect.top() ); + cairo_set_operator (ct, CAIRO_OPERATOR_SOURCE ); + cairo_set_source_surface (ct, cr->surface_rotated, 0, 0 ); + cairo_paint (ct); + cairo_restore (ct); + + + // Draw line from center to cursor + cairo_save (ct); + cairo_translate (ct, -buf->rect.left(), -buf->rect.top()); + cairo_new_path (ct); + cairo_move_to (ct, cr->center[Geom::X], cr->center[Geom::Y]); + cairo_rel_line_to (ct, cr->cursor[Geom::X], cr->cursor[Geom::Y]); + cairo_set_line_width (ct, 2); + ink_cairo_set_source_rgba32 (ct, 0xff00007f); + cairo_stroke (ct); + cairo_restore (ct); + +} + +static int sp_canvas_rotate_event (SPCanvasItem *item, GdkEvent *event) +{ + SPCanvasRotate *cr = SP_CANVAS_ROTATE(item); + +// ui_dump_event (event, Glib::ustring("sp_canvas_rotate_event")); + + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + Geom::Rect viewbox = desktop->canvas->getViewbox(); // Not SVG viewbox! + cr->center = viewbox.midpoint(); + + switch (event->type) { + case GDK_MOTION_NOTIFY: + { + Geom::Point cursor( event->motion.x, event->motion.y ); + + // Both cursor and center are in window coordinates + Geom::Point rcursor( cursor - cr->center ); + double angle = Geom::deg_from_rad( Geom::atan2(rcursor) ); + + // Set start angle + if (cr->start_angle < -360) { + cr->start_angle = angle; + } + + double rotation_snap = 15; + + double delta_angle = cr->start_angle - angle; + + if (event->motion.state & GDK_SHIFT_MASK && + event->motion.state & GDK_CONTROL_MASK) { + delta_angle = 0; + } else if (event->motion.state & GDK_SHIFT_MASK) { + delta_angle = round(delta_angle/rotation_snap) * rotation_snap; + } else if (event->motion.state & GDK_CONTROL_MASK) { + // ? + } else if (event->motion.state & GDK_MOD1_MASK) { + // Decimal raw angle + } else { + delta_angle = floor(delta_angle); + } + + cr->angle = delta_angle; + + // Correct line for snapping of angle + double distance = rcursor.length(); + cr->cursor = Geom::Point::polar( Geom::rad_from_deg(angle), distance ); + + // Update screen + // sp_canvas_item_request_update( item ); + sp_canvas_rotate_paint (cr, cr->canvas->_backing_store); + break; + } + case GDK_BUTTON_RELEASE: + + // Rotate the actual canvas + desktop->rotate_relative_center_point (desktop->w2d(cr->center), + (desktop->w2d().det() > 0 ? -1 : 1) * + Geom::rad_from_deg(cr->angle) ); + + // We're done + sp_canvas_item_ungrab (item); + sp_canvas_item_hide (item); + + cr->start_angle = -1000; + if (cr->surface_copy != nullptr) { + cairo_surface_destroy( cr->surface_copy ); + cr->surface_copy = nullptr; + } + if (cr->surface_rotated != nullptr) { + cairo_surface_destroy( cr->surface_rotated ); + cr->surface_rotated = nullptr; + } + // sp_canvas_item_show (desktop->drawing); + + break; + case GDK_KEY_PRESS: + // std::cout << " Key press: " << std::endl; + break; + case GDK_KEY_RELEASE: + // std::cout << " Key release: " << std::endl; + break; + default: + // ui_dump_event (event, "sp_canvas_rotate_event: unwanted event: "); + break; + } + + if (event->type == GDK_KEY_PRESS) return false; + + return true; +} + +} // namespace + +void sp_canvas_rotate_start (SPCanvasRotate *canvas_rotate, cairo_surface_t *background) +{ + if (background == nullptr) { + std::cerr << "sp_canvas_rotate_start: background is NULL!" << std::endl; + return; + } + + canvas_rotate->angle = 0.0; + + // Copy current background + canvas_rotate->surface_copy = ink_cairo_surface_copy( background ); + + // Paint canvas with background... since we are hiding drawing. + sp_canvas_item_request_update( canvas_rotate ); +} + +// Paint the canvas ourselves for speed.... +void sp_canvas_rotate_paint (SPCanvasRotate *canvas_rotate, cairo_surface_t *background) +{ + if (background == nullptr) { + std::cerr << "sp_canvas_rotate_paint: background is NULL!" << std::endl; + return; + } + + double width = cairo_image_surface_get_width (background); + double height = cairo_image_surface_get_height (background); + + // Draw rotated canvas + cairo_t *context = cairo_create( background ); + + cairo_save (context); + cairo_set_operator( context, CAIRO_OPERATOR_SOURCE ); + cairo_translate( context, width/2.0, height/2.0 ); + cairo_rotate( context, Geom::rad_from_deg(-canvas_rotate->angle) ); + cairo_translate( context, -width/2.0, -height/2.0 ); + cairo_set_source_surface( context, canvas_rotate->surface_copy, 0, 0 ); + cairo_paint( context ); + cairo_restore( context ); + cairo_destroy( context ); + + gtk_widget_queue_draw (GTK_WIDGET (canvas_rotate->canvas)); +} +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/canvas-rotate.h b/src/display/canvas-rotate.h new file mode 100644 index 0000000..b45eae2 --- /dev/null +++ b/src/display/canvas-rotate.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_CANVAS_ROTATE_H +#define SEEN_SP_CANVAS_ROTATE_H + +/* + * Temporary surface for previewing rotated canvas. + * + * Author: + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 2017 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-canvas-item.h" +#include "2geom/line.h" + +class SPItem; + +#define SP_TYPE_CANVAS_ROTATE (sp_canvas_rotate_get_type ()) +#define SP_CANVAS_ROTATE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SP_TYPE_CANVAS_ROTATE, SPCanvasRotate)) +#define SP_IS_CANVAS_ROTATE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SP_TYPE_CANVAS_ROTATE)) + +struct SPCanvasRotate : public SPCanvasItem { + Geom::Point center; // Center of screen + Geom::Point cursor; // Position of cursor relative to center (after angle snapping) + double angle; // Rotation angle in degrees + double start_angle; // Starting angle of cursor + cairo_surface_t *surface_copy; // Copy of original surface + cairo_surface_t *surface_rotated; // Copy of original surface, rotated +}; + +void sp_canvas_rotate_start( SPCanvasRotate *canvas_rotate, cairo_surface_t *background ); + +GType sp_canvas_rotate_get_type (); + +struct SPCanvasRotateClass : public SPCanvasItemClass{}; + +#endif // SEEN_SP_CANVAS_ROTATE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/display/canvas-temporary-item-list.cpp b/src/display/canvas-temporary-item-list.cpp new file mode 100644 index 0000000..caa9609 --- /dev/null +++ b/src/display/canvas-temporary-item-list.cpp @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Provides a class that can contain active TemporaryItem's on a desktop + * Code inspired by message-stack.cpp + * + * Authors: + * Johan Engelen + * + * Copyright (C) Johan Engelen 2008 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "display/canvas-temporary-item.h" +#include "display/canvas-temporary-item-list.h" + +namespace Inkscape { +namespace Display { + +TemporaryItemList::TemporaryItemList(SPDesktop *desktop) + : desktop(desktop) +{ + +} + +TemporaryItemList::~TemporaryItemList() +{ + // delete all items in list so the timeouts are removed + for (auto tempitem : itemlist) { + delete tempitem; + } + itemlist.clear(); +} + +/* Note that TemporaryItem or TemporaryItemList is responsible for deletion and such, so this return pointer can safely be ignored. */ +TemporaryItem * +TemporaryItemList::add_item(SPCanvasItem *item, unsigned int lifetime) +{ + // beware of strange things happening due to very short timeouts + TemporaryItem * tempitem = new TemporaryItem(item, lifetime); + itemlist.push_back(tempitem); + tempitem->signal_timeout.connect( sigc::mem_fun(*this, &TemporaryItemList::_item_timeout) ); + return tempitem; +} + +void +TemporaryItemList::delete_item( TemporaryItem * tempitem ) +{ + // check if the item is in the list, if so, delete it. (in other words, don't wait for the item to delete itself) + bool in_list = false; + for (auto & it : itemlist) { + if ( it == tempitem ) { + in_list = true; + break; + } + } + if (in_list) { + itemlist.remove(tempitem); + delete tempitem; + } +} + +void +TemporaryItemList::_item_timeout(TemporaryItem * tempitem) +{ + itemlist.remove(tempitem); + // no need to delete the item, it does that itself after signal_timeout.emit() completes +} + +} //namespace Display +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/display/canvas-temporary-item-list.h b/src/display/canvas-temporary-item-list.h new file mode 100644 index 0000000..4681c59 --- /dev/null +++ b/src/display/canvas-temporary-item-list.h @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_CANVAS_TEMPORARY_ITEM_LIST_H +#define INKSCAPE_CANVAS_TEMPORARY_ITEM_LIST_H + +/* + * Authors: + * Johan Engelen + * + * Copyright (C) Johan Engelen 2008 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <list> + +struct SPCanvasItem; +class SPDesktop; + +namespace Inkscape { +namespace Display { + +class TemporaryItem; + +/** + * Provides a class that can contain active TemporaryItem[s] on a desktop. + */ +class TemporaryItemList { +public: + TemporaryItemList(SPDesktop *desktop); + virtual ~TemporaryItemList(); + + TemporaryItem* add_item (SPCanvasItem *item, unsigned int lifetime); + void delete_item (TemporaryItem * tempitem); + +protected: + SPDesktop *desktop; /** Desktop we are on. */ + + std::list<TemporaryItem *> itemlist; /** list of temp items */ + + void _item_timeout (TemporaryItem * tempitem); + +private: + TemporaryItemList(const TemporaryItemList&) = delete; + TemporaryItemList& operator=(const TemporaryItemList&) = delete; +}; + +} //namespace Display +} //namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/display/canvas-temporary-item.cpp b/src/display/canvas-temporary-item.cpp new file mode 100644 index 0000000..3826687 --- /dev/null +++ b/src/display/canvas-temporary-item.cpp @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Provides a class that can contain active TemporaryItem's on a desktop + * When the object is deleted, it also deletes the canvasitem it contains! + * This object should be created/managed by a TemporaryItemList. + * After its lifetime, it fires the timeout signal, afterwards *it deletes itself*. + * + * (part of code inspired by message-stack.cpp) + * + * Authors: + * Johan Engelen + * + * Copyright (C) Johan Engelen 2008 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "display/canvas-temporary-item.h" + +#include <glib.h> +#include "display/sp-canvas-item.h" + +namespace Inkscape { +namespace Display { + +/** lifetime is measured in milliseconds + */ +TemporaryItem::TemporaryItem(SPCanvasItem *item, guint lifetime, bool deselect_destroy) + : canvasitem(item), + timeout_id(0), + destroy_on_deselect(deselect_destroy) +{ + if (lifetime > 0 && destroy_on_deselect) { + g_print ("Warning: lifetime should be 0 when destroy_on_deselect is true\n"); + lifetime = 0; + } + // zero lifetime means stay forever, so do not add timeout event. + if (lifetime > 0) { + timeout_id = g_timeout_add(lifetime, &TemporaryItem::_timeout, this); + } +} + +TemporaryItem::~TemporaryItem() +{ + // when it has not expired yet... + if (timeout_id) { + g_source_remove(timeout_id); + timeout_id = 0; + } + + if (canvasitem) { + // destroying the item automatically hides it + sp_canvas_item_destroy(canvasitem); + canvasitem = nullptr; + } +} + +/* static method */ +int TemporaryItem::_timeout(void* data) { + TemporaryItem *tempitem = static_cast<TemporaryItem *>(data); + tempitem->timeout_id = 0; + tempitem->signal_timeout.emit(tempitem); + delete tempitem; + return FALSE; +} + + +} //namespace Display +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/display/canvas-temporary-item.h b/src/display/canvas-temporary-item.h new file mode 100644 index 0000000..4e60e43 --- /dev/null +++ b/src/display/canvas-temporary-item.h @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_CANVAS_TEMPORARY_ITEM_H +#define INKSCAPE_CANVAS_TEMPORARY_ITEM_H + +/* + * Authors: + * Johan Engelen + * + * Copyright (C) Johan Engelen 2008 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#include <sigc++/signal.h> + +struct SPCanvasItem; + +namespace Inkscape { +namespace Display { + +/** + * Provides a class to put a canvasitem temporarily on-canvas. + */ +class TemporaryItem { +public: + TemporaryItem(SPCanvasItem *item, unsigned int lifetime, bool destroy_on_deselect = false); + virtual ~TemporaryItem(); + + TemporaryItem(const TemporaryItem&) = delete; + TemporaryItem& operator=(const TemporaryItem&) = delete; + + sigc::signal<void, TemporaryItem *> signal_timeout; + +protected: + friend class TemporaryItemList; + + SPCanvasItem * canvasitem; /** The item we are holding on to */ + unsigned int timeout_id; /** ID by which glib knows the timeout event */ + bool destroy_on_deselect; // only destroy when parent item is deselected, not when mouse leaves + + static int _timeout(void* data); ///< callback for when lifetime expired +}; + +} //namespace Display +} //namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/display/canvas-text.cpp b/src/display/canvas-text.cpp new file mode 100644 index 0000000..e725a1f --- /dev/null +++ b/src/display/canvas-text.cpp @@ -0,0 +1,323 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Canvas text + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Maximilian Albert <maximilian.albert@gmail.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2000-2002 Lauris Kaplinski + * Copyright (C) 2008 Maximilian Albert + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <sstream> +#include <cstring> + +#include "sp-canvas-util.h" +#include "canvas-text.h" +#include "display/cairo-utils.h" +#include "desktop.h" +#include "display/sp-canvas.h" + +static void sp_canvastext_destroy(SPCanvasItem *object); + +static void sp_canvastext_update (SPCanvasItem *item, Geom::Affine const &affine, unsigned int flags); +static void sp_canvastext_render (SPCanvasItem *item, SPCanvasBuf *buf); + +G_DEFINE_TYPE(SPCanvasText, sp_canvastext, SP_TYPE_CANVAS_ITEM); + +static void sp_canvastext_class_init(SPCanvasTextClass *klass) +{ + SPCanvasItemClass *item_class = SP_CANVAS_ITEM_CLASS(klass); + + item_class->destroy = sp_canvastext_destroy; + item_class->update = sp_canvastext_update; + item_class->render = sp_canvastext_render; +} + +static void +sp_canvastext_init (SPCanvasText *canvastext) +{ + canvastext->anchor_position = TEXT_ANCHOR_CENTER; + canvastext->anchor_pos_x_manual = 0; + canvastext->anchor_pos_y_manual = 0; + canvastext->anchor_offset_x = 0; + canvastext->anchor_offset_y = 0; + canvastext->rgba = 0x33337fff; + canvastext->rgba_stroke = 0xffffffff; + canvastext->rgba_background = 0x0000007f; + canvastext->background = false; + canvastext->s[Geom::X] = canvastext->s[Geom::Y] = 0.0; + canvastext->affine = Geom::identity(); + canvastext->fontsize = 10.0; + canvastext->item = nullptr; + canvastext->desktop = nullptr; + canvastext->text = nullptr; + canvastext->outline = false; + canvastext->background = false; + canvastext->border = 3; // must be a constant, and not proportional to any width, height, or fontsize to allow alignment with other text boxes +} + +static void sp_canvastext_destroy(SPCanvasItem *object) +{ + g_return_if_fail (object != nullptr); + g_return_if_fail (SP_IS_CANVASTEXT (object)); + + SPCanvasText *canvastext = SP_CANVASTEXT (object); + + g_free(canvastext->text); + canvastext->text = nullptr; + canvastext->item = nullptr; + + if (SP_CANVAS_ITEM_CLASS(sp_canvastext_parent_class)->destroy) + SP_CANVAS_ITEM_CLASS(sp_canvastext_parent_class)->destroy(object); +} + +static void +sp_canvastext_render (SPCanvasItem *item, SPCanvasBuf *buf) +{ + SPCanvasText *cl = SP_CANVASTEXT (item); + + if (!buf->ct) + return; + + cairo_select_font_face(buf->ct, "sans-serif", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL); + cairo_set_font_size(buf->ct, cl->fontsize); + + if (cl->background){ + cairo_text_extents_t extents; + cairo_text_extents(buf->ct, cl->text, &extents); + + cairo_rectangle(buf->ct, item->x1 - buf->rect.left(), + item->y1 - buf->rect.top(), + item->x2 - item->x1, + item->y2 - item->y1); + + ink_cairo_set_source_rgba32(buf->ct, cl->rgba_background); + cairo_fill(buf->ct); + } + + Geom::Point s = cl->s * cl->affine; + double offsetx = s[Geom::X] - cl->anchor_offset_x - buf->rect.left(); + double offsety = s[Geom::Y] - cl->anchor_offset_y - buf->rect.top(); + + cairo_move_to(buf->ct, round(offsetx), round(offsety)); + cairo_text_path(buf->ct, cl->text); + + if (cl->outline){ + ink_cairo_set_source_rgba32(buf->ct, cl->rgba_stroke); + cairo_set_line_width (buf->ct, 2.0); + cairo_stroke_preserve(buf->ct); + } + ink_cairo_set_source_rgba32(buf->ct, cl->rgba); + cairo_fill(buf->ct); +} + +static void +sp_canvastext_update (SPCanvasItem *item, Geom::Affine const &affine, unsigned int flags) +{ + SPCanvasText *cl = SP_CANVASTEXT (item); + + item->canvas->requestRedraw((int)item->x1, (int)item->y1, (int)item->x2, (int)item->y2); + + if (SP_CANVAS_ITEM_CLASS(sp_canvastext_parent_class)->update) + SP_CANVAS_ITEM_CLASS(sp_canvastext_parent_class)->update(item, affine, flags); + + sp_canvas_item_reset_bounds (item); + + cl->affine = affine; + + Geom::Point s = cl->s * affine; + // Point s specifies the position of the anchor, which is at the bounding box of the text itself (i.e. not at the border of the filled background rectangle) + // The relative position of the anchor can be set using e.g. anchor_position = TEXT_ANCHOR_LEFT + + // Set up a temporary cairo_t to measure the text extents; it would be better to compute this in the render() + // method but update() seems to be called before so we don't have the information available when we need it + cairo_surface_t *tmp_surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 1, 1); + cairo_t* tmp_buf = cairo_create(tmp_surface); + + cairo_select_font_face(tmp_buf, "sans-serif", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL); + cairo_set_font_size(tmp_buf, cl->fontsize); + cairo_text_extents_t extents; + cairo_text_extents(tmp_buf, cl->text, &extents); + double border = cl->border; + + item->x1 = s[Geom::X] + extents.x_bearing - border; + item->y1 = s[Geom::Y] + extents.y_bearing - border; + item->x2 = item->x1 + extents.width + 2*border; + item->y2 = item->y1 + extents.height + 2*border; + + /* FROM: http://lists.cairographics.org/archives/cairo-bugs/2009-March/003014.html + - Glyph surfaces: In most font rendering systems, glyph surfaces + have an origin at (0,0) and a bounding box that is typically + represented as (x_bearing,y_bearing,width,height). Depending on + which way y progresses in the system, y_bearing may typically be + negative (for systems similar to cairo, with origin at top left), + or be positive (in systems like PDF with origin at bottom left). + No matter which is the case, it is important to note that + (x_bearing,y_bearing) is the coordinates of top-left of the glyph + relative to the glyph origin. That is, for example: + + Scaled-glyph space: + + (x_bearing,y_bearing) <-- negative numbers + +----------------+ + | . | + | . | + |......(0,0) <---|-- glyph origin + | | + | | + +----------------+ + (width+x_bearing,height+y_bearing) + + Note the similarity of the origin to the device space. That is + exactly how we use the device_offset to represent scaled glyphs: + to use the device-space origin as the glyph origin. + */ + + // adjust update region according to anchor shift + + + switch (cl->anchor_position){ + case TEXT_ANCHOR_LEFT: + cl->anchor_offset_x = 0; + cl->anchor_offset_y = -extents.height/2; + break; + case TEXT_ANCHOR_RIGHT: + cl->anchor_offset_x = extents.width; + cl->anchor_offset_y = -extents.height/2; + break; + case TEXT_ANCHOR_BOTTOM: + cl->anchor_offset_x = extents.width/2; + cl->anchor_offset_y = 0; + break; + case TEXT_ANCHOR_TOP: + cl->anchor_offset_x = extents.width/2; + cl->anchor_offset_y = -extents.height; + break; + case TEXT_ANCHOR_ZERO: + cl->anchor_offset_x = 0; + cl->anchor_offset_y = 0; + break; + case TEXT_ANCHOR_MANUAL: + cl->anchor_offset_x = (1 + cl->anchor_pos_x_manual) * extents.width/2; + cl->anchor_offset_y = -(1 + cl->anchor_pos_y_manual) * extents.height/2; + break; + case TEXT_ANCHOR_CENTER: + default: + cl->anchor_offset_x = extents.width/2; + cl->anchor_offset_y = -extents.height/2; + break; + } + + cl->anchor_offset_x += extents.x_bearing; + cl->anchor_offset_y += extents.height + extents.y_bearing; + + item->x1 -= cl->anchor_offset_x; + item->x2 -= cl->anchor_offset_x; + item->y1 -= cl->anchor_offset_y; + item->y2 -= cl->anchor_offset_y; + + item->canvas->requestRedraw((int)item->x1, (int)item->y1, (int)item->x2, (int)item->y2); +} + +SPCanvasText *sp_canvastext_new(SPCanvasGroup *parent, SPDesktop *desktop, Geom::Point pos, gchar const *new_text) +{ + // Pos specifies the position of the anchor, which is at the bounding box of the text itself (i.e. not at the border of the filled background rectangle) + // The relative position of the anchor can be set using e.g. anchor_position = TEXT_ANCHOR_LEFT + SPCanvasItem *item = sp_canvas_item_new(parent, SP_TYPE_CANVASTEXT, nullptr); + + SPCanvasText *ct = SP_CANVASTEXT(item); + + ct->desktop = desktop; + + ct->s = pos; + g_free(ct->text); + ct->text = g_strdup(new_text); + + return ct; +} + + +void +sp_canvastext_set_rgba32 (SPCanvasText *ct, guint32 rgba, guint32 rgba_stroke) +{ + g_return_if_fail (ct != nullptr); + g_return_if_fail (SP_IS_CANVASTEXT (ct)); + + if (rgba != ct->rgba || rgba_stroke != ct->rgba_stroke) { + ct->rgba = rgba; + ct->rgba_stroke = rgba_stroke; + SPCanvasItem *item = SP_CANVAS_ITEM (ct); + sp_canvas_item_request_update( item ); + } +} + +#define EPSILON 1e-6 +#define DIFFER(a,b) (fabs ((a) - (b)) > EPSILON) + +void +sp_canvastext_set_coords (SPCanvasText *ct, gdouble x0, gdouble y0) +{ + sp_canvastext_set_coords(ct, Geom::Point(x0, y0)); +} + +void +sp_canvastext_set_coords (SPCanvasText *ct, const Geom::Point start) +{ + g_return_if_fail (ct && ct->desktop); + g_return_if_fail (SP_IS_CANVASTEXT (ct)); + + Geom::Point pos = ct->desktop->doc2dt(start); + + if (DIFFER (pos[0], ct->s[Geom::X]) || DIFFER (pos[1], ct->s[Geom::Y])) { + ct->s[Geom::X] = pos[0]; + ct->s[Geom::Y] = pos[1]; + sp_canvas_item_request_update (SP_CANVAS_ITEM (ct)); + } +} + +void +sp_canvastext_set_text (SPCanvasText *ct, gchar const * new_text) +{ + g_free (ct->text); + ct->text = g_strdup(new_text); + sp_canvas_item_request_update (SP_CANVAS_ITEM (ct)); +} + +void +sp_canvastext_set_number_as_text (SPCanvasText *ct, int num) +{ + std::ostringstream number; + number << num; + sp_canvastext_set_text(ct, number.str().c_str()); +} + +void +sp_canvastext_set_fontsize (SPCanvasText *ct, double size) +{ + ct->fontsize = size; +} + +void +sp_canvastext_set_anchor_manually (SPCanvasText *ct, double anchor_x, double anchor_y) +{ + ct->anchor_pos_x_manual = anchor_x; + ct->anchor_pos_y_manual = anchor_y; + ct->anchor_position = TEXT_ANCHOR_MANUAL; +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/canvas-text.h b/src/display/canvas-text.h new file mode 100644 index 0000000..257f360 --- /dev/null +++ b/src/display/canvas-text.h @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_CANVASTEXT_H +#define SEEN_SP_CANVASTEXT_H + +/* + * Canvas text. + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Maximilian Albert <maximilian.albert@gmail.com> + * + * Copyright (C) 2000-2002 Lauris Kaplinski + * Copyright (C) 2008 Maximilian Albert + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-canvas-item.h" + +class SPItem; +class SPDesktop; + +#define SP_TYPE_CANVASTEXT (sp_canvastext_get_type ()) +#define SP_CANVASTEXT(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SP_TYPE_CANVASTEXT, SPCanvasText)) +#define SP_IS_CANVASTEXT(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SP_TYPE_CANVASTEXT)) + +enum CanvasTextAnchorPositionEnum { + TEXT_ANCHOR_CENTER, + TEXT_ANCHOR_TOP, + TEXT_ANCHOR_BOTTOM, + TEXT_ANCHOR_LEFT, + TEXT_ANCHOR_RIGHT, + TEXT_ANCHOR_ZERO, + TEXT_ANCHOR_MANUAL +}; + +struct SPCanvasText : public SPCanvasItem { + SPItem *item; // the item to which this line belongs in some sense; may be NULL for some users + guint32 rgba; + guint32 rgba_stroke; + guint32 rgba_background; + bool outline; + bool background; + double border; + + SPDesktop *desktop; // the desktop to which this text is attached; needed for coordinate transforms (TODO: these should be eliminated) + + gchar* text; + Geom::Point s; // Is the coordinate of the anchor, which is related to the rectangle filled with the background color + CanvasTextAnchorPositionEnum anchor_position; // The anchor position specifies where the anchor is with respect to the rectangle filled with the background color + double anchor_pos_x_manual; + double anchor_pos_y_manual; + Geom::Affine affine; + double fontsize; + double anchor_offset_x; + double anchor_offset_y; +}; +struct SPCanvasTextClass : public SPCanvasItemClass{}; + +GType sp_canvastext_get_type (); + +SPCanvasText *sp_canvastext_new(SPCanvasGroup *parent, SPDesktop *desktop, Geom::Point pos, gchar const *text); + +void sp_canvastext_set_rgba32 (SPCanvasText *ct, guint32 rgba, guint32 rgba_stroke); +void sp_canvastext_set_coords (SPCanvasText *ct, gdouble x0, gdouble y0); +void sp_canvastext_set_coords (SPCanvasText *ct, const Geom::Point start); +void sp_canvastext_set_text (SPCanvasText *ct, gchar const* new_text); +void sp_canvastext_set_number_as_text (SPCanvasText *ct, int num); +void sp_canvastext_set_fontsize (SPCanvasText *ct, double size); +void sp_canvastext_set_anchor_manually (SPCanvasText *ct, double anchor_x, double anchor_y); + +#endif // SEEN_SP_CANVASTEXT_H + + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/display/curve.cpp b/src/display/curve.cpp new file mode 100644 index 0000000..3c46fb7 --- /dev/null +++ b/src/display/curve.cpp @@ -0,0 +1,713 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Johan Engelen + * + * Copyright (C) 2000 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2002 Lauris Kaplinski + * Copyright (C) 2008 Johan Engelen + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "display/curve.h" + +#include <glib.h> +#include <2geom/pathvector.h> +#include <2geom/sbasis-geometric.h> +#include <2geom/sbasis-to-bezier.h> +#include <2geom/point.h> + +#include <utility> + +/** + * Routines for SPCurve and for its Geom::PathVector + */ + +/* Constructors */ + +/** + * The returned curve's state is as if SPCurve::reset has just been called on it. + */ +SPCurve::SPCurve() + : _refcount(1), + _pathv() +{} + +SPCurve::SPCurve(Geom::PathVector pathv) + : _refcount(1), + _pathv(std::move(pathv)) +{} + +//concat constructor +SPCurve::SPCurve(std::list<SPCurve *> const& l) : _refcount(1) +{ + for (auto c:l) { + _pathv.insert( _pathv.end(), c->get_pathvector().begin(), c->get_pathvector().end() ); + } +} + + +SPCurve * +SPCurve::new_from_rect(Geom::Rect const &rect, bool all_four_sides) +{ + SPCurve *c = new SPCurve(); + + Geom::Point p = rect.corner(0); + c->moveto(p); + + for (int i=3; i>=1; --i) { + c->lineto(rect.corner(i)); + } + + if (all_four_sides) { + // When _constrained_ snapping to a path, the 2geom::SimpleCrosser will be invoked which doesn't consider the closing segment. + // of a path. Consequently, in case we want to snap to for example the page border, we must provide all four sides of the + // rectangle explicitly + c->lineto(rect.corner(0)); + } else { + // ... instead of just three plus a closing segment + c->closepath(); + } + + return c; +} + +SPCurve::~SPCurve() += default; + +/* Methods */ + +void +SPCurve::set_pathvector(Geom::PathVector const & new_pathv) +{ + _pathv = new_pathv; +} + +Geom::PathVector const & +SPCurve::get_pathvector() const +{ + return _pathv; +} + +/* + * Returns the number of segments of all paths summed + * This count includes the closing line segment of a closed path. + */ +size_t +SPCurve::get_segment_count() const +{ + return _pathv.curveCount(); +} + +/** + * Increase _refcount of curve. + * + * \todo should this be shared with other refcounting code? + */ +SPCurve * +SPCurve::ref() +{ + _refcount += 1; + + return this; +} + +/** + * Decrease refcount of curve, with possible destruction. + * + * \todo should this be shared with other refcounting code? + */ +SPCurve * +SPCurve::unref() +{ + _refcount -= 1; + + if (_refcount < 1) { + delete this; + } + + return nullptr; +} + +/** + * Create new curve from this curve's pathvector array. + */ +SPCurve * +SPCurve::copy() const +{ + return new SPCurve(_pathv); +} + +/** + * Return new curve that is the concatenation of all curves in list. + */ +SPCurve * +SPCurve::concat(std::list<SPCurve *> l){ + return new SPCurve(l); +}; + +/** + * Returns a list of new curves corresponding to the subpaths in \a curve. + * 2geomified + */ +std::list<SPCurve *> +SPCurve::split() const +{ + std::list<SPCurve *> l; + + for (const auto & path_it : _pathv) { + Geom::PathVector newpathv; + newpathv.push_back(path_it); + SPCurve * newcurve = new SPCurve(newpathv); + l.push_back(newcurve); + } + + return l; +} + +/** + * Transform all paths in curve using matrix. + */ +void +SPCurve::transform(Geom::Affine const &m) +{ + _pathv *= m; +} + +/** + * Set curve to empty curve. + * In more detail: this clears the internal pathvector from all its paths. + */ +void +SPCurve::reset() +{ + _pathv.clear(); +} + +/** Several consecutive movetos are ALLOWED + * Ref: http://www.w3.org/TR/SVG11/implnote.html#PathElementImplementationNotes + * (first subitem of the item about zero-length path segments) */ + +/** + * Calls SPCurve::moveto() with point made of given coordinates. + */ +void +SPCurve::moveto(double x, double y) +{ + moveto(Geom::Point(x, y)); +} +/** + * Perform a moveto to a point, thus starting a new subpath. + * Point p must be finite. + */ +void +SPCurve::moveto(Geom::Point const &p) +{ + Geom::Path path(p); + path.setStitching(true); + _pathv.push_back(path); +} + +/** + * Adds a line to the current subpath. + * Point p must be finite. + */ +void +SPCurve::lineto(Geom::Point const &p) +{ + if (_pathv.empty()) g_message("SPCurve::lineto - path is empty!"); + else _pathv.back().appendNew<Geom::LineSegment>( p ); +} +/** + * Calls SPCurve::lineto( Geom::Point(x,y) ) + */ +void +SPCurve::lineto(double x, double y) +{ + lineto(Geom::Point(x,y)); +} + +/** + * Adds a quadratic bezier segment to the current subpath. + * All points must be finite. + */ +void +SPCurve::quadto(Geom::Point const &p1, Geom::Point const &p2) +{ + if (_pathv.empty()) g_message("SPCurve::quadto - path is empty!"); + else _pathv.back().appendNew<Geom::QuadraticBezier>( p1, p2); +} +/** + * Calls SPCurve::quadto( Geom::Point(x1,y1), Geom::Point(x2,y2) ) + * All coordinates must be finite. + */ +void +SPCurve::quadto(double x1, double y1, double x2, double y2) +{ + quadto( Geom::Point(x1,y1), Geom::Point(x2,y2) ); +} + +/** + * Adds a bezier segment to the current subpath. + * All points must be finite. + */ +void +SPCurve::curveto(Geom::Point const &p0, Geom::Point const &p1, Geom::Point const &p2) +{ + if (_pathv.empty()) g_message("SPCurve::curveto - path is empty!"); + else _pathv.back().appendNew<Geom::CubicBezier>( p0, p1, p2 ); +} +/** + * Calls SPCurve::curveto( Geom::Point(x0,y0), Geom::Point(x1,y1), Geom::Point(x2,y2) ) + * All coordinates must be finite. + */ +void +SPCurve::curveto(double x0, double y0, double x1, double y1, double x2, double y2) +{ + curveto( Geom::Point(x0,y0), Geom::Point(x1,y1), Geom::Point(x2,y2) ); +} + +/** + * Close current subpath by possibly adding a line between start and end. + */ +void +SPCurve::closepath() +{ + _pathv.back().close(true); +} + +/** Like SPCurve::closepath() but sets the end point of the last subpath + to the subpath start point instead of adding a new lineto. + + Used for freehand drawing when the user draws back to the start point. +**/ +void +SPCurve::closepath_current() +{ + if (_pathv.back().size() > 0 && dynamic_cast<Geom::LineSegment const *>(&_pathv.back().back_open())) { + _pathv.back().erase_last(); + } else { + _pathv.back().setFinal(_pathv.back().initialPoint()); + } + _pathv.back().close(true); +} + +/** + * True if no paths are in curve. If it only contains a path with only a moveto, the path is considered NON-empty + */ +bool +SPCurve::is_empty() const +{ + return _pathv.empty(); +} + +/** + * True if paths are in curve. If it only contains a path with only a moveto, the path is considered as unset FALSE + */ +bool +SPCurve::is_unset() const +{ + if (get_segment_count()) { + return false; + } + return true; +} + +/** + * True iff all subpaths are closed. + * Returns false if the curve is empty. + */ +bool +SPCurve::is_closed() const +{ + if (is_empty()) { + return false; + } + + for (const auto & it : _pathv) { + if ( ! it.closed() ) { + return false; + } + } + + return true; +} + +/** + * True if both curves are equal + */ +bool +SPCurve::is_equal(SPCurve * other) const +{ + if(other == nullptr) { + return false; + } + + if(_pathv == other->get_pathvector()){ + return true; + } + + return false; +} + +/** + * Return last pathsegment (possibly the closing path segment) of the last path in PathVector or NULL. + * If the last path is empty (contains only a moveto), the function returns NULL + */ +Geom::Curve const * +SPCurve::last_segment() const +{ + if (is_empty()) { + return nullptr; + } + if (_pathv.back().empty()) { + return nullptr; + } + + return &_pathv.back().back_default(); +} + +/** + * Return last path in PathVector or NULL. + */ +Geom::Path const * +SPCurve::last_path() const +{ + if (is_empty()) { + return nullptr; + } + + return &_pathv.back(); +} + +/** + * Return first pathsegment in PathVector or NULL. + * equal in functionality to SPCurve::first_bpath() + */ +Geom::Curve const * +SPCurve::first_segment() const +{ + if (is_empty()) { + return nullptr; + } + if (_pathv.front().empty()) { + return nullptr; + } + + return &_pathv.front().front(); +} + +/** + * Return first path in PathVector or NULL. + */ +Geom::Path const * +SPCurve::first_path() const +{ + if (is_empty()) { + return nullptr; + } + + return &_pathv.front(); +} + +/** + * Return first point of first subpath or nothing when the path is empty. + */ +boost::optional<Geom::Point> +SPCurve::first_point() const +{ + boost::optional<Geom::Point> retval; + + if (!is_empty()) { + retval = _pathv.front().initialPoint(); + } + + return retval; +} + +/** + * Return the second point of first subpath or _movePos if curve too short. + * If the pathvector is empty, this returns nothing. If the first path is only a moveto, this method + * returns the first point of the second path, if it exists. If there is no 2nd path, it returns the + * first point of the first path. + */ +boost::optional<Geom::Point> +SPCurve::second_point() const +{ + boost::optional<Geom::Point> retval; + if (!is_empty()) { + if (_pathv.front().empty()) { + // first path is only a moveto + // check if there is second path + if (_pathv.size() > 1) { + retval = _pathv[1].initialPoint(); + } else { + retval = _pathv[0].initialPoint(); + } + } else { + retval = _pathv.front()[0].finalPoint(); + } + } + + return retval; +} + +/** + * Return the second-last point of last subpath or first point when that last subpath has only a moveto. + */ +boost::optional<Geom::Point> +SPCurve::penultimate_point() const +{ + boost::optional<Geom::Point> retval; + if (!is_empty()) { + Geom::Path const &lastpath = _pathv.back(); + if (!lastpath.empty()) { + Geom::Curve const &back = lastpath.back_default(); + retval = back.initialPoint(); + } else { + retval = lastpath.initialPoint(); + } + } + + return retval; +} + +/** + * Return last point of last subpath or nothing when the curve is empty. + * If the last path is only a moveto, then return that point. + */ +boost::optional<Geom::Point> +SPCurve::last_point() const +{ + boost::optional<Geom::Point> retval; + + if (!is_empty()) { + retval = _pathv.back().finalPoint(); + } + + return retval; +} + +/** + * Returns a *new* \a curve but drawn in the opposite direction. + * Should result in the same shape, but + * with all its markers drawn facing the other direction. + * Reverses the order of subpaths as well + **/ +SPCurve * +SPCurve::create_reverse() const +{ + SPCurve *new_curve = new SPCurve(_pathv.reversed()); + + return new_curve; +} + +/** + * Append \a curve2 to \a this. + * If \a use_lineto is false, simply add all paths in \a curve2 to \a this; + * if \a use_lineto is true, combine \a this's last path and \a curve2's first path and add the rest of the paths in \a curve2 to \a this. + */ +void +SPCurve::append(SPCurve const *curve2, + bool use_lineto) +{ + if (curve2->is_empty()) + return; + + if (use_lineto) { + Geom::PathVector::const_iterator it = curve2->_pathv.begin(); + if ( ! _pathv.empty() ) { + Geom::Path & lastpath = _pathv.back(); + lastpath.appendNew<Geom::LineSegment>( (*it).initialPoint() ); + lastpath.append( (*it) ); + } else { + _pathv.push_back( (*it) ); + } + + for (++it; it != curve2->_pathv.end(); ++it) { + _pathv.push_back( (*it) ); + } + } else { + for (const auto & it : curve2->_pathv) { + _pathv.push_back( it ); + } + } +} + +/** + * Append \a c1 to \a this with possible fusing of close endpoints. If the end of this curve and the start of c1 are within tolerance distance, + * then the startpoint of c1 is moved to the end of this curve and the first subpath of c1 is appended to the last subpath of this curve. + * When one of the curves (this curve or the argument curve) is closed, the returned value is NULL; otherwise the returned value is this curve. + * When one of the curves is empty, this curves path becomes the non-empty path. + */ +SPCurve * +SPCurve::append_continuous(SPCurve const *c1, double tolerance) +{ + using Geom::X; + using Geom::Y; + + g_return_val_if_fail(c1 != nullptr, NULL); + if ( this->is_closed() || c1->is_closed() ) { + return nullptr; + } + + if (c1->is_empty()) { + return this; + } + + if (this->is_empty()) { + _pathv = c1->_pathv; + return this; + } + + if ( (fabs((*this->last_point())[X] - (*c1->first_point())[X]) <= tolerance) + && (fabs((*this->last_point())[Y] - (*c1->first_point())[Y]) <= tolerance) ) + { + // c1's first subpath can be appended to this curve's last subpath + Geom::PathVector::const_iterator path_it = c1->_pathv.begin(); + Geom::Path & lastpath = _pathv.back(); + + Geom::Path newfirstpath(*path_it); + newfirstpath.setInitial(lastpath.finalPoint()); + lastpath.append( newfirstpath ); + + for (++path_it; path_it != c1->_pathv.end(); ++path_it) { + _pathv.push_back( (*path_it) ); + } + + } else { + append(c1, true); + } + + return this; +} + +/** + * Remove last segment of curve. + * (Only used once in /src/pen-context.cpp) + */ +void +SPCurve::backspace() +{ + if ( is_empty() ) + return; + + if ( !_pathv.back().empty() ) { + _pathv.back().erase_last(); + _pathv.back().close(false); + } +} + +/** + * TODO: add comments about what this method does and what assumptions are made and requirements are put on SPCurve + (2:08:18 AM) Johan: basically, i convert the path to pw<d2> +(2:08:27 AM) Johan: then i calculate an offset path +(2:08:29 AM) Johan: to move the knots +(2:08:36 AM) Johan: then i add it +(2:08:40 AM) Johan: then convert back to path +If I remember correctly, this moves the firstpoint to new_p0, and the lastpoint to new_p1, and moves all nodes in between according to their arclength (interpolates the movement amount) + */ +void +SPCurve::stretch_endpoints(Geom::Point const &new_p0, Geom::Point const &new_p1) +{ + if (is_empty()) { + return; + } + + Geom::Point const offset0( new_p0 - *first_point() ); + Geom::Point const offset1( new_p1 - *last_point() ); + + Geom::Piecewise<Geom::D2<Geom::SBasis> > pwd2 = _pathv.front().toPwSb(); + Geom::Piecewise<Geom::SBasis> arclength = Geom::arcLengthSb(pwd2); + if ( arclength.lastValue() <= 0 ) { + g_error("SPCurve::stretch_endpoints - arclength <= 0"); + throw; + } + arclength *= 1./arclength.lastValue(); + Geom::Point const A( offset0 ); + Geom::Point const B( offset1 ); + Geom::Piecewise<Geom::SBasis> offsetx = (arclength*-1.+1)*A[0] + arclength*B[0]; + Geom::Piecewise<Geom::SBasis> offsety = (arclength*-1.+1)*A[1] + arclength*B[1]; + Geom::Piecewise<Geom::D2<Geom::SBasis> > offsetpath = Geom::sectionize( Geom::D2<Geom::Piecewise<Geom::SBasis> >(offsetx, offsety) ); + pwd2 += offsetpath; + _pathv = Geom::path_from_piecewise( pwd2, 0.001 ); +} + +/** + * sets start of first path to new_p0, and end of first path to new_p1 + */ +void +SPCurve::move_endpoints(Geom::Point const &new_p0, Geom::Point const &new_p1) +{ + if (is_empty()) { + return; + } + _pathv.front().setInitial(new_p0); + _pathv.front().setFinal(new_p1); +} + +/** + * returns the number of nodes in a path, used for statusbar text when selecting an spcurve. + * Sum of nodes in all the paths. When a path is closed, and its closing line segment is of zero-length, + * this function will not count the closing knot double (so basically ignores the closing line segment when it has zero length) + */ +size_t +SPCurve::nodes_in_path() const +{ + size_t nr = 0; + for(const auto & it : _pathv) { + // if the path does not have any segments, it is a naked moveto, + // and therefore any path has at least one valid node + size_t psize = std::max<size_t>(1, it.size_closed()); + nr += psize; + if (it.closed() && it.size_closed() > 0) { + const Geom::Curve &closingline = it.back_closed(); + // the closing line segment is always of type + // Geom::LineSegment. + if (are_near(closingline.initialPoint(), closingline.finalPoint())) { + // closingline.isDegenerate() did not work, because it only checks for + // *exact* zero length, which goes wrong for relative coordinates and + // rounding errors... + // the closing line segment has zero-length. So stop before that one! + nr -= 1; + } + } + } + + return nr; +} + +/** + * Adds p to the last point (and last handle if present) of the last path + */ +void +SPCurve::last_point_additive_move(Geom::Point const & p) +{ + if (is_empty()) { + return; + } + + _pathv.back().setFinal( _pathv.back().finalPoint() + p ); + + // Move handle as well when the last segment is a cubic bezier segment: + // TODO: what to do for quadratic beziers? + if ( Geom::CubicBezier const *lastcube = dynamic_cast<Geom::CubicBezier const *>(&_pathv.back().back()) ) { + Geom::CubicBezier newcube( *lastcube ); + newcube.setPoint(2, newcube[2] + p); + _pathv.back().replace( --_pathv.back().end(), newcube ); + } +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8: diff --git a/src/display/curve.h b/src/display/curve.h new file mode 100644 index 0000000..c6ddb4c --- /dev/null +++ b/src/display/curve.h @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 2000 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2002 Lauris Kaplinski + * Copyright (C) 2008 Johan Engelen + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_DISPLAY_CURVE_H +#define SEEN_DISPLAY_CURVE_H + +#include <2geom/pathvector.h> +#include <cstddef> +#include <boost/optional.hpp> +#include <list> + +/** + * Wrapper around a Geom::PathVector object. + */ +class SPCurve { +public: + /* Constructors */ + explicit SPCurve(); + explicit SPCurve(Geom::PathVector pathv); + explicit SPCurve(std::list<SPCurve *> const& pathv); + static SPCurve * new_from_rect(Geom::Rect const &rect, bool all_four_sides = false); + + virtual ~SPCurve(); + + // Don't implement these: + SPCurve(const SPCurve&) = delete; + SPCurve& operator=(const SPCurve&) = delete; + + void set_pathvector(Geom::PathVector const & new_pathv); + Geom::PathVector const & get_pathvector() const; + + SPCurve * ref(); + SPCurve * unref(); + + SPCurve * copy() const; + + size_t get_segment_count() const; + size_t nodes_in_path() const; + + bool is_empty() const; + bool is_unset() const; + bool is_closed() const; + bool is_equal(SPCurve * other) const; + Geom::Curve const * last_segment() const; + Geom::Path const * last_path() const; + Geom::Curve const * first_segment() const; + Geom::Path const * first_path() const; + boost::optional<Geom::Point> first_point() const; + boost::optional<Geom::Point> last_point() const; + boost::optional<Geom::Point> second_point() const; + boost::optional<Geom::Point> penultimate_point() const; + + void reset(); + + void moveto(Geom::Point const &p); + void moveto(double x, double y); + void lineto(Geom::Point const &p); + void lineto(double x, double y); + void quadto(Geom::Point const &p1, Geom::Point const &p2); + void quadto(double x1, double y1, double x2, double y2); + void curveto(Geom::Point const &p0, Geom::Point const &p1, Geom::Point const &p2); + void curveto(double x0, double y0, double x1, double y1, double x2, double y2); + void closepath(); + void closepath_current(); + void backspace(); + + void transform(Geom::Affine const &m); + void stretch_endpoints(Geom::Point const &, Geom::Point const &); + void move_endpoints(Geom::Point const &, Geom::Point const &); + void last_point_additive_move(Geom::Point const & p); + + void append(SPCurve const *curve2, bool use_lineto); + SPCurve * append_continuous(SPCurve const *c1, double tolerance); + SPCurve * create_reverse() const; + + std::list<SPCurve *> split() const; + static SPCurve * concat(std::list<SPCurve *> l); + +protected: + size_t _refcount; + + Geom::PathVector _pathv; +}; + +#endif // !SEEN_DISPLAY_CURVE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/display/drawing-context.cpp b/src/display/drawing-context.cpp new file mode 100644 index 0000000..ac45df2 --- /dev/null +++ b/src/display/drawing-context.cpp @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Cairo drawing context with Inkscape extensions. + *//* + * Authors: + * Krzysztof KosiĆski <tweenk.pl@gmail.com> + * + * Copyright (C) 2011 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "display/drawing-context.h" +#include "display/drawing-surface.h" +#include "display/cairo-utils.h" + +namespace Inkscape { + +using Geom::X; +using Geom::Y; + +/** + * @class DrawingContext::Save + * RAII idiom for saving the state of DrawingContext. + */ + +DrawingContext::Save::Save() + : _dc(nullptr) +{} +DrawingContext::Save::Save(DrawingContext &dc) + : _dc(&dc) +{ + _dc->save(); +} +DrawingContext::Save::~Save() +{ + if (_dc) { + _dc->restore(); + } +} +void DrawingContext::Save::save(DrawingContext &dc) +{ + if (_dc) { + // TODO: it might be better to treat this occurrence as a bug + _dc->restore(); + } + _dc = &dc; + _dc->save(); +} + +/** + * @class DrawingContext + * Minimal wrapper over Cairo. + * + * This is a wrapper over cairo_t, extended with operations that work + * with 2Geom geometrical primitives. Some of this is probably duplicated + * in cairo-render-context.cpp, which provides higher level operations + * for drawing entire SPObjects when exporting. + */ + +// Uses existing Cairo surface +DrawingContext::DrawingContext(cairo_t *ct, Geom::Point const &origin) + : _ct(ct) + , _surface(new DrawingSurface(cairo_get_group_target(ct), origin)) + , _delete_surface(true) + , _restore_context(true) +{ + _surface->_has_context = true; + cairo_reference(_ct); + cairo_save(_ct); + cairo_translate(_ct, -origin[Geom::X], -origin[Geom::Y]); +} + +// Uses existing Cairo surface +DrawingContext::DrawingContext(cairo_surface_t *surface, Geom::Point const &origin) + : _ct(nullptr) + , _surface(new DrawingSurface(surface, origin)) + , _delete_surface(true) + , _restore_context(false) +{ + _surface->_has_context = true; + _ct = _surface->createRawContext(); +} + +DrawingContext::DrawingContext(DrawingSurface &s) + : _ct(s.createRawContext()) + , _surface(&s) + , _delete_surface(false) + , _restore_context(false) +{} + +DrawingContext::~DrawingContext() +{ + if (_restore_context) { + cairo_restore(_ct); + } + cairo_destroy(_ct); + _surface->_has_context = false; + if (_delete_surface) { + delete _surface; + } +} + +void DrawingContext::arc(Geom::Point const ¢er, double radius, Geom::AngleInterval const &angle) +{ + double from = angle.initialAngle(); + double to = angle.finalAngle(); + if (to > from) { + cairo_arc(_ct, center[X], center[Y], radius, from, to); + } else { + cairo_arc_negative(_ct, center[X], center[Y], radius, to, from); + } +} + +// Applies transform to Cairo surface +void DrawingContext::transform(Geom::Affine const &trans) { + ink_cairo_transform(_ct, trans); +} + +void DrawingContext::path(Geom::PathVector const &pv) { + feed_pathvector_to_cairo(_ct, pv); +} + +void DrawingContext::paint(double alpha) { + if (alpha == 1.0) cairo_paint(_ct); + else cairo_paint_with_alpha(_ct, alpha); +} +void DrawingContext::setSource(guint32 rgba) { + ink_cairo_set_source_rgba32(_ct, rgba); +} +void DrawingContext::setSource(DrawingSurface *s) { + Geom::Point origin = s->origin(); + cairo_set_source_surface(_ct, s->raw(), origin[X], origin[Y]); +} +void DrawingContext::setSourceCheckerboard() { + cairo_pattern_t *check = ink_cairo_pattern_create_checkerboard(); + cairo_set_source(_ct, check); + cairo_pattern_destroy(check); +} + +Geom::Rect DrawingContext::targetLogicalBounds() const +{ + Geom::Rect ret(_surface->area()); + return ret; +} + +} // end namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/drawing-context.h b/src/display/drawing-context.h new file mode 100644 index 0000000..aedb87e --- /dev/null +++ b/src/display/drawing-context.h @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Cairo drawing context with Inkscape extensions. + *//* + * Authors: + * Krzysztof KosiĆski <tweenk.pl@gmail.com> + * + * Copyright (C) 2011 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_DISPLAY_DRAWING_CONTEXT_H +#define SEEN_INKSCAPE_DISPLAY_DRAWING_CONTEXT_H + +#include <2geom/rect.h> +#include <2geom/transforms.h> +#include <boost/utility.hpp> +#include <cairo.h> +typedef unsigned int guint32; + +namespace Inkscape { + +class DrawingSurface; + +class DrawingContext + : boost::noncopyable +{ +public: + class Save { + public: + Save(); + Save(DrawingContext &dc); + ~Save(); + void save(DrawingContext &dc); + private: + DrawingContext *_dc; + }; + + DrawingContext(cairo_t *ct, Geom::Point const &origin); + DrawingContext(cairo_surface_t *surface, Geom::Point const &origin); + DrawingContext(DrawingSurface &s); + ~DrawingContext(); + + void save() { cairo_save(_ct); } + void restore() { cairo_restore(_ct); } + void pushGroup() { cairo_push_group(_ct); } + void pushAlphaGroup() { cairo_push_group_with_content(_ct, CAIRO_CONTENT_ALPHA); } + void popGroupToSource() { cairo_pop_group_to_source(_ct); } + + void transform(Geom::Affine const &trans); + void translate(Geom::Point const &t) { cairo_translate(_ct, t[Geom::X], t[Geom::Y]); } // todo: take Translate + void translate(double dx, double dy) { cairo_translate(_ct, dx, dy); } + void scale(Geom::Scale const &s) { cairo_scale(_ct, s[Geom::X], s[Geom::Y]); } + void scale(double sx, double sy) { cairo_scale(_ct, sx, sy); } + void device_to_user_distance(double &dx, double &dy) { cairo_device_to_user_distance(_ct, &dx, &dy); } + void user_to_device_distance(double &dx, double &dy) { cairo_user_to_device_distance(_ct, &dx, &dy); } + + void moveTo(Geom::Point const &p) { cairo_move_to(_ct, p[Geom::X], p[Geom::Y]); } + void lineTo(Geom::Point const &p) { cairo_line_to(_ct, p[Geom::X], p[Geom::Y]); } + void curveTo(Geom::Point const &p1, Geom::Point const &p2, Geom::Point const &p3) { + cairo_curve_to(_ct, p1[Geom::X], p1[Geom::Y], p2[Geom::X], p2[Geom::Y], p3[Geom::X], p3[Geom::Y]); + } + void arc(Geom::Point const ¢er, double radius, Geom::AngleInterval const &angle); + void closePath() { cairo_close_path(_ct); } + void rectangle(Geom::Rect const &r) { + cairo_rectangle(_ct, r.left(), r.top(), r.width(), r.height()); + } + void rectangle(Geom::IntRect const &r) { + cairo_rectangle(_ct, r.left(), r.top(), r.width(), r.height()); + } + // Used in drawing-text.cpp to overwrite glyphs, which have the opposite path rotation as a regular rect + void revrectangle(Geom::Rect const &r) { + cairo_move_to ( _ct, r.left(), r.top() ); + cairo_rel_line_to (_ct, 0, r.height() ); + cairo_rel_line_to (_ct, r.width(), 0 ); + cairo_rel_line_to (_ct, 0, -r.height() ); + cairo_close_path ( _ct); + } + void revrectangle(Geom::IntRect const &r) { + cairo_move_to ( _ct, r.left(), r.top() ); + cairo_rel_line_to (_ct, 0, r.height() ); + cairo_rel_line_to (_ct, r.width(), 0 ); + cairo_rel_line_to (_ct, 0, -r.height() ); + cairo_close_path ( _ct); + } + void newPath() { cairo_new_path(_ct); } + void newSubpath() { cairo_new_sub_path(_ct); } + void path(Geom::PathVector const &pv); + + void paint(double alpha = 1.0); + void fill() { cairo_fill(_ct); } + void fillPreserve() { cairo_fill_preserve(_ct); } + void stroke() { cairo_stroke(_ct); } + void strokePreserve() { cairo_stroke_preserve(_ct); } + void clip() { cairo_clip(_ct); } + + void setLineWidth(double w) { cairo_set_line_width(_ct, w); } + void setLineCap(cairo_line_cap_t cap) { cairo_set_line_cap(_ct, cap); } + void setLineJoin(cairo_line_join_t join) { cairo_set_line_join(_ct, join); } + void setMiterLimit(double miter) { cairo_set_miter_limit(_ct, miter); } + void setFillRule(cairo_fill_rule_t rule) { cairo_set_fill_rule(_ct, rule); } + void setOperator(cairo_operator_t op) { cairo_set_operator(_ct, op); } + cairo_operator_t getOperator() { return cairo_get_operator(_ct); } + void setTolerance(double tol) { cairo_set_tolerance(_ct, tol); } + void setSource(cairo_pattern_t *source) { cairo_set_source(_ct, source); } + void setSource(cairo_surface_t *surface, double x, double y) { + cairo_set_source_surface(_ct, surface, x, y); + } + void setSource(double r, double g, double b, double a = 1.0) { + cairo_set_source_rgba(_ct, r, g, b, a); + } + void setSource(guint32 rgba); + void setSource(DrawingSurface *s); + void setSourceCheckerboard(); + + void patternSetFilter(cairo_filter_t filter) { + cairo_pattern_set_filter(cairo_get_source(_ct), filter); + } + void patternSetExtend(cairo_extend_t extend) { cairo_pattern_set_extend(cairo_get_source(_ct), extend); } + + Geom::Rect targetLogicalBounds() const; + + cairo_t *raw() { return _ct; } + cairo_surface_t *rawTarget() { return cairo_get_group_target(_ct); } + + DrawingSurface *surface() { return _surface; } // Needed to find scale in drawing-item.cpp + +private: + DrawingContext(cairo_t *ct, DrawingSurface *surface, bool destroy); + + cairo_t *_ct; + DrawingSurface *_surface; + bool _delete_surface; + bool _restore_context; + + friend class DrawingSurface; +}; + +} // end namespace Inkscape + +#endif // !SEEN_INKSCAPE_DISPLAY_DRAWING_ITEM_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/drawing-group.cpp b/src/display/drawing-group.cpp new file mode 100644 index 0000000..397b30c --- /dev/null +++ b/src/display/drawing-group.cpp @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Group belonging to an SVG drawing element. + *//* + * Authors: + * Krzysztof KosiĆski <tweenk.pl@gmail.com> + * + * Copyright (C) 2011 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "display/drawing-group.h" +#include "display/cairo-utils.h" +#include "display/drawing-context.h" +#include "display/drawing-item.h" +#include "display/drawing-surface.h" +#include "display/drawing-text.h" +#include "display/drawing.h" +#include "style.h" + +namespace Inkscape { + +DrawingGroup::DrawingGroup(Drawing &drawing) + : DrawingItem(drawing) + , _child_transform(nullptr) +{} + +DrawingGroup::~DrawingGroup() +{ + delete _child_transform; // delete NULL; is safe +} + +/** + * Set whether the group returns children from pick calls. + * Previously this feature was called "transparent groups". + */ +void +DrawingGroup::setPickChildren(bool p) +{ + _pick_children = p; +} + +/** + * Set additional transform for the group. + * This is applied after the normal transform and mainly useful for + * markers, clipping paths, etc. + */ +void +DrawingGroup::setChildTransform(Geom::Affine const &new_trans) +{ + Geom::Affine current; + if (_child_transform) { + current = *_child_transform; + } + + if (!Geom::are_near(current, new_trans, 1e-18)) { + // mark the area where the object was for redraw. + _markForRendering(); + if (new_trans.isIdentity()) { + delete _child_transform; // delete NULL; is safe + _child_transform = nullptr; + } else { + _child_transform = new Geom::Affine(new_trans); + } + _markForUpdate(STATE_ALL, true); + } +} + +unsigned +DrawingGroup::_updateItem(Geom::IntRect const &area, UpdateContext const &ctx, unsigned flags, unsigned reset) +{ + unsigned beststate = STATE_ALL; + bool outline = _drawing.outline(); + + UpdateContext child_ctx(ctx); + if (_child_transform) { + child_ctx.ctm = *_child_transform * ctx.ctm; + } + for (auto & i : _children) { + i.update(area, child_ctx, flags, reset); + } + if (beststate & STATE_BBOX) { + _bbox = Geom::OptIntRect(); + for (auto & i : _children) { + if (i.visible()) { + _bbox.unionWith(outline ? i.geometricBounds() : i.visualBounds()); + } + } + } + return beststate; +} + +unsigned +DrawingGroup::_renderItem(DrawingContext &dc, Geom::IntRect const &area, unsigned flags, DrawingItem *stop_at) +{ + if (stop_at == nullptr) { + // normal rendering + for (auto &i : _children) { + i.setAntialiasing(_antialias); + i.render(dc, area, flags, stop_at); + } + } else { + // background rendering + for (auto &i : _children) { + if (&i == stop_at) + return RENDER_OK; // do not render the stop_at item at all + if (i.isAncestorOf(stop_at)) { + // render its ancestors without masks, opacity or filters + i.setAntialiasing(_antialias); + i.render(dc, area, flags | RENDER_FILTER_BACKGROUND, stop_at); + return RENDER_OK; + } else { + i.setAntialiasing(_antialias); + i.render(dc, area, flags, stop_at); + } + } + } + return RENDER_OK; +} + +void +DrawingGroup::_clipItem(DrawingContext &dc, Geom::IntRect const &area) +{ + for (auto & i : _children) { + i.clip(dc, area); + } +} + +DrawingItem * +DrawingGroup::_pickItem(Geom::Point const &p, double delta, unsigned flags) +{ + for (auto & i : _children) { + DrawingItem *picked = i.pick(p, delta, flags); + if (picked) { + return _pick_children ? picked : this; + } + } + return nullptr; +} + +bool +DrawingGroup::_canClip() +{ + return true; +} + +bool is_drawing_group(DrawingItem *item) +{ + return dynamic_cast<DrawingGroup *>(item) != nullptr; +} + +} // end namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/drawing-group.h b/src/display/drawing-group.h new file mode 100644 index 0000000..1f5f283 --- /dev/null +++ b/src/display/drawing-group.h @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Group belonging to an SVG drawing element. + *//* + * Authors: + * Krzysztof KosiĆski <tweenk.pl@gmail.com> + * + * Copyright (C) 2011 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_DISPLAY_DRAWING_GROUP_H +#define SEEN_INKSCAPE_DISPLAY_DRAWING_GROUP_H + +#include "display/drawing-item.h" + +namespace Inkscape { + +class DrawingGroup + : public DrawingItem +{ +public: + DrawingGroup(Drawing &drawing); + ~DrawingGroup() override; + + bool pickChildren() { return _pick_children; } + void setPickChildren(bool p); + + void setChildTransform(Geom::Affine const &new_trans); + +protected: + unsigned _updateItem(Geom::IntRect const &area, UpdateContext const &ctx, + unsigned flags, unsigned reset) override; + unsigned _renderItem(DrawingContext &dc, Geom::IntRect const &area, unsigned flags, + DrawingItem *stop_at) override; + void _clipItem(DrawingContext &dc, Geom::IntRect const &area) override; + DrawingItem *_pickItem(Geom::Point const &p, double delta, unsigned flags) override; + bool _canClip() override; + + Geom::Affine *_child_transform; +}; + +bool is_drawing_group(DrawingItem *item); + +} // end namespace Inkscape + +#endif // !SEEN_INKSCAPE_DISPLAY_DRAWING_ITEM_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/drawing-image.cpp b/src/display/drawing-image.cpp new file mode 100644 index 0000000..0ac716d --- /dev/null +++ b/src/display/drawing-image.cpp @@ -0,0 +1,254 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Bitmap image belonging to an SVG drawing. + *//* + * Authors: + * Krzysztof KosiĆski <tweenk.pl@gmail.com> + * + * Copyright (C) 2011 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/bezier-curve.h> + +#include "display/drawing.h" +#include "display/drawing-context.h" +#include "display/drawing-image.h" +#include "preferences.h" + +#include "display/cairo-utils.h" + +namespace Inkscape { + +DrawingImage::DrawingImage(Drawing &drawing) + : DrawingItem(drawing) + , _pixbuf(nullptr) +{} + +DrawingImage::~DrawingImage() +{ + // _pixbuf is owned by SPImage - do not delete it +} + +void +DrawingImage::setPixbuf(Inkscape::Pixbuf *pb) +{ + _pixbuf = pb; + + _markForUpdate(STATE_ALL, false); +} + +void +DrawingImage::setScale(double sx, double sy) +{ + _scale = Geom::Scale(sx, sy); + _markForUpdate(STATE_ALL, false); +} + +void +DrawingImage::setOrigin(Geom::Point const &o) +{ + _origin = o; + _markForUpdate(STATE_ALL, false); +} + +void +DrawingImage::setClipbox(Geom::Rect const &box) +{ + _clipbox = box; + _markForUpdate(STATE_ALL, false); +} + +Geom::Rect +DrawingImage::bounds() const +{ + if (!_pixbuf) return _clipbox; + + double pw = _pixbuf->width(); + double ph = _pixbuf->height(); + double vw = pw * _scale[Geom::X]; + double vh = ph * _scale[Geom::Y]; + Geom::Point wh(vw, vh); + Geom::Rect view(_origin, _origin+wh); + Geom::OptRect res = _clipbox & view; + Geom::Rect ret = res ? *res : _clipbox; + + return ret; +} + +unsigned +DrawingImage::_updateItem(Geom::IntRect const &, UpdateContext const &, unsigned, unsigned) +{ + _markForRendering(); + + // Calculate bbox + if (_pixbuf) { + Geom::Rect r = bounds() * _ctm; + _bbox = r.roundOutwards(); + } else { + _bbox = Geom::OptIntRect(); + } + + return STATE_ALL; +} + +unsigned DrawingImage::_renderItem(DrawingContext &dc, Geom::IntRect const &/*area*/, unsigned /*flags*/, DrawingItem * /*stop_at*/) +{ + bool outline = _drawing.outline(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool imgoutline = prefs->getBool("/options/rendering/imageinoutlinemode", false); + + if (!outline || imgoutline) { + if (!_pixbuf) return RENDER_OK; + + Inkscape::DrawingContext::Save save(dc); + dc.transform(_ctm); + dc.newPath(); + dc.rectangle(_clipbox); + dc.clip(); + + dc.translate(_origin); + dc.scale(_scale); + dc.setSource(_pixbuf->getSurfaceRaw(), 0, 0); + dc.patternSetExtend(CAIRO_EXTEND_PAD); + + if (_style) { + // See: http://www.w3.org/TR/SVG/painting.html#ImageRenderingProperty + // https://drafts.csswg.org/css-images-3/#the-image-rendering + // style.h/style.cpp, cairo-render-context.cpp + // + // CSS 3 defines: + // 'optimizeSpeed' as alias for "pixelated" + // 'optimizeQuality' as alias for "smooth" + switch (_style->image_rendering.computed) { + case SP_CSS_IMAGE_RENDERING_OPTIMIZESPEED: + case SP_CSS_IMAGE_RENDERING_PIXELATED: + // we don't have an implementation for crisp-edges, but it should *not* smooth or blur + case SP_CSS_IMAGE_RENDERING_CRISPEDGES: + dc.patternSetFilter( CAIRO_FILTER_NEAREST ); + break; + case SP_CSS_IMAGE_RENDERING_AUTO: + case SP_CSS_IMAGE_RENDERING_OPTIMIZEQUALITY: + default: + // In recent Cairo, BEST used Lanczos3, which is prohibitively slow + dc.patternSetFilter( CAIRO_FILTER_GOOD ); + break; + } + } + + dc.paint(1); + + } else { // outline; draw a rect instead + + guint32 rgba = prefs->getInt("/options/wireframecolors/images", 0xff0000ff); + + { Inkscape::DrawingContext::Save save(dc); + dc.transform(_ctm); + dc.newPath(); + + Geom::Rect r = bounds(); + Geom::Point c00 = r.corner(0); + Geom::Point c01 = r.corner(3); + Geom::Point c11 = r.corner(2); + Geom::Point c10 = r.corner(1); + + dc.moveTo(c00); + // the box + dc.lineTo(c10); + dc.lineTo(c11); + dc.lineTo(c01); + dc.lineTo(c00); + // the diagonals + dc.lineTo(c11); + dc.moveTo(c10); + dc.lineTo(c01); + } + + dc.setLineWidth(0.5); + dc.setSource(rgba); + dc.stroke(); + } + return RENDER_OK; +} + +/** Calculates the closest distance from p to the segment a1-a2*/ +static double +distance_to_segment (Geom::Point const &p, Geom::Point const &a1, Geom::Point const &a2) +{ + Geom::LineSegment l(a1, a2); + Geom::Point np = l.pointAt(l.nearestTime(p)); + return Geom::distance(np, p); +} + +DrawingItem * +DrawingImage::_pickItem(Geom::Point const &p, double delta, unsigned /*sticky*/) +{ + if (!_pixbuf) return nullptr; + + bool outline = _drawing.outline() || _drawing.getOutlineSensitive(); + + if (outline) { + Geom::Rect r = bounds(); + Geom::Point pick = p * _ctm.inverse(); + + // find whether any side or diagonal is within delta + // to do so, iterate over all pairs of corners + for (unsigned i = 0; i < 3; ++i) { // for i=3, there is nothing to do + for (unsigned j = i+1; j < 4; ++j) { + if (distance_to_segment(pick, r.corner(i), r.corner(j)) < delta) { + return this; + } + } + } + return nullptr; + + } else { + unsigned char *const pixels = _pixbuf->pixels(); + int width = _pixbuf->width(); + int height = _pixbuf->height(); + size_t rowstride = _pixbuf->rowstride(); + + Geom::Point tp = p * _ctm.inverse(); + Geom::Rect r = bounds(); + + if (!r.contains(tp)) + return nullptr; + + double vw = width * _scale[Geom::X]; + double vh = height * _scale[Geom::Y]; + int ix = floor((tp[Geom::X] - _origin[Geom::X]) / vw * width); + int iy = floor((tp[Geom::Y] - _origin[Geom::Y]) / vh * height); + + if ((ix < 0) || (iy < 0) || (ix >= width) || (iy >= height)) + return nullptr; + + unsigned char *pix_ptr = pixels + iy * rowstride + ix * 4; + // pick if the image is less than 99% transparent + guint32 alpha = 0; + if (_pixbuf->pixelFormat() == Inkscape::Pixbuf::PF_CAIRO) { + guint32 px = *reinterpret_cast<guint32 const *>(pix_ptr); + alpha = (px & 0xff000000) >> 24; + } else if (_pixbuf->pixelFormat() == Inkscape::Pixbuf::PF_GDK) { + alpha = pix_ptr[3]; + } else { + throw std::runtime_error("Unrecognized pixel format"); + } + float alpha_f = (alpha / 255.0f) * _opacity; + return alpha_f > 0.01 ? this : nullptr; + } +} + +} // end namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/drawing-image.h b/src/display/drawing-image.h new file mode 100644 index 0000000..fd962db --- /dev/null +++ b/src/display/drawing-image.h @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Bitmap image belonging to an SVG drawing. + *//* + * Authors: + * Krzysztof KosiĆski <tweenk.pl@gmail.com> + * + * Copyright (C) 2011 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_DISPLAY_DRAWING_IMAGE_H +#define SEEN_INKSCAPE_DISPLAY_DRAWING_IMAGE_H + +#include <cairo.h> +#include <gdk-pixbuf/gdk-pixbuf.h> +#include <2geom/transforms.h> + +#include "display/drawing-item.h" + +namespace Inkscape { +class Pixbuf; + +class DrawingImage + : public DrawingItem +{ +public: + DrawingImage(Drawing &drawing); + ~DrawingImage() override; + + void setPixbuf(Inkscape::Pixbuf *pb); + void setScale(double sx, double sy); + void setOrigin(Geom::Point const &o); + void setClipbox(Geom::Rect const &box); + Geom::Rect bounds() const; + +protected: + unsigned _updateItem(Geom::IntRect const &area, UpdateContext const &ctx, + unsigned flags, unsigned reset) override; + unsigned _renderItem(DrawingContext &dc, Geom::IntRect const &area, unsigned flags, + DrawingItem *stop_at) override; + DrawingItem *_pickItem(Geom::Point const &p, double delta, unsigned flags) override; + + Inkscape::Pixbuf *_pixbuf; + + // TODO: the following three should probably be merged into a new Geom::Viewbox object + Geom::Rect _clipbox; ///< for preserveAspectRatio + Geom::Point _origin; + Geom::Scale _scale; +}; + +} // end namespace Inkscape + +#endif // !SEEN_INKSCAPE_DISPLAY_DRAWING_ITEM_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/drawing-item.cpp b/src/display/drawing-item.cpp new file mode 100644 index 0000000..29a23b2 --- /dev/null +++ b/src/display/drawing-item.cpp @@ -0,0 +1,1221 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Canvas item belonging to an SVG drawing element. + *//* + * Authors: + * Krzysztof KosiĆski <tweenk.pl@gmail.com> + * + * Copyright (C) 2011 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <climits> + +#include "display/drawing-context.h" +#include "display/drawing-group.h" +#include "display/drawing-item.h" +#include "display/drawing-pattern.h" +#include "display/drawing-surface.h" +#include "display/drawing-text.h" +#include "display/drawing.h" +#include "nr-filter.h" +#include "preferences.h" +#include "style.h" + +#include "display/cairo-utils.h" +#include "display/cairo-templates.h" + +#include "object/sp-item.h" + +namespace Inkscape { +/** + * @class DrawingItem + * SVG drawing item for display. + * + * This was previously known as NRArenaItem. It represents the renderable + * portion of the SVG document. Typically this is created by the SP tree, + * in particular the show() virtual function. + * + * @section ObjectLifetime Object lifetime + * Deleting a DrawingItem will cause all of its children to be deleted as well. + * This can lead to nasty surprises if you hold references to things + * which are children of what is being deleted. Therefore, in the SP tree, + * you always need to delete the item views of children before deleting + * the view of the parent. Do not call delete on things returned from show() + * - this will cause dangling pointers inside the SPItem and lead to a crash. + * Use the corresponding hide() method. + * + * Outside of the SP tree, you should not use any references after the root node + * has been deleted. + */ + +DrawingItem::DrawingItem(Drawing &drawing) + : _drawing(drawing) + , _parent(nullptr) + , _key(0) + , _style(nullptr) + , _context_style(nullptr) + , _opacity(1.0) + , _transform(nullptr) + , _clip(nullptr) + , _mask(nullptr) + , _fill_pattern(nullptr) + , _stroke_pattern(nullptr) + , _filter(nullptr) + , _item(nullptr) + , _cache(nullptr) + , _state(0) + , _child_type(CHILD_ORPHAN) + , _background_new(0) + , _background_accumulate(0) + , _visible(true) + , _sensitive(true) + , _cached(0) + , _cached_persistent(0) + , _has_cache_iterator(0) + , _propagate(0) + // , _renders_opacity(0) + , _pick_children(0) + , _antialias(2) + , _prev_nir(false) + , _isolation(SP_CSS_ISOLATION_AUTO) + , _mix_blend_mode(SP_CSS_BLEND_NORMAL) +{} + +DrawingItem::~DrawingItem() +{ + _drawing.signal_item_deleted.emit(this); + //if (!_children.empty()) { + // g_warning("Removing item with children"); + //} + + // remove from the set of cached items and delete cache + setCached(false, true); + // remove this item from parent's children list + // due to the effect of clearChildren(), this only happens for the top-level deleted item + if (_parent) { + _markForRendering(); + } + switch (_child_type) { + case CHILD_NORMAL: { + ChildrenList::iterator ithis = _parent->_children.iterator_to(*this); + _parent->_children.erase(ithis); + } break; + case CHILD_CLIP: + // we cannot call setClip(NULL) or setMask(NULL), + // because that would be an endless loop + _parent->_clip = nullptr; + break; + case CHILD_MASK: + _parent->_mask = nullptr; + break; + case CHILD_ROOT: + _drawing._root = nullptr; + break; + case CHILD_FILL_PATTERN: + _parent->_fill_pattern = nullptr; + break; + case CHILD_STROKE_PATTERN: + _parent->_stroke_pattern = nullptr; + break; + default: ; + } + + if (_parent) { + _parent->_markForUpdate(STATE_ALL, false); + } + clearChildren(); + delete _transform; + delete _stroke_pattern; + delete _fill_pattern; + delete _clip; + delete _mask; + delete _filter; + if(_style) + sp_style_unref(_style); +} + +DrawingItem * +DrawingItem::parent() const +{ + // initially I wanted to return NULL if we are a clip or mask child, + // but the previous behavior was just to return the parent regardless of child type + return _parent; +} + +/// Returns true if item is among the descendants. Will return false if item == this. +bool +DrawingItem::isAncestorOf(DrawingItem *item) const +{ + for (DrawingItem *i = item->_parent; i; i = i->_parent) { + if (i == this) return true; + } + return false; +} + +void +DrawingItem::appendChild(DrawingItem *item) +{ + item->_parent = this; + assert(item->_child_type == CHILD_ORPHAN); + item->_child_type = CHILD_NORMAL; + _children.push_back(*item); + + // This ensures that _markForUpdate() called on the child will recurse to this item + item->_state = STATE_ALL; + // Because _markForUpdate recurses through ancestors, we can simply call it + // on the just-added child. This has the additional benefit that we do not + // rely on the appended child being in the default non-updated state. + // We set propagate to true, because the child might have descendants of its own. + item->_markForUpdate(STATE_ALL, true); +} + +void +DrawingItem::prependChild(DrawingItem *item) +{ + item->_parent = this; + assert(item->_child_type == CHILD_ORPHAN); + item->_child_type = CHILD_NORMAL; + _children.push_front(*item); + // See appendChild for explanation + item->_state = STATE_ALL; + item->_markForUpdate(STATE_ALL, true); +} + +/// Delete all regular children of this item (not mask or clip). +void +DrawingItem::clearChildren() +{ + if (_children.empty()) return; + + _markForRendering(); + // prevent children from referencing the parent during deletion + // this way, children won't try to remove themselves from a list + // from which they have already been removed by clear_and_dispose + for (auto & i : _children) { + i._parent = NULL; + i._child_type = CHILD_ORPHAN; + } + _children.clear_and_dispose(DeleteDisposer()); + _markForUpdate(STATE_ALL, false); +} + +/// Set the incremental transform for this item +void +DrawingItem::setTransform(Geom::Affine const &new_trans) +{ + Geom::Affine current; + if (_transform) { + current = *_transform; + } + + if (!Geom::are_near(current, new_trans, 1e-18)) { + // mark the area where the object was for redraw. + _markForRendering(); + delete _transform; + if (new_trans.isIdentity()) { + _transform = nullptr; + } else { + _transform = new Geom::Affine(new_trans); + } + _markForUpdate(STATE_ALL, true); + } +} + +void +DrawingItem::setOpacity(float opacity) +{ + if (_opacity != opacity) { + _opacity = opacity; + _markForRendering(); + } +} + +void +DrawingItem::setAntialiasing(unsigned a) +{ + if (_antialias != a) { + _antialias = a; + _markForRendering(); + } +} + +void +DrawingItem::setIsolation(bool isolation) +{ + _isolation = isolation; + //if( isolation != 0 ) std::cout << "isolation: " << isolation << std::endl; + _markForRendering(); +} + +void +DrawingItem::setBlendMode(SPBlendMode mix_blend_mode) +{ + _mix_blend_mode = mix_blend_mode; + //if( mix_blend_mode != 0 ) std::cout << "setBlendMode: " << mix_blend_mode << std::endl; + _markForRendering(); +} + +void +DrawingItem::setVisible(bool v) +{ + if (_visible != v) { + _visible = v; + _markForRendering(); + } +} + +/// This is currently unused +void +DrawingItem::setSensitive(bool s) +{ + _sensitive = s; +} + +/** + * Enable / disable storing the rendering in memory. + * Calling setCached(false, true) will also remove the persistent status + */ +void +DrawingItem::setCached(bool cached, bool persistent) +{ + static const char *cache_env = getenv("_INKSCAPE_DISABLE_CACHE"); + if (cache_env) return; + + if (_cached_persistent && !persistent) + return; + + _cached = cached; + _cached_persistent = persistent ? cached : false; + if (cached) { + _drawing._cached_items.insert(this); + } else { + _drawing._cached_items.erase(this); + delete _cache; + _cache = nullptr; + if (_has_cache_iterator) { + _drawing._candidate_items.erase(_cache_iterator); + _has_cache_iterator = false; + } + } +} + +/** + * Process information related to the new style. + * + * Note: _style is not used by DrawingGlyphs which uses its parent style. + */ +void +DrawingItem::setStyle(SPStyle *style, SPStyle *context_style) +{ + // std::cout << "DrawingItem::setStyle: " << name() << " " << style + // << " " << context_style << std::endl; + + if( style != _style ) { + if (style) sp_style_ref(style); + if (_style) sp_style_unref(_style); + _style = style; + } + + if (style && style->filter.set && style->getFilter()) { + if (!_filter) { + int primitives = SP_FILTER(style->getFilter())->primitive_count(); + _filter = new Inkscape::Filters::Filter(primitives); + } + SP_FILTER(style->getFilter())->build_renderer(_filter); + } else { + // no filter set for this group + delete _filter; + _filter = nullptr; + } + + if (style && style->enable_background.set) { + bool _background_new_check = _background_new; + if (style->enable_background.value == SP_CSS_BACKGROUND_NEW) { + _background_new = true; + } + if (style->enable_background.value == SP_CSS_BACKGROUND_ACCUMULATE) { + _background_new = false; + } + if (_background_new_check != _background_new) { + _markForUpdate(STATE_BACKGROUND, true); + } + } + + if (context_style != nullptr) { + _context_style = context_style; + } else if (_parent != nullptr) { + _context_style = _parent->_context_style; + } + + _markForUpdate(STATE_ALL, false); +} + + +/** + * Recursively update children style. + * The purpose of this call is to update fill and stroke for markers that have elements with + * fill/stroke property values of 'context-fill' or 'context-stroke'. Marker styling is not + * updated like other 'clones' as marker instances are not included the SP object tree. + * Note: this is a virtual function. + */ +void +DrawingItem::setChildrenStyle(SPStyle* context_style) +{ + _context_style = context_style; + for (auto & i : _children) { + i.setChildrenStyle( context_style ); + } +} + + +void +DrawingItem::setClip(DrawingItem *item) +{ + _markForRendering(); + delete _clip; + _clip = item; + if (item) { + item->_parent = this; + assert(item->_child_type == CHILD_ORPHAN); + item->_child_type = CHILD_CLIP; + } + _markForUpdate(STATE_ALL, true); +} + +void +DrawingItem::setMask(DrawingItem *item) +{ + _markForRendering(); + delete _mask; + _mask = item; + if (item) { + item->_parent = this; + assert(item->_child_type == CHILD_ORPHAN); + item->_child_type = CHILD_MASK; + } + _markForUpdate(STATE_ALL, true); +} + +void +DrawingItem::setFillPattern(DrawingPattern *pattern) +{ + _markForRendering(); + delete _fill_pattern; + _fill_pattern = pattern; + if (pattern) { + pattern->_parent = this; + assert(pattern->_child_type == CHILD_ORPHAN); + pattern->_child_type = CHILD_FILL_PATTERN; + } + _markForUpdate(STATE_ALL, true); +} + +void +DrawingItem::setStrokePattern(DrawingPattern *pattern) +{ + _markForRendering(); + delete _stroke_pattern; + _stroke_pattern = pattern; + if (pattern) { + pattern->_parent = this; + assert(pattern->_child_type == CHILD_ORPHAN); + pattern->_child_type = CHILD_STROKE_PATTERN; + } + _markForUpdate(STATE_ALL, true); +} + +/// Move this item to the given place in the Z order of siblings. +/// Does nothing if the item has no parent. +void +DrawingItem::setZOrder(unsigned z) +{ + if (!_parent) return; + + ChildrenList::iterator it = _parent->_children.iterator_to(*this); + _parent->_children.erase(it); + + ChildrenList::iterator i = _parent->_children.begin(); + std::advance(i, std::min(z, unsigned(_parent->_children.size()))); + _parent->_children.insert(i, *this); + _markForRendering(); +} + +void +DrawingItem::setItemBounds(Geom::OptRect const &bounds) +{ + _item_bbox = bounds; +} + +/** + * Update derived data before operations. + * The purpose of this call is to recompute internal data which depends + * on the attributes of the object, but is not directly settable by the user. + * Precomputing this data speeds up later rendering, because some items + * can be omitted. + * + * Currently this method handles updating the visual and geometric bounding boxes + * in pixels, storing the total transformation from item space to the screen + * and cache invalidation. + * + * @param area Area to which the update should be restricted. Only takes effect + * if the bounding box is known. + * @param ctx A structure to store cascading state. + * @param flags Which internal data should be recomputed. This can be any combination + * of StateFlags. + * @param reset State fields that should be reset before processing them. This is + * a means to force a recomputation of internal data even if the item + * considers it up to date. Mainly for internal use, such as + * propagating bounding box recomputation to children when the item's + * transform changes. + */ +void +DrawingItem::update(Geom::IntRect const &area, UpdateContext const &ctx, unsigned flags, unsigned reset) +{ + bool render_filters = _drawing.renderFilters(); + bool outline = _drawing.outline(); + + // Set reset flags according to propagation status + reset |= _propagate_state; + _propagate_state = 0; + + _state &= ~reset; // reset state of this item + + if ((~_state & flags) == 0) return; // nothing to do + + // TODO this might be wrong + if (_state & STATE_BBOX) { + // we have up-to-date bbox + if (!area.intersects(outline ? _bbox : _drawbox)) return; + } + + // compute which elements need an update + unsigned to_update = _state ^ flags; + + // this needs to be called before we recurse into children + if (to_update & STATE_BACKGROUND) { + _background_accumulate = _background_new; + if (_child_type == CHILD_NORMAL && _parent->_background_accumulate) + _background_accumulate = true; + } + + UpdateContext child_ctx(ctx); + if (_transform) { + child_ctx.ctm = *_transform * ctx.ctm; + } + + // Vector effects + if (_style) { + + if (_style->vector_effect.fixed) { + child_ctx.ctm.setTranslation(Geom::Point(0,0)); + } + + if (_style->vector_effect.size) { + double value = sqrt(child_ctx.ctm.det()); + if (value > 0 ) { + child_ctx.ctm[0] = child_ctx.ctm[0]/value; + child_ctx.ctm[1] = child_ctx.ctm[1]/value; + child_ctx.ctm[2] = child_ctx.ctm[2]/value; + child_ctx.ctm[3] = child_ctx.ctm[3]/value; + } + } + + if (_style->vector_effect.rotate) { + double value = sqrt(child_ctx.ctm.det()); + child_ctx.ctm[0] = value; + child_ctx.ctm[1] = 0; + child_ctx.ctm[2] = 0; + child_ctx.ctm[3] = value; + } + } + + /* Remember the transformation matrix */ + Geom::Affine ctm_change = _ctm.inverse() * child_ctx.ctm; + _ctm = child_ctx.ctm; + + // update _bbox and call this function for children + _state = _updateItem(area, child_ctx, flags, reset); + + if (to_update & STATE_BBOX) { + // compute drawbox + if (_filter && render_filters) { + Geom::OptRect enlarged = _filter->filter_effect_area(_item_bbox); + if (enlarged) { + *enlarged *= ctm(); + _drawbox = enlarged->roundOutwards(); + } else { + _drawbox = Geom::OptIntRect(); + } + } else { + _drawbox = _bbox; + } + + // Clipping + if (_clip) { + _clip->update(area, child_ctx, flags, reset); + if (outline) { + _bbox.unionWith(_clip->_bbox); + } else { + _drawbox.intersectWith(_clip->_bbox); + } + } + // Masking + if (_mask) { + _mask->update(area, child_ctx, flags, reset); + if (outline) { + _bbox.unionWith(_mask->_bbox); + } else { + // for masking, we need full drawbox of mask + _drawbox.intersectWith(_mask->_drawbox); + } + } + } + if (to_update & STATE_CACHE) { + // Update cache score for this item + if (_has_cache_iterator) { + // remove old score information + _drawing._candidate_items.erase(_cache_iterator); + _has_cache_iterator = false; + } + double score = _cacheScore(); + if (score >= _drawing._cache_score_threshold) { + CacheRecord cr; + cr.score = score; + // if _cacheRect() is empty, a negative score will be returned from _cacheScore(), + // so this will not execute (cache score threshold must be positive) + cr.cache_size = _cacheRect()->area() * 4; + cr.item = this; + _drawing._candidate_items.push_front(cr); + _cache_iterator = _drawing._candidate_items.begin(); + _has_cache_iterator = true; + } + + /* Update cache if enabled. + * General note: here we only tell the cache how it has to transform + * during the render phase. The transformation is deferred because + * after the update the item can have its caching turned off, + * e.g. because its filter was removed. This way we avoid tempoerarily + * using more memory than the cache budget */ + if (_cache) { + Geom::OptIntRect cl = _cacheRect(); + if (_visible && cl && _has_cache_iterator) { // never create cache for invisible items + // this takes care of invalidation on transform + _cache->scheduleTransform(*cl, ctm_change); + } else { + // Destroy cache for this item - outside of canvas or invisible. + // The opposite transition (invisible -> visible or object + // entering the canvas) is handled during the render phase + setCached(false, true); + } + } + } + + if (to_update & STATE_RENDER) { + // now that we know drawbox, dirty the corresponding rect on canvas + // unless filtered, groups do not need to render by themselves, only their members + if (_fill_pattern) { + _fill_pattern->update(area, child_ctx, flags, reset); + } + if (_stroke_pattern) { + _stroke_pattern->update(area, child_ctx, flags, reset); + } + if (!is_drawing_group(this) || (_filter && render_filters)) { + _markForRendering(); + } + } +} + +struct MaskLuminanceToAlpha { + guint32 operator()(guint32 in) { + guint r = 0, g = 0, b = 0; + Display::ExtractRGB32(in, r, g, b); + // the operation of unpremul -> luminance-to-alpha -> multiply by alpha + // is equivalent to luminance-to-alpha on premultiplied color values + // original computation in double: r*0.2125 + g*0.7154 + b*0.0721 + guint32 ao = r*109 + g*366 + b*37; // coeffs add up to 512 + return ((ao + 256) << 15) & 0xff000000; // equivalent to ((ao + 256) / 512) << 24 + } +}; + +/** + * Rasterize items. + * This method submits the drawing operations required to draw this item + * to the supplied DrawingContext, restricting drawing the specified area. + * + * This method does some common tasks and calls the item-specific rendering + * function, _renderItem(), to render e.g. paths or bitmaps. + * + * @param flags Rendering options. This deals mainly with cache control. + */ +unsigned +DrawingItem::render(DrawingContext &dc, Geom::IntRect const &area, unsigned flags, DrawingItem *stop_at) +{ + bool outline = _drawing.outline(); + bool render_filters = _drawing.renderFilters(); + // stop_at is handled in DrawingGroup, but this check is required to handle the case + // where a filtered item with background-accessing filter has enable-background: new + if (this == stop_at) { + return RENDER_STOP; + } + + // If we are invisible, return immediately + if (!_visible) { + return RENDER_OK; + } + + if (_ctm.isSingular(1e-18)) { + return RENDER_OK; + } + + // TODO convert outline rendering to a separate virtual function + if (outline) { + _renderOutline(dc, area, flags); + return RENDER_OK; + } + + // carea is the area to paint + Geom::OptIntRect carea = Geom::intersect(area, _drawbox); + + if (!carea) { + return RENDER_OK; + } + // iarea is the bounding box for intermediate rendering + // Note 1: Pixels inside iarea but outside carea are invalid + // (incomplete filter dependence region). + // Note 2: We only need to render carea of clip and mask, but + // iarea of the object. + + Geom::OptIntRect iarea = carea; + // expand carea to contain the dependent area of filters. + if (_filter && render_filters) { + iarea = _cacheRect(); + if (!iarea) { + iarea = carea; + _filter->area_enlarge(*iarea, this); + iarea.intersectWith(_drawbox); + setCached(false, true); + } else { + setCached(true, true); + } + } + + if (!iarea) { + return RENDER_OK; + } + + // Device scale for HiDPI screens (typically 1 or 2) + int device_scale = dc.surface()->device_scale(); + + switch(_antialias){ + case 0: + cairo_set_antialias(dc.raw(), CAIRO_ANTIALIAS_NONE); + break; + case 1: + cairo_set_antialias(dc.raw(), CAIRO_ANTIALIAS_FAST); + break; + case 2: + cairo_set_antialias(dc.raw(), CAIRO_ANTIALIAS_GOOD); + break; + case 3: + cairo_set_antialias(dc.raw(), CAIRO_ANTIALIAS_BEST); + break; + default: // should not happen + g_assert_not_reached(); + } + + // Render from cache if possible + // Bypass in case of pattern, see below. + if (_cached && !(flags & RENDER_BYPASS_CACHE)) { + if (_cache) { + _cache->prepare(); + dc.setOperator(ink_css_blend_to_cairo_operator(_mix_blend_mode)); + _cache->paintFromCache(dc, carea, _filter && render_filters); + if (!carea) { + dc.setSource(0, 0, 0, 0); + return RENDER_OK; + } + } else { + // There is no cache. This could be because caching of this item + // was just turned on after the last update phase, or because + // we were previously outside of the canvas. + if (iarea) { + _cache = new DrawingCache(*iarea, device_scale); + } + } + } else { + // if our caching was turned off after the last update, it was already + // deleted in setCached() + } + + // determine whether this shape needs intermediate rendering. + bool needs_intermediate_rendering = false; + bool &nir = needs_intermediate_rendering; + bool needs_opacity = (_opacity < 0.995); + + // this item needs an intermediate rendering if: + nir |= (_clip != nullptr); // 1. it has a clipping path + nir |= (_mask != nullptr); // 2. it has a mask + nir |= (_filter != nullptr && render_filters); // 3. it has a filter + nir |= needs_opacity; // 4. it is non-opaque + nir |= (_mix_blend_mode != SP_CSS_BLEND_NORMAL); // 5. it has blend mode + nir |= (_isolation == SP_CSS_ISOLATION_ISOLATE); // 6. it is isolated + nir |= !parent(); // 7. is root, need isolation from background + if (_prev_nir && !needs_intermediate_rendering) { + setCached(false, true); + } + _prev_nir = needs_intermediate_rendering; + nir |= (_cache != nullptr); // 5. it is to be cached + + /* How the rendering is done. + * + * Clipping, masking and opacity are done by rendering them to a surface + * and then compositing the object's rendering onto it with the IN operator. + * The object itself is rendered to a group. + * + * Opacity is done by rendering the clipping path with an alpha + * value corresponding to the opacity. If there is no clipping path, + * the entire intermediate surface is painted with alpha corresponding + * to the opacity value. + * + */ + // Short-circuit the simple case. + // We also use this path for filter background rendering, because masking, clipping, + // filters and opacity do not apply when rendering the ancestors of the filtered + // element + + if ((flags & RENDER_FILTER_BACKGROUND) || !needs_intermediate_rendering) { + dc.setOperator(ink_css_blend_to_cairo_operator(SP_CSS_BLEND_NORMAL)); + return _renderItem(dc, *iarea, flags & ~RENDER_FILTER_BACKGROUND, stop_at); + } + + + DrawingSurface intermediate(*iarea, device_scale); + DrawingContext ict(intermediate); + + // This path fails for patterns/hatches when stepping the pattern to handle overflows. + // The offsets are applied to drawing context (dc) but they are not copied to the + // intermediate context. Something like this is needed: + // Copy cairo matrix from dc to intermediate, needed for patterns/hatches + // cairo_matrix_t cairo_matrix; + // cairo_get_matrix(dc.raw(), &cairo_matrix); + // cairo_set_matrix(ict.raw(), &cairo_matrix); + // For the moment we disable caching for patterns, + // see https://gitlab.com/inkscape/inkscape/-/issues/309 + + unsigned render_result = RENDER_OK; + + // 1. Render clipping path with alpha = opacity. + ict.setSource(0,0,0,_opacity); + // Since clip can be combined with opacity, the result could be incorrect + // for overlapping clip children. To fix this we use the SOURCE operator + // instead of the default OVER. + ict.setOperator(CAIRO_OPERATOR_SOURCE); + ict.paint(); + if (_clip) { + ict.pushGroup(); + _clip->clip(ict, *carea); // fixme: carea or area? + ict.popGroupToSource(); + ict.setOperator(CAIRO_OPERATOR_IN); + ict.paint(); + } + ict.setOperator(CAIRO_OPERATOR_OVER); // reset back to default + + // 2. Render the mask if present and compose it with the clipping path + opacity. + if (_mask) { + ict.pushGroup(); + _mask->render(ict, *carea, flags); + + cairo_surface_t *mask_s = ict.rawTarget(); + // Convert mask's luminance to alpha + ink_cairo_surface_filter(mask_s, mask_s, MaskLuminanceToAlpha()); + ict.popGroupToSource(); + ict.setOperator(CAIRO_OPERATOR_IN); + ict.paint(); + ict.setOperator(CAIRO_OPERATOR_OVER); + } + + // 3. Render object itself + ict.pushGroup(); + render_result = _renderItem(ict, *iarea, flags, stop_at); + + // 4. Apply filter. + if (_filter && render_filters) { + bool rendered = false; + if (_filter->uses_background() && _background_accumulate) { + DrawingItem *bg_root = this; + for (; bg_root; bg_root = bg_root->_parent) { + if (bg_root->_background_new) break; + } + if (bg_root) { + DrawingSurface bg(*iarea, device_scale); + DrawingContext bgdc(bg); + bg_root->render(bgdc, *iarea, flags | RENDER_FILTER_BACKGROUND, this); + _filter->render(this, ict, &bgdc); + rendered = true; + } + } + if (!rendered) { + _filter->render(this, ict, nullptr); + } + // Note that because the object was rendered to a group, + // the internals of the filter need to use cairo_get_group_target() + // instead of cairo_get_target(). + } + + // 5. Render object inside the composited mask + clip + ict.popGroupToSource(); + ict.setOperator(CAIRO_OPERATOR_IN); + ict.paint(); + + // 6. Paint the completed rendering onto the base context (or into cache) + if (_cached && _cache) { + DrawingContext cachect(*_cache); + cachect.rectangle(*iarea); + cachect.setOperator(CAIRO_OPERATOR_SOURCE); + cachect.setSource(&intermediate); + cachect.fill(); + Geom::OptIntRect cl = _cacheRect(); + if (_filter && render_filters && cl) { + _cache->markClean(*cl); + } else { + _cache->markClean(*iarea); + } + } + + dc.rectangle(*carea); + dc.setSource(&intermediate); + // 7. Render blend mode + dc.setOperator(ink_css_blend_to_cairo_operator(_mix_blend_mode)); + dc.fill(); + dc.setSource(0,0,0,0); + // Web isolation only works if parent doesnt have transform + + + // the call above is to clear a ref on the intermediate surface held by dc + + return render_result; +} + +void +DrawingItem::_renderOutline(DrawingContext &dc, Geom::IntRect const &area, unsigned flags) +{ + // intersect with bbox rather than drawbox, as we want to render things outside + // of the clipping path as well + Geom::OptIntRect carea = Geom::intersect(area, _bbox); + if (!carea) return; + + // just render everything: item, clip, mask + // First, render the object itself + _renderItem(dc, *carea, flags, nullptr); + + // render clip and mask, if any + guint32 saved_rgba = _drawing.outlinecolor; // save current outline color + // render clippath as an object, using a different color + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (_clip) { + _drawing.outlinecolor = prefs->getInt("/options/wireframecolors/clips", 0x00ff00ff); // green clips + _clip->render(dc, *carea, flags); + } + // render mask as an object, using a different color + if (_mask) { + _drawing.outlinecolor = prefs->getInt("/options/wireframecolors/masks", 0x0000ffff); // blue masks + _mask->render(dc, *carea, flags); + } + _drawing.outlinecolor = saved_rgba; // restore outline color +} + +/** + * Rasterize the clipping path. + * This method submits drawing operations required to draw a basic filled shape + * of the item to the supplied drawing context. Rendering is limited to the + * given area. The rendering of the clipped object is composited into + * the result of this call using the IN operator. See the implementation + * of render() for details. + */ +void +DrawingItem::clip(Inkscape::DrawingContext &dc, Geom::IntRect const &area) +{ + // don't bother if the object does not implement clipping (e.g. DrawingImage) + if (!_canClip()) return; + if (!_visible) return; + if (!area.intersects(_bbox)) return; + + dc.setSource(0,0,0,1); + dc.pushGroup(); + // rasterize the clipping path + _clipItem(dc, area); + if (_clip) { + // The item used as the clipping path itself has a clipping path. + // Render this item's clipping path onto a temporary surface, then composite it + // with the item using the IN operator + dc.pushGroup(); + _clip->clip(dc, area); + dc.popGroupToSource(); + dc.setOperator(CAIRO_OPERATOR_IN); + dc.paint(); + } + dc.popGroupToSource(); + dc.setOperator(CAIRO_OPERATOR_OVER); + dc.paint(); + dc.setSource(0,0,0,0); +} + +/** + * Get the item under the specified point. + * Searches the tree for the first item in the Z-order which is closer than + * @a delta to the given point. The pick should be visual - for example + * an object with a thick stroke should pick on the entire area of the stroke. + * @param p Search point + * @param delta Maximum allowed distance from the point + * @param sticky Whether the pick should ignore visibility and sensitivity. + * When false, only visible and sensitive objects are considered. + * When true, invisible and insensitive objects can also be picked. + */ +DrawingItem * +DrawingItem::pick(Geom::Point const &p, double delta, unsigned flags) +{ + // Sometimes there's no BBOX in state, reason unknown (bug 992817) + // I made this not an assert to remove the warning + if (!(_state & STATE_BBOX) || !(_state & STATE_PICK)) { + g_warning("Invalid state when picking: STATE_BBOX = %d, STATE_PICK = %d", + _state & STATE_BBOX, _state & STATE_PICK); + return nullptr; + } + // ignore invisible and insensitive items unless sticky + if (!(flags & PICK_STICKY) && !(_visible && _sensitive)) + return nullptr; + + bool outline = _drawing.outline() || _drawing.getOutlineSensitive(); + + if (!_drawing.outline() && !_drawing.getOutlineSensitive()) { + // pick inside clipping path; if NULL, it means the object is clipped away there + if (_clip) { + DrawingItem *cpick = _clip->pick(p, delta, flags | PICK_AS_CLIP); + if (!cpick) return nullptr; + } + // same for mask + if (_mask) { + DrawingItem *mpick = _mask->pick(p, delta, flags); + if (!mpick) return nullptr; + } + } + + Geom::OptIntRect box = (outline || (flags & PICK_AS_CLIP)) ? _bbox : _drawbox; + if (!box) { + return nullptr; + } + + Geom::Rect expanded = *box; + expanded.expandBy(delta); + DrawingGlyphs *dglyps = dynamic_cast<DrawingGlyphs *>(this); + if (dglyps && !(flags & PICK_AS_CLIP)) { + expanded = (Geom::Rect)dglyps->getPickBox(); + } + + if (expanded.contains(p)) { + return _pickItem(p, delta, flags); + } + return nullptr; +} + +// For debugging +Glib::ustring +DrawingItem::name() +{ + if (_item) { + if (_item->getId()) + return _item->getId(); + else + return "No object id"; + } else { + return "No associated object"; + } +} + +// For debugging: Print drawing tree structure. +void +DrawingItem::recursivePrintTree( unsigned level ) +{ + if (level == 0) { + std::cout << "Display Item Tree" << std::endl; + } + std::cout << "DI: "; + for (unsigned i = 0; i < level; ++i) { + std::cout << " "; + } + std::cout << name() << std::endl; + for (auto & i : _children) { + i.recursivePrintTree( level+1 ); + } +} + + +/** + * Marks the current visual bounding box of the item for redrawing. + * This is called whenever the object changes its visible appearance. + * For some cases (such as setting opacity) this is enough, but for others + * _markForUpdate() also needs to be called. + */ +void +DrawingItem::_markForRendering() +{ + // TODO: this function does too much work when a large subtree + // is invalidated - fix + + bool outline = _drawing.outline(); + Geom::OptIntRect dirty = outline ? _bbox : _drawbox; + if (!dirty) return; + + // dirty the caches of all parents + DrawingItem *bkg_root = nullptr; + + for (DrawingItem *i = this; i; i = i->_parent) { + if (i != this && i->_filter) { + i->_filter->area_enlarge(*dirty, i); + } + if (i->_cache) { + i->_cache->markDirty(*dirty); + } + if (i->_background_accumulate) { + bkg_root = i; + } + } + + if (bkg_root && bkg_root->_parent && bkg_root->_parent->_parent) { + bkg_root->_invalidateFilterBackground(*dirty); + } + _drawing.signal_request_render.emit(*dirty); +} + +void +DrawingItem::_invalidateFilterBackground(Geom::IntRect const &area) +{ + if (!_drawbox.intersects(area)) return; + + if (_cache && _filter && _filter->uses_background()) { + _cache->markDirty(area); + } + + for (auto & i : _children) { + i._invalidateFilterBackground(area); + } +} + +/** + * Marks the item as needing a recomputation of internal data. + * + * This mechanism avoids traversing the entire rendering tree (which could be vast) + * on every trivial state changed in any item. Only items marked as needing + * an update (having some bits in their _state unset) will be traversed + * during the update call. + * + * The _propagate variable is another optimization. We use it to specify that + * all children should also have the corresponding flags unset before checking + * whether they need to be traversed. This way there is one less traversal + * of the tree. Without this we would need to unset state bits in all children. + * With _propagate we do this during the update call, when we have to recurse + * into children anyway. + */ +void +DrawingItem::_markForUpdate(unsigned flags, bool propagate) +{ + if (propagate) { + _propagate_state |= flags; + } + + if (_state & flags) { + unsigned oldstate = _state; + _state &= ~flags; + if (oldstate != _state && _parent) { + // If we actually reset anything in state, recurse on the parent. + _parent->_markForUpdate(flags, false); + } else { + // If nothing changed, it means our ancestors are already invalidated + // up to the root. Do not bother recursing, because it won't change anything. + // Also do this if we are the root item, because we have no more ancestors + // to invalidate. + _drawing.signal_request_update.emit(this); + } + } +} + +/** + * Compute the caching score. + * + * Higher scores mean the item is more aggressively prioritized for automatic + * caching by Inkscape::Drawing. + */ +double +DrawingItem::_cacheScore() +{ + Geom::OptIntRect cache_rect = _cacheRect(); + if (!cache_rect) return -1.0; + // a crude first approximation: + // the basic score is the number of pixels in the drawbox + double score = cache_rect->area(); + // this is multiplied by the filter complexity and its expansion + if (_filter &&_drawing.renderFilters()) { + score *= _filter->complexity(_ctm); + Geom::IntRect ref_area = Geom::IntRect::from_xywh(0, 0, 16, 16); + Geom::IntRect test_area = ref_area; + Geom::IntRect limit_area(0, INT_MIN, 16, INT_MAX); + _filter->area_enlarge(test_area, this); + // area_enlarge never shrinks the rect, so the result of intersection below + // must be non-empty + score *= double((test_area & limit_area)->area()) / ref_area.area(); + } + // if the object is clipped, add 1/2 of its bbox pixels + if (_clip && _clip->_bbox) { + score += _clip->_bbox->area() * 0.5; + } + // if masked, add mask score + if (_mask) { + score += _mask->_cacheScore(); + } + //g_message("caching score: %f", score); + return score; +} + +inline void expandByScale(Geom::IntRect &rect, double scale) +{ + double fraction = (scale - 1) / 2; + rect.expandBy(rect.width() * fraction, rect.height() * fraction); +} + + +Geom::OptIntRect DrawingItem::_cacheRect() +{ + Geom::OptIntRect r = _drawbox & _drawing.cacheLimit(); + if (_filter && _drawing.cacheLimit() && _drawing.renderFilters() && r && r != _drawbox) { + // we check unfiltered item is emought inside the cache area to render properly + Geom::OptIntRect canvas = r; + expandByScale(*canvas, 0.5); + Geom::OptIntRect valid = Geom::intersect(canvas, _bbox); + if (!valid) { + valid = _bbox; + // contract the item _bbox to get reduced size to render. $ seems good enought + expandByScale(*valid, 0.5); + // now we get the nearest point to cache area + Geom::IntPoint center = (*_drawing.cacheLimit()).midpoint(); + Geom::IntPoint nearest = (*valid).nearestEdgePoint(center); + r.expandTo(nearest); + } + return _drawbox & r; + } + return r; +} + +} // end namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/drawing-item.h b/src/display/drawing-item.h new file mode 100644 index 0000000..1cd677b --- /dev/null +++ b/src/display/drawing-item.h @@ -0,0 +1,256 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Canvas item belonging to an SVG drawing element. + *//* + * Authors: + * Krzysztof KosiĆski <tweenk.pl@gmail.com> + * + * Copyright (C) 2011 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_DISPLAY_DRAWING_ITEM_H +#define SEEN_INKSCAPE_DISPLAY_DRAWING_ITEM_H + +#include <2geom/rect.h> +#include <2geom/affine.h> +#include <boost/operators.hpp> +#include <boost/utility.hpp> +#include <boost/intrusive/list.hpp> +#include <exception> +#include <list> + +#include "style-enums.h" + +namespace Glib { +class ustring; +} + +class SPStyle; +class SPItem; + +namespace Inkscape { + +class Drawing; +class DrawingCache; +class DrawingContext; +class DrawingItem; +class DrawingPattern; + +namespace Filters { + +class Filter; + +} // namespace Filters + + + +struct UpdateContext { + Geom::Affine ctm; +}; + +struct CacheRecord + : boost::totally_ordered<CacheRecord> +{ + bool operator<(CacheRecord const &other) const { return score < other.score; } + bool operator==(CacheRecord const &other) const { return score == other.score; } + operator DrawingItem *() const { return item; } + double score; + size_t cache_size; + DrawingItem *item; +}; +typedef std::list<CacheRecord> CacheList; + +class InvalidItemException : public std::exception { + const char *what() const noexcept override { + return "Invalid item in drawing"; + } +}; + +class DrawingItem + : boost::noncopyable +{ +public: + enum RenderFlags { + RENDER_DEFAULT = 0, + RENDER_CACHE_ONLY = 1, + RENDER_BYPASS_CACHE = 2, + RENDER_FILTER_BACKGROUND = 4 + }; + enum StateFlags { + STATE_NONE = 0, + STATE_BBOX = (1<<0), // bounding boxes are up-to-date + STATE_CACHE = (1<<1), // cache extents and clean area are up-to-date + STATE_PICK = (1<<2), // can process pick requests + STATE_RENDER = (1<<3), // can be rendered + STATE_BACKGROUND = (1<<4), // filter background data is up to date + STATE_ALL = (1<<5)-1 + }; + enum PickFlags { + PICK_NORMAL = 0, // normal pick + PICK_STICKY = (1<<0), // sticky pick - ignore visibility and sensitivity + PICK_AS_CLIP = (1<<2) // pick with no stroke and opaque fill regardless of item style + }; + + DrawingItem(Drawing &drawing); + virtual ~DrawingItem(); + + Geom::OptIntRect geometricBounds() const { return _bbox; } + Geom::OptIntRect visualBounds() const { return _drawbox; } + Geom::OptRect itemBounds() const { return _item_bbox; } + Geom::Affine ctm() const { return _ctm; } + Geom::Affine transform() const { return _transform ? *_transform : Geom::identity(); } + Drawing &drawing() const { return _drawing; } + DrawingItem *parent() const; + bool isAncestorOf(DrawingItem *item) const; + + void appendChild(DrawingItem *item); + void prependChild(DrawingItem *item); + void clearChildren(); + + bool visible() const { return _visible; } + void setVisible(bool v); + bool sensitive() const { return _sensitive; } + void setSensitive(bool v); + bool cached() const { return _cached; } + void setCached(bool c, bool persistent = false); + + virtual void setStyle(SPStyle *style, SPStyle *context_style = nullptr); + virtual void setChildrenStyle(SPStyle *context_style); + void setOpacity(float opacity); + void setAntialiasing(unsigned a); + void setIsolation(bool isolation); // CSS Compositing and Blending + void setBlendMode(SPBlendMode blend_mode); + void setTransform(Geom::Affine const &trans); + void setClip(DrawingItem *item); + void setMask(DrawingItem *item); + void setFillPattern(DrawingPattern *pattern); + void setStrokePattern(DrawingPattern *pattern); + void setZOrder(unsigned z); + void setItemBounds(Geom::OptRect const &bounds); + void setFilterBounds(Geom::OptRect const &bounds); + + void setKey(unsigned key) { _key = key; } + unsigned key() const { return _key; } + void setItem(SPItem *item) { _item = item; } + SPItem* getItem() const { return _item; } // SPItem + + + void update(Geom::IntRect const &area = Geom::IntRect::infinite(), UpdateContext const &ctx = UpdateContext(), unsigned flags = STATE_ALL, unsigned reset = 0); + unsigned render(DrawingContext &dc, Geom::IntRect const &area, unsigned flags = 0, DrawingItem *stop_at = nullptr); + void clip(DrawingContext &dc, Geom::IntRect const &area); + DrawingItem *pick(Geom::Point const &p, double delta, unsigned flags = 0); + + virtual Glib::ustring name(); // For debugging + void recursivePrintTree(unsigned level = 0); // For debugging + +protected: + enum ChildType { + CHILD_ORPHAN = 0, // no parent - implies _parent == NULL + CHILD_NORMAL = 1, // contained in _children of parent + CHILD_CLIP = 2, // referenced by _clip member of parent + CHILD_MASK = 3, // referenced by _mask member of parent + CHILD_ROOT = 4, // root item of _drawing + CHILD_FILL_PATTERN = 5, // referenced by fill pattern of parent + CHILD_STROKE_PATTERN = 6 // referenced by stroke pattern of parent + }; + enum RenderResult { + RENDER_OK = 0, + RENDER_STOP = 1 + }; + void _renderOutline(DrawingContext &dc, Geom::IntRect const &area, unsigned flags); + void _markForUpdate(unsigned state, bool propagate); + void _markForRendering(); + void _invalidateFilterBackground(Geom::IntRect const &area); + double _cacheScore(); + Geom::OptIntRect _cacheRect(); + virtual unsigned _updateItem(Geom::IntRect const &/*area*/, UpdateContext const &/*ctx*/, + unsigned /*flags*/, unsigned /*reset*/) { return 0; } + virtual unsigned _renderItem(DrawingContext &/*dc*/, Geom::IntRect const &/*area*/, unsigned /*flags*/, + DrawingItem * /*stop_at*/) { return RENDER_OK; } + virtual void _clipItem(DrawingContext &/*dc*/, Geom::IntRect const &/*area*/) {} + virtual DrawingItem *_pickItem(Geom::Point const &/*p*/, double /*delta*/, unsigned /*flags*/) { return nullptr; } + virtual bool _canClip() { return false; } + + // member variables start here + + Drawing &_drawing; + DrawingItem *_parent; + + typedef boost::intrusive::list_member_hook<> ListHook; + ListHook _child_hook; + + typedef boost::intrusive::list< + DrawingItem, + boost::intrusive::member_hook<DrawingItem, ListHook, &DrawingItem::_child_hook> + > ChildrenList; + ChildrenList _children; + + unsigned _key; ///< Some SPItems can have more than one DrawingItem; + /// this value is a hack used to distinguish between them + SPStyle *_style; // Not used by DrawingGlyphs + SPStyle *_context_style; // Used for 'context-fill', 'context-stroke' + + float _opacity; + + Geom::Affine *_transform; ///< Incremental transform from parent to this item's coords + Geom::Affine _ctm; ///< Total transform from item coords to display coords + Geom::OptIntRect _bbox; ///< Bounding box in display (pixel) coords including stroke + Geom::OptIntRect _drawbox; ///< Full visual bounding box - enlarged by filters, shrunk by clips and masks + Geom::OptRect _item_bbox; ///< Geometric bounding box in item's user space. + /// This is used to compute the filter effect region and render in + /// objectBoundingBox units. + + DrawingItem *_clip; + DrawingItem *_mask; + DrawingPattern *_fill_pattern; + DrawingPattern *_stroke_pattern; + Inkscape::Filters::Filter *_filter; + SPItem *_item; ///< Used to associate DrawingItems with SPItems that created them + DrawingCache *_cache; + bool _prev_nir; + + CacheList::iterator _cache_iterator; + + unsigned _state : 8; + unsigned _propagate_state : 8; + unsigned _child_type : 3; // see ChildType enum + unsigned _background_new : 1; ///< Whether enable-background: new is set for this element + unsigned _background_accumulate : 1; ///< Whether this element accumulates background + /// (has any ancestor with enable-background: new) + unsigned _visible : 1; + unsigned _sensitive : 1; ///< Whether this item responds to events + unsigned _cached : 1; ///< Whether the rendering is stored for reuse + unsigned _cached_persistent : 1; ///< If set, will always be cached regardless of score + unsigned _has_cache_iterator : 1; ///< If set, _cache_iterator is valid + unsigned _propagate : 1; ///< Whether to call update for all children on next update + //unsigned _renders_opacity : 1; ///< Whether object needs temporary surface for opacity + unsigned _pick_children : 1; ///< For groups: if true, children are returned from pick(), + /// otherwise the group is returned + unsigned _antialias : 2; ///< antialiasing level (NONE/FAST/GOOD(DEFAULT)/BEST) + + bool _isolation : 1; + SPBlendMode _mix_blend_mode; + + friend class Drawing; +}; + +struct DeleteDisposer { + void operator()(DrawingItem *item) { delete item; } +}; + +} // end namespace Inkscape + +#endif // !SEEN_INKSCAPE_DISPLAY_DRAWING_ITEM_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/drawing-pattern.cpp b/src/display/drawing-pattern.cpp new file mode 100644 index 0000000..90bad03 --- /dev/null +++ b/src/display/drawing-pattern.cpp @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Canvas belonging to SVG pattern. + *//* + * Authors: + * Tomasz Boczkowski <penginsbacon@gmail.com> + * + * Copyright (C) 2014 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "display/cairo-utils.h" +#include "display/drawing-context.h" +#include "display/drawing-pattern.h" +#include "display/drawing-surface.h" + +namespace Inkscape { + +DrawingPattern::DrawingPattern(Drawing &drawing, bool debug) + : DrawingGroup(drawing) + , _pattern_to_user(nullptr) + , _overflow_steps(1) + , _debug(debug) +{ +} + +DrawingPattern::~DrawingPattern() +{ + delete _pattern_to_user; // delete NULL; is safe +} + +void +DrawingPattern::setPatternToUserTransform(Geom::Affine const &new_trans) { + Geom::Affine current; + if (_pattern_to_user) { + current = *_pattern_to_user; + } + + if (!Geom::are_near(current, new_trans, 1e-18)) { + // mark the area where the object was for redraw. + _markForRendering(); + if (new_trans.isIdentity()) { + delete _pattern_to_user; // delete NULL; is safe + _pattern_to_user = nullptr; + } else { + _pattern_to_user = new Geom::Affine(new_trans); + } + _markForUpdate(STATE_ALL, true); + } +} + +void +DrawingPattern::setTileRect(Geom::Rect const &tile_rect) { + _tile_rect = tile_rect; +} + +void +DrawingPattern::setOverflow(Geom::Affine initial_transform, int steps, Geom::Affine step_transform) { + _overflow_initial_transform = initial_transform; + _overflow_steps = steps; + _overflow_step_transform = step_transform; +} + +cairo_pattern_t * +DrawingPattern::renderPattern(float opacity) { + bool needs_opacity = (1.0 - opacity) >= 1e-3; + bool visible = opacity >= 1e-3; + + if (!visible) { + return nullptr; + } + + if (!_tile_rect || (_tile_rect->area() == 0)) { + return nullptr; + } + Geom::Rect pattern_tile = *_tile_rect; + + //TODO: If pattern_to_user set it to identity transform + + // The DrawingSurface class handles the mapping from "logical space" + // (coordinates in the rendering) to "physical space" (surface pixels). + // An oversampling is done as the pattern may not pixel align with the final surface. + // The cairo surface is created when the DrawingContext is declared. + // Create drawing surface with size of pattern tile (in pattern space) but with number of pixels + // based on required resolution (c). + Inkscape::DrawingSurface pattern_surface(pattern_tile, _pattern_resolution); + Inkscape::DrawingContext dc(pattern_surface); + dc.transform( pattern_surface.drawingTransform().inverse() ); + + pattern_tile *= pattern_surface.drawingTransform(); + Geom::IntRect one_tile = pattern_tile.roundOutwards(); + + // Render pattern. + if (needs_opacity) { + dc.pushGroup(); // this group is for pattern + opacity + } + + if (_debug) { + dc.setSource(0.8, 0.0, 0.8); + dc.paint(); + } + + //FIXME: What flags to choose? + if (_overflow_steps == 1) { + render(dc, one_tile, RENDER_DEFAULT); + } else { + //Overflow transforms need to be transformed to the new coordinate system + //introduced by dc.transform( pattern_surface.drawingTransform().inverse() ); + Geom::Affine dt = pattern_surface.drawingTransform(); + Geom::Affine idt = pattern_surface.drawingTransform().inverse(); + Geom::Affine initial_transform = idt * _overflow_initial_transform * dt; + Geom::Affine step_transform = idt * _overflow_step_transform * dt; + dc.transform(initial_transform); + for (int i = 0; i < _overflow_steps; i++) { + // render() fails to handle transforms applied here when using cache. + render(dc, one_tile, RENDER_BYPASS_CACHE); + dc.transform(step_transform); + // cairo_surface_t* raw = pattern_surface.raw(); + // std::string filename = "drawing-pattern" + std::to_string(i) + ".png"; + // cairo_surface_write_to_png( pattern_surface.raw(), filename.c_str() ); + } + } + + // Uncomment to debug + // cairo_surface_t* raw = pattern_surface.raw(); + // std::cout << " cairo_surface (sp-pattern): " + // << " width: " << cairo_image_surface_get_width( raw ) + // << " height: " << cairo_image_surface_get_height( raw ) + // << std::endl; + // std::string filename = "drawing-pattern.png"; + // cairo_surface_write_to_png( pattern_surface.raw(), filename.c_str() ); + + if (needs_opacity) { + dc.popGroupToSource(); // pop raw pattern + dc.paint(opacity); // apply opacity + } + + cairo_pattern_t *cp = cairo_pattern_create_for_surface(pattern_surface.raw()); + // Apply transformation to user space. Also compensate for oversampling. + if (_pattern_to_user) { + ink_cairo_pattern_set_matrix(cp, _pattern_to_user->inverse() * pattern_surface.drawingTransform()); + } else { + ink_cairo_pattern_set_matrix(cp, pattern_surface.drawingTransform()); + } + + if (_debug) { + cairo_pattern_set_extend(cp, CAIRO_EXTEND_NONE); + } else { + cairo_pattern_set_extend(cp, CAIRO_EXTEND_REPEAT); + } + + return cp; +} + +// TODO investigate if area should be used. +unsigned DrawingPattern::_updateItem(Geom::IntRect const &area, UpdateContext const &ctx, unsigned flags, unsigned reset) +{ + UpdateContext pattern_ctx; + + if (!_tile_rect || (_tile_rect->area() == 0)) { + return STATE_NONE; + } + + Geom::Rect pattern_tile = *_tile_rect; + Geom::Coord det_ctm = ctx.ctm.descrim(); + Geom::Coord det_ps2user = _pattern_to_user ? _pattern_to_user->descrim() : 1.0; + Geom::Coord det_child_transform = _child_transform ? _child_transform->descrim() : 1.0; + const double oversampling = 2.0; + double scale = det_ctm*det_ps2user*det_child_transform * oversampling; + //FIXME: When scale is too big (zooming in a hatch), cairo doesn't render the pattern + //More precisely it fails when setting pattern matrix in DrawingPattern::renderPattern + //Fully correct solution should make use of visible area bbox and change hatch tile rect + //accordingly + if (scale > 25) { + scale = 25; + } + Geom::Point c(pattern_tile.dimensions()*scale*oversampling); + _pattern_resolution = c.ceil(); + + Inkscape::DrawingSurface pattern_surface(pattern_tile, _pattern_resolution); + + pattern_ctx.ctm = pattern_surface.drawingTransform(); + return DrawingGroup::_updateItem(Geom::IntRect::infinite(), pattern_ctx, flags, reset); +} + +} // end namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/drawing-pattern.h b/src/display/drawing-pattern.h new file mode 100644 index 0000000..86b881a --- /dev/null +++ b/src/display/drawing-pattern.h @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Canvas belonging to SVG pattern. + *//* + * Authors: + * Tomasz Boczkowski <penginsbacon@gmail.com> + * + * Copyright (C) 2014 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_DISPLAY_DRAWING_PATTERN_H +#define SEEN_INKSCAPE_DISPLAY_DRAWING_PATTERN_H + +#include "display/drawing-group.h" + +typedef struct _cairo_pattern cairo_pattern_t; + +namespace Inkscape { + +/** + * @brief Drawing tree node used for rendering paints. + * + * DrawingPattern is used for rendering patterns and hatches. + * + * It renders it's children to a cairo_pattern_t structure that can be + * applied as source for fill or stroke operations. + */ +class DrawingPattern + : public DrawingGroup +{ +public: + DrawingPattern(Drawing &drawing, bool debug = false); + ~DrawingPattern() override; + + /** + * Set the transformation from pattern to user coordinate systems. + * @see SPPattern description for explanation of coordinate systems. + */ + void setPatternToUserTransform(Geom::Affine const &new_trans); + /** + * Set the tile rect position and dimensions in content coordinate system + */ + void setTileRect(Geom::Rect const &tile_rect); + /** + * Turn on overflow rendering. + * + * Overflow is implemented as repeated rendering of pattern contents. In every step + * a translation transform is applied. + */ + void setOverflow(Geom::Affine initial_transform, int steps, Geom::Affine step_transform); + /** + * Render the pattern. + * + * Returns caito_pattern_t structure that can be set as source surface. + */ + cairo_pattern_t *renderPattern(float opacity); +protected: + unsigned _updateItem(Geom::IntRect const &area, UpdateContext const &ctx, + unsigned flags, unsigned reset) override; + + Geom::Affine *_pattern_to_user; + Geom::Affine _overflow_initial_transform; + Geom::Affine _overflow_step_transform; + int _overflow_steps; + Geom::OptRect _tile_rect; + bool _debug; + Geom::IntPoint _pattern_resolution; +}; + +bool is_drawing_group(DrawingItem *item); + +} // end namespace Inkscape + +#endif // !SEEN_INKSCAPE_DISPLAY_DRAWING_PATTERN_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/drawing-shape.cpp b/src/display/drawing-shape.cpp new file mode 100644 index 0000000..aa46a70 --- /dev/null +++ b/src/display/drawing-shape.cpp @@ -0,0 +1,441 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Shape (styled path) belonging to an SVG drawing. + *//* + * Authors: + * Krzysztof KosiĆski <tweenk.pl@gmail.com> + * + * Copyright (C) 2011 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm.h> +#include <2geom/curves.h> +#include <2geom/pathvector.h> +#include <2geom/path-sink.h> +#include <2geom/svg-path-parser.h> + +#include "display/cairo-utils.h" +#include "display/canvas-arena.h" +#include "display/canvas-bpath.h" +#include "display/curve.h" +#include "display/drawing.h" +#include "display/drawing-context.h" +#include "display/drawing-group.h" +#include "display/drawing-shape.h" +#include "helper/geom-curves.h" +#include "helper/geom.h" +#include "preferences.h" +#include "style.h" +#include "svg/svg.h" + +namespace Inkscape { + +DrawingShape::DrawingShape(Drawing &drawing) + : DrawingItem(drawing) + , _curve(nullptr) + , _last_pick(nullptr) + , _repick_after(0) +{} + +DrawingShape::~DrawingShape() +{ + if (_curve) + _curve->unref(); +} + +void +DrawingShape::setPath(SPCurve *curve) +{ + _markForRendering(); + + if (_curve) { + _curve->unref(); + _curve = nullptr; + } + if (curve) { + _curve = curve; + curve->ref(); + } + + _markForUpdate(STATE_ALL, false); +} + +void +DrawingShape::setStyle(SPStyle *style, SPStyle *context_style) +{ + DrawingItem::setStyle(style, context_style); // Must be first + _nrstyle.set(_style, _context_style); +} + +void +DrawingShape::setChildrenStyle(SPStyle* context_style) +{ + DrawingItem::setChildrenStyle( context_style ); + _nrstyle.set(_style, _context_style); +} + +unsigned +DrawingShape::_updateItem(Geom::IntRect const &area, UpdateContext const &ctx, unsigned flags, unsigned reset) +{ + Geom::OptRect boundingbox; + + unsigned beststate = STATE_ALL; + + // update markers + for (auto & i : _children) { + i.update(area, ctx, flags, reset); + } + + if (!(flags & STATE_RENDER)) { + /* We do not have to create rendering structures */ + if (flags & STATE_BBOX) { + if (_curve) { + boundingbox = bounds_exact_transformed(_curve->get_pathvector(), ctx.ctm); + if (boundingbox) { + _bbox = boundingbox->roundOutwards(); + } else { + _bbox = Geom::OptIntRect(); + } + } + if (beststate & STATE_BBOX) { + for (auto & i : _children) { + _bbox.unionWith(i.geometricBounds()); + } + } + } + return (flags | _state); + } + + boundingbox = Geom::OptRect(); + bool outline = _drawing.outline(); + + // clear Cairo data to force update + _nrstyle.update(); + + if (_curve) { + boundingbox = bounds_exact_transformed(_curve->get_pathvector(), ctx.ctm); + + if (boundingbox && (_nrstyle.stroke.type != NRStyle::PAINT_NONE || outline)) { + float width, scale; + scale = ctx.ctm.descrim(); + width = std::max(0.125f, _nrstyle.stroke_width * scale); + if ( fabs(_nrstyle.stroke_width * scale) > 0.01 ) { // FIXME: this is always true + boundingbox->expandBy(width); + } + // those pesky miters, now + float miterMax = width * _nrstyle.miter_limit; + if ( miterMax > 0.01 ) { + // grunt mode. we should compute the various miters instead + // (one for each point on the curve) + boundingbox->expandBy(miterMax); + } + } + } + + _bbox = boundingbox ? boundingbox->roundOutwards() : Geom::OptIntRect(); + + if (!_curve || + !_style || + _curve->is_empty()) + { + return STATE_ALL; + } + + if (beststate & STATE_BBOX) { + for (auto & i : _children) { + _bbox.unionWith(i.geometricBounds()); + } + } + return STATE_ALL; +} + +void +DrawingShape::_renderFill(DrawingContext &dc) +{ + Inkscape::DrawingContext::Save save(dc); + dc.transform(_ctm); + + bool has_fill = _nrstyle.prepareFill(dc, _item_bbox, _fill_pattern); + + if( has_fill ) { + dc.path(_curve->get_pathvector()); + _nrstyle.applyFill(dc); + dc.fillPreserve(); + dc.newPath(); // clear path + } +} + +void +DrawingShape::_renderStroke(DrawingContext &dc) +{ + Inkscape::DrawingContext::Save save(dc); + dc.transform(_ctm); + + bool has_stroke = _nrstyle.prepareStroke(dc, _item_bbox, _stroke_pattern); + has_stroke &= (_nrstyle.stroke_width != 0); + + if( has_stroke ) { + // TODO: remove segments outside of bbox when no dashes present + dc.path(_curve->get_pathvector()); + if (_style && _style->vector_effect.stroke) { + dc.restore(); + dc.save(); + } + _nrstyle.applyStroke(dc); + + // If the draw mode is set to visible hairlines, don't let them get smaller than half a + // pixel. + if (_drawing.visibleHairlines()) { + double half_pixel_size = 0.5, trash = 0.5; + dc.device_to_user_distance(half_pixel_size, trash); + if (_nrstyle.stroke_width < half_pixel_size) { + dc.setLineWidth(half_pixel_size); + } + } + + dc.strokePreserve(); + dc.newPath(); // clear path + } +} + +void +DrawingShape::_renderMarkers(DrawingContext &dc, Geom::IntRect const &area, unsigned flags, DrawingItem *stop_at) +{ + // marker rendering + for (auto & i : _children) { + i.render(dc, area, flags, stop_at); + } +} + +unsigned +DrawingShape::_renderItem(DrawingContext &dc, Geom::IntRect const &area, unsigned flags, DrawingItem *stop_at) +{ + if (!_curve || !_style) return RENDER_OK; + if (!area.intersects(_bbox)) return RENDER_OK; // skip if not within bounding box + + bool outline = _drawing.outline(); + + if (outline) { + guint32 rgba = _drawing.outlinecolor; + + // paint-order doesn't matter + { Inkscape::DrawingContext::Save save(dc); + dc.transform(_ctm); + dc.path(_curve->get_pathvector()); + } + { Inkscape::DrawingContext::Save save(dc); + dc.setSource(rgba); + dc.setLineWidth(0.5); + dc.setTolerance(0.5); + dc.stroke(); + } + + _renderMarkers(dc, area, flags, stop_at); + return RENDER_OK; + + } + + if( _nrstyle.paint_order_layer[0] == NRStyle::PAINT_ORDER_NORMAL ) { + // This is the most common case, special case so we don't call get_pathvector(), etc. twice + + { + // we assume the context has no path + Inkscape::DrawingContext::Save save(dc); + dc.transform(_ctm); + + + // update fill and stroke paints. + // this cannot be done during nr_arena_shape_update, because we need a Cairo context + // to render svg:pattern + bool has_fill = _nrstyle.prepareFill(dc, _item_bbox, _fill_pattern); + bool has_stroke = _nrstyle.prepareStroke(dc, _item_bbox, _stroke_pattern); + has_stroke &= (_nrstyle.stroke_width != 0); + if (has_fill || has_stroke) { + dc.path(_curve->get_pathvector()); + // TODO: remove segments outside of bbox when no dashes present + if (has_fill) { + _nrstyle.applyFill(dc); + dc.fillPreserve(); + } + if (_style && _style->vector_effect.stroke) { + dc.restore(); + dc.save(); + } + if (has_stroke) { + _nrstyle.applyStroke(dc); + + // If the draw mode is set to visible hairlines, don't let anything get smaller + // than half a pixel. + if (_drawing.visibleHairlines()) { + double half_pixel_size = 0.5, trash = 0.5; + dc.device_to_user_distance(half_pixel_size, trash); + if (_nrstyle.stroke_width < half_pixel_size) { + dc.setLineWidth(half_pixel_size); + } + } + + dc.strokePreserve(); + } + dc.newPath(); // clear path + } // has fill or stroke pattern + } + _renderMarkers(dc, area, flags, stop_at); + return RENDER_OK; + + } + + // Handle different paint orders + for (auto & i : _nrstyle.paint_order_layer) { + switch (i) { + case NRStyle::PAINT_ORDER_FILL: + _renderFill(dc); + break; + case NRStyle::PAINT_ORDER_STROKE: + _renderStroke(dc); + break; + case NRStyle::PAINT_ORDER_MARKER: + _renderMarkers(dc, area, flags, stop_at); + break; + default: + // PAINT_ORDER_AUTO Should not happen + break; + } + } + return RENDER_OK; +} + +void DrawingShape::_clipItem(DrawingContext &dc, Geom::IntRect const & /*area*/) +{ + if (!_curve) return; + + Inkscape::DrawingContext::Save save(dc); + // handle clip-rule + if (_style) { + if (_style->clip_rule.computed == SP_WIND_RULE_EVENODD) { + dc.setFillRule(CAIRO_FILL_RULE_EVEN_ODD); + } else { + dc.setFillRule(CAIRO_FILL_RULE_WINDING); + } + } + dc.transform(_ctm); + dc.path(_curve->get_pathvector()); + dc.fill(); +} + +DrawingItem * +DrawingShape::_pickItem(Geom::Point const &p, double delta, unsigned flags) +{ + if (_repick_after > 0) + --_repick_after; + + if (_repick_after > 0) // we are a slow, huge path + return _last_pick; // skip this pick, returning what was returned last time + + if (!_curve) return nullptr; + if (!_style) return nullptr; + + bool outline = _drawing.outline() || _drawing.getOutlineSensitive(); + bool pick_as_clip = flags & PICK_AS_CLIP; + + if (SP_SCALE24_TO_FLOAT(_style->opacity.value) == 0 && !outline && !pick_as_clip) + // fully transparent, no pick unless outline mode + return nullptr; + + + gint64 tstart = g_get_monotonic_time(); + + double width; + if (pick_as_clip) { + width = 0; // no width should be applied to clip picking + // this overrides display mode and stroke style considerations + } else if (outline) { + width = 0.5; // in outline mode, everything is stroked with the same 0.5px line width + } else if (_nrstyle.stroke.type != NRStyle::PAINT_NONE && _nrstyle.stroke.opacity > 1e-3) { + // for normal picking calculate the distance corresponding top the stroke width + // FIXME BUG: this is incorrect for transformed strokes + float const scale = _ctm.descrim(); + width = std::max(0.125f, _nrstyle.stroke_width * scale) / 2; + } else { + width = 0; + } + + double dist = Geom::infinity(); + int wind = 0; + bool needfill = pick_as_clip || (_nrstyle.fill.type != NRStyle::PAINT_NONE && + _nrstyle.fill.opacity > 1e-3 && !outline); + bool wind_evenodd = pick_as_clip ? (_style->clip_rule.computed == SP_WIND_RULE_EVENODD) : + (_style->fill_rule.computed == SP_WIND_RULE_EVENODD); + + // actual shape picking + if (_drawing.arena()) { + Geom::Rect viewbox = _drawing.arena()->item.canvas->getViewbox(); + viewbox.expandBy (width); + pathv_matrix_point_bbox_wind_distance(_curve->get_pathvector(), _ctm, p, nullptr, needfill? &wind : nullptr, &dist, 0.5, &viewbox); + } else { + pathv_matrix_point_bbox_wind_distance(_curve->get_pathvector(), _ctm, p, nullptr, needfill? &wind : nullptr, &dist, 0.5, nullptr); + } + + gint64 tfinish = g_get_monotonic_time(); + gint64 this_pick = tfinish - tstart; + //g_print ("pick time %lu\n", this_pick); + if (this_pick > 10000) { // slow picking, remember to skip several new picks + _repick_after = this_pick / 5000; + } + + // covered by fill? + if (needfill) { + if (wind_evenodd) { + if (wind & 0x1) { + _last_pick = this; + return this; + } + } else { + if (wind != 0) { + _last_pick = this; + return this; + } + } + } + + // close to the edge, as defined by strokewidth and delta? + // this ignores dashing (as if the stroke is solid) and always works as if caps are round + if (needfill || width > 0) { // if either fill or stroke visible, + if ((dist - width) < delta) { + _last_pick = this; + return this; + } + } + + // if not picked on the shape itself, try its markers + for (auto & i : _children) { + DrawingItem *ret = i.pick(p, delta, flags & ~PICK_STICKY); + if (ret) { + _last_pick = this; + return this; + } + } + + _last_pick = nullptr; + return nullptr; +} + +bool +DrawingShape::_canClip() +{ + return true; +} + +} // end namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/drawing-shape.h b/src/display/drawing-shape.h new file mode 100644 index 0000000..f8a3d23 --- /dev/null +++ b/src/display/drawing-shape.h @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Group belonging to an SVG drawing element. + *//* + * Authors: + * Krzysztof KosiĆski <tweenk.pl@gmail.com> + * + * Copyright (C) 2011 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_DISPLAY_DRAWING_SHAPE_H +#define SEEN_INKSCAPE_DISPLAY_DRAWING_SHAPE_H + +#include "display/drawing-item.h" +#include "display/nr-style.h" + +class SPStyle; +class SPCurve; + +namespace Inkscape { + +class DrawingShape + : public DrawingItem +{ +public: + DrawingShape(Drawing &drawing); + ~DrawingShape() override; + + void setPath(SPCurve *curve); + void setStyle(SPStyle *style, SPStyle *context_style = nullptr) override; + void setChildrenStyle(SPStyle *context_style) override; + +protected: + unsigned _updateItem(Geom::IntRect const &area, UpdateContext const &ctx, + unsigned flags, unsigned reset) override; + unsigned _renderItem(DrawingContext &dc, Geom::IntRect const &area, unsigned flags, + DrawingItem *stop_at) override; + void _clipItem(DrawingContext &dc, Geom::IntRect const &area) override; + DrawingItem *_pickItem(Geom::Point const &p, double delta, unsigned flags) override; + bool _canClip() override; + + void _renderFill(DrawingContext &dc); + void _renderStroke(DrawingContext &dc); + void _renderMarkers(DrawingContext &dc, Geom::IntRect const &area, unsigned flags, + DrawingItem *stop_at); + + SPCurve *_curve; + NRStyle _nrstyle; + + DrawingItem *_last_pick; + unsigned _repick_after; +}; + +} // end namespace Inkscape + +#endif // !SEEN_INKSCAPE_DISPLAY_DRAWING_ITEM_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/drawing-surface.cpp b/src/display/drawing-surface.cpp new file mode 100644 index 0000000..c84e358 --- /dev/null +++ b/src/display/drawing-surface.cpp @@ -0,0 +1,399 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Cairo surface that remembers its origin. + *//* + * Authors: + * Krzysztof KosiĆski <tweenk.pl@gmail.com> + * + * Copyright (C) 2011 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +//#include <iostream> +#include "display/drawing-surface.h" +#include "display/drawing-context.h" +#include "display/cairo-utils.h" + +namespace Inkscape { + +using Geom::X; +using Geom::Y; + + +/** + * @class DrawingSurface + * Drawing surface that remembers its origin. + * + * This is a very minimalistic wrapper over cairo_surface_t. The main + * extra functionality provided by this class is that it automates + * the mapping from "logical space" (coordinates in the rendering) + * and the "physical space" (surface pixels). For example, patterns + * have to be rendered on tiles which have possibly non-integer + * widths and heights. + * + * This class has delayed allocation functionality - it creates + * the Cairo surface it wraps on the first call to createRawContext() + * of when a DrawingContext is constructed. + */ + +/** + * Creates a surface with the given physical extents. + * When a drawing context is created for this surface, its pixels + * will cover the area under the given rectangle. + */ +DrawingSurface::DrawingSurface(Geom::IntRect const &area, int device_scale) + : _surface(nullptr) + , _origin(area.min()) + , _scale(1, 1) + , _pixels(area.dimensions()) + , _device_scale(device_scale) +{ + assert(_device_scale > 0); +} + +/** + * Creates a surface with the given logical and physical extents. + * When a drawing context is created for this surface, its pixels + * will cover the area under the given rectangle. IT will contain + * the number of pixels specified by the second argument. + * @param logbox Logical extents of the surface + * @param pixdims Pixel dimensions of the surface. + */ +DrawingSurface::DrawingSurface(Geom::Rect const &logbox, Geom::IntPoint const &pixdims, int device_scale) + : _surface(nullptr) + , _origin(logbox.min()) + , _scale(pixdims[X] / logbox.width(), pixdims[Y] / logbox.height()) + , _pixels(pixdims) + , _device_scale(device_scale) +{ + assert(_device_scale > 0); +} + +/** + * Wrap a cairo_surface_t. + * This constructor will take an extra reference on @a surface, which will + * be released on destruction. + */ +DrawingSurface::DrawingSurface(cairo_surface_t *surface, Geom::Point const &origin) + : _surface(surface) + , _origin(origin) + , _scale(1, 1) +{ + cairo_surface_reference(surface); + + double x_scale = 0; + double y_scale = 0; + cairo_surface_get_device_scale( surface, &x_scale, &y_scale); + if (x_scale != y_scale) { + std::cerr << "DrawingSurface::DrawingSurface: non-uniform device scale!" << std::endl; + } + _device_scale = x_scale; + assert(_device_scale > 0); + + _pixels[X] = cairo_image_surface_get_width(surface)/_device_scale; + _pixels[Y] = cairo_image_surface_get_height(surface)/_device_scale; +} + +DrawingSurface::~DrawingSurface() +{ + if (_surface) + cairo_surface_destroy(_surface); +} + +/// Get the logical extents of the surface. +Geom::Rect +DrawingSurface::area() const +{ + Geom::Rect r = Geom::Rect::from_xywh(_origin, dimensions()); + return r; +} + +/// Get the pixel dimensions of the surface +Geom::IntPoint +DrawingSurface::pixels() const +{ + return _pixels; +} + +/// Get the logical width and weight of the surface as a point. +Geom::Point +DrawingSurface::dimensions() const +{ + Geom::Point logical_dims(_pixels[X] / _scale[X], _pixels[Y] / _scale[Y]); + return logical_dims; +} + +Geom::Point +DrawingSurface::origin() const +{ + return _origin; +} + +Geom::Scale +DrawingSurface::scale() const +{ + return _scale; +} + +/// Device scale for HiDPI screens +int +DrawingSurface::device_scale() const +{ + return _device_scale; +} + +/// Get the transformation applied to the drawing context on construction. +Geom::Affine +DrawingSurface::drawingTransform() const +{ + Geom::Affine ret = Geom::Translate(-_origin) * _scale; + return ret; +} + +cairo_surface_type_t +DrawingSurface::type() const +{ + // currently hardcoded + return CAIRO_SURFACE_TYPE_IMAGE; +} + +/// Drop contents of the surface and release the underlying Cairo object. +void +DrawingSurface::dropContents() +{ + if (_surface) { + cairo_surface_destroy(_surface); + _surface = nullptr; + } +} + +/** + * Create a drawing context for this surface. + * It's better to use the surface constructor of DrawingContext. + */ +cairo_t * +DrawingSurface::createRawContext() +{ + // deferred allocation + if (!_surface) { + _surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, + _pixels[X] * _device_scale, + _pixels[Y] * _device_scale); + cairo_surface_set_device_scale(_surface, _device_scale, _device_scale); + } + cairo_t *ct = cairo_create(_surface); + if (_scale != Geom::Scale::identity()) { + cairo_scale(ct, _scale[X], _scale[Y]); + } + cairo_translate(ct, -_origin[X], -_origin[Y]); + return ct; +} + +Geom::IntRect +DrawingSurface::pixelArea() const +{ + Geom::IntRect ret = Geom::IntRect::from_xywh(_origin.round(), _pixels); + return ret; +} + +////////////////////////////////////////////////////////////////////////////// + +DrawingCache::DrawingCache(Geom::IntRect const &area, int device_scale) + : DrawingSurface(area, device_scale) + , _clean_region(cairo_region_create()) + , _pending_area(area) +{} + +DrawingCache::~DrawingCache() +{ + cairo_region_destroy(_clean_region); +} + +void +DrawingCache::markDirty(Geom::IntRect const &area) +{ + cairo_rectangle_int_t dirty = _convertRect(area); + cairo_region_subtract_rectangle(_clean_region, &dirty); +} +void +DrawingCache::markClean(Geom::IntRect const &area) +{ + Geom::OptIntRect r = Geom::intersect(area, pixelArea()); + if (!r) return; + cairo_rectangle_int_t clean = _convertRect(*r); + cairo_region_union_rectangle(_clean_region, &clean); +} + +/// Call this during the update phase to schedule a transformation of the cache. +void +DrawingCache::scheduleTransform(Geom::IntRect const &new_area, Geom::Affine const &trans) +{ + _pending_area = new_area; + _pending_transform *= trans; +} + +/// Transforms the cache according to the transform specified during the update phase. +/// Call this during render phase, before painting. +void +DrawingCache::prepare() +{ + Geom::IntRect old_area = pixelArea(); + bool is_identity = _pending_transform.isIdentity(); + if (is_identity && _pending_area == old_area) return; // no change + + bool is_integer_translation = is_identity; + if (!is_identity && _pending_transform.isTranslation()) { + Geom::IntPoint t = _pending_transform.translation().round(); + if (Geom::are_near(Geom::Point(t), _pending_transform.translation())) { + is_integer_translation = true; + cairo_region_translate(_clean_region, t[X], t[Y]); + if (old_area + t == _pending_area) { + // if the areas match, the only thing to do + // is to ensure that the clean area is not too large + // we can exit early + cairo_rectangle_int_t limit = _convertRect(_pending_area); + cairo_region_intersect_rectangle(_clean_region, &limit); + _origin += t; + _pending_transform.setIdentity(); + return; + } + } + } + + // the area has changed, so the cache content needs to be copied + Geom::IntPoint old_origin = old_area.min(); + cairo_surface_t *old_surface = _surface; + _surface = nullptr; + _pixels = _pending_area.dimensions(); + _origin = _pending_area.min(); + + if (is_integer_translation) { + // transform the cache only for integer translations and identities + cairo_t *ct = createRawContext(); + if (!is_identity) { + ink_cairo_transform(ct, _pending_transform); + } + cairo_set_source_surface(ct, old_surface, old_origin[X], old_origin[Y]); + cairo_set_operator(ct, CAIRO_OPERATOR_SOURCE); + cairo_pattern_set_filter(cairo_get_source(ct), CAIRO_FILTER_NEAREST); + cairo_paint(ct); + cairo_destroy(ct); + + cairo_rectangle_int_t limit = _convertRect(_pending_area); + cairo_region_intersect_rectangle(_clean_region, &limit); + } else { + // dirty everything + cairo_region_destroy(_clean_region); + _clean_region = cairo_region_create(); + } + + //std::cout << _pending_transform << old_area << _pending_area << std::endl; + cairo_surface_destroy(old_surface); + _pending_transform.setIdentity(); +} + +/** + * Paints the clean area from cache and modifies the @a area + * parameter to the bounds of the region that must be repainted. + */ +void DrawingCache::paintFromCache(DrawingContext &dc, Geom::OptIntRect &area, bool is_filter) +{ + if (!area) return; + + // We subtract the clean region from the area, then get the bounds + // of the resulting region. This is the area that needs to be repainted + // by the item. + // Then we subtract the area that needs to be repainted from the + // original area and paint the resulting region from cache. + cairo_rectangle_int_t area_c = _convertRect(*area); + cairo_region_t *dirty_region = cairo_region_create_rectangle(&area_c); + cairo_region_t *cache_region = cairo_region_copy(dirty_region); + cairo_region_subtract(dirty_region, _clean_region); + + if (is_filter && !cairo_region_is_empty(dirty_region)) { // To allow fast panning on high zoom on filters + return; + } + if (cairo_region_is_empty(dirty_region)) { + area = Geom::OptIntRect(); + } else { + cairo_rectangle_int_t to_repaint; + cairo_region_get_extents(dirty_region, &to_repaint); + area = _convertRect(to_repaint); + markDirty(*area); + cairo_region_subtract_rectangle(cache_region, &to_repaint); + } + cairo_region_destroy(dirty_region); + + if (!cairo_region_is_empty(cache_region)) { + int nr = cairo_region_num_rectangles(cache_region); + cairo_rectangle_int_t tmp; + for (int i = 0; i < nr; ++i) { + cairo_region_get_rectangle(cache_region, i, &tmp); + dc.rectangle(_convertRect(tmp)); + } + dc.setSource(this); + dc.fill(); + } + cairo_region_destroy(cache_region); +} + +// debugging utility +void +DrawingCache::_dumpCache(Geom::OptIntRect const &area) +{ + static int dumpnr = 0; + cairo_surface_t *surface = ink_cairo_surface_copy(_surface); + DrawingContext dc(surface, _origin); + if (!cairo_region_is_empty(_clean_region)) { + Inkscape::DrawingContext::Save save(dc); + int nr = cairo_region_num_rectangles(_clean_region); + cairo_rectangle_int_t tmp; + for (int i = 0; i < nr; ++i) { + cairo_region_get_rectangle(_clean_region, i, &tmp); + dc.rectangle(_convertRect(tmp)); + } + dc.setSource(0,1,0,0.1); + dc.fill(); + } + dc.rectangle(*area); + dc.setSource(1,0,0,0.1); + dc.fill(); + char *fn = g_strdup_printf("dump%d.png", dumpnr++); + cairo_surface_write_to_png(surface, fn); + cairo_surface_destroy(surface); + g_free(fn); +} + +cairo_rectangle_int_t +DrawingCache::_convertRect(Geom::IntRect const &area) +{ + cairo_rectangle_int_t ret; + ret.x = area.left(); + ret.y = area.top(); + ret.width = area.width(); + ret.height = area.height(); + return ret; +} + +Geom::IntRect +DrawingCache::_convertRect(cairo_rectangle_int_t const &r) +{ + Geom::IntRect ret = Geom::IntRect::from_xywh( + r.x, r.y, + r.width, r.height); + return ret; +} + +} // end namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/drawing-surface.h b/src/display/drawing-surface.h new file mode 100644 index 0000000..0522b53 --- /dev/null +++ b/src/display/drawing-surface.h @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Cairo surface that remembers its origin. + *//* + * Authors: + * Krzysztof KosiĆski <tweenk.pl@gmail.com> + * + * Copyright (C) 2011 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_DISPLAY_DRAWING_SURFACE_H +#define SEEN_INKSCAPE_DISPLAY_DRAWING_SURFACE_H + +#include <cairo.h> +#include <2geom/affine.h> +#include <2geom/rect.h> +#include <2geom/transforms.h> + +extern "C" { +typedef struct _cairo cairo_t; +typedef struct _cairo_surface cairo_surface_t; +typedef struct _cairo_region cairo_region_t; +} + +namespace Inkscape { +class DrawingContext; + +class DrawingSurface +{ +public: + explicit DrawingSurface(Geom::IntRect const &area, int device_scale = 1); + DrawingSurface(Geom::Rect const &logbox, Geom::IntPoint const &pixdims, int device_scale = 1); + DrawingSurface(cairo_surface_t *surface, Geom::Point const &origin); + virtual ~DrawingSurface(); + + Geom::Rect area() const; + Geom::IntPoint pixels() const; + Geom::Point dimensions() const; + Geom::Point origin() const; + Geom::Scale scale() const; + int device_scale() const; + Geom::Affine drawingTransform() const; + cairo_surface_type_t type() const; + void dropContents(); + + cairo_surface_t *raw() { return _surface; } + cairo_t *createRawContext(); + +protected: + Geom::IntRect pixelArea() const; + + cairo_surface_t *_surface; + Geom::Point _origin; + Geom::Scale _scale; + Geom::IntPoint _pixels; + int _device_scale; // To support HiDPI screens + bool _has_context; + + friend class DrawingContext; +}; + +class DrawingCache + : public DrawingSurface +{ +public: + explicit DrawingCache(Geom::IntRect const &area, int device_scale = 1); + ~DrawingCache() override; + + void markDirty(Geom::IntRect const &area = Geom::IntRect::infinite()); + void markClean(Geom::IntRect const &area = Geom::IntRect::infinite()); + void scheduleTransform(Geom::IntRect const &new_area, Geom::Affine const &trans); + void prepare(); + void paintFromCache(DrawingContext &dc, Geom::OptIntRect &area, bool is_filter); + + protected: + cairo_region_t *_clean_region; + Geom::IntRect _pending_area; + Geom::Affine _pending_transform; +private: + void _dumpCache(Geom::OptIntRect const &area); + static cairo_rectangle_int_t _convertRect(Geom::IntRect const &r); + static Geom::IntRect _convertRect(cairo_rectangle_int_t const &r); +}; + +} // end namespace Inkscape + +#endif // !SEEN_INKSCAPE_DISPLAY_DRAWING_ITEM_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/drawing-text.cpp b/src/display/drawing-text.cpp new file mode 100644 index 0000000..4eb4224 --- /dev/null +++ b/src/display/drawing-text.cpp @@ -0,0 +1,758 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Group belonging to an SVG drawing element. + *//* + * Authors: + * Krzysztof KosiĆski <tweenk.pl@gmail.com> + * + * Copyright (C) 2011 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +//#include "display/cairo-utils.h" +//#include "display/canvas-bpath.h" // for SPWindRule (WTF!) +#include "display/drawing.h" +#include "display/drawing-context.h" +#include "display/drawing-surface.h" +#include "display/drawing-text.h" +#include "helper/geom.h" +#include "libnrtype/font-instance.h" +#include "style.h" +#include "2geom/pathvector.h" + +#include "display/cairo-utils.h" +#include "display/canvas-bpath.h" + +namespace Inkscape { + + +DrawingGlyphs::DrawingGlyphs(Drawing &drawing) + : DrawingItem(drawing) + , _font(nullptr) + , _glyph(0) +{} + +DrawingGlyphs::~DrawingGlyphs() +{ + if (_font) { + _font->Unref(); + _font = nullptr; + } +} + +void +DrawingGlyphs::setGlyph(font_instance *font, int glyph, Geom::Affine const &trans) +{ + _markForRendering(); + + setTransform(trans); + + if (font) font->Ref(); + if (_font) _font->Unref(); + _font = font; + _glyph = glyph; + + _markForUpdate(STATE_ALL, false); +} + +void +DrawingGlyphs::setStyle(SPStyle * /*style*/, SPStyle * /*context_style*/) +{ + std::cerr << "DrawingGlyphs: Use parent style" << std::endl; +} + + +unsigned DrawingGlyphs::_updateItem(Geom::IntRect const &/*area*/, UpdateContext const &ctx, unsigned /*flags*/, unsigned /*reset*/) +{ + DrawingText *ggroup = dynamic_cast<DrawingText *>(_parent); + if (!ggroup) { + throw InvalidItemException(); + } + + if (!_font || !ggroup->_style) { + return STATE_ALL; + } + + + _pick_bbox = Geom::IntRect(); + _bbox = Geom::IntRect(); + + /* + Make a bounding box for drawing that is a little taller and lower (currently 10% extra) than + the font's drawing box. Extra space is to hold overline or underline, if present. All + characters in a font use the same ascent and descent, but different widths. This lets leading + and trailing spaces have text decorations. If it is not done the bounding box is limited to + the box surrounding the drawn parts of visible glyphs only, and draws outside are ignored. + The box is also a hair wider than the text, since the glyphs do not always start or end at + the left and right edges of the box defined in the font. + */ + + float scale_bigbox = 1.0; + if (_transform) { + scale_bigbox /= _transform->descrim(); + } + + /* Because there can be text decorations the bounding box must correspond in Y to a little above the glyph's ascend + and a little below its descend. This leaves room for overline and underline. The left and right sides + come from the glyph's bounding box. Note that the initial direction of ascender is positive down in Y, and + this flips after the transform is applied. So change the sign on descender. 1.1 provides a little extra space + above and below the max/min y positions of the letters to place the text decorations.*/ + + Geom::Rect b; + if (_drawable) { + Geom::OptRect tiltb = bounds_exact(*_font->PathVector(_glyph)); + if (tiltb) { + Geom::Rect bigbox(Geom::Point(tiltb->left(),-_dsc*scale_bigbox*1.1),Geom::Point(tiltb->right(),_asc*scale_bigbox*1.1)); + b = bigbox * ctx.ctm; + } + } + if (b.hasZeroArea()) { // Fallback, spaces mostly + Geom::Rect bigbox(Geom::Point(0.0, -_dsc*scale_bigbox*1.1),Geom::Point(_width*scale_bigbox, _asc*scale_bigbox*1.1)); + b = bigbox * ctx.ctm; + } + + /* + The pick box matches the characters as best as it can, leaving no extra space above or below + for decorations. The pathvector may include spaces, and spaces have no drawable glyph. + Catch those and do not pass them to bounds_exact_transformed(), which crashes Inkscape if it + sees a nondrawable glyph. Instead mock up a pickbox for them using font characteristics. + There may also be some other similar white space characters in some other unforeseen context + which should be handled by this code as well.. + */ + + Geom::OptRect pb; + if (_drawable) { + Geom::PathVector *glyphv = _font->PathVector(_glyph); + if (glyphv && !glyphv->empty()) { + pb = bounds_exact_transformed(*glyphv, ctx.ctm); + } + glyphv = _font->PathVector(42); + if (glyphv && !glyphv->empty()) { + if (pb) { + pb.unionWith(bounds_exact_transformed(*glyphv, ctx.ctm)); + } else { + pb = bounds_exact_transformed(*glyphv, ctx.ctm); + } + pb.expandTo(Geom::Point((*pb).right() + (_width * ctx.ctm.descrim()), (*pb).bottom())); + } + } + if (!pb) { // Fallback + Geom::Rect pbigbox(Geom::Point(0.0, _asc*scale_bigbox*0.66),Geom::Point(_width*scale_bigbox, 0.0)); + pb = pbigbox * ctx.ctm; + } + +#if 0 + /* FIXME if this is commented out then not even an approximation of pick on decorations */ + /* adjust the pick box up or down to include the decorations. + This is only approximate since at this point we don't know how wide that line is, if it has + an unusual offset, and so forth. The selection point is set at what is roughly the center of + the decoration (vertically) for the wide ones, like wavy and double line. + The text decorations are not actually selectable. + */ + if (_decorations.overline || _decorations.underline) { + double top = _asc*scale_bigbox*0.66; + double bot = 0; + if (_decorations.overline) { top = _asc * scale_bigbox * 1.025; } + if (_decorations.underline) { bot = -_dsc * scale_bigbox * 0.2; } + Geom::Rect padjbox(Geom::Point(0.0, top),Geom::Point(_width*scale_bigbox, bot)); + pb.unionWith(padjbox * ctx.ctm); + } +#endif + + if (ggroup->_nrstyle.stroke.type != NRStyle::PAINT_NONE) { + // this expands the selection box for cases where the stroke is "thick" + float scale = ctx.ctm.descrim(); + if (_transform) { + scale /= _transform->descrim(); // FIXME temporary hack + } + float width = MAX(0.125, ggroup->_nrstyle.stroke_width * scale); + if ( fabs(ggroup->_nrstyle.stroke_width * scale) > 0.01 ) { // FIXME: this is always true + b.expandBy(0.5 * width); + pb->expandBy(0.5 * width); + } + + // save bbox without miters for picking + _pick_bbox = pb->roundOutwards(); + + float miterMax = width * ggroup->_nrstyle.miter_limit; + if ( miterMax > 0.01 ) { + // grunt mode. we should compute the various miters instead + // (one for each point on the curve) + b.expandBy(miterMax); + } + _bbox = b.roundOutwards(); + } else { + _bbox = b.roundOutwards(); + _pick_bbox = pb->roundOutwards(); + } + return STATE_ALL; +} + +DrawingItem *DrawingGlyphs::_pickItem(Geom::Point const &p, double /*delta*/, unsigned /*flags*/) +{ + DrawingText *ggroup = dynamic_cast<DrawingText *>(_parent); + if (!ggroup) { + throw InvalidItemException(); + } + DrawingItem *result = nullptr; + bool invisible = (ggroup->_nrstyle.fill.type == NRStyle::PAINT_NONE) && + (ggroup->_nrstyle.stroke.type == NRStyle::PAINT_NONE); + + if (_font && _bbox && (_drawing.outline() || _drawing.getOutlineSensitive() || !invisible)) { + // With text we take a simple approach: pick if the point is in a character bbox + Geom::Rect expanded(_pick_bbox); + // FIXME, why expand by delta? When is the next line needed? + // expanded.expandBy(delta); + if (expanded.contains(p)) { + result = this; + } + } + return result; +} + + + +DrawingText::DrawingText(Drawing &drawing) + : DrawingGroup(drawing) +{} + +DrawingText::~DrawingText() += default; + +void +DrawingText::clear() +{ + _markForRendering(); + _children.clear_and_dispose(DeleteDisposer()); +} + +bool +DrawingText::addComponent(font_instance *font, int glyph, Geom::Affine const &trans, + float width, float ascent, float descent, float phase_length) +{ +/* original, did not save a glyph for white space characters, causes problems for text-decoration + if (!font || !font->PathVector(glyph)) { + return(false); + } +*/ + if (!font)return(false); + + _markForRendering(); + DrawingGlyphs *ng = new DrawingGlyphs(_drawing); + ng->setGlyph(font, glyph, trans); + if(font->PathVector(glyph)){ ng->_drawable = true; } + else { ng->_drawable = false; } + ng->_width = width; // used especially when _drawable = false, otherwise, it is the advance of the font + ng->_asc = ascent; // of font, not of this one character + ng->_dsc = descent; // of font, not of this one character + ng->_pl = phase_length; // used for phase of dots, dashes, and wavy + appendChild(ng); + return(true); +} + +void +DrawingText::setStyle(SPStyle *style, SPStyle *context_style) +{ + DrawingGroup::setStyle(style, context_style); // Must be first + _nrstyle.set(_style, _context_style); +} + +void +DrawingText::setChildrenStyle(SPStyle* context_style) +{ + DrawingGroup::setChildrenStyle( context_style ); + _nrstyle.set(_style, _context_style); +} + +unsigned +DrawingText::_updateItem(Geom::IntRect const &area, UpdateContext const &ctx, unsigned flags, unsigned reset) +{ + _nrstyle.update(); + return DrawingGroup::_updateItem(area, ctx, flags, reset); +} + +void DrawingText::decorateStyle(DrawingContext &dc, double vextent, double xphase, Geom::Point const &p1, Geom::Point const &p2, double thickness) +{ + double wave[16]={ + 0.000000, 0.382499, 0.706825, 0.923651, 1.000000, 0.923651, 0.706825, 0.382499, + 0.000000, -0.382499, -0.706825, -0.923651, -1.000000, -0.923651, -0.706825, -0.382499, + }; + int dashes[16]={ + 8, 7, 6, 5, + 4, 3, 2, 1, + -8, -7, -6, -5, + -4, -3, -2, -1 + }; + int dots[16]={ + 4, 3, 2, 1, + -4, -3, -2, -1, + 4, 3, 2, 1, + -4, -3, -2, -1 + }; + double step = vextent/32.0; + unsigned i = 15 & (unsigned) round(xphase/step); // xphase is >= 0.0 + + /* For most spans draw the last little bit right to p2 or even a little beyond. + This allows decoration continuity within the line, and does not step outside the clip box off the end + For the first/last section on the line though, stay well clear of the edge, or when the + text is dragged it may "spray" pixels. + */ + /* snap to nearest step in X */ + Geom::Point ps = Geom::Point(step * round(p1[Geom::X]/step),p1[Geom::Y]); + Geom::Point pf = Geom::Point(step * round(p2[Geom::X]/step),p2[Geom::Y]); + Geom::Point poff = Geom::Point(0,thickness/2.0); + + if(_nrstyle.text_decoration_style & NRStyle::TEXT_DECORATION_STYLE_ISDOUBLE){ + ps -= Geom::Point(0, vextent/12.0); + pf -= Geom::Point(0, vextent/12.0); + dc.rectangle( Geom::Rect(ps + poff, pf - poff)); + ps += Geom::Point(0, vextent/6.0); + pf += Geom::Point(0, vextent/6.0); + dc.rectangle( Geom::Rect(ps + poff, pf - poff)); + } + /* The next three have a problem in that they are phase dependent. The bits of a line are not + necessarily passing through this routine in order, so we have to use the xphase information + to figure where in each of their cycles to start. Only accurate to 1 part in 16. + Huge positive offset should keep the phase calculation from ever being negative. + */ + else if(_nrstyle.text_decoration_style & NRStyle::TEXT_DECORATION_STYLE_DOTTED){ + // FIXME: Per spec, this should produce round dots. + Geom::Point pv = ps; + while(true){ + Geom::Point pvlast = pv; + if(dots[i]>0){ + if(pv[Geom::X] > pf[Geom::X]) break; + + pv += Geom::Point(step * (double)dots[i], 0.0); + + if(pv[Geom::X]>= pf[Geom::X]){ + // Last dot + dc.rectangle( Geom::Rect(pvlast + poff, pf - poff)); + break; + } else { + dc.rectangle( Geom::Rect(pvlast + poff, pv - poff)); + } + + pv += Geom::Point(step * 4.0, 0.0); + + } else { + pv += Geom::Point(step * -(double)dots[i], 0.0); + } + i = 0; // once in phase, it stays in phase + } + } + else if(_nrstyle.text_decoration_style & NRStyle::TEXT_DECORATION_STYLE_DASHED){ + Geom::Point pv = ps; + while(true){ + Geom::Point pvlast = pv; + if(dashes[i]>0){ + if(pv[Geom::X]> pf[Geom::X]) break; + + pv += Geom::Point(step * (double)dashes[i], 0.0); + + if(pv[Geom::X]>= pf[Geom::X]){ + // Last dash + dc.rectangle( Geom::Rect(pvlast + poff, pf - poff)); + break; + } else { + dc.rectangle( Geom::Rect(pvlast + poff, pv - poff)); + } + + pv += Geom::Point(step * 8.0, 0.0); + + } else { + pv += Geom::Point(step * -(double)dashes[i], 0.0); + } + i = 0; // once in phase, it stays in phase + } + } + else if(_nrstyle.text_decoration_style & NRStyle::TEXT_DECORATION_STYLE_WAVY){ + double amp = vextent/10.0; + double x = ps[Geom::X]; + double y = ps[Geom::Y] + poff[Geom::Y]; + dc.moveTo(Geom::Point(x, y + amp * wave[i])); + while(true){ + i = ((i + 1) & 15); + x += step; + dc.lineTo(Geom::Point(x, y + amp * wave[i])); + if(x >= pf[Geom::X])break; + } + y = ps[Geom::Y] - poff[Geom::Y]; + dc.lineTo(Geom::Point(x, y + amp * wave[i])); + while(true){ + i = ((i - 1) & 15); + x -= step; + dc.lineTo(Geom::Point(x, y + amp * wave[i])); + if(x <= ps[Geom::X])break; + } + dc.closePath(); + } + else { // TEXT_DECORATION_STYLE_SOLID, also default in case it was not set for some reason + dc.rectangle( Geom::Rect(ps + poff, pf - poff)); + } +} + +/* returns scaled line thickness */ +void DrawingText::decorateItem(DrawingContext &dc, double phase_length, bool under) +{ + if ( _nrstyle.font_size <= 1.0e-32 )return; // might cause a divide by zero or overflow and nothing would be visible anyway + double tsp_width_adj = _nrstyle.tspan_width / _nrstyle.font_size; + double tsp_asc_adj = _nrstyle.ascender / _nrstyle.font_size; + double tsp_size_adj = (_nrstyle.ascender + _nrstyle.descender) / _nrstyle.font_size; + + double final_underline_thickness = CLAMP(_nrstyle.underline_thickness, tsp_size_adj/30.0, tsp_size_adj/10.0); + double final_line_through_thickness = CLAMP(_nrstyle.line_through_thickness, tsp_size_adj/30.0, tsp_size_adj/10.0); + + double xphase = phase_length/ _nrstyle.font_size; // used to figure out phase of patterns + + Geom::Point p1; + Geom::Point p2; + // All lines must be the same thickness, in combinations, line_through trumps underline + double thickness = final_underline_thickness; + if ( thickness <= 1.0e-32 )return; // might cause a divide by zero or overflow and nothing would be visible anyway + dc.setTolerance(0.5); // Is this really necessary... could effect dots. + + if( under ) { + + if(_nrstyle.text_decoration_line & NRStyle::TEXT_DECORATION_LINE_UNDERLINE){ + p1 = Geom::Point(0.0, -_nrstyle.underline_position); + p2 = Geom::Point(tsp_width_adj,-_nrstyle.underline_position); + decorateStyle(dc, tsp_size_adj, xphase, p1, p2, thickness); + } + + if(_nrstyle.text_decoration_line & NRStyle::TEXT_DECORATION_LINE_OVERLINE){ + p1 = Geom::Point(0.0, tsp_asc_adj -_nrstyle.underline_position + 1 * final_underline_thickness); + p2 = Geom::Point(tsp_width_adj,tsp_asc_adj -_nrstyle.underline_position + 1 * final_underline_thickness); + decorateStyle(dc, tsp_size_adj, xphase, p1, p2, thickness); + } + + } else { + // Over + + if(_nrstyle.text_decoration_line & NRStyle::TEXT_DECORATION_LINE_LINETHROUGH){ + thickness = final_line_through_thickness; + p1 = Geom::Point(0.0, _nrstyle.line_through_position); + p2 = Geom::Point(tsp_width_adj,_nrstyle.line_through_position); + decorateStyle(dc, tsp_size_adj, xphase, p1, p2, thickness); + } + + // Obviously this does not blink, but it does indicate which text has been set with that attribute + if(_nrstyle.text_decoration_line & NRStyle::TEXT_DECORATION_LINE_BLINK){ + thickness = final_line_through_thickness; + p1 = Geom::Point(0.0, _nrstyle.line_through_position - 2*final_line_through_thickness); + p2 = Geom::Point(tsp_width_adj,_nrstyle.line_through_position - 2*final_line_through_thickness); + decorateStyle(dc, tsp_size_adj, xphase, p1, p2, thickness); + p1 = Geom::Point(0.0, _nrstyle.line_through_position + 2*final_line_through_thickness); + p2 = Geom::Point(tsp_width_adj,_nrstyle.line_through_position + 2*final_line_through_thickness); + decorateStyle(dc, tsp_size_adj, xphase, p1, p2, thickness); + } + } +} + +unsigned DrawingText::_renderItem(DrawingContext &dc, Geom::IntRect const &/*area*/, unsigned /*flags*/, DrawingItem * /*stop_at*/) +{ + if (_drawing.outline()) { + guint32 rgba = _drawing.outlinecolor; + Inkscape::DrawingContext::Save save(dc); + dc.setSource(rgba); + dc.setTolerance(0.5); // low quality, but good enough for outline mode + + for (auto & i : _children) { + DrawingGlyphs *g = dynamic_cast<DrawingGlyphs *>(&i); + if (!g) throw InvalidItemException(); + + Inkscape::DrawingContext::Save save(dc); + // skip glyphs with singular transforms + if (g->_ctm.isSingular()) continue; + dc.transform(g->_ctm); + if(g->_drawable){ + dc.path(*g->_font->PathVector(g->_glyph)); + dc.fill(); + } + } + return RENDER_OK; + } + + // NOTE: This is very similar to drawing-shape.cpp; the only differences are in path feeding + // and in applying text decorations. + + + // Do we have text decorations? + bool decorate = (_nrstyle.text_decoration_line != NRStyle::TEXT_DECORATION_LINE_CLEAR ); + + // prepareFill / prepareStroke need to be called with _ctm in effect. + // However, we might need to apply a different ctm for glyphs. + // Therefore, only apply this ctm temporarily. + bool has_stroke = false; + bool has_fill = false; + bool has_td_fill = false; + bool has_td_stroke = false; + { + Inkscape::DrawingContext::Save save(dc); + dc.transform(_ctm); + + has_fill = _nrstyle.prepareFill( dc, _item_bbox, _fill_pattern); + has_stroke = _nrstyle.prepareStroke( dc, _item_bbox, _stroke_pattern); + + // Avoid creating patterns if not needed + if( decorate ) { + has_td_fill = _nrstyle.prepareTextDecorationFill( dc, _item_bbox, _fill_pattern); + has_td_stroke = _nrstyle.prepareTextDecorationStroke(dc, _item_bbox, _stroke_pattern); + } + } + + if (has_fill || has_stroke || has_td_fill || has_td_stroke) { + + // Determine order for fill and stroke. + // Text doesn't have markers, we can do paint-order quick and dirty. + bool fill_first = false; + if( _nrstyle.paint_order_layer[0] == NRStyle::PAINT_ORDER_NORMAL || + _nrstyle.paint_order_layer[0] == NRStyle::PAINT_ORDER_FILL || + _nrstyle.paint_order_layer[2] == NRStyle::PAINT_ORDER_STROKE ) { + fill_first = true; + } // Won't get "stroke fill stroke" but that isn't 'valid' + + + // Determine geometry of text decoration + double phase_length = 0.0; + Geom::Affine aff; + if( decorate ) { + + Geom::Affine rotinv; + bool invset = false; + double leftmost = DBL_MAX; + bool first_y = true; + double start_y = 0.0; + for (auto & i : _children) { + + DrawingGlyphs *g = dynamic_cast<DrawingGlyphs *>(&i); + if (!g) throw InvalidItemException(); + + if (!invset) { + rotinv = g->_ctm.withoutTranslation().inverse(); + invset = true; + } + + Geom::Point pt = g->_ctm.translation() * rotinv; + if (pt[Geom::X] < leftmost) { + leftmost = pt[Geom::X]; + aff = g->_ctm; + phase_length = g->_pl; + } + + // Check for text on a path. FIXME: This needs better test (and probably not here). + if (first_y) { + first_y = false; + start_y = pt[Geom::Y]; + } + else if (fabs(pt[Geom::Y] - start_y) > 1.0e-6) { + // If the text has been mapped onto a path, which causes y to vary, drop the + // text decorations. To handle that properly would need a conformal map. + decorate = false; + } + } + } + + // Draw text decorations that go UNDER the text (underline, over-line) + if( decorate ) { + + { + Inkscape::DrawingContext::Save save(dc); + dc.transform(aff); // must be leftmost affine in span + decorateItem(dc, phase_length, true); + } + + { + Inkscape::DrawingContext::Save save(dc); + dc.transform(_ctm); // Needed so that fill pattern rotates with text + + if (has_td_fill && fill_first) { + _nrstyle.applyTextDecorationFill(dc); + dc.fillPreserve(); + } + + if (has_td_stroke) { + _nrstyle.applyTextDecorationStroke(dc); + dc.strokePreserve(); + } + + if (has_td_fill && !fill_first) { + _nrstyle.applyTextDecorationFill(dc); + dc.fillPreserve(); + } + + } + + dc.newPath(); // Clear text-decoration path + } + + // Accumulate the path that represents the glyphs and/or draw SVG glyphs. + for (auto & i : _children) { + DrawingGlyphs *g = dynamic_cast<DrawingGlyphs *>(&i); + if (!g) throw InvalidItemException(); + + Inkscape::DrawingContext::Save save(dc); + if (g->_ctm.isSingular()) continue; + dc.transform(g->_ctm); + if (g->_drawable) { + if (g->_font->FontHasSVG()) { + Inkscape::Pixbuf* pixbuf = g->_font->PixBuf(g->_glyph); + if (pixbuf) { + // Geom::OptRect box = bounds_exact(*g->_font->PathVector(g->_glyph)); + // if (box) { + // Inkscape::DrawingContext::Save save(dc); + // dc.newPath(); + // dc.rectangle(*box); + // dc.setLineWidth(0.01); + // dc.setSource(0x8080ffff); + // dc.stroke(); + // } + { + // pixbuf is in font design units, scale to embox. + double scale = g->_font->GetDesignUnits(); + if (scale <= 0) scale = 1000; + Inkscape::DrawingContext::Save save(dc); + dc.translate(0, 1); + dc.scale(1.0/scale, -1.0/scale); + dc.setSource(pixbuf->getSurfaceRaw(), 0, 0); + dc.paint(1); + } + } else { + dc.path(*g->_font->PathVector(g->_glyph)); + } + } else { + dc.path(*g->_font->PathVector(g->_glyph)); + } + } + } + + // Draw the glyphs (non-SVG glyphs). + { + Inkscape::DrawingContext::Save save(dc); + dc.transform(_ctm); + if (has_fill && fill_first) { + _nrstyle.applyFill(dc); + dc.fillPreserve(); + } + } + { + Inkscape::DrawingContext::Save save(dc); + if (!_style || !(_style->vector_effect.stroke)) { + dc.transform(_ctm); + } + if (has_stroke) { + _nrstyle.applyStroke(dc); + + // If the draw mode is set to visible hairlines, don't let anything get smaller + // than half a pixel. + if (_drawing.visibleHairlines()) { + double half_pixel_size = 0.5, trash = 0.5; + dc.device_to_user_distance(half_pixel_size, trash); + if (_nrstyle.stroke_width < half_pixel_size) { + dc.setLineWidth(half_pixel_size); + } + } + + dc.strokePreserve(); + } + } + { + Inkscape::DrawingContext::Save save(dc); + dc.transform(_ctm); + if (has_fill && !fill_first) { + _nrstyle.applyFill(dc); + dc.fillPreserve(); + } + } + dc.newPath(); // Clear glyphs path + + // Draw text decorations that go OVER the text (line through, blink) + if (decorate) { + + { + Inkscape::DrawingContext::Save save(dc); + dc.transform(aff); // must be leftmost affine in span + decorateItem(dc, phase_length, false); + } + + { + Inkscape::DrawingContext::Save save(dc); + dc.transform(_ctm); // Needed so that fill pattern rotates with text + + if (has_td_fill && fill_first) { + _nrstyle.applyTextDecorationFill(dc); + dc.fillPreserve(); + } + + if (has_td_stroke) { + _nrstyle.applyTextDecorationStroke(dc); + dc.strokePreserve(); + } + + if (has_td_fill && !fill_first) { + _nrstyle.applyTextDecorationFill(dc); + dc.fillPreserve(); + } + + } + + dc.newPath(); // Clear text-decoration path + } + + } + return RENDER_OK; +} + +void DrawingText::_clipItem(DrawingContext &dc, Geom::IntRect const &/*area*/) +{ + Inkscape::DrawingContext::Save save(dc); + + // handle clip-rule + if (_style) { + if (_style->clip_rule.computed == SP_WIND_RULE_EVENODD) { + dc.setFillRule(CAIRO_FILL_RULE_EVEN_ODD); + } else { + dc.setFillRule(CAIRO_FILL_RULE_WINDING); + } + } + + for (auto & i : _children) { + DrawingGlyphs *g = dynamic_cast<DrawingGlyphs *>(&i); + if (!g) { + throw InvalidItemException(); + } + + Inkscape::DrawingContext::Save save(dc); + dc.transform(g->_ctm); + if(g->_drawable){ + dc.path(*g->_font->PathVector(g->_glyph)); + } + } + dc.fill(); +} + +DrawingItem * +DrawingText::_pickItem(Geom::Point const &p, double delta, unsigned flags) +{ + return DrawingGroup::_pickItem(p, delta, flags) ? this : nullptr; +} + +bool +DrawingText::_canClip() +{ + return true; +} + +} // end namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/drawing-text.h b/src/display/drawing-text.h new file mode 100644 index 0000000..084c793 --- /dev/null +++ b/src/display/drawing-text.h @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Group belonging to an SVG drawing element. + *//* + * Authors: + * Krzysztof KosiĆski <tweenk.pl@gmail.com> + * + * Copyright (C) 2011 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_DISPLAY_DRAWING_TEXT_H +#define SEEN_INKSCAPE_DISPLAY_DRAWING_TEXT_H + +#include "display/drawing-group.h" +#include "display/nr-style.h" + +class SPStyle; +class font_instance; + +namespace Inkscape { + +class DrawingGlyphs + : public DrawingItem +{ +public: + DrawingGlyphs(Drawing &drawing); + ~DrawingGlyphs() override; + + void setGlyph(font_instance *font, int glyph, Geom::Affine const &trans); + void setStyle(SPStyle *style, SPStyle *context_style = nullptr) override; // Not to be used + Geom::IntRect getPickBox() const { return _pick_bbox; }; + + protected: + unsigned _updateItem(Geom::IntRect const &area, UpdateContext const &ctx, + unsigned flags, unsigned reset) override; + DrawingItem *_pickItem(Geom::Point const &p, double delta, unsigned flags) override; + + font_instance *_font; + int _glyph; + bool _drawable; + float _width; // These three are used to set up bounding box + float _asc; // + float _dsc; // + float _pl; // phase length + Geom::IntRect _pick_bbox; + + friend class DrawingText; +}; + +class DrawingText + : public DrawingGroup +{ +public: + DrawingText(Drawing &drawing); + ~DrawingText() override; + + void clear(); + bool addComponent(font_instance *font, int glyph, Geom::Affine const &trans, + float width, float ascent, float descent, float phase_length); + void setStyle(SPStyle *style, SPStyle *context_style = nullptr) override; + void setChildrenStyle(SPStyle *context_style) override; + +protected: + unsigned _updateItem(Geom::IntRect const &area, UpdateContext const &ctx, + unsigned flags, unsigned reset) override; + unsigned _renderItem(DrawingContext &dc, Geom::IntRect const &area, unsigned flags, + DrawingItem *stop_at) override; + void _clipItem(DrawingContext &dc, Geom::IntRect const &area) override; + DrawingItem *_pickItem(Geom::Point const &p, double delta, unsigned flags) override; + bool _canClip() override; + + void decorateItem(DrawingContext &dc, double phase_length, bool under); + void decorateStyle(DrawingContext &dc, double vextent, double xphase, Geom::Point const &p1, Geom::Point const &p2, double thickness); + NRStyle _nrstyle; + + friend class DrawingGlyphs; +}; + +} // end namespace Inkscape + +#endif // !SEEN_INKSCAPE_DISPLAY_DRAWING_ITEM_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/drawing.cpp b/src/display/drawing.cpp new file mode 100644 index 0000000..8b9de15 --- /dev/null +++ b/src/display/drawing.cpp @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * SVG drawing for display. + *//* + * Authors: + * Krzysztof KosiĆski <tweenk.pl@gmail.com> + * Johan Engelen <j.b.c.engelen@alumnus.utwente.nl> + * + * Copyright (C) 2011-2012 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <algorithm> +#include "display/drawing.h" +#include "nr-filter-gaussian.h" +#include "nr-filter-types.h" + +//grayscale colormode: +#include "cairo-templates.h" +#include "drawing-context.h" +#include "canvas-arena.h" + + +namespace Inkscape { + +// hardcoded grayscale color matrix values as default +static const gdouble grayscale_value_matrix[20] = { + 0.21, 0.72, 0.072, 0, 0, + 0.21, 0.72, 0.072, 0, 0, + 0.21, 0.72, 0.072, 0, 0, + 0 , 0 , 0 , 1, 0 +}; + +Drawing::Drawing(SPCanvasArena *arena) + : _root(nullptr) + , outlinecolor(0x000000ff) + , delta(0) + , _exact(false) + , _outline_sensitive(true) + , _rendermode(RENDERMODE_NORMAL) + , _colormode(COLORMODE_NORMAL) + , _blur_quality(BLUR_QUALITY_BEST) + , _filter_quality(Filters::FILTER_QUALITY_BEST) + , _cache_score_threshold(50000.0) + , _cache_budget(0) + , _grayscale_colormatrix(std::vector<gdouble>(grayscale_value_matrix, grayscale_value_matrix + 20)) + , _canvasarena(arena) +{ + +} + +Drawing::~Drawing() +{ + delete _root; +} + +void +Drawing::setRoot(DrawingItem *item) +{ + delete _root; + _root = item; + if (item) { + assert(item->_child_type == DrawingItem::CHILD_ORPHAN); + item->_child_type = DrawingItem::CHILD_ROOT; + } +} + +RenderMode +Drawing::renderMode() const +{ + return _exact ? RENDERMODE_NORMAL : _rendermode; +} +ColorMode +Drawing::colorMode() const +{ + return (outline() || _exact) ? COLORMODE_NORMAL : _colormode; +} +bool +Drawing::outline() const +{ + return renderMode() == RENDERMODE_OUTLINE; +} +bool +Drawing::visibleHairlines() const +{ + return renderMode() == RENDERMODE_VISIBLE_HAIRLINES; +} +bool +Drawing::renderFilters() const +{ + return renderMode() == RENDERMODE_NORMAL || renderMode() == RENDERMODE_VISIBLE_HAIRLINES; +} +int +Drawing::blurQuality() const +{ + if (renderMode() == RENDERMODE_NORMAL) { + return _exact ? BLUR_QUALITY_BEST : _blur_quality; + } else { + return BLUR_QUALITY_WORST; + } +} +int +Drawing::filterQuality() const +{ + if (renderMode() == RENDERMODE_NORMAL) { + return _exact ? Filters::FILTER_QUALITY_BEST : _filter_quality; + } else { + return Filters::FILTER_QUALITY_WORST; + } +} + +void +Drawing::setRenderMode(RenderMode mode) +{ + _rendermode = mode; +} +void +Drawing::setColorMode(ColorMode mode) +{ + _colormode = mode; +} +void +Drawing::setBlurQuality(int q) +{ + _blur_quality = q; +} +void +Drawing::setFilterQuality(int q) +{ + _filter_quality = q; +} +void +Drawing::setExact(bool e) +{ + _exact = e; +} + +void Drawing::setOutlineSensitive(bool e) { _outline_sensitive = e; }; + +Geom::OptIntRect const & +Drawing::cacheLimit() const +{ + return _cache_limit; +} +void +Drawing::setCacheLimit(Geom::OptIntRect const &r, bool update_cache) +{ + _cache_limit = r; + if (update_cache) { + for (auto _cached_item : _cached_items) + { + _cached_item->_markForUpdate(DrawingItem::STATE_CACHE, false); + } + } +} +void +Drawing::setCacheBudget(size_t bytes) +{ + _cache_budget = bytes; + _pickItemsForCaching(); +} + +void +Drawing::setGrayscaleMatrix(gdouble value_matrix[20]) { + _grayscale_colormatrix = Filters::FilterColorMatrix::ColorMatrixMatrix( + std::vector<gdouble> (value_matrix, value_matrix + 20) ); +} + +void +Drawing::update(Geom::IntRect const &area, unsigned flags, unsigned reset) +{ + if (_root) { + auto ctx = _canvasarena ? _canvasarena->ctx : UpdateContext(); + _root->update(area, ctx, flags, reset); + } + if ((flags & DrawingItem::STATE_CACHE) || (flags & DrawingItem::STATE_ALL)) { + // process the updated cache scores + _pickItemsForCaching(); + } +} + +void +Drawing::render(DrawingContext &dc, Geom::IntRect const &area, unsigned flags, int antialiasing) +{ + if (_root) { + int prev_a = _root->_antialias; + if(antialiasing >= 0) + _root->setAntialiasing(antialiasing); + _root->render(dc, area, flags); + _root->setAntialiasing(prev_a); + } + + if (colorMode() == COLORMODE_GRAYSCALE) { + // apply grayscale filter on top of everything + cairo_surface_t *input = dc.rawTarget(); + cairo_surface_t *out = ink_cairo_surface_create_identical(input); + ink_cairo_surface_filter(input, out, _grayscale_colormatrix); + Geom::Point origin = dc.targetLogicalBounds().min(); + dc.setSource(out, origin[Geom::X], origin[Geom::Y]); + dc.setOperator(CAIRO_OPERATOR_SOURCE); + dc.paint(); + dc.setOperator(CAIRO_OPERATOR_OVER); + + cairo_surface_destroy(out); + } +} + +DrawingItem * +Drawing::pick(Geom::Point const &p, double delta, unsigned flags) +{ + if (_root) { + return _root->pick(p, delta, flags); + } + return nullptr; +} + +void +Drawing::_pickItemsForCaching() +{ + // we cache the objects with the highest score until the budget is exhausted + _candidate_items.sort(std::greater<CacheRecord>()); + size_t used = 0; + CandidateList::iterator i; + for (i = _candidate_items.begin(); i != _candidate_items.end(); ++i) { + if (used + i->cache_size > _cache_budget) break; + used += i->cache_size; + } + + std::set<DrawingItem*> to_cache; + for (CandidateList::iterator j = _candidate_items.begin(); j != i; ++j) { + j->item->setCached(true); + to_cache.insert(j->item); + } + // Everything which is now in _cached_items but not in to_cache must be uncached + // Note that calling setCached on an item modifies _cached_items + // TODO: find a way to avoid the set copy + std::set<DrawingItem*> to_uncache; + std::set_difference(_cached_items.begin(), _cached_items.end(), + to_cache.begin(), to_cache.end(), + std::inserter(to_uncache, to_uncache.end())); + for (auto j : to_uncache) { + j->setCached(false); + } +} + +} // end namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/drawing.h b/src/display/drawing.h new file mode 100644 index 0000000..b33c897 --- /dev/null +++ b/src/display/drawing.h @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * SVG drawing for display. + *//* + * Authors: + * Krzysztof KosiĆski <tweenk.pl@gmail.com> + * + * Copyright (C) 2011 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_DISPLAY_DRAWING_H +#define SEEN_INKSCAPE_DISPLAY_DRAWING_H + +#include <2geom/rect.h> +#include <boost/operators.hpp> +#include <boost/utility.hpp> +#include <set> +#include <sigc++/sigc++.h> + +#include "display/drawing-item.h" +#include "display/rendermode.h" +#include "nr-filter-colormatrix.h" + +typedef struct _SPCanvasArena SPCanvasArena; +typedef unsigned int guint32; + +namespace Inkscape { + +class DrawingItem; + +class Drawing + : boost::noncopyable +{ +public: + struct OutlineColors { + guint32 paths; + guint32 clippaths; + guint32 masks; + guint32 images; + }; + + Drawing(SPCanvasArena *arena = nullptr); + ~Drawing(); + + DrawingItem *root() { return _root; } + SPCanvasArena *arena() { return _canvasarena; } + void setRoot(DrawingItem *item); + + RenderMode renderMode() const; + ColorMode colorMode() const; + bool outline() const; + bool visibleHairlines() const; + bool renderFilters() const; + int blurQuality() const; + int filterQuality() const; + void setRenderMode(RenderMode mode); + void setColorMode(ColorMode mode); + void setBlurQuality(int q); + void setFilterQuality(int q); + void setExact(bool e); + bool getExact() const { return _exact; }; + void setOutlineSensitive(bool e); + bool getOutlineSensitive() const { return _outline_sensitive; }; + + Geom::OptIntRect const &cacheLimit() const; + void setCacheLimit(Geom::OptIntRect const &r, bool update_cache = true); + void setCacheBudget(size_t bytes); + + OutlineColors const &colors() const { return _colors; } + + void setGrayscaleMatrix(double value_matrix[20]); + + void update(Geom::IntRect const &area = Geom::IntRect::infinite(), unsigned flags = DrawingItem::STATE_ALL, unsigned reset = 0); + void render(DrawingContext &dc, Geom::IntRect const &area, unsigned flags = 0, int antialiasing = -1); + DrawingItem *pick(Geom::Point const &p, double delta, unsigned flags); + + sigc::signal<void, DrawingItem *> signal_request_update; + sigc::signal<void, Geom::IntRect const &> signal_request_render; + sigc::signal<void, DrawingItem *> signal_item_deleted; + +private: + void _pickItemsForCaching(); + + typedef std::list<CacheRecord> CandidateList; + bool _outline_sensitive; + DrawingItem *_root; + std::set<DrawingItem *> _cached_items; // modified by DrawingItem::setCached() + CacheList _candidate_items; +public: + // TODO: remove these temporarily public members + guint32 outlinecolor; + double delta; +private: + bool _exact; // if true then rendering must be exact + RenderMode _rendermode; + ColorMode _colormode; + int _blur_quality; + int _filter_quality; + Geom::OptIntRect _cache_limit; + + double _cache_score_threshold; ///< do not consider objects for caching below this score + size_t _cache_budget; ///< maximum allowed size of cache + + OutlineColors _colors; + Filters::FilterColorMatrix::ColorMatrixMatrix _grayscale_colormatrix; + SPCanvasArena *_canvasarena; // may be NULL if this arena is not the screen + // but used for export etc. + + friend class DrawingItem; +}; + +} // end namespace Inkscape + +#endif // !SEEN_INKSCAPE_DRAWING_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/gnome-canvas-acetate.cpp b/src/display/gnome-canvas-acetate.cpp new file mode 100644 index 0000000..5157ab8 --- /dev/null +++ b/src/display/gnome-canvas-acetate.cpp @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Infinite invisible canvas item + * + * Author: + * Federico Mena <federico@nuclecu.unam.mx> + * Raph Levien <raph@acm.org> + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 1998-1999 The Free Software Foundation + * Copyright (C) 2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "gnome-canvas-acetate.h" +#include "ui/event-debug.h" + +static void sp_canvas_acetate_destroy(SPCanvasItem *object); + +static void sp_canvas_acetate_update (SPCanvasItem *item, Geom::Affine const &affine, unsigned int flags); +static double sp_canvas_acetate_point (SPCanvasItem *item, Geom::Point p, SPCanvasItem **actual_item); +static int sp_canvas_acetate_event( SPCanvasItem *item, GdkEvent *event); + +G_DEFINE_TYPE(SPCanvasAcetate, sp_canvas_acetate, SP_TYPE_CANVAS_ITEM); + +static void sp_canvas_acetate_class_init (SPCanvasAcetateClass *klass) +{ + SPCanvasItemClass *item_class = (SPCanvasItemClass *) klass; + + item_class->destroy = sp_canvas_acetate_destroy; + item_class->update = sp_canvas_acetate_update; + item_class->point = sp_canvas_acetate_point; + // item_class->event = sp_canvas_acetate_event; +} + +static void sp_canvas_acetate_init (SPCanvasAcetate */*acetate*/) +{ + /* Nothing here */ +} + +static void sp_canvas_acetate_destroy(SPCanvasItem *object) +{ + g_return_if_fail (object != nullptr); + g_return_if_fail (GNOME_IS_CANVAS_ACETATE (object)); + + if (SP_CANVAS_ITEM_CLASS(sp_canvas_acetate_parent_class)->destroy) + SP_CANVAS_ITEM_CLASS(sp_canvas_acetate_parent_class)->destroy(object); +} + +static void sp_canvas_acetate_update( SPCanvasItem *item, Geom::Affine const &/*affine*/, unsigned int /*flags*/ ) +{ + item->x1 = -G_MAXINT; + item->y1 = -G_MAXINT; + item->x2 = G_MAXINT; + item->y2 = G_MAXINT; +} + +static double sp_canvas_acetate_point( SPCanvasItem *item, Geom::Point /*p*/, SPCanvasItem **actual_item ) +{ + *actual_item = item; + + return 0.0; +} + +// static int sp_canvas_acetate_event( SPCanvasItem *item, GdkEvent *event) +// { +// ui_dump_event (event, "sp_canvas_acetate_event"); +// return 0; // ? +// } diff --git a/src/display/gnome-canvas-acetate.h b/src/display/gnome-canvas-acetate.h new file mode 100644 index 0000000..d08667d --- /dev/null +++ b/src/display/gnome-canvas-acetate.h @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_CANVAS_ACETATE_H +#define SEEN_SP_CANVAS_ACETATE_H + +/* + * Infinite invisible canvas item + * + * Author: + * Federico Mena <federico@nuclecu.unam.mx> + * Raph Levien <raph@acm.org> + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 1998-1999 The Free Software Foundation + * Copyright (C) 2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "display/sp-canvas-item.h" + +#define GNOME_TYPE_CANVAS_ACETATE (sp_canvas_acetate_get_type ()) +#define SP_CANVAS_ACETATE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GNOME_TYPE_CANVAS_ACETATE, SPCanvasAcetate)) +#define SP_CANVAS_ACETATE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GNOME_TYPE_CANVAS_ACETATE, SPCanvasAcetateClass)) +#define GNOME_IS_CANVAS_ACETATE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GNOME_TYPE_CANVAS_ACETATE)) +#define GNOME_IS_CANVAS_ACETATE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GNOME_TYPE_CANVAS_ACETATE)) + + +struct SPCanvasAcetate { + SPCanvasItem item; +}; + +struct SPCanvasAcetateClass { + SPCanvasItemClass parent_class; +}; + +GType sp_canvas_acetate_get_type (); + +#endif // SEEN_SP_CANVAS_ACETATE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/display/grayscale.cpp b/src/display/grayscale.cpp new file mode 100644 index 0000000..fb77dfa --- /dev/null +++ b/src/display/grayscale.cpp @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * Provide methods to calculate grayscale values (e.g. convert rgba value to grayscale rgba value) + */ + +/* + * Author: + * Johan Engelen <goejendaagh@zonnet.nl> + * + * Copyright (C) 2011 Author + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "display/grayscale.h" +#include "color.h" + +// for activeDesktopIsGrayscale: +#include "display/rendermode.h" +#include "inkscape.h" +#include "desktop.h" + +namespace Grayscale { + +/* Original values from Johan: +const float red_factor = 0.3; +const float green_factor = 0.59; +const float blue_factor = 0.11; +*/ + +// Values below are from the SVG specification +const float red_factor = 0.2125; +const float green_factor = 0.7154; +const float blue_factor = 0.0721; + +guint32 process(guint32 rgba) { + return process(SP_RGBA32_R_U(rgba), SP_RGBA32_G_U(rgba), SP_RGBA32_B_U(rgba), SP_RGBA32_A_U(rgba)); +} + +guint32 process(unsigned char r, unsigned char g, unsigned char b, unsigned char a) { + + /** To reduce banding in gradients, this calculation is tweaked a bit + * by outputting blue+1 or red+1 or both. The luminance is calculated + * times 4. Then last two bits are used to determine if red and/or blue + * can be increased by one. Then these two bits are discarded. + * So the output color it still looks gray, but has more than 256 steps. + * The assumption is that the eye is most sensitive to green, then red, then blue. + * (hope this trick works :-) Johan) + */ + + guint32 luminance = ( red_factor * (r << 3) + + green_factor * (g << 3) + + blue_factor * (b << 3) ); + unsigned blue_plus_one = (luminance & 0x01) ? 1 : 0; + unsigned red_plus_one = (luminance & 0x02) ? 1 : 0; + unsigned green_plus_one = (luminance & 0x04) ? 1 : 0; + luminance = luminance >> 3; + + if (luminance >= 0xff) { + return SP_RGBA32_U_COMPOSE(0xff, 0xff, 0xff, a); + } else { + return SP_RGBA32_U_COMPOSE(luminance + red_plus_one, luminance + green_plus_one, luminance + blue_plus_one, a); + } +} + +unsigned char luminance(unsigned char r, unsigned char g, unsigned char b) { + guint32 luminance = ( red_factor * r + + green_factor * g + + blue_factor * b ); + if (luminance > 0xff) { + luminance = 0xff; + } + + return luminance & 0xff; +} + +/** + * Use this method if there is no other way to find out if grayscale view or not. + * + * In some cases, the choice between normal or grayscale is so deep in the code hierarchy, + * that it is not possible to determine whether grayscale is desired or not, without using + * the global SP_ACTIVE_DESKTOP macro. Then use this method, so we know where the abuse is + * happening... + */ +bool activeDesktopIsGrayscale() { + if (SP_ACTIVE_DESKTOP) { + return (SP_ACTIVE_DESKTOP->getColorMode() == Inkscape::COLORMODE_GRAYSCALE); + } + + return false; +} + + +}; + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/grayscale.h b/src/display/grayscale.h new file mode 100644 index 0000000..fad1205 --- /dev/null +++ b/src/display/grayscale.h @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_DISPLAY_GRAYSCALE_H +#define SEEN_DISPLAY_GRAYSCALE_H + +/* + * Author: + * Johan Engelen <goejendaagh@zonnet.nl> + * + * Copyright (C) 2011 Author + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +typedef unsigned int guint32; + +/** + * Provide methods to calculate grayscale values (e.g. convert rgba value to grayscale rgba value). + */ +namespace Grayscale { + guint32 process(guint32 rgba); + guint32 process(unsigned char r, unsigned char g, unsigned char b, unsigned char a); + unsigned char luminance(unsigned char r, unsigned char g, unsigned char b); + + bool activeDesktopIsGrayscale(); +}; + +#endif // !SEEN_DISPLAY_GRAYSCALE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/guideline.cpp b/src/display/guideline.cpp new file mode 100644 index 0000000..d2816f1 --- /dev/null +++ b/src/display/guideline.cpp @@ -0,0 +1,301 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Horizontal/vertical but can also be angled line + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Johan Engelen + * Maximilian Albert <maximilian.albert@gmail.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2000-2002 Lauris Kaplinski + * Copyright (C) 2007 Johan Engelen + * Copyright (C) 2009 Maximilian Albert + * Copyright (C) 2017 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/coord.h> +#include <2geom/transforms.h> +#include <2geom/line.h> + +#include "sp-canvas-util.h" +#include "inkscape.h" +#include "guideline.h" +#include "display/cairo-utils.h" +#include "display/sp-canvas.h" +#include "display/sodipodi-ctrl.h" +#include "object/sp-namedview.h" + +static void sp_guideline_destroy(SPCanvasItem *object); + +static void sp_guideline_update(SPCanvasItem *item, Geom::Affine const &affine, unsigned int flags); +static void sp_guideline_render(SPCanvasItem *item, SPCanvasBuf *buf); + +static double sp_guideline_point(SPCanvasItem *item, Geom::Point p, SPCanvasItem **actual_item); + +G_DEFINE_TYPE(SPGuideLine, sp_guideline, SP_TYPE_CANVAS_ITEM); + +static void sp_guideline_class_init(SPGuideLineClass *c) +{ + SPCanvasItemClass *item_class = SP_CANVAS_ITEM_CLASS(c); + item_class->destroy = sp_guideline_destroy; + item_class->update = sp_guideline_update; + item_class->render = sp_guideline_render; + item_class->point = sp_guideline_point; +} + +static void sp_guideline_init(SPGuideLine *gl) +{ + gl->rgba = 0x0000ff7f; + + gl->locked = false; + gl->normal_to_line = Geom::Point(0,1); + gl->angle = M_PI_2; + gl->point_on_line = Geom::Point(0,0); + gl->sensitive = 0; + + gl->origin = nullptr; + gl->label = nullptr; +} + +static void sp_guideline_destroy(SPCanvasItem *object) +{ + g_return_if_fail (object != nullptr); + g_return_if_fail (SP_IS_GUIDELINE (object)); + + SPGuideLine *gl = SP_GUIDELINE(object); + + if (gl->origin) { + sp_canvas_item_destroy(SP_CANVAS_ITEM(gl->origin)); + } + + if (gl->label) { + g_free(gl->label); + } + + SP_CANVAS_ITEM_CLASS(sp_guideline_parent_class)->destroy(object); +} + +static void sp_guideline_render(SPCanvasItem *item, SPCanvasBuf *buf) +{ + SPGuideLine const *gl = SP_GUIDELINE (item); + + cairo_save(buf->ct); + cairo_translate(buf->ct, -buf->rect.left(), -buf->rect.top()); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool xrayactive = prefs->getBool("/desktop/xrayactive", false); + SPDesktop * desktop = SP_ACTIVE_DESKTOP; + if (xrayactive && desktop) { + guint32 bg = desktop->getNamedView()->pagecolor; + guint32 _color = SP_RGBA32_F_COMPOSE( + CLAMP(((1 - SP_RGBA32_A_F(gl->rgba)) * SP_RGBA32_R_F(bg)) + (SP_RGBA32_A_F(gl->rgba) * SP_RGBA32_R_F(gl->rgba)), 0.0, 1.0), + CLAMP(((1 - SP_RGBA32_A_F(gl->rgba)) * SP_RGBA32_G_F(bg)) + (SP_RGBA32_A_F(gl->rgba) * SP_RGBA32_G_F(gl->rgba)), 0.0, 1.0), + CLAMP(((1 - SP_RGBA32_A_F(gl->rgba)) * SP_RGBA32_B_F(bg)) + (SP_RGBA32_A_F(gl->rgba) * SP_RGBA32_B_F(gl->rgba)), 0.0, 1.0), + 1.0); + ink_cairo_set_source_rgba32(buf->ct, _color); + } else { + ink_cairo_set_source_rgba32(buf->ct, gl->rgba); + } + cairo_set_line_width(buf->ct, 1); + cairo_set_line_cap(buf->ct, CAIRO_LINE_CAP_SQUARE); + cairo_set_font_size(buf->ct, 10); + + // normal_dt does not have unit length + Geom::Point normal_dt = gl->normal_to_line * gl->affine.withoutTranslation(); + Geom::Point point_on_line_dt = gl->point_on_line * gl->affine; + + if (gl->label) { + int px = round(point_on_line_dt[Geom::X]); + int py = round(point_on_line_dt[Geom::Y]); + cairo_save(buf->ct); + cairo_translate(buf->ct, px, py); + cairo_rotate(buf->ct, atan2(normal_dt.cw()) + M_PI * (desktop && desktop->is_yaxisdown() ? 1 : 0)); + cairo_translate(buf->ct, 0, -5); + cairo_move_to(buf->ct, 0, 0); + cairo_show_text(buf->ct, gl->label); + cairo_restore(buf->ct); + } + + // Draw guide. + // Special case horizontal and vertical lines so they accurately align to the to pixels. + + // Need to use floor()+0.5 such that Cairo will draw us lines with a width of a single pixel, without any aliasing. + // For this we need to position the lines at exactly half pixels, see https://www.cairographics.org/FAQ/#sharp_lines + // Must be consistent with the pixel alignment of the grid lines, see CanvasXYGrid::Render(), and the drawing of the rulers + + // Don't use isHorizontal()/isVertical() as they test only exact matches. + if ( Geom::are_near(normal_dt[Geom::Y], 0.0) ) { // Vertical? + + double position = floor(point_on_line_dt[Geom::X]) + 0.5; + cairo_move_to(buf->ct, position, buf->rect.top() + 0.5); + cairo_line_to(buf->ct, position, buf->rect.bottom() - 0.5); + cairo_stroke(buf->ct); + + } else if ( Geom::are_near(normal_dt[Geom::X], 0.0) ) { // Horizontal? + + double position = floor(point_on_line_dt[Geom::Y]) + 0.5; + cairo_move_to(buf->ct, buf->rect.left() + 0.5, position); + cairo_line_to(buf->ct, buf->rect.right() - 0.5, position); + cairo_stroke(buf->ct); + + } else { + + Geom::Line guide = + Geom::Line::from_origin_and_vector( point_on_line_dt, Geom::rot90(normal_dt) ); + + // Find intersections of guide with buf rectangle. There should be zero or two. + std::vector<Geom::Point> intersections; + for (unsigned i = 0; i < 4; ++i) { + Geom::LineSegment side( buf->rect.corner(i), buf->rect.corner((i+1)%4) ); + try { + Geom::OptCrossing oc = Geom::intersection(guide, side); + if (oc) { + intersections.push_back( guide.pointAt((*oc).ta)); + } + } catch (Geom::InfiniteSolutions) { + // Shouldn't happen as we have already taken care of horizontal/vertical guides. + std::cerr << "sp_guideline_render: Error: Infinite intersections." << std::endl; + } + } + + if (intersections.size() == 2) { + double x0 = intersections[0][Geom::X]; + double x1 = intersections[1][Geom::X]; + double y0 = intersections[0][Geom::Y]; + double y1 = intersections[1][Geom::Y]; + cairo_move_to(buf->ct, x0, y0); + cairo_line_to(buf->ct, x1, y1); + cairo_stroke(buf->ct); + } + } + + cairo_restore(buf->ct); +} + +static void sp_guideline_update(SPCanvasItem *item, Geom::Affine const &affine, unsigned int flags) +{ + SPGuideLine *gl = SP_GUIDELINE(item); + + if ((SP_CANVAS_ITEM_CLASS(sp_guideline_parent_class))->update) { + (SP_CANVAS_ITEM_CLASS(sp_guideline_parent_class))->update(item, affine, flags); + } + + if (item->visible) { + if (gl->locked) { + g_object_set(G_OBJECT(gl->origin), "stroke_color", 0x0000ff88, + "shape", SP_CTRL_SHAPE_CROSS, + "size", 7, NULL); + } else { + g_object_set(G_OBJECT(gl->origin), "stroke_color", 0xff000088, + "shape", SP_CTRL_SHAPE_CIRCLE, + "size", 5, NULL); + } + gl->origin->moveto(gl->point_on_line); + sp_canvas_item_request_update(SP_CANVAS_ITEM(gl->origin)); + } + + gl->affine = affine; + + // Rotated canvas requires large bbox for all guides. + sp_canvas_update_bbox (item, -1000000, -1000000, 1000000, 1000000); + +} + +// Returns 0.0 if point is on the guideline +static double sp_guideline_point(SPCanvasItem *item, Geom::Point p, SPCanvasItem **actual_item) +{ + SPGuideLine *gl = SP_GUIDELINE (item); + + if (!gl->sensitive) { + return Geom::infinity(); + } + + *actual_item = item; + + Geom::Point vec = gl->normal_to_line * gl->affine.withoutTranslation(); + double distance = Geom::dot((p - gl->point_on_line * gl->affine), unit_vector(vec)); + return MAX(fabs(distance)-1, 0); +} + +SPCanvasItem *sp_guideline_new(SPCanvasGroup *parent, char* label, Geom::Point point_on_line, Geom::Point normal) +{ + SPCanvasItem *item = sp_canvas_item_new(parent, SP_TYPE_GUIDELINE, nullptr); + SPGuideLine *gl = SP_GUIDELINE(item); + + normal.normalize(); + gl->label = label; + gl->locked = false; + gl->normal_to_line = normal; + gl->angle = tan( -gl->normal_to_line[Geom::X] / gl->normal_to_line[Geom::Y]); + sp_guideline_set_position(gl, point_on_line); + + gl->origin = (SPCtrl *) sp_canvas_item_new(parent, SP_TYPE_CTRL, + "anchor", SP_ANCHOR_CENTER, + "mode", SP_CTRL_MODE_COLOR, + "filled", FALSE, + "stroked", TRUE, + "stroke_color", 0x01000000, NULL); + gl->origin->pickable = false; + + return item; +} + +void sp_guideline_set_label(SPGuideLine *gl, const char* label) +{ + if (gl->label) { + g_free(gl->label); + } + gl->label = g_strdup(label); + + sp_canvas_item_request_update(SP_CANVAS_ITEM (gl)); +} + +void sp_guideline_set_locked(SPGuideLine *gl, const bool locked) +{ + gl->locked = locked; + sp_canvas_item_request_update(SP_CANVAS_ITEM (gl)); +} + +void sp_guideline_set_position(SPGuideLine *gl, Geom::Point point_on_line) +{ + gl->point_on_line = point_on_line; + sp_canvas_item_request_update(SP_CANVAS_ITEM (gl)); +} + +void sp_guideline_set_normal(SPGuideLine *gl, Geom::Point normal_to_line) +{ + gl->normal_to_line = normal_to_line; + gl->angle = tan( -normal_to_line[Geom::X] / normal_to_line[Geom::Y]); + + sp_canvas_item_request_update(SP_CANVAS_ITEM (gl)); +} + +void sp_guideline_set_color(SPGuideLine *gl, unsigned int rgba) +{ + gl->rgba = rgba; + g_object_set(G_OBJECT(gl->origin), "stroke_color", rgba, NULL); + sp_canvas_item_request_update(SP_CANVAS_ITEM(gl)); +} + +void sp_guideline_set_sensitive(SPGuideLine *gl, int sensitive) +{ + gl->sensitive = sensitive; +} + +void sp_guideline_delete(SPGuideLine *gl) +{ + sp_canvas_item_destroy(SP_CANVAS_ITEM(gl)); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/guideline.h b/src/display/guideline.h new file mode 100644 index 0000000..e81cb35 --- /dev/null +++ b/src/display/guideline.h @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_GUIDELINE_H +#define SEEN_SP_GUIDELINE_H + +/* + * The visual representation of SPGuide. + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Johan Engelen + * + * Copyright (C) 2000-2002 Lauris Kaplinski + * Copyright (C) 2007 Johan Engelen + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/point.h> +#include "sp-canvas-item.h" + +#define SP_TYPE_GUIDELINE (sp_guideline_get_type()) +#define SP_GUIDELINE(o) (G_TYPE_CHECK_INSTANCE_CAST((o), SP_TYPE_GUIDELINE, SPGuideLine)) +#define SP_IS_GUIDELINE(o) (G_TYPE_CHECK_INSTANCE_TYPE((o), SP_TYPE_GUIDELINE)) + +struct SPCtrl; + +struct SPGuideLine { + SPCanvasItem item; + Geom::Affine affine; + + SPCtrl *origin; // unlike 'item', this is only held locally + + guint32 rgba; + + char* label; + bool locked; + Geom::Point normal_to_line; + Geom::Point point_on_line; + double angle; + + unsigned int sensitive : 1; + + inline bool is_horizontal() const { return (normal_to_line[Geom::X] == 0.); }; + inline bool is_vertical() const { return (normal_to_line[Geom::Y] == 0.); }; +}; + +struct SPGuideLineClass { + SPCanvasItemClass parent_class; +}; + +GType sp_guideline_get_type(); + +SPCanvasItem *sp_guideline_new(SPCanvasGroup *parent, char* label, Geom::Point point_on_line, Geom::Point normal); + +void sp_guideline_set_label(SPGuideLine *gl, const char* label); +void sp_guideline_set_locked(SPGuideLine *gl, const bool locked); +void sp_guideline_set_position(SPGuideLine *gl, Geom::Point point_on_line); +void sp_guideline_set_normal(SPGuideLine *gl, Geom::Point normal_to_line); +void sp_guideline_set_color(SPGuideLine *gl, unsigned int rgba); +void sp_guideline_set_sensitive(SPGuideLine *gl, int sensitive); +void sp_guideline_delete(SPGuideLine *gl); + +#endif // SEEN_SP_GUIDELINE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/display/nr-3dutils.cpp b/src/display/nr-3dutils.cpp new file mode 100644 index 0000000..3c0eb5e --- /dev/null +++ b/src/display/nr-3dutils.cpp @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * 3D utils. + * + * Authors: + * Jean-Rene Reinhard <jr@komite.net> + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glib.h> + +#include "display/nr-3dutils.h" +#include <cmath> +#include <2geom/point.h> +#include <2geom/affine.h> + +namespace NR { + +void convert_coord(gdouble &x, gdouble &y, gdouble &z, Geom::Affine const &trans) { + Geom::Point p = Geom::Point(x, y); + p *= trans; + x = p[Geom::X]; + y = p[Geom::Y]; + z *= trans[0]; +} + +gdouble norm(const Fvector &v) { + return sqrt(v[X_3D]*v[X_3D] + v[Y_3D]*v[Y_3D] + v[Z_3D]*v[Z_3D]); +} + +void normalize_vector(Fvector &v) { + gdouble nv = norm(v); + //TODO test nv == 0 + for (int j = 0; j < 3; j++) { + v[j] /= nv; + } +} + +gdouble scalar_product(const Fvector &a, const Fvector &b) { + return a[X_3D] * b[X_3D] + + a[Y_3D] * b[Y_3D] + + a[Z_3D] * b[Z_3D]; +} + +void normalized_sum(Fvector &r, const Fvector &a, const Fvector &b) { + r[X_3D] = a[X_3D] + b[X_3D]; + r[Y_3D] = a[Y_3D] + b[Y_3D]; + r[Z_3D] = a[Z_3D] + b[Z_3D]; + normalize_vector(r); +} + +}/* namespace NR */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-3dutils.h b/src/display/nr-3dutils.h new file mode 100644 index 0000000..21fcf5f --- /dev/null +++ b/src/display/nr-3dutils.h @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_NR_3DUTILS_H +#define SEEN_NR_3DUTILS_H + +/* + * 3D utils. Definition of gdouble vectors of dimension 3 and of some basic + * functions. + * This looks redundant, why not just use Geom::Point for this? + * + * Authors: + * Jean-Rene Reinhard <jr@komite.net> + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/forward.h> + +namespace NR { + +#define X_3D 0 +#define Y_3D 1 +#define Z_3D 2 + +/** + * a type of 3 gdouble components vectors + */ +struct Fvector { + Fvector() { + v[0] = v[1] = v[2] = 0.0; + } + Fvector(double x, double y, double z) { + v[0] = x; + v[1] = y; + v[2] = z; + } + double v[3]; + double &operator[](unsigned i) { return v[i]; } + double operator[](unsigned i) const { return v[i]; } +}; + +/** + * The eye vector + */ +const static Fvector EYE_VECTOR(0, 0, 1); + +/** + * returns the euclidian norm of the vector v + * + * \param v a reference to a vector with double components + * \return the euclidian norm of v + */ +double norm(const Fvector &v); + +/** + * Normalizes a vector + * + * \param v a reference to a vector to normalize + */ +void normalize_vector(Fvector &v); + +/** + * Computes the scalar product between two Fvectors + * + * \param a a Fvector reference + * \param b a Fvector reference + * \return the scalar product of a and b + */ +double scalar_product(const Fvector &a, const Fvector &b); + +/** + * Computes the normalized sum of two Fvectors + * + * \param r a Fvector reference where we store the result + * \param a a Fvector reference + * \param b a Fvector reference + */ +void normalized_sum(Fvector &r, const Fvector &a, const Fvector &b); + +/** + * Applies the transformation matrix to (x, y, z). This function assumes that + * trans[0] = trans[3]. x and y are transformed according to trans, z is + * multiplied by trans[0]. + * + * \param x a reference to a x coordinate + * \param y a reference to a y coordinate + * \param z a reference to a z coordinate + * \param z a reference to a transformation matrix + */ +void convert_coord(double &x, double &y, double &z, Geom::Affine const &trans); + +} /* namespace NR */ + +#endif /* __NR_3DUTILS_H__ */ +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter-blend.cpp b/src/display/nr-filter-blend.cpp new file mode 100644 index 0000000..99fc2d6 --- /dev/null +++ b/src/display/nr-filter-blend.cpp @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * SVG feBlend renderer + *//* + * "This filter composites two objects together using commonly used + * imaging software blending modes. It performs a pixel-wise combination + * of two input images." + * http://www.w3.org/TR/SVG11/filters.html#feBlend + * + * Authors: + * Niko Kiirala <niko@kiirala.com> + * Jasper van de Gronde <th.v.d.gronde@hccnet.nl> + * Krzysztof KosiĆski <tweenk.pl@gmail.com> + * + * Copyright (C) 2007-2008 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm.h> +#include "display/cairo-templates.h" +#include "display/cairo-utils.h" +#include "display/nr-filter-blend.h" +#include "display/nr-filter-primitive.h" +#include "display/nr-filter-slot.h" +#include "display/nr-filter-types.h" +#include "preferences.h" + +namespace Inkscape { +namespace Filters { + +const std::set<SPBlendMode> FilterBlend::_valid_modes { + SP_CSS_BLEND_NORMAL, SP_CSS_BLEND_MULTIPLY, + SP_CSS_BLEND_SCREEN, SP_CSS_BLEND_DARKEN, + SP_CSS_BLEND_LIGHTEN, SP_CSS_BLEND_OVERLAY, + SP_CSS_BLEND_COLORDODGE, SP_CSS_BLEND_COLORBURN, + SP_CSS_BLEND_HARDLIGHT, SP_CSS_BLEND_SOFTLIGHT, + SP_CSS_BLEND_DIFFERENCE, SP_CSS_BLEND_EXCLUSION, + SP_CSS_BLEND_HUE, SP_CSS_BLEND_SATURATION, + SP_CSS_BLEND_COLOR, SP_CSS_BLEND_LUMINOSITY + }; + +FilterBlend::FilterBlend() + : _blend_mode(SP_CSS_BLEND_NORMAL), + _input2(NR_FILTER_SLOT_NOT_SET) +{} + +FilterPrimitive * FilterBlend::create() { + return new FilterBlend(); +} + +FilterBlend::~FilterBlend() += default; + +static inline cairo_operator_t get_cairo_op(SPBlendMode _blend_mode) +{ + switch (_blend_mode) { + case SP_CSS_BLEND_MULTIPLY: + return CAIRO_OPERATOR_MULTIPLY; + case SP_CSS_BLEND_SCREEN: + return CAIRO_OPERATOR_SCREEN; + case SP_CSS_BLEND_DARKEN: + return CAIRO_OPERATOR_DARKEN; + case SP_CSS_BLEND_LIGHTEN: + return CAIRO_OPERATOR_LIGHTEN; + // New in CSS Compositing and Blending Level 1 + case SP_CSS_BLEND_OVERLAY: + return CAIRO_OPERATOR_OVERLAY; + case SP_CSS_BLEND_COLORDODGE: + return CAIRO_OPERATOR_COLOR_DODGE; + case SP_CSS_BLEND_COLORBURN: + return CAIRO_OPERATOR_COLOR_BURN; + case SP_CSS_BLEND_HARDLIGHT: + return CAIRO_OPERATOR_HARD_LIGHT; + case SP_CSS_BLEND_SOFTLIGHT: + return CAIRO_OPERATOR_SOFT_LIGHT; + case SP_CSS_BLEND_DIFFERENCE: + return CAIRO_OPERATOR_DIFFERENCE; + case SP_CSS_BLEND_EXCLUSION: + return CAIRO_OPERATOR_EXCLUSION; + case SP_CSS_BLEND_HUE: + return CAIRO_OPERATOR_HSL_HUE; + case SP_CSS_BLEND_SATURATION: + return CAIRO_OPERATOR_HSL_SATURATION; + case SP_CSS_BLEND_COLOR: + return CAIRO_OPERATOR_HSL_COLOR; + case SP_CSS_BLEND_LUMINOSITY: + return CAIRO_OPERATOR_HSL_LUMINOSITY; + + case SP_CSS_BLEND_NORMAL: + default: + return CAIRO_OPERATOR_OVER; + } +} + +void FilterBlend::render_cairo(FilterSlot &slot) +{ + cairo_surface_t *input1 = slot.getcairo(_input); + cairo_surface_t *input2 = slot.getcairo(_input2); + + // We may need to transform input surface to correct color interpolation space. The input surface + // might be used as input to another primitive but it is likely that all the primitives in a given + // filter use the same color interpolation space so we don't copy the input before converting. + SPColorInterpolation ci_fp = SP_CSS_COLOR_INTERPOLATION_AUTO; + if( _style ) { + ci_fp = (SPColorInterpolation)_style->color_interpolation_filters.computed; + } + set_cairo_surface_ci( input1, ci_fp ); + set_cairo_surface_ci( input2, ci_fp ); + + // input2 is the "background" image + // out should be ARGB32 if any of the inputs is ARGB32 + cairo_surface_t *out = ink_cairo_surface_create_output(input1, input2); + set_cairo_surface_ci( out, ci_fp ); + + ink_cairo_surface_blit(input2, out); + cairo_t *out_ct = cairo_create(out); + cairo_set_source_surface(out_ct, input1, 0, 0); + + // All of the blend modes are implemented in Cairo as of 1.10. + // For a detailed description, see: + // http://cairographics.org/operators/ + cairo_set_operator(out_ct, get_cairo_op(_blend_mode)); + + cairo_paint(out_ct); + cairo_destroy(out_ct); + + slot.set(_output, out); + cairo_surface_destroy(out); +} + +bool FilterBlend::can_handle_affine(Geom::Affine const &) +{ + // blend is a per-pixel primitive and is immutable under transformations + return true; +} + +double FilterBlend::complexity(Geom::Affine const &) +{ + return 1.1; +} + +bool FilterBlend::uses_background() +{ + return (_input == NR_FILTER_BACKGROUNDIMAGE || _input == NR_FILTER_BACKGROUNDALPHA || + _input2 == NR_FILTER_BACKGROUNDIMAGE || _input2 == NR_FILTER_BACKGROUNDALPHA); +} + +void FilterBlend::set_input(int slot) { + _input = slot; +} + +void FilterBlend::set_input(int input, int slot) { + if (input == 0) _input = slot; + if (input == 1) _input2 = slot; +} + +void FilterBlend::set_mode(SPBlendMode mode) { + if (_valid_modes.count(mode)) + { + _blend_mode = mode; + } +} + +} /* namespace Filters */ +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter-blend.h b/src/display/nr-filter-blend.h new file mode 100644 index 0000000..01cffeb --- /dev/null +++ b/src/display/nr-filter-blend.h @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_NR_FILTER_BLEND_H +#define SEEN_NR_FILTER_BLEND_H + +/* + * SVG feBlend renderer + * + * "This filter composites two objects together using commonly used + * imaging software blending modes. It performs a pixel-wise combination + * of two input images." + * http://www.w3.org/TR/SVG11/filters.html#feBlend + * + * Authors: + * Niko Kiirala <niko@kiirala.com> + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include <set> +#include "style-enums.h" +#include "display/nr-filter-primitive.h" + +namespace Inkscape { +namespace Filters { + +class FilterBlend : public FilterPrimitive { +public: + FilterBlend(); + static FilterPrimitive *create(); + ~FilterBlend() override; + + void render_cairo(FilterSlot &slot) override; + bool can_handle_affine(Geom::Affine const &) override; + double complexity(Geom::Affine const &ctm) override; + bool uses_background() override; + + void set_input(int slot) override; + void set_input(int input, int slot) override; + void set_mode(SPBlendMode mode); + + Glib::ustring name() override { return Glib::ustring("Blend"); } + +private: + static const std::set<SPBlendMode> _valid_modes; + SPBlendMode _blend_mode; + int _input2; +}; + + +} /* namespace Filters */ +} /* namespace Inkscape */ + + + + +#endif /* __NR_FILTER_BLEND_H__ */ +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter-colormatrix.cpp b/src/display/nr-filter-colormatrix.cpp new file mode 100644 index 0000000..eff5d93 --- /dev/null +++ b/src/display/nr-filter-colormatrix.cpp @@ -0,0 +1,229 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * feColorMatrix filter primitive renderer + * + * Authors: + * Felipe CorrĂȘa da Silva Sanches <juca@members.fsf.org> + * Jasper van de Gronde <th.v.d.gronde@hccnet.nl> + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cmath> +#include <algorithm> +#include "display/cairo-templates.h" +#include "display/cairo-utils.h" +#include "display/nr-filter-colormatrix.h" +#include "display/nr-filter-slot.h" +#include <2geom/math-utils.h> + +namespace Inkscape { +namespace Filters { + +FilterColorMatrix::FilterColorMatrix() += default; + +FilterPrimitive * FilterColorMatrix::create() { + return new FilterColorMatrix(); +} + +FilterColorMatrix::~FilterColorMatrix() += default; + +FilterColorMatrix::ColorMatrixMatrix::ColorMatrixMatrix(std::vector<double> const &values) { + unsigned limit = std::min(static_cast<size_t>(20), values.size()); + for (unsigned i = 0; i < limit; ++i) { + if (i % 5 == 4) { + _v[i] = round(values[i]*255*255); + } else { + _v[i] = round(values[i]*255); + } + } + for (unsigned i = limit; i < 20; ++i) { + _v[i] = 0; + } +} + +guint32 FilterColorMatrix::ColorMatrixMatrix::operator()(guint32 in) { + EXTRACT_ARGB32(in, a, r, g, b) + // we need to un-premultiply alpha values for this type of matrix + // TODO: unpremul can be ignored if there is an identity mapping on the alpha channel + if (a != 0) { + r = unpremul_alpha(r, a); + g = unpremul_alpha(g, a); + b = unpremul_alpha(b, a); + } + + gint32 ro = r*_v[0] + g*_v[1] + b*_v[2] + a*_v[3] + _v[4]; + gint32 go = r*_v[5] + g*_v[6] + b*_v[7] + a*_v[8] + _v[9]; + gint32 bo = r*_v[10] + g*_v[11] + b*_v[12] + a*_v[13] + _v[14]; + gint32 ao = r*_v[15] + g*_v[16] + b*_v[17] + a*_v[18] + _v[19]; + ro = (pxclamp(ro, 0, 255*255) + 127) / 255; + go = (pxclamp(go, 0, 255*255) + 127) / 255; + bo = (pxclamp(bo, 0, 255*255) + 127) / 255; + ao = (pxclamp(ao, 0, 255*255) + 127) / 255; + + ro = premul_alpha(ro, ao); + go = premul_alpha(go, ao); + bo = premul_alpha(bo, ao); + + ASSEMBLE_ARGB32(pxout, ao, ro, go, bo) + return pxout; +} + + +struct ColorMatrixSaturate { + ColorMatrixSaturate(double v_in) { + // clamp parameter instead of clamping color values + double v = CLAMP(v_in, 0.0, 1.0); + _v[0] = 0.213+0.787*v; _v[1] = 0.715-0.715*v; _v[2] = 0.072-0.072*v; + _v[3] = 0.213-0.213*v; _v[4] = 0.715+0.285*v; _v[5] = 0.072-0.072*v; + _v[6] = 0.213-0.213*v; _v[7] = 0.715-0.715*v; _v[8] = 0.072+0.928*v; + } + + guint32 operator()(guint32 in) { + EXTRACT_ARGB32(in, a, r, g, b) + + // Note: this cannot be done in fixed point, because the loss of precision + // causes overflow for some values of v + guint32 ro = r*_v[0] + g*_v[1] + b*_v[2] + 0.5; + guint32 go = r*_v[3] + g*_v[4] + b*_v[5] + 0.5; + guint32 bo = r*_v[6] + g*_v[7] + b*_v[8] + 0.5; + + ASSEMBLE_ARGB32(pxout, a, ro, go, bo) + return pxout; + } +private: + double _v[9]; +}; + +struct ColorMatrixHueRotate { + ColorMatrixHueRotate(double v) { + double sinhue, coshue; + Geom::sincos(v * M_PI/180.0, sinhue, coshue); + + _v[0] = round((0.213 +0.787*coshue -0.213*sinhue)*255); + _v[1] = round((0.715 -0.715*coshue -0.715*sinhue)*255); + _v[2] = round((0.072 -0.072*coshue +0.928*sinhue)*255); + + _v[3] = round((0.213 -0.213*coshue +0.143*sinhue)*255); + _v[4] = round((0.715 +0.285*coshue +0.140*sinhue)*255); + _v[5] = round((0.072 -0.072*coshue -0.283*sinhue)*255); + + _v[6] = round((0.213 -0.213*coshue -0.787*sinhue)*255); + _v[7] = round((0.715 -0.715*coshue +0.715*sinhue)*255); + _v[8] = round((0.072 +0.928*coshue +0.072*sinhue)*255); + } + guint32 operator()(guint32 in) { + EXTRACT_ARGB32(in, a, r, g, b) + gint32 maxpx = a*255; + gint32 ro = r*_v[0] + g*_v[1] + b*_v[2]; + gint32 go = r*_v[3] + g*_v[4] + b*_v[5]; + gint32 bo = r*_v[6] + g*_v[7] + b*_v[8]; + ro = (pxclamp(ro, 0, maxpx) + 127) / 255; + go = (pxclamp(go, 0, maxpx) + 127) / 255; + bo = (pxclamp(bo, 0, maxpx) + 127) / 255; + + ASSEMBLE_ARGB32(pxout, a, ro, go, bo) + return pxout; + } +private: + gint32 _v[9]; +}; + +struct ColorMatrixLuminanceToAlpha { + guint32 operator()(guint32 in) { + // original computation in double: r*0.2125 + g*0.7154 + b*0.0721 + EXTRACT_ARGB32(in, a, r, g, b) + // unpremultiply color values + if (a != 0) { + r = unpremul_alpha(r, a); + g = unpremul_alpha(g, a); + b = unpremul_alpha(b, a); + } + guint32 ao = r*54 + g*182 + b*18; + return ((ao + 127) / 255) << 24; + } +}; + +void FilterColorMatrix::render_cairo(FilterSlot &slot) +{ + cairo_surface_t *input = slot.getcairo(_input); + cairo_surface_t *out = nullptr; + + // We may need to transform input surface to correct color interpolation space. The input surface + // might be used as input to another primitive but it is likely that all the primitives in a given + // filter use the same color interpolation space so we don't copy the input before converting. + SPColorInterpolation ci_fp = SP_CSS_COLOR_INTERPOLATION_AUTO; + if( _style ) { + ci_fp = (SPColorInterpolation)_style->color_interpolation_filters.computed; + } + set_cairo_surface_ci( input, ci_fp ); + + if (type == COLORMATRIX_LUMINANCETOALPHA) { + out = ink_cairo_surface_create_same_size(input, CAIRO_CONTENT_ALPHA); + } else { + out = ink_cairo_surface_create_identical(input); + // Set ci to that used for computation + set_cairo_surface_ci(out, ci_fp); + } + + switch (type) { + case COLORMATRIX_MATRIX: + ink_cairo_surface_filter(input, out, FilterColorMatrix::ColorMatrixMatrix(values)); + break; + case COLORMATRIX_SATURATE: + ink_cairo_surface_filter(input, out, ColorMatrixSaturate(value)); + break; + case COLORMATRIX_HUEROTATE: + ink_cairo_surface_filter(input, out, ColorMatrixHueRotate(value)); + break; + case COLORMATRIX_LUMINANCETOALPHA: + ink_cairo_surface_filter(input, out, ColorMatrixLuminanceToAlpha()); + break; + case COLORMATRIX_ENDTYPE: + default: + break; + } + + slot.set(_output, out); + cairo_surface_destroy(out); +} + +bool FilterColorMatrix::can_handle_affine(Geom::Affine const &) +{ + return true; +} + +double FilterColorMatrix::complexity(Geom::Affine const &) +{ + return 2.0; +} + +void FilterColorMatrix::set_type(FilterColorMatrixType t){ + type = t; +} + +void FilterColorMatrix::set_value(gdouble v){ + value = v; +} + +void FilterColorMatrix::set_values(std::vector<gdouble> const &v){ + values = v; +} + +} /* namespace Filters */ +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter-colormatrix.h b/src/display/nr-filter-colormatrix.h new file mode 100644 index 0000000..8e35a35 --- /dev/null +++ b/src/display/nr-filter-colormatrix.h @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_NR_FILTER_COLOR_MATRIX_H +#define SEEN_NR_FILTER_COLOR_MATRIX_H + +/* + * feColorMatrix filter primitive renderer + * + * Authors: + * Felipe CorrĂȘa da Silva Sanches <juca@members.fsf.org> + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <vector> +#include <2geom/forward.h> +#include "display/nr-filter-primitive.h" + +typedef unsigned int guint32; +typedef signed int gint32; + +namespace Inkscape { +namespace Filters { + +class FilterSlot; + +enum FilterColorMatrixType { + COLORMATRIX_MATRIX, + COLORMATRIX_SATURATE, + COLORMATRIX_HUEROTATE, + COLORMATRIX_LUMINANCETOALPHA, + COLORMATRIX_ENDTYPE +}; + +class FilterColorMatrix : public FilterPrimitive { +public: + FilterColorMatrix(); + static FilterPrimitive *create(); + ~FilterColorMatrix() override; + + void render_cairo(FilterSlot &slot) override; + bool can_handle_affine(Geom::Affine const &) override; + double complexity(Geom::Affine const &ctm) override; + + virtual void set_type(FilterColorMatrixType type); + virtual void set_value(double value); + virtual void set_values(std::vector<double> const &values); + + Glib::ustring name() override { return Glib::ustring("Color Matrix"); } + +public: + struct ColorMatrixMatrix { + ColorMatrixMatrix(std::vector<double> const &values); + guint32 operator()(guint32 in); + private: + gint32 _v[20]; + }; + +private: + std::vector<double> values; + double value; + FilterColorMatrixType type; +}; + +} /* namespace Filters */ +} /* namespace Inkscape */ + +#endif /* __NR_FILTER_COLOR_MATRIX_H__ */ +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter-component-transfer.cpp b/src/display/nr-filter-component-transfer.cpp new file mode 100644 index 0000000..4a5b458 --- /dev/null +++ b/src/display/nr-filter-component-transfer.cpp @@ -0,0 +1,246 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * feComponentTransfer filter primitive renderer + * + * Authors: + * Felipe CorrĂȘa da Silva Sanches <juca@members.fsf.org> + * Jasper van de Gronde <th.v.d.gronde@hccnet.nl> + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cmath> +#include "display/cairo-templates.h" +#include "display/cairo-utils.h" +#include "display/nr-filter-component-transfer.h" +#include "display/nr-filter-slot.h" + +namespace Inkscape { +namespace Filters { + +FilterComponentTransfer::FilterComponentTransfer() += default; + +FilterPrimitive * FilterComponentTransfer::create() { + return new FilterComponentTransfer(); +} + +FilterComponentTransfer::~FilterComponentTransfer() += default; + +struct UnmultiplyAlpha { + guint32 operator()(guint32 in) { + EXTRACT_ARGB32(in, a, r, g, b); + if (a == 0 ) + return in; + r = unpremul_alpha(r, a); + g = unpremul_alpha(g, a); + b = unpremul_alpha(b, a); + ASSEMBLE_ARGB32(out, a, r, g, b); + return out; + } +}; + +struct MultiplyAlpha { + guint32 operator()(guint32 in) { + EXTRACT_ARGB32(in, a, r, g, b); + if (a == 0 ) + return in; + r = premul_alpha(r, a); + g = premul_alpha(g, a); + b = premul_alpha(b, a); + ASSEMBLE_ARGB32(out, a, r, g, b); + return out; + } +}; + +struct ComponentTransfer { + ComponentTransfer(guint32 color) + : _shift(color * 8) + , _mask(0xff << _shift) + {} +protected: + guint32 _shift; + guint32 _mask; +}; + +struct ComponentTransferTable : public ComponentTransfer { + ComponentTransferTable(guint32 color, std::vector<double> const &values) + : ComponentTransfer(color) + , _v(values.size()) + { + for (unsigned i = 0; i< values.size(); ++i) { + _v[i] = round(CLAMP(values[i], 0.0, 1.0) * 255); + } + } + guint32 operator()(guint32 in) { + if (_v.empty()) { + return in; + } + + guint32 component = (in & _mask) >> _shift; + if (_v.size() == 1 || component == 255) { + component = _v.back(); + } else { + guint32 k = ((_v.size() - 1) * component); + guint32 dx = k % 255; + k /= 255; + component = _v[k]*255 + (_v[k+1] - _v[k])*dx; + component = (component + 127) / 255; + } + return (in & ~_mask) | (component << _shift); + } +private: + std::vector<guint32> _v; +}; + +struct ComponentTransferDiscrete : public ComponentTransfer { + ComponentTransferDiscrete(guint32 color, std::vector<double> const &values) + : ComponentTransfer(color) + , _v(values.size()) + { + for (unsigned i = 0; i< values.size(); ++i) { + _v[i] = round(CLAMP(values[i], 0.0, 1.0) * 255); + } + } + guint32 operator()(guint32 in) { + guint32 component = (in & _mask) >> _shift; + guint32 k = (_v.size()) * component / 255; + if( k == _v.size() ) --k; + component = _v[k]; + return (in & ~_mask) | ((guint32)component << _shift); + } +private: + std::vector<guint32> _v; +}; + +struct ComponentTransferLinear : public ComponentTransfer { + ComponentTransferLinear(guint32 color, double intercept, double slope) + : ComponentTransfer(color) + , _intercept(round(intercept*255*255)) + , _slope(round(slope*255)) + {} + guint32 operator()(guint32 in) { + gint32 component = (in & _mask) >> _shift; + + // TODO: this can probably be reduced to something simpler + component = pxclamp(_slope * component + _intercept, 0, 255*255); + component = (component + 127) / 255; + return (in & ~_mask) | (component << _shift); + } +private: + gint32 _intercept; + gint32 _slope; +}; + +struct ComponentTransferGamma : public ComponentTransfer { + ComponentTransferGamma(guint32 color, double amplitude, double exponent, double offset) + : ComponentTransfer(color) + , _amplitude(amplitude) + , _exponent(exponent) + , _offset(offset) + {} + guint32 operator()(guint32 in) { + double component = (in & _mask) >> _shift; + component /= 255.0; + component = _amplitude * pow(component, _exponent) + _offset; + guint32 cpx = pxclamp(component * 255.0, 0, 255); + return (in & ~_mask) | (cpx << _shift); + } +private: + double _amplitude; + double _exponent; + double _offset; +}; + +void FilterComponentTransfer::render_cairo(FilterSlot &slot) +{ + cairo_surface_t *input = slot.getcairo(_input); + cairo_surface_t *out = ink_cairo_surface_create_same_size(input, CAIRO_CONTENT_COLOR_ALPHA); + + // We may need to transform input surface to correct color interpolation space. The input surface + // might be used as input to another primitive but it is likely that all the primitives in a given + // filter use the same color interpolation space so we don't copy the input before converting. + SPColorInterpolation ci_fp = SP_CSS_COLOR_INTERPOLATION_AUTO; + if( _style ) { + ci_fp = (SPColorInterpolation)_style->color_interpolation_filters.computed; + set_cairo_surface_ci(out, ci_fp ); + } + set_cairo_surface_ci( input, ci_fp ); + + //cairo_surface_t *outtemp = ink_cairo_surface_create_identical(out); + ink_cairo_surface_blit(input, out); + + // We need to operate on unmultipled by alpha color values otherwise a change in alpha screws + // up the premultiplied by alpha r, g, b values. + ink_cairo_surface_filter(out, out, UnmultiplyAlpha()); + + // parameters: R = 0, G = 1, B = 2, A = 3 + // Cairo: R = 2, G = 1, B = 0, A = 3 + // If tableValues is empty, use identity. + for (unsigned i = 0; i < 4; ++i) { + + guint32 color = 2 - i; + if(i==3) color = 3; // alpha + + switch (type[i]) { + case COMPONENTTRANSFER_TYPE_TABLE: + if(!tableValues[i].empty()) { + ink_cairo_surface_filter(out, out, + ComponentTransferTable(color, tableValues[i])); + } + break; + case COMPONENTTRANSFER_TYPE_DISCRETE: + if(!tableValues[i].empty()) { + ink_cairo_surface_filter(out, out, + ComponentTransferDiscrete(color, tableValues[i])); + } + break; + case COMPONENTTRANSFER_TYPE_LINEAR: + ink_cairo_surface_filter(out, out, + ComponentTransferLinear(color, intercept[i], slope[i])); + break; + case COMPONENTTRANSFER_TYPE_GAMMA: + ink_cairo_surface_filter(out, out, + ComponentTransferGamma(color, amplitude[i], exponent[i], offset[i])); + break; + case COMPONENTTRANSFER_TYPE_ERROR: + case COMPONENTTRANSFER_TYPE_IDENTITY: + default: + break; + } + //ink_cairo_surface_blit(out, outtemp); + } + + ink_cairo_surface_filter(out, out, MultiplyAlpha()); + + slot.set(_output, out); + cairo_surface_destroy(out); + //cairo_surface_destroy(outtemp); +} + +bool FilterComponentTransfer::can_handle_affine(Geom::Affine const &) +{ + return true; +} + +double FilterComponentTransfer::complexity(Geom::Affine const &) +{ + return 2.0; +} + +} /* namespace Filters */ +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter-component-transfer.h b/src/display/nr-filter-component-transfer.h new file mode 100644 index 0000000..9d1ea14 --- /dev/null +++ b/src/display/nr-filter-component-transfer.h @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_NR_FILTER_COMPONENT_TRANSFER_H +#define SEEN_NR_FILTER_COMPONENT_TRANSFER_H + +/* + * feComponentTransfer filter primitive renderer + * + * Authors: + * Felipe CorrĂȘa da Silva Sanches <juca@members.fsf.org> + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <vector> +#include "display/nr-filter-primitive.h" + +namespace Inkscape { +namespace Filters { + +class FilterSlot; + +enum FilterComponentTransferType { + COMPONENTTRANSFER_TYPE_IDENTITY, + COMPONENTTRANSFER_TYPE_TABLE, + COMPONENTTRANSFER_TYPE_DISCRETE, + COMPONENTTRANSFER_TYPE_LINEAR, + COMPONENTTRANSFER_TYPE_GAMMA, + COMPONENTTRANSFER_TYPE_ERROR +}; + +class FilterComponentTransfer : public FilterPrimitive { +public: + FilterComponentTransfer(); + static FilterPrimitive *create(); + ~FilterComponentTransfer() override; + + void render_cairo(FilterSlot &slot) override; + bool can_handle_affine(Geom::Affine const &) override; + double complexity(Geom::Affine const &ctm) override; + + FilterComponentTransferType type[4]; + std::vector<double> tableValues[4]; + double slope[4]; + double intercept[4]; + double amplitude[4]; + double exponent[4]; + double offset[4]; + + Glib::ustring name() override { return Glib::ustring("Component Transfer"); } +}; + +} /* namespace Filters */ +} /* namespace Inkscape */ + +#endif /* __NR_FILTER_COMPONENT_TRANSFER_H__ */ +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter-composite.cpp b/src/display/nr-filter-composite.cpp new file mode 100644 index 0000000..4bb5685 --- /dev/null +++ b/src/display/nr-filter-composite.cpp @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * feComposite filter effect renderer + * + * Authors: + * Niko Kiirala <niko@kiirala.com> + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cmath> + +#include "display/cairo-templates.h" +#include "display/cairo-utils.h" +#include "display/nr-filter-composite.h" +#include "display/nr-filter-slot.h" +#include "display/nr-filter-units.h" + +namespace Inkscape { +namespace Filters { + +FilterComposite::FilterComposite() : + op(COMPOSITE_DEFAULT), k1(0), k2(0), k3(0), k4(0), + _input2(Inkscape::Filters::NR_FILTER_SLOT_NOT_SET) +{} + +FilterPrimitive * FilterComposite::create() { + return new FilterComposite(); +} + +FilterComposite::~FilterComposite() += default; + +struct ComposeArithmetic { + ComposeArithmetic(double k1, double k2, double k3, double k4) + : _k1(round(k1 * 255)) + , _k2(round(k2 * 255*255)) + , _k3(round(k3 * 255*255)) + , _k4(round(k4 * 255*255*255)) + {} + guint32 operator()(guint32 in1, guint32 in2) { + EXTRACT_ARGB32(in1, aa, ra, ga, ba) + EXTRACT_ARGB32(in2, ab, rb, gb, bb) + + gint32 ao = _k1*aa*ab + _k2*aa + _k3*ab + _k4; + gint32 ro = _k1*ra*rb + _k2*ra + _k3*rb + _k4; + gint32 go = _k1*ga*gb + _k2*ga + _k3*gb + _k4; + gint32 bo = _k1*ba*bb + _k2*ba + _k3*bb + _k4; + + ao = pxclamp(ao, 0, 255*255*255); // r, g and b are premultiplied, so should be clamped to the alpha channel + ro = (pxclamp(ro, 0, ao) + (255*255/2)) / (255*255); + go = (pxclamp(go, 0, ao) + (255*255/2)) / (255*255); + bo = (pxclamp(bo, 0, ao) + (255*255/2)) / (255*255); + ao = (ao + (255*255/2)) / (255*255); + + ASSEMBLE_ARGB32(pxout, ao, ro, go, bo) + return pxout; + } +private: + gint32 _k1, _k2, _k3, _k4; +}; + +void FilterComposite::render_cairo(FilterSlot &slot) +{ + cairo_surface_t *input1 = slot.getcairo(_input); + cairo_surface_t *input2 = slot.getcairo(_input2); + + // We may need to transform input surface to correct color interpolation space. The input surface + // might be used as input to another primitive but it is likely that all the primitives in a given + // filter use the same color interpolation space so we don't copy the input before converting. + SPColorInterpolation ci_fp = SP_CSS_COLOR_INTERPOLATION_AUTO; + if( _style ) { + ci_fp = (SPColorInterpolation)_style->color_interpolation_filters.computed; + } + set_cairo_surface_ci( input1, ci_fp ); + set_cairo_surface_ci( input2, ci_fp ); + + cairo_surface_t *out = ink_cairo_surface_create_output(input1, input2); + set_cairo_surface_ci(out, ci_fp ); + + Geom::Rect vp = filter_primitive_area( slot.get_units() ); + slot.set_primitive_area(_output, vp); // Needed for tiling + + if (op == COMPOSITE_ARITHMETIC) { + ink_cairo_surface_blend(input1, input2, out, ComposeArithmetic(k1, k2, k3, k4)); + } else { + ink_cairo_surface_blit(input2, out); + cairo_t *ct = cairo_create(out); + cairo_set_source_surface(ct, input1, 0, 0); + switch(op) { + case COMPOSITE_IN: + cairo_set_operator(ct, CAIRO_OPERATOR_IN); + break; + case COMPOSITE_OUT: + cairo_set_operator(ct, CAIRO_OPERATOR_OUT); + break; + case COMPOSITE_ATOP: + cairo_set_operator(ct, CAIRO_OPERATOR_ATOP); + break; + case COMPOSITE_XOR: + cairo_set_operator(ct, CAIRO_OPERATOR_XOR); + break; +#ifdef WITH_CSSCOMPOSITE + /* New CSS Operators */ + case COMPOSITE_CLEAR: + cairo_set_operator(ct, CAIRO_OPERATOR_CLEAR); + break; + case COMPOSITE_COPY: + cairo_set_operator(ct, CAIRO_OPERATOR_SOURCE); + break; + case COMPOSITE_DESTINATION: + cairo_set_operator(ct, CAIRO_OPERATOR_DEST); + break; + case COMPOSITE_DESTINATION_OVER: + cairo_set_operator(ct, CAIRO_OPERATOR_DEST_OVER); + break; + case COMPOSITE_DESTINATION_IN: + cairo_set_operator(ct, CAIRO_OPERATOR_DEST_IN); + break; + case COMPOSITE_DESTINATION_OUT: + cairo_set_operator(ct, CAIRO_OPERATOR_DEST_OUT); + break; + case COMPOSITE_DESTINATION_ATOP: + cairo_set_operator(ct, CAIRO_OPERATOR_DEST_ATOP); + break; + case COMPOSITE_LIGHTER: + cairo_set_operator(ct, CAIRO_OPERATOR_ADD); + break; +#endif + case COMPOSITE_OVER: + case COMPOSITE_DEFAULT: + default: + // OVER is the default operator + break; + } + cairo_paint(ct); + cairo_destroy(ct); + } + + slot.set(_output, out); + cairo_surface_destroy(out); +} + +bool FilterComposite::can_handle_affine(Geom::Affine const &) +{ + return true; +} + +void FilterComposite::set_input(int input) { + _input = input; +} + +void FilterComposite::set_input(int input, int slot) { + if (input == 0) _input = slot; + if (input == 1) _input2 = slot; +} + +void FilterComposite::set_operator(FeCompositeOperator op) { + if (op == COMPOSITE_DEFAULT) { + this->op = COMPOSITE_OVER; + } else if (op != COMPOSITE_ENDOPERATOR) { + this->op = op; + } +} + +void FilterComposite::set_arithmetic(double k1, double k2, double k3, double k4) { + if (!std::isfinite(k1) || !std::isfinite(k2) || !std::isfinite(k3) || !std::isfinite(k4)) { + g_warning("Non-finite parameter for feComposite arithmetic operator"); + return; + } + this->k1 = k1; + this->k2 = k2; + this->k3 = k3; + this->k4 = k4; +} + +double FilterComposite::complexity(Geom::Affine const &) +{ + return 1.1; +} + +} /* namespace Filters */ +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter-composite.h b/src/display/nr-filter-composite.h new file mode 100644 index 0000000..a76f4cc --- /dev/null +++ b/src/display/nr-filter-composite.h @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_NR_FILTER_COMPOSITE_H +#define SEEN_NR_FILTER_COMPOSITE_H + +/* + * feComposite filter effect renderer + * + * Authors: + * Niko Kiirala <niko@kiirala.com> + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "object/filters/composite.h" +#include "display/nr-filter-primitive.h" +#include "display/nr-filter-slot.h" +#include "display/nr-filter-units.h" + +namespace Inkscape { +namespace Filters { + +class FilterComposite : public FilterPrimitive { +public: + FilterComposite(); + static FilterPrimitive *create(); + ~FilterComposite() override; + + void render_cairo(FilterSlot &) override; + bool can_handle_affine(Geom::Affine const &) override; + double complexity(Geom::Affine const &ctm) override; + + void set_input(int input) override; + void set_input(int input, int slot) override; + + void set_operator(FeCompositeOperator op); + void set_arithmetic(double k1, double k2, double k3, double k4); + + Glib::ustring name() override { return Glib::ustring("Composite"); } + +private: + FeCompositeOperator op; + double k1, k2, k3, k4; + int _input2; +}; + +} /* namespace Filters */ +} /* namespace Inkscape */ + +#endif /* __NR_FILTER_COMPOSITE_H__ */ +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter-convolve-matrix.cpp b/src/display/nr-filter-convolve-matrix.cpp new file mode 100644 index 0000000..1e9bdb0 --- /dev/null +++ b/src/display/nr-filter-convolve-matrix.cpp @@ -0,0 +1,243 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * feConvolveMatrix filter primitive renderer + * + * Authors: + * Felipe CorrĂȘa da Silva Sanches <juca@members.fsf.org> + * Jasper van de Gronde <th.v.d.gronde@hccnet.nl> + * + * Copyright (C) 2007,2009 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <vector> +#include "display/cairo-templates.h" +#include "display/cairo-utils.h" +#include "display/nr-filter-convolve-matrix.h" +#include "display/nr-filter-slot.h" +#include "display/nr-filter-units.h" +#include "display/nr-filter-utils.h" + +namespace Inkscape { +namespace Filters { + +FilterConvolveMatrix::FilterConvolveMatrix() += default; + +FilterPrimitive * FilterConvolveMatrix::create() { + return new FilterConvolveMatrix(); +} + +FilterConvolveMatrix::~FilterConvolveMatrix() += default; + +enum PreserveAlphaMode { + PRESERVE_ALPHA, + NO_PRESERVE_ALPHA +}; + +template <PreserveAlphaMode preserve_alpha> +struct ConvolveMatrix : public SurfaceSynth { + ConvolveMatrix(cairo_surface_t *s, int targetX, int targetY, int orderX, int orderY, + double divisor, double bias, std::vector<double> const &kernel) + : SurfaceSynth(s) + , _kernel(kernel.size()) + , _targetX(targetX) + , _targetY(targetY) + , _orderX(orderX) + , _orderY(orderY) + , _bias(bias) + { + for (unsigned i = 0; i < _kernel.size(); ++i) { + _kernel[i] = kernel[i] / divisor; + } + // the matrix is given rotated 180 degrees + // which corresponds to reverse element order + std::reverse(_kernel.begin(), _kernel.end()); + } + + guint32 operator()(int x, int y) const { + int startx = std::max(0, x - _targetX); + int starty = std::max(0, y - _targetY); + int endx = std::min(_w, startx + _orderX); + int endy = std::min(_h, starty + _orderY); + int limitx = endx - startx; + int limity = endy - starty; + double suma = 0.0, sumr = 0.0, sumg = 0.0, sumb = 0.0; + + for (int i = 0; i < limity; ++i) { + for (int j = 0; j < limitx; ++j) { + guint32 px = pixelAt(startx + j, starty + i); + double coeff = _kernel[i * _orderX + j]; + EXTRACT_ARGB32(px, a,r,g,b) + + sumr += r * coeff; + sumg += g * coeff; + sumb += b * coeff; + if (preserve_alpha == NO_PRESERVE_ALPHA) { + suma += a * coeff; + } + } + } + if (preserve_alpha == PRESERVE_ALPHA) { + suma = alphaAt(x, y); + } else { + suma += _bias * 255; + } + + guint32 ao = pxclamp(round(suma), 0, 255); + guint32 ro = pxclamp(round(sumr + ao * _bias), 0, ao); + guint32 go = pxclamp(round(sumg + ao * _bias), 0, ao); + guint32 bo = pxclamp(round(sumb + ao * _bias), 0, ao); + ASSEMBLE_ARGB32(pxout, ao,ro,go,bo); + return pxout; + } + +private: + std::vector<double> _kernel; + int _targetX, _targetY, _orderX, _orderY; + double _bias; +}; + +void FilterConvolveMatrix::render_cairo(FilterSlot &slot) +{ + static bool bias_warning = false; + static bool edge_warning = false; + + if (orderX<=0 || orderY<=0) { + g_warning("Empty kernel!"); + return; + } + if (targetX<0 || targetX>=orderX || targetY<0 || targetY>=orderY) { + g_warning("Invalid target!"); + return; + } + if (kernelMatrix.size()!=(unsigned int)(orderX*orderY)) { + //g_warning("kernelMatrix does not have orderX*orderY elements!"); + return; + } + + cairo_surface_t *input = slot.getcairo(_input); + cairo_surface_t *out = ink_cairo_surface_create_identical(input); + + // We may need to transform input surface to correct color interpolation space. The input surface + // might be used as input to another primitive but it is likely that all the primitives in a given + // filter use the same color interpolation space so we don't copy the input before converting. + SPColorInterpolation ci_fp = SP_CSS_COLOR_INTERPOLATION_AUTO; + if( _style ) { + ci_fp = (SPColorInterpolation)_style->color_interpolation_filters.computed; + set_cairo_surface_ci(out, ci_fp); + } + set_cairo_surface_ci( input, ci_fp ); + + if (bias!=0 && !bias_warning) { + g_warning("It is unknown whether Inkscape's implementation of bias in feConvolveMatrix " + "is correct!"); + bias_warning = true; + // The SVG specification implies that feConvolveMatrix is defined for premultiplied + // colors (which makes sense). It also says that bias should simply be added to the result + // for each color (without taking the alpha into account). However, it also says that one + // purpose of bias is "to have .5 gray value be the zero response of the filter". + // It seems sensible to indeed support the latter behaviour instead of the former, + // but this does appear to go against the standard. + // Note that Batik simply does not support bias!=0 + } + if (edgeMode!=CONVOLVEMATRIX_EDGEMODE_NONE && !edge_warning) { + g_warning("Inkscape only supports edgeMode=\"none\" (and a filter uses a different one)!"); + edge_warning = true; + } + + //guint32 *in_data = reinterpret_cast<guint32*>(cairo_image_surface_get_data(input)); + //guint32 *out_data = reinterpret_cast<guint32*>(cairo_image_surface_get_data(out)); + + //int width = cairo_image_surface_get_width(input); + //int height = cairo_image_surface_get_height(input); + + // Set up predivided kernel matrix + /*std::vector<double> kernel(kernelMatrix); + for(size_t i=0; i<kernel.size(); i++) { + kernel[i] /= divisor; // The code that creates this object makes sure that divisor != 0 + }*/ + + if (preserveAlpha) { + //convolve2D<true>(out_data, in_data, width, height, &kernel.front(), orderX, orderY, + // targetX, targetY, bias); + ink_cairo_surface_synthesize(out, ConvolveMatrix<PRESERVE_ALPHA>(input, + targetX, targetY, orderX, orderY, divisor, bias, kernelMatrix)); + } else { + //convolve2D<false>(out_data, in_data, width, height, &kernel.front(), orderX, orderY, + // targetX, targetY, bias); + ink_cairo_surface_synthesize(out, ConvolveMatrix<NO_PRESERVE_ALPHA>(input, + targetX, targetY, orderX, orderY, divisor, bias, kernelMatrix)); + } + + slot.set(_output, out); + cairo_surface_destroy(out); +} + +void FilterConvolveMatrix::set_targetX(int coord) { + targetX = coord; +} + +void FilterConvolveMatrix::set_targetY(int coord) { + targetY = coord; +} + +void FilterConvolveMatrix::set_orderX(int coord) { + orderX = coord; +} + +void FilterConvolveMatrix::set_orderY(int coord) { + orderY = coord; +} + +void FilterConvolveMatrix::set_divisor(double d) { + divisor = d; +} + +void FilterConvolveMatrix::set_bias(double b) { + bias = b; +} + +void FilterConvolveMatrix::set_kernelMatrix(std::vector<gdouble> &km) { + kernelMatrix = km; +} + +void FilterConvolveMatrix::set_edgeMode(FilterConvolveMatrixEdgeMode mode){ + edgeMode = mode; +} + +void FilterConvolveMatrix::set_preserveAlpha(bool pa){ + preserveAlpha = pa; +} + +void FilterConvolveMatrix::area_enlarge(Geom::IntRect &area, Geom::Affine const &/*trans*/) +{ + //Seems to me that since this filter's operation is resolution dependent, + // some spurious pixels may still appear at the borders when low zooming or rotating. Needs a better fix. + area.setMin(area.min() - Geom::IntPoint(targetX, targetY)); + // This makes sure the last row/column in the original image corresponds + // to the last row/column in the new image that can be convolved without + // adjusting the boundary conditions). + area.setMax(area.max() + Geom::IntPoint(orderX - targetX - 1, orderY - targetY -1)); +} + +double FilterConvolveMatrix::complexity(Geom::Affine const &) +{ + return kernelMatrix.size(); +} + +} /* namespace Filters */ +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter-convolve-matrix.h b/src/display/nr-filter-convolve-matrix.h new file mode 100644 index 0000000..26b2bf3 --- /dev/null +++ b/src/display/nr-filter-convolve-matrix.h @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_NR_FILTER_CONVOLVE_MATRIX_H +#define SEEN_NR_FILTER_CONVOLVE_MATRIX_H + +/* + * feConvolveMatrix filter primitive renderer + * + * Authors: + * Felipe CorrĂȘa da Silva Sanches <juca@members.fsf.org> + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "display/nr-filter-primitive.h" +#include <vector> + +namespace Inkscape { +namespace Filters { + +class FilterSlot; + +enum FilterConvolveMatrixEdgeMode { + CONVOLVEMATRIX_EDGEMODE_DUPLICATE, + CONVOLVEMATRIX_EDGEMODE_WRAP, + CONVOLVEMATRIX_EDGEMODE_NONE, + CONVOLVEMATRIX_EDGEMODE_ENDTYPE +}; + +class FilterConvolveMatrix : public FilterPrimitive { +public: + FilterConvolveMatrix(); + static FilterPrimitive *create(); + ~FilterConvolveMatrix() override; + + void render_cairo(FilterSlot &slot) override; + void area_enlarge(Geom::IntRect &area, Geom::Affine const &trans) override; + double complexity(Geom::Affine const &ctm) override; + + void set_targetY(int coord); + void set_targetX(int coord); + void set_orderY(int coord); + void set_orderX(int coord); + void set_kernelMatrix(std::vector<gdouble>& km); + void set_bias(double b); + void set_divisor(double d); + void set_edgeMode(FilterConvolveMatrixEdgeMode mode); + void set_preserveAlpha(bool pa); + + Glib::ustring name() override { return Glib::ustring("Convolve Matrix"); } + +private: + std::vector<double> kernelMatrix; + int targetX, targetY; + int orderX, orderY; + double divisor, bias; + int dx, dy, kernelUnitLength; + FilterConvolveMatrixEdgeMode edgeMode; + bool preserveAlpha; +}; + +} /* namespace Filters */ +} /* namespace Inkscape */ + +#endif /* __NR_FILTER_CONVOLVE_MATRIX_H__ */ +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter-diffuselighting.cpp b/src/display/nr-filter-diffuselighting.cpp new file mode 100644 index 0000000..d33d513 --- /dev/null +++ b/src/display/nr-filter-diffuselighting.cpp @@ -0,0 +1,239 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * feDiffuseLighting renderer + * + * Authors: + * Niko Kiirala <niko@kiirala.com> + * Jean-Rene Reinhard <jr@komite.net> + * Krzysztof KosiĆski <tweenk.pl@gmail.com> + * + * Copyright (C) 2007-2010 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include <glib.h> + +#include "display/cairo-templates.h" +#include "display/cairo-utils.h" +#include "display/nr-3dutils.h" +#include "display/nr-filter-diffuselighting.h" +#include "display/nr-filter-slot.h" +#include "display/nr-filter-units.h" +#include "display/nr-filter-utils.h" +#include "display/nr-light.h" +#include "svg/svg-icc-color.h" +#include "svg/svg-color.h" + +namespace Inkscape { +namespace Filters { + +FilterDiffuseLighting::FilterDiffuseLighting() +{ + light_type = NO_LIGHT; + diffuseConstant = 1; + surfaceScale = 1; + lighting_color = 0xffffffff; +} + +FilterPrimitive * FilterDiffuseLighting::create() { + return new FilterDiffuseLighting(); +} + +FilterDiffuseLighting::~FilterDiffuseLighting() += default; + +struct DiffuseLight : public SurfaceSynth { + DiffuseLight(cairo_surface_t *bumpmap, double scale, double kd) + : SurfaceSynth(bumpmap) + , _scale(scale) + , _kd(kd) + {} + +protected: + guint32 diffuseLighting(int x, int y, NR::Fvector const &light, NR::Fvector const &light_components) { + NR::Fvector normal = surfaceNormalAt(x, y, _scale); + double k = _kd * NR::scalar_product(normal, light); + + guint32 r = CLAMP_D_TO_U8(k * light_components[LIGHT_RED]); + guint32 g = CLAMP_D_TO_U8(k * light_components[LIGHT_GREEN]); + guint32 b = CLAMP_D_TO_U8(k * light_components[LIGHT_BLUE]); + + ASSEMBLE_ARGB32(pxout, 255,r,g,b) + return pxout; + } + double _scale, _kd; +}; + +struct DiffuseDistantLight : public DiffuseLight { + DiffuseDistantLight(cairo_surface_t *bumpmap, SPFeDistantLight *light, guint32 color, + double scale, double diffuse_constant) + : DiffuseLight(bumpmap, scale, diffuse_constant) + { + DistantLight dl(light, color); + dl.light_vector(_lightv); + dl.light_components(_light_components); + } + + guint32 operator()(int x, int y) { + return diffuseLighting(x, y, _lightv, _light_components); + } +private: + NR::Fvector _lightv, _light_components; +}; + +struct DiffusePointLight : public DiffuseLight { + DiffusePointLight(cairo_surface_t *bumpmap, SPFePointLight *light, guint32 color, + Geom::Affine const &trans, double scale, double diffuse_constant, + double x0, double y0, int device_scale) + : DiffuseLight(bumpmap, scale, diffuse_constant) + , _light(light, color, trans, device_scale) + , _x0(x0) + , _y0(y0) + { + _light.light_components(_light_components); + } + + guint32 operator()(int x, int y) { + NR::Fvector light; + _light.light_vector(light, _x0 + x, _y0 + y, _scale * alphaAt(x, y)/255.0); + return diffuseLighting(x, y, light, _light_components); + } +private: + PointLight _light; + NR::Fvector _light_components; + double _x0, _y0; +}; + +struct DiffuseSpotLight : public DiffuseLight { + DiffuseSpotLight(cairo_surface_t *bumpmap, SPFeSpotLight *light, guint32 color, + Geom::Affine const &trans, double scale, double diffuse_constant, + double x0, double y0, int device_scale) + : DiffuseLight(bumpmap, scale, diffuse_constant) + , _light(light, color, trans, device_scale) + , _x0(x0) + , _y0(y0) + {} + + guint32 operator()(int x, int y) { + NR::Fvector light, light_components; + _light.light_vector(light, _x0 + x, _y0 + y, _scale * alphaAt(x, y)/255.0); + _light.light_components(light_components, light); + return diffuseLighting(x, y, light, light_components); + } +private: + SpotLight _light; + double _x0, _y0; +}; + +void FilterDiffuseLighting::render_cairo(FilterSlot &slot) +{ + cairo_surface_t *input = slot.getcairo(_input); + cairo_surface_t *out = ink_cairo_surface_create_same_size(input, CAIRO_CONTENT_COLOR_ALPHA); + + double r = SP_RGBA32_R_F(lighting_color); + double g = SP_RGBA32_G_F(lighting_color); + double b = SP_RGBA32_B_F(lighting_color); + +#if defined(HAVE_LIBLCMS2) + + if (icc) { + guchar ru, gu, bu; + icc_color_to_sRGB(icc, &ru, &gu, &bu); + r = SP_COLOR_U_TO_F(ru); + g = SP_COLOR_U_TO_F(gu); + b = SP_COLOR_U_TO_F(bu); + } +#endif + + // Only alpha channel of input is used, no need to check input color_interpolation_filter value. + SPColorInterpolation ci_fp = SP_CSS_COLOR_INTERPOLATION_AUTO; + if( _style ) { + ci_fp = (SPColorInterpolation)_style->color_interpolation_filters.computed; + + // Lighting color is always defined in terms of sRGB, preconvert to linearRGB + // if color_interpolation_filters set to linearRGB (for efficiency assuming + // next filter primitive has same value of cif). + if( ci_fp == SP_CSS_COLOR_INTERPOLATION_LINEARRGB ) { + r = srgb_to_linear( r ); + g = srgb_to_linear( g ); + b = srgb_to_linear( b ); + } + } + set_cairo_surface_ci(out, ci_fp ); + guint32 color = SP_RGBA32_F_COMPOSE( r, g, b, 1.0 ); + + int device_scale = slot.get_device_scale(); + + Geom::Rect slot_area = slot.get_slot_area(); + Geom::Point p = slot_area.min(); + + // trans has inverse y... so we can't just scale by device_scale! We must instead explicitly + // scale the point and spot light coordinates (as well as "scale"). + + Geom::Affine trans = slot.get_units().get_matrix_primitiveunits2pb(); + + double x0 = p[Geom::X], y0 = p[Geom::Y]; + double scale = surfaceScale * trans.descrim() * device_scale; + + switch (light_type) { + case DISTANT_LIGHT: + ink_cairo_surface_synthesize(out, + DiffuseDistantLight(input, light.distant, color, scale, diffuseConstant)); + break; + case POINT_LIGHT: + ink_cairo_surface_synthesize(out, + DiffusePointLight(input, light.point, color, trans, scale, diffuseConstant, x0, y0, device_scale)); + break; + case SPOT_LIGHT: + ink_cairo_surface_synthesize(out, + DiffuseSpotLight(input, light.spot, color, trans, scale, diffuseConstant, x0, y0, device_scale)); + break; + default: { + cairo_t *ct = cairo_create(out); + cairo_set_source_rgba(ct, 0,0,0,1); + cairo_set_operator(ct, CAIRO_OPERATOR_SOURCE); + cairo_paint(ct); + cairo_destroy(ct); + } break; + } + + slot.set(_output, out); + cairo_surface_destroy(out); +} + +void FilterDiffuseLighting::set_icc(SVGICCColor *icc_color) { + icc = icc_color; +} + +void FilterDiffuseLighting::area_enlarge(Geom::IntRect &area, Geom::Affine const & /*trans*/) +{ + // TODO: support kernelUnitLength + + // We expand the area by 1 in every direction to avoid artifacts on tile edges. + // However, it means that edge pixels will be incorrect. + area.expandBy(1); +} + +double FilterDiffuseLighting::complexity(Geom::Affine const &) +{ + return 9.0; +} + +} /* namespace Filters */ +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter-diffuselighting.h b/src/display/nr-filter-diffuselighting.h new file mode 100644 index 0000000..5bf777b --- /dev/null +++ b/src/display/nr-filter-diffuselighting.h @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_NR_FILTER_DIFFUSELIGHTING_H +#define SEEN_NR_FILTER_DIFFUSELIGHTING_H + +/* + * feDiffuseLighting renderer + * + * Authors: + * Niko Kiirala <niko@kiirala.com> + * Jean-Rene Reinhard <jr@komite.net> + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "display/nr-light-types.h" +#include "display/nr-filter-primitive.h" +#include "display/nr-filter-slot.h" +#include "display/nr-filter-units.h" + +class SPFeDistantLight; +class SPFePointLight; +class SPFeSpotLight; +struct SVGICCColor; +typedef unsigned int guint32; + +namespace Inkscape { +namespace Filters { + +class FilterDiffuseLighting : public FilterPrimitive { +public: + FilterDiffuseLighting(); + static FilterPrimitive *create(); + ~FilterDiffuseLighting() override; + void render_cairo(FilterSlot &slot) override; + virtual void set_icc(SVGICCColor *icc_color); + void area_enlarge(Geom::IntRect &area, Geom::Affine const &trans) override; + double complexity(Geom::Affine const &ctm) override; + + union { + SPFeDistantLight *distant; + SPFePointLight *point; + SPFeSpotLight *spot; + } light; + LightType light_type; + double diffuseConstant; + double surfaceScale; + guint32 lighting_color; + + Glib::ustring name() override { return Glib::ustring("Diffuse Lighting"); } + +private: + SVGICCColor *icc; +}; + +} /* namespace Filters */ +} /* namespace Inkscape */ + +#endif /* SEEN_NR_FILTER_DIFFUSELIGHTING_H */ +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter-displacement-map.cpp b/src/display/nr-filter-displacement-map.cpp new file mode 100644 index 0000000..9d83e54 --- /dev/null +++ b/src/display/nr-filter-displacement-map.cpp @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * feDisplacementMap filter primitive renderer + * + * Authors: + * Felipe CorrĂȘa da Silva Sanches <juca@members.fsf.org> + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "display/cairo-templates.h" +#include "display/cairo-utils.h" +#include "display/nr-filter-displacement-map.h" +#include "display/nr-filter-types.h" +#include "display/nr-filter-units.h" + +namespace Inkscape { +namespace Filters { + +FilterDisplacementMap::FilterDisplacementMap() += default; + +FilterPrimitive * FilterDisplacementMap::create() { + return new FilterDisplacementMap(); +} + +FilterDisplacementMap::~FilterDisplacementMap() += default; + +struct Displace { + Displace(cairo_surface_t *texture, cairo_surface_t *map, + unsigned xch, unsigned ych, double scalex, double scaley) + : _texture(texture) + , _map(map) + , _xch(xch) + , _ych(ych) + , _scalex(scalex/255.0) + , _scaley(scaley/255.0) + {} + guint32 operator()(int x, int y) { + guint32 mappx = _map.pixelAt(x, y); + guint32 a = (mappx & 0xff000000) >> 24; + guint32 xpx = 0, ypx = 0; + double xtex = x, ytex = y; + + guint32 xshift = _xch * 8, yshift = _ych * 8; + xpx = (mappx & (0xff << xshift)) >> xshift; + ypx = (mappx & (0xff << yshift)) >> yshift; + if (a) { + if (_xch != 3) xpx = unpremul_alpha(xpx, a); + if (_ych != 3) ypx = unpremul_alpha(ypx, a); + } + xtex += _scalex * (xpx - 127.5); + ytex += _scaley * (ypx - 127.5); + + if (xtex >= 0 && xtex < (_texture._w - 1) && + ytex >= 0 && ytex < (_texture._h - 1)) + { + return _texture.pixelAt(xtex, ytex); + } else { + return 0; + } + } +private: + SurfaceSynth _texture; + SurfaceSynth _map; + unsigned _xch, _ych; + double _scalex, _scaley; +}; + +void FilterDisplacementMap::render_cairo(FilterSlot &slot) +{ + cairo_surface_t *texture = slot.getcairo(_input); + cairo_surface_t *map = slot.getcairo(_input2); + cairo_surface_t *out = ink_cairo_surface_create_identical(texture); + // color_interpolation_filters for out same as texture. See spec. + copy_cairo_surface_ci( texture, out ); + + // We may need to transform map surface to correct color interpolation space. The map surface + // might be used as input to another primitive but it is likely that all the primitives in a given + // filter use the same color interpolation space so we don't copy the map before converting. + SPColorInterpolation ci_fp = SP_CSS_COLOR_INTERPOLATION_AUTO; + if( _style ) { + ci_fp = (SPColorInterpolation)_style->color_interpolation_filters.computed; + } + set_cairo_surface_ci( map, ci_fp ); + + Geom::Affine trans = slot.get_units().get_matrix_primitiveunits2pb(); + + int device_scale = slot.get_device_scale(); + double scalex = scale * trans.expansionX() * device_scale; + double scaley = scale * trans.expansionY() * device_scale; + + ink_cairo_surface_synthesize(out, Displace(texture, map, Xchannel, Ychannel, scalex, scaley)); + + slot.set(_output, out); + cairo_surface_destroy(out); +} + +void FilterDisplacementMap::set_input(int slot) { + _input = slot; +} + +void FilterDisplacementMap::set_scale(double s) { + scale = s; +} + +void FilterDisplacementMap::set_input(int input, int slot) { + if (input == 0) _input = slot; + if (input == 1) _input2 = slot; +} + +void FilterDisplacementMap::set_channel_selector(int s, FilterDisplacementMapChannelSelector channel) { + if (channel > DISPLACEMENTMAP_CHANNEL_ALPHA || channel < DISPLACEMENTMAP_CHANNEL_RED) { + g_warning("Selected an invalid channel value. (%d)", channel); + return; + } + + // channel numbering: + // a = 3, r = 2, g = 1, b = 0 + // this way we can get the component value using: + // component = (color & (ch*8)) >> (ch*8) + unsigned ch = 4; + switch (channel) { + case DISPLACEMENTMAP_CHANNEL_ALPHA: + ch = 3; break; + case DISPLACEMENTMAP_CHANNEL_RED: + ch = 2; break; + case DISPLACEMENTMAP_CHANNEL_GREEN: + ch = 1; break; + case DISPLACEMENTMAP_CHANNEL_BLUE: + ch = 0; break; + default: break; + } + if (ch == 4) return; + + if (s == 0) Xchannel = ch; + if (s == 1) Ychannel = ch; +} + +void FilterDisplacementMap::area_enlarge(Geom::IntRect &area, Geom::Affine const &trans) +{ + //I assume scale is in user coordinates (?!?) + //FIXME: trans should be multiplied by some primitiveunits2user, shouldn't it? + + double scalex = scale/2.*(std::fabs(trans[0])+std::fabs(trans[1])); + double scaley = scale/2.*(std::fabs(trans[2])+std::fabs(trans[3])); + + //FIXME: no +2 should be there!... (noticeable only for big scales at big zoom factor) + area.expandBy(scalex+2, scaley+2); +} + +double FilterDisplacementMap::complexity(Geom::Affine const &) +{ + return 3.0; +} + +} /* namespace Filters */ +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter-displacement-map.h b/src/display/nr-filter-displacement-map.h new file mode 100644 index 0000000..32afcfb --- /dev/null +++ b/src/display/nr-filter-displacement-map.h @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_NR_FILTER_DISPLACEMENT_MAP_H +#define SEEN_NR_FILTER_DISPLACEMENT_MAP_H + +/* + * feDisplacementMap filter primitive renderer + * + * Authors: + * Felipe CorrĂȘa da Silva Sanches <juca@members.fsf.org> + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "object/filters/displacementmap.h" +#include "display/nr-filter-primitive.h" +#include "display/nr-filter-slot.h" +#include "display/nr-filter-units.h" + +namespace Inkscape { +namespace Filters { + +class FilterDisplacementMap : public FilterPrimitive { +public: + FilterDisplacementMap(); + static FilterPrimitive *create(); + ~FilterDisplacementMap() override; + + void render_cairo(FilterSlot &slot) override; + void area_enlarge(Geom::IntRect &area, Geom::Affine const &trans) override; + double complexity(Geom::Affine const &ctm) override; + + void set_input(int slot) override; + void set_input(int input, int slot) override; + virtual void set_scale(double s); + virtual void set_channel_selector(int s, FilterDisplacementMapChannelSelector channel); + + Glib::ustring name() override { return Glib::ustring("Displacement Map"); } + +private: + double scale; + int _input2; + unsigned Xchannel, Ychannel; +}; + +} /* namespace Filters */ +} /* namespace Inkscape */ + +#endif /* __NR_FILTER_DISPLACEMENT_MAP_H__ */ +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter-flood.cpp b/src/display/nr-filter-flood.cpp new file mode 100644 index 0000000..05c3293 --- /dev/null +++ b/src/display/nr-filter-flood.cpp @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * feFlood filter primitive renderer + * + * Authors: + * Felipe CorrĂȘa da Silva Sanches <juca@members.fsf.org> + * Tavmjong Bah <tavmjong@free.fr> (use primitive filter region) + * + * Copyright (C) 2007, 2011 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include "display/cairo-utils.h" +#include "display/nr-filter-flood.h" +#include "display/nr-filter-slot.h" +#include "svg/svg-icc-color.h" +#include "svg/svg-color.h" +#include "color.h" + +namespace Inkscape { +namespace Filters { + +FilterFlood::FilterFlood() += default; + +FilterPrimitive * FilterFlood::create() { + return new FilterFlood(); +} + +FilterFlood::~FilterFlood() += default; + +void FilterFlood::render_cairo(FilterSlot &slot) +{ + cairo_surface_t *input = slot.getcairo(_input); + + double r = SP_RGBA32_R_F(color); + double g = SP_RGBA32_G_F(color); + double b = SP_RGBA32_B_F(color); + double a = opacity; + +#if defined(HAVE_LIBLCMS2) + + if (icc) { + guchar ru, gu, bu; + icc_color_to_sRGB(icc, &ru, &gu, &bu); + r = SP_COLOR_U_TO_F(ru); + g = SP_COLOR_U_TO_F(gu); + b = SP_COLOR_U_TO_F(bu); + } +#endif + + cairo_surface_t *out = ink_cairo_surface_create_same_size(input, CAIRO_CONTENT_COLOR_ALPHA); + + SPColorInterpolation ci_fp = SP_CSS_COLOR_INTERPOLATION_AUTO; + if( _style ) { + ci_fp = (SPColorInterpolation)_style->color_interpolation_filters.computed; + + // Flood color is always defined in terms of sRGB, preconvert to linearRGB + // if color_interpolation_filters set to linearRGB (for efficiency assuming + // next filter primitive has same value of cif). + if( ci_fp == SP_CSS_COLOR_INTERPOLATION_LINEARRGB ) { + r = srgb_to_linear( r ); + g = srgb_to_linear( g ); + b = srgb_to_linear( b ); + } + } + set_cairo_surface_ci(out, ci_fp ); + + // Get filter primitive area in user units + Geom::Rect fp = filter_primitive_area( slot.get_units() ); + + // Convert to Cairo units + Geom::Rect fp_cairo = fp * slot.get_units().get_matrix_user2pb(); + + // Get area in slot (tile to fill) + Geom::Rect sa = slot.get_slot_area(); + + // Get overlap + Geom::OptRect optoverlap = intersect( fp_cairo, sa ); + if( optoverlap ) { + + Geom::Rect overlap = *optoverlap; + + double dx = fp_cairo.min()[Geom::X] - sa.min()[Geom::X]; + double dy = fp_cairo.min()[Geom::Y] - sa.min()[Geom::Y]; + if( dx < 0.0 ) dx = 0.0; + if( dy < 0.0 ) dy = 0.0; + + cairo_t *ct = cairo_create(out); + cairo_set_source_rgba(ct, r, g, b, a); + cairo_set_operator(ct, CAIRO_OPERATOR_SOURCE); + cairo_rectangle(ct, dx, dy, overlap.width(), overlap.height() ); + cairo_fill(ct); + cairo_destroy(ct); + } + + slot.set(_output, out); + cairo_surface_destroy(out); +} + +bool FilterFlood::can_handle_affine(Geom::Affine const &) +{ + // flood is a per-pixel primitive and is immutable under transformations + return true; +} + +void FilterFlood::set_color(guint32 c) { + color = c; +} + +void FilterFlood::set_opacity(double o) { + opacity = o; +} + +void FilterFlood::set_icc(SVGICCColor *icc_color) { + icc = icc_color; +} +double FilterFlood::complexity(Geom::Affine const &) +{ + // flood is actually less expensive than normal rendering, + // but when flood is processed, the object has already been rendered + return 1.0; +} + +} /* namespace Filters */ +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter-flood.h b/src/display/nr-filter-flood.h new file mode 100644 index 0000000..c2f51fa --- /dev/null +++ b/src/display/nr-filter-flood.h @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_NR_FILTER_FLOOD_H +#define SEEN_NR_FILTER_FLOOD_H + +/* + * feFlood filter primitive renderer + * + * Authors: + * Felipe CorrĂȘa da Silva Sanches <juca@members.fsf.org> + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "display/nr-filter-primitive.h" + +struct SVGICCColor; +typedef unsigned int guint32; + +namespace Inkscape { +namespace Filters { + +class FilterFlood : public FilterPrimitive { +public: + FilterFlood(); + static FilterPrimitive *create(); + ~FilterFlood() override; + + void render_cairo(FilterSlot &slot) override; + bool can_handle_affine(Geom::Affine const &) override; + double complexity(Geom::Affine const &ctm) override; + bool uses_background() override { return false; } + + virtual void set_opacity(double o); + virtual void set_color(guint32 c); + virtual void set_icc(SVGICCColor *icc_color); + + Glib::ustring name() override { return Glib::ustring("Flood"); } + +private: + double opacity; + guint32 color; + SVGICCColor *icc; +}; + +} /* namespace Filters */ +} /* namespace Inkscape */ + +#endif /* __NR_FILTER_FLOOD_H__ */ +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter-gaussian.cpp b/src/display/nr-filter-gaussian.cpp new file mode 100644 index 0000000..f145ba5 --- /dev/null +++ b/src/display/nr-filter-gaussian.cpp @@ -0,0 +1,758 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Gaussian blur renderer + * + * Authors: + * Niko Kiirala <niko@kiirala.com> + * bulia byak + * Jasper van de Gronde <th.v.d.gronde@hccnet.nl> + * + * Copyright (C) 2006-2008 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include <algorithm> +#include <cmath> +#include <complex> +#include <cstdlib> +#include <glib.h> +#include <limits> +#if HAVE_OPENMP +#include <omp.h> +#endif //HAVE_OPENMP + +#include "display/cairo-utils.h" +#include "display/nr-filter-primitive.h" +#include "display/nr-filter-gaussian.h" +#include "display/nr-filter-types.h" +#include "display/nr-filter-units.h" +#include "display/nr-filter-slot.h" +#include <2geom/affine.h> +#include "util/fixed_point.h" +#include "preferences.h" + +#ifndef INK_UNUSED +#define INK_UNUSED(x) ((void)(x)) +#endif + +// IIR filtering method based on: +// L.J. van Vliet, I.T. Young, and P.W. Verbeek, Recursive Gaussian Derivative Filters, +// in: A.K. Jain, S. Venkatesh, B.C. Lovell (eds.), +// ICPR'98, Proc. 14th Int. Conference on Pattern Recognition (Brisbane, Aug. 16-20), +// IEEE Computer Society Press, Los Alamitos, 1998, 509-514. +// +// Using the backwards-pass initialization procedure from: +// Boundary Conditions for Young - van Vliet Recursive Filtering +// Bill Triggs, Michael Sdika +// IEEE Transactions on Signal Processing, Volume 54, Number 5 - may 2006 + +// Number of IIR filter coefficients used. Currently only 3 is supported. +// "Recursive Gaussian Derivative Filters" says this is enough though (and +// some testing indeed shows that the quality doesn't improve much if larger +// filters are used). +static size_t const N = 3; + +template<typename InIt, typename OutIt, typename Size> +inline void copy_n(InIt beg_in, Size N, OutIt beg_out) { + std::copy(beg_in, beg_in+N, beg_out); +} + +// Type used for IIR filter coefficients (can be 10.21 signed fixed point, see Anisotropic Gaussian Filtering Using Fixed Point Arithmetic, Christoph H. Lampert & Oliver Wirjadi, 2006) +typedef double IIRValue; + +// Type used for FIR filter coefficients (can be 16.16 unsigned fixed point, should have 8 or more bits in the fractional part, the integer part should be capable of storing approximately 20*255) +typedef Inkscape::Util::FixedPoint<unsigned int,16> FIRValue; + +template<typename T> static inline T sqr(T const& v) { return v*v; } + +template<typename T> static inline T clip(T const& v, T const& a, T const& b) { + if ( v < a ) return a; + if ( v > b ) return b; + return v; +} + +template<typename Tt, typename Ts> +static inline Tt round_cast(Ts v) { + static Ts const rndoffset(.5); + return static_cast<Tt>(v+rndoffset); +} +/* +template<> +inline unsigned char round_cast(double v) { + // This (fast) rounding method is based on: + // http://stereopsis.com/sree/fpu2006.html +#if G_BYTE_ORDER==G_LITTLE_ENDIAN + double const dmr = 6755399441055744.0; + v = v + dmr; + return ((unsigned char*)&v)[0]; +#elif G_BYTE_ORDER==G_BIG_ENDIAN + double const dmr = 6755399441055744.0; + v = v + dmr; + return ((unsigned char*)&v)[7]; +#else + static double const rndoffset(.5); + return static_cast<unsigned char>(v+rndoffset); +#endif +}*/ + +template<typename Tt, typename Ts> +static inline Tt clip_round_cast(Ts const v) { + Ts const minval = std::numeric_limits<Tt>::min(); + Ts const maxval = std::numeric_limits<Tt>::max(); + Tt const minval_rounded = std::numeric_limits<Tt>::min(); + Tt const maxval_rounded = std::numeric_limits<Tt>::max(); + if ( v < minval ) return minval_rounded; + if ( v > maxval ) return maxval_rounded; + return round_cast<Tt>(v); +} + +template<typename Tt, typename Ts> +static inline Tt clip_round_cast_varmax(Ts const v, Tt const maxval_rounded) { + Ts const minval = std::numeric_limits<Tt>::min(); + Tt const maxval = maxval_rounded; + Tt const minval_rounded = std::numeric_limits<Tt>::min(); + if ( v < minval ) return minval_rounded; + if ( v > maxval ) return maxval_rounded; + return round_cast<Tt>(v); +} + +namespace Inkscape { +namespace Filters { + +FilterGaussian::FilterGaussian() +{ + _deviation_x = _deviation_y = 0.0; +} + +FilterPrimitive *FilterGaussian::create() +{ + return new FilterGaussian(); +} + +FilterGaussian::~FilterGaussian() +{ + // Nothing to do here +} + +static int +_effect_area_scr(double const deviation) +{ + return (int)std::ceil(std::fabs(deviation) * 3.0); +} + +static void +_make_kernel(FIRValue *const kernel, double const deviation) +{ + int const scr_len = _effect_area_scr(deviation); + g_assert(scr_len >= 0); + double const d_sq = sqr(deviation) * 2; + double k[scr_len+1]; // This is only called for small kernel sizes (above approximately 10 coefficients the IIR filter is used) + + // Compute kernel and sum of coefficients + // Note that actually only half the kernel is computed, as it is symmetric + double sum = 0; + for ( int i = scr_len; i >= 0 ; i-- ) { + k[i] = std::exp(-sqr(i) / d_sq); + if ( i > 0 ) sum += k[i]; + } + // the sum of the complete kernel is twice as large (plus the center element which we skipped above to prevent counting it twice) + sum = 2*sum + k[0]; + + // Normalize kernel (making sure the sum is exactly 1) + double ksum = 0; + FIRValue kernelsum = 0; + for ( int i = scr_len; i >= 1 ; i-- ) { + ksum += k[i]/sum; + kernel[i] = ksum-static_cast<double>(kernelsum); + kernelsum += kernel[i]; + } + kernel[0] = FIRValue(1)-2*kernelsum; +} + +// Return value (v) should satisfy: +// 2^(2*v)*255<2^32 +// 255<2^(32-2*v) +// 2^8<=2^(32-2*v) +// 8<=32-2*v +// 2*v<=24 +// v<=12 +static int +_effect_subsample_step_log2(double const deviation, int const quality) +{ + // To make sure FIR will always be used (unless the kernel is VERY big): + // deviation/step <= 3 + // deviation/3 <= step + // log(deviation/3) <= log(step) + // So when x below is >= 1/3 FIR will almost always be used. + // This means IIR is almost only used with the modes BETTER or BEST. + int stepsize_l2; + switch (quality) { + case BLUR_QUALITY_WORST: + // 2 == log(x*8/3)) + // 2^2 == x*2^3/3 + // x == 3/2 + stepsize_l2 = clip(static_cast<int>(log(deviation*(3./2.))/log(2.)), 0, 12); + break; + case BLUR_QUALITY_WORSE: + // 2 == log(x*16/3)) + // 2^2 == x*2^4/3 + // x == 3/2^2 + stepsize_l2 = clip(static_cast<int>(log(deviation*(3./4.))/log(2.)), 0, 12); + break; + case BLUR_QUALITY_BETTER: + // 2 == log(x*32/3)) + // 2 == x*2^5/3 + // x == 3/2^4 + stepsize_l2 = clip(static_cast<int>(log(deviation*(3./16.))/log(2.)), 0, 12); + break; + case BLUR_QUALITY_BEST: + stepsize_l2 = 0; // no subsampling at all + break; + case BLUR_QUALITY_NORMAL: + default: + // 2 == log(x*16/3)) + // 2 == x*2^4/3 + // x == 3/2^3 + stepsize_l2 = clip(static_cast<int>(log(deviation*(3./8.))/log(2.)), 0, 12); + break; + } + return stepsize_l2; +} + +static void calcFilter(double const sigma, double b[N]) { + assert(N==3); + std::complex<double> const d1_org(1.40098, 1.00236); + double const d3_org = 1.85132; + double qbeg = 1; // Don't go lower than sigma==2 (we'd probably want a normal convolution in that case anyway) + double qend = 2*sigma; + double const sigmasqr = sqr(sigma); + do { // Binary search for right q (a linear interpolation scheme is suggested, but this should work fine as well) + double const q = (qbeg+qend)/2; + // Compute scaled filter coefficients + std::complex<double> const d1 = pow(d1_org, 1.0/q); + double const d3 = pow(d3_org, 1.0/q); + // Compute actual sigma^2 + double const ssqr = 2*(2*(d1/sqr(d1-1.)).real()+d3/sqr(d3-1.)); + if ( ssqr < sigmasqr ) { + qbeg = q; + } else { + qend = q; + } + } while(qend-qbeg>(sigma/(1<<30))); + // Compute filter coefficients + double const q = (qbeg+qend)/2; + std::complex<double> const d1 = pow(d1_org, 1.0/q); + double const d3 = pow(d3_org, 1.0/q); + double const absd1sqr = std::norm(d1); // d1*d2 = d1*conj(d1) = |d1|^2 = std::norm(d1) + double const re2d1 = 2*d1.real(); // d1+d2 = d1+conj(d1) = 2*real(d1) + double const bscale = 1.0/(absd1sqr*d3); + b[2] = -bscale; + b[1] = bscale*(d3+re2d1); + b[0] = -bscale*(absd1sqr+d3*re2d1); +} + +static void calcTriggsSdikaM(double const b[N], double M[N*N]) { + assert(N==3); + double a1=b[0], a2=b[1], a3=b[2]; + double const Mscale = 1.0/((1+a1-a2+a3)*(1-a1-a2-a3)*(1+a2+(a1-a3)*a3)); + M[0] = 1-a2-a1*a3-sqr(a3); + M[1] = (a1+a3)*(a2+a1*a3); + M[2] = a3*(a1+a2*a3); + M[3] = a1+a2*a3; + M[4] = (1-a2)*(a2+a1*a3); + M[5] = a3*(1-a2-a1*a3-sqr(a3)); + M[6] = a1*(a1+a3)+a2*(1-a2); + M[7] = a1*(a2-sqr(a3))+a3*(1+a2*(a2-1)-sqr(a3)); + M[8] = a3*(a1+a2*a3); + for(unsigned int i=0; i<9; i++) M[i] *= Mscale; +} + +template<unsigned int SIZE> +static void calcTriggsSdikaInitialization(double const M[N*N], IIRValue const uold[N][SIZE], IIRValue const uplus[SIZE], IIRValue const vplus[SIZE], IIRValue const alpha, IIRValue vold[N][SIZE]) { + for(unsigned int c=0; c<SIZE; c++) { + double uminp[N]; + for(unsigned int i=0; i<N; i++) uminp[i] = uold[i][c] - uplus[c]; + for(unsigned int i=0; i<N; i++) { + double voldf = 0; + for(unsigned int j=0; j<N; j++) { + voldf += uminp[j]*M[i*N+j]; + } + // Properly takes care of the scaling coefficient alpha and vplus (which is already appropriately scaled) + // This was arrived at by starting from a version of the blur filter that ignored the scaling coefficient + // (and scaled the final output by alpha^2) and then gradually reintroducing the scaling coefficient. + vold[i][c] = voldf*alpha; + vold[i][c] += vplus[c]; + } + } +} + +// Filters over 1st dimension +template<typename PT, unsigned int PC, bool PREMULTIPLIED_ALPHA> +static void +filter2D_IIR(PT *const dest, int const dstr1, int const dstr2, + PT const *const src, int const sstr1, int const sstr2, + int const n1, int const n2, IIRValue const b[N+1], double const M[N*N], + IIRValue *const tmpdata[], int const num_threads) +{ +#if G_BYTE_ORDER == G_LITTLE_ENDIAN + static unsigned int const alpha_PC = PC-1; + #define PREMUL_ALPHA_LOOP for(unsigned int c=0; c<PC-1; ++c) +#else + static unsigned int const alpha_PC = 0; + #define PREMUL_ALPHA_LOOP for(unsigned int c=1; c<PC; ++c) +#endif + +INK_UNUSED(num_threads); // to suppress unused argument compiler warning +#if HAVE_OPENMP +#pragma omp parallel for num_threads(num_threads) +#endif // HAVE_OPENMP + for ( int c2 = 0 ; c2 < n2 ; c2++ ) { +#if HAVE_OPENMP + unsigned int tid = omp_get_thread_num(); +#else + unsigned int tid = 0; +#endif // HAVE_OPENMP + // corresponding line in the source and output buffer + PT const * srcimg = src + c2*sstr2; + PT * dstimg = dest + c2*dstr2 + n1*dstr1; + // Border constants + IIRValue imin[PC]; copy_n(srcimg + (0)*sstr1, PC, imin); + IIRValue iplus[PC]; copy_n(srcimg + (n1-1)*sstr1, PC, iplus); + // Forward pass + IIRValue u[N+1][PC]; + for(unsigned int i=0; i<N; i++) copy_n(imin, PC, u[i]); + for ( int c1 = 0 ; c1 < n1 ; c1++ ) { + for(unsigned int i=N; i>0; i--) copy_n(u[i-1], PC, u[i]); + copy_n(srcimg, PC, u[0]); + srcimg += sstr1; + for(unsigned int c=0; c<PC; c++) u[0][c] *= b[0]; + for(unsigned int i=1; i<N+1; i++) { + for(unsigned int c=0; c<PC; c++) u[0][c] += u[i][c]*b[i]; + } + copy_n(u[0], PC, tmpdata[tid]+c1*PC); + } + // Backward pass + IIRValue v[N+1][PC]; + calcTriggsSdikaInitialization<PC>(M, u, iplus, iplus, b[0], v); + dstimg -= dstr1; + if ( PREMULTIPLIED_ALPHA ) { + dstimg[alpha_PC] = clip_round_cast<PT>(v[0][alpha_PC]); + PREMUL_ALPHA_LOOP dstimg[c] = clip_round_cast_varmax<PT>(v[0][c], dstimg[alpha_PC]); + } else { + for(unsigned int c=0; c<PC; c++) dstimg[c] = clip_round_cast<PT>(v[0][c]); + } + int c1=n1-1; + while(c1-->0) { + for(unsigned int i=N; i>0; i--) copy_n(v[i-1], PC, v[i]); + copy_n(tmpdata[tid]+c1*PC, PC, v[0]); + for(unsigned int c=0; c<PC; c++) v[0][c] *= b[0]; + for(unsigned int i=1; i<N+1; i++) { + for(unsigned int c=0; c<PC; c++) v[0][c] += v[i][c]*b[i]; + } + dstimg -= dstr1; + if ( PREMULTIPLIED_ALPHA ) { + dstimg[alpha_PC] = clip_round_cast<PT>(v[0][alpha_PC]); + PREMUL_ALPHA_LOOP dstimg[c] = clip_round_cast_varmax<PT>(v[0][c], dstimg[alpha_PC]); + } else { + for(unsigned int c=0; c<PC; c++) dstimg[c] = clip_round_cast<PT>(v[0][c]); + } + } + } +} + +// Filters over 1st dimension +// Assumes kernel is symmetric +// Kernel should have scr_len+1 elements +template<typename PT, unsigned int PC> +static void +filter2D_FIR(PT *const dst, int const dstr1, int const dstr2, + PT const *const src, int const sstr1, int const sstr2, + int const n1, int const n2, FIRValue const *const kernel, int const scr_len, int const num_threads) +{ + // Past pixels seen (to enable in-place operation) + PT history[scr_len+1][PC]; + +INK_UNUSED(num_threads); // suppresses unused argument compiler warning +#if HAVE_OPENMP +#pragma omp parallel for num_threads(num_threads) private(history) +#endif // HAVE_OPENMP + for ( int c2 = 0 ; c2 < n2 ; c2++ ) { + + // corresponding line in the source buffer + int const src_line = c2 * sstr2; + + // current line in the output buffer + int const dst_line = c2 * dstr2; + + int skipbuf[4] = {INT_MIN, INT_MIN, INT_MIN, INT_MIN}; + + // history initialization + PT imin[PC]; copy_n(src + src_line, PC, imin); + for(int i=0; i<scr_len; i++) copy_n(imin, PC, history[i]); + + for ( int c1 = 0 ; c1 < n1 ; c1++ ) { + + int const src_disp = src_line + c1 * sstr1; + int const dst_disp = dst_line + c1 * dstr1; + + // update history + for(int i=scr_len; i>0; i--) copy_n(history[i-1], PC, history[i]); + copy_n(src + src_disp, PC, history[0]); + + // for all bytes of the pixel + for ( unsigned int byte = 0 ; byte < PC ; byte++) { + + if(skipbuf[byte] > c1) continue; + + FIRValue sum = 0; + int last_in = -1; + int different_count = 0; + + // go over our point's neighbours in the history + for ( int i = 0 ; i <= scr_len ; i++ ) { + // value at the pixel + PT in_byte = history[i][byte]; + + // is it the same as last one we saw? + if(in_byte != last_in) different_count++; + last_in = in_byte; + + // sum pixels weighted by the kernel + sum += in_byte * kernel[i]; + } + + // go over our point's neighborhood on x axis in the in buffer + int nb_src_disp = src_disp + byte; + for ( int i = 1 ; i <= scr_len ; i++ ) { + // the pixel we're looking at + int c1_in = c1 + i; + if (c1_in >= n1) { + c1_in = n1 - 1; + } else { + nb_src_disp += sstr1; + } + + // value at the pixel + PT in_byte = src[nb_src_disp]; + + // is it the same as last one we saw? + if(in_byte != last_in) different_count++; + last_in = in_byte; + + // sum pixels weighted by the kernel + sum += in_byte * kernel[i]; + } + + // store the result in bufx + dst[dst_disp + byte] = round_cast<PT>(sum); + + // optimization: if there was no variation within this point's neighborhood, + // skip ahead while we keep seeing the same last_in byte: + // blurring flat color would not change it anyway + if (different_count <= 1) { // note that different_count is at least 1, because last_in is initialized to -1 + int pos = c1 + 1; + int nb_src_disp = src_disp + (1+scr_len)*sstr1 + byte; // src_line + (pos+scr_len) * sstr1 + byte + int nb_dst_disp = dst_disp + (1) *dstr1 + byte; // dst_line + (pos) * sstr1 + byte + while(pos + scr_len < n1 && src[nb_src_disp] == last_in) { + dst[nb_dst_disp] = last_in; + pos++; + nb_src_disp += sstr1; + nb_dst_disp += dstr1; + } + skipbuf[byte] = pos; + } + } + } + } +} + +static void +gaussian_pass_IIR(Geom::Dim2 d, double deviation, cairo_surface_t *src, cairo_surface_t *dest, + IIRValue **tmpdata, int num_threads) +{ + // Filter variables + IIRValue b[N+1]; // scaling coefficient + filter coefficients (can be 10.21 fixed point) + double bf[N]; // computed filter coefficients + double M[N*N]; // matrix used for initialization procedure (has to be double) + + // Compute filter + calcFilter(deviation, bf); + for(double & i : bf) i = -i; + b[0] = 1; // b[0] == alpha (scaling coefficient) + for(size_t i=0; i<N; i++) { + b[i+1] = bf[i]; + b[0] -= b[i+1]; + } + + // Compute initialization matrix + calcTriggsSdikaM(bf, M); + + int stride = cairo_image_surface_get_stride(src); + int w = cairo_image_surface_get_width(src); + int h = cairo_image_surface_get_height(src); + if (d != Geom::X) std::swap(w, h); + + // Filter + switch (cairo_image_surface_get_format(src)) { + case CAIRO_FORMAT_A8: ///< Grayscale + filter2D_IIR<unsigned char,1,false>( + cairo_image_surface_get_data(dest), d == Geom::X ? 1 : stride, d == Geom::X ? stride : 1, + cairo_image_surface_get_data(src), d == Geom::X ? 1 : stride, d == Geom::X ? stride : 1, + w, h, b, M, tmpdata, num_threads); + break; + case CAIRO_FORMAT_ARGB32: ///< Premultiplied 8 bit RGBA + filter2D_IIR<unsigned char,4,true>( + cairo_image_surface_get_data(dest), d == Geom::X ? 4 : stride, d == Geom::X ? stride : 4, + cairo_image_surface_get_data(src), d == Geom::X ? 4 : stride, d == Geom::X ? stride : 4, + w, h, b, M, tmpdata, num_threads); + break; + default: + g_warning("gaussian_pass_IIR: unsupported image format"); + }; +} + +static void +gaussian_pass_FIR(Geom::Dim2 d, double deviation, cairo_surface_t *src, cairo_surface_t *dest, + int num_threads) +{ + int scr_len = _effect_area_scr(deviation); + // Filter kernel for x direction + std::vector<FIRValue> kernel(scr_len + 1); + _make_kernel(&kernel[0], deviation); + + int stride = cairo_image_surface_get_stride(src); + int w = cairo_image_surface_get_width(src); + int h = cairo_image_surface_get_height(src); + if (d != Geom::X) std::swap(w, h); + + // Filter (x) + switch (cairo_image_surface_get_format(src)) { + case CAIRO_FORMAT_A8: ///< Grayscale + filter2D_FIR<unsigned char,1>( + cairo_image_surface_get_data(dest), d == Geom::X ? 1 : stride, d == Geom::X ? stride : 1, + cairo_image_surface_get_data(src), d == Geom::X ? 1 : stride, d == Geom::X ? stride : 1, + w, h, &kernel[0], scr_len, num_threads); + break; + case CAIRO_FORMAT_ARGB32: ///< Premultiplied 8 bit RGBA + filter2D_FIR<unsigned char,4>( + cairo_image_surface_get_data(dest), d == Geom::X ? 4 : stride, d == Geom::X ? stride : 4, + cairo_image_surface_get_data(src), d == Geom::X ? 4 : stride, d == Geom::X ? stride : 4, + w, h, &kernel[0], scr_len, num_threads); + break; + default: + g_warning("gaussian_pass_FIR: unsupported image format"); + }; +} + +void FilterGaussian::render_cairo(FilterSlot &slot) +{ + cairo_surface_t *in = slot.getcairo(_input); + if (!in) return; + + // We may need to transform input surface to correct color interpolation space. The input surface + // might be used as input to another primitive but it is likely that all the primitives in a given + // filter use the same color interpolation space so we don't copy the input before converting. + SPColorInterpolation ci_fp = SP_CSS_COLOR_INTERPOLATION_AUTO; + if( _style ) { + ci_fp = (SPColorInterpolation)_style->color_interpolation_filters.computed; + } + set_cairo_surface_ci( in, ci_fp ); + + // zero deviation = no change in output + if (_deviation_x <= 0 && _deviation_y <= 0) { + cairo_surface_t *cp = ink_cairo_surface_copy(in); + slot.set(_output, cp); + cairo_surface_destroy(cp); + return; + } + + // Handle bounding box case. + double dx = _deviation_x; + double dy = _deviation_y; + if( slot.get_units().get_primitive_units() == SP_FILTER_UNITS_OBJECTBOUNDINGBOX ) { + Geom::OptRect const bbox = slot.get_units().get_item_bbox(); + if( bbox ) { + dx *= (*bbox).width(); + dy *= (*bbox).height(); + } + } + + Geom::Affine trans = slot.get_units().get_matrix_user2pb(); + + double deviation_x_orig = dx * trans.expansionX(); + double deviation_y_orig = dy * trans.expansionY(); + + int device_scale = slot.get_device_scale(); + + deviation_x_orig *= device_scale; + deviation_y_orig *= device_scale; + + cairo_format_t fmt = cairo_image_surface_get_format(in); + int bytes_per_pixel = 0; + switch (fmt) { + case CAIRO_FORMAT_A8: + bytes_per_pixel = 1; break; + case CAIRO_FORMAT_ARGB32: + default: + bytes_per_pixel = 4; break; + } + +#if HAVE_OPENMP + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int threads = prefs->getIntLimited("/options/threading/numthreads", omp_get_num_procs(), 1, 256); +#else + int threads = 1; +#endif + + int quality = slot.get_blurquality(); + int x_step = 1 << _effect_subsample_step_log2(deviation_x_orig, quality); + int y_step = 1 << _effect_subsample_step_log2(deviation_y_orig, quality); + bool resampling = x_step > 1 || y_step > 1; + int w_orig = ink_cairo_surface_get_width(in); // Pixels + int h_orig = ink_cairo_surface_get_height(in); + int w_downsampled = resampling ? static_cast<int>(ceil(static_cast<double>(w_orig)/x_step))+1 : w_orig; + int h_downsampled = resampling ? static_cast<int>(ceil(static_cast<double>(h_orig)/y_step))+1 : h_orig; + double deviation_x = deviation_x_orig / x_step; + double deviation_y = deviation_y_orig / y_step; + int scr_len_x = _effect_area_scr(deviation_x); + int scr_len_y = _effect_area_scr(deviation_y); + + // Decide which filter to use for X and Y + // This threshold was determined by trial-and-error for one specific machine, + // so there's a good chance that it's not optimal. + // Whatever you do, don't go below 1 (and preferably not even below 2), as + // the IIR filter gets unstable there. + bool use_IIR_x = deviation_x > 3; + bool use_IIR_y = deviation_y > 3; + + // Temporary storage for IIR filter + // NOTE: This can be eliminated, but it reduces the precision a bit + IIRValue * tmpdata[threads]; + std::fill_n(tmpdata, threads, (IIRValue*)0); + if ( use_IIR_x || use_IIR_y ) { + for(int i = 0; i < threads; ++i) { + tmpdata[i] = new IIRValue[std::max(w_downsampled,h_downsampled)*bytes_per_pixel]; + } + } + + cairo_surface_t *downsampled = nullptr; + if (resampling) { + // Divide by device scale as w_downsampled is in pixels while + // cairo_surface_create_similar() uses device units. + downsampled = cairo_surface_create_similar(in, cairo_surface_get_content(in), + w_downsampled/device_scale, h_downsampled/device_scale); + cairo_t *ct = cairo_create(downsampled); + cairo_scale(ct, static_cast<double>(w_downsampled)/w_orig, static_cast<double>(h_downsampled)/h_orig); + cairo_set_source_surface(ct, in, 0, 0); + cairo_paint(ct); + cairo_destroy(ct); + } else { + downsampled = ink_cairo_surface_copy(in); + } + cairo_surface_flush(downsampled); + + if (scr_len_x > 0) { + if (use_IIR_x) { + gaussian_pass_IIR(Geom::X, deviation_x, downsampled, downsampled, tmpdata, threads); + } else { + gaussian_pass_FIR(Geom::X, deviation_x, downsampled, downsampled, threads); + } + } + + if (scr_len_y > 0) { + if (use_IIR_y) { + gaussian_pass_IIR(Geom::Y, deviation_y, downsampled, downsampled, tmpdata, threads); + } else { + gaussian_pass_FIR(Geom::Y, deviation_y, downsampled, downsampled, threads); + } + } + + // free the temporary data + if ( use_IIR_x || use_IIR_y ) { + for(int i = 0; i < threads; ++i) { + delete[] tmpdata[i]; + } + } + + cairo_surface_mark_dirty(downsampled); + if (resampling) { + cairo_surface_t *upsampled = cairo_surface_create_similar(downsampled, cairo_surface_get_content(downsampled), + w_orig/device_scale, h_orig/device_scale); + cairo_t *ct = cairo_create(upsampled); + cairo_scale(ct, static_cast<double>(w_orig)/w_downsampled, static_cast<double>(h_orig)/h_downsampled); + cairo_set_source_surface(ct, downsampled, 0, 0); + cairo_paint(ct); + cairo_destroy(ct); + + set_cairo_surface_ci( upsampled, ci_fp ); + + slot.set(_output, upsampled); + cairo_surface_destroy(upsampled); + cairo_surface_destroy(downsampled); + } else { + set_cairo_surface_ci( downsampled, ci_fp ); + + slot.set(_output, downsampled); + cairo_surface_destroy(downsampled); + } +} + +void FilterGaussian::area_enlarge(Geom::IntRect &area, Geom::Affine const &trans) +{ + int area_x = _effect_area_scr(_deviation_x * trans.expansionX()); + int area_y = _effect_area_scr(_deviation_y * trans.expansionY()); + // maximum is used because rotations can mix up these directions + // TODO: calculate a more tight-fitting rendering area + int area_max = std::max(area_x, area_y); + area.expandBy(area_max); +} + +bool FilterGaussian::can_handle_affine(Geom::Affine const &) +{ + // Previously we tried to be smart and return true for rotations. + // However, the transform passed here is NOT the total transform + // from filter user space to screen. + // TODO: fix this, or replace can_handle_affine() with isotropic(). + return false; +} + +double FilterGaussian::complexity(Geom::Affine const &trans) +{ + int area_x = _effect_area_scr(_deviation_x * trans.expansionX()); + int area_y = _effect_area_scr(_deviation_y * trans.expansionY()); + return 2.0 * area_x * area_y; +} + +void FilterGaussian::set_deviation(double deviation) +{ + if(std::isfinite(deviation) && deviation >= 0) { + _deviation_x = _deviation_y = deviation; + } +} + +void FilterGaussian::set_deviation(double x, double y) +{ + if(std::isfinite(x) && x >= 0 && std::isfinite(y) && y >= 0) { + _deviation_x = x; + _deviation_y = y; + } +} + +} /* namespace Filters */ +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter-gaussian.h b/src/display/nr-filter-gaussian.h new file mode 100644 index 0000000..28ffb00 --- /dev/null +++ b/src/display/nr-filter-gaussian.h @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_NR_FILTER_GAUSSIAN_H +#define SEEN_NR_FILTER_GAUSSIAN_H + +/* + * Gaussian blur renderer + * + * Authors: + * Niko Kiirala <niko@kiirala.com> + * bulia byak + * Jasper van de Gronde <th.v.d.gronde@hccnet.nl> + * + * Copyright (C) 2006 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/forward.h> +#include "display/nr-filter-primitive.h" + +enum { + BLUR_QUALITY_BEST = 2, + BLUR_QUALITY_BETTER = 1, + BLUR_QUALITY_NORMAL = 0, + BLUR_QUALITY_WORSE = -1, + BLUR_QUALITY_WORST = -2 +}; + +namespace Inkscape { +namespace Filters { + +class FilterGaussian : public FilterPrimitive { +public: + FilterGaussian(); + static FilterPrimitive *create(); + ~FilterGaussian() override; + + void render_cairo(FilterSlot &slot) override; + void area_enlarge(Geom::IntRect &area, Geom::Affine const &m) override; + bool can_handle_affine(Geom::Affine const &m) override; + double complexity(Geom::Affine const &ctm) override; + + /** + * Set the standard deviation value for gaussian blur. Deviation along + * both axis is set to the provided value. + * Negative value, NaN and infinity are considered an error and no + * changes to filter state are made. If not set, default value of zero + * is used, which means the filter results in transparent black image. + */ + void set_deviation(double deviation); + /** + * Set the standard deviation value for gaussian blur. First parameter + * sets the deviation alogn x-axis, second along y-axis. + * Negative value, NaN and infinity are considered an error and no + * changes to filter state are made. If not set, default value of zero + * is used, which means the filter results in transparent black image. + */ + void set_deviation(double x, double y); + + Glib::ustring name() override { return Glib::ustring("Gaussian Blur"); } + +private: + double _deviation_x; + double _deviation_y; +}; + + +} /* namespace Filters */ +} /* namespace Inkscape */ + + + + +#endif /* __NR_FILTER_GAUSSIAN_H__ */ +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter-image.cpp b/src/display/nr-filter-image.cpp new file mode 100644 index 0000000..bfc2b6b --- /dev/null +++ b/src/display/nr-filter-image.cpp @@ -0,0 +1,344 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * feImage filter primitive renderer + * + * Authors: + * Felipe CorrĂȘa da Silva Sanches <juca@members.fsf.org> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * + * Copyright (C) 2007-2011 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "display/nr-filter-image.h" +#include "document.h" +#include "object/sp-item.h" +#include "display/cairo-utils.h" +#include "display/drawing-context.h" +#include "display/drawing.h" +#include "display/drawing-item.h" +#include "display/nr-filter.h" +#include "display/nr-filter-slot.h" +#include "display/nr-filter-units.h" +#include "enums.h" +#include <glibmm/fileutils.h> + +namespace Inkscape { +namespace Filters { + +FilterImage::FilterImage() + : SVGElem(nullptr) + , document(nullptr) + , feImageHref(nullptr) + , image(nullptr) + , broken_ref(false) +{ } + +FilterPrimitive * FilterImage::create() { + return new FilterImage(); +} + +FilterImage::~FilterImage() +{ + if (feImageHref) + g_free(feImageHref); + delete image; +} + +void FilterImage::render_cairo(FilterSlot &slot) +{ + std::cout << "FilterImage::render_cairo: Entrance" << std::endl; + if (!feImageHref) + return; + + //cairo_surface_t *input = slot.getcairo(_input); + + // Viewport is filter primitive area (in user coordinates). + // Note: viewport calculation in non-trivial. Do not rely + // on get_matrix_primitiveunits2pb(). + Geom::Rect vp = filter_primitive_area( slot.get_units() ); + slot.set_primitive_area(_output, vp); // Needed for tiling + + double feImageX = vp.min()[Geom::X]; + double feImageY = vp.min()[Geom::Y]; + double feImageWidth = vp.width(); + double feImageHeight = vp.height(); + + // feImage is suppose to use the same parameters as a normal SVG image. + // If a width or height is set to zero, the image is not suppose to be displayed. + // This does not seem to be what Firefox or Opera does, nor does the W3C displacement + // filter test expect this behavior. If the width and/or height are zero, we use + // the width and height of the object bounding box. + Geom::Affine m = slot.get_units().get_matrix_user2filterunits().inverse(); + Geom::Point bbox_00 = Geom::Point(0,0) * m; + Geom::Point bbox_w0 = Geom::Point(1,0) * m; + Geom::Point bbox_0h = Geom::Point(0,1) * m; + double bbox_width = Geom::distance(bbox_00, bbox_w0); + double bbox_height = Geom::distance(bbox_00, bbox_0h); + + if( feImageWidth == 0 ) feImageWidth = bbox_width; + if( feImageHeight == 0 ) feImageHeight = bbox_height; + + int device_scale = slot.get_device_scale(); + + // Internal image, like <use> + if (from_element) { + std::cout << " Internal image" << std::endl; + if (!SVGElem) return; + + // TODO: do not recreate the rendering tree every time + // TODO: the entire thing is a hack, we should give filter primitives an "update" method + // like the one for DrawingItems + document->ensureUpToDate(); + + Drawing drawing; + Geom::OptRect optarea = SVGElem->visualBounds(); + if (!optarea) return; + + unsigned const key = SPItem::display_key_new(1); + DrawingItem *ai = SVGElem->invoke_show(drawing, key, SP_ITEM_SHOW_DISPLAY); + if (!ai) { + g_warning("feImage renderer: error creating DrawingItem for SVG Element"); + return; + } + drawing.setRoot(ai); + + Geom::Rect area = *optarea; + Geom::Affine user2pb = slot.get_units().get_matrix_user2pb(); + + /* FIXME: These variables are currently unused. Why were they calculated? + double scaleX = feImageWidth / area.width(); + double scaleY = feImageHeight / area.height(); + */ + + Geom::Rect sa = slot.get_slot_area(); + cairo_surface_t *out = + cairo_image_surface_create(CAIRO_FORMAT_ARGB32, + sa.width() * device_scale, + sa.height() * device_scale); + cairo_surface_set_device_scale(out, device_scale, device_scale); + + Inkscape::DrawingContext dc(out, sa.min()); + dc.transform(user2pb); // we are now in primitive units + dc.translate(feImageX, feImageY); +// dc.scale(scaleX, scaleY); No scaling should be done + + Geom::IntRect render_rect = area.roundOutwards(); +// dc.translate(render_rect.min()); This seems incorrect + + // Update to renderable state + drawing.update(render_rect); + drawing.render(dc, render_rect); + SVGElem->invoke_hide(key); + + // For the moment, we'll assume that any image is in sRGB color space + set_cairo_surface_ci(out, SP_CSS_COLOR_INTERPOLATION_SRGB); + + slot.set(_output, out); + cairo_surface_destroy(out); + std::cout << " feImage: out: " << cairo_image_surface_get_width( out) << std::endl; + std::cout << "FilterImage::render_cairo: Exit 2" << std::endl; + return; + } + + // External image, like <image> + if (!image && !broken_ref) { + std::cout << " External image" << std::endl; + broken_ref = true; + + /* TODO: If feImageHref is absolute, then use that (preferably handling the + * case that it's not a file URI). Otherwise, go up the tree looking + * for an xml:base attribute, and use that as the base URI for resolving + * the relative feImageHref URI. Otherwise, if document->base is valid, + * then use that as the base URI. Otherwise, use feImageHref directly + * (i.e. interpreting it as relative to our current working directory). + * (See http://www.w3.org/TR/xmlbase/#resolution .) */ + gchar *fullname = feImageHref; + if ( !g_file_test( fullname, G_FILE_TEST_EXISTS ) ) { + // Try to load from relative position combined with document base + if( document ) { + fullname = g_build_filename( document->getDocumentBase(), feImageHref, NULL ); + } + } + if ( !g_file_test( fullname, G_FILE_TEST_EXISTS ) ) { + // Should display Broken Image png. + g_warning("FilterImage::render: Can not find: %s", feImageHref ); + return; + } + image = Inkscape::Pixbuf::create_from_file(fullname); + if( fullname != feImageHref ) g_free( fullname ); + + if ( !image ) { + g_warning("FilterImage::render: failed to load image: %s", feImageHref); + return; + } + + broken_ref = false; + } + + if (broken_ref) { + return; + } + + cairo_surface_t *image_surface = image->getSurfaceRaw(); + std::cout << " image: " << cairo_image_surface_get_width(image_surface) << std::endl; + Geom::Rect sa = slot.get_slot_area(); + cairo_surface_t *out = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, + sa.width() * device_scale, sa.height() * device_scale); + cairo_surface_set_device_scale( out, device_scale, device_scale ); + std::cout << " out: " << cairo_image_surface_get_width(out) << std::endl; + + // For the moment, we'll assume that any image is in sRGB color space + // set_cairo_surface_ci(out, SP_CSS_COLOR_INTERPOLATION_SRGB); + // This seemed like a sensible thing to do but it breaks filters-displace-01-f.svg + + cairo_t *ct = cairo_create(out); + cairo_translate(ct, -sa.min()[Geom::X], -sa.min()[Geom::Y]); + + // now ct is in pb coordinates, note the feWidth etc. are in user units + ink_cairo_transform(ct, slot.get_units().get_matrix_user2pb()); + + // now ct is in the coordinates of feImageX etc. + + // Now that we have the viewport, we must map image inside. + // Partially copied from sp-image.cpp. + + // Do nothing if preserveAspectRatio is "none". + if( aspect_align != SP_ASPECT_NONE ) { + + // Check aspect ratio of image vs. viewport + double feAspect = feImageHeight/feImageWidth; + double aspect = (double)image->height()/(double)image->width(); + bool ratio = (feAspect < aspect); + + double ax, ay; // Align side + switch( aspect_align ) { + case SP_ASPECT_XMIN_YMIN: + ax = 0.0; + ay = 0.0; + break; + case SP_ASPECT_XMID_YMIN: + ax = 0.5; + ay = 0.0; + break; + case SP_ASPECT_XMAX_YMIN: + ax = 1.0; + ay = 0.0; + break; + case SP_ASPECT_XMIN_YMID: + ax = 0.0; + ay = 0.5; + break; + case SP_ASPECT_XMID_YMID: + ax = 0.5; + ay = 0.5; + break; + case SP_ASPECT_XMAX_YMID: + ax = 1.0; + ay = 0.5; + break; + case SP_ASPECT_XMIN_YMAX: + ax = 0.0; + ay = 1.0; + break; + case SP_ASPECT_XMID_YMAX: + ax = 0.5; + ay = 1.0; + break; + case SP_ASPECT_XMAX_YMAX: + ax = 1.0; + ay = 1.0; + break; + default: + ax = 0.0; + ay = 0.0; + break; + } + + if( aspect_clip == SP_ASPECT_SLICE ) { + // image clipped by viewbox + + if( ratio ) { + // clip top/bottom + feImageY -= ay * (feImageWidth * aspect - feImageHeight); + feImageHeight = feImageWidth * aspect; + } else { + // clip sides + feImageX -= ax * (feImageHeight / aspect - feImageWidth); + feImageWidth = feImageHeight / aspect; + } + + } else { + // image fits into viewbox + + if( ratio ) { + // fit to height + feImageX += ax * (feImageWidth - feImageHeight / aspect ); + feImageWidth = feImageHeight / aspect; + } else { + // fit to width + feImageY += ay * (feImageHeight - feImageWidth * aspect); + feImageHeight = feImageWidth * aspect; + } + } + } + + double scaleX = feImageWidth / image->width(); + double scaleY = feImageHeight / image->height(); + + cairo_translate(ct, feImageX, feImageY); + cairo_scale(ct, scaleX, scaleY); + cairo_set_source_surface(ct, image_surface, 0, 0); + cairo_paint(ct); + cairo_destroy(ct); + slot.set(_output, out); + std::cout << "FilterImage::render_cairo: Exit 2" << std::endl; +} + +bool FilterImage::can_handle_affine(Geom::Affine const &) +{ + return true; +} + +double FilterImage::complexity(Geom::Affine const &) +{ + // TODO: right now we cannot actually measure this in any meaningful way. + return 1.1; +} + +void FilterImage::set_href(const gchar *href){ + + if (feImageHref) g_free (feImageHref); + feImageHref = (href) ? g_strdup (href) : nullptr; + + delete image; + image = nullptr; + broken_ref = false; +} + +void FilterImage::set_document(SPDocument *doc){ + document = doc; +} + +void FilterImage::set_align( unsigned int align ) { + aspect_align = align; +} + +void FilterImage::set_clip( unsigned int clip ) { + aspect_clip = clip; +} + +} /* namespace Filters */ +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter-image.h b/src/display/nr-filter-image.h new file mode 100644 index 0000000..6080bb4 --- /dev/null +++ b/src/display/nr-filter-image.h @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_NR_FILTER_IMAGE_H +#define SEEN_NR_FILTER_IMAGE_H + +/* + * feImage filter primitive renderer + * + * Authors: + * Niko Kiirala <niko@kiirala.com> + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "display/nr-filter-primitive.h" + +class SPDocument; +class SPItem; + +namespace Inkscape { +class Pixbuf; + +namespace Filters { +class FilterSlot; + +class FilterImage : public FilterPrimitive { +public: + FilterImage(); + static FilterPrimitive *create(); + ~FilterImage() override; + + void render_cairo(FilterSlot &slot) override; + bool can_handle_affine(Geom::Affine const &) override; + double complexity(Geom::Affine const &ctm) override; + + void set_document( SPDocument *document ); + void set_href(char const *href); + void set_align( unsigned int align ); + void set_clip( unsigned int clip ); + bool from_element; + SPItem* SVGElem; + + Glib::ustring name() override { return Glib::ustring("Image"); } + +private: + SPDocument *document; + char *feImageHref; + Inkscape::Pixbuf *image; + float feImageX, feImageY, feImageWidth, feImageHeight; + unsigned int aspect_align, aspect_clip; + bool broken_ref; +}; + +} /* namespace Filters */ +} /* namespace Inkscape */ + +#endif /* __NR_FILTER_IMAGE_H__ */ +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter-merge.cpp b/src/display/nr-filter-merge.cpp new file mode 100644 index 0000000..9ebfe40 --- /dev/null +++ b/src/display/nr-filter-merge.cpp @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * feMerge filter effect renderer + * + * Authors: + * Niko Kiirala <niko@kiirala.com> + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <vector> +#include "display/cairo-utils.h" +#include "display/nr-filter-merge.h" +#include "display/nr-filter-slot.h" +#include "display/nr-filter-utils.h" + +namespace Inkscape { +namespace Filters { + +FilterMerge::FilterMerge() : + _input_image(1, NR_FILTER_SLOT_NOT_SET) +{} + +FilterPrimitive * FilterMerge::create() { + return new FilterMerge(); +} + +FilterMerge::~FilterMerge() += default; + +void FilterMerge::render_cairo(FilterSlot &slot) +{ + if (_input_image.empty()) return; + + SPColorInterpolation ci_fp = SP_CSS_COLOR_INTERPOLATION_AUTO; + if( _style ) { + ci_fp = (SPColorInterpolation)_style->color_interpolation_filters.computed; + } + + Geom::Rect vp = filter_primitive_area( slot.get_units() ); + slot.set_primitive_area(_output, vp); // Needed for tiling + + // output is RGBA if at least one input is RGBA + bool rgba32 = false; + cairo_surface_t *out = nullptr; + for (int & i : _input_image) { + cairo_surface_t *in = slot.getcairo(i); + if (cairo_surface_get_content(in) == CAIRO_CONTENT_COLOR_ALPHA) { + out = ink_cairo_surface_create_identical(in); + set_cairo_surface_ci( out, ci_fp ); + rgba32 = true; + break; + } + } + + if (!rgba32) { + out = ink_cairo_surface_create_identical(slot.getcairo(_input_image[0])); + } + cairo_t *out_ct = cairo_create(out); + + for (int & i : _input_image) { + cairo_surface_t *in = slot.getcairo(i); + + set_cairo_surface_ci( in, ci_fp ); + cairo_set_source_surface(out_ct, in, 0, 0); + cairo_paint(out_ct); + } + + cairo_destroy(out_ct); + slot.set(_output, out); + cairo_surface_destroy(out); +} + +bool FilterMerge::can_handle_affine(Geom::Affine const &) +{ + // Merge is a per-pixel primitive and is immutable under transformations + return true; +} + +double FilterMerge::complexity(Geom::Affine const &) +{ + return 1.02; +} + +bool FilterMerge::uses_background() +{ + for (int input : _input_image) { + if (input == NR_FILTER_BACKGROUNDIMAGE || input == NR_FILTER_BACKGROUNDALPHA) { + return true; + } + } + return false; +} + +void FilterMerge::set_input(int slot) { + _input_image[0] = slot; +} + +void FilterMerge::set_input(int input, int slot) { + if (input < 0) return; + + if (static_cast<int>(_input_image.size()) > input) { + _input_image[input] = slot; + } else { + for (int i = static_cast<int>(_input_image.size()) ; i < input ; i++) { + _input_image.push_back(NR_FILTER_SLOT_NOT_SET); + } + _input_image.push_back(slot); + } +} + +} /* namespace Filters */ +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter-merge.h b/src/display/nr-filter-merge.h new file mode 100644 index 0000000..72356dc --- /dev/null +++ b/src/display/nr-filter-merge.h @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_NR_FILTER_MERGE_H +#define SEEN_NR_FILTER_MERGE_H + +/* + * feMerge filter effect renderer + * + * Authors: + * Niko Kiirala <niko@kiirala.com> + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <vector> +#include "display/nr-filter-primitive.h" + +namespace Inkscape { +namespace Filters { + +class FilterMerge : public FilterPrimitive { +public: + FilterMerge(); + static FilterPrimitive *create(); + ~FilterMerge() override; + + void render_cairo(FilterSlot &) override; + bool can_handle_affine(Geom::Affine const &) override; + double complexity(Geom::Affine const &ctm) override; + bool uses_background() override; + + void set_input(int input) override; + void set_input(int input, int slot) override; + + Glib::ustring name() override { return Glib::ustring("Merge"); } + +private: + std::vector<int> _input_image; +}; + +} /* namespace Filters */ +} /* namespace Inkscape */ + +#endif /* __NR_FILTER_MERGE_H__ */ +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter-morphology.cpp b/src/display/nr-filter-morphology.cpp new file mode 100644 index 0000000..700d69f --- /dev/null +++ b/src/display/nr-filter-morphology.cpp @@ -0,0 +1,264 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * feMorphology filter primitive renderer + * + * Authors: + * Felipe CorrĂȘa da Silva Sanches <juca@members.fsf.org> + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include <cmath> +#include <algorithm> +#include <deque> +#include <functional> +#include "display/cairo-templates.h" +#include "display/cairo-utils.h" +#include "display/nr-filter-morphology.h" +#include "display/nr-filter-slot.h" +#include "display/nr-filter-units.h" + +namespace Inkscape { +namespace Filters { + +FilterMorphology::FilterMorphology() += default; + +FilterPrimitive * FilterMorphology::create() { + return new FilterMorphology(); +} + +FilterMorphology::~FilterMorphology() += default; + +enum MorphologyOp { + ERODE, + DILATE +}; + +namespace { + +/* This performs one "half" of the morphology operation by calculating + * the componentwise extreme in the specified axis with the given radius. + * Extreme of row extremes is equal to the extreme of components, so this + * doesn't change the result. + * The algorithm is due to: Petr DoklĂĄdal, Eva DoklĂĄdalovĂĄ (2011), "Computationally efficient, one-pass algorithm for morphological filters" + * TODO: Currently only the 1D algorithm is implemented, but it should not be too difficult (and at the very least more memory efficient) to implement the full 2D algorithm. + * One problem with the 2D algorithm is that it is harder to parallelize. + */ +template <typename Comparison, Geom::Dim2 axis, int BPP> +void morphologicalFilter1D(cairo_surface_t * const input, cairo_surface_t * const out, double radius) { + Comparison comp; + + int w = cairo_image_surface_get_width(out); + int h = cairo_image_surface_get_height(out); + if (axis == Geom::Y) std::swap(w,h); + + int stridein = cairo_image_surface_get_stride(input); + int strideout = cairo_image_surface_get_stride(out); + + unsigned char *in_data = cairo_image_surface_get_data(input); + unsigned char *out_data = cairo_image_surface_get_data(out); + + int ri = round(radius); // TODO: Support fractional radii? + int wi = 2*ri+1; + + #if HAVE_OPENMP + int limit = w * h; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int numOfThreads = prefs->getIntLimited("/options/threading/numthreads", omp_get_num_procs(), 1, 256); + (void) numOfThreads; // suppress unused variable warning + #pragma omp parallel for if(limit > OPENMP_THRESHOLD) num_threads(numOfThreads) + #endif // HAVE_OPENMP + for (int i = 0; i < h; ++i) { + // TODO: Store position and value in one 32 bit integer? 24 bits should be enough for a position, it would be quite strange to have an image with a width/height of more than 16 million(!). + std::deque< std::pair<int,unsigned char> > vals[BPP]; // In my tests it was actually slightly faster to allocate it here than allocate it once for all threads and retrieving the correct set based on the thread id. + + // Initialize with transparent black + for(int p = 0; p < BPP; ++p) { + vals[p].push_back(std::pair<int,unsigned char>(-1,0)); // TODO: Only do this when performing an erosion? + } + + // Process "row" + unsigned char *in_p = in_data + i * (axis == Geom::X ? stridein : BPP); + unsigned char *out_p = out_data + i * (axis == Geom::X ? strideout : BPP); + /* This is the "short but slow" version, which might be easier to follow, the longer but faster version follows. + for (int j = 0; j < w+ri; ++j) { + for(int p = 0; p < BPP; ++p) { // Iterate over channels + // Push new value onto FIFO, erasing any previous values that are "useless" (see paper) or out-of-range + if (!vals[p].empty() && vals[p].front().first+wi <= j) vals[p].pop_front(); // out-of-range + if (j < w) { + while(!vals[p].empty() && !comp(vals[p].back().second, *in_p)) vals[p].pop_back(); // useless + vals[p].push_back(std::make_pair(j, *in_p)); + ++in_p; + } else if (j == w) { // Transparent black beyond the image. TODO: Only do this when performing an erosion? + while(!vals[p].empty() && !comp(vals[p].back().second, 0)) vals[p].pop_back(); + vals[p].push_back(std::make_pair(j, 0)); + } + // Set output + if (j >= ri) { + *out_p = vals[p].front().second; + ++out_p; + } + } + if (axis == Geom::Y && j < w ) in_p += stridein - BPP; + if (axis == Geom::Y && j >= ri) out_p += strideout - BPP; + }*/ + for (int j = 0; j < std::min(ri,w); ++j) { + for(int p = 0; p < BPP; ++p) { // Iterate over channels + // Push new value onto FIFO, erasing any previous values that are "useless" (see paper) or out-of-range + if (!vals[p].empty() && vals[p].front().first <= j) vals[p].pop_front(); // out-of-range + while(!vals[p].empty() && !comp(vals[p].back().second, *in_p)) vals[p].pop_back(); // useless + vals[p].push_back(std::make_pair(j+wi, *in_p)); + ++in_p; + } + if (axis == Geom::Y) in_p += stridein - BPP; + } + // We have now done all preparatory work. + // If w<=ri, then the following loop does nothing (which is as it should). + for (int j = ri; j < w; ++j) { + for(int p = 0; p < BPP; ++p) { // Iterate over channels + // Push new value onto FIFO, erasing any previous values that are "useless" (see paper) or out-of-range + if (!vals[p].empty() && vals[p].front().first <= j) vals[p].pop_front(); // out-of-range + while(!vals[p].empty() && !comp(vals[p].back().second, *in_p)) vals[p].pop_back(); // useless + vals[p].push_back(std::make_pair(j+wi, *in_p)); + ++in_p; + // Set output + *out_p = vals[p].front().second; + ++out_p; + } + if (axis == Geom::Y) { + in_p += stridein - BPP; + out_p += strideout - BPP; + } + } + // We have now done all work which involves both input and output. + // The following loop makes sure that the border is handled correctly. + for(int p = 0; p < BPP; ++p) { // Iterate over channels + while(!vals[p].empty() && !comp(vals[p].back().second, 0)) vals[p].pop_back(); + vals[p].push_back(std::make_pair(w+wi, 0)); + } + // Now we just have to finish the output. + for (int j = std::max(w,ri); j < w+ri; ++j) { + for(int p = 0; p < BPP; ++p) { // Iterate over channels + // Remove out-of-range values + if (!vals[p].empty() && vals[p].front().first <= j) vals[p].pop_front(); // out-of-range + // Set output + *out_p = vals[p].front().second; + ++out_p; + } + if (axis == Geom::Y) out_p += strideout - BPP; + } + } + + cairo_surface_mark_dirty(out); +} + +} // end anonymous namespace + +void FilterMorphology::render_cairo(FilterSlot &slot) +{ + cairo_surface_t *input = slot.getcairo(_input); + + if (xradius == 0.0 || yradius == 0.0) { + // output is transparent black + cairo_surface_t *out = ink_cairo_surface_create_identical(input); + copy_cairo_surface_ci(input, out); + slot.set(_output, out); + cairo_surface_destroy(out); + return; + } + + int device_scale = slot.get_device_scale(); + Geom::Affine p2pb = slot.get_units().get_matrix_primitiveunits2pb(); + double xr = fabs(xradius * p2pb.expansionX()) * device_scale; + double yr = fabs(yradius * p2pb.expansionY()) * device_scale; + int bpp = cairo_image_surface_get_format(input) == CAIRO_FORMAT_A8 ? 1 : 4; + + cairo_surface_t *interm = ink_cairo_surface_create_identical(input); + + if (Operator == MORPHOLOGY_OPERATOR_DILATE) { + if (bpp == 1) { + morphologicalFilter1D< std::greater<unsigned char>, Geom::X, 1 >(input, interm, xr); + } else { + morphologicalFilter1D< std::greater<unsigned char>, Geom::X, 4 >(input, interm, xr); + } + } else { + if (bpp == 1) { + morphologicalFilter1D< std::less<unsigned char>, Geom::X, 1 >(input, interm, xr); + } else { + morphologicalFilter1D< std::less<unsigned char>, Geom::X, 4 >(input, interm, xr); + } + } + + cairo_surface_t *out = ink_cairo_surface_create_identical(interm); + + // color_interpolation_filters for out same as input. See spec (DisplacementMap). + copy_cairo_surface_ci(input, out); + + if (Operator == MORPHOLOGY_OPERATOR_DILATE) { + if (bpp == 1) { + morphologicalFilter1D< std::greater<unsigned char>, Geom::Y, 1 >(interm, out, yr); + } else { + morphologicalFilter1D< std::greater<unsigned char>, Geom::Y, 4 >(interm, out, yr); + } + } else { + if (bpp == 1) { + morphologicalFilter1D< std::less<unsigned char>, Geom::Y, 1 >(interm, out, yr); + } else { + morphologicalFilter1D< std::less<unsigned char>, Geom::Y, 4 >(interm, out, yr); + } + } + + cairo_surface_destroy(interm); + + slot.set(_output, out); + cairo_surface_destroy(out); +} + +void FilterMorphology::area_enlarge(Geom::IntRect &area, Geom::Affine const &trans) +{ + int enlarge_x = ceil(xradius * trans.expansionX()); + int enlarge_y = ceil(yradius * trans.expansionY()); + + area.expandBy(enlarge_x, enlarge_y); +} + +double FilterMorphology::complexity(Geom::Affine const &trans) +{ + int enlarge_x = ceil(xradius * trans.expansionX()); + int enlarge_y = ceil(yradius * trans.expansionY()); + return enlarge_x * enlarge_y; +} + +void FilterMorphology::set_operator(FilterMorphologyOperator &o){ + Operator = o; +} + +void FilterMorphology::set_xradius(double x){ + xradius = x; +} + +void FilterMorphology::set_yradius(double y){ + yradius = y; +} + +} /* namespace Filters */ +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter-morphology.h b/src/display/nr-filter-morphology.h new file mode 100644 index 0000000..f91192b --- /dev/null +++ b/src/display/nr-filter-morphology.h @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_NR_FILTER_MORPHOLOGY_H +#define SEEN_NR_FILTER_MORPHOLOGY_H + +/* + * feMorphology filter primitive renderer + * + * Authors: + * Felipe CorrĂȘa da Silva Sanches <juca@members.fsf.org> + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "display/nr-filter-primitive.h" + +namespace Inkscape { +namespace Filters { + +class FilterSlot; + +enum FilterMorphologyOperator { + MORPHOLOGY_OPERATOR_ERODE, + MORPHOLOGY_OPERATOR_DILATE, + MORPHOLOGY_OPERATOR_END +}; + +class FilterMorphology : public FilterPrimitive { +public: + FilterMorphology(); + static FilterPrimitive *create(); + ~FilterMorphology() override; + + void render_cairo(FilterSlot &slot) override; + void area_enlarge(Geom::IntRect &area, Geom::Affine const &trans) override; + double complexity(Geom::Affine const &ctm) override; + + void set_operator(FilterMorphologyOperator &o); + void set_xradius(double x); + void set_yradius(double y); + + Glib::ustring name() override { return Glib::ustring("Morphology"); } + +private: + FilterMorphologyOperator Operator; + double xradius; + double yradius; +}; + +} /* namespace Filters */ +} /* namespace Inkscape */ + +#endif /* __NR_FILTER_MORPHOLOGY_H__ */ +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter-offset.cpp b/src/display/nr-filter-offset.cpp new file mode 100644 index 0000000..2f73389 --- /dev/null +++ b/src/display/nr-filter-offset.cpp @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * feOffset filter primitive renderer + * + * Authors: + * Niko Kiirala <niko@kiirala.com> + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "display/cairo-utils.h" +#include "display/nr-filter-offset.h" +#include "display/nr-filter-slot.h" +#include "display/nr-filter-units.h" + +namespace Inkscape { +namespace Filters { + +using Geom::X; +using Geom::Y; + +FilterOffset::FilterOffset() : + dx(0), dy(0) +{} + +FilterPrimitive * FilterOffset::create() { + return new FilterOffset(); +} + +FilterOffset::~FilterOffset() += default; + +void FilterOffset::render_cairo(FilterSlot &slot) +{ + cairo_surface_t *in = slot.getcairo(_input); + cairo_surface_t *out = ink_cairo_surface_create_identical(in); + // color_interpolation_filters for out same as in. See spec (DisplacementMap). + copy_cairo_surface_ci(in, out); + cairo_t *ct = cairo_create(out); + + Geom::Rect vp = filter_primitive_area( slot.get_units() ); + slot.set_primitive_area(_output, vp); // Needed for tiling + + Geom::Affine p2pb = slot.get_units().get_matrix_primitiveunits2pb(); + double x = dx * p2pb.expansionX(); + double y = dy * p2pb.expansionY(); + + cairo_set_source_surface(ct, in, x, y); + cairo_paint(ct); + cairo_destroy(ct); + + slot.set(_output, out); + cairo_surface_destroy(out); +} + +bool FilterOffset::can_handle_affine(Geom::Affine const &) +{ + return true; +} + +void FilterOffset::set_dx(double amount) { + dx = amount; +} + +void FilterOffset::set_dy(double amount) { + dy = amount; +} + +void FilterOffset::area_enlarge(Geom::IntRect &area, Geom::Affine const &trans) +{ + Geom::Point offset(dx, dy); + offset *= trans; + offset[X] -= trans[4]; + offset[Y] -= trans[5]; + double x0, y0, x1, y1; + x0 = area.left(); + y0 = area.top(); + x1 = area.right(); + y1 = area.bottom(); + + if (offset[X] > 0) { + x0 -= ceil(offset[X]); + } else { + x1 -= floor(offset[X]); + } + + if (offset[Y] > 0) { + y0 -= ceil(offset[Y]); + } else { + y1 -= floor(offset[Y]); + } + area = Geom::IntRect(x0, y0, x1, y1); +} + +double FilterOffset::complexity(Geom::Affine const &) +{ + return 1.02; +} + +} /* namespace Filters */ +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter-offset.h b/src/display/nr-filter-offset.h new file mode 100644 index 0000000..30e8527 --- /dev/null +++ b/src/display/nr-filter-offset.h @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_NR_FILTER_OFFSET_H +#define SEEN_NR_FILTER_OFFSET_H + +/* + * feOffset filter primitive renderer + * + * Authors: + * Niko Kiirala <niko@kiirala.com> + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "display/nr-filter-primitive.h" +#include "display/nr-filter-slot.h" +#include "display/nr-filter-units.h" + +namespace Inkscape { +namespace Filters { + +class FilterOffset : public FilterPrimitive { +public: + FilterOffset(); + static FilterPrimitive *create(); + ~FilterOffset() override; + + void render_cairo(FilterSlot &slot) override; + void area_enlarge(Geom::IntRect &area, Geom::Affine const &trans) override; + bool can_handle_affine(Geom::Affine const &) override; + double complexity(Geom::Affine const &ctm) override; + + void set_dx(double amount); + void set_dy(double amount); + + Glib::ustring name() override { return Glib::ustring("Offset"); } + +private: + double dx, dy; +}; + +} /* namespace Filters */ +} /* namespace Inkscape */ + +#endif /* __NR_FILTER_OFFSET_H__ */ +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter-primitive.cpp b/src/display/nr-filter-primitive.cpp new file mode 100644 index 0000000..65133ea --- /dev/null +++ b/src/display/nr-filter-primitive.cpp @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG filters rendering + * + * Author: + * Niko Kiirala <niko@kiirala.com> + * Tavmjong Bah <tavmjong@free.fr> (primitive subregion) + * + * Copyright (C) 2006 Niko Kiirala + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "display/nr-filter-primitive.h" +#include "display/nr-filter-slot.h" +#include "display/nr-filter-types.h" +#include "svg/svg-length.h" + +#include "inkscape.h" +#include "desktop.h" + +#include "document.h" +#include "style.h" + +namespace Inkscape { +namespace Filters { + +using Geom::X; +using Geom::Y; + +FilterPrimitive::FilterPrimitive() +{ + _input = NR_FILTER_SLOT_NOT_SET; + _output = NR_FILTER_SLOT_NOT_SET; + + // Primitive subregion, should default to the union of all subregions of referenced nodes + // (i.e. other filter primitives except feTile). If no referenced nodes, defaults to filter + // region expressed in percent. At the moment, we do not check referenced nodes. + + // We must keep track if a value is set or not, if not set then the region defaults to 0%, 0%, + // 100%, 100% ("x", "y", "width", "height") of the -> filter <- region. If set, then + // percentages are in terms of bounding box or viewbox, depending on value of "primitiveUnits". + + // NB: SVGLength.set takes prescaled percent values: 1 means 100% + _subregion_x.unset(SVGLength::PERCENT, 0, 0); + _subregion_y.unset(SVGLength::PERCENT, 0, 0); + _subregion_width.unset(SVGLength::PERCENT, 1, 0); + _subregion_height.unset(SVGLength::PERCENT, 1, 0); + + _style = nullptr; +} + +FilterPrimitive::~FilterPrimitive() +{ + if(_style) + sp_style_unref(_style); +} + +void FilterPrimitive::render_cairo(FilterSlot &slot) +{ + // passthrough + cairo_surface_t *in = slot.getcairo(_input); + slot.set(_output, in); +} + +void FilterPrimitive::area_enlarge(Geom::IntRect &/*area*/, Geom::Affine const &/*m*/) +{ + // This doesn't need to do anything by default +} + +void FilterPrimitive::set_input(int slot) { + set_input(0, slot); +} + +void FilterPrimitive::set_input(int input, int slot) { + if (input == 0) _input = slot; +} + +void FilterPrimitive::set_output(int slot) { + if (slot >= 0) _output = slot; +} + +// We need to copy reference even if unset as we need to know if +// someone has unset a value. +void FilterPrimitive::set_x(SVGLength const &length) +{ + _subregion_x = length; +} + +void FilterPrimitive::set_y(SVGLength const &length) +{ + _subregion_y = length; +} +void FilterPrimitive::set_width(SVGLength const &length) +{ + _subregion_width = length; +} +void FilterPrimitive::set_height(SVGLength const &length) +{ + _subregion_height = length; +} + +void FilterPrimitive::set_subregion(SVGLength const &x, SVGLength const &y, + SVGLength const &width, SVGLength const &height) { + _subregion_x = x; + _subregion_y = y; + _subregion_width = width; + _subregion_height = height; +} + +Geom::Rect FilterPrimitive::filter_primitive_area(FilterUnits const &units) +{ + Geom::OptRect const fa_opt = units.get_filter_area(); + if (!fa_opt) { + std::cerr << "FilterPrimitive::filter_primitive_area: filter area undefined." << std::endl; + return Geom::Rect (Geom::Point(0.,0.), Geom::Point(0.,0.)); + } + Geom::Rect fa = *fa_opt; + + // x, y, width, and height are independently defined (i.e. one can be defined, by default, to + // the filter area (via default value ) while another is defined relative to the bounding + // box). It is better to keep track of them separately and then compose the Rect at the end. + double x = 0; + double y = 0; + double width = 0; + double height = 0; + + // If subregion not set, by special case use filter region. + if( !_subregion_x._set ) x = fa.min()[X]; + if( !_subregion_y._set ) y = fa.min()[Y]; + if( !_subregion_width._set ) width = fa.width(); + if( !_subregion_height._set ) height = fa.height(); + + if( units.get_primitive_units() == SP_FILTER_UNITS_OBJECTBOUNDINGBOX ) { + + Geom::OptRect const bb_opt = units.get_item_bbox(); + if (!bb_opt) { + std::cerr << "FilterPrimitive::filter_primitive_area: bounding box undefined and 'primitiveUnits' is 'objectBoundingBox'." << std::endl; + return Geom::Rect (Geom::Point(0.,0.), Geom::Point(0.,0.)); + } + Geom::Rect bb = *bb_opt; + + + // Update computed values for ex, em, %. + // For %, assumes primitive unit is objectBoundingBox. + // TODO: fetch somehow the object ex and em lengths; 12, 6 are just dummy values. + double len_x = bb.width(); + double len_y = bb.height(); + _subregion_x.update(12, 6, len_x); + _subregion_y.update(12, 6, len_y); + _subregion_width.update(12, 6, len_x); + _subregion_height.update(12, 6, len_y); + + // Values are in terms of fraction of bounding box. + if( _subregion_x._set && (_subregion_x.unit != SVGLength::PERCENT) ) x = bb.min()[X] + bb.width() * _subregion_x.value; + if( _subregion_y._set && (_subregion_y.unit != SVGLength::PERCENT) ) y = bb.min()[Y] + bb.height() * _subregion_y.value; + if( _subregion_width._set && (_subregion_width.unit != SVGLength::PERCENT) ) width = bb.width() * _subregion_width.value; + if( _subregion_height._set && (_subregion_height.unit != SVGLength::PERCENT) ) height = bb.height() * _subregion_height.value; + // Values are in terms of percent + if( _subregion_x._set && (_subregion_x.unit == SVGLength::PERCENT) ) x = bb.min()[X] + _subregion_x.computed; + if( _subregion_y._set && (_subregion_y.unit == SVGLength::PERCENT) ) y = bb.min()[Y] + _subregion_y.computed; + if( _subregion_width._set && (_subregion_width.unit == SVGLength::PERCENT) ) width = _subregion_width.computed; + if( _subregion_height._set && (_subregion_height.unit == SVGLength::PERCENT) ) height = _subregion_height.computed; + } else { + // Values are in terms of user space coordinates or percent of viewport (already calculated in sp-filter-primitive.cpp). + if( _subregion_x._set ) x = _subregion_x.computed; + if( _subregion_y._set ) y = _subregion_y.computed; + if( _subregion_width._set ) width = _subregion_width.computed; + if( _subregion_height._set ) height = _subregion_height.computed; + } + + return Geom::Rect (Geom::Point(x,y), Geom::Point(x + width, y + height)); +} + +void FilterPrimitive::setStyle(SPStyle *style) +{ + if( style != _style ) { + if (style) sp_style_ref(style); + if (_style) sp_style_unref(_style); + _style = style; + } +} + + +} /* namespace Filters */ +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter-primitive.h b/src/display/nr-filter-primitive.h new file mode 100644 index 0000000..89cffac --- /dev/null +++ b/src/display/nr-filter-primitive.h @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG filters rendering + * + * Author: + * Niko Kiirala <niko@kiirala.com> + * + * Copyright (C) 2006-2007 Niko Kiirala + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_NR_FILTER_PRIMITIVE_H +#define SEEN_NR_FILTER_PRIMITIVE_H + +#include <2geom/forward.h> +#include <2geom/rect.h> + +#include <glibmm/ustring.h> + +#include "display/nr-filter-types.h" +#include "svg/svg-length.h" + +class SPStyle; + +namespace Inkscape { +namespace Filters { + +class FilterSlot; +class FilterUnits; + +class FilterPrimitive { +public: + FilterPrimitive(); + virtual ~FilterPrimitive(); + + virtual void render_cairo(FilterSlot &slot); + virtual int render(FilterSlot & /*slot*/, FilterUnits const & /*units*/) { return 0; } // pure virtual? + virtual void area_enlarge(Geom::IntRect &area, Geom::Affine const &m); + + /** + * Sets the input slot number 'slot' to be used as input in rendering + * filter primitive 'primitive' + * For filter primitive types accepting more than one input, this sets the + * first input. + * If any of the required input slots is not set, the output of previous + * filter primitive is used, or SourceGraphic if this is the first + * primitive for this filter. + */ + virtual void set_input(int slot); + + /** + * Sets the input slot number 'slot' to be user as input number 'input' in + * rendering filter primitive 'primitive' + * First input for a filter primitive is number 0. For primitives with + * attributes 'in' and 'in2', these are numbered 0 and 1, respectively. + * If any of required input slots for a filter is not set, the output of + * previous filter primitive is used, or SourceGraphic if this is the first + * filter primitive for this filter. + */ + virtual void set_input(int input, int slot); + + /** + * Sets the slot number 'slot' to be used as output from filter primitive + * 'primitive' + * If output slot for a filter element is not set, one of the unused image + * slots is used. + * It is an error to specify a pre-defined slot as 'slot'. Such call does + * not have any effect to the state of filter or its primitives. + */ + virtual void set_output(int slot); + + // returns cache score factor, reflecting the cost of rendering this filter + // this should return how many times slower this primitive is that normal rendering + virtual double complexity(Geom::Affine const &/*ctm*/) { return 1.0; } + + virtual bool uses_background() { + if (_input == NR_FILTER_BACKGROUNDIMAGE || _input == NR_FILTER_BACKGROUNDALPHA) { + return true; + } else { + return false; + } + } + + /** + * Sets the filter primitive subregion. Passing an unset length + * (length._set == false) WILL change the parameter as it is + * important to know if a parameter is unset. + */ + void set_x(SVGLength const &length); + void set_y(SVGLength const &length); + void set_width(SVGLength const &length); + void set_height(SVGLength const &length); + void set_subregion(SVGLength const &x, SVGLength const &y, + SVGLength const &width, SVGLength const &height); + + /** + * Resets the filter primitive subregion to its default value + */ + void reset_subregion(); // Not implemented + + /** + * Returns the filter primitive area in user coordinate system. + */ + Geom::Rect filter_primitive_area(FilterUnits const &units); + + /** + *Indicate whether the filter primitive can handle the given affine. + * + * Results of some filter primitives depend on the coordinate system used when rendering. + * A gaussian blur with equal x and y deviation will remain unchanged by rotations. + * Per-pixel filters like color matrix and blend will not change regardless of + * the transformation. + * + * When any filter returns false, filter rendering is performed on an intermediate surface + * with edges parallel to the axes of the user coordinate system. This means + * the matrices from FilterUnits will contain at most a (possibly non-uniform) scale + * and a translation. When all primitives of the filter return true, the rendering is + * performed in display coordinate space and no intermediate surface is used. + */ + virtual bool can_handle_affine(Geom::Affine const &) { return false; } + + /** + * Sets style for access to properties used by filter primitives. + */ + void setStyle(SPStyle *style); + + // Useful for debugging + virtual Glib::ustring name() { return Glib::ustring("No name"); } + +protected: + int _input; + int _output; + + /* Filter primitive subregion */ + SVGLength _subregion_x; + SVGLength _subregion_y; + SVGLength _subregion_width; + SVGLength _subregion_height; + + SPStyle *_style; +}; + + +} /* namespace Filters */ +} /* namespace Inkscape */ + +#endif /* SEEN_NR_FILTER_PRIMITIVE_H */ +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter-skeleton.cpp b/src/display/nr-filter-skeleton.cpp new file mode 100644 index 0000000..072abc1 --- /dev/null +++ b/src/display/nr-filter-skeleton.cpp @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Filter primitive renderer skeleton class + * You can create your new filter primitive renderer by replacing + * all occurrences of Skeleton, skeleton and SKELETON with your filter + * type, like gaussian or blend in respective case. + * + * This can be accomplished with the following sed command: + * sed -e "s/Skeleton/Name/g" -e "s/skeleton/name/" -e "s/SKELETON/NAME/" + * nr-filter-skeleton.cpp >nr-filter-name.cpp + * + * (on one line, replace occurrences of 'name' with your filter name) + * + * Remember to convert the .h file too. The sed command is same for both + * files. + * + * Authors: + * Niko Kiirala <niko@kiirala.com> + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "display/nr-filter-skeleton.h" +#include "display/cairo-utils.h" +#include "display/nr-filter-slot.h" +#include "display/nr-filter-units.h" + +namespace Inkscape { +namespace Filters { + +FilterSkeleton::FilterSkeleton() +{} + +FilterPrimitive * FilterSkeleton::create() { + return new FilterSkeleton(); +} + +FilterSkeleton::~FilterSkeleton() +{} + +void FilterSkeleton::render_cairo(FilterSlot &slot) { + cairo_surface_t *in = slot.getcairo(_input); + cairo_surface_t *out = ink_cairo_surface_create_identical(in); + +// cairo_t *ct = cairo_create(out); + +// cairo_set_source_surface(ct, in, offset[X], offset[Y]); +// cairo_paint(ct); +// cairo_destroy(ct); + + slot.set(_output, out); + cairo_surface_destroy(out); +} + +} /* namespace Filters */ +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter-skeleton.h b/src/display/nr-filter-skeleton.h new file mode 100644 index 0000000..83f1807 --- /dev/null +++ b/src/display/nr-filter-skeleton.h @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_NR_FILTER_SKELETON_H +#define SEEN_NR_FILTER_SKELETON_H + +/* + * Filter primitive renderer skeleton class + * You can create your new filter primitive renderer by replacing + * all occurrences of Skeleton, skeleton and SKELETON with your filter + * type, like gaussian or blend in respective case. + * + * This can be accomplished with the following sed command: + * sed -e "s/Skeleton/Name/g" -e "s/skeleton/name/" -e "s/SKELETON/NAME/" + * nr-filter-skeleton.h >nr-filter-name.h + * + * (on one line, replace occurrences of 'name' with your filter name) + * + * Remember to convert the .cpp file too. The sed command is same for both + * files. + * + * Authors: + * Niko Kiirala <niko@kiirala.com> + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "display/nr-filter-primitive.h" +#include "display/nr-filter-slot.h" +#include "display/nr-filter-units.h" + +namespace Inkscape { +namespace Filters { + +class FilterSkeleton : public FilterPrimitive { +public: + FilterSkeleton(); + static FilterPrimitive *create(); + virtual ~FilterSkeleton(); + + virtual void render_cairo(FilterSlot &slot); + +private: + +}; + +} /* namespace Filters */ +} /* namespace Inkscape */ + +#endif /* __NR_FILTER_SKELETON_H__ */ +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter-slot.cpp b/src/display/nr-filter-slot.cpp new file mode 100644 index 0000000..4d9dc23 --- /dev/null +++ b/src/display/nr-filter-slot.cpp @@ -0,0 +1,301 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A container class for filter slots. Allows for simple getting and + * setting images in filter slots without having to bother with + * table indexes and such. + * + * Author: + * Niko Kiirala <niko@kiirala.com> + * + * Copyright (C) 2006,2007 Niko Kiirala + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cassert> +#include <cstring> + +#include <2geom/transforms.h> +#include "display/cairo-utils.h" +#include "display/drawing-context.h" +#include "display/nr-filter-types.h" +#include "display/nr-filter-gaussian.h" +#include "display/nr-filter-slot.h" +#include "display/nr-filter-units.h" + +namespace Inkscape { +namespace Filters { + +FilterSlot::FilterSlot(DrawingItem *item, DrawingContext *bgdc, + DrawingContext &graphic, FilterUnits const &u) + : _item(item) + , _source_graphic(graphic.rawTarget()) + , _background_ct(bgdc ? bgdc->raw() : nullptr) + , _source_graphic_area(graphic.targetLogicalBounds().roundOutwards()) // fixme + , _background_area(bgdc ? bgdc->targetLogicalBounds().roundOutwards() : Geom::IntRect()) // fixme + , _units(u) + , _last_out(NR_FILTER_SOURCEGRAPHIC) + , filterquality(FILTER_QUALITY_BEST) + , blurquality(BLUR_QUALITY_BEST) +{ + using Geom::X; + using Geom::Y; + + // compute slot bbox + Geom::Affine trans = _units.get_matrix_display2pb(); + Geom::Rect bbox_trans = graphic.targetLogicalBounds() * trans; + Geom::Point min = bbox_trans.min(); + _slot_x = min[X]; + _slot_y = min[Y]; + + if (trans.isTranslation()) { + _slot_w = _source_graphic_area.width(); + _slot_h = _source_graphic_area.height(); + } else { + _slot_w = ceil(bbox_trans.width()); + _slot_h = ceil(bbox_trans.height()); + } +} + +FilterSlot::~FilterSlot() +{ + for (auto & _slot : _slots) { + cairo_surface_destroy(_slot.second); + } +} + +cairo_surface_t *FilterSlot::getcairo(int slot_nr) +{ + if (slot_nr == NR_FILTER_SLOT_NOT_SET) + slot_nr = _last_out; + + SlotMap::iterator s = _slots.find(slot_nr); + + /* If we didn't have the specified image, but we could create it + * from the other information we have, let's do that */ + if (s == _slots.end() + && (slot_nr == NR_FILTER_SOURCEGRAPHIC + || slot_nr == NR_FILTER_SOURCEALPHA + || slot_nr == NR_FILTER_BACKGROUNDIMAGE + || slot_nr == NR_FILTER_BACKGROUNDALPHA + || slot_nr == NR_FILTER_FILLPAINT + || slot_nr == NR_FILTER_STROKEPAINT)) + { + switch (slot_nr) { + case NR_FILTER_SOURCEGRAPHIC: { + cairo_surface_t *tr = _get_transformed_source_graphic(); + // Assume all source graphics are sRGB + set_cairo_surface_ci( tr, SP_CSS_COLOR_INTERPOLATION_SRGB ); + _set_internal(NR_FILTER_SOURCEGRAPHIC, tr); + cairo_surface_destroy(tr); + } break; + case NR_FILTER_BACKGROUNDIMAGE: { + cairo_surface_t *bg = _get_transformed_background(); + // Assume all backgrounds are sRGB + set_cairo_surface_ci( bg, SP_CSS_COLOR_INTERPOLATION_SRGB ); + _set_internal(NR_FILTER_BACKGROUNDIMAGE, bg); + cairo_surface_destroy(bg); + } break; + case NR_FILTER_SOURCEALPHA: { + cairo_surface_t *src = getcairo(NR_FILTER_SOURCEGRAPHIC); + cairo_surface_t *alpha = ink_cairo_extract_alpha(src); + _set_internal(NR_FILTER_SOURCEALPHA, alpha); + cairo_surface_destroy(alpha); + } break; + case NR_FILTER_BACKGROUNDALPHA: { + cairo_surface_t *src = getcairo(NR_FILTER_BACKGROUNDIMAGE); + cairo_surface_t *ba = ink_cairo_extract_alpha(src); + _set_internal(NR_FILTER_BACKGROUNDALPHA, ba); + cairo_surface_destroy(ba); + } break; + case NR_FILTER_FILLPAINT: //TODO + case NR_FILTER_STROKEPAINT: //TODO + default: + break; + } + s = _slots.find(slot_nr); + } + + if (s == _slots.end()) { + // create empty surface + cairo_surface_t *empty = cairo_surface_create_similar( + _source_graphic, cairo_surface_get_content(_source_graphic), + _slot_w, _slot_h); + _set_internal(slot_nr, empty); + cairo_surface_destroy(empty); + s = _slots.find(slot_nr); + } + return s->second; +} + +cairo_surface_t *FilterSlot::_get_transformed_source_graphic() +{ + Geom::Affine trans = _units.get_matrix_display2pb(); + + if (trans.isTranslation()) { + cairo_surface_reference(_source_graphic); + return _source_graphic; + } + + cairo_surface_t *tsg = cairo_surface_create_similar( + _source_graphic, cairo_surface_get_content(_source_graphic), + _slot_w, _slot_h); + cairo_t *tsg_ct = cairo_create(tsg); + + cairo_translate(tsg_ct, -_slot_x, -_slot_y); + ink_cairo_transform(tsg_ct, trans); + cairo_translate(tsg_ct, _source_graphic_area.left(), _source_graphic_area.top()); + cairo_set_source_surface(tsg_ct, _source_graphic, 0, 0); + cairo_set_operator(tsg_ct, CAIRO_OPERATOR_SOURCE); + cairo_paint(tsg_ct); + cairo_destroy(tsg_ct); + + return tsg; +} + +cairo_surface_t *FilterSlot::_get_transformed_background() +{ + Geom::Affine trans = _units.get_matrix_display2pb(); + + cairo_surface_t *tbg; + + if (_background_ct) { + cairo_surface_t *bg = cairo_get_group_target(_background_ct); + tbg = cairo_surface_create_similar( + bg, cairo_surface_get_content(bg), + _slot_w, _slot_h); + cairo_t *tbg_ct = cairo_create(tbg); + + cairo_translate(tbg_ct, -_slot_x, -_slot_y); + ink_cairo_transform(tbg_ct, trans); + cairo_translate(tbg_ct, _background_area.left(), _background_area.top()); + cairo_set_source_surface(tbg_ct, bg, 0, 0); + cairo_set_operator(tbg_ct, CAIRO_OPERATOR_SOURCE); + cairo_paint(tbg_ct); + cairo_destroy(tbg_ct); + } else { + tbg = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, _slot_w, _slot_h); + } + + return tbg; +} + +cairo_surface_t *FilterSlot::get_result(int res) +{ + cairo_surface_t *result = getcairo(res); + + Geom::Affine trans = _units.get_matrix_pb2display(); + if (trans.isIdentity()) { + cairo_surface_reference(result); + return result; + } + + cairo_surface_t *r = cairo_surface_create_similar(_source_graphic, + cairo_surface_get_content(_source_graphic), + _source_graphic_area.width(), + _source_graphic_area.height()); + copy_cairo_surface_ci( result, r ); + cairo_t *r_ct = cairo_create(r); + + cairo_translate(r_ct, -_source_graphic_area.left(), -_source_graphic_area.top()); + ink_cairo_transform(r_ct, trans); + cairo_translate(r_ct, _slot_x, _slot_y); + cairo_set_source_surface(r_ct, result, 0, 0); + cairo_set_operator(r_ct, CAIRO_OPERATOR_SOURCE); + cairo_paint(r_ct); + cairo_destroy(r_ct); + + return r; +} + +void FilterSlot::_set_internal(int slot_nr, cairo_surface_t *surface) +{ + // destroy after referencing + // this way assigning a surface to a slot it already occupies will not cause errors + cairo_surface_reference(surface); + + SlotMap::iterator s = _slots.find(slot_nr); + if (s != _slots.end()) { + cairo_surface_destroy(s->second); + } + + _slots[slot_nr] = surface; +} + +void FilterSlot::set(int slot_nr, cairo_surface_t *surface) +{ + g_return_if_fail(surface != nullptr); + + if (slot_nr == NR_FILTER_SLOT_NOT_SET) + slot_nr = NR_FILTER_UNNAMED_SLOT; + + _set_internal(slot_nr, surface); + _last_out = slot_nr; +} + +void FilterSlot::set_primitive_area(int slot_nr, Geom::Rect &area) +{ + if (slot_nr == NR_FILTER_SLOT_NOT_SET) + slot_nr = NR_FILTER_UNNAMED_SLOT; + + _primitiveAreas[slot_nr] = area; +} + +Geom::Rect FilterSlot::get_primitive_area(int slot_nr) +{ + if (slot_nr == NR_FILTER_SLOT_NOT_SET) + slot_nr = _last_out; + + PrimitiveAreaMap::iterator s = _primitiveAreas.find(slot_nr); + + if (s == _primitiveAreas.end()) { + return *(_units.get_filter_area()); + } + return s->second; +} + +int FilterSlot::get_slot_count() +{ + return _slots.size(); +} + +void FilterSlot::set_quality(FilterQuality const q) { + filterquality = q; +} + +void FilterSlot::set_blurquality(int const q) { + blurquality = q; +} + +int FilterSlot::get_blurquality() { + return blurquality; +} + +void FilterSlot::set_device_scale(int const s) { + device_scale = s; +} + +int FilterSlot::get_device_scale() { + return device_scale; +} + +Geom::Rect FilterSlot::get_slot_area() const { + Geom::Point p(_slot_x, _slot_y); + Geom::Point dim(_slot_w, _slot_h); + Geom::Rect r(p, p+dim); + return r; +} + +} /* namespace Filters */ +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter-slot.h b/src/display/nr-filter-slot.h new file mode 100644 index 0000000..1a695c4 --- /dev/null +++ b/src/display/nr-filter-slot.h @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_NR_FILTER_SLOT_H +#define SEEN_NR_FILTER_SLOT_H + +/* + * A container class for filter slots. Allows for simple getting and + * setting images in filter slots without having to bother with + * table indexes and such. + * + * Author: + * Niko Kiirala <niko@kiirala.com> + * + * Copyright (C) 2006,2007 Niko Kiirala + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <map> +#include "display/nr-filter-types.h" +#include "display/nr-filter-units.h" + +extern "C" { +typedef struct _cairo cairo_t; +typedef struct _cairo_surface cairo_surface_t; +} + +namespace Inkscape { +class DrawingContext; +class DrawingItem; + +namespace Filters { + +class FilterSlot { +public: + /** Creates a new FilterSlot object. */ + FilterSlot(DrawingItem *item, DrawingContext *bgdc, + DrawingContext &graphic, FilterUnits const &u); + /** Destroys the FilterSlot object and all its contents */ + virtual ~FilterSlot(); + + /** Returns the pixblock in specified slot. + * Parameter 'slot' may be either an positive integer or one of + * pre-defined filter slot types: NR_FILTER_SLOT_NOT_SET, + * NR_FILTER_SOURCEGRAPHIC, NR_FILTER_SOURCEALPHA, + * NR_FILTER_BACKGROUNDIMAGE, NR_FILTER_BACKGROUNDALPHA, + * NR_FILTER_FILLPAINT, NR_FILTER_SOURCEPAINT. + */ + cairo_surface_t *getcairo(int slot); + + /** Sets or re-sets the pixblock associated with given slot. + * If there was a pixblock already assigned with this slot, + * that pixblock is destroyed. + */ + void set(int slot, cairo_surface_t *s); + + cairo_surface_t *get_result(int slot_nr); + + void set_primitive_area(int slot, Geom::Rect &area); + Geom::Rect get_primitive_area(int slot); + + /** Returns the number of slots in use. */ + int get_slot_count(); + + /** Sets the unit system to be used for the internal images. */ + //void set_units(FilterUnits const &units); + + /** Sets the filtering quality. Affects used interpolation methods */ + void set_quality(FilterQuality const q); + + /** Sets the gaussian filtering quality. Affects used interpolation methods */ + void set_blurquality(int const q); + + /** Gets the gaussian filtering quality. Affects used interpolation methods */ + int get_blurquality(); + + /** Sets the device scale; for high DPI monitors. */ + void set_device_scale(int const s); + + /** Gets the device scale; for high DPI monitors. */ + int get_device_scale(); + + FilterUnits const &get_units() const { return _units; } + Geom::Rect get_slot_area() const; + +private: + typedef std::map<int, cairo_surface_t *> SlotMap; + SlotMap _slots; + + // We need to keep track of the primitive area as this is needed in feTile + typedef std::map<int, Geom::Rect> PrimitiveAreaMap; + PrimitiveAreaMap _primitiveAreas; + + DrawingItem *_item; + + //Geom::Rect _source_bbox; ///< bounding box of source graphic surface + //Geom::Rect _intermediate_bbox; ///< bounding box of intermediate surfaces + + int _slot_w, _slot_h; + double _slot_x, _slot_y; + cairo_surface_t *_source_graphic; + cairo_t *_background_ct; + Geom::IntRect _source_graphic_area; + Geom::IntRect _background_area; ///< needed to extract background + FilterUnits const &_units; + int _last_out; + FilterQuality filterquality; + int blurquality; + int device_scale; + + cairo_surface_t *_get_transformed_source_graphic(); + cairo_surface_t *_get_transformed_background(); + cairo_surface_t *_get_fill_paint(); + cairo_surface_t *_get_stroke_paint(); + + void _set_internal(int slot, cairo_surface_t *s); +}; + +} /* namespace Filters */ +} /* namespace Inkscape */ + +#endif // __NR_FILTER_SLOT_H__ +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter-specularlighting.cpp b/src/display/nr-filter-specularlighting.cpp new file mode 100644 index 0000000..330f19c --- /dev/null +++ b/src/display/nr-filter-specularlighting.cpp @@ -0,0 +1,248 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * feSpecularLighting renderer + * + * Authors: + * Niko Kiirala <niko@kiirala.com> + * Jean-Rene Reinhard <jr@komite.net> + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include <glib.h> +#include <cmath> + +#include "display/cairo-templates.h" +#include "display/cairo-utils.h" +#include "display/nr-3dutils.h" +#include "display/nr-filter-specularlighting.h" +#include "display/nr-filter-slot.h" +#include "display/nr-filter-units.h" +#include "display/nr-filter-utils.h" +#include "display/nr-light.h" +#include "svg/svg-icc-color.h" +#include "svg/svg-color.h" + +namespace Inkscape { +namespace Filters { + +FilterSpecularLighting::FilterSpecularLighting() +{ + light_type = NO_LIGHT; + specularConstant = 1; + specularExponent = 1; + surfaceScale = 1; + lighting_color = 0xffffffff; +} + +FilterPrimitive * FilterSpecularLighting::create() { + return new FilterSpecularLighting(); +} + +FilterSpecularLighting::~FilterSpecularLighting() += default; + +struct SpecularLight : public SurfaceSynth { + SpecularLight(cairo_surface_t *bumpmap, double scale, double specular_constant, + double specular_exponent) + : SurfaceSynth(bumpmap) + , _scale(scale) + , _ks(specular_constant) + , _exp(specular_exponent) + {} +protected: + guint32 specularLighting(int x, int y, NR::Fvector const &halfway, NR::Fvector const &light_components) { + NR::Fvector normal = surfaceNormalAt(x, y, _scale); + double sp = NR::scalar_product(normal, halfway); + double k = sp <= 0.0 ? 0.0 : _ks * pow(sp, _exp); + + guint32 r = CLAMP_D_TO_U8(k * light_components[LIGHT_RED]); + guint32 g = CLAMP_D_TO_U8(k * light_components[LIGHT_GREEN]); + guint32 b = CLAMP_D_TO_U8(k * light_components[LIGHT_BLUE]); + guint32 a = std::max(std::max(r, g), b); + + r = premul_alpha(r, a); + g = premul_alpha(g, a); + b = premul_alpha(b, a); + + ASSEMBLE_ARGB32(pxout, a,r,g,b) + return pxout; + } + double _scale, _ks, _exp; +}; + +struct SpecularDistantLight : public SpecularLight { + SpecularDistantLight(cairo_surface_t *bumpmap, SPFeDistantLight *light, guint32 color, + double scale, double specular_constant, double specular_exponent) + : SpecularLight(bumpmap, scale, specular_constant, specular_exponent) + { + DistantLight dl(light, color); + NR::Fvector lv; + dl.light_vector(lv); + dl.light_components(_light_components); + NR::normalized_sum(_halfway, lv, NR::EYE_VECTOR); + } + guint32 operator()(int x, int y) { + return specularLighting(x, y, _halfway, _light_components); + } +private: + NR::Fvector _halfway, _light_components; +}; + +struct SpecularPointLight : public SpecularLight { + SpecularPointLight(cairo_surface_t *bumpmap, SPFePointLight *light, guint32 color, + Geom::Affine const &trans, double scale, double specular_constant, + double specular_exponent, double x0, double y0, int device_scale) + : SpecularLight(bumpmap, scale, specular_constant, specular_exponent) + , _light(light, color, trans, device_scale) + , _x0(x0) + , _y0(y0) + { + _light.light_components(_light_components); + } + + guint32 operator()(int x, int y) { + NR::Fvector light, halfway; + _light.light_vector(light, _x0 + x, _y0 + y, _scale * alphaAt(x, y)/255.0); + NR::normalized_sum(halfway, light, NR::EYE_VECTOR); + return specularLighting(x, y, halfway, _light_components); + } +private: + PointLight _light; + NR::Fvector _light_components; + double _x0, _y0; +}; + +struct SpecularSpotLight : public SpecularLight { + SpecularSpotLight(cairo_surface_t *bumpmap, SPFeSpotLight *light, guint32 color, + Geom::Affine const &trans, double scale, double specular_constant, + double specular_exponent, double x0, double y0, int device_scale) + : SpecularLight(bumpmap, scale, specular_constant, specular_exponent) + , _light(light, color, trans, device_scale) + , _x0(x0) + , _y0(y0) + {} + + guint32 operator()(int x, int y) { + NR::Fvector light, halfway, light_components; + _light.light_vector(light, _x0 + x, _y0 + y, _scale * alphaAt(x, y)/255.0); + _light.light_components(light_components, light); + NR::normalized_sum(halfway, light, NR::EYE_VECTOR); + return specularLighting(x, y, halfway, light_components); + } +private: + SpotLight _light; + double _x0, _y0; +}; + +void FilterSpecularLighting::render_cairo(FilterSlot &slot) +{ + cairo_surface_t *input = slot.getcairo(_input); + cairo_surface_t *out = ink_cairo_surface_create_same_size(input, CAIRO_CONTENT_COLOR_ALPHA); + + double r = SP_RGBA32_R_F(lighting_color); + double g = SP_RGBA32_G_F(lighting_color); + double b = SP_RGBA32_B_F(lighting_color); + +#if defined(HAVE_LIBLCMS2) + + if (icc) { + guchar ru, gu, bu; + icc_color_to_sRGB(icc, &ru, &gu, &bu); + r = SP_COLOR_U_TO_F(ru); + g = SP_COLOR_U_TO_F(gu); + b = SP_COLOR_U_TO_F(bu); + } +#endif + + // Only alpha channel of input is used, no need to check input color_interpolation_filter value. + SPColorInterpolation ci_fp = SP_CSS_COLOR_INTERPOLATION_AUTO; + if( _style ) { + ci_fp = (SPColorInterpolation)_style->color_interpolation_filters.computed; + + // Lighting color is always defined in terms of sRGB, preconvert to linearRGB + // if color_interpolation_filters set to linearRGB (for efficiency assuming + // next filter primitive has same value of cif). + if( ci_fp == SP_CSS_COLOR_INTERPOLATION_LINEARRGB ) { + r = srgb_to_linear( r ); + g = srgb_to_linear( g ); + b = srgb_to_linear( b ); + } + } + set_cairo_surface_ci(out, ci_fp ); + guint32 color = SP_RGBA32_F_COMPOSE( r, g, b, 1.0 ); + + int device_scale = slot.get_device_scale(); + + // trans has inverse y... so we can't just scale by device_scale! We must instead explicitly + // scale the point and spot light coordinates (as well as "scale"). + + Geom::Affine trans = slot.get_units().get_matrix_primitiveunits2pb(); + + Geom::Point p = slot.get_slot_area().min(); + double x0 = p[Geom::X]; + double y0 = p[Geom::Y]; + double scale = surfaceScale * trans.descrim() * device_scale; + double ks = specularConstant; + double se = specularExponent; + + switch (light_type) { + case DISTANT_LIGHT: + ink_cairo_surface_synthesize(out, + SpecularDistantLight(input, light.distant, color, scale, ks, se)); + break; + case POINT_LIGHT: + ink_cairo_surface_synthesize(out, + SpecularPointLight(input, light.point, color, trans, scale, ks, se, x0, y0, device_scale)); + break; + case SPOT_LIGHT: + ink_cairo_surface_synthesize(out, + SpecularSpotLight(input, light.spot, color, trans, scale, ks, se, x0, y0, device_scale)); + break; + default: { + cairo_t *ct = cairo_create(out); + cairo_set_source_rgba(ct, 0,0,0,1); + cairo_set_operator(ct, CAIRO_OPERATOR_SOURCE); + cairo_paint(ct); + cairo_destroy(ct); + } break; + } + + slot.set(_output, out); + cairo_surface_destroy(out); +} + +void FilterSpecularLighting::set_icc(SVGICCColor *icc_color) { + icc = icc_color; +} + +void FilterSpecularLighting::area_enlarge(Geom::IntRect &area, Geom::Affine const & /*trans*/) +{ + // TODO: support kernelUnitLength + area.expandBy(1); +} + +double FilterSpecularLighting::complexity(Geom::Affine const &) +{ + return 9.0; +} + +} /* namespace Filters */ +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter-specularlighting.h b/src/display/nr-filter-specularlighting.h new file mode 100644 index 0000000..86fd8b5 --- /dev/null +++ b/src/display/nr-filter-specularlighting.h @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_NR_FILTER_SPECULARLIGHTING_H +#define SEEN_NR_FILTER_SPECULARLIGHTING_H + +/* + * feSpecularLighting renderer + * + * Authors: + * Niko Kiirala <niko@kiirala.com> + * Jean-Rene Reinhard <jr@komite.net> + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "display/nr-light-types.h" +#include "display/nr-filter-primitive.h" + +class SPFeDistantLight; +class SPFePointLight; +class SPFeSpotLight; +struct SVGICCColor; +typedef unsigned int guint32; + +namespace Inkscape { +namespace Filters { + +class FilterSlot; + +class FilterSpecularLighting : public FilterPrimitive { +public: + FilterSpecularLighting(); + static FilterPrimitive *create(); + ~FilterSpecularLighting() override; + + void render_cairo(FilterSlot &slot) override; + virtual void set_icc(SVGICCColor *icc_color); + void area_enlarge(Geom::IntRect &area, Geom::Affine const &trans) override; + double complexity(Geom::Affine const &ctm) override; + + union { + SPFeDistantLight *distant; + SPFePointLight *point; + SPFeSpotLight *spot; + } light; + LightType light_type; + double surfaceScale; + double specularConstant; + double specularExponent; + guint32 lighting_color; + + Glib::ustring name() override { return Glib::ustring("Specular Lighting"); } + +private: + SVGICCColor *icc; +}; + +} /* namespace Filters */ +} /* namespace Inkscape */ + +#endif /* __NR_FILTER_SPECULARLIGHTING_H__ */ +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter-tile.cpp b/src/display/nr-filter-tile.cpp new file mode 100644 index 0000000..4605dde --- /dev/null +++ b/src/display/nr-filter-tile.cpp @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * feTile filter primitive renderer + * + * Authors: + * Felipe CorrĂȘa da Silva Sanches <juca@members.fsf.org> + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glib.h> + +#include "display/cairo-utils.h" +#include "display/nr-filter-tile.h" +#include "display/nr-filter-slot.h" +#include "display/nr-filter-units.h" + +namespace Inkscape { +namespace Filters { + +FilterTile::FilterTile() += default; + +FilterPrimitive * FilterTile::create() { + return new FilterTile(); +} + +FilterTile::~FilterTile() += default; + +void FilterTile::render_cairo(FilterSlot &slot) +{ + // This input source contains only the "rendering" tile. + cairo_surface_t *in = slot.getcairo(_input); + + // For debugging + // static int i = 0; + // ++i; + // std::stringstream filename; + // filename << "dump." << i << ".png"; + // cairo_surface_write_to_png( in, filename.str().c_str() ); + + // This is the feTile source area as determined by the input primitive area (see SVG spec). + Geom::Rect tile_area = slot.get_primitive_area(_input); + + if( tile_area.width() == 0.0 || tile_area.height() == 0.0 ) { + + slot.set(_output, in); + std::cerr << "FileTile::render_cairo: tile has zero width or height" << std::endl; + + } else { + + cairo_surface_t *out = ink_cairo_surface_create_identical(in); + // color_interpolation_filters for out same as in. + copy_cairo_surface_ci(in, out); + cairo_t *ct = cairo_create(out); + + // The rectangle of the "rendering" tile. + Geom::Rect sa = slot.get_slot_area(); + + Geom::Affine trans = slot.get_units().get_matrix_user2pb(); + + // Create feTile tile ---------------- + + // Get tile area in pixbuf units (tile transformed). + Geom::Rect tt = tile_area * trans; + + // Shift between "rendering" tile and feTile tile + Geom::Point shift = sa.min() - tt.min(); + + // Create feTile tile surface + cairo_surface_t *tile = cairo_surface_create_similar(in, cairo_surface_get_content(in), + tt.width(), tt.height()); + cairo_t *ct_tile = cairo_create(tile); + cairo_set_source_surface(ct_tile, in, shift[Geom::X], shift[Geom::Y]); + cairo_paint(ct_tile); + + // Paint tiles ------------------ + + // For debugging + // std::stringstream filename; + // filename << "tile." << i << ".png"; + // cairo_surface_write_to_png( tile, filename.str().c_str() ); + + // Determine number of feTile rows and columns + Geom::Rect pr = filter_primitive_area( slot.get_units() ); + int tile_cols = ceil( pr.width() / tile_area.width() ); + int tile_rows = ceil( pr.height() / tile_area.height() ); + + // Do tiling (TO DO: restrict to slot area.) + for( int col=0; col < tile_cols; ++col ) { + for( int row=0; row < tile_rows; ++row ) { + + Geom::Point offset( col*tile_area.width(), row*tile_area.height() ); + offset *= trans; + offset[Geom::X] -= trans[4]; + offset[Geom::Y] -= trans[5]; + + cairo_set_source_surface(ct, tile, offset[Geom::X], offset[Geom::Y]); + cairo_paint(ct); + } + } + slot.set(_output, out); + + // Clean up + cairo_destroy(ct); + cairo_surface_destroy(out); + cairo_destroy(ct_tile); + cairo_surface_destroy(tile); + } +} + +void FilterTile::area_enlarge(Geom::IntRect &area, Geom::Affine const &trans) +{ + // Set to very large rectangle so we get tile source. It will be clipped later. + + // Note, setting to infinite using Geom::IntRect::infinite() causes overflow/underflow problems. + Geom::IntCoord max = std::numeric_limits<Geom::IntCoord>::max()/4; + area = Geom::IntRect(-max,-max,max,max); +} + +double FilterTile::complexity(Geom::Affine const &) +{ + return 1.0; +} + +} /* namespace Filters */ +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter-tile.h b/src/display/nr-filter-tile.h new file mode 100644 index 0000000..867d129 --- /dev/null +++ b/src/display/nr-filter-tile.h @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_NR_FILTER_TILE_H +#define SEEN_NR_FILTER_TILE_H + +/* + * feTile filter primitive renderer + * + * Authors: + * Felipe CorrĂȘa da Silva Sanches <juca@members.fsf.org> + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "display/nr-filter-primitive.h" + +namespace Inkscape { +namespace Filters { + +class FilterSlot; + +class FilterTile : public FilterPrimitive { +public: + FilterTile(); + static FilterPrimitive *create(); + ~FilterTile() override; + + void render_cairo(FilterSlot &slot) override; + void area_enlarge(Geom::IntRect &area, Geom::Affine const &trans) override; + double complexity(Geom::Affine const &ctm) override; + + Glib::ustring name() override { return Glib::ustring("Tile"); } +}; + +} /* namespace Filters */ +} /* namespace Inkscape */ + +#endif /* __NR_FILTER_TILE_H__ */ +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter-turbulence.cpp b/src/display/nr-filter-turbulence.cpp new file mode 100644 index 0000000..91a71ab --- /dev/null +++ b/src/display/nr-filter-turbulence.cpp @@ -0,0 +1,446 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * feTurbulence filter primitive renderer + * + * Authors: + * World Wide Web Consortium <http://www.w3.org/> + * Felipe CorrĂȘa da Silva Sanches <juca@members.fsf.org> + * + * This file has a considerable amount of code adapted from + * the W3C SVG filter specs, available at: + * http://www.w3.org/TR/SVG11/filters.html#feTurbulence + * + * W3C original code is licensed under the terms of + * the (GPL compatible) W3CÂź SOFTWARE NOTICE AND LICENSE: + * http://www.w3.org/Consortium/Legal/2002/copyright-software-20021231 + * + * Copyright (C) 2007 authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "display/cairo-templates.h" +#include "display/cairo-utils.h" +#include "display/nr-filter.h" +#include "display/nr-filter-turbulence.h" +#include "display/nr-filter-units.h" +#include "display/nr-filter-utils.h" +#include <cmath> + +namespace Inkscape { +namespace Filters{ + +class TurbulenceGenerator { +public: + TurbulenceGenerator() : + _tile(), + _baseFreq(), + _latticeSelector(), + _gradient(), + _seed(0), + _octaves(0), + _stitchTiles(false), + _wrapx(0), + _wrapy(0), + _wrapw(0), + _wraph(0), + _inited(false), + _fractalnoise(false) + {} + + void init(long seed, Geom::Rect const &tile, Geom::Point const &freq, bool stitch, + bool fractalnoise, int octaves) + { + // setup random number generator + _setupSeed(seed); + + // set values + _tile = tile; + _baseFreq = freq; + _stitchTiles = stitch; + _fractalnoise = fractalnoise; + _octaves = octaves; + + int i; + for (int k = 0; k < 4; ++k) { + for (i = 0; i < BSize; ++i) { + _latticeSelector[i] = i; + + do { + _gradient[i][k][0] = static_cast<double>(_random() % (BSize*2) - BSize) / BSize; + _gradient[i][k][1] = static_cast<double>(_random() % (BSize*2) - BSize) / BSize; + } while(_gradient[i][k][0] == 0 && _gradient[i][k][1] == 0); + + // normalize gradient + double s = hypot(_gradient[i][k][0], _gradient[i][k][1]); + _gradient[i][k][0] /= s; + _gradient[i][k][1] /= s; + } + } + while (--i) { + // shuffle lattice selectors + int j = _random() % BSize; + std::swap(_latticeSelector[i], _latticeSelector[j]); + } + + // fill out the remaining part of the gradient + for (i = 0; i < BSize + 2; ++i) + { + _latticeSelector[BSize + i] = _latticeSelector[i]; + + for(int k = 0; k < 4; ++k) { + _gradient[BSize + i][k][0] = _gradient[i][k][0]; + _gradient[BSize + i][k][1] = _gradient[i][k][1]; + } + } + + // When stitching tiled turbulence, the frequencies must be adjusted + // so that the tile borders will be continuous. + if (_stitchTiles) { + if (_baseFreq[Geom::X] != 0.0) + { + double freq = _baseFreq[Geom::X]; + double lo = floor(_tile.width() * freq) / _tile.width(); + double hi = ceil(_tile.width() * freq) / _tile.width(); + _baseFreq[Geom::X] = freq / lo < hi / freq ? lo : hi; + } + if (_baseFreq[Geom::Y] != 0.0) + { + double freq = _baseFreq[Geom::Y]; + double lo = floor(_tile.height() * freq) / _tile.height(); + double hi = ceil(_tile.height() * freq) / _tile.height(); + _baseFreq[Geom::Y] = freq / lo < hi / freq ? lo : hi; + } + + _wrapw = _tile.width() * _baseFreq[Geom::X] + 0.5; + _wraph = _tile.height() * _baseFreq[Geom::Y] + 0.5; + _wrapx = _tile.left() * _baseFreq[Geom::X] + PerlinOffset + _wrapw; + _wrapy = _tile.top() * _baseFreq[Geom::Y] + PerlinOffset + _wraph; + } + _inited = true; + } + + G_GNUC_PURE + guint32 turbulencePixel(Geom::Point const &p) const { + int wrapx = _wrapx, wrapy = _wrapy, wrapw = _wrapw, wraph = _wraph; + + double pixel[4]; + double x = p[Geom::X] * _baseFreq[Geom::X]; + double y = p[Geom::Y] * _baseFreq[Geom::Y]; + double ratio = 1.0; + + for (double & k : pixel) + k = 0.0; + + for(int octave = 0; octave < _octaves; ++octave) + { + double tx = x + PerlinOffset; + double bx = floor(tx); + double rx0 = tx - bx, rx1 = rx0 - 1.0; + int bx0 = bx, bx1 = bx0 + 1; + + double ty = y + PerlinOffset; + double by = floor(ty); + double ry0 = ty - by, ry1 = ry0 - 1.0; + int by0 = by, by1 = by0 + 1; + + if (_stitchTiles) { + if (bx0 >= wrapx) bx0 -= wrapw; + if (bx1 >= wrapx) bx1 -= wrapw; + if (by0 >= wrapy) by0 -= wraph; + if (by1 >= wrapy) by1 -= wraph; + } + bx0 &= BMask; + bx1 &= BMask; + by0 &= BMask; + by1 &= BMask; + + int i = _latticeSelector[bx0]; + int j = _latticeSelector[bx1]; + int b00 = _latticeSelector[i + by0]; + int b01 = _latticeSelector[i + by1]; + int b10 = _latticeSelector[j + by0]; + int b11 = _latticeSelector[j + by1]; + + double sx = _scurve(rx0); + double sy = _scurve(ry0); + + double result[4]; + // channel numbering: R=0, G=1, B=2, A=3 + for (int k = 0; k < 4; ++k) { + double const *qxa = _gradient[b00][k]; + double const *qxb = _gradient[b10][k]; + double a = _lerp(sx, rx0 * qxa[0] + ry0 * qxa[1], + rx1 * qxb[0] + ry0 * qxb[1]); + double const *qya = _gradient[b01][k]; + double const *qyb = _gradient[b11][k]; + double b = _lerp(sx, rx0 * qya[0] + ry1 * qya[1], + rx1 * qyb[0] + ry1 * qyb[1]); + result[k] = _lerp(sy, a, b); + } + + if (_fractalnoise) { + for (int k = 0; k < 4; ++k) + pixel[k] += result[k] / ratio; + } else { + for (int k = 0; k < 4; ++k) + pixel[k] += fabs(result[k]) / ratio; + } + + x *= 2; + y *= 2; + ratio *= 2; + + if(_stitchTiles) + { + // Update stitch values. Subtracting PerlinOffset before the multiplication and + // adding it afterward simplifies to subtracting it once. + wrapw *= 2; + wraph *= 2; + wrapx = wrapx*2 - PerlinOffset; + wrapy = wrapy*2 - PerlinOffset; + } + } + + if (_fractalnoise) { + guint32 r = CLAMP_D_TO_U8((pixel[0]*255.0 + 255.0) / 2); + guint32 g = CLAMP_D_TO_U8((pixel[1]*255.0 + 255.0) / 2); + guint32 b = CLAMP_D_TO_U8((pixel[2]*255.0 + 255.0) / 2); + guint32 a = CLAMP_D_TO_U8((pixel[3]*255.0 + 255.0) / 2); + r = premul_alpha(r, a); + g = premul_alpha(g, a); + b = premul_alpha(b, a); + ASSEMBLE_ARGB32(pxout, a,r,g,b); + return pxout; + } else { + guint32 r = CLAMP_D_TO_U8(pixel[0]*255.0); + guint32 g = CLAMP_D_TO_U8(pixel[1]*255.0); + guint32 b = CLAMP_D_TO_U8(pixel[2]*255.0); + guint32 a = CLAMP_D_TO_U8(pixel[3]*255.0); + r = premul_alpha(r, a); + g = premul_alpha(g, a); + b = premul_alpha(b, a); + ASSEMBLE_ARGB32(pxout, a,r,g,b); + return pxout; + } + } + + //G_GNUC_PURE + /*guint32 turbulencePixel(Geom::Point const &p) const { + if (!_fractalnoise) { + guint32 r = CLAMP_D_TO_U8(turbulence(0, p)*255.0); + guint32 g = CLAMP_D_TO_U8(turbulence(1, p)*255.0); + guint32 b = CLAMP_D_TO_U8(turbulence(2, p)*255.0); + guint32 a = CLAMP_D_TO_U8(turbulence(3, p)*255.0); + r = premul_alpha(r, a); + g = premul_alpha(g, a); + b = premul_alpha(b, a); + ASSEMBLE_ARGB32(pxout, a,r,g,b); + return pxout; + } else { + guint32 r = CLAMP_D_TO_U8((turbulence(0, p)*255.0 + 255.0) / 2); + guint32 g = CLAMP_D_TO_U8((turbulence(1, p)*255.0 + 255.0) / 2); + guint32 b = CLAMP_D_TO_U8((turbulence(2, p)*255.0 + 255.0) / 2); + guint32 a = CLAMP_D_TO_U8((turbulence(3, p)*255.0 + 255.0) / 2); + r = premul_alpha(r, a); + g = premul_alpha(g, a); + b = premul_alpha(b, a); + ASSEMBLE_ARGB32(pxout, a,r,g,b); + return pxout; + } + }*/ + + bool ready() const { return _inited; } + void dirty() { _inited = false; } + +private: + void _setupSeed(long seed) { + _seed = seed; + if (_seed <= 0) _seed = -(_seed % (RAND_m - 1)) + 1; + if (_seed > RAND_m - 1) _seed = RAND_m - 1; + } + long _random() { + /* Produces results in the range [1, 2**31 - 2]. + * Algorithm is: r = (a * r) mod m + * where a = 16807 and m = 2**31 - 1 = 2147483647 + * See [Park & Miller], CACM vol. 31 no. 10 p. 1195, Oct. 1988 + * To test: the algorithm should produce the result 1043618065 + * as the 10,000th generated number if the original seed is 1. */ + _seed = RAND_a * (_seed % RAND_q) - RAND_r * (_seed / RAND_q); + if (_seed <= 0) _seed += RAND_m; + return _seed; + } + static inline double _scurve(double t) { + return t * t * (3.0 - 2.0*t); + } + static inline double _lerp(double t, double a, double b) { + return a + t * (b-a); + } + + // random number generator constants + static long const + RAND_m = 2147483647, // 2**31 - 1 + RAND_a = 16807, // 7**5; primitive root of m + RAND_q = 127773, // m / a + RAND_r = 2836; // m % a + + // other constants + static int const BSize = 0x100; + static int const BMask = 0xff; + + static double constexpr PerlinOffset = 4096.0; + + Geom::Rect _tile; + Geom::Point _baseFreq; + int _latticeSelector[2*BSize + 2]; + double _gradient[2*BSize + 2][4][2]; + long _seed; + int _octaves; + bool _stitchTiles; + int _wrapx; + int _wrapy; + int _wrapw; + int _wraph; + bool _inited; + bool _fractalnoise; +}; + +FilterTurbulence::FilterTurbulence() + : gen(new TurbulenceGenerator()) + , XbaseFrequency(0) + , YbaseFrequency(0) + , numOctaves(1) + , seed(0) + , updated(false) + , fTileWidth(10) //guessed + , fTileHeight(10) //guessed + , fTileX(1) //guessed + , fTileY(1) //guessed +{ +} + +FilterPrimitive * FilterTurbulence::create() { + return new FilterTurbulence(); +} + +FilterTurbulence::~FilterTurbulence() +{ + delete gen; +} + +void FilterTurbulence::set_baseFrequency(int axis, double freq){ + if (axis==0) XbaseFrequency=freq; + if (axis==1) YbaseFrequency=freq; + gen->dirty(); +} + +void FilterTurbulence::set_numOctaves(int num){ + numOctaves = num; + gen->dirty(); +} + +void FilterTurbulence::set_seed(double s){ + seed = s; + gen->dirty(); +} + +void FilterTurbulence::set_stitchTiles(bool st){ + stitchTiles = st; + gen->dirty(); +} + +void FilterTurbulence::set_type(FilterTurbulenceType t){ + type = t; + gen->dirty(); +} + +void FilterTurbulence::set_updated(bool /*u*/) +{ +} + +struct Turbulence { + Turbulence(TurbulenceGenerator const &gen, Geom::Affine const &trans, int x0, int y0) + : _gen(gen) + , _trans(trans) + , _x0(x0), _y0(y0) + {} + guint32 operator()(int x, int y) { + Geom::Point point(x + _x0, y + _y0); + point *= _trans; + return _gen.turbulencePixel(point); + } +private: + TurbulenceGenerator const &_gen; + Geom::Affine _trans; + int _x0, _y0; +}; + +void FilterTurbulence::render_cairo(FilterSlot &slot) +{ + cairo_surface_t *input = slot.getcairo(_input); + cairo_surface_t *out = ink_cairo_surface_create_same_size(input, CAIRO_CONTENT_COLOR_ALPHA); + + // It is probably possible to render at a device scale greater than one + // but for the moment rendering at a device scale of one is the easiest. + // cairo_image_surface_get_width() returns width in pixels but + // cairo_surface_create_similar() requires width in device units so divide by device scale. + // We are rendering at a device scale of 1... so divide by device scale again! + double x_scale = 0; + double y_scale = 0; + cairo_surface_get_device_scale(input, &x_scale, &y_scale); + int width = ceil(cairo_image_surface_get_width( input)/x_scale/x_scale); + int height = ceil(cairo_image_surface_get_height(input)/y_scale/y_scale); + cairo_surface_t *temp = cairo_surface_create_similar (input, CAIRO_CONTENT_COLOR_ALPHA, width, height); + cairo_surface_set_device_scale( temp, 1, 1 ); + + // color_interpolation_filter is determined by CSS value (see spec. Turbulence). + if( _style ) { + set_cairo_surface_ci(out, (SPColorInterpolation)_style->color_interpolation_filters.computed ); + } + + if (!gen->ready()) { + Geom::Point ta(fTileX, fTileY); + Geom::Point tb(fTileX + fTileWidth, fTileY + fTileHeight); + gen->init(seed, Geom::Rect(ta, tb), + Geom::Point(XbaseFrequency, YbaseFrequency), stitchTiles, + type == TURBULENCE_FRACTALNOISE, numOctaves); + } + + Geom::Affine unit_trans = slot.get_units().get_matrix_primitiveunits2pb().inverse(); + Geom::Rect slot_area = slot.get_slot_area(); + double x0 = slot_area.min()[Geom::X]; + double y0 = slot_area.min()[Geom::Y]; + ink_cairo_surface_synthesize(temp, Turbulence(*gen, unit_trans, x0, y0)); + + // cairo_surface_write_to_png( temp, "turbulence0.png" ); + + cairo_t *ct = cairo_create(out); + cairo_set_source_surface(ct, temp, 0, 0); + cairo_paint(ct); + cairo_destroy(ct); + + cairo_surface_destroy(temp); + + cairo_surface_mark_dirty(out); + + slot.set(_output, out); + cairo_surface_destroy(out); +} + +double FilterTurbulence::complexity(Geom::Affine const &) +{ + return 5.0; +} + +} /* namespace Filters */ +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter-turbulence.h b/src/display/nr-filter-turbulence.h new file mode 100644 index 0000000..de21c58 --- /dev/null +++ b/src/display/nr-filter-turbulence.h @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_NR_FILTER_TURBULENCE_H +#define SEEN_NR_FILTER_TURBULENCE_H + +/* + * feTurbulence filter primitive renderer + * + * Authors: + * World Wide Web Consortium <http://www.w3.org/> + * Felipe CorrĂȘa da Silva Sanches <juca@members.fsf.org> + * Niko Kiirala <niko@kiirala.com> + * + * This file has a considerable amount of code adapted from + * the W3C SVG filter specs, available at: + * http://www.w3.org/TR/SVG11/filters.html#feTurbulence + * + * W3C original code is licensed under the terms of + * the (GPL compatible) W3CÂź SOFTWARE NOTICE AND LICENSE: + * http://www.w3.org/Consortium/Legal/2002/copyright-software-20021231 + * + * Copyright (C) 2007 authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/point.h> + +#include "display/nr-filter-primitive.h" +#include "display/nr-filter-slot.h" +#include "display/nr-filter-units.h" + +namespace Inkscape { +namespace Filters { + +enum FilterTurbulenceType { + TURBULENCE_FRACTALNOISE, + TURBULENCE_TURBULENCE, + TURBULENCE_ENDTYPE +}; + +class TurbulenceGenerator; + +class FilterTurbulence : public FilterPrimitive { +public: + FilterTurbulence(); + static FilterPrimitive *create(); + ~FilterTurbulence() override; + + void render_cairo(FilterSlot &slot) override; + double complexity(Geom::Affine const &ctm) override; + bool uses_background() override { return false; } + + void set_baseFrequency(int axis, double freq); + void set_numOctaves(int num); + void set_seed(double s); + void set_stitchTiles(bool st); + void set_type(FilterTurbulenceType t); + void set_updated(bool u); + + Glib::ustring name() override { return Glib::ustring("Turbulence"); } + +private: + + TurbulenceGenerator *gen; + + void turbulenceInit(long seed); + + double XbaseFrequency, YbaseFrequency; + int numOctaves; + double seed; + bool stitchTiles; + FilterTurbulenceType type; + bool updated; + unsigned char *pix_data; + + double fTileWidth; + double fTileHeight; + + double fTileX; + double fTileY; + +}; + +} /* namespace Filters */ +} /* namespace Inkscape */ + +#endif /* __NR_FILTER_TURBULENCE_H__ */ +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter-types.h b/src/display/nr-filter-types.h new file mode 100644 index 0000000..34cfdeb --- /dev/null +++ b/src/display/nr-filter-types.h @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2014 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_NR_FILTER_TYPES_H +#define SEEN_NR_FILTER_TYPES_H + +namespace Inkscape { +namespace Filters { + +enum FilterPrimitiveType { + NR_FILTER_BLEND, + NR_FILTER_COLORMATRIX, + NR_FILTER_COMPONENTTRANSFER, + NR_FILTER_COMPOSITE, + NR_FILTER_CONVOLVEMATRIX, + NR_FILTER_DIFFUSELIGHTING, + NR_FILTER_DISPLACEMENTMAP, + NR_FILTER_FLOOD, + NR_FILTER_GAUSSIANBLUR, + NR_FILTER_IMAGE, + NR_FILTER_MERGE, + NR_FILTER_MORPHOLOGY, + NR_FILTER_OFFSET, + NR_FILTER_SPECULARLIGHTING, + NR_FILTER_TILE, + NR_FILTER_TURBULENCE, + NR_FILTER_ENDPRIMITIVETYPE // This must be last +}; +//const int Filter::_filter_primitive_type_count = 16; + +enum FilterSlotType { + NR_FILTER_SLOT_NOT_SET = -1, + NR_FILTER_SOURCEGRAPHIC = -2, + NR_FILTER_SOURCEALPHA = -3, + NR_FILTER_BACKGROUNDIMAGE = -4, + NR_FILTER_BACKGROUNDALPHA = -5, + NR_FILTER_FILLPAINT = -6, + NR_FILTER_STROKEPAINT = -7, + NR_FILTER_UNNAMED_SLOT = -8 +}; +/* Unnamed slot is for Inkscape::Filters::FilterSlot internal use. Passing it as + * parameter to Inkscape::Filters::FilterSlot accessors may have unforeseen consequences. */ + +enum FilterQuality { + FILTER_QUALITY_BEST = 2, + FILTER_QUALITY_BETTER = 1, + FILTER_QUALITY_NORMAL = 0, + FILTER_QUALITY_WORSE = -1, + FILTER_QUALITY_WORST = -2 +}; + +} /* namespace Filters */ +} /* namespace Inkscape */ + +#endif // __NR_FILTER_TYPES_H__ +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter-units.cpp b/src/display/nr-filter-units.cpp new file mode 100644 index 0000000..7fdf936 --- /dev/null +++ b/src/display/nr-filter-units.cpp @@ -0,0 +1,176 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Utilities for handling coordinate system transformations in filters + * + * Author: + * Niko Kiirala <niko@kiirala.com> + * + * Copyright (C) 2007 Niko Kiirala + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glib.h> + +#include "display/nr-filter-units.h" +#include "object/sp-filter-units.h" +#include <2geom/transforms.h> + +using Geom::X; +using Geom::Y; + +namespace Inkscape { +namespace Filters { + +FilterUnits::FilterUnits() : + filterUnits(SP_FILTER_UNITS_OBJECTBOUNDINGBOX), + primitiveUnits(SP_FILTER_UNITS_USERSPACEONUSE), + resolution_x(-1), resolution_y(-1), + paraller_axis(false), automatic_resolution(true) +{} + +FilterUnits::FilterUnits(SPFilterUnits const filterUnits, SPFilterUnits const primitiveUnits) : + filterUnits(filterUnits), primitiveUnits(primitiveUnits), + resolution_x(-1), resolution_y(-1), + paraller_axis(false), automatic_resolution(true) +{} + +void FilterUnits::set_ctm(Geom::Affine const &ctm) { + this->ctm = ctm; +} + +void FilterUnits::set_resolution(double const x_res, double const y_res) { + g_assert(x_res > 0); + g_assert(y_res > 0); + + resolution_x = x_res; + resolution_y = y_res; +} + +void FilterUnits::set_item_bbox(Geom::OptRect const &bbox) { + item_bbox = bbox; +} + +void FilterUnits::set_filter_area(Geom::OptRect const &area) { + filter_area = area; +} + +void FilterUnits::set_paraller(bool const paraller) { + paraller_axis = paraller; +} + +void FilterUnits::set_automatic_resolution(bool const automatic) { + automatic_resolution = automatic; +} + +Geom::Affine FilterUnits::get_matrix_user2pb() const { + g_assert(resolution_x > 0); + g_assert(resolution_y > 0); + g_assert(filter_area); + + Geom::Affine u2pb = ctm; + + if (paraller_axis || !automatic_resolution) { + u2pb[0] = resolution_x / filter_area->width(); + u2pb[1] = 0; + u2pb[2] = 0; + u2pb[3] = resolution_y / filter_area->height(); + u2pb[4] = ctm[4]; + u2pb[5] = ctm[5]; + } + + return u2pb; +} + +Geom::Affine FilterUnits::get_matrix_units2pb(SPFilterUnits units) const { + if ( item_bbox && (units == SP_FILTER_UNITS_OBJECTBOUNDINGBOX) ) { + + Geom::Affine u2pb = get_matrix_user2pb(); + + /* TODO: make sure that user coordinate system (0,0) is in correct + * place in pixblock coordinates */ + Geom::Scale scaling(item_bbox->width(), item_bbox->height()); + u2pb *= scaling; + return u2pb; + + } else if (units == SP_FILTER_UNITS_USERSPACEONUSE) { + return get_matrix_user2pb(); + } else { + g_warning("Error in Inkscape::Filters::FilterUnits::get_matrix_units2pb: unrecognized unit type (%d)", units); + return Geom::Affine(); + } +} + +Geom::Affine FilterUnits::get_matrix_filterunits2pb() const { + return get_matrix_units2pb(filterUnits); +} + +Geom::Affine FilterUnits::get_matrix_primitiveunits2pb() const { + return get_matrix_units2pb(primitiveUnits); +} + +Geom::Affine FilterUnits::get_matrix_display2pb() const { + Geom::Affine d2pb = ctm.inverse(); + d2pb *= get_matrix_user2pb(); + return d2pb; +} + +Geom::Affine FilterUnits::get_matrix_pb2display() const { + Geom::Affine pb2d = get_matrix_user2pb().inverse(); + pb2d *= ctm; + return pb2d; +} + +Geom::Affine FilterUnits::get_matrix_user2units(SPFilterUnits units) const { + if (item_bbox && units == SP_FILTER_UNITS_OBJECTBOUNDINGBOX) { + /* No need to worry about rotations: bounding box coordinates + * always have base vectors paraller with userspace coordinates */ + Geom::Point min(item_bbox->min()); + Geom::Point max(item_bbox->max()); + double scale_x = 1.0 / (max[X] - min[X]); + double scale_y = 1.0 / (max[Y] - min[Y]); + //return Geom::Translate(min) * Geom::Scale(scale_x,scale_y); ? + return Geom::Affine(scale_x, 0, + 0, scale_y, + min[X] * scale_x, min[Y] * scale_y); + } else if (units == SP_FILTER_UNITS_USERSPACEONUSE) { + return Geom::identity(); + } else { + g_warning("Error in Inkscape::Filters::FilterUnits::get_matrix_user2units: unrecognized unit type (%d)", units); + return Geom::Affine(); + } +} + +Geom::Affine FilterUnits::get_matrix_user2filterunits() const { + return get_matrix_user2units(filterUnits); +} + +Geom::Affine FilterUnits::get_matrix_user2primitiveunits() const { + return get_matrix_user2units(primitiveUnits); +} + +Geom::IntRect FilterUnits::get_pixblock_filterarea_paraller() const { + g_assert(filter_area); + + Geom::Affine u2pb = get_matrix_user2pb(); + Geom::Rect r = *filter_area * u2pb; + Geom::IntRect ir = r.roundOutwards(); + return ir; +} + +FilterUnits& FilterUnits::operator=(FilterUnits const &other) = default; + +} /* namespace Filters */ +} /* namespace Inkscape */ + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter-units.h b/src/display/nr-filter-units.h new file mode 100644 index 0000000..432e681 --- /dev/null +++ b/src/display/nr-filter-units.h @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_NR_FILTER_UNITS_H +#define SEEN_NR_FILTER_UNITS_H + +/* + * Utilities for handling coordinate system transformations in filters + * + * Author: + * Niko Kiirala <niko@kiirala.com> + * + * Copyright (C) 2007 Niko Kiirala + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "object/sp-filter-units.h" +#include <2geom/affine.h> +#include <2geom/rect.h> + +namespace Inkscape { +namespace Filters { + +/* Notes: + * - "filter units" is a coordinate system where the filter region is contained + * between (0,0) and (1,1). Do not confuse this with the filterUnits property + * - "primitive units" is the coordinate system in which all lengths and distances + * in the filter definition should be interpreted. They are affected by the value + * of the primitiveUnits attribute + * - "pb" is the coordinate system in which filter rendering happens. + * It might be aligned with user or screen coordinates depending on + * the filter primitives used in the filter. + * - "display" are world coordinates of the canvas - pixel grid coordinates + * of the drawing area translated so that (0,0) corresponds to the document origin + */ +class FilterUnits { +public: + FilterUnits(); + FilterUnits(SPFilterUnits const filterUnits, SPFilterUnits const primitiveUnits); + + /** + * Sets the current transformation matrix, i.e. transformation matrix + * from object's user coordinates to screen coordinates + */ + void set_ctm(Geom::Affine const &ctm); + + /** + * Sets the resolution, the filter should be rendered with. + */ + void set_resolution(double const x_res, double const y_res); + + /** + * Sets the item bounding box in user coordinates + */ + void set_item_bbox(Geom::OptRect const &bbox); + + /** + * Sets the filter effects area in user coordinates + */ + void set_filter_area(Geom::OptRect const &area); + + /** + * Sets, if x and y axis in pixblock coordinates should be paraller + * to x and y of user coordinates. + */ + void set_paraller(bool const paraller); + + /** + * Sets, if filter resolution is automatic. + * NOTE: even if resolution is automatic, it must be set with + * set_resolution. This only tells, if the set value is automatic. + */ + void set_automatic_resolution(bool const automatic); + + /** + * Gets the item bounding box in user coordinates + */ + Geom::OptRect get_item_bbox() const { return item_bbox; }; + + /** + * Gets the filter effects area in user coordinates + */ + Geom::OptRect get_filter_area() const { return filter_area; }; + + /** + * Gets Filter Units (userSpaceOnUse or objectBoundingBox) + */ + SPFilterUnits get_filter_units() const { return filterUnits; }; + + /** + * Gets Primitive Units (userSpaceOnUse or objectBoundingBox) + */ + SPFilterUnits get_primitive_units() const { return primitiveUnits; }; + + /** + * Gets the user coordinates to pixblock coordinates transformation matrix. + */ + Geom::Affine get_matrix_user2pb() const; + + /** + * Gets the filterUnits to pixblock coordinates transformation matrix. + */ + Geom::Affine get_matrix_filterunits2pb() const; + + /** + * Gets the primitiveUnits to pixblock coordinates transformation matrix. + */ + Geom::Affine get_matrix_primitiveunits2pb() const; + + /** + * Gets the display coordinates to pixblock coordinates transformation + * matrix. + */ + Geom::Affine get_matrix_display2pb() const; + + /** + * Gets the pixblock coordinates to display coordinates transformation + * matrix + */ + Geom::Affine get_matrix_pb2display() const; + + /** + * Gets the user coordinates to filterUnits transformation matrix. + */ + Geom::Affine get_matrix_user2filterunits() const; + + /** + * Gets the user coordinates to primitiveUnits transformation matrix. + */ + Geom::Affine get_matrix_user2primitiveunits() const; + + /** + * Returns the filter area in pixblock coordinates. + * NOTE: use only in filters, that define TRAIT_PARALLER in + * get_input_traits. The filter effects area may not be representable + * by simple rectangle otherwise. */ + Geom::IntRect get_pixblock_filterarea_paraller() const; + + FilterUnits& operator=(FilterUnits const &other); + +private: + Geom::Affine get_matrix_units2pb(SPFilterUnits units) const; + Geom::Affine get_matrix_user2units(SPFilterUnits units) const; + + SPFilterUnits filterUnits, primitiveUnits; + double resolution_x, resolution_y; + bool paraller_axis; + bool automatic_resolution; + Geom::Affine ctm; + Geom::OptRect item_bbox; + Geom::OptRect filter_area; + +}; + + +} /* namespace Filters */ +} /* namespace Inkscape */ + + +#endif /* __NR_FILTER_UNITS_H__ */ +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter-utils.h b/src/display/nr-filter-utils.h new file mode 100644 index 0000000..0e7612d --- /dev/null +++ b/src/display/nr-filter-utils.h @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_NR_FILTER_UTILS_H +#define SEEN_NR_FILTER_UTILS_H + +/** + * @file + * Definition of functions needed by several filters. + */ +/* + * Authors: + * Jean-Rene Reinhard <jr@komite.net> + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +namespace Inkscape { +namespace Filters { + +/** + * Clamps an integer value to a value between 0 and 255. Needed by filters where + * rendering computations can lead to component values out of bound. + * + * \return 0 if the value is smaller than 0, 255 if it is greater 255, else v + * \param v the value to clamp + */ +inline int clamp(int const val) { + if (val < 0) return 0; + if (val > 255) return 255; + return val; +} + +/** + * Clamps an integer value to a value between 0 and 255^3. + * + * \return 0 if the value is smaller than 0, 255^3 (16581375) if it is greater than 255^3, else v + * \param v the value to clamp + */ +inline int clamp3(int const val) { + if (val < 0) return 0; + if (val > 16581375) return 16581375; + return val; +} + +/** + * Macro to use the clamp function with double inputs and unsigned char output + */ +#define CLAMP_D_TO_U8(v) (unsigned char) clamp((int)round((v))) + +/** + * Clamps an integer to a value between 0 and alpha. Useful when handling + * images with premultiplied alpha, as setting some of RGB channels + * to a value bigger than alpha confuses the alpha blending in Inkscape + * \return 0 if val is negative, alpha if val is bigger than alpha, val otherwise + * \param val the value to clamp + * \param alpha the maximum value to clamp to + */ +inline int clamp_alpha(int const val, int const alpha) { + if (val < 0) return 0; + if (val > alpha) return alpha; + return val; +} + +/** + * Macro to use the clamp function with double inputs and unsigned char output + */ +#define CLAMP_D_TO_U8_ALPHA(v,a) (unsigned char) clamp_alpha((int)round((v)),(a)) + +} /* namespace Filters */ +} /* namespace Inkscape */ + +#endif /* __NR_FILTER_UTILS_H__ */ +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter.cpp b/src/display/nr-filter.cpp new file mode 100644 index 0000000..eec7c1a --- /dev/null +++ b/src/display/nr-filter.cpp @@ -0,0 +1,499 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG filters rendering + * + * Author: + * Niko Kiirala <niko@kiirala.com> + * + * Copyright (C) 2006-2008 Niko Kiirala + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glib.h> +#include <cmath> +#include <cstring> +#include <string> +#include <cairo.h> + +#include "display/nr-filter.h" +#include "display/nr-filter-primitive.h" +#include "display/nr-filter-slot.h" +#include "display/nr-filter-types.h" +#include "display/nr-filter-units.h" + +#include "display/nr-filter-blend.h" +#include "display/nr-filter-composite.h" +#include "display/nr-filter-convolve-matrix.h" +#include "display/nr-filter-colormatrix.h" +#include "display/nr-filter-component-transfer.h" +#include "display/nr-filter-diffuselighting.h" +#include "display/nr-filter-displacement-map.h" +#include "display/nr-filter-image.h" +#include "display/nr-filter-flood.h" +#include "display/nr-filter-gaussian.h" +#include "display/nr-filter-merge.h" +#include "display/nr-filter-morphology.h" +#include "display/nr-filter-offset.h" +#include "display/nr-filter-specularlighting.h" +#include "display/nr-filter-tile.h" +#include "display/nr-filter-turbulence.h" + +#include "display/cairo-utils.h" +#include "display/drawing.h" +#include "display/drawing-item.h" +#include "display/drawing-context.h" +#include "display/drawing-surface.h" +#include <2geom/affine.h> +#include <2geom/rect.h> +#include "svg/svg-length.h" +//#include "sp-filter-units.h" +#include "preferences.h" + +namespace Inkscape { +namespace Filters { + +using Geom::X; +using Geom::Y; + +Filter::Filter() +{ + _common_init(); +} + +Filter::Filter(int n) +{ + if (n > 0) _primitive.reserve(n); + _common_init(); +} + +void Filter::_common_init() { + _slot_count = 1; + // Having "not set" here as value means the output of last filter + // primitive will be used as output of this filter + _output_slot = NR_FILTER_SLOT_NOT_SET; + + // These are the default values for filter region, + // as specified in SVG standard + // NB: SVGLength.set takes prescaled percent values: -.10 means -10% + _region_x.set(SVGLength::PERCENT, -.10, 0); + _region_y.set(SVGLength::PERCENT, -.10, 0); + _region_width.set(SVGLength::PERCENT, 1.20, 0); + _region_height.set(SVGLength::PERCENT, 1.20, 0); + + // Filter resolution, negative value here stands for "automatic" + _x_pixels = -1.0; + _y_pixels = -1.0; + + _filter_units = SP_FILTER_UNITS_OBJECTBOUNDINGBOX; + _primitive_units = SP_FILTER_UNITS_USERSPACEONUSE; +} + +Filter::~Filter() +{ + clear_primitives(); +} + + +int Filter::render(Inkscape::DrawingItem const *item, DrawingContext &graphic, DrawingContext *bgdc) +{ + // std::cout << "Filter::render() for: " << const_cast<Inkscape::DrawingItem *>(item)->name() << std::endl; + // std::cout << " graphic drawing_scale: " << graphic.surface()->device_scale() << std::endl; + + if (_primitive.empty()) { + // when no primitives are defined, clear source graphic + graphic.setSource(0,0,0,0); + graphic.setOperator(CAIRO_OPERATOR_SOURCE); + graphic.paint(); + graphic.setOperator(CAIRO_OPERATOR_OVER); + return 1; + } + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + item->drawing().setFilterQuality(prefs->getInt("/options/filterquality/value", 0)); + item->drawing().setBlurQuality(prefs->getInt("/options/blurquality/value", 0)); + FilterQuality const filterquality = (FilterQuality)item->drawing().filterQuality(); + int const blurquality = item->drawing().blurQuality(); + + Geom::Affine trans = item->ctm(); + + Geom::OptRect filter_area = filter_effect_area(item->itemBounds()); + if (!filter_area) return 1; + + FilterUnits units(_filter_units, _primitive_units); + units.set_ctm(trans); + units.set_item_bbox(item->itemBounds()); + units.set_filter_area(*filter_area); + + std::pair<double,double> resolution + = _filter_resolution(*filter_area, trans, filterquality); + if (!(resolution.first > 0 && resolution.second > 0)) { + // zero resolution - clear source graphic and return + graphic.setSource(0,0,0,0); + graphic.setOperator(CAIRO_OPERATOR_SOURCE); + graphic.paint(); + graphic.setOperator(CAIRO_OPERATOR_OVER); + return 1; + } + + units.set_resolution(resolution.first, resolution.second); + if (_x_pixels > 0) { + units.set_automatic_resolution(false); + } + else { + units.set_automatic_resolution(true); + } + + units.set_paraller(false); + Geom::Affine pbtrans = units.get_matrix_display2pb(); + for (auto & i : _primitive) { + if (!i->can_handle_affine(pbtrans)) { + units.set_paraller(true); + break; + } + } + + FilterSlot slot(const_cast<Inkscape::DrawingItem*>(item), bgdc, graphic, units); + slot.set_quality(filterquality); + slot.set_blurquality(blurquality); + slot.set_device_scale(graphic.surface()->device_scale()); + + for (auto & i : _primitive) { + i->render_cairo(slot); + } + + Geom::Point origin = graphic.targetLogicalBounds().min(); + cairo_surface_t *result = slot.get_result(_output_slot); + + // Assume for the moment that we paint the filter in sRGB + set_cairo_surface_ci( result, SP_CSS_COLOR_INTERPOLATION_SRGB ); + + graphic.setSource(result, origin[Geom::X], origin[Geom::Y]); + graphic.setOperator(CAIRO_OPERATOR_SOURCE); + graphic.paint(); + graphic.setOperator(CAIRO_OPERATOR_OVER); + cairo_surface_destroy(result); + + return 0; +} + +void Filter::set_filter_units(SPFilterUnits unit) { + _filter_units = unit; +} + +void Filter::set_primitive_units(SPFilterUnits unit) { + _primitive_units = unit; +} + +void Filter::area_enlarge(Geom::IntRect &bbox, Inkscape::DrawingItem const *item) const { + for (auto i : _primitive) { + if (i) i->area_enlarge(bbox, item->ctm()); + } + +/* + TODO: something. See images at the bottom of filters.svg with medium-low + filtering quality. + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + FilterQuality const filterquality = (FilterQuality)prefs->getInt("/options/filterquality/value"); + + if (_x_pixels <= 0 && (filterquality == FILTER_QUALITY_BEST || + filterquality == FILTER_QUALITY_BETTER)) { + return; + } + + Geom::Rect item_bbox; + Geom::OptRect maybe_bbox = item->itemBounds(); + if (maybe_bbox.empty()) { + // Code below needs a bounding box + return; + } + item_bbox = *maybe_bbox; + + std::pair<double,double> res_low + = _filter_resolution(item_bbox, item->ctm(), filterquality); + //std::pair<double,double> res_full + // = _filter_resolution(item_bbox, item->ctm(), FILTER_QUALITY_BEST); + double pixels_per_block = fmax(item_bbox.width() / res_low.first, + item_bbox.height() / res_low.second); + bbox.x0 -= (int)pixels_per_block; + bbox.x1 += (int)pixels_per_block; + bbox.y0 -= (int)pixels_per_block; + bbox.y1 += (int)pixels_per_block; +*/ +} + +Geom::OptRect Filter::filter_effect_area(Geom::OptRect const &bbox) +{ + Geom::Point minp, maxp; + + if (_filter_units == SP_FILTER_UNITS_OBJECTBOUNDINGBOX) { + + double len_x = bbox ? bbox->width() : 0; + double len_y = bbox ? bbox->height() : 0; + /* TODO: fetch somehow the object ex and em lengths */ + + // Update for em, ex, and % values + _region_x.update(12, 6, len_x); + _region_y.update(12, 6, len_y); + _region_width.update(12, 6, len_x); + _region_height.update(12, 6, len_y); + + if (!bbox) return Geom::OptRect(); + + if (_region_x.unit == SVGLength::PERCENT) { + minp[X] = bbox->left() + _region_x.computed; + } else { + minp[X] = bbox->left() + _region_x.computed * len_x; + } + if (_region_width.unit == SVGLength::PERCENT) { + maxp[X] = minp[X] + _region_width.computed; + } else { + maxp[X] = minp[X] + _region_width.computed * len_x; + } + + if (_region_y.unit == SVGLength::PERCENT) { + minp[Y] = bbox->top() + _region_y.computed; + } else { + minp[Y] = bbox->top() + _region_y.computed * len_y; + } + if (_region_height.unit == SVGLength::PERCENT) { + maxp[Y] = minp[Y] + _region_height.computed; + } else { + maxp[Y] = minp[Y] + _region_height.computed * len_y; + } + } else if (_filter_units == SP_FILTER_UNITS_USERSPACEONUSE) { + // Region already set in sp-filter.cpp + minp[X] = _region_x.computed; + maxp[X] = minp[X] + _region_width.computed; + minp[Y] = _region_y.computed; + maxp[Y] = minp[Y] + _region_height.computed; + } else { + g_warning("Error in Inkscape::Filters::Filter::filter_effect_area: unrecognized value of _filter_units"); + } + + Geom::OptRect area(minp, maxp); + // std::cout << "Filter::filter_effect_area: area: " << *area << std::endl; + return area; +} + +double Filter::complexity(Geom::Affine const &ctm) +{ + double factor = 1.0; + for (auto & i : _primitive) { + if (i) { + double f = i->complexity(ctm); + factor += (f - 1.0); + } + } + return factor; +} + +bool Filter::uses_background() +{ + for (auto & i : _primitive) { + if (i && i->uses_background()) { + return true; + } + } + return false; +} + +/* Constructor table holds pointers to static methods returning filter + * primitives. This table is indexed with FilterPrimitiveType, so that + * for example method in _constructor[NR_FILTER_GAUSSIANBLUR] + * returns a filter object of type Inkscape::Filters::FilterGaussian. + */ +typedef FilterPrimitive*(*FilterConstructor)(); +static FilterConstructor _constructor[NR_FILTER_ENDPRIMITIVETYPE]; + +void Filter::_create_constructor_table() +{ + // Constructor table won't change in run-time, so no need to recreate + static bool created = false; + if(created) return; + +/* Some filter classes are not implemented yet. + Some of them still have only boilerplate code.*/ + _constructor[NR_FILTER_BLEND] = &FilterBlend::create; + _constructor[NR_FILTER_COLORMATRIX] = &FilterColorMatrix::create; + _constructor[NR_FILTER_COMPONENTTRANSFER] = &FilterComponentTransfer::create; + _constructor[NR_FILTER_COMPOSITE] = &FilterComposite::create; + _constructor[NR_FILTER_CONVOLVEMATRIX] = &FilterConvolveMatrix::create; + _constructor[NR_FILTER_DIFFUSELIGHTING] = &FilterDiffuseLighting::create; + _constructor[NR_FILTER_DISPLACEMENTMAP] = &FilterDisplacementMap::create; + _constructor[NR_FILTER_FLOOD] = &FilterFlood::create; + _constructor[NR_FILTER_GAUSSIANBLUR] = &FilterGaussian::create; + _constructor[NR_FILTER_IMAGE] = &FilterImage::create; + _constructor[NR_FILTER_MERGE] = &FilterMerge::create; + _constructor[NR_FILTER_MORPHOLOGY] = &FilterMorphology::create; + _constructor[NR_FILTER_OFFSET] = &FilterOffset::create; + _constructor[NR_FILTER_SPECULARLIGHTING] = &FilterSpecularLighting::create; + _constructor[NR_FILTER_TILE] = &FilterTile::create; + _constructor[NR_FILTER_TURBULENCE] = &FilterTurbulence::create; + created = true; +} + +int Filter::add_primitive(FilterPrimitiveType type) +{ + _create_constructor_table(); + + // Check that we can create a new filter of specified type + if (type < 0 || type >= NR_FILTER_ENDPRIMITIVETYPE) + return -1; + if (!_constructor[type]) return -1; + FilterPrimitive *created = _constructor[type](); + + int handle = _primitive.size(); + _primitive.push_back(created); + return handle; +} + +int Filter::replace_primitive(int target, FilterPrimitiveType type) +{ + _create_constructor_table(); + + // Check that target is valid primitive inside this filter + if (target < 0) return -1; + if (static_cast<unsigned>(target) >= _primitive.size()) return -1; + + // Check that we can create a new filter of specified type + if (type < 0 || type >= NR_FILTER_ENDPRIMITIVETYPE) + return -1; + if (!_constructor[type]) return -1; + FilterPrimitive *created = _constructor[type](); + + delete _primitive[target]; + _primitive[target] = created; + return target; +} + +FilterPrimitive *Filter::get_primitive(int handle) { + if (handle < 0 || handle >= static_cast<int>(_primitive.size())) return nullptr; + return _primitive[handle]; +} + +void Filter::clear_primitives() +{ + for (auto & i : _primitive) { + delete i; + } + _primitive.clear(); +} + +void Filter::set_x(SVGLength const &length) +{ + if (length._set) + _region_x = length; +} +void Filter::set_y(SVGLength const &length) +{ + if (length._set) + _region_y = length; +} +void Filter::set_width(SVGLength const &length) +{ + if (length._set) + _region_width = length; +} +void Filter::set_height(SVGLength const &length) +{ + if (length._set) + _region_height = length; +} + +void Filter::set_resolution(double const pixels) { + if (pixels > 0) { + _x_pixels = pixels; + _y_pixels = pixels; + } +} + +void Filter::set_resolution(double const x_pixels, double const y_pixels) { + if (x_pixels >= 0 && y_pixels >= 0) { + _x_pixels = x_pixels; + _y_pixels = y_pixels; + } +} + +void Filter::reset_resolution() { + _x_pixels = -1; + _y_pixels = -1; +} + +int Filter::_resolution_limit(FilterQuality const quality) const { + int limit = -1; + switch (quality) { + case FILTER_QUALITY_WORST: + limit = 32; + break; + case FILTER_QUALITY_WORSE: + limit = 64; + break; + case FILTER_QUALITY_NORMAL: + limit = 256; + break; + case FILTER_QUALITY_BETTER: + case FILTER_QUALITY_BEST: + default: + break; + } + return limit; +} + +std::pair<double,double> Filter::_filter_resolution( + Geom::Rect const &area, Geom::Affine const &trans, + FilterQuality const filterquality) const +{ + std::pair<double,double> resolution; + if (_x_pixels > 0) { + double y_len; + if (_y_pixels > 0) { + y_len = _y_pixels; + } else { + y_len = (_x_pixels * (area.max()[Y] - area.min()[Y])) + / (area.max()[X] - area.min()[X]); + } + resolution.first = _x_pixels; + resolution.second = y_len; + } else { + Geom::Point origo = area.min(); + origo *= trans; + Geom::Point max_i(area.max()[X], area.min()[Y]); + max_i *= trans; + Geom::Point max_j(area.min()[X], area.max()[Y]); + max_j *= trans; + double i_len = sqrt((origo[X] - max_i[X]) * (origo[X] - max_i[X]) + + (origo[Y] - max_i[Y]) * (origo[Y] - max_i[Y])); + double j_len = sqrt((origo[X] - max_j[X]) * (origo[X] - max_j[X]) + + (origo[Y] - max_j[Y]) * (origo[Y] - max_j[Y])); + int limit = _resolution_limit(filterquality); + if (limit > 0 && (i_len > limit || j_len > limit)) { + double aspect_ratio = i_len / j_len; + if (i_len > j_len) { + i_len = limit; + j_len = i_len / aspect_ratio; + } + else { + j_len = limit; + i_len = j_len * aspect_ratio; + } + } + resolution.first = i_len; + resolution.second = j_len; + } + return resolution; +} + +} /* namespace Filters */ +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-filter.h b/src/display/nr-filter.h new file mode 100644 index 0000000..caa7188 --- /dev/null +++ b/src/display/nr-filter.h @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_NR_FILTER_H +#define SEEN_NR_FILTER_H + +/* + * SVG filters rendering + * + * Author: + * Niko Kiirala <niko@kiirala.com> + * + * Copyright (C) 2006 Niko Kiirala + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +//#include "display/nr-arena-item.h" +#include <cairo.h> +#include "display/nr-filter-primitive.h" +#include "display/nr-filter-types.h" +#include "svg/svg-length.h" +#include "object/sp-filter-units.h" +#include "inkgc/gc-managed.h" + +namespace Inkscape { +class DrawingContext; +class DrawingItem; + +namespace Filters { + +class Filter { +public: + /** Given background state from @a bgdc and an intermediate rendering from the surface + * backing @a graphic, modify the contents of the surface backing @a graphic to represent + * the results of filter rendering. @a bgarea and @a area specify bounding boxes + * of both surfaces in world coordinates; Cairo contexts are assumed to be in default state + * (0,0 = surface origin, no path, OVER operator) */ + int render(Inkscape::DrawingItem const *item, DrawingContext &graphic, DrawingContext *bgdc); + + /** + * Creates a new filter primitive under this filter object. + * New primitive is placed so that it will be executed after all filter + * primitives defined beforehand for this filter object. + * Should this filter not have enough space for a new primitive, the filter + * is enlarged to accommodate the new filter element. It may be enlarged by + * more that one element. + * Returns a handle (non-negative integer) to the filter primitive created. + * Returns -1, if type is not valid filter primitive type or filter + * primitive of such type cannot be created. + */ + int add_primitive(FilterPrimitiveType type); + /** + * Removes all filter primitives from this filter. + * All pointers to filter primitives inside this filter should be + * considered invalid after calling this function. + */ + void clear_primitives(); + /** + * Replaces filter primitive pointed by 'target' with a new filter + * primitive of type 'type' + * If 'target' does not correspond to any primitive inside this filter OR + * 'type' is not a valid filter primitive type OR + * filter primitive of such type cannot be created, + * this function returns -1 and doesn't change the internal state of this + * filter. + * Otherwise, a new filter primitive is created. Any pointers to filter + * primitive 'target' should be considered invalid. A handle to the + * newly created primitive is returned. + */ + int replace_primitive(int primitive, FilterPrimitiveType type); + + /** + * Returns a pointer to the primitive, which the handle corresponds to. + * If the handle is not valid, returns NULL. + */ + FilterPrimitive *get_primitive(int handle); + + /** + * Sets the slot number 'slot' to be used as result from this filter. + * If output is not set, the output from last filter primitive is used as + * output from the filter. + * It is an error to specify a pre-defined slot as 'slot'. Such call does + * not have any effect to the state of filter or its primitives. + */ + void set_output(int slot); + + void set_x(SVGLength const &length); + void set_y(SVGLength const &length); + void set_width(SVGLength const &length); + void set_height(SVGLength const &length); + + /** + * Sets the filter effects region. + * Passing an unset length (length._set == false) as any of the parameters + * results in that parameter not being changed. + * Filter will not hold any references to the passed SVGLength object after + * function returns. + * If any of these parameters does not get set, the default value, as + * defined in SVG standard, for that parameter is used instead. + */ + void set_region(SVGLength const &x, SVGLength const &y, + SVGLength const &width, SVGLength const &height); + + /** + * Resets the filter effects region to its default value as defined + * in SVG standard. + */ + void reset_region(); + + /** + * Sets the width of intermediate images in pixels. If not set, suitable + * resolution is determined automatically. If x_pixels is less than zero, + * calling this function results in no changes to filter state. + */ + void set_resolution(double const x_pixels); + + /** + * Sets the width and height of intermediate images in pixels. If not set, + * suitable resolution is determined automatically. If either parameter is + * less than zero, calling this function results in no changes to filter + * state. + */ + void set_resolution(double const x_pixels, double const y_pixels); + + /** + * Resets the filter resolution to its default value, i.e. automatically + * determined. + */ + void reset_resolution(); + + /** + * Set the filterUnits-property. If not set, the default value of + * objectBoundingBox is used. If the parameter value is not a + * valid enumeration value from SPFilterUnits, no changes to filter state + * are made. + */ + void set_filter_units(SPFilterUnits unit); + + /** + * Set the primitiveUnits-property. If not set, the default value of + * userSpaceOnUse is used. If the parameter value is not a valid + * enumeration value from SPFilterUnits, no changes to filter state + * are made. + */ + void set_primitive_units(SPFilterUnits unit); + + /** + * Modifies the given area to accommodate for filters needing pixels + * outside the rendered area. + * When this function returns, area contains the area that needs + * to be rendered so that after filtering, the original area is + * drawn correctly. + */ + void area_enlarge(Geom::IntRect &area, Inkscape::DrawingItem const *item) const; + /** + * Returns the filter effects area in user coordinate system. + * The given bounding box should be a bounding box as specified in + * SVG standard and in user coordinate system. + */ + Geom::OptRect filter_effect_area(Geom::OptRect const &bbox); + + // returns cache score factor + double complexity(Geom::Affine const &ctm); + + // says whether the filter accesses any of the background images + bool uses_background(); + + /** Creates a new filter with space for one filter element */ + Filter(); + /** + * Creates a new filter with space for n filter elements. If number of + * filter elements is known beforehand, it's better to use this + * constructor. + */ + Filter(int n); + /** Destroys the filter and all its primitives */ + virtual ~Filter(); + +private: + std::vector<FilterPrimitive*> _primitive; + /** Amount of image slots used, when this filter was rendered last time */ + int _slot_count; + + /** Image slot, from which filter output should be read. + * Negative values mean 'not set' */ + int _output_slot; + + SVGLength _region_x; + SVGLength _region_y; + SVGLength _region_width; + SVGLength _region_height; + + /* x- and y-resolutions for filter rendering. + * Negative values mean 'not set'. + * If _y_pixels is set, _x_pixels should be set, too. */ + double _x_pixels; + double _y_pixels; + + SPFilterUnits _filter_units; + SPFilterUnits _primitive_units; + + void _create_constructor_table(); + void _common_init(); + int _resolution_limit(FilterQuality const quality) const; + std::pair<double,double> _filter_resolution(Geom::Rect const &area, + Geom::Affine const &trans, + FilterQuality const q) const; +}; + + +} /* namespace Filters */ +} /* namespace Inkscape */ + + +#endif /* __NR_FILTER_H__ */ +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-light-types.h b/src/display/nr-light-types.h new file mode 100644 index 0000000..8de142f --- /dev/null +++ b/src/display/nr-light-types.h @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2014 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_NR_LIGHT_TYPES_H +#define SEEN_NR_LIGHT_TYPES_H + +namespace Inkscape { +namespace Filters { + +enum LightType{ + NO_LIGHT = 0, + DISTANT_LIGHT, + POINT_LIGHT, + SPOT_LIGHT +}; + +} /* namespace Filters */ +} /* namespace Inkscape */ + +#endif // __NR_LIGHT_TYPES_H__ +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-light.cpp b/src/display/nr-light.cpp new file mode 100644 index 0000000..3009955 --- /dev/null +++ b/src/display/nr-light.cpp @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Light rendering helpers + * + * Author: + * Jean-Rene Reinhard <jr@komite.net> + * + * Copyright (C) 2006 Jean-Rene Reinhard + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cmath> + +#include "display/nr-light.h" +#include "display/nr-3dutils.h" +#include "object/filters/distantlight.h" +#include "object/filters/pointlight.h" +#include "object/filters/spotlight.h" +#include "color.h" + +namespace Inkscape { +namespace Filters { + +DistantLight::DistantLight(SPFeDistantLight *light, guint32 lighting_color) { + color = lighting_color; + azimuth = M_PI / 180 * light->azimuth; + elevation = M_PI / 180 * light->elevation; +} + +DistantLight::~DistantLight() = default; + +void DistantLight::light_vector(NR::Fvector &v) { + v[X_3D] = std::cos(azimuth)*std::cos(elevation); + v[Y_3D] = std::sin(azimuth)*std::cos(elevation); + v[Z_3D] = std::sin(elevation); +} + +void DistantLight::light_components(NR::Fvector &lc) { + lc[LIGHT_RED] = SP_RGBA32_R_U(color); + lc[LIGHT_GREEN] = SP_RGBA32_G_U(color); + lc[LIGHT_BLUE] = SP_RGBA32_B_U(color); +} + +PointLight::PointLight(SPFePointLight *light, guint32 lighting_color, const Geom::Affine &trans, int device_scale) { + color = lighting_color; + l_x = light->x * device_scale; + l_y = light->y * device_scale; + l_z = light->z * device_scale; + NR::convert_coord(l_x, l_y, l_z, trans); +} + +PointLight::~PointLight() = default; + +void PointLight::light_vector(NR::Fvector &v, double x, double y, double z) { + v[X_3D] = l_x - x; + v[Y_3D] = l_y - y; + v[Z_3D] = l_z - z; + NR::normalize_vector(v); +} + +void PointLight::light_components(NR::Fvector &lc) { + lc[LIGHT_RED] = SP_RGBA32_R_U(color); + lc[LIGHT_GREEN] = SP_RGBA32_G_U(color); + lc[LIGHT_BLUE] = SP_RGBA32_B_U(color); +} + +SpotLight::SpotLight(SPFeSpotLight *light, guint32 lighting_color, const Geom::Affine &trans, int device_scale) { + double p_x, p_y, p_z; + color = lighting_color; + l_x = light->x * device_scale; + l_y = light->y * device_scale; + l_z = light->z * device_scale; + p_x = light->pointsAtX * device_scale; + p_y = light->pointsAtY * device_scale; + p_z = light->pointsAtZ * device_scale; + cos_lca = std::cos(M_PI / 180 * light->limitingConeAngle); + speExp = light->specularExponent; + NR::convert_coord(l_x, l_y, l_z, trans); + NR::convert_coord(p_x, p_y, p_z, trans); + S[X_3D] = p_x - l_x; + S[Y_3D] = p_y - l_y; + S[Z_3D] = p_z - l_z; + NR::normalize_vector(S); + +} + +SpotLight::~SpotLight() = default; + +void SpotLight::light_vector(NR::Fvector &v, double x, double y, double z) { + v[X_3D] = l_x - x; + v[Y_3D] = l_y - y; + v[Z_3D] = l_z - z; + NR::normalize_vector(v); +} + +void SpotLight::light_components(NR::Fvector &lc, const NR::Fvector &L) { + double spmod = (-1) * NR::scalar_product(L, S); + if (spmod <= cos_lca) + spmod = 0; + else + spmod = std::pow(spmod, speExp); + lc[LIGHT_RED] = spmod * SP_RGBA32_R_U(color); + lc[LIGHT_GREEN] = spmod * SP_RGBA32_G_U(color); + lc[LIGHT_BLUE] = spmod * SP_RGBA32_B_U(color); +} + +} /* namespace Filters */ +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-light.h b/src/display/nr-light.h new file mode 100644 index 0000000..ae119ce --- /dev/null +++ b/src/display/nr-light.h @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2017 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_NR_LIGHT_H +#define SEEN_NR_LIGHT_H + +/** \file + * These classes provide tools to compute interesting objects relative to light + * sources. Each class provides a constructor converting information contained + * in a sp light object into information useful in the current setting, a + * method to get the light vector (at a given point) and a method to get the + * light color components (at a given point). + */ + +#include <2geom/forward.h> + +#include "display/nr-3dutils.h" +#include "display/nr-light-types.h" + +class SPFeDistantLight; +class SPFePointLight; +class SPFeSpotLight; +typedef unsigned int guint32; + +namespace Inkscape { +namespace Filters { + +enum LightComponent { + LIGHT_RED = 0, + LIGHT_GREEN, + LIGHT_BLUE +}; + +class DistantLight { + public: + /** + * Constructor + * + * \param light the sp light object + * \param lighting_color the lighting_color used + */ + DistantLight(SPFeDistantLight *light, guint32 lighting_color); + virtual ~DistantLight(); + + /** + * Computes the light vector of the distant light + * + * \param v a Fvector reference where we store the result + */ + void light_vector(NR::Fvector &v); + + /** + * Computes the light components of the distant light + * + * \param lc a Fvector reference where we store the result, X=R, Y=G, Z=B + */ + void light_components(NR::Fvector &lc); + + private: + guint32 color; + double azimuth; //azimuth in rad + double elevation; //elevation in rad +}; + +class PointLight { + public: + /** + * Constructor + * + * \param light the sp light object + * \param lighting_color the lighting_color used + * \param trans the transformation between absolute coordinate (those + * employed in the sp light object) and current coordinate (those + * employed in the rendering) + * \param device_scale for high DPI monitors. + */ + PointLight(SPFePointLight *light, guint32 lighting_color, const Geom::Affine &trans, int device_scale = 1); + virtual ~PointLight(); + /** + * Computes the light vector of the distant light at point (x,y,z). + * x, y and z are given in the arena_item coordinate, they are used as + * is + * + * \param v a Fvector reference where we store the result + * \param x x coordinate of the current point + * \param y y coordinate of the current point + * \param z z coordinate of the current point + */ + void light_vector(NR::Fvector &v, double x, double y, double z); + + /** + * Computes the light components of the distant light + * + * \param lc a Fvector reference where we store the result, X=R, Y=G, Z=B + */ + void light_components(NR::Fvector &lc); + + private: + guint32 color; + //light position coordinates in render setting + double l_x; + double l_y; + double l_z; +}; + +class SpotLight { + public: + /** + * Constructor + * + * \param light the sp light object + * \param lighting_color the lighting_color used + * \param trans the transformation between absolute coordinate (those + * employed in the sp light object) and current coordinate (those + * employed in the rendering) + * \param device_scale for high DPI monitors. + */ + SpotLight(SPFeSpotLight *light, guint32 lighting_color, const Geom::Affine &trans, int device_scale = 1); + virtual ~SpotLight(); + + /** + * Computes the light vector of the distant light at point (x,y,z). + * x, y and z are given in the arena_item coordinate, they are used as + * is + * + * \param v a Fvector reference where we store the result + * \param x x coordinate of the current point + * \param y y coordinate of the current point + * \param z z coordinate of the current point + */ + void light_vector(NR::Fvector &v, double x, double y, double z); + + /** + * Computes the light components of the distant light at the current + * point. We only need the light vector to compute these + * + * \param lc a Fvector reference where we store the result, X=R, Y=G, Z=B + * \param L the light vector of the current point + */ + void light_components(NR::Fvector &lc, const NR::Fvector &L); + + private: + guint32 color; + //light position coordinates in render setting + double l_x; + double l_y; + double l_z; + double cos_lca; //cos of the limiting cone angle + double speExp; //specular exponent; + NR::Fvector S; //unit vector from light position in the direction + //the spot point at +}; + + +} /* namespace Filters */ +} /* namespace Inkscape */ + +#endif // __NR_LIGHT_H__ +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-style.cpp b/src/display/nr-style.cpp new file mode 100644 index 0000000..fc1b19a --- /dev/null +++ b/src/display/nr-style.cpp @@ -0,0 +1,431 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Style information for rendering. + *//* + * Authors: + * Krzysztof KosiĆski <tweenk.pl@gmail.com> + * + * Copyright (C) 2010 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "display/nr-style.h" +#include "style.h" +#include "object/sp-paint-server.h" +#include "display/canvas-bpath.h" // contains SPStrokeJoinType, SPStrokeCapType etc. (WTF!) +#include "display/drawing-context.h" +#include "display/drawing-pattern.h" + +void NRStyle::Paint::clear() +{ + if (server) { + sp_object_unref(server, nullptr); + server = nullptr; + } + type = PAINT_NONE; +} + +void NRStyle::Paint::set(SPColor const &c) +{ + clear(); + type = PAINT_COLOR; + color = c; +} + +void NRStyle::Paint::set(SPPaintServer *ps) +{ + clear(); + if (ps) { + type = PAINT_SERVER; + server = ps; + sp_object_ref(server, nullptr); + } +} + +void NRStyle::Paint::set(const SPIPaint* paint) +{ + if (paint->isPaintserver()) { + SPPaintServer* server = paint->value.href->getObject(); + if (server && server->isValid()) { + set(server); + } else if (paint->colorSet) { + set(paint->value.color); + } else { + clear(); + } + } else if (paint->isColor()) { + set(paint->value.color); + } else if (paint->isNone()) { + clear(); + } else if (paint->paintOrigin == SP_CSS_PAINT_ORIGIN_CONTEXT_FILL || + paint->paintOrigin == SP_CSS_PAINT_ORIGIN_CONTEXT_STROKE) { + // A marker in the defs section will result in ending up here. + // std::cerr << "NRStyle::Paint::set: Double" << std::endl; + } else { + g_assert_not_reached(); + } +} + + +NRStyle::NRStyle() + : fill() + , stroke() + , stroke_width(0.0) + , miter_limit(0.0) + , n_dash(0) + , dash(nullptr) + , dash_offset(0.0) + , fill_rule(CAIRO_FILL_RULE_EVEN_ODD) + , line_cap(CAIRO_LINE_CAP_BUTT) + , line_join(CAIRO_LINE_JOIN_MITER) + , fill_pattern(nullptr) + , stroke_pattern(nullptr) + , text_decoration_fill_pattern(nullptr) + , text_decoration_stroke_pattern(nullptr) + , text_decoration_line(TEXT_DECORATION_LINE_CLEAR) + , text_decoration_style(TEXT_DECORATION_STYLE_CLEAR) + , text_decoration_fill() + , text_decoration_stroke() + , text_decoration_stroke_width(0.0) + , phase_length(0.0) + , tspan_line_start(false) + , tspan_line_end(false) + , tspan_width(0) + , ascender(0) + , descender(0) + , underline_thickness(0) + , underline_position(0) + , line_through_thickness(0) + , line_through_position(0) + , font_size(0) +{ + paint_order_layer[0] = PAINT_ORDER_NORMAL; +} + +NRStyle::~NRStyle() +{ + if (fill_pattern) cairo_pattern_destroy(fill_pattern); + if (stroke_pattern) cairo_pattern_destroy(stroke_pattern); + if (text_decoration_fill_pattern) cairo_pattern_destroy(text_decoration_fill_pattern); + if (text_decoration_stroke_pattern) cairo_pattern_destroy(text_decoration_stroke_pattern); + if (dash){ + delete [] dash; + } + fill.clear(); + stroke.clear(); + text_decoration_fill.clear(); + text_decoration_stroke.clear(); +} + +void NRStyle::set(SPStyle *style, SPStyle *context_style) +{ + // Handle 'context-fill' and 'context-stroke': Work in progress + const SPIPaint *style_fill = &(style->fill); + if( style_fill->paintOrigin == SP_CSS_PAINT_ORIGIN_CONTEXT_FILL ) { + if( context_style != nullptr ) { + style_fill = &(context_style->fill); + } else { + // A marker in the defs section will result in ending up here. + //std::cerr << "NRStyle::set: 'context-fill': 'context_style' is NULL" << std::endl; + } + } else if ( style_fill->paintOrigin == SP_CSS_PAINT_ORIGIN_CONTEXT_STROKE ) { + if( context_style != nullptr ) { + style_fill = &(context_style->stroke); + } else { + //std::cerr << "NRStyle::set: 'context-stroke': 'context_style' is NULL" << std::endl; + } + } + + fill.set(style_fill); + fill.opacity = SP_SCALE24_TO_FLOAT(style->fill_opacity.value); + + switch (style->fill_rule.computed) { + case SP_WIND_RULE_EVENODD: + fill_rule = CAIRO_FILL_RULE_EVEN_ODD; + break; + case SP_WIND_RULE_NONZERO: + fill_rule = CAIRO_FILL_RULE_WINDING; + break; + default: + g_assert_not_reached(); + } + + const SPIPaint *style_stroke = &(style->stroke); + if( style_stroke->paintOrigin == SP_CSS_PAINT_ORIGIN_CONTEXT_FILL ) { + if( context_style != nullptr ) { + style_stroke = &(context_style->fill); + } else { + //std::cerr << "NRStyle::set: 'context-fill': 'context_style' is NULL" << std::endl; + } + } else if ( style_stroke->paintOrigin == SP_CSS_PAINT_ORIGIN_CONTEXT_STROKE ) { + if( context_style != nullptr ) { + style_stroke = &(context_style->stroke); + } else { + //std::cerr << "NRStyle::set: 'context-stroke': 'context_style' is NULL" << std::endl; + } + } + + stroke.set(style_stroke); + stroke.opacity = SP_SCALE24_TO_FLOAT(style->stroke_opacity.value); + stroke_width = style->stroke_width.computed; + switch (style->stroke_linecap.computed) { + case SP_STROKE_LINECAP_ROUND: + line_cap = CAIRO_LINE_CAP_ROUND; + break; + case SP_STROKE_LINECAP_SQUARE: + line_cap = CAIRO_LINE_CAP_SQUARE; + break; + case SP_STROKE_LINECAP_BUTT: + line_cap = CAIRO_LINE_CAP_BUTT; + break; + default: + g_assert_not_reached(); + } + switch (style->stroke_linejoin.computed) { + case SP_STROKE_LINEJOIN_ROUND: + line_join = CAIRO_LINE_JOIN_ROUND; + break; + case SP_STROKE_LINEJOIN_BEVEL: + line_join = CAIRO_LINE_JOIN_BEVEL; + break; + case SP_STROKE_LINEJOIN_MITER: + line_join = CAIRO_LINE_JOIN_MITER; + break; + default: + g_assert_not_reached(); + } + miter_limit = style->stroke_miterlimit.value; + + if (dash){ + delete [] dash; + } + + n_dash = style->stroke_dasharray.values.size(); + if (n_dash != 0) { + dash_offset = style->stroke_dashoffset.computed; + dash = new double[n_dash]; + for (unsigned int i = 0; i < n_dash; ++i) { + dash[i] = style->stroke_dasharray.values[i].computed; + } + } else { + dash_offset = 0.0; + dash = nullptr; + } + + + for( unsigned i = 0; i < PAINT_ORDER_LAYERS; ++i) { + switch (style->paint_order.layer[i]) { + case SP_CSS_PAINT_ORDER_NORMAL: + paint_order_layer[i]=PAINT_ORDER_NORMAL; + break; + case SP_CSS_PAINT_ORDER_FILL: + paint_order_layer[i]=PAINT_ORDER_FILL; + break; + case SP_CSS_PAINT_ORDER_STROKE: + paint_order_layer[i]=PAINT_ORDER_STROKE; + break; + case SP_CSS_PAINT_ORDER_MARKER: + paint_order_layer[i]=PAINT_ORDER_MARKER; + break; + } + } + + text_decoration_line = TEXT_DECORATION_LINE_CLEAR; + if(style->text_decoration_line.inherit ){ text_decoration_line |= TEXT_DECORATION_LINE_INHERIT; } + if(style->text_decoration_line.underline ){ text_decoration_line |= TEXT_DECORATION_LINE_UNDERLINE + TEXT_DECORATION_LINE_SET; } + if(style->text_decoration_line.overline ){ text_decoration_line |= TEXT_DECORATION_LINE_OVERLINE + TEXT_DECORATION_LINE_SET; } + if(style->text_decoration_line.line_through){ text_decoration_line |= TEXT_DECORATION_LINE_LINETHROUGH + TEXT_DECORATION_LINE_SET; } + if(style->text_decoration_line.blink ){ text_decoration_line |= TEXT_DECORATION_LINE_BLINK + TEXT_DECORATION_LINE_SET; } + + text_decoration_style = TEXT_DECORATION_STYLE_CLEAR; + if(style->text_decoration_style.inherit ){ text_decoration_style |= TEXT_DECORATION_STYLE_INHERIT; } + if(style->text_decoration_style.solid ){ text_decoration_style |= TEXT_DECORATION_STYLE_SOLID + TEXT_DECORATION_STYLE_SET; } + if(style->text_decoration_style.isdouble ){ text_decoration_style |= TEXT_DECORATION_STYLE_ISDOUBLE + TEXT_DECORATION_STYLE_SET; } + if(style->text_decoration_style.dotted ){ text_decoration_style |= TEXT_DECORATION_STYLE_DOTTED + TEXT_DECORATION_STYLE_SET; } + if(style->text_decoration_style.dashed ){ text_decoration_style |= TEXT_DECORATION_STYLE_DASHED + TEXT_DECORATION_STYLE_SET; } + if(style->text_decoration_style.wavy ){ text_decoration_style |= TEXT_DECORATION_STYLE_WAVY + TEXT_DECORATION_STYLE_SET; } + + /* FIXME + The meaning of text-decoration-color in CSS3 for SVG is ambiguous (2014-05-06). Set + it for fill, for stroke, for both? Both would seem like the obvious choice but what happens + is that for text which is just fill (very common) it makes the lines fatter because it + enables stroke on the decorations when it wasn't present on the text. That contradicts the + usual behavior where the text and decorations by default have the same fill/stroke. + + The behavior here is that if color is defined it is applied to text_decoration_fill/stroke + ONLY if the corresponding fill/stroke is also present. + + Hopefully the standard will be clarified to resolve this issue. + */ + + // Unless explicitly set on an element, text decoration is inherited from + // closest ancestor where 'text-decoration' was set. That is, setting + // 'text-decoration' on an ancestor fixes the fill and stroke of the + // decoration to the fill and stroke values of that ancestor. + SPStyle* style_td = style; + if ( style->text_decoration.style_td ) style_td = style->text_decoration.style_td; + text_decoration_stroke.opacity = SP_SCALE24_TO_FLOAT(style_td->stroke_opacity.value); + text_decoration_stroke_width = style_td->stroke_width.computed; + + // Priority is given in order: + // * text_decoration_fill + // * text_decoration_color (only if fill set) + // * fill + if (style_td->text_decoration_fill.set) { + text_decoration_fill.set(&(style_td->text_decoration_fill)); + } else if (style_td->text_decoration_color.set) { + if(style->fill.isPaintserver() || style->fill.isColor()) { + // SVG sets color specifically + text_decoration_fill.set(style->text_decoration_color.value.color); + } else { + // No decoration fill because no text fill + text_decoration_fill.clear(); + } + } else { + // Pick color/pattern from text + text_decoration_fill.set(&(style_td->fill)); + } + + if (style_td->text_decoration_stroke.set) { + text_decoration_stroke.set(&(style_td->text_decoration_stroke)); + } else if (style_td->text_decoration_color.set) { + if(style->stroke.isPaintserver() || style->stroke.isColor()) { + // SVG sets color specifically + text_decoration_stroke.set(style->text_decoration_color.value.color); + } else { + // No decoration stroke because no text stroke + text_decoration_stroke.clear(); + } + } else { + // Pick color/pattern from text + text_decoration_stroke.set(&(style_td->stroke)); + } + + if (text_decoration_line != TEXT_DECORATION_LINE_CLEAR) { + phase_length = style->text_decoration_data.phase_length; + tspan_line_start = style->text_decoration_data.tspan_line_start; + tspan_line_end = style->text_decoration_data.tspan_line_end; + tspan_width = style->text_decoration_data.tspan_width; + ascender = style->text_decoration_data.ascender; + descender = style->text_decoration_data.descender; + underline_thickness = style->text_decoration_data.underline_thickness; + underline_position = style->text_decoration_data.underline_position; + line_through_thickness = style->text_decoration_data.line_through_thickness; + line_through_position = style->text_decoration_data.line_through_position; + font_size = style->font_size.computed; + } + + text_direction = style->direction.computed; + + update(); +} + +cairo_pattern_t* NRStyle::preparePaint(Inkscape::DrawingContext &dc, Geom::OptRect const &paintbox, Inkscape::DrawingPattern *pattern, Paint& paint) +{ + cairo_pattern_t* cpattern = nullptr; + + switch (paint.type) { + case PAINT_SERVER: + if (pattern) { + cpattern = pattern->renderPattern(paint.opacity); + } else { + cpattern = paint.server->pattern_new(dc.raw(), paintbox, paint.opacity); + } + break; + case PAINT_COLOR: { + SPColor const &c = paint.color; + cpattern = cairo_pattern_create_rgba( + c.v.c[0], c.v.c[1], c.v.c[2], paint.opacity); + double red = 0; + double green = 0; + double blue = 0; + double alpha = 0; + cairo_pattern_get_rgba(cpattern, &red, &green, &blue, &alpha); + } + break; + default: + break; + } + return cpattern; +} + +bool NRStyle::prepareFill(Inkscape::DrawingContext &dc, Geom::OptRect const &paintbox, Inkscape::DrawingPattern *pattern) +{ + if (!fill_pattern) fill_pattern = preparePaint(dc, paintbox, pattern, fill); + return fill_pattern != nullptr; +} + +bool NRStyle::prepareStroke(Inkscape::DrawingContext &dc, Geom::OptRect const &paintbox, Inkscape::DrawingPattern *pattern) +{ + if (!stroke_pattern) stroke_pattern = preparePaint(dc, paintbox, pattern, stroke); + return stroke_pattern != nullptr; +} + +bool NRStyle::prepareTextDecorationFill(Inkscape::DrawingContext &dc, Geom::OptRect const &paintbox, Inkscape::DrawingPattern *pattern) +{ + if (!text_decoration_fill_pattern) text_decoration_fill_pattern = preparePaint(dc, paintbox, pattern, text_decoration_fill); + return text_decoration_fill_pattern != nullptr; +} + +bool NRStyle::prepareTextDecorationStroke(Inkscape::DrawingContext &dc, Geom::OptRect const &paintbox, Inkscape::DrawingPattern *pattern) +{ + if (!text_decoration_stroke_pattern) text_decoration_stroke_pattern = preparePaint(dc, paintbox, pattern, text_decoration_stroke); + return text_decoration_stroke_pattern != nullptr; +} + +void NRStyle::applyFill(Inkscape::DrawingContext &dc) +{ + dc.setSource(fill_pattern); + dc.setFillRule(fill_rule); +} + +void NRStyle::applyTextDecorationFill(Inkscape::DrawingContext &dc) +{ + dc.setSource(text_decoration_fill_pattern); + // Fill rule does not matter, no intersections. +} + +void NRStyle::applyStroke(Inkscape::DrawingContext &dc) +{ + dc.setSource(stroke_pattern); + dc.setLineWidth(stroke_width); + dc.setLineCap(line_cap); + dc.setLineJoin(line_join); + dc.setMiterLimit(miter_limit); + cairo_set_dash(dc.raw(), dash, n_dash, dash_offset); // fixme +} + +void NRStyle::applyTextDecorationStroke(Inkscape::DrawingContext &dc) +{ + dc.setSource(text_decoration_stroke_pattern); + dc.setLineWidth(text_decoration_stroke_width); + dc.setLineCap(CAIRO_LINE_CAP_BUTT); + dc.setLineJoin(CAIRO_LINE_JOIN_MITER); + dc.setMiterLimit(miter_limit); + cairo_set_dash(dc.raw(), nullptr, 0, 0.0); // fixme (no dash) +} + +void NRStyle::update() +{ + // force pattern update + if (fill_pattern) cairo_pattern_destroy(fill_pattern); + if (stroke_pattern) cairo_pattern_destroy(stroke_pattern); + if (text_decoration_fill_pattern) cairo_pattern_destroy(text_decoration_fill_pattern); + if (text_decoration_stroke_pattern) cairo_pattern_destroy(text_decoration_stroke_pattern); + fill_pattern = nullptr; + stroke_pattern = nullptr; + text_decoration_fill_pattern = nullptr; + text_decoration_stroke_pattern = nullptr; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-style.h b/src/display/nr-style.h new file mode 100644 index 0000000..f9d7fa5 --- /dev/null +++ b/src/display/nr-style.h @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Style information for rendering. + * Only used by classes DrawingShape and DrawingText + *//* + * Authors: + * Krzysztof KosiĆski <tweenk.pl@gmail.com> + * + * Copyright (C) 2010 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_DISPLAY_NR_ARENA_STYLE_H +#define SEEN_INKSCAPE_DISPLAY_NR_ARENA_STYLE_H + +#include <cairo.h> +#include <2geom/rect.h> +#include "color.h" + +class SPPaintServer; +class SPStyle; +class SPIPaint; + +namespace Inkscape { +class DrawingContext; +class DrawingPattern; +} + +struct NRStyle { + NRStyle(); + ~NRStyle(); + + enum PaintType { + PAINT_NONE, + PAINT_COLOR, + PAINT_SERVER + }; + + class Paint { + public: + Paint() : type(PAINT_NONE), color(0), server(nullptr), opacity(1.0) {} + ~Paint() { clear(); } + + PaintType type; + SPColor color; + SPPaintServer *server; + float opacity; + + void clear(); + void set(SPColor const &c); + void set(SPPaintServer *ps); + void set(const SPIPaint* paint); + }; + + void set(SPStyle *style, SPStyle *context_style = nullptr); + cairo_pattern_t* preparePaint(Inkscape::DrawingContext &dc, Geom::OptRect const &paintbox, Inkscape::DrawingPattern *pattern, Paint& paint); + bool prepareFill(Inkscape::DrawingContext &dc, Geom::OptRect const &paintbox, Inkscape::DrawingPattern *pattern); + bool prepareStroke(Inkscape::DrawingContext &dc, Geom::OptRect const &paintbox, Inkscape::DrawingPattern *pattern); + bool prepareTextDecorationFill(Inkscape::DrawingContext &dc, Geom::OptRect const &paintbox, Inkscape::DrawingPattern *pattern); + bool prepareTextDecorationStroke(Inkscape::DrawingContext &dc, Geom::OptRect const &paintbox, Inkscape::DrawingPattern *pattern); + void applyFill(Inkscape::DrawingContext &dc); + void applyStroke(Inkscape::DrawingContext &dc); + void applyTextDecorationFill(Inkscape::DrawingContext &dc); + void applyTextDecorationStroke(Inkscape::DrawingContext &dc); + void update(); + + Paint fill; + Paint stroke; + float stroke_width; + float miter_limit; + unsigned int n_dash; + double *dash; + float dash_offset; + cairo_fill_rule_t fill_rule; + cairo_line_cap_t line_cap; + cairo_line_join_t line_join; + + cairo_pattern_t *fill_pattern; + cairo_pattern_t *stroke_pattern; + cairo_pattern_t *text_decoration_fill_pattern; + cairo_pattern_t *text_decoration_stroke_pattern; + + enum PaintOrderType { + PAINT_ORDER_NORMAL, + PAINT_ORDER_FILL, + PAINT_ORDER_STROKE, + PAINT_ORDER_MARKER + }; + + static const size_t PAINT_ORDER_LAYERS = 3; + PaintOrderType paint_order_layer[PAINT_ORDER_LAYERS]; + + enum TextDecorationLine { + TEXT_DECORATION_LINE_CLEAR = 0x00, + TEXT_DECORATION_LINE_SET = 0x01, + TEXT_DECORATION_LINE_INHERIT = 0x02, + TEXT_DECORATION_LINE_UNDERLINE = 0x04, + TEXT_DECORATION_LINE_OVERLINE = 0x08, + TEXT_DECORATION_LINE_LINETHROUGH = 0x10, + TEXT_DECORATION_LINE_BLINK = 0x20 + }; + + enum TextDecorationStyle { + TEXT_DECORATION_STYLE_CLEAR = 0x00, + TEXT_DECORATION_STYLE_SET = 0x01, + TEXT_DECORATION_STYLE_INHERIT = 0x02, + TEXT_DECORATION_STYLE_SOLID = 0x04, + TEXT_DECORATION_STYLE_ISDOUBLE = 0x08, + TEXT_DECORATION_STYLE_DOTTED = 0x10, + TEXT_DECORATION_STYLE_DASHED = 0x20, + TEXT_DECORATION_STYLE_WAVY = 0x40 + }; + + int text_decoration_line; + int text_decoration_style; + Paint text_decoration_fill; + Paint text_decoration_stroke; + float text_decoration_stroke_width; + + // These are the same as in style.h + float phase_length; + bool tspan_line_start; + bool tspan_line_end; + float tspan_width; + float ascender; + float descender; + float underline_thickness; + float underline_position; + float line_through_thickness; + float line_through_position; + float font_size; + + int text_direction; +}; + +#endif + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-svgfonts.cpp b/src/display/nr-svgfonts.cpp new file mode 100644 index 0000000..e9679f3 --- /dev/null +++ b/src/display/nr-svgfonts.cpp @@ -0,0 +1,434 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVGFonts rendering implementation + * + * Authors: + * Felipe C. da S. Sanches <juca@members.fsf.org> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2008 Felipe C. da S. Sanches + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * Read the file 'COPYING' for more information. + */ + +#include <2geom/pathvector.h> +#include <2geom/transforms.h> +#include <cairo.h> +#include <vector> + +#include "svg/svg.h" +#include "display/cairo-utils.h" +#include "display/nr-svgfonts.h" +#include "display/nr-svgfonts.h" +#include "display/curve.h" + +#include "xml/repr.h" + +#include "object/sp-path.h" +#include "object/sp-object-group.h" +#include "object/sp-use.h" +#include "object/sp-use-reference.h" +#include "object/sp-font-face.h" +#include "object/sp-glyph.h" +#include "object/sp-missing-glyph.h" +#include "object/sp-font.h" +#include "object/sp-glyph-kerning.h" + +// ************************// +// UserFont Implementation // +// ************************// + +// I wrote this binding code because Cairomm does not yet support userfonts. I have moved this code to cairomm and sent them a patch. +// Once Cairomm incorporate the UserFonts binding, this code should be removed from inkscape and Cairomm API should be used. + +static cairo_user_data_key_t key; + +static cairo_status_t font_init_cb (cairo_scaled_font_t *scaled_font, + cairo_t * /*cairo*/, cairo_font_extents_t *metrics){ + cairo_font_face_t* face = cairo_scaled_font_get_font_face(scaled_font); + SvgFont* instance = static_cast<SvgFont*>(cairo_font_face_get_user_data(face, &key)); + return instance->scaled_font_init(scaled_font, metrics); +} + +static cairo_status_t font_text_to_glyphs_cb ( cairo_scaled_font_t *scaled_font, + const char *utf8, + int utf8_len, + cairo_glyph_t **glyphs, + int *num_glyphs, + cairo_text_cluster_t **clusters, + int *num_clusters, + cairo_text_cluster_flags_t *flags){ + cairo_font_face_t* face = cairo_scaled_font_get_font_face(scaled_font); + SvgFont* instance = static_cast<SvgFont*>(cairo_font_face_get_user_data(face, &key)); + return instance->scaled_font_text_to_glyphs(scaled_font, utf8, utf8_len, glyphs, num_glyphs, clusters, num_clusters, flags); +} + +static cairo_status_t font_render_glyph_cb (cairo_scaled_font_t *scaled_font, + unsigned long glyph, + cairo_t *cr, + cairo_text_extents_t *metrics){ + cairo_font_face_t* face = cairo_scaled_font_get_font_face(scaled_font); + SvgFont* instance = static_cast<SvgFont*>(cairo_font_face_get_user_data(face, &key)); + return instance->scaled_font_render_glyph(scaled_font, glyph, cr, metrics); +} + +UserFont::UserFont(SvgFont* instance){ + this->face = cairo_user_font_face_create (); + cairo_user_font_face_set_init_func (this->face, font_init_cb); + cairo_user_font_face_set_render_glyph_func (this->face, font_render_glyph_cb); + cairo_user_font_face_set_text_to_glyphs_func(this->face, font_text_to_glyphs_cb); + + cairo_font_face_set_user_data (this->face, &key, (void*)instance, (cairo_destroy_func_t) nullptr); +} + +//******************************// +// SvgFont class Implementation // +//******************************// +SvgFont::SvgFont(SPFont* spfont){ + this->font = spfont; + this->missingglyph = nullptr; + this->userfont = nullptr; +} + +cairo_status_t +SvgFont::scaled_font_init (cairo_scaled_font_t */*scaled_font*/, + cairo_font_extents_t */*metrics*/) +{ +//TODO +// metrics->ascent = .75; +// metrics->descent = .25; + return CAIRO_STATUS_SUCCESS; +} + +unsigned int size_of_substring(const char* substring, gchar* str){ + const gchar* original_substring = substring; + + while((g_utf8_get_char(substring)==g_utf8_get_char(str)) && g_utf8_get_char(substring) != 0 && g_utf8_get_char(str) != 0){ + substring = g_utf8_next_char(substring); + str = g_utf8_next_char(str); + } + if (g_utf8_get_char(substring)==0) + return substring - original_substring; + else + return 0; +} + + +namespace { + +//TODO: in these functions, verify what happens when using unicode strings. + +bool MatchVKerningRule(SPVkern const *vkern, + SPGlyph *glyph, + char const *previous_unicode, + gchar const *previous_glyph_name) +{ + bool value = (vkern->u1->contains(previous_unicode[0]) + || vkern->g1->contains(previous_glyph_name)) + && (vkern->u2->contains(glyph->unicode[0]) + || vkern->g2->contains(glyph->glyph_name.c_str())); + + return value; +} + +bool MatchHKerningRule(SPHkern const *hkern, + SPGlyph *glyph, + char const *previous_unicode, + gchar const *previous_glyph_name) +{ + bool value = (hkern->u1->contains(previous_unicode[0]) + || hkern->g1->contains(previous_glyph_name)) + && (hkern->u2->contains(glyph->unicode[0]) + || hkern->g2->contains(glyph->glyph_name.c_str())); + + return value; +} + +} // namespace + +cairo_status_t +SvgFont::scaled_font_text_to_glyphs (cairo_scaled_font_t */*scaled_font*/, + const char *utf8, + int /*utf8_len*/, + cairo_glyph_t **glyphs, + int *num_glyphs, + cairo_text_cluster_t **/*clusters*/, + int */*num_clusters*/, + cairo_text_cluster_flags_t */*flags*/) +{ + //This function receives a text string to be rendered. It then defines what is the sequence of glyphs that + // is used to properly render this string. It also defines the respective coordinates of each glyph. Thus, it + // has to read the attributes of the SVGFont hkern and vkern nodes in order to adjust the glyph kerning. + //It also determines the usage of the missing-glyph in portions of the string that does not match any of the declared glyphs. + + unsigned long i; + int count = 0; + gchar* _utf8 = (gchar*) utf8; + unsigned int len; + + bool missing; + //First we find out what's the number of glyphs needed. + while(g_utf8_get_char(_utf8)){ + missing = true; + for (i=0; i < (unsigned long) this->glyphs.size(); i++){ + if ( (len = size_of_substring(this->glyphs[i]->unicode.c_str(), _utf8)) ){ + //TODO: store this cluster + _utf8+=len; + count++; + missing=false; + break; + } + } + if (missing){ + //TODO: store this cluster + _utf8++; + count++; + } + } + + + //We use that info to allocate memory for the glyphs + *glyphs = (cairo_glyph_t*) malloc(count*sizeof(cairo_glyph_t)); + + char* previous_unicode = nullptr; //This is used for kerning + gchar* previous_glyph_name = nullptr; //This is used for kerning + + count=0; + double x=0, y=0;//These vars store the position of the glyph within the rendered string + bool is_horizontal_text = true; //TODO + _utf8 = (char*) utf8; + + double font_height = units_per_em(); + while(g_utf8_get_char(_utf8)){ + len = 0; + for (i=0; i < (unsigned long) this->glyphs.size(); i++){ + //check whether is there a glyph declared on the SVG document + // that matches with the text string in its current position + if ( (len = size_of_substring(this->glyphs[i]->unicode.c_str(), _utf8)) ){ + for(auto& node: font->children) { + if (!previous_unicode) { + break; + } + //apply glyph kerning if appropriate + SPHkern *hkern = dynamic_cast<SPHkern *>(&node); + if (hkern && is_horizontal_text && + MatchHKerningRule(hkern, this->glyphs[i], previous_unicode, previous_glyph_name) ){ + x -= (hkern->k / font_height); + } + SPVkern *vkern = dynamic_cast<SPVkern *>(&node); + if (vkern && !is_horizontal_text && + MatchVKerningRule(vkern, this->glyphs[i], previous_unicode, previous_glyph_name) ){ + y -= (vkern->k / font_height); + } + } + previous_unicode = const_cast<char*>(this->glyphs[i]->unicode.c_str());//used for kerning checking + previous_glyph_name = const_cast<char*>(this->glyphs[i]->glyph_name.c_str());//used for kerning checking + (*glyphs)[count].index = i; + (*glyphs)[count].x = x; + (*glyphs)[count++].y = y; + + //advance glyph coordinates: + if (is_horizontal_text) { + if (this->glyphs[i]->horiz_adv_x != 0) { + x+=(this->glyphs[i]->horiz_adv_x/font_height); + } else { + x+=(this->font->horiz_adv_x/font_height); + } + } else { + y+=(this->font->vert_adv_y/font_height); + } + _utf8+=len; //advance 'len' bytes in our string pointer + //continue; + goto raptorz; + } + } + raptorz: + if (len==0){ + (*glyphs)[count].index = i; + (*glyphs)[count].x = x; + (*glyphs)[count++].y = y; + + //advance glyph coordinates: + if (is_horizontal_text) x+=(this->font->horiz_adv_x/font_height);//TODO: use here the height of the font + else y+=(this->font->vert_adv_y/font_height);//TODO: use here the "height" of the font + + _utf8 = g_utf8_next_char(_utf8); //advance 1 char in our string pointer + } + } + *num_glyphs = count; + return CAIRO_STATUS_SUCCESS; +} + +void +SvgFont::render_glyph_path(cairo_t* cr, Geom::PathVector* pathv){ + if (!pathv->empty()){ + //This glyph has a path description on its d attribute, so we render it: + cairo_new_path(cr); + + //adjust scale of the glyph + Geom::Scale s(1.0/units_per_em()); + Geom::Rect area( Geom::Point(0,0), Geom::Point(1,1) ); //I need help here! (reaction: note that the 'area' parameter is an *optional* rect, so you can pass an empty Geom::OptRect() ) + + feed_pathvector_to_cairo (cr, *pathv, s, area, false, 0); + cairo_fill(cr); + } +} + +void +SvgFont::glyph_modified(SPObject* /* blah */, unsigned int /* bleh */){ + this->refresh(); + //TODO: update rendering on svgfonts preview widget (in the svg fonts dialog) +} + +Geom::PathVector +SvgFont::flip_coordinate_system(SPFont* spfont, Geom::PathVector pathv){ + double units_per_em = 1024; + for(auto& obj: spfont->children) { + if (dynamic_cast<SPFontFace *>(&obj)) { + //XML Tree being directly used here while it shouldn't be. + sp_repr_get_double(obj.getRepr(), "units_per_em", &units_per_em); + } + } + + double baseline_offset = units_per_em - spfont->horiz_origin_y; + + //This matrix flips y-axis and places the origin at baseline + Geom::Affine m(Geom::Coord(1),Geom::Coord(0),Geom::Coord(0),Geom::Coord(-1),Geom::Coord(0),Geom::Coord(baseline_offset)); + return pathv*m; +} + +cairo_status_t +SvgFont::scaled_font_render_glyph (cairo_scaled_font_t */*scaled_font*/, + unsigned long glyph, + cairo_t *cr, + cairo_text_extents_t */*metrics*/) +{ + // This method does the actual rendering of glyphs. + + // We have glyphs.size() glyphs and possibly one missing-glyph declared on this SVG document + // The id of the missing-glyph is always equal to glyphs.size() + // All the other glyphs have ids ranging from 0 to glyphs.size()-1 + + if (glyph > this->glyphs.size()) return CAIRO_STATUS_SUCCESS;//TODO: this is an error! + + SPObject *node = nullptr; + if (glyph == glyphs.size()){ + if (!missingglyph) { + return CAIRO_STATUS_SUCCESS; + } + node = missingglyph; + } else { + node = glyphs[glyph]; + } + + if (!dynamic_cast<SPGlyph *>(node) && !dynamic_cast<SPMissingGlyph *>(node)) { + return CAIRO_STATUS_SUCCESS; // FIXME: is this the right code to return? + } + + SPFont* spfont = dynamic_cast<SPFont *>(node->parent); + if (!spfont) { + return CAIRO_STATUS_SUCCESS; // FIXME: is this the right code to return? + } + + //glyphs can be described by arbitrary SVG declared in the childnodes of a glyph node + // or using the d attribute of a glyph node. + // pathv stores the path description from the d attribute: + Geom::PathVector pathv; + + SPGlyph *glyphNode = dynamic_cast<SPGlyph *>(node); + if (glyphNode && glyphNode->d) { + pathv = sp_svg_read_pathv(glyphNode->d); + pathv = flip_coordinate_system(spfont, pathv); + render_glyph_path(cr, &pathv); + } else { + SPMissingGlyph *missing = dynamic_cast<SPMissingGlyph *>(node); + if (missing && missing->d) { + pathv = sp_svg_read_pathv(missing->d); + pathv = flip_coordinate_system(spfont, pathv); + render_glyph_path(cr, &pathv); + } + } + + if (node->hasChildren()){ + //render the SVG described on this glyph's child nodes. + for(auto& child: node->children) { + { + SPPath *path = dynamic_cast<SPPath *>(&child); + if (path) { + pathv = path->_curve->get_pathvector(); + pathv = flip_coordinate_system(spfont, pathv); + render_glyph_path(cr, &pathv); + } + } + if (dynamic_cast<SPObjectGroup *>(&child)) { + g_warning("TODO: svgfonts: render OBJECTGROUP"); + } + SPUse *use = dynamic_cast<SPUse *>(&child); + if (use) { + SPItem* item = use->ref->getObject(); + SPPath *path = dynamic_cast<SPPath *>(item); + if (path) { + SPShape *shape = dynamic_cast<SPShape *>(item); + g_assert(shape != nullptr); + pathv = shape->_curve->get_pathvector(); + pathv = flip_coordinate_system(spfont, pathv); + this->render_glyph_path(cr, &pathv); + } + + glyph_modified_connection = item->connectModified(sigc::mem_fun(*this, &SvgFont::glyph_modified)); + } + } + } + + return CAIRO_STATUS_SUCCESS; +} + +cairo_font_face_t* +SvgFont::get_font_face(){ + if (!this->userfont) { + for(auto& node: font->children) { + SPGlyph *glyph = dynamic_cast<SPGlyph *>(&node); + if (glyph) { + glyphs.push_back(glyph); + } + SPMissingGlyph *missing = dynamic_cast<SPMissingGlyph *>(&node); + if (missing) { + missingglyph = missing; + } + } + this->userfont = new UserFont(this); + } + return this->userfont->face; +} + +void SvgFont::refresh(){ + this->glyphs.clear(); + delete this->userfont; + this->userfont = nullptr; +} + +double SvgFont::units_per_em() { + double units_per_em = 1024; + for (auto& obj: font->children) { + if (dynamic_cast<SPFontFace *>(&obj)) { + //XML Tree being directly used here while it shouldn't be. + sp_repr_get_double(obj.getRepr(), "units-per-em", &units_per_em); + } + } + if (units_per_em <= 0.0) { + units_per_em = 1024; + } + return units_per_em; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/nr-svgfonts.h b/src/display/nr-svgfonts.h new file mode 100644 index 0000000..48185b9 --- /dev/null +++ b/src/display/nr-svgfonts.h @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef NR_SVGFONTS_H_SEEN +#define NR_SVGFONTS_H_SEEN +/* + * SVGFonts rendering headear + * + * Authors: + * Felipe C. da S. Sanches <juca@members.fsf.org> + * + * Copyright (C) 2008 Felipe C. da S. Sanches + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * Read the file 'COPYING' for more information. + */ + +#include <cairo.h> +#include <sigc++/connection.h> + +class SvgFont; +class SPFont; +class SPGlyph; +class SPMissingGlyph; + +extern "C" { typedef struct _GdkEventExpose GdkEventExpose; } + +namespace Gtk { +class Widget; +} + +class UserFont { +public: + UserFont(SvgFont* instance); + cairo_font_face_t* face; +}; + +class SvgFont { +public: + SvgFont(SPFont* spfont); + void refresh(); + cairo_font_face_t* get_font_face(); + cairo_status_t scaled_font_init (cairo_scaled_font_t *scaled_font, cairo_font_extents_t *metrics); + cairo_status_t scaled_font_text_to_glyphs (cairo_scaled_font_t *scaled_font, const char *utf8, int utf8_len, cairo_glyph_t **glyphs, int *num_glyphs, cairo_text_cluster_t **clusters, int *num_clusters, cairo_text_cluster_flags_t *flags); + cairo_status_t scaled_font_render_glyph (cairo_scaled_font_t *scaled_font, unsigned long glyph, cairo_t *cr, cairo_text_extents_t *metrics); + + Geom::PathVector flip_coordinate_system(SPFont* spfont, Geom::PathVector pathv); + void render_glyph_path(cairo_t* cr, Geom::PathVector* pathv); + void glyph_modified(SPObject *, unsigned int); + +private: + SPFont* font; + UserFont* userfont; + std::vector<SPGlyph*> glyphs; + SPMissingGlyph* missingglyph; + sigc::connection glyph_modified_connection; + + double units_per_em(); + //bool drawing_expose_cb (Gtk::Widget *widget, GdkEventExpose *event, void* data); +}; + +#endif //#ifndef NR_SVGFONTS_H_SEEN + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/display/rendermode.h b/src/display/rendermode.h new file mode 100644 index 0000000..66130de --- /dev/null +++ b/src/display/rendermode.h @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2011 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* + * RenderMode enumeration. + * + * Trivially public domain. + */ + +#ifndef SEEN_INKSCAPE_DISPLAY_RENDERMODE_H +#define SEEN_INKSCAPE_DISPLAY_RENDERMODE_H + +namespace Inkscape { + +enum RenderMode { + RENDERMODE_NORMAL, + RENDERMODE_NO_FILTERS, + RENDERMODE_OUTLINE, + RENDERMODE_VISIBLE_HAIRLINES +}; + +enum ColorMode { + COLORMODE_NORMAL, + COLORMODE_GRAYSCALE, + COLORMODE_PRINT_COLORS_PREVIEW +}; + +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/display/snap-indicator.cpp b/src/display/snap-indicator.cpp new file mode 100644 index 0000000..cb798e6 --- /dev/null +++ b/src/display/snap-indicator.cpp @@ -0,0 +1,438 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Provides a class that shows a temporary indicator on the canvas of where the snap was, and what kind of snap + * + * Authors: + * Johan Engelen + * Diederik van Lierop + * + * Copyright (C) Johan Engelen 2009 <j.b.c.engelen@utwente.nl> + * Copyright (C) Diederik van Lierop 2010 - 2012 <mail@diedenrezi.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "display/snap-indicator.h" + +#include "desktop.h" + +#include "display/sodipodi-ctrl.h" +#include "display/sodipodi-ctrlrect.h" +#include "display/canvas-text.h" +#include "display/sp-canvas-util.h" +#include "knot.h" +#include "preferences.h" +#include <glibmm/i18n.h> +#include "ui/tools-switch.h" +#include "enums.h" + +namespace Inkscape { +namespace Display { + +SnapIndicator::SnapIndicator(SPDesktop * desktop) + : _snaptarget(nullptr), + _snaptarget_tooltip(nullptr), + _snaptarget_bbox(nullptr), + _snapsource(nullptr), + _snaptarget_is_presnap(false), + _desktop(desktop) +{ +} + +SnapIndicator::~SnapIndicator() +{ + // remove item that might be present + remove_snaptarget(); + remove_snapsource(); +} + +void +SnapIndicator::set_new_snaptarget(Inkscape::SnappedPoint const &p, bool pre_snap) +{ + remove_snaptarget(); //only display one snaptarget at a time + + g_assert(_desktop != nullptr); + + if (!p.getSnapped()) { + return; // If we haven't snapped, then it is of no use to draw a snapindicator + } + + if (p.getTarget() == SNAPTARGET_CONSTRAINT) { + // This is not a real snap, although moving along the constraint did affect the mouse pointer's position. + // Maybe we should only show a snap indicator when the user explicitly asked for a constraint by pressing ctrl? + // We should not show a snap indicator when stretching a selection box, which is also constrained. That would be + // too much information. + return; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool value = prefs->getBool("/options/snapindicator/value", true); + + if (value) { + // TRANSLATORS: undefined target for snapping + gchar *target_name = _("UNDEFINED"); + switch (p.getTarget()) { + case SNAPTARGET_UNDEFINED: + target_name = _("UNDEFINED"); + g_warning("Snap target has not been specified"); + break; + case SNAPTARGET_GRID: + target_name = _("grid line"); + break; + case SNAPTARGET_GRID_INTERSECTION: + target_name = _("grid intersection"); + break; + case SNAPTARGET_GRID_PERPENDICULAR: + target_name = _("grid line (perpendicular)"); + break; + case SNAPTARGET_GUIDE: + target_name = _("guide"); + break; + case SNAPTARGET_GUIDE_INTERSECTION: + target_name = _("guide intersection"); + break; + case SNAPTARGET_GUIDE_ORIGIN: + target_name = _("guide origin"); + break; + case SNAPTARGET_GUIDE_PERPENDICULAR: + target_name = _("guide (perpendicular)"); + break; + case SNAPTARGET_GRID_GUIDE_INTERSECTION: + target_name = _("grid-guide intersection"); + break; + case SNAPTARGET_NODE_CUSP: + target_name = _("cusp node"); + break; + case SNAPTARGET_NODE_SMOOTH: + target_name = _("smooth node"); + break; + case SNAPTARGET_PATH: + target_name = _("path"); + break; + case SNAPTARGET_PATH_PERPENDICULAR: + target_name = _("path (perpendicular)"); + break; + case SNAPTARGET_PATH_TANGENTIAL: + target_name = _("path (tangential)"); + break; + case SNAPTARGET_PATH_INTERSECTION: + target_name = _("path intersection"); + break; + case SNAPTARGET_PATH_GUIDE_INTERSECTION: + target_name = _("guide-path intersection"); + break; + case SNAPTARGET_PATH_CLIP: + target_name = _("clip-path"); + break; + case SNAPTARGET_PATH_MASK: + target_name = _("mask-path"); + break; + case SNAPTARGET_BBOX_CORNER: + target_name = _("bounding box corner"); + break; + case SNAPTARGET_BBOX_EDGE: + target_name = _("bounding box side"); + break; + case SNAPTARGET_PAGE_BORDER: + target_name = _("page border"); + break; + case SNAPTARGET_LINE_MIDPOINT: + target_name = _("line midpoint"); + break; + case SNAPTARGET_OBJECT_MIDPOINT: + target_name = _("object midpoint"); + break; + case SNAPTARGET_ROTATION_CENTER: + target_name = _("object rotation center"); + break; + case SNAPTARGET_BBOX_EDGE_MIDPOINT: + target_name = _("bounding box side midpoint"); + break; + case SNAPTARGET_BBOX_MIDPOINT: + target_name = _("bounding box midpoint"); + break; + case SNAPTARGET_PAGE_CORNER: + target_name = _("page corner"); + break; + case SNAPTARGET_ELLIPSE_QUADRANT_POINT: + target_name = _("quadrant point"); + break; + case SNAPTARGET_RECT_CORNER: + case SNAPTARGET_IMG_CORNER: + target_name = _("corner"); + break; + case SNAPTARGET_TEXT_ANCHOR: + target_name = _("text anchor"); + break; + case SNAPTARGET_TEXT_BASELINE: + target_name = _("text baseline"); + break; + case SNAPTARGET_CONSTRAINED_ANGLE: + target_name = _("constrained angle"); + break; + case SNAPTARGET_CONSTRAINT: + target_name = _("constraint"); + break; + default: + g_warning("Snap target not in SnapTargetType enum"); + break; + } + + gchar *source_name = _("UNDEFINED"); + switch (p.getSource()) { + case SNAPSOURCE_UNDEFINED: + source_name = _("UNDEFINED"); + g_warning("Snap source has not been specified"); + break; + case SNAPSOURCE_BBOX_CORNER: + source_name = _("Bounding box corner"); + break; + case SNAPSOURCE_BBOX_MIDPOINT: + source_name = _("Bounding box midpoint"); + break; + case SNAPSOURCE_BBOX_EDGE_MIDPOINT: + source_name = _("Bounding box side midpoint"); + break; + case SNAPSOURCE_NODE_SMOOTH: + source_name = _("Smooth node"); + break; + case SNAPSOURCE_NODE_CUSP: + source_name = _("Cusp node"); + break; + case SNAPSOURCE_LINE_MIDPOINT: + source_name = _("Line midpoint"); + break; + case SNAPSOURCE_OBJECT_MIDPOINT: + source_name = _("Object midpoint"); + break; + case SNAPSOURCE_ROTATION_CENTER: + source_name = _("Object rotation center"); + break; + case SNAPSOURCE_NODE_HANDLE: + case SNAPSOURCE_OTHER_HANDLE: + source_name = _("Handle"); + break; + case SNAPSOURCE_PATH_INTERSECTION: + source_name = _("Path intersection"); + break; + case SNAPSOURCE_GUIDE: + source_name = _("Guide"); + break; + case SNAPSOURCE_GUIDE_ORIGIN: + source_name = _("Guide origin"); + break; + case SNAPSOURCE_CONVEX_HULL_CORNER: + source_name = _("Convex hull corner"); + break; + case SNAPSOURCE_ELLIPSE_QUADRANT_POINT: + source_name = _("Quadrant point"); + break; + case SNAPSOURCE_RECT_CORNER: + case SNAPSOURCE_IMG_CORNER: + source_name = _("Corner"); + break; + case SNAPSOURCE_TEXT_ANCHOR: + source_name = _("Text anchor"); + break; + case SNAPSOURCE_GRID_PITCH: + source_name = _("Multiple of grid spacing"); + break; + default: + g_warning("Snap source not in SnapSourceType enum"); + break; + } + //std::cout << "Snapped " << source_name << " to " << target_name << std::endl; + + remove_snapsource(); // Don't set both the source and target indicators, as these will overlap + + // Display the snap indicator (i.e. the cross) + SPCanvasItem * canvasitem = nullptr; + canvasitem = sp_canvas_item_new(_desktop->getTempGroup(), + SP_TYPE_CTRL, + "anchor", SP_ANCHOR_CENTER, + "size", 11, + "stroked", TRUE, + "stroke_color", pre_snap ? 0x7f7f7fff : 0xff0000ff, + "mode", SP_KNOT_MODE_XOR, + "shape", SP_KNOT_SHAPE_CROSS, + NULL ); + + double timeout_val = prefs->getDouble("/options/snapindicatorpersistence/value", 2.0); + if (timeout_val < 0.1) { + timeout_val = 0.1; // a zero value would mean infinite persistence (i.e. until new snap occurs) + // Besides, negatives values would ....? + } + + + // The snap indicator will be deleted after some time-out, and sp_canvas_item_dispose + // will be called. This will set canvas->current_item to NULL if the snap indicator was + // the current item, after which any events will go to the root handler instead of any + // item handler. Dragging an object which has just snapped might therefore not be possible + // without selecting / repicking it again. To avoid this, we make sure here that the + // snap indicator will never be picked, and will therefore never be the current item. + // Reported bugs: + // - scrolling when hovering above a pre-snap indicator won't work (for example) + // (https://bugs.launchpad.net/inkscape/+bug/522335/comments/8) + // - dragging doesn't work without repicking + // (https://bugs.launchpad.net/inkscape/+bug/1420301/comments/15) + SP_CTRL(canvasitem)->pickable = false; + + SP_CTRL(canvasitem)->moveto(p.getPoint()); + _snaptarget = _desktop->add_temporary_canvasitem(canvasitem, timeout_val*1000.0); + _snaptarget_is_presnap = pre_snap; + + // Display the tooltip, which reveals the type of snap source and the type of snap target + gchar *tooltip_str = nullptr; + if ( (p.getSource() != SNAPSOURCE_GRID_PITCH) && (p.getTarget() != SNAPTARGET_UNDEFINED) ) { + tooltip_str = g_strconcat(source_name, _(" to "), target_name, NULL); + } else if (p.getSource() != SNAPSOURCE_UNDEFINED) { + tooltip_str = g_strdup(source_name); + } + double fontsize = prefs->getDouble("/tools/measure/fontsize", 10.0); + + if (tooltip_str) { + Geom::Point tooltip_pos = p.getPoint(); + if (tools_isactive(_desktop, TOOLS_MEASURE)) { + // Make sure that the snap tooltips do not overlap the ones from the measure tool + tooltip_pos += _desktop->w2d(Geom::Point(0, -3*fontsize)); + } else { + tooltip_pos += _desktop->w2d(Geom::Point(0, -2*fontsize)); + } + + SPCanvasItem *canvas_tooltip = sp_canvastext_new(_desktop->getTempGroup(), _desktop, tooltip_pos, tooltip_str); + sp_canvastext_set_fontsize(SP_CANVASTEXT(canvas_tooltip), fontsize); + SP_CANVASTEXT(canvas_tooltip)->pickable = false; // See the extensive comment above + SP_CANVASTEXT(canvas_tooltip)->rgba = 0xffffffff; + SP_CANVASTEXT(canvas_tooltip)->outline = false; + SP_CANVASTEXT(canvas_tooltip)->background = true; + if (pre_snap) { + SP_CANVASTEXT(canvas_tooltip)->rgba_background = 0x33337f40; + } else { + SP_CANVASTEXT(canvas_tooltip)->rgba_background = 0x33337f7f; + } + SP_CANVASTEXT(canvas_tooltip)->anchor_position = TEXT_ANCHOR_CENTER; + g_free(tooltip_str); + + _snaptarget_tooltip = _desktop->add_temporary_canvasitem(canvas_tooltip, timeout_val*1000.0); + } + + // Display the bounding box, if we snapped to one + Geom::OptRect const bbox = p.getTargetBBox(); + if (bbox) { + SPCanvasItem* box = sp_canvas_item_new(_desktop->getTempGroup(), + SP_TYPE_CTRLRECT, + nullptr); + + SP_CTRLRECT(box)->setRectangle(*bbox); + SP_CTRLRECT(box)->setColor(pre_snap ? 0x7f7f7fff : 0xff0000ff, false, 0); + SP_CTRLRECT(box)->setDashed(true); + SP_CTRLRECT(box)->pickable = false; // See the extensive comment above + sp_canvas_item_move_to_z(box, 0); + _snaptarget_bbox = _desktop->add_temporary_canvasitem(box, timeout_val*1000.0); + } + } +} + +void +SnapIndicator::remove_snaptarget(bool only_if_presnap) +{ + if (only_if_presnap && !_snaptarget_is_presnap) { + return; + } + + if (_snaptarget) { + _desktop->remove_temporary_canvasitem(_snaptarget); + _snaptarget = nullptr; + _snaptarget_is_presnap = false; + } + + if (_snaptarget_tooltip) { + _desktop->remove_temporary_canvasitem(_snaptarget_tooltip); + _snaptarget_tooltip = nullptr; + } + + if (_snaptarget_bbox) { + _desktop->remove_temporary_canvasitem(_snaptarget_bbox); + _snaptarget_bbox = nullptr; + } + +} + +void +SnapIndicator::set_new_snapsource(Inkscape::SnapCandidatePoint const &p) +{ + remove_snapsource(); + + g_assert(_desktop != nullptr); // If this fails, then likely setup() has not been called on the snap manager (see snap.cpp -> setup()) + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool value = prefs->getBool("/options/snapindicator/value", true); + + if (value) { + SPCanvasItem * canvasitem = sp_canvas_item_new( _desktop->getTempGroup(), + SP_TYPE_CTRL, + "anchor", SP_ANCHOR_CENTER, + "size", 7, + "stroked", TRUE, + "stroke_color", 0xff0000ff, + "mode", SP_KNOT_MODE_XOR, + "shape", SP_KNOT_SHAPE_CIRCLE, + NULL ); + + SP_CTRL(canvasitem)->moveto(p.getPoint()); + _snapsource = _desktop->add_temporary_canvasitem(canvasitem, 1000); + } +} + +void +SnapIndicator::set_new_debugging_point(Geom::Point const &p) +{ + g_assert(_desktop != nullptr); + SPCanvasItem * canvasitem = sp_canvas_item_new( _desktop->getTempGroup(), + SP_TYPE_CTRL, + "anchor", SP_ANCHOR_CENTER, + "size", 11, + "fill_color", 0x00ff00ff, + "stroked", FALSE, + "mode", SP_KNOT_MODE_XOR, + "shape", SP_KNOT_SHAPE_DIAMOND, + NULL ); + + SP_CTRL(canvasitem)->moveto(p); + _debugging_points.push_back(_desktop->add_temporary_canvasitem(canvasitem, 5000)); + +} + +void +SnapIndicator::remove_snapsource() +{ + if (_snapsource) { + _desktop->remove_temporary_canvasitem(_snapsource); + _snapsource = nullptr; + } +} + +void +SnapIndicator::remove_debugging_points() +{ + for (std::list<TemporaryItem *>::const_iterator i = _debugging_points.begin(); i != _debugging_points.end(); ++i) { + _desktop->remove_temporary_canvasitem(*i); + } + _debugging_points.clear(); +} + + +} //namespace Display +} /* namespace Inkscape */ + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=4:softtabstop=4 : diff --git a/src/display/snap-indicator.h b/src/display/snap-indicator.h new file mode 100644 index 0000000..463acaf --- /dev/null +++ b/src/display/snap-indicator.h @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_DISPLAY_SNAP_INDICATOR_H +#define INKSCAPE_DISPLAY_SNAP_INDICATOR_H + +/** + * @file + * Provides a class that shows a temporary indicator on the canvas of where the snap was, and what kind of snap + */ +/* + * Authors: + * Johan Engelen + * Diederik van Lierop + * + * Copyright (C) Johan Engelen 2008 <j.b.c.engelen@utwente.nl> + * Copyright (C) Diederik van Lierop 2010 <mail@diedenrezi.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "snapped-point.h" + +class SPDesktop; + +namespace Inkscape { +namespace Display { + +class TemporaryItem; + +class SnapIndicator { +public: + SnapIndicator(SPDesktop *desktop); + virtual ~SnapIndicator(); + + void set_new_snaptarget(Inkscape::SnappedPoint const &p, bool pre_snap = false); + void remove_snaptarget(bool only_if_presnap = false); + + void set_new_snapsource(Inkscape::SnapCandidatePoint const &p); + void remove_snapsource(); + + void set_new_debugging_point(Geom::Point const &p); + void remove_debugging_points(); + +protected: + TemporaryItem *_snaptarget; + TemporaryItem *_snaptarget_tooltip; + TemporaryItem *_snaptarget_bbox; + TemporaryItem *_snapsource; + std::list<TemporaryItem *> _debugging_points; + bool _snaptarget_is_presnap; + SPDesktop *_desktop; + +private: + SnapIndicator(const SnapIndicator&) = delete; + SnapIndicator& operator=(const SnapIndicator&) = delete; +}; + +} //namespace Display +} //namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/display/sodipodi-ctrl.cpp b/src/display/sodipodi-ctrl.cpp new file mode 100644 index 0000000..09e34f7 --- /dev/null +++ b/src/display/sodipodi-ctrl.cpp @@ -0,0 +1,695 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* + * SPCtrl + * + * We render it by hand to reduce allocing/freeing svps & to get clean + * (non-aa) images + * + */ + +#include <2geom/transforms.h> +#include <2geom/line.h> +#include "sp-canvas-util.h" +#include "sodipodi-ctrl.h" +#include "display/cairo-utils.h" +#include "display/sp-canvas.h" + +enum { + ARG_0, + ARG_SHAPE, + ARG_MODE, + ARG_ANCHOR, + ARG_SIZE, + ARG_ANGLE, + ARG_FILLED, + ARG_FILL_COLOR, + ARG_STROKED, + ARG_STROKE_COLOR, + ARG_PIXBUF +}; + +static void sp_ctrl_destroy(SPCanvasItem *object); +static void sp_ctrl_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec); +static void sp_ctrl_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec); +static void sp_ctrl_update (SPCanvasItem *item, Geom::Affine const &affine, unsigned int flags); +static void sp_ctrl_render (SPCanvasItem *item, SPCanvasBuf *buf); + +static double sp_ctrl_point (SPCanvasItem *item, Geom::Point p, SPCanvasItem **actual_item); + +G_DEFINE_TYPE(SPCtrl, sp_ctrl, SP_TYPE_CANVAS_ITEM); + +static void +sp_ctrl_class_init (SPCtrlClass *klass) +{ + SPCanvasItemClass *item_class = SP_CANVAS_ITEM_CLASS(klass); + GObjectClass *g_object_class = (GObjectClass *) klass; + + g_object_class->set_property = sp_ctrl_set_property; + g_object_class->get_property = sp_ctrl_get_property; + + g_object_class_install_property (g_object_class, + ARG_SHAPE, g_param_spec_int ("shape", "shape", "Shape", 0, G_MAXINT, SP_CTRL_SHAPE_SQUARE, (GParamFlags) G_PARAM_READWRITE)); + g_object_class_install_property (g_object_class, + ARG_MODE, g_param_spec_int ("mode", "mode", "Mode", 0, G_MAXINT, SP_CTRL_MODE_COLOR, (GParamFlags) G_PARAM_READWRITE)); + g_object_class_install_property (g_object_class, + ARG_ANCHOR, g_param_spec_int ("anchor", "anchor", "Anchor", 0, G_MAXINT, SP_ANCHOR_CENTER, (GParamFlags) G_PARAM_READWRITE)); + g_object_class_install_property (g_object_class, + ARG_SIZE, g_param_spec_uint ("size", "size", "Size", 0, G_MAXUINT, 7, (GParamFlags) G_PARAM_READWRITE)); + g_object_class_install_property (g_object_class, + ARG_ANGLE, g_param_spec_double ("angle", "angle", "Angle", -G_MAXDOUBLE, G_MAXDOUBLE, 0.0, (GParamFlags) G_PARAM_READWRITE)); + g_object_class_install_property (g_object_class, + ARG_FILLED, g_param_spec_boolean ("filled", "filled", "Filled", TRUE, (GParamFlags) G_PARAM_READWRITE)); + g_object_class_install_property (g_object_class, + ARG_FILL_COLOR, g_param_spec_int ("fill_color", "fill_color", "Fill Color", G_MININT, G_MAXINT, 0x000000ff, (GParamFlags) G_PARAM_READWRITE)); + g_object_class_install_property (g_object_class, + ARG_STROKED, g_param_spec_boolean ("stroked", "stroked", "Stroked", FALSE, (GParamFlags) G_PARAM_READWRITE)); + g_object_class_install_property (g_object_class, + ARG_STROKE_COLOR, g_param_spec_int ("stroke_color", "stroke_color", "Stroke Color", G_MININT, G_MAXINT, 0x000000ff, (GParamFlags) G_PARAM_READWRITE)); + g_object_class_install_property (g_object_class, + ARG_PIXBUF, g_param_spec_pointer ("pixbuf", "pixbuf", "Pixbuf", (GParamFlags) G_PARAM_READWRITE)); + + item_class->destroy = sp_ctrl_destroy; + item_class->update = sp_ctrl_update; + item_class->render = sp_ctrl_render; + item_class->point = sp_ctrl_point; +} + +static void +sp_ctrl_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) +{ + + SPCanvasItem *item; + SPCtrl *ctrl; + GdkPixbuf * pixbuf = nullptr; + + item = SP_CANVAS_ITEM (object); + ctrl = SP_CTRL (object); + + switch (prop_id) { + case ARG_SHAPE: + ctrl->shape = (SPCtrlShapeType) g_value_get_int(value); + break; + case ARG_MODE: + ctrl->mode = (SPCtrlModeType) g_value_get_int(value); + break; + case ARG_ANCHOR: + ctrl->anchor = (SPAnchorType) g_value_get_int(value); + break; + case ARG_SIZE: + ctrl->width = g_value_get_uint(value); + ctrl->height = ctrl->width; + ctrl->defined = (ctrl->width > 0); + break; + case ARG_ANGLE: + ctrl->angle = (double)g_value_get_double(value); + break; + case ARG_FILLED: + ctrl->filled = g_value_get_boolean(value); + break; + case ARG_FILL_COLOR: + ctrl->fill_color = (guint32)g_value_get_int(value); + break; + case ARG_STROKED: + ctrl->stroked = g_value_get_boolean(value); + break; + case ARG_STROKE_COLOR: + ctrl->stroke_color = (guint32)g_value_get_int(value); + break; + case ARG_PIXBUF: + pixbuf = (GdkPixbuf*) g_value_get_pointer(value); + // A pixbuf defines it's own size, don't mess about with size. + ctrl->width = gdk_pixbuf_get_width(pixbuf); + ctrl->height = gdk_pixbuf_get_height(pixbuf); + if (gdk_pixbuf_get_has_alpha(pixbuf)) { + ctrl->pixbuf = pixbuf; + } else { + ctrl->pixbuf = gdk_pixbuf_add_alpha(pixbuf, FALSE, 0, 0, 0); + g_object_unref(pixbuf); + } + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + return; // Do not do an update + } + ctrl->build = FALSE; + sp_canvas_item_request_update(item); +} + +static void +sp_ctrl_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + SPCtrl *ctrl; + ctrl = SP_CTRL (object); + + switch (prop_id) { + + case ARG_SHAPE: + g_value_set_int(value, ctrl->shape); + break; + + case ARG_MODE: + g_value_set_int(value, ctrl->mode); + break; + + case ARG_ANCHOR: + g_value_set_int(value, ctrl->anchor); + break; + + case ARG_SIZE: + g_value_set_uint(value, ctrl->width); + break; + + case ARG_ANGLE: + g_value_set_double(value, ctrl->angle); + break; + + case ARG_FILLED: + g_value_set_boolean(value, ctrl->filled); + break; + + case ARG_FILL_COLOR: + g_value_set_int(value, ctrl->fill_color); + break; + + case ARG_STROKED: + g_value_set_boolean(value, ctrl->stroked); + break; + + case ARG_STROKE_COLOR: + g_value_set_int(value, ctrl->stroke_color); + break; + + case ARG_PIXBUF: + g_value_set_pointer(value, ctrl->pixbuf); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } + +} +static void +sp_ctrl_init (SPCtrl *ctrl) +{ + ctrl->shape = SP_CTRL_SHAPE_SQUARE; + ctrl->mode = SP_CTRL_MODE_COLOR; + ctrl->anchor = SP_ANCHOR_CENTER; + ctrl->width = 6; + ctrl->height = 6; + ctrl->defined = TRUE; + ctrl->shown = FALSE; + ctrl->build = FALSE; + ctrl->filled = 1; + ctrl->stroked = 0; + ctrl->fill_color = 0x000000ff; + ctrl->stroke_color = 0x000000ff; + ctrl->angle = 0.0; + + new (&ctrl->box) Geom::IntRect(0,0,0,0); + ctrl->cache = nullptr; + ctrl->pixbuf = nullptr; + + ctrl->_point = Geom::Point(0,0); +} + +static void sp_ctrl_destroy(SPCanvasItem *object) +{ + g_return_if_fail (object != nullptr); + g_return_if_fail (SP_IS_CTRL (object)); + + SPCtrl *ctrl = SP_CTRL (object); + + if (ctrl->cache) { + delete[] ctrl->cache; + ctrl->cache = nullptr; + } + + if (SP_CANVAS_ITEM_CLASS(sp_ctrl_parent_class)->destroy) + SP_CANVAS_ITEM_CLASS(sp_ctrl_parent_class)->destroy(object); +} + +static void +sp_ctrl_update (SPCanvasItem *item, Geom::Affine const &affine, unsigned int flags) +{ + SPCtrl *ctrl; + ctrl = SP_CTRL (item); + + if (SP_CANVAS_ITEM_CLASS(sp_ctrl_parent_class)->update) + SP_CANVAS_ITEM_CLASS(sp_ctrl_parent_class)->update(item, affine, flags); + + sp_canvas_item_reset_bounds (item); + + if (ctrl->shown) { + item->canvas->requestRedraw(ctrl->box.left(), ctrl->box.top(), ctrl->box.right() + 1, ctrl->box.bottom() + 1); + } + + if (!ctrl->defined) return; + + int w_half = floor(ctrl->width/2.0); + int h_half = floor(ctrl->height/2.0); + + int x = floor(affine[4]) - w_half; + int y = floor(affine[5]) - h_half; + + switch (ctrl->anchor) { + case SP_ANCHOR_N: + case SP_ANCHOR_CENTER: + case SP_ANCHOR_S: + break; + + case SP_ANCHOR_NW: + case SP_ANCHOR_W: + case SP_ANCHOR_SW: + x += w_half; + break; + + case SP_ANCHOR_NE: + case SP_ANCHOR_E: + case SP_ANCHOR_SE: + x -= w_half; + break; + } + + switch (ctrl->anchor) { + case SP_ANCHOR_W: + case SP_ANCHOR_CENTER: + case SP_ANCHOR_E: + break; + + case SP_ANCHOR_NW: + case SP_ANCHOR_N: + case SP_ANCHOR_NE: + y += h_half; + break; + + case SP_ANCHOR_SW: + case SP_ANCHOR_S: + case SP_ANCHOR_SE: + y -= h_half; + break; + } + + ctrl->box = Geom::IntRect::from_xywh(x, y, lround(ctrl->width), lround(ctrl->height)); + sp_canvas_update_bbox (item, ctrl->box.left(), ctrl->box.top(), ctrl->box.right() + 1, ctrl->box.bottom() + 1); +} + +static double +sp_ctrl_point (SPCanvasItem *item, Geom::Point p, SPCanvasItem **actual_item) +{ + SPCtrl *ctrl = SP_CTRL (item); + + *actual_item = item; + + if (ctrl->box.contains(p.floor())) return 0.0; + return 1e18; +} + +bool +sp_point_inside_line(Geom::Point a, Geom::Point b, Geom::Point c, double tolerance = 0.1) { + Geom::LineSegment segment(a, b); + return Geom::are_near(c, segment, tolerance); +} + +bool +sp_point_inside_triangle(Geom::Point p1,Geom::Point p2,Geom::Point p3, Geom::Point point){ + using Geom::X; + using Geom::Y; + double denominator = (p1[X]*(p2[Y] - p3[Y]) + p1[Y]*(p3[X] - p2[X]) + p2[X]*p3[Y] - p2[Y]*p3[X]); + double t1 = (point[X]*(p3[Y] - p1[Y]) + point[Y]*(p1[X] - p3[X]) - p1[X]*p3[Y] + p1[Y]*p3[X]) / denominator; + double t2 = (point[X]*(p2[Y] - p1[Y]) + point[Y]*(p1[X] - p2[X]) - p1[X]*p2[Y] + p1[Y]*p2[X]) / -denominator; + double see = t1 + t2; + return 0 <= t1 && t1 <= 1 && 0 <= t2 && t2 <= 1 && see <= 1; +} + +static void +sp_ctrl_build_cache (SPCtrl *ctrl, int device_scale) +{ + guint32 stroke_color, fill_color; + if (ctrl->filled) { + if (ctrl->mode == SP_CTRL_MODE_XOR) { + fill_color = ctrl->fill_color; + } else { + fill_color = argb32_from_rgba(ctrl->fill_color); + } + } else { + fill_color = 0; + } + + if (ctrl->stroked) { + if (ctrl->mode == SP_CTRL_MODE_XOR) { + stroke_color = ctrl->stroke_color; + } else { + stroke_color = argb32_from_rgba(ctrl->stroke_color); + } + } else { + stroke_color = fill_color; + } + + gint width = ctrl->width * device_scale; + gint height = ctrl->height * device_scale; + + if (width < 2) return; + gint size = width * height; + + if (ctrl->cache) delete[] ctrl->cache; + ctrl->cache = new guint32[size]; + + switch (ctrl->shape) { + case SP_CTRL_SHAPE_SQUARE: + { + guint32* p = ctrl->cache; + + for (int i = 0; i < width; ++i) { + for (int j = 0; j < height; ++j) { + if ( i > device_scale - 1 && + j > device_scale - 1 && + width - i > device_scale && + height -j > device_scale) { + *p++ = fill_color; + } else { + *p++ = stroke_color; + } + } + } + + ctrl->build = TRUE; + break; + } + + case SP_CTRL_SHAPE_DIAMOND: + { + // width == height + guint32* p = ctrl->cache; + int m = (width+1)/2; + + for (int i = 0; i < width; ++i) { + for (int j = 0; j < height; ++j) { + if ( i + j > m-1+device_scale && + (width-1-i) + j > m-1+device_scale && + (width-1-i) + (height-1-j) > m-1+device_scale && + i + (height-1-j) > m-1+device_scale ) { + *p++ = fill_color; + } else + if ( i + j > m-2 && + (width-1-i) + j > m-2 && + (width-1-i) + (height-1-j) > m-2 && + i + (height-1-j) > m-2 ) { + *p++ = stroke_color; + } else { + *p++ = 0; + } + } + } + + ctrl->build = TRUE; + break; + } + + case SP_CTRL_SHAPE_CIRCLE: + { + guint32* p = ctrl->cache; + + double rs = width/2.0; + double rs2 = rs*rs; + double rf = rs-device_scale; + double rf2 = rf*rf; + + for (int i = 0; i < width; ++i) { + for (int j = 0; j < height; ++j) { + + double rx = i - (width /2.0) + 0.5; + double ry = j - (height/2.0) + 0.5; + double r2 = rx*rx + ry*ry; + + if (r2 < rf2) { + *p++ = fill_color; + } else if (r2 < rs2) { + *p++ = stroke_color; + } else { + *p++ = 0; + } + } + } + + ctrl->build = TRUE; + break; + } + + case SP_CTRL_SHAPE_TRIANGLE: + { + guint* p = ctrl->cache; + + Geom::Affine m = Geom::Translate(Geom::Point(-width/2.0,-height/2.0)); + m *= Geom::Rotate(-ctrl->angle); + m *= Geom::Translate(Geom::Point(width/2.0, height/2.0)); + + // Construct an arrowhead (triangle) of maximum size that won't leak out of rectangle + // defined by width and height, assuming width == height. + double w2 = width/2.0; + double h2 = height/2.0; + double w2cos = w2 * cos( M_PI/6 ); + double h2sin = h2 * sin( M_PI/6 ); + Geom::Point p1s(0, h2); + Geom::Point p2s(w2 + w2cos, h2 + h2sin); + Geom::Point p3s(w2 + w2cos, h2 - h2sin); + // Needed for constructing smaller arrowhead below. + double theta = atan2( Geom::Point( p2s - p1s ) ); + p1s *= m; + p2s *= m; + p3s *= m; + + // Construct a smaller arrow head for fill. + Geom::Point p1f(device_scale/sin(theta), h2); + Geom::Point p2f(w2 + w2cos, h2 - h2sin + device_scale/cos(theta)); + Geom::Point p3f(w2 + w2cos, h2 + h2sin - device_scale/cos(theta)); + p1f *= m; + p2f *= m; + p3f *= m; + + for(int y = 0; y < height; y++) { + for(int x = 0; x < width; x++) { + Geom::Point point = Geom::Point(x+0.5,y+0.5); + if (sp_point_inside_triangle(p1f, p2f, p3f, point)) { + p[(y*width)+x] = fill_color; + } else + if (sp_point_inside_triangle(p1s, p2s, p3s, point)) { + p[(y*width)+x] = stroke_color; + } else { + p[(y*width)+x] = 0; + } + } + } + + ctrl->build = TRUE; + break; + } + + case SP_CTRL_SHAPE_CROSS: + { + guint* p = ctrl->cache; + for(int y = 0; y < height; y++) { + for(int x = 0; x < width; x++) { + if ( abs(x - y) < device_scale || + abs(width - 1 - x - y) < device_scale ) { + *p++ = stroke_color; + } else { + *p++ = 0; + } + } + } + + ctrl->build = TRUE; + break; + } + + case SP_CTRL_SHAPE_BITMAP: + { + if (ctrl->pixbuf) { + unsigned char* px = gdk_pixbuf_get_pixels (ctrl->pixbuf); + unsigned int rs = gdk_pixbuf_get_rowstride (ctrl->pixbuf); + for (int y = 0; y < height/device_scale; y++){ + for (int x = 0; x < width/device_scale; x++) { + unsigned char *s = px + rs*y + 4*x; + guint32 color; + if (s[3] < 0x80) { + color = 0; + } else if (s[0] < 0x80) { + color = stroke_color; + } else { + color = fill_color; + } + + // Fill in device_scale x device_scale block + for (int i = 0; i < device_scale; ++i) { + for (int j = 0; j < device_scale; ++j) { + guint* p = ctrl->cache + + (x * device_scale + i) + // Column + (y * device_scale + j) * width; // Row + *p = color; + } + } + } + } + } else { + g_print ("control has no pixbuf\n"); + } + + ctrl->build = TRUE; + break; + } + + case SP_CTRL_SHAPE_IMAGE: + { + if (ctrl->pixbuf) { + guint rs = gdk_pixbuf_get_rowstride (ctrl->pixbuf); + guchar *data = gdk_pixbuf_get_pixels (ctrl->pixbuf); + + for (int y = 0; y < height/device_scale; y++){ + for (int x = 0; x < width/device_scale; x++) { + guint32 *px = reinterpret_cast<guint32*>(data + rs*y + 4*x); + + // Fill in device_scale x device_scale block + for (int i = 0; i < device_scale; ++i) { + for (int j = 0; j < device_scale; ++j) { + guint* p = ctrl->cache + + (x * device_scale + i) + // Column + (y * device_scale + j) * width; // Row + *p = *px; + } + } + } + } + } else { + g_print ("control has no pixbuf\n"); + } + ctrl->build = TRUE; + break; + } + + default: + break; + } +} + +static inline guint32 compose_xor(guint32 bg, guint32 fg, guint32 a) +{ + guint32 c = bg * (255-a) + (((bg ^ ~fg) + (bg >> 2) - (bg > 127 ? 63 : 0)) & 255) * a; + return (c + 127) / 255; +} + +static void +sp_ctrl_render (SPCanvasItem *item, SPCanvasBuf *buf) +{ + SPCtrl *ctrl = SP_CTRL (item); + + if (!ctrl->defined) return; + if ((!ctrl->filled) && (!ctrl->stroked)) return; + + // the control-image is rendered into ctrl->cache + if (!ctrl->build) { + sp_ctrl_build_cache (ctrl, buf->device_scale); + } + + // Must match width/height sp_ctrl_build_cache. + int w = ctrl->width * buf->device_scale; + int h = ctrl->height * buf->device_scale; + + double x = ctrl->box.left() - buf->rect.left(); + double y = ctrl->box.top() - buf->rect.top(); + + // The code below works even when the target is not an image surface + if (ctrl->mode == SP_CTRL_MODE_XOR) { + + // 1. Copy the affected part of output to a temporary surface + + // Size in device pixels. Does not set device scale. + cairo_surface_t *work = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h); + cairo_surface_set_device_scale(work, buf->device_scale, buf->device_scale); + + cairo_t *cr = cairo_create(work); + cairo_translate(cr, -ctrl->box.left(), -ctrl->box.top()); + cairo_set_source_surface(cr, cairo_get_target(buf->ct), buf->rect.left(), buf->rect.top()); + cairo_paint(cr); + cairo_destroy(cr); + // cairo_surface_write_to_png( work, "ctrl0.png" ); + + // 2. Composite the control on a temporary surface + cairo_surface_flush(work); + int strideb = cairo_image_surface_get_stride(work); + unsigned char *pxb = cairo_image_surface_get_data(work); + + guint32 *p = ctrl->cache; + for (int i=0; i<h; ++i) { + guint32 *pb = reinterpret_cast<guint32*>(pxb + i*strideb); + for (int j=0; j<w; ++j) { + guint32 cc = *p++; + guint32 ac = cc & 0xff; + if (ac == 0 && cc != 0) { + *pb++ = argb32_from_rgba(cc | 0x000000ff); + } else { + EXTRACT_ARGB32(*pb, ab,rb,gb,bb) + guint32 ro = compose_xor(rb, (cc & 0xff000000) >> 24, ac); + guint32 go = compose_xor(gb, (cc & 0x00ff0000) >> 16, ac); + guint32 bo = compose_xor(bb, (cc & 0x0000ff00) >> 8, ac); + ASSEMBLE_ARGB32(px, ab,ro,go,bo) + *pb++ = px; + } + } + } + cairo_surface_mark_dirty(work); + // cairo_surface_write_to_png( work, "ctrl1.png" ); + + // 3. Replace the affected part of output with contents of temporary surface + cairo_save(buf->ct); + cairo_set_source_surface(buf->ct, work, x, y); + cairo_rectangle(buf->ct, x, y, w/buf->device_scale, h/buf->device_scale); + cairo_clip(buf->ct); + cairo_set_operator(buf->ct, CAIRO_OPERATOR_SOURCE); + cairo_paint(buf->ct); + cairo_restore(buf->ct); + cairo_surface_destroy(work); + } else { + cairo_surface_t *cache = cairo_image_surface_create_for_data( + reinterpret_cast<unsigned char*>(ctrl->cache), CAIRO_FORMAT_ARGB32, w, h, w*4); + cairo_surface_set_device_scale(cache, buf->device_scale, buf->device_scale); + cairo_surface_mark_dirty(cache); + cairo_save(buf->ct); + cairo_set_source_surface(buf->ct, cache, x, y); + cairo_rectangle(buf->ct, x, y, w/buf->device_scale, h/buf->device_scale); + cairo_clip(buf->ct); + cairo_paint(buf->ct); + // cairo_surface_write_to_png(cache, "ctrl_cache.png" ); + cairo_restore(buf->ct); + cairo_surface_destroy(cache); + + } + ctrl->shown = TRUE; +} + +void SPCtrl::moveto (Geom::Point const p) { + if (p != _point) { + sp_canvas_item_affine_absolute (SP_CANVAS_ITEM (this), Geom::Affine(Geom::Translate (p))); + } + _point = p; +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/sodipodi-ctrl.h b/src/display/sodipodi-ctrl.h new file mode 100644 index 0000000..184c77e --- /dev/null +++ b/src/display/sodipodi-ctrl.h @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef INKSCAPE_CTRL_H +#define INKSCAPE_CTRL_H + +/* sodipodi-ctrl + * + * It is simply small square, which does not scale nor rotate + * + */ + +#include <gdk-pixbuf/gdk-pixbuf.h> +#include "sp-canvas-item.h" +#include "enums.h" + + +#define SP_TYPE_CTRL (sp_ctrl_get_type ()) +#define SP_CTRL(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SP_TYPE_CTRL, SPCtrl)) +#define SP_CTRL_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SP_TYPE_CTRL, SPCtrlClass)) +#define SP_IS_CTRL(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SP_TYPE_CTRL)) +#define SP_IS_CTRL_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), SP_TYPE_CTRL)) + +enum SPCtrlShapeType { + SP_CTRL_SHAPE_SQUARE, + SP_CTRL_SHAPE_DIAMOND, + SP_CTRL_SHAPE_CIRCLE, + SP_CTRL_SHAPE_TRIANGLE, + SP_CTRL_SHAPE_CROSS, + SP_CTRL_SHAPE_BITMAP, + SP_CTRL_SHAPE_IMAGE +}; + + +enum SPCtrlModeType { + SP_CTRL_MODE_COLOR, + SP_CTRL_MODE_XOR +}; + +struct SPCtrl : public SPCanvasItem { + SPCtrlShapeType shape; + SPCtrlModeType mode; + SPAnchorType anchor; + unsigned int width; + unsigned int height; + + guint defined : 1; + guint shown : 1; + guint build : 1; + guint filled : 1; + guint stroked : 1; + guint32 fill_color; + guint32 stroke_color; + gdouble angle; + + Geom::IntRect box; /* NB! x1 & y1 are included */ + guint32 *cache; + GdkPixbuf * pixbuf; + + void moveto(Geom::Point const p); + Geom::Point _point; +}; + +struct SPCtrlClass : public SPCanvasItemClass{ +}; + + + +/* Standard Gtk function */ +GType sp_ctrl_get_type (); + + +#endif /* !INKSCAPE_CTRL_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/sodipodi-ctrlrect.cpp b/src/display/sodipodi-ctrlrect.cpp new file mode 100644 index 0000000..b47f676 --- /dev/null +++ b/src/display/sodipodi-ctrlrect.cpp @@ -0,0 +1,323 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Simple non-transformed rectangle, usable for rubberband + * + * Author: + * Lauris Kaplinski <lauris@ximian.com> + * bulia byak <buliabyak@users.sf.net> + * Carl Hetherington <inkscape@carlh.net> + * Jon A. Cruz <jon@joncruz.org> + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 1999-2001 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2017 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + */ + +#include "inkscape.h" +#include "sodipodi-ctrlrect.h" +#include "sp-canvas-util.h" +#include "display/cairo-utils.h" +#include "display/sp-canvas.h" +#include <2geom/transforms.h> + +/* + * Currently we do not have point method, as it should always be painted + * during some transformation, which takes care of events... + * + * Corner coords can be in any order - i.e. x1 < x0 is allowed + */ + +static void sp_ctrlrect_destroy(SPCanvasItem *object); + +static void sp_ctrlrect_update(SPCanvasItem *item, Geom::Affine const &affine, unsigned int flags); +static void sp_ctrlrect_render(SPCanvasItem *item, SPCanvasBuf *buf); + +G_DEFINE_TYPE(CtrlRect, sp_ctrlrect, SP_TYPE_CANVAS_ITEM); + +static void sp_ctrlrect_class_init(CtrlRectClass *c) +{ + SPCanvasItemClass *item_class = SP_CANVAS_ITEM_CLASS(c); + + item_class->destroy = sp_ctrlrect_destroy; + item_class->update = sp_ctrlrect_update; + item_class->render = sp_ctrlrect_render; +} + +static void sp_ctrlrect_init(CtrlRect *cr) +{ + cr->init(); +} + +static void sp_ctrlrect_destroy(SPCanvasItem *object) +{ + if (SP_CANVAS_ITEM_CLASS(sp_ctrlrect_parent_class)->destroy) { + (* SP_CANVAS_ITEM_CLASS(sp_ctrlrect_parent_class)->destroy)(object); + } +} + + +static void sp_ctrlrect_render(SPCanvasItem *item, SPCanvasBuf *buf) +{ + SP_CTRLRECT(item)->render(buf); +} + + +static void sp_ctrlrect_update(SPCanvasItem *item, Geom::Affine const &affine, unsigned int flags) +{ + SP_CTRLRECT(item)->update(affine, flags); +} + + + +void CtrlRect::init() +{ + _has_fill = false; + _dashed = false; + _checkerboard = false; + + _shadow_width = 0; + + _area = Geom::OptIntRect(); + + _rect = Geom::Rect(Geom::Point(0,0),Geom::Point(0,0)); + + _border_color = 0x000000ff; + _fill_color = 0xffffffff; + _shadow_color = 0x000000ff; + _inverted = false; +} + + +void CtrlRect::render(SPCanvasBuf *buf) +{ + using Geom::X; + using Geom::Y; + + if (!_area) { + return; + } + + Geom::IntRect area = *_area; // _area already includes space for shadow. + if ( area.intersects(buf->rect) ) + { + + cairo_save(buf->ct); + cairo_translate(buf->ct, -buf->rect.left(), -buf->rect.top()); + + // Get canvas rotation (scale is isotropic). + double rotation = atan2( _affine[1], _affine[0] ); + + // Are we axis aligned? + double mod_rot = fmod(rotation * M_2_PI, 1); + bool axis_aligned = Geom::are_near( mod_rot, 0 ) || Geom::are_near( mod_rot, 1.0 ); + + // Get the points we need transformed to window coordinates. + Geom::Point rect_transformed[4]; + for (unsigned i = 0; i < 4; ++i) { + rect_transformed[i] = _rect.corner(i) * _affine; + } + + if(_inverted) { + cairo_set_operator(buf->ct, CAIRO_OPERATOR_DIFFERENCE); + } + + // Draw shadow first. Shadow extends under rectangle to reduce aliasing effects. + if (_shadow_width > 0 && !_dashed) { + Geom::Point const * corners = rect_transformed; + double shadowydir = _affine.det() > 0 ? -1 : 1; + + // is the desktop y-axis downwards? + if (SP_ACTIVE_DESKTOP && SP_ACTIVE_DESKTOP->is_yaxisdown()) { + ++corners; // need corners 1/2/3 instead of 0/1/2 + shadowydir *= -1; + } + + // Offset by half stroke width (_shadow_width is in window coordinates). + // Need to handle change in handedness with flips. + Geom::Point shadow( _shadow_width/2.0, shadowydir * _shadow_width/2.0 ); + shadow *= Geom::Rotate( rotation ); + + if (axis_aligned) { + // Snap to pixel grid (add 0.5 to center on pixel). + cairo_move_to( buf->ct, + floor(corners[0][X] + shadow[X]+0.5) + 0.5, + floor(corners[0][Y] + shadow[Y]+0.5) + 0.5 ); + cairo_line_to( buf->ct, + floor(corners[1][X] + shadow[X]+0.5) + 0.5, + floor(corners[1][Y] + shadow[Y]+0.5) + 0.5 ); + cairo_line_to( buf->ct, + floor(corners[2][X] + shadow[X]+0.5) + 0.5, + floor(corners[2][Y] + shadow[Y]+0.5) + 0.5 ); + } else { + cairo_move_to( buf->ct, + corners[0][X] + shadow[X], + corners[0][Y] + shadow[Y] ); + cairo_line_to( buf->ct, + corners[1][X] + shadow[X], + corners[1][Y] + shadow[Y] ); + cairo_line_to( buf->ct, + corners[2][X] + shadow[X], + corners[2][Y] + shadow[Y] ); + } + + ink_cairo_set_source_rgba32( buf->ct, _shadow_color ); + cairo_set_line_width( buf->ct, _shadow_width + 1 ); + cairo_stroke( buf->ct ); + } + + + // Setup rectangle path + if (axis_aligned) { + + // Snap to pixel grid + Geom::Rect outline( _rect.min() * _affine, _rect.max() * _affine); + cairo_rectangle (buf->ct, + floor(outline.min()[X])+0.5, + floor(outline.min()[Y])+0.5, + floor(outline.max()[X]) - floor(outline.min()[X]), + floor(outline.max()[Y]) - floor(outline.min()[Y])); + } else { + + // Angled + cairo_move_to( buf->ct, rect_transformed[0][X], rect_transformed[0][Y] ); + cairo_line_to( buf->ct, rect_transformed[1][X], rect_transformed[1][Y] ); + cairo_line_to( buf->ct, rect_transformed[2][X], rect_transformed[2][Y] ); + cairo_line_to( buf->ct, rect_transformed[3][X], rect_transformed[3][Y] ); + cairo_close_path( buf->ct ); + } + + // This doesn't seem to be used anywhere. If it is, then we should + // probably rotate the coordinate system and fill using a cairo_rectangle(). + if (_checkerboard) { + cairo_pattern_t *cb = ink_cairo_pattern_create_checkerboard(); + cairo_set_source(buf->ct, cb); + cairo_pattern_destroy(cb); + cairo_fill_preserve(buf->ct); + } + + if (_has_fill) { + ink_cairo_set_source_rgba32(buf->ct, _fill_color); + cairo_fill_preserve(buf->ct); + } + + // Set up stroke. + ink_cairo_set_source_rgba32(buf->ct, _border_color); + cairo_set_line_width(buf->ct, 1); + static double const dashes[2] = {4.0, 4.0}; + if (_dashed) cairo_set_dash(buf->ct, dashes, 2, 0); + + // Stroke rectangle. + cairo_stroke_preserve(buf->ct); + + // Highlight the border by drawing it in _shadow_color. + if (_shadow_width == 1 && _dashed) { + ink_cairo_set_source_rgba32(buf->ct, _shadow_color); + cairo_set_dash(buf->ct, dashes, 2, 4); // Dash offset by dash length. + cairo_stroke_preserve(buf->ct); + } + + cairo_new_path(buf->ct); // Clear path or get weird artifacts. + cairo_restore(buf->ct); + } +} + + +void CtrlRect::update(Geom::Affine const &affine, unsigned int flags) +{ + using Geom::X; + using Geom::Y; + + if ((SP_CANVAS_ITEM_CLASS(sp_ctrlrect_parent_class))->update) { + (SP_CANVAS_ITEM_CLASS(sp_ctrlrect_parent_class))->update(this, affine, flags); + } + + // Note: There is no harm if 'area' is too large other than a possible small slow down in + // rendering speed. + + // Calculate an axis-aligned bounding box that include all of transformed _rect. + Geom::Rect bbox = _rect; + bbox *= affine; + + // Enlarge bbox by twice shadow size (to allow for shadow on any side with a 45deg rotation). + bbox.expandBy( 2.0 *_shadow_width ); + + // Generate an integer rectangle that includes bbox. + Geom::OptIntRect _area_old = _area; + _area = bbox.roundOutwards(); + + // std::cout << " _rect: " << _rect << std::endl; + // std::cout << " bbox: " << bbox << std::endl; + // std::cout << " area: " << *_area << std::endl; + + if (_area) { + Geom::IntRect area = *_area; + // Windows glitches sometimes in cairo, possibly due to the way the surface is cleared. + // Increasing '_area' won't work as the box must be drawn 'inside' the updated area. + sp_canvas_update_bbox(this, area.left(), area.top(), area.right() + 1, area.bottom() + 1); + + } else { + std::cerr << "CtrlRect::update: No area!!" << std::endl; + } + + // At rendering stage, we need to know affine: + _affine = affine; +} + + +void CtrlRect::setColor(guint32 b, bool h, guint f) +{ + _border_color = b; + _has_fill = h; + _fill_color = f; + _requestUpdate(); +} + +void CtrlRect::setShadow(int s, guint c) +{ + _shadow_width = s; + _shadow_color = c; + _requestUpdate(); +} + +void CtrlRect::setInvert(bool invert) { + _inverted = invert; + _requestUpdate(); +} + +void CtrlRect::setRectangle(Geom::Rect const &r) +{ + _rect = r; + _requestUpdate(); +} + +void CtrlRect::setDashed(bool d) +{ + _dashed = d; + _requestUpdate(); +} + +void CtrlRect::setCheckerboard(bool d) +{ + _checkerboard = d; + _requestUpdate(); +} + +void CtrlRect::_requestUpdate() +{ + sp_canvas_item_request_update(SP_CANVAS_ITEM(this)); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/sodipodi-ctrlrect.h b/src/display/sodipodi-ctrlrect.h new file mode 100644 index 0000000..cdc4a3d --- /dev/null +++ b/src/display/sodipodi-ctrlrect.h @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_INKSCAPE_CTRLRECT_H +#define SEEN_INKSCAPE_CTRLRECT_H + +/** + * @file + * Simple non-transformed rectangle, usable for rubberband. + * Modified to work with rotated canvas. + */ +/* + * Authors: + * Lauris Kaplinski <lauris@ximian.com> + * Carl Hetherington <inkscape@carlh.net> + * Tavmjong Bah <tavjong@free.fr> + * + * Copyright (C) 1999-2001 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2017 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + */ + +#include <glib.h> +#include "sp-canvas-item.h" +#include <2geom/rect.h> +#include <2geom/int-rect.h> +#include <2geom/transforms.h> + +struct SPCanvasBuf; + +#define SP_TYPE_CTRLRECT (sp_ctrlrect_get_type ()) +#define SP_CTRLRECT(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), SP_TYPE_CTRLRECT, CtrlRect)) +#define SP_CTRLRECT_CLASS(c) (G_TYPE_CHECK_CLASS_CAST((c), SP_TYPE_CTRLRECT, CtrlRectClass)) +#define SP_IS_CTRLRECT(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SP_TYPE_CTRLRECT)) +#define SP_IS_CTRLRECT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), SP_TYPE_CTRLRECT)) + +class CtrlRect : public SPCanvasItem +{ +public: + + void init(); + void setColor(guint32 b, bool h, guint f); + void setShadow(int s, guint c); + void setInvert(bool invert); + void setRectangle(Geom::Rect const &r); + void setDashed(bool d); + void setCheckerboard(bool d); + + void render(SPCanvasBuf *buf); + void update(Geom::Affine const &affine, unsigned int flags); + +private: + void _requestUpdate(); + + Geom::Rect _rect; + Geom::Affine _affine; + + bool _has_fill; + bool _dashed; + bool _inverted; + bool _checkerboard; + + Geom::OptIntRect _area; + gint _shadow_width; + guint32 _border_color; + guint32 _fill_color; + guint32 _shadow_color; +}; + +struct CtrlRectClass : public SPCanvasItemClass {}; + +GType sp_ctrlrect_get_type(); + +#endif // SEEN_CTRLRECT_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/sp-canvas-group.h b/src/display/sp-canvas-group.h new file mode 100644 index 0000000..ff85b4e --- /dev/null +++ b/src/display/sp-canvas-group.h @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_CANVAS_GROUP_H +#define SEEN_SP_CANVAS_GROUP_H + +/** + * @file + * SPCanvasGroup. + */ +/* + * Authors: + * Federico Mena <federico@nuclecu.unam.mx> + * Raph Levien <raph@gimp.org> + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 1998 The Free Software Foundation + * Copyright (C) 2002 Lauris Kaplinski + * Copyright (C) 2010 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glib-object.h> + +#define SP_TYPE_CANVAS_GROUP (sp_canvas_group_get_type()) +#define SP_CANVAS_GROUP(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), SP_TYPE_CANVAS_GROUP, SPCanvasGroup)) +#define SP_IS_CANVAS_GROUP(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), SP_TYPE_CANVAS_GROUP)) + +GType sp_canvas_group_get_type(); + + + +#endif // SEEN_SP_CANVAS_GROUP_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/display/sp-canvas-item.h b/src/display/sp-canvas-item.h new file mode 100644 index 0000000..9856906 --- /dev/null +++ b/src/display/sp-canvas-item.h @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_CANVAS_ITEM_H +#define SEEN_SP_CANVAS_ITEM_H + +/** + * @file + * SPCanvasItem. + */ +/* + * Authors: + * Federico Mena <federico@nuclecu.unam.mx> + * Raph Levien <raph@gimp.org> + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 1998 The Free Software Foundation + * Copyright (C) 2002 Lauris Kaplinski + * Copyright (C) 2010 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/rect.h> +#include <boost/intrusive/list.hpp> +#include <glib-object.h> + +#include "ui/control-types.h" + +G_BEGIN_DECLS + +struct SPCanvas; +struct SPCanvasBuf; +struct SPCanvasGroup; + +typedef struct _SPCanvasItemClass SPCanvasItemClass; +typedef union _GdkEvent GdkEvent; +typedef struct _GdkCursor GdkCursor; + +#define SP_TYPE_CANVAS_ITEM (sp_canvas_item_get_type()) +#define SP_CANVAS_ITEM(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), SP_TYPE_CANVAS_ITEM, SPCanvasItem)) +#define SP_CANVAS_ITEM_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SP_TYPE_CANVAS_ITEM, SPCanvasItemClass)) +#define SP_IS_CANVAS_ITEM(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), SP_TYPE_CANVAS_ITEM)) +#define SP_CANVAS_ITEM_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS((o), SP_TYPE_CANVAS_ITEM, SPCanvasItemClass)) + + +/** + * An SPCanvasItem refers to a SPCanvas and to its parent item; it has + * four coordinates, a bounding rectangle, and a transformation matrix. + */ +struct SPCanvasItem { + GInitiallyUnowned parent_instance; + + // boost linked list member hook + boost::intrusive::list_member_hook<> member_hook_; + + SPCanvas *canvas; + SPCanvasItem *parent; + + double x1; + double y1; + double x2; + double y2; + Geom::Rect bounds; + Geom::Affine xform; + + int ctrlResize; + Inkscape::ControlType ctrlType; + Inkscape::ControlFlags ctrlFlags; + + // Replacement for custom GtkObject flag enumeration + gboolean visible; + gboolean need_update; + gboolean need_affine; + + // If true, then SPCanvasGroup::point() and sp_canvas_item_invoke_point() will calculate + // the distance to the pointer, such that this item can be picked in pickCurrentItem() + // Only if an item can be picked, then it can be set as current_item and receive events! + bool pickable; + + bool in_destruction; +}; + +GType sp_canvas_item_get_type(); + +//Define type for linked list storing SPCanvasItems +typedef boost::intrusive::list< + SPCanvasItem, + boost::intrusive::member_hook<SPCanvasItem, boost::intrusive::list_member_hook<>, &SPCanvasItem::member_hook_> > + SPCanvasItemList; + +/** + * The vtable of an SPCanvasItem. + */ +struct _SPCanvasItemClass { + GInitiallyUnownedClass parent_class; + + void (* update) (SPCanvasItem *item, Geom::Affine const &affine, unsigned int flags); + + void (* render) (SPCanvasItem *item, SPCanvasBuf *buf); + double (* point) (SPCanvasItem *item, Geom::Point p, SPCanvasItem **actual_item); + + int (* event) (SPCanvasItem *item, GdkEvent *event); + void (* viewbox_changed) (SPCanvasItem *item, Geom::IntRect const &new_area); + + /* Default signal handler for the ::destroy signal, which is + * invoked to request that references to the widget be dropped. + * If an object class overrides destroy() in order to perform class + * specific destruction then it must still invoke its superclass' + * implementation of the method after it is finished with its + * own cleanup. (See gtk_widget_real_destroy() for an example of + * how to do this). + */ + void (*destroy) (SPCanvasItem *object); +}; + +/** + * Constructs new SPCanvasItem on SPCanvasGroup. + */ +SPCanvasItem *sp_canvas_item_new(SPCanvasGroup *parent, GType type, const gchar *first_arg_name, ...); + +G_END_DECLS + + +void sp_canvas_item_affine_absolute(SPCanvasItem *item, Geom::Affine const &aff); + +void sp_canvas_item_raise(SPCanvasItem *item, int positions); +void sp_canvas_item_raise_to_top(SPCanvasItem *item); +void sp_canvas_item_lower(SPCanvasItem *item, int positions); +void sp_canvas_item_lower_to_bottom(SPCanvasItem *item); +bool sp_canvas_item_is_visible(SPCanvasItem *item); +void sp_canvas_item_show(SPCanvasItem *item); +void sp_canvas_item_hide(SPCanvasItem *item); +void sp_canvas_item_destroy(SPCanvasItem *item); +int sp_canvas_item_grab(SPCanvasItem *item, unsigned int event_mask, GdkCursor *cursor, guint32 etime); +void sp_canvas_item_ungrab(SPCanvasItem *item); + +Geom::Affine sp_canvas_item_i2w_affine(SPCanvasItem const *item); + +void sp_canvas_item_request_update(SPCanvasItem *item); + +/* get item z-order in parent group */ + +gint sp_canvas_item_order(SPCanvasItem * item); + + + +#endif // SEEN_SP_CANVAS_ITEM_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/display/sp-canvas-util.cpp b/src/display/sp-canvas-util.cpp new file mode 100644 index 0000000..dee7d8d --- /dev/null +++ b/src/display/sp-canvas-util.cpp @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Helper stuff for SPCanvas + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 1999-2002 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#include <2geom/affine.h> +#include "sp-canvas-util.h" +#include "sp-canvas-item.h" +#include "sp-canvas.h" + +void sp_canvas_update_bbox(SPCanvasItem *item, int x1, int y1, int x2, int y2) +{ + item->canvas->requestRedraw((int)item->x1, (int)item->y1, (int)item->x2, (int)item->y2); + item->x1 = x1; + item->y1 = y1; + item->x2 = x2; + item->y2 = y2; + item->canvas->requestRedraw((int)item->x1, (int)item->y1, (int)item->x2, (int)item->y2); +} + +void +sp_canvas_item_reset_bounds (SPCanvasItem *item) +{ + item->x1 = 0.0; + item->y1 = 0.0; + item->x2 = 0.0; + item->y2 = 0.0; +} + +void sp_canvas_prepare_buffer(SPCanvasBuf *) +{ +} + +Geom::Affine sp_canvas_item_i2p_affine (SPCanvasItem * item) +{ + g_assert (item != nullptr); /* this may be overly zealous - it is + * plausible that this gets called + * with item == 0. */ + + return item->xform; +} + +Geom::Affine sp_canvas_item_i2i_affine (SPCanvasItem * from, SPCanvasItem * to) +{ + g_assert (from != nullptr); + g_assert (to != nullptr); + + return sp_canvas_item_i2w_affine(from) * sp_canvas_item_i2w_affine(to).inverse(); +} + +void sp_canvas_item_set_i2w_affine (SPCanvasItem * item, Geom::Affine const &i2w) +{ + g_assert (item != nullptr); + + sp_canvas_item_affine_absolute(item, i2w * sp_canvas_item_i2w_affine(item->parent).inverse()); +} + +void sp_canvas_item_move_to_z (SPCanvasItem * item, gint z) +{ + g_assert (item != nullptr); + + if (z == 0) + return sp_canvas_item_lower_to_bottom(item); + + gint current_z = sp_canvas_item_order (item); + + if (current_z == -1) // not found in its parent + return; + + if (z == current_z) + return; + + if (z > current_z) { + sp_canvas_item_raise (item, z - current_z); + } else { + sp_canvas_item_lower (item, current_z - z); + } +} + +gint +sp_canvas_item_compare_z (SPCanvasItem * a, SPCanvasItem * b) +{ + gint const o_a = sp_canvas_item_order (a); + gint const o_b = sp_canvas_item_order (b); + + if (o_a > o_b) return -1; + if (o_a < o_b) return 1; + + return 0; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/sp-canvas-util.h b/src/display/sp-canvas-util.h new file mode 100644 index 0000000..f58deb1 --- /dev/null +++ b/src/display/sp-canvas-util.h @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_CANVAS_UTILS_H +#define SEEN_SP_CANVAS_UTILS_H + +/* + * Helper stuff for SPCanvas + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 1999-2002 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/value.h> + +struct SPCanvasItem; +struct SPCanvasBuf; + +namespace Geom { + class Affine; +} + +/* Miscellaneous utility & convenience functions for general canvas objects */ + +void sp_canvas_update_bbox (SPCanvasItem *item, int x1, int y1, int x2, int y2); +void sp_canvas_item_reset_bounds (SPCanvasItem *item); +void sp_canvas_prepare_buffer (SPCanvasBuf *buf); + +/* get i2p (item to parent) affine transformation as general 6-element array */ + +Geom::Affine sp_canvas_item_i2p_affine (SPCanvasItem * item); + +/* get i2i (item to item) affine transformation as general 6-element array */ + +Geom::Affine sp_canvas_item_i2i_affine (SPCanvasItem * from, SPCanvasItem * to); + +/* set item affine matrix to achieve given i2w matrix */ + +void sp_canvas_item_set_i2w_affine (SPCanvasItem * item, Geom::Affine const & aff); + +void sp_canvas_item_move_to_z (SPCanvasItem * item, gint z); + +gint sp_canvas_item_compare_z (SPCanvasItem * a, SPCanvasItem * b); + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/sp-canvas.cpp b/src/display/sp-canvas.cpp new file mode 100644 index 0000000..87a9722 --- /dev/null +++ b/src/display/sp-canvas.cpp @@ -0,0 +1,2959 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Port of GnomeCanvas for Inkscape needs + * + * Authors: + * Federico Mena <federico@nuclecu.unam.mx> + * Raph Levien <raph@gimp.org> + * Lauris Kaplinski <lauris@kaplinski.com> + * fred + * bbyak + * Jon A. Cruz <jon@joncruz.org> + * Krzysztof KosiĆski <tweenk.pl@gmail.com> + * + * Copyright (C) 1998 The Free Software Foundation + * Copyright (C) 2002-2006 authors + * Copyright (C) 2016 Google Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include <gdkmm/devicemanager.h> +#include <gdkmm/display.h> +#include <gdkmm/rectangle.h> +#include <gdkmm/seat.h> + +#include <cairomm/region.h> + +#include "cms-system.h" +#include "color.h" +#include "debug/gdk-event-latency-tracker.h" +#include "desktop.h" +#include "display/cairo-utils.h" +#include "display/canvas-arena.h" +#include "display/rendermode.h" +#include "display/sp-canvas-group.h" +#include "display/sp-canvas.h" +#include "helper/sp-marshal.h" +#include "inkscape-window.h" +#include "inkscape.h" +#include "preferences.h" +#include "sodipodi-ctrlrect.h" +#include "ui/tools/node-tool.h" +#include "ui/tools/tool-base.h" +#include "widgets/desktop-widget.h" +#include <2geom/affine.h> +#include <2geom/rect.h> + +using Inkscape::Debug::GdkEventLatencyTracker; + + +// Disabled by Mentalguy, many years ago in commit 427a81 +static bool const HAS_BROKEN_MOTION_HINTS = true; + +// Define this to visualize the regions to be redrawn +//#define DEBUG_REDRAW 1; + +// Define this to output the time spent in a full idle loop and the number of "tiles" painted +//#define DEBUG_PERFORMANCE 1; + +// Tiles are a way to minimize the number of redraws, eliminating too small redraws. +// The canvas stores a 2D array of ints, each representing a TILE_SIZExTILE_SIZE pixels tile. +// If any part of it is dirtied, the entire tile is dirtied (its int is nonzero) and repainted. +#define TILE_SIZE 16 + +/** + * The SPCanvasGroup vtable. + */ +struct SPCanvasGroupClass { + SPCanvasItemClass parent_class; +}; + +static void ungrab_default_client_pointer() +{ + auto const display = Gdk::Display::get_default(); + auto const seat = display->get_default_seat(); + seat->ungrab(); +} + +/** + * A group of items. + */ +struct SPCanvasGroup { + /** + * Adds an item to a canvas group. + */ + void add(SPCanvasItem *item); + + /** + * Removes an item from a canvas group. + */ + void remove(SPCanvasItem *item); + + /** + * Class initialization function for SPCanvasGroupClass. + */ + static void classInit(SPCanvasGroupClass *klass); + + /** + * Callback. Empty. + */ + static void init(SPCanvasGroup *group); + + /** + * Callback that destroys all items in group and calls group's virtual + * destroy() function. + */ + static void destroy(SPCanvasItem *object); + + /** + * Update handler for canvas groups. + */ + static void update(SPCanvasItem *item, Geom::Affine const &affine, unsigned int flags); + + /** + * Point handler for canvas groups. + */ + static double point(SPCanvasItem *item, Geom::Point p, SPCanvasItem **actual_item); + + /** + * Renders all visible canvas group items in buf rectangle. + */ + static void render(SPCanvasItem *item, SPCanvasBuf *buf); + + static void viewboxChanged(SPCanvasItem *item, Geom::IntRect const &new_area); + + + // Data members: ---------------------------------------------------------- + + SPCanvasItem item; + + SPCanvasItemList items; +}; + +/** + * The SPCanvas vtable. + */ +struct SPCanvasClass { + GtkWidgetClass parent_class; +}; + +namespace { + +GdkWindow *getWindow(SPCanvas *canvas) +{ + return gtk_widget_get_window(reinterpret_cast<GtkWidget *>(canvas)); +} + + +// SPCanvasItem + +enum { + ITEM_EVENT, + ITEM_LAST_SIGNAL +}; + +enum { + PROP_0, + PROP_VISIBLE +}; + + +void trackLatency(GdkEvent const *event); + +enum { + DESTROY, + LAST_SIGNAL +}; + +/** + * Callback that removes item from all referrers and destroys it. + */ +void sp_canvas_item_dispose(GObject *object); +void sp_canvas_item_finalize(GObject *object); +void sp_canvas_item_real_destroy(SPCanvasItem *object); + +static guint object_signals[LAST_SIGNAL] = { 0 }; + +/** + * Sets up the newly created SPCanvasItem. + * + * We make it static for encapsulation reasons since it was nowhere used. + */ +void sp_canvas_item_construct(SPCanvasItem *item, SPCanvasGroup *parent, gchar const *first_arg_name, va_list args); + +/** + * Helper that returns true iff item is descendant of parent. + */ +bool is_descendant(SPCanvasItem const *item, SPCanvasItem const *parent); + +guint item_signals[ITEM_LAST_SIGNAL] = { 0 }; + +} // namespace + +G_DEFINE_TYPE(SPCanvasItem, sp_canvas_item, G_TYPE_INITIALLY_UNOWNED); + +static void +sp_canvas_item_class_init(SPCanvasItemClass *klass) +{ + GObjectClass *gobject_class = (GObjectClass *) klass; + + item_signals[ITEM_EVENT] = g_signal_new ("event", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + ((glong)((guint8*)&(klass->event) - (guint8*)klass)), + nullptr, nullptr, + sp_marshal_BOOLEAN__POINTER, + G_TYPE_BOOLEAN, 1, + GDK_TYPE_EVENT); + + gobject_class->dispose = sp_canvas_item_dispose; + gobject_class->finalize = sp_canvas_item_finalize; + klass->destroy = sp_canvas_item_real_destroy; + + object_signals[DESTROY] = + g_signal_new ("destroy", + G_TYPE_FROM_CLASS (gobject_class), + (GSignalFlags)(G_SIGNAL_RUN_CLEANUP | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS), + G_STRUCT_OFFSET (SPCanvasItemClass, destroy), + nullptr, nullptr, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); +} + +static void +sp_canvas_item_init(SPCanvasItem *item) +{ + item->xform = Geom::Affine(Geom::identity()); + item->ctrlResize = 0; + item->ctrlType = Inkscape::CTRL_TYPE_UNKNOWN; + item->ctrlFlags = Inkscape::CTRL_FLAG_NORMAL; + + // TODO items should not be visible on creation - this causes kludges with items + // that should be initially invisible; examples of such items: node handles, the CtrlRect + // used for rubberbanding, path outline, etc. + item->visible = TRUE; + item->in_destruction = false; + item->pickable = true; +} + +SPCanvasItem *sp_canvas_item_new(SPCanvasGroup *parent, GType type, gchar const *first_arg_name, ...) +{ + va_list args; + + g_return_val_if_fail(parent != nullptr, NULL); + g_return_val_if_fail(SP_IS_CANVAS_GROUP(parent), NULL); + g_return_val_if_fail(g_type_is_a(type, SP_TYPE_CANVAS_ITEM), NULL); + + SPCanvasItem *item = SP_CANVAS_ITEM(g_object_new(type, nullptr)); + + va_start(args, first_arg_name); + sp_canvas_item_construct(item, parent, first_arg_name, args); + va_end(args); + + return item; +} + +namespace { + +void sp_canvas_item_construct(SPCanvasItem *item, SPCanvasGroup *parent, gchar const *first_arg_name, va_list args) +{ + g_return_if_fail(SP_IS_CANVAS_GROUP(parent)); + g_return_if_fail(SP_IS_CANVAS_ITEM(item)); + + // manually invoke list_member_hook constructor + new (&(item->member_hook_)) boost::intrusive::list_member_hook<>(); + + item->parent = SP_CANVAS_ITEM(parent); + item->canvas = item->parent->canvas; + + g_object_set_valist(G_OBJECT(item), first_arg_name, args); + + SP_CANVAS_GROUP(item->parent)->add(item); + + sp_canvas_item_request_update(item); +} + +} // namespace + +/** + * Helper function that requests redraw only if item's visible flag is set. + */ +static void redraw_if_visible(SPCanvasItem *item) +{ + if (item->visible) { + int x0 = (int)(item->x1); + int x1 = (int)(item->x2); + int y0 = (int)(item->y1); + int y1 = (int)(item->y2); + + if (x0 !=0 || x1 !=0 || y0 !=0 || y1 !=0) { + item->canvas->requestRedraw((int)(item->x1 - 1), (int)(item->y1 -1), (int)(item->x2 + 1), (int)(item->y2 + 1)); + } + } +} + +void sp_canvas_item_destroy(SPCanvasItem *item) +{ + g_return_if_fail(SP_IS_CANVAS_ITEM(item)); + + if (!item->in_destruction) + g_object_run_dispose(G_OBJECT(item)); +} + +namespace { +void sp_canvas_item_dispose(GObject *object) +{ + SPCanvasItem *item = SP_CANVAS_ITEM (object); + + /* guard against reinvocations during + * destruction with the in_destruction flag. + */ + if (!item->in_destruction) + { + item->in_destruction=true; + + // Hack: if this is a ctrlrect, move it to 0,0; + // this redraws only the stroke of the rect to be deleted, + // avoiding redraw of the entire area + if (SP_IS_CTRLRECT(item)) { + SP_CTRLRECT(object)->setRectangle(Geom::Rect(Geom::Point(0,0),Geom::Point(0,0))); + SP_CTRLRECT(object)->update(item->xform, 0); + } else { + redraw_if_visible (item); + } + item->visible = FALSE; + + if (item == item->canvas->_current_item) { + item->canvas->_current_item = nullptr; + item->canvas->_need_repick = TRUE; + } + + if (item == item->canvas->_new_current_item) { + item->canvas->_new_current_item = nullptr; + item->canvas->_need_repick = TRUE; + } + + if (item == item->canvas->_grabbed_item) { + item->canvas->_grabbed_item = nullptr; + ungrab_default_client_pointer(); + } + + if (item == item->canvas->_focused_item) { + item->canvas->_focused_item = nullptr; + } + + if (item->parent) { + SP_CANVAS_GROUP(item->parent)->remove(item); + } + + g_signal_emit (object, object_signals[DESTROY], 0); + item->in_destruction = false; + } + + G_OBJECT_CLASS(sp_canvas_item_parent_class)->dispose(object); +} + +void sp_reset_spliter(SPCanvas *canvas) +{ + canvas->_spliter = Geom::OptIntRect(); + canvas->_spliter_area = Geom::OptIntRect(); + canvas->_spliter_control = Geom::OptIntRect(); + canvas->_spliter_top = Geom::OptIntRect(); + canvas->_spliter_bottom = Geom::OptIntRect(); + canvas->_spliter_left = Geom::OptIntRect(); + canvas->_spliter_right = Geom::OptIntRect(); + canvas->_spliter_control_pos = Geom::Point(); + canvas->_spliter_in_control_pos = Geom::Point(); + canvas->_split_value = 0.5; + canvas->_split_vertical = true; + canvas->_split_inverse = false; + canvas->_split_hover_vertical = false; + canvas->_split_hover_horizontal = false; + canvas->_split_hover = false; + canvas->_split_pressed = false; + canvas->_split_control_pressed = false; + canvas->_split_dragging = false; +} + +void sp_canvas_item_real_destroy(SPCanvasItem *object) +{ + g_signal_handlers_destroy(object); +} + +void sp_canvas_item_finalize(GObject *gobject) +{ + SPCanvasItem *object = SP_CANVAS_ITEM(gobject); + + if (g_object_is_floating (object)) + { + g_warning ("A floating object was finalized. This means that someone\n" + "called g_object_unref() on an object that had only a floating\n" + "reference; the initial floating reference is not owned by anyone\n" + "and must be removed with g_object_ref_sink()."); + } + + G_OBJECT_CLASS (sp_canvas_item_parent_class)->finalize (gobject); +} +} // namespace + +/** + * Helper function to update item and its children. + * + * NB! affine is parent2canvas. + */ +static void sp_canvas_item_invoke_update(SPCanvasItem *item, Geom::Affine const &affine, unsigned int flags) +{ + // Apply the child item's transform + Geom::Affine child_affine = item->xform * affine; + + // apply object flags to child flags + int child_flags = flags & ~SP_CANVAS_UPDATE_REQUESTED; + + if (item->need_update) { + child_flags |= SP_CANVAS_UPDATE_REQUESTED; + } + + if (item->need_affine) { + child_flags |= SP_CANVAS_UPDATE_AFFINE; + } + + if (child_flags & (SP_CANVAS_UPDATE_REQUESTED | SP_CANVAS_UPDATE_AFFINE)) { + if (SP_CANVAS_ITEM_GET_CLASS (item)->update) { + SP_CANVAS_ITEM_GET_CLASS (item)->update(item, child_affine, child_flags); + } + } + + item->need_update = FALSE; + item->need_affine = FALSE; +} + +/** + * Helper function to invoke the point method of the item. + * + * The argument x, y should be in the parent's item-relative coordinate + * system. This routine applies the _split_inverse of the item's transform, + * maintaining the affine invariant. + */ +static double sp_canvas_item_invoke_point(SPCanvasItem *item, Geom::Point p, SPCanvasItem **actual_item) +{ + if (SP_CANVAS_ITEM_GET_CLASS(item)->point) { + return SP_CANVAS_ITEM_GET_CLASS (item)->point (item, p, actual_item); + } + + return Geom::infinity(); +} + +/** + * Makes the item's affine transformation matrix be equal to the specified + * matrix. + * + * @item: A canvas item. + * @affine: An affine transformation matrix. + */ +void sp_canvas_item_affine_absolute(SPCanvasItem *item, Geom::Affine const &affine) +{ + item->xform = affine; + + if (!item->need_affine) { + item->need_affine = TRUE; + if (item->parent != nullptr) { + sp_canvas_item_request_update (item->parent); + } else { + item->canvas->requestUpdate(); + } + } + + item->canvas->_need_repick = TRUE; +} + +/** + * Raises the item in its parent's stack by the specified number of positions. + * + * @param item A canvas item. + * @param positions Number of steps to raise the item. + * + * If the number of positions is greater than the distance to the top of the + * stack, then the item is put at the top. + */ +void sp_canvas_item_raise(SPCanvasItem *item, int positions) +{ + g_return_if_fail (item != nullptr); + g_return_if_fail (SP_IS_CANVAS_ITEM (item)); + g_return_if_fail (positions >= 0); + + if (!item->parent || positions == 0) { + return; + } + + SPCanvasGroup *parent = SP_CANVAS_GROUP (item->parent); + auto from = parent->items.iterator_to(*item); + auto to = from; + + for (int i = 0; i <= positions && to != parent->items.end(); ++i) { + ++to; + } + + parent->items.erase(from); + parent->items.insert(to, *item); + + redraw_if_visible (item); + item->canvas->_need_repick = TRUE; +} + +void sp_canvas_item_raise_to_top(SPCanvasItem *item) +{ + g_return_if_fail (item != nullptr); + g_return_if_fail (SP_IS_CANVAS_ITEM (item)); + if (!item->parent) + return; + SPCanvasGroup *parent = SP_CANVAS_GROUP (item->parent); + parent->items.erase(parent->items.iterator_to(*item)); + parent->items.push_back(*item); + redraw_if_visible (item); + item->canvas->_need_repick = TRUE; +} + + + +/** + * Lowers the item in its parent's stack by the specified number of positions. + * + * @param item A canvas item. + * @param positions Number of steps to lower the item. + * + * If the number of positions is greater than the distance to the bottom of the + * stack, then the item is put at the bottom. + */ +void sp_canvas_item_lower(SPCanvasItem *item, int positions) +{ + g_return_if_fail (item != nullptr); + g_return_if_fail (SP_IS_CANVAS_ITEM (item)); + g_return_if_fail (positions >= 1); + + SPCanvasGroup *parent = SP_CANVAS_GROUP(item->parent); + + if (!parent || positions == 0 || item == &parent->items.front()) { + return; + } + + auto from = parent->items.iterator_to(*item); + auto to = from; + g_assert(from != parent->items.end()); + + for (int i = 0; i < positions && to != parent->items.begin(); ++i) { + --to; + } + + parent->items.erase(from); + parent->items.insert(to, *item); + + redraw_if_visible (item); + item->canvas->_need_repick = TRUE; +} + +void sp_canvas_item_lower_to_bottom(SPCanvasItem *item) +{ + g_return_if_fail (item != nullptr); + g_return_if_fail (SP_IS_CANVAS_ITEM (item)); + if (!item->parent) + return; + SPCanvasGroup *parent = SP_CANVAS_GROUP (item->parent); + parent->items.erase(parent->items.iterator_to(*item)); + parent->items.push_front(*item); + redraw_if_visible (item); + item->canvas->_need_repick = TRUE; +} + +bool sp_canvas_item_is_visible(SPCanvasItem *item) +{ + return item->visible; +} + +/** + * Sets visible flag on item and requests a redraw. + */ +void sp_canvas_item_show(SPCanvasItem *item) +{ + g_return_if_fail (item != nullptr); + g_return_if_fail (SP_IS_CANVAS_ITEM (item)); + + if (item->visible) { + return; + } + + item->visible = TRUE; + + int x0 = (int)(item->x1); + int x1 = (int)(item->x2); + int y0 = (int)(item->y1); + int y1 = (int)(item->y2); + + if (x0 !=0 || x1 !=0 || y0 !=0 || y1 !=0) { + item->canvas->requestRedraw((int)(item->x1), (int)(item->y1), (int)(item->x2 + 1), (int)(item->y2 + 1)); + item->canvas->_need_repick = TRUE; + } +} + +/** + * Clears visible flag on item and requests a redraw. + */ +void sp_canvas_item_hide(SPCanvasItem *item) +{ + g_return_if_fail (item != nullptr); + g_return_if_fail (SP_IS_CANVAS_ITEM (item)); + + if (!item->visible) { + return; + } + + item->visible = FALSE; + + int x0 = (int)(item->x1); + int x1 = (int)(item->x2); + int y0 = (int)(item->y1); + int y1 = (int)(item->y2); + + if (x0 !=0 || x1 !=0 || y0 !=0 || y1 !=0) { + item->canvas->requestRedraw((int)item->x1, (int)item->y1, (int)(item->x2 + 1), (int)(item->y2 + 1)); + item->canvas->_need_repick = TRUE; + } +} + +/** + * Grab item under cursor. + * + * \pre !canvas->grabbed_item && item->flags & SP_CANVAS_ITEM_VISIBLE + */ +int sp_canvas_item_grab(SPCanvasItem *item, guint event_mask, GdkCursor *cursor, guint32 etime) +{ + g_return_val_if_fail (item != nullptr, -1); + g_return_val_if_fail (SP_IS_CANVAS_ITEM (item), -1); + g_return_val_if_fail (gtk_widget_get_mapped (GTK_WIDGET (item->canvas)), -1); + + if (item->canvas->_grabbed_item) { + return -1; + } + + // This test disallows grabbing events by an invisible item, which may be useful + // sometimes. An example is the hidden control point used for the selector component, + // where it is used for object selection and rubberbanding. There seems to be nothing + // preventing this except this test, so I removed it. + // -- Krzysztof KosiĆski, 2009.08.12 + //if (!(item->flags & SP_CANVAS_ITEM_VISIBLE)) + // return -1; + + if (HAS_BROKEN_MOTION_HINTS) { + event_mask &= ~GDK_POINTER_MOTION_HINT_MASK; + } + + // fixme: Top hack (Lauris) + // fixme: If we add key masks to event mask, Gdk will abort (Lauris) + // fixme: But Canvas actually does get key events, so all we need is routing these here + auto display = gdk_display_get_default(); + auto seat = gdk_display_get_default_seat(display); + gdk_seat_grab(seat, + getWindow(item->canvas), + GDK_SEAT_CAPABILITY_ALL_POINTING, + FALSE, + cursor, + nullptr, + nullptr, + nullptr); + + item->canvas->_grabbed_item = item; + item->canvas->_grabbed_event_mask = event_mask; + item->canvas->_current_item = item; // So that events go to the grabbed item + + return 0; +} + +/** + * Ungrabs the item, which must have been grabbed in the canvas, and ungrabs the + * mouse. + * + * @param item A canvas item that holds a grab. + */ +void sp_canvas_item_ungrab(SPCanvasItem *item) +{ + g_return_if_fail (item != nullptr); + g_return_if_fail (SP_IS_CANVAS_ITEM (item)); + + if (item->canvas->_grabbed_item != item) { + return; + } + + item->canvas->_grabbed_item = nullptr; + ungrab_default_client_pointer(); +} + +/** + * Returns the product of all transformation matrices from the root item down + * to the item. + */ +Geom::Affine sp_canvas_item_i2w_affine(SPCanvasItem const *item) +{ + g_assert (SP_IS_CANVAS_ITEM (item)); // should we get this? + + Geom::Affine affine = Geom::identity(); + + while (item) { + affine *= item->xform; + item = item->parent; + } + return affine; +} + +namespace { + +bool is_descendant(SPCanvasItem const *item, SPCanvasItem const *parent) +{ + while (item) { + if (item == parent) { + return true; + } + item = item->parent; + } + + return false; +} + +} // namespace + +/** + * Requests that the canvas queue an update for the specified item. + * + * To be used only by item implementations. + */ +void sp_canvas_item_request_update(SPCanvasItem *item) +{ + if (item->need_update) { + return; + } + + item->need_update = TRUE; + + if (item->parent != nullptr) { + // Recurse up the tree + sp_canvas_item_request_update (item->parent); + } else { + // Have reached the top of the tree, make sure the update call gets scheduled. + item->canvas->requestUpdate(); + } +} + +/** + * Returns position of item in group. + */ +gint sp_canvas_item_order (SPCanvasItem * item) +{ + SPCanvasGroup * p = SP_CANVAS_GROUP(item->parent); + size_t index = 0; + for (auto it = p->items.begin(); it != p->items.end(); ++it, ++index) { + if (item == &(*it)) { + return index; + } + } + + return -1; +} + +// SPCanvasGroup +G_DEFINE_TYPE(SPCanvasGroup, sp_canvas_group, SP_TYPE_CANVAS_ITEM); + +static void sp_canvas_group_class_init(SPCanvasGroupClass *klass) +{ + SPCanvasItemClass *item_class = reinterpret_cast<SPCanvasItemClass *>(klass); + + item_class->destroy = SPCanvasGroup::destroy; + item_class->update = SPCanvasGroup::update; + item_class->render = SPCanvasGroup::render; + item_class->point = SPCanvasGroup::point; + item_class->viewbox_changed = SPCanvasGroup::viewboxChanged; +} + +static void sp_canvas_group_init(SPCanvasGroup * group) +{ + new (&group->items) SPCanvasItemList; +} + +void SPCanvasGroup::destroy(SPCanvasItem *object) +{ + g_return_if_fail(object != nullptr); + g_return_if_fail(SP_IS_CANVAS_GROUP(object)); + + SPCanvasGroup *group = SP_CANVAS_GROUP(object); + + for (auto it = group->items.begin(); it != group->items.end();) { + SPCanvasItem *item = &(*it); + it++; + sp_canvas_item_destroy(item); + } + + group->items.clear(); + group->items.~SPCanvasItemList(); // invoke manually + + if (SP_CANVAS_ITEM_CLASS(sp_canvas_group_parent_class)->destroy) { + (* SP_CANVAS_ITEM_CLASS(sp_canvas_group_parent_class)->destroy)(object); + } +} + +void SPCanvasGroup::update(SPCanvasItem *item, Geom::Affine const &affine, unsigned int flags) +{ + SPCanvasGroup *group = SP_CANVAS_GROUP(item); + Geom::OptRect bounds; + + for (auto & item : group->items) { + SPCanvasItem *i = &item; + + sp_canvas_item_invoke_update (i, affine, flags); + + if ( (i->x2 > i->x1) && (i->y2 > i->y1) ) { + bounds.expandTo(Geom::Point(i->x1, i->y1)); + bounds.expandTo(Geom::Point(i->x2, i->y2)); + } + } + + if (bounds) { + item->x1 = bounds->min()[Geom::X]; + item->y1 = bounds->min()[Geom::Y]; + item->x2 = bounds->max()[Geom::X]; + item->y2 = bounds->max()[Geom::Y]; + } else { + // FIXME ? + item->x1 = item->x2 = item->y1 = item->y2 = 0; + } +} + +double SPCanvasGroup::point(SPCanvasItem *item, Geom::Point p, SPCanvasItem **actual_item) +{ + SPCanvasGroup *group = SP_CANVAS_GROUP(item); + double const x = p[Geom::X]; + double const y = p[Geom::Y]; + int x1 = (int)(x - item->canvas->_close_enough); + int y1 = (int)(y - item->canvas->_close_enough); + int x2 = (int)(x + item->canvas->_close_enough); + int y2 = (int)(y + item->canvas->_close_enough); + + double best = 0.0; + *actual_item = nullptr; + + double dist = 0.0; + for (auto & it : group->items) { + SPCanvasItem *child = ⁢ + + if ((child->x1 <= x2) && (child->y1 <= y2) && (child->x2 >= x1) && (child->y2 >= y1)) { + SPCanvasItem *point_item = nullptr; // cater for incomplete item implementations + + int pickable; + if (child->visible && child->pickable && SP_CANVAS_ITEM_GET_CLASS(child)->point) { + dist = sp_canvas_item_invoke_point(child, p, &point_item); + pickable = TRUE; + } else { + pickable = FALSE; + } + + // TODO: This metric should be improved, because in case of (partly) overlapping items we will now + // always select the last one that has been added to the group. We could instead select the one + // of which the center is the closest, for example. One can then move to the center + // of the item to be focused, and have that one selected. Of course this will only work if the + // centers are not coincident, but at least it's better than what we have now. + // See the extensive comment in Inkscape::SelTrans::_updateHandles() + if (pickable && point_item && ((int) (dist + 0.5) <= item->canvas->_close_enough)) { + best = dist; + *actual_item = point_item; + } + } + } + + return best; +} + +void SPCanvasGroup::render(SPCanvasItem *item, SPCanvasBuf *buf) +{ + SPCanvasGroup *group = SP_CANVAS_GROUP(item); + + for (auto & item : group->items) { + SPCanvasItem *child = &item; + if (child->visible) { + if ((child->x1 < buf->rect.right()) && + (child->y1 < buf->rect.bottom()) && + (child->x2 > buf->rect.left()) && + (child->y2 > buf->rect.top())) { + if (SP_CANVAS_ITEM_GET_CLASS(child)->render) { + SP_CANVAS_ITEM_GET_CLASS(child)->render(child, buf); + } + } + } + } +} + +void SPCanvasGroup::viewboxChanged(SPCanvasItem *item, Geom::IntRect const &new_area) +{ + SPCanvasGroup *group = SP_CANVAS_GROUP(item); + for (auto & item : group->items) { + SPCanvasItem *child = &item; + if (child->visible) { + if (SP_CANVAS_ITEM_GET_CLASS(child)->viewbox_changed) { + SP_CANVAS_ITEM_GET_CLASS(child)->viewbox_changed(child, new_area); + } + } + } +} + +void SPCanvasGroup::add(SPCanvasItem *item) +{ + g_object_ref(item); + g_object_ref_sink(item); + + items.push_back(*item); + + sp_canvas_item_request_update(item); +} + +void SPCanvasGroup::remove(SPCanvasItem *item) +{ + g_return_if_fail(item != nullptr); + + auto position = items.iterator_to(*item); + if (position != items.end()) { + items.erase(position); + } + + // Unparent the child + item->parent = nullptr; + g_object_unref(item); + +} + +G_DEFINE_TYPE(SPCanvas, sp_canvas, GTK_TYPE_WIDGET); + +static void sp_canvas_finalize(GObject *object) +{ + SPCanvas *canvas = SP_CANVAS(object); + +#if defined(HAVE_LIBLCMS2) + using S = decltype(canvas->_cms_key); + canvas->_cms_key.~S(); +#endif + + G_OBJECT_CLASS(sp_canvas_parent_class)->finalize(object); +} + +void sp_canvas_class_init(SPCanvasClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS(klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass); + + object_class->dispose = SPCanvas::dispose; + object_class->finalize = sp_canvas_finalize; + + widget_class->realize = SPCanvas::handle_realize; + widget_class->unrealize = SPCanvas::handle_unrealize; + widget_class->get_preferred_width = SPCanvas::handle_get_preferred_width; + widget_class->get_preferred_height = SPCanvas::handle_get_preferred_height; + widget_class->draw = SPCanvas::handle_draw; + widget_class->size_allocate = SPCanvas::handle_size_allocate; + widget_class->button_press_event = SPCanvas::handle_button; + widget_class->button_release_event = SPCanvas::handle_button; + widget_class->motion_notify_event = SPCanvas::handle_motion; + widget_class->scroll_event = SPCanvas::handle_scroll; + widget_class->key_press_event = SPCanvas::handle_key_event; + widget_class->key_release_event = SPCanvas::handle_key_event; + widget_class->enter_notify_event = SPCanvas::handle_crossing; + widget_class->leave_notify_event = SPCanvas::handle_crossing; + widget_class->focus_in_event = SPCanvas::handle_focus_in; + widget_class->focus_out_event = SPCanvas::handle_focus_out; +} + +static void sp_canvas_init(SPCanvas *canvas) +{ + gtk_widget_set_has_window (GTK_WIDGET (canvas), TRUE); + gtk_widget_set_can_focus (GTK_WIDGET (canvas), TRUE); + + canvas->_pick_event.type = GDK_LEAVE_NOTIFY; + canvas->_pick_event.crossing.x = 0; + canvas->_pick_event.crossing.y = 0; + + // Create the root item as a special case + canvas->_root = SP_CANVAS_ITEM(g_object_new(SP_TYPE_CANVAS_GROUP, nullptr)); + canvas->_root->canvas = canvas; + + g_object_ref (canvas->_root); + g_object_ref_sink (canvas->_root); + + canvas->_need_repick = TRUE; + + // See comment at in sp-canvas.h. + canvas->_gen_all_enter_events = false; + + canvas->_drawing_disabled = false; + + canvas->_backing_store = nullptr; + canvas->_surface_for_similar = nullptr; + canvas->_clean_region = cairo_region_create(); + canvas->_background = cairo_pattern_create_rgb(1, 1, 1); + canvas->_background_is_checkerboard = false; + + canvas->_forced_redraw_count = 0; + canvas->_forced_redraw_limit = -1; + canvas->_in_full_redraw = false; + // Split view controls + canvas->_spliter = Geom::OptIntRect(); + canvas->_spliter_area = Geom::OptIntRect(); + canvas->_spliter_control = Geom::OptIntRect(); + canvas->_spliter_top = Geom::OptIntRect(); + canvas->_spliter_bottom = Geom::OptIntRect(); + canvas->_spliter_left = Geom::OptIntRect(); + canvas->_spliter_right = Geom::OptIntRect(); + canvas->_spliter_control_pos = Geom::Point(); + canvas->_spliter_in_control_pos = Geom::Point(); + canvas->_xray_rect = Geom::OptIntRect(); + canvas->_split_value = 0.5; + canvas->_split_vertical = true; + canvas->_split_inverse = false; + canvas->_split_hover_vertical = false; + canvas->_split_hover_horizontal = false; + canvas->_split_hover = false; + canvas->_split_pressed = false; + canvas->_split_control_pressed = false; + canvas->_split_dragging = false; + canvas->_xray_radius = 100; + canvas->_xray = false; + canvas->_xray_orig = Geom::Point(); + canvas->_changecursor = 0; + canvas->_splits = 0; + canvas->_totalelapsed = 0; + canvas->_idle_time = g_get_monotonic_time(); + canvas->_is_dragging = false; + +#if defined(HAVE_LIBLCMS2) + canvas->_enable_cms_display_adj = false; + new (&canvas->_cms_key) Glib::ustring(""); +#endif // defined(HAVE_LIBLCMS2) +} + +void SPCanvas::shutdownTransients() +{ + // Reset the clean region + dirtyAll(); + + if (_grabbed_item) { + _grabbed_item = nullptr; + ungrab_default_client_pointer(); + } + removeIdle(); +} + +void SPCanvas::dispose(GObject *object) +{ + SPCanvas *canvas = SP_CANVAS(object); + + if (canvas->_root) { + g_object_unref (canvas->_root); + canvas->_root = nullptr; + } + if (canvas->_backing_store) { + cairo_surface_destroy(canvas->_backing_store); + canvas->_backing_store = nullptr; + } + if (canvas->_surface_for_similar) { + cairo_surface_destroy(canvas->_surface_for_similar); + canvas->_surface_for_similar = nullptr; + } + if (canvas->_clean_region) { + cairo_region_destroy(canvas->_clean_region); + canvas->_clean_region = nullptr; + } + if (canvas->_background) { + cairo_pattern_destroy(canvas->_background); + canvas->_background = nullptr; + } + + canvas->shutdownTransients(); + if (G_OBJECT_CLASS(sp_canvas_parent_class)->dispose) { + (* G_OBJECT_CLASS(sp_canvas_parent_class)->dispose)(object); + } +} + +namespace { + +void trackLatency(GdkEvent const *event) +{ + GdkEventLatencyTracker &tracker = GdkEventLatencyTracker::default_tracker(); + boost::optional<double> latency = tracker.process(event); + if (latency && *latency > 2.0) { + //g_warning("Event latency reached %f sec (%1.4f)", *latency, tracker.getSkew()); + } +} + +} // namespace + +GtkWidget *SPCanvas::createAA() +{ + SPCanvas *canvas = SP_CANVAS(g_object_new(SP_TYPE_CANVAS, nullptr)); + return GTK_WIDGET(canvas); +} + +void SPCanvas::handle_realize(GtkWidget *widget) +{ + GdkWindowAttr attributes; + GtkAllocation allocation; + attributes.window_type = GDK_WINDOW_CHILD; + gtk_widget_get_allocation (widget, &allocation); + attributes.x = allocation.x; + attributes.y = allocation.y; + attributes.width = allocation.width; + attributes.height = allocation.height; + attributes.wclass = GDK_INPUT_OUTPUT; + attributes.visual = gdk_screen_get_system_visual(gdk_screen_get_default()); + + attributes.event_mask = (gtk_widget_get_events (widget) | + GDK_EXPOSURE_MASK | + GDK_BUTTON_PRESS_MASK | + GDK_BUTTON_RELEASE_MASK | + GDK_POINTER_MOTION_MASK | + ( HAS_BROKEN_MOTION_HINTS ? + 0 : GDK_POINTER_MOTION_HINT_MASK ) | + GDK_PROXIMITY_IN_MASK | + GDK_PROXIMITY_OUT_MASK | + GDK_KEY_PRESS_MASK | + GDK_KEY_RELEASE_MASK | + GDK_ENTER_NOTIFY_MASK | + GDK_LEAVE_NOTIFY_MASK | + GDK_SCROLL_MASK | + GDK_SMOOTH_SCROLL_MASK | + GDK_FOCUS_CHANGE_MASK); + + gint attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL; + + GdkWindow *window = gdk_window_new (gtk_widget_get_parent_window (widget), &attributes, attributes_mask); + gtk_widget_set_window (widget, window); + gdk_window_set_user_data (window, widget); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/options/useextinput/value", true)) { + gtk_widget_set_events(widget, attributes.event_mask); + } + + gtk_widget_set_realized (widget, TRUE); +} + +void SPCanvas::handle_unrealize(GtkWidget *widget) +{ + SPCanvas *canvas = SP_CANVAS (widget); + + canvas->_current_item = nullptr; + canvas->_grabbed_item = nullptr; + canvas->_focused_item = nullptr; + + canvas->shutdownTransients(); + + if (GTK_WIDGET_CLASS(sp_canvas_parent_class)->unrealize) + (* GTK_WIDGET_CLASS(sp_canvas_parent_class)->unrealize)(widget); +} + +void SPCanvas::handle_get_preferred_width(GtkWidget *widget, gint *minimum_width, gint *natural_width) +{ + static_cast<void>(SP_CANVAS (widget)); + *minimum_width = 256; + *natural_width = 256; +} + +void SPCanvas::handle_get_preferred_height(GtkWidget *widget, gint *minimum_height, gint *natural_height) +{ + static_cast<void>(SP_CANVAS (widget)); + *minimum_height = 256; + *natural_height = 256; +} + +void SPCanvas::handle_size_allocate(GtkWidget *widget, GtkAllocation *allocation) +{ + SPCanvas *canvas = SP_CANVAS (widget); + // Allocation does not depend on device scale. + GtkAllocation old_allocation; + gtk_widget_get_allocation(widget, &old_allocation); + + // For HiDPI monitors. + canvas->_device_scale = gtk_widget_get_scale_factor( widget ); + + Geom::IntRect new_area = Geom::IntRect::from_xywh(canvas->_x0, canvas->_y0, + allocation->width, allocation->height); + + // Resize backing store. + cairo_surface_t *new_backing_store = nullptr; + if (canvas->_surface_for_similar != nullptr) { + + // Size in device pixels. Does not set device scale. + new_backing_store = + cairo_surface_create_similar_image(canvas->_surface_for_similar, + CAIRO_FORMAT_ARGB32, + allocation->width * canvas->_device_scale, + allocation->height * canvas->_device_scale); + } + if (new_backing_store == nullptr) { + + // Size in device pixels. Does not set device scale. + new_backing_store = + cairo_image_surface_create(CAIRO_FORMAT_ARGB32, + allocation->width * canvas->_device_scale, + allocation->height * canvas->_device_scale); + } + + // Set device scale + cairo_surface_set_device_scale(new_backing_store, canvas->_device_scale, canvas->_device_scale); + + if (canvas->_backing_store) { + cairo_t *cr = cairo_create(new_backing_store); + cairo_translate(cr, -canvas->_x0, -canvas->_y0); + cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE); + cairo_set_source(cr, canvas->_background); + cairo_paint(cr); + cairo_set_source_surface(cr, canvas->_backing_store, canvas->_x0, canvas->_y0); + cairo_paint(cr); + cairo_destroy(cr); + cairo_surface_destroy(canvas->_backing_store); + } + canvas->_backing_store = new_backing_store; + + // Clip the clean region to the new allocation + cairo_rectangle_int_t crect = { canvas->_x0, canvas->_y0, allocation->width, allocation->height }; + cairo_region_intersect_rectangle(canvas->_clean_region, &crect); + + gtk_widget_set_allocation (widget, allocation); + + if (SP_CANVAS_ITEM_GET_CLASS (canvas->_root)->viewbox_changed) + SP_CANVAS_ITEM_GET_CLASS (canvas->_root)->viewbox_changed (canvas->_root, new_area); + + if (gtk_widget_get_realized (widget)) { + gdk_window_move_resize (gtk_widget_get_window (widget), + allocation->x, allocation->y, + allocation->width, allocation->height); + } + // Schedule redraw of any newly exposed regions + canvas->_split_value = 0.5; + canvas->_spliter_control_pos = Geom::Point(); + canvas->requestFullRedraw(); +} + +int SPCanvas::emitEvent(GdkEvent *event) +{ + guint mask; + + if (_grabbed_item) { + switch (event->type) { + case GDK_ENTER_NOTIFY: + mask = GDK_ENTER_NOTIFY_MASK; + break; + case GDK_LEAVE_NOTIFY: + mask = GDK_LEAVE_NOTIFY_MASK; + break; + case GDK_MOTION_NOTIFY: + mask = GDK_POINTER_MOTION_MASK; + break; + case GDK_BUTTON_PRESS: + case GDK_2BUTTON_PRESS: + case GDK_3BUTTON_PRESS: + mask = GDK_BUTTON_PRESS_MASK; + break; + case GDK_BUTTON_RELEASE: + mask = GDK_BUTTON_RELEASE_MASK; + break; + case GDK_KEY_PRESS: + mask = GDK_KEY_PRESS_MASK; + break; + case GDK_KEY_RELEASE: + mask = GDK_KEY_RELEASE_MASK; + break; + case GDK_SCROLL: + mask = GDK_SCROLL_MASK; + mask |= GDK_SMOOTH_SCROLL_MASK; + break; + default: + mask = 0; + break; + } + + if (!(mask & _grabbed_event_mask)) return FALSE; + } + + // Convert to world coordinates -- we have two cases because of different + // offsets of the fields in the event structures. + + GdkEvent *ev = gdk_event_copy(event); + switch (ev->type) { + case GDK_ENTER_NOTIFY: + case GDK_LEAVE_NOTIFY: + ev->crossing.x += _x0; + ev->crossing.y += _y0; + break; + case GDK_MOTION_NOTIFY: + case GDK_BUTTON_PRESS: + case GDK_2BUTTON_PRESS: + case GDK_3BUTTON_PRESS: + case GDK_BUTTON_RELEASE: + ev->motion.x += _x0; + ev->motion.y += _y0; + break; + default: + break; + } + // Block Undo and Redo while we drag /anything/ + if(event->type == GDK_BUTTON_PRESS && event->button.button == 1) + _is_dragging = true; + else if(event->type == GDK_BUTTON_RELEASE) + _is_dragging = false; + + // Choose where we send the event + + // canvas->current_item becomes NULL in some cases under Win32 + // (e.g. if the pointer leaves the window). So this is a hack that + // Lauris applied to SP to get around the problem. + // + SPCanvasItem* item = nullptr; + if (_grabbed_item && !is_descendant(_current_item, _grabbed_item)) { + item = _grabbed_item; + } else { + item = _current_item; + } + + if (_focused_item && + ((event->type == GDK_KEY_PRESS) || + (event->type == GDK_KEY_RELEASE) || + (event->type == GDK_FOCUS_CHANGE))) { + item = _focused_item; + } + + // The event is propagated up the hierarchy (for if someone connected to + // a group instead of a leaf event), and emission is stopped if a + // handler returns TRUE, just like for GtkWidget events. + + gint finished = FALSE; + + while (item && !finished) { + g_object_ref (item); + g_signal_emit (G_OBJECT (item), item_signals[ITEM_EVENT], 0, ev, &finished); + SPCanvasItem *parent = item->parent; + g_object_unref (item); + item = parent; + } + + gdk_event_free(ev); + + return finished; +} + +int SPCanvas::pickCurrentItem(GdkEvent *event) +{ + int button_down = 0; + + if (!_root) // canvas may have already be destroyed by closing desktop during interrupted display! + return FALSE; + + int retval = FALSE; + + if (_gen_all_enter_events == false) { + // If a button is down, we'll perform enter and leave events on the + // current item, but not enter on any other item. This is more or + // less like X pointer grabbing for canvas items. + // + button_down = _state & (GDK_BUTTON1_MASK | GDK_BUTTON2_MASK | + GDK_BUTTON3_MASK | GDK_BUTTON4_MASK | GDK_BUTTON5_MASK); + + if (!button_down) _left_grabbed_item = FALSE; + } + + // Save the event in the canvas. This is used to synthesize enter and + // leave events in case the current item changes. It is also used to + // re-pick the current item if the current one gets deleted. Also, + // synthesize an enter event. + + if (event != &_pick_event) { + if ((event->type == GDK_MOTION_NOTIFY) || (event->type == GDK_BUTTON_RELEASE)) { + // these fields have the same offsets in both types of events + + _pick_event.crossing.type = GDK_ENTER_NOTIFY; + _pick_event.crossing.window = event->motion.window; + _pick_event.crossing.send_event = event->motion.send_event; + _pick_event.crossing.subwindow = nullptr; + _pick_event.crossing.x = event->motion.x; + _pick_event.crossing.y = event->motion.y; + _pick_event.crossing.mode = GDK_CROSSING_NORMAL; + _pick_event.crossing.detail = GDK_NOTIFY_NONLINEAR; + _pick_event.crossing.focus = FALSE; + _pick_event.crossing.state = event->motion.state; + + // these fields don't have the same offsets in both types of events + + if (event->type == GDK_MOTION_NOTIFY) { + _pick_event.crossing.x_root = event->motion.x_root; + _pick_event.crossing.y_root = event->motion.y_root; + } else { + _pick_event.crossing.x_root = event->button.x_root; + _pick_event.crossing.y_root = event->button.y_root; + } + } else { + _pick_event = *event; + } + } + + // Don't do anything else if this is a recursive call + if (_in_repick) { + return retval; + } + + // LeaveNotify means that there is no current item, so we don't look for one + if (_pick_event.type != GDK_LEAVE_NOTIFY) { + // these fields don't have the same offsets in both types of events + double x, y; + + if (_pick_event.type == GDK_ENTER_NOTIFY) { + x = _pick_event.crossing.x; + y = _pick_event.crossing.y; + } else { + x = _pick_event.motion.x; + y = _pick_event.motion.y; + } + + // world coords + x += _x0; + y += _y0; + + // find the closest item + if (_root->visible) { + sp_canvas_item_invoke_point (_root, Geom::Point(x, y), &_new_current_item); + } else { + _new_current_item = nullptr; + } + } else { + _new_current_item = nullptr; + } + + if ((_new_current_item == _current_item) && !_left_grabbed_item) { + return retval; // current item did not change + } + + // Synthesize events for old and new current items + + if ((_new_current_item != _current_item) && + _current_item != nullptr && !_left_grabbed_item) + { + GdkEvent new_event; + + new_event = _pick_event; + new_event.type = GDK_LEAVE_NOTIFY; + + new_event.crossing.detail = GDK_NOTIFY_ANCESTOR; + new_event.crossing.subwindow = nullptr; + _in_repick = TRUE; + retval = emitEvent(&new_event); + _in_repick = FALSE; + } + + if (_gen_all_enter_events == false) { + // new_current_item may have been set to NULL during the call to + // emitEvent() above + if ((_new_current_item != _current_item) && button_down) { + _left_grabbed_item = TRUE; + return retval; + } + } + + // Handle the rest of cases + _left_grabbed_item = FALSE; + _current_item = _new_current_item; + + if (_current_item != nullptr) { + GdkEvent new_event; + + new_event = _pick_event; + new_event.type = GDK_ENTER_NOTIFY; + new_event.crossing.detail = GDK_NOTIFY_ANCESTOR; + new_event.crossing.subwindow = nullptr; + retval = emitEvent(&new_event); + } + + return retval; +} + +gint SPCanvas::handle_doubleclick(GtkWidget *widget, GdkEventButton *event) +{ + SPCanvas *canvas = SP_CANVAS(widget); + // Maybe we want to use double click on canvas so retain here + return 0; +} + +gint SPCanvas::handle_button(GtkWidget *widget, GdkEventButton *event) +{ + SPCanvas *canvas = SP_CANVAS (widget); + + int retval = FALSE; + + // dispatch normally regardless of the event's window if an item + // has a pointer grab in effect + if (!canvas->_grabbed_item && + event->window != getWindow(canvas)) + return retval; + + int mask; + switch (event->button) { + case 1: + mask = GDK_BUTTON1_MASK; + break; + case 2: + mask = GDK_BUTTON2_MASK; + break; + case 3: + mask = GDK_BUTTON3_MASK; + break; + case 4: + mask = GDK_BUTTON4_MASK; + break; + case 5: + mask = GDK_BUTTON5_MASK; + break; + default: + mask = 0; + } + static unsigned next_canvas_doubleclick = 0; + + switch (event->type) { + case GDK_BUTTON_PRESS: + // Pick the current item as if the button were not pressed, and + // then process the event. + next_canvas_doubleclick = 0; + if (!canvas->_split_hover) { + canvas->_state = event->state; + canvas->pickCurrentItem(reinterpret_cast<GdkEvent *>(event)); + canvas->_state ^= mask; + retval = canvas->emitEvent((GdkEvent *)event); + } else { + canvas->_split_pressed = true; + Geom::IntPoint cursor_pos = Geom::IntPoint(event->x, event->y); + canvas->_spliter_in_control_pos = cursor_pos - (*canvas->_spliter_control).midpoint(); + if (canvas->_spliter && canvas->_spliter_control.contains(cursor_pos) && !canvas->_is_dragging) { + canvas->_split_control_pressed = true; + } + retval = TRUE; + } + break; + case GDK_2BUTTON_PRESS: + // Pick the current item as if the button were not pressed, and + // then process the event. + next_canvas_doubleclick = reinterpret_cast<GdkEvent *>(event)->button.button; + + if (!canvas->_split_hover) { + canvas->_state = event->state; + canvas->pickCurrentItem(reinterpret_cast<GdkEvent *>(event)); + canvas->_state ^= mask; + retval = canvas->emitEvent((GdkEvent *)event); + } else { + canvas->_split_pressed = true; + retval = TRUE; + } + break; + case GDK_3BUTTON_PRESS: + // Pick the current item as if the button were not pressed, and + // then process the event. + if (!canvas->_split_hover) { + canvas->_state = event->state; + canvas->pickCurrentItem(reinterpret_cast<GdkEvent *>(event)); + canvas->_state ^= mask; + retval = canvas->emitEvent((GdkEvent *)event); + } else { + canvas->_split_pressed = true; + retval = TRUE; + } + break; + + case GDK_BUTTON_RELEASE: + // Process the event as if the button were pressed, then repick + // after the button has been released + canvas->_split_pressed = false; + if (next_canvas_doubleclick) { + GdkEventButton *event2 = reinterpret_cast<GdkEventButton *>(event); + handle_doubleclick(GTK_WIDGET(canvas), event2); + } + if (canvas->_split_hover) { + retval = TRUE; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool spliter_clicked = false; + bool reset = false; + if (!canvas->_split_dragging) { + GtkAllocation allocation; + gtk_widget_get_allocation(GTK_WIDGET(canvas), &allocation); + Geom::Point pos = canvas->_spliter_control_pos; + double value = canvas->_split_vertical ? 1 / (allocation.height / (double)pos[Geom::Y]) + : 1 / (allocation.width / (double)pos[Geom::X]); + if (canvas->_split_hover_vertical) { + canvas->_split_inverse = !canvas->_split_inverse; + spliter_clicked = true; + reset = canvas->_split_vertical ? true : false; + if (reset) { + canvas->_split_value = value; + } + canvas->_split_vertical = false; + } else if (canvas->_split_hover_horizontal) { + canvas->_split_inverse = !canvas->_split_inverse; + spliter_clicked = true; + reset = !canvas->_split_vertical ? true : false; + if (reset) { + canvas->_split_value = value; + } + canvas->_split_vertical = true; + } + if (spliter_clicked) { + canvas->requestFullRedraw(); + } + } + canvas->_split_control_pressed = false; + canvas->_split_dragging = false; + } else { + canvas->_state = event->state; + retval = canvas->emitEvent((GdkEvent *)event); + event->state ^= mask; + canvas->_state = event->state; + canvas->pickCurrentItem(reinterpret_cast<GdkEvent *>(event)); + event->state ^= mask; + } + break; + + default: + g_assert_not_reached (); + } + + return retval; +} + +gint SPCanvas::handle_scroll(GtkWidget *widget, GdkEventScroll *event) +{ + return SP_CANVAS(widget)->emitEvent(reinterpret_cast<GdkEvent *>(event)); +} + +static inline void request_motions(GdkWindow *w, GdkEventMotion *event) { + gdk_window_get_device_position(w, + gdk_event_get_device((GdkEvent *)(event)), + nullptr, nullptr, nullptr); + gdk_event_request_motions(event); +} + +void SPCanvas::set_cursor(GtkWidget *widget) +{ + SPCanvas *canvas = SP_CANVAS(widget); + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + GdkDisplay *display = gdk_display_get_default(); + GdkCursor *cursor = nullptr; + if (canvas->_split_hover_vertical) { + if (canvas->_changecursor != 1) { + cursor = gdk_cursor_new_from_name(display, "pointer"); + gdk_window_set_cursor(gtk_widget_get_window(widget), cursor); + g_object_unref(cursor); + canvas->paintSpliter(); + canvas->_changecursor = 1; + } + } else if (canvas->_split_hover_horizontal) { + if (canvas->_changecursor != 2) { + cursor = gdk_cursor_new_from_name(display, "pointer"); + gdk_window_set_cursor(gtk_widget_get_window(widget), cursor); + g_object_unref(cursor); + canvas->paintSpliter(); + canvas->_changecursor = 2; + } + } else if (canvas->_split_hover) { + if (canvas->_changecursor != 3) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (_split_vertical) { + cursor = gdk_cursor_new_from_name(display, "ew-resize"); + } else { + cursor = gdk_cursor_new_from_name(display, "ns-resize"); + } + gdk_window_set_cursor(gtk_widget_get_window(widget), cursor); + g_object_unref(cursor); + canvas->paintSpliter(); + canvas->_changecursor = 3; + } + } else { + if (desktop && + desktop->event_context && + !canvas->_split_pressed && + (canvas->_changecursor != 0 && canvas->_changecursor != 4)) + { + desktop->event_context->sp_event_context_update_cursor(); + canvas->paintSpliter(); + canvas->_changecursor = 4; + } + } +} +int SPCanvas::handle_motion(GtkWidget *widget, GdkEventMotion *event) +{ + int status; + SPCanvas *canvas = SP_CANVAS (widget); + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + trackLatency((GdkEvent *)event); + + if (event->window != getWindow(canvas)) { + return FALSE; + } + + if (canvas->_root == nullptr) // canvas being deleted + return FALSE; + + Geom::IntPoint cursor_pos = Geom::IntPoint(event->x, event->y); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (desktop && desktop->splitMode()) { + if (canvas->_spliter && + ((*canvas->_spliter).contains(cursor_pos) || canvas->_spliter_control.contains(cursor_pos)) && + !canvas->_is_dragging) { + canvas->_split_hover = true; + } else { + canvas->_split_hover = false; + } + if (canvas->_spliter_left && canvas->_spliter_right && + ((*canvas->_spliter_left).contains(cursor_pos) || (*canvas->_spliter_right).contains(cursor_pos)) && + !canvas->_is_dragging) { + canvas->_split_hover_horizontal = true; + } else { + canvas->_split_hover_horizontal = false; + } + if (!canvas->_split_hover_horizontal && canvas->_spliter_top && canvas->_spliter_bottom && + ((*canvas->_spliter_top).contains(cursor_pos) || (*canvas->_spliter_bottom).contains(cursor_pos)) && + !canvas->_is_dragging) { + canvas->_split_hover_vertical = true; + } else { + canvas->_split_hover_vertical = false; + } + + canvas->set_cursor(widget); + } + if (canvas->_split_pressed && desktop && desktop->event_context && desktop->splitMode()) { + GtkAllocation allocation; + canvas->_split_dragging = true; + gtk_widget_get_allocation(GTK_WIDGET(canvas), &allocation); + double hide_horiz = 1 / (allocation.width / (double)cursor_pos[Geom::X]); + double hide_vert = 1 / (allocation.height / (double)cursor_pos[Geom::Y]); + double value = canvas->_split_vertical ? hide_horiz : hide_vert; + if (hide_horiz < 0.03 || hide_horiz > 0.97 || hide_vert < 0.03 || hide_vert > 0.97) { + if (desktop && desktop->event_context) { + desktop->event_context->sp_event_context_update_cursor(); + desktop->toggleSplitMode(); + sp_reset_spliter(canvas); + } + } else { + canvas->_split_value = value; + if (canvas->_split_control_pressed && !canvas->_is_dragging) { + canvas->_spliter_control_pos = cursor_pos - canvas->_spliter_in_control_pos; + } + } + canvas->requestFullRedraw(); + status = 1; + } else { + if (desktop && desktop->event_context && desktop->xrayMode()) { + sp_reset_spliter(canvas); + Geom::Point prev_orig = canvas->_xray_orig; + canvas->_xray_orig = desktop->point(true); + canvas->_xray_orig *= desktop->current_zoom(); + if (!SP_ACTIVE_DOCUMENT->is_yaxisdown()) { + canvas->_xray_orig[Geom::Y] *= -1.0; + } + canvas->_xray = true; + if (canvas->_xray_orig[Geom::X] != Geom::infinity()) { + if (canvas->_xray_rect) { + canvas->dirtyRect(*canvas->_xray_rect); + canvas->_xray_rect = Geom::OptIntRect(); + } + canvas->addIdle(); + } + status = 1; + } else { + if (canvas->_xray_rect) { + canvas->dirtyRect(*canvas->_xray_rect); + canvas->_xray_rect = Geom::OptIntRect(); + } + canvas->_xray = false; + } + canvas->_state = event->state; + canvas->pickCurrentItem(reinterpret_cast<GdkEvent *>(event)); + status = canvas->emitEvent(reinterpret_cast<GdkEvent *>(event)); + if (event->is_hint) { + request_motions(gtk_widget_get_window(widget), event); + } + } + + if (desktop) { + SPCanvasArena *arena = SP_CANVAS_ARENA(desktop->drawing); + if (desktop->splitMode()) { + bool contains = canvas->_spliter_area.contains(cursor_pos); + bool setoutline = canvas->_split_inverse ? !contains : contains; + arena->drawing.setOutlineSensitive(setoutline); + } else if (canvas->_xray) { + arena->drawing.setOutlineSensitive(true); + } else { + arena->drawing.setOutlineSensitive(false); + } + } + return status; +} + +void SPCanvas::paintSingleBuffer(Geom::IntRect const &paint_rect, Geom::IntRect const &canvas_rect, int /*sw*/) +{ + + // Prevent crash if paintSingleBuffer is called before _backing_store is + // initialized. + if (_backing_store == nullptr) + return; + + SPCanvasBuf buf; + buf.buf = nullptr; + buf.buf_rowstride = 0; + buf.rect = paint_rect; + buf.canvas_rect = canvas_rect; + buf.device_scale = _device_scale; + buf.is_empty = true; + + // Make sure the following code does not go outside of _backing_store's data + // FIXME for device_scale. + assert(cairo_image_surface_get_format(_backing_store) == CAIRO_FORMAT_ARGB32); + assert(paint_rect.left() - _x0 >= 0); + assert(paint_rect.top() - _y0 >= 0); + assert(paint_rect.right() - _x0 <= cairo_image_surface_get_width(_backing_store)); + assert(paint_rect.bottom() - _y0 <= cairo_image_surface_get_height(_backing_store)); + + // Create a temporary surface that draws directly to _backing_store + cairo_surface_flush(_backing_store); + // cairo_surface_write_to_png( _backing_store, "debug0.png" ); + unsigned char *data = cairo_image_surface_get_data(_backing_store); + int stride = cairo_image_surface_get_stride(_backing_store); + + // Check we are using correct device scale + double x_scale = 0; + double y_scale = 0; + cairo_surface_get_device_scale(_backing_store, &x_scale, &y_scale); + assert (_device_scale == (int)x_scale); + assert (_device_scale == (int)y_scale); + + // Move to the right row + data += stride * (paint_rect.top() - _y0) * (int)y_scale; + // Move to the right pixel inside of that row + data += 4 * (paint_rect.left() - _x0) * (int)x_scale; + cairo_surface_t *imgs = + cairo_image_surface_create_for_data(data, CAIRO_FORMAT_ARGB32, + paint_rect.width() * _device_scale, + paint_rect.height() * _device_scale, + stride); + cairo_surface_set_device_scale(imgs, _device_scale, _device_scale); + + buf.ct = cairo_create(imgs); + cairo_save(buf.ct); + cairo_translate(buf.ct, -paint_rect.left(), -paint_rect.top()); + cairo_set_source(buf.ct, _background); + cairo_set_operator(buf.ct, CAIRO_OPERATOR_SOURCE); + cairo_paint(buf.ct); + cairo_restore(buf.ct); + // cairo_surface_write_to_png( imgs, "debug1.png" ); + + if (_root->visible) { + SP_CANVAS_ITEM_GET_CLASS(_root)->render(_root, &buf); + } + + // cairo_surface_write_to_png( imgs, "debug2.png" ); + + // output to X + cairo_destroy(buf.ct); + +#if defined(HAVE_LIBLCMS2) + if (_enable_cms_display_adj) { + cmsHTRANSFORM transf = nullptr; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool fromDisplay = prefs->getBool( "/options/displayprofile/from_display"); + if ( fromDisplay ) { + transf = Inkscape::CMSSystem::getDisplayPer(_cms_key); + } else { + transf = Inkscape::CMSSystem::getDisplayTransform(); + } + + if (transf) { + cairo_surface_flush(imgs); + unsigned char *px = cairo_image_surface_get_data(imgs); + int stride = cairo_image_surface_get_stride(imgs); + for (int i=0; i<paint_rect.height(); ++i) { + unsigned char *row = px + i*stride; + Inkscape::CMSSystem::doTransform(transf, row, row, paint_rect.width()); + } + cairo_surface_mark_dirty(imgs); + } + } +#endif // defined(HAVE_LIBLCMS2) + + cairo_surface_mark_dirty(_backing_store); + // cairo_surface_write_to_png( _backing_store, "debug3.png" ); + + // Mark the painted rectangle clean + markRect(paint_rect, 0); + + cairo_surface_destroy(imgs); + + gtk_widget_queue_draw_area(GTK_WIDGET(this), paint_rect.left() -_x0, paint_rect.top() - _y0, + paint_rect.width(), paint_rect.height()); +} + +void SPCanvas::paintXRayBuffer(Geom::IntRect const &paint_rect, Geom::IntRect const &canvas_rect) +{ + + // Prevent crash if paintSingleBuffer is called before _backing_store is + // initialized. + if (_backing_store == nullptr) + return; + if (!canvas_rect.contains(paint_rect) && !canvas_rect.intersects(paint_rect)) { + return; + } + Geom::IntRect rect_moved = Geom::IntRect::from_xywh(_x0, _y0, _x0 + paint_rect.width(), _y0 + paint_rect.height()); + SPCanvasBuf buf; + buf.buf = nullptr; + buf.buf_rowstride = 0; + buf.rect = paint_rect; + buf.canvas_rect = canvas_rect; + buf.device_scale = _device_scale; + buf.is_empty = true; + // Make sure the following code does not go outside of _backing_store's data + // FIXME for device_scale. + assert(cairo_image_surface_get_format(_backing_store) == CAIRO_FORMAT_ARGB32); + cairo_surface_t *copy_backing = cairo_surface_create_similar_image(_backing_store, CAIRO_FORMAT_ARGB32, + paint_rect.width(), paint_rect.height()); + buf.ct = cairo_create(copy_backing); + cairo_t *result = cairo_create(_backing_store); + cairo_translate(result, -_x0, -_y0); + cairo_save(buf.ct); + cairo_set_source_rgba(buf.ct, 1, 1, 1, 0); + cairo_fill(buf.ct); + cairo_arc(buf.ct, _xray_radius, _xray_radius, _xray_radius, 0, 2 * M_PI); + cairo_clip(buf.ct); + cairo_paint(buf.ct); + cairo_translate(buf.ct, -paint_rect.left(), -paint_rect.top()); + cairo_set_source(buf.ct, _background); + cairo_set_operator(buf.ct, CAIRO_OPERATOR_SOURCE); + cairo_paint(buf.ct); + cairo_translate(buf.ct, paint_rect.left(), paint_rect.top()); + // cairo_surface_write_to_png( copy_backing, "debug1.png" ); + + + + if (_root->visible) { + SP_CANVAS_ITEM_GET_CLASS(_root)->render(_root, &buf); + } + cairo_restore(buf.ct); + cairo_arc(buf.ct, _xray_radius, _xray_radius, _xray_radius, 0, 2 * M_PI); + cairo_clip(buf.ct); + cairo_set_operator(buf.ct, CAIRO_OPERATOR_DEST_IN); + cairo_paint(buf.ct); + // cairo_surface_write_to_png( copy_backing, "debug2.png" ); + + // output to X + cairo_arc(buf.ct, _xray_radius, _xray_radius, _xray_radius, 0, 2 * M_PI); + + cairo_set_source_surface(result, copy_backing, paint_rect.left(), paint_rect.top()); + cairo_set_operator(buf.ct, CAIRO_OPERATOR_IN); + cairo_paint(result); + cairo_surface_destroy(copy_backing); + cairo_destroy(buf.ct); + cairo_destroy(result); + + // cairo_surface_write_to_png( _backing_store, "debug3.png" ); + cairo_surface_mark_dirty(_backing_store); + // Mark the painted rectangle un-clean to remove old x-ray when mouse change position + _xray_rect = paint_rect; + gtk_widget_queue_draw_area(GTK_WIDGET(this), paint_rect.left() - _x0, paint_rect.top() - _y0, paint_rect.width(), + paint_rect.height()); +} + +void SPCanvas::paintSpliter() +{ + // Prevent crash if paintSingleBuffer is called before _backing_store is + // initialized. + if (_backing_store == nullptr) + return; + // Todo: scale for HiDPI screens + SPCanvas *canvas = SP_CANVAS(this); + double ds = canvas->_device_scale; + Geom::IntRect linerect = (*canvas->_spliter); + Geom::IntPoint c0 = Geom::IntPoint(linerect.corner(0)); + Geom::IntPoint c1 = Geom::IntPoint(linerect.corner(1)); + Geom::IntPoint c2 = Geom::IntPoint(linerect.corner(2)); + Geom::IntPoint c3 = Geom::IntPoint(linerect.corner(3)); + // We need to draw the line in middle of pixel + // https://developer.gnome.org/gtkmm-tutorial/stable/sec-cairo-drawing-lines.html.en:17.2.3 + double gapx = _split_vertical ? 0.5 : 0; + double gapy = _split_vertical ? 0 : 0.5; + Geom::Point start = _split_vertical ? Geom::middle_point(c0, c1) : Geom::middle_point(c0, c3); + Geom::Point end = _split_vertical ? Geom::middle_point(c2, c3) : Geom::middle_point(c1, c2); + Geom::Point middle = Geom::middle_point(start, end); + if (canvas->_spliter_control_pos != Geom::Point()) { + middle[Geom::X] = _split_vertical ? middle[Geom::X] : canvas->_spliter_control_pos[Geom::X]; + middle[Geom::Y] = _split_vertical ? canvas->_spliter_control_pos[Geom::Y] : middle[Geom::Y]; + } + canvas->_spliter_control_pos = middle; + canvas->_spliter_control = Geom::OptIntRect(Geom::IntPoint(int(middle[0] - (25 * ds)), int(middle[1] - (25 * ds))), + Geom::IntPoint(int(middle[0] + (25 * ds)), int(middle[1] + (25 * ds)))); + canvas->_spliter_top = Geom::OptIntRect(Geom::IntPoint(int(middle[0] - (25 * ds)), int(middle[1] - (25 * ds))), + Geom::IntPoint(int(middle[0] + (25 * ds)), int(middle[1] - (10 * ds)))); + canvas->_spliter_bottom = Geom::OptIntRect(Geom::IntPoint(int(middle[0] - (25 * ds)), int(middle[1] + (25 * ds))), + Geom::IntPoint(int(middle[0] + (25 * ds)), int(middle[1] + (10 * ds)))); + canvas->_spliter_left = Geom::OptIntRect(Geom::IntPoint(int(middle[0] - (25 * ds)), int(middle[1] - (10 * ds))), + Geom::IntPoint(int(middle[0]), int(middle[1] + (10 * ds)))); + canvas->_spliter_right = Geom::OptIntRect(Geom::IntPoint(int(middle[0] + (25 * ds)), int(middle[1] + (10 * ds))), + Geom::IntPoint(int(middle[0]), int(middle[1] - (10 * ds)))); + cairo_t *ct = cairo_create(_backing_store); + cairo_set_antialias(ct, CAIRO_ANTIALIAS_BEST); + cairo_set_line_width(ct, 1.0 * ds); + cairo_line_to(ct, start[0] + gapx, start[1] + gapy); + cairo_line_to(ct, end[0] + gapx, end[1] + gapy); + cairo_stroke_preserve(ct); + if (canvas->_split_hover || canvas->_split_pressed) { + cairo_set_source_rgba(ct, 0.15, 0.15, 0.15, 1); + } else { + cairo_set_source_rgba(ct, 0.3, 0.3, 0.3, 1); + } + cairo_stroke(ct); + /* + Get by: https://gitlab.com/snippets/1777221 + M 40,19.999997 C 39.999998,8.9543032 31.045694,0 20,0 8.9543062,0 1.6568541e-6,8.9543032 0,19.999997 + 0,31.045692 8.954305,39.999997 20,39.999997 31.045695,39.999997 40,31.045692 40,19.999997 Z M + 11.109859,15.230724 2.8492384,19.999997 11.109861,24.769269 Z M 29.249158,15.230724 + 37.509779,19.999997 29.249158,24.769269 Z M 15.230728,29.03051 20,37.29113 24.769272,29.030509 Z + M 15.230728,10.891209 20,2.630586 24.769272,10.891209 Z */ + double updwidth = _split_vertical ? 0 : linerect.width(); + double updheight = _split_vertical ? linerect.height() : 0; + cairo_translate(ct, middle[0] - (20 * ds), middle[1] - (20 * ds)); + cairo_scale(ct, ds, ds); + cairo_move_to(ct, 40, 19.999997); + cairo_curve_to(ct, 39.999998, 8.9543032, 31.045694, 0, 20, 0); + cairo_curve_to(ct, 8.9543062, 0, 0, 8.9543032, 0, 19.999997); + cairo_curve_to(ct, 0, 31.045692, 8.954305, 39.999997, 20, 39.999997); + cairo_curve_to(ct, 31.045695, 39.999997, 40, 31.045692, 40, 19.999997); + cairo_close_path(ct); + cairo_fill(ct); + cairo_move_to(ct, 15.230728, 10.891209); + cairo_line_to(ct, 20, 2.630586); + cairo_line_to(ct, 24.769272, 10.891209); + cairo_close_path(ct); + if (canvas->_split_hover_vertical) { + cairo_set_source_rgba(ct, 0.90, 0.90, 0.90, 1); + } else { + cairo_set_source_rgba(ct, 0.6, 0.6, 0.6, 1); + } + cairo_fill(ct); + cairo_move_to(ct, 15.230728, 29.03051); + cairo_line_to(ct, 20, 37.29113); + cairo_line_to(ct, 24.769272, 29.030509); + cairo_close_path(ct); + cairo_fill(ct); + cairo_move_to(ct, 11.109859, 15.230724); + cairo_line_to(ct, 2.8492384, 19.999997); + cairo_line_to(ct, 11.109861, 24.769269); + cairo_close_path(ct); + if (canvas->_split_hover_horizontal) { + cairo_set_source_rgba(ct, 0.90, 0.90, 0.90, 1); + } else { + cairo_set_source_rgba(ct, 0.6, 0.6, 0.6, 1); + } + cairo_fill(ct); + cairo_move_to(ct, 29.249158, 15.230724); + cairo_line_to(ct, 37.509779, 19.999997); + cairo_line_to(ct, 29.249158, 24.769269); + cairo_close_path(ct); + cairo_fill(ct); + cairo_scale(ct, 1 / ds, 1 / ds); + cairo_translate(ct, -middle[0] - (20 * ds), -middle[1] - (20 * ds)); + cairo_restore(ct); + cairo_destroy(ct); + gtk_widget_queue_draw_area(GTK_WIDGET(this), start[0] - (21 * ds), start[1] - (21 * ds), updwidth + (42 * ds), + updheight + (42 * ds)); +} + +struct PaintRectSetup { + Geom::IntRect canvas_rect; + gint64 start_time; + int max_pixels; + Geom::Point mouse_loc; +}; + +int SPCanvas::paintRectInternal(PaintRectSetup const *setup, Geom::IntRect const &this_rect) +{ + gint64 now = g_get_monotonic_time(); + gint64 elapsed = now - setup->start_time; + + // Allow only very fast buffers to be run together; + // as soon as the total redraw time exceeds 1ms, cancel; + // this returns control to the idle loop and allows Inkscape to process user input + // (potentially interrupting the redraw); as soon as Inkscape has some more idle time, + if (elapsed > 1000) { + + // Interrupting redraw isn't always good. + // For example, when you drag one node of a big path, only the buffer containing + // the mouse cursor will be redrawn again and again, and the rest of the path + // will remain stale because Inkscape never has enough idle time to redraw all + // of the screen. To work around this, such operations set a forced_redraw_limit > 0. + // If this limit is set, and if we have aborted redraw more times than is allowed, + // interrupting is blocked and we're forced to redraw full screen once + // (after which we can again interrupt forced_redraw_limit times). + if (_forced_redraw_limit < 0 || + _forced_redraw_count < _forced_redraw_limit) { + + if (_forced_redraw_limit != -1) { + _forced_redraw_count++; + } + return false; + } + _forced_redraw_count = 0; + } + + // Find the optimal buffer dimensions + int bw = this_rect.width(); + int bh = this_rect.height(); + // we dont want to stop the idle process if the area is empty + if ((bw < 1) || (bh < 1)) + return 1; + + if (bw * bh < setup->max_pixels) { + // We are small enough + /* + GdkRectangle r; + r.x = this_rect.x0 - setup->canvas->x0; + r.y = this_rect.y0 - setup->canvas->y0; + r.width = this_rect.x1 - this_rect.x0; + r.height = this_rect.y1 - this_rect.y0; + + GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(setup->canvas)); + gdk_window_begin_paint_rect(window, &r); + */ + + paintSingleBuffer(this_rect, setup->canvas_rect, bw); + _splits++; + //gdk_window_end_paint(window); + return 1; + } + + Geom::IntRect lo, hi; + +/* +This test determines the redraw strategy: + +bw < bh (strips mode) splits across the smaller dimension of the rect and therefore (on +horizontally-stretched windows) results in redrawing in horizontal strips (from cursor point, in +both directions if the cursor is in the middle). This is traditional for Inkscape since old days, +and seems to be faster for drawings with many smaller objects at zoom-out. + +bw > bh (chunks mode) splits across the larger dimension of the rect and therefore paints in +almost-square chunks, again from the cursor point. It's sometimes faster for drawings with few slow +(e.g. blurred) objects crossing the entire screen. It also appears to be somewhat psychologically +faster. + +The default for now is the strips mode. +*/ + if (bw < bh || bh < 2 * TILE_SIZE) { + int mid = this_rect[Geom::X].middle(); + // Make sure that mid lies on a tile boundary + mid = (mid / TILE_SIZE) * TILE_SIZE; + + lo = Geom::IntRect(this_rect.left(), this_rect.top(), mid, this_rect.bottom()); + hi = Geom::IntRect(mid, this_rect.top(), this_rect.right(), this_rect.bottom()); + + if (setup->mouse_loc[Geom::X] < mid) { + // Always paint towards the mouse first + return paintRectInternal(setup, lo) + && paintRectInternal(setup, hi); + } else { + return paintRectInternal(setup, hi) + && paintRectInternal(setup, lo); + } + } else { + int mid = this_rect[Geom::Y].middle(); + // Make sure that mid lies on a tile boundary + mid = (mid / TILE_SIZE) * TILE_SIZE; + + lo = Geom::IntRect(this_rect.left(), this_rect.top(), this_rect.right(), mid); + hi = Geom::IntRect(this_rect.left(), mid, this_rect.right(), this_rect.bottom()); + + if (setup->mouse_loc[Geom::Y] < mid) { + // Always paint towards the mouse first + return paintRectInternal(setup, lo) + && paintRectInternal(setup, hi); + } else { + return paintRectInternal(setup, hi) + && paintRectInternal(setup, lo); + } + } +} + + +bool SPCanvas::paintRect(int xx0, int yy0, int xx1, int yy1) +{ + GtkAllocation allocation; + g_return_val_if_fail (!_need_update, false); + + gtk_widget_get_allocation(GTK_WIDGET(this), &allocation); + + // Find window rectangle in 'world coordinates'. + Geom::IntRect canvas_rect = Geom::IntRect::from_xywh(_x0, _y0, + allocation.width, allocation.height); + Geom::IntRect paint_rect(xx0, yy0, xx1, yy1); + + Geom::OptIntRect area = paint_rect & canvas_rect; + // we dont want to stop the idle process if the area is empty + if (!area || area->hasZeroArea()) { + return true; + } + paint_rect = *area; + + PaintRectSetup setup; + setup.canvas_rect = canvas_rect; + + // Save the mouse location + gint x, y; + + auto const display = Gdk::Display::get_default(); + auto const seat = display->get_default_seat(); + auto const device = seat->get_pointer(); + + gdk_window_get_device_position(gtk_widget_get_window(GTK_WIDGET(this)), + device->gobj(), + &x, &y, nullptr); + + setup.mouse_loc = sp_canvas_window_to_world(this, Geom::Point(x,y)); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + unsigned tile_multiplier = prefs->getIntLimited("/options/rendering/tile-multiplier", 16, 1, 512); + + if (_rendermode != Inkscape::RENDERMODE_OUTLINE) { + // use 256K as a compromise to not slow down gradients + // 256K is the cached buffer and we need 4 channels + setup.max_pixels = 65536 * tile_multiplier; // 256K/4 + } else { + // paths only, so 1M works faster + // 1M is the cached buffer and we need 4 channels + setup.max_pixels = 262144; + } + + // Start the clock + setup.start_time = g_get_monotonic_time(); + // Go + return paintRectInternal(&setup, paint_rect); +} + +void SPCanvas::forceFullRedrawAfterInterruptions(unsigned int count, bool reset) +{ + _forced_redraw_limit = count; + if (reset) { + _forced_redraw_count = 0; + } +} + +void SPCanvas::endForcedFullRedraws() +{ + _forced_redraw_limit = -1; +} + +gboolean SPCanvas::handle_draw(GtkWidget *widget, cairo_t *cr) { + + SPCanvas *canvas = SP_CANVAS(widget); + + if (canvas->_surface_for_similar == nullptr && canvas->_backing_store != nullptr) { + + // Device scale is copied but since this is only created one time, we'll + // need to check/set device scale anytime it is used in case window moved + // to monitor with different scale. + canvas->_surface_for_similar = + cairo_surface_create_similar(cairo_get_target(cr), CAIRO_CONTENT_COLOR_ALPHA, 1, 1); + + // Check we are using correct device scale + double x_scale = 0; + double y_scale = 0; + cairo_surface_get_device_scale(canvas->_backing_store, &x_scale, &y_scale); + assert (canvas->_device_scale == (int)x_scale); + assert (canvas->_device_scale == (int)y_scale); + + // Reallocate backing store so that cairo can use shared memory + // Function does NOT copy device scale! Width and height are in device pixels. + cairo_surface_t *new_backing_store = cairo_surface_create_similar_image( + canvas->_surface_for_similar, CAIRO_FORMAT_ARGB32, + cairo_image_surface_get_width(canvas->_backing_store), + cairo_image_surface_get_height(canvas->_backing_store)); + + cairo_surface_set_device_scale(new_backing_store, canvas->_device_scale, canvas->_device_scale); + + // Copy the old backing store contents + cairo_t *cr = cairo_create(new_backing_store); + cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE); + cairo_set_source_surface(cr, canvas->_backing_store, 0, 0); + cairo_paint(cr); + cairo_destroy(cr); + cairo_surface_destroy(canvas->_backing_store); + canvas->_backing_store = new_backing_store; + } + + // Blit from the backing store, without regard for the clean region. + // This is necessary because GTK clears the widget for us, which causes + // severe flicker while drawing if we don't blit the old contents. + cairo_set_source_surface(cr, canvas->_backing_store, 0, 0); + cairo_paint(cr); + + cairo_rectangle_list_t *rects = cairo_copy_clip_rectangle_list(cr); + cairo_region_t *dirty_region = cairo_region_create(); + + for (int i = 0; i < rects->num_rectangles; i++) { + cairo_rectangle_t rectangle = rects->rectangles[i]; + Geom::Rect dr = Geom::Rect::from_xywh(rectangle.x + canvas->_x0, rectangle.y + canvas->_y0, + rectangle.width, rectangle.height); + + Geom::IntRect ir = dr.roundOutwards(); + cairo_rectangle_int_t irect = { ir.left(), ir.top(), ir.width(), ir.height() }; + cairo_region_union_rectangle(dirty_region, &irect); + } + cairo_rectangle_list_destroy(rects); + cairo_region_subtract(dirty_region, canvas->_clean_region); + + // Render the dirty portion in the background + if (!cairo_region_is_empty(dirty_region)) { + canvas->addIdle(); + } + cairo_region_destroy(dirty_region); + + return TRUE; +} + +gint SPCanvas::handle_key_event(GtkWidget *widget, GdkEventKey *event) +{ + return SP_CANVAS(widget)->emitEvent(reinterpret_cast<GdkEvent *>(event)); +} + +gint SPCanvas::handle_crossing(GtkWidget *widget, GdkEventCrossing *event) +{ + SPCanvas *canvas = SP_CANVAS (widget); + + if (event->window != getWindow(canvas)) { + return FALSE; + } + canvas->_state = event->state; + return canvas->pickCurrentItem(reinterpret_cast<GdkEvent *>(event)); +} + +gint SPCanvas::handle_focus_in(GtkWidget *widget, GdkEventFocus *event) +{ + gtk_widget_grab_focus (widget); + + SPCanvas *canvas = SP_CANVAS (widget); + + if (canvas->_focused_item) { + return canvas->emitEvent(reinterpret_cast<GdkEvent *>(event)); + } else { + return FALSE; + } +} + +gint SPCanvas::handle_focus_out(GtkWidget *widget, GdkEventFocus *event) +{ + SPCanvas *canvas = SP_CANVAS(widget); + + if (canvas->_focused_item) { + return canvas->emitEvent(reinterpret_cast<GdkEvent *>(event)); + } else { + return FALSE; + } +} + +int SPCanvas::paint() +{ + if (_need_update) { + sp_canvas_item_invoke_update(_root, Geom::identity(), 0); + _need_update = FALSE; + } + SPCanvas *canvas = SP_CANVAS(this); + GtkAllocation allocation; + gtk_widget_get_allocation(GTK_WIDGET(canvas), &allocation); + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + SPCanvasArena *arena = nullptr; + bool split = false; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + _xray_radius = prefs->getIntLimited("/options/rendering/xray-radius", 100, 1, 1500); + + double split_x = 1; + double split_y = 1; + Inkscape::RenderMode rm = Inkscape::RENDERMODE_NORMAL; + if (desktop) { + split = desktop->splitMode(); + arena = SP_CANVAS_ARENA(desktop->drawing); + rm = arena->drawing.renderMode(); + if (split) { + auto window = desktop->getInkscapeWindow(); + auto dtw = window->get_desktop_widget(); + bool hasrullers = prefs->getBool(desktop->is_fullscreen() ? "/fullscreen/rulers/state" : "/window/rulers/state"); + int hruler_gap = hasrullers ? dtw->get_hruler_thickness() : 1; + int vruler_gap = hasrullers ? dtw->get_vruler_thickness() : 1; + + split_x = !_split_vertical ? 0 : _split_value; + split_y = _split_vertical ? 0 : _split_value; + + Geom::IntCoord coord1x = + allocation.x + (int((allocation.width) * split_x)) - (3 * canvas->_device_scale) - vruler_gap; + Geom::IntCoord coord1y = + allocation.y + (int((allocation.height) * split_y)) - (3 * canvas->_device_scale) - hruler_gap; + split_x = !_split_vertical ? 1 : _split_value; + split_y = _split_vertical ? 1 : _split_value; + Geom::IntCoord coord2x = + allocation.x + (int((allocation.width) * split_x)) + (3 * canvas->_device_scale) - vruler_gap; + Geom::IntCoord coord2y = + allocation.y + (int((allocation.height) * split_y)) + (3 * canvas->_device_scale) - hruler_gap; + _spliter = Geom::OptIntRect(coord1x, coord1y, coord2x, coord2y); + split_x = !_split_vertical ? 0 : _split_value; + split_y = _split_vertical ? 0 : _split_value; + coord1x = allocation.x + (int((allocation.width ) * split_x)) - vruler_gap; + coord1y = allocation.y + (int((allocation.height) * split_y)) - hruler_gap; + split_x = !_split_vertical ? 1 : _split_value; + split_y = _split_vertical ? 1 : _split_value; + coord2x = allocation.x + allocation.width; + coord2y = allocation.y + allocation.height; + _spliter_area = Geom::OptIntRect(coord1x, coord1y, coord2x, coord2y); + } else { + sp_reset_spliter(canvas); + } + } else { + sp_reset_spliter(canvas); + } + cairo_rectangle_int_t crect = { _x0, _y0, int(allocation.width * split_x), int(allocation.height * split_y) }; + split_x = !_split_vertical ? 0 : _split_value; + split_y = _split_vertical ? 0 : _split_value; + cairo_rectangle_int_t crect_outline = { _x0 + int(allocation.width * split_x), + _y0 + int(allocation.height * split_y), + int(allocation.width * (1 - split_x)), + int(allocation.height * (1 - split_y)) }; + cairo_region_t *draw = nullptr; + cairo_region_t *to_draw = nullptr; + cairo_region_t *to_draw_outline = nullptr; + if (_split_inverse && split) { + to_draw = cairo_region_create_rectangle(&crect_outline); + to_draw_outline = cairo_region_create_rectangle(&crect); + } else { + to_draw = cairo_region_create_rectangle(&crect); + to_draw_outline = cairo_region_create_rectangle(&crect_outline); + } + + cairo_region_get_extents(_clean_region, &crect); + draw = cairo_region_create_rectangle(&crect); + cairo_region_subtract(draw, _clean_region); + cairo_region_get_extents(draw, &crect); + cairo_region_subtract_rectangle(_clean_region, &crect); + cairo_region_subtract(to_draw, _clean_region); + cairo_region_subtract(to_draw_outline, _clean_region); + cairo_region_destroy(draw); + int n_rects = cairo_region_num_rectangles(to_draw); + for (int i = 0; i < n_rects; ++i) { + cairo_rectangle_int_t crect; + cairo_region_get_rectangle(to_draw, i, &crect); + if (!paintRect(crect.x, crect.y, crect.x + crect.width, crect.y + crect.height)) { + // Aborted + cairo_region_destroy(to_draw); + cairo_region_destroy(to_draw_outline); + return FALSE; + }; + } + + if (split) { + arena->drawing.setRenderMode(Inkscape::RENDERMODE_OUTLINE); + bool exact = arena->drawing.getExact(); + arena->drawing.setExact(false); + int n_rects = cairo_region_num_rectangles(to_draw_outline); + for (int i = 0; i < n_rects; ++i) { + cairo_rectangle_int_t crect; + cairo_region_get_rectangle(to_draw_outline, i, &crect); + if (!paintRect(crect.x, crect.y, crect.x + crect.width, crect.y + crect.height)) { + // Aborted + arena->drawing.setExact(exact); + arena->drawing.setRenderMode(rm); + cairo_region_destroy(to_draw); + cairo_region_destroy(to_draw_outline); + return FALSE; + }; + } + arena->drawing.setExact(exact); + arena->drawing.setRenderMode(rm); + canvas->paintSpliter(); + } else if (desktop && _xray) { + if (rm != Inkscape::RENDERMODE_OUTLINE) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/desktop/xrayactive", true); + arena->drawing.setRenderMode(Inkscape::RENDERMODE_OUTLINE); + bool exact = arena->drawing.getExact(); + arena->drawing.setExact(false); + Geom::IntRect canvas_rect = Geom::IntRect::from_xywh(_x0, _y0, allocation.width, allocation.height); + Geom::IntRect _xray_rect = Geom::IntRect::from_xywh( + _xray_orig[0] - _xray_radius, _xray_orig[1] - _xray_radius, (_xray_radius * 2), (_xray_radius * 2)); + paintXRayBuffer(_xray_rect, canvas_rect); + arena->drawing.setExact(exact); + arena->drawing.setRenderMode(rm); + prefs->setBool("/desktop/xrayactive", false); + } + } + + // we've had a full unaborted redraw, reset the full redraw counter + if (_forced_redraw_limit != -1) { + _forced_redraw_count = 0; + } + + cairo_region_destroy(to_draw); + cairo_region_destroy(to_draw_outline); + + return TRUE; +} + +int SPCanvas::doUpdate() +{ + if (!_root) { // canvas may have already be destroyed by closing desktop during interrupted display! + return TRUE; + } + if (_drawing_disabled) { + return TRUE; + } + + // Cause the update if necessary + if (_need_update) { + sp_canvas_item_invoke_update(_root, Geom::identity(), 0); + _need_update = FALSE; + } + + // Paint if able to + if (gtk_widget_is_drawable(GTK_WIDGET(this))) { + return paint(); + } + + // Pick new current item + while (_need_repick) { + _need_repick = FALSE; + pickCurrentItem(&_pick_event); + } + + return TRUE; +} + +gint SPCanvas::idle_handler(gpointer data) +{ + SPCanvas *canvas = SP_CANVAS (data); +#ifdef DEBUG_PERFORMANCE + static int totaloops = 1; + gint64 now = 0; + gint64 elapsed = 0; + now = g_get_monotonic_time(); + elapsed = now - canvas->_idle_time; + g_message("[%i] start loop %i in split %i at %f", canvas->_idle_id, totaloops, canvas->_splits, + canvas->_totalelapsed / (double)1000000 + elapsed / (double)1000000); +#endif + int ret = canvas->doUpdate(); + int n_rects = cairo_region_num_rectangles(canvas->_clean_region); + if (n_rects > 1) { // not fully painted, maybe clean region is updated in middle of idle, reload again + ret = 0; + } + +#ifdef DEBUG_PERFORMANCE + if (ret == 0) { + now = g_get_monotonic_time(); + elapsed = now - canvas->_idle_time; + g_message("[%i] loop ended unclean at %f", canvas->_idle_id, + canvas->_totalelapsed / (double)1000000 + elapsed / (double)1000000); + totaloops += 1; + } + if (ret) { + // Reset idle id + now = g_get_monotonic_time(); + elapsed = now - canvas->_idle_time; + canvas->_totalelapsed += elapsed; + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop) { + SPCanvasArena *arena = SP_CANVAS_ARENA(desktop->drawing); + Inkscape::RenderMode rm = arena->drawing.renderMode(); + if (rm == Inkscape::RENDERMODE_OUTLINE) { + canvas->_totalelapsed = 0; + g_message("Outline mode, we reset and stop total counter"); + } + g_message("[%i] finished", canvas->_idle_id); + g_message("[%i] loops %i", canvas->_idle_id, totaloops); + g_message("[%i] splits %i", canvas->_idle_id, canvas->_splits); + g_message("[%i] duration %f", canvas->_idle_id, elapsed / (double)1000000); + g_message("[%i] total %f (toggle outline mode to reset)", canvas->_idle_id, + canvas->_totalelapsed / (double)1000000); + g_message("[%i] :::::::::::::::::::::::::::::::::::::::", canvas->_idle_id); + } + canvas->_idle_id = 0; + totaloops = 1; + canvas->_splits = 0; + } +#else + if (ret) { + // Reset idle id + canvas->_idle_id = 0; + } +#endif + return !ret; +} + +void SPCanvas::addIdle() +{ + if (_idle_id == 0) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + guint redrawPriority = prefs->getIntLimited("/options/redrawpriority/value", G_PRIORITY_HIGH_IDLE, G_PRIORITY_HIGH_IDLE, G_PRIORITY_DEFAULT_IDLE); + SPCanvas *canvas = SP_CANVAS(this); + if (canvas->_in_full_redraw) { + canvas->_in_full_redraw = false; + redrawPriority = G_PRIORITY_DEFAULT_IDLE; + } +#ifdef DEBUG_PERFORMANCE + _idle_time = g_get_monotonic_time(); +#endif + _idle_id = gdk_threads_add_idle_full(redrawPriority, idle_handler, this, nullptr); +#ifdef DEBUG_PERFORMANCE + g_message("[%i] launched %f", _idle_id, _totalelapsed / (double)1000000); +#endif + } +} +void SPCanvas::removeIdle() +{ + if (_idle_id) { + g_source_remove(_idle_id); +#ifdef DEBUG_PERFORMANCE + g_message("[%i] aborted in split %i", _idle_id, _splits); + _splits = 0; +#endif + _idle_id = 0; + } +} + +SPCanvasGroup *SPCanvas::getRoot() +{ + return SP_CANVAS_GROUP(_root); +} + +/** + * Scroll screen to point 'c'. 'c' is measured in screen pixels. + */ +void SPCanvas::scrollTo( Geom::Point const &c, unsigned int clear, bool is_scrolling) +{ + // To do: extract out common code with SPCanvas::handle_size_allocate() + + // For HiDPI monitors + int device_scale = gtk_widget_get_scale_factor(GTK_WIDGET(this)); + assert( device_scale == _device_scale); + + double cx = c[Geom::X]; + double cy = c[Geom::Y]; + + int ix = (int) round(cx); // ix and iy are the new canvas coordinates (integer screen pixels) + int iy = (int) round(cy); // cx might be negative, so (int)(cx + 0.5) will not do! + int dx = ix - _x0; // dx and dy specify the displacement (scroll) of the + int dy = iy - _y0; // canvas w.r.t its previous position + + Geom::IntRect old_area = getViewboxIntegers(); + Geom::IntRect new_area = old_area + Geom::IntPoint(dx, dy); + bool outsidescrool = false; + if (!new_area.intersects(old_area)) { + outsidescrool = true; + } + GtkAllocation allocation; + gtk_widget_get_allocation(&_widget, &allocation); + + // cairo_surface_write_to_png( _backing_store, "scroll1.png" ); + bool split = false; + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop && desktop->splitMode()) { + split = true; + } + if (clear || split || _xray || outsidescrool) { + _dx0 = cx; // here the 'd' stands for double, not delta! + _dy0 = cy; + _x0 = ix; + _y0 = iy; + requestFullRedraw(); + } else { + // Adjust backing store contents + assert(_backing_store); + // this cairo operation is slow, improvements welcome + cairo_surface_t *new_backing_store = nullptr; + if (_surface_for_similar != nullptr) + + // Size in device pixels. Does not set device scale. + new_backing_store = + cairo_surface_create_similar_image(_surface_for_similar, + CAIRO_FORMAT_ARGB32, + allocation.width * _device_scale, + allocation.height * _device_scale); + if (new_backing_store == nullptr) + // Size in device pixels. Does not set device scale. + new_backing_store = + cairo_image_surface_create(CAIRO_FORMAT_ARGB32, + allocation.width * _device_scale, + allocation.height * _device_scale); + + // Set device scale + cairo_surface_set_device_scale(new_backing_store, _device_scale, _device_scale); + + cairo_t *cr = cairo_create(new_backing_store); + cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE); + // Paint the background + cairo_translate(cr, -ix, -iy); + cairo_set_source_surface(cr, _backing_store, ix, iy); + cairo_paint(cr); + + // cairo_surface_write_to_png( _backing_store, "scroll0.png" ); + + // Copy the old backing store contents + cairo_set_source_surface(cr, _backing_store, _x0, _y0); + cairo_rectangle(cr, _x0, _y0, allocation.width, allocation.height); + cairo_clip(cr); + cairo_paint(cr); + cairo_destroy(cr); + cairo_surface_destroy(_backing_store); + _backing_store = new_backing_store; + _dx0 = cx; // here the 'd' stands for double, not delta! + _dy0 = cy; + _x0 = ix; + _y0 = iy; + cairo_rectangle_int_t crect = { _x0, _y0, allocation.width, allocation.height }; + cairo_region_intersect_rectangle(_clean_region, &crect); + } + + SPCanvasArena *arena = SP_CANVAS_ARENA(desktop->drawing); + if (arena) { + Geom::IntRect expanded = new_area; + Geom::IntPoint expansion(new_area.width()/2, new_area.height()/2); + expanded.expandBy(expansion); + arena->drawing.setCacheLimit(expanded, false); + } + + if (!clear) { + // scrolling without zoom; redraw only the newly exposed areas + if ((dx != 0) || (dy != 0)) { + if (gtk_widget_get_realized(GTK_WIDGET(this))) { + SPCanvas *canvas = SP_CANVAS(this); + if (split) { + double scroll_horiz = 1 / (allocation.width / (double)-dx); + double scroll_vert = 1 / (allocation.height / (double)-dy); + double gap = canvas->_split_vertical ? scroll_horiz : scroll_vert; + canvas->_split_value = canvas->_split_value + gap; + if (scroll_horiz < 0.03 || scroll_horiz > 0.97 || scroll_vert < 0.03 || scroll_vert > 0.97) { + if (canvas->_split_value > 0.97) { + canvas->_split_value = 0.97; + } else if (canvas->_split_value < 0.03) { + canvas->_split_value = 0.03; + } + } + } + gdk_window_scroll(getWindow(this), -dx, -dy); + } + } + } +} + +void SPCanvas::updateNow() +{ + if (_need_update) { +#ifdef DEBUG_PERFORMANCE + guint64 now = g_get_monotonic_time(); + gint64 elapsed = now - _idle_time; + g_message("[%i] start updateNow(): %f at %f", _idle_id, elapsed / (double)1000000, + _totalelapsed / (double)1000000 + elapsed / (double)1000000); +#endif + doUpdate(); +#ifdef DEBUG_PERFORMANCE + now = g_get_monotonic_time(); + elapsed = now - _idle_time; + g_message("[%i] end updateNow(): %f at %f", _idle_id, elapsed / (double)1000000, + _totalelapsed / (double)1000000 + elapsed / (double)1000000); +#endif + } +} + +void SPCanvas::requestUpdate() +{ + _need_update = TRUE; + addIdle(); +} + +void SPCanvas::requestRedraw(int x0, int y0, int x1, int y1) +{ + if (!gtk_widget_is_drawable( GTK_WIDGET(this) )) { + return; + } + if (x0 >= x1 || y0 >= y1) { + return; + } + + Geom::IntRect bbox(x0, y0, x1, y1); + dirtyRect(bbox); + addIdle(); +} +void SPCanvas::requestFullRedraw() +{ + SPCanvas *canvas = SP_CANVAS(this); + canvas->_in_full_redraw = true; + dirtyAll(); + addIdle(); +} + +void SPCanvas::setBackgroundColor(guint32 rgba) { + double new_r = SP_RGBA32_R_F(rgba); + double new_g = SP_RGBA32_G_F(rgba); + double new_b = SP_RGBA32_B_F(rgba); + if (!_background_is_checkerboard) { + double old_r, old_g, old_b; + cairo_pattern_get_rgba(_background, &old_r, &old_g, &old_b, nullptr); + if (new_r == old_r && new_g == old_g && new_b == old_b) return; + } + if (_background) { + cairo_pattern_destroy(_background); + } + _background = cairo_pattern_create_rgb(new_r, new_g, new_b); + _background_is_checkerboard = false; + requestFullRedraw(); +} + +void SPCanvas::setBackgroundCheckerboard(guint32 rgba) +{ + if (_background_is_checkerboard) return; + if (_background) { + cairo_pattern_destroy(_background); + } + _background = ink_cairo_pattern_create_checkerboard(rgba); + _background_is_checkerboard = true; + requestFullRedraw(); +} + +/** + * Sets world coordinates from win and canvas. + */ +void sp_canvas_window_to_world(SPCanvas const *canvas, double winx, double winy, double *worldx, double *worldy) +{ + g_return_if_fail (canvas != nullptr); + g_return_if_fail (SP_IS_CANVAS (canvas)); + + if (worldx) *worldx = canvas->_x0 + winx; + if (worldy) *worldy = canvas->_y0 + winy; +} + +/** + * Sets win coordinates from world and canvas. + */ +void sp_canvas_world_to_window(SPCanvas const *canvas, double worldx, double worldy, double *winx, double *winy) +{ + g_return_if_fail (canvas != nullptr); + g_return_if_fail (SP_IS_CANVAS (canvas)); + + if (winx) *winx = worldx - canvas->_x0; + if (winy) *winy = worldy - canvas->_y0; +} + +/** + * Converts point from win to world coordinates. + */ +Geom::Point sp_canvas_window_to_world(SPCanvas const *canvas, Geom::Point const win) +{ + g_assert (canvas != nullptr); + g_assert (SP_IS_CANVAS (canvas)); + + return Geom::Point(canvas->_x0 + win[0], canvas->_y0 + win[1]); +} + +/** + * Converts point from world to win coordinates. + */ +Geom::Point sp_canvas_world_to_window(SPCanvas const *canvas, Geom::Point const world) +{ + g_assert (canvas != nullptr); + g_assert (SP_IS_CANVAS (canvas)); + + return Geom::Point(world[0] - canvas->_x0, world[1] - canvas->_y0); +} + +/** + * Returns true if point given in world coordinates is inside window. + */ +bool sp_canvas_world_pt_inside_window(SPCanvas const *canvas, Geom::Point const &world) +{ + GtkAllocation allocation; + + g_assert( canvas != nullptr ); + g_assert(SP_IS_CANVAS(canvas)); + + GtkWidget *w = GTK_WIDGET(canvas); + gtk_widget_get_allocation (w, &allocation); + + return ( ( canvas->_x0 <= world[Geom::X] ) && + ( canvas->_y0 <= world[Geom::Y] ) && + ( world[Geom::X] < canvas->_x0 + allocation.width ) && + ( world[Geom::Y] < canvas->_y0 + allocation.height ) ); +} + +/** + * Return canvas window coordinates as Geom::Rect. + */ +Geom::Rect SPCanvas::getViewbox() const +{ + GtkAllocation allocation; + + gtk_widget_get_allocation (GTK_WIDGET (this), &allocation); + return Geom::Rect::from_xywh(_dx0, _dy0, allocation.width, allocation.height); +} + +/** + * Return canvas window coordinates as integer rectangle. + */ +Geom::IntRect SPCanvas::getViewboxIntegers() const +{ + GtkAllocation allocation; + + gtk_widget_get_allocation (GTK_WIDGET(this), &allocation); + return Geom::IntRect::from_xywh(_x0, _y0, allocation.width, allocation.height); +} + +inline int sp_canvas_tile_floor(int x) +{ + return (x & (~(TILE_SIZE - 1))) / TILE_SIZE; +} + +inline int sp_canvas_tile_ceil(int x) +{ + return ((x + (TILE_SIZE - 1)) & (~(TILE_SIZE - 1))) / TILE_SIZE; +} + +void SPCanvas::dirtyRect(Geom::IntRect const &area) { + markRect(area, 1); +} + +void SPCanvas::dirtyAll() { + if (_clean_region && !cairo_region_is_empty(_clean_region)) { + cairo_region_destroy(_clean_region); + _clean_region = cairo_region_create(); + } +} + +void SPCanvas::markRect(Geom::IntRect const &area, uint8_t val) +{ + cairo_rectangle_int_t crect = { area.left(), area.top(), area.width(), area.height() }; + if (val) { + cairo_region_subtract_rectangle(_clean_region, &crect); + } else { + cairo_region_union_rectangle(_clean_region, &crect); + } +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/sp-canvas.h b/src/display/sp-canvas.h new file mode 100644 index 0000000..a079714 --- /dev/null +++ b/src/display/sp-canvas.h @@ -0,0 +1,302 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_CANVAS_H +#define SEEN_SP_CANVAS_H + +/** + * @file + * SPCanvas, SPCanvasBuf. + */ +/* + * Authors: + * Federico Mena <federico@nuclecu.unam.mx> + * Raph Levien <raph@gimp.org> + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon A. Cruz <jon@joncruz.org> + * Krzysztof KosiĆski <tweenk.pl@gmail.com> + * + * Copyright (C) 1998 The Free Software Foundation + * Copyright (C) 2002 Lauris Kaplinski + * Copyright (C) 2016 Google Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include <2geom/affine.h> +#include <2geom/rect.h> +#include <cstdint> +#include <glib.h> +#include <glibmm/ustring.h> +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define SP_TYPE_CANVAS (sp_canvas_get_type()) +#define SP_CANVAS(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), SP_TYPE_CANVAS, SPCanvas)) +#define SP_IS_CANVAS(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), SP_TYPE_CANVAS)) + +struct SPCanvas; +struct SPCanvasItem; +struct SPCanvasGroup; + +enum { + SP_CANVAS_UPDATE_REQUESTED = 1 << 0, + SP_CANVAS_UPDATE_AFFINE = 1 << 1 +}; + +/** + * Structure used when rendering canvas items. + */ +struct SPCanvasBuf { + cairo_t *ct; + Geom::IntRect rect; + Geom::IntRect canvas_rect; // visible window in world coordinates (i.e. offset by _x0, _y0) + + unsigned char *buf; + int buf_rowstride; + int device_scale; // For high DPI monitors. + bool is_empty; +}; + +G_END_DECLS + +// SPCanvas ------------------------------------------------- + +struct PaintRectSetup; + +GType sp_canvas_get_type() G_GNUC_CONST; + +/** + * Port of GnomeCanvas for inkscape needs. + */ +struct SPCanvas { + /// Scrolls canvas to specific position (c is measured in screen pixels). + void scrollTo(Geom::Point const &c, unsigned int clear, bool is_scrolling = false); + + /// Synchronously updates the canvas if necessary. + void updateNow(); + + /// Queues a redraw of rectangular canvas area. + void requestRedraw(int x1, int y1, int x2, int y2); + void requestFullRedraw(); + void requestUpdate(); + + void forceFullRedrawAfterInterruptions(unsigned int count, bool reset = true); + void endForcedFullRedraws(); + + Geom::Rect getViewbox() const; + Geom::IntRect getViewboxIntegers() const; + SPCanvasGroup *getRoot(); + + void setBackgroundColor(guint32 rgba); + void setBackgroundCheckerboard(guint32 rgba = 0xC4C4C4FF); + + /// Returns new canvas as widget. + static GtkWidget *createAA(); + +private: + /// Emits an event for an item in the canvas, be it the current + /// item, grabbed item, or focused item, as appropriate. + int emitEvent(GdkEvent *event); + + /// Re-picks the current item in the canvas, based on the event's + /// coordinates and emits enter/leave events for items as appropriate. + int pickCurrentItem(GdkEvent *event); + void shutdownTransients(); + + /// Allocates a new tile array for the canvas, copying overlapping tiles from the old array + void resizeTiles(int nl, int nt, int nr, int nb); + + /// Marks the specified area as dirty (requiring redraw) + void dirtyRect(Geom::IntRect const &area); + /// Marks the whole widget for redraw + void dirtyAll(); + void markRect(Geom::IntRect const &area, uint8_t val); + + /// Invokes update, paint, and repick on canvas. + int doUpdate(); + + void paintSingleBuffer(Geom::IntRect const &paint_rect, Geom::IntRect const &canvas_rect, int sw); + void paintXRayBuffer(Geom::IntRect const &paint_rect, Geom::IntRect const &canvas_rect); + + /** + * Paint the given rect, recursively subdividing the region until it is the size of a single + * buffer. + * + * @return true if the drawing completes + */ + int paintRectInternal(PaintRectSetup const *setup, Geom::IntRect const &this_rect); + + void paintSpliter(); + void set_cursor(GtkWidget *widget); + /// Draws a specific rectangular part of the canvas. + /// @return true if the rectangle painting succeeds. + bool paintRect(int xx0, int yy0, int xx1, int yy1); + + /// Repaints the areas in the canvas that need it. + /// @return true if all the dirty parts have been redrawn + int paint(); + + /// Idle handler for the canvas that deals with pending updates and redraws. + static gint idle_handler(gpointer data); + + /// Convenience function to add an idle handler to a canvas. + void addIdle(); + void removeIdle(); + +public: + // GTK virtual functions. + static void dispose(GObject *object); + static void handle_realize(GtkWidget *widget); + static void handle_unrealize(GtkWidget *widget); + static void handle_get_preferred_width(GtkWidget *widget, gint *min_w, gint *nat_w); + static void handle_get_preferred_height(GtkWidget *widget, gint *min_h, gint *nat_h); + static void handle_size_allocate(GtkWidget *widget, GtkAllocation *allocation); + static gint handle_button(GtkWidget *widget, GdkEventButton *event); + static gint handle_doubleclick(GtkWidget *widget, GdkEventButton *event); + + /** + * Scroll event handler for the canvas. + * + * @todo FIXME: generate motion events to re-select items. + */ + static gint handle_scroll(GtkWidget *widget, GdkEventScroll *event); + static gint handle_motion(GtkWidget *widget, GdkEventMotion *event); + static gboolean handle_draw(GtkWidget *widget, cairo_t *cr); + static gint handle_key_event(GtkWidget *widget, GdkEventKey *event); + static gint handle_crossing(GtkWidget *widget, GdkEventCrossing *event); + static gint handle_focus_in(GtkWidget *widget, GdkEventFocus *event); + static gint handle_focus_out(GtkWidget *widget, GdkEventFocus *event); + +public: + // Data members: ---------------------------------------------------------- + GtkWidget _widget; + + guint _idle_id; + + SPCanvasItem *_root; + + Geom::OptIntRect _spliter; + Geom::OptIntRect _spliter_area; + Geom::OptIntRect _spliter_control; + Geom::OptIntRect _spliter_top; + Geom::OptIntRect _spliter_bottom; + Geom::OptIntRect _spliter_left; + Geom::OptIntRect _spliter_right; + Geom::OptIntRect _xray_rect; + Geom::Point _spliter_control_pos; + Geom::Point _spliter_in_control_pos; + Geom::Point _xray_orig; + double _split_value; + bool _split_vertical; + bool _split_inverse; + bool _split_hover_vertical; + bool _split_hover_horizontal; + bool _split_hover; + bool _split_pressed; + bool _split_control_pressed; + bool _split_dragging; + double _xray_radius; + bool _xray; + bool _is_dragging; + bool _in_full_redraw; + guint _changecursor; + double _dx0; + double _dy0; + int _x0; ///< World coordinate of the leftmost pixels of window + int _y0; ///< World coordinate of the topmost pixels of window + int _device_scale; ///< Scale for high DPI montiors + gint64 _idle_time; + int _splits; + gint64 _totalelapsed; + + /// Image surface storing the contents of the widget + cairo_surface_t *_backing_store; + /// This should be e.g. a cairo-xlib surface that is used to allocate _backing_store; might be NULL. + cairo_surface_t *_surface_for_similar; + /// Area of the widget that has up-to-date content + cairo_region_t *_clean_region; + /// Widget background, defaults to white + cairo_pattern_t *_background; + bool _background_is_checkerboard; + + /// Last known modifier state, for deferred repick when a button is down. + int _state; + + /** The item containing the mouse pointer, or NULL if none. */ + SPCanvasItem *_current_item; + + /** Item that is about to become current (used to track deletions and such). */ + SPCanvasItem *_new_current_item; + + /** Item that holds a pointer grab, or NULL if none. */ + SPCanvasItem *_grabbed_item; + + /** Event mask specified when grabbing an item. */ + guint _grabbed_event_mask; + + /** If non-NULL, the currently focused item. */ + SPCanvasItem *_focused_item; + + /** Event on which selection of current item is based. */ + GdkEvent _pick_event; + + int _close_enough; + + unsigned int _need_update : 1; + unsigned int _need_repick : 1; + + int _forced_redraw_count; + int _forced_redraw_limit; + + /** For use by internal pick_current_item() function. */ + unsigned int _left_grabbed_item : 1; + + /** For use by internal pick_current_item() function. */ + unsigned int _in_repick : 1; + + // In most tools Inkscape only generates enter and leave events + // on the current item, but no other enter events if a mouse button + // is depressed -- see function pickCurrentItem(). Some tools + // may wish the canvas to generate to all enter events, (e.g., the + // connector tool). If so, they may temporarily set this flag to + // 'true'. + bool _gen_all_enter_events; + + /** For scripting, sometimes we want to delay drawing. */ + bool _drawing_disabled; + + int _rendermode; + int _colorrendermode; + +#if defined(HAVE_LIBLCMS2) + bool _enable_cms_display_adj; + Glib::ustring _cms_key; +#endif // defined(HAVE_LIBLCMS2) + + bool _is_scrolling; +}; + +bool sp_canvas_world_pt_inside_window(SPCanvas const *canvas, Geom::Point const &world); + +void sp_canvas_window_to_world(SPCanvas const *canvas, double winx, double winy, double *worldx, double *worldy); +void sp_canvas_world_to_window(SPCanvas const *canvas, double worldx, double worldy, double *winx, double *winy); + +Geom::Point sp_canvas_window_to_world(SPCanvas const *canvas, Geom::Point const win); +Geom::Point sp_canvas_world_to_window(SPCanvas const *canvas, Geom::Point const world); + +#endif // SEEN_SP_CANVAS_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/display/sp-ctrlcurve.cpp b/src/display/sp-ctrlcurve.cpp new file mode 100644 index 0000000..41572cd --- /dev/null +++ b/src/display/sp-ctrlcurve.cpp @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Simple bezier curve used for Mesh Gradients + * + * Author: + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 2011 Tavmjong Bah + * Copyright (C) 2007 Johan Engelen + * Copyright (C) 1999-2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "display/sp-ctrlcurve.h" +#include "display/sp-canvas-util.h" +#include "display/cairo-utils.h" +#include "color.h" +#include "display/sp-canvas.h" + +namespace { + +static void sp_ctrlcurve_destroy(SPCanvasItem *object); + +static void sp_ctrlcurve_update(SPCanvasItem *item, Geom::Affine const &affine, unsigned int flags); +static void sp_ctrlcurve_render(SPCanvasItem *item, SPCanvasBuf *buf); + +} // namespace + +G_DEFINE_TYPE(SPCtrlCurve, sp_ctrlcurve, SP_TYPE_CANVAS_ITEM); + +static void +sp_ctrlcurve_class_init(SPCtrlCurveClass *klass) +{ + klass->destroy = sp_ctrlcurve_destroy; + + klass->update = sp_ctrlcurve_update; + klass->render = sp_ctrlcurve_render; +} + +static void +sp_ctrlcurve_init(SPCtrlCurve *ctrlcurve) +{ + // Points are initialized to 0,0 + ctrlcurve->rgba = 0x0000ff7f; + ctrlcurve->item=nullptr; + ctrlcurve->corner0 = -1; + ctrlcurve->corner1 = -1; +} + +namespace { +static void +sp_ctrlcurve_destroy(SPCanvasItem *object) +{ + g_return_if_fail (object != nullptr); + g_return_if_fail (SP_IS_CTRLCURVE (object)); + + SPCtrlCurve *ctrlcurve = SP_CTRLCURVE (object); + + ctrlcurve->item=nullptr; + + if (SP_CANVAS_ITEM_CLASS(sp_ctrlcurve_parent_class)->destroy) + SP_CANVAS_ITEM_CLASS(sp_ctrlcurve_parent_class)->destroy(object); +} + +static void +sp_ctrlcurve_render(SPCanvasItem *item, SPCanvasBuf *buf) +{ + SPCtrlCurve *cl = SP_CTRLCURVE (item); + + if (!buf->ct) + return; + + if ( cl->p0 == cl->p1 && + cl->p1 == cl->p2 && + cl->p2 == cl->p3 ) + return; + + ink_cairo_set_source_rgba32(buf->ct, cl->rgba); + cairo_set_line_width(buf->ct, 1); + cairo_new_path(buf->ct); + + Geom::Point p0 = cl->p0 * cl->affine; + Geom::Point p1 = cl->p1 * cl->affine; + Geom::Point p2 = cl->p2 * cl->affine; + Geom::Point p3 = cl->p3 * cl->affine; + + cairo_move_to (buf->ct, p0[Geom::X] - buf->rect.left(), p0[Geom::Y] - buf->rect.top()); + cairo_curve_to (buf->ct, + p1[Geom::X] - buf->rect.left(), p1[Geom::Y] - buf->rect.top(), + p2[Geom::X] - buf->rect.left(), p2[Geom::Y] - buf->rect.top(), + p3[Geom::X] - buf->rect.left(), p3[Geom::Y] - buf->rect.top() ); + + cairo_stroke(buf->ct); +} + +static void +sp_ctrlcurve_update(SPCanvasItem *item, Geom::Affine const &affine, unsigned int flags) +{ + SPCtrlCurve *cl = SP_CTRLCURVE (item); + + item->canvas->requestRedraw(item->x1, item->y1, item->x2, item->y2); + + if (SP_CANVAS_ITEM_CLASS(sp_ctrlcurve_parent_class)->update) + SP_CANVAS_ITEM_CLASS(sp_ctrlcurve_parent_class)->update(item, affine, flags); + + sp_canvas_item_reset_bounds (item); + + cl->affine = affine; + + if (cl->p0 == cl->p1 && cl->p1 == cl->p2 && cl->p2 == cl->p3 ) { + item->x1 = item->x2 = item->y1 = item->y2 = 0; + } else { + + Geom::Point p0 = cl->p0 * affine; + Geom::Point p1 = cl->p1 * affine; + Geom::Point p2 = cl->p2 * affine; + Geom::Point p3 = cl->p3 * affine; + + double min_x = p0[Geom::X]; + double min_y = p0[Geom::Y]; + double max_x = p0[Geom::X]; + double max_y = p0[Geom::Y]; + + min_x = MIN( min_x, p1[Geom::X] ); + min_y = MIN( min_y, p1[Geom::Y] ); + max_x = MAX( max_x, p1[Geom::X] ); + max_y = MAX( max_y, p1[Geom::Y] ); + + min_x = MIN( min_x, p2[Geom::X] ); + min_y = MIN( min_y, p2[Geom::Y] ); + max_x = MAX( max_x, p2[Geom::X] ); + max_y = MAX( max_y, p2[Geom::Y] ); + + min_x = MIN( min_x, p3[Geom::X] ); + min_y = MIN( min_y, p3[Geom::Y] ); + max_x = MAX( max_x, p3[Geom::X] ); + max_y = MAX( max_y, p3[Geom::Y] ); + + item->x1 = round( min_x - 1 ); + item->y1 = round( min_y - 1 ); + item->x2 = round( max_x + 1 ); + item->y2 = round( max_y + 1 ); + + item->canvas->requestRedraw(item->x1, item->y1, item->x2, item->y2); + + } +} + +} // namespace + +#define EPSILON 1e-6 +#define DIFFER(a,b) (fabs ((a) - (b)) > EPSILON) + +void SPCtrlCurve::setCoords( gdouble x0, gdouble y0, gdouble x1, gdouble y1, + gdouble x2, gdouble y2, gdouble x3, gdouble y3 ) +{ + + Geom::Point q0( x0, y0 ); + Geom::Point q1( x1, y1 ); + Geom::Point q2( x2, y2 ); + Geom::Point q3( x3, y3 ); + + setCoords( q0, q1, q2, q3 ); + +} + +void SPCtrlCurve::setCoords( Geom::Point const &q0, Geom::Point const &q1, + Geom::Point const &q2, Geom::Point const &q3) +{ + if (DIFFER(p0[Geom::X], q0[Geom::X]) || DIFFER(p0[Geom::Y], q0[Geom::Y]) || + DIFFER(p1[Geom::X], q1[Geom::X]) || DIFFER(p1[Geom::Y], q1[Geom::Y]) || + DIFFER(p2[Geom::X], q2[Geom::X]) || DIFFER(p2[Geom::Y], q2[Geom::Y]) || + DIFFER(p3[Geom::X], q3[Geom::X]) || DIFFER(p3[Geom::Y], q3[Geom::Y]) ) { + p0 = q0; + p1 = q1; + p2 = q2; + p3 = q3; + sp_canvas_item_request_update( this ); + } +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/sp-ctrlcurve.h b/src/display/sp-ctrlcurve.h new file mode 100644 index 0000000..a499d52 --- /dev/null +++ b/src/display/sp-ctrlcurve.h @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_INKSCAPE_CTRLCURVE_H +#define SEEN_INKSCAPE_CTRLCURVE_H + +/* + * Simple bezier curve (used for Mesh Gradients) + * + * Author: + * Tavmjong Bah <tavmjong@free.fr> + * Derived from sp-ctrlline + * + * Copyright (C) 2011 Tavmjong Bah + * Copyright (C) 2007 Johan Engelen + * Copyright (C) 1999-2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "display/sp-ctrlline.h" + +class SPItem; + +#define SP_TYPE_CTRLCURVE (sp_ctrlcurve_get_type()) +#define SP_CTRLCURVE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SP_TYPE_CTRLCURVE, SPCtrlCurve)) +#define SP_IS_CTRLCURVE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SP_TYPE_CTRLCURVE)) + +struct SPCtrlCurve : public SPCtrlLine { + void setCoords( gdouble x0, gdouble y0, gdouble x1, gdouble y1, + gdouble x2, gdouble y2, gdouble x3, gdouble y3 ); + + void setCoords( Geom::Point const &q0, Geom::Point const &q1, + Geom::Point const &q2, Geom::Point const &q3); + + Geom::Point p0, p1, p2, p3; + + int corner0; // Used to store index of corner for finding dragger. + int corner1; +}; + +GType sp_ctrlcurve_get_type(); + +struct SPCtrlCurveClass : public SPCtrlLineClass{}; + +#endif // SEEN_INKSCAPE_CTRLCURVE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/sp-ctrlline.cpp b/src/display/sp-ctrlline.cpp new file mode 100644 index 0000000..504e3d0 --- /dev/null +++ b/src/display/sp-ctrlline.cpp @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Simple straight line + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2007 Johan Engelen + * Copyright (C) 1999-2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +/* + * TODO: + * Draw it by hand - we really do not need aa stuff for it + * + */ + +#include "display/sp-ctrlline.h" +#include "display/sp-canvas-util.h" +#include "display/cairo-utils.h" +#include "color.h" +#include "display/sp-canvas.h" + +namespace { + +void sp_ctrlline_destroy(SPCanvasItem *object); + +void sp_ctrlline_update(SPCanvasItem *item, Geom::Affine const &affine, unsigned int flags); +void sp_ctrlline_render(SPCanvasItem *item, SPCanvasBuf *buf); + +} // namespace + +G_DEFINE_TYPE(SPCtrlLine, sp_ctrlline, SP_TYPE_CANVAS_ITEM); + +static void sp_ctrlline_class_init(SPCtrlLineClass *klass) +{ + klass->destroy = sp_ctrlline_destroy; + + klass->update = sp_ctrlline_update; + klass->render = sp_ctrlline_render; +} + +static void sp_ctrlline_init(SPCtrlLine *ctrlline) +{ + ctrlline->rgba = 0x0000ff7f; + ctrlline->s[Geom::X] = ctrlline->s[Geom::Y] = ctrlline->e[Geom::X] = ctrlline->e[Geom::Y] = 0.0; + ctrlline->item=nullptr; + ctrlline->is_fill = true; +} + +namespace { +void sp_ctrlline_destroy(SPCanvasItem *object) +{ + g_return_if_fail(object != nullptr); + g_return_if_fail(SP_IS_CTRLLINE(object)); + + SPCtrlLine *ctrlline = SP_CTRLLINE(object); + + ctrlline->item = nullptr; + + if(SP_CANVAS_ITEM_CLASS (sp_ctrlline_parent_class)->destroy) { + SP_CANVAS_ITEM_CLASS (sp_ctrlline_parent_class)->destroy(object); + } +} + +void sp_ctrlline_render(SPCanvasItem *item, SPCanvasBuf *buf) +{ + SPCtrlLine *cl = SP_CTRLLINE(item); + + if (!buf->ct) { + return; + } + + if (cl->s == cl->e) { + return; + } + + Geom::Point s = cl->s * cl->affine; + Geom::Point e = cl->e * cl->affine; + + ink_cairo_set_source_rgba32(buf->ct, 0xffffff7f); + cairo_set_line_width(buf->ct, 2); + cairo_new_path(buf->ct); + + cairo_move_to(buf->ct, s[Geom::X] - buf->rect.left(), s[Geom::Y] - buf->rect.top()); + cairo_line_to(buf->ct, e[Geom::X] - buf->rect.left(), e[Geom::Y] - buf->rect.top()); + + cairo_stroke(buf->ct); + + + ink_cairo_set_source_rgba32(buf->ct, cl->rgba); + cairo_set_line_width(buf->ct, 1); + cairo_new_path(buf->ct); + + cairo_move_to(buf->ct, s[Geom::X] - buf->rect.left(), s[Geom::Y] - buf->rect.top()); + cairo_line_to(buf->ct, e[Geom::X] - buf->rect.left(), e[Geom::Y] - buf->rect.top()); + + cairo_stroke(buf->ct); +} + +void sp_ctrlline_update(SPCanvasItem *item, Geom::Affine const &affine, unsigned int flags) +{ + SPCtrlLine *cl = SP_CTRLLINE(item); + + item->canvas->requestRedraw(item->x1, item->y1, item->x2, item->y2); + + if (SP_CANVAS_ITEM_CLASS(sp_ctrlline_parent_class)->update) { + SP_CANVAS_ITEM_CLASS(sp_ctrlline_parent_class)->update(item, affine, flags); + } + + sp_canvas_item_reset_bounds(item); + + cl->affine = affine; + + if (cl->s == cl->e) { + item->x1 = item->x2 = item->y1 = item->y2 = 0; + } else { + + Geom::Point s = cl->s * affine; + Geom::Point e = cl->e * affine; + + item->x1 = round(MIN(s[Geom::X], e[Geom::X]) - 1); + item->y1 = round(MIN(s[Geom::Y], e[Geom::Y]) - 1); + item->x2 = round(MAX(s[Geom::X], e[Geom::X]) + 1); + item->y2 = round(MAX(s[Geom::Y], e[Geom::Y]) + 1); + + item->canvas->requestRedraw(item->x1, item->y1, item->x2, item->y2); + } +} + +} // namespace + +void SPCtrlLine::setRgba32(guint32 rgba) +{ + if (rgba != this->rgba) { + this->rgba = rgba; + canvas->requestRedraw(x1, y1, x2, y2); + } +} + +#define EPSILON 1e-6 +#define DIFFER(a,b) (fabs ((a) - (b)) > EPSILON) + +void SPCtrlLine::setCoords(gdouble x0, gdouble y0, gdouble x1, gdouble y1) +{ + if (DIFFER(x0, s[Geom::X]) || DIFFER(y0, s[Geom::Y]) || DIFFER(x1, e[Geom::X]) || DIFFER(y1, e[Geom::Y])) { + s[Geom::X] = x0; + s[Geom::Y] = y0; + e[Geom::X] = x1; + e[Geom::Y] = y1; + sp_canvas_item_request_update(this); + } +} + +void SPCtrlLine::setCoords(Geom::Point const &start, Geom::Point const &end) +{ + setCoords(start[0], start[1], end[0], end[1]); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/sp-ctrlline.h b/src/display/sp-ctrlline.h new file mode 100644 index 0000000..5ecd1ca --- /dev/null +++ b/src/display/sp-ctrlline.h @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_INKSCAPE_CTRLLINE_H +#define SEEN_INKSCAPE_CTRLLINE_H + +/* + * Simple straight line + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2012 Authors + * Copyright (C) 2007 Johan Engelen + * Copyright (C) 1999-2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-canvas-item.h" + +class SPItem; + +#define SP_TYPE_CTRLLINE (sp_ctrlline_get_type()) +#define SP_CTRLLINE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SP_TYPE_CTRLLINE, SPCtrlLine)) +#define SP_IS_CTRLLINE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SP_TYPE_CTRLLINE)) + +struct SPCtrlLine : public SPCanvasItem { + void setRgba32(guint32 rgba); + + void setCoords(gdouble x0, gdouble y0, gdouble x1, gdouble y1); + + void setCoords(Geom::Point const &start, Geom::Point const &end); + + + SPItem *item; // the item to which this line belongs in some sense; may be NULL for some users + bool is_fill; // fill or stroke... used with meshes. + + guint32 rgba; + Geom::Point s; + Geom::Point e; + Geom::Affine affine; +}; + +GType sp_ctrlline_get_type(); + +struct SPCtrlLineClass : public SPCanvasItemClass{}; + + + +#endif // SEEN_INKSCAPE_CTRLLINE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/sp-ctrlquadr.cpp b/src/display/sp-ctrlquadr.cpp new file mode 100644 index 0000000..ffca867 --- /dev/null +++ b/src/display/sp-ctrlquadr.cpp @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Quadrilateral + * + * Authors: + * bulia byak + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2005 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-canvas-item.h" +#include "sp-canvas.h" +#include "sp-canvas-util.h" +#include "sp-ctrlquadr.h" +#include "display/cairo-utils.h" +#include "color.h" + +struct SPCtrlQuadr : public SPCanvasItem{ + guint32 rgba; + Geom::Point p1, p2, p3, p4; + Geom::Affine affine; +}; + +struct SPCtrlQuadrClass : public SPCanvasItemClass{}; + +static void sp_ctrlquadr_destroy(SPCanvasItem *object); + +static void sp_ctrlquadr_update (SPCanvasItem *item, Geom::Affine const &affine, unsigned int flags); +static void sp_ctrlquadr_render (SPCanvasItem *item, SPCanvasBuf *buf); + +G_DEFINE_TYPE(SPCtrlQuadr, sp_ctrlquadr, SP_TYPE_CANVAS_ITEM); + +static void +sp_ctrlquadr_class_init (SPCtrlQuadrClass *klass) +{ + SPCanvasItemClass *item_class = SP_CANVAS_ITEM_CLASS(klass); + + item_class->destroy = sp_ctrlquadr_destroy; + item_class->update = sp_ctrlquadr_update; + item_class->render = sp_ctrlquadr_render; +} + +static void +sp_ctrlquadr_init (SPCtrlQuadr *ctrlquadr) +{ + ctrlquadr->rgba = 0x000000ff; + ctrlquadr->p1 = Geom::Point(0, 0); + ctrlquadr->p2 = Geom::Point(0, 0); + ctrlquadr->p3 = Geom::Point(0, 0); + ctrlquadr->p4 = Geom::Point(0, 0); +} + +static void sp_ctrlquadr_destroy(SPCanvasItem *object) +{ + g_return_if_fail (object != nullptr); + g_return_if_fail (SP_IS_CTRLQUADR (object)); + + if (SP_CANVAS_ITEM_CLASS(sp_ctrlquadr_parent_class)->destroy) + (* SP_CANVAS_ITEM_CLASS(sp_ctrlquadr_parent_class)->destroy) (object); +} + +static void +sp_ctrlquadr_render (SPCanvasItem *item, SPCanvasBuf *buf) +{ + SPCtrlQuadr *cq = SP_CTRLQUADR (item); + + if (!buf->ct) + return; + + // RGB / BGR + cairo_new_path(buf->ct); + + Geom::Point min = buf->rect.min(); + + Geom::Point p1 = (cq->p1 * cq->affine) - min; + Geom::Point p2 = (cq->p2 * cq->affine) - min; + Geom::Point p3 = (cq->p3 * cq->affine) - min; + Geom::Point p4 = (cq->p4 * cq->affine) - min; + + cairo_move_to(buf->ct, p1[Geom::X], p1[Geom::Y]); + cairo_line_to(buf->ct, p2[Geom::X], p2[Geom::Y]); + cairo_line_to(buf->ct, p3[Geom::X], p3[Geom::Y]); + cairo_line_to(buf->ct, p4[Geom::X], p4[Geom::Y]); + cairo_line_to(buf->ct, p1[Geom::X], p1[Geom::Y]); + + // FIXME: this is supposed to draw inverse but cairo apparently is unable of this trick :( + //cairo_set_operator (buf->ct, CAIRO_OPERATOR_XOR); + + cairo_set_source_rgba(buf->ct, SP_RGBA32_B_F(cq->rgba), SP_RGBA32_G_F(cq->rgba), SP_RGBA32_R_F(cq->rgba), SP_RGBA32_A_F(cq->rgba)); + cairo_fill(buf->ct); +} + +#define MIN4(a,b,c,d)\ + ((a <= b && a <= c && a <= d) ? a : \ + (b <= a && b <= c && b <= d) ? b : \ + (c <= a && c <= b && c <= d) ? c : \ + d ) + +#define MAX4(a,b,c,d)\ + ((a >= b && a >= c && a >= d) ? a : \ + (b >= a && b >= c && b >= d) ? b : \ + (c >= a && c >= b && c >= d) ? c : \ + d ) + + +static void sp_ctrlquadr_update(SPCanvasItem *item, Geom::Affine const &affine, unsigned int flags) +{ + SPCtrlQuadr *cq = SP_CTRLQUADR(item); + + item->canvas->requestRedraw((int)item->x1, (int)item->y1, (int)item->x2, (int)item->y2); + + if (SP_CANVAS_ITEM_CLASS(sp_ctrlquadr_parent_class)->update) { + SP_CANVAS_ITEM_CLASS(sp_ctrlquadr_parent_class)->update(item, affine, flags); + } + + sp_canvas_item_reset_bounds (item); + + cq->affine = affine; + + Geom::Point p1(cq->p1 * affine); + Geom::Point p2(cq->p2 * affine); + Geom::Point p3(cq->p3 * affine); + Geom::Point p4(cq->p4 * affine); + + item->x1 = (int)(MIN4(p1[Geom::X], p2[Geom::X], p3[Geom::X], p4[Geom::X])); + item->y1 = (int)(MIN4(p1[Geom::Y], p2[Geom::Y], p3[Geom::Y], p4[Geom::Y])); + item->x2 = (int)(MAX4(p1[Geom::X], p2[Geom::X], p3[Geom::X], p4[Geom::X])); + item->y2 = (int)(MAX4(p1[Geom::Y], p2[Geom::Y], p3[Geom::Y], p4[Geom::Y])); + + item->canvas->requestRedraw((int)item->x1, (int)item->y1, (int)item->x2, (int)item->y2); +} + +void +sp_ctrlquadr_set_rgba32 (SPCtrlQuadr *cl, guint32 rgba) +{ + g_return_if_fail (cl != nullptr); + g_return_if_fail (SP_IS_CTRLQUADR (cl)); + + if (rgba != cl->rgba) { + SPCanvasItem *item; + cl->rgba = rgba; + item = SP_CANVAS_ITEM (cl); + item->canvas->requestRedraw((int)item->x1, (int)item->y1, (int)item->x2, (int)item->y2); + } +} + +void +sp_ctrlquadr_set_coords (SPCtrlQuadr *cl, Geom::Point p1, Geom::Point p2, Geom::Point p3, Geom::Point p4) +{ + g_return_if_fail (cl != nullptr); + g_return_if_fail (SP_IS_CTRLQUADR (cl)); + + if (p1 != cl->p1 || p2 != cl->p2 || p3 != cl->p3 || p4 != cl->p4) { + cl->p1 = p1; + cl->p2 = p2; + cl->p3 = p3; + cl->p4 = p4; + sp_canvas_item_request_update (SP_CANVAS_ITEM (cl)); + } +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/sp-ctrlquadr.h b/src/display/sp-ctrlquadr.h new file mode 100644 index 0000000..502227a --- /dev/null +++ b/src/display/sp-ctrlquadr.h @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __INKSCAPE_CTRLQUADR_H__ +#define __INKSCAPE_CTRLQUADR_H__ + +/* + * Quadrilateral + * + * Authors: + * bulia byak + * + * Copyright (C) 2005 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/geom.h> + +#define SP_TYPE_CTRLQUADR (sp_ctrlquadr_get_type ()) +#define SP_CTRLQUADR(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SP_TYPE_CTRLQUADR, SPCtrlQuadr)) +#define SP_IS_CTRLQUADR(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SP_TYPE_CTRLQUADR)) + +struct SPCtrlQuadr; +struct SPCtrlQuadrClass; + +GType sp_ctrlquadr_get_type (); + +void sp_ctrlquadr_set_rgba32 (SPCtrlQuadr *cl, guint32 rgba); +void sp_ctrlquadr_set_coords (SPCtrlQuadr *cl, const Geom::Point p1, const Geom::Point p2, const Geom::Point p3, const Geom::Point p4); + + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : |