diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 11:50:49 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 11:50:49 +0000 |
commit | c853ffb5b2f75f5a889ed2e3ef89b818a736e87a (patch) | |
tree | 7d13a0883bb7936b84d6ecdd7bc332b41ed04bee /src/display | |
parent | Initial commit. (diff) | |
download | inkscape-c853ffb5b2f75f5a889ed2e3ef89b818a736e87a.tar.xz inkscape-c853ffb5b2f75f5a889ed2e3ef89b818a736e87a.zip |
Adding upstream version 1.3+ds.upstream/1.3+dsupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/display')
123 files changed, 24875 insertions, 0 deletions
diff --git a/src/display/CMakeLists.txt b/src/display/CMakeLists.txt new file mode 100644 index 0000000..70214bb --- /dev/null +++ b/src/display/CMakeLists.txt @@ -0,0 +1,134 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +set(display_SRC + cairo-utils.cpp + curve.cpp + drawing-context.cpp + drawing-group.cpp + drawing-image.cpp + drawing-item.cpp + drawing-paintserver.cpp + drawing-pattern.cpp + drawing-shape.cpp + drawing-surface.cpp + drawing-text.cpp + drawing.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 + + control/canvas-temporary-item-list.cpp + control/canvas-temporary-item.cpp + control/snap-indicator.cpp + + control/canvas-item.cpp + control/canvas-item-bpath.cpp + control/canvas-item-catchall.cpp + control/canvas-item-context.cpp + control/canvas-item-ctrl.cpp + control/canvas-item-curve.cpp + control/canvas-item-drawing.cpp + control/canvas-item-grid.cpp + control/canvas-item-group.cpp + control/canvas-item-guideline.cpp + control/canvas-item-quad.cpp + control/canvas-item-rect.cpp + control/canvas-item-text.cpp + control/canvas-page.cpp + + # ------- + # Headers + cairo-templates.h + cairo-utils.h + curve.h + dither-lock.h + drawing-context.h + drawing-group.h + drawing-image.h + drawing-item.h + drawing-item-ptr.h + drawing-paintserver.h + drawing-pattern.h + drawing-shape.h + drawing-surface.h + drawing-text.h + drawing.h + initlock.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 + tags.h + + control/canvas-temporary-item-list.h + control/canvas-temporary-item.h + control/snap-indicator.h + + control/canvas-item.h + control/canvas-item-bpath.h + control/canvas-item-buffer.h + control/canvas-item-catchall.h + control/canvas-item-context.h + control/canvas-item-ctrl.h + control/canvas-item-curve.h + control/canvas-item-drawing.h + control/canvas-item-enums.h + control/canvas-item-grid.h + control/canvas-item-group.h + control/canvas-item-guideline.h + control/canvas-item-ptr.h + control/canvas-item-quad.h + control/canvas-item-rect.h + control/canvas-item-text.h + control/canvas-page.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..0482a4e --- /dev/null +++ b/src/display/README @@ -0,0 +1,26 @@ + + +This directory contains code for: rendering graphics on the canvas, +"picking" objects on the canvas based on closeness to cursor, directing +events (mouse button, key press, etc.) to the correct object. + +The code can be divided into two types: handling SVG elements +(DrawingItem and derived classes) and handling controlled items +(SPCanvasItem and derived structures/classes). + +Currently we rely on the "Cairo" graphics library for rendering. + +DrawingItem: + + Item belonging to the drawing. These are rendered via the + CanvasArena canvas item. + +CanvasItem: + + See README in control sub-directory. + +To do: + +* Split into three directories: drawing, control, common (partially done). +* Rename control directory to canvas directory. +* Remove SPCurve. diff --git a/src/display/cairo-templates.h b/src/display/cairo-templates.h new file mode 100644 index 0000000..d23343e --- /dev/null +++ b/src/display/cairo-templates.h @@ -0,0 +1,658 @@ +// 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> +// single-threaded operation if the number of pixels is below this threshold +static const int OPENMP_THRESHOLD = 2048; +#endif + +#include <cmath> +#include <algorithm> +#include <cairo.h> +#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 + int numOfThreads = get_num_filter_threads(); + #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 + + 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 + int numOfThreads = get_num_filter_threads(); + #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 if (bppout == 1) { + // bppin == 1, bppout == 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 *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; + } + } + } + } else { + // bppin == 1, bppout == 4 + // used in COLORMATRIX_MATRIX when in is NR_FILTER_SOURCEALPHA + 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]; + out_data[i] = filter(guint32(in_p) << 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; + guint32 *out_p = out_data + i * strideout/4; + for (int j = 0; j < w; ++j) { + out_p[j] = filter(guint32(in_p[j]) << 24); + } + } + } + } + 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; + int numOfThreads = get_num_filter_threads(); + #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; +}; + +// 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..21ffb79 --- /dev/null +++ b/src/display/cairo-utils.cpp @@ -0,0 +1,1934 @@ +// 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 <2geom/affine.h> +#include <2geom/curves.h> +#include <2geom/path-sink.h> +#include <2geom/path.h> +#include <2geom/pathvector.h> +#include <2geom/point.h> +#include <2geom/sbasis-to-bezier.h> +#include <2geom/transforms.h> +#include <atomic> +#include <boost/algorithm/string.hpp> +#include <boost/operators.hpp> +#include <boost/optional/optional.hpp> +#include <gdk-pixbuf/gdk-pixbuf.h> +#include <glib/gstdio.h> +#include <glibmm/fileutils.h> +#include <stdexcept> + +#include "cairo-templates.h" +#include "color.h" +#include "document.h" +#include "helper/pixbuf-ops.h" +#include "preferences.h" +#include "ui/util.h" +#include "util/scope_exit.h" +#include "util/units.h" + +#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 17, 6) +#define CAIRO_HAS_HAIRLINE +#endif + +/** + * 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 + */ +static cairo_user_data_key_t ink_color_interpolation_key; + +namespace Inkscape { + +/* 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), + ink_cairo_pixbuf_cleanup, s)) + , _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) { + 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 + +/** + * Create a new Pixbuf with the image cropped to the given area. + */ +Pixbuf *Pixbuf::cropTo(const Geom::IntRect &area) const +{ + GdkPixbuf *copy = nullptr; + auto source = _pixbuf; + if (_pixel_format == PF_CAIRO) { + // This copies twice, but can be run on const, which is useful. + copy = gdk_pixbuf_copy(_pixbuf); + ensure_pixbuf(copy); + source = copy; + } + auto cropped = gdk_pixbuf_new_subpixbuf(source, + area.left(), area.top(), area.width(), area.height()); + if (copy) { + // Clean up our pixbuf copy + g_object_unref(copy); + } + return new Pixbuf(cropped); +} + +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); + bool has_ori = Pixbuf::get_embedded_orientation(buf) != Geom::identity(); + buf = Pixbuf::apply_embedded_orientation(buf); + pixbuf = new Pixbuf(buf); + + if (!has_ori) { + // We DO NOT want to store the original data if it contains orientation + // data since many exports that will use the surface do not handle it. + // TODO: Preserve the original meta data from the file by stripping out + // orientation but keeping all other aspects of the raster. + 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); + std::unique_ptr<SPDocument> svgDoc( + SPDocument::createNewDocFromMem(reinterpret_cast<gchar const *>(decoded), decoded_len, false)); + // Check the document loaded properly + if (!svgDoc || !svgDoc->getRoot()) { + 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"); + if (svgWidth_px < 0 || svgHeight_px < 0) { + g_warning("create_from_data_uri: malformed document: svgWidth_px=%f, svgHeight_px=%f", svgWidth_px, + svgHeight_px); + return nullptr; + } + + assert(!pixbuf); + Geom::Rect area(0, 0, svgWidth_px, svgHeight_px); + pixbuf = sp_generate_internal_bitmap(svgDoc.get(), area, dpi); + GdkPixbuf const *buf = pixbuf->getPixbufRaw(); + + // Tidy up + if (buf == nullptr) { + std::cerr << "Pixbuf::create_from_data: failed to load contents: " << std::endl; + delete pixbuf; + g_free(decoded); + return nullptr; + } else { + 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; +} + +GdkPixbuf *Pixbuf::apply_embedded_orientation(GdkPixbuf *buf) +{ + GdkPixbuf *old = buf; + buf = gdk_pixbuf_apply_embedded_orientation(buf); + g_object_unref(old); + return buf; +} + +/** + * Gets any available orientation data and returns it as an affine. + */ +Geom::Affine Pixbuf::get_embedded_orientation(GdkPixbuf *buf) +{ + // See gdk_pixbuf_apply_embedded_orientation in gdk-pixbuf + if (auto opt_str = gdk_pixbuf_get_option(buf, "orientation")) { + switch ((int)g_ascii_strtoll(opt_str, NULL, 10)) { + case 2: // Flip Horz + return Geom::Scale(-1, 1); + case 3: // +180 Rotate + return Geom::Scale(-1, -1); + case 4: // Flip Vert + return Geom::Scale(1, -1); + case 5: // +90 Rotate & Flip Horz + return Geom::Rotate(90) * Geom::Scale(-1, 1); + case 6: // +90 Rotate + return Geom::Rotate(90); + case 7: // +90 Rotate * Flip Vert + return Geom::Rotate(90) * Geom::Scale(1, -1); + case 8: // -90 Rotate + return Geom::Rotate(-90); + default: + break; + + } + } + return Geom::identity(); +} + +Pixbuf *Pixbuf::create_from_buffer(std::string const &buffer, double svgdpi, std::string const &fn) +{ +#if GLIB_CHECK_VERSION(2,67,3) + auto datacopy = (gchar *)g_memdup2(buffer.data(), buffer.size()); +#else + auto datacopy = (gchar *)g_memdup(buffer.data(), buffer.size()); +#endif + 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) +{ + bool has_ori = false; + 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")) { + std::unique_ptr<SPDocument> svgDoc(SPDocument::createNewDocFromMem(data, len, true, fn.c_str())); + + // Check the document loaded properly + if (!svgDoc || !svgDoc->getRoot()) { + 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"); + if (svgWidth_px < 0 || svgHeight_px < 0) { + g_warning("create_from_buffer: malformed document: svgWidth_px=%f, svgHeight_px=%f", svgWidth_px, + svgHeight_px); + return nullptr; + } + + Geom::Rect area(0, 0, svgWidth_px, svgHeight_px); + pb = sp_generate_internal_bitmap(svgDoc.get(), area, dpi); + buf = pb->getPixbufRaw(); + + // Tidy up + if (buf == nullptr) { + delete pb; + return nullptr; + } + buf = Pixbuf::apply_embedded_orientation(buf); + 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) { + // gdk_pixbuf_loader_get_pixbuf returns a borrowed reference + g_object_ref(buf); + has_ori = Pixbuf::get_embedded_orientation(buf) != Geom::identity(); + buf = Pixbuf::apply_embedded_orientation(buf); + pb = new Pixbuf(buf); + } + } + + if (pb) { + pb->_path = fn; + if (is_svg) { + pb->_setMimeData((guchar *) data, len, "svg"); + } else if(!has_ori) { + // We DO NOT want to store the original data if it contains orientation + // data since many exports that will use the surface do not handle it. + 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 { + 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; +} + +GdkPixbuf *Pixbuf::getPixbufRaw() const +{ + assert(_pixel_format == 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() +{ + ensurePixelFormat(PF_CAIRO); + return _surface; +} + +cairo_surface_t *Pixbuf::getSurfaceRaw() const +{ + assert(_pixel_format == 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() +{ + return Cairo::RefPtr<Cairo::Surface>(new Cairo::Surface(getSurfaceRaw(), false)); +} + +/** 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()); + } +} + +/** + * Convert the internal pixel format between CAIRO and GDK formats. + */ +void Pixbuf::ensurePixelFormat(PixelFormat fmt) +{ + if (fmt == PF_CAIRO && _pixel_format == PF_GDK) { + ensure_argb32(_pixbuf); + _pixel_format = fmt; + } else if (fmt == PF_GDK && _pixel_format == PF_CAIRO) { + ensure_pixbuf(_pixbuf); + _pixel_format = fmt; + } else if (fmt != _pixel_format) { + g_assert_not_reached(); + } +} + +/** + * 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 Pixbuf::ensure_argb32(GdkPixbuf *pb) +{ + 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)); +} + +/** + * Converts GdkPixbuf's data back to its native format. + * Once this is done, the pixbuf can be used with GTK again. + */ +void Pixbuf::ensure_pixbuf(GdkPixbuf *pb) +{ + 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)); +} + +} // 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 const &view, bool optimize_stroke) +{ + using Geom::X; + using Geom::Y; + + unsigned order = 0; + if (auto 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: + { + auto quadratic_bezier = static_cast<Geom::QuadraticBezier const*>(&c); + std::array<Geom::Point, 3> points; + for (int i = 0; i < 3; i++) { + points[i] = quadratic_bezier->controlPoint(i) * 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: + { + auto cubic_bezier = static_cast<Geom::CubicBezier const*>(&c); + std::array<Geom::Point, 4> points; + for (int i = 0; i < 4; i++) { + points[i] = cubic_bezier->controlPoint(i); + } + //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 *arc = dynamic_cast<Geom::EllipticalArc const*>(&c)) { + if (arc->isChord()) { + Geom::Point endPoint(arc->finalPoint()); + cairo_line_to(cr, endPoint[0], endPoint[1]); + } else { + Geom::Affine xform = arc->unitCircleTransform() * trans; + // Don't draw anything if the angle is borked + if(std::isnan(arc->initialAngle()) || std::isnan(arc->finalAngle())) { + g_warning("Bad angle while drawing EllipticalArc"); + break; + } + + // Apply the transformation to the current context + auto cm = geom_to_cairo(xform); + + cairo_save(cr); + cairo_transform(cr, &cm); + + // Draw the circle + if (arc->sweep()) { + cairo_arc(cr, 0, 0, 1, arc->initialAngle(), arc->finalAngle()); + } else { + cairo_arc_negative(cr, 0, 0, 1, arc->initialAngle(), arc->finalAngle()); + } + // 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); + } +} + +/* + * Pulls out the last cairo path context and reconstitutes it + * into a local geom path vector for inkscape use. + * + * @param ct - The cairo context + * + * @returns an optioal Geom::PathVector object + */ +std::optional<Geom::PathVector> extract_pathvector_from_cairo(cairo_t *ct) +{ + cairo_path_t *path = cairo_copy_path(ct); + if (!path) + return std::nullopt; + + auto path_freer = scope_exit([&] { cairo_path_destroy(path); }); + + Geom::PathBuilder res; + auto end = &path->data[path->num_data]; + for (auto p = &path->data[0]; p < end; p += p->header.length) { + switch (p->header.type) { + case CAIRO_PATH_MOVE_TO: + if (p->header.length != 2) + return std::nullopt; + res.moveTo(Geom::Point(p[1].point.x, p[1].point.y)); + break; + + case CAIRO_PATH_LINE_TO: + if (p->header.length != 2) + return std::nullopt; + res.lineTo(Geom::Point(p[1].point.x, p[1].point.y)); + break; + + case CAIRO_PATH_CURVE_TO: + if (p->header.length != 4) + return std::nullopt; + res.curveTo(Geom::Point(p[1].point.x, p[1].point.y), Geom::Point(p[2].point.x, p[2].point.y), + Geom::Point(p[3].point.x, p[3].point.y)); + break; + + case CAIRO_PATH_CLOSE_PATH: + if (p->header.length != 1) + return std::nullopt; + res.closePath(); + break; + default: + return std::nullopt; + } + } + + res.flush(); + return res.peek(); +} + +static std::atomic<int> num_filter_threads = 4; + +int get_num_filter_threads() +{ + return num_filter_threads.load(std::memory_order_relaxed); +} + +void set_num_filter_threads(int n) +{ + num_filter_threads.store(n, std::memory_order_relaxed); +} + +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); +} + +void +ink_cairo_set_hairline(cairo_t *ct) +{ +#ifdef CAIRO_HAS_HAIRLINE + cairo_set_hairline(ct, true); +#else + // As a backup, use a device unit of 1 + double x = 1.0, y = 0.0; + cairo_device_to_user_distance(ct, &x, &y); + cairo_set_line_width(ct, std::hypot(x, y)); +#endif +} + +void ink_cairo_set_dither(cairo_surface_t *surface, bool enabled) +{ +#ifdef CAIRO_HAS_DITHER + cairo_image_surface_set_dither(surface, enabled ? CAIRO_DITHER_BEST : CAIRO_DITHER_NONE); +#endif +} + +/** + * 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 an exact copy of an image surface. + */ +Cairo::RefPtr<Cairo::ImageSurface> +ink_cairo_surface_copy(Cairo::RefPtr<Cairo::ImageSurface> surface ) +{ + int width = surface->get_width(); + int height = surface->get_height(); + int stride = surface->get_stride(); + auto new_surface = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, width, height); // device scale? + + surface->flush(); + memcpy(new_surface->get_data(), surface->get_data(), stride * height); + new_surface->mark_dirty(); // Clear caches. Mandatory after messing directly with contents. + + return new_surface; +} + +/** + * 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; +} +// We extract colors from pattern background, if we need to extract sometimes from a gradient we can add +// a extra parameter with the spot number and use cairo_pattern_get_color_stop_rgba +// also if the pattern is a image we can pass a boolean like solid = false to get the color by image average ink_cairo_surface_average_color +guint32 ink_cairo_pattern_get_argb32(cairo_pattern_t *pattern) +{ + double red = 0; + double green = 0; + double blue = 0; + double alpha = 0; + auto status = cairo_pattern_get_rgba(pattern, &red, &green, &blue, &alpha); + if (status != CAIRO_STATUS_PATTERN_TYPE_MISMATCH) { + // in ARGB32 format + return SP_RGBA32_F_COMPOSE(alpha, red, green, blue); + } + + cairo_surface_t *surface; + status = cairo_pattern_get_surface (pattern, &surface); + if (status != CAIRO_STATUS_PATTERN_TYPE_MISMATCH) { + // first pixel only + auto *pxbsurface = cairo_image_surface_get_data(surface); + return *reinterpret_cast<guint32 const *>(pxbsurface); + } + return 0; +} + +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 ); +} + +static uint32_t srgb_to_linear_argb32(uint32_t in) +{ + EXTRACT_ARGB32(in, a, r, g, b); + 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; +} + +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); + + ink_cairo_surface_filter(surface, surface, srgb_to_linear_argb32); + + return width * height; +} + +static uint32_t linear_to_srgb_argb32(uint32_t in) +{ + EXTRACT_ARGB32(in, a, r, g, b); + 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; +} + +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/ + + switch (cairo_operator) { + case CAIRO_OPERATOR_MULTIPLY: + return SP_CSS_BLEND_MULTIPLY; + case CAIRO_OPERATOR_SCREEN: + return SP_CSS_BLEND_SCREEN; + case CAIRO_OPERATOR_DARKEN: + return SP_CSS_BLEND_DARKEN; + case CAIRO_OPERATOR_LIGHTEN: + return SP_CSS_BLEND_LIGHTEN; + case CAIRO_OPERATOR_OVERLAY: + return SP_CSS_BLEND_OVERLAY; + case CAIRO_OPERATOR_COLOR_DODGE: + return SP_CSS_BLEND_COLORDODGE; + case CAIRO_OPERATOR_COLOR_BURN: + return SP_CSS_BLEND_COLORBURN; + case CAIRO_OPERATOR_HARD_LIGHT: + return SP_CSS_BLEND_HARDLIGHT; + case CAIRO_OPERATOR_SOFT_LIGHT: + return SP_CSS_BLEND_SOFTLIGHT; + case CAIRO_OPERATOR_DIFFERENCE: + return SP_CSS_BLEND_DIFFERENCE; + case CAIRO_OPERATOR_EXCLUSION: + return SP_CSS_BLEND_EXCLUSION; + case CAIRO_OPERATOR_HSL_HUE: + return SP_CSS_BLEND_HUE; + case CAIRO_OPERATOR_HSL_SATURATION: + return SP_CSS_BLEND_SATURATION; + case CAIRO_OPERATOR_HSL_COLOR: + return SP_CSS_BLEND_COLOR; + case CAIRO_OPERATOR_HSL_LUMINOSITY: + return SP_CSS_BLEND_LUMINOSITY; + case CAIRO_OPERATOR_OVER: + return SP_CSS_BLEND_NORMAL; + default: + return SP_CSS_BLEND_NORMAL; + } +} + +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/ + + switch (css_blend) { + 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; + 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: + return CAIRO_OPERATOR_OVER; + default: + g_error("Invalid SPBlendMode %d", css_blend); + return CAIRO_OPERATOR_OVER; + } +} + +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); + + ink_cairo_surface_filter(surface, surface, linear_to_srgb_argb32); + + return width * height; +} + +cairo_pattern_t * +ink_cairo_pattern_create_checkerboard(guint32 rgba, bool use_alpha) +{ + 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); + if (use_alpha) { + // use alpha to show opacity cover checkerboard + double a = SP_RGBA32_A_F(rgba); + if (a > 0.0) { + cairo_set_operator(ct, CAIRO_OPERATOR_OVER); + cairo_rectangle(ct, 0, 0, 2 * w, 2 * h); + cairo_set_source_rgba(ct, r, g, b, a); + 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; +} + + +/** + * Draw drop shadow around the 'rect' with given 'size' and 'color'; shadow extends to the right and bottom of rect. + */ +void ink_cairo_draw_drop_shadow(const Cairo::RefPtr<Cairo::Context> &ctx, const Geom::Rect& rect, double size, guint32 color, double color_alpha) { + // draw fake drop shadow built from gradients + const auto r = SP_RGBA32_R_F(color); + const auto g = SP_RGBA32_G_F(color); + const auto b = SP_RGBA32_B_F(color); + const auto a = color_alpha; + const Geom::Point corners[] = { rect.corner(0), rect.corner(1), rect.corner(2), rect.corner(3) }; + // space for gradient shadow + double sw = size; + double half = sw / 2; + using Geom::X; + using Geom::Y; + // 8 gradients total: 4 sides + 4 corners + auto grad_top = Cairo::LinearGradient::create(0, corners[0][Y] + half, 0, corners[0][Y] - half); + auto grad_right = Cairo::LinearGradient::create(corners[1][X], 0, corners[1][X] + sw, 0); + auto grad_bottom = Cairo::LinearGradient::create(0, corners[2][Y], 0, corners[2][Y] + sw); + auto grad_left = Cairo::LinearGradient::create(corners[0][X] + half, 0, corners[0][X] - half, 0); + auto grad_btm_right = Cairo::RadialGradient::create(corners[2][X], corners[2][Y], 0, corners[2][X], corners[2][Y], sw); + auto grad_top_right = Cairo::RadialGradient::create(corners[1][X], corners[1][Y] + half, 0, corners[1][X], corners[1][Y] + half, sw); + auto grad_btm_left = Cairo::RadialGradient::create(corners[3][X] + half, corners[3][Y], 0, corners[3][X] + half, corners[3][Y], sw); + auto grad_top_left = Cairo::RadialGradient::create(corners[0][X], corners[0][Y], 0, corners[0][X], corners[0][Y], half); + const int N = 15; // number of gradient stops; stops used to make it non-linear + // using easing function here: (exp(a*(1-t)) - 1) / (exp(a) - 1); + // it has a nice property of growing from 0 to 1 for t in [0..1] + const auto A = 4.0; // this coefficient changes how steep the curve is and controls shadow drop-off + const auto denominator = exp(A) - 1; + for (int i = 0; i <= N; ++i) { + auto pos = static_cast<double>(i) / N; + // exponential decay for drop shadow - long tail, with values from 100% down to 0% opacity + auto t = 1 - pos; // reverse 't' so alpha drops from 1 to 0 + auto alpha = (exp(A * t) - 1) / denominator; + grad_top->add_color_stop_rgba(pos, r, g, b, alpha * a); + grad_bottom->add_color_stop_rgba(pos, r, g, b, alpha * a); + grad_right->add_color_stop_rgba(pos, r, g, b, alpha * a); + grad_left->add_color_stop_rgba(pos, r, g, b, alpha * a); + grad_btm_right->add_color_stop_rgba(pos, r, g, b, alpha * a); + grad_top_right->add_color_stop_rgba(pos, r, g, b, alpha * a); + grad_btm_left->add_color_stop_rgba(pos, r, g, b, alpha * a); + // this left/top corner is just a silver of the shadow: half of it is "hidden" beneath the page + if (pos >= 0.5) { + grad_top_left->add_color_stop_rgba(2 * (pos - 0.5), r, g, b, alpha * a); + } + } + + // shadow at the top (faint) + ctx->rectangle(corners[0][X], corners[0][Y] - half, std::max(corners[1][X] - corners[0][X], 0.0), half); + ctx->set_source(grad_top); + ctx->fill(); + + // right side + ctx->rectangle(corners[1][X], corners[1][Y] + half, sw, std::max(corners[2][Y] - corners[1][Y] - half, 0.0)); + ctx->set_source(grad_right); + ctx->fill(); + + // bottom side + ctx->rectangle(corners[0][X] + half, corners[2][Y], std::max(corners[1][X] - corners[0][X] - half, 0.0), sw); + ctx->set_source(grad_bottom); + ctx->fill(); + + // left side (faint) + ctx->rectangle(corners[0][X] - half, corners[0][Y], half, std::max(corners[2][Y] - corners[1][Y], 0.0)); + ctx->set_source(grad_left); + ctx->fill(); + + // bottom corners + ctx->rectangle(corners[2][X], corners[2][Y], sw, sw); + ctx->set_source(grad_btm_right); + ctx->fill(); + + ctx->rectangle(corners[3][X] - half, corners[3][Y], std::min(sw, rect.width() + half), sw); + ctx->set_source(grad_btm_left); + ctx->fill(); + + // top corners + ctx->rectangle(corners[1][X], corners[1][Y] - half, sw, std::min(sw, rect.height() + half)); + ctx->set_source(grad_top_right); + ctx->fill(); + + ctx->rectangle(corners[0][X] - half, corners[0][Y] - half, half, half); + ctx->set_source(grad_top_left); + ctx->fill(); +} + +/** + * 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) +{ + uint32_t a; + if constexpr (G_BYTE_ORDER == G_LITTLE_ENDIAN) { + a = (c & 0xff000000) >> 24; + } else { + a = (c & 0x000000ff); + } + + if (a == 0) { + return 0; + } + + // extract color components + uint32_t r, g, b; + if constexpr (G_BYTE_ORDER == G_LITTLE_ENDIAN) { + r = (c & 0x000000ff); + g = (c & 0x0000ff00) >> 8; + b = (c & 0x00ff0000) >> 16; + } else { + r = (c & 0xff000000) >> 24; + g = (c & 0x00ff0000) >> 16; + b = (c & 0x0000ff00) >> 8; + } + + // premultiply + r = premul_alpha(r, a); + b = premul_alpha(b, a); + g = premul_alpha(g, a); + + // combine into output + return (a << 24) | (r << 16) | (g << 8) | b; +} + +/** + * Convert one pixel from ARGB to GdkPixbuf format. + * + * @param c ARGB color + * @param bgcolor Color to use if c.alpha is zero (bgcolor.alpha is ignored) + */ +guint32 pixbuf_from_argb32(guint32 c, guint32 bgcolor) +{ + guint32 a = (c & 0xff000000) >> 24; + if (a == 0) { + assert(c == 0); + c = bgcolor; + } + + // extract color components + guint32 r = (c & 0x00ff0000) >> 16; + guint32 g = (c & 0x0000ff00) >> 8; + guint32 b = (c & 0x000000ff); + + if (a != 0) { + r = unpremul_alpha(r, a); + g = unpremul_alpha(g, a); + b = unpremul_alpha(b, a); + } + + // combine into output + if constexpr (G_BYTE_ORDER == G_LITTLE_ENDIAN) { + return r | (g << 8) | (b << 16) | (a << 24); + } else { + return (r << 24) | (g << 16) | (b << 8) | a; + } +} + +/** + * 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) +{ + if (!data || w < 1 || h < 1 || stride < 1) { + return; + } + + for (size_t i = 0; i < h; ++i) { + guint32 *px = reinterpret_cast<guint32*>(data + i*stride); + for (size_t 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, guint32 bgcolor) +{ + if (!data || w < 1 || h < 1 || stride < 1) { + return; + } + for (size_t i = 0; i < h; ++i) { + guint32 *px = reinterpret_cast<guint32*>(data + i*stride); + for (size_t j = 0; j < w; ++j) { + *px = pixbuf_from_argb32(*px, bgcolor); + ++px; + } + } +} + +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; +} + + +/** + * Convert one pixel from ARGB to GdkPixbuf format. + * + * @param c RGBA color + */ +guint32 rgba_from_argb32(guint32 c) +{ + guint32 a = (c & 0xff000000) >> 24; + guint32 r = (c & 0x00ff0000) >> 16; + guint32 g = (c & 0x0000ff00) >> 8; + guint32 b = (c & 0x000000ff); + + if (a != 0) { + r = unpremul_alpha(r, a); + g = unpremul_alpha(g, a); + b = unpremul_alpha(b, a); + } + + // combine into output + guint32 o = (r << 24) | (g << 16) | (b << 8) | (a); + + return o; +} + +/** + * 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_cols + 7)/8) * num_rows); + char* ptr = (char*) new_data; + // Used when we write image data smaller than one byte (for instance in + // black and white images where 1px = 1bit). Only possible with greyscale. + int pad = 0; + 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; + + guint64 pix3 = (*pixel & 0xff000000) >> 24; + guint64 pix2 = (*pixel & 0x00ff0000) >> 16; + guint64 pix1 = (*pixel & 0x0000ff00) >> 8; + guint64 pix0 = (*pixel & 0x000000ff); + + uint64_t a, r, g, b; + if constexpr (G_BYTE_ORDER == G_LITTLE_ENDIAN) { + a = pix3; + b = pix2; + g = pix1; + r = pix0; + } else { + r = pix3; + g = pix2; + b = pix1; + a = pix0; + } + + // 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 (color_type & 2) { // RGB or RGBA + // 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 + // for little-endian, and remove the <<0, <<16, <<32 and <<48 for big-endian. + if (color_type & 4) { // RGBA + if (bit_depth == 8) + *((guint32*)ptr) = *pixel; + else + // This uses the samples in the order they appear in pixel rather than + // normalised to abgr or rgba in order to make it endian agnostic, + // exploiting the symmetry of the expression (0x101 is the same in both + // endiannesses and each sample is multiplied by that). + *((guint64*)ptr) = (guint64)((pix3<<56)+(pix3<<48)+(pix2<<40)+(pix2<<32)+(pix1<<24)+(pix1<<16)+(pix0<<8)+(pix0)); + } else { // RGB + if (bit_depth == 8) { + *ptr = r; + *(ptr+1) = g; + *(ptr+2) = b; + } else { + *((guint16*)ptr) = (r<<8)+r; + *((guint16*)(ptr+2)) = (g<<8)+g; + *((guint16*)(ptr+4)) = (b<<8)+b; + } + } + } else { // Grayscale + if (bit_depth == 16) { + if constexpr (G_BYTE_ORDER == G_LITTLE_ENDIAN) { + *(guint16*)ptr = ((gray & 0xff00)>>8) + ((gray & 0x00ff)<<8); + } else { + *(guint16*)ptr = gray; + } + // For 8bit->16bit this mirrors RGB(A), multiplying by + // 0x101; if you prefer multiplying by 0x100, remove the + // <<8 for little-endian, and remove the unshifted value + // for big-endian. + if (color_type & 4) // Alpha channel + *((guint16*)(ptr+2)) = a + (a<<8); + } else if (bit_depth == 8) { + *ptr = guint8(gray >> 8); + if (color_type & 4) // Alpha channel + *((guint8*)(ptr+1)) = a; + } else { + if (!pad) *ptr=0; + // In PNG numbers are stored left to right, but in most significant bits first, so the first one processed is the ``big'' mask, etc. + int realpad = 8 - bit_depth - pad; + *ptr += guint8((gray >> (16-bit_depth))<<realpad); // Note the "+=" + if (color_type & 4) // Alpha channel + *(ptr+1) += guint8((a >> (8-bit_depth))<<(bit_depth + realpad)); + } + } + + pad += bit_depth*n_fields; + ptr += pad/8; + pad %= 8; + } + // Align bytes on rows + if (pad) { + pad = 0; + ptr++; + } + } + 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..fb1e3fd --- /dev/null +++ b/src/display/cairo-utils.h @@ -0,0 +1,227 @@ +// 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 *); + +namespace Inkscape { + +/** 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(); + + Pixbuf *cropTo(const Geom::IntRect &area) const; + + GdkPixbuf *getPixbufRaw(bool convert_format = true); + GdkPixbuf *getPixbufRaw() const; + + cairo_surface_t *getSurfaceRaw(); + cairo_surface_t *getSurfaceRaw() const; + Cairo::RefPtr<Cairo::Surface> getSurface(); + + 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 void ensure_pixbuf(GdkPixbuf *pb); + static void ensure_argb32(GdkPixbuf *pb); + + 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 = ""); + static Geom::Affine get_embedded_orientation(GdkPixbuf *buf); + static GdkPixbuf *apply_embedded_orientation(GdkPixbuf *buf); + + 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 + +// Atomic accessors to global variable governing number of filter threads. +int get_num_filter_threads(); +void set_num_filter_threads(int); + +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_cairo_set_hairline(cairo_t *ct); +void ink_cairo_set_dither(cairo_surface_t *surface, bool enabled); + +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::RefPtr<Cairo::ImageSurface> ink_cairo_surface_copy(Cairo::RefPtr<Cairo::ImageSurface> surface); +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); +guint32 ink_cairo_pattern_get_argb32(cairo_pattern_t *pattern); +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, bool use_alpha = false); +// draw drop shadow around the 'rect' with given 'size' and 'color'; shadow extends to the right and bottom of rect +void ink_cairo_draw_drop_shadow(const Cairo::RefPtr<Cairo::Context> &ctx, const Geom::Rect& rect, double size, guint32 color, double color_alpha); + +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, guint32 bgcolor=0); + +G_GNUC_CONST guint32 argb32_from_pixbuf(guint32 in); +G_GNUC_CONST guint32 pixbuf_from_argb32(guint32 in, guint32 bgcolor=0); +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); +/** Convert a pixel in 0xAARRGGBB format to 0xRRGGBBAA format. */ +G_GNUC_CONST guint32 rgba_from_argb32(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) +{ + if (color >= alpha) + return 0xff; + 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); + +std::optional<Geom::PathVector> extract_pathvector_from_cairo(cairo_t *ct); + +#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/control/README b/src/display/control/README new file mode 100644 index 0000000..680f0bf --- /dev/null +++ b/src/display/control/README @@ -0,0 +1,112 @@ + +This directory contains code for handling on-canvas editing objects as +well as document display. + +Historically, the code originates from GnomeCanvas but at this point it +has been completely rewritten for Inkscape's needs. + +One can think of a CanvasItem as a light-weight widget. There is a +selection mechanism based on an item's position and a method to handle +events. The selection mechanism is currently very simplistic, +selecting the top-most item whose bounding box contains the +cursor. Probably as a result, many items do not use these capabilities +with external code replacing this functionality (e.g CanvasItemQuad, +CanvasItemCurve). + +Points are stored as document coordinates. They are converted to +screen coordinates by multiplying by the document to window affine. +When an object's geometry changes, it must call request_update() to +flag that its bounds need to be recalculated (_need_update = +true). This will also cause all ancestors to also be marked and +finally the Canvas. Before picking or drawing, all bounds must be +up-to-date. (Changing the Canvas affine will also require bounds to be +recalculated.) This mechanism ensures that bounds are correct in the +most efficient manner. If only the style is changed (without geometric +ramifications) then call canvas->redraw_area() to trigger a redraw. + +CanvasItemGroup keeps a list of child items using a Boost intrusive +list. The pointers between nodes are kept inside the items. This +allows for quick deletion by avoiding the need to search through the +list to find the item to delete. This is important when a path +contains hundress of nodes. However, a Boost intrusive list cannot be +used with C++ smart pointers. Deleting an item can be done by either +calling CanvasItemGroup::remove(CanvasItem*) or by directly deleting +the item. Deleting a CanvasItemGroup will delete it and all of its +children. + + +Contents (x == pickable): + +* CanvasItem: Abstract base class. +* CanvasItemBPath: x An item representing a Bezier path. +* CanvasItemCatchall: x An infinite item, for capturing left-over events. +* CanvasItemCtrl: x An item representing a control point (knot, node, etc.). +* CanvasItemCurve: A single Bezier curve item (line or cubic). Not pickable! +* CanvasItemDrawing: x The SVG drawing. +* CanvasItemGrid: Base class for snapping grids. +* CanvasItemGroup: x An item that contains other items. +* CanvasItemGuideline: x A guideline for snapping. +* CanvasItemQuad: An object defined by four points (only used by Text tool). +* CanvasItemRect: An axis aligned rectangle item. +* CanvasItemRotate: x For previewing the rotation of the canvas. +* CanvasItemText: A text item. + +* CanvasItemEnum: All the enums you'll want to use! +* CanvasItemBuffer: A class that wraps a Cairo buffer for drawing. + +Classes here that use CanvasItem's: + +* CanvasGridXY: A Cartesian grid for snapping. +* CanvasGridAxonom An Axonometric grid for snapping. +* SnapIndicator: A class for showing a snap possibility on canvas. +* TemporaryItem: A class to manage the lifetime of a temporary CanvasItem. +* TemporaryItemList: A class to track TemporaryItem's. + + +Notes: + +CanvasItemCtrl (a.k.a. "Node", "Knot", "Handle", "Dragger", "Ctrl", "Mouse Grab", "Control Point") + + Used by several groups of classes: + + * Knot + ** KnotHolderEntity + *** live_effects/... + *** KnotHolder: Contains one or more KnotHolderEntity's. + Example: an object's fill and stroke gradients could have + overlapping knot-entities and are moved together via the knot holder. + **** shape-editor-knotholders.cpp + Classes derived from KnotHolder which contain classes derived from KnotHolderEntity for + editing shapes. + **** ShapeEditor contains two KnotHolders, one for shapes, one for LPE's. + + ** ui/tools/tool-base.h + ** seltrans.h,.cpp + ** display/snap-indicator.h + ** gradient-drag.cpp + ** vanishing-point.h + + + * ControlPoint, SelectorPoint, SelectableControlPoint, Handle, Node, CurveDragPoint, TransformHandle, plus + auxiliary classes (manipulator...). + * drag-anchor + * pen-tool + * measure-tool + * guide-line + * snap-indicator + + +TODO: + +Move files that use CanvasItem's to more appropriate places. + +All files that use CanvasItem's in src directory: +* Move rubberband.h/.cpp to src/ui +* Move gradient-drag to src/ui +* Move selcue* to src/ui +* Move seltrans* to src/ui +* Move vanishing-point to src/ui +* Move snap code to src/ui/snap display/snap-indicator.h/.cpp snap.h etc. + +See also src/ui/tool and src/ui/knot. + diff --git a/src/display/control/canvas-item-bpath.cpp b/src/display/control/canvas-item-bpath.cpp new file mode 100644 index 0000000..d99588e --- /dev/null +++ b/src/display/control/canvas-item-bpath.cpp @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * A class to represent a Bezier path. + */ + +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of SPCanvasBPath + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "canvas-item-bpath.h" + +#include "color.h" // SP_RGBA_x_F +#include "display/curve.h" +#include "display/cairo-utils.h" +#include "helper/geom.h" // bounds_exact_transformed() + +namespace Inkscape { + +/** + * Create a null control bpath. + */ +CanvasItemBpath::CanvasItemBpath(CanvasItemGroup *group) + : CanvasItem(group) +{ + _name = "CanvasItemBpath:Null"; + _pickable = true; // For now, everyone gets events from this class! +} + +/** + * Create a control bpath. Path is in document coordinates. + */ +CanvasItemBpath::CanvasItemBpath(CanvasItemGroup *group, Geom::PathVector path, bool phantom_line) + : CanvasItem(group) + , _path(std::move(path)) + , _phantom_line(phantom_line) +{ + _name = "CanvasItemBpath"; + _pickable = true; // For now, everyone gets events from this class! + request_update(); // Render immediately or temporary bpaths won't show. +} + +/** + * Set a control bpath. Curve is in document coordinates. + */ +void CanvasItemBpath::set_bpath(SPCurve const *curve, bool phantom_line) +{ + set_bpath(curve ? curve->get_pathvector() : Geom::PathVector(), phantom_line); +} + +/** + * Set a control bpath. Path is in document coordinates. + */ +void CanvasItemBpath::set_bpath(Geom::PathVector path, bool phantom_line) +{ + defer([=, path = std::move(path)] () mutable { + _path = std::move(path); + _phantom_line = phantom_line; + request_update(); + }); +} + +/** + * Set the fill color and fill rule. + */ +void CanvasItemBpath::set_fill(uint32_t fill, SPWindRule fill_rule) +{ + defer([=] { + if (_fill == fill && _fill_rule == fill_rule) return; + _fill = fill; + _fill_rule = fill_rule; + request_redraw(); + }); +} + +void CanvasItemBpath::set_dashes(std::vector<double> &&dashes) +{ + defer([=, dashes = std::move(dashes)] () mutable { + _dashes = std::move(dashes); + }); +} + +/** + * Set the stroke width + */ +void CanvasItemBpath::set_stroke_width(double width) +{ + defer([=] { + if (_stroke_width == width) return; + _stroke_width = width; + request_redraw(); + }); +} + +/** + * Returns distance between point in canvas units and nearest point on bpath. + */ +double CanvasItemBpath::closest_distance_to(Geom::Point const &p) const +{ + double d = Geom::infinity(); + + // Convert p to document coordinates (quicker than converting path to canvas units). + Geom::Point p_doc = p * affine().inverse(); + _path.nearestTime(p_doc, &d); + d *= affine().descrim(); // Uniform scaling and rotation only. + + return d; +} + +/** + * Returns true if point p (in canvas units) is within tolerance (canvas units) distance of bpath. + */ +bool CanvasItemBpath::contains(Geom::Point const &p, double tolerance) +{ + if (tolerance == 0) { + tolerance = 1; // Need a minimum tolerance value or always returns false. + } + + // Check for 'inside' a filled bpath if a fill is being used. + if ((_fill & 0xff) != 0) { + Geom::Point p_doc = p * affine().inverse(); + if (_path.winding(p_doc) % 2 != 0) { + return true; + } + } + + // Otherwise see how close we are to the outside line. + return closest_distance_to(p) < tolerance; +} + +/** + * Update and redraw control bpath. + */ +void CanvasItemBpath::_update(bool) +{ + // Queue redraw of old area (erase previous content). + request_redraw(); + + if (_path.empty()) { + _bounds = {}; + return; + } + + _bounds = expandedBy(bounds_exact_transformed(_path, affine()), 2); + + // Queue redraw of new area + request_redraw(); +} + +/** + * Render bpath to screen via Cairo. + */ +void CanvasItemBpath::_render(Inkscape::CanvasItemBuffer &buf) const +{ + bool do_fill = (_fill & 0xff) != 0; // Not invisible. + bool do_stroke = (_stroke & 0xff) != 0; // Not invisible. + + if (!do_fill && !do_stroke) { + // Both fill and stroke invisible. + return; + } + + buf.cr->save(); + + // Setup path + buf.cr->set_tolerance(0.5); + buf.cr->begin_new_path(); + + feed_pathvector_to_cairo(buf.cr->cobj(), _path, affine(), buf.rect, + /* optimize_stroke */ !do_fill, 1); + + // Do fill + if (do_fill) { + buf.cr->set_source_rgba(SP_RGBA32_R_F(_fill), SP_RGBA32_G_F(_fill), + SP_RGBA32_B_F(_fill), SP_RGBA32_A_F(_fill)); + buf.cr->set_fill_rule(_fill_rule == SP_WIND_RULE_EVENODD ? + Cairo::FILL_RULE_EVEN_ODD : Cairo::FILL_RULE_WINDING); + buf.cr->fill_preserve(); + } + + // Do stroke + if (do_stroke) { + + if (!_dashes.empty()) { + buf.cr->set_dash(_dashes, 0.0); // 0.0 is offset + } + + if (_phantom_line) { + buf.cr->set_source_rgba(1.0, 1.0, 1.0, 0.25); + buf.cr->set_line_width(2.0); + buf.cr->stroke_preserve(); + } + + buf.cr->set_source_rgba(SP_RGBA32_R_F(_stroke), SP_RGBA32_G_F(_stroke), + SP_RGBA32_B_F(_stroke), SP_RGBA32_A_F(_stroke)); + buf.cr->set_line_width(_stroke_width); + buf.cr->stroke(); + + } else { + buf.cr->begin_new_path(); // Clears path + } + + buf.cr->restore(); +} + +} // 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/control/canvas-item-bpath.h b/src/display/control/canvas-item-bpath.h new file mode 100644 index 0000000..3b87579 --- /dev/null +++ b/src/display/control/canvas-item-bpath.h @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_CANVAS_ITEM_BPATH_H +#define SEEN_CANVAS_ITEM_BPATH_H + +/** + * A class to represent a Bezier path. + */ + +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of SPCanvasBPath + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/transforms.h> +#include <2geom/pathvector.h> + +#include "canvas-item.h" + +#include "style-enums.h" // Fill rule + +class SPCurve; + +namespace Inkscape { + +class CanvasItemBpath final : public CanvasItem +{ +public: + CanvasItemBpath(CanvasItemGroup *group); + CanvasItemBpath(CanvasItemGroup *group, Geom::PathVector path, bool phantom_line = false); + + // Geometry + void set_bpath(SPCurve const *curve, bool phantom_line = false); + void set_bpath(Geom::PathVector path, bool phantom_line = false); + + double closest_distance_to(Geom::Point const &p) const; + + // Selection + bool contains(Geom::Point const &p, double tolerance = 0) override; + + // Properties + void set_fill(uint32_t rgba, SPWindRule fill_rule); + void set_dashes(std::vector<double> &&dashes); + void set_stroke_width(double width); + +protected: + ~CanvasItemBpath() override = default; + + void _update(bool propagate) override; + void _render(Inkscape::CanvasItemBuffer &buf) const override; + + // Geometry + Geom::PathVector _path; + + // Properties + SPWindRule _fill_rule = SP_WIND_RULE_EVENODD; + std::vector<double> _dashes; + bool _phantom_line = false; + double _stroke_width = 1.0; +}; + +} // namespace Inkscape + +#endif // SEEN_CANVAS_ITEM_BPATH_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/control/canvas-item-buffer.h b/src/display/control/canvas-item-buffer.h new file mode 100644 index 0000000..a66cb4c --- /dev/null +++ b/src/display/control/canvas-item-buffer.h @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_CANVAS_ITEM_BUFFER_H +#define SEEN_CANVAS_ITEM_BUFFER_H + +/** + * Buffer for rendering canvas items. + */ + +/* + * Author: + * See git history. + * + * Copyright (C) 2020 Authors + * + * Rewrite of SPCanvasBuf. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + */ + +#include <2geom/rect.h> +#include <cairomm/context.h> + +namespace Inkscape { + +/** + * Class used when rendering canvas items. + */ +struct CanvasItemBuffer +{ + Geom::IntRect rect; + int device_scale; // For high DPI monitors. + Cairo::RefPtr<Cairo::Context> cr; + bool outline_pass; +}; + +} // namespace Inkscape + +#endif // SEEN_CANVAS_ITEM_BUFFER_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/control/canvas-item-catchall.cpp b/src/display/control/canvas-item-catchall.cpp new file mode 100644 index 0000000..e6fbd47 --- /dev/null +++ b/src/display/control/canvas-item-catchall.cpp @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * A class to catch events after everyone else has had a go. + */ + +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of SPCanvasAcetate. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "canvas-item-catchall.h" + +namespace Inkscape { + +/** + * Create an null control catchall. + */ +CanvasItemCatchall::CanvasItemCatchall(CanvasItemGroup *group) + : CanvasItem(group) +{ + _name = "CanvasItemCatchall"; + _pickable = true; // Duh! That's the purpose of this class! +} + +/** + * Returns true if point p (in canvas units) is within tolerance (canvas units) distance of catchall. + */ +bool CanvasItemCatchall::contains(Geom::Point const &p, double tolerance) +{ + return true; // We contain every place! +} + +/** + * Update and redraw control catchall. + */ +void CanvasItemCatchall::_update(bool) +{ + _bounds = Geom::Rect(-Geom::infinity(), -Geom::infinity(), Geom::infinity(), Geom::infinity()); +} + +/** + * Render catchall to screen via Cairo. + */ +void CanvasItemCatchall::_render(Inkscape::CanvasItemBuffer &buf) const +{ + // Do nothing! (Needed as CanvasItem is abstract.) +} + +} // 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/control/canvas-item-catchall.h b/src/display/control/canvas-item-catchall.h new file mode 100644 index 0000000..6e0270a --- /dev/null +++ b/src/display/control/canvas-item-catchall.h @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_CANVAS_ITEM_CATCHALL_H +#define SEEN_CANVAS_ITEM_CATCHALL_H + +/** + * A class to catch events after everyone else has had a go. + */ + +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of SPCanvasAcetate. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "canvas-item.h" + +namespace Inkscape { + +class CanvasItemCatchall final : public CanvasItem +{ +public: + CanvasItemCatchall(CanvasItemGroup *group); + + // Selection + bool contains(Geom::Point const &p, double tolerance) override; + +protected: + ~CanvasItemCatchall() override = default; + + void _update(bool propagate) override; + void _render(Inkscape::CanvasItemBuffer &buf) const override; +}; + +} // namespace Inkscape + +#endif // SEEN_CANVAS_ITEM_CATCHALL_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/control/canvas-item-context.cpp b/src/display/control/canvas-item-context.cpp new file mode 100644 index 0000000..99afdc4 --- /dev/null +++ b/src/display/control/canvas-item-context.cpp @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "canvas-item-context.h" +#include "canvas-item-group.h" + +namespace Inkscape { + +CanvasItemContext::CanvasItemContext(UI::Widget::Canvas *canvas) + : _canvas(canvas) + , _root(new CanvasItemGroup(this)) +{ +} + +CanvasItemContext::~CanvasItemContext() +{ + delete _root; +} + +void CanvasItemContext::snapshot() +{ + assert(!_snapshotted); + _snapshotted = true; +} + +void CanvasItemContext::unsnapshot() +{ + assert(_snapshotted); + _snapshotted = false; + _funclog(); +} + +} // 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/control/canvas-item-context.h b/src/display/control/canvas-item-context.h new file mode 100644 index 0000000..117110a --- /dev/null +++ b/src/display/control/canvas-item-context.h @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * The context in which a single CanvasItem tree exists. Holds the root node and common state. + */ +#ifndef SEEN_CANVAS_ITEM_CONTEXT_H +#define SEEN_CANVAS_ITEM_CONTEXT_H + +#include <2geom/affine.h> +#include "util/funclog.h" + +namespace Inkscape { + +namespace UI::Widget { class Canvas; } +class CanvasItemGroup; + +class CanvasItemContext +{ +public: + CanvasItemContext(UI::Widget::Canvas *canvas); + CanvasItemContext(CanvasItemContext const &) = delete; + CanvasItemContext &operator=(CanvasItemContext const &) = delete; + ~CanvasItemContext(); + + // Structure + UI::Widget::Canvas *canvas() const { return _canvas; } + CanvasItemGroup *root() const { return _root; } + + // Geometry + Geom::Affine const &affine() const { return _affine; } + void setAffine(Geom::Affine const &affine) { _affine = affine; } + + // Snapshotting + void snapshot(); + void unsnapshot(); + bool snapshotted() const { return _snapshotted; } + + template<typename F> + void defer(F &&f) { _snapshotted ? _funclog.emplace(std::forward<F>(f)) : f(); } + +private: + // Structure + UI::Widget::Canvas *_canvas; + CanvasItemGroup *_root; + + // Geometry + Geom::Affine _affine; + + // Snapshotting + char _cacheline_separator[127]; + + bool _snapshotted = false; + Util::FuncLog _funclog; +}; + +} // namespace Inkscape + +#endif // SEEN_CANVAS_ITEM_CONTEXT_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/control/canvas-item-ctrl.cpp b/src/display/control/canvas-item-ctrl.cpp new file mode 100644 index 0000000..e0dcd90 --- /dev/null +++ b/src/display/control/canvas-item-ctrl.cpp @@ -0,0 +1,1181 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * A class to represent a control node. + */ + +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of SPCtrl + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/transforms.h> + +#include "canvas-item-ctrl.h" +#include "helper/geom.h" + +#include "preferences.h" // Default size. +#include "display/cairo-utils.h" // argb32_from_rgba() + +#include "ui/widget/canvas.h" + +namespace Inkscape { + +/** + * Create a null control node. + */ +CanvasItemCtrl::CanvasItemCtrl(CanvasItemGroup *group) + : CanvasItem(group) +{ + _name = "CanvasItemCtrl:Null"; + _pickable = true; // Everybody gets events from this class! +} + +/** + * Create a control ctrl. Shape auto-set by type. + */ +CanvasItemCtrl::CanvasItemCtrl(CanvasItemGroup *group, CanvasItemCtrlType type) + : CanvasItem(group) + , _type(type) +{ + _name = "CanvasItemCtrl:Type_" + std::to_string(_type); + _pickable = true; // Everybody gets events from this class! + + // Use _type to set default values: + set_shape_default(); + set_size_default(); +} + +/** + * Create a control ctrl. Point is in document coordinates. + */ +CanvasItemCtrl::CanvasItemCtrl(CanvasItemGroup *group, CanvasItemCtrlType type, Geom::Point const &p) + : CanvasItemCtrl(group, type) +{ + _position = p; + request_update(); +} + +/** + * Create a control ctrl. + */ +CanvasItemCtrl::CanvasItemCtrl(CanvasItemGroup *group, CanvasItemCtrlShape shape) + : CanvasItem(group) + , _shape(shape) + , _type(CANVAS_ITEM_CTRL_TYPE_DEFAULT) +{ + _name = "CanvasItemCtrl:Shape_" + std::to_string(_shape); + _pickable = true; // Everybody gets events from this class! +} + +/** + * Create a control ctrl. Point is in document coordinates. + */ +CanvasItemCtrl::CanvasItemCtrl(CanvasItemGroup *group, CanvasItemCtrlShape shape, Geom::Point const &p) + : CanvasItemCtrl(group, shape) +{ + _position = p; +} + +/** + * Set the position. Point is in document coordinates. + */ +void CanvasItemCtrl::set_position(Geom::Point const &position) +{ + // std::cout << "CanvasItemCtrl::set_ctrl: " << _name << ": " << position << std::endl; + defer([=] { + if (_position == position) return; + _position = position; + request_update(); + }); +} + +/** + * Returns distance between point in canvas units and position of ctrl. + */ +double CanvasItemCtrl::closest_distance_to(Geom::Point const &p) const +{ + // TODO: Different criteria for different shapes. + return Geom::distance(p, _position * affine()); +} + +/** + * If tolerance is zero, returns true if point p (in canvas units) is inside bounding box, + * else returns true if p (in canvas units) is within tolerance (canvas units) distance of ctrl. + * The latter assumes ctrl center anchored. + */ +bool CanvasItemCtrl::contains(Geom::Point const &p, double tolerance) +{ + // TODO: Different criteria for different shapes. + if (!_bounds) return false; + if (tolerance == 0) { + return _bounds->interiorContains(p); + } else { + return closest_distance_to(p) <= tolerance; + } +} + +static auto angle_of(Geom::Affine const &affine) +{ + return std::atan2(affine[1], affine[0]); +} + +/** + * Update and redraw control ctrl. + */ +void CanvasItemCtrl::_update(bool) +{ + // Queue redraw of old area (erase previous content). + request_redraw(); + + // Setting the position to (inf, inf) to hide it is a pervasive hack we need to support. + if (!_position.isFinite()) { + _bounds = {}; + return; + } + + // Width and height are always odd. + assert(_width % 2 == 1); + assert(_height % 2 == 1); + + // Get half width and height, rounded down. + int const w_half = _width / 2; + int const h_half = _height / 2; + + // Set _angle, and compute adjustment for anchor. + int dx = 0; + int dy = 0; + + switch (_shape) { + case CANVAS_ITEM_CTRL_SHAPE_DARROW: + case CANVAS_ITEM_CTRL_SHAPE_SARROW: + case CANVAS_ITEM_CTRL_SHAPE_CARROW: + case CANVAS_ITEM_CTRL_SHAPE_SALIGN: + case CANVAS_ITEM_CTRL_SHAPE_CALIGN: + { + double angle = _anchor * M_PI_4 + angle_of(affine()); + double const half = _width / 2.0; + + dx = -(half + 2) * cos(angle); // Add a bit to prevent tip from overlapping due to rounding errors. + dy = -(half + 2) * sin(angle); + + switch (_shape) { + case CANVAS_ITEM_CTRL_SHAPE_CARROW: + angle += 5 * M_PI_4; + break; + + case CANVAS_ITEM_CTRL_SHAPE_SARROW: + angle += M_PI_2; + break; + + case CANVAS_ITEM_CTRL_SHAPE_SALIGN: + dx = -(half / 2 + 2) * cos(angle); + dy = -(half / 2 + 2) * sin(angle); + angle -= M_PI_2; + break; + + case CANVAS_ITEM_CTRL_SHAPE_CALIGN: + angle -= M_PI_4; + dx = (half / 2 + 2) * ( sin(angle) - cos(angle)); + dy = (half / 2 + 2) * (-sin(angle) - cos(angle)); + break; + + default: + break; + } + + if (_angle != angle) { + _angle = angle; + _built.reset(); + } + + break; + } + + case CANVAS_ITEM_CTRL_SHAPE_PIVOT: + case CANVAS_ITEM_CTRL_SHAPE_MALIGN: { + double const angle = angle_of(affine()); + if (_angle != angle) { + _angle = angle; + _built.reset(); + } + break; + } + + default: + switch (_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: + dx = w_half; + break; + + case SP_ANCHOR_NE: + case SP_ANCHOR_E: + case SP_ANCHOR_SE: + dx = -w_half; + break; + } + + switch (_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: + dy = h_half; + break; + + case SP_ANCHOR_SW: + case SP_ANCHOR_S: + case SP_ANCHOR_SE: + dy = -h_half; + break; + } + break; + } + + auto const pt = Geom::IntPoint(-w_half, -h_half) + Geom::IntPoint(dx, dy) + (_position * affine()).floor(); + _bounds = Geom::IntRect(pt, pt + Geom::IntPoint(_width, _height)); + + // Queue redraw of new area + request_redraw(); +} + +static inline uint32_t compose_xor(uint32_t bg, uint32_t fg, uint32_t a) +{ + uint32_t c = bg * (255 - a) + (((bg ^ ~fg) + (bg >> 2) - (bg > 127 ? 63 : 0)) & 255) * a; + return (c + 127) / 255; +} + +/** + * Render ctrl to screen via Cairo. + */ +void CanvasItemCtrl::_render(CanvasItemBuffer &buf) const +{ + _built.init([&, this] { + build_cache(buf.device_scale); + }); + + Geom::Point c = _bounds->min() - buf.rect.min(); + int x = c.x(); // Must be pixel aligned. + int y = c.y(); + + buf.cr->save(); + + // This code works regardless of source type. + + // 1. Copy the affected part of output to a temporary surface + + // Size in device pixels. Does not set device scale. + int width = _width * buf.device_scale; + int height = _height * buf.device_scale; + auto work = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, width, height); + cairo_surface_set_device_scale(work->cobj(), buf.device_scale, buf.device_scale); // No C++ API! + + auto cr = Cairo::Context::create(work); + cr->translate(-_bounds->left(), -_bounds->top()); + cr->set_source(buf.cr->get_target(), buf.rect.left(), buf.rect.top()); + cr->paint(); + // static int a = 0; + // std::string name0 = "ctrl0_" + _name + "_" + std::to_string(a++) + ".png"; + // work->write_to_png(name0); + + // 2. Composite the control on a temporary surface + work->flush(); + int strideb = work->get_stride(); + unsigned char *pxb = work->get_data(); + + // this code allow background become isolated from rendering so we can do things like outline overlay + uint32_t backcolor = get_canvas()->get_effective_background(); + uint32_t *p = _cache.get(); + for (int i = 0; i < height; ++i) { + auto pb = reinterpret_cast<uint32_t*>(pxb + i * strideb); + for (int j = 0; j < width; ++j) { + uint32_t base = *pb; + uint32_t cc = *p++; + uint32_t ac = cc & 0xff; + if (*pb == 0 && cc != 0) { + base = backcolor; + } + if (ac == 0 && cc != 0) { + *pb++ = argb32_from_rgba(cc | 0x000000ff); + } else if (ac == 0) { + *pb++ = base; + } else if ( + _mode == CANVAS_ITEM_CTRL_MODE_XOR || + _mode == CANVAS_ITEM_CTRL_MODE_GRAYSCALED_XOR || + _mode == CANVAS_ITEM_CTRL_MODE_DESATURATED_XOR) + { + EXTRACT_ARGB32(base, ab,rb,gb,bb) + // here we get canvas color and if color to draw + // has opacity, we override base colors + // flattenig canvas color + EXTRACT_ARGB32(backcolor, abb,rbb,gbb,bbb) + if (abb != ab) { + rb = (ab/255.0) * rb + (1-(ab/255.0)) * rbb; + gb = (ab/255.0) * gb + (1-(ab/255.0)) * gbb; + bb = (ab/255.0) * bb + (1-(ab/255.0)) * bbb; + ab = 255; + } + uint32_t ro = compose_xor(rb, (cc & 0xff000000) >> 24, ac); + uint32_t go = compose_xor(gb, (cc & 0x00ff0000) >> 16, ac); + uint32_t bo = compose_xor(bb, (cc & 0x0000ff00) >> 8, ac); + if (_mode == CANVAS_ITEM_CTRL_MODE_GRAYSCALED_XOR || + _mode == CANVAS_ITEM_CTRL_MODE_DESATURATED_XOR) { + uint32_t gray = ro * 0.299 + go * 0.587 + bo * 0.114; + if (_mode == CANVAS_ITEM_CTRL_MODE_DESATURATED_XOR) { + double f = 0.85; // desaturate by 15% + double p = sqrt(ro * ro * 0.299 + go * go * 0.587 + bo * bo * 0.114); + ro = p + (ro - p) * f; + go = p + (go - p) * f; + bo = p + (bo - p) * f; + } else { + ro = gray; + go = gray; + bo = gray; + } + } + ASSEMBLE_ARGB32(px, ab,ro,go,bo) + *pb++ = px; + } else { + *pb++ = argb32_from_rgba(cc | 0x000000ff); + } + } + } + work->mark_dirty(); + // std::string name1 = "ctrl1_" + _name + "_" + std::to_string(a) + ".png"; + // work->write_to_png(name1); + + // 3. Replace the affected part of output with contents of temporary surface + buf.cr->set_source(work, x, y); + + buf.cr->rectangle(x, y, _width, _height); + buf.cr->clip(); + buf.cr->set_operator(Cairo::OPERATOR_SOURCE); + buf.cr->paint(); + buf.cr->restore(); +} + +void CanvasItemCtrl::set_fill(uint32_t fill) +{ + defer([=] { + if (_fill == fill) return; + _fill = fill; + _built.reset(); + request_redraw(); + }); +} + +void CanvasItemCtrl::set_stroke(uint32_t stroke) +{ + defer([=] { + if (_stroke == stroke) return; + _stroke = stroke; + _built.reset(); + request_redraw(); + }); +} + +void CanvasItemCtrl::set_shape(CanvasItemCtrlShape shape) +{ + defer([=] { + if (_shape == shape) return; + _shape = shape; + _built.reset(); + request_update(); // Geometry could change + }); +} + +void CanvasItemCtrl::set_shape_default() +{ + switch (_type) { + case CANVAS_ITEM_CTRL_TYPE_ADJ_HANDLE: + _shape = CANVAS_ITEM_CTRL_SHAPE_DARROW; + break; + + case CANVAS_ITEM_CTRL_TYPE_ADJ_SKEW: + _shape = CANVAS_ITEM_CTRL_SHAPE_SARROW; + break; + + case CANVAS_ITEM_CTRL_TYPE_ADJ_ROTATE: + _shape = CANVAS_ITEM_CTRL_SHAPE_CARROW; + break; + + case CANVAS_ITEM_CTRL_TYPE_ADJ_CENTER: + _shape = CANVAS_ITEM_CTRL_SHAPE_PIVOT; + break; + + case CANVAS_ITEM_CTRL_TYPE_ADJ_SALIGN: + _shape = CANVAS_ITEM_CTRL_SHAPE_SALIGN; + break; + + case CANVAS_ITEM_CTRL_TYPE_ADJ_CALIGN: + _shape = CANVAS_ITEM_CTRL_SHAPE_CALIGN; + break; + + case CANVAS_ITEM_CTRL_TYPE_ADJ_MALIGN: + _shape = CANVAS_ITEM_CTRL_SHAPE_MALIGN; + break; + + case CANVAS_ITEM_CTRL_TYPE_NODE_AUTO: + case CANVAS_ITEM_CTRL_TYPE_ROTATE: + case CANVAS_ITEM_CTRL_TYPE_MARGIN: + _shape = CANVAS_ITEM_CTRL_SHAPE_CIRCLE; + break; + + case CANVAS_ITEM_CTRL_TYPE_CENTER: + _shape = CANVAS_ITEM_CTRL_SHAPE_PLUS; + break; + + case CANVAS_ITEM_CTRL_TYPE_SHAPER: + case CANVAS_ITEM_CTRL_TYPE_LPE: + case CANVAS_ITEM_CTRL_TYPE_NODE_CUSP: + _shape = CANVAS_ITEM_CTRL_SHAPE_DIAMOND; + break; + + case CANVAS_ITEM_CTRL_TYPE_POINT: + _shape = CANVAS_ITEM_CTRL_SHAPE_CROSS; + break; + + default: + _shape = CANVAS_ITEM_CTRL_SHAPE_SQUARE; + } +} + +void CanvasItemCtrl::set_mode(CanvasItemCtrlMode mode) +{ + defer([=] { + if (_mode == mode) return; + _mode = mode; + _built.reset(); + request_update(); + }); +} + +void CanvasItemCtrl::set_pixbuf(Glib::RefPtr<Gdk::Pixbuf> pixbuf) +{ + defer([=, pixbuf = std::move(pixbuf)] () mutable { + if (_pixbuf != pixbuf) return; + _pixbuf = std::move(pixbuf); + _width = _pixbuf->get_width(); + _height = _pixbuf->get_height(); + _built.reset(); + request_update(); + }); +} + +// Nominally width == height == size except possibly for pixmaps. +void CanvasItemCtrl::set_size(int size) +{ + defer([=] { + if (_pixbuf) { + // std::cerr << "CanvasItemCtrl::set_size: Attempting to set size on pixbuf control!" << std::endl; + return; + } + if (_width == size + _extra && _height == size + _extra) return; + _width = size + _extra; + _height = size + _extra; + _built.reset(); + request_update(); // Geometry change + }); +} + +void CanvasItemCtrl::set_size_via_index(int size_index) +{ + // Size must always be an odd number to center on pixel. + + if (size_index < 1 || size_index > 15) { + std::cerr << "CanvasItemCtrl::set_size_via_index: size_index out of range!" << std::endl; + size_index = 3; + } + + int size = 0; + switch (_type) { + case CANVAS_ITEM_CTRL_TYPE_ADJ_HANDLE: + case CANVAS_ITEM_CTRL_TYPE_ADJ_SKEW: + size = size_index * 2 + 7; + break; + + case CANVAS_ITEM_CTRL_TYPE_ADJ_ROTATE: + case CANVAS_ITEM_CTRL_TYPE_ADJ_CENTER: + size = size_index * 2 + 9; // 2 larger than HANDLE/SKEW + break; + + case CANVAS_ITEM_CTRL_TYPE_ADJ_SALIGN: + case CANVAS_ITEM_CTRL_TYPE_ADJ_CALIGN: + case CANVAS_ITEM_CTRL_TYPE_ADJ_MALIGN: + size = size_index * 4 + 5; // Needs to be larger to allow for rotating. + break; + + case CANVAS_ITEM_CTRL_TYPE_POINT: + case CANVAS_ITEM_CTRL_TYPE_ROTATE: + case CANVAS_ITEM_CTRL_TYPE_MARGIN: + case CANVAS_ITEM_CTRL_TYPE_CENTER: + case CANVAS_ITEM_CTRL_TYPE_SIZER: + case CANVAS_ITEM_CTRL_TYPE_SHAPER: + case CANVAS_ITEM_CTRL_TYPE_LPE: + case CANVAS_ITEM_CTRL_TYPE_NODE_AUTO: + case CANVAS_ITEM_CTRL_TYPE_NODE_CUSP: + size = size_index * 2 + 5; + break; + + case CANVAS_ITEM_CTRL_TYPE_NODE_SMOOTH: + case CANVAS_ITEM_CTRL_TYPE_NODE_SYMETRICAL: + size = size_index * 2 + 3; + break; + + case CANVAS_ITEM_CTRL_TYPE_INVISIPOINT: + size = 1; + break; + + case CANVAS_ITEM_CTRL_TYPE_ANCHOR: // vanishing point for 3D box and anchor for pencil + size = size_index * 2 + 1; + break; + + case CANVAS_ITEM_CTRL_TYPE_DEFAULT: + size = size_index * 2 + 1; + break; + + default: + g_warning("set_size_via_index: missing case for handle type: %d", static_cast<int>(_type)); + size = size_index * 2 + 1; + break; + } + + set_size(size); +} + +void CanvasItemCtrl::set_size_default() +{ + int size = Preferences::get()->getIntLimited("/options/grabsize/value", 3, 1, 15); + set_size_via_index(size); +} + +void CanvasItemCtrl::set_size_extra(int extra) +{ + defer([=] { + if (_extra == extra || _pixbuf) return; // Don't enlarge pixbuf! + _width += extra - _extra; + _height += extra - _extra; + _extra = extra; + _built.reset(); + request_update(); // Geometry change + }); +} + +void CanvasItemCtrl::set_type(CanvasItemCtrlType type) +{ + defer([=] { + if (_type == type) return; + _type = type; + + // Use _type to set default values. + set_shape_default(); + set_size_default(); + _built.reset(); + request_update(); // Possible geometry change + }); +} + +void CanvasItemCtrl::set_angle(double angle) +{ + defer([=] { + if (_angle == angle) return; + _angle = angle; + _built.reset(); + request_update(); // Geometry change + }); +} + +void CanvasItemCtrl::set_anchor(SPAnchorType anchor) +{ + defer([=] { + if (_anchor == anchor) return; + _anchor = anchor; + request_update(); // Geometry change + }); +} + +// ---------- Protected ---------- + +static void draw_darrow(Cairo::RefPtr<Cairo::Context> const &cr, double size) +{ + // Find points, starting from tip of one arrowhead, working clockwise. + /* 1 4 + â±â ââČ + â± ââââââââââ âČ + 0â± 2 3 âČ5 + âČ 8 7 â± + âČ ââââââââââ â± + âČâ9 6ââ± + */ + + // Length of arrowhead (not including stroke). + double delta = (size-1)/4.0; // Use unscaled width. + + // Tip of arrow (0) + double tip_x = 0.5; // At edge, allow room for stroke. + double tip_y = size/2.0; // Center, assuming width == height. + + // Outer corner (1) + double out_x = tip_x + delta; + double out_y = tip_y - delta; + + // Inner corner (2) + double in_x = out_x; + double in_y = out_y + (delta/2.0); + + double x0 = tip_x; double y0 = tip_y; + double x1 = out_x; double y1 = out_y; + double x2 = in_x; double y2 = in_y; + double x3 = size - in_x; double y3 = in_y; + double x4 = size - out_x; double y4 = out_y; + double x5 = size - tip_x; double y5 = tip_y; + double x6 = size - out_x; double y6 = size - out_y; + double x7 = size - in_x; double y7 = size - in_y; + double x8 = in_x; double y8 = size - in_y; + double x9 = out_x; double y9 = size - out_y; + + // Draw arrow + cr->move_to(x0, y0); + cr->line_to(x1, y1); + cr->line_to(x2, y2); + cr->line_to(x3, y3); + cr->line_to(x4, y4); + cr->line_to(x5, y5); + cr->line_to(x6, y6); + cr->line_to(x7, y7); + cr->line_to(x8, y8); + cr->line_to(x9, y9); + cr->close_path(); +} + +static void draw_carrow(Cairo::RefPtr<Cairo::Context> const &cr, double size) +{ + // Length of arrowhead (not including stroke). + double delta = (size-3)/4.0; // Use unscaled width. + + // Tip of arrow + double tip_x = 1.5; // Edge, allow room for stroke when rotated. + double tip_y = delta + 1.5; + + // Outer corner (1) + double out_x = tip_x + delta; + double out_y = tip_y - delta; + + // Inner corner (2) + double in_x = out_x; + double in_y = out_y + (delta/2.0); + + double x0 = tip_x; double y0 = tip_y; + double x1 = out_x; double y1 = out_y; + double x2 = in_x; double y2 = in_y; + double x3 = size - in_y; //double y3 = size - in_x; + double x4 = size - out_y; double y4 = size - out_x; + double x5 = size - tip_y; double y5 = size - tip_x; + double x6 = x5 - delta; double y6 = y4; + double x7 = x5 - delta/2.0; double y7 = y4; + double x8 = x1; //double y8 = y0 + delta/2.0; + double x9 = x1; double y9 = y0 + delta; + + // Draw arrow + cr->move_to(x0, y0); + cr->line_to(x1, y1); + cr->line_to(x2, y2); + cr->arc(x1, y4, x3-x2, 3.0*M_PI/2.0, 0); + cr->line_to(x4, y4); + cr->line_to(x5, y5); + cr->line_to(x6, y6); + cr->line_to(x7, y7); + cr->arc_negative(x1, y4, x7-x8, 0, 3.0*M_PI/2.0); + cr->line_to(x9, y9); + cr->close_path(); +} + +static void draw_triangle(Cairo::RefPtr<Cairo::Context> const &cr, double size) +{ + // Construct an arrowhead (triangle) + double s = size/2.0; + double wcos = s * cos( M_PI/6 ); + double hsin = s * sin( M_PI/6 ); + // Construct a smaller arrow head for fill. + Geom::Point p1f(1, s); + Geom::Point p2f(s + wcos - 1, s + hsin); + Geom::Point p3f(s + wcos - 1, s - hsin); + // Draw arrow + cr->move_to(p1f[0], p1f[1]); + cr->line_to(p2f[0], p2f[1]); + cr->line_to(p3f[0], p3f[1]); + cr->close_path(); +} + +static void draw_triangle_angled(Cairo::RefPtr<Cairo::Context> const &cr, double size) +{ + // Construct an arrowhead (triangle) of half size. + double s = size/2.0; + double wcos = s * cos( M_PI/9 ); + double hsin = s * sin( M_PI/9 ); + Geom::Point p1f(s + 1, s); + Geom::Point p2f(s + wcos - 1, s + hsin - 1); + Geom::Point p3f(s + wcos - 1, s - (hsin - 1)); + // Draw arrow + cr->move_to(p1f[0], p1f[1]); + cr->line_to(p2f[0], p2f[1]); + cr->line_to(p3f[0], p3f[1]); + cr->close_path(); +} + +static void draw_pivot(Cairo::RefPtr<Cairo::Context> const &cr, double size) +{ + double delta4 = (size-5)/4.0; // Keep away from edge or will clip when rotating. + double delta8 = delta4/2; + + // Line start + double center = size/2.0; + + cr->move_to (center - delta8, center - 2*delta4 - delta8); + cr->rel_line_to ( delta4, 0 ); + cr->rel_line_to ( 0, delta4); + + cr->rel_line_to ( delta4, delta4); + + cr->rel_line_to ( delta4, 0 ); + cr->rel_line_to ( 0, delta4); + cr->rel_line_to (-delta4, 0 ); + + cr->rel_line_to (-delta4, delta4); + + cr->rel_line_to ( 0, delta4); + cr->rel_line_to (-delta4, 0 ); + cr->rel_line_to ( 0, -delta4); + + cr->rel_line_to (-delta4, -delta4); + + cr->rel_line_to (-delta4, 0 ); + cr->rel_line_to ( 0, -delta4); + cr->rel_line_to ( delta4, 0 ); + + cr->rel_line_to ( delta4, -delta4); + cr->close_path(); + + cr->begin_new_sub_path(); + cr->arc_negative(center, center, delta4, 0, -2 * M_PI); +} + +static void draw_salign(Cairo::RefPtr<Cairo::Context> const &cr, double size) +{ + // Triangle pointing at line. + + // Basic units. + double delta4 = (size-1)/4.0; // Use unscaled width. + double delta8 = delta4/2; + if (delta8 < 2) { + // Keep a minimum gap of at least one pixel (after stroking). + delta8 = 2; + } + + // Tip of triangle + double tip_x = size/2.0; // Center (also rotation point). + double tip_y = size/2.0; + + // Corner triangle position. + double outer = size/2.0 - delta4; + + // Outer line position + double oline = size/2.0 + (int)delta4; + + // Inner line position + double iline = size/2.0 + (int)delta8; + + // Draw triangle + cr->move_to(tip_x, tip_y); + cr->line_to(outer, outer); + cr->line_to(size - outer, outer); + cr->close_path(); + + // Draw line + cr->move_to(outer, iline); + cr->line_to(size - outer, iline); + cr->line_to(size - outer, oline); + cr->line_to(outer, oline); + cr->close_path(); +} + +static void draw_calign(Cairo::RefPtr<Cairo::Context> const &cr, double size) +{ + // Basic units. + double delta4 = (size-1)/4.0; // Use unscaled width. + double delta8 = delta4/2; + if (delta8 < 2) { + // Keep a minimum gap of at least one pixel (after stroking). + delta8 = 2; + } + + // Tip of triangle + double tip_x = size/2.0; // Center (also rotation point). + double tip_y = size/2.0; + + // Corner triangle position. + double outer = size/2.0 - delta8 - delta4; + + // End of line positin + double eline = size/2.0 - delta8; + + // Outer line position + double oline = size/2.0 + (int)delta4; + + // Inner line position + double iline = size/2.0 + (int)delta8; + + // Draw triangle + cr->move_to(tip_x, tip_y); + cr->line_to(outer, tip_y); + cr->line_to(tip_x, outer); + cr->close_path(); + + // Draw line + cr->move_to(iline, iline); + cr->line_to(iline, eline); + cr->line_to(oline, eline); + cr->line_to(oline, oline); + cr->line_to(eline, oline); + cr->line_to(eline, iline); + cr->close_path(); +} + +static void draw_malign(Cairo::RefPtr<Cairo::Context> const &cr, double size) +{ + // Basic units. + double delta4 = (size-1)/4.0; // Use unscaled width. + double delta8 = delta4/2; + if (delta8 < 2) { + // Keep a minimum gap of at least one pixel (after stroking). + delta8 = 2; + } + + // Tip of triangle + double tip_0 = size/2.0; + double tip_1 = size/2.0 - delta8; + + // Draw triangles + cr->move_to(tip_0, tip_1); + cr->line_to(tip_0 - delta4, tip_1 - delta4); + cr->line_to(tip_0 + delta4, tip_1 - delta4); + cr->close_path(); + + cr->move_to(size - tip_1, tip_0); + cr->line_to(size - tip_1 + delta4, tip_0 - delta4); + cr->line_to(size - tip_1 + delta4, tip_0 + delta4); + cr->close_path(); + + cr->move_to(size - tip_0, size - tip_1); + cr->line_to(size - tip_0 + delta4, size - tip_1 + delta4); + cr->line_to(size - tip_0 - delta4, size - tip_1 + delta4); + cr->close_path(); + + cr->move_to(tip_1, tip_0); + cr->line_to(tip_1 - delta4, tip_0 + delta4); + cr->line_to(tip_1 - delta4, tip_0 - delta4); + cr->close_path(); +} + +void CanvasItemCtrl::build_cache(int device_scale) const +{ + if (_width < 2 || _height < 2) { + return; // Nothing to render + } + + if (_shape != CANVAS_ITEM_CTRL_SHAPE_BITMAP) { + if (_width % 2 == 0 || _height % 2 == 0) { + std::cerr << "CanvasItemCtrl::build_cache: Width and/or height not odd integer! " + << _name << ": width: " << _width << " height: " << _height << std::endl; + } + } + + // Get memory for cache. + int width = _width * device_scale; // Not unsigned or math errors occur! + int height = _height * device_scale; + int size = width * height; + + _cache = std::make_unique<uint32_t[]>(size); + auto p = _cache.get(); + + switch (_shape) { + case CANVAS_ITEM_CTRL_SHAPE_SQUARE: + // Actually any rectanglular shape. + for (int i = 0; i < width; ++i) { + for (int j = 0; j < width; ++j) { + if (i + 1 > device_scale && device_scale < width - i && + j + 1 > device_scale && device_scale < height - j) + { + *p++ = _fill; + } else { + *p++ = _stroke; + } + } + } + break; + + case CANVAS_ITEM_CTRL_SHAPE_DIAMOND: { + // Assume width == height. + 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; + } 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; + } else { + *p++ = 0; + } + } + } + break; + } + + case CANVAS_ITEM_CTRL_SHAPE_CIRCLE: { + // Assume width == height. + 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; + } else if (r2 < rs2) { + *p++ = _stroke; + } else { + *p++ = 0; + } + } + } + break; + } + + case CANVAS_ITEM_CTRL_SHAPE_CROSS: + // Actually an 'X'. + 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; + } else { + *p++ = 0; + } + } + } + break; + + case CANVAS_ITEM_CTRL_SHAPE_PLUS: + // Actually an '+'. + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + if ( std::abs(x-width/2) < device_scale || + std::abs(y-height/2) < device_scale ) { + *p++ = _stroke; + } else { + *p++ = 0; + } + } + } + break; + case CANVAS_ITEM_CTRL_SHAPE_TRIANGLE: //triangle optionaly rotated + case CANVAS_ITEM_CTRL_SHAPE_TRIANGLE_ANGLED: // triangle with pointing to center of knot and rotated this way + case CANVAS_ITEM_CTRL_SHAPE_DARROW: // Double arrow + case CANVAS_ITEM_CTRL_SHAPE_SARROW: // Same shape as darrow but rendered rotated 90 degrees. + case CANVAS_ITEM_CTRL_SHAPE_CARROW: // Double corner arrow + case CANVAS_ITEM_CTRL_SHAPE_PIVOT: // Fancy "plus" + case CANVAS_ITEM_CTRL_SHAPE_SALIGN: // Side align (triangle pointing toward line) + case CANVAS_ITEM_CTRL_SHAPE_CALIGN: // Corner align (triangle pointing into "L") + case CANVAS_ITEM_CTRL_SHAPE_MALIGN: // Middle align (four triangles poining inward) + { + double size = _width; // Use unscaled width. + + auto work = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, device_scale * size, device_scale * size); + cairo_surface_set_device_scale(work->cobj(), device_scale, device_scale); // No C++ API! + auto cr = Cairo::Context::create(work); + + // Rotate around center + cr->translate( size/2.0, size/2.0); + cr->rotate(_angle); + cr->translate(-size/2.0, -size/2.0); + + // Construct path + bool triangles = _shape == CANVAS_ITEM_CTRL_SHAPE_TRIANGLE_ANGLED || _shape == CANVAS_ITEM_CTRL_SHAPE_TRIANGLE; + switch (_shape) { + case CANVAS_ITEM_CTRL_SHAPE_DARROW: + case CANVAS_ITEM_CTRL_SHAPE_SARROW: + draw_darrow(cr, size); + break; + + case CANVAS_ITEM_CTRL_SHAPE_TRIANGLE: + draw_triangle(cr, size); + break; + + case CANVAS_ITEM_CTRL_SHAPE_TRIANGLE_ANGLED: + draw_triangle_angled(cr, size); + break; + + case CANVAS_ITEM_CTRL_SHAPE_CARROW: + draw_carrow(cr, size); + break; + + case CANVAS_ITEM_CTRL_SHAPE_PIVOT: + draw_pivot(cr, size); + break; + + case CANVAS_ITEM_CTRL_SHAPE_SALIGN: + draw_salign(cr, size); + break; + + case CANVAS_ITEM_CTRL_SHAPE_CALIGN: + draw_calign(cr, size); + break; + + case CANVAS_ITEM_CTRL_SHAPE_MALIGN: + draw_malign(cr, size); + break; + + default: + // Shouldn't happen + break; + } + + // Fill and stroke. + cr->set_source_rgba(SP_RGBA32_R_F(_fill), + SP_RGBA32_G_F(_fill), + SP_RGBA32_B_F(_fill), + SP_RGBA32_A_F(_fill)); + cr->fill_preserve(); + cr->set_source_rgba(SP_RGBA32_R_F(_stroke), + SP_RGBA32_G_F(_stroke), + SP_RGBA32_B_F(_stroke), + SP_RGBA32_A_F(_stroke)); + cr->set_line_width(1); + cr->stroke(); + + // Copy to buffer. + work->flush(); + int strideb = work->get_stride(); + unsigned char* pxb = work->get_data(); + auto p = _cache.get(); + for (int i = 0; i < device_scale * size; ++i) { + auto pb = reinterpret_cast<uint32_t*>(pxb + i * strideb); + for (int j = 0; j < width; ++j) { + + if (triangles) { + *p++ = rgba_from_argb32(*pb); + } else { + uint32_t color = 0x0; + + // Need to un-premultiply alpha and change order argb -> rgba. + uint32_t alpha = (*pb & 0xff000000) >> 24; + if (alpha == 0x0) { + color = 0x0; + } else { + uint32_t rgb = unpremul_alpha(*pb & 0xffffff, alpha); + color = (rgb << 8) + alpha; + } + *p++ = color; + } + pb++; + } + } + break; + } + + case CANVAS_ITEM_CTRL_SHAPE_BITMAP: + { + if (_pixbuf) { + unsigned char* px = _pixbuf->get_pixels(); + unsigned int rs = _pixbuf->get_rowstride(); + 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; + uint32_t color; + if (s[3] < 0x80) { + color = 0; + } else if (s[0] < 0x80) { + color = _stroke; + } else { + color = _fill; + } + + // Fill in device_scale x device_scale block + for (int i = 0; i < device_scale; ++i) { + for (int j = 0; j < device_scale; ++j) { + auto p = _cache.get() + + (x * device_scale + i) + // Column + (y * device_scale + j) * width; // Row + *p = color; + } + } + } + } + } else { + std::cerr << "CanvasItemCtrl::build_cache: No bitmap!" << std::endl; + auto p = _cache.get(); + for (int y = 0; y < height/device_scale; y++){ + for (int x = 0; x < width/device_scale; x++) { + if (x == y) { + *p++ = 0xffff0000; + } else { + *p++ = 0; + } + } + } + } + break; + } + + case CANVAS_ITEM_CTRL_SHAPE_IMAGE: + std::cerr << "CanvasItemCtrl::build_cache: image: UNIMPLEMENTED" << std::endl; + break; + + default: + std::cerr << "CanvasItemCtrl::build_cache: unhandled shape!" << std::endl; + break; + } +} + +} // 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/control/canvas-item-ctrl.h b/src/display/control/canvas-item-ctrl.h new file mode 100644 index 0000000..6c072a2 --- /dev/null +++ b/src/display/control/canvas-item-ctrl.h @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_CANVAS_ITEM_CTRL_H +#define SEEN_CANVAS_ITEM_CTRL_H + +/** + * A class to represent a control node. + */ + +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of SPCtrl + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <memory> +#include <2geom/point.h> +#include <gdk-pixbuf/gdk-pixbuf.h> + +#include "canvas-item.h" +#include "canvas-item-enums.h" + +#include "enums.h" // SP_ANCHOR_X +#include "display/initlock.h" + +namespace Inkscape { + +class CanvasItemCtrl : public CanvasItem +{ +public: + CanvasItemCtrl(CanvasItemGroup *group); + CanvasItemCtrl(CanvasItemGroup *group, CanvasItemCtrlType type); + CanvasItemCtrl(CanvasItemGroup *group, CanvasItemCtrlType type, Geom::Point const &p); + CanvasItemCtrl(CanvasItemGroup *group, CanvasItemCtrlShape shape); + CanvasItemCtrl(CanvasItemGroup *group, CanvasItemCtrlShape shape, Geom::Point const &p); + + // Geometry + void set_position(Geom::Point const &position); + + double closest_distance_to(Geom::Point const &p) const; + + // Selection + bool contains(Geom::Point const &p, double tolerance = 0) override; + + // Properties + void set_fill(uint32_t rgba) override; + void set_stroke(uint32_t rgba) override; + void set_shape(CanvasItemCtrlShape shape); + void set_shape_default(); // Use type to determine shape. + void set_mode(CanvasItemCtrlMode mode); + void set_mode_default(); + void set_size(int size); + virtual void set_size_via_index(int size_index); + void set_size_default(); // Use preference and type to set size. + void set_size_extra(int extra); // Used to temporary increase size of ctrl. + void set_anchor(SPAnchorType anchor); + void set_angle(double angle); + void set_type(CanvasItemCtrlType type); + void set_pixbuf(Glib::RefPtr<Gdk::Pixbuf> pixbuf); + +protected: + ~CanvasItemCtrl() override = default; + + void _update(bool propagate) override; + void _render(Inkscape::CanvasItemBuffer &buf) const override; + + void build_cache(int device_scale) const; + + // Geometry + Geom::Point _position; + + // Display + InitLock _built; + mutable std::unique_ptr<uint32_t[]> _cache; + + // Properties + CanvasItemCtrlType _type = CANVAS_ITEM_CTRL_TYPE_DEFAULT; + CanvasItemCtrlShape _shape = CANVAS_ITEM_CTRL_SHAPE_SQUARE; + CanvasItemCtrlMode _mode = CANVAS_ITEM_CTRL_MODE_XOR; + int _width = 5; // Nominally width == height == size... unless we use a pixmap. + int _height = 5; + int _extra = 0; // Used to temporarily increase size. + double _angle = 0; // Used for triangles, could be used for arrows. + SPAnchorType _anchor = SP_ANCHOR_CENTER; + Glib::RefPtr<Gdk::Pixbuf> _pixbuf; +}; + +} // namespace Inkscape + +#endif // SEEN_CANVAS_ITEM_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 : diff --git a/src/display/control/canvas-item-curve.cpp b/src/display/control/canvas-item-curve.cpp new file mode 100644 index 0000000..c37c1a0 --- /dev/null +++ b/src/display/control/canvas-item-curve.cpp @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * A class to represent a single Bezier control curve, either a line or a cubic Bezier. + */ + +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of SPCtrlLine and SPCtrlCurve + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/bezier-curve.h> + +#include "canvas-item-curve.h" + +#include "color.h" // SP_RGBA_x_F + +#include "helper/geom.h" +#include "ui/widget/canvas.h" + +namespace Inkscape { + +/** + * Create an null control curve. + */ +CanvasItemCurve::CanvasItemCurve(CanvasItemGroup *group) + : CanvasItem(group) +{ + _name = "CanvasItemCurve:Null"; +} + +/** + * Create a linear control curve. Points are in document coordinates. + */ +CanvasItemCurve::CanvasItemCurve(CanvasItemGroup *group, Geom::Point const &p0, Geom::Point const &p1) + : CanvasItem(group) + , _curve(std::make_unique<Geom::LineSegment>(p0, p1)) +{ + _name = "CanvasItemCurve:Line"; +} + +/** + * Create a cubic Bezier control curve. Points are in document coordinates. + */ +CanvasItemCurve::CanvasItemCurve(CanvasItemGroup *group, + Geom::Point const &p0, Geom::Point const &p1, + Geom::Point const &p2, Geom::Point const &p3) + : CanvasItem(group) + , _curve(std::make_unique<Geom::CubicBezier>(p0, p1, p2, p3)) +{ + _name = "CanvasItemCurve:CubicBezier"; +} + +/** + * Set a linear control curve. Points are in document coordinates. + */ +void CanvasItemCurve::set_coords(Geom::Point const &p0, Geom::Point const &p1) +{ + defer([=] { + _name = "CanvasItemCurve:Line"; + _curve = std::make_unique<Geom::LineSegment>(p0, p1); + request_update(); + }); +} + +/** + * Set a cubic Bezier control curve. Points are in document coordinates. + */ +void CanvasItemCurve::set_coords(Geom::Point const &p0, Geom::Point const &p1, Geom::Point const &p2, Geom::Point const &p3) +{ + defer([=] { + _name = "CanvasItemCurve:CubicBezier"; + _curve = std::make_unique<Geom::CubicBezier>(p0, p1, p2, p3); + request_update(); + }); +} + +/** + * Set stroke width. + */ +void CanvasItemCurve::set_width(int width) +{ + defer([=] { + if (_width == width) return; + _width = width; + request_update(); + }); +} + +/** + * Set background stroke alpha. + */ +void CanvasItemCurve::set_bg_alpha(float alpha) +{ + defer([=] { + if (bg_alpha == alpha) return; + bg_alpha = alpha; + request_update(); + }); +} + +/** + * Returns distance between point in canvas units and nearest point on curve. + */ +double CanvasItemCurve::closest_distance_to(Geom::Point const &p) const +{ + double d = Geom::infinity(); + if (_curve) { + Geom::BezierCurve curve = *_curve; + curve *= affine(); // Document to canvas. + Geom::Point n = curve.pointAt(curve.nearestTime(p)); + d = Geom::distance(p, n); + } + return d; +} + +/** + * Returns true if point p (in canvas units) is within tolerance (canvas units) distance of curve. + */ +bool CanvasItemCurve::contains(Geom::Point const &p, double tolerance) +{ + return closest_distance_to(p) <= tolerance; +} + +/** + * Update and redraw control curve. + */ +void CanvasItemCurve::_update(bool) +{ + // Queue redraw of old area (erase previous content). + request_redraw(); // This is actually never useful as curves are always deleted + // and recreated when a node is moved! But keep it in case we change that. + + if (!_curve || _curve->isDegenerate()) { + _bounds = {}; + return; // No curve! Can happen - see node.h. + } + + // Tradeoff between updating a larger area (typically twice for Beziers?) vs computation time for bounds. + _bounds = expandedBy(_curve->boundsExact() * affine(), 2); // Room for stroke. + + // Queue redraw of new area + request_redraw(); +} + +/** + * Render curve to screen via Cairo. + */ +void CanvasItemCurve::_render(Inkscape::CanvasItemBuffer &buf) const +{ + assert(_curve); // Not called if _curve is null, since _bounds would be null. + + // Todo: Transform, rather than copy. + Geom::BezierCurve curve = *_curve; + curve *= affine(); // Document to canvas. + curve *= Geom::Translate(-buf.rect.min()); // Canvas to screen. + + buf.cr->save(); + + buf.cr->begin_new_path(); + + if (curve.size() == 2) { + // Line + buf.cr->move_to(curve[0].x(), curve[0].y()); + buf.cr->line_to(curve[1].x(), curve[1].y()); + } else { + // Curve + buf.cr->move_to(curve[0].x(), curve[0].y()); + buf.cr->curve_to(curve[1].x(), curve[1].y(), curve[2].x(), curve[2].y(), curve[3].x(), curve[3].y()); + } + + buf.cr->set_source_rgba(1.0, 1.0, 1.0, bg_alpha); + buf.cr->set_line_width(background_width); + buf.cr->stroke_preserve(); + + buf.cr->set_source_rgba(SP_RGBA32_R_F(_stroke), SP_RGBA32_G_F(_stroke), SP_RGBA32_B_F(_stroke), SP_RGBA32_A_F(_stroke)); + buf.cr->set_line_width(_width); + buf.cr->stroke(); + + // Uncomment to show bounds + // Geom::Rect bounds = _bounds; + // bounds.expandBy(-1); + // bounds -= buf.rect.min(); + // buf.cr->set_source_rgba(1.0, 0.0, 0.0, 1.0); + // buf.cr->rectangle(bounds.min().x(), bounds.min().y(), bounds.width(), bounds.height()); + // buf.cr->stroke(); + + buf.cr->restore(); +} + +} // 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/control/canvas-item-curve.h b/src/display/control/canvas-item-curve.h new file mode 100644 index 0000000..8e1f9b5 --- /dev/null +++ b/src/display/control/canvas-item-curve.h @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_CANVAS_ITEM_CURVE_H +#define SEEN_CANVAS_ITEM_CURVE_H + +/** + * A class to represent a single Bezier control curve. + */ + +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of SPCtrlLine and SPCtrlCurve + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <memory> +#include <2geom/path.h> + +#include "canvas-item.h" + +namespace Inkscape { + +class CanvasItemCurve final : public CanvasItem +{ +public: + CanvasItemCurve(CanvasItemGroup *group); + CanvasItemCurve(CanvasItemGroup *group, Geom::Point const &p0, Geom::Point const &p1); + CanvasItemCurve(CanvasItemGroup *group, Geom::Point const &p0, Geom::Point const &p1, + Geom::Point const &p2, Geom::Point const &p3); + + // Geometry + void set_coords(Geom::Point const &p0, Geom::Point const &p1); + void set_coords(Geom::Point const &p0, Geom::Point const &p1, Geom::Point const &p2, Geom::Point const &p3); + void set_width(int width); + void set_bg_alpha(float alpha); + bool is_line() const { return _curve->size() == 2; } + + double closest_distance_to(Geom::Point const &p) const; + + // Selection + bool contains(Geom::Point const &p, double tolerance = 0) override; + +protected: + ~CanvasItemCurve() override = default; + + void _update(bool propagate) override; + void _render(Inkscape::CanvasItemBuffer &buf) const override; + + // Display + std::unique_ptr<Geom::BezierCurve> _curve; + + int _width = 1; + int background_width = 3; // this should be an odd number so that the background appears on both the sides of the curve. + float bg_alpha = 0.5f; +}; + +} // namespace Inkscape + +#endif // SEEN_CANVAS_ITEM_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 : diff --git a/src/display/control/canvas-item-drawing.cpp b/src/display/control/canvas-item-drawing.cpp new file mode 100644 index 0000000..cfa527a --- /dev/null +++ b/src/display/control/canvas-item-drawing.cpp @@ -0,0 +1,220 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * A class to render the SVG drawing. + */ + +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of _SPCanvasArena. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "canvas-item-drawing.h" + +#include "desktop.h" + +#include "display/drawing.h" +#include "display/drawing-context.h" +#include "display/drawing-item.h" +#include "display/drawing-group.h" + +#include "helper/geom.h" +#include "ui/widget/canvas.h" +#include "ui/modifiers.h" + +namespace Inkscape { + +/** + * Create the drawing. One per window! + */ +CanvasItemDrawing::CanvasItemDrawing(CanvasItemGroup *group) + : CanvasItem(group) +{ + _name = "CanvasItemDrawing"; + _pickable = true; + + _drawing = std::make_unique<Drawing>(this); + auto root = new DrawingGroup(*_drawing); + root->setPickChildren(true); + _drawing->setRoot(root); +} + +/** + * Returns true if point p (in canvas units) is inside some object in drawing. + */ +bool CanvasItemDrawing::contains(Geom::Point const &p, double tolerance) +{ + if (tolerance != 0) { + std::cerr << "CanvasItemDrawing::contains: Non-zero tolerance not implemented!" << std::endl; + } + + _picked_item = _drawing->pick(p, _drawing->cursorTolerance(), _sticky * DrawingItem::PICK_STICKY | _pick_outline * DrawingItem::PICK_OUTLINE); + + if (_picked_item) { + // This will trigger a signal that is handled by our event handler. Seems a bit of a + // round-about way of doing things but it matches what other pickable canvas-item classes do. + return true; + } + + return false; +} + +/** + * Update and redraw drawing. + */ +void CanvasItemDrawing::_update(bool) +{ + // Undo y-axis flip. This should not be here!!!! + auto new_drawing_affine = affine(); + if (auto desktop = get_canvas()->get_desktop()) { + new_drawing_affine = desktop->doc2dt() * new_drawing_affine; + } + + bool affine_changed = _drawing_affine != new_drawing_affine; + if (affine_changed) { + _drawing_affine = new_drawing_affine; + } + + _drawing->update(Geom::IntRect::infinite(), _drawing_affine, DrawingItem::STATE_ALL, affine_changed * DrawingItem::STATE_ALL); + + _bounds = expandedBy(_drawing->root()->drawbox(), 1); // Avoid aliasing artifacts + + // Todo: This should be managed elsewhere. + if (_cursor) { + /* Mess with enter/leave notifiers */ + DrawingItem *new_drawing_item = _drawing->pick(_c, _delta, _sticky * DrawingItem::PICK_STICKY | _pick_outline * DrawingItem::PICK_OUTLINE); + if (_active_item != new_drawing_item) { + + GdkEventCrossing ec; + ec.window = get_canvas()->get_window()->gobj(); + ec.send_event = true; + ec.subwindow = ec.window; + ec.time = GDK_CURRENT_TIME; + ec.x = _c.x(); + ec.y = _c.y(); + + /* fixme: Why? */ + if (_active_item) { + ec.type = GDK_LEAVE_NOTIFY; + _drawing_event_signal.emit((GdkEvent *) &ec, _active_item); + } + + _active_item = new_drawing_item; + + if (_active_item) { + ec.type = GDK_ENTER_NOTIFY; + _drawing_event_signal.emit((GdkEvent *) &ec, _active_item); + } + } + } +} + +/** + * Render drawing to screen via Cairo. + */ +void CanvasItemDrawing::_render(Inkscape::CanvasItemBuffer &buf) const +{ + auto dc = Inkscape::DrawingContext(buf.cr->cobj(), buf.rect.min()); + _drawing->render(dc, buf.rect, buf.outline_pass * DrawingItem::RENDER_OUTLINE); +} + +/** + * Handle events directed at the drawing. We first attempt to handle them here. + */ +bool CanvasItemDrawing::handle_event(GdkEvent *event) +{ + bool retval = false; + + switch (event->type) { + case GDK_ENTER_NOTIFY: + if (!_cursor) { + if (_active_item) { + std::cerr << "CanvasItemDrawing::event_handler: cursor entered drawing with an active item!" << std::endl; + } + _cursor = true; + + /* TODO ... event -> arena transform? */ + _c = Geom::Point(event->crossing.x, event->crossing.y); + + _active_item = _drawing->pick(_c, _drawing->cursorTolerance(), _sticky * DrawingItem::PICK_STICKY | _pick_outline * DrawingItem::PICK_OUTLINE); + retval = _drawing_event_signal.emit(event, _active_item); + } + break; + + case GDK_LEAVE_NOTIFY: + if (_cursor) { + retval = _drawing_event_signal.emit(event, _active_item); + _active_item = nullptr; + _cursor = false; + } + break; + + case GDK_MOTION_NOTIFY: + { + /* TODO ... event -> arena transform? */ + _c = Geom::Point(event->motion.x, event->motion.y); + + auto new_drawing_item = _drawing->pick(_c, _drawing->cursorTolerance(), _sticky * DrawingItem::PICK_STICKY | _pick_outline * DrawingItem::PICK_OUTLINE); + if (_active_item != new_drawing_item) { + + 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: What is wrong? */ + if (_active_item) { + ec.type = GDK_LEAVE_NOTIFY; + retval = _drawing_event_signal.emit((GdkEvent *) &ec, _active_item); + } + + _active_item = new_drawing_item; + + if (_active_item) { + ec.type = GDK_ENTER_NOTIFY; + retval = _drawing_event_signal.emit((GdkEvent *) &ec, _active_item); + } + } + retval = retval || _drawing_event_signal.emit(event, _active_item); + break; + } + + case GDK_SCROLL: + { + if (Modifiers::Modifier::get(Modifiers::Type::CANVAS_ZOOM)->active(event->scroll.state)) { + /* Zoom is emitted by the canvas as well, ignore here */ + return false; + } + retval = _drawing_event_signal.emit(event, _active_item); + break; + } + + default: + /* Just send event */ + retval = _drawing_event_signal.emit(event, _active_item); + break; + } + + return retval; +} + +} // 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/control/canvas-item-drawing.h b/src/display/control/canvas-item-drawing.h new file mode 100644 index 0000000..f3fd494 --- /dev/null +++ b/src/display/control/canvas-item-drawing.h @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_CANVAS_ITEM_DRAWING_H +#define SEEN_CANVAS_ITEM_DRAWING_H + +/** + * A class to render the SVG drawing. + */ + +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of _SPCanvasArena. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <sigc++/sigc++.h> + + +#include "canvas-item.h" + +namespace Inkscape { + +class Drawing; +class DrawingItem; +class Updatecontext; + +class CanvasItemDrawing final : public CanvasItem +{ +public: + CanvasItemDrawing(CanvasItemGroup *group); + + // Selection + bool contains(Geom::Point const &p, double tolerance = 0) override; + + // Display + Inkscape::Drawing *get_drawing() { return _drawing.get(); } + + // Drawing items + void set_active(Inkscape::DrawingItem *active) { _active_item = active; } + Inkscape::DrawingItem *get_active() { return _active_item; } + + // Events + bool handle_event(GdkEvent *event) override; + void set_sticky(bool sticky) { _sticky = sticky; } + void set_pick_outline(bool pick_outline) { _pick_outline = pick_outline; } + + // Signals + sigc::connection connect_drawing_event(sigc::slot<bool (GdkEvent*, Inkscape::DrawingItem *)> slot) { + return _drawing_event_signal.connect(slot); + } + +protected: + ~CanvasItemDrawing() override = default; + + void _update(bool propagate) override; + void _render(Inkscape::CanvasItemBuffer &buf) const override; + + // Selection + Geom::Point _c; + double _delta = Geom::infinity(); + Inkscape::DrawingItem *_active_item = nullptr; + Inkscape::DrawingItem *_picked_item = nullptr; + + // Display + std::unique_ptr<Inkscape::Drawing> _drawing; + Geom::Affine _drawing_affine; + + // Events + bool _cursor = false; + bool _sticky = false; // Pick anything, even if hidden. + bool _pick_outline = false; + + // Signals + sigc::signal<bool (GdkEvent*, Inkscape::DrawingItem *)> _drawing_event_signal; +}; + +} // namespace Inkscape + +#endif // SEEN_CANVAS_ITEM_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 : diff --git a/src/display/control/canvas-item-enums.h b/src/display/control/canvas-item-enums.h new file mode 100644 index 0000000..ab173f3 --- /dev/null +++ b/src/display/control/canvas-item-enums.h @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Enums for CanvasItems. + */ +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + */ + +#ifndef SEEN_CANVAS_ITEM_ENUMS_H +#define SEEN_CANVAS_ITEM_ENUMS_H + +namespace Inkscape { + +enum CanvasItemColor { + CANVAS_ITEM_PRIMARY, + CANVAS_ITEM_SECONDARY, + CANVAS_ITEM_TERTIARY +}; + +enum CanvasItemCtrlShape { + CANVAS_ITEM_CTRL_SHAPE_SQUARE, + CANVAS_ITEM_CTRL_SHAPE_DIAMOND, + CANVAS_ITEM_CTRL_SHAPE_CIRCLE, + CANVAS_ITEM_CTRL_SHAPE_TRIANGLE, + CANVAS_ITEM_CTRL_SHAPE_CROSS, + CANVAS_ITEM_CTRL_SHAPE_PLUS, + CANVAS_ITEM_CTRL_SHAPE_PIVOT, // Fancy "plus" + CANVAS_ITEM_CTRL_SHAPE_DARROW, // Double headed arrow. + CANVAS_ITEM_CTRL_SHAPE_SARROW, // Double headed arrow, rotated (skew). + CANVAS_ITEM_CTRL_SHAPE_CARROW, // Double headed curved arrow. + CANVAS_ITEM_CTRL_SHAPE_SALIGN, // Side alignment. + CANVAS_ITEM_CTRL_SHAPE_CALIGN, // Corner alignment. + CANVAS_ITEM_CTRL_SHAPE_MALIGN, // Center (middle) alignment. + CANVAS_ITEM_CTRL_SHAPE_BITMAP, + CANVAS_ITEM_CTRL_SHAPE_IMAGE, + CANVAS_ITEM_CTRL_SHAPE_LINE, + CANVAS_ITEM_CTRL_SHAPE_TRIANGLE_ANGLED, +}; + +// Applies to control points. +enum CanvasItemCtrlType { + CANVAS_ITEM_CTRL_TYPE_DEFAULT, + CANVAS_ITEM_CTRL_TYPE_ADJ_HANDLE, // Stretch & Scale + CANVAS_ITEM_CTRL_TYPE_ADJ_SKEW, + CANVAS_ITEM_CTRL_TYPE_ADJ_ROTATE, + CANVAS_ITEM_CTRL_TYPE_ADJ_CENTER, + CANVAS_ITEM_CTRL_TYPE_ADJ_SALIGN, + CANVAS_ITEM_CTRL_TYPE_ADJ_CALIGN, + CANVAS_ITEM_CTRL_TYPE_ADJ_MALIGN, + CANVAS_ITEM_CTRL_TYPE_ANCHOR, + CANVAS_ITEM_CTRL_TYPE_POINT, + CANVAS_ITEM_CTRL_TYPE_ROTATE, + CANVAS_ITEM_CTRL_TYPE_MARGIN, + CANVAS_ITEM_CTRL_TYPE_CENTER, + CANVAS_ITEM_CTRL_TYPE_SIZER, + CANVAS_ITEM_CTRL_TYPE_SHAPER, + CANVAS_ITEM_CTRL_TYPE_LPE, + CANVAS_ITEM_CTRL_TYPE_NODE_AUTO, + CANVAS_ITEM_CTRL_TYPE_NODE_CUSP, + CANVAS_ITEM_CTRL_TYPE_NODE_SMOOTH, + CANVAS_ITEM_CTRL_TYPE_NODE_SYMETRICAL, + CANVAS_ITEM_CTRL_TYPE_INVISIPOINT +}; + +enum CanvasItemCtrlMode { + CANVAS_ITEM_CTRL_MODE_COLOR, + CANVAS_ITEM_CTRL_MODE_XOR, + CANVAS_ITEM_CTRL_MODE_DESATURATED_XOR, + CANVAS_ITEM_CTRL_MODE_GRAYSCALED_XOR +}; + +} // namespace Inkscape + +#endif // SEEN_CANVAS_ITEM_ENUMS_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/control/canvas-item-grid.cpp b/src/display/control/canvas-item-grid.cpp new file mode 100644 index 0000000..2c14ae2 --- /dev/null +++ b/src/display/control/canvas-item-grid.cpp @@ -0,0 +1,522 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of GridCanvasItem. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/line.h> + +#include "canvas-item-grid.h" +#include "color.h" +#include "helper/geom.h" + +enum Dim3 { X, Y, Z }; + +static int calculate_scaling_factor(double length, int major) +{ + int multiply = 1; + int step = std::max(major, 1); + int watchdog = 0; + + while (length * multiply < 8.0 && watchdog < 100) { + multiply *= step; + // First pass, go up to the major line spacing, then keep increasing by two. + step = 2; + watchdog++; + } + + return multiply; +} + +namespace Inkscape { + +/** + * Create a null control grid. + */ +CanvasItemGrid::CanvasItemGrid(CanvasItemGroup *group) + : CanvasItem(group) + , _origin(0, 0) + , _spacing(1, 1) + , _minor_color(GRID_DEFAULT_MINOR_COLOR) + , _major_color(GRID_DEFAULT_MAJOR_COLOR) + , _major_line_interval(5) + , _dotted(false) +{ + _no_emp_when_zoomed_out = Preferences::get()->getBool("/options/grids/no_emphasize_when_zoomedout"); + _pref_tracker = Preferences::PreferencesObserver::create("/options/grids/no_emphasize_when_zoomedout", [this] (auto &entry) { + set_no_emp_when_zoomed_out(entry.getBool()); + }); + + request_update(); +} + +/** + * Returns true if point p (in canvas units) is within tolerance (canvas units) distance of grid. + */ +bool CanvasItemGrid::contains(Geom::Point const &p, double tolerance) +{ + return false; // We're not pickable! +} + +// 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) +{ + return Geom::cross(point - line.initialPoint(), line.versor()); +} + +// 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 (int i = 0; i < 4; ++i) { + Geom::LineSegment side(rect.corner(i), rect.corner((i + 1) % 4)); + try { + if (auto oc = Geom::intersection(line, side)) { + intersections.emplace_back(line.pointAt(oc->ta)); + } + } catch (Geom::InfiniteSolutions const &) { + return { side.pointAt(0), side.pointAt(1) }; + } + } + return intersections; +} + +void CanvasItemGrid::set_origin(Geom::Point const &point) +{ + defer([=] { + if (_origin == point) return; + _origin = point; + request_update(); + }); +} + +void CanvasItemGrid::set_major_color(uint32_t color) +{ + defer([=] { + if (_major_color == color) return; + _major_color = color; + request_update(); + }); +} + +void CanvasItemGrid::set_minor_color(uint32_t color) +{ + defer([=] { + if (_minor_color == color) return; + _minor_color = color; + request_update(); + }); +} + +void CanvasItemGrid::set_dotted(bool dotted) +{ + defer([=] { + if (_dotted == dotted) return; + _dotted = dotted; + request_update(); + }); +} + +void CanvasItemGrid::set_spacing(Geom::Point const &point) +{ + defer([=] { + if (_spacing == point) return; + _spacing = point; + request_update(); + }); +} + +void CanvasItemGrid::set_major_line_interval(int n) +{ + if (n < 1) return; + defer([=] { + if (_major_line_interval == n) return; + _major_line_interval = n; + request_update(); + }); +} + +void CanvasItemGrid::set_no_emp_when_zoomed_out(bool noemp) +{ + if (_no_emp_when_zoomed_out != noemp) { + _no_emp_when_zoomed_out = noemp; + request_redraw(); + } +} + +/** ====== Rectangular Grid ====== **/ + +CanvasItemGridXY::CanvasItemGridXY(Inkscape::CanvasItemGroup *group) + : CanvasItemGrid(group) +{ + _name = "CanvasItemGridXY"; +} + +void CanvasItemGridXY::_update(bool) +{ + _bounds = Geom::Rect(-Geom::infinity(), -Geom::infinity(), Geom::infinity(), Geom::infinity()); + + // Queue redraw of grid area + 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, 1}) { + int const scaling_factor = calculate_scaling_factor(sw[dim].length(), _major_line_interval); + sw[dim] *= scaling_factor; + scaled[dim] = scaling_factor > 1; + } + + request_redraw(); +} + +void CanvasItemGridXY::_render(Inkscape::CanvasItemBuffer &buf) const +{ + // no_emphasize_when_zoomedout determines color (minor or major) when only major grid lines/dots shown. + uint32_t empcolor = ((scaled[Geom::X] || scaled[Geom::Y]) && _no_emp_when_zoomed_out) ? _minor_color : _major_color; + uint32_t color = _minor_color; + + buf.cr->save(); + buf.cr->translate(-buf.rect.left(), -buf.rect.top()); + buf.cr->set_line_width(1.0); + buf.cr->set_line_cap(Cairo::LINE_CAP_SQUARE); + + // Add a 2px margin to the buffer rectangle to avoid missing intersections (in case of rounding errors, and due to adding 0.5 below) + auto const buf_rect_with_margin = expandedBy(buf.rect, 2); + + for (int dim : {0, 1}) { + int const nrm = dim ^ 0x1; + + // 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[nrm]); + + double spacing = sw[nrm].length(); // Spacing between grid lines. + double dash = sw[dim].length(); // Total length of dash pattern. + + // Find the minimum and maximum distances of the buffer corners from axis. + double min = Geom::infinity(); + double max = -Geom::infinity(); + for (int 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; + } + + min = std::min(min, distance); + max = std::max(max, distance); + } + int start = std::floor(min / spacing); + int stop = std::floor(max / spacing); + + // 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[nrm], 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 CanvasItemGridXY::render(), and the drawing of the rulers + buf.cr->move_to(floor(x[0].x()) + 0.5, floor(x[0].y()) + 0.5); + buf.cr->line_to(floor(x[1].x()) + 0.5, floor(x[1].y()) + 0.5); + + // Determine whether to draw with the emphasis color. + bool const noemp = !scaled[dim] && j % _major_line_interval != 0; + + // Set dash pattern and color. + if (_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. + uint32_t _empdot = (empcolor & 0xff) << 2; + if (_empdot > 0xff) + _empdot = 0xff; + _empdot += (empcolor & 0xffffff00); + + uint32_t _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 = std::fmod(signed_distance(x[0], orth), sw[dim].length()); + if (Geom::cross(axis.vector(), orth.vector()) > 0) { + offset = -offset; + } + + std::vector<double> dashes; + if (noemp) { + // Minor lines + dashes.push_back(1.0); + dashes.push_back(dash - 1.0); + offset -= 0.5; + buf.cr->set_source_rgba(SP_RGBA32_R_F(_colordot), SP_RGBA32_G_F(_colordot), + SP_RGBA32_B_F(_colordot), SP_RGBA32_A_F(_colordot)); + } else { + // Major lines + dashes.push_back(3.0); + dashes.push_back(dash - 3.0); + offset -= 1.5; // Center dash on intersection. + buf.cr->set_source_rgba(SP_RGBA32_R_F(_empdot), SP_RGBA32_G_F(_empdot), + SP_RGBA32_B_F(_empdot), SP_RGBA32_A_F(_empdot)); + } + + buf.cr->set_line_cap(Cairo::LINE_CAP_BUTT); + buf.cr->set_dash(dashes, -offset); + + } else { + // Solid lines + uint32_t col = noemp ? color : empcolor; + buf.cr->set_source_rgba(SP_RGBA32_R_F(col), SP_RGBA32_G_F(col), + SP_RGBA32_B_F(col), SP_RGBA32_A_F(col)); + } + + buf.cr->stroke(); + + } else { + std::cerr << "CanvasItemGridXY::render: Grid line doesn't intersect!" << std::endl; + } + } + } + + buf.cr->restore(); +} + +/** ========= Axonometric Grids ======== */ + +/* + * 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. + */ +CanvasItemGridAxonom::CanvasItemGridAxonom(Inkscape::CanvasItemGroup *group) + : CanvasItemGrid(group) +{ + _name = "CanvasItemGridAxonom"; + + angle_deg[X] = 30.0; + angle_deg[Y] = 30.0; + angle_deg[Z] = 0.0; + + angle_rad[X] = Geom::rad_from_deg(angle_deg[X]); + angle_rad[Y] = Geom::rad_from_deg(angle_deg[Y]); + angle_rad[Z] = Geom::rad_from_deg(angle_deg[Z]); + + tan_angle[X] = std::tan(angle_rad[X]); + tan_angle[Y] = std::tan(angle_rad[Y]); + tan_angle[Z] = std::tan(angle_rad[Z]); +} + +void CanvasItemGridAxonom::_update(bool) +{ + _bounds = Geom::Rect(-Geom::infinity(), -Geom::infinity(), Geom::infinity(), Geom::infinity()); + + ow = _origin * affine(); + lyw = _spacing.y() * affine().descrim(); + + int const scaling_factor = calculate_scaling_factor(lyw, _major_line_interval); + lyw *= scaling_factor; + scaled = scaling_factor > 1; + + spacing_ylines = lyw / (tan_angle[X] + tan_angle[Z]); + lxw_x = Geom::are_near(tan_angle[X], 0) ? Geom::infinity() : lyw / tan_angle[X]; + lxw_z = Geom::are_near(tan_angle[Z], 0) ? Geom::infinity() : lyw / tan_angle[Z]; + + if (_major_line_interval == 0) { + scaled = true; + } + + request_redraw(); +} + +// expects value given to be in degrees +void CanvasItemGridAxonom::set_angle_x(double deg) +{ + defer([=] { + angle_deg[X] = std::clamp(deg, 0.0, 89.0); // setting to 90 and values close cause extreme slowdowns + angle_rad[X] = Geom::rad_from_deg(angle_deg[X]); + tan_angle[X] = std::tan(angle_rad[X]); + request_update(); + }); +} + +// expects value given to be in degrees +void CanvasItemGridAxonom::set_angle_z(double deg) +{ + defer([=] { + angle_deg[Z] = std::clamp(deg, 0.0, 89.0); // setting to 90 and values close cause extreme slowdowns + angle_rad[Z] = Geom::rad_from_deg(angle_deg[Z]); + tan_angle[Z] = std::tan(angle_rad[Z]); + request_update(); + }); +} + +static void drawline(Inkscape::CanvasItemBuffer &buf, int x0, int y0, int x1, int y1, uint32_t rgba) +{ + buf.cr->move_to(0.5 + x0, 0.5 + y0); + buf.cr->line_to(0.5 + x1, 0.5 + y1); + buf.cr->set_source_rgba(SP_RGBA32_R_F(rgba), SP_RGBA32_G_F(rgba), + SP_RGBA32_B_F(rgba), SP_RGBA32_A_F(rgba)); + buf.cr->stroke(); +} + +static void vline(Inkscape::CanvasItemBuffer &buf, int x, int ys, int ye, uint32_t rgba) +{ + if (x < buf.rect.left() || x >= buf.rect.right()) + return; + + buf.cr->move_to(0.5 + x, 0.5 + ys); + buf.cr->line_to(0.5 + x, 0.5 + ye); + buf.cr->set_source_rgba(SP_RGBA32_R_F(rgba), SP_RGBA32_G_F(rgba), + SP_RGBA32_B_F(rgba), SP_RGBA32_A_F(rgba)); + buf.cr->stroke(); +} + +/** + * This function calls Cairo to render a line on a particular canvas buffer. + * Coordinates are interpreted as SCREENcoordinates + */ +void CanvasItemGridAxonom::_render(Inkscape::CanvasItemBuffer &buf) const +{ + // Set correct coloring, depending preference (when zoomed out, always major coloring or minor coloring) + uint32_t empcolor = (scaled && _no_emp_when_zoomed_out) ? _minor_color : _major_color; + uint32_t color = _minor_color; + + buf.cr->save(); + buf.cr->translate(-buf.rect.left(), -buf.rect.top()); + buf.cr->set_line_width(1.0); + buf.cr->set_line_cap(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 + auto const buf_tl_gc = buf.rect.min() - ow; + + // render the three separate line groups representing the main-axes + + // x-axis always goes from topleft to bottomright. (0,0) - (1,1) + double const xintercept_y_bc = (buf_tl_gc.x() * tan_angle[X]) - buf_tl_gc.y(); + double const xstart_y_sc = (xintercept_y_bc - std::floor(xintercept_y_bc / lyw) * lyw) + buf.rect.top(); + int const xlinestart = std::round((xstart_y_sc - buf_tl_gc.x() * tan_angle[X] - ow.y()) / lyw); + int xlinenum = xlinestart; + + // lines starting on left side. + for (double y = xstart_y_sc; y < buf.rect.bottom(); y += lyw, xlinenum++) { + int const x0 = buf.rect.left(); + int const y0 = round(y); + int x1 = x0 + round((buf.rect.bottom() - y) / tan_angle[X]); + int y1 = buf.rect.bottom(); + if (Geom::are_near(tan_angle[X], 0)) { + x1 = buf.rect.right(); + y1 = y0; + } + + bool const noemp = !scaled && xlinenum % _major_line_interval != 0; + drawline(buf, x0, y0, x1, y1, noemp ? color : empcolor); + } + + // lines starting from top side + if (!Geom::are_near(tan_angle[X], 0)) { + double const xstart_x_sc = buf.rect.left() + (lxw_x - (xstart_y_sc - buf.rect.top()) / tan_angle[X]); + xlinenum = xlinestart-1; + for (double x = xstart_x_sc; x < buf.rect.right(); x += lxw_x, xlinenum--) { + int const y0 = buf.rect.top(); + int const y1 = buf.rect.bottom(); + int const x0 = round(x); + int const x1 = x0 + round((y1 - y0) / tan_angle[X]); + + bool const noemp = !scaled && xlinenum % _major_line_interval != 0; + drawline(buf, x0, y0, x1, y1, noemp ? color : empcolor); + } + } + + // y-axis lines (vertical) + double const ystart_x_sc = floor (buf_tl_gc[Geom::X] / spacing_ylines) * spacing_ylines + ow[Geom::X]; + int const ylinestart = round((ystart_x_sc - ow[Geom::X]) / spacing_ylines); + int ylinenum = ylinestart; + for (double x = ystart_x_sc; x < buf.rect.right(); x += spacing_ylines, ylinenum++) { + int 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 + // CanvasItemGridXY::render() for more details + bool const noemp = !scaled && ylinenum % _major_line_interval != 0; + vline(buf, x0, buf.rect.top(), buf.rect.bottom() - 1, noemp ? color : empcolor); + } + + // z-axis always goes from bottomleft to topright. (0,1) - (1,0) + double const zintercept_y_bc = (buf_tl_gc.x() * -tan_angle[Z]) - buf_tl_gc.y(); + double const zstart_y_sc = (zintercept_y_bc - std::floor(zintercept_y_bc / lyw) * lyw) + buf.rect.top(); + int const zlinestart = std::round((zstart_y_sc + buf_tl_gc.x() * tan_angle[Z] - ow.y()) / lyw); + int zlinenum = zlinestart; + // lines starting from left side + double next_y = zstart_y_sc; + for (double y = zstart_y_sc; y < buf.rect.bottom(); y += lyw, zlinenum++, next_y = y) { + int const x0 = buf.rect.left(); + int const y0 = round(y); + int x1 = x0 + round((y - buf.rect.top()) / tan_angle[Z]); + int y1 = buf.rect.top(); + if (Geom::are_near(tan_angle[Z], 0)) { + x1 = buf.rect.right(); + y1 = y0; + } + + bool const noemp = !scaled && zlinenum % _major_line_interval != 0; + drawline(buf, x0, y0, x1, y1, noemp ? color : empcolor); + } + + // draw lines from bottom-up + if (!Geom::are_near(tan_angle[Z], 0)) { + double const zstart_x_sc = buf.rect.left() + (next_y - buf.rect.bottom()) / tan_angle[Z]; + for (double x = zstart_x_sc; x < buf.rect.right(); x += lxw_z, zlinenum++) { + int const y0 = buf.rect.bottom(); + int const y1 = buf.rect.top(); + int const x0 = round(x); + int const x1 = x0 + round(buf.rect.height() / tan_angle[Z]); + + bool const noemp = !scaled && zlinenum % _major_line_interval != 0; + drawline(buf, x0, y0, x1, y1, noemp ? color : empcolor); + } + } + + buf.cr->restore(); +} + +} // 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/control/canvas-item-grid.h b/src/display/control/canvas-item-grid.h new file mode 100644 index 0000000..3e8bc28 --- /dev/null +++ b/src/display/control/canvas-item-grid.h @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of GridCanvasItem. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_CANVAS_ITEM_GRID_H +#define SEEN_CANVAS_ITEM_GRID_H + +#include <cstdint> +#include <2geom/point.h> + +#include "canvas-item.h" +#include "preferences.h" + +uint32_t constexpr GRID_DEFAULT_MAJOR_COLOR = 0x0099e54d; +uint32_t constexpr GRID_DEFAULT_MINOR_COLOR = 0x0099e526; + +namespace Inkscape { + +class CanvasItemGrid : public CanvasItem +{ +public: + CanvasItemGrid(CanvasItemGroup *group); + + // Selection + bool contains(Geom::Point const &p, double tolerance = 0) override; + + // Properties + void set_major_color(uint32_t color); + void set_minor_color(uint32_t color); + void set_origin(Geom::Point const &point); + void set_spacing(Geom::Point const &point); + void set_dotted(bool b); + void set_major_line_interval(int n); + void set_no_emp_when_zoomed_out(bool noemp); + +protected: + ~CanvasItemGrid() override = default; + + bool _dotted; + + Geom::Point _origin; + + Geom::Point _spacing; /**< Spacing between elements of the grid */ + + int _major_line_interval; + bool _no_emp_when_zoomed_out; + uint32_t _major_color; + uint32_t _minor_color; + +private: + std::unique_ptr<Preferences::PreferencesObserver> _pref_tracker; +}; + +/** Canvas Item for rectangular grids */ +class CanvasItemGridXY final : public CanvasItemGrid +{ +public: + CanvasItemGridXY(CanvasItemGroup *group); + +protected: + friend class GridSnapperXY; + + void _update(bool propagate) override; + void _render(CanvasItemBuffer &buf) const override; + + 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 */ +}; + +/** Canvas Item for axonometric grids */ +class CanvasItemGridAxonom final : public CanvasItemGrid +{ +public: + CanvasItemGridAxonom(CanvasItemGroup *group); + + // Properties + void set_angle_x(double value); + void set_angle_z(double value); + +protected: + friend class GridSnapperAxonom; + + void _update(bool propagate) override; + void _render(CanvasItemBuffer &buf) const override; + + bool scaled; /**< Whether the grid is in scaled mode */ + + 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[.]) */ + + double lyw = 1.0; /**< Transformed length y by the affine for the zoom */ + double lxw_x = 1.0; + double lxw_z = 1.0; + double spacing_ylines = 1.0; + + Geom::Point ow; /**< Transformed origin by the affine for the zoom */ +}; + +} // namespace Inkscape + +#endif // SEEN_CANVAS_ITEM_GRID_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/control/canvas-item-group.cpp b/src/display/control/canvas-item-group.cpp new file mode 100644 index 0000000..93193c3 --- /dev/null +++ b/src/display/control/canvas-item-group.cpp @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * A CanvasItem that contains other CanvasItem's. + */ +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of SPCanvasGroup + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <boost/range/adaptor/reversed.hpp> +#include "canvas-item-group.h" + +constexpr bool DEBUG_LOGGING = false; + +namespace Inkscape { + +CanvasItemGroup::CanvasItemGroup(CanvasItemGroup *group) + : CanvasItem(group) +{ + _name = "CanvasItemGroup"; + _pickable = true; // For now all groups are pickable... look into turning this off for some groups (e.g. temp). +} + +CanvasItemGroup::CanvasItemGroup(CanvasItemContext *context) + : CanvasItem(context) +{ + _name = "CanvasItemGroup:Root"; + _pickable = true; // see above +} + +CanvasItemGroup::~CanvasItemGroup() +{ + items.clear_and_dispose([] (auto c) { delete c; }); +} + +void CanvasItemGroup::_update(bool propagate) +{ + _bounds = {}; + + // Update all children and calculate new bounds. + for (auto &item : items) { + item.update(propagate); + _bounds |= item.get_bounds(); + } +} + +void CanvasItemGroup::_mark_net_invisible() +{ + if (!_net_visible) { + return; + } + _net_visible = false; + _need_update = false; + for (auto &item : items) { + item._mark_net_invisible(); + } + _bounds = {}; +} + +void CanvasItemGroup::visit_page_rects(std::function<void(Geom::Rect const &)> const &f) const +{ + for (auto &item : items) { + if (!item.is_visible()) continue; + item.visit_page_rects(f); + } +} + +void CanvasItemGroup::_render(Inkscape::CanvasItemBuffer &buf) const +{ + for (auto &item : items) { + item.render(buf); + } +} + +// Return last visible and pickable item that contains point. +// SPCanvasGroup returned distance but it was not used. +CanvasItem *CanvasItemGroup::pick_item(Geom::Point const &p) +{ + if constexpr (DEBUG_LOGGING) { + std::cout << "CanvasItemGroup::pick_item:" << std::endl; + std::cout << " PICKING: In group: " << _name << " bounds: " << _bounds << std::endl; + } + + for (auto &item : boost::adaptors::reverse(items)) { + if constexpr (DEBUG_LOGGING) std::cout << " PICKING: Checking: " << item.get_name() << " bounds: " << item.get_bounds() << std::endl; + + if (item.is_visible() && item.is_pickable() && item.contains(p)) { + if (auto group = dynamic_cast<CanvasItemGroup*>(&item)) { + if (auto ret = group->pick_item(p)) { + return ret; + } + } else { + return &item; + } + } + } + + return nullptr; +} + +} // 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/control/canvas-item-group.h b/src/display/control/canvas-item-group.h new file mode 100644 index 0000000..4ab5884 --- /dev/null +++ b/src/display/control/canvas-item-group.h @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_CANVAS_ITEM_GROUP_H +#define SEEN_CANVAS_ITEM_GROUP_H + +/** + * A CanvasItem that contains other CanvasItems. + */ +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of SPCanvasGroup + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "canvas-item.h" + +namespace Inkscape { + +class CanvasItemGroup final : public CanvasItem +{ +public: + CanvasItemGroup(CanvasItemGroup *group); + CanvasItemGroup(CanvasItemContext *context); + + // Geometry + void visit_page_rects(std::function<void(Geom::Rect const &)> const &) const override; + + // Selection + CanvasItem *pick_item(Geom::Point const &p); + +protected: + friend class CanvasItem; // access to items + friend class CanvasItemContext; // access to destructor + + ~CanvasItemGroup() override; + + void _update(bool propagate) override; + void _mark_net_invisible() override; + void _render(Inkscape::CanvasItemBuffer &buf) const override; + + /** + * Type for linked list storing CanvasItems. + * Used to speed deletion when a group contains a large number of items (as in nodes for a complex path). + */ + using CanvasItemList = boost::intrusive::list< + Inkscape::CanvasItem, + boost::intrusive::member_hook<Inkscape::CanvasItem, boost::intrusive::list_member_hook<>, + &Inkscape::CanvasItem::member_hook>>; + + CanvasItemList items; +}; + +} // namespace Inkscape + +#endif // SEEN_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/control/canvas-item-guideline.cpp b/src/display/control/canvas-item-guideline.cpp new file mode 100644 index 0000000..7cefc2b --- /dev/null +++ b/src/display/control/canvas-item-guideline.cpp @@ -0,0 +1,312 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * A class to represent a control guide line. + */ + +/* + * Authors: + * Tavmjong Bah - Rewrite of SPGuideLine + * Rafael Siejakowski - Tweaks to handle appearance + * + * Copyright (C) 2020-2022 the Authors. + * + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/line.h> + +#include "canvas-item-guideline.h" +#include "canvas-item-ctrl.h" + +#include "desktop.h" // Canvas orientation so label is orientated correctly. +#include "ui/widget/canvas.h" + +namespace Inkscape { + +/** + * Create a control guide line. Points are in document units. + */ +CanvasItemGuideLine::CanvasItemGuideLine(CanvasItemGroup *group, Glib::ustring label, + Geom::Point const &origin, Geom::Point const &normal) + : CanvasItem(group) + , _origin(origin) + , _normal(normal) + , _label(std::move(label)) +{ + _name = "CanvasItemGuideLine:" + _label; + _pickable = true; // For now, everybody gets events from this class! + + // Control to move guide line. + _origin_ctrl = make_canvasitem<CanvasItemGuideHandle>(group, _origin, this); + _origin_ctrl->set_name("CanvasItemGuideLine:Ctrl:" + _label); + _origin_ctrl->set_size_default(); + _origin_ctrl->set_pickable(true); // The handle will also react to dragging + set_locked(false); // Init _origin_ctrl shape and stroke. +} + +/** + * Sets origin of guide line (place where handle is located). + */ +void CanvasItemGuideLine::set_origin(Geom::Point const &origin) +{ + if (_origin != origin) { + _origin = origin; + _origin_ctrl->set_position(_origin); + request_update(); + } +} + +/** + * Sets orientation of guide line. + */ +void CanvasItemGuideLine::set_normal(Geom::Point const &normal) +{ + if (_normal != normal) { + _normal = normal; + request_update(); + } +} + +/** + * Sets the inverted nature of the line + */ +void CanvasItemGuideLine::set_inverted(bool inverted) +{ + if (_inverted != inverted) { + _inverted = inverted; + request_update(); + } +} + +/** + * Returns distance between point in canvas units and nearest point on guideLine. + */ +double CanvasItemGuideLine::closest_distance_to(Geom::Point const &p) +{ + // Maybe store guide as a Geom::Line? + auto guide = Geom::Line::from_origin_and_vector(_origin, Geom::rot90(_normal)); + guide *= affine(); + return Geom::distance(p, guide); +} + +/** + * Returns true if point p (in canvas units) is within tolerance (canvas units) distance of guideLine (or 1 if tolerance is zero). + */ +bool CanvasItemGuideLine::contains(Geom::Point const &p, double tolerance) +{ + if (tolerance == 0) { + tolerance = 1; // Can't pick of zero! + } + + return closest_distance_to(p) < tolerance; +} + +/** + * Returns the pointer to the origin control (the "dot") + */ +CanvasItemGuideHandle* CanvasItemGuideLine::dot() const +{ + return _origin_ctrl.get(); +} + +/** + * Update and redraw control guideLine. + */ +void CanvasItemGuideLine::_update(bool) +{ + // Required when rotating canvas + _bounds = Geom::Rect(-Geom::infinity(), -Geom::infinity(), Geom::infinity(), Geom::infinity()); + + // Queue redraw of new area (and old too). + request_redraw(); +} + +/** + * Render guideLine to screen via Cairo. + */ +void CanvasItemGuideLine::_render(Inkscape::CanvasItemBuffer &buf) const +{ + // Document to canvas + Geom::Point const normal = _normal * affine().withoutTranslation(); // Direction only + Geom::Point const origin = _origin * affine(); + + /* 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. + * Lastly, the origin control is also pixel-aligned and we want to visually cut through its + * exact center. + */ + Geom::Point const aligned_origin = origin.floor() + Geom::Point(0.5, 0.5); + + // Set up the Cairo rendering context + auto ctx = buf.cr; + ctx->save(); + ctx->translate(-buf.rect.left(), -buf.rect.top()); // Canvas to screen + ctx->set_source_rgba(SP_RGBA32_R_F(_stroke), SP_RGBA32_G_F(_stroke), + SP_RGBA32_B_F(_stroke), SP_RGBA32_A_F(_stroke)); + ctx->set_line_width(1); + + if (_inverted) { + // operator not available in cairo C++ bindings + cairo_set_operator(ctx->cobj(), CAIRO_OPERATOR_DIFFERENCE); + } + + if (!_label.empty()) { // Render text label + ctx->save(); + ctx->translate(aligned_origin.x(), aligned_origin.y()); + + auto desktop = get_canvas()->get_desktop(); + ctx->rotate(atan2(normal.cw()) + M_PI * (desktop && desktop->is_yaxisdown() ? 1 : 0)); + ctx->translate(0, -(_origin_ctrl->radius() + LABEL_SEP)); // Offset by dot radius + 2 + ctx->move_to(0, 0); + ctx->show_text(_label); + ctx->restore(); + } + + // Draw guide. + // Special case: horizontal and vertical lines (easier calculations) + + // Don't use isHorizontal()/isVertical() as they test only exact matches. + if (Geom::are_near(normal.y(), 0.0)) { + // Vertical + double const position = aligned_origin.x(); + ctx->move_to(position, buf.rect.top() + 0.5); + ctx->line_to(position, buf.rect.bottom() - 0.5); + } else if (Geom::are_near(normal.x(), 0.0)) { + // Horizontal + double position = aligned_origin.y(); + ctx->move_to(buf.rect.left() + 0.5, position); + ctx->line_to(buf.rect.right() - 0.5, position); + } else { + // Angled + Geom::Line line = Geom::Line::from_origin_and_vector(aligned_origin, Geom::rot90(normal)); + + // Find intersections of the line 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(line, side); + if (oc) { + intersections.push_back(line.pointAt(oc->ta)); + } + } catch (Geom::InfiniteSolutions const &) { + // Shouldn't happen as we have already taken care of horizontal/vertical guides. + std::cerr << "CanvasItemGuideLine::render: Error: Infinite intersections." << std::endl; + } + } + + if (intersections.size() == 2) { + double const x0 = intersections[0].x(); + double const x1 = intersections[1].x(); + double const y0 = intersections[0].y(); + double const y1 = intersections[1].y(); + ctx->move_to(x0, y0); + ctx->line_to(x1, y1); + } + } + ctx->stroke(); + + ctx->restore(); +} + +void CanvasItemGuideLine::set_visible(bool visible) +{ + CanvasItem::set_visible(visible); + _origin_ctrl->set_visible(visible); +} + +void CanvasItemGuideLine::set_stroke(uint32_t color) +{ + // Make sure the fill of the control is the same as the stroke + // of the guide-line: + _origin_ctrl->set_fill(color); + CanvasItem::set_stroke(color); +} + +void CanvasItemGuideLine::set_label(Glib::ustring &&label) +{ + defer([=, label = std::move(label)] () mutable { + if (_label == label) return; + _label = std::move(label); + request_update(); + }); +} + +void CanvasItemGuideLine::set_locked(bool locked) +{ + defer([=] { + if (_locked == locked) return; + _locked = locked; + if (_locked) { + _origin_ctrl->set_shape(CANVAS_ITEM_CTRL_SHAPE_CROSS); + _origin_ctrl->set_stroke(CONTROL_LOCKED_COLOR); + _origin_ctrl->set_fill(0x00000000); // no fill + } else { + _origin_ctrl->set_shape(CANVAS_ITEM_CTRL_SHAPE_CIRCLE); + _origin_ctrl->set_stroke(0x00000000); // no stroke + _origin_ctrl->set_fill(_stroke); // fill the control with this guide's color + } + }); +} + +//=============================================================================================== + +/** + * @brief Create a handle ("dot") along a guide line + * @param group - the associated canvas item group + * @param pos - position + * @param line - pointer to the corresponding guide line + */ +CanvasItemGuideHandle::CanvasItemGuideHandle(CanvasItemGroup *group, + Geom::Point const &pos, + CanvasItemGuideLine* line) + : CanvasItemCtrl(group, CANVAS_ITEM_CTRL_SHAPE_CIRCLE, pos) + , _my_line(line) // Save a pointer to our guide line +{ +} + +/** + * Return the radius of the handle dot + */ +double CanvasItemGuideHandle::radius() const +{ + return 0.5 * static_cast<double>(_width); // radius is half the width +} + +/** + * Update the size of the handle based on the index from Preferences + */ +void CanvasItemGuideHandle::set_size_via_index(int index) +{ + double const r = static_cast<double>(index) * SCALE; + unsigned long const rounded_diameter = std::lround(r * 2.0); // diameter is twice the radius + unsigned long size = rounded_diameter | 0x1; // make sure the size is always odd + if (size < MINIMUM_SIZE) { + size = MINIMUM_SIZE; + } + defer([=] { + if (_width == size) return; + _width = size; + _height = size; + _built.reset(); + request_update(); + _my_line->request_update(); + }); +} + +} // 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/control/canvas-item-guideline.h b/src/display/control/canvas-item-guideline.h new file mode 100644 index 0000000..424fb27 --- /dev/null +++ b/src/display/control/canvas-item-guideline.h @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_CANVAS_ITEM_GUIDELINE_H +#define SEEN_CANVAS_ITEM_GUIDELINE_H + +/** + * A class to represent a control guide line. + */ + +/* + * Authors: + * Tavmjong Bah - Rewrite of SPGuideLine + * Rafael Siejakowski - Tweaks to handle appearance + * + * Copyright (C) 2020-2022 the Authors. + * + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/ustring.h> + +#include <2geom/point.h> +#include <2geom/transforms.h> + +#include "canvas-item.h" +#include "canvas-item-ctrl.h" +#include "canvas-item-ptr.h" + +namespace Inkscape { + +class CanvasItemGuideHandle; + +class CanvasItemGuideLine final : public CanvasItem +{ +public: + CanvasItemGuideLine(CanvasItemGroup *group, Glib::ustring label, Geom::Point const &origin, Geom::Point const &normal); + + // Geometry + void set_origin(Geom::Point const &origin); + void set_normal(Geom::Point const &normal); + double closest_distance_to(Geom::Point const &p); + + // Selection + bool contains(Geom::Point const &p, double tolerance = 0) override; + + // Properties + void set_visible(bool visible) override; + void set_stroke(uint32_t color) override; + void set_label(Glib::ustring &&label); + void set_locked(bool locked); + void set_inverted(bool inverted); + + // Getters + CanvasItemGuideHandle *dot() const; + +protected: + ~CanvasItemGuideLine() override = default; + + void _update(bool propagate) override; + void _render(Inkscape::CanvasItemBuffer &buf) const override; + + Geom::Point _origin; + Geom::Point _normal = Geom::Point(0, 1); + Glib::ustring _label; + bool _locked = true; // Flipped in constructor to trigger init of _origin_ctrl. + bool _inverted = false; + CanvasItemPtr<CanvasItemGuideHandle> _origin_ctrl; + + static constexpr uint32_t CONTROL_LOCKED_COLOR = 0x00000080; // RGBA black semitranslucent + static constexpr double LABEL_SEP = 2.0; // Distance between the label and the origin control +}; + +// A handle ("dot") serving as draggable origin control +class CanvasItemGuideHandle final : public CanvasItemCtrl +{ +public: + CanvasItemGuideHandle(CanvasItemGroup *group, Geom::Point const &pos, CanvasItemGuideLine *line); + double radius() const; + void set_size_via_index(int index) override; + +protected: + ~CanvasItemGuideHandle() override = default; + + CanvasItemGuideLine *_my_line; // The guide line we belong to + + // static data + static constexpr double SCALE = 0.55; // handle size relative to an auto-smooth node + static constexpr unsigned MINIMUM_SIZE = 7; // smallest handle size, must be an odd int +}; + +} // namespace Inkscape + +#endif // SEEN_CANVAS_ITEM_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/control/canvas-item-ptr.h b/src/display/control/canvas-item-ptr.h new file mode 100644 index 0000000..b8c198c --- /dev/null +++ b/src/display/control/canvas-item-ptr.h @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_CANVAS_ITEM_PTR_H +#define SEEN_CANVAS_ITEM_PTR_H + +/* + * An entirely analogous file to display/drawing-item-ptr.h. + */ + +#include <memory> +#include <type_traits> + +namespace Inkscape { class CanvasItem; } + +/// Deleter object which calls the unlink() method of CanvasItem. +struct CanvasItemUnlinkDeleter +{ + template <typename T> + void operator()(T *t) + { + static_assert(std::is_base_of_v<Inkscape::CanvasItem, T>); + t->unlink(); + } +}; + +/// Smart pointer used to hold CanvasItems, like std::unique_ptr. +template <typename T> +using CanvasItemPtr = std::unique_ptr<T, CanvasItemUnlinkDeleter>; + +/// Convienence function to create a CanvasItemPtr, like std::make_unique. +template <typename T, typename... Args> +auto make_canvasitem(Args&&... args) +{ + return CanvasItemPtr<T>(new T(std::forward<Args>(args)...)); +}; + +#endif // SEEN_CANVAS_ITEM_PTR_H diff --git a/src/display/control/canvas-item-quad.cpp b/src/display/control/canvas-item-quad.cpp new file mode 100644 index 0000000..06914de --- /dev/null +++ b/src/display/control/canvas-item-quad.cpp @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * A class to represent a control quadrilateral. Used to highlight selected text. + */ + +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of SPCtrlQuadr + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cassert> + +#include "canvas-item-quad.h" + +#include "color.h" // SP_RGBA_x_F +#include "helper/geom.h" + +namespace Inkscape { + +/** + * Create an null control quad. + */ +CanvasItemQuad::CanvasItemQuad(CanvasItemGroup *group) + : CanvasItem(group) +{ + _name = "CanvasItemQuad:Null"; +} + +/** + * Create a control quad. Points are in document coordinates. + */ +CanvasItemQuad::CanvasItemQuad(CanvasItemGroup *group, + Geom::Point const &p0, Geom::Point const &p1, + Geom::Point const &p2, Geom::Point const &p3) + : CanvasItem(group) + , _p0(p0) + , _p1(p1) + , _p2(p2) + , _p3(p3) +{ + _name = "CanvasItemQuad"; +} + +/** + * Set a control quad. Points are in document coordinates. + */ +void CanvasItemQuad::set_coords(Geom::Point const &p0, Geom::Point const &p1, Geom::Point const &p2, Geom::Point const &p3) +{ + defer([=] { + _p0 = p0; + _p1 = p1; + _p2 = p2; + _p3 = p3; + request_update(); + }); +} + +/** + * Returns true if point p (in canvas units) is within tolerance (canvas units) distance of quad. + */ +bool CanvasItemQuad::contains(Geom::Point const &p, double tolerance) +{ + if (tolerance != 0) { + std::cerr << "CanvasItemQuad::contains: Non-zero tolerance not implemented!" << std::endl; + } + + Geom::Point p0 = _p0 * affine(); + Geom::Point p1 = _p1 * affine(); + Geom::Point p2 = _p2 * affine(); + Geom::Point p3 = _p3 * affine(); + + // From 2geom rotated-rect.cpp + return + Geom::cross(p1 - p0, p - p0) >= 0 && + Geom::cross(p2 - p1, p - p1) >= 0 && + Geom::cross(p3 - p2, p - p2) >= 0 && + Geom::cross(p0 - p3, p - p3) >= 0; +} + +/** + * Update and redraw control quad. + */ +void CanvasItemQuad::_update(bool) +{ + if (_p0 == _p1 || _p1 == _p2 || _p2 == _p3 || _p3 == _p0) { + _bounds = {}; + return; // Not quad or not initialized. + } + + // Queue redraw of old area (erase previous content). + request_redraw(); // This is actually never useful as quads are always deleted + // and recreated when a node is moved! But keep it in case we change that. + + _bounds = expandedBy(bounds_of(_p0, _p1, _p2, _p3) * affine(), 2); // Room for anti-aliasing effects. + + // Queue redraw of new area + request_redraw(); +} + +/** + * Render quad to screen via Cairo. + */ +void CanvasItemQuad::_render(Inkscape::CanvasItemBuffer &buf) const +{ + // Document to canvas + Geom::Point p0 = _p0 * affine(); + Geom::Point p1 = _p1 * affine(); + Geom::Point p2 = _p2 * affine(); + Geom::Point p3 = _p3 * affine(); + + // Canvas to screen + p0 *= Geom::Translate(-buf.rect.min()); + p1 *= Geom::Translate(-buf.rect.min()); + p2 *= Geom::Translate(-buf.rect.min()); + p3 *= Geom::Translate(-buf.rect.min()); + + buf.cr->save(); + + buf.cr->begin_new_path(); + + buf.cr->move_to(p0.x(), p0.y()); + buf.cr->line_to(p1.x(), p1.y()); + buf.cr->line_to(p2.x(), p2.y()); + buf.cr->line_to(p3.x(), p3.y()); + buf.cr->close_path(); + + if (_inverted) { + cairo_set_operator(buf.cr->cobj(), CAIRO_OPERATOR_DIFFERENCE); + } + + buf.cr->set_source_rgba(SP_RGBA32_R_F(_fill), SP_RGBA32_G_F(_fill), + SP_RGBA32_B_F(_fill), SP_RGBA32_A_F(_fill)); + buf.cr->fill_preserve(); + + buf.cr->set_line_width(1); + buf.cr->set_source_rgba(SP_RGBA32_R_F(_stroke), SP_RGBA32_G_F(_stroke), + SP_RGBA32_B_F(_stroke), SP_RGBA32_A_F(_stroke)); + buf.cr->stroke_preserve(); + buf.cr->begin_new_path(); + + buf.cr->restore(); +} + +void CanvasItemQuad::set_inverted(bool inverted) +{ + defer([=] { + if (_inverted == inverted) return; + _inverted = inverted; + request_redraw(); + }); +} + +} // 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/control/canvas-item-quad.h b/src/display/control/canvas-item-quad.h new file mode 100644 index 0000000..deb8e46 --- /dev/null +++ b/src/display/control/canvas-item-quad.h @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_CANVAS_ITEM_QUAD_H +#define SEEN_CANVAS_ITEM_QUAD_H + +/** + * A class to represent a control quadrilateral. Used to highlight selected text. + */ + +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of SPCtrlQuadr + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/point.h> +#include <2geom/transforms.h> + +#include "canvas-item.h" + +namespace Inkscape { + +class CanvasItemQuad final : public CanvasItem +{ +public: + CanvasItemQuad(CanvasItemGroup *group); + CanvasItemQuad(CanvasItemGroup *group, Geom::Point const &p0, Geom::Point const &p1, + Geom::Point const &p2, Geom::Point const &p3); + + // Geometry + void set_coords(Geom::Point const &p0, Geom::Point const &p1, Geom::Point const &p2, Geom::Point const &p3); + + // Selection + bool contains(Geom::Point const &p, double tolerance = 0) override; + + void set_inverted(bool inverted); + +protected: + ~CanvasItemQuad() override = default; + + void _update(bool propagate) override; + void _render(Inkscape::CanvasItemBuffer &buf) const override; + + Geom::Point _p0; + Geom::Point _p1; + Geom::Point _p2; + Geom::Point _p3; + + bool _inverted = false; +}; + +} // namespace Inkscape + +#endif // SEEN_CANVAS_ITEM_QUAD_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/control/canvas-item-rect.cpp b/src/display/control/canvas-item-rect.cpp new file mode 100644 index 0000000..0204cdf --- /dev/null +++ b/src/display/control/canvas-item-rect.cpp @@ -0,0 +1,262 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * A class to represent a control rectangle. Used for rubberband selector, page outline, etc. + */ + +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of CtrlRect + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cairo/cairo.h> + +#include "canvas-item-rect.h" + +#include "display/cairo-utils.h" +#include "color.h" // SP_RGBA_x_F +#include "helper/geom.h" +#include "inkscape.h" +#include "ui/util.h" +#include "ui/widget/canvas.h" + +namespace Inkscape { + +/** + * Create an null control rect. + */ +CanvasItemRect::CanvasItemRect(CanvasItemGroup *group) + : CanvasItem(group) +{ + _name = "CanvasItemRect:Null"; +} + +/** + * Create a control rect. Point are in document coordinates. + */ +CanvasItemRect::CanvasItemRect(CanvasItemGroup *group, Geom::Rect const &rect) + : CanvasItem(group) + , _rect(rect) +{ + _name = "CanvasItemRect"; +} + +/** + * Set a control rect. Points are in document coordinates. + */ +void CanvasItemRect::set_rect(Geom::Rect const &rect) +{ + defer([=] { + if (_rect == rect) return; + _rect = rect; + request_update(); + }); +} + +/** + * Run a callback for each rectangle that should be filled and painted in the background. + */ +void CanvasItemRect::visit_page_rects(std::function<void(Geom::Rect const &)> const &f) const +{ + if (_is_page && _fill != 0) { + f(_rect); + } +} + +/** + * Returns true if point p (in canvas units) is within tolerance (canvas units) distance of rect. + * Non-zero tolerance not implemented! Is valid for a rotated canvas. + */ +bool CanvasItemRect::contains(Geom::Point const &p, double tolerance) +{ + if (tolerance != 0) { + std::cerr << "CanvasItemRect::contains: Non-zero tolerance not implemented!" << std::endl; + } + + return _rect.contains(p * affine().inverse()); +} + +/** + * Update and redraw control rect. + */ +void CanvasItemRect::_update(bool) +{ + // Queue redraw of old area (erase previous content). + request_redraw(); + + // Enlarge bbox by twice shadow size (to allow for shadow on any side with a 45deg rotation). + _bounds = _rect; + // note: add shadow size before applying transformation, since get_shadow_size accounts for scale + if (_shadow_width > 0 && !_dashed) { + _bounds->expandBy(2 * get_shadow_size()); + } + *_bounds *= affine(); + _bounds->expandBy(2); // Room for stroke. + + // Queue redraw of new area + request_redraw(); +} + +/** + * Render rect to screen via Cairo. + */ +void CanvasItemRect::_render(Inkscape::CanvasItemBuffer &buf) const +{ + // Are we axis aligned? + auto const &aff = affine(); + bool const axis_aligned = (Geom::are_near(aff[1], 0) && Geom::are_near(aff[2], 0)) + || (Geom::are_near(aff[0], 0) && Geom::are_near(aff[3], 0)); + + // If so, then snap the rectangle to the pixel grid. + auto rect = _rect; + if (axis_aligned) { + rect = (floor(_rect * aff) + Geom::Point(0.5, 0.5)) * aff.inverse(); + } + + buf.cr->save(); + buf.cr->translate(-buf.rect.left(), -buf.rect.top()); + + if (_inverted) { + cairo_set_operator(buf.cr->cobj(), CAIRO_OPERATOR_DIFFERENCE); + } + + // Draw shadow first. Shadow extends under rectangle to reduce aliasing effects. Canvas draws page shadows in OpenGL mode. + if (_shadow_width > 0 && !_dashed && !(_is_page && get_canvas()->get_opengl_enabled())) { + // There's only one UI knob to adjust border and shadow color, so instead of using border color + // transparency as is, it is boosted by this function, since shadow attenuates it. + auto const alpha = (std::exp(-3 * SP_RGBA32_A_F(_shadow_color)) - 1) / (std::exp(-3) - 1); + + // Flip shadow upside-down if y-axis is inverted. + auto doc2dt = Geom::identity(); + if (auto desktop = get_canvas()->get_desktop()) { + doc2dt = desktop->doc2dt(); + } + + buf.cr->save(); + buf.cr->transform(geom_to_cairo(doc2dt * aff)); + ink_cairo_draw_drop_shadow(buf.cr, rect * doc2dt, get_shadow_size(), _shadow_color, alpha); + buf.cr->restore(); + } + + // Get the points we need transformed into window coordinates. + buf.cr->begin_new_path(); + for (int i = 0; i < 4; ++i) { + auto pt = rect.corner(i) * aff; + buf.cr->line_to(pt.x(), pt.y()); + } + buf.cr->close_path(); + + // Draw border. + static std::valarray<double> dashes = {4.0, 4.0}; + if (_dashed) { + buf.cr->set_dash(dashes, -0.5); + } + buf.cr->set_line_width(1); + // we maybe have painted the background, back to "normal" compositing + buf.cr->set_source_rgba(SP_RGBA32_R_F(_stroke), SP_RGBA32_G_F(_stroke), + SP_RGBA32_B_F(_stroke), SP_RGBA32_A_F(_stroke)); + buf.cr->stroke_preserve(); + + // Highlight the border by drawing it in _shadow_color. + if (_shadow_width == 1 && _dashed) { + buf.cr->set_dash(dashes, 3.5); // Dash offset by dash length. + buf.cr->set_source_rgba(SP_RGBA32_R_F(_shadow_color), SP_RGBA32_G_F(_shadow_color), + SP_RGBA32_B_F(_shadow_color), SP_RGBA32_A_F(_shadow_color)); + buf.cr->stroke_preserve(); + } + + buf.cr->begin_new_path(); // Clear path or get weird artifacts. + + // Uncomment to show bounds + // Geom::Rect bounds = _bounds; + // bounds.expandBy(-1); + // bounds -= buf.rect.min(); + // buf.cr->set_source_rgba(1.0, 0.0, _shadow_width / 3.0, 1.0); + // buf.cr->rectangle(bounds.min().x(), bounds.min().y(), bounds.width(), bounds.height()); + // buf.cr->stroke(); + + buf.cr->restore(); +} + +void CanvasItemRect::set_is_page(bool is_page) +{ + defer([=] { + if (_is_page == is_page) return; + _is_page = is_page; + request_redraw(); + }); +} + +void CanvasItemRect::set_fill(uint32_t fill) +{ + if (fill != _fill && _is_page) get_canvas()->set_page(fill); + CanvasItem::set_fill(fill); +} + +void CanvasItemRect::set_dashed(bool dashed) +{ + defer([=] { + if (_dashed == dashed) return; + _dashed = dashed; + request_redraw(); + }); +} + +void CanvasItemRect::set_inverted(bool inverted) +{ + defer([=] { + if (_inverted == inverted) return; + _inverted = inverted; + request_redraw(); + }); +} + +void CanvasItemRect::set_shadow(uint32_t color, int width) +{ + defer([=] { + if (_shadow_color == color && _shadow_width == width) return; + _shadow_color = color; + _shadow_width = width; + request_redraw(); + if (_is_page) get_canvas()->set_border(_shadow_width > 0 ? color : 0x0); + }); +} + +double CanvasItemRect::get_shadow_size() const +{ + // gradient drop shadow needs much more room than solid one, so inflating the size; + // fudge factor of 6 used to make sizes baked in svg documents work as steps: + // typical value of 2 will work out to 12 pixels which is a narrow shadow (b/c of exponential fall of) + auto size = _shadow_width * 6; + if (size < 0) { + size = 0; + } else if (size > 120) { + // arbitrarily selected max size, so Cairo gradient doesn't blow up if document has bogus shadow values + size = 120; + } + auto scale = affine().descrim(); + + // calculate space for gradient shadow; if divided by 'scale' it would be zoom independent (fixed in size); + // if 'scale' is not used, drop shadow will be getting smaller with document zoom; + // here hybrid approach is used: "unscaling" with square root of scale allows shadows to diminish + // more slowly at small zoom levels (so it's still perceptible) and grow more slowly at high mag (where it doesn't matter, b/c it's typically off-screen) + return size / (scale > 0 ? sqrt(scale) : 1); +} + +} // 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/control/canvas-item-rect.h b/src/display/control/canvas-item-rect.h new file mode 100644 index 0000000..4f67734 --- /dev/null +++ b/src/display/control/canvas-item-rect.h @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_CANVAS_ITEM_RECT_H +#define SEEN_CANVAS_ITEM_RECT_H + +/** + * A class to represent a control rectangle. Used for rubberband selector, page outline, etc. + */ + +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of CtrlRect + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <memory> +#include <2geom/path.h> + +#include "canvas-item.h" + +namespace Inkscape { + +class CanvasItemRect final : public CanvasItem +{ +public: + CanvasItemRect(CanvasItemGroup *group); + CanvasItemRect(CanvasItemGroup *group, Geom::Rect const &rect); + + // Geometry + void set_rect(Geom::Rect const &rect); + void visit_page_rects(std::function<void(Geom::Rect const &)> const &) const override; + + // Selection + bool contains(Geom::Point const &p, double tolerance = 0) override; + + // Properties + void set_is_page(bool is_page); + void set_fill(uint32_t color) override; + void set_dashed(bool dash = true); + void set_inverted(bool inverted = false); + void set_shadow(uint32_t color, int width); + +protected: + ~CanvasItemRect() override = default; + + void _update(bool propagate) override; + void _render(Inkscape::CanvasItemBuffer &buf) const override; + + // Geometry + double get_shadow_size() const; + + Geom::Rect _rect; + bool _is_page = false; + bool _dashed = false; + bool _inverted = false; + int _shadow_width = 0; + uint32_t _shadow_color = 0x0; +}; + +} // namespace Inkscape + +#endif // SEEN_CANVAS_ITEM_RECT_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/control/canvas-item-text.cpp b/src/display/control/canvas-item-text.cpp new file mode 100644 index 0000000..5049543 --- /dev/null +++ b/src/display/control/canvas-item-text.cpp @@ -0,0 +1,303 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * A class to represent a control textrilateral. Used to highlight selected text. + */ + +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of SPCtrlTextr + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "canvas-item-text.h" + +#include <cmath> +#include <utility> // std::move +#include <glibmm/i18n.h> + +#include "color.h" // SP_RGBA_x_F + +#include "ui/util.h" + +namespace Inkscape { + +/** + * Create a null control text. + */ +CanvasItemText::CanvasItemText(CanvasItemGroup *group) + : CanvasItem(group) +{ + _name = "CanvasItemText"; + _fill = 0x33337fff; // Override CanvasItem default. +} + +/** + * Create a control text. Point are in document coordinates. + */ +CanvasItemText::CanvasItemText(CanvasItemGroup *group, Geom::Point const &p, Glib::ustring text, bool scaled) + : CanvasItem(group) + , _p(p) + , _text(std::move(text)) + , _scaled(scaled) +{ + _name = "CanvasItemText"; + _fill = 0x33337fff; // Override CanvasItem default. + + request_update(); +} + +/** + * Set a text position. Position is in document coordinates. + */ +void CanvasItemText::set_coord(Geom::Point const &p) +{ + defer([=] { + if (_p == p) return; + _p = p; + request_update(); + }); +} + +/** + * Set a text position. Position is in document coordinates. + */ +void CanvasItemText::set_bg_radius(double rad) +{ + defer([=] { + if (_bg_rad == rad) return; + _bg_rad = rad; + request_update(); + }); +} + +/** + * Returns true if point p (in canvas units) is within tolerance (canvas units) distance of text. + */ +bool CanvasItemText::contains(Geom::Point const &p, double tolerance) +{ + return false; // We never select text. +} + +/** + * Update and redraw control text. + */ +void CanvasItemText::_update(bool) +{ + // Queue redraw of old area (erase previous content). + request_redraw(); + + // Point needs to be scaled manually if not cairo scaling + Geom::Point p = _scaled ? _p : _p * affine(); + + // Measure text size + _text_box = load_text_extents(); + + // Offset relative to requested point + double offset_x = -(_anchor_position.x() * _text_box.width()); + double offset_y = -(_anchor_position.y() * _text_box.height()); + offset_x += p.x() + _adjust_offset.x(); + offset_y += p.y() + _adjust_offset.y(); + _text_box *= Geom::Translate(Geom::Point(offset_x, offset_y).floor()); + + // Pixel alignment of background. Avoid aliasing artifacts on redraw. + _text_box = _text_box.roundOutwards(); + + // Don't apply affine here, to keep text at the same size in screen coords. + _bounds = _text_box; + if (_scaled && _bounds) { + *_bounds *= affine(); + _bounds = _bounds->roundOutwards(); + } + + // Queue redraw of new area + request_redraw(); +} + +/** + * Render text to screen via Cairo. + */ +void CanvasItemText::_render(Inkscape::CanvasItemBuffer &buf) const +{ + buf.cr->save(); + + // Screen to desktop coords. + buf.cr->translate(-buf.rect.left(), -buf.rect.top()); + + if (_scaled) { + // Convert from canvas space to document space + buf.cr->transform(geom_to_cairo(affine())); + } + + double x = _text_box.min().x(); + double y = _text_box.min().y(); + double w = _text_box.width(); + double h = _text_box.height(); + + // Background + if (_use_background) { + if (_bg_rad == 0.0) { + buf.cr->rectangle(x, y, w, h); + } else { + double radius = _bg_rad * (std::min(w ,h) / 2); + buf.cr->arc(x + w - radius, y + radius, radius, -M_PI_2, 0); + buf.cr->arc(x + w - radius, y + h - radius, radius, 0, M_PI_2); + buf.cr->arc(x + radius, y + h - radius, radius, M_PI_2, M_PI); + buf.cr->arc(x + radius, y + radius, radius, M_PI, 3*M_PI_2); + } + buf.cr->set_line_width(2); + buf.cr->set_source_rgba(SP_RGBA32_R_F(_background), SP_RGBA32_G_F(_background), + SP_RGBA32_B_F(_background), SP_RGBA32_A_F(_background)); + buf.cr->fill(); + } + + // Center the text inside the draw background box + auto bx = x + w / 2.0; + auto by = y + h / 2.0 + 1; + buf.cr->move_to(int(bx - _text_size.x_bearing - _text_size.width/2.0), + int(by - _text_size.y_bearing - _text_extent.height/2.0)); + + buf.cr->select_font_face(_fontname, Cairo::FONT_SLANT_NORMAL, Cairo::FONT_WEIGHT_NORMAL); + buf.cr->set_font_size(_fontsize); + buf.cr->text_path(_text); + buf.cr->set_source_rgba(SP_RGBA32_R_F(_fill), SP_RGBA32_G_F(_fill), + SP_RGBA32_B_F(_fill), SP_RGBA32_A_F(_fill)); + buf.cr->fill(); + buf.cr->restore(); +} + +void CanvasItemText::set_text(Glib::ustring text) +{ + defer([=, text = std::move(text)] () mutable { + if (_text == text) return; + _text = std::move(text); + request_update(); // Might be larger than before! + }); +} + +void CanvasItemText::set_fontsize(double fontsize) +{ + defer([=] { + if (_fontsize == fontsize) return; + _fontsize = fontsize; + request_update(); // Might be larger than before! + }); +} + +/** + * Load the sizes of the text extent using the given font. + */ +Geom::Rect CanvasItemText::load_text_extents() +{ + auto surface = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, 1, 1); + auto context = Cairo::Context::create(surface); + context->select_font_face(_fontname, Cairo::FONT_SLANT_NORMAL, Cairo::FONT_WEIGHT_NORMAL); + context->set_font_size(_fontsize); + context->get_text_extents(_text, _text_size); + + if (_fixed_line) { + // TRANSLATORS: This is a set of letters to test for font ascender and descenders. + context->get_text_extents(_("lg1p$"), _text_extent); + } else { + _text_extent = _text_size; + } + + return Geom::Rect::from_xywh(0, 0, + _text_size.x_advance + _border * 2, + _text_extent.height + _border * 2); +} + +void CanvasItemText::set_background(uint32_t background) +{ + defer([=] { + if (_background != background) { + _background = background; + request_redraw(); + } + _use_background = true; + }); +} + +/** + * Set the anchor point, x and y between 0.0 and 1.0. + */ +void CanvasItemText::set_anchor(Geom::Point const &anchor_pt) +{ + defer([=] { + if (_anchor_position == anchor_pt) return; + _anchor_position = anchor_pt; + request_update(); + }); +} + +void CanvasItemText::set_adjust(Geom::Point const &adjust_pt) +{ + defer([=] { + if (_adjust_offset == adjust_pt) return; + _adjust_offset = adjust_pt; + request_update(); + }); +} + +void CanvasItemText::set_fixed_line(bool fixed_line) +{ + defer([=] { + if (_fixed_line == fixed_line) return; + _fixed_line = fixed_line; + request_update(); + }); +} + +void CanvasItemText::set_border(double border) +{ + defer([=] { + if (_border == border) return; + _border = border; + request_update(); + }); +} + +} // namespace Inkscape + +/* 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. +*/ + +/* + 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/control/canvas-item-text.h b/src/display/control/canvas-item-text.h new file mode 100644 index 0000000..a3ad4f4 --- /dev/null +++ b/src/display/control/canvas-item-text.h @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_CANVAS_ITEM_TEXT_H +#define SEEN_CANVAS_ITEM_TEXT_H + +/** + * A class to represent on-screen text. + */ + +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of SPCanvasText. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/point.h> +#include <2geom/transforms.h> + +#include <glibmm/ustring.h> + +#include "canvas-item.h" + +namespace Inkscape { + +class CanvasItemText final : public CanvasItem +{ +public: + CanvasItemText(CanvasItemGroup *group); + CanvasItemText(CanvasItemGroup *group, Geom::Point const &p, Glib::ustring text, bool scaled = false); + + // Geometry + void set_coord(Geom::Point const &p); + void set_bg_radius(double rad); + + // Selection + bool contains(Geom::Point const &p, double tolerance = 0) override; + + // Properties + void set_text(Glib::ustring text); + void set_fontsize(double fontsize); + void set_border(double border); + void set_background(uint32_t background); + void set_anchor(Geom::Point const &anchor_pt); + void set_adjust(Geom::Point const &adjust_pt); + void set_fixed_line(bool fixed_line); + +protected: + ~CanvasItemText() override = default; + + void _update(bool propagate) override; + void _render(Inkscape::CanvasItemBuffer &buf) const override; + + Geom::Point _p; // Position of text (not box around text). + Cairo::TextExtents _text_extent; + Cairo::TextExtents _text_size; + Geom::Point _anchor_position; + Geom::Point _adjust_offset; + Geom::Rect _text_box; + Glib::ustring _text; + std::string _fontname = "sans-serif"; + double _fontsize = 10; + double _border = 3; + double _bg_rad = 0; + uint32_t _background = 0x0000007f; + bool _use_background = false; + bool _fixed_line = false; // Correction for font heights + bool _scaled = false; + + Geom::Rect load_text_extents(); +}; + +} // namespace Inkscape + +#endif // SEEN_CANVAS_ITEM_TEXT_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/control/canvas-item.cpp b/src/display/control/canvas-item.cpp new file mode 100644 index 0000000..984d24d --- /dev/null +++ b/src/display/control/canvas-item.cpp @@ -0,0 +1,334 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * Abstract base class for on-canvas control items. + */ + +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of SPCanvasItem + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "canvas-item.h" +#include "canvas-item-group.h" +#include "canvas-item-ctrl.h" + +#include "ui/widget/canvas.h" + +constexpr bool DEBUG_LOGGING = false; +constexpr bool DEBUG_BOUNDS = false; + +namespace Inkscape { + +CanvasItem::CanvasItem(CanvasItemContext *context) + : _context(context) + , _parent(nullptr) +{ + if constexpr (DEBUG_LOGGING) std::cout << "CanvasItem: create root " << get_name() << std::endl; + request_update(); +} + +CanvasItem::CanvasItem(CanvasItemGroup *parent) + : _context(parent->_context) + , _parent(parent) +{ + if constexpr (DEBUG_LOGGING) std::cout << "CanvasItem: add " << get_name() << " to " << parent->get_name() << " " << parent->items.size() << std::endl; + defer([=] { + parent->items.push_back(*this); + request_update(); + }); +} + +void CanvasItem::unlink() +{ + defer([=] { + // Clear canvas of item. + request_redraw(); + + // Remove from parent. + if (_parent) { + if constexpr (DEBUG_LOGGING) std::cout << "CanvasItem: remove " << get_name() << " from " << _parent->get_name() << " " << _parent->items.size() << std::endl; + auto it = _parent->items.iterator_to(*this); + assert(it != _parent->items.end()); + _parent->items.erase(it); + _parent->request_update(); + } else { + if constexpr (DEBUG_LOGGING) std::cout << "CanvasItem: destroy root " << get_name() << std::endl; + } + + delete this; + }); +} + +CanvasItem::~CanvasItem() +{ + // Clear any pointers to this object in canvas. + get_canvas()->canvas_item_destructed(this); +} + +bool CanvasItem::is_descendant_of(CanvasItem const *ancestor) const +{ + auto item = this; + while (item) { + if (item == ancestor) { + return true; + } + item = item->_parent; + } + return false; +} + +void CanvasItem::set_z_position(int zpos) +{ + if (!_parent) { + std::cerr << "CanvasItem::set_z_position: No parent!" << std::endl; + return; + } + + defer([=] { + _parent->items.erase(_parent->items.iterator_to(*this)); + + if (zpos <= 0) { + _parent->items.push_front(*this); + } else if (zpos >= _parent->items.size() - 1) { + _parent->items.push_back(*this); + } else { + auto it = _parent->items.begin(); + std::advance(it, zpos); + _parent->items.insert(it, *this); + } + }); +} + +void CanvasItem::raise_to_top() +{ + if (!_parent) { + std::cerr << "CanvasItem::raise_to_top: No parent!" << std::endl; + return; + } + + defer([=] { + _parent->items.erase(_parent->items.iterator_to(*this)); + _parent->items.push_back(*this); + }); +} + +void CanvasItem::lower_to_bottom() +{ + if (!_parent) { + std::cerr << "CanvasItem::lower_to_bottom: No parent!" << std::endl; + return; + } + + defer([=] { + _parent->items.erase(_parent->items.iterator_to(*this)); + _parent->items.push_front(*this); + }); +} + +// Indicate geometry changed and bounds needs recalculating. +void CanvasItem::request_update() +{ + if (_need_update || !_visible) { + return; + } + + _need_update = true; + + if (_parent) { + _parent->request_update(); + } else { + get_canvas()->request_update(); + } +} + +void CanvasItem::update(bool propagate) +{ + if (!_visible) { + _mark_net_invisible(); + return; + } + + bool reappearing = !_net_visible; + _net_visible = true; + + if (!_need_update && !reappearing && !propagate) { + return; + } + + _need_update = false; + + // Get new bounds + _update(propagate); + + if (reappearing) { + request_redraw(); + } +} + +void CanvasItem::_mark_net_invisible() +{ + if (!_net_visible) { + return; + } + _net_visible = false; + _need_update = false; + request_redraw(); + _bounds = {}; +} + +// Grab all events! +void CanvasItem::grab(Gdk::EventMask event_mask, Glib::RefPtr<Gdk::Cursor> const &cursor) +{ + if constexpr (DEBUG_LOGGING) std::cout << "CanvasItem::grab: " << _name << std::endl; + + auto canvas = get_canvas(); + + // Don't grab if we already have a grabbed item! + if (canvas->get_grabbed_canvas_item()) { + return; + } + + gtk_grab_add(GTK_WIDGET(canvas->gobj())); + + canvas->set_grabbed_canvas_item(this, event_mask); + canvas->set_current_canvas_item(this); // So that all events go to grabbed item. +} + +void CanvasItem::ungrab() +{ + if constexpr (DEBUG_LOGGING) std::cout << "CanvasItem::ungrab: " << _name << std::endl; + + auto canvas = get_canvas(); + + if (canvas->get_grabbed_canvas_item() != this) { + return; // Sanity check + } + + canvas->set_grabbed_canvas_item(nullptr, (Gdk::EventMask)0); // Zero mask + + gtk_grab_remove(GTK_WIDGET(canvas->gobj())); +} + +void CanvasItem::render(CanvasItemBuffer &buf) const +{ + if (_visible && _bounds && _bounds->interiorIntersects(buf.rect)) { + _render(buf); + if constexpr (DEBUG_BOUNDS) { + auto bounds = *_bounds; + bounds.expandBy(-1); + bounds -= buf.rect.min(); + buf.cr->set_source_rgba(1.0, 0.0, 0.0, 1.0); + buf.cr->rectangle(bounds.min().x(), bounds.min().y(), bounds.width(), bounds.height()); + buf.cr->stroke(); + } + } +} + +/* + * The main invariant of the invisibility system is + * + * x needs update and is visible ==> parent(x) needs update or is invisible + * + * When x belongs to the visible subtree, meaning it and all its parents are visible, + * this condition reduces to + * + * x needs update ==> parent(x) needs update + * + * Thus within the visible subtree, the subset of nodes that need updating forms a subtree. + * + * In the update() function, we only walk this latter subtree. + */ + +void CanvasItem::set_visible(bool visible) +{ + defer([=] { + if (_visible == visible) return; + if (_visible) { + request_update(); + _visible = false; + } else { + _visible = true; + _need_update = false; + request_update(); + } + }); +} + +void CanvasItem::request_redraw() +{ + // Queue redraw request + if (_bounds) { + get_canvas()->redraw_area(*_bounds); + } +} + +void CanvasItem::set_fill(uint32_t fill) +{ + defer([=] { + if (_fill == fill) return; + _fill = fill; + request_redraw(); + }); +} + +void CanvasItem::set_stroke(uint32_t stroke) +{ + defer([=] { + if (_stroke == stroke) return; + _stroke = stroke; + request_redraw(); + }); +} + +void CanvasItem::update_canvas_item_ctrl_sizes(int size_index) +{ + if (auto ctrl = dynamic_cast<CanvasItemCtrl*>(this)) { + // We can't use set_size_default as the preference file is updated ->after<- the signal is emitted! + ctrl->set_size_via_index(size_index); + } else if (auto group = dynamic_cast<CanvasItemGroup*>(this)) { + for (auto &item : group->items) { + item.update_canvas_item_ctrl_sizes(size_index); + } + } +} + +void CanvasItem::canvas_item_print_tree(int level, int zorder) const +{ + if (level == 0) { + std::cout << "Canvas Item Tree" << std::endl; + } + + std::cout << "CC: "; + for (int i = 0; i < level; ++i) { + std::cout << " "; + } + + std::cout << zorder << ": " << _name << std::endl; + + if (auto group = dynamic_cast<Inkscape::CanvasItemGroup const*>(this)) { + int i = 0; + for (auto &item : group->items) { + item.canvas_item_print_tree(level + 1, i); + i++; + } + } +} + +} // 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/control/canvas-item.h b/src/display/control/canvas-item.h new file mode 100644 index 0000000..fc34176 --- /dev/null +++ b/src/display/control/canvas-item.h @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_CANVAS_ITEM_H +#define SEEN_CANVAS_ITEM_H + +/** + * Abstract base class for on-canvas control items. + */ + +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of SPCanvasItem + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + * A note about coordinates: + * + * 1. Canvas items are constructed using document (SVG) coordinates. + * 2. Calculations are made in canvas units, which is equivalent of SVG units multiplied by zoom factor. + * This is true for bounds and closest distance calculations. + * 3 Drawing is done in screen units which is the same as canvas units but translated. + * The document and canvas origins overlap. + * The affine contains only scaling and rotating components. + */ + +#include <cstdint> +#include <boost/intrusive/list.hpp> +#include <2geom/rect.h> +#include <sigc++/sigc++.h> + +#include <gdkmm/device.h> // Gdk::EventMask +#include <gdk/gdk.h> // GdkEvent + +#include "canvas-item-enums.h" +#include "canvas-item-buffer.h" +#include "canvas-item-context.h" + +class SPItem; + +namespace Inkscape { + +inline constexpr uint32_t CANVAS_ITEM_COLORS[] = { 0x0000ff7f, 0xff00007f, 0xffff007f }; + +namespace UI::Widget { class Canvas; } +class CanvasItemGroup; + +class CanvasItem +{ +public: + CanvasItem(CanvasItemContext *context); + CanvasItem(CanvasItemGroup *parent); + CanvasItem(CanvasItem const &) = delete; + CanvasItem &operator=(CanvasItem const &) = delete; + void unlink(); + + // Structure + UI::Widget::Canvas *get_canvas() const { return _context->canvas(); } + CanvasItemGroup *get_parent() const { return _parent; } + bool is_descendant_of(CanvasItem const *ancestor) const; + + // Z Position + void set_z_position(int zpos); + void raise_to_top(); // Move to top of group (last entry). + void lower_to_bottom(); // Move to bottom of group (first entry). + + // Geometry + void request_update(); + void update(bool propagate); + virtual void visit_page_rects(std::function<void(Geom::Rect const &)> const &) const {} + Geom::OptRect const &get_bounds() const { return _bounds; } + + // Selection + virtual bool contains(Geom::Point const &p, double tolerance = 0) { return _bounds && _bounds->interiorContains(p); } + void grab(Gdk::EventMask event_mask, Glib::RefPtr<Gdk::Cursor> const & = {}); + void ungrab(); + + // Display + void render(Inkscape::CanvasItemBuffer &buf) const; + bool is_visible() const { return _visible; } + virtual void set_visible(bool visible); + void show() { set_visible(true); } + void hide() { set_visible(false); } + void request_redraw(); // queue redraw request + + // Properties + virtual void set_fill(uint32_t rgba); + void set_fill(CanvasItemColor color) { set_fill(CANVAS_ITEM_COLORS[color]); } + virtual void set_stroke(uint32_t rgba); + void set_stroke(CanvasItemColor color) { set_stroke(CANVAS_ITEM_COLORS[color]); } + void set_name(std::string &&name) { _name = std::move(name); } + std::string const &get_name() const { return _name; } + void update_canvas_item_ctrl_sizes(int size_index); + + // Events + void set_pickable(bool pickable) { _pickable = pickable; } + bool is_pickable() const { return _pickable; } + sigc::connection connect_event(sigc::slot<bool(GdkEvent*)> const &slot) { + return _event_signal.connect(slot); + } + virtual bool handle_event(GdkEvent *event) { + return _event_signal.emit(event); // Default just emits event. + } + + // Recursively print CanvasItem tree. + void canvas_item_print_tree(int level = 0, int zorder = 0) const; + + // Boost linked list member hook, speeds deletion. + boost::intrusive::list_member_hook<> member_hook; + +protected: + friend class CanvasItemGroup; + + virtual ~CanvasItem(); + + // Structure + CanvasItemContext *_context; + CanvasItemGroup *_parent; + + // Geometry + Geom::OptRect _bounds; + bool _need_update = false; + Geom::Affine const &affine() const { return _context->affine(); } + virtual void _update(bool propagate) = 0; + virtual void _mark_net_invisible(); + + // Display + bool _visible = true; + bool _net_visible = true; + virtual void _render(Inkscape::CanvasItemBuffer &buf) const = 0; + + // Selection + bool _pickable = false; // Most items are just for display and are not pickable! + + // Properties + uint32_t _fill = CANVAS_ITEM_COLORS[CANVAS_ITEM_SECONDARY]; + uint32_t _stroke = CANVAS_ITEM_COLORS[CANVAS_ITEM_PRIMARY]; + std::string _name; // For debugging + + // Events + sigc::signal<bool (GdkEvent*)> _event_signal; + + // Snapshotting + template<typename F> + void defer(F &&f) { _context->defer(std::forward<F>(f)); } +}; + +} // namespace Inkscape + +// Todo: Move to lib2geom. +inline auto &operator<<(std::ostream &s, Geom::OptRect const &rect) +{ + return rect ? (s << *rect) : (s << "(empty)"); +} + +#endif // SEEN_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/control/canvas-page.cpp b/src/display/control/canvas-page.cpp new file mode 100644 index 0000000..5fa337d --- /dev/null +++ b/src/display/control/canvas-page.cpp @@ -0,0 +1,282 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape pages implementation + * + * Authors: + * Martin Owens <doctormo@geek-2.com> + * + * Copyright (C) 2021 Martin Owens + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "canvas-page.h" +#include "canvas-item-rect.h" +#include "canvas-item-text.h" +#include "color.h" + +namespace Inkscape { + +CanvasPage::CanvasPage() = default; + +CanvasPage::~CanvasPage() = default; + +/** + * Add the page canvas to the given canvas item groups (canvas view is implicit) + */ +void CanvasPage::add(Geom::Rect size, CanvasItemGroup *background_group, CanvasItemGroup *border_group) +{ + // Foreground 'border' + if (auto item = new CanvasItemRect(border_group, size)) { + item->set_name("foreground"); + item->set_is_page(true); + canvas_items.emplace_back(item); + } + + // Background rectangle 'fill' + if (auto item = new CanvasItemRect(background_group, size)) { + item->set_name("background"); + item->set_is_page(true); + item->set_dashed(false); + item->set_inverted(false); + item->set_stroke(0x00000000); + canvas_items.emplace_back(item); + } + + if (auto item = new CanvasItemRect(border_group, size)) { + item->set_name("margin"); + item->set_dashed(false); + item->set_inverted(false); + item->set_stroke(_margin_color); + canvas_items.emplace_back(item); + } + + if (auto item = new CanvasItemRect(border_group, size)) { + item->set_name("bleed"); + item->set_dashed(false); + item->set_inverted(false); + item->set_stroke(_bleed_color); + canvas_items.emplace_back(item); + } + + if (auto label = new CanvasItemText(border_group, Geom::Point(0, 0), "{Page Label}")) { + label->set_fixed_line(false); + canvas_items.emplace_back(label); + } +} +/** + * Hide the page in the given canvas widget. + */ +void CanvasPage::remove(UI::Widget::Canvas *canvas) +{ + g_assert(canvas != nullptr); + for (auto it = canvas_items.begin(); it != canvas_items.end();) { + if (canvas == (*it)->get_canvas()) { + it = canvas_items.erase(it); + } else { + ++it; + } + } +} + +void CanvasPage::show() +{ + for (auto &item : canvas_items) { + item->show(); + } +} + +void CanvasPage::hide() +{ + for (auto &item : canvas_items) { + item->hide(); + } +} + +void CanvasPage::set_guides_visible(bool show) { + for (auto& item: canvas_items) { + if (item->get_name() == "margin" || item->get_name() == "bleed") { + item->set_visible(show); + } + } +} + +/** + * Update the visual representation of a page on screen. + * + * @param size - The size of the page in desktop units + * @param txt - An optional label for the page + * @param outline - Disable normal rendering and show as an outline. + */ +void CanvasPage::update(Geom::Rect size, Geom::OptRect margin, Geom::OptRect bleed, const char *txt, bool outline) +{ + // Put these in the preferences? + bool border_on_top = _border_on_top; + guint32 shadow_color = _border_color; // there's no separate shadow color in the UI, border color is used + guint32 select_color = 0x000000cc; + guint32 border_color = _border_color; + guint32 margin_color = _margin_color; + guint32 bleed_color = _bleed_color; + + // This is used when showing the viewport as *not a page* it's mostly + // never used as the first page is normally the viewport too. + if (outline) { + border_on_top = false; + _shadow_size = 0; + border_color = select_color; + } + + for (auto &item : canvas_items) { + if (auto rect = dynamic_cast<CanvasItemRect *>(item.get())) { + if (rect->get_name() == "margin") { + rect->set_stroke(margin_color); + bool vis = margin && *margin != size; + rect->set_visible(vis); + if (vis) { + rect->set_rect(*margin); + } + continue; + } + if (rect->get_name() == "bleed") { + rect->set_stroke(bleed_color); + bool vis = bleed && *bleed != size; + rect->set_visible(vis); + if (vis) { + rect->set_rect(*bleed); + } + continue; + } + + rect->set_rect(size); + + bool is_foreground = (rect->get_name() == "foreground"); + // This will put the border on the background OR foreground layer as needed. + if (is_foreground == border_on_top) { + rect->show(); + rect->set_stroke(is_selected ? select_color : border_color); + } else { + rect->hide(); + rect->set_stroke(0x0); + } + // This undoes the hide for the background rect, and additionally gives it a fill and shadow. + if (!is_foreground) { + rect->show(); +/* + if (_checkerboard) { + // draw checkerboard pattern, ignore alpha (background color doesn't support it) + rect->set_background_checkerboard(_background_color, false); + } + else { + // Background color does not support transparency; draw opaque pages + rect->set_background(_background_color | 0xff); + } +*/ + rect->set_fill(_background_color); + rect->set_shadow(shadow_color, _shadow_size); + } else { + rect->set_fill(0x0); + rect->set_shadow(0x0, 0); + } + } else if (auto label = dynamic_cast<CanvasItemText *>(item.get())) { + _updateTextItem(label, size, txt ? txt : ""); + } + } +} + +/** + * Update the page's textual label. + */ +void CanvasPage::_updateTextItem(CanvasItemText *label, Geom::Rect page, std::string txt) +{ + // Default style for the label + int fontsize = 10.0; + uint32_t foreground = 0xffffffff; + uint32_t background = 0x00000099; + uint32_t selected = 0x0e5bf199; + Geom::Point anchor(0.0, 1.0); + Geom::Point coord = page.corner(0); + double radius = 0.2; + + // Change the colors for whiter/lighter backgrounds + unsigned char luminance = SP_RGBA32_LUMINANCE(_canvas_color); + if (luminance < 0x88) { + foreground = 0x000000ff; + background = 0xffffff99; + selected = 0x50afe7ff; + } + + if (_label_style == "below") { + radius = 1.0; + fontsize = 14.0; + anchor = Geom::Point(0.5, -0.2); + coord = Geom::Point(page.midpoint()[Geom::X], page.bottom()); + + if (!txt.empty()) { + std::string bullet = is_selected ? " \u2022 " : " "; + txt = bullet + txt + bullet; + } + } + + label->set_fontsize(fontsize); + label->set_fill(foreground); + label->set_background(is_selected ? selected : background); + label->set_bg_radius(radius); + label->set_anchor(anchor); + label->set_coord(coord); + label->set_visible(!txt.empty()); + label->set_text(std::move(txt)); + label->set_border(4.0); +} + +bool CanvasPage::setOnTop(bool on_top) +{ + if (on_top != _border_on_top) { + _border_on_top = on_top; + return true; + } + return false; +} + +bool CanvasPage::setShadow(int shadow) +{ + if (_shadow_size != shadow) { + _shadow_size = shadow; + return true; + } + return false; +} + +bool CanvasPage::setPageColor(uint32_t border, uint32_t bg, uint32_t canvas, uint32_t margin, uint32_t bleed) +{ + if (border != _border_color || bg != _background_color || canvas != _canvas_color) { + _border_color = border; + _background_color = bg; + _canvas_color = canvas; + _margin_color = margin; + _bleed_color = bleed; + return true; + } + return false; +} + +bool CanvasPage::setLabelStyle(const std::string &style) +{ + if (style != _label_style) { + _label_style = style; + return true; + } + return false; +} + +} // 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/control/canvas-page.h b/src/display/control/canvas-page.h new file mode 100644 index 0000000..e3227bf --- /dev/null +++ b/src/display/control/canvas-page.h @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * + *//* + * Authors: + * Martin Owens 2021 + * + * Copyright (C) 2021 Martin Owens + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_CANVAS_PAGE_H +#define SEEN_CANVAS_PAGE_H + +#include <2geom/rect.h> +#include <glib.h> +#include <vector> + +#include "canvas-item-ptr.h" + +namespace Inkscape { +namespace UI::Widget { class Canvas; } + +class CanvasItemGroup; +class CanvasItemText; + +class CanvasPage +{ +public: + CanvasPage(); + ~CanvasPage(); + + void update(Geom::Rect size, Geom::OptRect margin, Geom::OptRect bleed, const char *txt, bool outline = false); + void add(Geom::Rect size, CanvasItemGroup *background_group, CanvasItemGroup *foreground_group); + void remove(UI::Widget::Canvas *canvas); + void show(); + void hide(); + void set_guides_visible(bool show); + + bool setOnTop(bool on_top); + bool setShadow(int shadow); + bool setPageColor(uint32_t border, uint32_t bg, uint32_t canvas, uint32_t margin, uint32_t bleed); + bool setLabelStyle(const std::string &style); + + bool is_selected = false; +private: + void _updateTextItem(CanvasItemText *label, Geom::Rect page, std::string txt); + + // This may make this look like a CanvasItemGroup, but it's not one. This + // isn't a collection of items, but a set of items in multiple Canvases. + // Each item can belong in either a foreground or background group. + std::vector<CanvasItemPtr<CanvasItem>> canvas_items; + + int _shadow_size = 0; + bool _border_on_top = true; + uint32_t _background_color = 0xffffffff; + uint32_t _border_color = 0x00000040; + uint32_t _canvas_color = 0xffffffff; + uint32_t _margin_color = 0x1699d771; // Blue'ish + uint32_t _bleed_color = 0xbe310e62; // Red'ish + + std::string _label_style = "default"; +}; + +} // namespace Inkscape + +#endif // SEEN_CANVAS_PAGE_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/control/canvas-temporary-item-list.cpp b/src/display/control/canvas-temporary-item-list.cpp new file mode 100644 index 0000000..6ff516d --- /dev/null +++ b/src/display/control/canvas-temporary-item-list.cpp @@ -0,0 +1,72 @@ +// 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 <algorithm> +#include "canvas-temporary-item.h" +#include "canvas-temporary-item-list.h" + +namespace Inkscape { +namespace Display { + +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(CanvasItem *item, int lifetime_msecs) +{ + // beware of strange things happening due to very short timeouts + TemporaryItem *tempitem; + if (lifetime_msecs == 0) + tempitem = new TemporaryItem(item, 0); + else { + tempitem = new TemporaryItem(item, lifetime_msecs); + tempitem->signal_timeout.connect([this] (auto tempitem) { itemlist.remove(tempitem); }); + // no need to delete the item, it does that itself after signal_timeout.emit() completes + } + + itemlist.emplace_back(tempitem); + 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) + auto it = std::find_if(itemlist.begin(), itemlist.end(), [=] (auto *item) { + return item == tempitem; + }); + + if (it != itemlist.end()) { + itemlist.erase(it); + delete tempitem; + } +} + +} // 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/control/canvas-temporary-item-list.h b/src/display/control/canvas-temporary-item-list.h new file mode 100644 index 0000000..1321cbf --- /dev/null +++ b/src/display/control/canvas-temporary-item-list.h @@ -0,0 +1,58 @@ +// 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> + +class SPDesktop; + +namespace Inkscape { + +class CanvasItem; + +namespace Display { + +class TemporaryItem; + +/** + * Provides a class that can contain active TemporaryItems on a desktop. + */ +class TemporaryItemList final +{ +public: + TemporaryItemList() = default; + TemporaryItemList(TemporaryItemList const &) = delete; + TemporaryItemList &operator=(TemporaryItemList const &) = delete; + ~TemporaryItemList(); + + TemporaryItem* add_item(CanvasItem *item, int lifetime_msecs); + void delete_item(TemporaryItem *tempitem); + +protected: + std::list<TemporaryItem *> itemlist; ///< List of temp items. +}; + +} // namespace Display +} // namespace Inkscape + +#endif // INKSCAPE_CANVAS_TEMPORARY_ITEM_LIST_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/control/canvas-temporary-item.cpp b/src/display/control/canvas-temporary-item.cpp new file mode 100644 index 0000000..d95ad21 --- /dev/null +++ b/src/display/control/canvas-temporary-item.cpp @@ -0,0 +1,53 @@ +// 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 <glibmm/main.h> + +#include "canvas-temporary-item.h" +#include "canvas-item.h" + +namespace Inkscape { +namespace Display { + +TemporaryItem::TemporaryItem(CanvasItem *item, int lifetime_msecs) + : canvasitem(std::move(item)) +{ + // Zero lifetime means stay forever, so do not add timeout event. + if (lifetime_msecs > 0) { + timeout_conn = Glib::signal_timeout().connect([this] { + signal_timeout.emit(this); + delete this; + return false; + }, lifetime_msecs); + } +} + +TemporaryItem::~TemporaryItem() = default; + +} // 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/control/canvas-temporary-item.h b/src/display/control/canvas-temporary-item.h new file mode 100644 index 0000000..4ce2787 --- /dev/null +++ b/src/display/control/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> +#include <sigc++/connection.h> +#include "display/control/canvas-item-ptr.h" +#include "helper/auto-connection.h" + +namespace Inkscape { + +class CanvasItem; + +namespace Display { + +/** + * Provides a class to put a canvasitem temporarily on-canvas. + */ +class TemporaryItem final +{ +public: + TemporaryItem(CanvasItem *item, int lifetime_msecs); + TemporaryItem(TemporaryItem const &) = delete; + TemporaryItem &operator=(TemporaryItem const &) = delete; + ~TemporaryItem(); + + sigc::signal<void (TemporaryItem *)> signal_timeout; + +protected: + friend class TemporaryItemList; + + CanvasItemPtr<CanvasItem> canvasitem; ///< The item we are holding on to. + auto_connection timeout_conn; +}; + +} //namespace Display +} //namespace Inkscape + +#endif // INKSCAPE_CANVAS_TEMPORARY_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/control/snap-indicator.cpp b/src/display/control/snap-indicator.cpp new file mode 100644 index 0000000..8672d88 --- /dev/null +++ b/src/display/control/snap-indicator.cpp @@ -0,0 +1,648 @@ +// 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 <glibmm/i18n.h> +#include <string> +#include <iomanip> +#include <unordered_map> + +#include "snap-indicator.h" + +#include "desktop.h" +#include "enums.h" +#include "preferences.h" +#include "util/units.h" +#include "document.h" + + +#include "canvas-item-ctrl.h" +#include "canvas-item-rect.h" +#include "canvas-item-text.h" +#include "canvas-item-curve.h" + +#include "ui/tools/measure-tool.h" + +#define DISTANCE_BG_RADIUS 0.3 + +namespace Inkscape { +namespace Display { + +static std::map<SnapSourceType, Glib::ustring> source2string = { + {SNAPSOURCE_UNDEFINED, _("UNDEFINED")}, + {SNAPSOURCE_BBOX_CORNER, _("Bounding box corner")}, + {SNAPSOURCE_BBOX_MIDPOINT, _("Bounding box midpoint")}, + {SNAPSOURCE_BBOX_EDGE_MIDPOINT, _("Bounding box side midpoint")}, + {SNAPSOURCE_NODE_SMOOTH, _("Smooth node")}, + {SNAPSOURCE_NODE_CUSP, _("Cusp node")}, + {SNAPSOURCE_LINE_MIDPOINT, _("Line midpoint")}, + {SNAPSOURCE_PATH_INTERSECTION, _("Path intersection")}, + {SNAPSOURCE_RECT_CORNER, _("Corner")}, + {SNAPSOURCE_CONVEX_HULL_CORNER, _("Convex hull corner")}, + {SNAPSOURCE_ELLIPSE_QUADRANT_POINT, _("Quadrant point")}, + {SNAPSOURCE_NODE_HANDLE, _("Handle")}, + {SNAPSOURCE_GUIDE, _("Guide")}, + {SNAPSOURCE_GUIDE_ORIGIN, _("Guide origin")}, + {SNAPSOURCE_ROTATION_CENTER, _("Object rotation center")}, + {SNAPSOURCE_OBJECT_MIDPOINT, _("Object midpoint")}, + {SNAPSOURCE_IMG_CORNER, _("Corner")}, + {SNAPSOURCE_TEXT_ANCHOR, _("Text anchor")}, + {SNAPSOURCE_OTHER_HANDLE, _("Handle")}, + {SNAPSOURCE_GRID_PITCH, _("Multiple of grid spacing")}, + {SNAPSOURCE_PAGE_CORNER, _("Page corner")}, + {SNAPSOURCE_PAGE_CENTER, _("Page center")}, +}; + +static std::map<SnapTargetType, Glib::ustring> target2string = { + {SNAPTARGET_UNDEFINED, _("UNDEFINED")}, + {SNAPTARGET_BBOX_CORNER, _("bounding box corner")}, + {SNAPTARGET_BBOX_EDGE, _("bounding box side")}, + {SNAPTARGET_BBOX_EDGE_MIDPOINT, _("bounding box side midpoint")}, + {SNAPTARGET_BBOX_MIDPOINT, _("bounding box midpoint")}, + {SNAPTARGET_NODE_SMOOTH, _("smooth node")}, + {SNAPTARGET_NODE_CUSP, _("cusp node")}, + {SNAPTARGET_LINE_MIDPOINT, _("line midpoint")}, + {SNAPTARGET_PATH, _("path")}, + {SNAPTARGET_PATH_PERPENDICULAR, _("path (perpendicular)")}, + {SNAPTARGET_PATH_TANGENTIAL, _("path (tangential)")}, + {SNAPTARGET_PATH_INTERSECTION, _("path intersection")}, + {SNAPTARGET_PATH_GUIDE_INTERSECTION, _("guide-path intersection")}, + {SNAPTARGET_PATH_CLIP, _("clip-path")}, + {SNAPTARGET_PATH_MASK, _("mask-path")}, + {SNAPTARGET_ELLIPSE_QUADRANT_POINT, _("quadrant point")}, + {SNAPTARGET_RECT_CORNER, _("corner")}, + {SNAPTARGET_GRID, _("grid line")}, + {SNAPTARGET_GRID_INTERSECTION, _("grid intersection")}, + {SNAPTARGET_GRID_PERPENDICULAR, _("grid line (perpendicular)")}, + {SNAPTARGET_GUIDE, _("guide")}, + {SNAPTARGET_GUIDE_INTERSECTION, _("guide intersection")}, + {SNAPTARGET_GUIDE_ORIGIN, _("guide origin")}, + {SNAPTARGET_GUIDE_PERPENDICULAR, _("guide (perpendicular)")}, + {SNAPTARGET_GRID_GUIDE_INTERSECTION, _("grid-guide intersection")}, + {SNAPTARGET_PAGE_EDGE_BORDER, _("page border")}, + {SNAPTARGET_PAGE_EDGE_CORNER, _("page corner")}, + {SNAPTARGET_PAGE_EDGE_CENTER, _("page center")}, + {SNAPTARGET_PAGE_MARGIN_BORDER, _("page margin border")}, + {SNAPTARGET_PAGE_MARGIN_CORNER, _("page margin corner")}, + {SNAPTARGET_PAGE_MARGIN_CENTER, _("page margin center")}, + {SNAPTARGET_PAGE_BLEED_BORDER, _("page bleed border")}, + {SNAPTARGET_PAGE_BLEED_CORNER, _("page bleed corner")}, + {SNAPTARGET_OBJECT_MIDPOINT, _("object midpoint")}, + {SNAPTARGET_IMG_CORNER, _("corner")}, + {SNAPTARGET_ROTATION_CENTER, _("object rotation center")}, + {SNAPTARGET_TEXT_ANCHOR, _("text anchor")}, + {SNAPTARGET_TEXT_BASELINE, _("text baseline")}, + {SNAPTARGET_CONSTRAINED_ANGLE, _("constrained angle")}, + {SNAPTARGET_CONSTRAINT, _("constraint")}, +}; + +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; + } + + bool is_alignment = p.getAlignmentTarget().has_value(); + bool is_distribution = p.getTarget() & SNAPTARGET_DISTRIBUTION_CATEGORY; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double scale = prefs->getDouble("/tools/measure/scale", 100.0) / 100.0; + + bool value = prefs->getBool("/options/snapindicator/value", true); + + if (value) { + Glib::ustring target_name = _("UNDEFINED"); + Glib::ustring source_name = _("UNDEFINED"); + + if (!is_alignment && !is_distribution) { + if (target2string.find(p.getTarget()) == target2string.end()) + g_warning("Target type %i not present in target2string", p.getTarget()); + + if (source2string.find(p.getSource()) == source2string.end()) + g_warning("Source type %i not present in target2string", p.getSource()); + + target_name = _(target2string[p.getTarget()].c_str()); + source_name = _(source2string[p.getSource()].c_str()); + } + //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 + + 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 ....? + } + + // TODO: should this be a constant or a separate prefrence + // we are using the preference of measure tool here. + double fontsize = prefs->getDouble("/tools/measure/fontsize", 10.0); + + if (is_distribution) { + make_distribution_indicators(p, fontsize, scale); + } + + if (is_alignment) { + auto color = pre_snap ? 0x7f7f7fff : get_guide_color(p.getAlignmentTargetType()); + make_alignment_indicator(p.getPoint(), *p.getAlignmentTarget(), color, fontsize, scale); + if (p.getAlignmentTargetType() == SNAPTARGET_ALIGNMENT_INTERSECTION) { + make_alignment_indicator(p.getPoint(), *p.getAlignmentTarget2(), color, fontsize, scale); + } + } + + _snaptarget_is_presnap = pre_snap; + + // Display the snap indicator (i.e. the cross) + Inkscape::CanvasItemCtrl *ctrl; + + if (!is_alignment && !is_distribution) { + // Display snap indicator at snap target + ctrl = new Inkscape::CanvasItemCtrl(_desktop->getCanvasTemp(), Inkscape::CANVAS_ITEM_CTRL_SHAPE_CROSS); + ctrl->set_size(11); + ctrl->set_stroke( pre_snap ? 0x7f7f7fff : 0xff0000ff); + ctrl->set_position(p.getPoint()); + + _snaptarget = _desktop->add_temporary_canvasitem(ctrl, timeout_val*1000.0); + // 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) + ctrl->set_pickable(false); + + // Display the tooltip, which reveals the type of snap source and the type of snap target + Glib::ustring tooltip_str; + if ( (p.getSource() != SNAPSOURCE_GRID_PITCH) && (p.getTarget() != SNAPTARGET_UNDEFINED) ) { + tooltip_str = source_name + _(" to ") + target_name; + } else if (p.getSource() != SNAPSOURCE_UNDEFINED) { + tooltip_str = source_name; + } + + + if (!tooltip_str.empty()) { + Geom::Point tooltip_pos = p.getPoint(); + if (dynamic_cast<Inkscape::UI::Tools::MeasureTool *>(_desktop->event_context)) { + // 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)); + } + + auto canvas_tooltip = new Inkscape::CanvasItemText(_desktop->getCanvasTemp(), tooltip_pos, tooltip_str); + canvas_tooltip->set_fontsize(fontsize); + canvas_tooltip->set_fill(0xffffffff); + canvas_tooltip->set_background(pre_snap ? 0x33337f40 : 0x33337f7f); + + _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) { + auto box = new Inkscape::CanvasItemRect(_desktop->getCanvasTemp(), *bbox); + box->set_stroke(pre_snap ? 0x7f7f7fff : 0xff0000ff); + box->set_dashed(true); + box->set_pickable(false); // Is false by default. + box->lower_to_bottom(); + _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; + } + + for (auto *item : _alignment_snap_indicators) { + _desktop->remove_temporary_canvasitem(item); + } + _alignment_snap_indicators.clear(); + + for (auto *item : _distribution_snap_indicators) { + _desktop->remove_temporary_canvasitem(item); + } + _distribution_snap_indicators.clear(); +} + +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) { + auto ctrl = new Inkscape::CanvasItemCtrl(_desktop->getCanvasTemp(), Inkscape::CANVAS_ITEM_CTRL_SHAPE_CIRCLE); + ctrl->set_size(7); + ctrl->set_stroke(0xff0000ff); + ctrl->set_position(p.getPoint()); + _snapsource = _desktop->add_temporary_canvasitem(ctrl, 1000); + } +} + +void +SnapIndicator::set_new_debugging_point(Geom::Point const &p) +{ + g_assert(_desktop != nullptr); + auto ctrl = new Inkscape::CanvasItemCtrl(_desktop->getCanvasTemp(), Inkscape::CANVAS_ITEM_CTRL_SHAPE_DIAMOND); + ctrl->set_size(11); + ctrl->set_stroke(0x00ff00ff); + ctrl->set_position(p); + _debugging_points.push_back(_desktop->add_temporary_canvasitem(ctrl, 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(); +} + +guint32 SnapIndicator::get_guide_color(SnapTargetType t) +{ + switch(t) { + case SNAPTARGET_ALIGNMENT_BBOX_CORNER: + case SNAPTARGET_ALIGNMENT_BBOX_MIDPOINT: + case SNAPTARGET_ALIGNMENT_BBOX_EDGE_MIDPOINT: + return 0xff0000ff; + case SNAPTARGET_ALIGNMENT_PAGE_EDGE_CENTER: + case SNAPTARGET_ALIGNMENT_PAGE_EDGE_CORNER: + case SNAPTARGET_ALIGNMENT_PAGE_MARGIN_CENTER: + case SNAPTARGET_ALIGNMENT_PAGE_MARGIN_CORNER: + case SNAPTARGET_ALIGNMENT_PAGE_BLEED_CORNER: + return 0x00ff00ff; + case SNAPTARGET_ALIGNMENT_HANDLE: + return 0x0000ffff; + case SNAPTARGET_ALIGNMENT_INTERSECTION: + return 0xd13bd1ff; + default: + g_warning("Alignment guide color not handled %i", t); + return 0x000000ff; + } +} + +std::pair<Geom::Coord, int> get_y_and_sign(Geom::Rect const &source, Geom::Rect const &target, double const offset) +{ + Geom::Coord y; + int sign; + + // We add a margin of 5px here to make sure that very small movements of mouse + // pointer do not cause the position of distribution indicator to change. + if (source.midpoint().y() < target.midpoint().y() + 5) { + y = source.max().y() + offset; + sign = 1; + } else { + y = source.min().y() - offset; + sign = -1; + } + + return {y, sign}; +} + +std::pair<Geom::Coord, int> get_x_and_sign(Geom::Rect const &source, Geom::Rect const &target, double const offset) +{ + Geom::Coord x; + int sign; + + // We add a margin of 5px here to make sure that very small movements of mouse + // pointer do not cause the position of distribution indicator to change. + if (source.midpoint().x() < target.midpoint().x() + 5) { + x = source.max().x() + offset; + sign = 1; + } else { + x = source.min().x() - offset; + sign = -1; + } + + return {x, sign}; +} + +void SnapIndicator::make_alignment_indicator(Geom::Point const &p1, Geom::Point const &p2, guint32 color, double fontsize, double scale) +{ + //make sure the line is straight + g_assert(p1.x() == p2.x() || p1.y() == p2.y()); + + Preferences *prefs = Preferences::get(); + bool show_distance = prefs->getBool("/options/snapindicatordistance/value", false); + + Inkscape::CanvasItemCurve *line; + + auto ctrl = new Inkscape::CanvasItemCtrl(_desktop->getCanvasTemp(), Inkscape::CANVAS_ITEM_CTRL_SHAPE_CIRCLE); + ctrl->set_size(7); + ctrl->set_mode(Inkscape::CanvasItemCtrlMode::CANVAS_ITEM_CTRL_MODE_COLOR); + ctrl->set_stroke(0xffffffff); + ctrl->set_fill(color); + ctrl->set_position(p1); + ctrl->set_pickable(false); + _alignment_snap_indicators.push_back(_desktop->add_temporary_canvasitem(ctrl, 0)); + + ctrl = new Inkscape::CanvasItemCtrl(_desktop->getCanvasTemp(), Inkscape::CANVAS_ITEM_CTRL_SHAPE_CIRCLE); + ctrl->set_size(7); + ctrl->set_mode(Inkscape::CanvasItemCtrlMode::CANVAS_ITEM_CTRL_MODE_COLOR); + ctrl->set_stroke(0xffffffff); + ctrl->set_fill(color); + ctrl->set_position(p2); + ctrl->set_pickable(false); + _alignment_snap_indicators.push_back(_desktop->add_temporary_canvasitem(ctrl, 0)); + + auto dist = Geom::L2(p2 - p1); + double offset = (fontsize + 5) / _desktop->current_zoom(); + if (show_distance && dist > 2 * offset) { + auto direction = Geom::unit_vector(p1 - p2); + auto text_pos = (p1 + p2)/2; + + Glib::ustring unit_name = _desktop->doc()->getDisplayUnit()->abbr.c_str(); + if (!unit_name.compare("")) { + unit_name = DEFAULT_UNIT_NAME; + } + + dist = Inkscape::Util::Quantity::convert(dist, "px", unit_name); + + Glib::ustring distance = Glib::ustring::format(std::fixed, std::setprecision(1), std::noshowpoint, scale*dist); + + auto text = new Inkscape::CanvasItemText(_desktop->getCanvasTemp(), text_pos, distance); + text->set_fontsize(fontsize); + text->set_fill(color); + text->set_background(0xffffffc8); + text->set_bg_radius(DISTANCE_BG_RADIUS); + text->set_anchor({0.5, 0.5}); + _alignment_snap_indicators.push_back(_desktop->add_temporary_canvasitem(text, 0)); + + auto temp_point = text_pos + offset*direction; + line = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), p1, temp_point); + line->set_stroke(color); + line->set_bg_alpha(1.0f); + _alignment_snap_indicators.push_back(_desktop->add_temporary_canvasitem(line, 0)); + + temp_point = text_pos - offset*direction; + line = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), temp_point, p2); + line->set_stroke(color); + line->set_bg_alpha(1.0f); + _alignment_snap_indicators.push_back(_desktop->add_temporary_canvasitem(line, 0)); + } else { + line = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), p1, p2); + line->set_stroke(color); + line->set_bg_alpha(1.0f); + _alignment_snap_indicators.push_back(_desktop->add_temporary_canvasitem(line, 0)); + } +} + +Inkscape::CanvasItemCurve* SnapIndicator::make_stub_line_v(Geom::Point const & p) +{ + Geom::Coord length = 10/_desktop->current_zoom(); + auto line = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), p + Geom::Point(0, length/2), p - Geom::Point(0, length/2)); + line->set_stroke(0xff5f1fff); + return line; +} + +Inkscape::CanvasItemCurve* SnapIndicator::make_stub_line_h(Geom::Point const & p) +{ + Geom::Coord length = 10/_desktop->current_zoom(); + auto line = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), p + Geom::Point(length/2, 0), p - Geom::Point(length/2, 0)); + line->set_stroke(0xff5f1fff); + return line; +} + +void SnapIndicator::make_distribution_indicators(SnappedPoint const &p, + double fontsize, + double scale) +{ + Preferences *prefs = Preferences::get(); + bool show_distance = prefs->getBool("/options/snapindicatordistance/value", false); + + guint32 color = 0xff5f1fff; + guint32 text_fill = 0xffffffff; + guint32 text_bg = 0xff5f1fff; //0x33337f7f + Geom::Point text_pos; + double text_offset = (fontsize * 2); + // double line_offset = 5/_desktop->current_zoom(); + double line_offset = 0; + + Glib::ustring unit_name = _desktop->doc()->getDisplayUnit()->abbr.c_str(); + if (!unit_name.compare("")) { + unit_name = DEFAULT_UNIT_NAME; + } + auto equal_dist = Inkscape::Util::Quantity::convert(p.getDistributionDistance(), "px", unit_name); + Glib::ustring distance = Glib::ustring::format(std::fixed, std::setprecision(1), std::noshowpoint, scale*equal_dist); + + switch (p.getTarget()) { + case SNAPTARGET_DISTRIBUTION_Y: + case SNAPTARGET_DISTRIBUTION_X: + case SNAPTARGET_DISTRIBUTION_RIGHT: + case SNAPTARGET_DISTRIBUTION_LEFT: + case SNAPTARGET_DISTRIBUTION_UP: + case SNAPTARGET_DISTRIBUTION_DOWN: { + Geom::Point p1, p2; + Inkscape::CanvasItemCurve *point1, *point2; + + for (auto it = p.getBBoxes().begin(); it + 1 != p.getBBoxes().end(); it++) { + switch (p.getTarget()) { + case SNAPTARGET_DISTRIBUTION_RIGHT: + case SNAPTARGET_DISTRIBUTION_LEFT: + case SNAPTARGET_DISTRIBUTION_X: { + auto [y, sign] = get_y_and_sign(*it, *std::next(it), 5/_desktop->current_zoom()); + p1 = Geom::Point(it->max().x() + line_offset, y); + p2 = Geom::Point(std::next(it)->min().x() - line_offset, y); + text_pos = (p1 + p2)/2 + _desktop->w2d(Geom::Point(0, sign*text_offset)); + + point1 = make_stub_line_v(p1); + point2 = make_stub_line_v(p2); + break; + } + + case SNAPTARGET_DISTRIBUTION_DOWN: + case SNAPTARGET_DISTRIBUTION_UP: + case SNAPTARGET_DISTRIBUTION_Y: { + auto [x, sign] = get_x_and_sign(*it, *std::next(it), 5/_desktop->current_zoom()); + p1 = Geom::Point(x, it->max().y() + line_offset); + p2 = Geom::Point(x, std::next(it)->min().y() - line_offset); + text_pos = (p1 + p2)/2 + _desktop->w2d(Geom::Point(sign*text_offset, 0)); + + point1 = make_stub_line_h(p1); + point2 = make_stub_line_h(p2); + break; + } + } + + _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(point1, 0)); + _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(point2, 0)); + + auto line1 = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), p1, p2); + line1->set_stroke(color); + line1->set_width(2); + _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(line1, 0)); + + if (show_distance) { + auto text = new Inkscape::CanvasItemText(_desktop->getCanvasTemp(), text_pos, distance); + text->set_fontsize(fontsize); + text->set_fill(text_fill); + text->set_background(text_bg); + text->set_bg_radius(DISTANCE_BG_RADIUS); + text->set_anchor({0.5, 0.5}); + _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(text, 0)); + } + } + break; + } + case SNAPTARGET_DISTRIBUTION_XY: { + Geom::Point p1, p2; + Inkscape::CanvasItemCurve *point1, *point2; + + auto equal_dist2 = Inkscape::Util::Quantity::convert(p.getDistributionDistance2(), "px", unit_name); + Glib::ustring distance2 = Glib::ustring::format(std::fixed, std::setprecision(1), std::noshowpoint, scale*equal_dist2); + + for (auto it = p.getBBoxes().begin(); it + 1 != p.getBBoxes().end(); it++) { + auto [y, sign] = get_y_and_sign(*it, *std::next(it), 5/_desktop->current_zoom()); + p1 = Geom::Point(it->max().x() + line_offset, y); + p2 = Geom::Point(std::next(it)->min().x() - line_offset, y); + text_pos = (p1 + p2)/2 + _desktop->w2d(Geom::Point(0, sign*text_offset)); + + point1 = make_stub_line_v(p1); + point2 = make_stub_line_v(p2); + + _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(point1, 0)); + _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(point2, 0)); + + auto line1 = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), p1, p2); + line1->set_stroke(color); + line1->set_width(2); + _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(line1, 0)); + + if (show_distance) { + auto text = new Inkscape::CanvasItemText(_desktop->getCanvasTemp(), text_pos, distance); + text->set_fontsize(fontsize); + text->set_fill(text_fill); + text->set_background(text_bg); + text->set_bg_radius(DISTANCE_BG_RADIUS); + text->set_anchor({0.5, 0.5}); + _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(text, 0)); + } + } + + for (auto it = p.getBBoxes2().begin(); it + 1 != p.getBBoxes2().end(); it++) { + auto [x, sign] = get_x_and_sign(*it, *std::next(it), 5/_desktop->current_zoom()); + p1 = Geom::Point(x, it->max().y() + line_offset); + p2 = Geom::Point(x, std::next(it)->min().y() - line_offset); + text_pos = (p1 + p2)/2 + _desktop->w2d(Geom::Point(sign*text_offset, 0)); + + point1 = make_stub_line_h(p1); + point2 = make_stub_line_h(p2); + + _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(point1, 0)); + _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(point2, 0)); + + auto line1 = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), p1, p2); + line1->set_stroke(color); + line1->set_width(2); + _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(line1, 0)); + + if (show_distance) { + auto text = new Inkscape::CanvasItemText(_desktop->getCanvasTemp(), text_pos, distance2); + text->set_fontsize(fontsize); + text->set_fill(text_fill); + text->set_background(text_bg); + text->set_bg_radius(DISTANCE_BG_RADIUS); + text->set_anchor({0.5, 0.5}); + _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(text, 0)); + } + } + + break; + } + } +} + +} //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/control/snap-indicator.h b/src/display/control/snap-indicator.h new file mode 100644 index 0000000..ca4721b --- /dev/null +++ b/src/display/control/snap-indicator.h @@ -0,0 +1,85 @@ +// 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 "snap-enums.h" +#include "snapped-point.h" +#include "display/control/canvas-item-curve.h" + +#include <glib.h> +#include <glibmm/i18n.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 *> _alignment_snap_indicators; + std::list<TemporaryItem *> _distribution_snap_indicators; + std::list<TemporaryItem *> _debugging_points; + bool _snaptarget_is_presnap; + SPDesktop *_desktop; + +private: + SnapIndicator(const SnapIndicator&) = delete; + SnapIndicator& operator=(const SnapIndicator&) = delete; + + void make_distribution_indicators(SnappedPoint const &p, double fontsize, double scale); + void make_alignment_indicator(Geom::Point const &p1, Geom::Point const &p2, guint32 color, double fontsize, double scale); + guint32 get_guide_color(SnapTargetType t); + Inkscape::CanvasItemCurve* make_stub_line_h(Geom::Point const &p); + Inkscape::CanvasItemCurve* make_stub_line_v(Geom::Point const &p); +}; + +} //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/curve.cpp b/src/display/curve.cpp new file mode 100644 index 0000000..a921cc1 --- /dev/null +++ b/src/display/curve.cpp @@ -0,0 +1,624 @@ +// 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> // g_error +#include <2geom/sbasis-geometric.h> +#include <2geom/sbasis-to-bezier.h> +#include <2geom/point.h> +#include "helper/geom.h" +#include "helper/geom-pathstroke.h" + +/** + * Routines for SPCurve and for its Geom::PathVector + */ + +SPCurve::SPCurve(Geom::Rect const &rect, bool all_four_sides) +{ + moveto(rect.corner(0)); + + for (int i = 3; i >= 1; --i) { + 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 + lineto(rect.corner(0)); + } else { + // ... instead of just three plus a closing segment + closepath(); + } +} + +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(); +} + +/** + * Returns a list of curves corresponding to the subpaths in \a curve. + */ +std::vector<SPCurve> SPCurve::split() const +{ + std::vector<SPCurve> result; + + for (auto const &path_it : _pathv) { + Geom::PathVector newpathv; + newpathv.push_back(path_it); + result.emplace_back(std::move(newpathv)); + } + + return result; +} + +/** + * Returns a list of curves of non-overlapping subpath in \a curve. + */ +std::vector<SPCurve> SPCurve::split_non_overlapping() const +{ + std::vector<SPCurve> result; + + auto curves = Inkscape::split_non_intersecting_paths(Geom::PathVector(_pathv)); + + for (auto &curve : curves) { + result.emplace_back(std::move(curve)); + } + + return result; +} + +/** + * Transform all paths in curve by matrix. + */ +void SPCurve::transform(Geom::Affine const &m) +{ + _pathv *= m; +} + +/** + * Return a copy of the curve with all paths transformed by matrix. + */ +SPCurve SPCurve::transformed(const Geom::Affine &m) const +{ + return SPCurve(_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() +{ + if (_pathv.empty()) { + g_message("%s - path is empty", __PRETTY_FUNCTION__); + } else { + _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 +{ + return get_segment_count() == 0; +} + +/** + * True iff all subpaths are closed. + * Returns false if the curve is empty. + */ +bool SPCurve::is_closed() const +{ + if (is_empty()) { + return false; + } + + for (auto const &it : _pathv) { + if (!it.closed()) { + return false; + } + } + + return true; +} + +/** + * True if both curves are equal + */ +bool SPCurve::is_equal(SPCurve const *other) const +{ + return other && _pathv == other->get_pathvector(); +} + +/** + * True if both curves are near + */ +bool SPCurve::is_similar(SPCurve const *other, double precision) const +{ + return other && pathv_similar(_pathv, other->get_pathvector(), precision); +} + +/** + * 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. + */ +std::optional<Geom::Point> SPCurve::first_point() const +{ + if (is_empty()) { + return {}; + } + + return _pathv.front().initialPoint(); +} + +/** + * 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. + */ +std::optional<Geom::Point> SPCurve::second_point() const +{ + if (is_empty()) { + return {}; + } + + if (_pathv.front().empty()) { + // first path is only a moveto + // check if there is second path + if (_pathv.size() > 1) { + return _pathv[1].initialPoint(); + } else { + return _pathv[0].initialPoint(); + } + } + + return _pathv.front()[0].finalPoint(); +} + +/** + * Return the second-last point of last subpath or first point when that last subpath has only a moveto. + */ +std::optional<Geom::Point> SPCurve::penultimate_point() const +{ + if (is_empty()) { + return {}; + } + + Geom::Path const &lastpath = _pathv.back(); + if (!lastpath.empty()) { + return lastpath.back_default().initialPoint(); + } else { + return lastpath.initialPoint(); + } +} + +/** + * 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. + */ +std::optional<Geom::Point> SPCurve::last_point() const +{ + if (is_empty()) { + return {}; + } + + return _pathv.back().finalPoint(); +} + +/** + * Reverse the direction of all paths. + */ +void SPCurve::reverse() +{ + _pathv.reverse(); +} + +/** + * Return a copy with the direction of all paths reversed. + */ +SPCurve SPCurve::reversed() const +{ + return SPCurve(_pathv.reversed()); +} + +/** + * 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) +{ + append(curve2._pathv, use_lineto); +} + +void SPCurve::append(Geom::PathVector const &pathv, bool use_lineto) +{ + if (pathv.empty()) { + return; + } + + if (use_lineto) { + auto it = 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 != pathv.end(); ++it) { + _pathv.push_back(*it); + } + } else { + for (auto const &it : 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 is empty, this curves path becomes the non-empty path. + * + * @param tolerance Tolerance for endpoint fusion (applied to x and y separately) + * @return False if one of the curves (this curve or the argument curve) is closed, true otherwise. + */ +bool SPCurve::append_continuous(SPCurve const &c1, double tolerance) +{ + if (is_closed() || c1.is_closed()) { + return false; + } + + if (c1.is_empty()) { + return true; + } + + if (this->is_empty()) { + _pathv = c1._pathv; + return true; + } + + if ((fabs(last_point()->x() - c1.first_point()->x()) <= tolerance) && + (fabs(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 true; +} + +/** + * Remove last segment of curve. + */ +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; + } + + auto const offset0 = new_p0 - *first_point(); + auto 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"); + } + arclength *= 1./arclength.lastValue(); + auto const A = offset0; + auto 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 (auto const &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) { + Geom::Curve const &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 (auto 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..9e99620 --- /dev/null +++ b/src/display/curve.h @@ -0,0 +1,107 @@ +// 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 <optional> +#include <vector> +#include <utility> + +/** + * Wrapper around a Geom::PathVector object. + */ +class SPCurve { +public: + explicit SPCurve() = default; + explicit SPCurve(Geom::PathVector pathv) : _pathv(std::move(pathv)) {} + explicit SPCurve(Geom::Rect const &rect, bool all_four_sides = false); + + void set_pathvector(Geom::PathVector const &new_pathv); + Geom::PathVector const &get_pathvector() 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 const *other) const; + bool is_similar(SPCurve const *other, double precision = 0.001) 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; + std::optional<Geom::Point> first_point() const; + std::optional<Geom::Point> last_point() const; + std::optional<Geom::Point> second_point() const; + std::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); + SPCurve transformed(Geom::Affine const &m) const; + 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(Geom::PathVector const &, bool use_lineto = false); + void append(SPCurve const &curve2, bool use_lineto = false); + bool append_continuous(SPCurve const &c1, double tolerance = 0.0625); + + void reverse(); + SPCurve reversed() const; + + std::vector<SPCurve> split() const; + std::vector<SPCurve> split_non_overlapping() const; + + template <typename T> + static std::optional<SPCurve> ptr_to_opt(T const &p) + { + if (p) { + return *p; + } else { + return {}; + } + } + +protected: + 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/dither-lock.h b/src/display/dither-lock.h new file mode 100644 index 0000000..975b91f --- /dev/null +++ b/src/display/dither-lock.h @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_DISPLAY_DITHER_LOCK_H +#define INKSCAPE_DISPLAY_DITHER_LOCK_H + +#include "drawing-context.h" +#include "cairo-utils.h" + +namespace Inkscape { + +/// RAII object for temporarily turning dithering on. +class DitherLock +{ +public: + DitherLock(DrawingContext &dc, bool on) + : _cr(dc.rawTarget()) + , _on(on) + { + if (_on) { + ink_cairo_set_dither(_cr, true); + } + } + + ~DitherLock() + { + if (_on) { + ink_cairo_set_dither(_cr, false); + } + } + +private: + cairo_surface_t *_cr; + bool _on; +}; + +} // namespace Inkscape + +#endif // INKSCAPE_DISPLAY_DITHER_LOCK_H diff --git a/src/display/drawing-context.cpp b/src/display/drawing-context.cpp new file mode 100644 index 0000000..516202b --- /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::setHairline() { + ink_cairo_set_hairline(_ct); +} + +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]); +} + +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..ec488ac --- /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 setHairline(); + 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 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..12cbf29 --- /dev/null +++ b/src/display/drawing-group.cpp @@ -0,0 +1,131 @@ +// 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 "drawing-group.h" +#include "cairo-utils.h" +#include "drawing-context.h" +#include "drawing-surface.h" +#include "drawing-text.h" +#include "drawing.h" +#include "style.h" + +namespace Inkscape { + +DrawingGroup::DrawingGroup(Drawing &drawing) + : DrawingItem(drawing) {} + +/** + * Set whether the group returns children from pick calls. + * Previously this feature was called "transparent groups". + */ +void DrawingGroup::setPickChildren(bool pick_children) +{ + defer([=] { + _pick_children = pick_children; + }); +} + +/** + * 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 &transform) +{ + defer([=] { + auto constexpr EPS = 1e-18; + auto current = _child_transform ? *_child_transform : Geom::identity(); + if (Geom::are_near(transform, current, EPS)) return; + _markForRendering(); + _child_transform = transform.isIdentity(EPS) ? nullptr : std::make_unique<Geom::Affine>(transform); + _markForUpdate(STATE_ALL, true); + }); +} + +unsigned DrawingGroup::_updateItem(Geom::IntRect const &area, UpdateContext const &ctx, unsigned flags, unsigned reset) +{ + bool outline = _drawing.renderMode() == RenderMode::OUTLINE || _drawing.outlineOverlay(); + + UpdateContext child_ctx(ctx); + if (_child_transform) { + child_ctx.ctm = *_child_transform * ctx.ctm; + } + + _bbox = {}; + + for (auto &c : _children) { + c.update(area, child_ctx, flags, reset); + if (c.visible()) { + _bbox.unionWith(outline ? c.bbox() : c.drawbox()); + } + _update_complexity += c.getUpdateComplexity(); + _contains_unisolated_blend |= c.unisolatedBlend(); + } + + return STATE_ALL; +} + +unsigned DrawingGroup::_renderItem(DrawingContext &dc, RenderContext &rc, Geom::IntRect const &area, unsigned flags, DrawingItem const *stop_at) const +{ + if (!stop_at) { + // normal rendering + for (auto &i : _children) { + i.render(dc, rc, 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.render(dc, rc, area, flags | RENDER_FILTER_BACKGROUND, stop_at); + return RENDER_OK; + } else { + i.render(dc, rc, area, flags, stop_at); + } + } + } + return RENDER_OK; +} + +void DrawingGroup::_clipItem(DrawingContext &dc, RenderContext &rc, Geom::IntRect const &area) const +{ + for (auto &i : _children) { + i.clip(dc, rc, 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; +} + +} // 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..2c7aab2 --- /dev/null +++ b/src/display/drawing-group.h @@ -0,0 +1,57 @@ +// 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 INKSCAPE_DISPLAY_DRAWING_GROUP_H +#define INKSCAPE_DISPLAY_DRAWING_GROUP_H + +#include "display/drawing-item.h" + +namespace Inkscape { + +class DrawingGroup + : public DrawingItem +{ +public: + DrawingGroup(Drawing &drawing); + int tag() const override { return tag_of<decltype(*this)>; } + + bool pickChildren() { return _pick_children; } + void setPickChildren(bool); + + void setChildTransform(Geom::Affine const &); + +protected: + ~DrawingGroup() override = default; + + unsigned _updateItem(Geom::IntRect const &area, UpdateContext const &ctx, unsigned flags, unsigned reset) override; + unsigned _renderItem(DrawingContext &dc, RenderContext &rc, Geom::IntRect const &area, unsigned flags, DrawingItem const *stop_at) const override; + void _clipItem(DrawingContext &dc, RenderContext &rc, Geom::IntRect const &area) const override; + DrawingItem *_pickItem(Geom::Point const &p, double delta, unsigned flags) override; + bool _canClip() const override { return true; } + + std::unique_ptr<Geom::Affine> _child_transform; +}; + +} // namespace Inkscape + +#endif // INKSCAPE_DISPLAY_DRAWING_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: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..e82c8d0 --- /dev/null +++ b/src/display/drawing-image.cpp @@ -0,0 +1,270 @@ +// 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 "drawing.h" +#include "drawing-context.h" +#include "drawing-image.h" +#include "cairo-utils.h" +#include "cairo-templates.h" + +namespace Inkscape { + +DrawingImage::DrawingImage(Drawing &drawing) + : DrawingItem(drawing) + , style_image_rendering(SP_CSS_IMAGE_RENDERING_AUTO) +{ +} + +void DrawingImage::setPixbuf(std::shared_ptr<Inkscape::Pixbuf const> pixbuf) +{ + defer([this, pixbuf = std::move(pixbuf)] () mutable { + _pixbuf = std::move(pixbuf); + _markForUpdate(STATE_ALL, false); + }); +} + +void DrawingImage::setScale(double sx, double sy) +{ + defer([=] { + _scale = Geom::Scale(sx, sy); + _markForUpdate(STATE_ALL, false); + }); +} + +void DrawingImage::setOrigin(Geom::Point const &origin) +{ + defer([=] { + _origin = origin; + _markForUpdate(STATE_ALL, false); + }); +} + +void DrawingImage::setClipbox(Geom::Rect const &box) +{ + defer([=] { + _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; +} + +void DrawingImage::setStyle(SPStyle const *style, SPStyle const *context_style) +{ + DrawingItem::setStyle(style, context_style); + + auto image_rendering = SP_CSS_IMAGE_RENDERING_AUTO; + if (_style) { + image_rendering = _style->image_rendering.computed; + } + + defer([=] { + style_image_rendering = image_rendering; + }); +} + +unsigned DrawingImage::_updateItem(Geom::IntRect const &, UpdateContext const &, unsigned, unsigned) +{ + // Calculate bbox + if (_pixbuf) { + Geom::Rect r = bounds() * _ctm; + _bbox = r.roundOutwards(); + } else { + _bbox = Geom::OptIntRect(); + } + + return STATE_ALL; +} + +unsigned DrawingImage::_renderItem(DrawingContext &dc, RenderContext &rc, Geom::IntRect const &/*area*/, unsigned flags, DrawingItem const */*stop_at*/) const +{ + bool const outline = (flags & RENDER_OUTLINE) && !_drawing.imageOutlineMode(); + + if (!outline) { + 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); + // const_cast required since Cairo needs to modify the internal refcount variable, but we do not want to give up the + // benefits of const for the rest of our code. The underlying object is guaranteed to be non-const, so this is well-defined. + // It is also thread-safe to modify the refcount in this way, since Cairo uses atomics internally. + dc.setSource(const_cast<cairo_surface_t*>(_pixbuf->getSurfaceRaw()), 0, 0); + dc.patternSetExtend(CAIRO_EXTEND_PAD); + + // 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) { + 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; + } + + // Handle an exceptional case where the greyscale color mode needs to be applied per-image. + bool const greyscale_exception = (flags & RENDER_OUTLINE) && _drawing.colorMode() == ColorMode::GRAYSCALE; + if (greyscale_exception) { + dc.pushGroup(); + } + + dc.paint(); + + if (greyscale_exception) { + ink_cairo_surface_filter(dc.rawTarget(), dc.rawTarget(), _drawing.grayscaleMatrix()); + dc.popGroupToSource(); + dc.paint(); + } + + } else { // outline; draw a rect instead + + auto rgba = _drawing.imageOutlineColor(); + + { 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 flags) +{ + if (!_pixbuf) return nullptr; + + bool outline = (flags & PICK_OUTLINE) && !_drawing.imageOutlineMode(); + + 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 { + auto 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; + + auto 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; + } +} + +} // 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..94a8c94 --- /dev/null +++ b/src/display/drawing-image.h @@ -0,0 +1,71 @@ +// 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 INKSCAPE_DISPLAY_DRAWING_IMAGE_H +#define INKSCAPE_DISPLAY_DRAWING_IMAGE_H + +#include <memory> +#include <2geom/transforms.h> +#include <gdk-pixbuf/gdk-pixbuf.h> +#include <cairo.h> + +#include "display/drawing-item.h" + +namespace Inkscape { +class Pixbuf; + +class DrawingImage + : public DrawingItem +{ +public: + DrawingImage(Drawing &drawing); + int tag() const override { return tag_of<decltype(*this)>; } + + void setStyle(SPStyle const *style, SPStyle const *context_style = nullptr) override; + + void setPixbuf(std::shared_ptr<Inkscape::Pixbuf const> pb); + void setScale(double sx, double sy); + void setOrigin(Geom::Point const &o); + void setClipbox(Geom::Rect const &box); + Geom::Rect bounds() const; + +protected: + ~DrawingImage() override = default; + + unsigned _updateItem(Geom::IntRect const &area, UpdateContext const &ctx, unsigned flags, unsigned reset) override; + unsigned _renderItem(DrawingContext &dc, RenderContext &rc, Geom::IntRect const &area, unsigned flags, DrawingItem const *stop_at) const override; + DrawingItem *_pickItem(Geom::Point const &p, double delta, unsigned flags) override; + + std::shared_ptr<Inkscape::Pixbuf const> _pixbuf; + + SPImageRendering style_image_rendering; + + // 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; +}; + +} // namespace Inkscape + +#endif // INKSCAPE_DISPLAY_DRAWING_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/drawing-item-ptr.h b/src/display/drawing-item-ptr.h new file mode 100644 index 0000000..0707567 --- /dev/null +++ b/src/display/drawing-item-ptr.h @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_DISPLAY_DRAWINGITEM_PTR_H +#define INKSCAPE_DISPLAY_DRAWINGITEM_PTR_H + +#include <memory> +#include <type_traits> + +namespace Inkscape { class DrawingItem; } + +/** + * Deleter object which calls the unlink() method of DrawingItem to schedule deferred destruction. + */ +struct UnlinkDeleter +{ + template <typename T> + void operator()(T *t) + { + static_assert(std::is_base_of_v<Inkscape::DrawingItem, T>); + t->unlink(); + } +}; + +/** + * Smart pointer used by the Object Tree to hold items in the Display Tree, like std::unique_ptr. + * + * Upon deletion, the pointed-to object and its subtree will be destroyed immediately if not currently in use by a snapshot. + * Otherwise, destruction is deferred to after the snapshot is released. + */ +template <typename T> +using DrawingItemPtr = std::unique_ptr<T, UnlinkDeleter>; + +/** + * Convienence function to create a DrawingItemPtr, like std::make_unique. + */ +template <typename T, typename... Args> +auto make_drawingitem(Args&&... args) +{ + return DrawingItemPtr<T>(new T(std::forward<Args>(args)...)); +}; + +#endif // INKSCAPE_DISPLAY_DRAWINGITEM_PTR_H diff --git a/src/display/drawing-item.cpp b/src/display/drawing-item.cpp new file mode 100644 index 0000000..57e658b --- /dev/null +++ b/src/display/drawing-item.cpp @@ -0,0 +1,1325 @@ +// 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 "display/cairo-utils.h" +#include "display/cairo-templates.h" + +#include "display/control/canvas-item-drawing.h" +#include "ui/widget/canvas.h" // Mark area for redrawing. + +#include "nr-filter.h" +#include "style.h" + +#include "object/sp-item.h" + +static constexpr auto CACHE_SCORE_THRESHOLD = 50000.0; ///< Do not consider objects for caching below this score. + +namespace Inkscape { + +struct CacheData +{ + mutable std::mutex mutables; + mutable std::optional<DrawingCache> surface; +}; + +/** + * @class DrawingItem + * SVG drawing item for display. + * + * This class represents the renderable portion of the SVG document. Typically this + * is created by the SP tree, in particular the invoke_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 invoke_show() + * - this will cause dangling pointers inside the SPItem and lead to a crash. + * Use the corresponding invoke_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) + , _contains_unisolated_blend(false) + , style_vector_effect_size(false) + , style_vector_effect_rotate(false) + , style_vector_effect_fixed(false) + , _opacity(1.0) + , _clip(nullptr) + , _mask(nullptr) + , _fill_pattern(nullptr) + , _stroke_pattern(nullptr) + , _item(nullptr) + , _state(0) + , _child_type(ChildType::ORPHAN) + , _background_new(0) + , _background_accumulate(0) + , _visible(true) + , _sensitive(true) + , _cached_persistent(0) + , _has_cache_iterator(0) + , _propagate_state(0) + , _pick_children(0) + , _antialias(2) + , _isolation(SP_CSS_ISOLATION_AUTO) + , _blend_mode(SP_CSS_BLEND_NORMAL) +{ +} + +DrawingItem::~DrawingItem() +{ + // Unactivate if active. + if (auto itemdrawing = _drawing.getCanvasItemDrawing()) { + if (itemdrawing->get_active() == this) { + itemdrawing->set_active(nullptr); + } + } else { + // Typically happens, e.g. for any non-Canvas Drawing. + } + + // Remove caching candidate entry. + if (_has_cache_iterator) { + _drawing._candidate_items.erase(_cache_iterator); + } + + // Remove from the set of cached items and delete cache. + _setCached(false, true); + + _children.clear_and_dispose([] (auto c) { delete c; }); + delete _clip; + delete _mask; + delete static_cast<DrawingItem*>(_fill_pattern); + delete static_cast<DrawingItem*>(_stroke_pattern); +} + +/// Returns true if item is among the descendants. Will return false if item == this. +bool DrawingItem::isAncestorOf(DrawingItem const *item) const +{ + for (auto c = item->_parent; c; c = c->_parent) { + if (c == this) return true; + } + return false; +} + +bool DrawingItem::unisolatedBlend() const +{ + if (_blend_mode != SP_CSS_BLEND_NORMAL) { + return true; + } else if (_mask || _filter || _opacity < 0.995 || _isolation == SP_CSS_ISOLATION_ISOLATE) { + return false; + } else { + return _contains_unisolated_blend; + } +} + +void DrawingItem::appendChild(DrawingItem *item) +{ + // Ok to perform non-deferred modification of child, because not part of rendering tree yet. + assert(item->_child_type == ChildType::ORPHAN); + item->_parent = this; + item->_child_type = ChildType::NORMAL; + + defer([=] { + _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) +{ + // See appendChild for explanations. + assert(item->_child_type == ChildType::ORPHAN); + item->_parent = this; + item->_child_type = ChildType::NORMAL; + + defer([=] { + _children.push_front(*item); + item->_state = STATE_ALL; + item->_markForUpdate(STATE_ALL, true); + }); +} + +// Clear this node's ordinary children, deleting them and their descendants without otherwise changing them in any way. +void DrawingItem::clearChildren() +{ + defer([=] { + if (_children.empty()) return; + _markForRendering(); + _children.clear_and_dispose([] (auto c) { delete c; }); + _markForUpdate(STATE_ALL, false); + }); +} + +void DrawingItem::setTransform(Geom::Affine const &transform) +{ + defer([=] { + auto constexpr EPS = 1e-18; + auto current = _transform ? *_transform : Geom::identity(); + if (Geom::are_near(transform, current, EPS)) return; + + _markForRendering(); + _transform = transform.isIdentity(EPS) ? nullptr : std::make_unique<Geom::Affine>(transform); + _markForUpdate(STATE_ALL, true); + }); +} + +void DrawingItem::setOpacity(float opacity) +{ + defer([=] { + if (opacity == _opacity) return; + _opacity = opacity; + _markForRendering(); + }); +} + +void DrawingItem::setAntialiasing(unsigned antialias) +{ + defer([=] { + if (_antialias == antialias) return; + _antialias = antialias; + _markForRendering(); + }); +} + +void DrawingItem::setIsolation(bool isolation) +{ + defer([=] { + if (isolation == _isolation) return; + _isolation = isolation; + _markForRendering(); + }); +} + +void DrawingItem::setBlendMode(SPBlendMode blend_mode) +{ + defer([=] { + if (blend_mode == _blend_mode) return; + _blend_mode = blend_mode; + _markForRendering(); + }); +} + +void DrawingItem::setVisible(bool visible) +{ + defer([=] { + if (visible == _visible) return; + _visible = visible; + _markForRendering(); + }); +} + +void DrawingItem::setSensitive(bool sensitive) +{ + defer([=] { // Must be deferred, since in bitfield. + _sensitive = sensitive; + }); +} + +/** + * 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 bool const cache_env = getenv("_INKSCAPE_DISABLE_CACHE"); + if (cache_env) { + return; + } + + if (persistent) { + _cached_persistent = cached && persistent; + } else if (_cached_persistent) { + return; + } + + if (cached == (bool)_cache) { + return; + } + + if (cached) { + _cache = std::make_unique<CacheData>(); + _drawing._cached_items.insert(this); + } else { + _cache.reset(); + _drawing._cached_items.erase(this); + } +} + +/** + * Process information related to the new style. + * + * Note: _style is not used by DrawingGlyphs which uses its parent style. + */ +void DrawingItem::setStyle(SPStyle const *style, SPStyle const *context_style) +{ + // Ok to not defer setting the style pointer, because the pointer itself is only read by SPObject-side code. + _style = style; + if (context_style) { + _context_style = context_style; + } else if (_parent) { + _context_style = _parent->_context_style; + } + + // Copy required information out of style. + bool background_new = false; + bool vector_effect_size = false; + bool vector_effect_rotate = false; + bool vector_effect_fixed = false; + if (style) { + background_new = style->enable_background.set && style->enable_background.value == SP_CSS_BACKGROUND_NEW; + vector_effect_size = _style->vector_effect.size; + vector_effect_rotate = _style->vector_effect.rotate; + vector_effect_fixed = _style->vector_effect.fixed; + } + + // Defer setting the style information on the DrawingItem. + defer([=] { + _markForRendering(); + + if (background_new != _background_new) { + _background_new = background_new; + _markForUpdate(STATE_BACKGROUND, true); + } + + style_vector_effect_size = vector_effect_size; + style_vector_effect_rotate = vector_effect_rotate; + style_vector_effect_fixed = vector_effect_fixed; + + _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 const *context_style) +{ + _context_style = context_style; + for (auto &i : _children) { + i.setChildrenStyle(context_style); + } +} + +void DrawingItem::setClip(DrawingItem *item) +{ + if (item) { + assert(item->_child_type == ChildType::ORPHAN); + item->_parent = this; + item->_child_type = ChildType::CLIP; + } + + defer([=] { + _markForRendering(); + delete _clip; + _clip = item; + _markForUpdate(STATE_ALL, true); + }); +} + +void DrawingItem::setMask(DrawingItem *item) +{ + if (item) { + assert(item->_child_type == ChildType::ORPHAN); + item->_parent = this; + item->_child_type = ChildType::MASK; + } + + defer([=] { + _markForRendering(); + delete _mask; + _mask = item; + _markForUpdate(STATE_ALL, true); + }); +} + +void DrawingItem::setFillPattern(DrawingPattern *pattern) +{ + if (pattern) { + assert(pattern->_child_type == ChildType::ORPHAN); + pattern->_parent = this; + pattern->_child_type = ChildType::FILL; + } + + defer([=] { + _markForRendering(); + delete static_cast<DrawingItem*>(_fill_pattern); + _fill_pattern = pattern; + _markForUpdate(STATE_ALL, false); + }); +} + +void DrawingItem::setStrokePattern(DrawingPattern *pattern) +{ + if (pattern) { + assert(pattern->_child_type == ChildType::ORPHAN); + pattern->_parent = this; + pattern->_child_type = ChildType::STROKE; + } + + defer([=] { + _markForRendering(); + delete static_cast<DrawingItem*>(_stroke_pattern); + _stroke_pattern = pattern; + _markForUpdate(STATE_ALL, false); + }); +} + +/// Move this item to the given place in the Z order of siblings. Does nothing if the item is not a normal child. +void DrawingItem::setZOrder(unsigned zorder) +{ + if (_child_type != ChildType::NORMAL) return; + + defer([=] { + auto it = _parent->_children.iterator_to(*this); + _parent->_children.erase(it); + + auto it2 = _parent->_children.begin(); + std::advance(it2, std::min<unsigned>(zorder, _parent->_children.size())); + _parent->_children.insert(it2, *this); + _markForRendering(); + }); +} + +void DrawingItem::setItemBounds(Geom::OptRect const &bounds) +{ + defer([=] { + _item_bbox = bounds; + }); +} + +void DrawingItem::setFilterRenderer(std::unique_ptr<Filters::Filter> filter) +{ + defer([=, filter = std::move(filter)] () mutable { + _filter = std::move(filter); + _markForRendering(); + }); +} + +/** + * 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) +{ + // We don't need to update what is not visible + if (!_visible) { + _state = STATE_ALL; // Touch the state for future change to this item + return; + } + + bool const outline = _drawing.renderMode() == RenderMode::OUTLINE || _drawing.outlineOverlay(); + bool const filters = _drawing.renderMode() != RenderMode::NO_FILTERS; + bool const forcecache = _filter && filters; + + // 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 == ChildType::NORMAL && _parent->_background_accumulate) + _background_accumulate = true; + } + + UpdateContext child_ctx(ctx); + if (_transform) { + child_ctx.ctm = *_transform * ctx.ctm; + } + + // Vector effects + if (style_vector_effect_fixed) { + child_ctx.ctm.setTranslation(Geom::Point(0, 0)); + } + + if (style_vector_effect_size) { + double value = child_ctx.ctm.descrim(); + if (value > 0.0) { + child_ctx.ctm[0] /= value; + child_ctx.ctm[1] /= value; + child_ctx.ctm[2] /= value; + child_ctx.ctm[3] /= value; + } + } + + if (style_vector_effect_rotate) { + double value = child_ctx.ctm.descrim(); + child_ctx.ctm[0] = value; + child_ctx.ctm[1] = 0.0; + child_ctx.ctm[2] = 0.0; + child_ctx.ctm[3] = value; + } + + // Remember the transformation matrix. + Geom::Affine ctm_change; + bool affine_changed = false; + if (!Geom::are_near(_ctm, child_ctx.ctm)) { + ctm_change = _ctm.inverse() * child_ctx.ctm; + affine_changed = true; + } + _ctm = child_ctx.ctm; + + bool const totally_invalidated = reset & STATE_TOTAL_INV; + if (totally_invalidated) { + // Perform work that would have been done by our call to _markForRendering(), + // had it not been overshadowed by a totally-invalidating node. + if (_cache && _cache->surface) { + _cache->surface->markDirty(); + } + _dropPatternCache(); + } + + // Decide whether this node should be a totally-invalidating node. + bool const totally_invalidate = _update_complexity >= 20 && affine_changed; + if (totally_invalidate) { + reset |= STATE_TOTAL_INV; + } + + // Recalculate update complexity; to be recalculated immediately below and by _updateItem(). + _update_complexity = 1; + auto add_complexity_if = [&] (DrawingItem *c) { + if (c) { + _update_complexity += c->_update_complexity; + } + }; + add_complexity_if(_clip); + add_complexity_if(_mask); + add_complexity_if(_fill_pattern); + add_complexity_if(_stroke_pattern); + + // Reset contains_unisolated_blend; to be recalculated by _updateItem(). + _contains_unisolated_blend = false; + + // Moved from code that was previously in render(). + if (forcecache) { + _setCached((bool)_cacheRect(), true); + } + + // update _bbox and call this function for children + _state = _updateItem(area, child_ctx, flags, reset); + + // update drawingitems contained in filter + if (_filter) { + _filter->update(); + } + + if (to_update & STATE_BBOX) { + // compute drawbox + if (_filter && 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); + } + } + // Crude fix for outline overlay bbox issues with filtered objects. + // (Real solution is to carefully review all bbox/drawbox uses.) + if (_drawing.outlineOverlay()) { + _bbox |= _drawbox; + } + } + if (to_update & STATE_CACHE) { + // Remove old cache iterator. + if (_has_cache_iterator) { + _drawing._candidate_items.erase(_cache_iterator); + _has_cache_iterator = false; + } + + // Determine whether this item is cachable. + bool isolated = _mask || _filter || _opacity < 0.995 + || _blend_mode != SP_CSS_BLEND_NORMAL + || _isolation == SP_CSS_ISOLATION_ISOLATE + || _child_type == ChildType::ROOT; + bool cacheable = !_contains_unisolated_blend || isolated; + + // Determine whether to make this item eligible for caching, by creating a cache iterator. + double score = _cacheScore(); + if (score >= CACHE_SCORE_THRESHOLD && cacheable) { + 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; + auto it = std::lower_bound(_drawing._candidate_items.begin(), _drawing._candidate_items.end(), cr, std::greater<CacheRecord>()); + _cache_iterator = _drawing._candidate_items.insert(it, cr); + _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 temporarily + * using more memory than the cache budget */ + if (_cache && _cache->surface) { + Geom::OptIntRect cl = _cacheRect(); + if (_visible && cl && _has_cache_iterator) { // never create cache for invisible items + // this takes care of invalidation on transform + _cache->surface->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 (!totally_invalidated) { + if (!is<DrawingGroup>(this) || (_filter && filters) || totally_invalidate) { + _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, RenderContext &rc, Geom::IntRect const &area, unsigned flags, DrawingItem const *stop_at) const +{ + bool const outline = flags & RENDER_OUTLINE; + bool const render_filters = !(flags & RENDER_NO_FILTERS); + bool const forcecache = _filter && render_filters; + + // 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, rc, area, flags); + return RENDER_OK; + } + + Geom::OptIntRect carea = area & _drawbox; + if (!carea) { + return RENDER_OK; + } + + Geom::OptIntRect iarea = carea; + // expand carea to contain the dependent area of filters. + if (forcecache) { + iarea = _cacheRect(); + if (!iarea) { + iarea = carea; + _filter->area_enlarge(*iarea, this); + iarea.intersectWith(_drawbox); + } + } + // carea is the area to paint + carea = iarea & _drawbox; + if (!carea) { + return RENDER_OK; + } + + // Device scale for HiDPI screens (typically 1 or 2) + int const device_scale = dc.surface()->device_scale(); + + std::unique_lock<std::mutex> lock; + + // Render from cache if possible, unless requested not to (hatches). + if (_cache && !(flags & RENDER_BYPASS_CACHE)) { + lock = std::unique_lock(_cache->mutables); + + if (_cache->surface) { + if (_cache->surface->device_scale() != device_scale) { + _cache->surface->markDirty(); + } + _cache->surface->prepare(); + dc.setOperator(ink_css_blend_to_cairo_operator(_blend_mode)); + _cache->surface->paintFromCache(dc, carea, forcecache); + 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. + Geom::OptIntRect cl = _cacheRect(); + if (!cl) + cl = carea; + _cache->surface.emplace(*cl, device_scale); + } + + if (!forcecache) { + lock.unlock(); // Only hold the lock for the full duration of rendering for filters. + } + } 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 const greyscale = _drawing.colorMode() == ColorMode::GRAYSCALE && !(flags & RENDER_OUTLINE); + bool const isolate_root = _contains_unisolated_blend || greyscale; + bool const needs_intermediate_rendering = + _clip // 1. it has a clipping path + || _mask // 2. it has a mask + || (_filter && render_filters) // 3. it has a filter + || _opacity < 0.995 // 4. it is non-opaque + || _blend_mode != SP_CSS_BLEND_NORMAL // 5. it has blend mode + || _isolation == SP_CSS_ISOLATION_ISOLATE // 6. it is isolated + || (_child_type == ChildType::ROOT && isolate_root) // 7. it is the root and needs isolation + || (bool)_cache; // 8. 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, rc, *carea, flags & ~RENDER_FILTER_BACKGROUND, stop_at); + } + + DrawingSurface intermediate(*carea, device_scale); + DrawingContext ict(intermediate); + cairo_set_antialias(ict.raw(), cairo_get_antialias(dc.raw())); // propagate antialias setting + + // 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, rc, *carea); + 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, rc, *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, rc, *carea, flags, stop_at); + + // 4. Apply filter. + if (_filter && render_filters) { + bool rendered = false; + if (_filter->uses_background() && _background_accumulate) { + auto bg_root = this; + for (; bg_root; bg_root = bg_root->_parent) { + if (bg_root->_background_new || bg_root->_filter) break; + } + if (bg_root) { + DrawingSurface bg(*carea, device_scale); + DrawingContext bgdc(bg); + bg_root->render(bgdc, rc, *carea, flags | RENDER_FILTER_BACKGROUND, this); + _filter->render(this, ict, &bgdc, rc); + rendered = true; + } + } + if (!rendered) { + _filter->render(this, ict, nullptr, rc); + } + // 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(). + } + + // 4b. Apply greyscale rendering mode, if root node. + if (greyscale && _child_type == ChildType::ROOT) { + ink_cairo_surface_filter(ict.rawTarget(), ict.rawTarget(), _drawing.grayscaleMatrix()); + } + + // 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 (_cache && !(flags & RENDER_BYPASS_CACHE)) { + if (!forcecache) { + lock.lock(); // Only hold the lock for the full duration of rendering for filters. + } + assert(lock); + assert(_cache->surface); + + auto cachect = DrawingContext(*_cache->surface); + cachect.rectangle(*carea); + cachect.setOperator(CAIRO_OPERATOR_SOURCE); + cachect.setSource(&intermediate); + cachect.fill(); + _cache->surface->markClean(*carea); + } + + dc.rectangle(*carea); + dc.setSource(&intermediate); + + // 7. Render blend mode + dc.setOperator(ink_css_blend_to_cairo_operator(_blend_mode)); + dc.fill(); + dc.setSource(0,0,0,0); + // Web isolation only works if parent doesn't have transform + + // the call above is to clear a ref on the intermediate surface held by dc + + return render_result; +} + +/** + * A stand alone render, ignoring all other objects in the document. + */ +unsigned DrawingItem::render(DrawingContext &dc, Geom::IntRect const &area, unsigned flags) const +{ + auto rc = RenderContext{ 0xff }; // black outlines + return render(dc, rc, area, flags); +} + +void DrawingItem::_renderOutline(DrawingContext &dc, RenderContext &rc, Geom::IntRect const &area, unsigned flags) const +{ + // intersect with bbox rather than drawbox, as we want to render things outside + // of the clipping path as well + auto carea = Geom::intersect(area, _bbox); + if (!carea) return; + + // just render everything: item, clip, mask + // First, render the object itself + _renderItem(dc, rc, *carea, flags, nullptr); + + // render clip and mask, if any + auto saved_rgba = rc.outline_color; // save current outline color + // render clippath as an object, using a different color + if (_clip) { + rc.outline_color = _drawing.clipOutlineColor(); + _clip->render(dc, rc, *carea, flags); + } + // render mask as an object, using a different color + if (_mask) { + rc.outline_color = _drawing.maskOutlineColor(); + _mask->render(dc, rc, *carea, flags); + } + rc.outline_color = 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(DrawingContext &dc, Inkscape::RenderContext &rc, Geom::IntRect const &area) const +{ + // 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, rc, 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, rc, 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 = flags & PICK_OUTLINE; + + if (!outline) { + // 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); + auto dglyps = cast<DrawingGlyphs>(this); + if (dglyps && !(flags & PICK_AS_CLIP)) { + expanded = dglyps->getPickBox(); + } + + if (expanded.contains(p)) { + return _pickItem(p, delta, flags); + } + return nullptr; +} + +// For debugging +Glib::ustring DrawingItem::name() const +{ + 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) const +{ + if (level == 0) { + std::cout << "Display Item Tree" << std::endl; + } + std::cout << "DI: "; + for (int 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() +{ + bool outline = _drawing.renderMode() == RenderMode::OUTLINE || _drawing.outlineOverlay(); + Geom::OptIntRect dirty = outline ? _bbox : _drawbox; + if (!dirty) return; + + // dirty the caches of all parents + DrawingItem *bkg_root = nullptr; + + for (auto i = this; i; i = i->_parent) { + if (i != this && i->_filter) { + i->_filter->area_enlarge(*dirty, i); + } + if (i->_cache && i->_cache->surface) { + i->_cache->surface->markDirty(*dirty); + } + i->_dropPatternCache(); + if (i->_background_accumulate) { + bkg_root = i; + } + } + + if (bkg_root && bkg_root->_parent && bkg_root->_parent->_parent) { + bkg_root->_invalidateFilterBackground(*dirty); + } + + if (auto canvasitem = drawing().getCanvasItemDrawing()) { + canvasitem->get_canvas()->redraw_area(*dirty); + } +} + +void DrawingItem::_invalidateFilterBackground(Geom::IntRect const &area) +{ + if (!_drawbox.intersects(area)) return; + + if (_cache && _cache->surface && _filter && _filter->uses_background()) { + _cache->surface->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. + if (drawing().getCanvasItemDrawing()) { + drawing().getCanvasItemDrawing()->request_update(); + } else { + // Typically happens, e.g. for any non-Canvas Drawing. + } + } + } +} + +/** + * 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.renderMode() != RenderMode::NO_FILTERS) { + 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() const +{ + Geom::OptIntRect r = _drawbox & _drawing.cacheLimit(); + if (_filter && _drawing.cacheLimit() && _drawing.renderMode() != RenderMode::NO_FILTERS && r && r != _drawbox) { + // we check unfiltered item is enough inside the cache area to render properly + Geom::OptIntRect canvas = r; + expandByScale(*canvas, 0.5); + Geom::OptIntRect valid = Geom::intersect(canvas, _bbox); + if (!valid && _bbox) { + valid = _bbox; + // contract the item _bbox to get reduced size to render. $ seems good enough + 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; +} + +void apply_antialias(DrawingContext &dc, int antialias) +{ + 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: + g_assert_not_reached(); + } +} + +// Remove this node from its parent, then delete it. +void DrawingItem::unlink() +{ + defer([=] { + // This only happens for the top-level deleted item. + if (_parent) { + _markForRendering(); + } + + switch (_child_type) { + case ChildType::NORMAL: { + auto it = _parent->_children.iterator_to(*this); + _parent->_children.erase(it); + break; + } + case ChildType::CLIP: + _parent->_clip = nullptr; + break; + case ChildType::MASK: + _parent->_mask = nullptr; + break; + case ChildType::FILL: + _parent->_fill_pattern = nullptr; + break; + case ChildType::STROKE: + _parent->_stroke_pattern = nullptr; + break; + case ChildType::ROOT: + _drawing._root = nullptr; + break; + default: + break; + } + + if (_parent) { + bool propagate = _child_type == ChildType::CLIP || _child_type == ChildType::MASK; + _parent->_markForUpdate(STATE_ALL, propagate); + } + + delete this; + }); +} + +} // 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..a003444 --- /dev/null +++ b/src/display/drawing-item.h @@ -0,0 +1,276 @@ +// 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 INKSCAPE_DISPLAY_DRAWING_ITEM_H +#define INKSCAPE_DISPLAY_DRAWING_ITEM_H + +#include <memory> +#include <list> +#include <exception> + +#include <boost/operators.hpp> +#include <boost/utility.hpp> +#include <boost/intrusive/list.hpp> + +#include <2geom/rect.h> +#include <2geom/affine.h> + +#include "style-enums.h" +#include "tags.h" + +namespace Glib { class ustring; } +class SPStyle; +class SPItem; + +namespace Inkscape { + +class Drawing; +class DrawingCache; +class DrawingItem; +class DrawingPattern; +class DrawingContext; +namespace Filters { class Filter; } + +struct RenderContext +{ + uint32_t outline_color; +}; + +struct UpdateContext +{ + Geom::Affine ctm; +}; + +struct CacheData; + +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; +}; +using CacheList = std::list<CacheRecord>; + +struct InvalidItemException : std::exception +{ + char const *what() const noexcept override { return "Invalid item in drawing"; } +}; + +class DrawingItem +{ +public: + enum RenderFlags + { + RENDER_DEFAULT = 0, + RENDER_CACHE_ONLY = 1 << 0, + RENDER_BYPASS_CACHE = 1 << 1, + RENDER_FILTER_BACKGROUND = 1 << 2, + RENDER_OUTLINE = 1 << 3, + RENDER_NO_FILTERS = 1 << 4, + RENDER_VISIBLE_HAIRLINES = 1 << 5 + }; + 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, + STATE_TOTAL_INV = 1 << 5, // used as a reset flag only + }; + enum PickFlags + { + PICK_NORMAL = 0, // normal pick + PICK_STICKY = 1 << 0, // sticky pick - ignore visibility and sensitivity + PICK_AS_CLIP = 1 << 1, // pick with no stroke and opaque fill regardless of item style + PICK_OUTLINE = 1 << 2 // pick in outline mode + }; + + DrawingItem(Drawing &drawing); + DrawingItem(DrawingItem const &) = delete; + DrawingItem &operator=(DrawingItem const &) = delete; + void unlink(); /// Unlink this node and its subtree from the rendering tree and destroy. + virtual int tag() const { return tag_of<decltype(*this)>; } + + Geom::OptIntRect const &bbox() const { return _bbox; } + Geom::OptIntRect const &drawbox() const { return _drawbox; } + Geom::OptRect const &itemBounds() const { return _item_bbox; } + Geom::Affine const &ctm() const { return _ctm; } + Geom::Affine transform() const { return _transform ? *_transform : Geom::identity(); } + Drawing &drawing() const { return _drawing; } + DrawingItem *parent() const { return _parent; } + bool isAncestorOf(DrawingItem const *item) const; + int getUpdateComplexity() const { return _update_complexity; } + bool unisolatedBlend() const; + + void appendChild(DrawingItem *item); + void prependChild(DrawingItem *item); + void clearChildren(); + + bool visible() const { return _visible; } + void setVisible(bool visible); + bool sensitive() const { return _sensitive; } + void setSensitive(bool sensitive); + + virtual void setStyle(SPStyle const *style, SPStyle const *context_style = nullptr); + virtual void setChildrenStyle(SPStyle const *context_style); + void setOpacity(float opacity); + void setAntialiasing(unsigned antialias); + unsigned antialiasing() const { return _antialias; } + 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 zorder); + void setItemBounds(Geom::OptRect const &bounds); + void setFilterRenderer(std::unique_ptr<Filters::Filter> renderer); + + 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, RenderContext &rc, Geom::IntRect const &area, unsigned flags = 0, DrawingItem const *stop_at = nullptr) const; + unsigned render(DrawingContext &dc, Geom::IntRect const &area, unsigned flags = 0) const; + void clip(DrawingContext &dc, RenderContext &rc, Geom::IntRect const &area) const; + DrawingItem *pick(Geom::Point const &p, double delta, unsigned flags = 0); + + Glib::ustring name() const; // For debugging + void recursivePrintTree(unsigned level = 0) const; // For debugging + +protected: + enum class ChildType : unsigned char + { + ORPHAN = 0, // No parent - implies !parent. + NORMAL = 1, // Contained in children of parent. + CLIP = 2, // Referenced by clip of parent. + MASK = 3, // Referenced by mask of parent. + FILL = 4, // Referenced by fill pattern of parent. + STROKE = 5, // Referenced by stroke pattern of parent. + ROOT = 6 // Referenced by root of drawing. + }; + enum RenderResult + { + RENDER_OK = 0, + RENDER_STOP = 1 + }; + virtual ~DrawingItem(); // Private to prevent deletion of items that are still in use by a snapshot. + void _renderOutline(DrawingContext &dc, RenderContext &rc, Geom::IntRect const &area, unsigned flags) const; + void _markForUpdate(unsigned state, bool propagate); + void _markForRendering(); + void _invalidateFilterBackground(Geom::IntRect const &area); + double _cacheScore(); + Geom::OptIntRect _cacheRect() const; + void _setCached(bool cached, bool persistent = false); + virtual unsigned _updateItem(Geom::IntRect const &area, UpdateContext const &ctx, unsigned flags, unsigned reset) { return 0; } + virtual unsigned _renderItem(DrawingContext &dc, RenderContext &rc, Geom::IntRect const &area, unsigned flags, DrawingItem const *stop_at) const { return RENDER_OK; } + virtual void _clipItem(DrawingContext &dc, RenderContext &rc, Geom::IntRect const &area) const {} + virtual DrawingItem *_pickItem(Geom::Point const &p, double delta, unsigned flags) { return nullptr; } + virtual bool _canClip() const { return false; } + virtual void _dropPatternCache() {} + + Drawing &_drawing; + DrawingItem *_parent; + + using ListHook = boost::intrusive::list_member_hook<>; + ListHook _child_hook; + + using ChildrenList = boost::intrusive::list< + DrawingItem, + boost::intrusive::member_hook<DrawingItem, ListHook, &DrawingItem::_child_hook> + >; + ChildrenList _children; + + // Todo: Try to get rid of all of these variables, moving them into the object tree. + unsigned _key; ///< Auxiliary key used by the object tree for showing clips/masks/patterns. + SPItem *_item; ///< Used to associate DrawingItems with SPItems that created them + SPStyle const *_style; // Not used by DrawingGlyphs + SPStyle const *_context_style; // Used for 'context-fill', 'context-stroke' + + float _opacity; + std::unique_ptr<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; + std::unique_ptr<Inkscape::Filters::Filter> _filter; + std::unique_ptr<CacheData> _cache; + int _update_complexity = 0; + bool _contains_unisolated_blend : 1; + + CacheList::iterator _cache_iterator; + + bool style_vector_effect_size : 1; + bool style_vector_effect_rotate : 1; + bool style_vector_effect_fixed : 1; + + unsigned _state : 8; + unsigned _propagate_state : 8; + ChildType _child_type : 3; + 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_persistent : 1; ///< If set, will always be cached regardless of score + unsigned _has_cache_iterator : 1; ///< If set, _cache_iterator is valid + 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 _blend_mode; + + template<typename F> + void defer(F &&f) + { + // Introduce artificial dependence on a template parameter to allow definition with Drawing forward-declared. + auto &drawing = static_cast<std::enable_if_t<(sizeof(F) > 0), Drawing&>>(_drawing); + drawing.defer(std::forward<F>(f)); + } + + friend class Drawing; +}; + +/// Apply antialias setting to Cairo. +void apply_antialias(DrawingContext &dc, int antialias); + +} // namespace Inkscape + +#endif // 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-paintserver.cpp b/src/display/drawing-paintserver.cpp new file mode 100644 index 0000000..e018d8b --- /dev/null +++ b/src/display/drawing-paintserver.cpp @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "drawing-paintserver.h" +#include "cairo-utils.h" + +namespace Inkscape { + +DrawingPaintServer::~DrawingPaintServer() = default; + +cairo_pattern_t *DrawingSolidColor::create_pattern(cairo_t *, Geom::OptRect const &, double opacity) const +{ + return cairo_pattern_create_rgba(c[0], c[1], c[2], alpha * opacity); +} + +void DrawingGradient::common_setup(cairo_pattern_t *pat, Geom::OptRect const &bbox, double opacity) const +{ + // set spread type + switch (spread) { + case SP_GRADIENT_SPREAD_REFLECT: + cairo_pattern_set_extend(pat, CAIRO_EXTEND_REFLECT); + break; + case SP_GRADIENT_SPREAD_REPEAT: + cairo_pattern_set_extend(pat, CAIRO_EXTEND_REPEAT); + break; + case SP_GRADIENT_SPREAD_PAD: + default: + cairo_pattern_set_extend(pat, CAIRO_EXTEND_PAD); + break; + } + + // set pattern transform matrix + auto gs2user = transform; + if (units == SP_GRADIENT_UNITS_OBJECTBOUNDINGBOX && bbox) { + auto bbox2user = Geom::Affine(bbox->width(), 0, 0, bbox->height(), bbox->left(), bbox->top()); + gs2user *= bbox2user; + } + ink_cairo_pattern_set_matrix(pat, gs2user.inverse()); +} + +cairo_pattern_t *DrawingLinearGradient::create_pattern(cairo_t *, Geom::OptRect const &bbox, double opacity) const +{ + auto pat = cairo_pattern_create_linear(x1, y1, x2, y2); + + common_setup(pat, bbox, opacity); + + // add stops + for (auto &stop : stops) { + // multiply stop opacity by paint opacity + cairo_pattern_add_color_stop_rgba(pat, stop.offset, stop.color.v.c[0], stop.color.v.c[1], stop.color.v.c[2], stop.opacity * opacity); + } + + return pat; +} + +cairo_pattern_t *DrawingRadialGradient::create_pattern(cairo_t *ct, Geom::OptRect const &bbox, double opacity) const +{ + Geom::Point focus(fx, fy); + Geom::Point center(cx, cy); + + double radius = r; + double focusr = fr; + double scale = 1.0; + double tolerance = cairo_get_tolerance(ct); + + Geom::Affine gs2user = transform; + + if (units == SP_GRADIENT_UNITS_OBJECTBOUNDINGBOX && bbox) { + Geom::Affine bbox2user(bbox->width(), 0, 0, bbox->height(), bbox->left(), bbox->top()); + gs2user *= bbox2user; + } + + // we need to use vectors with the same direction to represent the transformed + // radius and the focus-center delta, because gs2user might contain non-uniform scaling + Geom::Point d(focus - center); + Geom::Point d_user(d.length(), 0); + Geom::Point r_user(radius, 0); + Geom::Point fr_user(focusr, 0); + d_user *= gs2user.withoutTranslation(); + r_user *= gs2user.withoutTranslation(); + fr_user *= gs2user.withoutTranslation(); + + double dx = d_user.x(), dy = d_user.y(); + cairo_user_to_device_distance(ct, &dx, &dy); + + // compute the tolerance distance in user space + // create a vector with the same direction as the transformed d, + // with the length equal to tolerance + double dl = hypot(dx, dy); + double tx = tolerance * dx / dl, ty = tolerance * dy / dl; + cairo_device_to_user_distance(ct, &tx, &ty); + double tolerance_user = hypot(tx, ty); + + if (d_user.length() + tolerance_user > r_user.length()) { + scale = r_user.length() / d_user.length(); + + // nudge the focus slightly inside + scale *= 1.0 - 2.0 * tolerance / dl; + } + + auto pat = cairo_pattern_create_radial(scale * d.x() + center.x(), scale * d.y() + center.y(), focusr, center.x(), center.y(), radius); + + common_setup(pat, bbox, opacity); + + // add stops + for (auto &stop : stops) { + // multiply stop opacity by paint opacity + cairo_pattern_add_color_stop_rgba(pat, stop.offset, stop.color.v.c[0], stop.color.v.c[1], stop.color.v.c[2], stop.opacity * opacity); + } + + return pat; +} + +cairo_pattern_t *DrawingMeshGradient::create_pattern(cairo_t *, Geom::OptRect const &bbox, double opacity) const +{ +#ifdef MESH_DEBUG + std::cout << "sp_meshgradient_create_pattern: " << bbox << " " << opacity << std::endl; +#endif + + auto pat = cairo_pattern_create_mesh(); + + for (int i = 0; i < rows; i++) { + for (int j = 0; j < cols; j++) { + auto &data = patchdata[i][j]; + + cairo_mesh_pattern_begin_patch(pat); + cairo_mesh_pattern_move_to(pat, data.points[0][0].x(), data.points[0][0].y()); + + for (int k = 0; k < 4; k++) { + switch (data.pathtype[k]) { + case 'l': + case 'L': + case 'z': + case 'Z': + cairo_mesh_pattern_line_to(pat, data.points[k][3].x(), data.points[k][3].y()); + break; + case 'c': + case 'C': + cairo_mesh_pattern_curve_to(pat, data.points[k][1].x(), data.points[k][1].y(), + data.points[k][2].x(), data.points[k][2].y(), + data.points[k][3].x(), data.points[k][3].y()); + break; + default: + // Shouldn't happen + std::cerr << "sp_mesh_create_pattern: path error" << std::endl; + } + + if (data.tensorIsSet[k]) { + Geom::Point t = data.tensorpoints[k]; + cairo_mesh_pattern_set_control_point(pat, k, t.x(), t.y()); + } + + cairo_mesh_pattern_set_corner_color_rgba(pat, k, + data.color[k][0], + data.color[k][1], + data.color[k][2], + data.opacity[k] * opacity); + } + + cairo_mesh_pattern_end_patch(pat); + } + } + + // set pattern transform matrix + Geom::Affine gs2user = transform; + if (units == SP_GRADIENT_UNITS_OBJECTBOUNDINGBOX && bbox) { + Geom::Affine bbox2user(bbox->width(), 0, 0, bbox->height(), bbox->left(), bbox->top()); + gs2user *= bbox2user; + } + ink_cairo_pattern_set_matrix(pat, gs2user.inverse()); + + return pat; +} + +} // 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-paintserver.h b/src/display/drawing-paintserver.h new file mode 100644 index 0000000..eb68f6b --- /dev/null +++ b/src/display/drawing-paintserver.h @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_DISPLAY_DRAWING_PAINT_SERVER_H +#define INKSCAPE_DISPLAY_DRAWING_PAINT_SERVER_H +/** + * @file + * Representation of paint servers used when rendering. + */ +/* + * Author: PBS <pbs3141@gmail.com> + * Copyright (C) 2022 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <array> +#include <vector> +#include <cairo.h> +#include <2geom/rect.h> +#include <2geom/affine.h> +#include "object/sp-gradient-spread.h" +#include "object/sp-gradient-units.h" +#include "object/sp-gradient-vector.h" + +class SPGradient; + +namespace Inkscape { + +/** + * A DrawingPaintServer is a lightweight copy of the resources needed to paint using a paint server. + * + * It is built by an SPPaintServer, stored in the DrawingItem tree, and used when rendering. + * + * The pattern information is stored in a rendering-backend agnostic way, and the object remains + * valid even after the original SPPaintServer is modified or destroyed. + */ +class DrawingPaintServer +{ +public: + virtual ~DrawingPaintServer() = 0; + + /// Produce a pattern that can be used for painting with Cairo. + virtual cairo_pattern_t *create_pattern(cairo_t *ct, Geom::OptRect const &bbox, double opacity) const = 0; + + /// Return whether this paint server could benefit from dithering. + virtual bool ditherable() const { return false; } + + /// Return whether create_pattern() uses its cairo_t argument. Such pattern cannot be cached, but recreated each time. + /// Fixme: The only reson this exists is to work around https://gitlab.freedesktop.org/cairo/cairo/-/issues/146. + virtual bool uses_cairo_ctx() const { return false; } +}; + +// Todo: Remove, merging with existing implementation for solid colours. +/** + * A simple solid color, storing an RGB color and an opacity. + */ +class DrawingSolidColor final + : public DrawingPaintServer +{ +public: + DrawingSolidColor(float *c, double alpha) + : c({c[0], c[1], c[2]}) + , alpha(alpha) {} + + cairo_pattern_t *create_pattern(cairo_t *, Geom::OptRect const &, double opacity) const override; + +private: + std::array<float, 3> c; ///< RGB color components. + double alpha; +}; + +/** + * The base class for all gradients. + */ +class DrawingGradient + : public DrawingPaintServer +{ +protected: + DrawingGradient(SPGradientSpread spread, SPGradientUnits units, Geom::Affine const &transform) + : spread(spread) + , units(units) + , transform(transform) {} + + bool ditherable() const override { return true; } + + /// Perform some common initialization steps on the given Cairo pattern. + void common_setup(cairo_pattern_t *pat, Geom::OptRect const &bbox, double opacity) const; + + SPGradientSpread spread; + SPGradientUnits units; + Geom::Affine transform; +}; + +/** + * A linear gradient. + */ +class DrawingLinearGradient final + : public DrawingGradient +{ +public: + DrawingLinearGradient(SPGradientSpread spread, SPGradientUnits units, Geom::Affine const &transform, + float x1, float y1, float x2, float y2, std::vector<SPGradientStop> stops) + : DrawingGradient(spread, units, transform) + , x1(x1) + , y1(y1) + , x2(x2) + , y2(y2) + , stops(std::move(stops)) {} + + cairo_pattern_t *create_pattern(cairo_t*, Geom::OptRect const &bbox, double opacity) const override; + +private: + float x1, y1, x2, y2; + std::vector<SPGradientStop> stops; +}; + +/** + * A radial gradient. + */ +class DrawingRadialGradient final + : public DrawingGradient +{ +public: + DrawingRadialGradient(SPGradientSpread spread, SPGradientUnits units, Geom::Affine const &transform, + float fx, float fy, float cx, float cy, float r, float fr, std::vector<SPGradientStop> stops) + : DrawingGradient(spread, units, transform) + , fx(fx) + , fy(fy) + , cx(cx) + , cy(cy) + , r(r) + , fr(fr) + , stops(std::move(stops)) {} + + cairo_pattern_t *create_pattern(cairo_t *ct, Geom::OptRect const &bbox, double opacity) const override; + + bool uses_cairo_ctx() const override { return true; } + +private: + float fx, fy, cx, cy, r, fr; + std::vector<SPGradientStop> stops; +}; + +/** + * A mesh gradient. + */ +class DrawingMeshGradient final + : public DrawingGradient +{ +public: + struct PatchData + { + Geom::Point points[4][4]; + char pathtype[4]; + bool tensorIsSet[4]; + Geom::Point tensorpoints[4]; + std::array<float, 3> color[4]; + double opacity[4]; + }; + + DrawingMeshGradient(SPGradientSpread spread, SPGradientUnits units, Geom::Affine const &transform, + int rows, int cols, std::vector<std::vector<PatchData>> patchdata) + : DrawingGradient(spread, units, transform) + , rows(rows) + , cols(cols) + , patchdata(std::move(patchdata)) {} + + cairo_pattern_t *create_pattern(cairo_t*, Geom::OptRect const &bbox, double opacity) const override; + +private: + int rows; + int cols; + std::vector<std::vector<PatchData>> patchdata; +}; + +} // namespace Inkscape + +#endif // INKSCAPE_DISPLAY_DRAWING_PAINT_SERVER_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..fb21cbc --- /dev/null +++ b/src/display/drawing-pattern.cpp @@ -0,0 +1,304 @@ +// 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 <cairomm/region.h> +#include "cairo-utils.h" +#include "drawing-context.h" +#include "drawing-pattern.h" +#include "drawing-surface.h" +#include "drawing.h" +#include "helper/geom.h" +#include "ui/util.h" + +namespace Inkscape { + +DrawingPattern::Surface::Surface(Geom::IntRect const &rect, int device_scale) + : rect(rect) + , surface(Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, rect.width() * device_scale, rect.height() * device_scale)) +{ + cairo_surface_set_device_scale(surface->cobj(), device_scale, device_scale); +} + +DrawingPattern::DrawingPattern(Drawing &drawing) + : DrawingGroup(drawing) + , _overflow_steps(1) +{ +} + +void DrawingPattern::setPatternToUserTransform(Geom::Affine const &transform) +{ + defer([=] { + auto constexpr EPS = 1e-18; + auto current = _pattern_to_user ? *_pattern_to_user : Geom::identity(); + if (Geom::are_near(transform, current, EPS)) return; + _markForRendering(); + _pattern_to_user = transform.isIdentity(EPS) ? nullptr : std::make_unique<Geom::Affine>(transform); + _markForUpdate(STATE_ALL, true); + }); +} + +void DrawingPattern::setTileRect(Geom::Rect const &tile_rect) +{ + defer([=] { + _tile_rect = tile_rect; + _markForUpdate(STATE_ALL, true); + }); +} + +void DrawingPattern::setOverflow(Geom::Affine const &initial_transform, int steps, Geom::Affine const &step_transform) +{ + defer([=] { + _overflow_initial_transform = initial_transform; + _overflow_steps = steps; + _overflow_step_transform = step_transform; + }); +} + +cairo_pattern_t *DrawingPattern::renderPattern(RenderContext &rc, Geom::IntRect const &area, float opacity, int device_scale) const +{ + if (opacity < 1e-3) { + // Invisible. + return nullptr; + } + + if (!_tile_rect || _tile_rect->hasZeroArea()) { + // Empty. + return nullptr; + } + + // Calculate various transforms. + auto const dt = Geom::Translate(-_tile_rect->min()) * Geom::Scale(_pattern_resolution / _tile_rect->dimensions()); // AKA user_to_tile. + auto const idt = dt.inverse(); + auto const pattern_to_tile = _pattern_to_user ? _pattern_to_user->inverse() * dt : dt; + auto const screen_to_tile = _ctm.inverse() * pattern_to_tile; + + // Return a canonical choice of rectangle with the same periodic tiling as rect. + auto canonicalised = [&, this] (Geom::IntRect rect) { + for (int i = 0; i < 2; i++) { + if (rect.dimensions()[i] >= _pattern_resolution[i]) { + rect[i] = {0, _pattern_resolution[i]}; + } else { + rect[i] -= Util::rounddown(rect[i].min(), _pattern_resolution[i]); + } + } + return rect; + }; + + // Return whether the periodic tiling of a contains the periodic tiling of b. + auto wrapped_contains = [&] (Geom::IntRect const &a, Geom::IntRect const &b) { + auto check = [&] (int i) { + int const period = _pattern_resolution[i]; + if (a[i].extent() >= period) return true; + if (b[i].extent() > a[i].extent()) return false; + return Util::rounddown(b[i].min() - a[i].min(), period) >= b[i].max() - a[i].max(); + }; + return check(0) && check(1); + }; + + // Return whether the periodic tiling of a intersects with or touches the periodic tiling of b. + auto wrapped_touches = [&] (Geom::IntRect const &a, Geom::IntRect const &b) { + auto check = [&] (int i) { + int const period = _pattern_resolution[i]; + if (a[i].extent() >= period) return true; + if (b[i].extent() >= period) return true; + return Util::rounddown(b[i].max() - a[i].min(), period) >= b[i].min() - a[i].max(); + }; + return check(0) && check(1); + }; + + // Calculate the minimum and maximum translates of a that overlap with b. + auto overlapping_translates = [&, this] (Geom::IntRect const &a, Geom::IntRect const &b) { + Geom::IntPoint min, max; + for (int i = 0; i < 2; i++) { + min[i] = Util::roundup (b[i].min() - a[i].max() + 1, _pattern_resolution[i]); + max[i] = Util::rounddown(b[i].max() - a[i].min() - 1, _pattern_resolution[i]); + } + return std::make_pair(min, max); + }; + + // Paint the periodic tiling of a into b, and remove the painted region from dirty. + auto wrapped_paint = [&, this] (Surface const &a, Geom::IntRect &b, Cairo::RefPtr<Cairo::Context> const &cr, Cairo::RefPtr<Cairo::Region> const &dirty) { + auto const [min, max] = overlapping_translates(a.rect, b); + for (int x = min.x(); x <= max.x(); x += _pattern_resolution.x()) { + for (int y = min.y(); y <= max.y(); y += _pattern_resolution.y()) { + auto const rect = a.rect + Geom::IntPoint(x, y); + dirty->subtract(geom_to_cairo(rect)); + cr->set_source(a.surface, rect.left(), rect.top()); + cr->paint(); + } + } + }; + + // Calculate the requested area to draw within tile rasterisation space. + auto const area_orig = (Geom::Rect(area) * screen_to_tile).roundOutwards(); + auto const area_tile = canonicalised(area_orig); + + // Simplest solution for now to protecting pattern cache is a mutex. This makes all + // pattern rendering single-threaded, however patterns are typically not the bottleneck. + auto lock = std::lock_guard(mutables); + + auto get_surface = [&, this] () -> std::pair<Surface*, Cairo::RefPtr<Cairo::Region>> { + // If there is a rectangle containing the requested area, just use that. + for (auto &s : surfaces) { + if (wrapped_contains(s.rect, area_tile)) { + return { &s, {} }; + } + } + + // Otherwise, recursively merge the requested area with all overlapping or touching rectangles, and paint the missing part. + std::vector<Surface> merged; + auto expanded = area_tile; + + while (true) { + bool modified = false; + + for (auto it = surfaces.begin(); it != surfaces.end(); ) { + if (wrapped_touches(expanded, it->rect)) { + expanded.unionWith(it->rect + rounddown(expanded.max() - it->rect.min(), _pattern_resolution)); + merged.emplace_back(std::move(*it)); + *it = std::move(surfaces.back()); + surfaces.pop_back(); + modified = true; + } else { + ++it; + } + } + + if (!modified) break; + } + + // Canonicalise the expanded rectangle. (Stops Cairo's coordinates overflowing and the pattern disappearing.) + expanded = canonicalised(expanded); + + // Create a new surface covering the expanded rectangle. + auto surface = Surface(expanded, device_scale); + auto cr = Cairo::Context::create(surface.surface); + cr->translate(-surface.rect.left(), -surface.rect.top()); + + // Paste all the old surfaces into the new surface, tracking the remaining dirty region. + auto dirty = Cairo::Region::create(geom_to_cairo(expanded)); + + for (auto &m : merged) { + wrapped_paint(m, expanded, cr, dirty); + } + + // Emplace the surface, and return it along with the remaining dirty region. + surfaces.emplace_back(std::move(surface)); + return std::make_pair(&surfaces.back(), std::move(dirty)); + }; + + // Find an already-drawn surface containing the requested area, or create if it none exists. + auto [surface, dirty] = get_surface(); + + // Draw the pattern contents to the dirty areas of the surface, taking care of possible wrapping. + Inkscape::DrawingContext dc(surface->surface->cobj(), surface->rect.min()); + + auto paint = [&, this] (Geom::IntRect const &rect) { + if (_overflow_steps == 1) { + render(dc, rc, rect); + } else { + // Overflow transforms need to be transformed to the old coordinate system + // before stretching to the pattern resolution. + auto const initial_transform = idt * _overflow_initial_transform * dt; + auto const 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, rc, rect, RENDER_BYPASS_CACHE); + dc.transform(step_transform); + // auto raw = pattern_surface.raw(); + // auto filename = "drawing-pattern" + std::to_string(i) + ".png"; + // cairo_surface_write_to_png(pattern_surface.raw(), filename.c_str()); + } + } + }; + + if (dirty) { + for (int i = 0; i < dirty->get_num_rectangles(); i++) { + auto const rect = cairo_to_geom(dirty->get_rectangle(i)); + for (int x = 0; x <= 1; x++) { + for (int y = 0; y <= 1; y++) { + auto const wrap = _pattern_resolution * Geom::IntPoint(x, y); + auto const rect2 = rect & Geom::IntRect(wrap, wrap + _pattern_resolution); + if (!rect2) continue; + auto save = DrawingContext::Save(dc); + // Clip to rectangle to be drawn. + dc.rectangle(*rect2); + dc.clip(); + // Draw the pattern. + dc.translate(wrap); + paint(*rect2 - wrap); + // Apply opacity, if necessary. + if (opacity < 1.0 - 1e-3) { + dc.setOperator(CAIRO_OPERATOR_DEST_IN); + dc.setSource(0.0, 0.0, 0.0, opacity); + dc.paint(); + } + } + } + } + dirty.clear(); + } + + // Debug: Show pattern tile. + // surface->surface->write_to_png("/tmp/patternsurface.png"); + + // Create and return pattern. + auto cp = cairo_pattern_create_for_surface(surface->surface->cobj()); + auto const shift = surface->rect.min() + rounddown(area_orig.min() - surface->rect.min(), _pattern_resolution); + ink_cairo_pattern_set_matrix(cp, pattern_to_tile * Geom::Translate(-shift)); + cairo_pattern_set_extend(cp, CAIRO_EXTEND_REPEAT); + return cp; +} + +unsigned DrawingPattern::_updateItem(Geom::IntRect const &area, UpdateContext const &ctx, unsigned flags, unsigned reset) +{ + _dropPatternCache(); + + if (!_tile_rect || _tile_rect->hasZeroArea()) { + return STATE_NONE; + } + + // Calculate the desired resolution of a pattern tile. + double const det_ctm = ctx.ctm.det(); + double const det_ps2user = _pattern_to_user ? _pattern_to_user->det() : 1.0; + double scale = std::sqrt(std::abs(det_ctm * det_ps2user)); + // Fixme: When scale is too big (zooming in a pattern), Cairo doesn't render the pattern. + // More precisely it fails when setting pattern matrix in DrawingPattern::renderPattern. + // Correct solution should make use of visible area and change pattern tile rect accordingly. + auto const c = _tile_rect->dimensions() * scale; + _pattern_resolution = c.ceil(); + + // Map tile rect to the origin and stretch it to the desired resolution. + auto const dt = Geom::Translate(-_tile_rect->min()) * Geom::Scale(_pattern_resolution / _tile_rect->dimensions()); + + // Apply this transform to the actual pattern tree. + return DrawingGroup::_updateItem(Geom::IntRect::infinite(), { dt }, flags, reset); +} + +void DrawingPattern::_dropPatternCache() +{ + surfaces.clear(); +} + +} // 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..8cd0f39 --- /dev/null +++ b/src/display/drawing-pattern.h @@ -0,0 +1,110 @@ +// 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 INKSCAPE_DISPLAY_DRAWING_PATTERN_H +#define INKSCAPE_DISPLAY_DRAWING_PATTERN_H + +#include <mutex> +#include <cairomm/surface.h> +#include "drawing-group.h" + +using cairo_pattern_t = struct _cairo_pattern; + +namespace Inkscape { + +/** + * @brief Drawing tree node used for rendering paints. + * + * DrawingPattern is used for rendering patterns and hatches. + * + * It renders its 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); + int tag() const override { return tag_of<decltype(*this)>; } + + /** + * Set the transformation from pattern to user coordinate systems. + * @see SPPattern description for explanation of coordinate systems. + */ + void setPatternToUserTransform(Geom::Affine const &); + + /** + * Set the tile rect position and dimensions in content coordinate system + */ + void setTileRect(Geom::Rect const &); + + /** + * 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 const &initial_transform, int steps, Geom::Affine const &step_transform); + + /** + * Render the pattern. + * + * Returns cairo_pattern_t structure that can be set as source surface. + */ + cairo_pattern_t *renderPattern(RenderContext &rc, Geom::IntRect const &area, float opacity, int device_scale) const; + +protected: + ~DrawingPattern() override = default; + + unsigned _updateItem(Geom::IntRect const &area, UpdateContext const &ctx, unsigned flags, unsigned reset) override; + + void _dropPatternCache() override; + + std::unique_ptr<Geom::Affine> _pattern_to_user; + + // Set by overflow. + Geom::Affine _overflow_initial_transform; + Geom::Affine _overflow_step_transform; + int _overflow_steps; + + Geom::OptRect _tile_rect; + + // Set on update. + Geom::IntPoint _pattern_resolution; + + struct Surface + { + Surface(Geom::IntRect const &rect, int device_scale); + Geom::IntRect rect; + Cairo::RefPtr<Cairo::ImageSurface> surface; + }; + + mutable std::mutex mutables; + + // Parts of the pattern tile that have been rendered. Read/written on render, cleared on update. + mutable std::vector<Surface> surfaces; +}; + +} // namespace Inkscape + +#endif // 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..d99e99f --- /dev/null +++ b/src/display/drawing-shape.cpp @@ -0,0 +1,444 @@ +// 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 "dither-lock.h" +#include "style.h" + +#include "curve.h" +#include "drawing.h" +#include "drawing-context.h" +#include "drawing-shape.h" +#include "control/canvas-item-drawing.h" + +#include "helper/geom.h" + +#include "ui/widget/canvas.h" // Canvas area + +namespace Inkscape { + +DrawingShape::DrawingShape(Drawing &drawing) + : DrawingItem(drawing) + , style_vector_effect_stroke(false) + , style_stroke_extensions_hairline(false) + , style_clip_rule(SP_WIND_RULE_EVENODD) + , style_fill_rule(SP_WIND_RULE_EVENODD) + , style_opacity(SP_SCALE24_MAX) + , _last_pick(nullptr) + , _repick_after(0) +{ +} + +void DrawingShape::setPath(std::shared_ptr<SPCurve const> curve) +{ + defer([this, curve = std::move(curve)] () mutable { + _markForRendering(); + _curve = std::move(curve); + _markForUpdate(STATE_ALL, false); + }); +} + +void DrawingShape::setStyle(SPStyle const *style, SPStyle const *context_style) +{ + DrawingItem::setStyle(style, context_style); + + auto vector_effect_stroke = false; + auto stroke_extensions_hairline = false; + auto clip_rule = SP_WIND_RULE_EVENODD; + auto fill_rule = SP_WIND_RULE_EVENODD; + auto opacity = SP_SCALE24_MAX; + if (style) { + vector_effect_stroke = style->vector_effect.stroke; + stroke_extensions_hairline = style->stroke_extensions.hairline; + clip_rule = style->clip_rule.value; + fill_rule = style->fill_rule.value; + opacity = style->opacity.value; + } + + defer([=, nrstyle = NRStyleData(_style)] () mutable { + _nrstyle.set(std::move(nrstyle)); + style_vector_effect_stroke = vector_effect_stroke; + style_stroke_extensions_hairline = stroke_extensions_hairline; + style_clip_rule = clip_rule; + style_fill_rule = fill_rule; + style_opacity = opacity; + }); +} + +void DrawingShape::setChildrenStyle(SPStyle const *context_style) +{ + DrawingItem::setChildrenStyle(context_style); + + defer([this, nrstyle = NRStyleData(_style, _context_style)] () mutable { + _nrstyle.set(std::move(nrstyle)); + }); +} + +unsigned DrawingShape::_updateItem(Geom::IntRect const &area, UpdateContext const &ctx, unsigned flags, unsigned reset) +{ + // update markers + for (auto &c : _children) { + c.update(area, ctx, flags, reset); + } + + // clear Cairo data to force update + if (flags & STATE_RENDER) { + _nrstyle.invalidate(); + } + + auto calc_curve_bbox = [&, this] () -> Geom::OptIntRect { + if (!_curve) { + return {}; + } + + auto rect = bounds_exact_transformed(_curve->get_pathvector(), ctx.ctm); + if (!rect) { + return {}; + } + + float stroke_max = 0.0f; + + // Get the normal stroke. + if (_drawing.renderMode() != RenderMode::OUTLINE && _nrstyle.data.stroke.type != NRStyleData::PaintType::NONE) { + // Expand by stroke width. + stroke_max = _nrstyle.data.stroke_width * 0.5f; + + // Scale by view transformation, unless vector effect stroke. + if (!style_vector_effect_stroke) { + stroke_max *= max_expansion(ctx.ctm); + } + + // Cap minimum line width if asked. + if (_drawing.renderMode() == RenderMode::VISIBLE_HAIRLINES || style_stroke_extensions_hairline) { + stroke_max = std::max(stroke_max, 0.5f); + } + } + + // Get the outline stroke. + if (_drawing.renderMode() == RenderMode::OUTLINE || _drawing.outlineOverlay()) { + stroke_max = std::max(stroke_max, 0.5f); + } + + if (stroke_max > 0.0f) { + // Expand by mitres, if present. + if (_nrstyle.data.line_join == CAIRO_LINE_JOIN_MITER && _nrstyle.data.miter_limit >= 1.0f) { + stroke_max *= _nrstyle.data.miter_limit; + } + + // Apply expansion if non-zero. + if (stroke_max > 0.01) { + rect->expandBy(stroke_max); + } + } + + return rect->roundOutwards(); + }; + + if (flags & STATE_BBOX) { + _bbox = calc_curve_bbox(); + + for (auto &c : _children) { + _bbox.unionWith(c.bbox()); + } + } + + return _state | flags; +} + +void DrawingShape::_renderFill(DrawingContext &dc, RenderContext &rc, Geom::IntRect const &area) const +{ + Inkscape::DrawingContext::Save save(dc); + dc.transform(_ctm); + + auto has_fill = _nrstyle.prepareFill(dc, rc, area, _item_bbox, _fill_pattern); + + if (has_fill) { + dc.path(_curve->get_pathvector()); + auto dl = DitherLock(dc, _nrstyle.data.fill.ditherable() && _drawing.useDithering()); + _nrstyle.applyFill(dc, has_fill); + dc.fillPreserve(); + dc.newPath(); // clear path + } +} + +void DrawingShape::_renderStroke(DrawingContext &dc, RenderContext &rc, Geom::IntRect const &area, unsigned flags) const +{ + Inkscape::DrawingContext::Save save(dc); + dc.transform(_ctm); + + auto has_stroke = _nrstyle.prepareStroke(dc, rc, area, _item_bbox, _stroke_pattern); + if (!style_stroke_extensions_hairline && _nrstyle.data.stroke_width == 0) { + has_stroke.reset(); + } + + if (has_stroke) { + // TODO: remove segments outside of bbox when no dashes present + dc.path(_curve->get_pathvector()); + if (style_vector_effect_stroke) { + dc.restore(); + dc.save(); + } + auto dl = DitherLock(dc, _nrstyle.data.stroke.ditherable() && _drawing.useDithering()); + _nrstyle.applyStroke(dc, has_stroke); + + // If the stroke is a hairline, set it to exactly 1px on screen. + // If visible hairline mode is on, make sure the line is at least 1px. + if (flags & RENDER_VISIBLE_HAIRLINES || style_stroke_extensions_hairline) { + double dx = 1.0, dy = 0.0; + dc.device_to_user_distance(dx, dy); + auto pixel_size = std::hypot(dx, dy); + if (style_stroke_extensions_hairline || _nrstyle.data.stroke_width < pixel_size) { + dc.setHairline(); + } + } + + dc.strokePreserve(); + dc.newPath(); // clear path + } +} + +void DrawingShape::_renderMarkers(DrawingContext &dc, RenderContext &rc, Geom::IntRect const &area, unsigned flags, DrawingItem const *stop_at) const +{ + // marker rendering + for (auto &i : _children) { + i.render(dc, rc, area, flags, stop_at); + } +} + +unsigned DrawingShape::_renderItem(DrawingContext &dc, RenderContext &rc, Geom::IntRect const &area, unsigned flags, DrawingItem const *stop_at) const +{ + if (!_curve) return RENDER_OK; + + auto visible = area & _bbox; + if (!visible) return RENDER_OK; // skip if not within bounding box + + bool outline = flags & RENDER_OUTLINE; + + if (outline) { + auto rgba = rc.outline_color; + + // 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, rc, area, flags, stop_at); + return RENDER_OK; + } + + if (_nrstyle.data.paint_order_layer[0] == NRStyleData::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 + auto has_fill = _nrstyle.prepareFill(dc, rc, *visible, _item_bbox, _fill_pattern); + auto has_stroke = _nrstyle.prepareStroke(dc, rc, *visible, _item_bbox, _stroke_pattern); + if (!_nrstyle.data.hairline && _nrstyle.data.stroke_width == 0) { + has_stroke.reset(); + } + if (has_fill || has_stroke) { + dc.path(_curve->get_pathvector()); + // TODO: remove segments outside of bbox when no dashes present + if (has_fill) { + auto dl = DitherLock(dc, _nrstyle.data.fill.ditherable() && _drawing.useDithering()); + _nrstyle.applyFill(dc, has_fill); + dc.fillPreserve(); + } + if (style_vector_effect_stroke) { + dc.restore(); + dc.save(); + } + if (has_stroke) { + auto dl = DitherLock(dc, _nrstyle.data.stroke.ditherable() && _drawing.useDithering()); + _nrstyle.applyStroke(dc, has_stroke); + + // If the draw mode is set to visible hairlines, don't let anything get smaller + // than half a pixel. + if (flags & RENDER_VISIBLE_HAIRLINES) { + double dx = 1.0, dy = 0.0; + dc.device_to_user_distance(dx, dy); + auto half_pixel_size = std::hypot(dx, dy) * 0.5; + if (_nrstyle.data.stroke_width < half_pixel_size) { + dc.setLineWidth(half_pixel_size); + } + } + + dc.strokePreserve(); + } + dc.newPath(); // clear path + } // has fill or stroke pattern + } + _renderMarkers(dc, rc, area, flags, stop_at); + return RENDER_OK; + + } + + // Handle different paint orders + for (auto &i : _nrstyle.data.paint_order_layer) { + switch (i) { + case NRStyleData::PAINT_ORDER_FILL: + _renderFill(dc, rc, *visible); + break; + case NRStyleData::PAINT_ORDER_STROKE: + _renderStroke(dc, rc, *visible, flags); + break; + case NRStyleData::PAINT_ORDER_MARKER: + _renderMarkers(dc, rc, area, flags, stop_at); + break; + default: + // PAINT_ORDER_AUTO Should not happen + break; + } + } + + return RENDER_OK; +} + +void DrawingShape::_clipItem(DrawingContext &dc, RenderContext &rc, Geom::IntRect const &/*area*/) const +{ + if (!_curve) return; + + Inkscape::DrawingContext::Save save(dc); + if (style_clip_rule == 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; + bool outline = flags & PICK_OUTLINE; + bool pick_as_clip = flags & PICK_AS_CLIP; + + if (SP_SCALE24_TO_FLOAT(style_opacity) == 0 && !outline && !pick_as_clip && !_drawing.selectZeroOpacity()) { + // 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.data.stroke.type != NRStyleData::PaintType::NONE && (_nrstyle.data.stroke.opacity > 1e-3 || _drawing.selectZeroOpacity())) { + // for normal picking calculate the distance corresponding top the stroke width + float scale = max_expansion(_ctm); + width = std::max(0.125f, _nrstyle.data.stroke_width * scale) / 2; + } else { + width = 0; + } + + double dist = Geom::infinity(); + int wind = 0; + bool needfill = pick_as_clip || (_nrstyle.data.fill.type != NRStyleData::PaintType::NONE && (_nrstyle.data.fill.opacity > 1e-3 || _drawing.selectZeroOpacity()) && !outline); + bool wind_evenodd = (pick_as_clip ? style_clip_rule : style_fill_rule) == SP_WIND_RULE_EVENODD; + + // actual shape picking + if (_drawing.getCanvasItemDrawing()) { + Geom::Rect viewbox = _drawing.getCanvasItemDrawing()->get_canvas()->get_area_world(); + 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; +} + +} // 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..0edd7ea --- /dev/null +++ b/src/display/drawing-shape.h @@ -0,0 +1,74 @@ +// 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 INKSCAPE_DISPLAY_DRAWING_SHAPE_H +#define 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); + int tag() const override { return tag_of<decltype(*this)>; } + + void setPath(std::shared_ptr<SPCurve const> curve); + void setStyle(SPStyle const *style, SPStyle const *context_style = nullptr) override; + void setChildrenStyle(SPStyle const *context_style) override; + +protected: + ~DrawingShape() override = default; + + unsigned _updateItem(Geom::IntRect const &area, UpdateContext const &ctx, unsigned flags, unsigned reset) override; + unsigned _renderItem(DrawingContext &dc, RenderContext &rc, Geom::IntRect const &area, unsigned flags, DrawingItem const *stop_at) const override; + void _clipItem(DrawingContext &dc, RenderContext &rc, Geom::IntRect const &area) const override; + DrawingItem *_pickItem(Geom::Point const &p, double delta, unsigned flags) override; + bool _canClip() const override { return true; } + + void _renderFill(DrawingContext &dc, RenderContext &rc, Geom::IntRect const &area) const; + void _renderStroke(DrawingContext &dc, RenderContext &rc, Geom::IntRect const &area, unsigned flags) const; + void _renderMarkers(DrawingContext &dc, RenderContext &rc, Geom::IntRect const &area, unsigned flags, DrawingItem const *stop_at) const; + + bool style_vector_effect_stroke : 1; + bool style_stroke_extensions_hairline : 1; + SPWindRule style_clip_rule; + SPWindRule style_fill_rule; + unsigned style_opacity : 24; + + std::shared_ptr<SPCurve const> _curve; + NRStyle _nrstyle; + + DrawingItem *_last_pick; + unsigned _repick_after; +}; + +} // namespace Inkscape + +#endif // INKSCAPE_DISPLAY_DRAWING_SHAPE_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..9926134 --- /dev/null +++ b/src/display/drawing-surface.cpp @@ -0,0 +1,315 @@ +// 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 "display/drawing-surface.h" +#include "display/drawing-context.h" +#include "display/cairo-utils.h" +#include "ui/util.h" + +namespace Inkscape { + +/** + * @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 / logbox.dimensions()) + , _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 = Geom::IntPoint(cairo_image_surface_get_width(surface) / _device_scale, cairo_image_surface_get_height(surface) / _device_scale); +} + +DrawingSurface::~DrawingSurface() +{ + if (_surface) { + cairo_surface_destroy(_surface); + } +} + +/// 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.vector().x(), _scale.vector().y()); + } + cairo_translate(ct, -_origin.x(), -_origin.y()); + return ct; +} + +Geom::IntRect DrawingSurface::pixelArea() const +{ + return Geom::IntRect::from_xywh(_origin.round(), _pixels); +} + +////////////////////////////////////////////////////////////////////////////// + +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) +{ + auto const dirty = geom_to_cairo(area); + cairo_region_subtract_rectangle(_clean_region, &dirty); +} + +void DrawingCache::markClean(Geom::IntRect const &area) +{ + auto const r = area & pixelArea(); + if (!r) return; + auto const clean = geom_to_cairo(*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 + auto const limit = geom_to_cairo(_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); + + auto const limit = geom_to_cairo(_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. + auto const area_c = geom_to_cairo(*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 + cairo_region_destroy(cache_region); + cairo_region_destroy(dirty_region); + cairo_region_destroy(_clean_region); + _clean_region = cairo_region_create(); + 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 = cairo_to_geom(to_repaint); + 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); + for (int i = 0; i < nr; ++i) { + cairo_rectangle_int_t tmp; + cairo_region_get_rectangle(cache_region, i, &tmp); + dc.rectangle(cairo_to_geom(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(cairo_to_geom(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); +} + +} // 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..423285a --- /dev/null +++ b/src/display/drawing-surface.h @@ -0,0 +1,99 @@ +// 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 INKSCAPE_DISPLAY_DRAWING_SURFACE_H +#define INKSCAPE_DISPLAY_DRAWING_SURFACE_H + +#include <cairo.h> +#include <2geom/affine.h> +#include <2geom/rect.h> +#include <2geom/transforms.h> +#include "helper/geom.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 { return Geom::Rect::from_xywh(_origin, dimensions()); } ///< Get the logical extents of the surface. + Geom::IntPoint pixels() const { return _pixels; } ///< Get the pixel dimensions of the surface + Geom::Point dimensions() const { return _pixels / _scale.vector(); } ///< Get the logical width and weight of the surface as a point. + Geom::Point origin() const { return _origin; } + Geom::Scale scale() const { return _scale; } + int device_scale() const { return _device_scale; } + Geom::Affine drawingTransform() const { return Geom::Translate(-_origin) * _scale; } ///< Get the transformation applied to the drawing context on construction. + 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; + 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); +}; + +} // namespace Inkscape + +#endif // INKSCAPE_DISPLAY_DRAWING_SURFACE_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..934ab2b --- /dev/null +++ b/src/display/drawing-text.cpp @@ -0,0 +1,766 @@ +// 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 "2geom/pathvector.h" + +#include "dither-lock.h" +#include "style.h" + +#include "cairo-utils.h" +#include "drawing-context.h" +#include "drawing-surface.h" +#include "drawing-text.h" +#include "drawing.h" + +#include "helper/geom.h" + +#include "libnrtype/font-instance.h" + +namespace Inkscape { + + +DrawingGlyphs::DrawingGlyphs(Drawing &drawing) + : DrawingItem(drawing) + , _glyph(0) +{ +} + +void DrawingGlyphs::setGlyph(std::shared_ptr<FontInstance> font, int glyph, Geom::Affine const &trans) +{ + defer([=, font = std::move(font)] { + _markForRendering(); + + assert(!_drawing.snapshotted()); + setTransform(trans); + + _font_data = font->share_data(); + _glyph = glyph; + + design_units = 1.0; + pathvec = nullptr; + pathvec_ref = nullptr; + pixbuf = nullptr; + + // Load pathvectors and pixbufs in advance, as must be done on main thread. + if (font) { + design_units = font->GetDesignUnits(); + pathvec = font->PathVector(_glyph); + pathvec_ref = font->PathVector(42); + + if (font->FontHasSVG()) { + pixbuf = font->PixBuf(_glyph); + } + } + + _markForUpdate(STATE_ALL, false); + }); +} + +void DrawingGlyphs::setStyle(SPStyle const *, SPStyle const *) +{ + std::cerr << "DrawingGlyphs: Use parent style" << std::endl; +} + +unsigned DrawingGlyphs::_updateItem(Geom::IntRect const &/*area*/, UpdateContext const &ctx, unsigned /*flags*/, unsigned /*reset*/) +{ + auto ggroup = cast<DrawingText>(&std::as_const(*_parent)); + if (!ggroup) { + throw InvalidItemException(); + } + + if (!pathvec) { + 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 (pathvec) { + Geom::OptRect tiltb = Geom::bounds_exact(*pathvec); + 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 (pathvec) { + if (!pathvec->empty()) { + pb = bounds_exact_transformed(*pathvec, ctx.ctm); + } + if (pathvec_ref && !pathvec_ref->empty()) { + pb.unionWith(bounds_exact_transformed(*pathvec_ref, 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.data.stroke.type != NRStyleData::PaintType::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 = std::max<double>(0.125, ggroup->_nrstyle.data.stroke_width * scale); + if (std::fabs(ggroup->_nrstyle.data.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.data.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) +{ + auto ggroup = cast<DrawingText>(_parent); + if (!ggroup) { + throw InvalidItemException(); + } + DrawingItem *result = nullptr; + bool invisible = ggroup->_nrstyle.data.fill.type == NRStyleData::PaintType::NONE && + ggroup->_nrstyle.data.stroke.type == NRStyleData::PaintType::NONE; + bool outline = flags & PICK_OUTLINE; + + if (pathvec && _bbox && (outline || !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) + , style_vector_effect_stroke(false) + , style_stroke_extensions_hairline(false) + , style_clip_rule(SP_WIND_RULE_EVENODD) +{ +} + +bool DrawingText::addComponent(std::shared_ptr<FontInstance> const &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; + + defer([=, font = std::move(font)] () mutable { + _markForRendering(); + auto ng = new DrawingGlyphs(_drawing); + assert(!_drawing.snapshotted()); + ng->setGlyph(font, glyph, trans); + 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 const *style, SPStyle const *context_style) +{ + DrawingGroup::setStyle(style, context_style); + + auto vector_effect_stroke = false; + auto stroke_extensions_hairline = false; + auto clip_rule = SP_WIND_RULE_EVENODD; + if (_style) { + vector_effect_stroke = _style->vector_effect.stroke; + stroke_extensions_hairline = _style->stroke_extensions.hairline; + clip_rule = _style->clip_rule.computed; + } + + defer([=, nrstyle = NRStyleData(_style, _context_style)] () mutable { + _nrstyle.set(std::move(nrstyle)); + style_vector_effect_stroke = vector_effect_stroke; + style_stroke_extensions_hairline = stroke_extensions_hairline; + style_clip_rule = clip_rule; + }); +} + +void DrawingText::setChildrenStyle(SPStyle const *context_style) +{ + DrawingGroup::setChildrenStyle(context_style); + + defer([this, nrstyle = NRStyleData(_style, _context_style)] () mutable { + _nrstyle.set(std::move(nrstyle)); + }); +} + +unsigned DrawingText::_updateItem(Geom::IntRect const &area, UpdateContext const &ctx, unsigned flags, unsigned reset) +{ + _nrstyle.invalidate(); + 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) const +{ + double wave[16]={ + // clang-format off + 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, + // clang-format on + }; + int dashes[16]={ + // clang-format off + 8, 7, 6, 5, + 4, 3, 2, 1, + -8, -7, -6, -5, + -4, -3, -2, -1 + // clang-format on + }; + int dots[16]={ + // clang-format off + 4, 3, 2, 1, + -4, -3, -2, -1, + 4, 3, 2, 1, + -4, -3, -2, -1 + // clang-format on + }; + 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.data.text_decoration_style & NRStyleData::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.data.text_decoration_style & NRStyleData::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.data.text_decoration_style & NRStyleData::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.data.text_decoration_style & NRStyleData::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) const +{ + if ( _nrstyle.data.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.data.tspan_width / _nrstyle.data.font_size; + double tsp_asc_adj = _nrstyle.data.ascender / _nrstyle.data.font_size; + double tsp_size_adj = (_nrstyle.data.ascender + _nrstyle.data.descender) / _nrstyle.data.font_size; + + double final_underline_thickness = CLAMP(_nrstyle.data.underline_thickness, tsp_size_adj/30.0, tsp_size_adj/10.0); + double final_line_through_thickness = CLAMP(_nrstyle.data.line_through_thickness, tsp_size_adj/30.0, tsp_size_adj/10.0); + + double xphase = phase_length/ _nrstyle.data.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.data.text_decoration_line & NRStyleData::TEXT_DECORATION_LINE_UNDERLINE){ + p1 = Geom::Point(0.0, -_nrstyle.data.underline_position); + p2 = Geom::Point(tsp_width_adj,-_nrstyle.data.underline_position); + decorateStyle(dc, tsp_size_adj, xphase, p1, p2, thickness); + } + + if(_nrstyle.data.text_decoration_line & NRStyleData::TEXT_DECORATION_LINE_OVERLINE){ + p1 = Geom::Point(0.0, tsp_asc_adj -_nrstyle.data.underline_position + 1 * final_underline_thickness); + p2 = Geom::Point(tsp_width_adj,tsp_asc_adj -_nrstyle.data.underline_position + 1 * final_underline_thickness); + decorateStyle(dc, tsp_size_adj, xphase, p1, p2, thickness); + } + + } else { + // Over + + if(_nrstyle.data.text_decoration_line & NRStyleData::TEXT_DECORATION_LINE_LINETHROUGH){ + thickness = final_line_through_thickness; + p1 = Geom::Point(0.0, _nrstyle.data.line_through_position); + p2 = Geom::Point(tsp_width_adj,_nrstyle.data.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.data.text_decoration_line & NRStyleData::TEXT_DECORATION_LINE_BLINK){ + thickness = final_line_through_thickness; + p1 = Geom::Point(0.0, _nrstyle.data.line_through_position - 2*final_line_through_thickness); + p2 = Geom::Point(tsp_width_adj,_nrstyle.data.line_through_position - 2*final_line_through_thickness); + decorateStyle(dc, tsp_size_adj, xphase, p1, p2, thickness); + p1 = Geom::Point(0.0, _nrstyle.data.line_through_position + 2*final_line_through_thickness); + p2 = Geom::Point(tsp_width_adj,_nrstyle.data.line_through_position + 2*final_line_through_thickness); + decorateStyle(dc, tsp_size_adj, xphase, p1, p2, thickness); + } + } +} + +unsigned DrawingText::_renderItem(DrawingContext &dc, RenderContext &rc, Geom::IntRect const &area, unsigned flags, DrawingItem const *stop_at) const +{ + auto visible = area & _bbox; + if (!visible) return RENDER_OK; + + bool outline = flags & RENDER_OUTLINE; + + if (outline) { + auto rgba = rc.outline_color; + Inkscape::DrawingContext::Save save(dc); + dc.setSource(rgba); + dc.setTolerance(0.5); // low quality, but good enough for outline mode + + for (auto & i : _children) { + auto g = 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->pathvec){ + dc.path(*g->pathvec); + 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.data.text_decoration_line != NRStyleData::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. + CairoPatternUniqPtr has_stroke; + CairoPatternUniqPtr has_fill; + CairoPatternUniqPtr has_td_fill; + CairoPatternUniqPtr has_td_stroke; + + { + Inkscape::DrawingContext::Save save(dc); + dc.transform(_ctm); + + has_fill = _nrstyle.prepareFill (dc, rc, *visible, _item_bbox, _fill_pattern); + has_stroke = _nrstyle.prepareStroke(dc, rc, *visible, _item_bbox, _stroke_pattern); + + // Avoid creating patterns if not needed + if (decorate) { + has_td_fill = _nrstyle.prepareTextDecorationFill (dc, rc, *visible, _item_bbox, _fill_pattern); + has_td_stroke = _nrstyle.prepareTextDecorationStroke(dc, rc, *visible, _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.data.paint_order_layer[0] == NRStyleData::PAINT_ORDER_NORMAL || + _nrstyle.data.paint_order_layer[0] == NRStyleData::PAINT_ORDER_FILL || + _nrstyle.data.paint_order_layer[2] == NRStyleData::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) { + + auto g = 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 (std::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, has_td_fill); + dc.fillPreserve(); + } + + if (has_td_stroke) { + _nrstyle.applyTextDecorationStroke(dc, has_td_stroke); + dc.strokePreserve(); + } + + if (has_td_fill && !fill_first) { + _nrstyle.applyTextDecorationFill(dc, has_td_fill); + dc.fillPreserve(); + } + + } + + dc.newPath(); // Clear text-decoration path + } + + // Accumulate the path that represents the glyphs and/or draw SVG glyphs. + for (auto &i : _children) { + auto g = cast<DrawingGlyphs>(&i); + if (!g) throw InvalidItemException(); + + Inkscape::DrawingContext::Save save(dc); + if (g->_ctm.isSingular()) continue; + dc.transform(g->_ctm); + if (g->pathvec) { + if (g->pixbuf) { + // Geom::OptRect box = bounds_exact(*g->pathvec); + // 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->design_units; + if (scale <= 0) scale = 1000; + Inkscape::DrawingContext::Save save(dc); + dc.translate(0, 1); + dc.scale(1.0 / scale, -1.0 / scale); + dc.setSource(g->pixbuf->getSurfaceRaw(), 0, 0); + dc.paint(1); + } + } else { + dc.path(*g->pathvec); + } + } + } + + // Draw the glyphs (non-SVG glyphs). + { + Inkscape::DrawingContext::Save save(dc); + dc.transform(_ctm); + if (has_fill && fill_first) { + auto dl = DitherLock(dc, _nrstyle.data.fill.ditherable() && _drawing.useDithering()); + _nrstyle.applyFill(dc, has_fill); + dc.fillPreserve(); + } + } + { + Inkscape::DrawingContext::Save save(dc); + if (!style_vector_effect_stroke) { + dc.transform(_ctm); + } + if (has_stroke) { + auto dl = DitherLock(dc, _nrstyle.data.stroke.ditherable() && _drawing.useDithering()); + _nrstyle.applyStroke(dc, has_stroke); + + // If the stroke is a hairline, set it to exactly 1px on screen. + // If visible hairline mode is on, make sure the line is at least 1px. + if (flags & RENDER_VISIBLE_HAIRLINES || style_stroke_extensions_hairline) { + double dx = 1.0, dy = 0.0; + dc.device_to_user_distance(dx, dy); + auto pixel_size = std::hypot(dx, dy); + if (style_stroke_extensions_hairline || _nrstyle.data.stroke_width < pixel_size) { + dc.setHairline(); + } + } + + dc.strokePreserve(); + } + } + { + Inkscape::DrawingContext::Save save(dc); + dc.transform(_ctm); + if (has_fill && !fill_first) { + auto dl = DitherLock(dc, _nrstyle.data.fill.ditherable() && _drawing.useDithering()); + _nrstyle.applyFill(dc, has_fill); + 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, has_td_fill); + dc.fillPreserve(); + } + + if (has_td_stroke) { + _nrstyle.applyTextDecorationStroke(dc, has_td_stroke); + dc.strokePreserve(); + } + + if (has_td_fill && !fill_first) { + _nrstyle.applyTextDecorationFill(dc, has_td_fill); + dc.fillPreserve(); + } + + } + + dc.newPath(); // Clear text-decoration path + } + + } + return RENDER_OK; +} + +void DrawingText::_clipItem(DrawingContext &dc, RenderContext &rc, Geom::IntRect const &/*area*/) const +{ + Inkscape::DrawingContext::Save save(dc); + + if (style_clip_rule == SP_WIND_RULE_EVENODD) { + dc.setFillRule(CAIRO_FILL_RULE_EVEN_ODD); + } else { + dc.setFillRule(CAIRO_FILL_RULE_WINDING); + } + + for (auto & i : _children) { + auto g = cast<DrawingGlyphs>(&i); + if (!g) { + throw InvalidItemException(); + } + + Inkscape::DrawingContext::Save save(dc); + dc.transform(g->_ctm); + if (g->pathvec){ + dc.path(*g->pathvec); + } + } + dc.fill(); +} + +DrawingItem *DrawingText::_pickItem(Geom::Point const &p, double delta, unsigned flags) +{ + return DrawingGroup::_pickItem(p, delta, flags) ? 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-text.h b/src/display/drawing-text.h new file mode 100644 index 0000000..84b43a9 --- /dev/null +++ b/src/display/drawing-text.h @@ -0,0 +1,104 @@ +// 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 INKSCAPE_DISPLAY_DRAWING_TEXT_H +#define INKSCAPE_DISPLAY_DRAWING_TEXT_H + +#include <memory> +#include "display/drawing-group.h" +#include "display/nr-style.h" + +class SPStyle; +class FontInstance; + +namespace Inkscape { + +class Pixbuf; + +class DrawingGlyphs + : public DrawingItem +{ +public: + DrawingGlyphs(Drawing &drawing); + int tag() const override { return tag_of<decltype(*this)>; } + + void setGlyph(std::shared_ptr<FontInstance> font, int glyph, Geom::Affine const &trans); + void setStyle(SPStyle const *style, SPStyle const *context_style = nullptr) override; // Not to be used + Geom::IntRect getPickBox() const { return _pick_bbox; }; + +protected: + ~DrawingGlyphs() override = default; + + 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; + + std::shared_ptr<void const> _font_data; // keeps alive pathvec, pathvec_ref, and pixbuf + int _glyph; + float _width; // These three are used to set up bounding box + float _asc; // + float _dsc; // + float _pl; // phase length + Geom::IntRect _pick_bbox; + + double design_units; + Geom::PathVector const *pathvec; // pathvector of actual glyph + Geom::PathVector const *pathvec_ref; // pathvector of reference glyph 42 + Inkscape::Pixbuf const *pixbuf; // pixbuf, if SVG font + + friend class DrawingText; +}; + +class DrawingText + : public DrawingGroup +{ +public: + DrawingText(Drawing &drawing); + int tag() const override { return tag_of<decltype(*this)>; } + + bool addComponent(std::shared_ptr<FontInstance> const &font, int glyph, Geom::Affine const &trans, float width, float ascent, float descent, float phase_length); + void setStyle(SPStyle const *style, SPStyle const *context_style = nullptr) override; + void setChildrenStyle(SPStyle const *context_style) override; + +protected: + ~DrawingText() override = default; + + unsigned _updateItem(Geom::IntRect const &area, UpdateContext const &ctx, unsigned flags, unsigned reset) override; + unsigned _renderItem(DrawingContext &dc, RenderContext &rc, Geom::IntRect const &area, unsigned flags, DrawingItem const *stop_at) const override; + void _clipItem(DrawingContext &dc, RenderContext &rc, Geom::IntRect const &area) const override; + DrawingItem *_pickItem(Geom::Point const &p, double delta, unsigned flags) override; + bool _canClip() const override { return true; } + + void decorateItem(DrawingContext &dc, double phase_length, bool under) const; + void decorateStyle(DrawingContext &dc, double vextent, double xphase, Geom::Point const &p1, Geom::Point const &p2, double thickness) const; + NRStyle _nrstyle; + + bool style_vector_effect_stroke : 1; + bool style_stroke_extensions_hairline : 1; + SPWindRule style_clip_rule; + + friend class DrawingGlyphs; +}; + +} // namespace Inkscape + +#endif // INKSCAPE_DISPLAY_DRAWING_TEXT_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..6e4966b --- /dev/null +++ b/src/display/drawing.cpp @@ -0,0 +1,390 @@ +// 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 <array> +#include <thread> +#include "display/drawing.h" +#include "display/control/canvas-item-drawing.h" +#include "nr-filter-gaussian.h" +#include "nr-filter-types.h" + +// Grayscale colormode +#include "cairo-templates.h" +#include "drawing-context.h" + +namespace Inkscape { + +// Hardcoded grayscale color matrix values as default. +static auto constexpr grayscale_matrix = std::array{ + 0.21, 0.72, 0.072, 0.0, 0.0, + 0.21, 0.72, 0.072, 0.0, 0.0, + 0.21, 0.72, 0.072, 0.0, 0.0, + 0.0 , 0.0 , 0.0 , 1.0, 0.0 +}; + +static auto rendermode_to_renderflags(RenderMode mode) +{ + switch (mode) { + case RenderMode::OUTLINE: return DrawingItem::RENDER_OUTLINE; + case RenderMode::NO_FILTERS: return DrawingItem::RENDER_NO_FILTERS; + case RenderMode::VISIBLE_HAIRLINES: return DrawingItem::RENDER_VISIBLE_HAIRLINES; + default: return DrawingItem::RenderFlags::RENDER_DEFAULT; + } +} + +static auto default_numthreads() +{ + auto ret = std::thread::hardware_concurrency(); + return ret == 0 ? 4 : ret; // Sensible fallback if not reported. +} + +Drawing::Drawing(Inkscape::CanvasItemDrawing *canvas_item_drawing) + : _canvas_item_drawing(canvas_item_drawing) + , _grayscale_matrix(std::vector<double>(grayscale_matrix.begin(), grayscale_matrix.end())) +{ + _loadPrefs(); +} + +Drawing::~Drawing() +{ + delete _root; +} + +void Drawing::setRoot(DrawingItem *root) +{ + delete _root; + _root = root; + if (_root) { + assert(_root->_child_type == DrawingItem::ChildType::ORPHAN); + _root->_child_type = DrawingItem::ChildType::ROOT; + } +} + +void Drawing::setRenderMode(RenderMode mode) +{ + assert(mode != RenderMode::OUTLINE_OVERLAY && "Drawing::setRenderMode: OUTLINE_OVERLAY is not a true render mode"); + + defer([=] { + if (mode == _rendermode) return; + _root->_markForRendering(); + _rendermode = mode; + _root->_markForUpdate(DrawingItem::STATE_ALL, true); + _clearCache(); + }); +} + +void Drawing::setColorMode(ColorMode mode) +{ + defer([=] { + if (mode == _colormode) return; + _colormode = mode; + if (_rendermode != RenderMode::OUTLINE || _image_outline_mode) { + _root->_markForRendering(); + } + }); +} + +void Drawing::setOutlineOverlay(bool outlineoverlay) +{ + defer([=] { + if (outlineoverlay == _outlineoverlay) return; + _outlineoverlay = outlineoverlay; + _root->_markForUpdate(DrawingItem::STATE_ALL, true); + }); +} + +void Drawing::setGrayscaleMatrix(double value_matrix[20]) +{ + defer([=] { + _grayscale_matrix = Filters::FilterColorMatrix::ColorMatrixMatrix(std::vector<double>(value_matrix, value_matrix + 20)); + if (_rendermode != RenderMode::OUTLINE) { + _root->_markForRendering(); + } + }); +} + +void Drawing::setClipOutlineColor(uint32_t col) +{ + defer([=] { + _clip_outline_color = col; + if (_rendermode == RenderMode::OUTLINE || _outlineoverlay) { + _root->_markForRendering(); + } + }); +} + +void Drawing::setMaskOutlineColor(uint32_t col) +{ + defer([=] { + _mask_outline_color = col; + if (_rendermode == RenderMode::OUTLINE || _outlineoverlay) { + _root->_markForRendering(); + } + }); +} + +void Drawing::setImageOutlineColor(uint32_t col) +{ + defer([=] { + _image_outline_color = col; + if ((_rendermode == RenderMode::OUTLINE || _outlineoverlay) && !_image_outline_mode) { + _root->_markForRendering(); + } + }); +} + +void Drawing::setImageOutlineMode(bool enabled) +{ + defer([=] { + _image_outline_mode = enabled; + if (_rendermode == RenderMode::OUTLINE || _outlineoverlay) { + _root->_markForRendering(); + } + }); +} + +void Drawing::setFilterQuality(int quality) +{ + defer([=] { + _filter_quality = quality; + if (!(_rendermode == RenderMode::OUTLINE || _rendermode == RenderMode::NO_FILTERS)) { + _root->_markForUpdate(DrawingItem::STATE_ALL, true); + _clearCache(); + } + }); +} + +void Drawing::setBlurQuality(int quality) +{ + defer([=] { + _blur_quality = quality; + if (!(_rendermode == RenderMode::OUTLINE || _rendermode == RenderMode::NO_FILTERS)) { + _root->_markForUpdate(DrawingItem::STATE_ALL, true); + _clearCache(); + } + }); +} + +void Drawing::setDithering(bool use_dithering) +{ + defer([=] { + _use_dithering = use_dithering; + #ifdef CAIRO_HAS_DITHER + if (_rendermode != RenderMode::OUTLINE) { + _root->_markForUpdate(DrawingItem::STATE_ALL, true); + _clearCache(); + } + #endif + }); +} + +void Drawing::setCacheBudget(size_t bytes) +{ + defer([=] { + _cache_budget = bytes; + _pickItemsForCaching(); + }); +} + +void Drawing::setCacheLimit(Geom::OptIntRect const &rect) +{ + defer([=] { + _cache_limit = rect; + for (auto item : _cached_items) { + item->_markForUpdate(DrawingItem::STATE_CACHE, false); + } + }); +} + +void Drawing::setClip(std::optional<Geom::PathVector> &&clip) +{ + defer([=] { + if (clip == _clip) return; + _clip = std::move(clip); + _root->_markForRendering(); + }); +} + +void Drawing::update(Geom::IntRect const &area, Geom::Affine const &affine, unsigned flags, unsigned reset) +{ + if (_root) { + _root->update(area, { affine }, flags, reset); + } + if (flags & DrawingItem::STATE_CACHE) { + // Process the updated cache scores. + _pickItemsForCaching(); + } +} + +void Drawing::render(DrawingContext &dc, Geom::IntRect const &area, unsigned flags, int antialiasing_override) const +{ + int antialias = _root->antialiasing(); + if (antialiasing_override >= 0) { + antialias = antialiasing_override; + } + apply_antialias(dc, antialias); + + auto rc = RenderContext{ 0xff }; // black outlines + flags |= rendermode_to_renderflags(_rendermode); + + if (_clip) { + dc.save(); + dc.path(*_clip * _root->_ctm); + dc.clip(); + } + _root->render(dc, rc, area, flags); + if (_clip) { + dc.restore(); + } +} + +DrawingItem *Drawing::pick(Geom::Point const &p, double delta, unsigned flags) +{ + return _root->pick(p, delta, flags); +} + +void Drawing::snapshot() +{ + assert(!_snapshotted); + _snapshotted = true; +} + +void Drawing::unsnapshot() +{ + assert(_snapshotted); + _snapshotted = false; // Unsnapshot before replaying log so further work is not deferred. + _funclog(); +} + +void Drawing::_pickItemsForCaching() +{ + // Build sorted list of items that should be cached. + std::vector<DrawingItem*> to_cache; + size_t used = 0; + for (auto &rec : _candidate_items) { + if (used + rec.cache_size > _cache_budget) break; + to_cache.emplace_back(rec.item); + used += rec.cache_size; + } + std::sort(to_cache.begin(), to_cache.end()); + + // Uncache the items that are cached but should not be cached. + // Note: setCached() modifies _cached_items, so the temporary container is necessary. + std::vector<DrawingItem*> to_uncache; + std::set_difference(_cached_items.begin(), _cached_items.end(), + to_cache.begin(), to_cache.end(), + std::back_inserter(to_uncache)); + for (auto item : to_uncache) { + item->_setCached(false); + } + + // Cache all items that should be cached (no-op if already cached). + for (auto item : to_cache) { + item->_setCached(true); + } +} + +void Drawing::_clearCache() +{ + // Note: setCached() modifies _cached_items, so the temporary container is necessary. + std::vector<DrawingItem*> to_uncache; + std::copy(_cached_items.begin(), _cached_items.end(), std::back_inserter(to_uncache)); + for (auto item : to_uncache) { + item->_setCached(false, true); + } +} + +void Drawing::_loadPrefs() +{ + auto prefs = Inkscape::Preferences::get(); + + // Set the initial values of preferences. + _clip_outline_color = prefs->getIntLimited("/options/wireframecolors/clips", 0x00ff00ff, 0, 0xffffffff); // Green clip outlines by default. + _mask_outline_color = prefs->getIntLimited("/options/wireframecolors/masks", 0x0000ffff, 0, 0xffffffff); // Blue mask outlines by default. + _image_outline_color = prefs->getIntLimited("/options/wireframecolors/images", 0xff0000ff, 0, 0xffffffff); // Red image outlines by default. + _image_outline_mode = prefs->getBool ("/options/rendering/imageinoutlinemode", false); + _filter_quality = prefs->getIntLimited("/options/filterquality/value", 0, Filters::FILTER_QUALITY_WORST, Filters::FILTER_QUALITY_BEST); + _blur_quality = prefs->getInt ("/options/blurquality/value", 0); + _use_dithering = prefs->getBool ("/options/dithering/value", true); + _cursor_tolerance = prefs->getDouble ("/options/cursortolerance/value", 1.0); + _select_zero_opacity = prefs->getBool ("/options/selection/zeroopacity", false); + + // Enable caching only for the Canvas's drawing, since only it is persistent. + if (_canvas_item_drawing) { + // Preference is stored in MiB; convert to bytes, taking care not to overflow. + _cache_budget = (size_t{1} << 20) * prefs->getIntLimited("/options/renderingcache/size", 64, 0, 4096); + } else { + _cache_budget = 0; + } + + // Set the global variable governing the number of filter threads, and track it too. (This is ugly, but hopefully transitional.) + set_num_filter_threads(prefs->getIntLimited("/options/threading/numthreads", default_numthreads(), 1, 256)); + + // Similarly, enable preference tracking only for the Canvas's drawing. + if (_canvas_item_drawing) { + std::unordered_map<std::string, std::function<void (Preferences::Entry const &)>> actions; + + // Todo: (C++20) Eliminate this repetition by baking the preference metadata into the variables themselves using structural templates. + actions.emplace("/options/wireframecolors/clips", [this] (auto &entry) { setClipOutlineColor (entry.getIntLimited(0x00ff00ff, 0, 0xffffffff)); }); + actions.emplace("/options/wireframecolors/masks", [this] (auto &entry) { setMaskOutlineColor (entry.getIntLimited(0x0000ffff, 0, 0xffffffff)); }); + actions.emplace("/options/wireframecolors/images", [this] (auto &entry) { setImageOutlineColor(entry.getIntLimited(0xff0000ff, 0, 0xffffffff)); }); + actions.emplace("/options/rendering/imageinoutlinemode", [this] (auto &entry) { setImageOutlineMode(entry.getBool(false)); }); + actions.emplace("/options/filterquality/value", [this] (auto &entry) { setFilterQuality(entry.getIntLimited(0, Filters::FILTER_QUALITY_WORST, Filters::FILTER_QUALITY_BEST)); }); + actions.emplace("/options/blurquality/value", [this] (auto &entry) { setBlurQuality(entry.getInt(0)); }); + actions.emplace("/options/dithering/value", [this] (auto &entry) { setDithering(entry.getBool(true)); }); + actions.emplace("/options/cursortolerance/value", [this] (auto &entry) { setCursorTolerance(entry.getDouble(1.0)); }); + actions.emplace("/options/selection/zeroopacity", [this] (auto &entry) { setSelectZeroOpacity(entry.getBool(false)); }); + actions.emplace("/options/renderingcache/size", [this] (auto &entry) { setCacheBudget((1 << 20) * entry.getIntLimited(64, 0, 4096)); }); + actions.emplace("/options/threading/numthreads", [this] (auto &entry) { set_num_filter_threads(entry.getIntLimited(default_numthreads(), 1, 256)); }); + + _pref_tracker = Inkscape::Preferences::PreferencesObserver::create("/options", [actions = std::move(actions)] (auto &entry) { + auto it = actions.find(entry.getPath()); + if (it == actions.end()) return; + it->second(entry); + }); + } +} + +/* + * Return average color over area. Used by Calligraphic, Dropper, and Spray tools. + */ +void Drawing::averageColor(Geom::IntRect const &area, double &R, double &G, double &B, double &A) const +{ + auto surface = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, area.width(), area.height()); + auto dc = Inkscape::DrawingContext(surface->cobj(), area.min()); + render(dc, area); + + ink_cairo_surface_average_color_premul(surface->cobj(), R, G, B, A); +} + +/* + * Convenience function to set high quality options for export. + */ +void Drawing::setExact() +{ + setFilterQuality(Filters::FILTER_QUALITY_BEST); + setBlurQuality(BLUR_QUALITY_BEST); +} + +} // 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..0fc676b --- /dev/null +++ b/src/display/drawing.h @@ -0,0 +1,151 @@ +// 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 INKSCAPE_DISPLAY_DRAWING_H +#define INKSCAPE_DISPLAY_DRAWING_H + +#include <set> +#include <cstdint> +#include <vector> +#include <boost/operators.hpp> +#include <2geom/rect.h> +#include <2geom/pathvector.h> +#include <sigc++/sigc++.h> + +#include "display/drawing-item.h" +#include "display/rendermode.h" +#include "nr-filter-colormatrix.h" +#include "preferences.h" +#include "util/funclog.h" + +namespace Inkscape { + +class DrawingItem; +class CanvasItemDrawing; +class DrawingContext; + +class Drawing +{ +public: + Drawing(CanvasItemDrawing *drawing = nullptr); + Drawing(Drawing const &) = delete; + Drawing &operator=(Drawing const &) = delete; + ~Drawing(); + + void setRoot(DrawingItem *root); + DrawingItem *root() { return _root; } + CanvasItemDrawing *getCanvasItemDrawing() { return _canvas_item_drawing; } + + void setRenderMode(RenderMode); + void setColorMode(ColorMode); + void setOutlineOverlay(bool); + void setGrayscaleMatrix(double[20]); + void setClipOutlineColor(uint32_t); + void setMaskOutlineColor(uint32_t); + void setImageOutlineColor(uint32_t); + void setImageOutlineMode(bool); + void setFilterQuality(int); + void setBlurQuality(int); + void setDithering(bool); + void setCursorTolerance(double tol) { _cursor_tolerance = tol; } + void setSelectZeroOpacity(bool select_zero_opacity) { _select_zero_opacity = select_zero_opacity; } + void setCacheBudget(size_t bytes); + void setCacheLimit(Geom::OptIntRect const &rect); + void setClip(std::optional<Geom::PathVector> &&clip); + + RenderMode renderMode() const { return _rendermode; } + ColorMode colorMode() const { return _colormode; } + bool outlineOverlay() const { return _outlineoverlay; } + auto &grayscaleMatrix() const { return _grayscale_matrix; } + uint32_t clipOutlineColor() const { return _clip_outline_color; } + uint32_t maskOutlineColor() const { return _mask_outline_color; } + uint32_t imageOutlineColor() const { return _image_outline_color; } + bool imageOutlineMode() const { return _image_outline_mode; } + int filterQuality() const { return _filter_quality; } + int blurQuality() const { return _blur_quality; } + bool useDithering() const { return _use_dithering; } + double cursorTolerance() const { return _cursor_tolerance; } + bool selectZeroOpacity() const { return _select_zero_opacity; } + Geom::OptIntRect const &cacheLimit() const { return _cache_limit; } + + void update(Geom::IntRect const &area = Geom::IntRect::infinite(), Geom::Affine const &affine = Geom::identity(), + unsigned flags = DrawingItem::STATE_ALL, unsigned reset = 0); + void render(DrawingContext &dc, Geom::IntRect const &area, unsigned flags = 0, int antialiasing_override = -1) const; + DrawingItem *pick(Geom::Point const &p, double delta, unsigned flags); + + void snapshot(); + void unsnapshot(); + bool snapshotted() const { return _snapshotted; } + + // Convenience + void averageColor(Geom::IntRect const &area, double &R, double &G, double &B, double &A) const; + void setExact(); + +private: + void _pickItemsForCaching(); + void _clearCache(); + void _loadPrefs(); + + DrawingItem *_root = nullptr; + CanvasItemDrawing *_canvas_item_drawing = nullptr; + std::unique_ptr<Preferences::PreferencesObserver> _pref_tracker; + + RenderMode _rendermode = RenderMode::NORMAL; + ColorMode _colormode = ColorMode::NORMAL; + bool _outlineoverlay = false; + Filters::FilterColorMatrix::ColorMatrixMatrix _grayscale_matrix; + uint32_t _clip_outline_color; + uint32_t _mask_outline_color; + uint32_t _image_outline_color; + bool _image_outline_mode; ///< Always draw images as images, even in outline mode. + int _filter_quality; + int _blur_quality; + bool _use_dithering; + double _cursor_tolerance; + size_t _cache_budget; ///< Maximum allowed size of cache. + Geom::OptIntRect _cache_limit; + std::optional<Geom::PathVector> _clip; + bool _select_zero_opacity; + + std::set<DrawingItem*> _cached_items; // modified by DrawingItem::_setCached() + CacheList _candidate_items; // keep this list always sorted with std::greater + + /* + * Simple cacheline separator compatible with x86 (64 bytes) and M* (128 bytes). + * Ideally alignas(std::hardware_destructive_interference_size) could be used instead, + * but this is extremely painful to make work across all supported platforms/compilers. + */ + char cacheline_separator[127]; + + bool _snapshotted = false; + Util::FuncLog _funclog; + + template<typename F> + void defer(F &&f) { _snapshotted ? _funclog.emplace(std::forward<F>(f)) : f(); } + + friend class DrawingItem; +}; + +} // namespace Inkscape + +#endif // INKSCAPE_DISPLAY_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/initlock.h b/src/display/initlock.h new file mode 100644 index 0000000..1a94069 --- /dev/null +++ b/src/display/initlock.h @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INITLOCK_H +#define INITLOCK_H + +#include <atomic> +#include <mutex> + +/** + * Almost entirely analogous to std::once_flag, but with the ability to be reset if the caller knows it is safe to do so. + */ +class InitLock +{ +public: + template <typename F> + void init(F &&f) const + { + // Make sure the fast path is a single load-acquire - not guaranteed with std::once_flag. + [[likely]] if (inited.load(std::memory_order_acquire)) { + return; + } + + std::call_once(once, [&] { + f(); + inited.store(true, std::memory_order_release); + }); + } + + void reset() + { + inited.store(false, std::memory_order_relaxed); + + // Abomination, but tenuously allowed by Standard. + // (The alternative, std::mutex, is too large - we want to create a lot of these objects.) + once.~once_flag(); + new (&once) std::once_flag(); + } + +private: + mutable std::once_flag once; + mutable std::atomic<bool> inited = false; +}; + +#endif // INITLOCK_H 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..52b595c --- /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 euclidean norm of the vector v + * + * \param v a reference to a vector with double components + * \return the euclidean 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..249a5df --- /dev/null +++ b/src/display/nr-filter-blend.cpp @@ -0,0 +1,172 @@ +// 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" + +namespace Inkscape { +namespace Filters { + +std::set<SPBlendMode> const FilterBlend::_valid_modes { + // clang-format off + 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 + // clang-format on + }; + +FilterBlend::FilterBlend() + : _blend_mode(SP_CSS_BLEND_NORMAL) + , _input2(NR_FILTER_SLOT_NOT_SET) +{ +} + +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) const +{ + 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. + set_cairo_surface_ci(input1, color_interpolation); + set_cairo_surface_ci(input2, color_interpolation); + + // 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, color_interpolation); + + 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 &) const +{ + // blend is a per-pixel primitive and is immutable under transformations + return true; +} + +double FilterBlend::complexity(Geom::Affine const &) const +{ + return 1.1; +} + +bool FilterBlend::uses_background() const +{ + 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..d05cd65 --- /dev/null +++ b/src/display/nr-filter-blend.h @@ -0,0 +1,64 @@ +// 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(); + ~FilterBlend() override; + + void render_cairo(FilterSlot &slot) const override; + bool can_handle_affine(Geom::Affine const &) const override; + double complexity(Geom::Affine const &ctm) const override; + bool uses_background() const override; + + void set_input(int slot) override; + void set_input(int input, int slot) override; + void set_mode(SPBlendMode mode); + + Glib::ustring name() const override { return Glib::ustring("Blend"); } + +private: + static const std::set<SPBlendMode> _valid_modes; + SPBlendMode _blend_mode; + int _input2; +}; + + +} // namespace Filters +} // namespace Inkscape + +#endif // SEEN_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..ae16932 --- /dev/null +++ b/src/display/nr-filter-colormatrix.cpp @@ -0,0 +1,234 @@ +// 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; + +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] = (i % 6 == 0) ? 255 : 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 = std::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] = std::round((0.213 +0.787*coshue -0.213*sinhue)*255); + _v[1] = std::round((0.715 -0.715*coshue -0.715*sinhue)*255); + _v[2] = std::round((0.072 -0.072*coshue +0.928*sinhue)*255); + + _v[3] = std::round((0.213 -0.213*coshue +0.143*sinhue)*255); + _v[4] = std::round((0.715 +0.285*coshue +0.140*sinhue)*255); + _v[5] = std::round((0.072 -0.072*coshue -0.283*sinhue)*255); + + _v[6] = std::round((0.213 -0.213*coshue -0.787*sinhue)*255); + _v[7] = std::round((0.715 -0.715*coshue +0.715*sinhue)*255); + _v[8] = std::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) const +{ + 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. + set_cairo_surface_ci(input, color_interpolation); + + if (type == COLORMATRIX_LUMINANCETOALPHA) { + out = ink_cairo_surface_create_same_size(input, CAIRO_CONTENT_ALPHA); + } else { + out = ink_cairo_surface_create_same_size(input, CAIRO_CONTENT_COLOR_ALPHA); + // Set ci to that used for computation + set_cairo_surface_ci(out, color_interpolation); + } + + 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 &) const +{ + return true; +} + +double FilterColorMatrix::complexity(Geom::Affine const &) const +{ + return 2.0; +} + +void FilterColorMatrix::set_type(FilterColorMatrixType t) +{ + type = t; +} + +void FilterColorMatrix::set_value(double v) +{ + value = v; +} + +void FilterColorMatrix::set_values(std::vector<double> 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..20d2f72 --- /dev/null +++ b/src/display/nr-filter-colormatrix.h @@ -0,0 +1,81 @@ +// 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(); + ~FilterColorMatrix() override; + + void render_cairo(FilterSlot &slot) const override; + bool can_handle_affine(Geom::Affine const &) const override; + double complexity(Geom::Affine const &ctm) const 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() const 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 // SEEN_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..31ba077 --- /dev/null +++ b/src/display/nr-filter-component-transfer.cpp @@ -0,0 +1,245 @@ +// 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; + +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); + 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] = std::round(std::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] = std::round(std::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 * std::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) const +{ + 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. + set_cairo_surface_ci(out, color_interpolation); + set_cairo_surface_ci(input, color_interpolation); + + 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_filter(out, out, MultiplyAlpha()); + + slot.set(_output, out); + cairo_surface_destroy(out); +} + +bool FilterComponentTransfer::can_handle_affine(Geom::Affine const &) const +{ + return true; +} + +double FilterComponentTransfer::complexity(Geom::Affine const &) 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..4e2b0d2 --- /dev/null +++ b/src/display/nr-filter-component-transfer.h @@ -0,0 +1,68 @@ +// 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(); + ~FilterComponentTransfer() override; + + void render_cairo(FilterSlot &slot) const override; + bool can_handle_affine(Geom::Affine const &) const override; + double complexity(Geom::Affine const &ctm) const 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() const override { return Glib::ustring("Component Transfer"); } +}; + +} // namespace Filters +} // namespace Inkscape + +#endif // SEEN_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..7817573 --- /dev/null +++ b/src/display/nr-filter-composite.cpp @@ -0,0 +1,173 @@ +// 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) {} + +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) const +{ + 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. + set_cairo_surface_ci(input1, color_interpolation); + set_cairo_surface_ci(input2, color_interpolation); + + cairo_surface_t *out = ink_cairo_surface_create_output(input1, input2); + set_cairo_surface_ci(out, color_interpolation); + + 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; + case COMPOSITE_LIGHTER: + cairo_set_operator(ct, CAIRO_OPERATOR_ADD); + break; + 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 &) 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) { + op = COMPOSITE_OVER; + } else if (op_ != COMPOSITE_ENDOPERATOR) { + 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; + } + k1 = k1_; + k2 = k2_; + k3 = k3_; + k4 = k4_; +} + +double FilterComposite::complexity(Geom::Affine const &) 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..6196303 --- /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(); + ~FilterComposite() override; + + void render_cairo(FilterSlot &) const override; + bool can_handle_affine(Geom::Affine const &) const override; + double complexity(Geom::Affine const &ctm) const 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() const override { return Glib::ustring("Composite"); } + +private: + FeCompositeOperator op; + double k1, k2, k3, k4; + int _input2; +}; + +} // namespace Filters +} // namespace Inkscape + +#endif // SEEN_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..059ae40 --- /dev/null +++ b/src/display/nr-filter-convolve-matrix.cpp @@ -0,0 +1,244 @@ +// 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; + +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) const +{ + 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. + set_cairo_surface_ci(out, color_interpolation); + set_cairo_surface_ci(input, color_interpolation); + + 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 = std::move(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*/) const +{ + //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 &) 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..44cb3e4 --- /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(); + ~FilterConvolveMatrix() override; + + void render_cairo(FilterSlot &slot) const override; + void area_enlarge(Geom::IntRect &area, Geom::Affine const &trans) const override; + double complexity(Geom::Affine const &ctm) const 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() const override { return Glib::ustring("Convolve Matrix"); } + +private: + std::vector<double> kernelMatrix; + int targetX, targetY; + int orderX, orderY; + double divisor, bias; + FilterConvolveMatrixEdgeMode edgeMode; + bool preserveAlpha; +}; + +} // namespace Filters +} // namespace Inkscape + +#endif // SEEN_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..8f80998 --- /dev/null +++ b/src/display/nr-filter-diffuselighting.cpp @@ -0,0 +1,227 @@ +// 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-color.h" + +namespace Inkscape { +namespace Filters { + +FilterDiffuseLighting::FilterDiffuseLighting() + : light_type(NO_LIGHT) + , diffuseConstant(1) + , surfaceScale(1) + , lighting_color(0xffffffff) {} + +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, DistantLightData const &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, PointLightData const &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, SpotLightData const &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) const +{ + 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 (icc) { + unsigned char 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); + } + + // Only alpha channel of input is used, no need to check input color_interpolation_filter value. + // 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 (color_interpolation == 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, color_interpolation); + 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.x(), y0 = p.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::area_enlarge(Geom::IntRect &area, Geom::Affine const & /*trans*/) const +{ + // 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 &) 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..69577b5 --- /dev/null +++ b/src/display/nr-filter-diffuselighting.h @@ -0,0 +1,72 @@ +// 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 <optional> +#include "display/nr-light-types.h" +#include "display/nr-filter-primitive.h" +#include "display/nr-filter-slot.h" +#include "display/nr-filter-units.h" +#include "svg/svg-icc-color.h" + +class SPFeDistantLight; +class SPFePointLight; +class SPFeSpotLight; +struct SVGICCColor; + +namespace Inkscape { +namespace Filters { + +class FilterDiffuseLighting : public FilterPrimitive +{ +public: + FilterDiffuseLighting(); + ~FilterDiffuseLighting() override; + + void render_cairo(FilterSlot &slot) const override; + void set_icc(SVGICCColor const &icc_) { icc = icc_; } + void area_enlarge(Geom::IntRect &area, Geom::Affine const &trans) const override; + double complexity(Geom::Affine const &ctm) const override; + + union { + DistantLightData distant; + PointLightData point; + SpotLightData spot; + } light; + LightType light_type; + double diffuseConstant; + double surfaceScale; + guint32 lighting_color; + + Glib::ustring name() const override { return "Diffuse Lighting"; } + +private: + std::optional<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..869bccb --- /dev/null +++ b/src/display/nr-filter-displacement-map.cpp @@ -0,0 +1,171 @@ +// 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 { + +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) const +{ + 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. + set_cairo_surface_ci(map, color_interpolation); + + 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; + 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: + return; + } + + if (s == 0) Xchannel = ch; + if (s == 1) Ychannel = ch; +} + +void FilterDisplacementMap::area_enlarge(Geom::IntRect &area, Geom::Affine const &trans) const +{ + //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 &) 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..e6813d6 --- /dev/null +++ b/src/display/nr-filter-displacement-map.h @@ -0,0 +1,57 @@ +// 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: + void render_cairo(FilterSlot &slot) const override; + void area_enlarge(Geom::IntRect &area, Geom::Affine const &trans) const override; + double complexity(Geom::Affine const &ctm) const override; + + void set_input(int slot) override; + void set_input(int input, int slot) override; + void set_scale(double s); + void set_channel_selector(int s, FilterDisplacementMapChannelSelector channel); + + Glib::ustring name() const override { return Glib::ustring("Displacement Map"); } + +private: + double scale; + int _input2; + unsigned Xchannel, Ychannel; +}; + +} // namespace Filters +} // namespace Inkscape + +#endif // SEEN_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..cc7fcb2 --- /dev/null +++ b/src/display/nr-filter-flood.cpp @@ -0,0 +1,126 @@ +// 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; + +FilterFlood::~FilterFlood() = default; + +void FilterFlood::render_cairo(FilterSlot &slot) const +{ + 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 (icc) { + unsigned char 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); + } + + cairo_surface_t *out = ink_cairo_surface_create_same_size(input, CAIRO_CONTENT_COLOR_ALPHA); + + // 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 (color_interpolation == 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, color_interpolation); + + // 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; + + auto d = fp_cairo.min() - sa.min(); + if (d.x() < 0.0) d.x() = 0.0; + if (d.y() < 0.0) d.y() = 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, d.x(), d.y(), 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 &) 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; +} + +double FilterFlood::complexity(Geom::Affine const &) 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..2228e00 --- /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 <optional> +#include "display/nr-filter-primitive.h" + +struct SVGICCColor; + +namespace Inkscape { +namespace Filters { + +class FilterFlood : public FilterPrimitive +{ +public: + FilterFlood(); + ~FilterFlood() override; + + void render_cairo(FilterSlot &slot) const override; + bool can_handle_affine(Geom::Affine const &) const override; + double complexity(Geom::Affine const &ctm) const override; + bool uses_background() const override { return false; } + + void set_opacity(double o); + void set_color(guint32 c); + void set_icc(SVGICCColor const &icc_) { icc = icc_; } + + Glib::ustring name() const override { return Glib::ustring("Flood"); } + +private: + double opacity; + guint32 color; + std::optional<SVGICCColor> icc; +}; + +} // namespace Filters +} // namespace Inkscape + +#endif // SEEN_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..4e04bfd --- /dev/null +++ b/src/display/nr-filter-gaussian.cpp @@ -0,0 +1,747 @@ +// 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" + +#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; +} + +FilterGaussian::~FilterGaussian() +{ +} + +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) +{ + assert(src && dest); + +#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) +{ + assert(src && dst); + + // 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) const +{ + cairo_surface_t *in = slot.getcairo(_input); + if (!(in && ink_cairo_surface_get_width(in) && ink_cairo_surface_get_height(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. + set_cairo_surface_ci(in, color_interpolation); + + // 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; + } + + int quality = slot.get_blurquality(); + int threads = get_num_filter_threads(); + 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, color_interpolation); + + slot.set(_output, upsampled); + cairo_surface_destroy(upsampled); + cairo_surface_destroy(downsampled); + } else { + set_cairo_surface_ci(downsampled, color_interpolation); + + slot.set(_output, downsampled); + cairo_surface_destroy(downsampled); + } +} + +void FilterGaussian::area_enlarge(Geom::IntRect &area, Geom::Affine const &trans) const +{ + 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 &) 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) const +{ + 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..837ca87 --- /dev/null +++ b/src/display/nr-filter-gaussian.h @@ -0,0 +1,82 @@ +// 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(); + ~FilterGaussian() override; + + void render_cairo(FilterSlot &slot) const override; + void area_enlarge(Geom::IntRect &area, Geom::Affine const &m) const override; + bool can_handle_affine(Geom::Affine const &m) const override; + double complexity(Geom::Affine const &ctm) const 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() const override { return Glib::ustring("Gaussian Blur"); } + +private: + double _deviation_x; + double _deviation_y; +}; + +} // namespace Filters +} // namespace Inkscape + +#endif // SEEN_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..efc25b6 --- /dev/null +++ b/src/display/nr-filter-image.cpp @@ -0,0 +1,231 @@ +// 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 { + +void FilterImage::update() +{ + if (!item) { + return; + } + + item->update(); +} + +void FilterImage::render_cairo(FilterSlot &slot) const +{ + if (!item) { + return; + } + + Geom::OptRect area = item->drawbox(); + if (!area) { + return; + } + + // 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.left(); + double feImageY = vp.top(); + 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(); + + 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()); + Geom::Affine user2pb = slot.get_units().get_matrix_user2pb(); + dc.transform(user2pb); // we are now in primitive units + + Geom::IntRect render_rect = area->roundOutwards(); + + // Internal image, like <use> + if (from_element) { + dc.translate(feImageX, feImageY); + item->render(dc, slot.get_rendercontext(), render_rect); + + // For the moment, we'll assume that any image is in sRGB color space + set_cairo_surface_ci(out, SP_CSS_COLOR_INTERPOLATION_SRGB); + } else { + // 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 + + // Now that we have the viewport, we must map image inside. + // Partially copied from sp-image.cpp. + + auto image_width = area->width(); + auto image_height = area->height(); + + // 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 / 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; + + dc.translate(feImageX, feImageY); + dc.scale(scaleX, scaleY); + item->render(dc, slot.get_rendercontext(), render_rect); + } + + slot.set(_output, out); + cairo_surface_destroy(out); +} + +bool FilterImage::can_handle_affine(Geom::Affine const &) const +{ + return true; +} + +double FilterImage::complexity(Geom::Affine const &) const +{ + // TODO: right now we cannot actually measure this in any meaningful way. + return 1.1; +} + +void FilterImage::set_align(unsigned align) +{ + aspect_align = align; +} + +void FilterImage::set_clip(unsigned 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..0cfca31 --- /dev/null +++ b/src/display/nr-filter-image.h @@ -0,0 +1,64 @@ +// 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 <memory> +#include <string> +#include "display/nr-filter-primitive.h" + +class SPDocument; +class SPItem; + +namespace Inkscape { +class Pixbuf; +class DrawingItem; + +namespace Filters { +class FilterSlot; + +class FilterImage : public FilterPrimitive +{ +public: + void update() override; + void render_cairo(FilterSlot &slot) const override; + bool can_handle_affine(Geom::Affine const &) const override; + double complexity(Geom::Affine const &ctm) const override; + + void set_document(SPDocument *document); + void set_href(char const *href); + void set_align(unsigned align); + void set_clip(unsigned clip); + + Glib::ustring name() const override { return Glib::ustring("Image"); } + + Inkscape::DrawingItem *item; + bool from_element = false; + + unsigned aspect_align, aspect_clip; +}; + +} // namespace Filters +} // namespace Inkscape + +#endif // SEEN_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..050a879 --- /dev/null +++ b/src/display/nr-filter-merge.cpp @@ -0,0 +1,115 @@ +// 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({NR_FILTER_SLOT_NOT_SET}) {} + +void FilterMerge::render_cairo(FilterSlot &slot) const +{ + if (_input_image.empty()) return; + + 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 (auto &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, color_interpolation); + rgba32 = true; + break; + } + } + + if (!rgba32) { + out = ink_cairo_surface_create_identical(slot.getcairo(_input_image[0])); + } + cairo_t *out_ct = cairo_create(out); + + for (auto &i : _input_image) { + cairo_surface_t *in = slot.getcairo(i); + + set_cairo_surface_ci(in, color_interpolation); + 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 &) const +{ + // Merge is a per-pixel primitive and is immutable under transformations + return true; +} + +double FilterMerge::complexity(Geom::Affine const &) const +{ + return 1.02; +} + +bool FilterMerge::uses_background() const +{ + 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 (_input_image.size() > input) { + _input_image[input] = slot; + } else { + for (int i = _input_image.size(); i < input ; i++) { + _input_image.emplace_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..c55f0df --- /dev/null +++ b/src/display/nr-filter-merge.h @@ -0,0 +1,54 @@ +// 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(); + + void render_cairo(FilterSlot &) const override; + bool can_handle_affine(Geom::Affine const &) const override; + double complexity(Geom::Affine const &ctm) const override; + bool uses_background() const override; + + void set_input(int input) override; + void set_input(int input, int slot) override; + + Glib::ustring name() const override { return Glib::ustring("Merge"); } + +private: + std::vector<int> _input_image; +}; + +} // namespace Filters +} // namespace Inkscape + +#endif // SEEN_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..1cc2aad --- /dev/null +++ b/src/display/nr-filter-morphology.cpp @@ -0,0 +1,260 @@ +// 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; + +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; + #pragma omp parallel for if(limit > OPENMP_THRESHOLD) num_threads(get_num_filter_threads()) + #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].emplace_back(-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].emplace_back(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].emplace_back(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].emplace_back(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].emplace_back(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].emplace_back(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); +} + +} // namespace + +void FilterMorphology::render_cairo(FilterSlot &slot) const +{ + 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) const +{ + int enlarge_x = std::ceil(xradius * trans.expansionX()); + int enlarge_y = std::ceil(yradius * trans.expansionY()); + + area.expandBy(enlarge_x, enlarge_y); +} + +double FilterMorphology::complexity(Geom::Affine const &trans) const +{ + int enlarge_x = std::ceil(xradius * trans.expansionX()); + int enlarge_y = std::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..e1944e0 --- /dev/null +++ b/src/display/nr-filter-morphology.h @@ -0,0 +1,65 @@ +// 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(); + ~FilterMorphology() override; + + void render_cairo(FilterSlot &slot) const override; + void area_enlarge(Geom::IntRect &area, Geom::Affine const &trans) const override; + double complexity(Geom::Affine const &ctm) const override; + + void set_operator(FilterMorphologyOperator o); + void set_xradius(double x); + void set_yradius(double y); + + Glib::ustring name() const override { return Glib::ustring("Morphology"); } + +private: + FilterMorphologyOperator Operator; + double xradius; + double yradius; +}; + +} // namespace Filters +} // namespace Inkscape + +#endif // SEEN_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..6a1ccaa --- /dev/null +++ b/src/display/nr-filter-offset.cpp @@ -0,0 +1,112 @@ +// 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) {} + +FilterOffset::~FilterOffset() = default; + +void FilterOffset::render_cairo(FilterSlot &slot) const +{ + 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 &) 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) const +{ + 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 -= std::ceil(offset[X]); + } else { + x1 -= std::floor(offset[X]); + } + + if (offset[Y] > 0) { + y0 -= std::ceil(offset[Y]); + } else { + y1 -= std::floor(offset[Y]); + } + + area = Geom::IntRect(x0, y0, x1, y1); +} + +double FilterOffset::complexity(Geom::Affine const &) 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..f27cc5d --- /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(); + ~FilterOffset() override; + + void render_cairo(FilterSlot &slot) const override; + void area_enlarge(Geom::IntRect &area, Geom::Affine const &trans) const override; + bool can_handle_affine(Geom::Affine const &) const override; + double complexity(Geom::Affine const &ctm) const override; + + void set_dx(double amount); + void set_dy(double amount); + + Glib::ustring name() const override { return Glib::ustring("Offset"); } + +private: + double dx, dy; +}; + +} // namespace Filters +} // namespace Inkscape + +#endif // SEEN_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..ac53238 --- /dev/null +++ b/src/display/nr-filter-primitive.cpp @@ -0,0 +1,195 @@ +// 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 { + +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); + + color_interpolation = SP_CSS_COLOR_INTERPOLATION_AUTO; +} + +FilterPrimitive::~FilterPrimitive() = default; + +void FilterPrimitive::render_cairo(FilterSlot &slot) const +{ + // passthrough + cairo_surface_t *in = slot.getcairo(_input); + slot.set(_output, in); +} + +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) const +{ + 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::from_xywh(0, 0, 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.left(); + if (!_subregion_y._set) y = fa.top(); + 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::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. + auto const len_x = bb.width(); + auto const len_y = bb.height(); + + auto compute = [] (SVGLength length, double scale) { + length.update(12, 6, scale); + return length.computed; + }; + + auto const subregion_x_computed = compute(_subregion_x, len_x); + auto const subregion_y_computed = compute(_subregion_y, len_y); + auto const subregion_w_computed = compute(_subregion_width, len_x); + auto const subregion_h_computed = compute(_subregion_height, len_y); + + // Values are in terms of fraction of bounding box. + if (_subregion_x._set && _subregion_x.unit != SVGLength::PERCENT) x = bb.left() + bb.width() * _subregion_x.value; + if (_subregion_y._set && _subregion_y.unit != SVGLength::PERCENT) y = bb.top() + 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.left() + subregion_x_computed; + if (_subregion_y._set && _subregion_y.unit == SVGLength::PERCENT) y = bb.top() + subregion_y_computed; + if (_subregion_width._set && _subregion_width.unit == SVGLength::PERCENT) width = subregion_w_computed; + if (_subregion_height._set && _subregion_height.unit == SVGLength::PERCENT) height = subregion_h_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::from_xywh(x, y, width, height); +} + +void FilterPrimitive::setStyle(SPStyle const *style) +{ + if (style) { + color_interpolation = style->color_interpolation_filters.computed; + } else { + color_interpolation = SP_CSS_COLOR_INTERPOLATION_AUTO; + } +} + +} // 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..82891a7 --- /dev/null +++ b/src/display/nr-filter-primitive.h @@ -0,0 +1,151 @@ +// 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 <memory> +#include <2geom/forward.h> +#include <2geom/rect.h> + +#include <glibmm/ustring.h> + +#include "display/nr-filter-types.h" +#include "svg/svg-length.h" +#include "style-enums.h" + +class SPStyle; + +namespace Inkscape { +namespace Filters { + +class FilterSlot; +class FilterUnits; + +class FilterPrimitive +{ +public: + FilterPrimitive(); + virtual ~FilterPrimitive(); + + virtual void update() {} + virtual void render_cairo(FilterSlot &slot) const; + virtual void area_enlarge(Geom::IntRect &area, Geom::Affine const &m) const {} + + /** + * 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*/) const { return 1.0; } + + virtual bool uses_background() const + { + return _input == NR_FILTER_BACKGROUNDIMAGE || _input == NR_FILTER_BACKGROUNDALPHA; + } + + /** + * 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); + + /** + * Returns the filter primitive area in user coordinate system. + */ + Geom::Rect filter_primitive_area(FilterUnits const &units) const; + + /** + *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 &) const { return false; } + + /** + * Sets style for access to properties used by filter primitives. + */ + void setStyle(SPStyle const *style); + + // Useful for debugging + virtual Glib::ustring name() const { return "No name"; } + +protected: + int _input; + int _output; + + /* Filter primitive subregion */ + SVGLength _subregion_x; + SVGLength _subregion_y; + SVGLength _subregion_width; + SVGLength _subregion_height; + + SPColorInterpolation color_interpolation; +}; + +} // 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..3c55005 --- /dev/null +++ b/src/display/nr-filter-skeleton.cpp @@ -0,0 +1,68 @@ +// 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() +{ +} + +FilterSkeleton::~FilterSkeleton() +{ +} + +void FilterSkeleton::render_cairo(FilterSlot &slot) const +{ + 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..111bcb5 --- /dev/null +++ b/src/display/nr-filter-skeleton.h @@ -0,0 +1,57 @@ +// 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(); + ~FilterSkeleton() override; + + void render_cairo(FilterSlot &slot) const override; +}; + +} // namespace Filters +} // namespace Inkscape + +#endif // SEEN_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..b9d3b1a --- /dev/null +++ b/src/display/nr-filter-slot.cpp @@ -0,0 +1,280 @@ +// 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 "cairo-utils.h" +#include "drawing-context.h" +#include "drawing-surface.h" +#include "nr-filter-types.h" +#include "nr-filter-gaussian.h" +#include "nr-filter-slot.h" +#include "nr-filter-units.h" + +namespace Inkscape { +namespace Filters { + +FilterSlot::FilterSlot(DrawingContext *bgdc, DrawingContext &graphic, FilterUnits const &units, RenderContext &rc, int blurquality) + : _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(units) + , _last_out(NR_FILTER_SOURCEGRAPHIC) + , _blurquality(blurquality) + , rc(rc) + , device_scale(graphic.surface()->device_scale()) +{ + 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 = std::ceil(bbox_trans.width()); + _slot_h = std::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); + } + + if (s->second && cairo_surface_status(s->second) == CAIRO_STATUS_NO_MEMORY) { + // Simulate Cairomm behaviour by throwing std::bad_alloc. + throw std::bad_alloc(); + } + + return s->second; +} + +cairo_surface_t *FilterSlot::_get_transformed_source_graphic() const +{ + 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() const +{ + 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 * device_scale, _slot_h * device_scale); + } + + 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) const +{ + if (slot_nr == NR_FILTER_SLOT_NOT_SET) + slot_nr = _last_out; + + auto s = _primitiveAreas.find(slot_nr); + + if (s == _primitiveAreas.end()) { + return *_units.get_filter_area(); + } + return s->second; +} + +Geom::Rect FilterSlot::get_slot_area() const +{ + return Geom::Rect::from_xywh(_slot_x, _slot_y, _slot_w, _slot_h); +} + +} // 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..ecd9f00 --- /dev/null +++ b/src/display/nr-filter-slot.h @@ -0,0 +1,118 @@ +// 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 "nr-filter-types.h" +#include "nr-filter-units.h" + +extern "C" { +typedef struct _cairo cairo_t; +typedef struct _cairo_surface cairo_surface_t; +} + +namespace Inkscape { +class DrawingContext; +class DrawingItem; +class RenderContext; + +namespace Filters { + +class FilterSlot final +{ +public: + /** Creates a new FilterSlot object. */ + FilterSlot(DrawingContext *bgdc, DrawingContext &graphic, FilterUnits const &units, RenderContext &rc, int blurquality); + + /** Destroys the FilterSlot object and all its contents */ + ~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) const; + + /** Returns the number of slots in use. */ + int get_slot_count() const { return _slots.size(); } + + /** Gets the gaussian filtering quality. Affects used interpolation methods */ + int get_blurquality() const { return _blurquality; } + + /** Gets the device scale; for high DPI monitors. */ + int get_device_scale() const { return device_scale; } + + FilterUnits const &get_units() const { return _units; } + Geom::Rect get_slot_area() const; + + RenderContext &get_rendercontext() const { return rc; } + +private: + using SlotMap = std::map<int, cairo_surface_t *>; + SlotMap _slots; + + // We need to keep track of the primitive area as this is needed in feTile + using PrimitiveAreaMap = std::map<int, Geom::Rect>; + PrimitiveAreaMap _primitiveAreas; + + 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; + int _blurquality; + int device_scale; + RenderContext &rc; + + cairo_surface_t *_get_transformed_source_graphic() const; + cairo_surface_t *_get_transformed_background() const; + cairo_surface_t *_get_fill_paint() const; + cairo_surface_t *_get_stroke_paint() const; + + void _set_internal(int slot, cairo_surface_t *s); +}; + +} // namespace Filters +} // namespace Inkscape + +#endif // SEEN_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..64402bb --- /dev/null +++ b/src/display/nr-filter-specularlighting.cpp @@ -0,0 +1,241 @@ +// 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) {} + +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 * std::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, DistantLightData const &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, PointLightData const &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, SpotLightData const &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) const +{ + 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 (icc) { + unsigned char 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); + } + + // Only alpha channel of input is used, no need to check input color_interpolation_filter value. + // 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 (color_interpolation == 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, color_interpolation); + 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::area_enlarge(Geom::IntRect &area, Geom::Affine const & /*trans*/) const +{ + // TODO: support kernelUnitLength + area.expandBy(1); +} + +double FilterSpecularLighting::complexity(Geom::Affine const &) 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..fdc7eb9 --- /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; + +namespace Inkscape { +namespace Filters { + +class FilterSlot; + +class FilterSpecularLighting : public FilterPrimitive +{ +public: + FilterSpecularLighting(); + ~FilterSpecularLighting() override; + + void render_cairo(FilterSlot &slot) const override; + void set_icc(SVGICCColor const &icc_) { icc = icc_; } + void area_enlarge(Geom::IntRect &area, Geom::Affine const &trans) const override; + double complexity(Geom::Affine const &ctm) const override; + + union + { + DistantLightData distant; + PointLightData point; + SpotLightData spot; + } light; + LightType light_type; + double surfaceScale; + double specularConstant; + double specularExponent; + guint32 lighting_color; + + Glib::ustring name() const override { return Glib::ustring("Specular Lighting"); } + +private: + std::optional<SVGICCColor> icc; +}; + +} // namespace Filters +} // namespace Inkscape + +#endif // SEEN_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..81966ba --- /dev/null +++ b/src/display/nr-filter-tile.cpp @@ -0,0 +1,134 @@ +// 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; + +FilterTile::~FilterTile() = default; + +void FilterTile::render_cairo(FilterSlot &slot) const +{ + // 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 = std::ceil(pr.width() / tile_area.width()); + int tile_rows = std::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) const +{ + // 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 &) 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..653eee8 --- /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(); + ~FilterTile() override; + + void render_cairo(FilterSlot &slot) const override; + void area_enlarge(Geom::IntRect &area, Geom::Affine const &trans) const override; + double complexity(Geom::Affine const &ctm) const override; + + Glib::ustring name() const override { return Glib::ustring("Tile"); } +}; + +} // namespace Filters +} // namespace Inkscape + +#endif // SEEN_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..f13b977 --- /dev/null +++ b/src/display/nr-filter-turbulence.cpp @@ -0,0 +1,450 @@ +// 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 = std::floor(_tile.width() * freq) / _tile.width(); + double hi = std::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 = std::floor(_tile.height() * freq) / _tile.height(); + double hi = std::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 constexpr + 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 constexpr BSize = 0x100; + static int constexpr 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(std::make_unique<TurbulenceGenerator>()) + , XbaseFrequency(0) + , YbaseFrequency(0) + , numOctaves(1) + , seed(0) + , updated(false) + , fTileWidth(10) //guessed + , fTileHeight(10) //guessed + , fTileX(1) //guessed + , fTileY(1) //guessed +{ +} + +FilterTurbulence::~FilterTurbulence() = default; + +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) const +{ + 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). + set_cairo_surface_ci(out, color_interpolation); + + 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 &) 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..37decc9 --- /dev/null +++ b/src/display/nr-filter-turbulence.h @@ -0,0 +1,95 @@ +// 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 <memory> +#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(); + ~FilterTurbulence() override; + + void render_cairo(FilterSlot &slot) const override; + double complexity(Geom::Affine const &ctm) const override; + bool uses_background() const 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() const override { return Glib::ustring("Turbulence"); } + +private: + std::unique_ptr<TurbulenceGenerator> gen; + + void turbulenceInit(long seed); + + double XbaseFrequency, YbaseFrequency; + int numOctaves; + double seed; + bool stitchTiles; + FilterTurbulenceType type; + bool updated; + + double fTileWidth; + double fTileHeight; + + double fTileX; + double fTileY; +}; + +} // namespace Filters +} // namespace Inkscape + +#endif // SEEN_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..41112c5 --- /dev/null +++ b/src/display/nr-filter-types.h @@ -0,0 +1,73 @@ +// 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 +}; + +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 // SEEN_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..3c2cd9c --- /dev/null +++ b/src/display/nr-filter-units.h @@ -0,0 +1,166 @@ +// 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..e776cce --- /dev/null +++ b/src/display/nr-filter.cpp @@ -0,0 +1,420 @@ +// 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" + +namespace Inkscape { +namespace Filters { + +using Geom::X; +using Geom::Y; + +Filter::Filter() +{ + _common_init(); +} + +Filter::Filter(int n) +{ + if (n > 0) primitives.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; +} + +void Filter::update() +{ + for (auto &p : primitives) { + p->update(); + } +} + +int Filter::render(Inkscape::DrawingItem const *item, DrawingContext &graphic, DrawingContext *bgdc, RenderContext &rc) const +{ + // 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 (primitives.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; + } + FilterQuality filterquality = (FilterQuality)item->drawing().filterQuality(); + int 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); + + auto 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 : primitives) { + if (!i->can_handle_affine(pbtrans)) { + units.set_paraller(true); + break; + } + } + + auto slot = FilterSlot(bgdc, graphic, units, rc, blurquality); + + for (auto &i : primitives) { + 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::add_primitive(std::unique_ptr<FilterPrimitive> primitive) +{ + primitives.emplace_back(std::move(primitive)); +} + +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 const &i : primitives) { + if (i) i->area_enlarge(bbox, item->ctm()); + } + +/* + TODO: something. See images at the bottom of filters.svg with medium-low + filtering quality. + + FilterQuality const filterquality = ... + + 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) const +{ + 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 + auto compute = [] (SVGLength length, double scale) { + length.update(12, 6, scale); + return length.computed; + }; + auto const region_x_computed = compute(_region_x, len_x); + auto const region_y_computed = compute(_region_y, len_y); + auto const region_w_computed = compute(_region_width, len_x); + auto const region_h_computed = compute(_region_height, 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_w_computed; + } else { + maxp[X] = minp[X] + region_w_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_h_computed; + } else { + maxp[Y] = minp[Y] + region_h_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) const +{ + double factor = 1.0; + for (auto &i : primitives) { + if (i) { + double f = i->complexity(ctm); + factor += f - 1.0; + } + } + return factor; +} + +bool Filter::uses_background() const +{ + for (auto &i : primitives) { + if (i && i->uses_background()) { + return true; + } + } + return false; +} + +void Filter::clear_primitives() +{ + primitives.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 pixels) +{ + if (pixels > 0) { + _x_pixels = pixels; + _y_pixels = pixels; + } +} + +void Filter::set_resolution(double x_pixels, double 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 quality) +{ + switch (quality) { + case FILTER_QUALITY_WORST: + return 32; + case FILTER_QUALITY_WORSE: + return 64; + case FILTER_QUALITY_NORMAL: + return 256; + case FILTER_QUALITY_BETTER: + case FILTER_QUALITY_BEST: + default: + return -1; + } +} + +std::pair<double, double> Filter::_filter_resolution(Geom::Rect const &area, Geom::Affine const &trans, FilterQuality 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 { + auto origo = area.min() * trans; + auto max_i = Geom::Point(area.max()[X], area.min()[Y]) * trans; + auto max_j = Geom::Point(area.min()[X], area.max()[Y]) * trans; + double i_len = (origo - max_i).length(); + double j_len = (origo - max_j).length(); + 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..a8787b2 --- /dev/null +++ b/src/display/nr-filter.h @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_DISPLAY_NR_FILTER_H +#define INKSCAPE_DISPLAY_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 <memory> +#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" + +namespace Inkscape { +class DrawingContext; +class DrawingItem; +class RenderContext; + +namespace Filters { + +class Filter final +{ +public: + /// Update any embedded DrawingItems prior to rendering. + void update(); + + /** 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, RenderContext &rc) const; + + /** + * 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 a valid filter primitive type or a filter + * primitive of such type cannot be created. + */ + void add_primitive(std::unique_ptr<FilterPrimitive> primitive); + + /** + * 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(); + + /** + * 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 for that + * parameter as defined in the SVG standard 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 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 x_pixels, double 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) const; + + // returns cache score factor + double complexity(Geom::Affine const &ctm) const; + + // says whether the filter accesses any of the background images + bool uses_background() const; + + /** 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); + +private: + std::vector<std::unique_ptr<FilterPrimitive>> primitives; + + /** 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 _common_init(); + static int _resolution_limit(FilterQuality quality); + std::pair<double, double> _filter_resolution(Geom::Rect const &area, + Geom::Affine const &trans, + FilterQuality q) const; +}; + +} // namespace Filters +} // namespace Inkscape + +#endif // INKSCAPE_DISPLAY_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..a111f6c --- /dev/null +++ b/src/display/nr-light-types.h @@ -0,0 +1,55 @@ +// 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 +}; + +struct DistantLightData +{ + double azimuth, elevation; +}; + +struct PointLightData +{ + double x, y, z; +}; + +struct SpotLightData +{ + double x, y, z; + double pointsAtX, pointsAtY, pointsAtZ; + double limitingConeAngle; + double specularExponent; +}; + +} // namespace Filters +} // namespace Inkscape + +#endif // SEEN_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..00404fd --- /dev/null +++ b/src/display/nr-light.cpp @@ -0,0 +1,121 @@ +// 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(DistantLightData const &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(PointLightData const &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(SpotLightData const &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..7f59c87 --- /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(DistantLightData const &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(PointLightData const &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(SpotLightData const &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..a6a0ded --- /dev/null +++ b/src/display/nr-style.cpp @@ -0,0 +1,423 @@ +// 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 "display/drawing-context.h" +#include "display/drawing-pattern.h" +#include "display/drawing-surface.h" + +#include "object/sp-paint-server.h" + +namespace Inkscape { + +void NRStyleData::Paint::clear() +{ + server.reset(); + type = PaintType::NONE; +} + +void NRStyleData::Paint::set(SPColor const &c) +{ + clear(); + type = PaintType::COLOR; + color = c; +} + +void NRStyleData::Paint::set(SPPaintServer *ps) +{ + clear(); + if (ps) { + type = PaintType::SERVER; + server = ps->create_drawing_paintserver(); + } +} + +void NRStyleData::Paint::set(SPIPaint const *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 << "NRStyleData::Paint::set: Double" << std::endl; + } else { + g_assert_not_reached(); + } +} + +NRStyleData::NRStyleData() + : fill() + , stroke() + , stroke_width(0.0) + , hairline(false) + , miter_limit(0.0) + , n_dash(0) + , dash_offset(0.0) + , fill_rule(CAIRO_FILL_RULE_EVEN_ODD) + , line_cap(CAIRO_LINE_CAP_BUTT) + , line_join(CAIRO_LINE_JOIN_MITER) + , 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; +} + +bool NRStyleData::Paint::ditherable() const +{ + return type == PaintType::SERVER && server && server->ditherable(); +} + +NRStyleData::NRStyleData(SPStyle const *style, SPStyle const *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) { + style_fill = &context_style->fill; + } else { + // A marker in the defs section will result in ending up here. + //std::cerr << "NRStyleData::set: 'context-fill': 'context_style' is NULL" << std::endl; + } + } else if (style_fill->paintOrigin == SP_CSS_PAINT_ORIGIN_CONTEXT_STROKE) { + if (context_style) { + style_fill = &context_style->stroke; + } else { + //std::cerr << "NRStyleData::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) { + style_stroke = &context_style->fill; + } else { + //std::cerr << "NRStyleData::set: 'context-fill': 'context_style' is NULL" << std::endl; + } + } else if (style_stroke->paintOrigin == SP_CSS_PAINT_ORIGIN_CONTEXT_STROKE) { + if (context_style) { + style_stroke = &context_style->stroke; + } else { + //std::cerr << "NRStyleData::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; + hairline = style->stroke_extensions.hairline; + 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; + + n_dash = style->stroke_dasharray.values.size(); + if (n_dash > 0 && style->stroke_dasharray.is_valid()) { + dash_offset = style->stroke_dashoffset.computed; + dash.resize(n_dash); + for (int i = 0; i < n_dash; ++i) { + dash[i] = style->stroke_dasharray.values[i].computed; + } + } else { + dash_offset = 0.0; + dash.clear(); + } + + for (int 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. + auto 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; +} + +auto NRStyle::preparePaint(Inkscape::DrawingContext &dc, Inkscape::RenderContext &rc, Geom::IntRect const &area, Geom::OptRect const &paintbox, Inkscape::DrawingPattern const *pattern, NRStyleData::Paint const &paint, CachedPattern const &cp) const -> CairoPatternUniqPtr +{ + if (paint.type == NRStyleData::PaintType::SERVER && pattern) { + // If a DrawingPattern, then always regenerate the pattern, because it may depend on 'area'. + // Even if not, regenerating the pattern is a no-op because DrawingPattern has a cache. + return CairoPatternUniqPtr(pattern->renderPattern(rc, area, paint.opacity, dc.surface()->device_scale())); + } + + // Otherwise, init or re-use cached pattern. + cp.inited.init([&, this] { + // Handle remaining non-DrawingPattern cases. + switch (paint.type) { + case NRStyleData::PaintType::SERVER: + if (paint.server) { + cp.pattern = CairoPatternUniqPtr(paint.server->create_pattern(dc.raw(), paintbox, paint.opacity)); + } else { + std::cerr << "Null pattern detected" << std::endl; + cp.pattern = CairoPatternUniqPtr(cairo_pattern_create_rgba(0, 0, 0, 0)); + } + break; + case NRStyleData::PaintType::COLOR: { + auto const &c = paint.color.v.c; + cp.pattern = CairoPatternUniqPtr(cairo_pattern_create_rgba(c[0], c[1], c[2], paint.opacity)); + break; + } + default: + cp.pattern.reset(); + break; + } + }); + + return copy(cp.pattern); +} + +void NRStyle::set(NRStyleData &&data_) +{ + data = std::move(data_); + invalidate(); +} + +auto NRStyle::prepareFill(Inkscape::DrawingContext &dc, Inkscape::RenderContext &rc, Geom::IntRect const &area, Geom::OptRect const &paintbox, Inkscape::DrawingPattern const *pattern) const -> CairoPatternUniqPtr +{ + return preparePaint(dc, rc, area, paintbox, pattern, data.fill, fill_pattern); +} + +auto NRStyle::prepareStroke(Inkscape::DrawingContext &dc, Inkscape::RenderContext &rc, Geom::IntRect const &area, Geom::OptRect const &paintbox, Inkscape::DrawingPattern const *pattern) const -> CairoPatternUniqPtr +{ + return preparePaint(dc, rc, area, paintbox, pattern, data.stroke, stroke_pattern); +} + +auto NRStyle::prepareTextDecorationFill(Inkscape::DrawingContext &dc, Inkscape::RenderContext &rc, Geom::IntRect const &area, Geom::OptRect const &paintbox, Inkscape::DrawingPattern const *pattern) const -> CairoPatternUniqPtr +{ + return preparePaint(dc, rc, area, paintbox, pattern, data.text_decoration_fill, text_decoration_fill_pattern); +} + +auto NRStyle::prepareTextDecorationStroke(Inkscape::DrawingContext &dc, Inkscape::RenderContext &rc, Geom::IntRect const &area, Geom::OptRect const &paintbox, Inkscape::DrawingPattern const *pattern) const -> CairoPatternUniqPtr +{ + return preparePaint(dc, rc, area, paintbox, pattern, data.text_decoration_stroke, text_decoration_stroke_pattern); +} + +void NRStyle::applyFill(Inkscape::DrawingContext &dc, CairoPatternUniqPtr const &cp) const +{ + dc.setSource(cp.get()); + dc.setFillRule(data.fill_rule); +} + +void NRStyle::applyTextDecorationFill(Inkscape::DrawingContext &dc, CairoPatternUniqPtr const &cp) const +{ + dc.setSource(cp.get()); + // Fill rule does not matter, no intersections. +} + +void NRStyle::applyStroke(Inkscape::DrawingContext &dc, CairoPatternUniqPtr const &cp) const +{ + dc.setSource(cp.get()); + if (data.hairline) { + dc.setHairline(); + } else { + dc.setLineWidth(data.stroke_width); + } + dc.setLineCap(data.line_cap); + dc.setLineJoin(data.line_join); + dc.setMiterLimit(data.miter_limit); + cairo_set_dash(dc.raw(), data.dash.empty() ? nullptr : data.dash.data(), data.dash.empty() ? 0 : data.n_dash, data.dash_offset); // fixme +} + +void NRStyle::applyTextDecorationStroke(Inkscape::DrawingContext &dc, CairoPatternUniqPtr const &cp) const +{ + dc.setSource(cp.get()); + if (data.hairline) { + dc.setHairline(); + } else { + dc.setLineWidth(data.text_decoration_stroke_width); + } + dc.setLineCap(CAIRO_LINE_CAP_BUTT); + dc.setLineJoin(CAIRO_LINE_JOIN_MITER); + dc.setMiterLimit(data.miter_limit); + cairo_set_dash(dc.raw(), nullptr, 0, 0.0); // fixme (no dash) +} + +void NRStyle::invalidate() +{ + // force pattern update + fill_pattern.reset(); + stroke_pattern.reset(); + text_decoration_fill_pattern.reset(); + text_decoration_stroke_pattern.reset(); +} + +} // 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-style.h b/src/display/nr-style.h new file mode 100644 index 0000000..c3d889e --- /dev/null +++ b/src/display/nr-style.h @@ -0,0 +1,181 @@ +// 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 INKSCAPE_DISPLAY_NR_STYLE_H +#define INKSCAPE_DISPLAY_NR_STYLE_H + +#include <memory> +#include <array> +#include <cairo.h> +#include <2geom/rect.h> +#include "color.h" +#include "drawing-paintserver.h" +#include "initlock.h" + +class SPPaintServer; +class SPStyle; +class SPIPaint; + +namespace Inkscape { +class DrawingContext; +class DrawingPattern; +class RenderContext; + +struct CairoPatternFreer { void operator()(cairo_pattern_t *p) const { cairo_pattern_destroy(p); } }; +using CairoPatternUniqPtr = std::unique_ptr<cairo_pattern_t, CairoPatternFreer>; +inline CairoPatternUniqPtr copy(CairoPatternUniqPtr const &p) +{ + if (!p) return {}; + cairo_pattern_reference(p.get()); + return CairoPatternUniqPtr(p.get()); +} + +struct NRStyleData +{ + NRStyleData(); + explicit NRStyleData(SPStyle const *style, SPStyle const *context_style = nullptr); + + enum class PaintType + { + NONE, + COLOR, + SERVER + }; + + struct Paint + { + PaintType type = PaintType::NONE; + SPColor color = 0; + std::unique_ptr<DrawingPaintServer> server; + float opacity = 1.0; + + void clear(); + void set(SPColor const &c); + void set(SPPaintServer *ps); + void set(SPIPaint const *paint); + bool ditherable() const; + }; + + Paint fill; + Paint stroke; + float stroke_width; + bool hairline; + float miter_limit; + int n_dash; + std::vector<double> dash; + float dash_offset; + cairo_fill_rule_t fill_rule; + cairo_line_cap_t line_cap; + cairo_line_join_t line_join; + + enum PaintOrderType + { + PAINT_ORDER_NORMAL, + PAINT_ORDER_FILL, + PAINT_ORDER_STROKE, + PAINT_ORDER_MARKER + }; + + std::array<PaintOrderType, 3> paint_order_layer; + + 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; +}; + +class NRStyle +{ +public: + void set(NRStyleData &&data); + CairoPatternUniqPtr prepareFill(DrawingContext &dc, RenderContext &rc, Geom::IntRect const &area, Geom::OptRect const &paintbox, DrawingPattern const *pattern) const; + CairoPatternUniqPtr prepareStroke(DrawingContext &dc, RenderContext &rc, Geom::IntRect const &area, Geom::OptRect const &paintbox, DrawingPattern const *pattern) const; + CairoPatternUniqPtr prepareTextDecorationFill(DrawingContext &dc, RenderContext &rc, Geom::IntRect const &area, Geom::OptRect const &paintbox, DrawingPattern const *pattern) const; + CairoPatternUniqPtr prepareTextDecorationStroke(DrawingContext &dc, RenderContext &rc, Geom::IntRect const &area, Geom::OptRect const &paintbox, DrawingPattern const *pattern) const; + void applyFill(DrawingContext &dc, CairoPatternUniqPtr const &cp) const; + void applyStroke(DrawingContext &dc, CairoPatternUniqPtr const &cp) const; + void applyTextDecorationFill(DrawingContext &dc, CairoPatternUniqPtr const &cp) const; + void applyTextDecorationStroke(DrawingContext &dc, CairoPatternUniqPtr const &cp) const; + void invalidate(); + + NRStyleData data; + +private: + struct CachedPattern + { + InitLock inited; + mutable CairoPatternUniqPtr pattern; + void reset() { inited.reset(); pattern.reset(); } + }; + + CairoPatternUniqPtr preparePaint(DrawingContext &dc, RenderContext &rc, Geom::IntRect const &area, Geom::OptRect const &paintbox, DrawingPattern const *pattern, NRStyleData::Paint const &paint, CachedPattern const &cp) const; + + CachedPattern fill_pattern; + CachedPattern stroke_pattern; + CachedPattern text_decoration_fill_pattern; + CachedPattern text_decoration_stroke_pattern; +}; + +} // namespace Inkscape + +#endif // INKSCAPE_DISPLAY_NR_STYLE_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-svgfonts.cpp b/src/display/nr-svgfonts.cpp new file mode 100644 index 0000000..feea0e7 --- /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 + auto hkern = cast<SPHkern>(&node); + if (hkern && is_horizontal_text && + MatchHKerningRule(hkern, this->glyphs[i], previous_unicode, previous_glyph_name) ){ + x -= (hkern->k / font_height); + } + auto vkern = 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 (is<SPFontFace>(&obj)) { + //XML Tree being directly used here while it shouldn't be. + units_per_em = obj.getRepr()->getAttributeDouble("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 (!is<SPGlyph>(node) && !is<SPMissingGlyph>(node)) { + return CAIRO_STATUS_SUCCESS; // FIXME: is this the right code to return? + } + + auto spfont = 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; + + auto glyphNode = 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 { + auto missing = 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) { + { + auto path = cast<SPPath>(&child); + if (path) { + pathv = path->curve()->get_pathvector(); + pathv = flip_coordinate_system(spfont, pathv); + render_glyph_path(cr, &pathv); + } + } + if (is<SPObjectGroup>(&child)) { + g_warning("TODO: svgfonts: render OBJECTGROUP"); + } + auto use = cast<SPUse>(&child); + if (use) { + SPItem* item = use->ref->getObject(); + auto path = cast<SPPath>(item); + if (path) { + auto shape = 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) { + auto glyph = cast<SPGlyph>(&node); + if (glyph) { + glyphs.push_back(glyph); + } + auto missing = 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 (is<SPFontFace>(&obj)) { + //XML Tree being directly used here while it shouldn't be. + units_per_em = obj.getRepr()->getAttributeDouble("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..ffc54f6 --- /dev/null +++ b/src/display/nr-svgfonts.h @@ -0,0 +1,73 @@ +// 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> +#include <2geom/pathvector.h> + +class SvgFont; +class SPFont; +class SPGlyph; +class SPMissingGlyph; +class SPObject; + +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..dec852c --- /dev/null +++ b/src/display/rendermode.h @@ -0,0 +1,66 @@ +// 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 class RenderMode { + NORMAL, + OUTLINE, + NO_FILTERS, + VISIBLE_HAIRLINES, + OUTLINE_OVERLAY, + size +}; + +enum class SplitMode { + NORMAL, + SPLIT, + XRAY, + size +}; + +enum class SplitDirection { + NONE, + NORTH, + EAST, + SOUTH, + WEST, + HORIZONTAL, // Only used when hovering + VERTICAL // Only used when hovering +}; + +enum class ColorMode { + NORMAL, + GRAYSCALE, + PRINT_COLORS_PREVIEW +}; + +} // 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/tags.h b/src/display/tags.h new file mode 100644 index 0000000..c09d361 --- /dev/null +++ b/src/display/tags.h @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: PBS <pbs3141@gmail.com> + * Copyright (C) 2022 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_DRAWINGITEM_TAGS_H +#define INKSCAPE_DRAWINGITEM_TAGS_H + +#include "util/cast.h" + +// Class hierarchy structure + +#define DRAWINGITEM_HIERARCHY_DATA(X)\ +X(DrawingItem,\ + X(DrawingShape)\ + X(DrawingImage)\ + X(DrawingGroup,\ + X(DrawingPattern)\ + X(DrawingText)\ + )\ + X(DrawingGlyphs)\ +) + +namespace Inkscape { + +// Forward declarations + +#define X(n, ...) class n; __VA_ARGS__ +DRAWINGITEM_HIERARCHY_DATA(X) +#undef X + +// Tag generation + +enum class DrawingItemTag : int +{ + #define X(n, ...) n##_first, __VA_ARGS__ n##_tmp, n##_last = n##_tmp - 1, + DRAWINGITEM_HIERARCHY_DATA(X) + #undef X +}; + +} // namespace Inkscape + +// Tag specialization + +#define X(n, ...) template <> inline constexpr int first_tag<Inkscape::n> = static_cast<int>(Inkscape::DrawingItemTag::n##_first); __VA_ARGS__ +DRAWINGITEM_HIERARCHY_DATA(X) +#undef X + +#define X(n, ...) template <> inline constexpr int last_tag<Inkscape::n> = static_cast<int>(Inkscape::DrawingItemTag::n##_last); __VA_ARGS__ +DRAWINGITEM_HIERARCHY_DATA(X) +#undef X + +#undef DRAWINGITEM_HIERARCHY_DATA + +#endif // INKSCAPE_DRAWINGITEM_TAGS_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 : |