diff --git a/src/colors/CMakeLists.txt b/src/colors/CMakeLists.txt index a1a4b705ae515a0ee4c282bcc13377aa2a8c9f6a..2047ffb26625c951e15e7883b429888d4e0ef2d2 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 06de05693f7950a50af8a15365b17e12895b8b23..f607ffefefebfa6eb5bed5010186d94fc949ac3f 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 @@ -34,9 +33,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 edb31d37d571b24c1df35fcb88bcbc0ec00f695b..8469a9016d3ac6fc41ded8043a8014b432b743a1 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 05b8b6985bd6065471bc4e5c4d368b656af1d88a..a563ff70f8e13c69678d11fc4f8dfa394c121191 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 720682e28697e1b370b7d93772a1b0165a2081b4..0000000000000000000000000000000000000000 --- 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 8ddccdc26dc47f656a4930b0dca32c020f3679d9..0000000000000000000000000000000000000000 --- 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 678a113692d472424c141643acdc0b179a038dfe..9f62db1aeb6ac36cad66bba873262c317e2af273 100644 --- a/src/colors/cms/transform-color.cpp +++ b/src/colors/cms/transform-color.cpp @@ -24,15 +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(), - 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()) { @@ -47,17 +47,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 0000000000000000000000000000000000000000..383bb93a559122c21053af8732ac8042305b3d2f --- /dev/null +++ b/src/colors/cms/transform-surface.h @@ -0,0 +1,116 @@ +// 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) + requires(!gamut_warn || (std::is_same_v && std::is_same_v)) + : Transform(proof + ? cmsCreateProofingTransformTHR( + cmsCreateContext(nullptr, nullptr), from->getHandle(), + lcms_color_format(from, sizeof(TypeIn), !std::is_integral_v, alpha_mode(premultiplied, true)), + to->getHandle(), + 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 | (gamut_warn ? cmsFLAGS_GAMUTCHECK : 0) | + lcms_bpc(proof_intent)) + : cmsCreateTransformTHR( + cmsCreateContext(nullptr, nullptr), from->getHandle(), + lcms_color_format(from, sizeof(TypeIn), !std::is_integral_v, alpha_mode(premultiplied, true)), + to->getHandle(), + 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(TypeIn)) + , _pixel_size_out((_channels_out + 1) * sizeof(TypeOut)) + {} + + /** + * 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 uncontiguous. + * @arg stride_out - The optional stride for the output image, if known to be uncontiguous. + */ + 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, + 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 1fe394d7e1d6d5fafbd9d7923dee931b9960278b..5864be7526e4f9c3259c518fcf02f38e0d5cc06d 100644 --- a/src/colors/cms/transform.cpp +++ b/src/colors/cms/transform.cpp @@ -9,10 +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. @@ -23,19 +25,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); + // 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 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 4ec449685d080a5837d49f228d9618c3996570af..5c4f2c1460b3fcaf3ffc5e0c72b549d5af50c870 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; @@ -52,7 +53,13 @@ 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 aeab010389b1d07116d42c8611762b37ce158ee6..205164a4edba51dbe4e7063be9ac2ac08b6a0fea 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 83c711851a3e41081f1571f9378771016c88ef49..b559f7e1e2f2bcf3a9a6a2ecdcf3f16630a26b33 100644 --- a/src/colors/spaces/base.h +++ b/src/colors/spaces/base.h @@ -13,7 +13,6 @@ #include #include -#include #include #include @@ -64,6 +63,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 +73,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 +93,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 ebb161e22040d7b24672c539948a974f060f79e1..d8b40559ef3a93e9542ac37f5287473d20ca5b9b 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 2c196394c7f93f79ac4e0c9a3ed06603fa01478a..aa7a42910bd31497cb67ba4359cc4965b3cac70b 100644 --- a/src/colors/spaces/cmyk.h +++ b/src/colors/spaces/cmyk.h @@ -24,12 +24,14 @@ 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 0dd73c67352774a00f6cd0a5d9f45538460dc931..b9c7592784bf61cabe77bdcc9e21eacf6e0b896f 100644 --- a/src/colors/spaces/gray.h +++ b/src/colors/spaces/gray.h @@ -20,6 +20,8 @@ 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 07bdd3547d2661b093806bcd3572c43d6aa1338d..79edc23e9a79830bea61c102be7f467067f6df75 100644 --- a/src/colors/spaces/hsl.h +++ b/src/colors/spaces/hsl.h @@ -21,6 +21,8 @@ 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 1afc70fd868f08cf3430cf7839900206deefe865..428c2555e90a63cfb7024c8ee83f865356045f6d 100644 --- a/src/colors/spaces/hsluv.h +++ b/src/colors/spaces/hsluv.h @@ -27,6 +27,8 @@ 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 f4a43411d04db30adb9a892eea56fb05fa3fce13..a598ffd1a1d683969913db970814d7430db785b8 100644 --- a/src/colors/spaces/hsv.h +++ b/src/colors/spaces/hsv.h @@ -21,12 +21,13 @@ 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 31ae911933b0b22ea4366ec43356b17ce3366336..f190ff1a899ac62b30488581ab12f6f9f0c6391b 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 42ae4131923d63c404e27786a679379fdf967ce8..49bc260ebc8c07f1ccfd5e33cb5865fc072cd568 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 47f9b87568eae5dada8c883a5f6c0ddc86da2d1d..3779e70d0521176a975ce7e988e2e89c16bd642c 100644 --- a/src/colors/spaces/lch.h +++ b/src/colors/spaces/lch.h @@ -21,6 +21,8 @@ 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 e622b57830f422861390facc7c5cbe559aff7a7a..3cd1e3bf2bc4a3f400797393f5c32e082cdde127 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 7a00914364fb011ad07a861c21dab3a9a7e34e9c..4a0213b5d9e55521c00ccc9f126b9480200fd639 100644 --- a/src/colors/spaces/luv.h +++ b/src/colors/spaces/luv.h @@ -25,6 +25,8 @@ 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 07d8d5dce3342103df05ff5d8696d1771915d61c..0b8fc8c6dc1d0a4143b4396deb2dc5092cb7b019 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 61f0786391da3ab2943b3b474f8e6c16b75a9f9b..96bc03d7b5c458c909e98d27cf5af52fc32bff03 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 846a1c9d80cef701627f72a080adf45e0b121752..3836626b311b24b520b85a1d3ce081cd91fe970e 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 52bfb929c65e25e4467964decc779e93784e090b..4e38f6739fbf8f381a9a0ec09d0f615cc358a715 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 760568573bb3e742d08716eb50f25a610c841bbb..8cbaa25266d3aaf9529d15b4b854a6c63582e3c3 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 d3ffc55edc2640a28b3846696b32c9646a46e6e4..ce8479c4a55e69d1d56f188ae54f4a471889369f 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 87caf1ceb1265dbf737ffa682a523ef975cbe723..ccb61129dda8a0a202ed99c9a03317a0734ebcdc 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,12 +44,14 @@ 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); } }; diff --git a/src/display/CMakeUnit.txt b/src/display/CMakeUnit.txt new file mode 100644 index 0000000000000000000000000000000000000000..aa5d334b7cb14415fbb92aac1a4d52e2f4cab625 --- /dev/null +++ b/src/display/CMakeUnit.txt @@ -0,0 +1,29 @@ +# 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}") + 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) + diff --git a/src/display/drawing-access.h b/src/display/drawing-access.h new file mode 100644 index 0000000000000000000000000000000000000000..4b4a95e55e6f357bed3061d0b44c7f06ea27002d --- /dev/null +++ b/src/display/drawing-access.h @@ -0,0 +1,468 @@ +// 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 +#include +#include + +#include "helper/mathfns.h" + +/** + * 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 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. + * 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 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 +class DrawingAccess +{ +public: + // Is the format an integer based format + constexpr static bool is_integer = format != CAIRO_FORMAT_RGBA128F; + + // How many primaries are there in this format + 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; + + // Scale of each primary to convert to a double used in Channels + constexpr static double primary_scale = is_integer ? 255.0 : 1.0; + + // Position of the alpha primary in this format + constexpr static int primary_alpha = is_integer ? 0 : primary_count; + + // Does this DrawingAccess need two surfaces? + constexpr static bool has_more_channels = channel_count > primary_count; + + // Actual number of channels when including alpha + constexpr static int channel_total = channel_count + 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. + */ + explicit DrawingAccess(Cairo::RefPtr cairo_surface, + Cairo::RefPtr next_surface = {}) + 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 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) { + // 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(PrimaryType *memory, int width, int height) + : _width(width) + , _height(height) + , _stride(width * primary_total) + , _size(_height * _stride) + , _memory(memory) + {} + + /** + * Set the edge mode for this drawing access object + */ + void setEdgeMode(DrawingAccessEdgeMode mode) requires(check_edge) + { + _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 - 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 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 < channel_count; c++) { + ret[c] = _get_channel(pos, c, alpha_mult); + } + ret[channel_count] = alpha; + return ret; + } + + /** + * Using bilinear interpolation get the effective pixel at the given coordinates. + * Note: Bilinear interpolation is two linear interpolations across 4 pixels + * + * @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. + */ + 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; + + 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[channel_count]); + for (int c = 0; c < channel_count; c++) { + ret[c] *= alpha_mult; + } + } + return ret; + } + + /** + * 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 + * @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[channel_count], 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; + + 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); + } + + /** + * 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 + */ + static int getOutputChannels() { return channel_count; } + + /** + * Get access to the memory directly + */ + PrimaryType *memory() + requires(!has_more_channels) + { + return _memory; + } + PrimaryType const *memory() const + requires(!has_more_channels) + { + return _memory; + } + + /** + * Copy the surface into a single contiguous memory surface and return. + */ + template + std::vector contiguousCopy(bool unpremultiply_alpha = false) + { + std::vector memory; + memory.reserve(_width * _height * channel_total); + + 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 < channel_count; c++) { + memory.emplace_back(_get_channel(pos, c, alpha_mult)); + } + memory.emplace_back(_get_channel(pos, channel_count, 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 (_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 (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 < primary_total && offset < values.size(); p++) { + if (p != primary_alpha) { + _next_memory[pos + _primary_pos(p)] = values[offset] * mult * primary_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_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_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 / primary_scale); // T=float|char T0=float|double + + if (_edge_check(pos)) { + return 0.0; + } + 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 < channel_count ? channel + is_integer : primary_alpha)] * scale * + alpha_mult; + } + + /** + * Get the alpha primary only + */ + inline double _get_alpha(int pos) const + { + 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; + } + + /** + * 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 + */ + inline int _pixel_pos(int x, int y) const + { + 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 * primary_total; + } + + /** + * 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. + */ + static inline int _primary_pos(int p) + { + if constexpr (G_BYTE_ORDER == G_LITTLE_ENDIAN) { + return is_integer ? primary_count - p : p; + } else { + return p; + } + } + + /** + * Standard bilinear interpolation + */ + 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 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); + } + + // Basic metrics for the surface + int const _width; + int const _height; + int const _stride; + int const _size; + PrimaryType *_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 + PrimaryType *_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 0000000000000000000000000000000000000000..f9abf69c9db877410b1fc53a2767b1ff45e17623 --- /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 <2geom/math-utils.h> + +namespace Inkscape::Filters { + +struct ColorMatrix +{ + explicit 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) + { + 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); + 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 + 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 + } + + explicit 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 + } + + explicit 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 0000000000000000000000000000000000000000..70033cec5dea9d246bacc64fa77528f7bf6303c7 --- /dev/null +++ b/src/display/filters/color-space.h @@ -0,0 +1,102 @@ +// 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 "colors/cms/transform-surface.h" +#include "colors/spaces/base.h" + +namespace Inkscape::Filters { + +template +struct ColorSpaceTransform +{ + /// Whether a direct conversion might be allowed. + 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) + : _from(std::move(from)) + , _to(std::move(to)) + { + // Direct color spaces use lcms2 and no other transformation + 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) + { + 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++) { + auto c1 = dst.colorAt(x, y, true); + for (int i = 0; i < c1.size() - 1; i++) { + c1[i] *= c1.back(); + } + dst.colorTo(x, y, c1, true); + } + } + + return; + } + } + + // Manual + 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 + auto c0 = src.colorAt(x, y, true); + // Expensive conversion, array to vector and back + cin.assign(c0.begin(), c0.end()); + _from->convert(cin, _to); + for (int i = 0; i < c1.size(); i++) { + c1[i] = cin[i]; + } + dst.colorTo(x, y, c1, true); + } + } + } + + std::optional> + _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 0000000000000000000000000000000000000000..8cd70a5d5b40218200cfbe4b018b21b03846cb6d --- /dev/null +++ b/src/display/filters/component-transfer.h @@ -0,0 +1,144 @@ +// 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 +}; + +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 + explicit ComponentTransfer(std::vector functions) + : _functions(std::move(functions)) + {} + + template + void filter(AccessDst &dst, AccessSrc const &src) + { + for (int y = 0; y < dst.height(); y++) { + for (int x = 0; x < dst.width(); x++) { + auto c = src.colorAt(x, y, true); + filterColor(c); + dst.colorTo(x, y, c, true); + } + } + } + + template + inline void filterColor(std::array &c) + { + 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) { + case TransferType::TABLE: + if (f._next.empty() || c[i] == 1.0) { + c[i] = f._table.back(); + } else { + 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; + } + 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 0000000000000000000000000000000000000000..e2e9fc8735f2baa9a061b2dae705dbe7f7d293bc --- /dev/null +++ b/src/display/filters/composite.h @@ -0,0 +1,62 @@ +// 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 + +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 + for (int y = 0; y < dst.height(); y++) { + for (int x = 0; x < dst.width(); x++) { + 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); + } + 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 0000000000000000000000000000000000000000..f83b31b37ece5ec214e7f65a01d95a381515a831 --- /dev/null +++ b/src/display/filters/convolve-matrix.h @@ -0,0 +1,124 @@ +// 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 + +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); + typename AccessDst::Color output; + std::vector patch(_kernel.size()); + + 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 + patch[i + j * _orderX] = src.colorAt(x + i - _targetX, j - _targetY, 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 + 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(); + + // 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 constexpr bool 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 0000000000000000000000000000000000000000..583670715aba40455cf7f47cdd7f2bba1b9ea964 --- /dev/null +++ b/src/display/filters/displacement-map.h @@ -0,0 +1,61 @@ +// 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 + +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) + { + for (int y = 0; y < dst.height(); y++) { + for (int x = 0; x < dst.width(); x++) { + 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 + 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); + } + } + } + + 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 0000000000000000000000000000000000000000..1b8904ca00365dc0ef4abfcf8773e610f8712f1e --- /dev/null +++ b/src/display/filters/gaussian-blur.h @@ -0,0 +1,616 @@ +// 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_GAUSSIAN_BLUR_H +#define INKSCAPE_DISPLAY_FILTER_GAUSSIAN_BLUR_H + +#include +#include +#include +#include <2geom/point.h> + +#include "display/dispatch-pool.h" +#include "display/threading.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) +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) +// using FIRValue = Util::FixedPoint; + +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 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)) + , _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 const &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 std::array colorAt(Access &surface, int x, int y) const + { + if constexpr (axis == Geom::X) { + 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) + { + 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 + 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]; + 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]; + } + 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::channel_total; 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::channel_total; c++) { + v[0][c] *= b[0]; + } + for (int i = 1; i < N + 1; i++) { + for (int c = 0; c < Access::channel_total; 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_out; + std::array px_cmp; + skipbuf.fill(INT_MIN); + + // history initialization + auto imin = colorAt(surface, 0, row); + 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]; + } + history[0] = colorAt(surface, col, row); + + // for all bytes of the pixel + for (unsigned int byte = 0; byte < Access::channel_total; 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 + auto px_in = colorAt(surface, col_in, row); + + // 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 + int pos = 0; + while (col + pos + scr_len < col_count - 1 && (!pos || px_cmp[byte] == last_in)) { + px_cmp = colorAt(surface, col + pos + scr_len, row); + if (pos) { + px_cmp = colorAt(surface, col + pos, row); + 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) const + { + 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 alpha, + std::array vold[N]) + { + 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]; + 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_GAUSSIAN_BLUR_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 0000000000000000000000000000000000000000..ef20bce8fe12f38dbc0cd78dd2bd2a0a3a006684 --- /dev/null +++ b/src/display/filters/light.h @@ -0,0 +1,449 @@ +// 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> + +namespace Inkscape::Filters { + +inline constexpr int X_3D = 0; +inline constexpr int Y_3D = 1; +inline constexpr int Z_3D = 2; + +using vector3d = std::array; + +// The eye vector for specular lighting +inline constexpr vector3d 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], coords[Y_3D]) * device_scale * trans; + coords[X_3D] = p.x(); + coords[Y_3D] = p.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) + { + typename AccessSrc::Color lit_color; + // Conversion of color here + for (auto i = 0; i < _color.size(); i++) { + lit_color[i] = _color[i]; + } + typename AccessDst::Color 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) + { + typename AccessSrc::Color lit_color; + typename AccessSrc::Color 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 0000000000000000000000000000000000000000..a825dcf23d47469c66e878473476ec4d867ae964 --- /dev/null +++ b/src/display/filters/morphology.h @@ -0,0 +1,141 @@ +// 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/threading.h" + +inline constexpr int 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); + typename AccessDst::Color output; + + // Initialize with transparent black + 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; + int out_x = 0; + + for (int j = 0; j < w + ri; ++j) { + for (int p = 0; p < channels; ++p) { + 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) + 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++; + } + } + }); + } +}; + +} // 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 0000000000000000000000000000000000000000..b379017f10908ea44ea6a725ec38590872705b98 --- /dev/null +++ b/src/display/filters/turbulence.h @@ -0,0 +1,323 @@ +// 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> + +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 const &trans, int x0, int y0) + { + if (!_ready) { + init(); + } + + 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); + 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 < 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); + 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 < channel_total; 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 1df50de9ab391449f2ff54feb6ffbd71e64f0e51..e140c9b25e3807274eee5f1e502784673ca70588 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 3ab9ede887daeb2191782e29239f0233d62f5d3e..3db2e45abdaae6cebcf3b2e9247fea002c44370c 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 fdd7b2bf91efdd8d4af219e3557e0e1e02acf769..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..f057cfc50411bade562737ec5f9b577760fa649f --- /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 "../test-utils.h" +#include "colors/cms/profile.h" +#include "colors/cms/transform-surface.h" +#include "colors/cms/transform.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 { + +// clang-format off +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); +} +// clang-format on + +} // 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 87ecf5bf859c93bd1cafd6bea90a8f2b64c203a3..4c8ad9c60b06c857de29c1c01f5c7e612a8fd50c 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 7a8769c8bfb1c61165da4ea507e2c6d7b84b3250..cf3328cd34719993ade16abe1b48f3814a2a6512 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 0000000000000000000000000000000000000000..28d6a191d5046dabb8c5dcb8c78051ab9723dc83 --- /dev/null +++ b/testfiles/src/display/drawing-access-test.cpp @@ -0,0 +1,530 @@ +// 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) + { + for (int y = 0; y < dst.height(); y++) { + for (int x = 0; x < dst.width(); x++) { + if (x / 3 == y / 3) { + auto c = src.colorAt(x, y); + 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, 0.6); + 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", + PatchMethod::COLORS, true)) + << "Format: " << get_format_name(format) << "\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"; + } +} + +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); + + 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) +{ + 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, 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++) { + 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), 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)); + + 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 " + " 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 TestcontiguousCopy(std::array in, std::array cmp, bool unpre = false) +{ + int w = 21; + int h = 21; + auto src = TestSurface<4, false, F>(w, h); + src.rect(6, 6, 9, 9, in); + + std::vector copy = src._d->template contiguousCopy(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_v) { + 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, contiguousCopy) +{ + // Direct copy of original data + 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 + 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 + 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 + 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++ + c-file-style:"stroustrup" + c-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 0000000000000000000000000000000000000000..fb39d9850cf33f011b124f91b79c9ee968cbfe20 --- /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 0000000000000000000000000000000000000000..8293f7666368858f9d57e51f0dcd0298f73c7d68 --- /dev/null +++ b/testfiles/src/display/filter-color-space-test.cpp @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#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; + +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 0000000000000000000000000000000000000000..7cfcc64d3149dba64b6231ef281d9d98c46dbe21 --- /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 0000000000000000000000000000000000000000..db151148618629c72f112445ca714a3d06d3baa7 --- /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 0000000000000000000000000000000000000000..6a1564c036180edfa5d97237db49e046540f9b88 --- /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 0000000000000000000000000000000000000000..9b12f51ccafb68a0710e6201457a538b0427dca0 --- /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, true>(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 0000000000000000000000000000000000000000..c5fa526e1644b37da870c29a8bbbc58829879878 --- /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 0000000000000000000000000000000000000000..0c2e8d4d3f9bf745925d49173313522532f042ad --- /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 component 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 0000000000000000000000000000000000000000..e7597add533b87bf36760fe7f412d9e2a4137771 --- /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, 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}); + + 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, 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}); + + 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 0000000000000000000000000000000000000000..9979c22311f0ecf86e4a09824728bacf7ce92ed7 --- /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 0000000000000000000000000000000000000000..4745d97716e2f98cbd1c5b740dfe55574997e954 --- /dev/null +++ b/testfiles/src/display/test-base.h @@ -0,0 +1,288 @@ +// 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 (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 (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]); + } + } + + 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 < 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); + } + } + + std::vector _cobj; + std::vector> _s; + std::shared_ptr> _d; +}; + +template +struct TestCustomSurface +{ + TestCustomSurface(int w, int 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) + { + 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, PrimaryType x, PrimaryType y, std::array const &c, + bool unnmultiply = false) +{ + auto ct = d.colorAt(x, y, 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 = {}) +{ + if (!x2) + x2 = x; + if (!y2) + y2 = y; + + 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); + 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"; +} + +/** + * 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; + + // 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(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++) { + PrimaryType tx = x * patch_x + cx; + PrimaryType ty = y * patch_y + cy; + if (method == PatchMethod::ALPHA) { + lights.back() += d.alphaAt(tx, ty); + } else { + 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]; + } + } + } + } + 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 const std::map map = { + {CAIRO_FORMAT_A8, "A8"}, + {CAIRO_FORMAT_ARGB32, "ARGB32"}, + {CAIRO_FORMAT_RGBA128F, "RGBA128F"}, + }; + return map.at(format); +} + +template +::testing::AssertionResult FilterIs(Filter &&f, std::string const &test, PatchMethod method = PatchMethod::COLORS, + bool debug = false) +{ + 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) { + 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); + auto p = src1._d->colorAt((unsigned)1, 1, 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 1b42b879b45dbcfdeb81177a40589207c06513dd..f8aa5ff89b01b157300093b8ce6edd8bcf771a7d 100644 --- a/testfiles/src/test-utils.h +++ b/testfiles/src/test-utils.h @@ -30,27 +30,46 @@ struct traced_data __FILE__, __LINE__, __VA_ARGS__ \ } - /** * 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) { + 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 << ", "; - } - first = false; - oo << std::setprecision(3) << item; - } - oo << "}"; + } + oo << "}(" << v.size() << ")"; 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(); @@ -61,14 +80,20 @@ 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(); }