From ac8131cd1ec29248ee581b4a93c3f34f7c0278a1 Mon Sep 17 00:00:00 2001 From: Martin Owens Date: Sun, 5 Oct 2025 12:19:02 -0400 Subject: [PATCH 1/7] New Image Surface data access and Filter Primatives To support more color channels, and greater flexibility in our rendering engine, this additional code impliments a new API for accessing pixel data in a cairo surface and expands it's capabilities so that they can work with floating point pixel data as well as CMYKA (5 channel) interpolations. The new interface makes heavy use of compile time templating for channel count and cairo surface formats. The primary use of this API is for filters. Each filter which depends on non-cairo functionality is recoded to use this new API and split out from their nr-filter and display object files so they can be unit tested. One useful filter in the API is color space transformation and converting the data into different data types such as int16 memory buffers. Other refactoring: * Added isDirect to Colors::Space::* * Moved and cleaned protected and public methods for consistancy in Spaces * Removed Lab toXYZ and fromXYZ which were orphaned by previous commit * Reformatted cairo transform into generic surface transform and added tests * Make the vector comparisons pretty and more useful --- src/colors/CMakeLists.txt | 1 - src/colors/CMakeUnit.txt | 3 +- src/colors/cms/system.cpp | 35 - src/colors/cms/system.h | 1 - src/colors/cms/transform-cairo.cpp | 204 ------ src/colors/cms/transform-cairo.h | 56 -- src/colors/cms/transform-color.cpp | 8 +- src/colors/cms/transform-surface.h | 133 ++++ src/colors/cms/transform.cpp | 9 +- src/colors/cms/transform.h | 6 +- src/colors/spaces/base.cpp | 30 +- src/colors/spaces/base.h | 8 +- src/colors/spaces/cms.h | 1 + src/colors/spaces/cmyk.h | 7 +- src/colors/spaces/gray.h | 1 + src/colors/spaces/hsl.h | 1 + src/colors/spaces/hsluv.h | 1 + src/colors/spaces/hsv.h | 6 +- src/colors/spaces/lab.cpp | 57 -- src/colors/spaces/lab.h | 7 +- src/colors/spaces/lch.h | 1 + src/colors/spaces/linear-rgb.h | 4 +- src/colors/spaces/luv.h | 1 + src/colors/spaces/named.h | 2 + src/colors/spaces/okhsl.h | 2 + src/colors/spaces/okhsv.h | 2 + src/colors/spaces/oklab.h | 2 + src/colors/spaces/oklch.h | 2 + src/colors/spaces/rgb.h | 1 + src/colors/spaces/xyz.h | 9 +- src/display/CMakeUnit.txt | 28 + src/display/drawing-access.h | 443 +++++++++++++ src/display/filters/color-matrix.h | 143 ++++ src/display/filters/color-space.h | 98 +++ src/display/filters/component-transfer.h | 145 ++++ src/display/filters/composite.h | 68 ++ src/display/filters/convolve-matrix.h | 128 ++++ src/display/filters/displacement-map.h | 68 ++ src/display/filters/gaussian-blur.h | 623 ++++++++++++++++++ src/display/filters/light.h | 409 ++++++++++++ src/display/filters/morphology.h | 201 ++++++ src/display/filters/turbulence.h | 325 +++++++++ src/ui/widget/canvas.cpp | 10 +- testfiles/CMakeLists.txt | 19 +- .../src/colors/cms-transform-cairo-test.cpp | 35 - .../src/colors/cms-transform-surface-test.cpp | 218 ++++++ testfiles/src/colors/spaces-cms-test.cpp | 8 + testfiles/src/colors/spaces-rgb-test.cpp | 5 + testfiles/src/display/drawing-access-test.cpp | 510 ++++++++++++++ .../src/display/filter-color-matrix-test.cpp | 61 ++ .../src/display/filter-color-space-test.cpp | 43 ++ .../filter-component-transfer-test.cpp | 80 +++ .../src/display/filter-composite-test.cpp | 26 + .../display/filter-convolve-matrix-test.cpp | 118 ++++ .../display/filter-displacement-map-test.cpp | 51 ++ .../src/display/filter-gaussian-blur-test.cpp | 53 ++ testfiles/src/display/filter-light-test.cpp | 100 +++ .../src/display/filter-morphology-test.cpp | 79 +++ .../src/display/filter-turbulence-test.cpp | 41 ++ testfiles/src/display/test-base.h | 293 ++++++++ testfiles/src/test-utils.h | 56 +- 61 files changed, 4642 insertions(+), 444 deletions(-) delete mode 100644 src/colors/cms/transform-cairo.cpp delete mode 100644 src/colors/cms/transform-cairo.h create mode 100644 src/colors/cms/transform-surface.h create mode 100644 src/display/CMakeUnit.txt create mode 100644 src/display/drawing-access.h create mode 100644 src/display/filters/color-matrix.h create mode 100644 src/display/filters/color-space.h create mode 100644 src/display/filters/component-transfer.h create mode 100644 src/display/filters/composite.h create mode 100644 src/display/filters/convolve-matrix.h create mode 100644 src/display/filters/displacement-map.h create mode 100644 src/display/filters/gaussian-blur.h create mode 100644 src/display/filters/light.h create mode 100644 src/display/filters/morphology.h create mode 100644 src/display/filters/turbulence.h delete mode 100644 testfiles/src/colors/cms-transform-cairo-test.cpp create mode 100644 testfiles/src/colors/cms-transform-surface-test.cpp create mode 100644 testfiles/src/display/drawing-access-test.cpp create mode 100644 testfiles/src/display/filter-color-matrix-test.cpp create mode 100644 testfiles/src/display/filter-color-space-test.cpp create mode 100644 testfiles/src/display/filter-component-transfer-test.cpp create mode 100644 testfiles/src/display/filter-composite-test.cpp create mode 100644 testfiles/src/display/filter-convolve-matrix-test.cpp create mode 100644 testfiles/src/display/filter-displacement-map-test.cpp create mode 100644 testfiles/src/display/filter-gaussian-blur-test.cpp create mode 100644 testfiles/src/display/filter-light-test.cpp create mode 100644 testfiles/src/display/filter-morphology-test.cpp create mode 100644 testfiles/src/display/filter-turbulence-test.cpp create mode 100644 testfiles/src/display/test-base.h diff --git a/src/colors/CMakeLists.txt b/src/colors/CMakeLists.txt index a1a4b705ae..2047ffb266 100644 --- a/src/colors/CMakeLists.txt +++ b/src/colors/CMakeLists.txt @@ -9,7 +9,6 @@ set(colors_SRC xml-color.cpp # sp-document and document-cms cms/system.h - cms/profile.h document-cms.h dragndrop.h color-set.h diff --git a/src/colors/CMakeUnit.txt b/src/colors/CMakeUnit.txt index 06de05693f..409c69955f 100644 --- a/src/colors/CMakeUnit.txt +++ b/src/colors/CMakeUnit.txt @@ -34,9 +34,10 @@ set(colors_unit_SRC # ------- # Headers + cms/profile.h cms/transform.h cms/transform-color.h - cms/transform-cairo.h + cms/transform-surface.h color.h manager.h parser.h diff --git a/src/colors/cms/system.cpp b/src/colors/cms/system.cpp index edb31d37d5..8469a9016d 100644 --- a/src/colors/cms/system.cpp +++ b/src/colors/cms/system.cpp @@ -15,7 +15,6 @@ #include "io/resource.h" #include "profile.h" -#include "transform-cairo.h" // clang-format off #ifdef _WIN32 @@ -229,40 +228,6 @@ const std::shared_ptr &System::getProfile(std::string const &name) cons return not_found; } -/** - * Get the color managed transform for the screen. - * - * There is one transform for all displays, anything more complex and the user should - * use their operating system CMS configurations instead of the Inkscape display cms. - * - * Transform immutably shared between System and Canvas. - */ -const std::shared_ptr &System::getDisplayTransform() -{ - bool need_to_update = false; - - Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - bool display = prefs->getIntLimited("/options/displayprofile/enabled", false); - int display_intent = prefs->getIntLimited("/options/displayprofile/intent", 0, 0, 3); - - if (_display != display || _display_intent != display_intent) { - need_to_update = true; - _display = display; - _display_intent = display_intent; - } - - auto display_profile = display ? getDisplayProfile(need_to_update) : nullptr; - - if (need_to_update) { - if (display_profile) { - _display_transform = std::make_shared(Profile::create_srgb(), display_profile); - } else { - _display_transform = nullptr; - } - } - return _display_transform; -} - } // namespace Inkscape::Colors::CMS /* diff --git a/src/colors/cms/system.h b/src/colors/cms/system.h index 05b8b6985b..a563ff70f8 100644 --- a/src/colors/cms/system.h +++ b/src/colors/cms/system.h @@ -47,7 +47,6 @@ public: std::vector> getDisplayProfiles() const; const std::shared_ptr &getDisplayProfile(bool &updated); - const std::shared_ptr &getDisplayTransform(); std::vector> getOutputProfiles() const; diff --git a/src/colors/cms/transform-cairo.cpp b/src/colors/cms/transform-cairo.cpp deleted file mode 100644 index 720682e286..0000000000 --- a/src/colors/cms/transform-cairo.cpp +++ /dev/null @@ -1,204 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/** @file - * Convert cairo surfaces between various color spaces - *//* - * Authors: see git history - * - * Copyright (C) 2024-2025 Authors - * Released under GNU GPL v2+, read the file 'COPYING' for more information. - */ - -#include "transform-cairo.h" -#include "profile.h" -#include "../color.h" - -#include -#include -#include -#include - -namespace Inkscape::Colors::CMS { - -/** - * Convert from cairo memory format to lcms2 memory format - */ -static int get_memory_format(cairo_surface_t *in) -{ - switch (cairo_image_surface_get_format(in)) { - case CAIRO_FORMAT_ARGB32: - return TYPE_ARGB_8_PREMUL; - case CAIRO_FORMAT_RGB24: - return TYPE_RGB_8; - case CAIRO_FORMAT_A8: - return TYPE_GRAY_8; - case CAIRO_FORMAT_RGB96F: - return TYPE_RGB_FLT; - case CAIRO_FORMAT_RGBA128F: - return TYPE_RGBA_FLT_PREMUL; - default: - return 0x0; - } -} - -/** - * Construct a transformation suitable for display conversion in a cairo buffer - * - * @arg from - The RGB CMS Profile the cairo data will start in. - * @arg to - The target RGB CMS Profile the cairo data needs to end up in. - * @arg proof - A profile to apply a proofing step to, this can be CMYK for example. - */ -TransformCairo::TransformCairo(std::shared_ptr const &from, - std::shared_ptr const &to, - std::shared_ptr const &proof, - RenderingIntent proof_intent, bool with_gamut_warn) - : Transform(proof ? - cmsCreateProofingTransformTHR( - cmsCreateContext(nullptr, nullptr), - from->getHandle(), - lcms_color_format(from, true, Alpha::PREMULTIPLIED), - to->getHandle(), - lcms_color_format(from, true, Alpha::PRESENT), - proof->getHandle(), - INTENT_PERCEPTUAL, - lcms_intent(proof_intent), - cmsFLAGS_SOFTPROOFING | (with_gamut_warn ? cmsFLAGS_GAMUTCHECK : 0) | lcms_bpc(proof_intent)) - : cmsCreateTransformTHR( - cmsCreateContext(nullptr, nullptr), - from->getHandle(), - lcms_color_format(from, true, Alpha::PREMULTIPLIED), - to->getHandle(), - lcms_color_format(from, true, Alpha::PRESENT), - INTENT_PERCEPTUAL, - 0) - , false) - , _pixel_size_in((_channels_in + 1) * sizeof(float)) - , _pixel_size_out((_channels_out + 1) * sizeof(float)) -{} - -/** - * Apply the CMS transform to the cairo surface and paint it into the output surface. - * - * @arg in - The source cairo surface with the pixels to transform. - * @arg out - The destination cairo surface which may be the same as in. - */ -void TransformCairo::do_transform(cairo_surface_t *in, cairo_surface_t *out) const -{ - cairo_surface_flush(in); - - int width = cairo_image_surface_get_width(in); - int height = cairo_image_surface_get_height(in); - - if (width != cairo_image_surface_get_width(out) || - height != cairo_image_surface_get_height(out)) { - throw ColorError("Different image formats while applying CMS!"); - } - - auto px_in = cairo_image_surface_get_data(in); - auto px_out = cairo_image_surface_get_data(out); - - cmsDoTransformLineStride( - _handle, - px_in, - px_out, - width, - height, - width * _pixel_size_in, - width * _pixel_size_out, - 0, 0 - ); - - cairo_surface_mark_dirty(out); -} - -/** - * Apply the CMS transform to the cairomm surface and paint it into the output surface. - * - * @arg in - The source cairomm surface with the pixels to transform. - * @arg out - The destination cairomm surface which may be the same as in. - */ -void TransformCairo::do_transform(Cairo::RefPtr &in, Cairo::RefPtr &out) const -{ - do_transform(in->cobj(), out->cobj()); -} - -/** - * Splice two Cairo RGBA128F formatted memory patches into one contigious - * memory region suuitable for transformation in lcms2. - * - * @arg inputs - A collection of raw data from a cairo image surface. - * Each one should be 4 floats per pixel and each alpha should be the same. - * @arg width - The width of the surface in pixels. - * @arg height - The height of the surface in pixels. - * @arg channels - The number of expected output channels not including alpha. - * - * @returns A newly allocated contigious region of floats. You should expect this region - * to contain alpha pre-multiplied channels so use accordingly. - */ -std::vector TransformCairo::splice(std::vector inputs, int width, int height, int channels) -{ - std::vector memory; - memory.reserve((channels + 1) * width * height); - - for (int px = 0; px < (width * height); px++) { - int c_out = 0; - for (auto &input : inputs) { - for (int c_in = 0; c_in < 3; c_in++) { - if (c_out < channels) { - memory.emplace_back(*(input)); - c_out++; - } - input++; - } - // alpha from the last surface - if (c_out == channels) { - memory.emplace_back(*(input)); - c_out++; - } - input++; // alpha - } - } - - return memory; -} - -/** - * Premultiply alpha in a Cairo RGBA128F memory region. - * - * Because lcms2 does not premultiply outputs but does allow them as inputs - * we do this conversion after a cms transform to premultiply the color - * channels in the way that cairo expects. Allowing for further processing - * in a consistant way. - * - * @arg input - The cairo memory data to be modified - * @arg width - The width in pixels of the data - * @arg height - The height in pixels of the data - * @arg channels - The number of channels, this is always 3 unless - * you are doing something special with spliced cairo. - * - */ -void TransformCairo::premultiply(float *input, int width, int height, int channels) -{ - // Premultiply result back into cairo-like format for further operations - for (int px = 0; px < (width * height); px++) { - float a = input[channels]; - for (int c = 0; c < channels; c++) { - input[c] *= a; - } - input += channels + 1; - } -} - - - -} // namespace Inkscape::Colors::CMS - -/* - 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/colors/cms/transform-cairo.h b/src/colors/cms/transform-cairo.h deleted file mode 100644 index 8ddccdc26d..0000000000 --- a/src/colors/cms/transform-cairo.h +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * Authors: see git history - * - * Copyright (C) 2025 Authors - * Released under GNU GPL v2+, read the file 'COPYING' for more information. - */ -#ifndef SEEN_COLORS_CMS_TRANSFORM_CAIRO_H -#define SEEN_COLORS_CMS_TRANSFORM_CAIRO_H - -#include -#include - -#include "transform.h" - -#include "colors/spaces/enum.h" - -namespace Inkscape::Colors::CMS { - -class Profile; -class TransformCairo : public Transform -{ -public: - TransformCairo( - std::shared_ptr const &from, - std::shared_ptr const &to, - std::shared_ptr const &proof = nullptr, - RenderingIntent proof_intent = RenderingIntent::AUTO, - bool with_gamut_warn = false); - - void do_transform(cairo_surface_t *in, cairo_surface_t *out) const; - void do_transform(Cairo::RefPtr &in, Cairo::RefPtr &out) const; - - void set_gamut_warn(std::vector const &input); - - static std::vector splice(std::vector inputs, int width, int height, int channels); - static void premultiply(float *input, int width, int height, int channels = 3); -private: - int _pixel_size_in; - int _pixel_size_out; -}; - -} // namespace Inkscape::Colors::CMS - -#endif // SEEN_COLORS_CMS_TRANSFORM_CAIRO_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/colors/cms/transform-color.cpp b/src/colors/cms/transform-color.cpp index 678a113692..3188535ad1 100644 --- a/src/colors/cms/transform-color.cpp +++ b/src/colors/cms/transform-color.cpp @@ -28,6 +28,8 @@ TransformColor::TransformColor(std::shared_ptr const &from, std::shared_ptr const &to, RenderingIntent intent) : Transform(cmsCreateTransform( from->getHandle(), + // Colors may have Alpha, but are NEVER premultiplied and we only convert one "pixel" + // at a time so there's no need to specify the presence of the alpha which is optional lcms_color_format(from), to->getHandle(), lcms_color_format(to), @@ -47,17 +49,17 @@ TransformColor::TransformColor(std::shared_ptr const &from, */ bool TransformColor::do_transform(std::vector &io) const { - bool alpha = io.size() == _channels_in + 1; + bool alpha = (int)io.size() == _channels_in + 1; // Pad data for output channels - while (io.size() < _channels_out + alpha) { + while ((int)io.size() < _channels_out + alpha) { io.insert(io.begin() + _channels_in, 0.0); } cmsDoTransform(_handle, &io.front(), &io.front(), 1); // Trim data for output channels - while (io.size() > _channels_out + alpha) { + while ((int)io.size() > _channels_out + alpha) { io.erase(io.end() - 1 - alpha); } return true; diff --git a/src/colors/cms/transform-surface.h b/src/colors/cms/transform-surface.h new file mode 100644 index 0000000000..68c6cf5a0e --- /dev/null +++ b/src/colors/cms/transform-surface.h @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: see git history + * + * Copyright (C) 2025 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_COLORS_CMS_TRANSFORM_SURFACE_H +#define SEEN_COLORS_CMS_TRANSFORM_SURFACE_H + +#include +#include +#include +#include + +#include "profile.h" +#include "transform.h" + +namespace Inkscape::Colors::CMS { + +class Profile; + +template +class TransformSurface : public Transform +{ +public: + /** + * Construct a transformation suitable for display conversion in a surface buffer + * + * @arg from - The RGB CMS Profile the surface data will start in. + * @arg to - The target RGB CMS Profile the surface data needs to end up in. + * @arg intent - The rendering intent for the conversion between from and to. + * @arg proof - A profile to apply a proofing step to, this can be CMYK for example. + * @arg proof_intent - An optional intent for the proofing conversion + * @arg with_gamut_warn - Optional flag for rendering out of gamut colors with a warning color. + */ + TransformSurface( + std::shared_ptr const &from, + std::shared_ptr const &to, + RenderingIntent intent = RenderingIntent::PERCEPTUAL, + std::shared_ptr const &proof = nullptr, + RenderingIntent proof_intent = RenderingIntent::AUTO) + : Transform(proof ? + cmsCreateProofingTransformTHR( + cmsCreateContext(nullptr, nullptr), + from->getHandle(), + lcms_color_format(from, sizeof(TIN), !std::is_integral::value, alpha_mode(PREMULT, true)), + to->getHandle(), + lcms_color_format(to, sizeof(TOUT), !std::is_integral::value, alpha_mode(false, true)), + proof->getHandle(), + lcms_intent(intent), + lcms_intent(proof_intent), + cmsFLAGS_COPY_ALPHA | cmsFLAGS_SOFTPROOFING | (GAMUTWARN ? cmsFLAGS_GAMUTCHECK : 0) | lcms_bpc(proof_intent)) + : cmsCreateTransformTHR( + cmsCreateContext(nullptr, nullptr), + from->getHandle(), + lcms_color_format(from, sizeof(TIN), !std::is_integral::value, alpha_mode(PREMULT, true)), + to->getHandle(), + lcms_color_format(to, sizeof(TOUT), !std::is_integral::value, alpha_mode(false, true)), + lcms_intent(intent), + cmsFLAGS_COPY_ALPHA) + , false) + , _pixel_size_in((_channels_in + 1) * sizeof(TIN)) + , _pixel_size_out((_channels_out + 1) * sizeof(TOUT)) + { + if constexpr (GAMUTWARN && (!std::is_same::value || !std::is_same::value)) { + throw std::logic_error("Gamut warnings only work with 16bit integer proof checking CMS transformations."); + } + + } + + /** + * Apply the CMS transform to the surface and paint it into the output surface. + * + * @arg width - The width of the image to transform + * @arg height - The height of the image to transform + * @arg px_in - The source surface with the pixels to transform. + * @arg px_out - The destination surface which may be the same as in. + * @arg stride_in - The optional stride for the input image, if known to be uncontigious. + * @arg stride_out - The optional stride for the output image, if known to be uncontigious. + */ + void do_transform(int width, int height, TIN const *px_in, TOUT *px_out, int stride_in = 0, int stride_out = 0) const + { + cmsDoTransformLineStride( + _handle, + px_in, + px_out, + width, + height, + stride_in ? stride_in : width * _pixel_size_in, + stride_out ? stride_out : width * _pixel_size_out, + 0, 0 + ); + } + + /** + * Set the alarm code / gamut warn color for this transformation. + */ + void set_gamut_warn_color(std::vector const &input) + { + std::array color; + color.fill(0); + + for (auto i = 0; i < input.size(); i++) { + color[i] = input[i] * std::numeric_limits::max(); + } + + if (_context) { + cmsSetAlarmCodesTHR(_context, color.data()); + } else { + cmsSetAlarmCodes(color.data()); + } + } + +private: + int const _pixel_size_in; + int const _pixel_size_out; +}; + +} // namespace Inkscape::Colors::CMS + +#endif // SEEN_COLORS_CMS_TRANSFORM_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/colors/cms/transform.cpp b/src/colors/cms/transform.cpp index 1fe394d7e1..0bdc616047 100644 --- a/src/colors/cms/transform.cpp +++ b/src/colors/cms/transform.cpp @@ -11,6 +11,7 @@ #include "transform.h" #include "profile.h" +#include #include namespace Inkscape::Colors::CMS { @@ -23,19 +24,21 @@ static constexpr cmsUInt32Number mask_colorspace = ~COLORSPACE_SH(0b11111); * cms api ranges and format: 64bit doubles with no scaling except xyz. * * @arg profile - The color profile which will be transformed into or out of. - * @arg small - If true, the format will be 32bit instead of 64bit (default: false) + * @arg size - Number of bytes in this format, default 0 which means 8 byte double + * @arg decimal - True if float or double, false if int. Default is true. * @arg alpha - What kind of alpha processing to do (see Alpha) * * @return The lcms2 transform format for this color profile. */ -int Transform::lcms_color_format(std::shared_ptr const &profile, bool small, Alpha alpha) +int Transform::lcms_color_format(std::shared_ptr const &profile, int size, bool decimal, Alpha alpha) { // Format is 64bit floating point (double) or 32bit (float) // Note: size of 8 will clobber channel size bit and cause errors, pass zero (see lcms API docs) - auto format = cmsFormatterForColorspaceOfProfile(profile->getHandle(), small ? 4 : 0, true); + auto format = cmsFormatterForColorspaceOfProfile(profile->getHandle(), std::clamp(size, 0, 4), decimal); // Add the alpha channel into the formatter if (alpha != Alpha::NONE) { + // Note that this does not mean the output will have the alpha copied into it, see cmsFLAGS_COPY_ALPHA format |= EXTRA_SH(1); } diff --git a/src/colors/cms/transform.h b/src/colors/cms/transform.h index 4ec449685d..33657745ac 100644 --- a/src/colors/cms/transform.h +++ b/src/colors/cms/transform.h @@ -52,7 +52,11 @@ protected: cmsHTRANSFORM _handle; cmsContext _context; - static int lcms_color_format(std::shared_ptr const &profile, bool small = false, Alpha alpha = Alpha::NONE); + static Alpha alpha_mode(bool premultiplied, bool present) { + return !present ? Alpha::NONE : (premultiplied ? Alpha::PREMULTIPLIED : Alpha::PRESENT); + } + + static int lcms_color_format(std::shared_ptr const &profile, int size = 0, bool decimal = true, Alpha alpha = Alpha::NONE); static int lcms_intent(RenderingIntent intent); static int lcms_bpc(RenderingIntent intent); diff --git a/src/colors/spaces/base.cpp b/src/colors/spaces/base.cpp index aeab010389..205164a4ed 100644 --- a/src/colors/spaces/base.cpp +++ b/src/colors/spaces/base.cpp @@ -52,6 +52,7 @@ bool AnySpace::convert(std::vector &io, std::shared_ptr to_spa // Firstly convert from the formatted values (i.e. hsl) into the profile values (i.e. sRGB) spaceToProfile(io); + // Secondly convert the color profile itself using lcms2 if the profiles are different if (profileToProfile(io, to_space)) { // Thirdly convert to the formatted values (i.e. hsl) from the profile values (i.e. sRGB) @@ -84,16 +85,7 @@ bool AnySpace::profileToProfile(std::vector &io, std::shared_ptr_intent_priority || getIntent() == RenderingIntent::UNKNOWN) { - intent = to_space->getIntent(); - } else { - intent = getIntent(); - } - if (intent == RenderingIntent::UNKNOWN) { - intent = RenderingIntent::PERCEPTUAL; - } + auto intent = getBestIntent(to_space); // Look in the transform cache for the color profile auto to_profile_id = to_profile->getChecksum() + "-" + intentIds[intent]; @@ -110,6 +102,24 @@ bool AnySpace::profileToProfile(std::vector &io, std::shared_ptr &to_space) const +{ + // Choose best rendering intent based on the intent priority + auto intent = RenderingIntent::UNKNOWN; + if (_intent_priority <= to_space->_intent_priority || getIntent() == RenderingIntent::UNKNOWN) { + intent = to_space->getIntent(); + } else { + intent = getIntent(); + } + if (intent == RenderingIntent::UNKNOWN) { + intent = RenderingIntent::PERCEPTUAL; + } + return intent; +} + /** * Convert the color into an RGBA32 for use within Gdk rendering. */ diff --git a/src/colors/spaces/base.h b/src/colors/spaces/base.h index 83c711851a..ac658c9e96 100644 --- a/src/colors/spaces/base.h +++ b/src/colors/spaces/base.h @@ -64,6 +64,7 @@ public: virtual unsigned int getComponentCount() const { return _components; } virtual std::shared_ptr const getProfile() const = 0; RenderingIntent getIntent() const { return _intent; } + RenderingIntent getBestIntent(std::shared_ptr &to_space) const; // Some color spaces (like XYZ or LAB) to not put restrictions on valid ranges of values; // others (like sRGB) do, which means that channels outside those bounds represent colors out of gamut. bool isUnbounded() const { return _spaceIsUnbounded; } @@ -73,11 +74,17 @@ public: // Bring 'color' into gamut of '*this' color space Color toGamut(const Colors::Color& color); + // isDirect is true when the child class implements getProfile + // TODO: Turn this into a compile time flag, as we know at compile time if it's direct or not. + virtual bool isDirect() const { return false; } + Components const &getComponents(bool alpha = false) const; std::string const getPrefsPath() const { return "/colorselector/" + getName() + "/"; } virtual bool isValid() const { return true; } + bool convert(std::vector &io, std::shared_ptr to_space) const; + protected: friend class Colors::Color; @@ -87,7 +94,6 @@ protected: virtual std::vector getParsers() const { return {}; } virtual std::string toString(std::vector const &values, bool opacity = true) const = 0; - bool convert(std::vector &io, std::shared_ptr to_space) const; bool profileToProfile(std::vector &io, std::shared_ptr to_space) const; virtual void spaceToProfile(std::vector &io) const; virtual void profileToSpace(std::vector &io) const; diff --git a/src/colors/spaces/cms.h b/src/colors/spaces/cms.h index ebb161e220..d8b40559ef 100644 --- a/src/colors/spaces/cms.h +++ b/src/colors/spaces/cms.h @@ -30,6 +30,7 @@ public: Type getComponentType() const override { return _profile_type; } unsigned int getComponentCount() const override; + bool isDirect() const override { return true; } std::shared_ptr const getProfile() const override; void setIntent(RenderingIntent intent) { _intent = intent; } diff --git a/src/colors/spaces/cmyk.h b/src/colors/spaces/cmyk.h index 2c196394c7..c2d5f831c2 100644 --- a/src/colors/spaces/cmyk.h +++ b/src/colors/spaces/cmyk.h @@ -24,12 +24,13 @@ public: DeviceCMYK(): RGB(Type::CMYK, 4, "DeviceCMYK", "CMYK", "color-selector-cmyk") {} ~DeviceCMYK() override = default; - void spaceToProfile(std::vector &output) const override; - void profileToSpace(std::vector &output) const override; - + bool isDirect() const override { return false; } protected: friend class Inkscape::Colors::Color; + void spaceToProfile(std::vector &output) const override; + void profileToSpace(std::vector &output) const override; + std::string toString(std::vector const &values, bool opacity = true) const override; bool overInk(std::vector const &input) const override; }; diff --git a/src/colors/spaces/gray.h b/src/colors/spaces/gray.h index 0dd73c6735..511ffc7079 100644 --- a/src/colors/spaces/gray.h +++ b/src/colors/spaces/gray.h @@ -20,6 +20,7 @@ class Gray : public RGB public: Gray(): RGB(Type::Gray, 1, "Gray", "Gray", "color-selector-gray") {} + bool isDirect() const override { return false; } protected: friend class Inkscape::Colors::Color; diff --git a/src/colors/spaces/hsl.h b/src/colors/spaces/hsl.h index 07bdd3547d..1d76331ede 100644 --- a/src/colors/spaces/hsl.h +++ b/src/colors/spaces/hsl.h @@ -21,6 +21,7 @@ public: HSL(): RGB(Type::HSL, 3, "HSL", "HSL", "color-selector-hsx") {} ~HSL() override = default; + bool isDirect() const override { return false; } protected: friend class Inkscape::Colors::Color; diff --git a/src/colors/spaces/hsluv.h b/src/colors/spaces/hsluv.h index 1afc70fd86..960ff6ff6e 100644 --- a/src/colors/spaces/hsluv.h +++ b/src/colors/spaces/hsluv.h @@ -27,6 +27,7 @@ public: HSLuv(): XYZ(Type::HSLUV, 3, "HSLuv", "HSLuv", "color-selector-hsluv") {} ~HSLuv() override = default; + bool isDirect() const override { return false; } protected: friend class Inkscape::Colors::Color; diff --git a/src/colors/spaces/hsv.h b/src/colors/spaces/hsv.h index f4a43411d0..914e861076 100644 --- a/src/colors/spaces/hsv.h +++ b/src/colors/spaces/hsv.h @@ -21,12 +21,12 @@ public: HSV(): RGB(Type::HSV, 3, "HSV", "HSV", "color-selector-hsx") {} ~HSV() override = default; - void spaceToProfile(std::vector &output) const override; - void profileToSpace(std::vector &output) const override; - + bool isDirect() const override { return false; } protected: friend class Inkscape::Colors::Color; + void spaceToProfile(std::vector &output) const override; + void profileToSpace(std::vector &output) const override; std::string toString(std::vector const &values, bool opacity) const override; public: diff --git a/src/colors/spaces/lab.cpp b/src/colors/spaces/lab.cpp index 31ae911933..f190ff1a89 100644 --- a/src/colors/spaces/lab.cpp +++ b/src/colors/spaces/lab.cpp @@ -56,63 +56,6 @@ void Lab::scaleDown(std::vector &in_out) in_out[2] = SCALE_DOWN(in_out[2], MIN_SCALE, MAX_SCALE); } -/** - * Convert a color from the the Lab colorspace to the XYZ colorspace. - * - * @param in_out[in,out] The Lab color converted to a XYZ color. - */ -void Lab::toXYZ(std::vector &in_out) -{ - scaleUp(in_out); - - double y = (in_out[0] + 16.0) / 116.0; - in_out[0] = in_out[1] / 500.0 + y; - in_out[1] = y; - in_out[2] = y - in_out[2] / 200.0; - - for (unsigned i = 0; i < 3; i++) { - double x3 = std::pow(in_out[i], 3); - if (x3 > 0.008856) { - in_out[i] = x3; - } else { - in_out[i] = (in_out[i] - 16.0 / 116.0) / 7.787; - } - //in_out[i] *= illuminant_d65[i]; - } -} - -/** - * Convert a color from the the XYZ colorspace to the Lab colorspace. - * - * @param in_out[in,out] The XYZ color converted to a Lab color. - */ -void Lab::fromXYZ(std::vector &in_out) -{ - for (unsigned i = 0; i < 3; i++) { - //in_out[i] /= illuminant_d65[i]; - } - - double l; - if (in_out[1] > 0.008856) { - l = 116 * std::pow(in_out[1], 0.33333) - 16; - } else { - l = 903.3 * in_out[1]; - } - - for (unsigned i = 0; i < 3; i++) { - if (in_out[i] > 0.008856) { - in_out[i] = std::pow(in_out[i], 0.33333); - } else { - in_out[i] = 7.787 * in_out[i] + 16.0 / 116.0; - } - }; - in_out[2] = 200 * (in_out[1] - in_out[2]); - in_out[1] = 500 * (in_out[0] - in_out[1]); - in_out[0] = l; - - scaleDown(in_out); -} - /** * Print the Lab color to a CSS string. * diff --git a/src/colors/spaces/lab.h b/src/colors/spaces/lab.h index 42ae413192..49bc260ebc 100644 --- a/src/colors/spaces/lab.h +++ b/src/colors/spaces/lab.h @@ -24,12 +24,14 @@ public: } ~Lab() override = default; + bool isDirect() const override { return true; } + std::shared_ptr const getProfile() const override; + protected: friend class Inkscape::Colors::Color; Lab(Type type, int components, std::string name, std::string shortName, std::string icon, bool spaceIsUnbounded = false); - std::shared_ptr const getProfile() const override; std::string toString(std::vector const &values, bool opacity) const override; public: @@ -42,9 +44,6 @@ public: bool parse(std::istringstream &input, std::vector &output) const override; }; - static void toXYZ(std::vector &output); - static void fromXYZ(std::vector &output); - static void scaleDown(std::vector &in_out); static void scaleUp(std::vector &in_out); }; diff --git a/src/colors/spaces/lch.h b/src/colors/spaces/lch.h index 47f9b87568..eb569ab934 100644 --- a/src/colors/spaces/lch.h +++ b/src/colors/spaces/lch.h @@ -21,6 +21,7 @@ public: Lch(): Lab(Type::LCH, 3, "Lch", "Lch", "color-selector-lch", true) {} ~Lch() override = default; + bool isDirect() const override { return false; } protected: friend class Inkscape::Colors::Color; diff --git a/src/colors/spaces/linear-rgb.h b/src/colors/spaces/linear-rgb.h index e622b57830..3cd1e3bf2b 100644 --- a/src/colors/spaces/linear-rgb.h +++ b/src/colors/spaces/linear-rgb.h @@ -24,10 +24,12 @@ public: } ~LinearRGB() override = default; + bool isDirect() const override { return true; } + std::shared_ptr const getProfile() const override; + protected: friend class Inkscape::Colors::Color; - std::shared_ptr const getProfile() const override; std::string toString(std::vector const &values, bool opacity = true) const override; public: diff --git a/src/colors/spaces/luv.h b/src/colors/spaces/luv.h index 7a00914364..0206dd156a 100644 --- a/src/colors/spaces/luv.h +++ b/src/colors/spaces/luv.h @@ -25,6 +25,7 @@ public: Luv(): XYZ(Type::LUV, 3, "Luv", "Luv", "color-selector-luv") {} ~Luv() override = default; + bool isDirect() const override { return false; } protected: friend class Inkscape::Colors::Color; diff --git a/src/colors/spaces/named.h b/src/colors/spaces/named.h index 07d8d5dce3..0b8fc8c6dc 100644 --- a/src/colors/spaces/named.h +++ b/src/colors/spaces/named.h @@ -29,6 +29,8 @@ public: static std::string getNameFor(unsigned int rgba); + bool isDirect() const override { return false; } + protected: friend class Inkscape::Colors::Color; diff --git a/src/colors/spaces/okhsl.h b/src/colors/spaces/okhsl.h index 61f0786391..96bc03d7b5 100644 --- a/src/colors/spaces/okhsl.h +++ b/src/colors/spaces/okhsl.h @@ -21,6 +21,8 @@ public: OkHsl(): RGB(Type::OKHSL, 3, "OkHsl", "OkHsl", "color-selector-okhsl", true) {} ~OkHsl() override = default; + bool isDirect() const override { return false; } + protected: friend class Inkscape::Colors::Color; diff --git a/src/colors/spaces/okhsv.h b/src/colors/spaces/okhsv.h index 846a1c9d80..3836626b31 100644 --- a/src/colors/spaces/okhsv.h +++ b/src/colors/spaces/okhsv.h @@ -21,6 +21,8 @@ public: OkHsv(): RGB(Type::OKHSV, 3, "OkHsv", "OkHsv", "color-selector-okhsv") {} ~OkHsv() override = default; + bool isDirect() const override { return false; } + protected: void spaceToProfile(std::vector &output) const override; void profileToSpace(std::vector &output) const override; diff --git a/src/colors/spaces/oklab.h b/src/colors/spaces/oklab.h index 52bfb929c6..4e38f6739f 100644 --- a/src/colors/spaces/oklab.h +++ b/src/colors/spaces/oklab.h @@ -21,6 +21,8 @@ public: OkLab(): RGB(Type::OKLAB, 3, "OkLab", "OkLab", "color-selector-oklab", true) {} ~OkLab() override = default; + bool isDirect() const override { return false; } + protected: friend class Inkscape::Colors::Color; diff --git a/src/colors/spaces/oklch.h b/src/colors/spaces/oklch.h index 760568573b..8cbaa25266 100644 --- a/src/colors/spaces/oklch.h +++ b/src/colors/spaces/oklch.h @@ -21,6 +21,8 @@ public: OkLch(): RGB(Type::OKLCH, 3, "OkLch", "OkLch", "color-selector-oklch") {} ~OkLch() override = default; + bool isDirect() const override { return false; } + protected: friend class Inkscape::Colors::Color; diff --git a/src/colors/spaces/rgb.h b/src/colors/spaces/rgb.h index d3ffc55edc..ce8479c4a5 100644 --- a/src/colors/spaces/rgb.h +++ b/src/colors/spaces/rgb.h @@ -21,6 +21,7 @@ public: RGB(): AnySpace(Type::RGB, 3, "RGB", "RGB", "color-selector-rgb") {} ~RGB() override = default; + bool isDirect() const override { return true; } std::shared_ptr const getProfile() const override; protected: diff --git a/src/colors/spaces/xyz.h b/src/colors/spaces/xyz.h index 87caf1ceb1..d14c0bc522 100644 --- a/src/colors/spaces/xyz.h +++ b/src/colors/spaces/xyz.h @@ -26,12 +26,14 @@ public: unsigned int getComponentCount() const override { return 3; } + bool isDirect() const override { return true; } + std::shared_ptr const getProfile() const override; + protected: friend class Inkscape::Colors::Color; XYZ(Type type, int components, std::string name, std::string shortName, std::string icon, bool spaceIsUnbounded = false); - std::shared_ptr const getProfile() const override; std::string toString(std::vector const &values, bool opacity = true) const override { return _toString(values, opacity, false); } std::string _toString(std::vector const &values, bool opacity, bool d50) const; }; @@ -42,13 +44,16 @@ public: XYZ50(): XYZ(Type::XYZ50, 3, "XYZ D50", "XYZ D50", "color-selector-xyz", true) {} ~XYZ50() override = default; + bool isDirect() const override { return true; } + std::shared_ptr const getProfile() const override; + protected: friend class Inkscape::Colors::Color; XYZ50(Type type, int components, std::string name, std::string shortName, std::string icon, bool spaceIsUnbounded = false); - std::shared_ptr const getProfile() const override; std::string toString(std::vector const &values, bool opacity = true) const override { return _toString(values, opacity, true); } + }; } // namespace Inkscape::Colors::Space diff --git a/src/display/CMakeUnit.txt b/src/display/CMakeUnit.txt new file mode 100644 index 0000000000..2e974822ea --- /dev/null +++ b/src/display/CMakeUnit.txt @@ -0,0 +1,28 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +set(display_unit_SRC + + # ------- + # Headers + drawing-access.h + filters/color-matrix.h + filters/color-space.h + filters/component-transfer.h + filters/composite.h + filters/convolve-matrix.h + filters/displacement-map.h + filters/gaussian-blur.h + filters/light.h + filters/morphology.h + filters/turbulence.h +) + +function(get_display_unit_lib) + list(TRANSFORM display_unit_SRC PREPEND "display/") + list(PREPEND display_unit_SRC "display/threading.cpp" "display/dispatch-pool.cpp") + list(TRANSFORM display_unit_SRC PREPEND "${CMAKE_SOURCE_DIR}/src/") + add_library(display_unit_lib SHARED "${display_unit_SRC}") + target_link_libraries(display_unit_lib cairo cairomm-1.16 2Geom::2geom) + make_target_unit_testable(display_unit_lib) +endfunction(get_display_unit_lib) + diff --git a/src/display/drawing-access.h b/src/display/drawing-access.h new file mode 100644 index 0000000000..e8bfd25c8c --- /dev/null +++ b/src/display/drawing-access.h @@ -0,0 +1,443 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Access the memory of a surface drawing in a predictable way. + *//* + * Authors: + * Martin Owens + * + * Copyright (C) 2025 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_DISPLAY_DRAWING_ACCESS_H +#define INKSCAPE_DISPLAY_DRAWING_ACCESS_H + +#include +#include +#include // DEBUG +#include +#include +#include +#include + +/** + * Terms: + * + * Color - Is a collection of channels, plus an alpha of an inkscape color space. + * Channel - Is one of those color space double values where alpha is always the last + * item. For example in CMYKA, C is channel 0, M is 1 and A is 4 + * Surface - Is a collection of Cairo pixels in a 2d grid with a specific stride. + * Pixel - A collectiion of one OR four Primaries packed into this + * surface grid. These may be floats or integers of various scales. + * Primary - One of the values packed into a pixel. These get turned into channels + * through unpacking of specific memory locations. + * Coordinates - Or Coords, are a pair of X,Y values within the surface image. + * Position - A single memory address offset which a coordinate can be transformed + * into to locate the pixel or primary in the surface memory. + */ +namespace Inkscape { + +/** + * What to do when a x/y coordinate is outside the width and height. This happens + * when filters are asking for small grids of pixels. + */ +enum class DrawingAccessEdgeMode +{ + ERROR, // Raise an error + EXTEND, // Clamp the x,y to 0,0,w,h + WRAP, // Treat surface as a spherical space + ZERO, // Return zero for getter, and ignore OOB setter +}; + +/** + * Image surface memory access for different types which can span multiple surfaces. + * + * @template_arg F - The cairo type this drawing access is for. + * @template_arg C - The total number of channels in this format across all surfaces. + * @template_arg P0 - Optionally override primary count for accessing non-cairo + * memory layouts. + */ +template +class DrawingAccess +{ +public: + // Is the format an integer based format + constexpr static bool I = F != CAIRO_FORMAT_RGBA128F; + + // How many primaries are there in this format + constexpr static int P = P0 ? P0 : (F == CAIRO_FORMAT_A8 ? 0 : 3); + + // The internal type used by each channel in the format + using T = typename std::conditional::type; + + // Scale of each primary to convert to a double + constexpr static double _scale = I ? 255.0 : 1.0; + + // Position of the alpha primary in this format + constexpr static int _alpha_primary = I ? 0 : P; + + // Does this DrawingAccess need two surfaces? + constexpr static bool NEXT = C - P > 0; + + // Actual number of channels when including alpha + constexpr static int SIZE = C + 1; + using COLOR = std::array; + + /** + * Create a drawing access object for the given cairo surface. + * + * @arg cairo_surface - The Cairo Surface to gain memory access to. + * @arg next_surface - Optionally add another surface to handle color interpolation + * in spaces like CMYKA with more than 3 primaries. + */ + DrawingAccess(Cairo::RefPtr cairo_surface, + Cairo::RefPtr next_surface = {}) + : _width(cairo_surface->get_width()) + , _height(cairo_surface->get_height()) + , _stride(cairo_surface->get_stride() / sizeof(T)) + , _memory(reinterpret_cast(cairo_surface->get_data())) + , _edge_mode(DrawingAccessEdgeMode::ERROR) + , _cairo_surface(cairo_surface) + , _next_surface(next_surface) + { + if (cairo_image_surface_get_format(cairo_surface->cobj()) != F) { + // throw std::exception("Format of the cairo surface doesn't match the DrawingAccess type."); + } + if constexpr (C > P * (NEXT + 1)) { + throw std::logic_error("DataAccess format does not permit this number of primaries."); + } + if constexpr (NEXT) { + _next_memory = reinterpret_cast(_next_surface->get_data()); + + if (_width != _next_surface->get_width() || _height != _next_surface->get_height() || + _stride != _next_surface->get_stride() / sizeof(T) || + cairo_image_surface_get_format(_next_surface->cobj()) != F) { + // throw std::exception("Drawing Access Next Surface must be the same formats."); + } + } + } + + /** + * Create access to a patch of memory which isn't part of a cairo surface. This can be used + * to do color convertions using lcms2 and run filters on the same memory without needing + * to convert to cairo formats first. + */ + DrawingAccess(T *memory, int width, int height) + : _width(width) + , _height(height) + , _stride(width * (P0 + 1)) + , _memory(memory) + {} + + /** + * Set the edge mode for this drawing access object + */ + void setEdgeMode(DrawingAccessEdgeMode mode) { _edge_mode = mode; } + + /** + * Get a color from the surface at the given coordinates. + * + * @arg x - The pixel x coordinate to get + * @arg y - The pixel y coordinate to get + * @arg unmultiply_alpha - Remove premultiplied alpha if true + * + * @return_arg ret - The pre-sized memory for the returned color space including alpha. + * We use the same memory so we don't have to re-allocate for every pixel in a filter. + */ + inline void colorAt(int x, int y, COLOR &ret, bool unmultiply_alpha = false) const + { + int pos = _pixel_pos(x, y); + double alpha = _get_alpha(pos); + for (int c = 0; c < C; c++) { + ret[c] = _get_channel(pos, c, (unmultiply_alpha && alpha > 0.0 ? alpha : 1.0)); + } + ret[C] = alpha; + } + + /** + * Using bilinear interpolation get the effective pixel at the given coordinates. + * Note: Bilinear interpolation is two linear interpolations across 4 pixels + * + * @arg x - The position in the x coordinate to get. + * @arg y - The position in the y coordinate to get. + * @return_arg - The pre-sized memory for the returned color space including alpha. + * We use the same memory so we don't have to re-allocate for every pixel in a filter. + */ + void colorAt(double x, double y, COLOR &ret) const + { + int fx = floor(x), fy = floor(y); + int cx = ceil(x), cy = ceil(y); + double weight_x = x - fx, weight_y = y - fy; + + int pos_a = _pixel_pos(fx, fy); + int pos_b = _pixel_pos(cx, fy); + int pos_c = _pixel_pos(fx, cy); + int pos_d = _pixel_pos(cx, cy); + + // Calculation always uses inpremultiplied colors + double alpha_a = _get_alpha(pos_a); + double alpha_b = _get_alpha(pos_b); + double alpha_c = _get_alpha(pos_c); + double alpha_d = _get_alpha(pos_d); + + for (int c = 0; c <= C; c++) { + ret[c] = _bilinear_interpolate(_get_channel(pos_a, c, alpha_a), _get_channel(pos_b, c, alpha_b), + _get_channel(pos_c, c, alpha_c), _get_channel(pos_d, c, alpha_d), weight_x, + weight_y); + } + } + + /** + * Set the given pixel to the color values, apply premultiplicatiion of alpha is neccessary to + * keep the surface in a premultiplied state for further drawing operations. + * + * @arg x - The x coordinate to set + * @arg y - The y coordinate to set + * @arg values - The color values to set + * @arg not_premultiplied - If true, values are premultiplied before saving + * + * @arg values - A set of doubles to apply to the pixel data + */ + void colorTo(int x, int y, COLOR const &values, bool unmultiply_alpha = false) + { + _set_primaries_recursively(_pixel_pos(x, y), values[C], values, unmultiply_alpha); + } + + /** + * Return the alpha compnent only. + * + * @arg x - The x coordinate to set + * @arg y - The y coordinate to set + * + * @returns The alpha channel at the given coordinates + */ + double alphaAt(int x, int y) const { return _get_alpha(_pixel_pos(x, y)); } + + /** + * Use bilinear interpolation to get an alpha channel value inbetween pixels. + */ + double alphaAt(double x, double y) const + { + int fx = floor(x), fy = floor(y); + int cx = ceil(x), cy = ceil(y); + double weight_x = x - fx, weight_y = y - fy; + + int pos_a = _pixel_pos(fx, fy); + int pos_b = _pixel_pos(cx, fy); + int pos_c = _pixel_pos(fx, cy); + int pos_d = _pixel_pos(cx, cy); + + return _bilinear_interpolate(_get_alpha(pos_a), _get_alpha(pos_b), _get_alpha(pos_c), _get_alpha(pos_d), + weight_x, weight_y); + } + + /** + * Get the width of the surface image + */ + int width() const { return _width; } + + /** + * Get the height of the surface image + */ + int height() const { return _height; } + + /** + * Get the number of output channels minus alpha + */ + int getOutputChannels() const { return C; } + + /** + * Get access to the memory directly + */ + T* memory() { + if constexpr (NEXT) { + throw std::logic_error("DataAccess will not allow direct access for surfaces spanning multiple memory pools"); + } + return _memory; + } + const T* memory() const { return memory(); } + + /** + * Copy the surface into a single contigious memory surface and return. + */ + template + std::vector contigiousCopy(bool unpremultiply_alpha = false) + { + std::vector memory; + memory.reserve(SIZE * _width * _height); + + for (int pos = 0; pos < _height * _width * (P+1); pos += P+1) { + double alpha = unpremultiply_alpha ? _get_alpha(pos) : 1.0; + if (alpha == 0.0) alpha = 1.0; + for (int c = 0; c < C; c++) { + memory.emplace_back(_get_channel(pos, c, alpha)); + } + memory.emplace_back(_get_channel(pos, C, 1.0)); + } + return memory; + } + + + +private: + /* + * Sets the Primaries from this Color. + * + * If the next access is set it will recursively set the next unused channels to the next + * surface primaries until all are exhausted. + * + * @param pos - The typed memory Position in the surface to get a value from + * @param alpha - The value from the alpha Channel in the Color. + * @param values - The Color we're setting to this surface + * @param offset - Internal recursive value off where in the Color we have gotten to. + * + */ + inline void _set_primaries_recursively(int pos, double alpha, COLOR const &values, bool unmultiply_alpha, + int offset = 0) + { + if (pos < _height * _stride) { + // Set alpha in the surface + _memory[pos + _primary_pos(_alpha_primary)] = alpha * _scale; + auto mult = unmultiply_alpha ? alpha : 1.0; + + // Set the primaries in the surface + for (int p = 0; p < P + 1 && offset < values.size(); p++) { + if (p != _alpha_primary) { + _memory[pos + _primary_pos(p)] = values[offset] * mult * _scale; + offset++; + } + } + + // If we have more channels, keep setting them + if constexpr (NEXT) { + // Alpha is always set in every surface + _next_memory[pos + _primary_pos(_alpha_primary)] = alpha * _scale; + + for (int p = 0; p < P + 1 && offset < values.size(); p++) { + if (p != _alpha_primary) { + _next_memory[pos + _primary_pos(p)] = values[offset] * mult * _scale; + offset++; + } + } + } + } + } + + /** + * Get the channel value from a specific memory position + * + * @arg pos - The memory position in the surface (see _pixel_pos) + * @arg channel - Which channel to get, NOT the primary number. + * @arg alpha - If set will unpremultiply the channel, we do it here to preserve + * as much precision before possible conversion to int. + */ + template + inline T0 _get_channel(int pos, int channel, double alpha) const + { + // Allow this function to output integer types of various sizes as well as floating point types + constexpr static double scale = std::is_integral::value + ? (std::is_integral::value + ? std::numeric_limits::max() / std::numeric_limits::max() // T=char T0=int|char + : std::numeric_limits::max()) // T=float T0=int|char + : (T0)(1.0 / _scale); // T=float|char T0=float|double + + if (pos >= _height * _stride) { + return 0.0; + } + if constexpr (NEXT) { + if (channel >= P && channel != C) { + return _next_memory[pos + _primary_pos(channel - P + I)] * scale / alpha; + } + } + return _memory[pos + _primary_pos(channel < C ? channel + I : _alpha_primary)] * scale / alpha; + } + + /** + * Get the alpha primary only + */ + inline double _get_alpha(int pos) const + { + return pos >= _height * _stride ? 0.0 : _memory[pos + _primary_pos(_alpha_primary)] / _scale; + } + + /** + * Get the position in the memory of this pixel + */ + inline int _pixel_pos(int x, int y) const + { + if (x < 0 || y < 0 || x >= _width || y >= _height) { + switch (_edge_mode) { + case DrawingAccessEdgeMode::EXTEND: + x = std::clamp(x, (int)0, _width - 1); + y = std::clamp(y, (int)0, _height - 1); + break; + case DrawingAccessEdgeMode::WRAP: + x %= _width; + y %= _height; + break; + case DrawingAccessEdgeMode::ZERO: + // This means OOB to _get_channel, which will return zero + return _height * _stride + 1; + case DrawingAccessEdgeMode::ERROR: + default: + throw std::exception(); + break; + } + } + return y * _stride + x * (P + 1); + } + + /** + * Convert the primary position into a memory location based on the Endianess + * of the uint32 cairo stores things in. This might need adjustinng for platforms. + */ + inline int _primary_pos(int p) const + { +#if G_BYTE_ORDER == G_LITTLE_ENDIAN + return I ? P - p : p; +#else + return p; +#endif + } + /** + * Standard bilinear interpolation + */ + inline double _bilinear_interpolate(double a, double b, double c, double d, double wx, double wy) const + { + // This should only be useful for linearRGB color space, gamut curved colors such as sRGB would + // give bad results. This equasion is for premultiplied values. + return (a * wx + b * (1 - wx)) * wy + (c * wx + d * (1 - wx)) * (1 - wy); + } + + // Basic metrics for the surface + int const _width; + int const _height; + int const _stride; + T *_memory; + + // How are out of range x,y coordinates treated + DrawingAccessEdgeMode _edge_mode; + + // Keep a copy of the cairo surface RefPtr to keep it alive while we exist (we don't use it directly) + Cairo::RefPtr _cairo_surface; + + // When the color space involves more channels than primaries available in one cairo surface + T *_next_memory = nullptr; + Cairo::RefPtr _next_surface; +}; + +} // namespace Inkscape + +#endif // INKSCAPE_DISPLAY_DRAWING_ACCESS_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/filters/color-matrix.h b/src/display/filters/color-matrix.h new file mode 100644 index 0000000000..1caa7b4585 --- /dev/null +++ b/src/display/filters/color-matrix.h @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Raw filter functions for color matrix transforms + *//* + * Authors: + * Felipe CorrĂȘa da Silva Sanches + * Jasper van de Gronde + * Martin Owens + * + * Copyright (C) 2025 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_DISPLAY_FILTER_MATRIX_H +#define INKSCAPE_DISPLAY_FILTER_MATRIX_H + +#include +#include +#include +#include <2geom/math-utils.h> + +namespace Inkscape::Filters { + +struct ColorMatrix +{ + ColorMatrix(std::vector matrix, unsigned channels = 4, double adj = 0.0) + : _matrix(std::move(matrix)) + , _width(channels + 1) + , _adj(adj) + { + // Pad the matrix with the identity. + for (unsigned i = _matrix.size(); i < (_width * (_width - 1)); i++) { + _matrix.emplace_back(i % _width == i / _width); + } + } + + template + void filter(AccessDst &dst, AccessSrc const &src) + { + std::array c; + std::array o; + + for (int y = 0; y < dst.height(); y++) { + for (int x = 0; x < dst.width(); x++) { + std::fill(o.begin(), o.end(), 0); + src.colorAt(x, y, c, true); + + for (unsigned i = 0; i < c.size(); i++) { + if (i < _width - 1) { // Matrix is allowed to be smaller + for (unsigned j = 0; j < _width - 1; j++) { + o[i] += c[j] * _matrix[j + i * _width]; + } + } else { + o[i] = c[i]; + } + } + for (unsigned i = 0; i < c.size(); i++) { + o[i] = std::clamp(o[i], 0.0, 1.0); + } + dst.colorTo(x, y, o, true); + } + } + } + + std::vector _matrix; + unsigned const _width; + double const _adj; +}; + +struct ColorMatrixSaturate : ColorMatrix +{ + inline static std::vector get_matrix(double v_in) + { + // clamp parameter instead of clamping color values + double v = std::clamp(v_in, 0.0, 1.0); + // clang-format off + return { + 0.213+0.787*v, 0.715-0.715*v, 0.072-0.072*v, 0.0, + 0.213-0.213*v, 0.715+0.285*v, 0.072-0.072*v, 0.0, + 0.213-0.213*v, 0.715-0.715*v, 0.072+0.928*v, 0.0 + }; + // clang-format on + } + + ColorMatrixSaturate(double v_in) + : ColorMatrix(get_matrix(v_in), 3, 0.5) + {} +}; + +struct ColorMatrixHueRotate : ColorMatrix +{ + inline static std::vector get_matrix(double v_in) + { + double s, c; + Geom::sincos(v_in * M_PI / 180.0, s, c); + + // clang-format off + return { + 0.213 +0.787*c -0.213*s, 0.715 -0.715*c -0.715*s, 0.072 -0.072*c +0.928*s, 0.0, + 0.213 -0.213*c +0.143*s, 0.715 +0.285*c +0.140*s, 0.072 -0.072*c -0.283*s, 0.0, + 0.213 -0.213*c -0.787*s, 0.715 -0.715*c +0.715*s, 0.072 +0.928*c +0.072*s, 0.0 + }; + // clang-format on + } + ColorMatrixHueRotate(double v_in) + : ColorMatrix(get_matrix(v_in), 3) + {} +}; + +struct ColorMatrixLuminance : ColorMatrix +{ + inline static std::vector get_matrix() + { + // clang-format off + // If we can sort out in vs. out this can be a single line matrix. + return { + 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, + 0.2125, 0.7154, 0.0721, 1.0, 0.0, + }; + // clang-format on + } + ColorMatrixLuminance() + : ColorMatrix(get_matrix()) + {} +}; + +} // namespace Inkscape::Filters + +#endif // INKSCAPE_DISPLAY_FILTER_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/filters/color-space.h b/src/display/filters/color-space.h new file mode 100644 index 0000000000..4294aae613 --- /dev/null +++ b/src/display/filters/color-space.h @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Convert between color spaces + *//* + * Authors: + * Martin Owens + * + * Copyright (C) 2025 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_DISPLAY_FILTER_COLOR_SPACE_H +#define INKSCAPE_DISPLAY_FILTER_COLOR_SPACE_H + +#include +#include +#include + +#include "colors/cms/transform-surface.h" +#include "colors/spaces/base.h" + +namespace Inkscape::Filters { + +template +struct ColorSpaceTransform +{ + // We expect to get transfer functions in the correct order for the input color space + ColorSpaceTransform(std::shared_ptr from, std::shared_ptr to) + : _from(std::move(from)) + , _to(std::move(to)) + { + + // Direct color spaces use lcms2 and no other transformation + if (_from->isDirect() && _to->isDirect()) { + if constexpr (AccessDst::SIZE == AccessSrc::SIZE && !AccessDst::NEXT && !AccessSrc::NEXT) { + _transform = TransformSurface(_from->getProfile(), _to->getProfile(), _from->getBestIntent(_to), {}, Colors::RenderingIntent::AUTO); + } + } + } + + void filter(AccessDst &dst, AccessSrc const &src) + { + std::array c0; + std::array c1; + if (_transform) { + _transform->do_transform(dst.width(), dst.height(), src.memory(), dst.memory()); + + // lcms2 transforms always returns unpremultiplied values, premultiply now + for (int y = 0; y < dst.height(); y++) { + for (int x = 0; x < dst.width(); x++) { + dst.colorAt(x, y, c1, true); + for (auto i = 0; i < AccessDst::SIZE - 1; i++) { + c1[i] *= c1.back(); + } + dst.colorTo(x, y, c1, true); + } + } + + return; + } + + // Manual + std::vector cin(c0.size(), 0.0); + for (int y = 0; y < dst.height(); y++) { + for (int x = 0; x < dst.width(); x++) { + // Conversions in inkscape are always alpha unmultiplied + src.colorAt(x, y, c0, true); + // Expensive conversion, array to vector and back + cin.assign(c0.begin(), c0.end()); + _from->convert(cin, _to); + for (int c = 0; c < c1.size(); c++) { + c1[c] = cin[c]; + } + dst.colorTo(x, y, c1, true); + } + } + } + + std::unique_ptr> _transform; + std::shared_ptr _from; + std::shared_ptr _to; +}; + +} // namespace Inkscape::Filters + +#endif // INKSCAPE_DISPLAY_FILTER_COLOR_SPACE_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/filters/component-transfer.h b/src/display/filters/component-transfer.h new file mode 100644 index 0000000000..6006deb4e3 --- /dev/null +++ b/src/display/filters/component-transfer.h @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Raw filter functions for component transfer + *//* + * Authors: + * Martin Owens + * + * Copyright (C) 2025 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_DISPLAY_FILTER_COMPONENT_TRANSFER_H +#define INKSCAPE_DISPLAY_FILTER_COMPONENT_TRANSFER_H + +#include +#include +#include + +namespace Inkscape::Filters { + +enum class TransferType +{ + IDENTITY, + TABLE, + DISCRETE, + LINEAR, + GAMMA, + ERROR +}; + +// A visit pattern was tested and found to be too slow +struct TransferFunction +{ + TransferType _type; + + // type=TABLE|DISCRETE + TransferFunction(std::vector table, bool discrete) + : _type(discrete ? TransferType::DISCRETE : TransferType::TABLE) + { + for (unsigned i = 0; i < table.size(); ++i) { + _table.emplace_back(std::round(std::clamp(table[i], 0.0, 1.0))); + } + if (_type == TransferType::TABLE) { + for (unsigned i = 0; i < _table.size() - 1; ++i) { + _next.emplace_back(_table[i + 1] - _table[i]); + } + } + } + std::vector _table; + std::vector _next; // Shadow table of next - this + + // type=LINEAR + TransferFunction(double slope, double intercept) + : _type(TransferType::LINEAR) + , _slope(slope) + , _intercept(intercept) + {} + double _slope; + double _intercept; + + // type=GAMMA + TransferFunction(double amplitude, double exponent, double offset) + : _type(TransferType::GAMMA) + , _amplitude(amplitude) + , _exponent(exponent) + , _offset(offset) + {} + double _amplitude; + double _exponent; + double _offset; +}; + +struct ComponentTransfer +{ + // We expect to get transfer functions in the correct order for the input color space + ComponentTransfer(std::vector functions) + : _functions(std::move(functions)) + {} + + template + void filter(AccessDst &dst, AccessSrc const &src) + { + std::array c; + for (int y = 0; y < dst.height(); y++) { + for (int x = 0; x < dst.width(); x++) { + src.colorAt(x, y, c, true); + filterColor(c); + dst.colorTo(x, y, c, true); + } + } + } + + template + inline void filterColor(std::array &c) + { + double iptr; + for (unsigned i = 0; i < SIZE && i < _functions.size(); i++) { + auto &f = _functions[i]; + switch (f._type) { + case TransferType::TABLE: + if (f._next.empty() || c[i] == 1.0) { + c[i] = f._table.back(); + } else { + auto dx = std::modf((f._next.size()) * c[i], &iptr); + unsigned k = iptr; + c[i] = ((f._table[k] * (1.0 - dx) + f._next[k] * dx)); + } + break; + case TransferType::DISCRETE: { + unsigned k = f._table.size() * c[i]; + c[i] = (k == f._table.size()) ? f._table.back() : f._table[k]; + break; + } + case TransferType::LINEAR: + c[i] = f._slope * c[i] + f._intercept; + break; + case TransferType::GAMMA: + c[i] = std::clamp(f._amplitude * std::pow(c[i], f._exponent) + f._offset, 0.0, 1.0); + break; + case TransferType::IDENTITY: + case TransferType::ERROR: + default: + break; + } + } + } + + std::vector _functions; +}; + +} // namespace Inkscape::Filters + +#endif // INKSCAPE_DISPLAY_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/filters/composite.h b/src/display/filters/composite.h new file mode 100644 index 0000000000..b37ee5feca --- /dev/null +++ b/src/display/filters/composite.h @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Raw filter functions for composite, most of the options are + * handled directly by cairo, these is just the arithmetic function. + *//* + * Authors: + * Martin Owens + * + * Copyright (C) 2025 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_DISPLAY_FILTER_COMPOSITE_H +#define INKSCAPE_DISPLAY_FILTER_COMPOSITE_H + +#include +#include + +#include "display/drawing-access.h" + +namespace Inkscape::Filters { + +struct CompositeArithmetic +{ + double _k1, _k2, _k3, _k4; + + CompositeArithmetic(double k1, double k2, double k3, double k4) + : _k1(k1) + , _k2(k2) + , _k3(k3) + , _k4(k4) + {} + + template + void filter(AccessDst &dst, AccessSrc const &src) + { + // TODO: Detect different color spaces and convert + std::array c1; + std::array c2; + + for (int y = 0; y < dst.height(); y++) { + for (int x = 0; x < dst.width(); x++) { + dst.colorAt(x, y, c1, true); + src.colorAt(x, y, c2, true); + for (unsigned i = 0; i < c1.size() - 1 && i < c2.size() - 1; i++) { + c1[i] = std::clamp(_k1 * c1[i] * c2[i] + _k2 * c1[i] + _k3 * c2[i] + _k4, 0.0, 1.0); + } + dst.colorTo(x, y, c1, true); + } + } + } +}; + +} // namespace Inkscape::Filters + +#endif // INKSCAPE_DISPLAY_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/filters/convolve-matrix.h b/src/display/filters/convolve-matrix.h new file mode 100644 index 0000000000..e118f4e3a9 --- /dev/null +++ b/src/display/filters/convolve-matrix.h @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Raw filter primative for convolve matrix + *//* + * Authors: + * Martin Owens + * + * Copyright (C) 2025 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_DISPLAY_FILTER_CONVOLVE_MATRIX_H +#define INKSCAPE_DISPLAY_FILTER_CONVOLVE_MATRIX_H + +#include +#include +#include +#include +#include // DEBUG +#include + +namespace Inkscape { + +struct ConvolveMatrix +{ + ConvolveMatrix(int targetX, int targetY, int orderX, int orderY, double divisor, double bias, + std::vector const &kernel, bool preserve_alpha) + : _kernel(kernel.size()) + , _targetX(targetX) + , _targetY(targetY) + , _orderX(orderX) + , _orderY(orderY) + , _bias(bias) + , _preserve_alpha(preserve_alpha) + { + 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()); + } + + template + void filter(AccessDst &dst, AccessSrc const &src) + { + unsigned alpha = dst.getOutputChannels() + (_preserve_alpha ? 0 : 1); + std::array output; + std::vector> patch(_kernel.size(), output); + + auto width = dst.width(); + auto height = dst.height(); + auto last_line = _orderY - 1; + auto read_pos = last_line - _targetY; + + // IF this code is too complicated, then change it to be simple get/set per pixel + for (int x = 0; x < width; x++) { + for (int j = 0; j < last_line; j++) { + for (int i = 0; i < _orderX; i++) { + // It's ok to ask for negative coordinates, they use EdgeMode set in src + src.colorAt(x + i - _targetX, j - _targetY, patch[i + j * _orderX], true); + } + } + + // We build the source data matrix progressively so we don't have to + // read as many of the same pixels over and over again. + for (int y = 0; y < height; y++) { + // Result starts off with bias + std::fill(output.begin(), output.end(), _bias); + + // This is the line in the patch we need to write to + auto offset = y % _orderY; + auto read_line = offset == 0 ? last_line : offset - 1; + + for (int i = 0; i < _orderX; i++) { + // May read beyond height and width, result depends on EdgeMode in src + src.colorAt(x + i - _targetX, y + read_pos, patch[read_line * _orderX + i], true); + for (int j = 0; j < _orderY; j++) { + double coeff = _kernel[j * _orderX + i]; + auto pos = ((j + offset) * _orderX + i) % patch.size(); + + // Covolve each color channel + for (auto k = 0; k < alpha; k++) { + output[k] += (patch[pos][k] * coeff); + } + } + } + + // Alpha is preserved if needed + if (_preserve_alpha) { + output[alpha] = patch[((_targetY + offset) * _orderX + _targetX) % patch.size()][alpha]; + } + + // Clamp result + for (auto k = 0; k < alpha; k++) { + output[k] = std::clamp(output[k], 0.0, 1.0); + } + + // Save result to dest (hopefully not the same as src!) + dst.colorTo(x, y, output, true); + } + } + } + + std::vector _kernel; + int _targetX, _targetY, _orderX, _orderY; + double _bias; + bool _preserve_alpha; + + // We expect unpremultiplied Alpha + static bool const needs_unmultiplied = true; +}; + +} // namespace Inkscape + +#endif // INKSCAPE_DISPLAY_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/filters/displacement-map.h b/src/display/filters/displacement-map.h new file mode 100644 index 0000000000..9dab63b000 --- /dev/null +++ b/src/display/filters/displacement-map.h @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Raw filter functions for displacement map. + *//* + * Authors: + * Martin Owens + * + * Copyright (C) 2025 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_DISPLAY_FILTER_DISPLACEMENT_MAP_H +#define INKSCAPE_DISPLAY_FILTER_DISPLACEMENT_MAP_H + +#include + +#include "display/drawing-access.h" + +namespace Inkscape::Filters { + +struct DisplacementMap +{ + DisplacementMap(unsigned xch, unsigned ych, double scalex, double scaley) + : _xch(xch) + , _ych(ych) + , _scalex(scalex / 255.0) + , _scaley(scaley / 255.0) + {} + + template + void filter(AccessDst &dst, AccessTexture const &texture, AccessMap const &map) + { + std::array output; + std::array mappx; + + for (int y = 0; y < dst.height(); y++) { + for (int x = 0; x < dst.width(); x++) { + map.colorAt(x, y, mappx, true); + + // This is allowed to request out of bounds; You should + // set the texture's edgeMode to ZERO for SVG spec + texture.colorAt(x + _scalex * (mappx[_xch] - 0.5), y + _scaley * (mappx[_ych] - 0.5), output, true); + + // TODO: convert color space of output to dst + dst.colorTo(x, y, output, true); + } + } + } + + double _xch, _ych; + double _scalex, _scaley; +}; + +} // namespace Inkscape::Filters + +#endif // INKSCAPE_DISPLAY_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/filters/gaussian-blur.h b/src/display/filters/gaussian-blur.h new file mode 100644 index 0000000000..e5fe0967d3 --- /dev/null +++ b/src/display/filters/gaussian-blur.h @@ -0,0 +1,623 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Raw filter functions for gaussian blur + * + * 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 + * + *//* + * + * Authors: + * Martin Owens + * Niko Kiirala + * bulia byak + * Jasper van de Gronde + * + * Copyright (C) 2006-2025 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_DISPLAY_FILTER_DISPLACEMENT_MAP_H +#define INKSCAPE_DISPLAY_FILTER_DISPLACEMENT_MAP_H + +#include +#include +#include +#include <2geom/point.h> + +#include "display/dispatch-pool.h" +#include "display/drawing-access.h" +#include "display/threading.h" +#include "util/fixed_point.h" + +namespace Inkscape::Filters { + +enum class BlurQuality +{ + BEST = 2, + BETTER = 1, + NORMAL = 0, + WORSE = -1, + WORST = -2 +}; + +/* + * 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). + */ +constexpr size_t N = 3; + +template +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; +typedef double FIRValue; + +// 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 Util::FixedPoint FIRValue; + +template +static inline T sqr(T const &v) +{ + return v * v; +} + +template +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 +static inline Tt round_cast(Ts v) +{ + static Ts const rndoffset(.5); + return static_cast(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(v+rndoffset); +#endif +}*/ + +template +static inline Tt clip_round_cast(Ts const v) +{ + Ts const minval = std::numeric_limits::min(); + Ts const maxval = std::numeric_limits::max(); + Tt const minval_rounded = std::numeric_limits::min(); + Tt const maxval_rounded = std::numeric_limits::max(); + if (v < minval) + return minval_rounded; + if (v > maxval) + return maxval_rounded; + return round_cast(v); +} + +template +static inline Tt clip_round_cast_varmax(Ts const v, Tt const maxval_rounded) +{ + Ts const minval = std::numeric_limits::min(); + Tt const maxval = maxval_rounded; + Tt const minval_rounded = std::numeric_limits::min(); + if (v < minval) + return minval_rounded; + if (v > maxval) + return maxval_rounded; + return round_cast(v); +} + +struct GaussianBlur +{ + BlurQuality _quality; + Geom::Point _step; + Geom::Point _deviation; + + GaussianBlur(Geom::Point deviation, BlurQuality quality) + : _quality(quality) + , _step(1 << _effect_subsample_step_log2(deviation[Geom::X], _quality), + 1 << _effect_subsample_step_log2(deviation[Geom::Y], _quality)) + , _deviation(deviation[Geom::X] / _step[Geom::X], deviation[Geom::Y] / _step[Geom::Y]) + {} + + /** + * Get the downsampled size that the given destination should be at the given quality + */ + template + std::optional getDownsampleSize(Access surface) + { + if (_step[Geom::X] > 1 || _step[Geom::Y] > 1) { + return {{static_cast(ceil(static_cast(surface.width()) / _step[Geom::X])) + 1, + static_cast(ceil(static_cast(surface.height()) / _step[Geom::Y])) + 1}}; + } + return {}; + } + + template + void filter(Access &surface) + { + auto const pool = get_global_dispatch_pool(); + auto scr_len = get_effect_area_scr(); + + // 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. + // I/FIR: In/finite impulse response + bool use_IIR_x = _deviation[Geom::X] > 3; + bool use_IIR_y = _deviation[Geom::Y] > 3; + + // Temporary storage for IIR filter + // NOTE: This can be eliminated, but it reduces the precision a bit + int threads = pool->size(); + std::vector *> tmpdata(threads, nullptr); + if (use_IIR_x || use_IIR_y) { + for (int i = 0; i < threads; ++i) { + tmpdata[i] = new std::array[std::max(surface.width(), surface.height())]; + } + } + + if (scr_len[Geom::X] > 0) { + if (use_IIR_x) { + gaussian_pass_IIR(surface, tmpdata.data(), *pool); + } else { + gaussian_pass_FIR(surface, *pool); + } + } + + if (scr_len[Geom::Y] > 0) { + if (use_IIR_y) { + gaussian_pass_IIR(surface, tmpdata.data(), *pool); + } else { + gaussian_pass_FIR(surface, *pool); + } + } + + // free the temporary data + if (use_IIR_x || use_IIR_y) { + for (int i = 0; i < threads; ++i) { + delete[] tmpdata[i]; + } + } + } + +private: + inline Geom::IntPoint get_effect_area_scr() const + { + return {(int)std::ceil(std::fabs(_deviation[Geom::X]) * 3.0), + (int)std::ceil(std::fabs(_deviation[Geom::X]) * 3.0)}; + } + + /** + * Request or set a color and allow the axis to be flipped. Is always alpha unpremultiplied. + */ + template + constexpr inline void colorAt(Access &surface, int x, int y, std::array &output) + { + if constexpr (axis == Geom::X) { + surface.colorAt(x, y, output, false); + } else { + surface.colorAt(y, x, output, false); + } + } + template + constexpr inline void colorTo(Access &surface, int x, int y, std::array const &input) + { + if constexpr (axis == Geom::X) { + surface.colorTo(x, y, input, false); + } else { + surface.colorTo(y, x, input, false); + } + } + + template + void gaussian_pass_IIR(Access &surface, std::array **tmpdata, dispatch_pool &pool) + { + // 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[axis], 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 w = surface.width(); + int h = surface.height(); + int col_count = axis == Geom::X ? w : h; + int row_count = axis == Geom::X ? h : w; + + pool.dispatch(row_count, [&](int row, int tid) { + // Border constants + std::array imin; + std::array imax; + colorAt(surface, 0, row, imin); + colorAt(surface, col_count - 1, row, imax); + + int col_write = col_count; + + // Forward pass + std::array u[N + 1]; + for (int i = 0; i < N; i++) { + u[i] = imin; + } + + for (int col = 0; col < col_count; col++) { + for (int i = N; i > 0; i--) { + u[i] = u[i - 1]; + } + colorAt(surface, col, row, u[0]); + for (int c = 0; c < Access::SIZE; c++) { + u[0][c] *= b[0]; + } + for (int i = 1; i < N + 1; i++) { + for (int c = 0; c < Access::SIZE; c++) { + u[0][c] += u[i][c] * b[i]; + } + } + *(tmpdata[tid] + col) = u[0]; + } + + // Backward pass + std::array v[N + 1]; + calcTriggsSdikaInitialization(M, u, imax, imax, b[0], v); + + colorTo(surface, --col_write, row, v[0]); + + for (int col = col_count - 2; col >= 0; col--) { + for (int i = N; i > 0; i--) { + v[i] = v[i - 1]; + } + v[0] = *(tmpdata[tid] + col); + + for (int c = 0; c < Access::SIZE; c++) { + v[0][c] *= b[0]; + } + for (int i = 1; i < N + 1; i++) { + for (int c = 0; c < Access::SIZE; c++) { + v[0][c] += v[i][c] * b[i]; + } + } + colorTo(surface, --col_write, row, v[0]); + } + }); + } + + template + void gaussian_pass_FIR(Access &surface, dispatch_pool &pool) + { + int scr_len = get_effect_area_scr()[axis]; + // Filter kernel for x direction + std::vector kernel(scr_len + 1); + _make_kernel(&kernel[0]); + + int w = surface.width(); + int h = surface.height(); + int col_count = axis == Geom::X ? w : h; + int row_count = axis == Geom::X ? h : w; + + int xm = 1; + int ym = scr_len; + if (axis == Geom::Y) { + std::swap(xm, ym); + } + + // Filters over 1st dimension + // Assumes kernel is symmetric + // Kernel should have scr_len+1 elements + pool.dispatch(row_count, [&](int row, int tid) { + // Past pixels seen (to enable in-place operation) + boost::container::small_vector, 10> history(scr_len + 1); + + std::array skipbuf; + std::array px_in; + std::array px_out; + std::array px_cmp; + skipbuf.fill(INT_MIN); + + // history initialization + std::array imin; + colorAt(surface, 0, row, imin); + for (int i = 0; i < scr_len; i++) { + history[i] = imin; + } + + for (int col = 0; col < col_count; col++) { + // update history + for (int i = scr_len; i > 0; i--) { + history[i] = history[i - 1]; + } + colorAt(surface, col, row, history[0]); + + // for all bytes of the pixel + for (unsigned int byte = 0; byte < Access::SIZE; byte++) { + if (skipbuf[byte] > col) + continue; + + FIRValue sum = 0; + double 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 + double 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 + for (int i = 1; i <= scr_len; i++) { + // the pixel we're looking at + int col_in = col + i; + if (col_in >= col_count) { + col_in = col_count - 1; + } + + // value at the pixel + colorAt(surface, col_in, row, px_in); + + // is it the same as last one we saw? + if (px_in[byte] != last_in) + different_count++; + last_in = px_in[byte]; + + // sum pixels weighted by the kernel + sum += px_in[byte] * kernel[i]; + } + + // store the result for setting later + px_out[byte] = 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 + // This is HORRIFYING, but is needed to maintain the same logic as the previous code + // the truth is this might not be even possible to do any more. + int pos = 0; + while (col + pos + scr_len < col_count - 1 && (!pos || px_cmp[byte] == last_in)) { + colorAt(surface, col + pos + scr_len, row, px_cmp); + if (pos) { + colorAt(surface, col + pos, row, px_cmp); + px_cmp[byte] = last_in; + colorTo(surface, col + pos, row, px_cmp); + } + pos++; + } + skipbuf[byte] = pos; + } + } + colorTo(surface, col, row, px_out); + } + }); + } + + template + void _make_kernel(FIRValue *const kernel) + { + int const scr_len = get_effect_area_scr()[axis]; + if (scr_len < 0) { + throw std::exception(); + } + double const d_sq = sqr(_deviation[axis]) * 2; + boost::container::small_vector 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(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, BlurQuality 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 BlurQuality::WORST: + // 2 == log(x*8/3)) + // 2^2 == x*2^3/3 + // x == 3/2 + stepsize_l2 = clip(static_cast(log(deviation * (3. / 2.)) / log(2.)), 0, 12); + break; + case BlurQuality::WORSE: + // 2 == log(x*16/3)) + // 2^2 == x*2^4/3 + // x == 3/2^2 + stepsize_l2 = clip(static_cast(log(deviation * (3. / 4.)) / log(2.)), 0, 12); + break; + case BlurQuality::BETTER: + // 2 == log(x*32/3)) + // 2 == x*2^5/3 + // x == 3/2^4 + stepsize_l2 = clip(static_cast(log(deviation * (3. / 16.)) / log(2.)), 0, 12); + break; + case BlurQuality::BEST: + stepsize_l2 = 0; // no subsampling at all + break; + case BlurQuality::NORMAL: + default: + // 2 == log(x*16/3)) + // 2 == x*2^4/3 + // x == 3/2^3 + stepsize_l2 = clip(static_cast(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 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 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 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 + static void calcTriggsSdikaInitialization(double const M[N * N], std::array const uold[N], + std::array const uplus, + std::array const vplus, IIRValue const alpha, + std::array vold[N]) + { + for (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]; + } + } + } +}; + +} // namespace Inkscape::Filters + +#endif // INKSCAPE_DISPLAY_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/filters/light.h b/src/display/filters/light.h new file mode 100644 index 0000000000..9522c35616 --- /dev/null +++ b/src/display/filters/light.h @@ -0,0 +1,409 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Diffuse Lighting raw filtering + *//* + * Authors: + * Niko Kiirala + * Jean-Rene Reinhard + * Martin Owens + * + * Copyright (C) 2014-2025 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_DISPLAY_FILTER_LIGHT_H +#define INKSCAPE_DISPLAY_FILTER_LIGHT_H + +#include +#include +#include <2geom/affine.h> + +#include "display/drawing-access.h" + +namespace Inkscape::Filters { + +#define X_3D 0 +#define Y_3D 1 +#define Z_3D 2 + +typedef std::array vector3d; + +// The eye vector for specular lighting +static vector3d const EYE_VECTOR = {0.0, 0.0, 1.0}; + +/** + * 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(vector3d const &v) +{ + return sqrt(v[X_3D] * v[X_3D] + v[Y_3D] * v[Y_3D] + v[Z_3D] * v[Z_3D]); +} + +/** + * Normalizes a vector + * + * \param v a reference to a vector to normalize + */ +void normalize_vector(vector3d &v) +{ + double nv = norm(v); + // TODO test nv == 0 + for (int j = 0; j < 3; j++) { + v[j] /= nv; + } +} + +/** + * Computes the scalar product between two vector3ds + * + * \param a a vector3d reference + * \param b a vector3d reference + * \return the scalar product of a and b + */ +double scalar_product(vector3d const &a, vector3d const &b) +{ + return a[X_3D] * b[X_3D] + a[Y_3D] * b[Y_3D] + a[Z_3D] * b[Z_3D]; +} + +/** + * Computes the normalized sum of two vector3ds + * + * \param r a vector3d reference where we store the result + * \param a a vector3d reference + * \param b a vector3d reference + */ +void normalized_sum(vector3d &r, vector3d const &a, vector3d const &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); +} + +/** + * 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 coords a reference to coordinates + * \param trans a reference to a transformation matrix + * \param device_scale an optional device scale for HiDPI + */ +void convert_coord(vector3d coords, Geom::Affine const &trans, double device_scale) +{ + Geom::Point p = Geom::Point(coords[X_3D] * device_scale, coords[Y_3D] * device_scale) * trans; + coords[X_3D] = p[Geom::X]; + coords[Y_3D] = p[Geom::Y]; + coords[Z_3D] *= device_scale * trans[0]; +} + +/** + * Base functionality for diffuse and specular lighting filters + */ +struct Lighting +{ + Lighting(double scale, double light_constant, std::optional specular_exponent) + : _specular(specular_exponent) + , _scale(scale) + , _const(light_constant) + , _exp(_specular ? *specular_exponent : 1.0) + {} + +protected: + template + void doLighting(AccessSrc const &src, int x, int y, vector3d light, + std::array const &color, std::array &output) + { + if (_specular) { + normalized_sum(light, light, EYE_VECTOR); + } + vector3d normal = surfaceNormalAt(src, x, y, _scale); + double sp = scalar_product(normal, light); + double k = sp <= 0.0 ? 0.0 : _const * std::pow(sp, _exp); + + for (unsigned i = 0; i < color.size() - 1; i++) { + output[i] = std::clamp(k * color[i], 0.0, 1.0); + output.back() = std::max(output[i], output.back()); + } + } + + // compute surface normal at given coordinates using 3x3 Sobel gradient filter + template + vector3d surfaceNormalAt(AccessSrc const &src, 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 src.alphaAt is slightly lazy. + vector3d normal; + double fx = -scale, fy = -scale; + normal[Z_3D] = 1.0; + if (x == 0) [[unlikely]] { + // leftmost column + if (y == 0) [[unlikely]] { + // upper left corner + fx *= (2.0 / 3.0); + fy *= (2.0 / 3.0); + double p00 = src.alphaAt(x, y), p10 = src.alphaAt(x + 1, y), p01 = src.alphaAt(x, y + 1), + p11 = src.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 (y == (src.height() - 1)) [[unlikely]] { + // lower left corner + fx *= (2.0 / 3.0); + fy *= (2.0 / 3.0); + double p00 = src.alphaAt(x, y - 1), p10 = src.alphaAt(x + 1, y - 1), p01 = src.alphaAt(x, y), + p11 = src.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 = src.alphaAt(x, y - 1), p10 = src.alphaAt(x + 1, y - 1), p01 = src.alphaAt(x, y), + p11 = src.alphaAt(x + 1, y), p02 = src.alphaAt(x, y + 1), p12 = src.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 (x == (src.width() - 1)) [[unlikely]] { + // rightmost column + if (y == 0) [[unlikely]] { + // top right corner + fx *= (2.0 / 3.0); + fy *= (2.0 / 3.0); + double p00 = src.alphaAt(x - 1, y), p10 = src.alphaAt(x, y), p01 = src.alphaAt(x - 1, y + 1), + p11 = src.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 (y == (src.height() - 1)) [[unlikely]] { + // bottom right corner + fx *= (2.0 / 3.0); + fy *= (2.0 / 3.0); + double p00 = src.alphaAt(x - 1, y - 1), p10 = src.alphaAt(x, y - 1), p01 = src.alphaAt(x - 1, y), + p11 = src.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 = src.alphaAt(x - 1, y - 1), p10 = src.alphaAt(x, y - 1), p01 = src.alphaAt(x - 1, y), + p11 = src.alphaAt(x, y), p02 = src.alphaAt(x - 1, y + 1), p12 = src.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 (y == 0) [[unlikely]] { + // top row + fx *= (1.0 / 3.0); + fy *= (1.0 / 2.0); + double p00 = src.alphaAt(x - 1, y), p10 = src.alphaAt(x, y), p20 = src.alphaAt(x + 1, y), + p01 = src.alphaAt(x - 1, y + 1), p11 = src.alphaAt(x, y + 1), p21 = src.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 (y == (src.height() - 1)) [[unlikely]] { + // bottom row + fx *= (1.0 / 3.0); + fy *= (1.0 / 2.0); + double p00 = src.alphaAt(x - 1, y - 1), p10 = src.alphaAt(x, y - 1), p20 = src.alphaAt(x + 1, y - 1), + p01 = src.alphaAt(x - 1, y), p11 = src.alphaAt(x, y), p21 = src.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 = src.alphaAt(x - 1, y - 1), p10 = src.alphaAt(x, y - 1), p20 = src.alphaAt(x + 1, y - 1), + p01 = src.alphaAt(x - 1, y), p11 = 0.0, p21 = src.alphaAt(x + 1, y), + p02 = src.alphaAt(x - 1, y + 1), p12 = src.alphaAt(x, y + 1), p22 = src.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; + normalize_vector(normal); + return normal; + } + + bool _specular; + double _scale; + double _const; + double _exp; +}; + +struct DistantLight : public Lighting +{ + DistantLight(double azimuth, double elevation, std::vector color, double scale, double light_constant, + std::optional specular_exponent = {}) + : Lighting(scale, light_constant, specular_exponent) + , _azimuth(M_PI / 180 * azimuth) + , _elevation(M_PI / 180 * elevation) + , _color(color) + // Computes the light vector of the distant light + , _lightv{std::cos(_azimuth) * std::cos(_elevation), std::sin(_azimuth) * std::cos(_elevation), + std::sin(_elevation)} + {} + + template + void filter(AccessDst &dst, AccessSrc const &src) + { + std::array lit_color; + // Conversion of color here + for (auto i = 0; i < _color.size(); i++) { + lit_color[i] = _color[i]; + } + std::array output; + if (!_specular) { // Diffuse is alpha 1.0 + output.back() = 1.0; + } + for (int y = 0; y < dst.height(); y++) { + for (int x = 0; x < dst.width(); x++) { + doLighting(src, x, y, _lightv, lit_color, output); + dst.colorTo(x, y, output, true); + } + } + } + +private: + double _azimuth; + double _elevation; + std::vector _color; + vector3d _lightv; +}; + +/** + * @arg device_scale - high DPI monitors + * @arg trans - The transformation between absolute coordinate use in the svg + * and current coordinate used in the rendering + */ +struct PointLight : public Lighting +{ + PointLight(vector3d coords, double x0, double y0, Geom::Affine const &trans, int device_scale, + std::vector color, double scale, double light_constant, + std::optional specular_exponent = {}) + : Lighting(scale, light_constant, specular_exponent) + , _coords(coords) + , _x0(x0) + , _y0(y0) + , _color(color) + { + // Computes the light vector of the distant light at point (x,y,z) + convert_coord(_coords, trans, device_scale); + } + + template + void filter(AccessDst &dst, AccessSrc const &src) + { + std::array lit_color; + // Conversion of color here + for (auto i = 0; i < _color.size(); i++) { + lit_color[i] = _color[i]; + } + std::array output; + if (!_specular) { // Diffuse is alpha 1.0 + output.back() = 1.0; + } + for (int y = 0; y < dst.height(); y++) { + for (int x = 0; x < dst.width(); x++) { + vector3d light{_coords[X_3D] - (_x0 + x), _coords[Y_3D] - (_y0 + y), + _coords[Z_3D] - _scale * src.alphaAt(x, y)}; + normalize_vector(light); + doLighting(src, x, y, light, lit_color, output); + dst.colorTo(x, y, output, true); + } + } + } + +private: + vector3d _coords; + double _x0, _y0; + std::vector _color; +}; + +struct SpotLight : public Lighting +{ + SpotLight(vector3d coords, vector3d pointAt, double limitingConeAngle, double specularExponent, double x0, + double y0, Geom::Affine const &trans, int device_scale, std::vector color, double scale, + double light_constant, std::optional specular_exponent = {}) + : Lighting(scale, light_constant, specular_exponent) + , _coords(coords) + , _pointAt(pointAt) + , _cos_lca(std::cos(M_PI / 180 * limitingConeAngle)) + , _spe_exp(specularExponent) + , _color(color) + , _x0(x0) + , _y0(y0) + { + convert_coord(_coords, trans, device_scale); + convert_coord(_pointAt, trans, device_scale); + S = {_pointAt[X_3D] - _coords[X_3D], _pointAt[Y_3D] - _coords[Y_3D], _pointAt[Z_3D] - _coords[Z_3D]}; + normalize_vector(S); + } + + template + void filter(AccessDst &dst, AccessSrc const &src) + { + std::array lit_color; + std::array output; + if (!_specular) { // Diffuse is alpha 1.0 + output.back() = 1.0; + } + + for (int y = 0; y < dst.height(); y++) { + for (int x = 0; x < dst.width(); x++) { + vector3d light{_coords[X_3D] - (_x0 + x), _coords[Y_3D] - (_y0 + y), + _coords[Z_3D] - _scale * src.alphaAt(x, y)}; + normalize_vector(light); + + double spmod = (-1) * scalar_product(light, S); + if (spmod <= _cos_lca) { + spmod = 0; + } else { + spmod = std::pow(spmod, _spe_exp); + } + for (unsigned i = 0; i < _color.size() - 1; i++) { + lit_color[i] = _color[i] * spmod; + } + doLighting(src, x, y, light, lit_color, output); + dst.colorTo(x, y, output, true); + } + } + } + +private: + // light position coordinates in render setting + vector3d _coords; + vector3d _pointAt; + double _cos_lca; // cos of the limiting cone angle + double _spe_exp; // specular exponent; + + std::vector _color; + double _x0, _y0; + + vector3d S; // unit vector from light position in the direction + // the spot point at +}; + +} // namespace Inkscape::Filters + +#endif // INKSCAPE_DISPLAY_FILTER_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/filters/morphology.h b/src/display/filters/morphology.h new file mode 100644 index 0000000000..156e42e3d3 --- /dev/null +++ b/src/display/filters/morphology.h @@ -0,0 +1,201 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Raw filter functions for morpholoy filters. + *//* + * Authors: + * Felipe CorrĂȘa da Silva Sanches + * Martin Owens + * + * Copyright (C) 2007-2025 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_DISPLAY_FILTER_MORPHOLOGY_H +#define INKSCAPE_DISPLAY_FILTER_MORPHOLOGY_H + +#include +#include +#include +#include +#include +#include <2geom/point.h> + +#include "display/dispatch-pool.h" +#include "display/drawing-access.h" +#include "display/threading.h" + +static int const POOL_THRESHOLD = 2048; + +namespace Inkscape::Filters { + +/* 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. + */ + +struct Morphology +{ + bool _erode; // true: erode, false: dilate + Geom::Point _radius; + + Morphology(bool erode, Geom::Point radius) + : _erode(erode) + , _radius(radius) + {} + + // The mid aurface can be eliminnated when we have a 2d algo + template + void filter(AccessDst &dst, AccessMid &mid, AccessSrc const &src) + { + if (_erode) { + singleAxisPass, AccessDst, AccessMid, Geom::X>(mid, src); + singleAxisPass, AccessDst, AccessMid, Geom::Y>(dst, mid); + } else { + singleAxisPass, AccessMid, AccessSrc, Geom::X>(mid, src); + singleAxisPass, AccessMid, AccessSrc, Geom::Y>(dst, mid); + } + } + + template + void singleAxisPass(AccessDst &dst, AccessSrc const &src) + { + int channels = dst.getOutputChannels() + 1; + Comparison comp; + + int w = dst.width(); + int h = dst.height(); + if (axis == Geom::Y) + std::swap(w, h); + + int ri = round(_radius[axis]); // TODO: Support fractional radii? + int wi = 2 * ri + 1; + int const limit = w * h; + + auto const pool = get_global_dispatch_pool(); + pool->dispatch_threshold(h, limit > POOL_THRESHOLD, [&](int i, int) { + // In 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. + std::vector>> vals(channels); + std::array input; + std::array output; + + // Initialize with transparent black + for (int p = 0; p < AccessDst::SIZE; ++p) { + vals[p].emplace_back(-1, 0); // TODO: Only do this when performing an erosion? + } + int in_x = 0; + int out_x = 0; + + for (int j = 0; j < w + ri; ++j) { + for (int p = 0; p < channels; ++p) { + src.colorAt(axis == Geom::Y ? in_x : i, axis == Geom::Y ? i : in_x, input); + // 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, input[p])) + vals[p].pop_back(); // useless + vals[p].emplace_back(j, input[p]); + if (p == channels - 1) + in_x++; + } 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) { + output[p] = vals[p].front().second; + } + } + if (j >= ri) { + dst.colorTo(axis == Geom::Y ? out_x : i, axis == Geom::Y ? i : out_x, output); + out_x++; + } + } + + /* This is supposed to be quicker, but I couldn't get it to work: + + for (int j = 0; j < std::min(ri, w); ++j) { + src.colorAt(axis == Geom::Y ? in_x : i, axis == Geom::Y ? i : in_x, input); + for(int p = 0; p < channels; ++p) { + // 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, input[p])) { + vals[p].pop_back(); // useless + } + vals[p].emplace_back(j + wi, input[p]); + } + in_x++; + } + + // 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) { + src.colorAt(axis == Geom::Y ? in_x : i, axis == Geom::Y ? in_x : i, input); + for(int p = 0; p < channels; ++p) { + // 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, input[p])) { + vals[p].pop_back(); // useless + } + vals[p].emplace_back(j + wi, input[p]); + output[p] = vals[p].front().second; + } + dst.colorTo(axis == Geom::Y ? out_x : i, axis == Geom::Y ? i : out_x, output); + in_x++; + out_x++; + } + // 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 < channels; ++p) { + 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 < channels; ++p) { + // Remove out-of-range values + if (!vals[p].empty() && vals[p].front().first <= j) { + vals[p].pop_front(); // out-of-range + } + output[p] = vals[p].front().second; + } + dst.colorTo(axis == Geom::Y ? out_x : i, axis == Geom::Y ? i : out_x, output); + out_x++; + } + */ + }); + } +}; + +} // namespace Inkscape::Filters + +#endif // INKSCAPE_DISPLAY_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/filters/turbulence.h b/src/display/filters/turbulence.h new file mode 100644 index 0000000000..e2b8d55ab4 --- /dev/null +++ b/src/display/filters/turbulence.h @@ -0,0 +1,325 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Raw filter functions for turbulence and fractal noise + *//* + * Authors: + * Felipe CorrĂȘa da Silva Sanches + * Martin Owens + * + * 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-2025 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_DISPLAY_FILTER_TURBULENCE_H +#define INKSCAPE_DISPLAY_FILTER_TURBULENCE_H + +#include +#include <2geom/point.h> +#include <2geom/rect.h> + +#include "display/drawing-access.h" + +namespace Inkscape::Filters { + +class Turbulence +{ +public: + Turbulence(long seed, Geom::Rect const &tile, Geom::Point const &freq, bool stitch, bool fractalnoise, int octaves, + int channels = 4) + : _seed(seed) + , _tile(tile) + , _baseFreq(freq) + , _stitchTiles(stitch) + , _fractalnoise(fractalnoise) + , _octaves(octaves) + , _channels(channels) + , _latticeSelector() + , _wrapx(0) + , _wrapy(0) + , _wrapw(0) + , _wraph(0) + {} + + void setSeed(long seed) + { + _seed = seed; + _ready = false; + } + // Why no setTile() ? + void setbaseFrequency(Geom::Dim2 axis, double freq) + { + _baseFreq[axis] = freq; + _ready = false; + } + void setOctaves(int octaves) + { + _octaves = octaves; + _ready = false; + } + void setStitchTiles(bool stitch) + { + _stitchTiles = stitch; + _ready = false; + } + void setFractalnoise(bool fractalnoise) + { + _fractalnoise = fractalnoise; + _ready = false; + } + void setChannels(int channels) + { + _channels = channels; + _ready = false; + } + + template + void filter(AccessDst &dst, Geom::Affine trans, int x0, int y0) + { + if (!_ready) { + init(); + } + + std::array output; + + for (int y = 0; y < dst.height(); y++) { + for (int x = 0; x < dst.width(); x++) { + // transform is added now to keep randomness the same regardless + // of how the surface may have been transformed. + turbulencePixel(Geom::Point(x + x0, y + y0) * trans, output); + dst.colorTo(x, y, output, true); + } + } + } + + void init() + { + if (_ready) + return; + + // setup random number generator + _setupSeed(_seed); + + // Prep gradient memory + for (auto i = 0; i < 2 * BSize + 2; ++i) { + _gradient[i][0] = std::vector(_channels, 0.0); + _gradient[i][1] = std::vector(_channels, 0.0); + } + + int i; + for (int k = 0; k < _channels; ++k) { + for (i = 0; i < BSize; ++i) { + _latticeSelector[i] = i; + + do { + _gradient[i][0][k] = static_cast(_random() % (BSize * 2) - BSize) / BSize; + _gradient[i][1][k] = static_cast(_random() % (BSize * 2) - BSize) / BSize; + } while (_gradient[i][0][k] == 0 && _gradient[i][1][k] == 0); + + // normalize gradient + double s = hypot(_gradient[i][0][k], _gradient[i][1][k]); + _gradient[i][0][k] /= s; + _gradient[i][1][k] /= 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 < _channels; ++k) { + _gradient[BSize + i][0][k] = _gradient[i][0][k]; + _gradient[BSize + i][1][k] = _gradient[i][1][k]; + } + } + + // 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; + } + _ready = true; + } + + template + inline void turbulencePixel(Geom::Point const &point, std::array &output) const + { + std::fill(output.begin(), output.end(), 0.0); + int wrapx = _wrapx, wrapy = _wrapy, wrapw = _wrapw, wraph = _wraph; + + double x = point[Geom::X] * _baseFreq[Geom::X]; + double y = point[Geom::Y] * _baseFreq[Geom::Y]; + double ratio = 1.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); + + auto const *qxa = _gradient[b00]; + auto const *qxb = _gradient[b10]; + auto const *qya = _gradient[b01]; + auto const *qyb = _gradient[b11]; + for (int k = 0; k < SIZE; ++k) { + double a = _lerp(sx, rx0 * qxa[0][k] + ry0 * qxa[1][k], rx1 * qxb[0][k] + ry0 * qxb[1][k]); + double b = _lerp(sx, rx0 * qya[0][k] + ry1 * qya[1][k], rx1 * qyb[0][k] + ry1 * qyb[1][k]); + double r = _lerp(sy, a, b); + output[k] += _fractalnoise ? r / ratio : fabs(r) / 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; + } + } + + for (auto i = 0; i < SIZE; i++) { + if (_fractalnoise) { + output[i] += 1; + output[i] /= 2; + } + output[i] = std::clamp(output[i], 0.0, 1.0); + } + } + +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; + + // Input arguments + long _seed; + Geom::Rect _tile; + Geom::Point _baseFreq; + bool _stitchTiles; + bool _fractalnoise; + int _octaves; + int _channels; + + // Generated in init + int _latticeSelector[2 * BSize + 2]; + std::vector _gradient[2 * BSize + 2][2]; + int _wrapx; + int _wrapy; + int _wrapw; + int _wraph; + bool _ready = false; +}; + +} // namespace Inkscape::Filters + +#endif // INKSCAPE_DISPLAY_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/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp index 1df50de9ab..54adf07c6a 100644 --- a/src/ui/widget/canvas.cpp +++ b/src/ui/widget/canvas.cpp @@ -31,7 +31,6 @@ #include "canvas/stores.h" #include "canvas/synchronizer.h" #include "canvas/util.h" -#include "colors/cms/transform-cairo.h" #include "colors/cms/system.h" #include "desktop.h" #include "desktop-events.h" @@ -142,7 +141,6 @@ struct RedrawData Fragment store; bool decoupled_mode; Cairo::RefPtr snapshot_drawn; - std::shared_ptr cms_transform; // Saved prefs int coarsener_min_size; @@ -741,7 +739,7 @@ void CanvasPrivate::launch_redraw() rd.debug_show_redraw = prefs.debug_show_redraw; rd.snapshot_drawn = stores.snapshot().drawn ? stores.snapshot().drawn->copy() : Cairo::RefPtr(); - rd.cms_transform = q->_cms_active ? q->_cms_transform : nullptr; + //rd.cms_transform = q->_cms_active ? q->_cms_transform : nullptr; abort_flags.store((int)AbortFlags::None, std::memory_order_relaxed); @@ -1873,7 +1871,7 @@ void Canvas::set_cms_transform() // auto surface = get_surface(); // auto the_monitor = display->get_monitor_at_surface(surface); - _cms_transform = Colors::CMS::System::get().getDisplayTransform(); + //_cms_transform = Colors::CMS::System::get().getDisplayTransform(); } // Change cursor @@ -2470,9 +2468,9 @@ void CanvasPrivate::paint_single_buffer(Cairo::RefPtr const // Apply CMS transform for the screen. This rarely is used by modern desktops, but sometimes // the user will apply an RGB transform to color correct their screen. This happens now, so the // drawing plus all other canvas items (selection boxes, handles, etc) are also color corrected. - if (rd.cms_transform) { + /*if (rd.cms_transform) { rd.cms_transform->do_transform(surface->cobj(), surface->cobj()); - } + }*/ // Paint over newly drawn content with a translucent random colour. if (rd.debug_show_redraw) { diff --git a/testfiles/CMakeLists.txt b/testfiles/CMakeLists.txt index 3ab9ede887..3db2e45abd 100644 --- a/testfiles/CMakeLists.txt +++ b/testfiles/CMakeLists.txt @@ -63,7 +63,6 @@ set(TEST_SOURCES async_progress-test boolop-attr-test colors/cms-system-test - colors/cms-transform-color-test colors/document-cms-test colors/dragndrop-test colors/color-set-test @@ -158,8 +157,9 @@ add_unit_test(version-test TEST_SOURCE "version-test.cpp" include("${CMAKE_SOURCE_DIR}/src/colors/CMakeUnit.txt") get_color_unit_lib() - add_unit_tests(TEST_SOURCES "colors/cms-profile-test.cpp" + "colors/cms-transform-color-test.cpp" + "colors/cms-transform-surface-test.cpp" "colors/color-test.cpp" "colors/gamut-test.cpp" "colors/manager-test.cpp" @@ -185,6 +185,21 @@ add_unit_tests(TEST_SOURCES "colors/cms-profile-test.cpp" "colors/utils-test.cpp" EXTRA_LIBS colors_unit_lib) +include("${CMAKE_SOURCE_DIR}/src/display/CMakeUnit.txt") +get_display_unit_lib() +add_unit_tests(TEST_SOURCES "display/drawing-access-test.cpp" + "display/filter-color-matrix-test.cpp" + "display/filter-color-space-test.cpp" + "display/filter-component-transfer-test.cpp" + "display/filter-composite-test.cpp" + "display/filter-convolve-matrix-test.cpp" + "display/filter-light-test.cpp" + "display/filter-displacement-map-test.cpp" + "display/filter-turbulence-test.cpp" + "display/filter-morphology-test.cpp" + "display/filter-gaussian-blur-test.cpp" + EXTRA_LIBS display_unit_lib colors_unit_lib) + add_unit_test(css-syntactic-decomposition-test TEST_SOURCE "css-syntactic-decomposition-test.cpp" SOURCES "css/syntactic-decomposition.cpp" EXTRA_LIBS GLibmm::GLibmm croco_LIB) diff --git a/testfiles/src/colors/cms-transform-cairo-test.cpp b/testfiles/src/colors/cms-transform-cairo-test.cpp deleted file mode 100644 index fdd7b2bf91..0000000000 --- a/testfiles/src/colors/cms-transform-cairo-test.cpp +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * Unit test for Cairo Image Surface conversions - * - * Copyright (C) 2025 Authors - * - * Released under GNU GPL v2+, read the file 'COPYING' for more information. - */ - -#include - -#include "colors/cms/profile.h" -#include "colors/cms/transform.h" -#include "colors/cms/transform-cairo.h" - -using namespace Inkscape::Colors; - -namespace { - -TEST(ColorsCmsTransformCairo, writeTestsHere) -{ -} - -} // namespace - -/* - Local Variables: - mode:c++ - c-file-style:"stroustrup" - c-file-offsets:((innamespace . 0)(inline-open . 0)) - indent-tabs-mode:nil - fill-column:99 - End: -*/ -// vim: expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/testfiles/src/colors/cms-transform-surface-test.cpp b/testfiles/src/colors/cms-transform-surface-test.cpp new file mode 100644 index 0000000000..3a477664cc --- /dev/null +++ b/testfiles/src/colors/cms-transform-surface-test.cpp @@ -0,0 +1,218 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Unit test for Image Surface conversions + * + * Copyright (C) 2025 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include "../test-utils.h" + +#include "colors/cms/profile.h" +#include "colors/cms/transform.h" +#include "colors/cms/transform-surface.h" +#include "colors/spaces/enum.h" + +using namespace Inkscape::Colors; +using namespace Inkscape::Colors::CMS; + +static auto rgb = Profile::create_srgb(); +static auto grb = Profile::create_from_uri(INKSCAPE_TESTS_DIR "/data/colors/SwappedRedAndGreen.icc"); +static auto cmyk = Profile::create_from_uri(INKSCAPE_TESTS_DIR "/data/colors/default_cmyk.icc"); + +namespace { + +TEST(ColorsCmsTransformSurface, TransformFloatTypeIn) +{ + static const std::vector img = { + 0.2, 0.1, 0.3, 1.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.5, 1.0, 1.0, 1.0, 0.2, + }; + + // Test FloatOut + { + auto tr1 = TransformSurface(rgb, grb, RenderingIntent::PERCEPTUAL, {}, RenderingIntent::AUTO); + + std::vector out(img.size(), 0.0); + tr1.do_transform(2, 2, img.data(), out.data()); + + EXPECT_TRUE(VectorIsNear(out, { + 0.1, 0.2, 0.3, 1.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.5, 1.0, 1.0, 1.0, 0.2, + }, 0.001)); + } + + // Test IntOut + { + auto tr2 = TransformSurface(rgb, grb, RenderingIntent::PERCEPTUAL, {}, RenderingIntent::AUTO); + + std::vector out(img.size(), 0.0); + tr2.do_transform(2, 2, img.data(), out.data()); + + std::vector ret = { + 6553, 13109, 19661, 65535, 0, 0, 0, 0, + 0, 0, 0, 32768, 65534, 65535, 65535, 13107 + }; + ASSERT_TRUE(VectorIsNear(out, ret, 2)); + } + +} + +TEST(ColorsCmsTransformSurface, TransformIntTypeIn) +{ + static const std::vector img = { + 6553, 13109, 19661, 65535, 0, 0, 0, 0, + 0, 0, 0, 32768, 65534, 65535, 65535, 13107 + }; + + // Test FloatOut + { + auto tr1 = TransformSurface(rgb, grb, RenderingIntent::PERCEPTUAL, {}, RenderingIntent::AUTO); + + std::vector out(img.size(), 0.0); + tr1.do_transform(2, 2, img.data(), out.data()); + + EXPECT_TRUE(VectorIsNear(out, { + 0.2, 0.1, 0.3, 1.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.5, 1.0, 1.0, 1.0, 0.2, + }, 0.001)); + } + + // Test IntOut + { + auto tr2 = TransformSurface(rgb, grb, RenderingIntent::PERCEPTUAL, {}, RenderingIntent::AUTO); + + std::vector out(img.size(), 0.0); + tr2.do_transform(2, 2, img.data(), out.data()); + + std::vector ret = { + 13108, 6549, 19661, 65535, 0, 0, 0, 0, + 0, 0, 0, 32768, 65534, 65534, 65535, 13107 + }; + ASSERT_EQ(out, ret); + } +} + +TEST(ColorsCmsTransformSurface, TransformPremultiplied) +{ + static const std::vector img = { + 0.2, 0.1, 0.3, 0.5, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.5, 0.2, 0.2, 0.2, 0.2, + }; + + auto tr = TransformSurface(rgb, grb, RenderingIntent::PERCEPTUAL, {}, RenderingIntent::AUTO); + + std::vector out(img.size(), 0.0); + tr.do_transform(2, 2, img.data(), out.data()); + + EXPECT_TRUE(VectorIsNear(out, { + 0.2, 0.4, 0.6, 0.5, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.5, 1.0, 1.0, 1.0, 0.2, + }, 0.001)); + +} + +TEST(ColorsCmsTransformSurface, TransformCMYKToRGB) +{ + static const std::vector img = { + 1.0, 0.1, 0.3, 0.2, 0.5, 0.0, 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 0.4, 0.5, 0.2, 0.2, 0.2, 0.2, 0.2, + }; + + auto tr = TransformSurface(cmyk, rgb, RenderingIntent::PERCEPTUAL, {}, RenderingIntent::AUTO); + + std::vector out(img.size() - 4, 0.0); + tr.do_transform(2, 2, img.data(), out.data()); + + EXPECT_TRUE(VectorIsNear(out, { + -1.053, 0.529, 0.6, 0.5, 0.172, 0.16, 0.163, 0, + 0.659, 0.667, 0.677, 0.5, 0.667, 0.644, 0.639, 0.2 + }, 0.001)); + +} + +TEST(ColorsCmsTransformSurface, TransformRGBToCMYK) +{ + static const std::vector img = { + 0, 0.529, 0.6, 0.5, 0.172, 0.16, 0.163, 0, + 0.659, 0.667, 0.677, 0.5, 0.667, 0.644, 0.639, 0.2 + }; + + auto tr = TransformSurface(rgb, cmyk, RenderingIntent::PERCEPTUAL, {}, RenderingIntent::AUTO); + + std::vector out(img.size() + 4, 0.0); + tr.do_transform(2, 2, img.data(), out.data()); + + EXPECT_TRUE(VectorIsNear(out, { + 0.892, 0.329, 0.363, 0.037, 0.5, 0.686, 0.693, 0.653, 0.867, 0, + 0.365, 0.293, 0.287, 0.0, 0.5, 0.361, 0.329, 0.329, 0.003, 0.2 + }, 0.001)); + +} + +TEST(ColorsCmsTransformSurface, TransformForProof) +{ + static const std::vector img = { + 1.0, 0.1, 0.3, 0.5, 0.0, 0.0, 0.0, 0.0, + 1.0, 0.0, 0.0, 0.5, 0.2, 0.2, 0.2, 0.2, + }; + std::vector out(img.size(), 0.0); + + { + auto tr = TransformSurface(rgb, rgb, RenderingIntent::ABSOLUTE_COLORIMETRIC, cmyk, RenderingIntent::ABSOLUTE_COLORIMETRIC); + + tr.do_transform(2, 2, img.data(), out.data()); + + EXPECT_TRUE(VectorIsNear(out, { + 0.815, 0.176, 0.319, 0.5, 0.136, 0.134, 0.13, 0, + 0.813, 0.172, 0.176, 0.5, 0.204, 0.199, 0.197, 0.2 + }, 0.001)); + } + { + auto tr = TransformSurface(rgb, rgb, RenderingIntent::RELATIVE_COLORIMETRIC, cmyk, RenderingIntent::RELATIVE_COLORIMETRIC); + + tr.do_transform(2, 2, img.data(), out.data()); + + EXPECT_TRUE(VectorIsNear(out, { + 0.934, 0.226, 0.351, 0.5, 0.168, 0.165, 0.164, 0, + 0.932, 0.203, 0.219, 0.5, 0.264, 0.258, 0.255, 0.2 + }, 0.001)); + } +} + +TEST(ColorsCmsTransformSurface, TransformWithGamutWarning) +{ + static const std::vector img = { + 65535, 0, 65535, 65535, 0, 0, 0, 65535, + 0, 65535, 65535, 32768, 65534, 65535, 65535, 13107 + }; + + auto tr = TransformSurface(rgb, rgb, RenderingIntent::PERCEPTUAL, cmyk, RenderingIntent::PERCEPTUAL); + tr.set_gamut_warn_color({1.0, 0.0, 0.0, 0.5}); + + std::vector out(img.size(), 0.0); + tr.do_transform(2, 2, img.data(), out.data()); + + std::vector ret = { + 65535, 0, 0, 65535, 65535, 0, 0, 65535, + 65535, 0, 0, 32768, 65535, 65535, 65535, 13107 + }; + EXPECT_EQ(out, ret); +} + + +} // namespace + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/testfiles/src/colors/spaces-cms-test.cpp b/testfiles/src/colors/spaces-cms-test.cpp index 87ecf5bf85..4c8ad9c60b 100644 --- a/testfiles/src/colors/spaces-cms-test.cpp +++ b/testfiles/src/colors/spaces-cms-test.cpp @@ -67,6 +67,14 @@ TEST(ColorsSpaceCms, getType) EXPECT_EQ(cmyk->getComponentType(), Space::Type::CMYK); } +TEST(ColorsSpacesRgb, isDirect) +{ + auto cmyk_profile = Inkscape::Colors::CMS::Profile::create_from_uri(cmyk_icc); + auto cmyk = std::make_shared(cmyk_profile); + + ASSERT_TRUE(cmyk->isDirect()); +} + TEST(ColorsSpacesCms, realColor) { auto cmyk_profile = Inkscape::Colors::CMS::Profile::create_from_uri(cmyk_icc); diff --git a/testfiles/src/colors/spaces-rgb-test.cpp b/testfiles/src/colors/spaces-rgb-test.cpp index 7a8769c8bf..cf3328cd34 100644 --- a/testfiles/src/colors/spaces-rgb-test.cpp +++ b/testfiles/src/colors/spaces-rgb-test.cpp @@ -88,6 +88,11 @@ TEST(ColorsSpacesRgb, components) ASSERT_EQ(c2[3].index, 3); } +TEST(ColorsSpacesRgb, isDirect) +{ + ASSERT_TRUE(Manager::get().find(RGB)->isDirect()); +} + /*TEST(ColorsSpacesRgb, colorVarFallback) { auto &cm = Manager::get(); diff --git a/testfiles/src/display/drawing-access-test.cpp b/testfiles/src/display/drawing-access-test.cpp new file mode 100644 index 0000000000..e56edceea1 --- /dev/null +++ b/testfiles/src/display/drawing-access-test.cpp @@ -0,0 +1,510 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "test-base.h" + +using namespace Inkscape; + +/***** TEST DATA *******/ +constexpr inline double dbl(bool a) +{ + return a ? 1.0 : 0.0; +} + +struct TestFilter +{ + template + void filter(AccessDst &dst, AccessSrc const &src) + { + std::array c; + for (int y = 0; y < dst.height(); y++) { + for (int x = 0; x < dst.width(); x++) { + if (x / 3 == y / 3) { + src.colorAt(x, y, c); + dst.colorTo(x, y, c); + } + } + } + } +}; + +/**** TESTS *****/ + +TEST(DrawingAccessTest, ColorIs) +{ + for (auto format : {CAIRO_FORMAT_ARGB32, CAIRO_FORMAT_RGBA128F}) { + auto cobj = cairo_image_surface_create(format, 21, 21); + auto s = Cairo::RefPtr(new Cairo::ImageSurface(cobj, true)); + + // Draw something here + { + auto c = cairo_create(cobj); + for (auto channel = 0; channel < (format == CAIRO_FORMAT_A8 ? 1 : 3); channel++) { + cairo_rectangle(c, 3 + channel * 6, 3 + channel * 6, 6, 6); + cairo_set_source_rgba(c, channel == 0, channel == 1, channel == 2, 1.0); + cairo_fill(c); + } + cairo_destroy(c); + } + s->flush(); + s->write_to_png("/tmp/output.png"); + + ASSERT_TRUE(ImageIs(s, " " + " 22 " + " 22 " + " 88 " + " 88 " + " PP" + " PP")) + << "Format: " << get_format_name(format) << "\n" + << "Method: INTEGER COORDS\n"; + + // Bilinear should be the same as Int (just slower) + ASSERT_TRUE(ImageIs(s, " " + " 22 " + " 22 " + " 88 " + " 88 " + " PP" + " PP")) + << "Format: " << get_format_name(format) << "\n" + << "Method: BILINEAR DECIMAL COORDS\n"; + } +} + +TEST(DrawingAccessTest, AlphaIs) +{ + for (auto format : {CAIRO_FORMAT_A8, CAIRO_FORMAT_ARGB32, CAIRO_FORMAT_RGBA128F}) { + auto cobj = cairo_image_surface_create(format, 21, 21); + auto s = Cairo::RefPtr(new Cairo::ImageSurface(cobj, true)); + + // Draw something here + { + auto c = cairo_create(cobj); + cairo_rectangle(c, 3, 3, 15, 15); + cairo_set_source_rgba(c, 0.0, 0.0, 0.0, 1.0); + cairo_fill(c); + cairo_destroy(c); + } + s->flush(); + // s->write_to_png("/tmp/output.png"); + + ASSERT_TRUE(ImageIs(s, + " " + " &&&&& " + " &&&&& " + " &&&&& " + " &&&&& " + " &&&&& " + " ", + PatchMethod::ALPHA)) + << "Format: " << get_format_name(format) << "\n" + << "Method: INTEGER COORDS\n"; + + ASSERT_TRUE(ImageIs(s, + " " + " &&&&& " + " &&&&& " + " &&&&& " + " &&&&& " + " &&&&& " + " ", + PatchMethod::ALPHA)) + << "Format: " << get_format_name(format) << "\n" + << "Method: BILINEAR FLOAT COORDS\n"; + + if (format == CAIRO_FORMAT_A8) { + ASSERT_TRUE(ImageIs(s, " " + " ..... " + " ..... " + " ..... " + " ..... " + " ..... " + " ")) + << "Format: " << get_format_name(format) << "\n" + << "Method: Color for Alpha\n"; + } + } +} + +TEST(DrawingAccessTest, BilinearInterpolation) +{ + auto src = TestSurface<3>(4, 4); + src.rect(1, 1, 2, 2, {1.0, 0.0, 1.0, 1.0}); + + EXPECT_NEAR(src._d->alphaAt(0.5, 0.5), 0.25, 0.001); + EXPECT_NEAR(src._d->alphaAt(2.5, 2.5), 0.25, 0.001); + EXPECT_NEAR(src._d->alphaAt(0.5, 2.5), 0.25, 0.001); + EXPECT_NEAR(src._d->alphaAt(2.5, 0.5), 0.25, 0.001); + EXPECT_NEAR(src._d->alphaAt(1.5, 0.5), 0.50, 0.001); + EXPECT_NEAR(src._d->alphaAt(0.5, 1.5), 0.50, 0.001); + EXPECT_NEAR(src._d->alphaAt(0.3, 1.3), 0.6999, 0.001); + + ColorIs(*src._d, 0.5, 0.5, {0.25, 0.0, 0.25, 0.25}, true); + ColorIs(*src._d, 0.5, 1.5, {0.50, 0.0, 0.50, 0.50}, true); + ColorIs(*src._d, 0.3, 1.3, {0.6999, 0.0, 0.6999, 0.6999}, true); +} + +TEST(DrawingAccessTest, UnmultiplyColor) +{ + auto src = TestSurface<3>(4, 4); + src.rect(1, 1, 2, 2, {1.0, 0.0, 1.0, 0.5}); + + ASSERT_TRUE(ColorIs(*src._d, 1, 1, {0.5, 0.0, 0.5, 0.5}, false)); + ASSERT_TRUE(ColorWillBe(*src._d, 2, 2, {0.5, 0.5, 0.5, 0.5}, false)); + + ASSERT_TRUE(ColorIs(*src._d, 1, 1, {1.0, 0.0, 1.0, 0.5}, true)); + ASSERT_TRUE(ColorWillBe(*src._d, 3, 3, {0.5, 0.5, 0.5, 0.5}, true)); + + auto src2 = TestSurface<4>(4, 4); + src2.rect(1, 1, 2, 2, {1.0, 0.0, 1.0, 0.4, 0.5}); + + ASSERT_TRUE(ColorIs(*src2._d, 1, 1, {0.5, 0.0, 0.5, 0.2, 0.5}, false)); + ASSERT_TRUE(ColorWillBe(*src2._d, 2, 2, {0.5, 0.5, 0.5, 0.5, 0.5}, false)); + + ASSERT_TRUE(ColorIs(*src2._d, 1, 1, {1.0, 0.0, 1.0, 0.4, 0.5}, true)); + ASSERT_TRUE(ColorWillBe(*src2._d, 3, 3, {0.5, 0.5, 0.5, 0.5, 0.5}, true)); +} + +DrawingAccess getEdgeModeSurface(int width, int height) +{ + auto src = TestSurface<4>(width, height); + + // Draw a box of 4 channels, one edge per channel, overlaps at the corners + for (int x = 0; x < (int)width; x++) { + for (int y = 0; y < (int)height; y++) { + src._d->colorTo(x, y, {dbl(y == 0), dbl(y == height - 1), dbl(x == 0), dbl(x == width - 1), 1.0}); + } + } + return *src._d; +} + +TEST(DrawingAccessTest, EdgeModeError) +{ + std::array c; + auto d = getEdgeModeSurface(4, 4); + d.setEdgeMode(DrawingAccessEdgeMode::ERROR); + for (int x = -1; x < 5; x++) { + for (int y = -1; y < 5; y++) { + if (x < 0 || x > 3 || y < 0 || y > 3) { + EXPECT_THROW(d.colorAt(x, y, c), std::exception); + EXPECT_THROW(d.colorTo(x, y, c), std::exception); + } else { + // Checking for no error, and confirming the test-suite + ASSERT_TRUE(ColorIs(d, x, y, {dbl(y == 0), dbl(y == 3), dbl(x == 0), dbl(x == 3), 1.0}, false)); + ASSERT_TRUE(ColorIs(d, x, y, {dbl(y == 0), dbl(y == 3), dbl(x == 0), dbl(x == 3), 1.0}, true)); + ASSERT_TRUE(ColorWillBe(d, x, y, {0.5, 0.5, 0.5, 0.5, 0.5})); + } + } + } +} + +TEST(DrawingAccessTest, EdgeModeExtend) +{ + auto d = getEdgeModeSurface(4, 4); + d.setEdgeMode(DrawingAccessEdgeMode::EXTEND); + + for (int x = 0; x < 5; x++) { + for (int y = 0; y < 5; y++) { + ASSERT_TRUE(ColorIs(d, x, y, {dbl(y <= 0), dbl(y >= 3), dbl(x <= 0), dbl(x >= 3), 1.0}, false)); + ASSERT_TRUE(ColorIs(d, x, y, {dbl(y <= 0), dbl(y >= 3), dbl(x <= 0), dbl(x >= 3), 1.0}, true)); + ASSERT_TRUE( + ColorWillBe(d, x, y, {0.5, 0.5, 0.5, 0.5, 0.5}, true, std::clamp(x, 0, 3), std::clamp(y, 0, 3))); + } + } +} + +TEST(DrawingAccessTest, EdgeModeWrap) +{ + auto d = getEdgeModeSurface(4, 4); + d.setEdgeMode(DrawingAccessEdgeMode::WRAP); + + for (int x = 0; x < 5; x++) { + for (int y = 0; y < 5; y++) { + ASSERT_TRUE(ColorIs( + d, x, y, + {dbl(y == 0 || y == 4), dbl(y == -1 || y == 3), dbl(x == 0 || x == 4), dbl(x == -1 || x == 3), 1.0}, + false)); + ASSERT_TRUE(ColorIs( + d, x, y, + {dbl(y == 0 || y == 4), dbl(y == -1 || y == 3), dbl(x == 0 || x == 4), dbl(x == -1 || x == 3), 1.0}, + true)); + ASSERT_TRUE(ColorWillBe(d, x, y, {0.5, 0.5, 0.5, 0.5, 0.5}, true, x % 4, y % 4)); + } + } +} + +TEST(DrawingAccessTest, EdgeModeNone) +{ + auto d = getEdgeModeSurface(4, 4); + d.setEdgeMode(DrawingAccessEdgeMode::ZERO); + + for (int x = 0; x < 5; x++) { + for (int y = 0; y < 5; y++) { + if (x < 0 || x > 3 || y < 0 || y > 3) { + ASSERT_TRUE(ColorIs(d, x, y, {0.0, 0.0, 0.0, 0.0, 0.0}, false)); + ASSERT_TRUE(ColorIs(d, x, y, {0.0, 0.0, 0.0, 0.0, 0.0}, true)); + ASSERT_FALSE(ColorWillBe(d, x, y, {0.5, 0.5, 0.5, 0.5, 0.5})); + } + } + } +} + +template +void TestColorTo() +{ + auto cobj = cairo_image_surface_create(F, 21, 21); + auto s = Cairo::RefPtr(new Cairo::ImageSurface(cobj, true)); + auto d = DrawingAccess(s); + + for (unsigned x = 0; x < 21; x++) { + for (unsigned y = 0; y < 21; y++) { + // Build a red X in the image surface + if (x == y || 20 - x == y) { + d.colorTo(x, y, {1.0, 0.0, 0.0, 1.0}, true); + // Build blue square outline + } else if (x == 0 || x == 20 || y == 0 || y == 20) { + d.colorTo(x, y, {0.0, 0.0, 0.5, 0.5}, false); + // Build a green cross (unpremultiplied) + } else if (x == 10 || y == 10) { + d.colorTo(x, y, {0.0, 0.7, 0.0, 0.7}, true); + } + } + } + + // Premultiplied test ignores semi-transparent: + // blue - because it's value of 1.0 is READ as 0.5 premultiplied + // green - because it's value of 0.7 is WRITTEN as 0.49 premultiplied + ASSERT_TRUE(ImageIs(s, + "1 . 1" + " 1 1 " + " 1 1 " + ". 1 ." + " 1 1 " + " 1 1 " + "1 . 1", + PatchMethod::COLORS, false)); + + // Unpremultiplied includes semi-transparent blue and green. + ASSERT_TRUE(ImageIs(s, + "A@@@@@A" + "@1 4 1@" + "@ 141 @" + "@44544@" + "@ 141 @" + "@1 4 1@" + "A@@@@@A", + PatchMethod::COLORS, true)); +} + +TEST(DrawingAccessTest, colorTo) +{ + { + auto cobj = cairo_image_surface_create(CAIRO_FORMAT_A8, 21, 21); + auto s = Cairo::RefPtr(new Cairo::ImageSurface(cobj, true)); + auto d = DrawingAccess(s); + + for (unsigned x = 0; x < 21; x++) { + for (unsigned y = 0; y < 21; y++) { + // Build a cross in the image surface + if (x == y || 20 - x == y) { + d.colorTo(x, y, {1.0}); + } + } + } + + s->write_to_png("/tmp/alpha.png"); + ASSERT_TRUE(ImageIs(s, + ". ." + " . . " + " . . " + " + " + " . . " + " . . " + ". .", + PatchMethod::ALPHA)); + } + + TestColorTo(); + TestColorTo(); +} + +TEST(DrawingAccessTest, multiSpanChannels) +{ + // We only test RGBA128F, since this is what is going to be used + auto src = TestSurface<4>(21, 21); + { + auto c1 = cairo_create(src._cobj[0]); + auto c2 = cairo_create(src._cobj[1]); + for (auto channel = 0; channel < 4; channel++) { + cairo_rectangle(c1, channel * 5, channel * 5, 6, 6); + cairo_set_source_rgba(c1, channel == 0, channel == 1, channel == 2, 1.0); + cairo_fill(c1); + + cairo_rectangle(c2, channel * 5, channel * 5, 6, 6); + cairo_set_source_rgba(c2, channel == 3, 0.0, 0.0, 1.0); + cairo_fill(c2); + } + cairo_destroy(c1); + cairo_destroy(c2); + } + + EXPECT_TRUE(ImageIs(*src._d, + "&& " + "&&.. " + " .&o " + " .o*o. " + " o&. " + " ..&&" + " &&", + PatchMethod::ALPHA)); + + ColorIs(*src._d, 0, 0, {1.0, 0.0, 0.0, 0.0, 1.0}, true); + ColorIs(*src._d, 20, 20, {0.0, 0.0, 0.0, 1.0, 1.0}, true); + + ASSERT_TRUE(ImageIs(*src._d, "22 " + "224 " + " 488 " + " 8DP " + " PP@ " + " @ff" + " ff")); + ASSERT_TRUE(ImageIs(*src._d, "22 " + "224 " + " 488 " + " 8DP " + " PP@ " + " @ff" + " ff")); + + for (auto x = 0; x < 21; x++) { + for (auto y = 0; y < 21; y++) { + src._d->colorTo(x, y, {1.0, 0.0, 0.0, 1.0, 1.0}); + } + } + + ASSERT_TRUE(ImageIs(*src._d, "hhhhhhh" + "hhhhhhh" + "hhhhhhh" + "hhhhhhh" + "hhhhhhh" + "hhhhhhh" + "hhhhhhh")); +} + +TEST(DrawingAccessTest, Filter) +{ + auto src1 = TestSurface<4>(21, 21); + src1.rect(3, 3, 15, 15, {1.0, 0.0, 1.0, 0.0, 0.5}); + + ASSERT_TRUE(ImageIs(*src1._d, " " + " RRRRR " + " RRRRR " + " RRRRR " + " RRRRR " + " RRRRR " + " ")); + + auto src2 = TestSurface<4>(21, 21); + src2.rect(12, 12, 9, 9, {1.0, 0.5, 1.0, 0.5, 1.0}); + + ASSERT_TRUE(ImageIs(*src2._d, " " + " " + " " + " " + " FFF" + " FFF" + " FFF")); + + TestFilter().filter(*src1._d, *src2._d); + + ASSERT_TRUE(ImageIs(*src1._d, " " + " RRRR " + " R RRR " + " RR RR " + " RRRFR " + " RRRRF " + " F")); +} + +TEST(DrawingAccessTest, nonCairoMemoryAccess) +{ + auto src = TestCustomSurface<4>(21, 21); + src.rect(6, 6, 9, 9, {1.0, 0.0, 1.0, 0.5, 1.0}); + + EXPECT_TRUE(ImageIs(*src._d, + " " + " " + " &&& " + " &&& " + " &&& " + " " + " ", + PatchMethod::ALPHA)); +} + +template +void TestcontigiousCopy(std::array in, std::array cmp, bool unpre = false) { + int w = 21; + int h = 21; + auto src = TestSurface<4, F>(w, h); + src.rect(6, 6, 9, 9, in); + + std::vector copy = src._d->template contigiousCopy(unpre); + ASSERT_EQ(copy.size(), w * h * 5); + + std::array zero; + std::array first; + std::array mid; + int x = 10, y = 10; + for (auto i = 0; i < 5; i++) { + first[i] = copy[(0 * w + 0) * 5 + i]; + mid[i] = copy[(y * w + x) * 5 + i]; + } + if constexpr (std::is_integral::value) { + EXPECT_EQ(first, zero); + EXPECT_EQ(mid, cmp); + } else { + EXPECT_TRUE(VectorIsNear(first, zero, 0.005)); + EXPECT_TRUE(VectorIsNear(mid, cmp, 0.005)); + } +} + +TEST(DrawingAccessTest, contigiousCopy) +{ + // Direct copy of original data + TestcontigiousCopy({0.2, 0.4, 0.6, 0.8, 0.5}, {25, 51, 76, 102, 128}, false); + + // Data upscaling shows small numbers just get rounded down to zero + TestcontigiousCopy({0.002, 0.004, 0.006, 0.008, 0.5}, {0, 0, 0, 257, 32896}, false); + TestcontigiousCopy({0.002, 0.004, 0.006, 0.008, 0.5}, {0, 0, 0, 16843009, 2155905152}, false); + TestcontigiousCopy({1.0, 0.0, 1.0, 0.5, 0.5}, {0.5, 0.0, 0.5, 0.25, 0.5}, false); + TestcontigiousCopy({1.0, 0.0, 1.0, 0.5, 0.5}, {0.5, 0.0, 0.5, 0.25, 0.5}, false); + + // Data isn't rescaled so we see actual values not rounded + TestcontigiousCopy({0.2, 0.4, 0.6, 0.8, 0.5}, {25, 51, 76, 102, 127}, false); + TestcontigiousCopy({0.002, 0.004, 0.006, 0.008, 0.5}, {65, 130, 196, 261, 32767}, false); + // TODO: DOESNT COMPILE TestcontigiousCopy(); + TestcontigiousCopy({1.0, 0.0, 1.0, 0.5, 0.5}, {0.5, 0.0, 0.5, 0.25, 0.5}, false); + TestcontigiousCopy({1.0, 0.0, 1.0, 0.5, 0.5}, {0.5, 0.0, 0.5, 0.25, 0.5}, false); + + // Unpremultiply alpha in returned values + TestcontigiousCopy({0.1, 0.2, 0.3, 0.4, 0.5}, {23, 49, 75, 101, 128}, true); + TestcontigiousCopy({0.001, 0.002, 0.003, 0.004, 0.5}, {0, 0, 0, 0, 32896}, true); + TestcontigiousCopy({0.001, 0.002, 0.003, 0.004, 0.5}, {0, 0, 0, 0, 2155905152}, true); + TestcontigiousCopy({1.0, 0.0, 1.0, 0.5, 0.5}, {1.0, 0.0, 1.0, 0.5, 0.5}, true); + TestcontigiousCopy({1.0, 0.0, 1.0, 0.5, 0.5}, {1.0, 0.0, 1.0, 0.5, 0.5}, true); + TestcontigiousCopy({0.1, 0.2, 0.3, 0.4, 0.5}, {25, 51, 76, 101, 127}, true); + TestcontigiousCopy({0.001, 0.002, 0.003, 0.004, 0.5}, {65, 131, 195, 261, 32767}, true); +} + + +/* + 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/testfiles/src/display/filter-color-matrix-test.cpp b/testfiles/src/display/filter-color-matrix-test.cpp new file mode 100644 index 0000000000..fb39d9850c --- /dev/null +++ b/testfiles/src/display/filter-color-matrix-test.cpp @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "display/filters/color-matrix.h" +#include "test-base.h" + +using namespace Inkscape::Filters; + +TEST(DrawingFilterTest, ColorMatrix) +{ + // Identity Matrix + EXPECT_TRUE(FilterColors<3>(ColorMatrix({ + // clang-format off + 1, 0, 0, 0, 0, + 0, 1, 0, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 0, 1, 0 + // clang-format on + }), + {1.0, 0.0, 1.0, 0.5}, {1.0, 0.0, 1.0, 0.5})); + // Default matrix is Identity + EXPECT_TRUE(FilterColors<3>(ColorMatrix({}), {1.0, 0.0, 1.0, 0.5}, {1.0, 0.0, 1.0, 0.5})); + // Morphius Matrix + EXPECT_TRUE(FilterColors<3>(ColorMatrix({ + // clang-format off + 0, 0, 0, 0, 0, + 1, 1, 1, 1, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 1, 0 + // clang-format on + }), + {0.0, 1.0, 0.0, 0.5}, {1.0, 0.0, 1.0, 0.5})); +} + +TEST(DrawingFilterTest, ColorMatrixSaturate) +{ + // Testing in sRGB color space (browsers use linearRGB by default) + EXPECT_TRUE(FilterColors<3>(ColorMatrixSaturate(0.2), {0.428, 0.228, 0.428, 0.5}, {1.0, 0.0, 1.0, 0.5})); + EXPECT_TRUE(FilterColors<3>(ColorMatrixSaturate(0.4), {0.571, 0.171, 0.571, 0.5}, {1.0, 0.0, 1.0, 0.5})); +} + +TEST(DrawingFilterTest, ColorMatrixHueRotate) +{ + EXPECT_TRUE(FilterColors<3>(ColorMatrixHueRotate(180.0), {0, 0.57, 0, 0.5}, {1.0, 0.0, 1.0, 0.5})); + EXPECT_TRUE(FilterColors<3>(ColorMatrixHueRotate(90.0), {1, 0.145, 0, 0.5}, {1.0, 0.0, 1.0, 0.5})); +} + +TEST(DrawingFilterTest, ColorMatrixLuminance) +{ + EXPECT_TRUE(FilterColors<3>(ColorMatrixLuminance(), {0, 0, 0, 0.785}, {1.0, 0.0, 1.0, 0.5})); +} + +/* + 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/testfiles/src/display/filter-color-space-test.cpp b/testfiles/src/display/filter-color-space-test.cpp new file mode 100644 index 0000000000..0d24af755d --- /dev/null +++ b/testfiles/src/display/filter-color-space-test.cpp @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "test-base.h" + +#include "colors/manager.h" +#include "colors/spaces/cms.h" + +#include "display/filters/color-space.h" + +using namespace Inkscape::Colors; +using namespace Inkscape::Filters; + +static std::string cmyk_icc = INKSCAPE_TESTS_DIR "/data/colors/default_cmyk.icc"; + +TEST(DrawingFilterTest, ColorSpace) +{ + auto rgb = Colors::Manager::get().find(Space::Type::RGB); + auto cmyk_profile = Colors::CMS::Profile::create_from_uri(cmyk_icc); + auto cmyk = std::make_shared(cmyk_profile, "cmyk"); + + auto dst = TestSurface<3>(4, 4); + auto src = TestSurface<4>(4, 4); + src.rect(1, 1, 2, 2, {1.0, 0.0, 1.0, 0.5, 0.5}); + + auto tr = ColorSpaceTransform, DrawingAccess>(cmyk, rgb); + + tr.filter(*dst._d, *src._d); + + ASSERT_TRUE(ColorIs(*dst._d, 1, 1, {-0.4505, 0.407, 0.207, 0.5}, true)); +} + +/* + 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/testfiles/src/display/filter-component-transfer-test.cpp b/testfiles/src/display/filter-component-transfer-test.cpp new file mode 100644 index 0000000000..7cfcc64d31 --- /dev/null +++ b/testfiles/src/display/filter-component-transfer-test.cpp @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "display/filters/component-transfer.h" +#include "test-base.h" + +using namespace Inkscape::Filters; + +TEST(DrawingFilterTest, ComponentTransfer) +{ + // Default transfer is Identity + EXPECT_TRUE(FilterColors<3>(ComponentTransfer({}), {1.0, 0.0, 1.0, 0.5}, {1.0, 0.0, 1.0, 0.5})); +} + +TEST(DrawingFilterTest, ComponentTransferTable) +{ + std::vector tfs = { + TransferFunction({0, 0, 1, 1}, false), + TransferFunction({1, 1, 0, 0}, false), + TransferFunction({0, 1, 1, 0}, false), + }; + // NOTE: Input colors are in sRGB, not in linearRGB + EXPECT_TRUE(FilterColors<3>(ComponentTransfer(tfs), {1.0, 1.0, 0.0, 1.0}, {1.0, 0.0, 0.0, 1.0})); + EXPECT_TRUE(FilterColors<3>(ComponentTransfer(tfs), {1.0, 0.4, 0.0, 1.0}, {1.0, 0.2, 0.0, 1.0})); + EXPECT_TRUE(FilterColors<3>(ComponentTransfer(tfs), {1.0, 0.0, 0.0, 1.0}, {1.0, 1.0, 0.0, 1.0})); + EXPECT_TRUE(FilterColors<3>(ComponentTransfer(tfs), {0.0, 0.0, 0.0, 1.0}, {0.0, 1.0, 0.0, 1.0})); + EXPECT_TRUE(FilterColors<3>(ComponentTransfer(tfs), {0.0, 0.0, 0.8, 1.0}, {0.0, 1.0, 0.7, 1.0})); +} + +TEST(DrawingFilterTest, ComponentTransferDiscrete) +{ + std::vector tfs = { + TransferFunction({0, 0, 1, 1}, true), + TransferFunction({1, 1, 0, 0}, true), + TransferFunction({0, 1, 1, 0}, true), + }; + EXPECT_TRUE(FilterColors<3>(ComponentTransfer(tfs), {1.0, 1.0, 0.0, 1.0}, {1.0, 0.0, 0.0, 1.0})); + EXPECT_TRUE(FilterColors<3>(ComponentTransfer(tfs), {1.0, 1.0, 0.0, 1.0}, {1.0, 0.2, 0.0, 1.0})); + EXPECT_TRUE(FilterColors<3>(ComponentTransfer(tfs), {1.0, 0.0, 0.0, 1.0}, {1.0, 1.0, 0.0, 1.0})); + EXPECT_TRUE(FilterColors<3>(ComponentTransfer(tfs), {0.0, 0.0, 0.0, 1.0}, {0.0, 1.0, 0.0, 1.0})); + EXPECT_TRUE(FilterColors<3>(ComponentTransfer(tfs), {0.0, 0.0, 1.0, 1.0}, {0.0, 1.0, 0.7, 1.0})); +} + +TEST(DrawingFilterTest, ComponentTransferLinear) +{ + std::vector tfs = { + TransferFunction(0.5, 0.0), + TransferFunction(0.5, 0.25), + TransferFunction(0.5, 0.5), + }; + EXPECT_TRUE(FilterColors<3>(ComponentTransfer(tfs), {0.5, 0.25, 0.5, 1.0}, {1.0, 0.0, 0.0, 1.0})); + EXPECT_TRUE(FilterColors<3>(ComponentTransfer(tfs), {0.5, 0.35, 0.5, 1.0}, {1.0, 0.2, 0.0, 1.0})); + EXPECT_TRUE(FilterColors<3>(ComponentTransfer(tfs), {0.5, 0.75, 0.5, 1.0}, {1.0, 1.0, 0.0, 1.0})); + EXPECT_TRUE(FilterColors<3>(ComponentTransfer(tfs), {0.0, 0.75, 0.5, 1.0}, {0.0, 1.0, 0.0, 1.0})); + EXPECT_TRUE(FilterColors<3>(ComponentTransfer(tfs), {0.0, 0.75, 0.85, 1.0}, {0.0, 1.0, 0.7, 1.0})); +} + +TEST(DrawingFilterTest, ComponentTransferGamma) +{ + std::vector tfs = { + TransferFunction(4.0, 7.0, 0.0), + TransferFunction(4.0, 4.0, 0.0), + TransferFunction(4.0, 1.0, 0.0), + }; + EXPECT_TRUE(FilterColors<3>(ComponentTransfer(tfs), {1.0, 0.0, 0.0, 1.0}, {1.0, 0.0, 0.0, 1.0})); + EXPECT_TRUE(FilterColors<3>(ComponentTransfer(tfs), {1.0, 0.25, 0.0, 1.0}, {1.0, 0.5, 0.0, 1.0})); + EXPECT_TRUE(FilterColors<3>(ComponentTransfer(tfs), {1.0, 1.0, 0.0, 1.0}, {1.0, 1.0, 0.0, 1.0})); + EXPECT_TRUE(FilterColors<3>(ComponentTransfer(tfs), {0.0, 1.0, 0.0, 1.0}, {0.0, 1.0, 0.0, 1.0})); + EXPECT_TRUE(FilterColors<3>(ComponentTransfer(tfs), {0.0, 1.0, 1.0, 1.0}, {0.0, 1.0, 0.5, 1.0})); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/testfiles/src/display/filter-composite-test.cpp b/testfiles/src/display/filter-composite-test.cpp new file mode 100644 index 0000000000..db15114861 --- /dev/null +++ b/testfiles/src/display/filter-composite-test.cpp @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "display/filters/composite.h" +#include "test-base.h" + +using namespace Inkscape::Filters; + +TEST(DrawingFilterTest, CompositeArithmetic) +{ + EXPECT_TRUE(FilterColors<3>(CompositeArithmetic(0.5, 0.5, 0.5, 0.5), {1.0, 1.0, 0.5, 1.0}, + {1.0, 0.0, 0.0, 1.0}, // input1 + {{0.0, 1.0, 0.0, 1.0}})); // input2 + EXPECT_TRUE(FilterColors<3>(CompositeArithmetic(0.2, 0.2, 0.2, 0.8), {0.8, 1.0, 1.0, 1.0}, {0.0, 0.0, 1.0, 1.0}, + {{0.0, 1.0, 0.0, 1.0}})); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/testfiles/src/display/filter-convolve-matrix-test.cpp b/testfiles/src/display/filter-convolve-matrix-test.cpp new file mode 100644 index 0000000000..6a1564c036 --- /dev/null +++ b/testfiles/src/display/filter-convolve-matrix-test.cpp @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "display/filters/convolve-matrix.h" +#include "test-base.h" + +using namespace Inkscape; + +/* + * Filer is done on the same image of a square defined in test-base.h + */ + +TEST(DrawingFilterConvolveMatrixTest, Laplacian3x3) +{ + EXPECT_TRUE(FilterIs(ConvolveMatrix(1, 1, 3, 3, 1, 0.0, {0.0, -2.0, 0.0, -2.0, 8.0, -2.0, 0.0, -2.0, 0.0}, true), + " " + " qqqqq " + " q...q " + " q...q " + " q...q " + " qqqqq " + " ")); +} + +TEST(DrawingFilterConvolveMatrixTest, Laplacian5x5) +{ + EXPECT_TRUE(FilterIs( + ConvolveMatrix(3, 3, 5, 5, 1, 0.0, + {0, 0, -1, 0, 0, 0, -1, -2, -1, 0, -1, -2, 16, -2, -1, 0, -1, -2, -1, 0, 0, 0, -1, 0, 0}, true), + " " + " qhhhh " + " h...q " + " h...q " + " h...q " + " hqqqq " + " ")); +} + +TEST(DrawingFilterConvolveMatrixTest, Prewitt) +{ + // Tests direction bias in matrix + EXPECT_TRUE(FilterIs(ConvolveMatrix(1, 1, 3, 3, 1, 0.0, {-1.0, 0.0, 1.0, -1.0, 0.0, 1.0, -1.0, 0.0, 1.0}, true), + " " + " ....q " + " ....q " + " ....q " + " ....q " + " ....q " + " ")); + EXPECT_TRUE(FilterIs(ConvolveMatrix(1, 1, 3, 3, 1, 0.0, {1.0, 0.0, -1.0, 1.0, 0.0, -1.0, 1.0, 0.0, -1.0}, true), + " " + " q.... " + " q.... " + " q.... " + " q.... " + " q.... " + " ")); + EXPECT_TRUE(FilterIs(ConvolveMatrix(1, 1, 3, 3, 1, 0.0, {-1.0, -1.0, -1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0}, true), + " " + " ..... " + " ..... " + " ..... " + " ..... " + " qqqqq " + " ")); + EXPECT_TRUE(FilterIs(ConvolveMatrix(1, 1, 3, 3, 1, 0.0, {1.0, 1.0, 1.0, 0.0, 0.0, 0.0, -1.0, -1.0, -1.0}, true), + " " + " qqqqq " + " ..... " + " ..... " + " ..... " + " ..... " + " ")); +} + +TEST(DrawingFilterConvolveMatrixTest, ElongatedKernel) +{ + EXPECT_TRUE(FilterIs(ConvolveMatrix(4, 1, 9, 3, 1, 0.0, {1.0, 1.0, 1.0, 1.0, 0.0, -1.0, -1.0, -1.0, -1.0, + 1.0, 1.0, 1.0, 1.0, 0.0, -1.0, -1.0, -1.0, -1.0, + 1.0, 1.0, 1.0, 1.0, 0.0, -1.0, -1.0, -1.0, -1.0}, + true), + " " + " hq... " + " hq... " + " hq... " + " hq... " + " hq... " + " ")); +} +TEST(DrawingFilterConvolveMatrixTest, PreserveAlpha) +{ + EXPECT_TRUE(FilterIs(ConvolveMatrix(1, 1, 3, 3, 1, 0.0, {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, false), + " " + " " + " " + " " + " " + " " + " ")); + EXPECT_TRUE(FilterIs(ConvolveMatrix(1, 1, 3, 3, 1, 0.0, {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, true), + " " + " ..... " + " ..... " + " ..... " + " ..... " + " ..... " + " ")); +} + +/* + 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/testfiles/src/display/filter-displacement-map-test.cpp b/testfiles/src/display/filter-displacement-map-test.cpp new file mode 100644 index 0000000000..1134a0f53b --- /dev/null +++ b/testfiles/src/display/filter-displacement-map-test.cpp @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "display/filters/displacement-map.h" +#include "test-base.h" + +using namespace Inkscape::Filters; + +TEST(DrawingFilterTest, DisplacementMap) +{ + auto texture = TestSurface<4>(21, 21); + texture.rect(3, 3, 15, 15, {0.5, 0.0, 0.0, 1.0, 1.0}); + texture._d->setEdgeMode(DrawingAccessEdgeMode::ZERO); + + // This map splits off the top, bottom, left and right rows and moves them out + auto map = TestSurface<3>(21, 21); + for (auto x = 0; x < 21; x++) { + for (auto y = 0; y < 21; y++) { + auto x1 = x / 3; + auto y1 = y / 3; + map._d->colorTo(x, y, + {x1 == 0 || x1 == 5 ? 1.0 : (x1 == 1 || x1 == 6 ? 0.0 : 0.5), + y1 == 0 || y1 == 5 ? 1.0 : (y1 == 1 || y1 == 6 ? 0.0 : 0.5), 0.0, 1.0}, + true); + } + } + + auto f = DisplacementMap(0, 1, 255 * 6, 255 * 6); + auto dst = TestSurface<4>(21, 21); + f.filter(*dst._d, *texture._d, *map._d); + + ASSERT_TRUE(ImageIs(*dst._d, + "h hhh h" + " " + "h hhh h" + "h hhh h" + "h hhh h" + " " + "h hhh h", + PatchMethod::COLORS, true)); +} + +/* + 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/testfiles/src/display/filter-gaussian-blur-test.cpp b/testfiles/src/display/filter-gaussian-blur-test.cpp new file mode 100644 index 0000000000..7dc03a03ad --- /dev/null +++ b/testfiles/src/display/filter-gaussian-blur-test.cpp @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "display/filters/gaussian-blur.h" +#include "test-base.h" + +using namespace Inkscape::Filters; + +TEST(DrawingFilterTest, GaussianBlurFIR) +{ + auto src = TestSurface<3, CAIRO_FORMAT_ARGB32>(21, 21); + src.rect(3, 3, 15, 15, {0.5, 0.75, 1.0, 1.0}); + + GaussianBlur({2, 2}, BlurQuality::NORMAL).filter(*src._d); + + EXPECT_TRUE(ImageIs(*src._d, + " ..... " + ".+OOO+." + ".O$$$O." + ".O$&$O." + ".O$$$O." + ".+OOO+." + " ..... ", + PatchMethod::ALPHA, true)); +} + +TEST(DrawingFilterTest, GaussianBlurIIR) +{ + auto src = TestSurface<3, CAIRO_FORMAT_ARGB32>(21, 21); + src.rect(3, 3, 15, 15, {0.5, 0.75, 1.0, 1.0}); + + GaussianBlur({4, 4}, BlurQuality::NORMAL).filter(*src._d); + + EXPECT_TRUE(ImageIs(*src._d, + " ..... " + ".:+=+:." + ".+O*O+." + ".=*x*=." + ".+O*O+." + ".:+=+:." + " ..... ", + PatchMethod::ALPHA, true)); +} + +/* + 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/testfiles/src/display/filter-light-test.cpp b/testfiles/src/display/filter-light-test.cpp new file mode 100644 index 0000000000..d82d365363 --- /dev/null +++ b/testfiles/src/display/filter-light-test.cpp @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "display/filters/light.h" +#include "test-base.h" + +using namespace Inkscape::Filters; + +TEST(DrawingFilterTest, DiffuseDistantLight) +{ + EXPECT_TRUE(FilterIs(DistantLight(240, 20, {1.0, 1.0, 1.0, 1.0, 1.0}, 1.0, 1.0), + ".:::::." + ".::::.." + "......." + "......." + "......." + "..... ." + "......." + + , + PatchMethod::LIGHT)); + // TODO: Add test for color commonent here +} + +TEST(DrawingFilterTest, SpecularDistantLight) +{ + EXPECT_TRUE(FilterIs(DistantLight(240, 20, {1.0, 1.0, 1.0, 1.0, 1.0}, 1.0, 1.0, 2.0), + "+====++" + "++===::" + "--+++::" + "--+++::" + "--+++::" + "-::::.:" + "-:::::-", + PatchMethod::LIGHT)); +} + +TEST(DrawingFilterTest, DiffusePointLight) +{ + EXPECT_TRUE(FilterIs(PointLight({9, 9, 3}, 0.0, 0.0, Geom::identity(), 1, {1.0, 1.0, 1.0, 1.0, 1.0}, 1.0, 1.0), + ". " + " .. " + " .:-. " + " .-=. " + " ... " + " " + " ", + PatchMethod::LIGHT)); +} + +TEST(DrawingFilterTest, SpecularPointLight) +{ + EXPECT_TRUE(FilterIs(PointLight({9, 9, 3}, 0.0, 0.0, Geom::identity(), 1, {1.0, 1.0, 1.0, 1.0, 1.0}, 1.0, 1.0, 2.0), + "-::::::" + ":.:::.." + "::=o+.." + "::oO+.." + "::+++.." + ":......" + ":.....:", + PatchMethod::LIGHT, true)); +} + +TEST(DrawingFilterTest, DiffuseSpotLight) +{ + EXPECT_TRUE(FilterIs( + SpotLight({0, 0, 9}, {15, 15, 0}, 45, 1.0, 0.0, 0.0, Geom::identity(), 1, {1.0, 1.0, 1.0, 1.0, 1.0}, 1.0, 1.0), + " ::..." + " +-::.." + ":--::.." + "::::..." + ".::...." + "..... ." + ".......", + PatchMethod::LIGHT)); +} + +TEST(DrawingFilterTest, SpecularSpotLight) +{ + EXPECT_TRUE(FilterIs(SpotLight({0, 0, 9}, {15, 15, 0}, 45, 1.0, 0.0, 0.0, Geom::identity(), 1, + {1.0, 1.0, 1.0, 1.0, 1.0}, 1.0, 1.0, 0.5), + " .-++++" + ".=oo=++" + "-oOOO++" + "+oOOO==" + "+=OOO==" + "+++==+=" + "+++===o", + PatchMethod::LIGHT)); +} + +/* + 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/testfiles/src/display/filter-morphology-test.cpp b/testfiles/src/display/filter-morphology-test.cpp new file mode 100644 index 0000000000..b04a581e40 --- /dev/null +++ b/testfiles/src/display/filter-morphology-test.cpp @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "display/filters/morphology.h" +#include "test-base.h" + +using namespace Inkscape::Filters; + +TEST(DrawingFilterTest, MorphologyErode) +{ + auto src = TestSurface<4>(21, 21); + auto mid = TestSurface<4>(21, 21); + auto dst = TestSurface<4>(21, 21); + + src.rect(3, 3, 15, 15, {0.5, 0.0, 0.0, 1.0, 1.0}); + + src._d->setEdgeMode(DrawingAccessEdgeMode::ZERO); + mid._d->setEdgeMode(DrawingAccessEdgeMode::ZERO); + + Morphology(true, {3, 3}).filter(*dst._d, *mid._d, *src._d); + + EXPECT_TRUE(ImageIs(*dst._d, + " " + " " + " hhh " + " hhh " + " hhh " + " " + " ", + PatchMethod::COLORS, true)); +} + +TEST(DrawingFilterTest, MorphologyDilate) +{ + auto src = TestSurface<4>(21, 21); + auto mid = TestSurface<4>(21, 21); + auto dst = TestSurface<4>(21, 21); + + src.rect(3, 3, 15, 15, {0.5, 0.0, 0.0, 1.0, 1.0}); + + src._d->setEdgeMode(DrawingAccessEdgeMode::ZERO); + mid._d->setEdgeMode(DrawingAccessEdgeMode::ZERO); + dst._d->setEdgeMode(DrawingAccessEdgeMode::ZERO); + + Morphology(false, {3, 3}).filter(*dst._d, *mid._d, *src._d); + + EXPECT_TRUE(ImageIs(*dst._d, + "hhhhhhh" + "hhhhhhh" + "hhhhhhh" + "hhhhhhh" + "hhhhhhh" + "hhhhhhh" + "hhhhhhh", + PatchMethod::COLORS, true)); + + // Erode after shouldn't leave marks + Morphology(true, {6, 6}).filter(*src._d, *mid._d, *dst._d); + + EXPECT_TRUE(ImageIs(*src._d, + " " + " " + " hhh " + " hhh " + " hhh " + " " + " ", + PatchMethod::COLORS, true)); +} + +/* + 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/testfiles/src/display/filter-turbulence-test.cpp b/testfiles/src/display/filter-turbulence-test.cpp new file mode 100644 index 0000000000..9979c22311 --- /dev/null +++ b/testfiles/src/display/filter-turbulence-test.cpp @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "display/filters/turbulence.h" +#include "test-base.h" + +using namespace Inkscape::Filters; + +TEST(DrawingFilterTest, Turbulence) +{ + auto surface = TestSurface<4>(21, 21); + auto spiky = Turbulence(0, // random generator seed + {0, 0, 20, 20}, // tile size + {0.6, 0.6}, // base frequency + true, // stitch + false, // fractal noise + 8, // octaves + 5 // number of channels + ); + + spiky.filter(*surface._d, Geom::identity(), 0, 0); + EXPECT_TRUE(ImageIs(*surface._d, + "......." + ".:..:.." + ":.-:.::" + ".::...." + ":.....:" + "....:.." + "..:.:..", + PatchMethod::ALPHA)); +} + +/* + 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/testfiles/src/display/test-base.h b/testfiles/src/display/test-base.h new file mode 100644 index 0000000000..1ee3a9b2e6 --- /dev/null +++ b/testfiles/src/display/test-base.h @@ -0,0 +1,293 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/***** TEST UTITLTIES *******/ + +#include +#include + +#include "../test-utils.h" +#include "display/drawing-access.h" + +using namespace Inkscape; + +enum class PatchMethod +{ + ALPHA, + COLORS, + LIGHT +}; + +template +struct TestSurface +{ + TestSurface(int w, int h) + { + if constexpr (CHANNELS == 3) { + _cobj = {cairo_image_surface_create(format, w, h)}; + _s = {Cairo::RefPtr(new Cairo::ImageSurface(_cobj[0], true))}; + _d = std::make_shared>(_s[0]); + } else if constexpr (CHANNELS == 4) { + _cobj = {cairo_image_surface_create(format, w, h), cairo_image_surface_create(format, w, h)}; + _s = {Cairo::RefPtr(new Cairo::ImageSurface(_cobj[0], true)), + Cairo::RefPtr(new Cairo::ImageSurface(_cobj[1], true))}; + _d = std::make_shared>(_s[0], _s[1]); + } + } + + void rect(int x, int y, int w, int h, std::array const &c) + { + for (unsigned i = 0; i < _cobj.size(); i++) { + unsigned off = i * 3; + auto cro = cairo_create(_cobj[i]); + cairo_rectangle(cro, x, y, w, h); + cairo_set_source_rgba(cro, c[off + 0], c.size() > off + 1 && off + 1 < CHANNELS ? c[off + 1] : 0.0, + c.size() > off + 2 && off + 2 < CHANNELS ? c[off + 2] : 0.0, c.back()); + cairo_fill(cro); + cairo_destroy(cro); + } + } + + std::vector _cobj; + std::vector> _s; + std::shared_ptr> _d; +}; + +template +struct TestCustomSurface +{ + TestCustomSurface(int w, int h) + { + _custom_memory = std::vector((CHANNELS + 1) * w * h); + _d = std::make_shared>(_custom_memory.data(), w, h); + } + + void rect(int const x, int const y, int const w, int const h, std::array const &c) + { + for (auto x0 = x; x0 < x + w; x0++) { + for (auto y0 = y; y0 < y + h; y0++) { + _d->colorTo(x0, y0, c); + } + } + } + + std::vector _custom_memory; // Like CAIRO_FORMAT_RGBA128F but with custom primaries + std::shared_ptr> _d; +}; + + +/** + * Test single pixel getter, double and int modes + */ +template +::testing::AssertionResult ColorIs(DrawingAccess d, T0 x, T0 y, std::array const &c, + bool unnmultiply = false) +{ + std::array ct; + d.colorAt(x, y, ct, unnmultiply); + return VectorIsNear(c, ct, 0.001) << "\n X:" << x << "\n Y:" << y << "\n\n"; +} + +/** + * Test single pixel setter + * + * d - Surface to test + * x,y - Coordinates to SET the color to + * c - Color values to set + * x2,y2 - Optional coordinates to GET where the new color will be tested (for edge testing) + */ +template +::testing::AssertionResult ColorWillBe(DrawingAccess d, int x, int y, std::array const &c, + bool unnmultiply = false, std::optional x2 = {}, std::optional y2 = {}) +{ + std::array before; + std::array after; + if (!x2) + x2 = x; + if (!y2) + y2 = y; + + d.colorAt(*x2, *y2, before, unnmultiply); + if (VectorIsNear(c, before, 0.001) == ::testing::AssertionSuccess()) { + return ::testing::AssertionFailure() << "\n" + << print_values(c) << "\n ALREADY SET at " << *x2 << "," << *y2 << "\n"; + } + d.colorTo(x, y, c, unnmultiply); + d.colorAt(*x2, *y2, after, unnmultiply); + d.colorTo(*x2, *y2, before, unnmultiply); // Set value back + return VectorIsNear(c, after, 0.001) << "\n Write:" << x << "," << y << "\n Read:" << *x2 << "," << *y2 + << "\n"; +} + +/** + * Format a string into a character image for test output when failing. + */ +std::string format_patch(std::string const &in, unsigned stride) +{ + std::stringstream out; + for (unsigned c = 0; c < in.size(); c++) { + if (stride == 0 || c % stride == 0) { + if (c) + out << "\""; + out << "\n \""; + } + out << in[c]; + } + out << "\"\n"; + return out.str(); +} + +template +std::string build_patch(DrawingAccess const &d, PatchMethod method, unsigned patch_x = 3, unsigned patch_y = 3, + bool unmult = false) +{ + static std::vector const weights = {' ', ' ', ' ', '.', '.', '.', ':', ':', '-', + '+', '=', 'o', 'O', '*', 'x', 'X', '$', '&'}; + + double size = patch_x * patch_y; + char r0 = (method == PatchMethod::ALPHA) ? 0x40 : 0x30; + std::array color; + + // We collect a 3x3 grid of pixels into a patch so it can be shown as test output + std::stringstream output; + for (int y = 0; y < d.height() / patch_y; y++) { + for (int x = 0; x < d.width() / patch_x; x++) { + // inital values + std::vector colors(C + 1, 0.0); + std::vector lights(C + 1, 0.0); + + for (unsigned cy = 0; cy < patch_x; cy++) { + for (unsigned cx = 0; cx < patch_x; cx++) { + T0 tx = x * patch_x + cx; + T0 ty = y * patch_y + cy; + if (method == PatchMethod::ALPHA) { + lights.back() += d.alphaAt(tx, ty); + } else { + d.colorAt(tx, ty, color, unmult); + for (int c = 0; c < colors.size(); c++) { + colors[c] += color[c] > 0.5; + lights[c] += color[c]; + } + } + } + } + unsigned char r = r0; + double light = 0.0; + for (unsigned c = 0; c < colors.size() - 1; c++) { + r += (unsigned char)(((colors[c] / size > 0.3) + (colors[c] / size > 0.6)) << (c * 2)); + light += lights[c] / lights.size() / size; + } + switch (method) { + case PatchMethod::ALPHA: + r = weights[(int)(lights.back() / size * (weights.size() - 1))]; + break; + case PatchMethod::LIGHT: + r = weights[(int)(std::clamp(light, 0.0, 1.0) * (weights.size() - 1))]; + break; + } + // Map zero to space for readability + if (r == r0) { + r = lights.back() / size > 0.3 ? '.' : ' '; + } + // Cap anything higher than ascii + while (r > 'z') { + r -= ('z' - r0); + } + output << r; + } + } + return output.str(); +} + +template +::testing::AssertionResult ImageIs(DrawingAccess d, std::string const &test, + PatchMethod method = PatchMethod::COLORS, bool unmult = false) +{ + unsigned patch_x = 3; + std::string patch = build_patch(d, method, patch_x, patch_x, unmult); + + if (test != patch) { + return ::testing::AssertionFailure() << format_patch(test, d.width() / patch_x) << "!=\n" + << format_patch(patch, d.height() / patch_x); + } + return ::testing::AssertionSuccess(); +} + +/** + * Test the drawing surface against a compressed example + */ +template +::testing::AssertionResult ImageIs(Cairo::RefPtr s, std::string const &test, + PatchMethod method = PatchMethod::COLORS, bool unmult = false) +{ + // SurfaceAccess is templated requiring compile time information about type formatting. + switch (cairo_image_surface_get_format(s->cobj())) { + case CAIRO_FORMAT_A8: + return ImageIs(DrawingAccess(s), test, method, unmult); + case CAIRO_FORMAT_ARGB32: + return ImageIs(DrawingAccess(s), test, method, unmult); + case CAIRO_FORMAT_RGBA128F: + return ImageIs(DrawingAccess(s), test, method, unmult); + default: + return ::testing::AssertionFailure() << "UNHANDLED_FORMAT"; + } +} + +/** + * Get the cairo format as a printable name + */ +std::string get_format_name(cairo_format_t format) +{ + static std::map map = { + {CAIRO_FORMAT_A8, "A8"}, + {CAIRO_FORMAT_ARGB32, "ARGB32"}, + {CAIRO_FORMAT_RGBA128F, "RGBA128F"}, + }; + return map[format]; +} + +template +::testing::AssertionResult FilterIs(Filter &&f, std::string const &test, PatchMethod method = PatchMethod::COLORS, + bool debug = false) +{ + auto src = TestSurface<4>(21, 21); + src.rect(3, 3, 15, 15, {0.5, 0.0, 0.0, 1.0, 1.0}); + src._d->setEdgeMode(DrawingAccessEdgeMode::ZERO); + + auto dst = TestSurface<4>(21, 21); + f.filter(*dst._d, *src._d); + if (debug) { + src._s[0]->write_to_png("/tmp/filter_debug_before_0.png"); + src._s[1]->write_to_png("/tmp/filter_debug_before_1.png"); + dst._s[0]->write_to_png("/tmp/filter_debug_after_0.png"); + dst._s[1]->write_to_png("/tmp/filter_debug_after_1.png"); + } + return ImageIs(*dst._d, test, method, true); +} + +template +::testing::AssertionResult FilterColors(Filter &&f, std::array const &test, + std::array const &i1, + std::optional> const &i2 = {}) +{ + auto src1 = TestSurface(6, 6); + auto src2 = TestSurface(6, 6); + if (i2) { + src1.rect(0, 0, 6, 6, i1); + src2.rect(0, 0, 6, 6, *i2); + } else { + src2.rect(0, 0, 6, 6, i1); + } + f.filter(*src1._d, *src2._d); + std::array p; + src1._d->colorAt((unsigned)1, 1, p, true); + return VectorIsNear(p, test, 0.001); +} +/* + 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/testfiles/src/test-utils.h b/testfiles/src/test-utils.h index 1b42b879b4..bcfaf8462e 100644 --- a/testfiles/src/test-utils.h +++ b/testfiles/src/test-utils.h @@ -33,20 +33,39 @@ struct traced_data /** * Print a vector of doubles for debugging + * + * @arg v - The values to be printed + * @arg other - When provided, pads the values to the same width between other and v + * @arg failed - When provided, colors the failed entries for console output. */ -std::string print_values(const std::vector &v) +template +std::string print_values(T const &v, T const *other = nullptr, std::vector failed = {}) { + auto as_string = [](double v, int precision) { + std::ostringstream ch; + ch << std::setprecision(precision) << v; + return ch.str(); + }; + std::ostringstream oo; oo << "{"; - bool first = true; - for (double const &item : v) { - if (!first) { - oo << ", "; - } - first = false; - oo << std::setprecision(3) << item; - } - oo << "}"; + for (auto i = 0; i < v.size(); i++) { + int precision = (other ? std::min((*other)[i], v[i]) : v[i]) < 0 ? 4 : 3; + int other_size = other ? as_string((*other)[i], precision).length() : 0; + auto item = as_string(v[i], precision); + + if (failed.size() && failed[i]) { + // Turn a failed text RED in a console + oo << "\x1B[91m"; + } + oo << std::setw(std::max((int)item.length(), other_size)) << item; + if (failed.size() && failed[i]) { + oo << "\033[0m"; + } + + if (i < v.size() - 1) oo << ", "; + } + oo << "}(" << v.size() << ")"; return oo.str(); } @@ -61,14 +80,21 @@ std::string print_values(const std::vector &v) /** * Test each value in a values list is within a certain distance from each other. */ -::testing::AssertionResult VectorIsNear(std::vector const &A, std::vector const &B, double epsilon) +template +::testing::AssertionResult VectorIsNear(T const &A, T const &B, double epsilon) { - bool is_same = A.size() == B.size(); - for (size_t i = 0; is_same && i < A.size(); i++) { - is_same = is_same and (std::fabs((A[i]) - (B[i])) < epsilon); + bool same_size = A.size() == B.size(); + bool is_same = same_size; + std::vector failed(A.size(), false); + for (size_t i = 0; i < A.size() && i < B.size(); i++) { + failed[i] = (std::fabs(A[i] - B[i]) >= epsilon); + is_same = is_same and !failed[i]; } if (!is_same) { - return ::testing::AssertionFailure() << "\n" << print_values(A) << "\n != \n" << print_values(B); + return ::testing::AssertionFailure() << "\n" + << print_values(A, same_size ? &B : nullptr, failed) + << "\n != \n" + << print_values(B, same_size ? &A : nullptr, failed); } return ::testing::AssertionSuccess(); } -- GitLab From 8029371deecc0cf3d5fe421caf57bc962459f543 Mon Sep 17 00:00:00 2001 From: PBS Date: Sun, 9 Nov 2025 11:44:52 +0100 Subject: [PATCH 2/7] Some review fixes * Use requires instead of run-time checks. * Use more modern _v/_t instead of ::value/::type. * Fix const T* memory() calling itself in infinite loop. * Make some functions static (_primary_pos, _bilinear_interpolate). * Make _transform std::optional. * Convert G_BYTE_ORDER == G_LITTLE_ENDIAN to if constexpr, revealing missing glib.h header. * Use safemod for wrapping. --- src/colors/cms/transform-surface.h | 72 +++++++++---------- src/colors/cms/transform.h | 5 +- src/colors/spaces/base.h | 1 - src/display/drawing-access.h | 65 ++++++++--------- src/display/filters/color-matrix.h | 11 +-- src/display/filters/color-space.h | 48 +++++++------ testfiles/src/display/drawing-access-test.cpp | 43 ++++++----- 7 files changed, 118 insertions(+), 127 deletions(-) diff --git a/src/colors/cms/transform-surface.h b/src/colors/cms/transform-surface.h index 68c6cf5a0e..1a905b2e55 100644 --- a/src/colors/cms/transform-surface.h +++ b/src/colors/cms/transform-surface.h @@ -40,34 +40,30 @@ public: RenderingIntent intent = RenderingIntent::PERCEPTUAL, std::shared_ptr const &proof = nullptr, RenderingIntent proof_intent = RenderingIntent::AUTO) - : Transform(proof ? - cmsCreateProofingTransformTHR( - cmsCreateContext(nullptr, nullptr), - from->getHandle(), - lcms_color_format(from, sizeof(TIN), !std::is_integral::value, alpha_mode(PREMULT, true)), - to->getHandle(), - lcms_color_format(to, sizeof(TOUT), !std::is_integral::value, alpha_mode(false, true)), - proof->getHandle(), - lcms_intent(intent), - lcms_intent(proof_intent), - cmsFLAGS_COPY_ALPHA | cmsFLAGS_SOFTPROOFING | (GAMUTWARN ? cmsFLAGS_GAMUTCHECK : 0) | lcms_bpc(proof_intent)) - : cmsCreateTransformTHR( - cmsCreateContext(nullptr, nullptr), - from->getHandle(), - lcms_color_format(from, sizeof(TIN), !std::is_integral::value, alpha_mode(PREMULT, true)), - to->getHandle(), - lcms_color_format(to, sizeof(TOUT), !std::is_integral::value, alpha_mode(false, true)), - lcms_intent(intent), - cmsFLAGS_COPY_ALPHA) - , false) - , _pixel_size_in((_channels_in + 1) * sizeof(TIN)) - , _pixel_size_out((_channels_out + 1) * sizeof(TOUT)) - { - if constexpr (GAMUTWARN && (!std::is_same::value || !std::is_same::value)) { - throw std::logic_error("Gamut warnings only work with 16bit integer proof checking CMS transformations."); - } - - } + requires (!GAMUTWARN || (std::is_same_v && std::is_same_v)) + : Transform(proof ? + cmsCreateProofingTransformTHR( + cmsCreateContext(nullptr, nullptr), + from->getHandle(), + lcms_color_format(from, sizeof(TIN), !std::is_integral_v, alpha_mode(PREMULT, true)), + to->getHandle(), + lcms_color_format(to, sizeof(TOUT), !std::is_integral_v, alpha_mode(false, true)), + proof->getHandle(), + lcms_intent(intent), + lcms_intent(proof_intent), + cmsFLAGS_COPY_ALPHA | cmsFLAGS_SOFTPROOFING | (GAMUTWARN ? cmsFLAGS_GAMUTCHECK : 0) | lcms_bpc(proof_intent)) + : cmsCreateTransformTHR( + cmsCreateContext(nullptr, nullptr), + from->getHandle(), + lcms_color_format(from, sizeof(TIN), !std::is_integral_v, alpha_mode(PREMULT, true)), + to->getHandle(), + lcms_color_format(to, sizeof(TOUT), !std::is_integral_v, alpha_mode(false, true)), + lcms_intent(intent), + cmsFLAGS_COPY_ALPHA) + , false) + , _pixel_size_in((_channels_in + 1) * sizeof(TIN)) + , _pixel_size_out((_channels_out + 1) * sizeof(TOUT)) + {} /** * Apply the CMS transform to the surface and paint it into the output surface. @@ -76,20 +72,20 @@ public: * @arg height - The height of the image to transform * @arg px_in - The source surface with the pixels to transform. * @arg px_out - The destination surface which may be the same as in. - * @arg stride_in - The optional stride for the input image, if known to be uncontigious. - * @arg stride_out - The optional stride for the output image, if known to be uncontigious. + * @arg stride_in - The optional stride for the input image, if known to be uncontiguous. + * @arg stride_out - The optional stride for the output image, if known to be uncontiguous. */ void do_transform(int width, int height, TIN const *px_in, TOUT *px_out, int stride_in = 0, int stride_out = 0) const { cmsDoTransformLineStride( - _handle, - px_in, - px_out, - width, - height, - stride_in ? stride_in : width * _pixel_size_in, - stride_out ? stride_out : width * _pixel_size_out, - 0, 0 + _handle, + px_in, + px_out, + width, + height, + stride_in ? stride_in : width * _pixel_size_in, + stride_out ? stride_out : width * _pixel_size_out, + 0, 0 ); } diff --git a/src/colors/cms/transform.h b/src/colors/cms/transform.h index 33657745ac..e1743ed323 100644 --- a/src/colors/cms/transform.h +++ b/src/colors/cms/transform.h @@ -26,7 +26,7 @@ class Profile; class Transform { public: - Transform(cmsHTRANSFORM handle, bool global = false) + explicit Transform(cmsHTRANSFORM handle, bool global = false) : _handle(handle) , _context(!global ? cmsGetTransformContextID(handle) : nullptr) , _format_in(cmsGetTransformInputFormat(handle)) @@ -39,8 +39,9 @@ public: ~Transform() { cmsDeleteTransform(_handle); - if (_context) + if (_context) { cmsDeleteContext(_context); + } } Transform(Transform const &) = delete; Transform &operator=(Transform const &) = delete; diff --git a/src/colors/spaces/base.h b/src/colors/spaces/base.h index ac658c9e96..b559f7e1e2 100644 --- a/src/colors/spaces/base.h +++ b/src/colors/spaces/base.h @@ -13,7 +13,6 @@ #include #include -#include #include #include diff --git a/src/display/drawing-access.h b/src/display/drawing-access.h index e8bfd25c8c..72f14198db 100644 --- a/src/display/drawing-access.h +++ b/src/display/drawing-access.h @@ -15,11 +15,10 @@ #include #include -#include // DEBUG -#include -#include #include +#include #include +#include "helper/mathfns.h" /** * Terms: @@ -28,7 +27,7 @@ * Channel - Is one of those color space double values where alpha is always the last * item. For example in CMYKA, C is channel 0, M is 1 and A is 4 * Surface - Is a collection of Cairo pixels in a 2d grid with a specific stride. - * Pixel - A collectiion of one OR four Primaries packed into this + * Pixel - A collection of one OR four Primaries packed into this * surface grid. These may be floats or integers of various scales. * Primary - One of the values packed into a pixel. These get turned into channels * through unpacking of specific memory locations. @@ -69,7 +68,7 @@ public: constexpr static int P = P0 ? P0 : (F == CAIRO_FORMAT_A8 ? 0 : 3); // The internal type used by each channel in the format - using T = typename std::conditional::type; + using T = std::conditional_t; // Scale of each primary to convert to a double constexpr static double _scale = I ? 255.0 : 1.0; @@ -78,7 +77,7 @@ public: constexpr static int _alpha_primary = I ? 0 : P; // Does this DrawingAccess need two surfaces? - constexpr static bool NEXT = C - P > 0; + constexpr static bool NEXT = C > P; // Actual number of channels when including alpha constexpr static int SIZE = C + 1; @@ -91,8 +90,9 @@ public: * @arg next_surface - Optionally add another surface to handle color interpolation * in spaces like CMYKA with more than 3 primaries. */ - DrawingAccess(Cairo::RefPtr cairo_surface, - Cairo::RefPtr next_surface = {}) + explicit DrawingAccess(Cairo::RefPtr cairo_surface, + Cairo::RefPtr next_surface = {}) + requires (C <= P * (NEXT + 1)) : _width(cairo_surface->get_width()) , _height(cairo_surface->get_height()) , _stride(cairo_surface->get_stride() / sizeof(T)) @@ -104,9 +104,6 @@ public: if (cairo_image_surface_get_format(cairo_surface->cobj()) != F) { // throw std::exception("Format of the cairo surface doesn't match the DrawingAccess type."); } - if constexpr (C > P * (NEXT + 1)) { - throw std::logic_error("DataAccess format does not permit this number of primaries."); - } if constexpr (NEXT) { _next_memory = reinterpret_cast(_next_surface->get_data()); @@ -189,7 +186,7 @@ public: } /** - * Set the given pixel to the color values, apply premultiplicatiion of alpha is neccessary to + * Set the given pixel to the color values, apply premultiplication of alpha if neccessary to * keep the surface in a premultiplied state for further drawing operations. * * @arg x - The x coordinate to set @@ -250,19 +247,14 @@ public: /** * Get access to the memory directly */ - T* memory() { - if constexpr (NEXT) { - throw std::logic_error("DataAccess will not allow direct access for surfaces spanning multiple memory pools"); - } - return _memory; - } - const T* memory() const { return memory(); } + T *memory() requires (!NEXT) { return _memory; } + T const *memory() const requires (!NEXT) { return _memory; } /** - * Copy the surface into a single contigious memory surface and return. + * Copy the surface into a single contiguous memory surface and return. */ template - std::vector contigiousCopy(bool unpremultiply_alpha = false) + std::vector contiguousCopy(bool unpremultiply_alpha = false) { std::vector memory; memory.reserve(SIZE * _width * _height); @@ -278,8 +270,6 @@ public: return memory; } - - private: /* * Sets the Primaries from this Color. @@ -336,8 +326,8 @@ private: inline T0 _get_channel(int pos, int channel, double alpha) const { // Allow this function to output integer types of various sizes as well as floating point types - constexpr static double scale = std::is_integral::value - ? (std::is_integral::value + constexpr static double scale = std::is_integral_v + ? (std::is_integral_v ? std::numeric_limits::max() / std::numeric_limits::max() // T=char T0=int|char : std::numeric_limits::max()) // T=float T0=int|char : (T0)(1.0 / _scale); // T=float|char T0=float|double @@ -373,8 +363,8 @@ private: y = std::clamp(y, (int)0, _height - 1); break; case DrawingAccessEdgeMode::WRAP: - x %= _width; - y %= _height; + x = Util::safemod(x, _width); + y = Util::safemod(y, _height); break; case DrawingAccessEdgeMode::ZERO: // This means OOB to _get_channel, which will return zero @@ -389,24 +379,25 @@ private: } /** - * Convert the primary position into a memory location based on the Endianess - * of the uint32 cairo stores things in. This might need adjustinng for platforms. + * Convert the primary position into a memory location based on the endianness + * of the uint32 Cairo stores things in. This might need adjusting for platforms. */ - inline int _primary_pos(int p) const + static inline int _primary_pos(int p) { -#if G_BYTE_ORDER == G_LITTLE_ENDIAN - return I ? P - p : p; -#else - return p; -#endif + if constexpr (G_BYTE_ORDER == G_LITTLE_ENDIAN) { + return I ? P - p : p; + } else { + return p; + } } + /** * Standard bilinear interpolation */ - inline double _bilinear_interpolate(double a, double b, double c, double d, double wx, double wy) const + static inline double _bilinear_interpolate(double a, double b, double c, double d, double wx, double wy) { // This should only be useful for linearRGB color space, gamut curved colors such as sRGB would - // give bad results. This equasion is for premultiplied values. + // give bad results. This equation is for premultiplied values. return (a * wx + b * (1 - wx)) * wy + (c * wx + d * (1 - wx)) * (1 - wy); } diff --git a/src/display/filters/color-matrix.h b/src/display/filters/color-matrix.h index 1caa7b4585..b632ff78a5 100644 --- a/src/display/filters/color-matrix.h +++ b/src/display/filters/color-matrix.h @@ -16,7 +16,6 @@ #define INKSCAPE_DISPLAY_FILTER_MATRIX_H #include -#include #include #include <2geom/math-utils.h> @@ -38,8 +37,8 @@ struct ColorMatrix template void filter(AccessDst &dst, AccessSrc const &src) { - std::array c; - std::array o; + typename AccessDst::COLOR c; + typename AccessSrc::COLOR o; for (int y = 0; y < dst.height(); y++) { for (int x = 0; x < dst.width(); x++) { @@ -83,7 +82,7 @@ struct ColorMatrixSaturate : ColorMatrix // clang-format on } - ColorMatrixSaturate(double v_in) + explicit ColorMatrixSaturate(double v_in) : ColorMatrix(get_matrix(v_in), 3, 0.5) {} }; @@ -103,7 +102,8 @@ struct ColorMatrixHueRotate : ColorMatrix }; // clang-format on } - ColorMatrixHueRotate(double v_in) + + explicit ColorMatrixHueRotate(double v_in) : ColorMatrix(get_matrix(v_in), 3) {} }; @@ -122,6 +122,7 @@ struct ColorMatrixLuminance : ColorMatrix }; // clang-format on } + ColorMatrixLuminance() : ColorMatrix(get_matrix()) {} diff --git a/src/display/filters/color-space.h b/src/display/filters/color-space.h index 4294aae613..22fe1d267a 100644 --- a/src/display/filters/color-space.h +++ b/src/display/filters/color-space.h @@ -13,8 +13,7 @@ #ifndef INKSCAPE_DISPLAY_FILTER_COLOR_SPACE_H #define INKSCAPE_DISPLAY_FILTER_COLOR_SPACE_H -#include -#include +#include #include #include "colors/cms/transform-surface.h" @@ -25,39 +24,44 @@ namespace Inkscape::Filters { template struct ColorSpaceTransform { + /// Whether a direct conversion might be allowed. + static constexpr bool allow_direct = AccessDst::SIZE == AccessSrc::SIZE && !AccessDst::NEXT && !AccessSrc::NEXT; + // We expect to get transfer functions in the correct order for the input color space ColorSpaceTransform(std::shared_ptr from, std::shared_ptr to) : _from(std::move(from)) , _to(std::move(to)) { - // Direct color spaces use lcms2 and no other transformation - if (_from->isDirect() && _to->isDirect()) { - if constexpr (AccessDst::SIZE == AccessSrc::SIZE && !AccessDst::NEXT && !AccessSrc::NEXT) { - _transform = TransformSurface(_from->getProfile(), _to->getProfile(), _from->getBestIntent(_to), {}, Colors::RenderingIntent::AUTO); + if constexpr (allow_direct) { + if (_from->isDirect() && _to->isDirect()) { + _transform.emplace(_from->getProfile(), _to->getProfile(), _from->getBestIntent(_to)); } } } void filter(AccessDst &dst, AccessSrc const &src) { - std::array c0; - std::array c1; - if (_transform) { - _transform->do_transform(dst.width(), dst.height(), src.memory(), dst.memory()); + typename AccessSrc::COLOR c0; + typename AccessDst::COLOR c1; + + if constexpr (allow_direct) { + if (_transform) { + _transform->do_transform(dst.width(), dst.height(), src.memory(), dst.memory()); - // lcms2 transforms always returns unpremultiplied values, premultiply now - for (int y = 0; y < dst.height(); y++) { - for (int x = 0; x < dst.width(); x++) { - dst.colorAt(x, y, c1, true); - for (auto i = 0; i < AccessDst::SIZE - 1; i++) { - c1[i] *= c1.back(); + // lcms2 transforms always returns unpremultiplied values, premultiply now + for (int y = 0; y < dst.height(); y++) { + for (int x = 0; x < dst.width(); x++) { + dst.colorAt(x, y, c1, true); + for (int i = 0; i < c1.size() - 1; i++) { + c1[i] *= c1.back(); + } + dst.colorTo(x, y, c1, true); } - dst.colorTo(x, y, c1, true); } - } - return; + return; + } } // Manual @@ -69,15 +73,15 @@ struct ColorSpaceTransform // Expensive conversion, array to vector and back cin.assign(c0.begin(), c0.end()); _from->convert(cin, _to); - for (int c = 0; c < c1.size(); c++) { - c1[c] = cin[c]; + for (int i = 0; i < c1.size(); i++) { + c1[i] = cin[i]; } dst.colorTo(x, y, c1, true); } } } - std::unique_ptr> _transform; + std::optional> _transform; std::shared_ptr _from; std::shared_ptr _to; }; diff --git a/testfiles/src/display/drawing-access-test.cpp b/testfiles/src/display/drawing-access-test.cpp index e56edceea1..37a5b8c6cf 100644 --- a/testfiles/src/display/drawing-access-test.cpp +++ b/testfiles/src/display/drawing-access-test.cpp @@ -443,13 +443,13 @@ TEST(DrawingAccessTest, nonCairoMemoryAccess) } template -void TestcontigiousCopy(std::array in, std::array cmp, bool unpre = false) { +void TestcontiguousCopy(std::array in, std::array cmp, bool unpre = false) { int w = 21; int h = 21; auto src = TestSurface<4, F>(w, h); src.rect(6, 6, 9, 9, in); - std::vector copy = src._d->template contigiousCopy(unpre); + std::vector copy = src._d->template contiguousCopy(unpre); ASSERT_EQ(copy.size(), w * h * 5); std::array zero; @@ -460,7 +460,7 @@ void TestcontigiousCopy(std::array in, std::array cmp, bool un first[i] = copy[(0 * w + 0) * 5 + i]; mid[i] = copy[(y * w + x) * 5 + i]; } - if constexpr (std::is_integral::value) { + if constexpr (std::is_integral_v) { EXPECT_EQ(first, zero); EXPECT_EQ(mid, cmp); } else { @@ -469,35 +469,34 @@ void TestcontigiousCopy(std::array in, std::array cmp, bool un } } -TEST(DrawingAccessTest, contigiousCopy) +TEST(DrawingAccessTest, contiguousCopy) { // Direct copy of original data - TestcontigiousCopy({0.2, 0.4, 0.6, 0.8, 0.5}, {25, 51, 76, 102, 128}, false); + TestcontiguousCopy({0.2, 0.4, 0.6, 0.8, 0.5}, {25, 51, 76, 102, 128}, false); // Data upscaling shows small numbers just get rounded down to zero - TestcontigiousCopy({0.002, 0.004, 0.006, 0.008, 0.5}, {0, 0, 0, 257, 32896}, false); - TestcontigiousCopy({0.002, 0.004, 0.006, 0.008, 0.5}, {0, 0, 0, 16843009, 2155905152}, false); - TestcontigiousCopy({1.0, 0.0, 1.0, 0.5, 0.5}, {0.5, 0.0, 0.5, 0.25, 0.5}, false); - TestcontigiousCopy({1.0, 0.0, 1.0, 0.5, 0.5}, {0.5, 0.0, 0.5, 0.25, 0.5}, false); + TestcontiguousCopy({0.002, 0.004, 0.006, 0.008, 0.5}, {0, 0, 0, 257, 32896}, false); + TestcontiguousCopy({0.002, 0.004, 0.006, 0.008, 0.5}, {0, 0, 0, 16843009, 2155905152}, false); + TestcontiguousCopy({1.0, 0.0, 1.0, 0.5, 0.5}, {0.5, 0.0, 0.5, 0.25, 0.5}, false); + TestcontiguousCopy({1.0, 0.0, 1.0, 0.5, 0.5}, {0.5, 0.0, 0.5, 0.25, 0.5}, false); // Data isn't rescaled so we see actual values not rounded - TestcontigiousCopy({0.2, 0.4, 0.6, 0.8, 0.5}, {25, 51, 76, 102, 127}, false); - TestcontigiousCopy({0.002, 0.004, 0.006, 0.008, 0.5}, {65, 130, 196, 261, 32767}, false); - // TODO: DOESNT COMPILE TestcontigiousCopy(); - TestcontigiousCopy({1.0, 0.0, 1.0, 0.5, 0.5}, {0.5, 0.0, 0.5, 0.25, 0.5}, false); - TestcontigiousCopy({1.0, 0.0, 1.0, 0.5, 0.5}, {0.5, 0.0, 0.5, 0.25, 0.5}, false); + TestcontiguousCopy({0.2, 0.4, 0.6, 0.8, 0.5}, {25, 51, 76, 102, 127}, false); + TestcontiguousCopy({0.002, 0.004, 0.006, 0.008, 0.5}, {65, 130, 196, 261, 32767}, false); + // TODO: DOESN'T COMPILE TestcontiguousCopy(); + TestcontiguousCopy({1.0, 0.0, 1.0, 0.5, 0.5}, {0.5, 0.0, 0.5, 0.25, 0.5}, false); + TestcontiguousCopy({1.0, 0.0, 1.0, 0.5, 0.5}, {0.5, 0.0, 0.5, 0.25, 0.5}, false); // Unpremultiply alpha in returned values - TestcontigiousCopy({0.1, 0.2, 0.3, 0.4, 0.5}, {23, 49, 75, 101, 128}, true); - TestcontigiousCopy({0.001, 0.002, 0.003, 0.004, 0.5}, {0, 0, 0, 0, 32896}, true); - TestcontigiousCopy({0.001, 0.002, 0.003, 0.004, 0.5}, {0, 0, 0, 0, 2155905152}, true); - TestcontigiousCopy({1.0, 0.0, 1.0, 0.5, 0.5}, {1.0, 0.0, 1.0, 0.5, 0.5}, true); - TestcontigiousCopy({1.0, 0.0, 1.0, 0.5, 0.5}, {1.0, 0.0, 1.0, 0.5, 0.5}, true); - TestcontigiousCopy({0.1, 0.2, 0.3, 0.4, 0.5}, {25, 51, 76, 101, 127}, true); - TestcontigiousCopy({0.001, 0.002, 0.003, 0.004, 0.5}, {65, 131, 195, 261, 32767}, true); + TestcontiguousCopy({0.1, 0.2, 0.3, 0.4, 0.5}, {23, 49, 75, 101, 128}, true); + TestcontiguousCopy({0.001, 0.002, 0.003, 0.004, 0.5}, {0, 0, 0, 0, 32896}, true); + TestcontiguousCopy({0.001, 0.002, 0.003, 0.004, 0.5}, {0, 0, 0, 0, 2155905152}, true); + TestcontiguousCopy({1.0, 0.0, 1.0, 0.5, 0.5}, {1.0, 0.0, 1.0, 0.5, 0.5}, true); + TestcontiguousCopy({1.0, 0.0, 1.0, 0.5, 0.5}, {1.0, 0.0, 1.0, 0.5, 0.5}, true); + TestcontiguousCopy({0.1, 0.2, 0.3, 0.4, 0.5}, {25, 51, 76, 101, 127}, true); + TestcontiguousCopy({0.001, 0.002, 0.003, 0.004, 0.5}, {65, 131, 195, 261, 32767}, true); } - /* Local Variables: mode:c++ -- GitLab From 1bbed4e5063c6d3ce1d6cd3d6ac859aa942ec027 Mon Sep 17 00:00:00 2001 From: Martin Owens Date: Tue, 18 Nov 2025 16:08:07 -0500 Subject: [PATCH 3/7] Review fixes --- src/display/drawing-access.h | 187 +++++++++--------- src/display/filters/color-matrix.h | 4 +- src/display/filters/color-space.h | 8 +- src/display/filters/component-transfer.h | 10 +- src/display/filters/composite.h | 4 +- src/display/filters/convolve-matrix.h | 4 +- src/display/filters/displacement-map.h | 4 +- src/display/filters/gaussian-blur.h | 55 +++--- src/display/filters/light.h | 14 +- src/display/filters/morphology.h | 6 +- src/display/filters/turbulence.h | 12 +- testfiles/src/display/drawing-access-test.cpp | 30 ++- testfiles/src/test-utils.h | 2 +- 13 files changed, 178 insertions(+), 162 deletions(-) diff --git a/src/display/drawing-access.h b/src/display/drawing-access.h index 72f14198db..d92f6f1c5f 100644 --- a/src/display/drawing-access.h +++ b/src/display/drawing-access.h @@ -52,36 +52,37 @@ enum class DrawingAccessEdgeMode /** * Image surface memory access for different types which can span multiple surfaces. * - * @template_arg F - The cairo type this drawing access is for. - * @template_arg C - The total number of channels in this format across all surfaces. - * @template_arg P0 - Optionally override primary count for accessing non-cairo - * memory layouts. + * @template_arg Format - The cairo type this drawing access is for. + * @template_arg ChannelCount - The total number of channels in this format across all surfaces. + * @template_arg PrimaryOverride - Optionally override primary count for accessing non-cairo + * memory layouts. */ -template +template class DrawingAccess { public: // Is the format an integer based format - constexpr static bool I = F != CAIRO_FORMAT_RGBA128F; + constexpr static bool IsInteger = Format != CAIRO_FORMAT_RGBA128F; // How many primaries are there in this format - constexpr static int P = P0 ? P0 : (F == CAIRO_FORMAT_A8 ? 0 : 3); + constexpr static int PrimaryCount = PrimaryOverride ? PrimaryOverride : (Format == CAIRO_FORMAT_A8 ? 0 : 3); + constexpr static int PrimaryTotal = PrimaryCount + 1; // Plus Alpha // The internal type used by each channel in the format - using T = std::conditional_t; + using PrimaryType = std::conditional_t; - // Scale of each primary to convert to a double - constexpr static double _scale = I ? 255.0 : 1.0; + // Scale of each primary to convert to a double used in Channels + constexpr static double PrimaryScale = IsInteger ? 255.0 : 1.0; // Position of the alpha primary in this format - constexpr static int _alpha_primary = I ? 0 : P; + constexpr static int PrimaryAlpha = IsInteger ? 0 : PrimaryCount; // Does this DrawingAccess need two surfaces? - constexpr static bool NEXT = C > P; + constexpr static bool HasMoreChannels = ChannelCount > PrimaryCount; // Actual number of channels when including alpha - constexpr static int SIZE = C + 1; - using COLOR = std::array; + constexpr static int ChannelTotal = ChannelCount + 1; + using Color = std::array; /** * Create a drawing access object for the given cairo surface. @@ -92,24 +93,24 @@ public: */ explicit DrawingAccess(Cairo::RefPtr cairo_surface, Cairo::RefPtr next_surface = {}) - requires (C <= P * (NEXT + 1)) + requires(ChannelCount <= PrimaryCount * (HasMoreChannels + 1)) : _width(cairo_surface->get_width()) , _height(cairo_surface->get_height()) - , _stride(cairo_surface->get_stride() / sizeof(T)) - , _memory(reinterpret_cast(cairo_surface->get_data())) + , _stride(cairo_surface->get_stride() / sizeof(PrimaryType)) + , _memory(reinterpret_cast(cairo_surface->get_data())) , _edge_mode(DrawingAccessEdgeMode::ERROR) , _cairo_surface(cairo_surface) , _next_surface(next_surface) { - if (cairo_image_surface_get_format(cairo_surface->cobj()) != F) { + if (cairo_image_surface_get_format(cairo_surface->cobj()) != Format) { // throw std::exception("Format of the cairo surface doesn't match the DrawingAccess type."); } - if constexpr (NEXT) { - _next_memory = reinterpret_cast(_next_surface->get_data()); + if constexpr (HasMoreChannels) { + _next_memory = reinterpret_cast(_next_surface->get_data()); if (_width != _next_surface->get_width() || _height != _next_surface->get_height() || - _stride != _next_surface->get_stride() / sizeof(T) || - cairo_image_surface_get_format(_next_surface->cobj()) != F) { + _stride != _next_surface->get_stride() / sizeof(PrimaryType) || + cairo_image_surface_get_format(_next_surface->cobj()) != Format) { // throw std::exception("Drawing Access Next Surface must be the same formats."); } } @@ -120,10 +121,10 @@ public: * to do color convertions using lcms2 and run filters on the same memory without needing * to convert to cairo formats first. */ - DrawingAccess(T *memory, int width, int height) + DrawingAccess(PrimaryType *memory, int width, int height) : _width(width) , _height(height) - , _stride(width * (P0 + 1)) + , _stride(width * (PrimaryOverride + 1)) , _memory(memory) {} @@ -142,46 +143,44 @@ public: * @return_arg ret - The pre-sized memory for the returned color space including alpha. * We use the same memory so we don't have to re-allocate for every pixel in a filter. */ - inline void colorAt(int x, int y, COLOR &ret, bool unmultiply_alpha = false) const + inline void colorAt(int x, int y, Color &ret, bool unmultiply_alpha = false) const { int pos = _pixel_pos(x, y); double alpha = _get_alpha(pos); - for (int c = 0; c < C; c++) { - ret[c] = _get_channel(pos, c, (unmultiply_alpha && alpha > 0.0 ? alpha : 1.0)); + double alpha_mult = unmultiply_alpha ? _mult(alpha) : 1.0; + for (int c = 0; c < ChannelCount; c++) { + ret[c] = _get_channel(pos, c, alpha_mult); } - ret[C] = alpha; + ret[ChannelCount] = alpha; } /** * Using bilinear interpolation get the effective pixel at the given coordinates. * Note: Bilinear interpolation is two linear interpolations across 4 pixels * - * @arg x - The position in the x coordinate to get. - * @arg y - The position in the y coordinate to get. + * @arg x - The fractional position in the x coordinate to get. + * @arg y - The fractional position in the y coordinate to get. + * @arg unmultiply_alpha - Remove premultiplied alpha if true + * * @return_arg - The pre-sized memory for the returned color space including alpha. * We use the same memory so we don't have to re-allocate for every pixel in a filter. */ - void colorAt(double x, double y, COLOR &ret) const + void colorAt(double x, double y, Color &ret, bool unmultiply_alpha = false) const { int fx = floor(x), fy = floor(y); int cx = ceil(x), cy = ceil(y); double weight_x = x - fx, weight_y = y - fy; - int pos_a = _pixel_pos(fx, fy); - int pos_b = _pixel_pos(cx, fy); - int pos_c = _pixel_pos(fx, cy); - int pos_d = _pixel_pos(cx, cy); - - // Calculation always uses inpremultiplied colors - double alpha_a = _get_alpha(pos_a); - double alpha_b = _get_alpha(pos_b); - double alpha_c = _get_alpha(pos_c); - double alpha_d = _get_alpha(pos_d); - - for (int c = 0; c <= C; c++) { - ret[c] = _bilinear_interpolate(_get_channel(pos_a, c, alpha_a), _get_channel(pos_b, c, alpha_b), - _get_channel(pos_c, c, alpha_c), _get_channel(pos_d, c, alpha_d), weight_x, - weight_y); + for (int c = 0; c < ChannelTotal; c++) { + ret[c] = _bilinear_interpolate( + _get_channel(_pixel_pos(fx, fy), c, 1.0), _get_channel(_pixel_pos(cx, fy), c, 1.0), + _get_channel(_pixel_pos(fx, cy), c, 1.0), _get_channel(_pixel_pos(cx, cy), c, 1.0), weight_x, weight_y); + } + if (unmultiply_alpha) { + auto alpha_mult = _mult(ret[ChannelCount]); + for (int c = 0; c < ChannelCount; c++) { + ret[c] *= alpha_mult; + } } } @@ -196,9 +195,9 @@ public: * * @arg values - A set of doubles to apply to the pixel data */ - void colorTo(int x, int y, COLOR const &values, bool unmultiply_alpha = false) + void colorTo(int x, int y, Color const &values, bool unmultiply_alpha = false) { - _set_primaries_recursively(_pixel_pos(x, y), values[C], values, unmultiply_alpha); + _set_primaries_recursively(_pixel_pos(x, y), values[ChannelCount], values, unmultiply_alpha); } /** @@ -220,13 +219,9 @@ public: int cx = ceil(x), cy = ceil(y); double weight_x = x - fx, weight_y = y - fy; - int pos_a = _pixel_pos(fx, fy); - int pos_b = _pixel_pos(cx, fy); - int pos_c = _pixel_pos(fx, cy); - int pos_d = _pixel_pos(cx, cy); - - return _bilinear_interpolate(_get_alpha(pos_a), _get_alpha(pos_b), _get_alpha(pos_c), _get_alpha(pos_d), - weight_x, weight_y); + return _bilinear_interpolate(_get_alpha(_pixel_pos(fx, fy)), _get_alpha(_pixel_pos(cx, fy)), + _get_alpha(_pixel_pos(fx, cy)), _get_alpha(_pixel_pos(cx, cy)), weight_x, + weight_y); } /** @@ -242,13 +237,13 @@ public: /** * Get the number of output channels minus alpha */ - int getOutputChannels() const { return C; } + static int getOutputChannels() { return ChannelCount; } /** * Get access to the memory directly */ - T *memory() requires (!NEXT) { return _memory; } - T const *memory() const requires (!NEXT) { return _memory; } + PrimaryType *memory() requires(!HasMoreChannels) { return _memory; } + PrimaryType const *memory() const requires(!HasMoreChannels) { return _memory; } /** * Copy the surface into a single contiguous memory surface and return. @@ -257,15 +252,14 @@ public: std::vector contiguousCopy(bool unpremultiply_alpha = false) { std::vector memory; - memory.reserve(SIZE * _width * _height); + memory.reserve(ChannelTotal * _width * _height); - for (int pos = 0; pos < _height * _width * (P+1); pos += P+1) { - double alpha = unpremultiply_alpha ? _get_alpha(pos) : 1.0; - if (alpha == 0.0) alpha = 1.0; - for (int c = 0; c < C; c++) { - memory.emplace_back(_get_channel(pos, c, alpha)); + for (int pos = 0; pos < _height * _width * PrimaryTotal; pos += PrimaryTotal) { + double alpha_mult = unpremultiply_alpha ? _mult(_get_alpha(pos)) : 1.0; + for (int c = 0; c < ChannelCount; c++) { + memory.emplace_back(_get_channel(pos, c, alpha_mult)); } - memory.emplace_back(_get_channel(pos, C, 1.0)); + memory.emplace_back(_get_channel(pos, ChannelCount, 1.0)); } return memory; } @@ -283,30 +277,30 @@ private: * @param offset - Internal recursive value off where in the Color we have gotten to. * */ - inline void _set_primaries_recursively(int pos, double alpha, COLOR const &values, bool unmultiply_alpha, + inline void _set_primaries_recursively(int pos, double alpha, Color const &values, bool unmultiply_alpha, int offset = 0) { if (pos < _height * _stride) { // Set alpha in the surface - _memory[pos + _primary_pos(_alpha_primary)] = alpha * _scale; + _memory[pos + _primary_pos(PrimaryAlpha)] = alpha * PrimaryScale; auto mult = unmultiply_alpha ? alpha : 1.0; // Set the primaries in the surface - for (int p = 0; p < P + 1 && offset < values.size(); p++) { - if (p != _alpha_primary) { - _memory[pos + _primary_pos(p)] = values[offset] * mult * _scale; + for (int p = 0; p < PrimaryTotal && offset < values.size(); p++) { + if (p != PrimaryAlpha) { + _memory[pos + _primary_pos(p)] = values[offset] * mult * PrimaryScale; offset++; } } // If we have more channels, keep setting them - if constexpr (NEXT) { + if constexpr (HasMoreChannels) { // Alpha is always set in every surface - _next_memory[pos + _primary_pos(_alpha_primary)] = alpha * _scale; + _next_memory[pos + _primary_pos(PrimaryAlpha)] = alpha * PrimaryScale; - for (int p = 0; p < P + 1 && offset < values.size(); p++) { - if (p != _alpha_primary) { - _next_memory[pos + _primary_pos(p)] = values[offset] * mult * _scale; + for (int p = 0; p < PrimaryTotal && offset < values.size(); p++) { + if (p != PrimaryAlpha) { + _next_memory[pos + _primary_pos(p)] = values[offset] * mult * PrimaryScale; offset++; } } @@ -317,30 +311,30 @@ private: /** * Get the channel value from a specific memory position * - * @arg pos - The memory position in the surface (see _pixel_pos) - * @arg channel - Which channel to get, NOT the primary number. - * @arg alpha - If set will unpremultiply the channel, we do it here to preserve - * as much precision before possible conversion to int. + * @arg pos - The memory position in the surface (see _pixel_pos) + * @arg channel - Which channel to get, NOT the primary number. + * @arg alpha_mult - If set will unpremultiply the channel, we do it here to preserve + * as much precision before possible conversion to int. */ template - inline T0 _get_channel(int pos, int channel, double alpha) const + inline T0 _get_channel(int pos, int channel, double alpha_mult) const { // Allow this function to output integer types of various sizes as well as floating point types constexpr static double scale = std::is_integral_v - ? (std::is_integral_v - ? std::numeric_limits::max() / std::numeric_limits::max() // T=char T0=int|char + ? (std::is_integral_v + ? std::numeric_limits::max() / std::numeric_limits::max() // T=char T0=int|char : std::numeric_limits::max()) // T=float T0=int|char - : (T0)(1.0 / _scale); // T=float|char T0=float|double + : (T0)(1.0 / PrimaryScale); // T=float|char T0=float|double if (pos >= _height * _stride) { return 0.0; } - if constexpr (NEXT) { - if (channel >= P && channel != C) { - return _next_memory[pos + _primary_pos(channel - P + I)] * scale / alpha; + if constexpr (HasMoreChannels) { + if (channel >= PrimaryCount && channel != ChannelCount) { + return _next_memory[pos + _primary_pos(channel - PrimaryCount + IsInteger)] * scale * alpha_mult; } } - return _memory[pos + _primary_pos(channel < C ? channel + I : _alpha_primary)] * scale / alpha; + return _memory[pos + _primary_pos(channel < ChannelCount ? channel + IsInteger : PrimaryAlpha)] * scale * alpha_mult; } /** @@ -348,9 +342,14 @@ private: */ inline double _get_alpha(int pos) const { - return pos >= _height * _stride ? 0.0 : _memory[pos + _primary_pos(_alpha_primary)] / _scale; + return pos >= _height * _stride ? 0.0 : _memory[pos + _primary_pos(PrimaryAlpha)] / PrimaryScale; } + /** + * Get the multiplication alpha for use in premultiplications + */ + static inline double _mult(double alpha) { return alpha > 0 ? 1.0 / alpha : 0.0; } + /** * Get the position in the memory of this pixel */ @@ -375,7 +374,7 @@ private: break; } } - return y * _stride + x * (P + 1); + return y * _stride + x * PrimaryTotal; } /** @@ -385,7 +384,7 @@ private: static inline int _primary_pos(int p) { if constexpr (G_BYTE_ORDER == G_LITTLE_ENDIAN) { - return I ? P - p : p; + return IsInteger ? PrimaryCount - p : p; } else { return p; } @@ -396,8 +395,8 @@ private: */ static inline double _bilinear_interpolate(double a, double b, double c, double d, double wx, double wy) { - // This should only be useful for linearRGB color space, gamut curved colors such as sRGB would - // give bad results. This equation is for premultiplied values. + // This should only be useful for linearRGB color space, gamut curved colors such as sRGB and + // periodic channels like HSL/HSV would give bad results. This equation is for premultiplied values. return (a * wx + b * (1 - wx)) * wy + (c * wx + d * (1 - wx)) * (1 - wy); } @@ -405,7 +404,7 @@ private: int const _width; int const _height; int const _stride; - T *_memory; + PrimaryType *_memory; // How are out of range x,y coordinates treated DrawingAccessEdgeMode _edge_mode; @@ -414,7 +413,7 @@ private: Cairo::RefPtr _cairo_surface; // When the color space involves more channels than primaries available in one cairo surface - T *_next_memory = nullptr; + PrimaryType *_next_memory = nullptr; Cairo::RefPtr _next_surface; }; diff --git a/src/display/filters/color-matrix.h b/src/display/filters/color-matrix.h index b632ff78a5..e38e352ef0 100644 --- a/src/display/filters/color-matrix.h +++ b/src/display/filters/color-matrix.h @@ -37,8 +37,8 @@ struct ColorMatrix template void filter(AccessDst &dst, AccessSrc const &src) { - typename AccessDst::COLOR c; - typename AccessSrc::COLOR o; + typename AccessDst::Color c; + typename AccessSrc::Color o; for (int y = 0; y < dst.height(); y++) { for (int x = 0; x < dst.width(); x++) { diff --git a/src/display/filters/color-space.h b/src/display/filters/color-space.h index 22fe1d267a..ce168f187c 100644 --- a/src/display/filters/color-space.h +++ b/src/display/filters/color-space.h @@ -25,7 +25,7 @@ template struct ColorSpaceTransform { /// Whether a direct conversion might be allowed. - static constexpr bool allow_direct = AccessDst::SIZE == AccessSrc::SIZE && !AccessDst::NEXT && !AccessSrc::NEXT; + static constexpr bool allow_direct = AccessDst::ChannelTotal == AccessSrc::ChannelTotal && !AccessDst::HasMoreChannels && !AccessSrc::HasMoreChannels; // We expect to get transfer functions in the correct order for the input color space ColorSpaceTransform(std::shared_ptr from, std::shared_ptr to) @@ -42,8 +42,8 @@ struct ColorSpaceTransform void filter(AccessDst &dst, AccessSrc const &src) { - typename AccessSrc::COLOR c0; - typename AccessDst::COLOR c1; + typename AccessSrc::Color c0; + typename AccessDst::Color c1; if constexpr (allow_direct) { if (_transform) { @@ -81,7 +81,7 @@ struct ColorSpaceTransform } } - std::optional> _transform; + std::optional> _transform; std::shared_ptr _from; std::shared_ptr _to; }; diff --git a/src/display/filters/component-transfer.h b/src/display/filters/component-transfer.h index 6006deb4e3..36a34777a1 100644 --- a/src/display/filters/component-transfer.h +++ b/src/display/filters/component-transfer.h @@ -81,21 +81,21 @@ struct ComponentTransfer template void filter(AccessDst &dst, AccessSrc const &src) { - std::array c; + typename AccessDst::Color c; for (int y = 0; y < dst.height(); y++) { for (int x = 0; x < dst.width(); x++) { src.colorAt(x, y, c, true); - filterColor(c); + filterColor(c); dst.colorTo(x, y, c, true); } } } - template - inline void filterColor(std::array &c) + template + inline void filterColor(std::array &c) { double iptr; - for (unsigned i = 0; i < SIZE && i < _functions.size(); i++) { + for (unsigned i = 0; i < ChannelTotal && i < _functions.size(); i++) { auto &f = _functions[i]; switch (f._type) { case TransferType::TABLE: diff --git a/src/display/filters/composite.h b/src/display/filters/composite.h index b37ee5feca..5aecdde893 100644 --- a/src/display/filters/composite.h +++ b/src/display/filters/composite.h @@ -36,8 +36,8 @@ struct CompositeArithmetic void filter(AccessDst &dst, AccessSrc const &src) { // TODO: Detect different color spaces and convert - std::array c1; - std::array c2; + typename AccessDst::Color c1; + typename AccessSrc::Color c2; for (int y = 0; y < dst.height(); y++) { for (int x = 0; x < dst.width(); x++) { diff --git a/src/display/filters/convolve-matrix.h b/src/display/filters/convolve-matrix.h index e118f4e3a9..8ca991bbc8 100644 --- a/src/display/filters/convolve-matrix.h +++ b/src/display/filters/convolve-matrix.h @@ -46,8 +46,8 @@ struct ConvolveMatrix void filter(AccessDst &dst, AccessSrc const &src) { unsigned alpha = dst.getOutputChannels() + (_preserve_alpha ? 0 : 1); - std::array output; - std::vector> patch(_kernel.size(), output); + typename AccessDst::Color output; + std::vector patch(_kernel.size(), output); auto width = dst.width(); auto height = dst.height(); diff --git a/src/display/filters/displacement-map.h b/src/display/filters/displacement-map.h index 9dab63b000..edd20ec246 100644 --- a/src/display/filters/displacement-map.h +++ b/src/display/filters/displacement-map.h @@ -31,8 +31,8 @@ struct DisplacementMap template void filter(AccessDst &dst, AccessTexture const &texture, AccessMap const &map) { - std::array output; - std::array mappx; + typename AccessTexture::Color output; + typename AccessMap::Color mappx; for (int y = 0; y < dst.height(); y++) { for (int x = 0; x < dst.width(); x++) { diff --git a/src/display/filters/gaussian-blur.h b/src/display/filters/gaussian-blur.h index e5fe0967d3..0fabc648f2 100644 --- a/src/display/filters/gaussian-blur.h +++ b/src/display/filters/gaussian-blur.h @@ -186,10 +186,11 @@ struct GaussianBlur // Temporary storage for IIR filter // NOTE: This can be eliminated, but it reduces the precision a bit int threads = pool->size(); - std::vector *> tmpdata(threads, nullptr); + std::vector *> tmpdata(threads, nullptr); if (use_IIR_x || use_IIR_y) { for (int i = 0; i < threads; ++i) { - tmpdata[i] = new std::array[std::max(surface.width(), surface.height())]; + tmpdata[i] = + new std::array[std::max(surface.width(), surface.height())]; } } @@ -228,7 +229,7 @@ private: * Request or set a color and allow the axis to be flipped. Is always alpha unpremultiplied. */ template - constexpr inline void colorAt(Access &surface, int x, int y, std::array &output) + constexpr inline void colorAt(Access &surface, int x, int y, std::array &output) { if constexpr (axis == Geom::X) { surface.colorAt(x, y, output, false); @@ -237,7 +238,7 @@ private: } } template - constexpr inline void colorTo(Access &surface, int x, int y, std::array const &input) + constexpr inline void colorTo(Access &surface, int x, int y, std::array const &input) { if constexpr (axis == Geom::X) { surface.colorTo(x, y, input, false); @@ -247,7 +248,7 @@ private: } template - void gaussian_pass_IIR(Access &surface, std::array **tmpdata, dispatch_pool &pool) + void gaussian_pass_IIR(Access &surface, std::array **tmpdata, dispatch_pool &pool) { // Filter variables IIRValue b[N + 1]; // scaling coefficient + filter coefficients (can be 10.21 fixed point) @@ -274,15 +275,15 @@ private: pool.dispatch(row_count, [&](int row, int tid) { // Border constants - std::array imin; - std::array imax; + std::array imin; + std::array imax; colorAt(surface, 0, row, imin); colorAt(surface, col_count - 1, row, imax); int col_write = col_count; // Forward pass - std::array u[N + 1]; + std::array u[N + 1]; for (int i = 0; i < N; i++) { u[i] = imin; } @@ -292,11 +293,11 @@ private: u[i] = u[i - 1]; } colorAt(surface, col, row, u[0]); - for (int c = 0; c < Access::SIZE; c++) { + for (int c = 0; c < Access::ChannelTotal; c++) { u[0][c] *= b[0]; } for (int i = 1; i < N + 1; i++) { - for (int c = 0; c < Access::SIZE; c++) { + for (int c = 0; c < Access::ChannelTotal; c++) { u[0][c] += u[i][c] * b[i]; } } @@ -304,8 +305,8 @@ private: } // Backward pass - std::array v[N + 1]; - calcTriggsSdikaInitialization(M, u, imax, imax, b[0], v); + std::array v[N + 1]; + calcTriggsSdikaInitialization(M, u, imax, imax, b[0], v); colorTo(surface, --col_write, row, v[0]); @@ -315,11 +316,11 @@ private: } v[0] = *(tmpdata[tid] + col); - for (int c = 0; c < Access::SIZE; c++) { + for (int c = 0; c < Access::ChannelTotal; c++) { v[0][c] *= b[0]; } for (int i = 1; i < N + 1; i++) { - for (int c = 0; c < Access::SIZE; c++) { + for (int c = 0; c < Access::ChannelTotal; c++) { v[0][c] += v[i][c] * b[i]; } } @@ -352,16 +353,16 @@ private: // Kernel should have scr_len+1 elements pool.dispatch(row_count, [&](int row, int tid) { // Past pixels seen (to enable in-place operation) - boost::container::small_vector, 10> history(scr_len + 1); + boost::container::small_vector, 10> history(scr_len + 1); - std::array skipbuf; - std::array px_in; - std::array px_out; - std::array px_cmp; + std::array skipbuf; + std::array px_in; + std::array px_out; + std::array px_cmp; skipbuf.fill(INT_MIN); // history initialization - std::array imin; + std::array imin; colorAt(surface, 0, row, imin); for (int i = 0; i < scr_len; i++) { history[i] = imin; @@ -375,7 +376,7 @@ private: colorAt(surface, col, row, history[0]); // for all bytes of the pixel - for (unsigned int byte = 0; byte < Access::SIZE; byte++) { + for (unsigned int byte = 0; byte < Access::ChannelTotal; byte++) { if (skipbuf[byte] > col) continue; @@ -581,13 +582,13 @@ private: M[i] *= Mscale; } - template - static void calcTriggsSdikaInitialization(double const M[N * N], std::array const uold[N], - std::array const uplus, - std::array const vplus, IIRValue const alpha, - std::array vold[N]) + template + static void calcTriggsSdikaInitialization(double const M[N * N], std::array const uold[N], + std::array const uplus, + std::array const vplus, IIRValue const alpha, + std::array vold[N]) { - for (int c = 0; c < SIZE; c++) { + for (int c = 0; c < ChannelTotal; c++) { double uminp[N]; for (unsigned int i = 0; i < N; i++) uminp[i] = uold[i][c] - uplus[c]; diff --git a/src/display/filters/light.h b/src/display/filters/light.h index 9522c35616..3dbaca89ef 100644 --- a/src/display/filters/light.h +++ b/src/display/filters/light.h @@ -116,7 +116,7 @@ struct Lighting protected: template void doLighting(AccessSrc const &src, int x, int y, vector3d light, - std::array const &color, std::array &output) + std::array const &color, std::array &output) { if (_specular) { normalized_sum(light, light, EYE_VECTOR); @@ -256,12 +256,12 @@ struct DistantLight : public Lighting template void filter(AccessDst &dst, AccessSrc const &src) { - std::array lit_color; + std::array lit_color; // Conversion of color here for (auto i = 0; i < _color.size(); i++) { lit_color[i] = _color[i]; } - std::array output; + std::array output; if (!_specular) { // Diffuse is alpha 1.0 output.back() = 1.0; } @@ -303,12 +303,12 @@ struct PointLight : public Lighting template void filter(AccessDst &dst, AccessSrc const &src) { - std::array lit_color; + std::array lit_color; // Conversion of color here for (auto i = 0; i < _color.size(); i++) { lit_color[i] = _color[i]; } - std::array output; + std::array output; if (!_specular) { // Diffuse is alpha 1.0 output.back() = 1.0; } @@ -352,8 +352,8 @@ struct SpotLight : public Lighting template void filter(AccessDst &dst, AccessSrc const &src) { - std::array lit_color; - std::array output; + typename AccessSrc::Color lit_color; + typename AccessSrc::Color output; if (!_specular) { // Diffuse is alpha 1.0 output.back() = 1.0; } diff --git a/src/display/filters/morphology.h b/src/display/filters/morphology.h index 156e42e3d3..3cea1bb61e 100644 --- a/src/display/filters/morphology.h +++ b/src/display/filters/morphology.h @@ -84,11 +84,11 @@ struct Morphology // allocate it once for all threads and retrieving the correct set based // on the thread id. std::vector>> vals(channels); - std::array input; - std::array output; + typename AccessSrc::Color input; + typename AccessDst::Color output; // Initialize with transparent black - for (int p = 0; p < AccessDst::SIZE; ++p) { + for (int p = 0; p < AccessSrc::ChannelTotal; ++p) { vals[p].emplace_back(-1, 0); // TODO: Only do this when performing an erosion? } int in_x = 0; diff --git a/src/display/filters/turbulence.h b/src/display/filters/turbulence.h index e2b8d55ab4..fcbac41a2d 100644 --- a/src/display/filters/turbulence.h +++ b/src/display/filters/turbulence.h @@ -88,13 +88,13 @@ public: init(); } - std::array output; + typename AccessDst::Color output; for (int y = 0; y < dst.height(); y++) { for (int x = 0; x < dst.width(); x++) { // transform is added now to keep randomness the same regardless // of how the surface may have been transformed. - turbulencePixel(Geom::Point(x + x0, y + y0) * trans, output); + turbulencePixel(Geom::Point(x + x0, y + y0) * trans, output); dst.colorTo(x, y, output, true); } } @@ -170,8 +170,8 @@ public: _ready = true; } - template - inline void turbulencePixel(Geom::Point const &point, std::array &output) const + template + inline void turbulencePixel(Geom::Point const &point, std::array &output) const { std::fill(output.begin(), output.end(), 0.0); int wrapx = _wrapx, wrapy = _wrapy, wrapw = _wrapw, wraph = _wraph; @@ -220,7 +220,7 @@ public: auto const *qxb = _gradient[b10]; auto const *qya = _gradient[b01]; auto const *qyb = _gradient[b11]; - for (int k = 0; k < SIZE; ++k) { + for (int k = 0; k < ChannelTotal; ++k) { double a = _lerp(sx, rx0 * qxa[0][k] + ry0 * qxa[1][k], rx1 * qxb[0][k] + ry0 * qxb[1][k]); double b = _lerp(sx, rx0 * qya[0][k] + ry1 * qya[1][k], rx1 * qyb[0][k] + ry1 * qyb[1][k]); double r = _lerp(sy, a, b); @@ -241,7 +241,7 @@ public: } } - for (auto i = 0; i < SIZE; i++) { + for (auto i = 0; i < ChannelTotal; i++) { if (_fractalnoise) { output[i] += 1; output[i] /= 2; diff --git a/testfiles/src/display/drawing-access-test.cpp b/testfiles/src/display/drawing-access-test.cpp index 37a5b8c6cf..094752e485 100644 --- a/testfiles/src/display/drawing-access-test.cpp +++ b/testfiles/src/display/drawing-access-test.cpp @@ -15,7 +15,7 @@ struct TestFilter template void filter(AccessDst &dst, AccessSrc const &src) { - std::array c; + typename AccessSrc::Color c; for (int y = 0; y < dst.height(); y++) { for (int x = 0; x < dst.width(); x++) { if (x / 3 == y / 3) { @@ -40,7 +40,7 @@ TEST(DrawingAccessTest, ColorIs) auto c = cairo_create(cobj); for (auto channel = 0; channel < (format == CAIRO_FORMAT_A8 ? 1 : 3); channel++) { cairo_rectangle(c, 3 + channel * 6, 3 + channel * 6, 6, 6); - cairo_set_source_rgba(c, channel == 0, channel == 1, channel == 2, 1.0); + cairo_set_source_rgba(c, channel == 0, channel == 1, channel == 2, 0.6); cairo_fill(c); } cairo_destroy(c); @@ -65,9 +65,22 @@ TEST(DrawingAccessTest, ColorIs) " 88 " " 88 " " PP" - " PP")) + " PP", + PatchMethod::COLORS, true)) << "Format: " << get_format_name(format) << "\n" - << "Method: BILINEAR DECIMAL COORDS\n"; + << "Method: BILINEAR DECIMAL COORDS (Premult)\n"; + + ASSERT_TRUE(ImageIs(s, + " " + " 22 " + " 22 " + " 88 " + " 88 " + " PP" + " PP", + PatchMethod::COLORS, false)) + << "Format: " << get_format_name(format) << "\n" + << "Method: BILINEAR DECIMAL COORDS (Unpremult)\n"; } } @@ -139,9 +152,12 @@ TEST(DrawingAccessTest, BilinearInterpolation) EXPECT_NEAR(src._d->alphaAt(0.5, 1.5), 0.50, 0.001); EXPECT_NEAR(src._d->alphaAt(0.3, 1.3), 0.6999, 0.001); - ColorIs(*src._d, 0.5, 0.5, {0.25, 0.0, 0.25, 0.25}, true); - ColorIs(*src._d, 0.5, 1.5, {0.50, 0.0, 0.50, 0.50}, true); - ColorIs(*src._d, 0.3, 1.3, {0.6999, 0.0, 0.6999, 0.6999}, true); + EXPECT_TRUE(ColorIs(*src._d, 0.5, 0.5, {1.0, 0.0, 1.0, 0.25}, true)); + EXPECT_TRUE(ColorIs(*src._d, 0.5, 1.5, {1.0, 0.0, 1.0, 0.50}, true)); + EXPECT_TRUE(ColorIs(*src._d, 0.3, 1.3, {1.0, 0.0, 1.0, 0.7}, true)); + EXPECT_TRUE(ColorIs(*src._d, 0.5, 0.5, {0.25, 0.0, 0.25, 0.25}, false)); + EXPECT_TRUE(ColorIs(*src._d, 0.5, 1.5, {0.5, 0.0, 0.5, 0.50}, false)); + EXPECT_TRUE(ColorIs(*src._d, 0.3, 1.3, {0.7, 0.0, 0.7, 0.7}, false)); } TEST(DrawingAccessTest, UnmultiplyColor) diff --git a/testfiles/src/test-utils.h b/testfiles/src/test-utils.h index bcfaf8462e..14fb81e985 100644 --- a/testfiles/src/test-utils.h +++ b/testfiles/src/test-utils.h @@ -69,7 +69,7 @@ std::string print_values(T const &v, T const *other = nullptr, std::vector return oo.str(); } -::testing::AssertionResult IsNear(double a, double b, double epsilon = 0.01) +inline static ::testing::AssertionResult IsNear(double a, double b, double epsilon = 0.01) { if (std::fabs(a - b) < epsilon) { return ::testing::AssertionSuccess(); -- GitLab From a7bb509ed68bf11d0371fdf8a647f067740946a3 Mon Sep 17 00:00:00 2001 From: Martin Owens Date: Wed, 19 Nov 2025 00:31:06 +0100 Subject: [PATCH 4/7] Large-scale auto-formatting --- src/colors/cms/transform-color.cpp | 20 +++--- src/colors/cms/transform-surface.h | 65 ++++++++----------- src/colors/cms/transform.cpp | 5 +- src/colors/cms/transform.h | 6 +- src/colors/spaces/cmyk.h | 1 + src/colors/spaces/gray.h | 1 + src/colors/spaces/hsl.h | 1 + src/colors/spaces/hsluv.h | 1 + src/colors/spaces/hsv.h | 1 + src/colors/spaces/lch.h | 1 + src/colors/spaces/luv.h | 1 + src/colors/spaces/xyz.h | 1 - src/display/drawing-access.h | 35 ++++++---- src/display/filters/color-space.h | 6 +- src/display/filters/gaussian-blur.h | 3 +- src/display/filters/light.h | 3 +- src/ui/widget/canvas.cpp | 2 +- .../src/colors/cms-transform-surface-test.cpp | 7 +- testfiles/src/display/drawing-access-test.cpp | 46 +++++++------ .../src/display/filter-color-space-test.cpp | 7 +- testfiles/src/display/test-base.h | 5 +- testfiles/src/test-utils.h | 9 ++- 22 files changed, 120 insertions(+), 107 deletions(-) diff --git a/src/colors/cms/transform-color.cpp b/src/colors/cms/transform-color.cpp index 3188535ad1..9f62db1aeb 100644 --- a/src/colors/cms/transform-color.cpp +++ b/src/colors/cms/transform-color.cpp @@ -24,17 +24,15 @@ namespace Inkscape::Colors::CMS { * @arg to - The target CMS Profile the color data needs to end up in. * @arg intent - The rendering intent to use when changing the gamut and white balance. */ -TransformColor::TransformColor(std::shared_ptr const &from, - std::shared_ptr const &to, RenderingIntent intent) - : Transform(cmsCreateTransform( - from->getHandle(), - // Colors may have Alpha, but are NEVER premultiplied and we only convert one "pixel" - // at a time so there's no need to specify the presence of the alpha which is optional - lcms_color_format(from), - to->getHandle(), - lcms_color_format(to), - lcms_intent(intent), - lcms_bpc(intent)), true) +TransformColor::TransformColor(std::shared_ptr const &from, std::shared_ptr const &to, + RenderingIntent intent) + : Transform(cmsCreateTransform(from->getHandle(), + // Colors may have Alpha, but are NEVER premultiplied and we only convert one "pixel" + // at a time so there's no need to specify the presence of the alpha which is + // optional + lcms_color_format(from), to->getHandle(), lcms_color_format(to), lcms_intent(intent), + lcms_bpc(intent)), + true) , _channels_in(from->getSize()) , _channels_out(to->getSize()) { diff --git a/src/colors/cms/transform-surface.h b/src/colors/cms/transform-surface.h index 1a905b2e55..c588bdfc9d 100644 --- a/src/colors/cms/transform-surface.h +++ b/src/colors/cms/transform-surface.h @@ -9,8 +9,8 @@ #define SEEN_COLORS_CMS_TRANSFORM_SURFACE_H #include -#include #include +#include #include #include "profile.h" @@ -34,33 +34,27 @@ public: * @arg proof_intent - An optional intent for the proofing conversion * @arg with_gamut_warn - Optional flag for rendering out of gamut colors with a warning color. */ - TransformSurface( - std::shared_ptr const &from, - std::shared_ptr const &to, - RenderingIntent intent = RenderingIntent::PERCEPTUAL, - std::shared_ptr const &proof = nullptr, - RenderingIntent proof_intent = RenderingIntent::AUTO) - requires (!GAMUTWARN || (std::is_same_v && std::is_same_v)) - : Transform(proof ? - cmsCreateProofingTransformTHR( - cmsCreateContext(nullptr, nullptr), - from->getHandle(), - lcms_color_format(from, sizeof(TIN), !std::is_integral_v, alpha_mode(PREMULT, true)), - to->getHandle(), - lcms_color_format(to, sizeof(TOUT), !std::is_integral_v, alpha_mode(false, true)), - proof->getHandle(), - lcms_intent(intent), - lcms_intent(proof_intent), - cmsFLAGS_COPY_ALPHA | cmsFLAGS_SOFTPROOFING | (GAMUTWARN ? cmsFLAGS_GAMUTCHECK : 0) | lcms_bpc(proof_intent)) - : cmsCreateTransformTHR( - cmsCreateContext(nullptr, nullptr), - from->getHandle(), - lcms_color_format(from, sizeof(TIN), !std::is_integral_v, alpha_mode(PREMULT, true)), - to->getHandle(), - lcms_color_format(to, sizeof(TOUT), !std::is_integral_v, alpha_mode(false, true)), - lcms_intent(intent), - cmsFLAGS_COPY_ALPHA) - , false) + TransformSurface(std::shared_ptr const &from, std::shared_ptr const &to, + RenderingIntent intent = RenderingIntent::PERCEPTUAL, + std::shared_ptr const &proof = nullptr, + RenderingIntent proof_intent = RenderingIntent::AUTO) + requires(!GAMUTWARN || (std::is_same_v && std::is_same_v)) + : Transform(proof + ? cmsCreateProofingTransformTHR( + cmsCreateContext(nullptr, nullptr), from->getHandle(), + lcms_color_format(from, sizeof(TIN), !std::is_integral_v, alpha_mode(PREMULT, true)), + to->getHandle(), + lcms_color_format(to, sizeof(TOUT), !std::is_integral_v, alpha_mode(false, true)), + proof->getHandle(), lcms_intent(intent), lcms_intent(proof_intent), + cmsFLAGS_COPY_ALPHA | cmsFLAGS_SOFTPROOFING | (GAMUTWARN ? cmsFLAGS_GAMUTCHECK : 0) | + lcms_bpc(proof_intent)) + : cmsCreateTransformTHR( + cmsCreateContext(nullptr, nullptr), from->getHandle(), + lcms_color_format(from, sizeof(TIN), !std::is_integral_v, alpha_mode(PREMULT, true)), + to->getHandle(), + lcms_color_format(to, sizeof(TOUT), !std::is_integral_v, alpha_mode(false, true)), + lcms_intent(intent), cmsFLAGS_COPY_ALPHA), + false) , _pixel_size_in((_channels_in + 1) * sizeof(TIN)) , _pixel_size_out((_channels_out + 1) * sizeof(TOUT)) {} @@ -75,18 +69,11 @@ public: * @arg stride_in - The optional stride for the input image, if known to be uncontiguous. * @arg stride_out - The optional stride for the output image, if known to be uncontiguous. */ - void do_transform(int width, int height, TIN const *px_in, TOUT *px_out, int stride_in = 0, int stride_out = 0) const + void do_transform(int width, int height, TIN const *px_in, TOUT *px_out, int stride_in = 0, + int stride_out = 0) const { - cmsDoTransformLineStride( - _handle, - px_in, - px_out, - width, - height, - stride_in ? stride_in : width * _pixel_size_in, - stride_out ? stride_out : width * _pixel_size_out, - 0, 0 - ); + cmsDoTransformLineStride(_handle, px_in, px_out, width, height, stride_in ? stride_in : width * _pixel_size_in, + stride_out ? stride_out : width * _pixel_size_out, 0, 0); } /** diff --git a/src/colors/cms/transform.cpp b/src/colors/cms/transform.cpp index 0bdc616047..5864be7526 100644 --- a/src/colors/cms/transform.cpp +++ b/src/colors/cms/transform.cpp @@ -9,11 +9,12 @@ */ #include "transform.h" -#include "profile.h" #include #include +#include "profile.h" + namespace Inkscape::Colors::CMS { // Color space is used in lcms2 to scale input and output values, we don't want this. @@ -33,7 +34,7 @@ static constexpr cmsUInt32Number mask_colorspace = ~COLORSPACE_SH(0b11111); int Transform::lcms_color_format(std::shared_ptr const &profile, int size, bool decimal, Alpha alpha) { // Format is 64bit floating point (double) or 32bit (float) - // Note: size of 8 will clobber channel size bit and cause errors, pass zero (see lcms API docs) + // Note: size of 8 will clobber channel size bit and cause errors, pass zero (see lcms API docs) auto format = cmsFormatterForColorspaceOfProfile(profile->getHandle(), std::clamp(size, 0, 4), decimal); // Add the alpha channel into the formatter diff --git a/src/colors/cms/transform.h b/src/colors/cms/transform.h index e1743ed323..5c4f2c1460 100644 --- a/src/colors/cms/transform.h +++ b/src/colors/cms/transform.h @@ -53,11 +53,13 @@ protected: cmsHTRANSFORM _handle; cmsContext _context; - static Alpha alpha_mode(bool premultiplied, bool present) { + static Alpha alpha_mode(bool premultiplied, bool present) + { return !present ? Alpha::NONE : (premultiplied ? Alpha::PREMULTIPLIED : Alpha::PRESENT); } - static int lcms_color_format(std::shared_ptr const &profile, int size = 0, bool decimal = true, Alpha alpha = Alpha::NONE); + static int lcms_color_format(std::shared_ptr const &profile, int size = 0, bool decimal = true, + Alpha alpha = Alpha::NONE); static int lcms_intent(RenderingIntent intent); static int lcms_bpc(RenderingIntent intent); diff --git a/src/colors/spaces/cmyk.h b/src/colors/spaces/cmyk.h index c2d5f831c2..aa7a42910b 100644 --- a/src/colors/spaces/cmyk.h +++ b/src/colors/spaces/cmyk.h @@ -25,6 +25,7 @@ public: ~DeviceCMYK() override = default; bool isDirect() const override { return false; } + protected: friend class Inkscape::Colors::Color; diff --git a/src/colors/spaces/gray.h b/src/colors/spaces/gray.h index 511ffc7079..b9c7592784 100644 --- a/src/colors/spaces/gray.h +++ b/src/colors/spaces/gray.h @@ -21,6 +21,7 @@ public: Gray(): RGB(Type::Gray, 1, "Gray", "Gray", "color-selector-gray") {} bool isDirect() const override { return false; } + protected: friend class Inkscape::Colors::Color; diff --git a/src/colors/spaces/hsl.h b/src/colors/spaces/hsl.h index 1d76331ede..79edc23e9a 100644 --- a/src/colors/spaces/hsl.h +++ b/src/colors/spaces/hsl.h @@ -22,6 +22,7 @@ public: ~HSL() override = default; bool isDirect() const override { return false; } + protected: friend class Inkscape::Colors::Color; diff --git a/src/colors/spaces/hsluv.h b/src/colors/spaces/hsluv.h index 960ff6ff6e..428c2555e9 100644 --- a/src/colors/spaces/hsluv.h +++ b/src/colors/spaces/hsluv.h @@ -28,6 +28,7 @@ public: ~HSLuv() override = default; bool isDirect() const override { return false; } + protected: friend class Inkscape::Colors::Color; diff --git a/src/colors/spaces/hsv.h b/src/colors/spaces/hsv.h index 914e861076..a598ffd1a1 100644 --- a/src/colors/spaces/hsv.h +++ b/src/colors/spaces/hsv.h @@ -22,6 +22,7 @@ public: ~HSV() override = default; bool isDirect() const override { return false; } + protected: friend class Inkscape::Colors::Color; diff --git a/src/colors/spaces/lch.h b/src/colors/spaces/lch.h index eb569ab934..3779e70d05 100644 --- a/src/colors/spaces/lch.h +++ b/src/colors/spaces/lch.h @@ -22,6 +22,7 @@ public: ~Lch() override = default; bool isDirect() const override { return false; } + protected: friend class Inkscape::Colors::Color; diff --git a/src/colors/spaces/luv.h b/src/colors/spaces/luv.h index 0206dd156a..4a0213b5d9 100644 --- a/src/colors/spaces/luv.h +++ b/src/colors/spaces/luv.h @@ -26,6 +26,7 @@ public: ~Luv() override = default; bool isDirect() const override { return false; } + protected: friend class Inkscape::Colors::Color; diff --git a/src/colors/spaces/xyz.h b/src/colors/spaces/xyz.h index d14c0bc522..ccb61129dd 100644 --- a/src/colors/spaces/xyz.h +++ b/src/colors/spaces/xyz.h @@ -53,7 +53,6 @@ protected: XYZ50(Type type, int components, std::string name, std::string shortName, std::string icon, bool spaceIsUnbounded = false); std::string toString(std::vector const &values, bool opacity = true) const override { return _toString(values, opacity, true); } - }; } // namespace Inkscape::Colors::Space diff --git a/src/display/drawing-access.h b/src/display/drawing-access.h index d92f6f1c5f..862f328cdb 100644 --- a/src/display/drawing-access.h +++ b/src/display/drawing-access.h @@ -16,8 +16,9 @@ #include #include #include -#include #include +#include + #include "helper/mathfns.h" /** @@ -93,7 +94,7 @@ public: */ explicit DrawingAccess(Cairo::RefPtr cairo_surface, Cairo::RefPtr next_surface = {}) - requires(ChannelCount <= PrimaryCount * (HasMoreChannels + 1)) + requires(ChannelCount <= PrimaryCount * (HasMoreChannels + 1)) : _width(cairo_surface->get_width()) , _height(cairo_surface->get_height()) , _stride(cairo_surface->get_stride() / sizeof(PrimaryType)) @@ -242,12 +243,20 @@ public: /** * Get access to the memory directly */ - PrimaryType *memory() requires(!HasMoreChannels) { return _memory; } - PrimaryType const *memory() const requires(!HasMoreChannels) { return _memory; } + PrimaryType *memory() + requires(!HasMoreChannels) + { + return _memory; + } + PrimaryType const *memory() const + requires(!HasMoreChannels) + { + return _memory; + } /** - * Copy the surface into a single contiguous memory surface and return. - */ + * Copy the surface into a single contiguous memory surface and return. + */ template std::vector contiguousCopy(bool unpremultiply_alpha = false) { @@ -320,11 +329,12 @@ private: inline T0 _get_channel(int pos, int channel, double alpha_mult) const { // Allow this function to output integer types of various sizes as well as floating point types - constexpr static double scale = std::is_integral_v - ? (std::is_integral_v - ? std::numeric_limits::max() / std::numeric_limits::max() // T=char T0=int|char - : std::numeric_limits::max()) // T=float T0=int|char - : (T0)(1.0 / PrimaryScale); // T=float|char T0=float|double + constexpr static double scale = + std::is_integral_v + ? (std::is_integral_v + ? std::numeric_limits::max() / std::numeric_limits::max() // T=char T0=int|char + : std::numeric_limits::max()) // T=float T0=int|char + : (T0)(1.0 / PrimaryScale); // T=float|char T0=float|double if (pos >= _height * _stride) { return 0.0; @@ -334,7 +344,8 @@ private: return _next_memory[pos + _primary_pos(channel - PrimaryCount + IsInteger)] * scale * alpha_mult; } } - return _memory[pos + _primary_pos(channel < ChannelCount ? channel + IsInteger : PrimaryAlpha)] * scale * alpha_mult; + return _memory[pos + _primary_pos(channel < ChannelCount ? channel + IsInteger : PrimaryAlpha)] * scale * + alpha_mult; } /** diff --git a/src/display/filters/color-space.h b/src/display/filters/color-space.h index ce168f187c..b8f5732863 100644 --- a/src/display/filters/color-space.h +++ b/src/display/filters/color-space.h @@ -25,7 +25,8 @@ template struct ColorSpaceTransform { /// Whether a direct conversion might be allowed. - static constexpr bool allow_direct = AccessDst::ChannelTotal == AccessSrc::ChannelTotal && !AccessDst::HasMoreChannels && !AccessSrc::HasMoreChannels; + static constexpr bool allow_direct = AccessDst::ChannelTotal == AccessSrc::ChannelTotal && + !AccessDst::HasMoreChannels && !AccessSrc::HasMoreChannels; // We expect to get transfer functions in the correct order for the input color space ColorSpaceTransform(std::shared_ptr from, std::shared_ptr to) @@ -81,7 +82,8 @@ struct ColorSpaceTransform } } - std::optional> _transform; + std::optional> + _transform; std::shared_ptr _from; std::shared_ptr _to; }; diff --git a/src/display/filters/gaussian-blur.h b/src/display/filters/gaussian-blur.h index 0fabc648f2..3aa953d635 100644 --- a/src/display/filters/gaussian-blur.h +++ b/src/display/filters/gaussian-blur.h @@ -238,7 +238,8 @@ private: } } template - constexpr inline void colorTo(Access &surface, int x, int y, std::array const &input) + constexpr inline void colorTo(Access &surface, int x, int y, + std::array const &input) { if constexpr (axis == Geom::X) { surface.colorTo(x, y, input, false); diff --git a/src/display/filters/light.h b/src/display/filters/light.h index 3dbaca89ef..9a3ae92360 100644 --- a/src/display/filters/light.h +++ b/src/display/filters/light.h @@ -116,7 +116,8 @@ struct Lighting protected: template void doLighting(AccessSrc const &src, int x, int y, vector3d light, - std::array const &color, std::array &output) + std::array const &color, + std::array &output) { if (_specular) { normalized_sum(light, light, EYE_VECTOR); diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp index 54adf07c6a..e140c9b25e 100644 --- a/src/ui/widget/canvas.cpp +++ b/src/ui/widget/canvas.cpp @@ -739,7 +739,7 @@ void CanvasPrivate::launch_redraw() rd.debug_show_redraw = prefs.debug_show_redraw; rd.snapshot_drawn = stores.snapshot().drawn ? stores.snapshot().drawn->copy() : Cairo::RefPtr(); - //rd.cms_transform = q->_cms_active ? q->_cms_transform : nullptr; + // rd.cms_transform = q->_cms_active ? q->_cms_transform : nullptr; abort_flags.store((int)AbortFlags::None, std::memory_order_relaxed); diff --git a/testfiles/src/colors/cms-transform-surface-test.cpp b/testfiles/src/colors/cms-transform-surface-test.cpp index 3a477664cc..85d6f75e11 100644 --- a/testfiles/src/colors/cms-transform-surface-test.cpp +++ b/testfiles/src/colors/cms-transform-surface-test.cpp @@ -9,11 +9,11 @@ #include #include -#include "../test-utils.h" +#include "../test-utils.h" #include "colors/cms/profile.h" -#include "colors/cms/transform.h" #include "colors/cms/transform-surface.h" +#include "colors/cms/transform.h" #include "colors/spaces/enum.h" using namespace Inkscape::Colors; @@ -25,6 +25,7 @@ static auto cmyk = Profile::create_from_uri(INKSCAPE_TESTS_DIR "/data/colors/def namespace { +// clang-format off TEST(ColorsCmsTransformSurface, TransformFloatTypeIn) { static const std::vector img = { @@ -202,7 +203,7 @@ TEST(ColorsCmsTransformSurface, TransformWithGamutWarning) }; EXPECT_EQ(out, ret); } - +// clang-format on } // namespace diff --git a/testfiles/src/display/drawing-access-test.cpp b/testfiles/src/display/drawing-access-test.cpp index 094752e485..4e8ac36a36 100644 --- a/testfiles/src/display/drawing-access-test.cpp +++ b/testfiles/src/display/drawing-access-test.cpp @@ -59,14 +59,15 @@ TEST(DrawingAccessTest, ColorIs) << "Method: INTEGER COORDS\n"; // Bilinear should be the same as Int (just slower) - ASSERT_TRUE(ImageIs(s, " " - " 22 " - " 22 " - " 88 " - " 88 " - " PP" - " PP", - PatchMethod::COLORS, true)) + ASSERT_TRUE(ImageIs(s, + " " + " 22 " + " 22 " + " 88 " + " 88 " + " PP" + " PP", + PatchMethod::COLORS, true)) << "Format: " << get_format_name(format) << "\n" << "Method: BILINEAR DECIMAL COORDS (Premult)\n"; @@ -448,18 +449,19 @@ TEST(DrawingAccessTest, nonCairoMemoryAccess) src.rect(6, 6, 9, 9, {1.0, 0.0, 1.0, 0.5, 1.0}); EXPECT_TRUE(ImageIs(*src._d, - " " - " " - " &&& " - " &&& " - " &&& " - " " - " ", + " " + " " + " &&& " + " &&& " + " &&& " + " " + " ", PatchMethod::ALPHA)); } template -void TestcontiguousCopy(std::array in, std::array cmp, bool unpre = false) { +void TestcontiguousCopy(std::array in, std::array cmp, bool unpre = false) +{ int w = 21; int h = 21; auto src = TestSurface<4, F>(w, h); @@ -492,13 +494,15 @@ TEST(DrawingAccessTest, contiguousCopy) // Data upscaling shows small numbers just get rounded down to zero TestcontiguousCopy({0.002, 0.004, 0.006, 0.008, 0.5}, {0, 0, 0, 257, 32896}, false); - TestcontiguousCopy({0.002, 0.004, 0.006, 0.008, 0.5}, {0, 0, 0, 16843009, 2155905152}, false); + TestcontiguousCopy({0.002, 0.004, 0.006, 0.008, 0.5}, + {0, 0, 0, 16843009, 2155905152}, false); TestcontiguousCopy({1.0, 0.0, 1.0, 0.5, 0.5}, {0.5, 0.0, 0.5, 0.25, 0.5}, false); TestcontiguousCopy({1.0, 0.0, 1.0, 0.5, 0.5}, {0.5, 0.0, 0.5, 0.25, 0.5}, false); // Data isn't rescaled so we see actual values not rounded TestcontiguousCopy({0.2, 0.4, 0.6, 0.8, 0.5}, {25, 51, 76, 102, 127}, false); - TestcontiguousCopy({0.002, 0.004, 0.006, 0.008, 0.5}, {65, 130, 196, 261, 32767}, false); + TestcontiguousCopy({0.002, 0.004, 0.006, 0.008, 0.5}, {65, 130, 196, 261, 32767}, + false); // TODO: DOESN'T COMPILE TestcontiguousCopy(); TestcontiguousCopy({1.0, 0.0, 1.0, 0.5, 0.5}, {0.5, 0.0, 0.5, 0.25, 0.5}, false); TestcontiguousCopy({1.0, 0.0, 1.0, 0.5, 0.5}, {0.5, 0.0, 0.5, 0.25, 0.5}, false); @@ -506,11 +510,13 @@ TEST(DrawingAccessTest, contiguousCopy) // Unpremultiply alpha in returned values TestcontiguousCopy({0.1, 0.2, 0.3, 0.4, 0.5}, {23, 49, 75, 101, 128}, true); TestcontiguousCopy({0.001, 0.002, 0.003, 0.004, 0.5}, {0, 0, 0, 0, 32896}, true); - TestcontiguousCopy({0.001, 0.002, 0.003, 0.004, 0.5}, {0, 0, 0, 0, 2155905152}, true); + TestcontiguousCopy({0.001, 0.002, 0.003, 0.004, 0.5}, {0, 0, 0, 0, 2155905152}, + true); TestcontiguousCopy({1.0, 0.0, 1.0, 0.5, 0.5}, {1.0, 0.0, 1.0, 0.5, 0.5}, true); TestcontiguousCopy({1.0, 0.0, 1.0, 0.5, 0.5}, {1.0, 0.0, 1.0, 0.5, 0.5}, true); TestcontiguousCopy({0.1, 0.2, 0.3, 0.4, 0.5}, {25, 51, 76, 101, 127}, true); - TestcontiguousCopy({0.001, 0.002, 0.003, 0.004, 0.5}, {65, 131, 195, 261, 32767}, true); + TestcontiguousCopy({0.001, 0.002, 0.003, 0.004, 0.5}, {65, 131, 195, 261, 32767}, + true); } /* diff --git a/testfiles/src/display/filter-color-space-test.cpp b/testfiles/src/display/filter-color-space-test.cpp index 0d24af755d..8293f76663 100644 --- a/testfiles/src/display/filter-color-space-test.cpp +++ b/testfiles/src/display/filter-color-space-test.cpp @@ -2,12 +2,10 @@ #include -#include "test-base.h" - #include "colors/manager.h" #include "colors/spaces/cms.h" - #include "display/filters/color-space.h" +#include "test-base.h" using namespace Inkscape::Colors; using namespace Inkscape::Filters; @@ -24,7 +22,8 @@ TEST(DrawingFilterTest, ColorSpace) auto src = TestSurface<4>(4, 4); src.rect(1, 1, 2, 2, {1.0, 0.0, 1.0, 0.5, 0.5}); - auto tr = ColorSpaceTransform, DrawingAccess>(cmyk, rgb); + auto tr = ColorSpaceTransform, DrawingAccess>( + cmyk, rgb); tr.filter(*dst._d, *src._d); diff --git a/testfiles/src/display/test-base.h b/testfiles/src/display/test-base.h index 1ee3a9b2e6..eb0488c8b0 100644 --- a/testfiles/src/display/test-base.h +++ b/testfiles/src/display/test-base.h @@ -73,7 +73,6 @@ struct TestCustomSurface std::shared_ptr> _d; }; - /** * Test single pixel getter, double and int modes */ @@ -136,8 +135,8 @@ std::string format_patch(std::string const &in, unsigned stride) } template -std::string build_patch(DrawingAccess const &d, PatchMethod method, unsigned patch_x = 3, unsigned patch_y = 3, - bool unmult = false) +std::string build_patch(DrawingAccess const &d, PatchMethod method, unsigned patch_x = 3, + unsigned patch_y = 3, bool unmult = false) { static std::vector const weights = {' ', ' ', ' ', '.', '.', '.', ':', ':', '-', '+', '=', 'o', 'O', '*', 'x', 'X', '$', '&'}; diff --git a/testfiles/src/test-utils.h b/testfiles/src/test-utils.h index 14fb81e985..f8aa5ff89b 100644 --- a/testfiles/src/test-utils.h +++ b/testfiles/src/test-utils.h @@ -30,7 +30,6 @@ struct traced_data __FILE__, __LINE__, __VA_ARGS__ \ } - /** * Print a vector of doubles for debugging * @@ -63,7 +62,8 @@ std::string print_values(T const &v, T const *other = nullptr, std::vector oo << "\033[0m"; } - if (i < v.size() - 1) oo << ", "; + if (i < v.size() - 1) + oo << ", "; } oo << "}(" << v.size() << ")"; return oo.str(); @@ -92,9 +92,8 @@ template } if (!is_same) { return ::testing::AssertionFailure() << "\n" - << print_values(A, same_size ? &B : nullptr, failed) - << "\n != \n" - << print_values(B, same_size ? &A : nullptr, failed); + << print_values(A, same_size ? &B : nullptr, failed) << "\n != \n" + << print_values(B, same_size ? &A : nullptr, failed); } return ::testing::AssertionSuccess(); } -- GitLab From 2b82b785b7e27491b05f6ffeee67f45286f83bd9 Mon Sep 17 00:00:00 2001 From: Martin Owens Date: Tue, 2 Dec 2025 11:23:08 -0500 Subject: [PATCH 5/7] Fix build on macOS --- src/colors/CMakeUnit.txt | 1 - src/display/CMakeUnit.txt | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/colors/CMakeUnit.txt b/src/colors/CMakeUnit.txt index 409c69955f..f607ffefef 100644 --- a/src/colors/CMakeUnit.txt +++ b/src/colors/CMakeUnit.txt @@ -4,7 +4,6 @@ set(colors_unit_SRC cms/profile.cpp cms/transform.cpp cms/transform-color.cpp - cms/transform-cairo.cpp color.cpp manager.cpp parser.cpp diff --git a/src/display/CMakeUnit.txt b/src/display/CMakeUnit.txt index 2e974822ea..aa5d334b7c 100644 --- a/src/display/CMakeUnit.txt +++ b/src/display/CMakeUnit.txt @@ -22,7 +22,8 @@ function(get_display_unit_lib) list(PREPEND display_unit_SRC "display/threading.cpp" "display/dispatch-pool.cpp") list(TRANSFORM display_unit_SRC PREPEND "${CMAKE_SOURCE_DIR}/src/") add_library(display_unit_lib SHARED "${display_unit_SRC}") - target_link_libraries(display_unit_lib cairo cairomm-1.16 2Geom::2geom) + pkg_check_modules(DISPLAY_UNIT_DEPS REQUIRED IMPORTED_TARGET cairomm-1.16) + target_link_libraries(display_unit_lib PkgConfig::DISPLAY_UNIT_DEPS 2Geom::2geom) make_target_unit_testable(display_unit_lib) endfunction(get_display_unit_lib) -- GitLab From beaa988fedd7f27354fa3557770f5981239ebb69 Mon Sep 17 00:00:00 2001 From: PBS Date: Wed, 3 Dec 2025 23:54:48 +0100 Subject: [PATCH 6/7] Another pass --- src/display/drawing-access.h | 2 +- src/display/filters/color-matrix.h | 2 +- src/display/filters/component-transfer.h | 13 +++++----- src/display/filters/composite.h | 3 --- src/display/filters/convolve-matrix.h | 8 ++---- src/display/filters/displacement-map.h | 6 +---- src/display/filters/gaussian-blur.h | 26 +++++++++---------- src/display/filters/light.h | 24 ++++++++--------- src/display/filters/morphology.h | 3 +-- src/display/filters/turbulence.h | 4 +-- .../src/colors/cms-transform-surface-test.cpp | 1 - testfiles/src/display/drawing-access-test.cpp | 4 +-- testfiles/src/display/filter-light-test.cpp | 2 +- testfiles/src/display/test-base.h | 4 +-- 14 files changed, 42 insertions(+), 60 deletions(-) diff --git a/src/display/drawing-access.h b/src/display/drawing-access.h index 862f328cdb..f454e4ea60 100644 --- a/src/display/drawing-access.h +++ b/src/display/drawing-access.h @@ -415,7 +415,7 @@ private: int const _width; int const _height; int const _stride; - PrimaryType *_memory; + PrimaryType *_memory{}; // How are out of range x,y coordinates treated DrawingAccessEdgeMode _edge_mode; diff --git a/src/display/filters/color-matrix.h b/src/display/filters/color-matrix.h index e38e352ef0..70fe9c0e60 100644 --- a/src/display/filters/color-matrix.h +++ b/src/display/filters/color-matrix.h @@ -23,7 +23,7 @@ namespace Inkscape::Filters { struct ColorMatrix { - ColorMatrix(std::vector matrix, unsigned channels = 4, double adj = 0.0) + explicit ColorMatrix(std::vector matrix, unsigned channels = 4, double adj = 0.0) : _matrix(std::move(matrix)) , _width(channels + 1) , _adj(adj) diff --git a/src/display/filters/component-transfer.h b/src/display/filters/component-transfer.h index 36a34777a1..cb166c8e5d 100644 --- a/src/display/filters/component-transfer.h +++ b/src/display/filters/component-transfer.h @@ -74,7 +74,7 @@ struct TransferFunction struct ComponentTransfer { // We expect to get transfer functions in the correct order for the input color space - ComponentTransfer(std::vector functions) + explicit ComponentTransfer(std::vector functions) : _functions(std::move(functions)) {} @@ -94,22 +94,23 @@ struct ComponentTransfer template inline void filterColor(std::array &c) { - double iptr; - for (unsigned i = 0; i < ChannelTotal && i < _functions.size(); i++) { + auto const num_i = std::min(ChannelTotal, _functions.size()); + for (unsigned i = 0; i < num_i; i++) { auto &f = _functions[i]; switch (f._type) { case TransferType::TABLE: if (f._next.empty() || c[i] == 1.0) { c[i] = f._table.back(); } else { - auto dx = std::modf((f._next.size()) * c[i], &iptr); + double iptr; + auto dx = std::modf(f._next.size() * c[i], &iptr); unsigned k = iptr; - c[i] = ((f._table[k] * (1.0 - dx) + f._next[k] * dx)); + c[i] = f._table[k] * (1.0 - dx) + f._next[k] * dx; } break; case TransferType::DISCRETE: { unsigned k = f._table.size() * c[i]; - c[i] = (k == f._table.size()) ? f._table.back() : f._table[k]; + c[i] = k == f._table.size() ? f._table.back() : f._table[k]; break; } case TransferType::LINEAR: diff --git a/src/display/filters/composite.h b/src/display/filters/composite.h index 5aecdde893..ec348c9ef5 100644 --- a/src/display/filters/composite.h +++ b/src/display/filters/composite.h @@ -15,9 +15,6 @@ #define INKSCAPE_DISPLAY_FILTER_COMPOSITE_H #include -#include - -#include "display/drawing-access.h" namespace Inkscape::Filters { diff --git a/src/display/filters/convolve-matrix.h b/src/display/filters/convolve-matrix.h index 8ca991bbc8..be563e60ce 100644 --- a/src/display/filters/convolve-matrix.h +++ b/src/display/filters/convolve-matrix.h @@ -14,10 +14,6 @@ #define INKSCAPE_DISPLAY_FILTER_CONVOLVE_MATRIX_H #include -#include -#include -#include -#include // DEBUG #include namespace Inkscape { @@ -82,7 +78,7 @@ struct ConvolveMatrix // Covolve each color channel for (auto k = 0; k < alpha; k++) { - output[k] += (patch[pos][k] * coeff); + output[k] += patch[pos][k] * coeff; } } } @@ -109,7 +105,7 @@ struct ConvolveMatrix bool _preserve_alpha; // We expect unpremultiplied Alpha - static bool const needs_unmultiplied = true; + static constexpr bool needs_unmultiplied = true; }; } // namespace Inkscape diff --git a/src/display/filters/displacement-map.h b/src/display/filters/displacement-map.h index edd20ec246..06f3d39ada 100644 --- a/src/display/filters/displacement-map.h +++ b/src/display/filters/displacement-map.h @@ -13,10 +13,6 @@ #ifndef INKSCAPE_DISPLAY_FILTER_DISPLACEMENT_MAP_H #define INKSCAPE_DISPLAY_FILTER_DISPLACEMENT_MAP_H -#include - -#include "display/drawing-access.h" - namespace Inkscape::Filters { struct DisplacementMap @@ -57,7 +53,7 @@ struct DisplacementMap #endif // INKSCAPE_DISPLAY_FILTER_DISPLACEMENT_MAP_H /* - ;Local Variables: + Local Variables: mode:c++ c-file-style:"stroustrup" c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) diff --git a/src/display/filters/gaussian-blur.h b/src/display/filters/gaussian-blur.h index 3aa953d635..91574bbdf5 100644 --- a/src/display/filters/gaussian-blur.h +++ b/src/display/filters/gaussian-blur.h @@ -26,8 +26,8 @@ * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ -#ifndef INKSCAPE_DISPLAY_FILTER_DISPLACEMENT_MAP_H -#define INKSCAPE_DISPLAY_FILTER_DISPLACEMENT_MAP_H +#ifndef INKSCAPE_DISPLAY_FILTER_GAUSSIAN_BLUR_H +#define INKSCAPE_DISPLAY_FILTER_GAUSSIAN_BLUR_H #include #include @@ -35,9 +35,7 @@ #include <2geom/point.h> #include "display/dispatch-pool.h" -#include "display/drawing-access.h" #include "display/threading.h" -#include "util/fixed_point.h" namespace Inkscape::Filters { @@ -66,12 +64,12 @@ inline void copy_n(InIt beg_in, Size N, OutIt 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; -typedef double FIRValue; +using IIRValue = double; +using FIRValue = double; // 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 Util::FixedPoint FIRValue; +// using FIRValue = Util::FixedPoint; template static inline T sqr(T const &v) @@ -148,7 +146,7 @@ struct GaussianBlur Geom::Point _step; Geom::Point _deviation; - GaussianBlur(Geom::Point deviation, BlurQuality quality) + GaussianBlur(Geom::Point const &deviation, BlurQuality quality) : _quality(quality) , _step(1 << _effect_subsample_step_log2(deviation[Geom::X], _quality), 1 << _effect_subsample_step_log2(deviation[Geom::Y], _quality)) @@ -159,7 +157,7 @@ struct GaussianBlur * Get the downsampled size that the given destination should be at the given quality */ template - std::optional getDownsampleSize(Access surface) + std::optional getDownsampleSize(Access const &surface) { if (_step[Geom::X] > 1 || _step[Geom::Y] > 1) { return {{static_cast(ceil(static_cast(surface.width()) / _step[Geom::X])) + 1, @@ -229,7 +227,7 @@ private: * Request or set a color and allow the axis to be flipped. Is always alpha unpremultiplied. */ template - constexpr inline void colorAt(Access &surface, int x, int y, std::array &output) + constexpr inline void colorAt(Access &surface, int x, int y, std::array &output) const { if constexpr (axis == Geom::X) { surface.colorAt(x, y, output, false); @@ -448,7 +446,7 @@ private: } template - void _make_kernel(FIRValue *const kernel) + void _make_kernel(FIRValue *const kernel) const { int const scr_len = get_effect_area_scr()[axis]; if (scr_len < 0) { @@ -585,8 +583,8 @@ private: template static void calcTriggsSdikaInitialization(double const M[N * N], std::array const uold[N], - std::array const uplus, - std::array const vplus, IIRValue const alpha, + std::array const &uplus, + std::array const &vplus, IIRValue alpha, std::array vold[N]) { for (int c = 0; c < ChannelTotal; c++) { @@ -611,7 +609,7 @@ private: } // namespace Inkscape::Filters -#endif // INKSCAPE_DISPLAY_FILTER_DISPLACEMENT_MAP_H +#endif // INKSCAPE_DISPLAY_FILTER_GAUSSIAN_BLUR_H /* ;Local Variables: diff --git a/src/display/filters/light.h b/src/display/filters/light.h index 9a3ae92360..04a9a79ea1 100644 --- a/src/display/filters/light.h +++ b/src/display/filters/light.h @@ -19,18 +19,16 @@ #include #include <2geom/affine.h> -#include "display/drawing-access.h" - namespace Inkscape::Filters { -#define X_3D 0 -#define Y_3D 1 -#define Z_3D 2 +inline constexpr int X_3D = 0; +inline constexpr int Y_3D = 1; +inline constexpr int Z_3D = 2; -typedef std::array vector3d; +using vector3d = std::array; // The eye vector for specular lighting -static vector3d const EYE_VECTOR = {0.0, 0.0, 1.0}; +inline constexpr vector3d EYE_VECTOR = {0.0, 0.0, 1.0}; /** * returns the euclidean norm of the vector v @@ -93,11 +91,11 @@ void normalized_sum(vector3d &r, vector3d const &a, vector3d const &b) * \param trans a reference to a transformation matrix * \param device_scale an optional device scale for HiDPI */ -void convert_coord(vector3d coords, Geom::Affine const &trans, double device_scale) +void convert_coord(vector3d &coords, Geom::Affine const &trans, double device_scale) { - Geom::Point p = Geom::Point(coords[X_3D] * device_scale, coords[Y_3D] * device_scale) * trans; - coords[X_3D] = p[Geom::X]; - coords[Y_3D] = p[Geom::Y]; + Geom::Point p = Geom::Point(coords[X_3D], coords[Y_3D]) * device_scale * trans; + coords[X_3D] = p.x(); + coords[Y_3D] = p.y(); coords[Z_3D] *= device_scale * trans[0]; } @@ -257,12 +255,12 @@ struct DistantLight : public Lighting template void filter(AccessDst &dst, AccessSrc const &src) { - std::array lit_color; + typename AccessSrc::Color lit_color; // Conversion of color here for (auto i = 0; i < _color.size(); i++) { lit_color[i] = _color[i]; } - std::array output; + typename AccessDst::Color output; if (!_specular) { // Diffuse is alpha 1.0 output.back() = 1.0; } diff --git a/src/display/filters/morphology.h b/src/display/filters/morphology.h index 3cea1bb61e..e29af05d67 100644 --- a/src/display/filters/morphology.h +++ b/src/display/filters/morphology.h @@ -22,10 +22,9 @@ #include <2geom/point.h> #include "display/dispatch-pool.h" -#include "display/drawing-access.h" #include "display/threading.h" -static int const POOL_THRESHOLD = 2048; +inline constexpr int POOL_THRESHOLD = 2048; namespace Inkscape::Filters { diff --git a/src/display/filters/turbulence.h b/src/display/filters/turbulence.h index fcbac41a2d..6ef1fbcf61 100644 --- a/src/display/filters/turbulence.h +++ b/src/display/filters/turbulence.h @@ -26,8 +26,6 @@ #include <2geom/point.h> #include <2geom/rect.h> -#include "display/drawing-access.h" - namespace Inkscape::Filters { class Turbulence @@ -82,7 +80,7 @@ public: } template - void filter(AccessDst &dst, Geom::Affine trans, int x0, int y0) + void filter(AccessDst &dst, Geom::Affine const &trans, int x0, int y0) { if (!_ready) { init(); diff --git a/testfiles/src/colors/cms-transform-surface-test.cpp b/testfiles/src/colors/cms-transform-surface-test.cpp index 85d6f75e11..f057cfc504 100644 --- a/testfiles/src/colors/cms-transform-surface-test.cpp +++ b/testfiles/src/colors/cms-transform-surface-test.cpp @@ -7,7 +7,6 @@ * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ -#include #include #include "../test-utils.h" diff --git a/testfiles/src/display/drawing-access-test.cpp b/testfiles/src/display/drawing-access-test.cpp index 4e8ac36a36..dffd93dfba 100644 --- a/testfiles/src/display/drawing-access-test.cpp +++ b/testfiles/src/display/drawing-access-test.cpp @@ -375,8 +375,8 @@ TEST(DrawingAccessTest, multiSpanChannels) " &&", PatchMethod::ALPHA)); - ColorIs(*src._d, 0, 0, {1.0, 0.0, 0.0, 0.0, 1.0}, true); - ColorIs(*src._d, 20, 20, {0.0, 0.0, 0.0, 1.0, 1.0}, true); + ASSERT_TRUE(ColorIs(*src._d, 0, 0, {1.0, 0.0, 0.0, 0.0, 1.0}, true)); + ASSERT_TRUE(ColorIs(*src._d, 20, 20, {0.0, 0.0, 0.0, 1.0, 1.0}, true)); ASSERT_TRUE(ImageIs(*src._d, "22 " "224 " diff --git a/testfiles/src/display/filter-light-test.cpp b/testfiles/src/display/filter-light-test.cpp index d82d365363..0c2e8d4d3f 100644 --- a/testfiles/src/display/filter-light-test.cpp +++ b/testfiles/src/display/filter-light-test.cpp @@ -18,7 +18,7 @@ TEST(DrawingFilterTest, DiffuseDistantLight) , PatchMethod::LIGHT)); - // TODO: Add test for color commonent here + // TODO: Add test for color component here } TEST(DrawingFilterTest, SpecularDistantLight) diff --git a/testfiles/src/display/test-base.h b/testfiles/src/display/test-base.h index eb0488c8b0..212a0e032e 100644 --- a/testfiles/src/display/test-base.h +++ b/testfiles/src/display/test-base.h @@ -235,12 +235,12 @@ template */ std::string get_format_name(cairo_format_t format) { - static std::map map = { + static const std::map map = { {CAIRO_FORMAT_A8, "A8"}, {CAIRO_FORMAT_ARGB32, "ARGB32"}, {CAIRO_FORMAT_RGBA128F, "RGBA128F"}, }; - return map[format]; + return map.at(format); } template -- GitLab From 0fc6baf3c42fbc6d1afa5b9cdfe1e28804d83b8f Mon Sep 17 00:00:00 2001 From: Martin Owens Date: Thu, 4 Dec 2025 22:47:04 -0500 Subject: [PATCH 7/7] Second round of code review * Edge none is now -1 instead of _size * Save size for later use * Check for negatives in edge checking * Edge detect now a compile time optional * Adjust template variable names to lower_case * Return arrays in colorAt instead of retvar * Revert handcoded spacing in light.h * Remove unworkable code --- src/colors/cms/transform-surface.h | 20 +- src/display/drawing-access.h | 190 ++++++++++-------- src/display/filters/color-matrix.h | 3 +- src/display/filters/color-space.h | 14 +- src/display/filters/component-transfer.h | 12 +- src/display/filters/composite.h | 7 +- src/display/filters/convolve-matrix.h | 6 +- src/display/filters/displacement-map.h | 7 +- src/display/filters/gaussian-blur.h | 73 +++---- src/display/filters/light.h | 121 +++++++---- src/display/filters/morphology.h | 63 +----- src/display/filters/turbulence.h | 10 +- testfiles/src/display/drawing-access-test.cpp | 11 +- .../display/filter-displacement-map-test.cpp | 2 +- .../src/display/filter-gaussian-blur-test.cpp | 2 +- .../src/display/filter-morphology-test.cpp | 12 +- testfiles/src/display/test-base.h | 94 +++++---- 17 files changed, 315 insertions(+), 332 deletions(-) diff --git a/src/colors/cms/transform-surface.h b/src/colors/cms/transform-surface.h index c588bdfc9d..383bb93a55 100644 --- a/src/colors/cms/transform-surface.h +++ b/src/colors/cms/transform-surface.h @@ -20,7 +20,7 @@ namespace Inkscape::Colors::CMS { class Profile; -template +template class TransformSurface : public Transform { public: @@ -38,25 +38,25 @@ public: RenderingIntent intent = RenderingIntent::PERCEPTUAL, std::shared_ptr const &proof = nullptr, RenderingIntent proof_intent = RenderingIntent::AUTO) - requires(!GAMUTWARN || (std::is_same_v && std::is_same_v)) + requires(!gamut_warn || (std::is_same_v && std::is_same_v)) : Transform(proof ? cmsCreateProofingTransformTHR( cmsCreateContext(nullptr, nullptr), from->getHandle(), - lcms_color_format(from, sizeof(TIN), !std::is_integral_v, alpha_mode(PREMULT, true)), + lcms_color_format(from, sizeof(TypeIn), !std::is_integral_v, alpha_mode(premultiplied, true)), to->getHandle(), - lcms_color_format(to, sizeof(TOUT), !std::is_integral_v, alpha_mode(false, true)), + lcms_color_format(to, sizeof(TypeOut), !std::is_integral_v, alpha_mode(false, true)), proof->getHandle(), lcms_intent(intent), lcms_intent(proof_intent), - cmsFLAGS_COPY_ALPHA | cmsFLAGS_SOFTPROOFING | (GAMUTWARN ? cmsFLAGS_GAMUTCHECK : 0) | + cmsFLAGS_COPY_ALPHA | cmsFLAGS_SOFTPROOFING | (gamut_warn ? cmsFLAGS_GAMUTCHECK : 0) | lcms_bpc(proof_intent)) : cmsCreateTransformTHR( cmsCreateContext(nullptr, nullptr), from->getHandle(), - lcms_color_format(from, sizeof(TIN), !std::is_integral_v, alpha_mode(PREMULT, true)), + lcms_color_format(from, sizeof(TypeIn), !std::is_integral_v, alpha_mode(premultiplied, true)), to->getHandle(), - lcms_color_format(to, sizeof(TOUT), !std::is_integral_v, alpha_mode(false, true)), + lcms_color_format(to, sizeof(TypeOut), !std::is_integral_v, alpha_mode(false, true)), lcms_intent(intent), cmsFLAGS_COPY_ALPHA), false) - , _pixel_size_in((_channels_in + 1) * sizeof(TIN)) - , _pixel_size_out((_channels_out + 1) * sizeof(TOUT)) + , _pixel_size_in((_channels_in + 1) * sizeof(TypeIn)) + , _pixel_size_out((_channels_out + 1) * sizeof(TypeOut)) {} /** @@ -69,7 +69,7 @@ public: * @arg stride_in - The optional stride for the input image, if known to be uncontiguous. * @arg stride_out - The optional stride for the output image, if known to be uncontiguous. */ - void do_transform(int width, int height, TIN const *px_in, TOUT *px_out, int stride_in = 0, + void do_transform(int width, int height, TypeIn const *px_in, TypeOut *px_out, int stride_in = 0, int stride_out = 0) const { cmsDoTransformLineStride(_handle, px_in, px_out, width, height, stride_in ? stride_in : width * _pixel_size_in, diff --git a/src/display/drawing-access.h b/src/display/drawing-access.h index f454e4ea60..4b4a95e55e 100644 --- a/src/display/drawing-access.h +++ b/src/display/drawing-access.h @@ -53,37 +53,38 @@ enum class DrawingAccessEdgeMode /** * Image surface memory access for different types which can span multiple surfaces. * - * @template_arg Format - The cairo type this drawing access is for. - * @template_arg ChannelCount - The total number of channels in this format across all surfaces. - * @template_arg PrimaryOverride - Optionally override primary count for accessing non-cairo + * @template_arg format - The cairo type this drawing access is for. + * @template_arg channel_count - The total number of channels in this format across all surfaces. + * @template_arg primary_override - Optionally override primary count for accessing non-cairo * memory layouts. + * @template_arg check_edge - Set the edge checking */ -template +template class DrawingAccess { public: // Is the format an integer based format - constexpr static bool IsInteger = Format != CAIRO_FORMAT_RGBA128F; + constexpr static bool is_integer = format != CAIRO_FORMAT_RGBA128F; // How many primaries are there in this format - constexpr static int PrimaryCount = PrimaryOverride ? PrimaryOverride : (Format == CAIRO_FORMAT_A8 ? 0 : 3); - constexpr static int PrimaryTotal = PrimaryCount + 1; // Plus Alpha + constexpr static int primary_count = primary_override ? primary_override : (format == CAIRO_FORMAT_A8 ? 0 : 3); + constexpr static int primary_total = primary_count + 1; // Plus Alpha // The internal type used by each channel in the format - using PrimaryType = std::conditional_t; + using PrimaryType = std::conditional_t; // Scale of each primary to convert to a double used in Channels - constexpr static double PrimaryScale = IsInteger ? 255.0 : 1.0; + constexpr static double primary_scale = is_integer ? 255.0 : 1.0; // Position of the alpha primary in this format - constexpr static int PrimaryAlpha = IsInteger ? 0 : PrimaryCount; + constexpr static int primary_alpha = is_integer ? 0 : primary_count; // Does this DrawingAccess need two surfaces? - constexpr static bool HasMoreChannels = ChannelCount > PrimaryCount; + constexpr static bool has_more_channels = channel_count > primary_count; // Actual number of channels when including alpha - constexpr static int ChannelTotal = ChannelCount + 1; - using Color = std::array; + constexpr static int channel_total = channel_count + 1; + using Color = std::array; /** * Create a drawing access object for the given cairo surface. @@ -94,24 +95,25 @@ public: */ explicit DrawingAccess(Cairo::RefPtr cairo_surface, Cairo::RefPtr next_surface = {}) - requires(ChannelCount <= PrimaryCount * (HasMoreChannels + 1)) + requires(channel_count <= primary_count * (has_more_channels + 1)) : _width(cairo_surface->get_width()) , _height(cairo_surface->get_height()) , _stride(cairo_surface->get_stride() / sizeof(PrimaryType)) + , _size(_height * _stride) , _memory(reinterpret_cast(cairo_surface->get_data())) , _edge_mode(DrawingAccessEdgeMode::ERROR) , _cairo_surface(cairo_surface) , _next_surface(next_surface) { - if (cairo_image_surface_get_format(cairo_surface->cobj()) != Format) { - // throw std::exception("Format of the cairo surface doesn't match the DrawingAccess type."); + if (cairo_image_surface_get_format(cairo_surface->cobj()) != format) { + // throw std::exception("format of the cairo surface doesn't match the DrawingAccess type."); } - if constexpr (HasMoreChannels) { + if constexpr (has_more_channels) { _next_memory = reinterpret_cast(_next_surface->get_data()); if (_width != _next_surface->get_width() || _height != _next_surface->get_height() || _stride != _next_surface->get_stride() / sizeof(PrimaryType) || - cairo_image_surface_get_format(_next_surface->cobj()) != Format) { + cairo_image_surface_get_format(_next_surface->cobj()) != format) { // throw std::exception("Drawing Access Next Surface must be the same formats."); } } @@ -125,14 +127,18 @@ public: DrawingAccess(PrimaryType *memory, int width, int height) : _width(width) , _height(height) - , _stride(width * (PrimaryOverride + 1)) + , _stride(width * primary_total) + , _size(_height * _stride) , _memory(memory) {} /** * Set the edge mode for this drawing access object */ - void setEdgeMode(DrawingAccessEdgeMode mode) { _edge_mode = mode; } + void setEdgeMode(DrawingAccessEdgeMode mode) requires(check_edge) + { + _edge_mode = mode; + } /** * Get a color from the surface at the given coordinates. @@ -141,18 +147,20 @@ public: * @arg y - The pixel y coordinate to get * @arg unmultiply_alpha - Remove premultiplied alpha if true * - * @return_arg ret - The pre-sized memory for the returned color space including alpha. - * We use the same memory so we don't have to re-allocate for every pixel in a filter. + * @return - The pre-sized memory for the returned color space including alpha. + * We use the same memory so we don't have to re-allocate for every pixel in a filter. */ - inline void colorAt(int x, int y, Color &ret, bool unmultiply_alpha = false) const + inline Color colorAt(int x, int y, bool unmultiply_alpha = false) const { + Color ret; int pos = _pixel_pos(x, y); double alpha = _get_alpha(pos); double alpha_mult = unmultiply_alpha ? _mult(alpha) : 1.0; - for (int c = 0; c < ChannelCount; c++) { + for (int c = 0; c < channel_count; c++) { ret[c] = _get_channel(pos, c, alpha_mult); } - ret[ChannelCount] = alpha; + ret[channel_count] = alpha; + return ret; } /** @@ -166,23 +174,25 @@ public: * @return_arg - The pre-sized memory for the returned color space including alpha. * We use the same memory so we don't have to re-allocate for every pixel in a filter. */ - void colorAt(double x, double y, Color &ret, bool unmultiply_alpha = false) const + Color colorAt(double x, double y, bool unmultiply_alpha = false) const { int fx = floor(x), fy = floor(y); int cx = ceil(x), cy = ceil(y); double weight_x = x - fx, weight_y = y - fy; - for (int c = 0; c < ChannelTotal; c++) { + Color ret; + for (int c = 0; c < channel_total; c++) { ret[c] = _bilinear_interpolate( _get_channel(_pixel_pos(fx, fy), c, 1.0), _get_channel(_pixel_pos(cx, fy), c, 1.0), _get_channel(_pixel_pos(fx, cy), c, 1.0), _get_channel(_pixel_pos(cx, cy), c, 1.0), weight_x, weight_y); } if (unmultiply_alpha) { - auto alpha_mult = _mult(ret[ChannelCount]); - for (int c = 0; c < ChannelCount; c++) { + auto alpha_mult = _mult(ret[channel_count]); + for (int c = 0; c < channel_count; c++) { ret[c] *= alpha_mult; } } + return ret; } /** @@ -198,7 +208,7 @@ public: */ void colorTo(int x, int y, Color const &values, bool unmultiply_alpha = false) { - _set_primaries_recursively(_pixel_pos(x, y), values[ChannelCount], values, unmultiply_alpha); + _set_primaries_recursively(_pixel_pos(x, y), values[channel_count], values, unmultiply_alpha); } /** @@ -238,18 +248,18 @@ public: /** * Get the number of output channels minus alpha */ - static int getOutputChannels() { return ChannelCount; } + static int getOutputChannels() { return channel_count; } /** * Get access to the memory directly */ PrimaryType *memory() - requires(!HasMoreChannels) + requires(!has_more_channels) { return _memory; } PrimaryType const *memory() const - requires(!HasMoreChannels) + requires(!has_more_channels) { return _memory; } @@ -261,14 +271,14 @@ public: std::vector contiguousCopy(bool unpremultiply_alpha = false) { std::vector memory; - memory.reserve(ChannelTotal * _width * _height); + memory.reserve(_width * _height * channel_total); - for (int pos = 0; pos < _height * _width * PrimaryTotal; pos += PrimaryTotal) { + for (int pos = 0; pos < _size; pos += primary_total) { double alpha_mult = unpremultiply_alpha ? _mult(_get_alpha(pos)) : 1.0; - for (int c = 0; c < ChannelCount; c++) { + for (int c = 0; c < channel_count; c++) { memory.emplace_back(_get_channel(pos, c, alpha_mult)); } - memory.emplace_back(_get_channel(pos, ChannelCount, 1.0)); + memory.emplace_back(_get_channel(pos, channel_count, 1.0)); } return memory; } @@ -289,29 +299,30 @@ private: inline void _set_primaries_recursively(int pos, double alpha, Color const &values, bool unmultiply_alpha, int offset = 0) { - if (pos < _height * _stride) { - // Set alpha in the surface - _memory[pos + _primary_pos(PrimaryAlpha)] = alpha * PrimaryScale; - auto mult = unmultiply_alpha ? alpha : 1.0; - - // Set the primaries in the surface - for (int p = 0; p < PrimaryTotal && offset < values.size(); p++) { - if (p != PrimaryAlpha) { - _memory[pos + _primary_pos(p)] = values[offset] * mult * PrimaryScale; - offset++; - } + if (_edge_check(pos)) { + return; + } + // Set alpha in the surface + _memory[pos + _primary_pos(primary_alpha)] = alpha * primary_scale; + auto mult = unmultiply_alpha ? alpha : 1.0; + + // Set the primaries in the surface + for (int p = 0; p < primary_total && offset < values.size(); p++) { + if (p != primary_alpha) { + _memory[pos + _primary_pos(p)] = values[offset] * mult * primary_scale; + offset++; } + } - // If we have more channels, keep setting them - if constexpr (HasMoreChannels) { - // Alpha is always set in every surface - _next_memory[pos + _primary_pos(PrimaryAlpha)] = alpha * PrimaryScale; + // If we have more channels, keep setting them + if constexpr (has_more_channels) { + // Alpha is always set in every surface + _next_memory[pos + _primary_pos(primary_alpha)] = alpha * primary_scale; - for (int p = 0; p < PrimaryTotal && offset < values.size(); p++) { - if (p != PrimaryAlpha) { - _next_memory[pos + _primary_pos(p)] = values[offset] * mult * PrimaryScale; - offset++; - } + for (int p = 0; p < primary_total && offset < values.size(); p++) { + if (p != primary_alpha) { + _next_memory[pos + _primary_pos(p)] = values[offset] * mult * primary_scale; + offset++; } } } @@ -334,17 +345,17 @@ private: ? (std::is_integral_v ? std::numeric_limits::max() / std::numeric_limits::max() // T=char T0=int|char : std::numeric_limits::max()) // T=float T0=int|char - : (T0)(1.0 / PrimaryScale); // T=float|char T0=float|double + : (T0)(1.0 / primary_scale); // T=float|char T0=float|double - if (pos >= _height * _stride) { + if (_edge_check(pos)) { return 0.0; } - if constexpr (HasMoreChannels) { - if (channel >= PrimaryCount && channel != ChannelCount) { - return _next_memory[pos + _primary_pos(channel - PrimaryCount + IsInteger)] * scale * alpha_mult; + if constexpr (has_more_channels) { + if (channel >= primary_count && channel != channel_count) { + return _next_memory[pos + _primary_pos(channel - primary_count + is_integer)] * scale * alpha_mult; } } - return _memory[pos + _primary_pos(channel < ChannelCount ? channel + IsInteger : PrimaryAlpha)] * scale * + return _memory[pos + _primary_pos(channel < channel_count ? channel + is_integer : primary_alpha)] * scale * alpha_mult; } @@ -353,7 +364,17 @@ private: */ inline double _get_alpha(int pos) const { - return pos >= _height * _stride ? 0.0 : _memory[pos + _primary_pos(PrimaryAlpha)] / PrimaryScale; + return _edge_check(pos) ? 0.0 : _memory[pos + _primary_pos(primary_alpha)] / primary_scale; + } + + /** + * Return true if pos is off the edge of the surface. Compiled out when not needed. + */ + inline bool _edge_check(int pos) const { + if constexpr(check_edge) { + return pos < 0 || pos >= _size; + } + return false; } /** @@ -366,26 +387,28 @@ private: */ inline int _pixel_pos(int x, int y) const { - if (x < 0 || y < 0 || x >= _width || y >= _height) { - switch (_edge_mode) { - case DrawingAccessEdgeMode::EXTEND: - x = std::clamp(x, (int)0, _width - 1); - y = std::clamp(y, (int)0, _height - 1); - break; - case DrawingAccessEdgeMode::WRAP: - x = Util::safemod(x, _width); - y = Util::safemod(y, _height); - break; - case DrawingAccessEdgeMode::ZERO: - // This means OOB to _get_channel, which will return zero - return _height * _stride + 1; - case DrawingAccessEdgeMode::ERROR: - default: - throw std::exception(); - break; + if constexpr (check_edge) { + if (x < 0 || y < 0 || x >= _width || y >= _height) { + switch (_edge_mode) { + case DrawingAccessEdgeMode::EXTEND: + x = std::clamp(x, (int)0, _width - 1); + y = std::clamp(y, (int)0, _height - 1); + break; + case DrawingAccessEdgeMode::WRAP: + x = Util::safemod(x, _width); + y = Util::safemod(y, _height); + break; + case DrawingAccessEdgeMode::ZERO: + // This means OOB to _get_channel, which will return zero + return -1; + case DrawingAccessEdgeMode::ERROR: + default: + throw std::exception(); + break; + } } } - return y * _stride + x * PrimaryTotal; + return y * _stride + x * primary_total; } /** @@ -395,7 +418,7 @@ private: static inline int _primary_pos(int p) { if constexpr (G_BYTE_ORDER == G_LITTLE_ENDIAN) { - return IsInteger ? PrimaryCount - p : p; + return is_integer ? primary_count - p : p; } else { return p; } @@ -415,6 +438,7 @@ private: int const _width; int const _height; int const _stride; + int const _size; PrimaryType *_memory{}; // How are out of range x,y coordinates treated diff --git a/src/display/filters/color-matrix.h b/src/display/filters/color-matrix.h index 70fe9c0e60..f9abf69c9d 100644 --- a/src/display/filters/color-matrix.h +++ b/src/display/filters/color-matrix.h @@ -37,13 +37,12 @@ struct ColorMatrix template void filter(AccessDst &dst, AccessSrc const &src) { - typename AccessDst::Color c; typename AccessSrc::Color o; for (int y = 0; y < dst.height(); y++) { for (int x = 0; x < dst.width(); x++) { std::fill(o.begin(), o.end(), 0); - src.colorAt(x, y, c, true); + auto c = src.colorAt(x, y, true); for (unsigned i = 0; i < c.size(); i++) { if (i < _width - 1) { // Matrix is allowed to be smaller diff --git a/src/display/filters/color-space.h b/src/display/filters/color-space.h index b8f5732863..70033cec5d 100644 --- a/src/display/filters/color-space.h +++ b/src/display/filters/color-space.h @@ -25,8 +25,8 @@ template struct ColorSpaceTransform { /// Whether a direct conversion might be allowed. - static constexpr bool allow_direct = AccessDst::ChannelTotal == AccessSrc::ChannelTotal && - !AccessDst::HasMoreChannels && !AccessSrc::HasMoreChannels; + static constexpr bool allow_direct = AccessDst::channel_total == AccessSrc::channel_total && + !AccessDst::has_more_channels && !AccessSrc::has_more_channels; // We expect to get transfer functions in the correct order for the input color space ColorSpaceTransform(std::shared_ptr from, std::shared_ptr to) @@ -43,9 +43,6 @@ struct ColorSpaceTransform void filter(AccessDst &dst, AccessSrc const &src) { - typename AccessSrc::Color c0; - typename AccessDst::Color c1; - if constexpr (allow_direct) { if (_transform) { _transform->do_transform(dst.width(), dst.height(), src.memory(), dst.memory()); @@ -53,7 +50,7 @@ struct ColorSpaceTransform // lcms2 transforms always returns unpremultiplied values, premultiply now for (int y = 0; y < dst.height(); y++) { for (int x = 0; x < dst.width(); x++) { - dst.colorAt(x, y, c1, true); + auto c1 = dst.colorAt(x, y, true); for (int i = 0; i < c1.size() - 1; i++) { c1[i] *= c1.back(); } @@ -66,11 +63,12 @@ struct ColorSpaceTransform } // Manual - std::vector cin(c0.size(), 0.0); + typename AccessDst::Color c1; + std::vector cin(c1.size(), 0.0); for (int y = 0; y < dst.height(); y++) { for (int x = 0; x < dst.width(); x++) { // Conversions in inkscape are always alpha unmultiplied - src.colorAt(x, y, c0, true); + auto c0 = src.colorAt(x, y, true); // Expensive conversion, array to vector and back cin.assign(c0.begin(), c0.end()); _from->convert(cin, _to); diff --git a/src/display/filters/component-transfer.h b/src/display/filters/component-transfer.h index cb166c8e5d..8cd70a5d5b 100644 --- a/src/display/filters/component-transfer.h +++ b/src/display/filters/component-transfer.h @@ -29,7 +29,6 @@ enum class TransferType ERROR }; -// A visit pattern was tested and found to be too slow struct TransferFunction { TransferType _type; @@ -81,20 +80,19 @@ struct ComponentTransfer template void filter(AccessDst &dst, AccessSrc const &src) { - typename AccessDst::Color c; for (int y = 0; y < dst.height(); y++) { for (int x = 0; x < dst.width(); x++) { - src.colorAt(x, y, c, true); - filterColor(c); + auto c = src.colorAt(x, y, true); + filterColor(c); dst.colorTo(x, y, c, true); } } } - template - inline void filterColor(std::array &c) + template + inline void filterColor(std::array &c) { - auto const num_i = std::min(ChannelTotal, _functions.size()); + auto const num_i = std::min(channel_total, _functions.size()); for (unsigned i = 0; i < num_i; i++) { auto &f = _functions[i]; switch (f._type) { diff --git a/src/display/filters/composite.h b/src/display/filters/composite.h index ec348c9ef5..e2e9fc8735 100644 --- a/src/display/filters/composite.h +++ b/src/display/filters/composite.h @@ -33,13 +33,10 @@ struct CompositeArithmetic void filter(AccessDst &dst, AccessSrc const &src) { // TODO: Detect different color spaces and convert - typename AccessDst::Color c1; - typename AccessSrc::Color c2; - for (int y = 0; y < dst.height(); y++) { for (int x = 0; x < dst.width(); x++) { - dst.colorAt(x, y, c1, true); - src.colorAt(x, y, c2, true); + auto c1 = dst.colorAt(x, y, true); + auto c2 = src.colorAt(x, y, true); for (unsigned i = 0; i < c1.size() - 1 && i < c2.size() - 1; i++) { c1[i] = std::clamp(_k1 * c1[i] * c2[i] + _k2 * c1[i] + _k3 * c2[i] + _k4, 0.0, 1.0); } diff --git a/src/display/filters/convolve-matrix.h b/src/display/filters/convolve-matrix.h index be563e60ce..f83b31b37e 100644 --- a/src/display/filters/convolve-matrix.h +++ b/src/display/filters/convolve-matrix.h @@ -43,7 +43,7 @@ struct ConvolveMatrix { unsigned alpha = dst.getOutputChannels() + (_preserve_alpha ? 0 : 1); typename AccessDst::Color output; - std::vector patch(_kernel.size(), output); + std::vector patch(_kernel.size()); auto width = dst.width(); auto height = dst.height(); @@ -55,7 +55,7 @@ struct ConvolveMatrix for (int j = 0; j < last_line; j++) { for (int i = 0; i < _orderX; i++) { // It's ok to ask for negative coordinates, they use EdgeMode set in src - src.colorAt(x + i - _targetX, j - _targetY, patch[i + j * _orderX], true); + patch[i + j * _orderX] = src.colorAt(x + i - _targetX, j - _targetY, true); } } @@ -71,7 +71,7 @@ struct ConvolveMatrix for (int i = 0; i < _orderX; i++) { // May read beyond height and width, result depends on EdgeMode in src - src.colorAt(x + i - _targetX, y + read_pos, patch[read_line * _orderX + i], true); + patch[read_line * _orderX + i] = src.colorAt(x + i - _targetX, y + read_pos, true); for (int j = 0; j < _orderY; j++) { double coeff = _kernel[j * _orderX + i]; auto pos = ((j + offset) * _orderX + i) % patch.size(); diff --git a/src/display/filters/displacement-map.h b/src/display/filters/displacement-map.h index 06f3d39ada..583670715a 100644 --- a/src/display/filters/displacement-map.h +++ b/src/display/filters/displacement-map.h @@ -27,16 +27,13 @@ struct DisplacementMap template void filter(AccessDst &dst, AccessTexture const &texture, AccessMap const &map) { - typename AccessTexture::Color output; - typename AccessMap::Color mappx; - for (int y = 0; y < dst.height(); y++) { for (int x = 0; x < dst.width(); x++) { - map.colorAt(x, y, mappx, true); + auto mappx = map.colorAt(x, y, true); // This is allowed to request out of bounds; You should // set the texture's edgeMode to ZERO for SVG spec - texture.colorAt(x + _scalex * (mappx[_xch] - 0.5), y + _scaley * (mappx[_ych] - 0.5), output, true); + auto output = texture.colorAt(x + _scalex * (mappx[_xch] - 0.5), y + _scaley * (mappx[_ych] - 0.5), true); // TODO: convert color space of output to dst dst.colorTo(x, y, output, true); diff --git a/src/display/filters/gaussian-blur.h b/src/display/filters/gaussian-blur.h index 91574bbdf5..1b8904ca00 100644 --- a/src/display/filters/gaussian-blur.h +++ b/src/display/filters/gaussian-blur.h @@ -184,11 +184,11 @@ struct GaussianBlur // Temporary storage for IIR filter // NOTE: This can be eliminated, but it reduces the precision a bit int threads = pool->size(); - std::vector *> tmpdata(threads, nullptr); + std::vector *> tmpdata(threads, nullptr); if (use_IIR_x || use_IIR_y) { for (int i = 0; i < threads; ++i) { tmpdata[i] = - new std::array[std::max(surface.width(), surface.height())]; + new std::array[std::max(surface.width(), surface.height())]; } } @@ -227,17 +227,16 @@ private: * Request or set a color and allow the axis to be flipped. Is always alpha unpremultiplied. */ template - constexpr inline void colorAt(Access &surface, int x, int y, std::array &output) const + constexpr inline std::array colorAt(Access &surface, int x, int y) const { if constexpr (axis == Geom::X) { - surface.colorAt(x, y, output, false); - } else { - surface.colorAt(y, x, output, false); + return surface.colorAt(x, y, false); } + return surface.colorAt(y, x, false); } template constexpr inline void colorTo(Access &surface, int x, int y, - std::array const &input) + std::array const &input) { if constexpr (axis == Geom::X) { surface.colorTo(x, y, input, false); @@ -247,7 +246,7 @@ private: } template - void gaussian_pass_IIR(Access &surface, std::array **tmpdata, dispatch_pool &pool) + void gaussian_pass_IIR(Access &surface, std::array **tmpdata, dispatch_pool &pool) { // Filter variables IIRValue b[N + 1]; // scaling coefficient + filter coefficients (can be 10.21 fixed point) @@ -274,15 +273,13 @@ private: pool.dispatch(row_count, [&](int row, int tid) { // Border constants - std::array imin; - std::array imax; - colorAt(surface, 0, row, imin); - colorAt(surface, col_count - 1, row, imax); + auto imin = colorAt(surface, 0, row); + auto imax = colorAt(surface, col_count - 1, row); int col_write = col_count; // Forward pass - std::array u[N + 1]; + std::array u[N + 1]; for (int i = 0; i < N; i++) { u[i] = imin; } @@ -291,12 +288,12 @@ private: for (int i = N; i > 0; i--) { u[i] = u[i - 1]; } - colorAt(surface, col, row, u[0]); - for (int c = 0; c < Access::ChannelTotal; c++) { + u[0] = colorAt(surface, col, row); + for (int c = 0; c < Access::channel_total; c++) { u[0][c] *= b[0]; } for (int i = 1; i < N + 1; i++) { - for (int c = 0; c < Access::ChannelTotal; c++) { + for (int c = 0; c < Access::channel_total; c++) { u[0][c] += u[i][c] * b[i]; } } @@ -304,8 +301,8 @@ private: } // Backward pass - std::array v[N + 1]; - calcTriggsSdikaInitialization(M, u, imax, imax, b[0], v); + std::array v[N + 1]; + calcTriggsSdikaInitialization(M, u, imax, imax, b[0], v); colorTo(surface, --col_write, row, v[0]); @@ -315,11 +312,11 @@ private: } v[0] = *(tmpdata[tid] + col); - for (int c = 0; c < Access::ChannelTotal; c++) { + for (int c = 0; c < Access::channel_total; c++) { v[0][c] *= b[0]; } for (int i = 1; i < N + 1; i++) { - for (int c = 0; c < Access::ChannelTotal; c++) { + for (int c = 0; c < Access::channel_total; c++) { v[0][c] += v[i][c] * b[i]; } } @@ -352,17 +349,15 @@ private: // Kernel should have scr_len+1 elements pool.dispatch(row_count, [&](int row, int tid) { // Past pixels seen (to enable in-place operation) - boost::container::small_vector, 10> history(scr_len + 1); + boost::container::small_vector, 10> history(scr_len + 1); - std::array skipbuf; - std::array px_in; - std::array px_out; - std::array px_cmp; + std::array skipbuf; + std::array px_out; + std::array px_cmp; skipbuf.fill(INT_MIN); // history initialization - std::array imin; - colorAt(surface, 0, row, imin); + auto imin = colorAt(surface, 0, row); for (int i = 0; i < scr_len; i++) { history[i] = imin; } @@ -372,10 +367,10 @@ private: for (int i = scr_len; i > 0; i--) { history[i] = history[i - 1]; } - colorAt(surface, col, row, history[0]); + history[0] = colorAt(surface, col, row); // for all bytes of the pixel - for (unsigned int byte = 0; byte < Access::ChannelTotal; byte++) { + for (unsigned int byte = 0; byte < Access::channel_total; byte++) { if (skipbuf[byte] > col) continue; @@ -406,7 +401,7 @@ private: } // value at the pixel - colorAt(surface, col_in, row, px_in); + auto px_in = colorAt(surface, col_in, row); // is it the same as last one we saw? if (px_in[byte] != last_in) @@ -425,13 +420,11 @@ private: // 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 - // This is HORRIFYING, but is needed to maintain the same logic as the previous code - // the truth is this might not be even possible to do any more. int pos = 0; while (col + pos + scr_len < col_count - 1 && (!pos || px_cmp[byte] == last_in)) { - colorAt(surface, col + pos + scr_len, row, px_cmp); + px_cmp = colorAt(surface, col + pos + scr_len, row); if (pos) { - colorAt(surface, col + pos, row, px_cmp); + px_cmp = colorAt(surface, col + pos, row); px_cmp[byte] = last_in; colorTo(surface, col + pos, row, px_cmp); } @@ -581,13 +574,13 @@ private: M[i] *= Mscale; } - template - static void calcTriggsSdikaInitialization(double const M[N * N], std::array const uold[N], - std::array const &uplus, - std::array const &vplus, IIRValue alpha, - std::array vold[N]) + template + static void calcTriggsSdikaInitialization(double const M[N * N], std::array const uold[N], + std::array const &uplus, + std::array const &vplus, IIRValue alpha, + std::array vold[N]) { - for (int c = 0; c < ChannelTotal; c++) { + for (int c = 0; c < channel_total; c++) { double uminp[N]; for (unsigned int i = 0; i < N; i++) uminp[i] = uold[i][c] - uplus[c]; diff --git a/src/display/filters/light.h b/src/display/filters/light.h index 04a9a79ea1..ef20bce8fe 100644 --- a/src/display/filters/light.h +++ b/src/display/filters/light.h @@ -114,8 +114,8 @@ struct Lighting protected: template void doLighting(AccessSrc const &src, int x, int y, vector3d light, - std::array const &color, - std::array &output) + std::array const &color, + std::array &output) { if (_specular) { normalized_sum(light, light, EYE_VECTOR); @@ -146,27 +146,41 @@ protected: // upper left corner fx *= (2.0 / 3.0); fy *= (2.0 / 3.0); - double p00 = src.alphaAt(x, y), p10 = src.alphaAt(x + 1, y), p01 = src.alphaAt(x, y + 1), - p11 = src.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; + double p00 = src.alphaAt(x, y), p10 = src.alphaAt(x + 1, y), + p01 = src.alphaAt(x, y + 1), p11 = src.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 (y == (src.height() - 1)) [[unlikely]] { // lower left corner fx *= (2.0 / 3.0); fy *= (2.0 / 3.0); - double p00 = src.alphaAt(x, y - 1), p10 = src.alphaAt(x + 1, y - 1), p01 = src.alphaAt(x, y), - p11 = src.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; + double p00 = src.alphaAt(x, y - 1), p10 = src.alphaAt(x + 1, y - 1), + p01 = src.alphaAt(x, y ), p11 = src.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 = src.alphaAt(x, y - 1), p10 = src.alphaAt(x + 1, y - 1), p01 = src.alphaAt(x, y), - p11 = src.alphaAt(x + 1, y), p02 = src.alphaAt(x, y + 1), p12 = src.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; + double p00 = src.alphaAt(x, y - 1), p10 = src.alphaAt(x + 1, y - 1), + p01 = src.alphaAt(x, y ), p11 = src.alphaAt(x + 1, y), + p02 = src.alphaAt(x, y + 1), p12 = src.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 (x == (src.width() - 1)) [[unlikely]] { // rightmost column @@ -174,26 +188,41 @@ protected: // top right corner fx *= (2.0 / 3.0); fy *= (2.0 / 3.0); - double p00 = src.alphaAt(x - 1, y), p10 = src.alphaAt(x, y), p01 = src.alphaAt(x - 1, y + 1), - p11 = src.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; + double p00 = src.alphaAt(x - 1, y ), p10 = src.alphaAt(x, y), + p01 = src.alphaAt(x - 1, y + 1), p11 = src.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 (y == (src.height() - 1)) [[unlikely]] { // bottom right corner fx *= (2.0 / 3.0); fy *= (2.0 / 3.0); - double p00 = src.alphaAt(x - 1, y - 1), p10 = src.alphaAt(x, y - 1), p01 = src.alphaAt(x - 1, y), - p11 = src.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; + double p00 = src.alphaAt(x - 1, y - 1), p10 = src.alphaAt(x, y - 1), + p01 = src.alphaAt(x - 1, y ), p11 = src.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 = src.alphaAt(x - 1, y - 1), p10 = src.alphaAt(x, y - 1), p01 = src.alphaAt(x - 1, y), - p11 = src.alphaAt(x, y), p02 = src.alphaAt(x - 1, y + 1), p12 = src.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; + double p00 = src.alphaAt(x - 1, y - 1), p10 = src.alphaAt(x, y - 1), + p01 = src.alphaAt(x - 1, y ), p11 = src.alphaAt(x, y), + p02 = src.alphaAt(x - 1, y + 1), p12 = src.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 @@ -201,30 +230,42 @@ protected: // top row fx *= (1.0 / 3.0); fy *= (1.0 / 2.0); - double p00 = src.alphaAt(x - 1, y), p10 = src.alphaAt(x, y), p20 = src.alphaAt(x + 1, y), + double p00 = src.alphaAt(x - 1, y ), p10 = src.alphaAt(x, y ), p20 = src.alphaAt(x + 1, y ), p01 = src.alphaAt(x - 1, y + 1), p11 = src.alphaAt(x, y + 1), p21 = src.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; + 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 (y == (src.height() - 1)) [[unlikely]] { // bottom row fx *= (1.0 / 3.0); fy *= (1.0 / 2.0); double p00 = src.alphaAt(x - 1, y - 1), p10 = src.alphaAt(x, y - 1), p20 = src.alphaAt(x + 1, y - 1), - p01 = src.alphaAt(x - 1, y), p11 = src.alphaAt(x, y), p21 = src.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; + p01 = src.alphaAt(x - 1, y ), p11 = src.alphaAt(x, y ), p21 = src.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 = src.alphaAt(x - 1, y - 1), p10 = src.alphaAt(x, y - 1), p20 = src.alphaAt(x + 1, y - 1), - p01 = src.alphaAt(x - 1, y), p11 = 0.0, p21 = src.alphaAt(x + 1, y), + p01 = src.alphaAt(x - 1, y ), p11 = 0.0, p21 = src.alphaAt(x + 1, y ), p02 = src.alphaAt(x - 1, y + 1), p12 = src.alphaAt(x, y + 1), p22 = src.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] = + -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; @@ -302,12 +343,12 @@ struct PointLight : public Lighting template void filter(AccessDst &dst, AccessSrc const &src) { - std::array lit_color; + std::array lit_color; // Conversion of color here for (auto i = 0; i < _color.size(); i++) { lit_color[i] = _color[i]; } - std::array output; + std::array output; if (!_specular) { // Diffuse is alpha 1.0 output.back() = 1.0; } diff --git a/src/display/filters/morphology.h b/src/display/filters/morphology.h index e29af05d67..a825dcf23d 100644 --- a/src/display/filters/morphology.h +++ b/src/display/filters/morphology.h @@ -83,11 +83,10 @@ struct Morphology // allocate it once for all threads and retrieving the correct set based // on the thread id. std::vector>> vals(channels); - typename AccessSrc::Color input; typename AccessDst::Color output; // Initialize with transparent black - for (int p = 0; p < AccessSrc::ChannelTotal; ++p) { + for (int p = 0; p < AccessSrc::channel_total; ++p) { vals[p].emplace_back(-1, 0); // TODO: Only do this when performing an erosion? } int in_x = 0; @@ -95,7 +94,7 @@ struct Morphology for (int j = 0; j < w + ri; ++j) { for (int p = 0; p < channels; ++p) { - src.colorAt(axis == Geom::Y ? in_x : i, axis == Geom::Y ? i : in_x, input); + auto input = src.colorAt(axis == Geom::Y ? in_x : i, axis == Geom::Y ? i : in_x); // 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) @@ -122,64 +121,6 @@ struct Morphology out_x++; } } - - /* This is supposed to be quicker, but I couldn't get it to work: - - for (int j = 0; j < std::min(ri, w); ++j) { - src.colorAt(axis == Geom::Y ? in_x : i, axis == Geom::Y ? i : in_x, input); - for(int p = 0; p < channels; ++p) { - // 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, input[p])) { - vals[p].pop_back(); // useless - } - vals[p].emplace_back(j + wi, input[p]); - } - in_x++; - } - - // 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) { - src.colorAt(axis == Geom::Y ? in_x : i, axis == Geom::Y ? in_x : i, input); - for(int p = 0; p < channels; ++p) { - // 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, input[p])) { - vals[p].pop_back(); // useless - } - vals[p].emplace_back(j + wi, input[p]); - output[p] = vals[p].front().second; - } - dst.colorTo(axis == Geom::Y ? out_x : i, axis == Geom::Y ? i : out_x, output); - in_x++; - out_x++; - } - // 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 < channels; ++p) { - 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 < channels; ++p) { - // Remove out-of-range values - if (!vals[p].empty() && vals[p].front().first <= j) { - vals[p].pop_front(); // out-of-range - } - output[p] = vals[p].front().second; - } - dst.colorTo(axis == Geom::Y ? out_x : i, axis == Geom::Y ? i : out_x, output); - out_x++; - } - */ }); } }; diff --git a/src/display/filters/turbulence.h b/src/display/filters/turbulence.h index 6ef1fbcf61..b379017f10 100644 --- a/src/display/filters/turbulence.h +++ b/src/display/filters/turbulence.h @@ -92,7 +92,7 @@ public: for (int x = 0; x < dst.width(); x++) { // transform is added now to keep randomness the same regardless // of how the surface may have been transformed. - turbulencePixel(Geom::Point(x + x0, y + y0) * trans, output); + turbulencePixel(Geom::Point(x + x0, y + y0) * trans, output); dst.colorTo(x, y, output, true); } } @@ -168,8 +168,8 @@ public: _ready = true; } - template - inline void turbulencePixel(Geom::Point const &point, std::array &output) const + template + inline void turbulencePixel(Geom::Point const &point, std::array &output) const { std::fill(output.begin(), output.end(), 0.0); int wrapx = _wrapx, wrapy = _wrapy, wrapw = _wrapw, wraph = _wraph; @@ -218,7 +218,7 @@ public: auto const *qxb = _gradient[b10]; auto const *qya = _gradient[b01]; auto const *qyb = _gradient[b11]; - for (int k = 0; k < ChannelTotal; ++k) { + for (int k = 0; k < channel_total; ++k) { double a = _lerp(sx, rx0 * qxa[0][k] + ry0 * qxa[1][k], rx1 * qxb[0][k] + ry0 * qxb[1][k]); double b = _lerp(sx, rx0 * qya[0][k] + ry1 * qya[1][k], rx1 * qyb[0][k] + ry1 * qyb[1][k]); double r = _lerp(sy, a, b); @@ -239,7 +239,7 @@ public: } } - for (auto i = 0; i < ChannelTotal; i++) { + for (auto i = 0; i < channel_total; i++) { if (_fractalnoise) { output[i] += 1; output[i] /= 2; diff --git a/testfiles/src/display/drawing-access-test.cpp b/testfiles/src/display/drawing-access-test.cpp index dffd93dfba..28d6a191d5 100644 --- a/testfiles/src/display/drawing-access-test.cpp +++ b/testfiles/src/display/drawing-access-test.cpp @@ -15,11 +15,10 @@ struct TestFilter template void filter(AccessDst &dst, AccessSrc const &src) { - typename AccessSrc::Color c; for (int y = 0; y < dst.height(); y++) { for (int x = 0; x < dst.width(); x++) { if (x / 3 == y / 3) { - src.colorAt(x, y, c); + auto c = src.colorAt(x, y); dst.colorTo(x, y, c); } } @@ -182,9 +181,9 @@ TEST(DrawingAccessTest, UnmultiplyColor) ASSERT_TRUE(ColorWillBe(*src2._d, 3, 3, {0.5, 0.5, 0.5, 0.5, 0.5}, true)); } -DrawingAccess getEdgeModeSurface(int width, int height) +DrawingAccess getEdgeModeSurface(int width, int height) { - auto src = TestSurface<4>(width, height); + auto src = TestSurface<4, true>(width, height); // Draw a box of 4 channels, one edge per channel, overlaps at the corners for (int x = 0; x < (int)width; x++) { @@ -203,7 +202,7 @@ TEST(DrawingAccessTest, EdgeModeError) for (int x = -1; x < 5; x++) { for (int y = -1; y < 5; y++) { if (x < 0 || x > 3 || y < 0 || y > 3) { - EXPECT_THROW(d.colorAt(x, y, c), std::exception); + EXPECT_THROW(d.colorAt(x, y), std::exception); EXPECT_THROW(d.colorTo(x, y, c), std::exception); } else { // Checking for no error, and confirming the test-suite @@ -464,7 +463,7 @@ void TestcontiguousCopy(std::array in, std::array cmp, bool un { int w = 21; int h = 21; - auto src = TestSurface<4, F>(w, h); + auto src = TestSurface<4, false, F>(w, h); src.rect(6, 6, 9, 9, in); std::vector copy = src._d->template contiguousCopy(unpre); diff --git a/testfiles/src/display/filter-displacement-map-test.cpp b/testfiles/src/display/filter-displacement-map-test.cpp index 1134a0f53b..9b12f51cca 100644 --- a/testfiles/src/display/filter-displacement-map-test.cpp +++ b/testfiles/src/display/filter-displacement-map-test.cpp @@ -7,7 +7,7 @@ using namespace Inkscape::Filters; TEST(DrawingFilterTest, DisplacementMap) { - auto texture = TestSurface<4>(21, 21); + auto texture = TestSurface<4, true>(21, 21); texture.rect(3, 3, 15, 15, {0.5, 0.0, 0.0, 1.0, 1.0}); texture._d->setEdgeMode(DrawingAccessEdgeMode::ZERO); diff --git a/testfiles/src/display/filter-gaussian-blur-test.cpp b/testfiles/src/display/filter-gaussian-blur-test.cpp index 7dc03a03ad..c5fa526e16 100644 --- a/testfiles/src/display/filter-gaussian-blur-test.cpp +++ b/testfiles/src/display/filter-gaussian-blur-test.cpp @@ -34,7 +34,7 @@ TEST(DrawingFilterTest, GaussianBlurIIR) " ..... " ".:+=+:." ".+O*O+." - ".=*x*=." + ".=*X*=." ".+O*O+." ".:+=+:." " ..... ", diff --git a/testfiles/src/display/filter-morphology-test.cpp b/testfiles/src/display/filter-morphology-test.cpp index b04a581e40..e7597add53 100644 --- a/testfiles/src/display/filter-morphology-test.cpp +++ b/testfiles/src/display/filter-morphology-test.cpp @@ -7,9 +7,9 @@ using namespace Inkscape::Filters; TEST(DrawingFilterTest, MorphologyErode) { - auto src = TestSurface<4>(21, 21); - auto mid = TestSurface<4>(21, 21); - auto dst = TestSurface<4>(21, 21); + auto src = TestSurface<4, true>(21, 21); + auto mid = TestSurface<4, true>(21, 21); + auto dst = TestSurface<4, true>(21, 21); src.rect(3, 3, 15, 15, {0.5, 0.0, 0.0, 1.0, 1.0}); @@ -31,9 +31,9 @@ TEST(DrawingFilterTest, MorphologyErode) TEST(DrawingFilterTest, MorphologyDilate) { - auto src = TestSurface<4>(21, 21); - auto mid = TestSurface<4>(21, 21); - auto dst = TestSurface<4>(21, 21); + auto src = TestSurface<4, true>(21, 21); + auto mid = TestSurface<4, true>(21, 21); + auto dst = TestSurface<4, true>(21, 21); src.rect(3, 3, 15, 15, {0.5, 0.0, 0.0, 1.0, 1.0}); diff --git a/testfiles/src/display/test-base.h b/testfiles/src/display/test-base.h index 212a0e032e..4745d97716 100644 --- a/testfiles/src/display/test-base.h +++ b/testfiles/src/display/test-base.h @@ -16,31 +16,31 @@ enum class PatchMethod LIGHT }; -template +template struct TestSurface { TestSurface(int w, int h) { - if constexpr (CHANNELS == 3) { + if constexpr (channel_count == 3) { _cobj = {cairo_image_surface_create(format, w, h)}; _s = {Cairo::RefPtr(new Cairo::ImageSurface(_cobj[0], true))}; - _d = std::make_shared>(_s[0]); - } else if constexpr (CHANNELS == 4) { + _d = std::make_shared>(_s[0]); + } else if constexpr (channel_count == 4) { _cobj = {cairo_image_surface_create(format, w, h), cairo_image_surface_create(format, w, h)}; _s = {Cairo::RefPtr(new Cairo::ImageSurface(_cobj[0], true)), Cairo::RefPtr(new Cairo::ImageSurface(_cobj[1], true))}; - _d = std::make_shared>(_s[0], _s[1]); + _d = std::make_shared>(_s[0], _s[1]); } } - void rect(int x, int y, int w, int h, std::array const &c) + void rect(int x, int y, int w, int h, std::array const &c) { for (unsigned i = 0; i < _cobj.size(); i++) { unsigned off = i * 3; auto cro = cairo_create(_cobj[i]); cairo_rectangle(cro, x, y, w, h); - cairo_set_source_rgba(cro, c[off + 0], c.size() > off + 1 && off + 1 < CHANNELS ? c[off + 1] : 0.0, - c.size() > off + 2 && off + 2 < CHANNELS ? c[off + 2] : 0.0, c.back()); + cairo_set_source_rgba(cro, c[off + 0], c.size() > off + 1 && off + 1 < channel_count ? c[off + 1] : 0.0, + c.size() > off + 2 && off + 2 < channel_count ? c[off + 2] : 0.0, c.back()); cairo_fill(cro); cairo_destroy(cro); } @@ -48,19 +48,19 @@ struct TestSurface std::vector _cobj; std::vector> _s; - std::shared_ptr> _d; + std::shared_ptr> _d; }; -template +template struct TestCustomSurface { TestCustomSurface(int w, int h) { - _custom_memory = std::vector((CHANNELS + 1) * w * h); - _d = std::make_shared>(_custom_memory.data(), w, h); + _custom_memory = std::vector((channel_count + 1) * w * h); + _d = std::make_shared>(_custom_memory.data(), w, h); } - void rect(int const x, int const y, int const w, int const h, std::array const &c) + void rect(int const x, int const y, int const w, int const h, std::array const &c) { for (auto x0 = x; x0 < x + w; x0++) { for (auto y0 = y; y0 < y + h; y0++) { @@ -70,18 +70,17 @@ struct TestCustomSurface } std::vector _custom_memory; // Like CAIRO_FORMAT_RGBA128F but with custom primaries - std::shared_ptr> _d; + std::shared_ptr> _d; }; /** * Test single pixel getter, double and int modes */ -template -::testing::AssertionResult ColorIs(DrawingAccess d, T0 x, T0 y, std::array const &c, +template +::testing::AssertionResult ColorIs(DrawingAccess d, PrimaryType x, PrimaryType y, std::array const &c, bool unnmultiply = false) { - std::array ct; - d.colorAt(x, y, ct, unnmultiply); + auto ct = d.colorAt(x, y, unnmultiply); return VectorIsNear(c, ct, 0.001) << "\n X:" << x << "\n Y:" << y << "\n\n"; } @@ -93,24 +92,22 @@ template * c - Color values to set * x2,y2 - Optional coordinates to GET where the new color will be tested (for edge testing) */ -template -::testing::AssertionResult ColorWillBe(DrawingAccess d, int x, int y, std::array const &c, +template +::testing::AssertionResult ColorWillBe(DrawingAccess d, int x, int y, std::array const &c, bool unnmultiply = false, std::optional x2 = {}, std::optional y2 = {}) { - std::array before; - std::array after; if (!x2) x2 = x; if (!y2) y2 = y; - d.colorAt(*x2, *y2, before, unnmultiply); + auto before = d.colorAt(*x2, *y2, unnmultiply); if (VectorIsNear(c, before, 0.001) == ::testing::AssertionSuccess()) { return ::testing::AssertionFailure() << "\n" << print_values(c) << "\n ALREADY SET at " << *x2 << "," << *y2 << "\n"; } d.colorTo(x, y, c, unnmultiply); - d.colorAt(*x2, *y2, after, unnmultiply); + auto after = d.colorAt(*x2, *y2, unnmultiply); d.colorTo(*x2, *y2, before, unnmultiply); // Set value back return VectorIsNear(c, after, 0.001) << "\n Write:" << x << "," << y << "\n Read:" << *x2 << "," << *y2 << "\n"; @@ -134,8 +131,8 @@ std::string format_patch(std::string const &in, unsigned stride) return out.str(); } -template -std::string build_patch(DrawingAccess const &d, PatchMethod method, unsigned patch_x = 3, +template +std::string build_patch(DrawingAccess const &d, PatchMethod method, unsigned patch_x = 3, unsigned patch_y = 3, bool unmult = false) { static std::vector const weights = {' ', ' ', ' ', '.', '.', '.', ':', ':', '-', @@ -143,24 +140,23 @@ std::string build_patch(DrawingAccess const &d, PatchMethod method, un double size = patch_x * patch_y; char r0 = (method == PatchMethod::ALPHA) ? 0x40 : 0x30; - std::array color; // We collect a 3x3 grid of pixels into a patch so it can be shown as test output std::stringstream output; for (int y = 0; y < d.height() / patch_y; y++) { for (int x = 0; x < d.width() / patch_x; x++) { // inital values - std::vector colors(C + 1, 0.0); - std::vector lights(C + 1, 0.0); + std::vector colors(channel_count + 1, 0.0); + std::vector lights(channel_count + 1, 0.0); for (unsigned cy = 0; cy < patch_x; cy++) { for (unsigned cx = 0; cx < patch_x; cx++) { - T0 tx = x * patch_x + cx; - T0 ty = y * patch_y + cy; + PrimaryType tx = x * patch_x + cx; + PrimaryType ty = y * patch_y + cy; if (method == PatchMethod::ALPHA) { lights.back() += d.alphaAt(tx, ty); } else { - d.colorAt(tx, ty, color, unmult); + auto color = d.colorAt(tx, ty, unmult); for (int c = 0; c < colors.size(); c++) { colors[c] += color[c] > 0.5; lights[c] += color[c]; @@ -196,12 +192,12 @@ std::string build_patch(DrawingAccess const &d, PatchMethod method, un return output.str(); } -template -::testing::AssertionResult ImageIs(DrawingAccess d, std::string const &test, +template +::testing::AssertionResult ImageIs(DrawingAccess d, std::string const &test, PatchMethod method = PatchMethod::COLORS, bool unmult = false) { unsigned patch_x = 3; - std::string patch = build_patch(d, method, patch_x, patch_x, unmult); + std::string patch = build_patch(d, method, patch_x, patch_x, unmult); if (test != patch) { return ::testing::AssertionFailure() << format_patch(test, d.width() / patch_x) << "!=\n" @@ -213,18 +209,18 @@ template /** * Test the drawing surface against a compressed example */ -template +template ::testing::AssertionResult ImageIs(Cairo::RefPtr s, std::string const &test, PatchMethod method = PatchMethod::COLORS, bool unmult = false) { // SurfaceAccess is templated requiring compile time information about type formatting. switch (cairo_image_surface_get_format(s->cobj())) { case CAIRO_FORMAT_A8: - return ImageIs(DrawingAccess(s), test, method, unmult); + return ImageIs(DrawingAccess(s), test, method, unmult); case CAIRO_FORMAT_ARGB32: - return ImageIs(DrawingAccess(s), test, method, unmult); + return ImageIs(DrawingAccess(s), test, method, unmult); case CAIRO_FORMAT_RGBA128F: - return ImageIs(DrawingAccess(s), test, method, unmult); + return ImageIs(DrawingAccess(s), test, method, unmult); default: return ::testing::AssertionFailure() << "UNHANDLED_FORMAT"; } @@ -247,10 +243,11 @@ template ::testing::AssertionResult FilterIs(Filter &&f, std::string const &test, PatchMethod method = PatchMethod::COLORS, bool debug = false) { - auto src = TestSurface<4>(21, 21); - src.rect(3, 3, 15, 15, {0.5, 0.0, 0.0, 1.0, 1.0}); + auto src = TestSurface<4, true>(21, 21); src._d->setEdgeMode(DrawingAccessEdgeMode::ZERO); + src.rect(3, 3, 15, 15, {0.5, 0.0, 0.0, 1.0, 1.0}); + auto dst = TestSurface<4>(21, 21); f.filter(*dst._d, *src._d); if (debug) { @@ -262,13 +259,13 @@ template return ImageIs(*dst._d, test, method, true); } -template -::testing::AssertionResult FilterColors(Filter &&f, std::array const &test, - std::array const &i1, - std::optional> const &i2 = {}) +template +::testing::AssertionResult FilterColors(Filter &&f, std::array const &test, + std::array const &i1, + std::optional> const &i2 = {}) { - auto src1 = TestSurface(6, 6); - auto src2 = TestSurface(6, 6); + auto src1 = TestSurface(6, 6); + auto src2 = TestSurface(6, 6); if (i2) { src1.rect(0, 0, 6, 6, i1); src2.rect(0, 0, 6, 6, *i2); @@ -276,8 +273,7 @@ template src2.rect(0, 0, 6, 6, i1); } f.filter(*src1._d, *src2._d); - std::array p; - src1._d->colorAt((unsigned)1, 1, p, true); + auto p = src1._d->colorAt((unsigned)1, 1, true); return VectorIsNear(p, test, 0.001); } /* -- GitLab