From 1f6e0b4c0b24233070fa78136b2a6dee84d5232d Mon Sep 17 00:00:00 2001 From: Martin Owens Date: Thu, 11 Jan 2024 23:22:17 -0500 Subject: [PATCH 1/2] Add new Color Space management functionality A new complete color management directory with all the classes needed to create and control colors in both documents as well as pixels on screen. Much of Inkscape previous color code has been rewritten into this new feature; but this commit does not remove the previous code please see the color refactoring branch for the later changes. This should enable CSS Color Module support, icc color conversions and tracking color profiles in a safe and orderly fashion. Drag and drop improvements incorperated from pbs's branch. Redrafted to remove std::string Color constructors, parser confusion and use of strings to find internal color spaces for most cases. --- buildtools/check_license_headers.py | 3 +- po/POTFILES.src.in | 1 + src/CMakeLists.txt | 1 + src/color/cms-color-types.h | 3 - src/colors/CMakeLists.txt | 70 ++ src/colors/cms/profile.cpp | 296 +++++++++ src/colors/cms/profile.h | 83 +++ src/colors/cms/system.cpp | 277 ++++++++ src/colors/cms/system.h | 103 +++ src/colors/cms/transform.cpp | 275 ++++++++ src/colors/cms/transform.h | 87 +++ src/colors/color-set.cpp | 402 +++++++++++ src/colors/color-set.h | 115 ++++ src/colors/color.cpp | 629 ++++++++++++++++++ src/colors/color.h | 119 ++++ src/colors/document-cms.cpp | 356 ++++++++++ src/colors/document-cms.h | 109 +++ src/colors/dragndrop.cpp | 93 +++ src/colors/dragndrop.h | 43 ++ src/colors/manager.cpp | 113 ++++ src/colors/manager.h | 67 ++ src/colors/parser.cpp | 359 ++++++++++ src/colors/parser.h | 125 ++++ src/colors/printer.cpp | 66 ++ src/colors/printer.h | 98 +++ src/colors/spaces/base.cpp | 139 ++++ src/colors/spaces/base.h | 92 +++ src/colors/spaces/cms.cpp | 208 ++++++ src/colors/spaces/cms.h | 73 ++ src/colors/spaces/cmyk.cpp | 85 +++ src/colors/spaces/cmyk.h | 44 ++ src/colors/spaces/components.cpp | 158 +++++ src/colors/spaces/components.h | 62 ++ src/colors/spaces/enum.h | 75 +++ src/colors/spaces/gray.cpp | 36 + src/colors/spaces/gray.h | 34 + src/colors/spaces/hsl.cpp | 117 ++++ src/colors/spaces/hsl.h | 48 ++ src/colors/spaces/hsluv.cpp | 139 ++++ src/colors/spaces/hsluv.h | 62 ++ src/colors/spaces/hsv.cpp | 124 ++++ src/colors/spaces/hsv.h | 49 ++ src/colors/spaces/lab.cpp | 141 ++++ src/colors/spaces/lab.h | 65 ++ src/colors/spaces/lch.cpp | 120 ++++ src/colors/spaces/lch.h | 69 ++ src/colors/spaces/linear-rgb.cpp | 89 +++ src/colors/spaces/linear-rgb.h | 43 ++ src/colors/spaces/luv.cpp | 147 ++++ src/colors/spaces/luv.h | 63 ++ src/colors/spaces/named.cpp | 235 +++++++ src/colors/spaces/named.h | 53 ++ src/colors/spaces/okhsl.cpp | 69 ++ src/colors/spaces/okhsl.h | 51 ++ src/colors/spaces/oklab.cpp | 150 +++++ src/colors/spaces/oklab.h | 65 ++ src/colors/spaces/oklch.cpp | 389 +++++++++++ src/colors/spaces/oklch.h | 67 ++ src/colors/spaces/rgb.cpp | 79 +++ src/colors/spaces/rgb.h | 52 ++ src/colors/spaces/xyz.cpp | 78 +++ src/colors/spaces/xyz.h | 64 ++ src/colors/utils.cpp | 202 ++++++ src/colors/utils.h | 95 +++ src/colors/xml-color.cpp | 181 +++++ src/colors/xml-color.h | 53 ++ src/document.cpp | 2 + src/document.h | 6 + src/object/color-profile.cpp | 488 +++----------- src/object/color-profile.h | 113 ++-- src/ui/widget/color-icc-selector.cpp | 15 +- testfiles/CMakeLists.txt | 28 +- testfiles/data/colors/SwappedRedAndGreen.icc | Bin 0 -> 15720 bytes testfiles/data/colors/cms-in-defs.svg | 87 +++ testfiles/data/colors/cms-in-objs.svg | 29 + testfiles/data/colors/default_cmyk.icc | Bin 0 -> 187484 bytes testfiles/data/colors/display.icc | Bin 0 -> 864 bytes testfiles/src/color-profile-test.cpp | 127 ---- testfiles/src/colors/cms-test.cpp | 349 ++++++++++ testfiles/src/colors/color-set-test.cpp | 286 ++++++++ testfiles/src/colors/color-test.cpp | 424 ++++++++++++ testfiles/src/colors/document-cms-test.cpp | 268 ++++++++ testfiles/src/colors/dragndrop-test.cpp | 83 +++ testfiles/src/colors/manager-test.cpp | 105 +++ testfiles/src/colors/parser-test.cpp | 161 +++++ testfiles/src/colors/printer-test.cpp | 90 +++ testfiles/src/colors/spaces-cms-test.cpp | 224 +++++++ testfiles/src/colors/spaces-cmyk-test.cpp | 105 +++ testfiles/src/colors/spaces-gray-test.cpp | 62 ++ testfiles/src/colors/spaces-hsl-test.cpp | 105 +++ testfiles/src/colors/spaces-hsluv-test.cpp | 73 ++ testfiles/src/colors/spaces-hsv-test.cpp | 111 ++++ testfiles/src/colors/spaces-lab-test.cpp | 83 +++ testfiles/src/colors/spaces-lch-test.cpp | 91 +++ .../src/colors/spaces-linear-rgb-test.cpp | 80 +++ testfiles/src/colors/spaces-luv-test.cpp | 73 ++ testfiles/src/colors/spaces-named-test.cpp | 46 ++ testfiles/src/colors/spaces-okhsl-test.cpp | 67 ++ testfiles/src/colors/spaces-oklab-test.cpp | 81 +++ testfiles/src/colors/spaces-oklch-test.cpp | 88 +++ testfiles/src/colors/spaces-rgb-test.cpp | 107 +++ testfiles/src/colors/spaces-testbase.h | 370 +++++++++++ testfiles/src/colors/spaces-xyz-test.cpp | 81 +++ testfiles/src/colors/utils-test.cpp | 111 ++++ testfiles/src/colors/xml-color-test.cpp | 142 ++++ testfiles/src/style-test.cpp | 4 +- testfiles/src/test-utils.h | 97 +++ 107 files changed, 12340 insertions(+), 580 deletions(-) create mode 100644 src/colors/CMakeLists.txt create mode 100644 src/colors/cms/profile.cpp create mode 100644 src/colors/cms/profile.h create mode 100644 src/colors/cms/system.cpp create mode 100644 src/colors/cms/system.h create mode 100644 src/colors/cms/transform.cpp create mode 100644 src/colors/cms/transform.h create mode 100644 src/colors/color-set.cpp create mode 100644 src/colors/color-set.h create mode 100644 src/colors/color.cpp create mode 100644 src/colors/color.h create mode 100644 src/colors/document-cms.cpp create mode 100644 src/colors/document-cms.h create mode 100644 src/colors/dragndrop.cpp create mode 100644 src/colors/dragndrop.h create mode 100644 src/colors/manager.cpp create mode 100644 src/colors/manager.h create mode 100644 src/colors/parser.cpp create mode 100644 src/colors/parser.h create mode 100644 src/colors/printer.cpp create mode 100644 src/colors/printer.h create mode 100644 src/colors/spaces/base.cpp create mode 100644 src/colors/spaces/base.h create mode 100644 src/colors/spaces/cms.cpp create mode 100644 src/colors/spaces/cms.h create mode 100644 src/colors/spaces/cmyk.cpp create mode 100644 src/colors/spaces/cmyk.h create mode 100644 src/colors/spaces/components.cpp create mode 100644 src/colors/spaces/components.h create mode 100644 src/colors/spaces/enum.h create mode 100644 src/colors/spaces/gray.cpp create mode 100644 src/colors/spaces/gray.h create mode 100644 src/colors/spaces/hsl.cpp create mode 100644 src/colors/spaces/hsl.h create mode 100644 src/colors/spaces/hsluv.cpp create mode 100644 src/colors/spaces/hsluv.h create mode 100644 src/colors/spaces/hsv.cpp create mode 100644 src/colors/spaces/hsv.h create mode 100644 src/colors/spaces/lab.cpp create mode 100644 src/colors/spaces/lab.h create mode 100644 src/colors/spaces/lch.cpp create mode 100644 src/colors/spaces/lch.h create mode 100644 src/colors/spaces/linear-rgb.cpp create mode 100644 src/colors/spaces/linear-rgb.h create mode 100644 src/colors/spaces/luv.cpp create mode 100644 src/colors/spaces/luv.h create mode 100644 src/colors/spaces/named.cpp create mode 100644 src/colors/spaces/named.h create mode 100644 src/colors/spaces/okhsl.cpp create mode 100644 src/colors/spaces/okhsl.h create mode 100644 src/colors/spaces/oklab.cpp create mode 100644 src/colors/spaces/oklab.h create mode 100644 src/colors/spaces/oklch.cpp create mode 100644 src/colors/spaces/oklch.h create mode 100644 src/colors/spaces/rgb.cpp create mode 100644 src/colors/spaces/rgb.h create mode 100644 src/colors/spaces/xyz.cpp create mode 100644 src/colors/spaces/xyz.h create mode 100644 src/colors/utils.cpp create mode 100644 src/colors/utils.h create mode 100644 src/colors/xml-color.cpp create mode 100644 src/colors/xml-color.h create mode 100644 testfiles/data/colors/SwappedRedAndGreen.icc create mode 100644 testfiles/data/colors/cms-in-defs.svg create mode 100644 testfiles/data/colors/cms-in-objs.svg create mode 100644 testfiles/data/colors/default_cmyk.icc create mode 100644 testfiles/data/colors/display.icc delete mode 100644 testfiles/src/color-profile-test.cpp create mode 100644 testfiles/src/colors/cms-test.cpp create mode 100644 testfiles/src/colors/color-set-test.cpp create mode 100644 testfiles/src/colors/color-test.cpp create mode 100644 testfiles/src/colors/document-cms-test.cpp create mode 100644 testfiles/src/colors/dragndrop-test.cpp create mode 100644 testfiles/src/colors/manager-test.cpp create mode 100644 testfiles/src/colors/parser-test.cpp create mode 100644 testfiles/src/colors/printer-test.cpp create mode 100644 testfiles/src/colors/spaces-cms-test.cpp create mode 100644 testfiles/src/colors/spaces-cmyk-test.cpp create mode 100644 testfiles/src/colors/spaces-gray-test.cpp create mode 100644 testfiles/src/colors/spaces-hsl-test.cpp create mode 100644 testfiles/src/colors/spaces-hsluv-test.cpp create mode 100644 testfiles/src/colors/spaces-hsv-test.cpp create mode 100644 testfiles/src/colors/spaces-lab-test.cpp create mode 100644 testfiles/src/colors/spaces-lch-test.cpp create mode 100644 testfiles/src/colors/spaces-linear-rgb-test.cpp create mode 100644 testfiles/src/colors/spaces-luv-test.cpp create mode 100644 testfiles/src/colors/spaces-named-test.cpp create mode 100644 testfiles/src/colors/spaces-okhsl-test.cpp create mode 100644 testfiles/src/colors/spaces-oklab-test.cpp create mode 100644 testfiles/src/colors/spaces-oklch-test.cpp create mode 100644 testfiles/src/colors/spaces-rgb-test.cpp create mode 100644 testfiles/src/colors/spaces-testbase.h create mode 100644 testfiles/src/colors/spaces-xyz-test.cpp create mode 100644 testfiles/src/colors/utils-test.cpp create mode 100644 testfiles/src/colors/xml-color-test.cpp create mode 100644 testfiles/src/test-utils.h diff --git a/buildtools/check_license_headers.py b/buildtools/check_license_headers.py index cf6796c454..773a225018 100755 --- a/buildtools/check_license_headers.py +++ b/buildtools/check_license_headers.py @@ -44,6 +44,7 @@ IGNORE_FILE_ENDINGS = [ ".dia", ".dll", ".eps", + ".icc", ".kate-swp", ".ods", ".otf", @@ -147,7 +148,7 @@ if __name__ == '__main__': main(files_all()) except LicenseCheckError as e: print(e, file=sys.stderr) - print("If you think this message is wrong, edit buildtools/check_license_header.py", file=sys.stderr) + print("If you think this message is wrong, edit buildtools/check_license_headers.py", file=sys.stderr) sys.exit(1) # vi:sw=4:expandtab: diff --git a/po/POTFILES.src.in b/po/POTFILES.src.in index 6af969bbb5..8a8393a76a 100644 --- a/po/POTFILES.src.in +++ b/po/POTFILES.src.in @@ -42,6 +42,7 @@ ${_build_dir}/share/templates/templates.h ../src/actions/actions-window.cpp ../src/auto-save.cpp ../src/color/cms-util.cpp +../src/colors/spaces/components.cpp ../src/context-fns.cpp ../src/desktop-events.cpp ../src/desktop-style.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f454c988e4..5a84fb6664 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -310,6 +310,7 @@ list(APPEND inkscape_SRC add_subdirectory(actions) add_subdirectory(async) add_subdirectory(color) +add_subdirectory(colors) add_subdirectory(debug) add_subdirectory(display) add_subdirectory(extension) diff --git a/src/color/cms-color-types.h b/src/color/cms-color-types.h index c6b90878b2..a9ee3d880d 100644 --- a/src/color/cms-color-types.h +++ b/src/color/cms-color-types.h @@ -36,7 +36,6 @@ class FourCCSig { public: FourCCSig( FourCCSig const &other ) = default; -protected: FourCCSig( guint32 value ) : value(value) {}; guint32 value; @@ -46,7 +45,6 @@ class ColorSpaceSig : public FourCCSig { public: ColorSpaceSig( ColorSpaceSig const &other ) = default; -protected: ColorSpaceSig( guint32 value ) : FourCCSig(value) {}; }; @@ -54,7 +52,6 @@ class ColorProfileClassSig : public FourCCSig { public: ColorProfileClassSig( ColorProfileClassSig const &other ) = default; -protected: ColorProfileClassSig( guint32 value ) : FourCCSig(value) {}; }; diff --git a/src/colors/CMakeLists.txt b/src/colors/CMakeLists.txt new file mode 100644 index 0000000000..97dc58f39f --- /dev/null +++ b/src/colors/CMakeLists.txt @@ -0,0 +1,70 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +set(colors_SRC + cms/profile.cpp + cms/system.cpp + cms/transform.cpp + color.cpp + document-cms.cpp + dragndrop.cpp + manager.cpp + parser.cpp + printer.cpp + color-set.cpp + spaces/base.cpp + spaces/cms.cpp + spaces/cmyk.cpp + spaces/components.cpp + spaces/gray.cpp + spaces/hsl.cpp + spaces/hsluv.cpp + spaces/hsv.cpp + spaces/lab.cpp + spaces/lch.cpp + spaces/linear-rgb.cpp + spaces/luv.cpp + spaces/okhsl.cpp + spaces/oklab.cpp + spaces/oklch.cpp + spaces/named.cpp + spaces/rgb.cpp + spaces/xyz.cpp + utils.cpp + xml-color.cpp + + # ------- + # Headers + cms/profile.h + cms/system.h + cms/transform.h + color.h + document-cms.h + dragndrop.h + manager.h + parser.h + printer.h + color-set.h + spaces/base.h + spaces/cms.h + spaces/cmyk.h + spaces/components.h + spaces/enum.h + spaces/gray.h + spaces/hsl.h + spaces/hsluv.h + spaces/hsv.h + spaces/lab.h + spaces/lch.h + spaces/linear-rgb.h + spaces/luv.h + spaces/okhsl.h + spaces/oklab.h + spaces/oklch.h + spaces/named.h + spaces/rgb.h + spaces/xyz.h + utils.h + xml-color.h +) + +add_inkscape_source("${colors_SRC}") diff --git a/src/colors/cms/profile.cpp b/src/colors/cms/profile.cpp new file mode 100644 index 0000000000..41a374d477 --- /dev/null +++ b/src/colors/cms/profile.cpp @@ -0,0 +1,296 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * A C++ wrapper for lcms2 profiles + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "profile.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "system.h" + +namespace Inkscape::Colors::CMS { + +/** + * Construct a color profile object from the lcms2 object. + */ +std::shared_ptr Profile::create(cmsHPROFILE handle, std::string path, bool in_home) +{ + return handle ? std::make_shared(handle, std::move(path), in_home) : nullptr; +} + +/** + * Construct a color profile object from a uri. Ownership of the lcms2 object is contained + * within the Profile object and will be destroyed when it is. + */ +std::shared_ptr Profile::create_from_uri(std::string path, bool in_home) +{ + if (cmsHPROFILE profile = cmsOpenProfileFromFile(path.c_str(), "r")) + return Profile::create(profile, std::move(path), in_home); + return nullptr; +} + +/** + * Construct a color profile object from the raw data. + */ +std::shared_ptr Profile::create_from_data(std::string const &contents) +{ + if (cmsHPROFILE profile = cmsOpenProfileFromMem(contents.data(), contents.size())) + return Profile::create(profile, "", false); + return nullptr; +} + +/** + * Construct the default lcms sRGB color profile and return. + */ +std::shared_ptr Profile::create_srgb() +{ + return Profile::create(cmsCreate_sRGBProfile()); +} + +Profile::Profile(cmsHPROFILE handle, std::string path, bool in_home) + : _handle(handle) + , _path(std::move(path)) + , _id(generate_id()) + , _checksum(generate_checksum()) + , _in_home(in_home) +{ + assert(_handle); +} + +/** + * Return true if this profile is for display/monitor correction. + */ +bool Profile::isForDisplay() const +{ + // If the profile has a Video Card Gamma Table (VCGT), then it's very likely to + // be an actual monitor/display icc profile, and not just a display RGB profile. + return getProfileClass() == cmsSigDisplayClass && getColorSpace() == cmsSigRgbData && + cmsIsTag(_handle, cmsSigVcgtTag); +} + +/** + * Cleans up name to remove disallowed characters. + * + * Allowed ASCII first characters: ':', 'A'-'Z', '_', 'a'-'z' + * Allowed ASCII remaining chars add: '-', '.', '0'-'9', + * + * @param str the string to clean up. + */ +static void sanitize_name(std::string &str) +{ + if (str.empty()) + return; + auto val = str[0]; + if ((val < 'A' || val > 'Z') && (val < 'a' || val > 'z') && val != '_' && val != ':') { + str.insert(0, "_"); + } + for (std::size_t i = 1; i < str.size(); i++) { + auto val = str[i]; + if ((val < 'A' || val > 'Z') && (val < 'a' || val > 'z') && (val < '0' || val > '9') && val != '_' && + val != ':' && val != '-' && val != '.') { + if (str.at(i - 1) == '-') { + str.erase(i, 1); + i--; + } else { + str[i] = '-'; + } + } + } + if (str.back() == '-') { + str.pop_back(); + } +} + +/** + * Returns the name inside the icc profile, or empty string if it couldn't be + * parsed out of the icc data correctly. + */ +std::string Profile::getName(bool sanitize) const +{ + std::string name; + cmsUInt32Number byteLen = cmsGetProfileInfoASCII(_handle, cmsInfoDescription, "en", "US", nullptr, 0); + if (byteLen > 0) { + std::vector data(byteLen); + cmsUInt32Number readLen = + cmsGetProfileInfoASCII(_handle, cmsInfoDescription, "en", "US", data.data(), data.size()); + if (readLen < data.size()) { + g_warning("Profile::get_name(): icc data read less than expected!"); + data.resize(readLen); + } + // Remove nulls at end which will cause an invalid utf8 string. + while (!data.empty() && data.back() == 0) { + data.pop_back(); + } + name = std::string(data.begin(), data.end()); + } + if (sanitize) + sanitize_name(name); + return name; +} + +cmsColorSpaceSignature Profile::getColorSpace() const +{ + return cmsGetColorSpace(_handle); +} + +cmsProfileClassSignature Profile::getProfileClass() const +{ + return cmsGetDeviceClass(_handle); +} + +struct InputFormatMap +{ + cmsColorSpaceSignature space; + cmsUInt32Number inForm; +}; + +/** + * Returns the number of channels this profile stores for color information. + */ +unsigned int Profile::getSize() const +{ + switch (getColorSpace()) { + case cmsSigGrayData: + return 1; + case cmsSigCmykData: + return 4; + default: + return 3; + } +} + +bool Profile::isIccFile(std::string const &filepath) +{ + bool is_icc_file = false; + GStatBuf st; + if (g_stat(filepath.c_str(), &st) == 0 && st.st_size > 128) { + // 0-3 == size + // 36-39 == 'acsp' 0x61637370 + int fd = g_open(filepath.c_str(), O_RDONLY, S_IRWXU); + if (fd != -1) { + guchar scratch[40] = {0}; + size_t len = sizeof(scratch); + + ssize_t got = read(fd, scratch, len); + if (got != -1) { + size_t calcSize = (scratch[0] << 24) | (scratch[1] << 16) | (scratch[2] << 8) | (scratch[3]); + if (calcSize > 128 && calcSize <= static_cast(st.st_size)) { + is_icc_file = + (scratch[36] == 'a') && (scratch[37] == 'c') && (scratch[38] == 's') && (scratch[39] == 'p'); + } + } + close(fd); + + if (is_icc_file) { + cmsHPROFILE profile = cmsOpenProfileFromFile(filepath.c_str(), "r"); + if (profile) { + cmsProfileClassSignature profClass = cmsGetDeviceClass(profile); + if (profClass == cmsSigNamedColorClass) { + is_icc_file = false; // Ignore named color profiles for now. + } + cmsCloseProfile(profile); + } + } + } + } + return is_icc_file; +} + +/** + * Get or generate a profile Id, and save in the object for later use. + */ +std::string Profile::generate_id() const +{ + // 1. get the id from the cms header itself, ususally correct. + cmsUInt8Number tmp[16]; + cmsGetHeaderProfileID(_handle, tmp); + + std::ostringstream oo; + oo << std::hex << std::setfill('0'); + for (auto &digit : tmp) { + // Setw must happen each loop + oo << std::setw(2) << static_cast(digit); + } +#ifdef __APPLE__ + auto s = oo.str(); + if (std::count_if(s.begin(), s.end(), [](char c){ return c == '0'; }) < 24) +#else + if (std::ranges::count(oo.str(), '0') < 24) +#endif + return oo.str(); // Done + // If there's no path, then what we have is a generated or in-memory profile + // which is unlikely to ever need to be matched with anything via id but it's + // also true that this id would change between computers, and creation date. + if (_path.empty()) + return ""; + return generate_checksum(); +} + +/** + * Generate a checksum of the data according to the ICC specification + */ +std::string Profile::generate_checksum() const +{ + // 2. If the id is empty, for some reason, we're going to generate it + // from the data using the same method that should have been used originally + // See ICC.1-2022-05 7.2.18 Profile ID field. + auto data = dumpData(); + if (data.size() < 100) { + g_warning("Bad icc profile data when generating profile id."); + return "~"; + } + // Zero out the required bytes as per the above specification + for (unsigned i = 64; i < 68; i++) + data[i] = 0; + for (unsigned i = 84; i < 100; i++) + data[i] = 0; + return Glib::Checksum::compute_checksum(Glib::Checksum::Type::MD5, std::string(data.begin(), data.end())); +} + +/** + * Dump the entire profile as a base64 encoded string. This is used for color-profile href data. + */ +std::string Profile::dumpBase64() const +{ + auto buf = dumpData(); + return Glib::Base64::encode(std::string(buf.begin(), buf.end())); +} + +/** + * Dump the entire profile as raw data. Used in dumpBase64 and generateId + */ +std::vector Profile::dumpData() const +{ + cmsUInt32Number len = 0; + if (!cmsSaveProfileToMem(_handle, nullptr, &len)) { + throw CmsError("Can't extract profile data"); + } + auto buf = std::vector(len); + cmsSaveProfileToMem(_handle, &buf.front(), &len); + return buf; +} + +} // 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/profile.h b/src/colors/cms/profile.h new file mode 100644 index 0000000000..b6b40fd993 --- /dev/null +++ b/src/colors/cms/profile.h @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * + * Authors: see git history + * + * Copyright (C) 2023 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_COLORS_CMS_PROFILE_H +#define SEEN_COLORS_CMS_PROFILE_H + +#include +#include // cmsHPROFILE +#include +#include +#include + +namespace Inkscape::Colors::CMS { + +class Profile +{ +public: + static std::shared_ptr create(cmsHPROFILE handle, std::string path = "", bool in_home = false); + static std::shared_ptr create_from_uri(std::string path, bool in_home = false); + static std::shared_ptr create_from_data(std::string const &contents); + static std::shared_ptr create_srgb(); + + static bool sortByName(std::shared_ptr const &p1, std::shared_ptr const &p2) + { + return p1->getName() < p2->getName(); + } + static bool sortById(std::shared_ptr const &p1, std::shared_ptr const &p2) + { + return p1->_id < p2->_id; + } + + Profile(cmsHPROFILE handle, std::string path, bool in_home); + ~Profile() { cmsCloseProfile(_handle); } + Profile(Profile const &) = delete; + Profile &operator=(Profile const &) = delete; + bool operator==(Profile const &other) const { return _checksum == other._checksum; } + + cmsHPROFILE getHandle() const { return _handle; } + std::string const &getPath() const { return _path; } + bool inHome() const { return _in_home; } + + bool isForDisplay() const; + const std::string &getId() const { return _id; } + const std::string &getChecksum() const { return _checksum; } + std::string getName(bool sanitize = false) const; + unsigned int getSize() const; + cmsColorSpaceSignature getColorSpace() const; + cmsProfileClassSignature getProfileClass() const; + + static bool isIccFile(std::string const &filepath); + std::string dumpBase64() const; + std::vector dumpData() const; + +private: + cmsHPROFILE _handle; + std::string _path; + std::string _id; + std::string _checksum; + bool _in_home = false; + + std::string generate_id() const; + std::string generate_checksum() const; +}; + +} // namespace Inkscape::Colors::CMS + +#endif // SEEN_COLORS_CMS_PROFILE_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/system.cpp b/src/colors/cms/system.cpp new file mode 100644 index 0000000000..75c1cffd61 --- /dev/null +++ b/src/colors/cms/system.cpp @@ -0,0 +1,277 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * A class to provide access to system/user ICC color profiles. + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "system.h" + +#include // home-dir and filename building +#include + +#include "io/resource.h" +#include "profile.h" +#include "transform.h" + +// clang-format off +#ifdef _WIN32 +#undef NOGDI +#include +#include +#endif +// clang-format on + +namespace Inkscape::Colors::CMS { + +System::System() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + _prefs_observer = prefs->createObserver("/options/displayprofile", [this]() { + _display_profile.reset(); + _display_transform.reset(); + }); +} + +/** + * Search for system ICC profile files and add them to list. + */ +void System::refreshProfiles() +{ + _profiles.clear(); // Allows us to refresh list if necessary. + + // Get list of all possible file directories, with flag if they are "home" directories or not. + // Look for icc files in specified directories. + for (auto const &directory_path : getDirectoryPaths()) { + using Inkscape::IO::Resource::get_filenames; + for (auto &&filename : get_filenames(directory_path.first, {".icc", ".icm"})) { + // Check if files are ICC files and extract out basic information, add to list. + if (!Profile::isIccFile(filename)) { + g_warning("System::load_profiles: '%s' is not an ICC file!", filename.c_str()); + continue; + } + + auto profile = Profile::create_from_uri(std::move(filename), directory_path.second); + + for (auto const &other : _profiles) { + if (other->getName() == profile->getName() && other->getId() != profile->getId()) { + std::cerr << "System::load_profiles: Different ICC profile with duplicate name: " + << profile->getName() << ":" << std::endl; + std::cerr << " " << profile->getPath() << " (" << profile->getId() << ")" << std::endl; + std::cerr << " " << other->getPath() << " (" << other->getId() << ")" << std::endl; + return; + } + } + _profiles.emplace_back(std::move(profile)); + } + } +} + +static DirPaths get_directory_paths() +{ + DirPaths paths; + + // First try user's local directory. + paths.emplace_back(Glib::build_filename(Glib::get_user_data_dir(), "color", "icc"), true); + + // See + // https://github.com/hughsie/colord/blob/fe10f76536bb27614ced04e0ff944dc6fb4625c0/lib/colord/cd-icc-store.c#L590 + + // User store + paths.emplace_back(Glib::build_filename(Glib::get_user_data_dir(), "icc"), true); + paths.emplace_back(Glib::build_filename(Glib::get_home_dir(), ".color", "icc"), true); + + // System store + paths.emplace_back("/var/lib/color/icc", false); + paths.emplace_back("/var/lib/colord/icc", false); + + auto data_directories = Glib::get_system_data_dirs(); + for (auto const &data_directory : data_directories) { + paths.emplace_back(Glib::build_filename(data_directory, "color", "icc"), false); + } + +#ifdef __APPLE__ + paths.emplace_back("/System/Library/ColorSync/Profiles", false); + paths.emplace_back("/Library/ColorSync/Profiles", false); + + paths.emplace_back(Glib::build_filename(Glib::get_home_dir(), "Library", "ColorSync", "Profiles"), true); +#endif // __APPLE__ + +#ifdef _WIN32 + wchar_t pathBuf[MAX_PATH + 1]; + pathBuf[0] = 0; + DWORD pathSize = sizeof(pathBuf); + g_assert(sizeof(wchar_t) == sizeof(gunichar2)); + if (GetColorDirectoryW(NULL, pathBuf, &pathSize)) { + auto utf8Path = g_utf16_to_utf8((gunichar2 *)(&pathBuf[0]), -1, NULL, NULL, NULL); + if (!g_utf8_validate(utf8Path, -1, NULL)) { + g_warning("GetColorDirectoryW() resulted in invalid UTF-8"); + } else { + paths.emplace_back(utf8Path, false); + } + g_free(utf8Path); + } +#endif // _WIN32 + + return paths; +} + +/** + * Create list of all directories where ICC profiles are expected to be found. + */ +DirPaths const &System::getDirectoryPaths() +{ + if (_paths.empty()) { + _paths = get_directory_paths(); + } + return _paths; +} + +/** + * Remove all directory paths that might have been generated (useful for refreshing) + */ +void System::clearDirectoryPaths() +{ + _paths.clear(); +} + +/** + * Replace all generated profile paths with this single path, useful for testing. + */ +void System::addDirectoryPath(std::string path, bool is_user) +{ + _paths.emplace_back(std::move(path), is_user); +} + +/** + * Returns a list of profiles sorted by their internal names. + */ +std::vector> System::getProfiles() const +{ + auto result = _profiles; // copy + std::sort(result.begin(), result.end(), Profile::sortByName); + return result; +} + +/** + * Get the user set display profile, if set. + */ +const std::shared_ptr &System::getDisplayProfile(bool &updated) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + std::string uri = prefs->getString("/options/displayprofile/uri"); + + if (!uri.empty() && (!_display_profile || _display_profile->getPath() != uri)) { + auto profile = Profile::create_from_uri(uri, false); + if (!profile->isForDisplay()) { + g_warning("System::get_display_profile: Not a display (display) profile: %s", uri.c_str()); + } else { + updated = true; + _display_profile = profile; + } + } + return _display_profile; +} + +/** + * Returns a list of profiles that can apply to the display (display), sorted by their internal names. + */ +std::vector> System::getDisplayProfiles() const +{ + std::vector> result; + result.reserve(_profiles.size()); + + for (auto const &profile : _profiles) { + if (profile->isForDisplay()) { + result.push_back(profile); + } + } + std::sort(result.begin(), result.end(), Profile::sortByName); + return result; +} + +/** + * Return vector of profiles which can be used for cms output + */ +std::vector> System::getOutputProfiles() const +{ + std::vector> result; + result.reserve(_profiles.size()); + + for (auto const &profile : _profiles) { + if (profile->getProfileClass() == cmsSigOutputClass) { + result.push_back(profile); + } + } + std::sort(result.begin(), result.end(), Profile::sortByName); + return result; +} + +/** + * Return the profile object which is matched by the given name, id or path + * + * @arg name - A string that can contain either the profile name as stored in the icc file + * the ID which is a hex value different for each version of the profile also + * stored in the icc file. Or the path where the profile was found. + * @returns A pointer to the profile object, or nullptr + */ +const std::shared_ptr &System::getProfile(std::string const &name) const +{ + for (auto const &profile : _profiles) { + if (name == profile->getName() || name == profile->getId() || name == profile->getPath()) { + return profile; + } + } + static std::shared_ptr not_found; + return not_found; +} + +/** + * Get the color managed trasform 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 = Transform::create_for_cairo(Profile::create_srgb(), display_profile); + } else { + _display_transform = nullptr; + } + } + return _display_transform; +} + +} // 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/system.h b/src/colors/cms/system.h new file mode 100644 index 0000000000..94379142e0 --- /dev/null +++ b/src/colors/cms/system.h @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Access operating system wide information about color management. + *//* + * Authors: see git history + * + * Copyright (C) 2017 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_COLORS_CMS_SYSTEM_H +#define SEEN_COLORS_CMS_SYSTEM_H + +#include +#include +#include +#include +#include + +#include "preferences.h" + +using DirPaths = std::vector>; + +namespace Inkscape::Colors::CMS { + +class Profile; +class Transform; +class System +{ +private: + System(System const &) = delete; + void operator=(System const &) = delete; + +public: + // Access the singleton CMS::System object. + static System &get() + { + static System instance; + return instance; + } + + DirPaths const &getDirectoryPaths(); + void addDirectoryPath(std::string path, bool is_user); + void clearDirectoryPaths(); + + std::vector> getProfiles() const; + const std::shared_ptr &getProfile(std::string const &name) const; + + std::vector> getDisplayProfiles() const; + const std::shared_ptr &getDisplayProfile(bool &updated); + const std::shared_ptr &getDisplayTransform(); + + std::vector> getOutputProfiles() const; + + void refreshProfiles(); + + System(); + ~System() = default; + + // Used by testing to add profiles when needed + void addProfile(std::shared_ptr profile) { _profiles.emplace_back(profile); } + +private: + // List of ICC profiles found on system + std::vector> _profiles; + + // Paths to search for icc profiles + DirPaths _paths; + + // We track last display transform settings. If there is a change, we delete create new transform. + std::shared_ptr _display_profile; + std::shared_ptr _display_transform; + bool _display = false; + int _display_intent = -1; + + Inkscape::PrefObserver _prefs_observer; +}; + +class CmsError : public std::exception +{ +public: + CmsError(std::string &&msg) + : _msg(msg) + {} + char const *what() const noexcept override { return _msg.c_str(); } + +private: + std::string _msg; +}; + +} // namespace Inkscape::Colors::CMS + +#endif // SEEN_COLORS_CMS_SYSTEM_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 new file mode 100644 index 0000000000..78b670ff89 --- /dev/null +++ b/src/colors/cms/transform.cpp @@ -0,0 +1,275 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * A C++ wrapper for lcms2 transforms + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "transform.h" + +#include +#include +#include +#include + +#include "colors/color.h" +#include "profile.h" + +namespace Inkscape::Colors::CMS { + +/** + * Construct a color transform object from the lcms2 object. + */ +std::shared_ptr const Transform::create(cmsHTRANSFORM handle, bool global) +{ + return handle ? std::make_shared(handle, global) : nullptr; +} + +/** + * 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. + */ +std::shared_ptr const Transform::create_for_cairo(std::shared_ptr const &from, + std::shared_ptr const &to, + std::shared_ptr const &proof, + RenderingIntent proof_intent, bool with_gamut_warn) +{ + if (!to || !from) + return nullptr; + + auto cms_context = cmsCreateContext(nullptr, nullptr); + + if (proof) { + unsigned int flags = cmsFLAGS_SOFTPROOFING | (with_gamut_warn ? cmsFLAGS_GAMUTCHECK : 0); + unsigned int lt = lcms_intent(proof_intent, flags); + + return create(cmsCreateProofingTransformTHR(cms_context, from->getHandle(), TYPE_BGRA_8, to->getHandle(), + TYPE_BGRA_8, proof->getHandle(), INTENT_PERCEPTUAL, lt, flags)); + } + return create(cmsCreateTransformTHR(cms_context, from->getHandle(), TYPE_BGRA_8, to->getHandle(), TYPE_BGRA_8, + INTENT_PERCEPTUAL, 0)); +} + +/** + * Construct a transformation suitable for Space::CMS transformations using the given rendering intent + * + * @arg from - The CMS Profile the color data will start in + * @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. + */ +std::shared_ptr const Transform::create_for_cms(std::shared_ptr const &from, + std::shared_ptr const &to, RenderingIntent intent) +{ + if (!to || !from) + return nullptr; + unsigned int flags = 0; + unsigned int lt = lcms_intent(intent, flags); + + // Format is 16bit integer in whatever color space it's in. + auto from_format = cmsFormatterForColorspaceOfProfile(from->getHandle(), 2, false); + auto to_format = cmsFormatterForColorspaceOfProfile(to->getHandle(), 2, false); + return create(cmsCreateTransform(from->getHandle(), from_format, to->getHandle(), to_format, lt, flags)); +} + +/** + * Construct a transformation suitable for gamut checking Space::CMS colors. + * + * @arg from - The CMS Profile the color data will start in + * @arg to - The target CMS Profile the color data needs to end up in. + */ +std::shared_ptr const Transform::create_for_cms_checker(std::shared_ptr const &from, + std::shared_ptr const &to) +{ + if (!to || !from) + return nullptr; + + static cmsContext check_context = nullptr; + if (!check_context) { + // Create an lcms context just for checking out of gamut colors, this can live as long as inkscape. + check_context = cmsCreateContext(nullptr, nullptr); + cmsUInt16Number alarmCodes[cmsMAXCHANNELS] = {0, 0, 0, 0, 0}; + cmsSetAlarmCodesTHR(check_context, alarmCodes); + } + // Format is 16bit integer in whatever color space it's in. + auto from_format = cmsFormatterForColorspaceOfProfile(from->getHandle(), 2, false); + return create(cmsCreateProofingTransformTHR(check_context, from->getHandle(), from_format, + from->getHandle(), from_format, to->getHandle(), + INTENT_RELATIVE_COLORIMETRIC, INTENT_RELATIVE_COLORIMETRIC, + cmsFLAGS_GAMUTCHECK | cmsFLAGS_SOFTPROOFING), + true); +} + +/** + * Set the gamut alarm code for this cms transform (and only this one). + * + * NOTE: If the transform doesn't have a context because it was created for cms color + * transforms instead of cairo transforms, this won't do anything. + * + * @arg input - The values per channel in the _output_ to use. For example if the transform + * is RGB to CMYK, the input vector should be four channels in size. + * + */ +void Transform::set_gamut_warn(std::vector const &input) +{ + std::vector color(_channels_in); + for (unsigned int i = 0; i < _channels_in; i++) { + // double to uint16 conversion + color[i] = input.size() > i ? input[i] * 65535 : 0; + } + if (_context) { + cmsSetAlarmCodesTHR(_context, &color.front()); + } +} + +/** + * Wrap lcms2 cmsDoTransform to transform the pixel buffer's color channels. + * + * @arg inBuf - The input pixel buffer to transform. + * @arg outBug - The output pixel buffer, which can be the same as inBuf. + * @arg size - The size of the buffer to transform. + */ +void Transform::do_transform(unsigned char *inBuf, unsigned char *outBuf, unsigned size) const +{ + if ((cmsGetTransformInputFormat(_handle) & TYPE_BGRA_8) != TYPE_BGRA_8 || + (cmsGetTransformOutputFormat(_handle) & TYPE_BGRA_8) != TYPE_BGRA_8) { + throw ColorError("Using a color-channel transform object to do a cairo transform operation!"); + } + + cmsDoTransform(_handle, inBuf, outBuf, size); +} + +/** + * 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 Transform::do_transform(cairo_surface_t *in, cairo_surface_t *out) const +{ + cairo_surface_flush(in); + + auto px_in = cairo_image_surface_get_data(in); + auto px_out = cairo_image_surface_get_data(out); + + int stride = cairo_image_surface_get_stride(in); + int width = cairo_image_surface_get_width(in); + int height = cairo_image_surface_get_height(in); + + if (stride != cairo_image_surface_get_stride(out) || width != cairo_image_surface_get_width(out) || + height != cairo_image_surface_get_height(out)) { + throw ColorError("Different image formats while applying CMS!"); + } + + for (int i = 0; i < height; i++) { + auto row_in = px_in + i * stride; + auto row_out = px_out + i * stride; + do_transform(row_in, row_out, width); + } + + 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 Transform::do_transform(Cairo::RefPtr &in, Cairo::RefPtr &out) const +{ + do_transform(in->cobj(), out->cobj()); +} + +/** + * Apply the CMS transform to a single Color object's data. + * + * @arg io - The input/output color as a vector of numbers between 0.0 and 1.0 + * + * @returns the modified color in io + */ +bool Transform::do_transform(std::vector &io) const +{ + std::vector input(_channels_in); + std::vector output(_channels_out); + + for (unsigned int a = 0; a < _channels_in; a++) { + // double to uint16 conversion + input[a] = io[0] * 65535; + io.erase(io.begin()); + } + cmsDoTransform(_handle, &input.front(), &output.front(), 1); + + // Preserve any extra non-color channels (i.e. transparency) by inserting + // into the front of the vector instead of the end. + for (auto &val : boost::adaptors::reverse(output)) { + io.insert(io.begin(), val / 65535.0); + } + return true; +} + +/** + * Return true if the input color is outside of the gamut if it was transformed using + * this cms transform. + * + * @arg input - The input color as a vector of numbers between 0.0 and 1.0 + */ +bool Transform::check_gamut(std::vector const &input) const +{ + cmsUInt16Number in[cmsMAXCHANNELS]; + cmsUInt16Number out[cmsMAXCHANNELS]; + for (unsigned int i = 0; i < cmsMAXCHANNELS; i++) { + in[i] = input.size() > i ? input[i] * 65535 : 0; + out[i] = 0; + } + cmsDoTransform(_handle, &in, &out, 1); + + // All channels are set to zero in the alarm context, so the result is zero if out of gamut. + return std::accumulate(std::begin(out), std::end(out), 0, std::plus()) == 0; +} + +/** + * Get the lcms2 intent enum from the inkscape intent enum + * + * @args intent - The Inkscape RenderingIntent enum + * + * @returns flags - Any flags modifications for the given intent + * @returns lcms intent enum, default is INTENT_PERCEPTUAL + */ +unsigned int Transform::lcms_intent(RenderingIntent intent, unsigned int &flags) +{ + switch (intent) { + case RenderingIntent::RELATIVE_COLORIMETRIC: + // Black point compensation only matters to relative colorimetric + flags |= cmsFLAGS_BLACKPOINTCOMPENSATION; + case RenderingIntent::RELATIVE_COLORIMETRIC_NOBPC: + return INTENT_RELATIVE_COLORIMETRIC; + case RenderingIntent::SATURATION: + return INTENT_SATURATION; + case RenderingIntent::ABSOLUTE_COLORIMETRIC: + return INTENT_ABSOLUTE_COLORIMETRIC; + case RenderingIntent::PERCEPTUAL: + case RenderingIntent::UNKNOWN: + case RenderingIntent::AUTO: + default: + return INTENT_PERCEPTUAL; + } +} + +} // 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.h b/src/colors/cms/transform.h new file mode 100644 index 0000000000..3377914553 --- /dev/null +++ b/src/colors/cms/transform.h @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: see git history + * + * Copyright (C) 2017 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_COLORS_CMS_TRANSFORM_H +#define SEEN_COLORS_CMS_TRANSFORM_H + +#include +#include +#include // cmsHTRANSFORM +#include +#include + +#include "colors/spaces/enum.h" + +namespace Inkscape::Colors::CMS { + +class Profile; +class Transform +{ +public: + static std::shared_ptr const create(cmsHTRANSFORM handle, bool global = false); + static std::shared_ptr const create_for_cairo(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); + static std::shared_ptr const create_for_cms(std::shared_ptr const &from, + std::shared_ptr const &to, RenderingIntent intent); + static std::shared_ptr const create_for_cms_checker(std::shared_ptr const &from, + std::shared_ptr const &to); + + Transform(cmsHTRANSFORM handle, bool global = false) + : _handle(handle) + , _context(!global ? cmsGetTransformContextID(handle) : nullptr) + , _channels_in(T_CHANNELS(cmsGetTransformInputFormat(handle))) + , _channels_out(T_CHANNELS(cmsGetTransformOutputFormat(handle))) + { + assert(_handle); + } + ~Transform() + { + cmsDeleteTransform(_handle); + if (_context) + cmsDeleteContext(_context); + } + Transform(Transform const &) = delete; + Transform &operator=(Transform const &) = delete; + + cmsHTRANSFORM getHandle() const { return _handle; } + + void do_transform(unsigned char *inBuf, unsigned char *outBuf, unsigned size) const; + void do_transform(cairo_surface_t *in, cairo_surface_t *out) const; + void do_transform(Cairo::RefPtr &in, Cairo::RefPtr &out) const; + bool do_transform(std::vector &io) const; + + void set_gamut_warn(std::vector const &input); + bool check_gamut(std::vector const &input) const; + +private: + cmsHTRANSFORM _handle; + cmsContext _context; + + static unsigned int lcms_intent(RenderingIntent intent, unsigned int &flags); + +public: + unsigned int _channels_in; + unsigned int _channels_out; +}; + +} // namespace Inkscape::Colors::CMS + +#endif // SEEN_COLORS_CMS_TRANSFORM_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/color-set.cpp b/src/colors/color-set.cpp new file mode 100644 index 0000000000..90152ae961 --- /dev/null +++ b/src/colors/color-set.cpp @@ -0,0 +1,402 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * A set of colors which can be modified together used for color pickers + *//* + * Copyright (C) 2024 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "color-set.h" + +#include + +#include "colors/color.h" +#include "colors/spaces/base.h" +#include "colors/spaces/components.h" + +namespace Inkscape::Colors { + +/** + * Construct a new ColorSet object to contain a group of colors which will + * be modified collectively. + * + * @arg space - optionally contrain each color added to be in this color space + * @arg alpha - optionally contrain each color to have or not have an alpha channel + */ +ColorSet::ColorSet(std::shared_ptr space, std::optional alpha) + : _space_constraint(std::move(space)) + , _alpha_constraint(alpha) +{} + +/** + * Get a list of components for the color space set to this color set. + */ +Space::Components const &ColorSet::getComponents() const +{ + if (!_space_constraint) + throw ColorError("Components are only available on a color space constrained ColorSet."); + return _space_constraint->getComponents(_alpha_constraint && *_alpha_constraint); +} + +/** + * Reset the color set and remove all colors from it. + */ +void ColorSet::clear() +{ + if (!_colors.empty()) { + _colors.clear(); + colors_cleared(); + } +} + +/** + * Set this color to being grabbed for a continuous set of changes. + */ +void ColorSet::grab() +{ + if (!_blocked && !_grabbed) { + block(); + signal_grabbed.emit(); + unblock(); + _grabbed = true; + } +} + +/** + * Set the color as being released from continuous changes. + */ +void ColorSet::release() +{ + if (!_blocked && _grabbed) { + _grabbed = false; + block(); + signal_released.emit(); + unblock(); + } +} + +/** + * Called when the colors change in some way. + */ +void ColorSet::colors_changed() +{ + // Send signals about this change + if (!_blocked) { + block(); + signal_changed.emit(); + unblock(); + } +} + +/** + * Called when the list of colors changes (add or clear) + */ +void ColorSet::colors_cleared() +{ + if (!_blocked) { + block(); + signal_cleared.emit(); + unblock(); + } +} + +/** + * Returns true if all of the colors are the same color. + */ +bool ColorSet::isSame() const +{ + if (_colors.empty()) + return true; + for (auto const &[id, color] : _colors) { + if (color != _colors[0].second) + return false; + } + return true; +} + +/** + * Overwrite all colors so they equal the the new color + * + * @arg other - The other color to replace all colors in this set with. + * + * @returns the number of colors changed. + */ +unsigned ColorSet::setAll(Color const &other) +{ + unsigned changed = 0; + for (auto &[id, color] : _colors) { + auto was = color; + color.set(other, true); + // Comparison after possible color space conversion + changed += (was != color); + } + if (changed) { + colors_changed(); + } + return changed; +} + +/** + * Set each of the colors from the other color set by id. Creating new entries where the id is not found. + * + * @arg other - The other color set to find colors from. + * + * @returns the number of colors changed or added + */ +unsigned ColorSet::setAll(ColorSet const &other) +{ + unsigned changed = 0; + for (auto &[id, color] : other) { + changed += _set(id, color); + } + if (changed > 0) { + colors_changed(); + } + return changed; +} + +/** + * Set a single color in the color set by its id. + * + * @arg id - The id to use, if this id isn't set a new item is created. + * @arg other - The other color to replace all colors in this set with. + * + * @returns true if this color was changed. + */ +bool ColorSet::set(std::string id, Color const &other) +{ + if (_set(std::move(id), other)) { + colors_changed(); + return true; + } + return false; +} + +/** + * Remove any other colors and set to just this one color. + * + * @arg color - The color to set this color-set to. + * + * @returns true if the color was new or changed. + */ +bool ColorSet::set(Color const &other) +{ + // Always clear the colors if it's being used differently + if (_colors.size() != 1 || _colors[0].first != "single") + _colors.clear(); + return set("single", other); +} + +/** + * Get the color if there is only one color set with set(Color) + * + * @returns the available color, normalized + */ +std::optional ColorSet::get() const +{ + return get("single"); +} + +/* + * Internal function for setting a color by id without calling the changed singal. + */ +bool ColorSet::_set(std::string id, Color const &other) +{ + for (auto &[cid, color] : _colors) { + if (cid == id) { + auto was = color; + color.set(other, true); + return was != color; + } + } + // Add a new entry for this id + Color copy = other; + + // Enforce constraints on space and alpha if any + if (_space_constraint) + copy.convert(_space_constraint); + + if (_alpha_constraint) { + copy.enableOpacity(*_alpha_constraint); + } + + _colors.emplace_back(std::move(id), copy); + return true; +} + +/** + * Return a single color by it's index. The color will be normalized + * before returning a copy as some functions can modify colors out + * of bounds. + * + * @arg index - The id of the color used in ColorSet::set() + * + * @returns a normalized color object from the given index or none if the id is not found. + */ +std::optional ColorSet::get(std::string const &id) const +{ + for (auto &[cid, color] : _colors) { + if (cid == id) + return color.normalized(); + } + return {}; +} + +/** + * Set this one component to this specific value for all colors. + * + * @arg index - The index to replace, will cause an error if out of bounds. + * @arg other - The other color to replace all colors in this set with. + * + * @returns the number of colors changed. + */ +unsigned ColorSet::setAll(Space::Component const &c, double value) +{ + if (!_space_constraint || _space_constraint->getType() != c.type) + throw ColorError("Incompatible color component used in ColorSet::set."); + unsigned changed = 0; + for (auto &[id, color] : _colors) { + auto was = color; + color.set(c.index, value); + // Comparison after possible color space conversion + changed += (was != color); + } + if (changed) { + colors_changed(); + } + return changed; +} + +/** + * Set the average value in this component by taking the average + * finding the delta and moving all colors by the given delta. + * + * This will not run normalization so out of bound changes can remember + * their values until the mutation period is finished and normalization + * is run on the returned colors. see ColorSet::get(). + * + * @arg c - The component to change the average for + * @arg value - The new average value that the set will calculate to + */ +void ColorSet::setAverage(Space::Component const &c, double value) +{ + if (!_space_constraint || _space_constraint->getType() != c.type) + throw ColorError("Incompatible color component used in ColorSet::moveAverageTo."); + auto delta = value - getAverage(c); + for (auto &[id, color] : _colors) { + color.set(c.index, color[c.index] + delta); + } + colors_changed(); +} + +/** + * Get the average value for this component across all colors. + * + * @arg c - The component to get the average for + * + * @returns the normalized average value for all colors in this set. + */ +double ColorSet::getAverage(Space::Component const &c) const +{ + if (!_space_constraint || _space_constraint->getType() != c.type) + throw ColorError("Incompatible color component used in ColorSet::get."); + double value = 0.0; + for (auto const &[id, color] : _colors) { + value += color[c.index]; + } + return c.normalize(value / _colors.size()); +} + +/** + * Get a list of all normalized values for this one component. + * + * @arg c - The component to collect all the values for. + * + * @returns a list of normalized values in this component. + */ +std::vector ColorSet::getAll(Space::Component const &c) const +{ + if (!_space_constraint || _space_constraint->getType() != c.type) + throw ColorError("Incompatible color component used in ColorSet::getAll."); + std::vector ret(_colors.size()); + std::transform(_colors.begin(), _colors.end(), ret.begin(), + [c](auto &iter) { return c.normalize(iter.second[c.index]); }); + return ret; +} + +/** + * Return the best color space from this collection of colors. If the color + * space is constrained then the result will be that space. Otherwise picks + * the space with the most colors. + */ +std::shared_ptr ColorSet::getBestSpace() const +{ + if (_space_constraint) + return _space_constraint; + + unsigned biggest = 0; + std::shared_ptr ret; + std::map, unsigned> counts; + for (auto const &[id, color] : _colors) { + if (++counts[color.getSpace()] > biggest) { + biggest = counts[color.getSpace()]; + ret = color.getSpace(); + } + } + return ret; +} + +/** + * Return the average color between this set of colors. + * + * If space is not constrained, it will return the average in the best color space. + * If alpha is not constrained, the average will always include an alpha channel + */ +Color ColorSet::getAverage() const +{ + if (isEmpty()) + throw ColorError("Can't get the average color of no colors."); + + auto avg_space = getBestSpace(); + auto avg_alpha = _alpha_constraint.value_or(true); + + std::vector values(avg_space->getComponentCount() + avg_alpha); + + for (auto const &[id, color] : _colors) { + // Colors::Color will return the alpha channel as 1.0 if it doesn't exist. + if (color.getSpace() == avg_space) { + for (unsigned int i = 0; i < values.size(); i++) { + values[i] += color[i]; + } + } else if (auto copy = color.converted(avg_space)) { + for (unsigned int i = 0; i < values.size(); i++) { + values[i] += (*copy)[i]; + } + } + } + for (double &value : values) { + value /= _colors.size(); + } + return Color(avg_space, values); +} + +/** + * Return the number of items in the color set. + */ +unsigned ColorSet::size() const +{ + return _colors.size(); +} + +} // namespace Inkscape::Colors + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 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/color-set.h b/src/colors/color-set.h new file mode 100644 index 0000000000..9cfdf3fb55 --- /dev/null +++ b/src/colors/color-set.h @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * A set of colors which can be modified together used for color pickers + *//* + * Copyright (C) 2024 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_COLORS_COLOR_SET +#define SEEN_COLORS_COLOR_SET + +#include +#include +#include +#include + +#include "helper/auto-connection.h" + +namespace Inkscape::Colors { +class Color; +namespace Space { +class AnySpace; +class Components; +class Component; +} // namespace Space + +using IdColors = std::vector>; + +class ColorSet +{ +public: + ColorSet(std::shared_ptr space = {}, std::optional alpha = {}); + + // By default, disallow copy constructor + ColorSet(ColorSet const &obj) = delete; + + IdColors::const_iterator begin() const { return std::begin(_colors); } + IdColors::const_iterator end() const { return std::end(_colors); } + IdColors::iterator begin() { return std::begin(_colors); } + IdColors::iterator end() { return std::end(_colors); } + + // Signals allow the set to be tied into various interfaces + bool isBlocked() const { return _blocked; } + bool isGrabbed() const { return _grabbed; } + + sigc::signal signal_grabbed; + sigc::signal signal_released; + sigc::signal signal_changed; + sigc::signal signal_cleared; + + void grab(); + void release(); + void block() { _blocked = true; } + void unblock() { _blocked = false; } + + // Information about the color set's meta data + Space::Components const &getComponents() const; + std::shared_ptr const getSpaceConstraint() const { return _space_constraint; } + std::optional const getAlphaConstraint() const { return _alpha_constraint; } + + // Set and get a single color (i.e. not a list of colors) + bool set(Color const &color); + std::optional get() const; + void clear(); + + // Control the list of colors by id + std::optional get(std::string const &id) const; + bool set(std::string id, Color const &color); + + // Change the colors in some way and send signals + unsigned setAll(ColorSet const &other); + unsigned setAll(Color const &other); + unsigned setAll(Space::Component const &c, double value); + std::vector getAll(Space::Component const &c) const; + + // Set and get specific color components + void setAverage(Space::Component const &c, double value); + double getAverage(Space::Component const &c) const; + Color getAverage() const; + + // Ask for information about the whole set of colors + unsigned size() const; + + bool isEmpty() const { return _colors.empty(); } + bool isSame() const; + std::shared_ptr getBestSpace() const; + +private: + bool _set(std::string id, Color const &color); + void colors_changed(); + void colors_cleared(); + + IdColors _colors; + + // Constraints can only be set up at construction so are immutable. + std::shared_ptr const _space_constraint; + std::optional const _alpha_constraint; + + // States which are set during the lifetime of the set + bool _grabbed = false; + bool _blocked = false; +}; + +} // namespace Inkscape::Colors + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/colors/color.cpp b/src/colors/color.cpp new file mode 100644 index 0000000000..d77a7df977 --- /dev/null +++ b/src/colors/color.cpp @@ -0,0 +1,629 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Martin Owens + * + * Copyright (C) 2023 AUTHORS + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "colors/color.h" + +#include +#include +#include + +#include "colors/manager.h" +#include "spaces/base.h" +#include "spaces/components.h" + +namespace Inkscape::Colors { +namespace { + +template +static decltype(auto) assert_nonnull(T &&t) +{ + assert(t); + return std::forward(t); +} + +} // namespace + +/** + * Create a color, given the type of the space and the values to store. + * + * @arg space_type - The type of the color space these values are in. + * @arg values - A vector of numbers usually between 0.0 and 1.0 per channel + * which will be moved to the new Color object. + */ +Color::Color(Space::Type space_type, std::vector values) + : Color(assert_nonnull(Manager::get().find(space_type)), std::move(values)) +{} + +/** + * Compatability layer for making blind RGB colors + */ +Color::Color(uint32_t rgba, bool opacity) + : Color(Space::Type::RGB, rgba_to_values(rgba, opacity)) +{} + +/** + * Construct a color in the given color space. + * + * @arg space - The color space these channel values exist in. + * @arg colors - Each channel in the color space must have a value between 0.0 and 1.0 + * an extra value may be appended to indicate the opacity to support CSS + * formatted opacity parsing but is not expected to be writen by Inkscape + * when being generated. + */ +Color::Color(std::shared_ptr space, std::vector colors) + : _values(std::move(colors)) + , _space(std::move(space)) +{ + assert(_space->isValidData(_values)); +} + +/** + * Return true if the two colors are the same. + * + * The color space AND the values must be the same. But the name doesn't not + * have to be the same in both colors. + */ +bool Color::operator==(Color const &other) const +{ + return _space == other._space && _isnear(other._values); +} + +/** + * Get a single channel from this color. + */ +double Color::get(unsigned index) const +{ + assert(index <= getOpacityChannel()); + if (index < _values.size()) { + return _values[index]; + } else { + return 1.0; + } +} + +/** + * Format the color as a css string and return it. + * + * @arg opacity - If set to false the opacity will be ignored, even if present. + * + * Note: Choose the color space for your color carefully before printing. If you + * are outputting to a CSS version that only supports RGB hex codes, then convert + * the color to that color space before printing. + */ +std::string Color::toString(bool opacity) const +{ + return _space->toString(_values, opacity); +} + +/** + * Return an sRGB conversion of the color in RGBA int32 format. + * + * @args opacity - optional opacity to be mixed into any existing + * opacity in this color. + */ +uint32_t Color::toRGBA(double opacity) const +{ + return _space->toRGBA(_values, opacity); +} + +/** + * Return the RGBA int32 as an ARGB format number. + */ +uint32_t Color::toARGB(double opacity) const +{ + auto value = toRGBA(opacity); + return (value >> 8) | ((value & 0xff) << 24); +} + +/** + * Return the RGBA int32 as an ABGR format color. + */ +uint32_t Color::toABGR(double opacity) const +{ + auto value = toRGBA(opacity); + return (value << 24) | ((value << 8) & 0x00ff0000) | ((value >> 8) & 0x0000ff00) | (value >> 24); +} + +/** + * Convert to the same format as the other color. + * + * @arg other - Another color to copy the space and opacity from. + */ +bool Color::convert(Color const &other) +{ + if (convert(other._space)) { + enableOpacity(other.hasOpacity()); + return true; + } + return false; +} + +/** + * Convert this color into a different color space. + * + * @arg to_space - The target space to convert the color values to + */ +bool Color::convert(std::shared_ptr to_space) +{ + if (!to_space || !to_space->isValid()) { + return false; + } + + if (_space != to_space) { + _space->convert(_values, to_space); + _space = std::move(to_space); + assert(_space->isValidData(_values)); + } + _name = ""; + + return true; +} + +/** + * Convert this color into the first matched color space of the given type. + */ +bool Color::convert(Space::Type type) +{ + if (auto space = Manager::get().find(type)) { + return convert(space); + } + return false; +} + +/** + * Return a copy of this color converted to the same format as the other color. + */ +std::optional Color::converted(Color const &other) const +{ + Color copy = *this; + if (copy.convert(other)) { + return copy; + } + return {}; +} + +/** + * Convert a copy of this color into a different color space. + */ +std::optional Color::converted(std::shared_ptr to_space) const +{ + Color copy = *this; + if (copy.convert(std::move(to_space))) { + return copy; + } + return {}; +} + +/** + * Convert a copy of this color into the first matching color space type. + * + * if the space is not available in this color manager, an empty color is returned. + */ +std::optional Color::converted(Space::Type type) const +{ + Color copy = *this; + if (copy.convert(type)) { + return copy; + } + return {}; +} + +/** + * Set the channels directly without checking if the space is correct. + * + * @arg values - A vector of doubles, one value between 0.0 and 1.0 for + * each channel. An extra channel can be included for opacity. + */ +void Color::setValues(std::vector values) +{ + _name = ""; + _values = std::move(values); + assert(_space->isValidData(_values)); +} + +/** + * Set this color to the values from another color. + * + * @arg other - The other color which is a source for the values. + * if the other color is from an unknown color space which + * has never been seen before, it will cause an error. + * @arg keep_space - If true, this color's color-space will stay the same + * and the new values will be converted. This includes + * discarding opacity if this color didn't have opacity. + * + * @returns true if the new value if different from the old value + */ +bool Color::set(Color const &other, bool keep_space) +{ + if (keep_space) { + auto prev_space = _space; + auto prev_values = _values; + bool prev_opacity = hasOpacity(); + + if (set(other, false)) { + // Convert back to the previous space if needed. + convert(prev_space); + enableOpacity(prev_opacity); + // Return true if the converted result is different + return !_isnear(prev_values); + } + } else if (*this != other) { + _space = other._space; + _values = other._values; + _name = other._name; + return true; + } + return false; +} + +/** + * Set this color by parsing the given string. If there's a parser error + * it will not change the existing color. + * + * @arg parsable - A string with a typical css color value. + * @arg keep_space - If true, the existing space will stay the same (see previous Color::set) + * + * @returns true if the new value if different from the old value + */ +bool Color::set(std::string const &parsable, bool keep_space) +{ + if (auto color = Color::parse(parsable)) { + return set(*color, keep_space); + } + return false; +} + +/** + * Returns true if the values are near to the other values + */ +bool Color::_isnear(std::vector const &other, double epsilon) const +{ + bool is_near = _values.size() == other.size(); + for (size_t i = 0; is_near && i < _values.size(); i++) { + is_near &= std::abs(_values[i] - other[i]) < epsilon; + } + return is_near; +} + +/** + * Create an optional color if value is valid. + */ +std::optional Color::parse(char const *value) +{ + if (!value) { + return {}; + } + return parse(std::string(value)); +} + +/** + * Create an optional color, if possible, from the given string. + */ +std::optional Color::parse(std::string const &value) +{ + Space::Type space_type; + std::string cms_name; + std::vector values; + std::vector fallback; + if (Parsers::get().parse(value, space_type, cms_name, values, fallback)) { + return ifValid(space_type, std::move(values)); + } + // Couldn't be parsed as a color at all + return {}; +} + +/** + * Construct a color from the space type and values, if the values are valid + */ +std::optional Color::ifValid(Space::Type space_type, std::vector values) +{ + if (auto space = Manager::get().find(space_type)) { + if (space->isValidData(values)) { + return std::make_optional(std::move(space), std::move(values)); + } + } + // Invalid color data, return empty optional + return {}; +} + +/** + * Set a specific channel in the color. + * + * @arg index - The channel/component index to set + * @arg value - The new value to set it to. + * + * @returns true if the new value if different from the old value + */ +bool Color::set(unsigned int index, double value) +{ + assert(index <= getOpacityChannel()); + if (index == _values.size()) { + _values.push_back(1.0); + } + auto const changed = std::abs(_values[index] - value) >= 0.001; + _values[index] = value; + return changed; +} + +/** + * Set this color from an RGBA unsigned int. + * + * @arg rgba - The RGBA color encoded as a single unsigned integer, 8bpc + * @arg opacity - True if the opacity (Alpha) should be stored too. + * + * @returns true if the new value if different from the old value + */ +bool Color::set(uint32_t rgba, bool opacity) +{ + if (_space->getType() != Space::Type::RGB) { + // Ensure we are in RGB + _space = assert_nonnull(Manager::get().find(Space::Type::RGB)); + } else if (rgba == toRGBA(opacity)) { + return false; // nothing to do. + } + _name = ""; + _values = std::move(rgba_to_values(rgba, opacity)); + return true; +} + +/** + * Enables or disables the opacity channel. + */ +void Color::enableOpacity(bool enable) +{ + auto const has_opacity = hasOpacity(); + if (enable && !has_opacity) { + _values.push_back(1.0); + } else if (!enable && has_opacity) { + _values.pop_back(); + } +} + +/** + * Returns true if there is an opacity channel in this color. + */ +bool Color::hasOpacity() const +{ + return _values.size() > getOpacityChannel(); +} + +/** + * Get the opacity in this color, if it's stored. Returns 1.0 if no + * opacity exists in this color or 0.0 if this color is empty. + */ +double Color::getOpacity() const +{ + return hasOpacity() ? _values.back() : 1.0; +} + +/** + * Get the opacity, and remove it from this color. This is useful when setting a color + * into an svg css property that has it's own opacity property but you aren't ready to + * create a string (see toString(false) for that use) + */ +double Color::stealOpacity() +{ + auto ret = getOpacity(); + enableOpacity(false); + return ret; +} + +/** + * Get the opacity channel index + */ +unsigned int Color::getOpacityChannel() const +{ + return _space->getComponentCount(); +} + +/** + * Return the pin number (pow2) of the channel index to pin + * that channel in a mutation. + */ +unsigned int Color::getPin(unsigned int channel) const +{ + return 1 << channel; +} + +/** + * Set the opacity of this color object. + */ +bool Color::setOpacity(double opacity) +{ + if (hasOpacity()) { + if (opacity == _values.back()) { + return false; + } + _values.back() = opacity; + } else { + _values.emplace_back(opacity); + } + return true; +} + +/** + * Make sure the values for this color are within acceptable ranges. + */ +void Color::normalize() +{ + for (auto const &comp : _space->getComponents(hasOpacity())) { + _values[comp.index] = comp.normalize(_values[comp.index]); + } +} + +/** + * Return a normalized copy of this color so the values are within acceptable ranges. + */ +Color Color::normalized() const +{ + Color copy = *this; + copy.normalize(); + return copy; +} + +/** + * Invert the color for each channel. + * + * @arg pin - Bit field, which channels should not change if not specified + * the opacity pin is used as this is the most reasonable default. + */ +void Color::invert(unsigned int pin) +{ + for (unsigned int i = 0; i < _values.size(); i++) { + if (pin & (1 << i)) { + continue; + } + _values[i] = 1.0 - _values[i]; + } +} + +/** + * Jitter the color for each channel. + * + @ @arg force - The amount of jitter to add to each channel. + * @arg pin - Bit field, which channels should not change (see invert). + */ +void Color::jitter(double force, unsigned int pin) +{ + for (unsigned int i = 0; i < _values.size(); i++) { + if (pin & (1 << i)) { + continue; + } + // Random number between -0.5 and 0.5 times the force. + double r = (static_cast(std::rand()) / RAND_MAX - 0.5); + _values[i] += r * force; + } + normalize(); +} + +/* + * Modify this color to be the average between two colors, modifying the first. + * + * @arg other - The other color to average with + * @arg pos - The position (i.e. t) between the two colors. + * @pin pin - Bit field, which channels should not change (see invert) + */ +void Color::average(Color const &other, double pos, unsigned int pin) +{ + _color_mutate_inplace(other, pin, + [pos](auto &value, auto otherValue) { value = value * (1 - pos) + otherValue * pos; }); +} + +/** + * Return the average between this and another color. + * + * @arg other - The other color to average with + * @arg pos - The weighting to give each color + */ +Color Color::averaged(Color const &other, double pos) const +{ + Color copy = *this; + copy.average(other, pos); + return copy; +} + +/** + * Get the mean square difference between this color and another. + */ +double Color::difference(Color const &other) const +{ + double ret = 0.0; + if (auto copy = other.converted(*this)) { + for (unsigned int i = 0; i < _values.size(); i++) { + ret += std::pow(_values[i] - (*copy)[i], 2); + } + } + return ret; +} + +/** + * Find out if a color is a close match to another color of the same type. + * + * @returns true if the colors are the same structure (space, opacity) + * and have values which are no more than epison apart. + */ +bool Color::isClose(Color const &other, double epsilon) const +{ + bool match = _space == other._space && _values.size() == other._values.size(); + for (unsigned int i = 0; match && i < _values.size(); i++) { + match &= (std::fabs(_values[i] - other._values[i]) < epsilon); + } + return match; +} + +/** + * Find out if a color is similar to another color, converting it + * first if it's a different type. If one has opacity and the other does not + * it always returns false. + * + * @returns true if the colors are similar when converted to the same space. + */ +bool Color::isSimilar(Color const &other, double epsilon) const +{ + if (other._space != _space) { + if (auto copy = other.converted(_space)) { + return isClose(*copy, epsilon); + } + return false; // bad color conversion + } + return isClose(other, epsilon); +} + +/** + * Return true if this color would be out of gamut when converted to another space. + * + * @arg other - The other color space to compare against. + */ +bool Color::isOutOfGamut(std::shared_ptr other) const +{ + return _space->outOfGamut(_values, other); +} + +/** + * Return true if this color would be considered over-inked. + */ +bool Color::isOverInked() const +{ + return _space->overInk(_values); +} + +// Color-color in-place modification template +template +void Color::_color_mutate_inplace(Color const &other, unsigned int pin, Func func) +{ + // Convert the other's space and opacity if it's different + if (other._space != _space || other.hasOpacity() != hasOpacity()) { + if (auto copy = other.converted(*this)) { + return _color_mutate_inplace(*copy, pin, func); + } + return; // Bad conversion + } + + // Both are good, so average each channel + for (unsigned int i = 0; i < _values.size(); i++) { + if (pin & (1 << i)) { + continue; + } + func(_values[i], other[i]); + } +} + +}; // namespace Inkscape::Colors + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/colors/color.h b/src/colors/color.h new file mode 100644 index 0000000000..62b6094f8e --- /dev/null +++ b/src/colors/color.h @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Martin Owens + * + * Copyright (C) 2023 AUTHORS + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_COLORS_COLOR_H +#define SEEN_COLORS_COLOR_H + +#include +#include +#include + +#include "colors/spaces/enum.h" +#include "utils.h" + +namespace Inkscape::Colors { +namespace Space { +class AnySpace; +} // namespace Space + +class Color final +{ +public: + Color(std::shared_ptr space, std::vector colors); + Color(Space::Type space_type, std::vector values); + explicit Color(uint32_t color, bool alpha = true); + + static std::optional parse(char const *value); + static std::optional parse(std::string const &value); + static std::optional ifValid(Space::Type space_type, std::vector values); + + bool operator==(Color const &other) const; + double operator[](unsigned int index) const { return get(index); } + + std::shared_ptr const &getSpace() const { return _space; } + const std::vector &getValues() const { return _values; } + void setValues(std::vector values); + size_t size() const { return _values.size(); } + + double get(unsigned int index) const; + bool set(unsigned int index, double value); + bool set(Color const &other, bool keep_space = true); + bool set(std::string const &parsable, bool keep_space = true); + bool set(uint32_t rgba, bool opacity = true); + + bool hasOpacity() const; + void enableOpacity(bool enabled); + unsigned int getOpacityChannel() const; + double getOpacity() const; + double stealOpacity(); + bool setOpacity(double opacity); + bool addOpacity(double opacity = 1.0) { return setOpacity(opacity * getOpacity()); } + + unsigned int getPin(unsigned int channel) const; + + static constexpr double EPSILON = 1e-4; + + double difference(Color const &other) const; + bool isClose(Color const &other, double epsilon = EPSILON) const; + bool isSimilar(Color const &other, double epsilon = EPSILON) const; + + bool convert(Color const &other); + bool convert(std::shared_ptr space); + bool convert(Space::Type type); + std::optional converted(Color const &other) const; + std::optional converted(std::shared_ptr to_space) const; + std::optional converted(Space::Type type) const; + + std::string toString(bool opacity = true) const; + uint32_t toRGBA(double opacity = 1.0) const; + uint32_t toARGB(double opacity = 1.0) const; + uint32_t toABGR(double opacity = 1.0) const; + + std::string getName() const { return _name; } + void setName(std::string name) { _name = std::move(name); } + + bool isOutOfGamut(std::shared_ptr other) const; + bool isOverInked() const; + + void normalize(); + Color normalized() const; + + void average(Color const &other, double pos = 0.5, unsigned int pin = 0); + Color averaged(Color const &other, double pos = 0.5) const; + + void invert(unsigned int pin); + void invert() { invert(getPin(getOpacityChannel())); } + void jitter(double force, unsigned int pin = 0); + +private: + std::string _name; + std::vector _values; + std::shared_ptr _space; + + template + void _color_mutate_inplace(Color const &other, unsigned int pin, Func avgFunc); + + bool _isnear(std::vector const &other, double epsilon = 0.001) const; +}; + +class ColorError : public std::exception +{ +public: + ColorError(std::string &&msg) + : _msg(msg) + {} + char const *what() const noexcept override { return _msg.c_str(); } + +private: + std::string _msg; +}; + +} // namespace Inkscape::Colors + +#endif // SEEN_COLORS_COLOR_H diff --git a/src/colors/document-cms.cpp b/src/colors/document-cms.cpp new file mode 100644 index 0000000000..b2b74560b7 --- /dev/null +++ b/src/colors/document-cms.cpp @@ -0,0 +1,356 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * DocumentCMS - Look after all a document's icc profiles and lists of used colors. + * + * Copyright 2023 Martin Owens + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "document-cms.h" + +#include "cms/profile.h" +#include "cms/system.h" +#include "colors/color.h" +#include "colors/manager.h" +#include "colors/spaces/cms.h" +#include "document.h" +#include "io/sys.h" +#include "object/color-profile.h" +#include "object/sp-defs.h" +#include "object/sp-root.h" + +namespace Inkscape::Colors { + +class ColorProfileLink +{ +public: + ColorProfileLink(DocumentCMS *man, ColorProfile *elem); + ~ColorProfileLink(); + + DocumentCMS *tracker = nullptr; + ColorProfile *cp = nullptr; + std::shared_ptr space; + +private: + Inkscape::auto_connection _modified_connection; + + bool generateSpace(); + bool updateSpace(); +}; + +/** + * A local private class for tracking the signals between the color-profile xml in an SPDocument + * and the Space::CMS object which is the functional end of the color system. + */ +ColorProfileLink::ColorProfileLink(DocumentCMS *man, ColorProfile *elem) + : tracker(man) + , cp(elem) +{ + _modified_connection = cp->connectModified([this](SPObject *obj, guint flags) { + if (space ? updateSpace() : generateSpace()) { + tracker->_modified_signal.emit(space); + } + }); + generateSpace(); +} + +ColorProfileLink::~ColorProfileLink() +{ + _modified_connection.disconnect(); + if (space) + tracker->removeProfile(space); +} + +/** + * Attempt to turn the data in the ColorProfile into a Space::CMS object + * + * @returns true if a space was generated + */ +bool ColorProfileLink::generateSpace() +{ + if (space) + throw ColorError("Unexpected generation of CMS profile space"); + + std::shared_ptr profile; + + auto data = cp->getProfileData(); + auto local_id = cp->getLocalProfileId(); + if (!data.empty()) { + profile = CMS::Profile::create_from_data(data); + } else if (!local_id.empty()) { + profile = CMS::System::get().getProfile(local_id); + } + + if (profile) { + space = tracker->addProfile(profile, cp->getName(), cp->getRenderingIntent()); + } else { + g_warning("Incomplete CMS profile, no color space created for '%s'", cp->getName().c_str()); + } + return (bool)space; +} + +/** + * Update the space, this typically means the intent has changed. + */ +bool ColorProfileLink::updateSpace() +{ + if (space->getName() != cp->getName()) { + return generateSpace(); + } + if (space->getIntent() != cp->getRenderingIntent()) { + space->setIntent(cp->getRenderingIntent()); + return true; + } + return false; +} + +DocumentCMS::DocumentCMS(SPDocument *document) + : _document(document) +{ + if (document) + _resource_connection = + _document->connectResourcesChanged("iccprofile", sigc::mem_fun(*this, &DocumentCMS::refreshResources)); +} + +DocumentCMS::~DocumentCMS() +{ + _document = nullptr; +} + +/** + * Create an optional color, like Color::parse but with the document's cms spaces. + */ +std::optional DocumentCMS::parse(char const *value) const +{ + if (!value) + return {}; + return parse(std::string(value)); +} + +/** + * Create an optional color, like Color::parse, but match to document cms profiles where needed. + */ +std::optional DocumentCMS::parse(std::string const &value) const +{ + Space::Type space_type; + std::string cms_name; + std::vector values; + std::vector fallback; + if (Parsers::get().parse(value, space_type, cms_name, values, fallback)) { + if (cms_name.empty()) + return Color::ifValid(space_type, std::move(values)); + + // Find a space or construct an anonymous one so we don't lose data. + if (!_spaces.contains(cms_name)) { + _spaces[cms_name] = std::make_shared(cms_name, values.size()); + } + auto space = _spaces.find(cms_name)->second; + + if (!space->isValid()) { + for (int i = 2; i >= 0; i--) { + // Assume RGB fallback data if three doubles. Else black. + values.insert(values.begin(), fallback.size() == 3 ? fallback[i] : 0.0); + } + } + return Color(std::move(space), std::move(values)); + } + // Couldn't be parsed as a color at all + return {}; +} + +/** + * Make sure the icc-profile resource list is linked and up to date + * with the color manager's list of available color spaces. + */ +void DocumentCMS::refreshResources() +{ + bool changed = false; + + // 1. Look for color profile which have been created + std::vector objs; + for (auto obj : _document->getResourceList("iccprofile")) { + if (!obj->getId()) + continue; + if (auto cp = cast(obj)) { + objs.push_back(cp); + bool found = false; + for (auto &link : _links) { + found = found || (link->cp == cp); + } + if (!found) { + _links.emplace_back(new ColorProfileLink(this, cp)); + changed = true; + } + } + } + // 2. Look for color profiles which have been deleted + for (auto iter = _links.begin(); iter != _links.end();) { + if (std::find(objs.begin(), objs.end(), (*iter)->cp) == objs.end()) { + iter = _links.erase(iter); + changed = true; + } else + ++iter; + } + + // 3. Tell the rest of inkscape if something is added or removed + if (changed) { + _changed_signal.emit(); + } +} + +/** + * Add the icc profile via a URI as a color space with the attending settings. + */ +std::shared_ptr DocumentCMS::addProfileURI(std::string uri, std::string name, RenderingIntent intent) +{ + return addProfile(Inkscape::Colors::CMS::Profile::create_from_uri(std::move(uri)), std::move(name), intent); +} + +/** + * Add the icc profile as a color space with the attending settings. + */ +std::shared_ptr DocumentCMS::addProfile(std::shared_ptr profile, std::string name, + RenderingIntent intent) +{ + auto space = std::make_shared(profile); + + if (!name.empty()) { + // The name from the color-profile xml element overrides any internal name + space->setName(std::move(name)); + } + name = space->getName(); + if (_spaces.contains(name)) + throw ColorError("Color profile with that name already exists."); + + space->setIntent(intent != RenderingIntent::UNKNOWN ? intent : RenderingIntent::PERCEPTUAL); + _spaces[name] = space; + return space; +} + +/** + * Remove the icc profile as a color space + */ +void DocumentCMS::removeProfile(std::shared_ptr space) +{ + auto result = std::find_if(_spaces.begin(), _spaces.end(), [space](const auto &it) { return it.second == space; }); + + if (result != _spaces.end()) + _spaces.erase(result); +} + +/** + * Attach the named profile to the document. The name is used as a look-up in + * the CMS::System database then attached to the manager's document using the + * given storage mechanism. + * + * @args lookup - The string name, Id or path to look up in the systems database. + * @args storage - The mechanism to use when storing the profile in the document. + * @args name - The new name to use, if empty the name from the profile is used. + * @args intent - The rendering intent to use when transforming colors in this profile. + */ +void DocumentCMS::attachProfileToDoc(std::string const &lookup, ColorProfileStorage storage, RenderingIntent intent, + std::string name) +{ + auto &cms = Inkscape::Colors::CMS::System::get(); + if (auto profile = cms.getProfile(lookup)) { + std::string new_name = name.empty() ? profile->getName() : std::move(name); + if (auto cp = Inkscape::ColorProfile::createFromProfile(_document, *profile, std::move(new_name), storage)) { + cp->setRenderingIntent(intent); + _document->ensureUpToDate(); + } + } else { + g_error("Couldn't get the icc profile '%s'", lookup.c_str()); + } +} + +/** + * Get the specific color space from the list of available spaces. + * + * @arg name - The name of the color space + */ +std::shared_ptr DocumentCMS::getSpace(std::string const &name) const +{ + auto it = _spaces.find(name); + if (it != _spaces.end()) + return it->second; + return nullptr; +} + +/** + * Get the document color-profile SPObject for the named cms profile. Returns nullptr + * if the name is not found, if the link has not yet been made or the space is + * not a CMS color space (i.e. sRGB). + */ +ColorProfile *DocumentCMS::getColorProfileForSpace(std::string const &name) const +{ + return getColorProfileForSpace(getSpace(name)); +} + +/** + * Get the document color-profile SPObject for the given space. + * Returns nullptr if the space is not a CMS color space. + */ +ColorProfile *DocumentCMS::getColorProfileForSpace(std::shared_ptr space) const +{ + for (auto &link : _links) { + if (space && link->space && link->space->getName() == space->getName()) { + return link->cp; + } + } + return nullptr; +} + +/** + * Sets the rendering intent for the given color space in an agnostic way. + * + * If the space is a CMS space then the intent is updated in the SPObject. + */ +void DocumentCMS::setRenderingIntent(std::string const &name, RenderingIntent intent) +{ + if (auto cp = getColorProfileForSpace(name)) { + cp->setRenderingIntent(intent); + _document->ensureUpToDate(); + } +} + +/** + * Generate a list of CMS spaces linked in this tracker + */ +std::vector> DocumentCMS::getSpaces() const +{ + std::vector> ret; + for (auto &link : _links) { + if (link->space) { + ret.push_back(link->space); + } + } + return ret; +} + +/** + * Generate a list of SP-objects linked in this tracker + */ +std::vector DocumentCMS::getObjects() const +{ + std::vector ret; + for (auto &link : _links) { + if (link->cp) { + ret.push_back(link->cp); + } + } + return ret; +} + +} // namespace Inkscape::Colors + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 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/document-cms.h b/src/colors/document-cms.h new file mode 100644 index 0000000000..0361951c13 --- /dev/null +++ b/src/colors/document-cms.h @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Colors::DocumentCMS - Look after a document's icc profiles and keep + * track of all the colors in use and their color spaces. + * + * Copyright 2023 Martin Owens + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_COLORS_DOCUMENTCMS_H +#define SEEN_COLORS_DOCUMENTCMS_H + +#include +#include +#include +#include +#include + +#include "color.h" +#include "helper/auto-connection.h" + +class SPDocument; + +namespace Inkscape { +class ColorProfile; +enum class ColorProfileStorage; + +namespace Colors { +namespace CMS { +class Profile; +} +namespace Space { +enum class Type; +class AnySpace; +class CMS; +} // namespace Space +class ColorProfileLink; +enum class RenderingIntent; + +class DocumentCMS +{ +public: + DocumentCMS() = delete; + ~DocumentCMS(); + + DocumentCMS(DocumentCMS const &) = delete; + void operator=(DocumentCMS const &) = delete; + + DocumentCMS(SPDocument *document); + + std::optional parse(char const *value) const; + std::optional parse(std::string const &value) const; + + std::shared_ptr addProfileURI(std::string uri, std::string name, RenderingIntent intent); + std::shared_ptr addProfile(std::shared_ptr profile, std::string name, + RenderingIntent intent); + void removeProfile(std::shared_ptr space); + + sigc::connection connectChanged(const sigc::slot &slot) { return _changed_signal.connect(slot); } + sigc::connection connectModified(const sigc::slot)> &slot) + { + return _modified_signal.connect(slot); + } + + void attachProfileToDoc(std::string const &lookup, ColorProfileStorage storage, RenderingIntent intent, + std::string name = ""); + void setRenderingIntent(std::string const &name, RenderingIntent intent); + + std::shared_ptr getSpace(std::string const &name) const; + + ColorProfile *getColorProfileForSpace(std::string const &name) const; + ColorProfile *getColorProfileForSpace(std::shared_ptr space) const; + + std::vector> getSpaces() const; + std::vector getObjects() const; + +private: + void refreshResources(); + + SPDocument *_document = nullptr; + std::vector> _links; + + Inkscape::auto_connection _resource_connection; + sigc::signal _changed_signal; + + mutable std::map> _spaces; + +protected: + friend class ColorProfileLink; + + sigc::signal)> _modified_signal; +}; + +} // namespace Colors +} // namespace Inkscape + +#endif // SEEN_COLORS_DOCUMENTCMS_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/dragndrop.cpp b/src/colors/dragndrop.cpp new file mode 100644 index 0000000000..350b4fdda2 --- /dev/null +++ b/src/colors/dragndrop.cpp @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Jon A. Cruz + * Martin Owens + * + * Copyright (C) 2009-2023 author + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "dragndrop.h" + +#include +#include + +#include "colors/color.h" + +namespace Inkscape::Colors { + +/** + * Convert a paint into a draggable object. + */ +std::vector getMIMEData(Paint const &paint, char const *mime_type) +{ + // XML Handles all types of paint + if (std::strcmp(mime_type, mimeOSWB_COLOR) == 0) { + auto const xml = paint_to_xml_string(paint); + return {xml.begin(), xml.end()}; + } + + // Handle NoColor first + if (std::holds_alternative(paint)) { + if (std::strcmp(mime_type, mimeTEXT) == 0) + return {'n', 'o', 'n', 'e'}; + if (std::strcmp(mime_type, mimeX_COLOR) == 0) + return std::vector(8); // transparent black + return {}; + } + + auto &color = std::get(paint); + if (std::strcmp(mime_type, mimeTEXT) == 0) { + auto const str = color.toString(); + return {str.begin(), str.end()}; + } else if (std::strcmp(mime_type, mimeX_COLOR) == 0) { + // X-color is only ever in RGBA + auto const rgb = color.toRGBA(); + return { + (char)SP_RGBA32_R_U(rgb), (char)SP_RGBA32_R_U(rgb), (char)SP_RGBA32_G_U(rgb), (char)SP_RGBA32_G_U(rgb), + (char)SP_RGBA32_B_U(rgb), (char)SP_RGBA32_B_U(rgb), (char)SP_RGBA32_A_U(rgb), (char)SP_RGBA32_A_U(rgb), + }; + } + return {}; +} + +/** + * Convert a dropped object into a color, if possible. + */ +Paint fromMIMEData(std::span data, char const *mime_type) +{ + if (std::strcmp(mime_type, mimeX_COLOR) == 0) { + if (data.size() != 8) + throw ColorError("Data is the wrong size for color mime type"); + return Color{SP_RGBA32_U_COMPOSE(data[0], data[2], data[4], data[6])}; + } + + auto const str = std::string{data.data(), data.size()}; + if (std::strcmp(mime_type, mimeTEXT) == 0) { + if (str == "none") { + return NoColor(); + } + if (auto color = Color::parse(str)) { + return *color; + } + } + if (std::strcmp(mime_type, mimeOSWB_COLOR) == 0) { + return xml_string_to_paint(str, nullptr); + } + throw ColorError("Unknown color data found"); +} + +} // namespace Inkscape::Colors + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 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/dragndrop.h b/src/colors/dragndrop.h new file mode 100644 index 0000000000..d7a30ea6c9 --- /dev/null +++ b/src/colors/dragndrop.h @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Jon A. Cruz + * Martin Owens + * + * Copyright (C) 2009-2023 author + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_COLORS_DRAGNDROP +#define INKSCAPE_COLORS_DRAGNDROP + +#include +#include +#include + +#include "xml-color.h" + +namespace Inkscape::Colors { + +inline constexpr auto mimeOSWB_COLOR = "application/x-oswb-color"; +inline constexpr auto mimeX_COLOR = "application/x-color"; +inline constexpr auto mimeTEXT = "text/plain"; + +std::vector getMIMEData(Paint const &paint, char const *mime_type); +Paint fromMIMEData(std::span data, char const *mime_type); + +} // namespace Inkscape::Colors + +#endif // INKSCAPE_COLORS_DRAGNDROP + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 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/manager.cpp b/src/colors/manager.cpp new file mode 100644 index 0000000000..74569d0425 --- /dev/null +++ b/src/colors/manager.cpp @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Manager - Look after all a document's icc profiles. + * + * Copyright 2023 Martin Owens + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "manager.h" + +#include +#include + +#include "colors/color.h" +#include "colors/parser.h" +#include "document.h" + +// Each Internal space should be imported here. +#include "spaces/cms.h" +#include "spaces/cmyk.h" +#include "spaces/gray.h" +#include "spaces/hsl.h" +#include "spaces/hsluv.h" +#include "spaces/hsv.h" +#include "spaces/lab.h" +#include "spaces/lch.h" +#include "spaces/linear-rgb.h" +#include "spaces/luv.h" +#include "spaces/named.h" +#include "spaces/okhsl.h" +#include "spaces/oklab.h" +#include "spaces/oklch.h" +#include "spaces/rgb.h" +#include "spaces/xyz.h" + +namespace Inkscape::Colors { + +Manager::Manager() +{ + // Regular SVG 1.1 Colors + addSpace(new Space::RGB()); + addSpace(new Space::NamedColor()); + + // Color module 4 and 5 support + addSpace(new Space::DeviceCMYK()); + addSpace(new Space::Gray()); + addSpace(new Space::HSL()); + addSpace(new Space::HSLuv()); + addSpace(new Space::HSV()); + addSpace(new Space::Lab()); + addSpace(new Space::LinearRGB()); + addSpace(new Space::Lch()); + addSpace(new Space::Luv()); + addSpace(new Space::OkHsl()); + addSpace(new Space::OkLab()); + addSpace(new Space::OkLch()); + addSpace(new Space::XYZ()); +} + +/** + * Add the given space and assume ownership over it. + */ +std::shared_ptr Manager::addSpace(Space::AnySpace *space) +{ + if (find(space->getType())) { + throw ColorError("Can not add the same color space twice."); + } + _spaces.emplace_back(space); + return _spaces.back(); +} + +/** + * Removes the given space from the list of available spaces. + */ +bool Manager::removeSpace(std::shared_ptr space) +{ + return std::erase(_spaces, space); +} + +/** + * Finds the first global color space matching the given type + * + * @arg type - The type enum to match + */ +std::shared_ptr Manager::find(Space::Type type) const +{ + auto it = std::find_if(_spaces.begin(), _spaces.end(), [type](auto &v) { return v->getType() == type; }); + return it != _spaces.end() ? *it : nullptr; +} + +/** + * Finds the global space matching the given name + * + * @arg name - The name of the space as given by getName + */ +std::shared_ptr Manager::find(std::string const &name) const +{ + auto it = std::find_if(_spaces.begin(), _spaces.end(), [name](auto &v) { return v->getName() == name; }); + return it != _spaces.end() ? *it : nullptr; +} +} // namespace Inkscape::Colors + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 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/manager.h b/src/colors/manager.h new file mode 100644 index 0000000000..b4434a8eea --- /dev/null +++ b/src/colors/manager.h @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Colors::Manager - Look after all a document's icc profiles. + * + * Copyright 2023 Martin Owens + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_COLORS_MANAGER_H +#define SEEN_COLORS_MANAGER_H + +#include +#include +#include +#include + +namespace Inkscape::Colors { +namespace Space { +enum class Type; +class AnySpace; +} // namespace Space +class Color; +class Parser; + +class Manager +{ +private: + Manager(Manager const &) = delete; + void operator=(Manager const &) = delete; + +public: + + static Manager &get() + { + static Manager instance; + return instance; + } + + std::shared_ptr find(Space::Type type) const; + std::shared_ptr find(std::string const &name) const; + +protected: + Manager(); + ~Manager() = default; + + std::shared_ptr addSpace(Space::AnySpace *space); + bool removeSpace(std::shared_ptr space); + +private: + std::vector> _spaces; +}; + +} // namespace Inkscape::Colors + +#endif // SEEN_COLORS_MANAGER_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/parser.cpp b/src/colors/parser.cpp new file mode 100644 index 0000000000..eb68de53fc --- /dev/null +++ b/src/colors/parser.cpp @@ -0,0 +1,359 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2023 AUTHORS + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "parser.h" + +#include +#include + +#include "spaces/cms.h" +#include "spaces/cmyk.h" +#include "spaces/gray.h" +#include "spaces/hsl.h" +#include "spaces/hsluv.h" +#include "spaces/hsv.h" +#include "spaces/lab.h" +#include "spaces/lch.h" +#include "spaces/linear-rgb.h" +#include "spaces/luv.h" +#include "spaces/named.h" +#include "spaces/okhsl.h" +#include "spaces/oklab.h" +#include "spaces/oklch.h" +#include "spaces/rgb.h" +#include "spaces/xyz.h" +#include "utils.h" + +namespace Inkscape::Colors { + +Parsers::Parsers() +{ + addParser(new HexParser()); + addParser(new Space::NamedColor::NameParser()); + addParser(new Space::CMS::CmsParser()); + addParser(new Space::RGB::Parser(false)); + addParser(new Space::RGB::Parser(true)); + addParser(new Space::HSL::Parser(false)); + addParser(new Space::HSL::Parser(true)); + addParser(new Space::HSV::fromHwbParser(false)); + addParser(new Space::HSV::fromHwbParser(true)); + addParser(new Space::Lab::Parser()); + addParser(new Space::Lch::Parser()); + addParser(new Space::OkLab::Parser()); + addParser(new Space::OkLch::Parser()); + addParser(new CssParser("srgb", Space::Type::RGB, 3)); + addParser(new CssParser("srgb-linear", Space::Type::linearRGB, 3)); + addParser(new CssParser("device-cmyk", Space::Type::CMYK, 4)); + addParser(new CssParser("xyz", Space::Type::XYZ, 3)); +} + +/** + * Turn a string into a color data, used in Color object creation. + * + * Each available color parser will be asked to parse the color in turn + * and the successful parser will return object data. + * + * @arg input - The string to parse. + * @retval type - The type (enum) of the color space to understand the values + * @retval cms - The name of the cms color space to understand the values + * @retval values - A list of values for use with the color space. + * @retval fallback - A returned list of values for fallback colors. + * + * @returns true if the color was parsed. + */ +bool Parsers::parse(std::string const &input, Space::Type &type, std::string &cms, std::vector &values, + std::vector &fallback) const +{ + std::istringstream ss(input); + return _parse(ss, type, cms, values, fallback); +} + +/** + * Internal recursive parser that scans through a string stream. + * + * @arg ss - The string stream, parsing starts at it's set position + * @retval type - The type (enum) of the color space to understand the values + * @retval cms - The return name of the color space to understand the values + * @retval values - A returned list of values for use with the color space. + * @retval fallback - A returned list of values for the fallback (sRGB) color + * + * @returns true if the color was parsed. + */ +bool Parsers::_parse(std::istringstream &ss, Space::Type &type, std::string &name, std::vector &values, + std::vector &fallback) const +{ + auto ptype = Parser::getCssPrefix(ss); + auto iter = _parsers.find(ptype); + if (iter == _parsers.end()) + return false; + + for (auto &parser : iter->second) { + auto pos = ss.tellg(); + bool more = false; + values.clear(); + + name = parser->parseColor(ss, values, more); + + // We have an RGB color but there's more string, look for an icc-color next. + if (more && ptype == "#") { + std::vector icc_values; + if (_parse(ss, type, name, icc_values, fallback)) { + if (type == Space::Type::CMS) { + fallback = std::move(values); + values = std::move(icc_values); + return true; + } + } + } + + if (!values.empty()) { + type = parser->getType(); + return true; + } + + ss.clear(); + ss.seekg(pos); + } + return false; +} + +/** + * Add a prser to the list of parser objects used when parsing color strings. + */ +void Parsers::addParser(Parser *parser) +{ + // Map parsers by their expected prefix for quicker lookup + auto const [it, inserted] = _parsers.emplace(parser->getPrefix(), std::vector>{}); + it->second.emplace_back(parser); +} + +/** + * Parse this specific color format into output values + * + * @arg ss - The stream stream to parse + * @arg output - The returned list of values + * @arg more - Indicates if there is more string to parse. + * + * @returns The type of the space found by this parser (if any). + */ +std::string Parser::parseColor(std::istringstream &ss, std::vector &output, bool &more) const +{ + if (!parse(ss, output, more)) { + output.clear(); + } + return ""; +} + +bool HueParser::parse(std::istringstream &ss, std::vector &output) const +{ + bool end = false; + return append_css_value(ss, output, end, ',', 360) && append_css_value(ss, output, end, ',') && + append_css_value(ss, output, end, !_alpha ? '/' : ',') && (append_css_value(ss, output, end) || !_alpha) && + end; +} + +/** + * Parse either a hex code or an rgb() css string. + */ +bool HexParser::parse(std::istringstream &ss, std::vector &output, bool &more) const +{ + unsigned int hex; + unsigned int size = 0; + + size = ss.tellg(); + ss >> std::hex >> hex; + // This mess is required because istream countg is inconsistant + size = (ss.tellg() == -1 ? ss.str().size() : (int)ss.tellg()) - size; + + if (size == 3 || size == 4) { // #rgb(a) + for (int p = (4 * (size - 1)); p >= 0; p -= 4) { + auto val = ((hex & (0xf << p)) >> p); + output.emplace_back((val + (val << 4)) / 255.0); + } + } else if (size == 6 || size == 8) { // #rrggbb(aa) + if (size == 6) + hex <<= 8; + output.emplace_back(SP_RGBA32_R_F(hex)); + output.emplace_back(SP_RGBA32_G_F(hex)); + output.emplace_back(SP_RGBA32_B_F(hex)); + if (size == 8) + output.emplace_back(SP_RGBA32_A_F(hex)); + } + ss >> std::ws; + more = (ss.peek() == 'i'); // icc-color is next + return !output.empty(); +} + +/** + * Prase the given string stream as a CSS Color Module Level 4/5 string. + */ +bool CssParser::parse(std::istringstream &ss, std::vector &output) const +{ + bool end = false; + while (!end && output.size() < _channels + 1) { + if (!append_css_value(ss, output, end, output.size() == _channels - 1 ? '/' : 0x0)) + break; + } + return end; +} + +/** + * Parse CSS color numbers after the function name + * + * @arg ss - The string stream to read + * + * @returns - The color prefix or color name detected in this color function. + * either the first part of the function, for example rgb or hsla + * or the first variable in the case of color(), icc-color() and var() + */ +std::string Parser::getCssPrefix(std::istringstream &ss) +{ + std::string token; + ss >> std::ws; + if (ss.peek() == '#') { + return {(char)ss.get()}; + } + auto pos = ss.tellg(); + if (!std::getline(ss, token, '(') || ss.eof()) { + ss.seekg(pos); + return ""; // No paren + } + + if (token == "color") { + // CSS Color module 4 color() function + ss >> token; + } + return token; +} + +/** + * Parse a CSS color number after the function name + * + * @arg ss - The string stream to read + * @returns value - The value read in without adjustment + * @returns unit - The unit, ususally an empty string + * @returns end - True if this is the end of the css function + * @arg sep - An optional seperator argument + * + * @returns true if a number and unit was parsed correctly. + */ +bool Parser::css_number(std::istringstream &ss, double &value, std::string &unit, bool &end, char const sep) +{ + ss.imbue(std::locale("C")); + + /* + * LLVM uses libc++ and this C library has a broken double parser compared to GCC/libstdc++ + * So we have written our own double parser to get around the limitations for APPLE + * + * Detailed description can be found at: + * https://github.com/tardate/LittleCodingKata/blob/main/cpp/DoubleTrouble/README.md + */ +#ifdef __APPLE__ + bool parsed = false; + bool dot = false; + + ss >> std::ws; + std::string result; + while (true) { + auto c = ss.peek(); + + if (c == '-' && result.empty()) { + result += ss.get(); + } else if (c == '.') { + if (dot) + break; + dot = true; + result += ss.get(); + } else if (c >= '0' && c <= '9') { + result += ss.get(); + parsed = true; + } else { + break; + } + } + if (parsed) { + value = std::stod(result); + } +#else + bool parsed = (bool)(ss >> value); +#endif + if (!parsed) { + ss.clear(); + return false; + } + + unit.clear(); + auto c = ss.peek(); + if (c == '.' || (c >= '0' && c <= '9')) { + return true; + } + while (ss && (c = ss.get())) { + if (c == ')') { + end = true; + break; + } else if (c == sep) { + break; + } + if (c == ' ') { + auto p = ss.peek(); + if (p != ' ' && p != sep && p != ')') { + break; + } + } else { + unit += c; + } + } + return true; +} + +/** + * Parse a CSS color number and format it according to it's unit. + * + * @arg ss - The string stream set at the location of the next number. + * @arg output - The vector to append the new number to. + * @arg end - Is set to true if this number is the last one. + * @arg sep - The separator to expect after this number (consumed) + * @arg scale - The default scale of the number of no unit is detected. + * + * @returns True if a number was found and appended. + */ +bool Parser::append_css_value(std::istringstream &ss, std::vector &output, bool &end, char const sep, + double scale) +{ + double value; + std::string unit; + if (!end && css_number(ss, value, unit, end, sep)) { + if (unit == "%") { + value /= 100; + } else if (unit == "deg") { + value /= 360; + } else if (unit == "turn") { + // no need to modify + } else if (!unit.empty()) { + std::cerr << "Unknown unit in css color parsing '" << unit.c_str() << "'\n"; + return false; + } else { + value /= scale; + } + output.emplace_back(value); + return true; + } + return false; +} + +}; // namespace Inkscape::Colors + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/colors/parser.h b/src/colors/parser.h new file mode 100644 index 0000000000..2a526e32bf --- /dev/null +++ b/src/colors/parser.h @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2023 AUTHORS + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_COLORS_PARSING_H +#define SEEN_COLORS_PARSING_H + +#include +#include +#include +#include + +#include "spaces/enum.h" + +namespace Inkscape::Colors { + +class Parser +{ +public: + Parser(std::string prefix, Space::Type type) + : _prefix(std::move(prefix)) + , _type(type) + {} + virtual ~Parser() = default; + + Space::Type getType() const { return _type; } + std::string const getPrefix() const { return _prefix; } + static std::string getCssPrefix(std::istringstream &ss); + static bool css_number(std::istringstream &ss, double &value, std::string &unit, bool &end, char const sep = 0x0); + static bool append_css_value(std::istringstream &ss, std::vector &output, bool &end, char const sep = 0x0, + double scale = 1.0); + + // TODO message: return reason for parsing failure. Otherwise we cannot tell the user why we reject certain colors. + virtual bool parse(std::istringstream &ss, std::vector &output) const { return false; }; + virtual bool parse(std::istringstream &ss, std::vector &output, bool &more) const + { + return parse(ss, output); + }; + virtual std::string parseColor(std::istringstream &ss, std::vector &output, bool &more) const; + +private: + std::string _prefix; + Space::Type _type; +}; + +class LegacyParser : public Parser +{ +public: + LegacyParser(std::string const &prefix, Space::Type type, bool alpha) + : Parser(alpha ? prefix + "a" : prefix, type) + , _alpha(alpha) + {} + +protected: + bool _alpha = false; +}; + +class HueParser : public LegacyParser +{ +public: + HueParser(std::string const &prefix, Space::Type type, bool alpha) + : LegacyParser(prefix, type, alpha) + {} + bool parse(std::istringstream &ss, std::vector &output) const override; +}; + +class HexParser : public Parser +{ +public: + HexParser() + : Parser("#", Space::Type::RGB) + {} + bool parse(std::istringstream &input, std::vector &output, bool &more) const override; +}; + +class CssParser : public Parser +{ +public: + CssParser(std::string prefix, Space::Type type, unsigned int channels) + : Parser(prefix, type) + , _channels(channels) + {} + +private: + bool parse(std::istringstream &ss, std::vector &output) const override; + + unsigned int _channels; +}; + +class Parsers +{ +private: + Parsers(Parsers const &) = delete; + void operator=(Parsers const &) = delete; + +public: + Parsers(); + ~Parsers() = default; + + static Parsers &get() + { + static Parsers instance; + return instance; + } + + bool parse(std::string const &input, Space::Type &type, std::string &cms, std::vector &values, + std::vector &fallback) const; + +protected: + friend class Manager; + + void addParser(Parser *parser); + +private: + bool _parse(std::istringstream &ss, Space::Type &type, std::string &cms, std::vector &values, + std::vector &fallback) const; + + std::map>> _parsers; +}; + +} // namespace Inkscape::Colors + +#endif // SEEN_COLORS_PARSING_H diff --git a/src/colors/printer.cpp b/src/colors/printer.cpp new file mode 100644 index 0000000000..5412a7b72c --- /dev/null +++ b/src/colors/printer.cpp @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2023 AUTHORS + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "printer.h" + +namespace Inkscape::Colors { + +CssPrinter &CssPrinter::operator<<(double value) +{ + if (!_done) { + if (_count == _channels && _slash_opacity) { + // We print opacity as a percentage + *this << " / " << (int)(value * 100) << "%"; + } else if (_count < _channels) { + std::ostringstream oo; + oo.imbue(getloc()); + oo.precision(precision()); + oo << std::fixed << value; + + // To bypass the lack of significant-figures option we strip trailing zeros in a sub-process. + auto number = oo.str(); + while (number.find(".") != std::string::npos && + (number.substr(number.length() - 1, 1) == "0" || number.substr(number.length() - 1, 1) == ".")) + number.pop_back(); + + *this << (_count ? _sep : "") << number; + } + _count++; + } + return *this; +} + +CssPrinter &CssPrinter::operator<<(int value) +{ + if (!_done && _count < _channels) { + *this << (_count ? _sep : "") << value; + _count++; + } + return *this; +} + +CssPrinter &CssPrinter::operator<<(std::vector const &values) +{ + for (auto &value : values) { + if (_count < _channels) + *this << value; + } + return *this; +} + +}; // namespace Inkscape::Colors + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/colors/printer.h b/src/colors/printer.h new file mode 100644 index 0000000000..32fded543a --- /dev/null +++ b/src/colors/printer.h @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2023 AUTHORS + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_COLORS_PRINTER_H +#define SEEN_COLORS_PRINTER_H + +#include +#include +#include +#include +#include + +namespace Inkscape::Colors { + +class CssPrinter : public std::ostringstream +{ +public: + CssPrinter(unsigned channels, std::string prefix, std::string ident = "", std::string sep = " ") + : _channels(channels) + , _sep(std::move(sep)) + { + imbue(std::locale("C")); + precision(3); + *this << prefix << "("; + if (!ident.empty()) { + *this << ident; + _count = 1; + _channels += 1; + } + } + + CssPrinter &operator<<(double); + CssPrinter &operator<<(int); + CssPrinter &operator<<(std::vector const &); + + operator std::string() + { + if (!_done) { + _done = true; + *this << ")"; + } + if (_count < _channels) { + std::cerr << " Expected " << _channels << " channels but got " << _count << "\n"; + return ""; + } + return str(); + } + +protected: + bool _slash_opacity = false; + bool _done = false; + unsigned _count = 0; + unsigned _channels = 0; + std::string _sep; +}; + +class IccColorPrinter : public CssPrinter +{ +public: + IccColorPrinter(unsigned channels, std::string ident) + : CssPrinter(channels, "icc-color", std::move(ident), ", ") + {} +}; + +class CssLegacyPrinter : public CssPrinter +{ +public: + CssLegacyPrinter(unsigned channels, std::string prefix, bool opacity) + : CssPrinter(channels + (int)opacity, prefix + (opacity ? "a" : ""), "", ", ") + {} +}; + +class CssFuncPrinter : public CssPrinter +{ +public: + CssFuncPrinter(unsigned channels, std::string prefix) + : CssPrinter(channels, std::move(prefix)) + { + _slash_opacity = true; + } +}; + +class CssColorPrinter : public CssPrinter +{ +public: + CssColorPrinter(unsigned channels, std::string ident) + : CssPrinter(channels, "color", std::move(ident)) + { + _slash_opacity = true; + } +}; + +} // namespace Inkscape::Colors + +#endif // SEEN_COLORS_PRINTER_H diff --git a/src/colors/spaces/base.cpp b/src/colors/spaces/base.cpp new file mode 100644 index 0000000000..1537e7cc71 --- /dev/null +++ b/src/colors/spaces/base.cpp @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Manage color spaces + *//* + * Authors: see git history + * + * Copyright (C) 2023 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "base.h" + +#include + +#include "colors/cms/profile.h" +#include "colors/cms/transform.h" +#include "colors/color.h" +#include "components.h" + +namespace Inkscape::Colors::Space { + +AnySpace::AnySpace() +{ + if (!srgb_profile) { + srgb_profile = Colors::CMS::Profile::create_srgb(); + } +} + +/** + * Return true if the given data would be valid for this color space. + */ +bool AnySpace::isValidData(std::vector const &values) const +{ + auto const n_values = values.size(); + auto const n_space = getComponentCount(); + return n_values == n_space || n_values == n_space + 1; +} + +/** + * In place conversion of a color object to the given space. + * + * This three part conversion may not mutate the input at all, depending on + * the space it's already in and the format of the data. + */ +bool AnySpace::convert(std::vector &io, std::shared_ptr to_space) const +{ + // 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) + to_space->profileToSpace(io); + return true; + } + profileToSpace(io); + return false; +} + +/** + * Convert from the space's format, to the profile's data format. + */ +void AnySpace::spaceToProfile(std::vector &io) const {} + +/** + * Convert from the profile's format, to the space's data format. + */ +void AnySpace::profileToSpace(std::vector &io) const {} + +/** + * Step two in coverting a color, convert it's profile to another profile (if needed) + */ +bool AnySpace::profileToProfile(std::vector &io, std::shared_ptr to_space) const +{ + auto from_profile = getProfile(); + auto to_profile = to_space->getProfile(); + if (*to_profile == *from_profile) + return true; + + // Choose best rendering intent, first ours, then theirs, finally a default + auto intent = getIntent(); + if (intent == RenderingIntent::UNKNOWN) + intent = to_space->getIntent(); + if (intent == RenderingIntent::UNKNOWN) + intent = RenderingIntent::PERCEPTUAL; + + // Look in the transform cache for the color profile + auto to_profile_id = to_profile->getChecksum() + intentIds[intent]; + + if (!_transforms.contains(to_profile_id)) { + // Create a new transform for this one way profile-pair + _transforms.emplace(to_profile_id, Colors::CMS::Transform::create_for_cms(from_profile, to_profile, intent)); + } + + // Use the transform to convert the output colors. + if (auto &tr = _transforms[to_profile_id]) { + return tr->do_transform(io); + } + return false; +} + +/** + * Return true if the color would be out of gamut in the target color space. + * + * NOTE: This can NOT work if the base color spaces are exactly the same. i.e. device-cmyk(sRGB) + * will always return false despite not being reversable with RGB (which is also sRGB). + * + * If you want gamut checking via lcms2, you must use different icc profiles. + * + * @arg input - Channel values in this space. + * @arg to_space - The target space to compare against. + */ +bool AnySpace::outOfGamut(std::vector const &input, std::shared_ptr to_space) const +{ + auto from_profile = getProfile(); + auto to_profile = to_space->getProfile(); + if (*to_profile == *from_profile) + return false; + // 1. Look in the checker cache for the color profile + auto to_profile_id = to_profile->getId(); + if (_gamut_checkers.find(to_profile_id) == _gamut_checkers.end()) { + // 2. Create a new transform for this one way profile-pair + _gamut_checkers.emplace(to_profile_id, + Colors::CMS::Transform::create_for_cms_checker(from_profile, to_profile)); + } + + return _gamut_checkers[to_profile_id]->check_gamut(input); +} + +/** + * Return a list of Component objects, in order of the channels in this color space + * + * @arg alpha - If true, returns the alpha component as well + */ +Components const &AnySpace::getComponents(bool alpha) const +{ + return Space::Components::get(getComponentType(), alpha); +} + +}; // namespace Inkscape::Colors::Space diff --git a/src/colors/spaces/base.h b/src/colors/spaces/base.h new file mode 100644 index 0000000000..e9678f9fa0 --- /dev/null +++ b/src/colors/spaces/base.h @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Martin Owens + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_COLORS_SPACES_BASE_H +#define SEEN_COLORS_SPACES_BASE_H + +#include +#include +#include +#include +#include + +#include "colors/parser.h" +#include "enum.h" + +constexpr double SCALE_UP(double v, double a, double b) +{ + return (v * (b - a)) + a; +} +constexpr double SCALE_DOWN(double v, double a, double b) +{ + return (v - a) / (b - a); +} + +namespace Inkscape::Colors { +namespace CMS { +class Profile; +class Transform; +} // namespace CMS +class Color; +class Manager; + +namespace Space { + +class Components; + +class AnySpace +{ +public: + virtual ~AnySpace() = default; + + bool operator==(AnySpace const &other) const { return other.getName() == getName(); } + bool operator!=(AnySpace const &other) const { return !(*this == other); }; + + virtual Type getType() const = 0; + virtual std::string const getName() const = 0; + virtual std::string const getIcon() const = 0; + virtual Type getComponentType() const { return getType(); } + virtual unsigned int getComponentCount() const = 0; + virtual std::shared_ptr const getProfile() const = 0; + virtual RenderingIntent getIntent() const { return RenderingIntent::UNKNOWN; } + + Components const &getComponents(bool alpha = false) const; + std::string const getPrefsPath() const { return "/colorselector/" + getName() + "/"; } + + virtual bool isValid() const { return true; } + +protected: + friend class Colors::Color; + + AnySpace(); + bool isValidData(std::vector const &values) const; + virtual std::vector getParsers() const { return {}; } + virtual std::string toString(std::vector const &values, bool opacity = true) const = 0; + virtual uint32_t toRGBA(std::vector const &values, double opacity = 1.0) 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; + virtual bool overInk(std::vector const &input) const { return false; } + + std::shared_ptr srgb_profile; + + bool outOfGamut(std::vector const &input, std::shared_ptr to_space) const; + +private: + mutable std::map> _transforms; + mutable std::map> _gamut_checkers; +}; + +} // namespace Space +} // namespace Inkscape::Colors + +#endif // SEEN_COLORS_SPACES_BASE_H diff --git a/src/colors/spaces/cms.cpp b/src/colors/spaces/cms.cpp new file mode 100644 index 0000000000..75c537e8bf --- /dev/null +++ b/src/colors/spaces/cms.cpp @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + *//* + * Authors: see git history + * + * Copyright (C) 2023 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "cms.h" + +#include + +#include "colors/cms/profile.h" +#include "colors/cms/transform.h" +#include "colors/color.h" +#include "colors/manager.h" +#include "colors/printer.h" + +namespace Inkscape::Colors::Space { + +// When we support a color space that lcms2 does not, record here +static cmsUInt32Number customSigOKLabData = 0x4f4b4c42; // 'OKLB'; + +static std::map _lcmssig_to_space = { + {cmsSigRgbData, Space::Type::RGB}, {cmsSigHlsData, Space::Type::HSL}, + {cmsSigCmykData, Space::Type::CMYK}, {cmsSigCmyData, Space::Type::CMY}, + {cmsSigHsvData, Space::Type::HSV}, {cmsSigLuvData, Space::Type::HSLUV}, + {customSigOKLabData, Space::Type::OKLAB}, {cmsSigXYZData, Space::Type::XYZ}, + {cmsSigXYZData, Space::Type::YXY}, {cmsSigLabData, Space::Type::LAB}, + {cmsSigYCbCrData, Space::Type::YCbCr}, {cmsSigGrayData, Space::Type::Gray}, +}; + +CMS::CMS(std::shared_ptr profile) + : AnySpace() + , _profile_name(profile->getName(true)) + , _profile_size(profile->getSize()) + , _profile_type(_lcmssig_to_space[profile->getColorSpace()]) + , _profile(profile) +{} + +/** + * Naked CMS space for testing and data retention where the profile is unavailable. + */ +CMS::CMS(std::string profile_name, unsigned profile_size, Space::Type profile_type) + : AnySpace() + , _profile_name(std::move(profile_name)) + , _profile_size(profile_size) + , _profile_type(profile_type) + , _profile(nullptr) +{} + +/** + * Return the profile for this cms space. If this is anonymous, it returns srgb + * so the transformation on the fallback color is transparent. + */ +std::shared_ptr const CMS::getProfile() const +{ + if (!isValid()) + return srgb_profile; + return _profile; +} + +/** + * If this space lacks a profile, it's really the sRGB fallback values, + * so we strip out any other values from the io. + */ +void CMS::spaceToProfile(std::vector &io) const +{ + if (isValid()) + return; // Do nothing for valid spaces + + bool has_opacity = io.size() == _profile_size + 4; + + // Keep deleting the icc color values + while (io.size() > (3 + has_opacity)) { + io.erase(io.begin() + 3); + } +} + +/** + * Get the number of components for this cms color space. + * + * If the color space is not valid, three extra channels are + * used to hold the red green and blue values. + */ +unsigned int CMS::getComponentCount() const +{ + return _profile ? _profile_size : _profile_size + 3; +} + +/** + * Parse a string stream into a vector of doubles which are always values in + * this CMS space / icc profile. + * + * @args ss - String input which doesn't have to be at the start of the string. + * @returns output - The vector to populate with the numbers found in the string. + * @returns the name of the cms profile requested. + */ +std::string CMS::CmsParser::parseColor(std::istringstream &ss, std::vector &output, bool &more) const +{ + std::string icc_name; + ss >> icc_name; + + if (!icc_name.empty() && icc_name.back() == ',') + icc_name.pop_back(); + + bool end = false; + while (!end && append_css_value(ss, output, end, ',')) + continue; + + if (output.size() == 0) { + std::string named; + ss >> named; + if (!named.empty() && ss.get() == ')') { + std::cerr << "Found SVG2 ICC named color '" << named.c_str() << "' for profile '" << icc_name.c_str() + << "', which not supported yet.\n"; + } + } + + return icc_name; +} + +/** + * Output these values into this CMS space. + * + * @args values - The values for each channel in the icc profile. + * @args opacity - Should opacity be included. This is ignored since cms + * output is ALWAYS without opacity. + * + * @returns the string suitable for css and style use. + */ +std::string CMS::toString(std::vector const &values, bool /*opacity*/) const +{ + if (values.size() < _profile_size) + return ""; + + // RGBA Hex fallback plus icc-color section + auto oo = IccColorPrinter(_profile_size, _profile_name); + + // When an icc color was parsed, but there is no profile + if (!isValid()) { + // Not enough values for a fallback option (maybe corrupt?) + if (values.size() < _profile_size + 3) + return ""; + std::vector remains(values.begin() + 3, values.end()); + oo << remains; + } else { + oo << values; + } + // opacity is never added to the printer here, always ignored + return rgba_to_hex(toRGBA(values), false) + " " + (std::string)oo; +} + +/** + * Convert this CMS color into a simple sRGB based RGBA value. + * + * @arg values - The values for each channel in the icc profile + * @arg opacity - An extra opacity to mix into the output. + * + * @returns An integer of the sRGB value. + */ +uint32_t CMS::toRGBA(std::vector const &values, double opacity) const +{ + if (!isValid()) { + if (values.size() == _profile_size + 3) + return SP_RGBA32_F_COMPOSE(values[0], values[1], values[2], opacity); + if (values.size() == _profile_size + 4) + return SP_RGBA32_F_COMPOSE(values[0], values[1], values[2], opacity * values.back()); + std::cerr << "Can not convert CMS color to sRGB, no profile available and no fallback color\n"; + return 0x0; + } + + static auto rgb = Manager::get().find(Space::Type::RGB); + std::vector copy = values; + if (convert(copy, rgb)) { + // The opacity will be copied during conversion (if present) so we only + // need to test if the rgb has opacity and add it on if it does. + if (copy.size() == rgb->getComponentCount() + 1) + opacity *= copy.back(); + + // CMS color channels never include opacity, it's not in the specification + return SP_RGBA32_F_COMPOSE(copy[0], copy[1], copy[2], opacity); + } + std::cerr << "Can not convert CMS color to sRGB.\n"; + return 0x0; +} + +/** + * Return true if, using a rough hueruistic, this color could be considered to be using + * too much ink if it was printed using the ink as specified. + * + * NOTE: This is only useful for CMYK profiles. Anything else will return false. + * + * @arg input - Channel values in this space. + */ +bool CMS::overInk(std::vector const &input) const +{ + if (!input.size() || getType() != Type::CMYK) + return false; + + /* Some literature states that when the sum of paint values exceed 320%, it is considered to be a satured color, + which means the paper can get too wet due to an excessive amount of ink. This may lead to several issues + such as misalignment and poor quality of printing in general.*/ + return input[0] + input[1] + input[2] + input[3] > 3.2; +} + +}; // namespace Inkscape::Colors::Space diff --git a/src/colors/spaces/cms.h b/src/colors/spaces/cms.h new file mode 100644 index 0000000000..fb1e5ef1f7 --- /dev/null +++ b/src/colors/spaces/cms.h @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Martin Owens + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_COLORS_SPACES_CMS_H +#define SEEN_COLORS_SPACES_CMS_H + +#include "base.h" + +namespace Inkscape::Colors { +class DocumentCMS; +} + +namespace Inkscape::Colors::Space { + +// A class for containing the icc profile and the machinery for converting colors +class CMS : public AnySpace +{ +public: + CMS(std::shared_ptr profile); + CMS(std::string profile_name, unsigned profile_size, Type profile_type = Type::NONE); + ~CMS() override = default; + + Type getType() const override { return _profile_type; } + std::string const getName() const override { return _profile_name; } + std::string const getIcon() const override { return "color-selector-cms"; } + unsigned int getComponentCount() const override; + + std::shared_ptr const getProfile() const override; + RenderingIntent getIntent() const override { return _intent; } + void setIntent(RenderingIntent intent) { _intent = intent; } + + /** Returns false if this icc profile is not connected to any actual profile */ + bool isValid() const override { return (bool)_profile; } + + void spaceToProfile(std::vector &io) const override; +protected: + friend class Colors::Color; + friend class Colors::DocumentCMS; + + std::string toString(std::vector const &values, bool opacity = true) const override; + uint32_t toRGBA(std::vector const &values, double opacity = 1.0) const override; + + void setName(std::string name) { _profile_name = std::move(name); } + bool overInk(std::vector const &input) const override; + +private: + std::string _profile_name; + unsigned _profile_size; + Type _profile_type; + std::shared_ptr _profile; + RenderingIntent _intent = RenderingIntent::UNKNOWN; + +public: + class CmsParser : public Parser + { + public: + CmsParser() + : Parser("icc-color", Type::CMS) + {} + std::string parseColor(std::istringstream &input, std::vector &output, bool &more) const override; + }; +}; + +} // namespace Inkscape::Colors::Space + +#endif // SEEN_COLORS_SPACES_CMS_H diff --git a/src/colors/spaces/cmyk.cpp b/src/colors/spaces/cmyk.cpp new file mode 100644 index 0000000000..a4ca9ea0e7 --- /dev/null +++ b/src/colors/spaces/cmyk.cpp @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * DeviceCMYK is NOT a color managed color space for ink values, for those + * please see the CMS icc profile based color spaces. + *//* + * Authors: see git history + * + * Copyright (C) 2023 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "cmyk.h" + +#include "colors/color.h" +#include "colors/printer.h" + +namespace Inkscape::Colors::Space { + +/** + * Convert the DeviceCMYK color into sRGB components used in the sRGB icc profile. + * + * See CSS Color Module Level 5, device-cmyk Uncalibrated conversion. + * + * @arg io - A vector of the input values, where the new values will be stored. + */ +void DeviceCMYK::spaceToProfile(std::vector &io) const +{ + double white = 1.0 - io[3]; + io[0] = 1.0 - std::min(1.0, io[0] * white + io[3]); + io[1] = 1.0 - std::min(1.0, io[1] * white + io[3]); + io[2] = 1.0 - std::min(1.0, io[2] * white + io[3]); + + // Delete black from position 3 + io.erase(io.begin() + 3); +} + +/** + * Convert from sRGB icc values to DeviceCMYK values + * + * See CSS Color Module Level 5, device-cmyk Uncalibrated conversion. + * + * @arg io - A vector of the input values, where the new values will be stored. + */ +void DeviceCMYK::profileToSpace(std::vector &io) const +{ + // Insert black channel at position 3 + io.insert(io.begin() + 3, 1.0 - std::max(std::max(io[0], io[1]), io[2])); + double const white = 1.0 - io[3]; + + // Each channel is it's color chart oposite (cyan->red) with a bit of white removed. + io[0] = white ? (1.0 - io[0] - io[3]) / white : 0.0; + io[1] = white ? (1.0 - io[1] - io[3]) / white : 0.0; + io[2] = white ? (1.0 - io[2] - io[3]) / white : 0.0; +} + +/** + * Print the DeviceCMYK color to a CSS Color Module Level 5 string. + * + * @arg values - A vector of doubles for each channel in the DeviceCMYK space + * @arg opacity - True if the opacity should be included in the output. + */ +std::string DeviceCMYK::toString(std::vector const &values, bool opacity) const +{ + auto os = CssFuncPrinter(4, "device-cmyk"); + os << values; + if (opacity && values.size() == 5) + os << values[4]; + return os; +} + +/** + * Return true if, using a rough hueruistic, this color could be considered to be using + * too much ink if it was printed using the ink as specified. + * + * @arg input - Channel values in this space. + */ +bool DeviceCMYK::overInk(std::vector const &input) const +{ + if (!input.size()) + return false; + // Over 320% is considered over inked, see cms.cpp for details. + return input[0] + input[1] + input[2] + input[3] > 3.2; +} + +}; // namespace Inkscape::Colors::Space diff --git a/src/colors/spaces/cmyk.h b/src/colors/spaces/cmyk.h new file mode 100644 index 0000000000..134f92da06 --- /dev/null +++ b/src/colors/spaces/cmyk.h @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Martin Owens + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_COLORS_SPACES_DEVICECMYK_H +#define SEEN_COLORS_SPACES_DEVICECMYK_H + +#include "rgb.h" + +namespace Inkscape::Colors::Space { + +/** + * This sRGB based DeviceCMYK space is uncalibrated and fixed to the sRGB icc profile. + */ +class DeviceCMYK : public RGB +{ +public: + DeviceCMYK() = default; + ~DeviceCMYK() override = default; + + Type getType() const override { return Type::CMYK; } + std::string const getName() const override { return "DeviceCMYK"; } + std::string const getIcon() const override { return "color-selector-cmyk"; } + unsigned int getComponentCount() const override { return 4; } + + void spaceToProfile(std::vector &output) const override; + void profileToSpace(std::vector &output) const override; + +protected: + friend class Inkscape::Colors::Color; + + std::string toString(std::vector const &values, bool opacity = true) const override; + bool overInk(std::vector const &input) const override; +}; + +} // namespace Inkscape::Colors::Space + +#endif // SEEN_COLORS_SPACES_DEVICECMYK_H diff --git a/src/colors/spaces/components.cpp b/src/colors/spaces/components.cpp new file mode 100644 index 0000000000..463bd3168c --- /dev/null +++ b/src/colors/spaces/components.cpp @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Meta data about color channels and how they are presented to users. + *//* + * Authors: see git history + * + * Copyright (C) 2023 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "components.h" + +#include +#include +#include // avoid glib include +#include +#include + +#include "enum.h" + +#define _(String) gettext(String) + +namespace Inkscape::Colors::Space { + +Component::Component(Type type, unsigned int index, std::string id, std::string name, std::string tip, unsigned scale) + : type(type) + , index(index) + , id(std::move(id)) + , name(std::move(name)) + , tip(std::move(tip)) + , scale(scale) +{} + +/** + * Clamp the value to between 0.0 and 1.0, except for hue which is wrapped around. + */ +double Component::normalize(double value) const +{ + if (scale == 360 && (value < 0.0 || value > 1.0)) { + return value - std::floor(value); + } + return std::clamp(value, 0.0, 1.0); +} + +void Components::add(std::string id, std::string name, std::string tip, unsigned scale) +{ + _components.emplace_back(_type, _components.size(), std::move(id), std::move(name), std::move(tip), scale); +} + +std::map _build(bool alpha) +{ + std::map sets; + + sets[Type::RGB].setType(Type::RGB); + sets[Type::RGB].add("r", _("_R:"), _("Red"), 255); + sets[Type::RGB].add("g", _("_G:"), _("Green"), 255); + sets[Type::RGB].add("b", _("_B:"), _("Blue"), 255); + + sets[Type::linearRGB].setType(Type::linearRGB); + sets[Type::linearRGB].add("r", _("l_R:"), _("Linear Red"), 255); + sets[Type::linearRGB].add("g", _("l_G:"), _("Linear Green"), 255); + sets[Type::linearRGB].add("b", _("l_B:"), _("Linear Blue"), 255); + + sets[Type::HSL].setType(Type::HSL); + sets[Type::HSL].add("h", _("_H:"), _("Hue"), 360); + sets[Type::HSL].add("s", _("_S:"), _("Saturation"), 100); + sets[Type::HSL].add("l", _("_L:"), _("Lightness"), 100); + + sets[Type::HSL].setType(Type::HSL); + sets[Type::CMYK].add("c", _("_C:"), _("Cyan"), 100); + sets[Type::CMYK].add("m", _("_M:"), _("Magenta"), 100); + sets[Type::CMYK].add("y", _("_Y:"), _("Yellow"), 100); + sets[Type::CMYK].add("k", _("_K:"), _("Black"), 100); + + sets[Type::CMY].setType(Type::CMY); + sets[Type::CMY].add("c", _("_C:"), _("Cyan"), 100); + sets[Type::CMY].add("m", _("_M:"), _("Magenta"), 100); + sets[Type::CMY].add("y", _("_Y:"), _("Yellow"), 100); + + sets[Type::HSV].setType(Type::HSV); + sets[Type::HSV].add("h", _("_H:"), _("Hue"), 360); + sets[Type::HSV].add("s", _("_S:"), _("Saturation"), 100); + sets[Type::HSV].add("v", _("_V:"), _("Value"), 100); + + sets[Type::HSLUV].setType(Type::HSLUV); + sets[Type::HSLUV].add("h", _("_H*"), _("Hue"), 360); + sets[Type::HSLUV].add("s", _("_S*"), _("Saturation"), 100); + sets[Type::HSLUV].add("l", _("_L*"), _("Lightness"), 100); + + sets[Type::LUV].setType(Type::LUV); + sets[Type::LUV].add("l", _("_L*"), _("Luminance"), 100); + sets[Type::LUV].add("u", _("_u*"), _("Chroma U"), 100); + sets[Type::LUV].add("v", _("_v*"), _("Chroma V"), 100); + + sets[Type::LCH].setType(Type::LCH); + sets[Type::LCH].add("l", _("_L"), _("Luminance"), 255); + sets[Type::LCH].add("c", _("_C"), _("Chroma"), 255); + sets[Type::LCH].add("h", _("_H"), _("Hue"), 360); + + sets[Type::OKHSL].setType(Type::OKHSL); + sets[Type::OKHSL].add("h", _("_Hok"), _("Hue"), 360); + sets[Type::OKHSL].add("s", _("_Sok"), _("Saturation"), 100); + sets[Type::OKHSL].add("l", _("_Lok"), _("Lightness"), 100); + + sets[Type::OKLAB].setType(Type::OKLAB); + sets[Type::OKLAB].add("h", _("_Lok"), _("Lightness"), 100); + sets[Type::OKLAB].add("s", _("_Aok"), _("Component A"), 100); + sets[Type::OKLAB].add("l", _("_Bok"), _("Component B"), 100); + + sets[Type::OKLCH].setType(Type::OKLCH); + sets[Type::OKLCH].add("l", _("_Lok"), _("Lightness"), 100); + sets[Type::OKLCH].add("c", _("_Cok"), _("Chroma"), 100); + sets[Type::OKLCH].add("h", _("_Hok"), _("Hue"), 360); + + sets[Type::XYZ].setType(Type::XYZ); + sets[Type::XYZ].add("x", "_X", "X", 255); + sets[Type::XYZ].add("y", "_Y", "Y", 100); + sets[Type::XYZ].add("z", "_Z", "Z", 255); + + sets[Type::YCbCr].setType(Type::YCbCr); + sets[Type::YCbCr].add("y", "_Y", "Y", 255); + sets[Type::YCbCr].add("cb", "C_b", "Cb", 255); + sets[Type::YCbCr].add("cr", "C_r", "Cr", 255); + + sets[Type::LAB].setType(Type::LAB); + sets[Type::LAB].add("l", "_L", "L", 100); + sets[Type::LAB].add("a", "_a", "a", 256); + sets[Type::LAB].add("b", "_b", "b", 256); + + sets[Type::YXY].setType(Type::YXY); + sets[Type::YXY].add("y1", "_Y", "Y", 255); + sets[Type::YXY].add("x", "_x", "x", 255); + sets[Type::YXY].add("y2", "y", "y", 255); + + sets[Type::Gray].setType(Type::Gray); + sets[Type::Gray].add("gray", _("G:"), _("Gray"), 1024); + + if (alpha) { + for (auto &[key, val] : sets) { + val.add("a", _("_A:"), _("Alpha"), 255); + } + } + return sets; +} + +Components const &Components::get(Type space, bool alpha) +{ + static std::map sets_no_alpha = _build(false); + static std::map sets_with_alpha = _build(true); + + auto &lookup_set = alpha ? sets_with_alpha : sets_no_alpha; + if (auto search = lookup_set.find(space); search != lookup_set.end()) { + return search->second; + } + return lookup_set[Type::NONE]; +} + +}; // namespace Inkscape::Colors::Space diff --git a/src/colors/spaces/components.h b/src/colors/spaces/components.h new file mode 100644 index 0000000000..82c1f1230c --- /dev/null +++ b/src/colors/spaces/components.h @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Jon A. Cruz + * Martin Owens + * + * Copyright (C) 2013-2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_COLORS_COMPONENTS_H +#define SEEN_COLORS_COMPONENTS_H + +#include +#include +#include +#include + +namespace Inkscape::Colors::Space { + +enum class Type; + +struct Component +{ + Component(Type type, unsigned int index, std::string id, std::string name, std::string tip, unsigned scale); + + Type type; + unsigned int index; + std::string id; + std::string name; + std::string tip; + unsigned scale; + + double normalize(double value) const; +}; + +class Components +{ +public: + Components() = default; + + static Components const &get(Type type, bool alpha = false); + + std::vector::const_iterator begin() const { return std::begin(_components); } + std::vector::const_iterator end() const { return std::end(_components); } + Component const &operator[](const unsigned int index) const { return _components[index]; } + + Type getType() const { return _type; } + unsigned size() const { return _components.size(); } + + void add(std::string id, std::string name, std::string tip, unsigned scale); + void setType(Type type) { _type = type; } + +private: + Type _type; + std::vector _components; +}; + +} // namespace Inkscape::Colors::Space + +#endif // SEEN_COLORS_COMPONENTS_H diff --git a/src/colors/spaces/enum.h b/src/colors/spaces/enum.h new file mode 100644 index 0000000000..b5834a577d --- /dev/null +++ b/src/colors/spaces/enum.h @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Martin Owens + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_COLORS_SPACES_ENUM_H +#define SEEN_COLORS_SPACES_ENUM_H + +#include +#include + +namespace Inkscape::Colors { +enum class RenderingIntent +{ + UNKNOWN = 0, + AUTO = 1, + PERCEPTUAL = 2, + RELATIVE_COLORIMETRIC = 3, + SATURATION = 4, + ABSOLUTE_COLORIMETRIC = 5, + // This isn't an SVG standard value, this is an Inkscape additional + // value that means RENDERING_INTENT_RELATIVE_COLORIMETRIC minus + // the black point compensation. This BPC doesn't apply to any other + // rendering intent so is safely folded in here. + RELATIVE_COLORIMETRIC_NOBPC = 6 +}; + +// Used in caching keys and in svg rendering-intent attributes +static std::map intentIds = { + {RenderingIntent::UNKNOWN, ""}, + {RenderingIntent::AUTO, "auto"}, + {RenderingIntent::PERCEPTUAL, "perceptual"}, + {RenderingIntent::SATURATION, "saturation"}, + {RenderingIntent::ABSOLUTE_COLORIMETRIC, "absolute-colorimetric"}, + {RenderingIntent::RELATIVE_COLORIMETRIC, "relative-colorimetric"}, + {RenderingIntent::RELATIVE_COLORIMETRIC_NOBPC, "relative-colorimetric-nobpc"}, +}; + +namespace Space { +// The spaces we support are a mixture of ICC profile spaces +// and internal spaces converted to and from RGB +enum class Type +{ + NONE, // An error of some kind, or destroyed object + Gray, // Grayscale, typical of some print icc profiles + RGB, // sRGB color space typical with SVG + linearRGB, // linear RGB used in SVG and other transforms + HSL, // Hue, Saturation and Lightness, sometimes called HLS + HSV, // Hue, Saturation and Value, similar to HSL and HWB + HWB, // Hue, Whiteness and Blackness, similar to HSL and HSV + CMYK, // Cyan, Magenta, Yellow and Black for print + CMY, // CMYK without the black, used in some icc profiles + XYZ, // Color, Luminance and Blueness, same CIE as RGB + YXY, + LUV, // Lightness and chromaticity, aka CIELUV + LCH, // Lunimance, Chroma and Hue, aka HCL + LAB, // Lightness, Green-Magenta and Blue-Yellow, aka CIELAB + HSLUV, + OKHSL, + OKLCH, + OKLAB, + YCbCr, + CSSNAME, // Special css-named colors + CMS // Special cms type +}; + +} // namespace Space +} // namespace Inkscape::Colors + +#endif // SEEN_COLORS_SPACES_ENUM_H diff --git a/src/colors/spaces/gray.cpp b/src/colors/spaces/gray.cpp new file mode 100644 index 0000000000..069d9ee5ec --- /dev/null +++ b/src/colors/spaces/gray.cpp @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + *//* + * Authors: see git history + * + * Copyright (C) 2023 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "gray.h" + +namespace Inkscape::Colors::Space { + +/** + * Convert a single gray channel into an RGB + */ +void Gray::spaceToProfile(std::vector &io) const +{ + io.insert(io.begin(), io[0]); + io.insert(io.begin(), io[0]); +} + +/** + * Convert an RGB into a gray channel using the HSL method + */ +void Gray::profileToSpace(std::vector &io) const +{ + double max = std::max(std::max(io[0], io[1]), io[2]); + double min = std::min(std::min(io[0], io[1]), io[2]); + // Retain opacity if it exists by removing channels + io.erase(io.begin()); + io.erase(io.begin()); + io[0] = (max + min) / 2.0; // Based on HSL conversion +} + +}; // namespace Inkscape::Colors::Space diff --git a/src/colors/spaces/gray.h b/src/colors/spaces/gray.h new file mode 100644 index 0000000000..7f66a317ec --- /dev/null +++ b/src/colors/spaces/gray.h @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Martin Owens + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_COLORS_SPACES_GRAY_H +#define SEEN_COLORS_SPACES_GRAY_H + +#include "rgb.h" + +namespace Inkscape::Colors::Space { + +class Gray : public RGB +{ + Type getType() const override { return Type::Gray; } + std::string const getName() const override { return "Gray"; } + std::string const getIcon() const override { return "color-selector-gray"; } + unsigned int getComponentCount() const override { return 1; } + +protected: + friend class Inkscape::Colors::Color; + + void spaceToProfile(std::vector &output) const override; + void profileToSpace(std::vector &output) const override; +}; + +} // namespace Inkscape::Colors::Space + +#endif // SEEN_COLORS_SPACES_GRAY_H diff --git a/src/colors/spaces/hsl.cpp b/src/colors/spaces/hsl.cpp new file mode 100644 index 0000000000..c8a2dca1ca --- /dev/null +++ b/src/colors/spaces/hsl.cpp @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + *//* + * Authors: see git history + * + * Copyright (C) 2023 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "hsl.h" + +#include + +#include "colors/color.h" +#include "colors/printer.h" + +namespace Inkscape::Colors::Space { + +static float hue_2_rgb(float v1, float v2, float h) +{ + if (h < 0) + h += 6.0; + if (h > 6) + h -= 6.0; + if (h < 1) + return v1 + (v2 - v1) * h; + if (h < 3) + return v2; + if (h < 4) + return v1 + (v2 - v1) * (4 - h); + return v1; +} + +/** + * Convert the HSL color into sRGB components used in the sRGB icc profile. + */ +void HSL::spaceToProfile(std::vector &output) const +{ + double h = output[0]; + double s = output[1]; + double l = output[2]; + + if (s == 0) { // Gray + output[0] = l; + output[1] = l; + output[2] = l; + } else { + double v2; + if (l < 0.5) { + v2 = l * (1 + s); + } else { + v2 = l + s - l * s; + } + double v1 = 2 * l - v2; + + output[0] = hue_2_rgb(v1, v2, h * 6 + 2.0); + output[1] = hue_2_rgb(v1, v2, h * 6); + output[2] = hue_2_rgb(v1, v2, h * 6 - 2.0); + } +} + +/** + * Convert from sRGB icc values to HSL values + */ +void HSL::profileToSpace(std::vector &output) const +{ + double r = output[0]; + double g = output[1]; + double b = output[2]; + + double max = std::max(std::max(r, g), b); + double min = std::min(std::min(r, g), b); + double delta = max - min; + + double h = 0; + double s = 0; + double l = (max + min) / 2.0; + + if (delta != 0) { + if (l <= 0.5) + s = delta / (max + min); + else + s = delta / (2 - max - min); + + if (r == max) + h = (g - b) / delta; + else if (g == max) + h = 2.0 + (b - r) / delta; + else if (b == max) + h = 4.0 + (r - g) / delta; + + h = h / 6.0; + + if (h < 0) + h += 1; + if (h > 1) + h -= 1; + } + output[0] = h; + output[1] = s; + output[2] = l; +} + +/** + * Print the HSL color to a CSS string. + * + * @arg values - A vector of doubles for each channel in the HSL space + * @arg opacity - True if the opacity should be included in the output. + */ +std::string HSL::toString(std::vector const &values, bool opacity) const +{ + auto oo = CssLegacyPrinter(3, "hsl", opacity && values.size() == 4); + // First entry is Hue, which is in degrees + return oo << (int)(values[0] * 360) << values[1] << values[2] << values.back(); +} + +}; // namespace Inkscape::Colors::Space diff --git a/src/colors/spaces/hsl.h b/src/colors/spaces/hsl.h new file mode 100644 index 0000000000..5a77008595 --- /dev/null +++ b/src/colors/spaces/hsl.h @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Martin Owens + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_COLORS_SPACES_HSL_H +#define SEEN_COLORS_SPACES_HSL_H + +#include "rgb.h" + +namespace Inkscape::Colors::Space { + +class HSL : public RGB +{ +public: + HSL() = default; + ~HSL() override = default; + + Type getType() const override { return Type::HSL; } + std::string const getName() const override { return "HSL"; } + std::string const getIcon() const override { return "color-selector-hsx"; } + +protected: + friend class Inkscape::Colors::Color; + + std::string toString(std::vector const &values, bool opacity = true) const override; + + void spaceToProfile(std::vector &output) const override; + void profileToSpace(std::vector &output) const override; + +public: + class Parser : public HueParser + { + public: + Parser(bool alpha) + : HueParser("hsl", Type::HSL, alpha) + {} + }; +}; + +} // namespace Inkscape::Colors::Space + +#endif // SEEN_COLORS_SPACES_HSL_H diff --git a/src/colors/spaces/hsluv.cpp b/src/colors/spaces/hsluv.cpp new file mode 100644 index 0000000000..653bbae167 --- /dev/null +++ b/src/colors/spaces/hsluv.cpp @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + *//* + * Authors: + * 2015 Alexei Boronine (original idea, JavaScript implementation) + * 2015 Roger Tallada (Obj-C implementation) + * 2017 Martin Mitas (C implementation, based on Obj-C implementation) + * 2021 Massinissa Derriche (C++ implementation for Inkscape, based on C implementation) + * 2023 Martin Owens (New Color classes) + * + * Copyright (C) 2023 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "hsluv.h" + +#include + +namespace Inkscape::Colors::Space { + +/** + * Calculate the bounds of the Luv colors in RGB gamut. + * + * @param l Lightness. Between 0.0 and 100.0. + * @return Bounds of Luv colors in RGB gamut. + */ +std::array HSLuv::get_bounds(double l) +{ + std::array bounds; + + double tl = l + 16.0; + double sub1 = (tl * tl * tl) / 1560896.0; + double sub2 = (sub1 > EPSILON ? sub1 : (l / KAPPA)); + int channel; + int t; + + for (channel = 0; channel < 3; channel++) { + double m1 = d65[channel][0]; + double m2 = d65[channel][1]; + double m3 = d65[channel][2]; + + for (t = 0; t < 2; t++) { + double top1 = (284517.0 * m1 - 94839.0 * m3) * sub2; + double top2 = (838422.0 * m3 + 769860.0 * m2 + 731718.0 * m1) * l * sub2 - 769860.0 * t * l; + double bottom = (632260.0 * m3 - 126452.0 * m2) * sub2 + 126452.0 * t; + + bounds[channel * 2 + t].setCoefficients(top1, -bottom, top2); + } + } + + return bounds; +} + +/** + * Calculate the maximum in gamut chromaticity for the given luminance and hue. + * + * @param l Luminance. + * @param h Hue. + * @return The maximum chromaticity. + */ +static double max_chroma_for_lh(double l, double h) +{ + double min_len = std::numeric_limits::max(); + auto const ray = Geom::Ray(Geom::Point(0, 0), Geom::rad_from_deg(h)); + + for (auto const &line : HSLuv::get_bounds(l)) { + auto intersections = line.intersect(ray); + if (intersections.empty()) { + continue; + } + double len = intersections[0].point().length(); + + if (len >= 0 && len < min_len) { + min_len = len; + } + } + + return min_len; +} + +/** + * Convert a color from the the HSLuv colorspace to the LCH colorspace. + * + * @param in_out[in,out] The HSLuv color converted to a LCH color. + */ +void HSLuv::toLch(std::vector &in_out) +{ + double h = in_out[0] * 360; + double s = in_out[1] * 100; + double l = in_out[2] * 100; + double c; + + /* White and black: disambiguate chroma */ + if (l > 99.9999999 || l < 0.00000001) { + c = 0.0; + } else { + c = max_chroma_for_lh(l, h) / 100.0 * s; + } + + /* Grays: disambiguate hue */ + if (s < 0.00000001) { + h = 0.0; + } + + in_out[0] = l; + in_out[1] = c; + in_out[2] = h; +} + +/** + * Convert a color from the the LCH colorspace to the HSLuv colorspace. + * + * @param in_out[in,out] The LCH color converted to a HSLuv color. + */ +void HSLuv::fromLch(std::vector &in_out) +{ + double l = in_out[0]; + double c = in_out[1]; + double h = in_out[2]; + double s; + + /* White and black: disambiguate saturation */ + if (l > 99.9999999 || l < 0.00000001) { + s = 0.0; + } else { + s = c / max_chroma_for_lh(l, h) * 100.0; + } + + /* Grays: disambiguate hue */ + if (c < 0.00000001) { + h = 0.0; + } + + in_out[0] = h / 360; + in_out[1] = s / 100; + in_out[2] = l / 100; +} + +}; // namespace Inkscape::Colors::Space diff --git a/src/colors/spaces/hsluv.h b/src/colors/spaces/hsluv.h new file mode 100644 index 0000000000..ceaa48aaa6 --- /dev/null +++ b/src/colors/spaces/hsluv.h @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Martin Owens + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_COLORS_SPACES_HSLUV_H +#define SEEN_COLORS_SPACES_HSLUV_H + +#include <2geom/line.h> +#include <2geom/ray.h> + +#include "lch.h" + +#include <2geom/line.h> +#include <2geom/ray.h> + +namespace Inkscape::Colors::Space { + +class HSLuv : public RGB +{ +public: + HSLuv() = default; + ~HSLuv() override = default; + + Type getType() const override { return Type::HSLUV; } + std::string const getName() const override { return "HSLuv"; } + std::string const getIcon() const override { return "color-selector-hsluv"; } + +protected: + friend class Inkscape::Colors::Color; + + void spaceToProfile(std::vector &output) const override + { + HSLuv::toLch(output); + Lch::toLuv(output); + Luv::toXYZ(output); + XYZ::toLinearRGB(output); + LinearRGB::toRGB(output); + } + void profileToSpace(std::vector &output) const override + { + LinearRGB::fromRGB(output); + XYZ::fromLinearRGB(output); + Luv::fromXYZ(output); + Lch::fromLuv(output); + HSLuv::fromLch(output); + } + +public: + static void toLch(std::vector &output); + static void fromLch(std::vector &output); + static std::array get_bounds(double l); +}; + +} // namespace Inkscape::Colors::Space + +#endif // SEEN_COLORS_SPACES_HSLUV_H diff --git a/src/colors/spaces/hsv.cpp b/src/colors/spaces/hsv.cpp new file mode 100644 index 0000000000..88da405343 --- /dev/null +++ b/src/colors/spaces/hsv.cpp @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + *//* + * Authors: see git history + * + * Copyright (C) 2023 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "hsv.h" + +#include + +#include "colors/color.h" +#include "colors/printer.h" + +namespace Inkscape::Colors::Space { + +/** + * Convert the HSV color into sRGB components used in the sRGB icc profile. + */ +void HSV::spaceToProfile(std::vector &output) const +{ + double v = output[2]; + double d = output[0] * 5.99999999; + double f = d - std::floor(d); + double w = v * (1.0 - output[1]); + double q = v * (1.0 - (output[1] * f)); + double t = v * (1.0 - (output[1] * (1.0 - f))); + + if (d < 1.0) { + output[0] = v; + output[1] = t; + output[2] = w; + } else if (d < 2.0) { + output[0] = q; + output[1] = v; + output[2] = w; + } else if (d < 3.0) { + output[0] = w; + output[1] = v; + output[2] = t; + } else if (d < 4.0) { + output[0] = w; + output[1] = q; + output[2] = v; + } else if (d < 5.0) { + output[0] = t; + output[1] = w; + output[2] = v; + } else { + output[0] = v; + output[1] = w; + output[2] = q; + } +} + +/** + * Convert from sRGB icc values to HSV values + */ +void HSV::profileToSpace(std::vector &output) const +{ + double r = output[0]; + double g = output[1]; + double b = output[2]; + + double max = std::max(std::max(r, g), b); + double min = std::min(std::min(r, g), b); + double delta = max - min; + + output[2] = max; + output[1] = max > 0 ? delta / max : 0.0; + + if (output[1] != 0.0) { + if (r == max) { + output[0] = (g - b) / delta; + } else if (g == max) { + output[0] = 2.0 + (b - r) / delta; + } else { + output[0] = 4.0 + (r - g) / delta; + } + output[0] = output[0] / 6.0; + if (output[0] < 0) + output[0] += 1.0; + } else + output[0] = 0.0; +} + +/** + * Parse the hwb css string and convert to hsv inline, if it exists in the input string stream. + */ +bool HSV::fromHwbParser::parse(std::istringstream &ss, std::vector &output) const +{ + if (HueParser::parse(ss, output)) { + // See https://en.wikipedia.org/wiki/HWB_color_model#Converting_to_and_from_HSV + auto scale = output[1] + output[2]; + if (scale > 1.0) { + output[1] /= scale; + output[2] /= scale; + } + output[1] = output[2] == 1.0 ? 0.0 : (1.0 - (output[1] / (1.0 - output[2]))); + output[2] = 1.0 - output[2]; + return true; + } + return false; +} + +/** + * Print the HSV color to a CSS hwb() string. + * + * @arg values - A vector of doubles for each channel in the HSV space + * @arg opacity - True if the opacity should be included in the output. + */ +std::string HSV::toString(std::vector const &values, bool opacity) const +{ + auto oo = CssLegacyPrinter(3, "hwb", opacity && values.size() == 4); + // First entry is Hue, which is in degrees, white and black are dirived + return oo << (int)(values[0] * 360) // Hue, degrees, 0..360 + << (1.0 - values[1]) * values[2] // White + << 1.0 - values[2] // Black + << values.back(); // Opacity +} + +}; // namespace Inkscape::Colors::Space diff --git a/src/colors/spaces/hsv.h b/src/colors/spaces/hsv.h new file mode 100644 index 0000000000..a591775bb3 --- /dev/null +++ b/src/colors/spaces/hsv.h @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Martin Owens + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_COLORS_SPACES_HSV_H +#define SEEN_COLORS_SPACES_HSV_H + +#include "rgb.h" + +namespace Inkscape::Colors::Space { + +class HSV : public RGB +{ +public: + HSV() = default; + ~HSV() override = default; + + Type getType() const override { return Type::HSV; } + std::string const getName() const override { return "HSV"; } + std::string const getIcon() const override { return "color-selector-hsx"; } + + void spaceToProfile(std::vector &output) const override; + void profileToSpace(std::vector &output) const override; + +protected: + friend class Inkscape::Colors::Color; + + std::string toString(std::vector const &values, bool opacity) const override; + +public: + class fromHwbParser : public HueParser + { + public: + fromHwbParser(bool alpha) + : HueParser("hwb", Space::Type::HSV, alpha) + {} + bool parse(std::istringstream &input, std::vector &output) const override; + }; +}; + +} // namespace Inkscape::Colors::Space + +#endif // SEEN_COLORS_SPACES_HSV_H diff --git a/src/colors/spaces/lab.cpp b/src/colors/spaces/lab.cpp new file mode 100644 index 0000000000..96a5ea5a3f --- /dev/null +++ b/src/colors/spaces/lab.cpp @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + *//* + * Authors: + * 2023 Martin Owens + * + * Copyright (C) 2023 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "lab.h" + +#include + +#include "colors/color.h" +#include "colors/printer.h" + +namespace Inkscape::Colors::Space { + +constexpr double LUMA_SCALE = 100; + +// NOTE! Inkscape's calculations use a range of 256, while CSS uses 250 +constexpr double MIN_SCALE = -128; +constexpr double MAX_SCALE = 128; +constexpr double MIN_CSS_SCALE = -125; +constexpr double MAX_CSS_SCALE = 125; + +/** + * Changes the values from 0..1, to typical lab scaling used in calculations. + */ +void Lab::scaleUp(std::vector &in_out) +{ + in_out[0] = SCALE_UP(in_out[0], 0, LUMA_SCALE); + in_out[1] = SCALE_UP(in_out[1], MIN_SCALE, MAX_SCALE); + in_out[2] = SCALE_UP(in_out[2], MIN_SCALE, MAX_SCALE); +} + +/** + * Changes the values from typical lab scaling (see above) to values 0..1. + */ +void Lab::scaleDown(std::vector &in_out) +{ + in_out[0] = SCALE_DOWN(in_out[0], 0, LUMA_SCALE); + in_out[1] = SCALE_DOWN(in_out[1], MIN_SCALE, MAX_SCALE); + 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. + * + * @arg values - A vector of doubles for each channel in the Lab space + * @arg opacity - True if the opacity should be included in the output. + */ +std::string Lab::toString(std::vector const &values, bool opacity) const +{ + auto os = CssFuncPrinter(3, "lab"); + + os << values[0] * LUMA_SCALE // Luminance + << SCALE_UP(values[1], MIN_CSS_SCALE, MAX_CSS_SCALE) // Chroma A + << SCALE_UP(values[2], MIN_CSS_SCALE, MAX_CSS_SCALE); // Chroma B + + if (opacity && values.size() == 4) + os << values[3]; // Optional opacity + + return os; +} + +bool Lab::Parser::parse(std::istringstream &ss, std::vector &output) const +{ + bool end = false; + if (append_css_value(ss, output, end, ',', LUMA_SCALE) // Lightness + && append_css_value(ss, output, end, ',', MAX_CSS_SCALE) // Chroma-A + && append_css_value(ss, output, end, '/', MAX_CSS_SCALE) // Chroma-B + && (append_css_value(ss, output, end) || true) // Optional opacity + && end) { + // The A and B portions are between -100% and 100% leading to this post unit aditional conversion. + output[1] = (output[1] + 1) / 2; + output[2] = (output[2] + 1) / 2; + return true; + } + return false; +} + +}; // namespace Inkscape::Colors::Space diff --git a/src/colors/spaces/lab.h b/src/colors/spaces/lab.h new file mode 100644 index 0000000000..f39e0f33f9 --- /dev/null +++ b/src/colors/spaces/lab.h @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Martin Owens + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_COLORS_SPACES_LAB_H +#define SEEN_COLORS_SPACES_LAB_H + +#include "xyz.h" + +namespace Inkscape::Colors::Space { + +class Lab : public RGB +{ +public: + Lab() = default; + ~Lab() override = default; + + Type getType() const override { return Type::LAB; } + std::string const getName() const override { return "Lab"; } + std::string const getIcon() const override { return "color-selector-lab"; } + +protected: + friend class Inkscape::Colors::Color; + + void spaceToProfile(std::vector &output) const override + { + Lab::toXYZ(output); + XYZ::toLinearRGB(output); + LinearRGB::toRGB(output); + } + void profileToSpace(std::vector &output) const override + { + LinearRGB::fromRGB(output); + XYZ::fromLinearRGB(output); + Lab::fromXYZ(output); + } + + std::string toString(std::vector const &values, bool opacity) const override; + +public: + class Parser : public Colors::Parser + { + public: + Parser() + : Colors::Parser("lab", Type::LAB) + {} + 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); +}; + +} // namespace Inkscape::Colors::Space + +#endif // SEEN_COLORS_SPACES_LAB_H diff --git a/src/colors/spaces/lch.cpp b/src/colors/spaces/lch.cpp new file mode 100644 index 0000000000..b9915e9835 --- /dev/null +++ b/src/colors/spaces/lch.cpp @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + *//* + * Authors: + * 2015 Alexei Boronine (original idea, JavaScript implementation) + * 2015 Roger Tallada (Obj-C implementation) + * 2017 Martin Mitas (C implementation, based on Obj-C implementation) + * 2021 Massinissa Derriche (C++ implementation for Inkscape, based on C implementation) + * 2023 Martin Owens (New Color classes) + * + * Copyright (C) 2023 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "lch.h" + +#include <2geom/ray.h> + +#include "colors/printer.h" + +namespace Inkscape::Colors::Space { + +constexpr double LUMA_SCALE = 100; +constexpr double CHROMA_SCALE = 150; +constexpr double HUE_SCALE = 360; + +/** + * Changes the values from 0..1, to typical lch scaling used + * in calculations. L:0..100, C:0..150 H:0..360 + */ +void Lch::scaleUp(std::vector &in_out) +{ + in_out[0] = SCALE_UP(in_out[0], 0, LUMA_SCALE); + in_out[1] = SCALE_UP(in_out[1], 0, CHROMA_SCALE); + in_out[2] = SCALE_UP(in_out[2], 0, HUE_SCALE); +} + +/** + * Changes the values from typical lch scaling (see above) to + * values 0..1 used in the color module. + */ +void Lch::scaleDown(std::vector &in_out) +{ + in_out[0] = SCALE_DOWN(in_out[0], 0, LUMA_SCALE); + in_out[1] = SCALE_DOWN(in_out[1], 0, CHROMA_SCALE); + in_out[2] = SCALE_DOWN(in_out[2], 0, HUE_SCALE); +} + +/** + * Convert a color from the the LCH colorspace to the Luv colorspace. + * + * @param in_out[in,out] The LCH color converted to a Luv color. + */ +void Lch::toLuv(std::vector &in_out) +{ + double sinhrad, coshrad; + Geom::sincos(Geom::rad_from_deg(in_out[2]), sinhrad, coshrad); + double u = coshrad * in_out[1]; + double v = sinhrad * in_out[1]; + + in_out[1] = u; + in_out[2] = v; +} + +/** + * Convert a color from the the Luv colorspace to the LCH colorspace. + * + * @param in_out[in,out] The Luv color converted to a LCH color. + */ +void Lch::fromLuv(std::vector &in_out) +{ + double l = in_out[0]; + auto uv = Geom::Point(in_out[1], in_out[2]); + double h; + double const c = uv.length(); + + /* Grays: disambiguate hue */ + if (c < 0.00000001) { + h = 0; + } else { + h = Geom::deg_from_rad(Geom::atan2(uv)); + if (h < 0.0) { + h += 360.0; + } + } + in_out[0] = l; + in_out[1] = c; + in_out[2] = h; +} + +/** + * Print the Lch color to a CSS string. + * + * @arg values - A vector of doubles for each channel in the Lch space + * @arg opacity - True if the opacity should be included in the output. + */ +std::string Lch::toString(std::vector const &values, bool opacity) const +{ + auto os = CssFuncPrinter(3, "lch"); + + os << values[0] * LUMA_SCALE // Luminance + << values[1] * CHROMA_SCALE // Chroma + << values[2] * HUE_SCALE; // Hue + + if (opacity && values.size() == 4) + os << values[3]; // Optional opacity + return os; +} + +bool Lch::Parser::parse(std::istringstream &ss, std::vector &output) const +{ + bool end = false; + return append_css_value(ss, output, end, ',', LUMA_SCALE) // Luminance + && append_css_value(ss, output, end, ',', CHROMA_SCALE) // Chroma + && append_css_value(ss, output, end, '/', HUE_SCALE) // Hue + && (append_css_value(ss, output, end) || true) // Optional opacity + && end; +} + +}; // namespace Inkscape::Colors::Space diff --git a/src/colors/spaces/lch.h b/src/colors/spaces/lch.h new file mode 100644 index 0000000000..b423356ff8 --- /dev/null +++ b/src/colors/spaces/lch.h @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Martin Owens + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_COLORS_SPACES_LCH_H +#define SEEN_COLORS_SPACES_LCH_H + +#include "luv.h" + +namespace Inkscape::Colors::Space { + +class Lch : public RGB +{ +public: + Lch() = default; + ~Lch() override = default; + + Type getType() const override { return Type::LCH; } + std::string const getName() const override { return "Lch"; } + std::string const getIcon() const override { return "color-selector-lch"; } + +protected: + friend class Inkscape::Colors::Color; + + void spaceToProfile(std::vector &output) const override + { + Lch::scaleUp(output); + Lch::toLuv(output); + Luv::toXYZ(output); + XYZ::toLinearRGB(output); + LinearRGB::toRGB(output); + } + void profileToSpace(std::vector &output) const override + { + LinearRGB::fromRGB(output); + XYZ::fromLinearRGB(output); + Luv::fromXYZ(output); + Lch::fromLuv(output); + Lch::scaleDown(output); + } + + std::string toString(std::vector const &values, bool opacity) const override; + +public: + class Parser : public Colors::Parser + { + public: + Parser() + : Colors::Parser("lch", Type::LCH) + {} + bool parse(std::istringstream &input, std::vector &output) const override; + }; + + static void toLuv(std::vector &output); + static void fromLuv(std::vector &output); + + static void scaleDown(std::vector &in_out); + static void scaleUp(std::vector &in_out); +}; + +} // namespace Inkscape::Colors::Space + +#endif // SEEN_COLORS_SPACES_LCH_H diff --git a/src/colors/spaces/linear-rgb.cpp b/src/colors/spaces/linear-rgb.cpp new file mode 100644 index 0000000000..34be758976 --- /dev/null +++ b/src/colors/spaces/linear-rgb.cpp @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + *//* + * Authors: + * RafaƂ Siejakowski + * Martin Owens + * + * Copyright (C) 2023 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "linear-rgb.h" + +#include + +#include "colors/printer.h" + +namespace Inkscape::Colors::Space { + +/** + * Convenience function used for RGB conversions. + * + * @param c Value. + * @return RGB color component. + */ +double from_linear(double c) +{ + if (c <= 0.0031308) { + return 12.92 * c; + } else { + return 1.055 * std::pow(c, 1.0 / 2.4) - 0.055; + } +} + +/** + * Convenience function used for RGB conversions. + * + * @param c Value. + * @return XYZ color component. + */ +double to_linear(double c) +{ + if (c > 0.04045) { + return std::pow((c + 0.055) / 1.055, 2.4); + } else { + return c / 12.92; + } +} + +/** + * Convert a color from the a linear RGB colorspace to the sRGB colorspace. + * + * @param in_out[in,out] The linear RGB color converted to a RGB color. + */ +void LinearRGB::toRGB(std::vector &in_out) +{ + in_out[0] = from_linear(in_out[0]); + in_out[1] = from_linear(in_out[1]); + in_out[2] = from_linear(in_out[2]); +} + +/** + * Convert from sRGB icc values to linear RGB values + * + * @param in_out[in,out] The RGB color converted to a linear RGB color. + */ +void LinearRGB::fromRGB(std::vector &in_out) +{ + in_out[0] = to_linear(in_out[0]); + in_out[1] = to_linear(in_out[1]); + in_out[2] = to_linear(in_out[2]); +} + +/** + * Print the RGB color to a CSS Color module 4 srgb-linear color. + * + * @arg values - A vector of doubles for each channel in the RGB space + * @arg opacity - True if the opacity should be included in the output. + */ +std::string LinearRGB::toString(std::vector const &values, bool opacity) const +{ + auto os = CssColorPrinter(3, "srgb-linear"); + os << values; + if (opacity && values.size() == 4) + os << values.back(); + return os; +} + +}; // namespace Inkscape::Colors::Space diff --git a/src/colors/spaces/linear-rgb.h b/src/colors/spaces/linear-rgb.h new file mode 100644 index 0000000000..031fa10ce1 --- /dev/null +++ b/src/colors/spaces/linear-rgb.h @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Martin Owens + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_COLORS_SPACES_LINEARRGB_H +#define SEEN_COLORS_SPACES_LINEARRGB_H + +#include "rgb.h" + +namespace Inkscape::Colors::Space { + +class LinearRGB : public RGB +{ +public: + LinearRGB() = default; + ~LinearRGB() override = default; + + Type getType() const override { return Type::linearRGB; } + std::string const getName() const override { return "linearRGB"; } + std::string const getIcon() const override { return "color-selector-linear-rgb"; } + +protected: + friend class Inkscape::Colors::Color; + + void spaceToProfile(std::vector &output) const override { LinearRGB::toRGB(output); } + void profileToSpace(std::vector &output) const override { LinearRGB::fromRGB(output); } + + std::string toString(std::vector const &values, bool opacity = true) const override; + +public: + static void toRGB(std::vector &output); + static void fromRGB(std::vector &output); +}; + +} // namespace Inkscape::Colors::Space + +#endif // SEEN_COLORS_SPACES_LINEARRGB_H diff --git a/src/colors/spaces/luv.cpp b/src/colors/spaces/luv.cpp new file mode 100644 index 0000000000..da67311f98 --- /dev/null +++ b/src/colors/spaces/luv.cpp @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + *//* + * Authors: + * 2015 Alexei Boronine (original idea, JavaScript implementation) + * 2015 Roger Tallada (Obj-C implementation) + * 2017 Martin Mitas (C implementation, based on Obj-C implementation) + * 2021 Massinissa Derriche (C++ implementation for Inkscape, based on C implementation) + * 2023 Martin Owens (New Color classes) + * + * Copyright (C) 2023 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "luv.h" + +#include + +namespace Inkscape::Colors::Space { + +constexpr double REF_U = 0.19783000664283680764; +constexpr double REF_V = 0.46831999493879100370; + +// There's no CSS for LUV yet, so we don't know what scales +// the w3c might choose to use. Our own calculations use these. +constexpr double LUMA_SCALE = 100; +constexpr double MIN_U = -100; +constexpr double MAX_U = 200; +constexpr double MIN_V = -200; +constexpr double MAX_V = 120; + +/** + * Changes the values from 0..1, to typical luv scaling used in calculations. + */ +void Luv::scaleUp(std::vector &in_out) +{ + in_out[0] = SCALE_UP(in_out[0], 0, LUMA_SCALE); + in_out[1] = SCALE_UP(in_out[1], MIN_U, MAX_U); + in_out[2] = SCALE_UP(in_out[2], MIN_V, MAX_V); +} + +/** + * Changes the values from typical luv scaling (see above) to values 0..1. + */ +void Luv::scaleDown(std::vector &in_out) +{ + in_out[0] = SCALE_DOWN(in_out[0], 0, LUMA_SCALE); + in_out[1] = SCALE_DOWN(in_out[1], MIN_U, MAX_U); + in_out[2] = SCALE_DOWN(in_out[2], MIN_V, MAX_V); +} + +std::vector Luv::fromCoordinates(std::vector const &in) +{ + auto out = in; + scaleDown(out); + return out; +} + +std::vector Luv::toCoordinates(std::vector const &in) +{ + auto out = in; + scaleUp(out); + return out; +} + +/** + * Utility function used to convert from the XYZ colorspace to CIELuv. + * https://en.wikipedia.org/wiki/CIELUV + * + * @param y Y component of the XYZ color. + * @return Luminance component of Luv color. + */ +static double y2l(double y) +{ + if (y <= EPSILON) + return y * KAPPA; + else + return 116.0 * std::cbrt(y) - 16.0; +} + +/** + * Utility function used to convert from CIELuv colorspace to XYZ. + * + * @param l Luminance component of Luv color. + * @return Y component of the XYZ color. + */ +static double l2y(double l) +{ + if (l <= 8.0) { + return l / KAPPA; + } else { + double x = (l + 16.0) / 116.0; + return (x * x * x); + } +} + +/** + * Convert a color from the the Luv colorspace to the XYZ colorspace. + * + * @param in_out[in,out] The Luv color converted to a XYZ color. + */ +void Luv::toXYZ(std::vector &in_out) +{ + if (in_out[0] <= 0.00000001) { + /* Black would create a divide-by-zero error. */ + in_out[0] = 0.0; + in_out[1] = 0.0; + in_out[2] = 0.0; + return; + } + + double var_u = in_out[1] / (13.0 * in_out[0]) + REF_U; + double var_v = in_out[2] / (13.0 * in_out[0]) + REF_V; + double y = l2y(in_out[0]); + double x = -(9.0 * y * var_u) / ((var_u - 4.0) * var_v - var_u * var_v); + double z = (9.0 * y - (15.0 * var_v * y) - (var_v * x)) / (3.0 * var_v); + + in_out[0] = x; + in_out[1] = y; + in_out[2] = z; +} + +/** + * Convert a color from the the XYZ colorspace to the Luv colorspace. + * + * @param in_out[in,out] The XYZ color converted to a Luv color. + */ +void Luv::fromXYZ(std::vector &in_out) +{ + double const denominator = in_out[0] + (15.0 * in_out[1]) + (3.0 * in_out[2]); + double var_u = 4.0 * in_out[0] / denominator; + double var_v = 9.0 * in_out[1] / denominator; + double l = y2l(in_out[1]); + double u = 13.0 * l * (var_u - REF_U); + double v = 13.0 * l * (var_v - REF_V); + + in_out[0] = l; + if (l < 0.00000001) { + in_out[1] = 0.0; + in_out[2] = 0.0; + } else { + in_out[1] = u; + in_out[2] = v; + } +} + +}; // namespace Inkscape::Colors::Space diff --git a/src/colors/spaces/luv.h b/src/colors/spaces/luv.h new file mode 100644 index 0000000000..2acc48e414 --- /dev/null +++ b/src/colors/spaces/luv.h @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Martin Owens + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_COLORS_SPACES_LUV_H +#define SEEN_COLORS_SPACES_LUV_H + +#include "xyz.h" + +namespace Inkscape::Colors::Space { + +// CIE LUV constants +constexpr double KAPPA = 903.29629629629629629630; +constexpr double EPSILON = 0.00885645167903563082; + +class Luv : public RGB +{ +public: + Luv() = default; + ~Luv() override = default; + + Type getType() const override { return Type::LUV; } + std::string const getName() const override { return "Luv"; } + std::string const getIcon() const override { return "color-selector-luv"; } + +protected: + friend class Inkscape::Colors::Color; + + void spaceToProfile(std::vector &output) const override + { + Luv::scaleUp(output); + Luv::toXYZ(output); + XYZ::toLinearRGB(output); + LinearRGB::toRGB(output); + } + void profileToSpace(std::vector &output) const override + { + LinearRGB::fromRGB(output); + XYZ::fromLinearRGB(output); + Luv::fromXYZ(output); + Luv::scaleDown(output); + } + +public: + 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); + + static std::vector fromCoordinates(std::vector const &in); + static std::vector toCoordinates(std::vector const &in); +}; + +} // namespace Inkscape::Colors::Space + +#endif // SEEN_COLORS_SPACES_LUV_H diff --git a/src/colors/spaces/named.cpp b/src/colors/spaces/named.cpp new file mode 100644 index 0000000000..d498592455 --- /dev/null +++ b/src/colors/spaces/named.cpp @@ -0,0 +1,235 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + *//* + * Authors: see git history + * + * Copyright (C) 2023 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "named.h" + +#include +#include + +#include "colors/color.h" // SP_RGBA32_*_F + +namespace Inkscape::Colors::Space { + +class CssColor +{ +public: + typedef std::map ColorMap; + static const ColorMap color_map; +}; + +// These are all the colors defined in the CSS standard +CssColor::ColorMap const CssColor::color_map = { + // clang-format off + {"aliceblue", 0xF0F8FF}, + {"antiquewhite", 0xFAEBD7}, + {"aqua", 0x00FFFF}, + {"aquamarine", 0x7FFFD4}, + {"azure", 0xF0FFFF}, + {"beige", 0xF5F5DC}, + {"bisque", 0xFFE4C4}, + {"black", 0x000000}, + {"blanchedalmond", 0xFFEBCD}, + {"blue", 0x0000FF}, + {"blueviolet", 0x8A2BE2}, + {"brown", 0xA52A2A}, + {"burlywood", 0xDEB887}, + {"cadetblue", 0x5F9EA0}, + {"chartreuse", 0x7FFF00}, + {"chocolate", 0xD2691E}, + {"coral", 0xFF7F50}, + {"cornflowerblue", 0x6495ED}, + {"cornsilk", 0xFFF8DC}, + {"crimson", 0xDC143C}, + {"cyan", 0x00FFFF}, + {"darkblue", 0x00008B}, + {"darkcyan", 0x008B8B}, + {"darkgoldenrod", 0xB8860B}, + {"darkgray", 0xA9A9A9}, + {"darkgreen", 0x006400}, + {"darkgrey", 0xA9A9A9}, + {"darkkhaki", 0xBDB76B}, + {"darkmagenta", 0x8B008B}, + {"darkolivegreen", 0x556B2F}, + {"darkorange", 0xFF8C00}, + {"darkorchid", 0x9932CC}, + {"darkred", 0x8B0000}, + {"darksalmon", 0xE9967A}, + {"darkseagreen", 0x8FBC8F}, + {"darkslateblue", 0x483D8B}, + {"darkslategray", 0x2F4F4F}, + {"darkslategrey", 0x2F4F4F}, + {"darkturquoise", 0x00CED1}, + {"darkviolet", 0x9400D3}, + {"deeppink", 0xFF1493}, + {"deepskyblue", 0x00BFFF}, + {"dimgray", 0x696969}, + {"dimgrey", 0x696969}, + {"dodgerblue", 0x1E90FF}, + {"firebrick", 0xB22222}, + {"floralwhite", 0xFFFAF0}, + {"forestgreen", 0x228B22}, + {"fuchsia", 0xFF00FF}, + {"gainsboro", 0xDCDCDC}, + {"ghostwhite", 0xF8F8FF}, + {"gold", 0xFFD700}, + {"goldenrod", 0xDAA520}, + {"gray", 0x808080}, + {"grey", 0x808080}, + {"green", 0x008000}, + {"greenyellow", 0xADFF2F}, + {"honeydew", 0xF0FFF0}, + {"hotpink", 0xFF69B4}, + {"indianred", 0xCD5C5C}, + {"indigo", 0x4B0082}, + {"ivory", 0xFFFFF0}, + {"khaki", 0xF0E68C}, + {"lavender", 0xE6E6FA}, + {"lavenderblush", 0xFFF0F5}, + {"lawngreen", 0x7CFC00}, + {"lemonchiffon", 0xFFFACD}, + {"lightblue", 0xADD8E6}, + {"lightcoral", 0xF08080}, + {"lightcyan", 0xE0FFFF}, + {"lightgoldenrodyellow", 0xFAFAD2}, + {"lightgray", 0xD3D3D3}, + {"lightgreen", 0x90EE90}, + {"lightgrey", 0xD3D3D3}, + {"lightpink", 0xFFB6C1}, + {"lightsalmon", 0xFFA07A}, + {"lightseagreen", 0x20B2AA}, + {"lightskyblue", 0x87CEFA}, + {"lightslategray", 0x778899}, + {"lightslategrey", 0x778899}, + {"lightsteelblue", 0xB0C4DE}, + {"lightyellow", 0xFFFFE0}, + {"lime", 0x00FF00}, + {"limegreen", 0x32CD32}, + {"linen", 0xFAF0E6}, + {"magenta", 0xFF00FF}, + {"maroon", 0x800000}, + {"mediumaquamarine", 0x66CDAA}, + {"mediumblue", 0x0000CD}, + {"mediumorchid", 0xBA55D3}, + {"mediumpurple", 0x9370DB}, + {"mediumseagreen", 0x3CB371}, + {"mediumslateblue", 0x7B68EE}, + {"mediumspringgreen", 0x00FA9A}, + {"mediumturquoise", 0x48D1CC}, + {"mediumvioletred", 0xC71585}, + {"midnightblue", 0x191970}, + {"mintcream", 0xF5FFFA}, + {"mistyrose", 0xFFE4E1}, + {"moccasin", 0xFFE4B5}, + {"navajowhite", 0xFFDEAD}, + {"navy", 0x000080}, + {"oldlace", 0xFDF5E6}, + {"olive", 0x808000}, + {"olivedrab", 0x6B8E23}, + {"orange", 0xFFA500}, + {"orangered", 0xFF4500}, + {"orchid", 0xDA70D6}, + {"palegoldenrod", 0xEEE8AA}, + {"palegreen", 0x98FB98}, + {"paleturquoise", 0xAFEEEE}, + {"palevioletred", 0xDB7093}, + {"papayawhip", 0xFFEFD5}, + {"peachpuff", 0xFFDAB9}, + {"peru", 0xCD853F}, + {"pink", 0xFFC0CB}, + {"plum", 0xDDA0DD}, + {"powderblue", 0xB0E0E6}, + {"purple", 0x800080}, + {"rebeccapurple", 0x663399}, + {"red", 0xFF0000}, + {"rosybrown", 0xBC8F8F}, + {"royalblue", 0x4169E1}, + {"saddlebrown", 0x8B4513}, + {"salmon", 0xFA8072}, + {"sandybrown", 0xF4A460}, + {"seagreen", 0x2E8B57}, + {"seashell", 0xFFF5EE}, + {"sienna", 0xA0522D}, + {"silver", 0xC0C0C0}, + {"skyblue", 0x87CEEB}, + {"slateblue", 0x6A5ACD}, + {"slategray", 0x708090}, + {"slategrey", 0x708090}, + {"snow", 0xFFFAFA}, + {"springgreen", 0x00FF7F}, + {"steelblue", 0x4682B4}, + {"tan", 0xD2B48C}, + {"teal", 0x008080}, + {"thistle", 0xD8BFD8}, + {"tomato", 0xFF6347}, + {"turquoise", 0x40E0D0}, + {"violet", 0xEE82EE}, + {"wheat", 0xF5DEB3}, + {"white", 0xFFFFFF}, + {"whitesmoke", 0xF5F5F5}, + {"yellow", 0xFFFF00}, + {"yellowgreen", 0x9ACD32}, + // clang-format on +}; + +/** + * Parse a name into RGB values. + */ +bool NamedColor::NameParser::parse(std::istringstream &ss, std::vector &output) const +{ + std::string name; + ss >> name; // ignore whitespace then string + std::transform(name.begin(), name.end(), name.begin(), ::tolower); + auto it = CssColor::color_map.find(name); + if (it != CssColor::color_map.end()) { + auto rgb32 = it->second << 8; + output.emplace_back(SP_RGBA32_R_F(rgb32)); + output.emplace_back(SP_RGBA32_G_F(rgb32)); + output.emplace_back(SP_RGBA32_B_F(rgb32)); + // There is never any opacity set for named colors + return true; + } + return false; +} + +/** + * Print the RGB color to an SVG Tiny compatible color name, or fall back to RGB hex. + * + * @arg values - A vector of doubles for each channel in the RGB space + * @arg opacity - True if the opacity should be included in the output. + */ +std::string NamedColor::toString(std::vector const &values, bool opacity) const +{ + // If opacity is set, pass down + if (values.size() == 4 && opacity) + return RGB::toString(values, true); + + auto name = getNameFor(toRGBA(values)); + if (!name.empty()) + return name; + + return RGB::toString(values, opacity); +} + +/** + * Look up the css color name, if not found returns empty string. + */ +std::string NamedColor::getNameFor(unsigned int rgba) +{ + rgba >>= 8; + // We removed the SVG Tiny support for named colors because it disrupted our ability + // to support SVG named colors properly. If support is needed, add it to a new space. + for (auto &pair : CssColor::color_map) { + if (pair.second == rgba) { + return pair.first; + } + } + return ""; +} + +}; // namespace Inkscape::Colors::Space diff --git a/src/colors/spaces/named.h b/src/colors/spaces/named.h new file mode 100644 index 0000000000..b3dbb46fa7 --- /dev/null +++ b/src/colors/spaces/named.h @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Martin Owens + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_COLORS_SPACES_NAMED_H +#define SEEN_COLORS_SPACES_NAMED_H + +#include "rgb.h" + +namespace Inkscape::Colors::Space { + +/** + * A named color is still a purely RGB color, it's just formatted so it + * can be written back out as a named color faithfully. + */ +class NamedColor : public RGB +{ +public: + NamedColor() = default; + ~NamedColor() override = default; + + Type getType() const override { return Type::CSSNAME; } + Type getComponentType() const override { return Type::RGB; } + std::string const getName() const override { return "CSSNAME"; } + std::string const getIcon() const override { return "color-selector-named"; } + + static std::string getNameFor(unsigned int rgba); + +protected: + friend class Inkscape::Colors::Color; + + std::string toString(std::vector const &values, bool opacity = true) const override; + +public: + class NameParser : public Colors::Parser + { + public: + NameParser() + : Colors::Parser("", Type::CSSNAME) + {} + bool parse(std::istringstream &input, std::vector &output) const override; + }; +}; + +} // namespace Inkscape::Colors::Space + +#endif // SEEN_COLORS_SPACES_NAMED_H diff --git a/src/colors/spaces/okhsl.cpp b/src/colors/spaces/okhsl.cpp new file mode 100644 index 0000000000..54ad62eaea --- /dev/null +++ b/src/colors/spaces/okhsl.cpp @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + *//* + * Authors: + * RafaƂ Siejakowski + * Martin Owens + * + * Copyright (C) 2023 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "okhsl.h" + +#include <2geom/angle.h> +#include + +#include "oklch.h" // max_chroma + +namespace Inkscape::Colors::Space { + +/** + * Convert a color from the the OkHsl colorspace to the OkLab colorspace. + * + * @param in_out[in,out] The OkHsl color converted to an OkLab color. + */ +void OkHsl::toOkLab(std::vector &in_out) +{ + double l = std::clamp(in_out[2], 0.0, 1.0); + + // Get max chroma for this hue and lightness and compute the absolute chroma. + double const chromax = OkLch::max_chroma(l, in_out[0] * 360.0); + double const absolute_chroma = in_out[1] * chromax; + + // Convert hue and chroma to the Cartesian a, b coordinates. + Geom::sincos(in_out[0] * 2.0 * M_PI, in_out[2], in_out[1]); + in_out[0] = l; + in_out[1] *= absolute_chroma; + in_out[2] *= absolute_chroma; +} + +/** + * Convert a color from the the OkLab colorspace to the OkHsl colorspace. + * + * @param in_out[in,out] The OkLab color converted to an OkHsl color. + */ +void OkHsl::fromOkLab(std::vector &in_out) +{ + // Compute the chroma. + double const absolute_chroma = std::hypot(in_out[1], in_out[2]); + if (absolute_chroma < 1e-7) { + // It would be numerically unstable to calculate the hue for this + // color, so we set the hue and saturation to zero (grayscale color). + in_out[2] = in_out[0]; + in_out[1] = 0.0; + in_out[0] = 0.0; + } + + // Compute the hue (in the unit interval). + Geom::Angle const hue_angle = std::atan2(in_out[2], in_out[1]); + in_out[2] = std::clamp(in_out[0], 0.0, 1.0); + in_out[0] = hue_angle.radians0() / (2.0 * M_PI); + + // Compute the linear saturation. + double const hue_degrees = Geom::deg_from_rad(hue_angle.radians0()); + double const chromax = OkLch::max_chroma(in_out[2], hue_degrees); + in_out[1] = (chromax == 0.0) ? 0.0 : std::clamp(absolute_chroma / chromax, 0.0, 1.0); +} + +}; // namespace Inkscape::Colors::Space diff --git a/src/colors/spaces/okhsl.h b/src/colors/spaces/okhsl.h new file mode 100644 index 0000000000..1a9384796b --- /dev/null +++ b/src/colors/spaces/okhsl.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Martin Owens + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_COLORS_SPACES_OKHSL_H +#define SEEN_COLORS_SPACES_OKHSL_H + +#include "oklab.h" + +namespace Inkscape::Colors::Space { + +class OkHsl : public RGB +{ +public: + OkHsl() = default; + ~OkHsl() override = default; + + Type getType() const override { return Type::OKHSL; } + std::string const getName() const override { return "OkHsl"; } + std::string const getIcon() const override { return "color-selector-okhsl"; } + +protected: + friend class Inkscape::Colors::Color; + + void spaceToProfile(std::vector &output) const override + { + OkHsl::toOkLab(output); + OkLab::toLinearRGB(output); + LinearRGB::toRGB(output); + } + void profileToSpace(std::vector &output) const override + { + LinearRGB::fromRGB(output); + OkLab::fromLinearRGB(output); + OkHsl::fromOkLab(output); + } + +public: + static void toOkLab(std::vector &output); + static void fromOkLab(std::vector &output); +}; + +} // namespace Inkscape::Colors::Space + +#endif // SEEN_COLORS_SPACES_OKHSL_H diff --git a/src/colors/spaces/oklab.cpp b/src/colors/spaces/oklab.cpp new file mode 100644 index 0000000000..a666cdf277 --- /dev/null +++ b/src/colors/spaces/oklab.cpp @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + *//* + * Authors: + * RafaƂ Siejakowski + * Martin Owens + * + * Copyright (C) 2023 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "oklab.h" + +#include <2geom/math-utils.h> +#include + +#include "colors/printer.h" + +namespace Inkscape::Colors::Space { + +/* These values are technically unbounded in the actual calculations but are + * defined between -0.4 and 0.4 by the CSS Color Module specification + * as the reasonable upper and lower limits for display. Our internal model + * always scales from 0 to 1 of this expected range of values. + */ +constexpr double MIN_SCALE = -0.4; +constexpr double MAX_SCALE = 0.4; + +void OkLab::scaleUp(std::vector &in_out) +{ + in_out[1] = SCALE_UP(in_out[1], MIN_SCALE, MAX_SCALE); + in_out[2] = SCALE_UP(in_out[2], MIN_SCALE, MAX_SCALE); +} + +void OkLab::scaleDown(std::vector &in_out) +{ + in_out[1] = SCALE_DOWN(in_out[1], MIN_SCALE, MAX_SCALE); + in_out[2] = SCALE_DOWN(in_out[2], MIN_SCALE, MAX_SCALE); +} + +/** Two-dimensional array to store a constant 3x3 matrix. */ +using Matrix = const double[3][3]; + +// clang-format off +/** Matrix of the linear transformation from linear RGB space to linear + * cone responses, used in the first step of RGB to OKLab conversion. + */ +Matrix LRGB2CONE = {{0.4122214708, 0.5363325363, 0.0514459929}, + {0.2119034982, 0.6806995451, 0.1073969566}, + {0.0883024619, 0.2817188376, 0.6299787005}}; + +/** The inverse of the matrix LRGB2CONE. */ +Matrix CONE2LRGB = { + { 4.0767416613479942676681908333711298900607278264432, -3.30771159040819331315866078424893188865618253342, 0.230969928729427886449650619561935920170561518112 }, + { -1.2684380040921760691815055595117506020901414005992, 2.60975740066337143024050095284233623056192338553, -0.341319396310219620992658250306535533187548361872 }, + { -0.0041960865418371092973767821251846315637521173374, -0.70341861445944960601310996913659932654899822384, 1.707614700930944853864541790660472961199090408527 } +}; + +/** The matrix M2 used in the second step of RGB to OKLab conversion. + * Taken from https://bottosson.github.io/posts/oklab/ (retrieved 2022). + */ +Matrix M2 = {{0.2104542553, 0.793617785, -0.0040720468}, + {1.9779984951, -2.428592205, 0.4505937099}, + {0.0259040371, 0.7827717662, -0.808675766}}; + +/** The inverse of the matrix M2. The first column looks like it wants to be 1 but + * this form is closer to the actual inverse (due to numerics). */ +Matrix M2_INVERSE = { + { 0.99999999845051981426207542502031373637162589278552, 0.39633779217376785682345989261573192476766903603, 0.215803758060758803423141461830037892590617787467 }, + { 1.00000000888176077671607524567047071276183677410134, -0.10556134232365634941095687705472233997368274024, -0.063854174771705903405254198817795633810975771082 }, + { 1.00000005467241091770129286515344610721841028698942, -0.08948418209496575968905274586339134130669669716, -1.291485537864091739948928752914772401878545675371 } +}; +// clang-format on + +/** Compute the dot-product between two 3D-vectors. */ +template +inline constexpr double dot3(const A1 &a1, const A2 &a2) +{ + return a1[0] * a2[0] + a1[1] * a2[1] + a1[2] * a2[2]; +} + +/** + * Convert a color from the the OKLab colorspace to the Linear RGB colorspace. + * + * @param in_out[in,out] The OKLab color converted to a Linear RGB color. + */ +void OkLab::toLinearRGB(std::vector &in_out) +{ + std::array cones; + for (unsigned i = 0; i < 3; i++) { + cones[i] = Geom::cube(dot3(M2_INVERSE[i], in_out)); + } + for (unsigned i = 0; i < 3; i++) { + in_out[i] = std::clamp(dot3(CONE2LRGB[i], cones), 0.0, 1.0); + } +} + +/** + * Convert a color from the the Linear RGB colorspace to the OKLab colorspace. + * + * @param in_out[in,out] The Linear RGB color converted to a OKLab color. + */ +void OkLab::fromLinearRGB(std::vector &in_out) +{ + std::vector cones(3); + for (unsigned i = 0; i < 3; i++) { + cones[i] = std::cbrt(dot3(LRGB2CONE[i], in_out)); + } + for (unsigned i = 0; i < 3; i++) { + in_out[i] = dot3(M2[i], cones); + } +} + +bool OkLab::Parser::parse(std::istringstream &ss, std::vector &output) const +{ + bool end = false; + if (append_css_value(ss, output, end, ',') // Luminance + && append_css_value(ss, output, end, ',', MAX_SCALE) // Chroma A + && append_css_value(ss, output, end, '/', MAX_SCALE) // Chroma B + && (append_css_value(ss, output, end) || true) // Optional opacity + && end) { + // Values are between -100% and 100% so post processed into the range 0 to 1 + output[1] = (output[1] + 1) / 2; + output[2] = (output[2] + 1) / 2; + return true; + } + return false; +} + +/** + * Print the Lab color to a CSS string. + * + * @arg values - A vector of doubles for each channel in the Lch space + * @arg opacity - True if the opacity should be included in the output. + */ +std::string OkLab::toString(std::vector const &values, bool opacity) const +{ + auto os = CssFuncPrinter(3, "oklab"); + + os << values[0] // Luminance + << SCALE_UP(values[1], MIN_SCALE, MAX_SCALE) // Chroma A + << SCALE_UP(values[2], MIN_SCALE, MAX_SCALE); // Chroma B + + if (opacity && values.size() == 4) + os << values[3]; // Optional opacity + + return os; +} + +}; // namespace Inkscape::Colors::Space diff --git a/src/colors/spaces/oklab.h b/src/colors/spaces/oklab.h new file mode 100644 index 0000000000..5a8c9908c5 --- /dev/null +++ b/src/colors/spaces/oklab.h @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Martin Owens + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_COLORS_SPACES_OKLAB_H +#define SEEN_COLORS_SPACES_OKLAB_H + +#include "linear-rgb.h" + +namespace Inkscape::Colors::Space { + +class OkLab : public RGB +{ +public: + OkLab() = default; + ~OkLab() override = default; + + Type getType() const override { return Type::OKLAB; } + std::string const getName() const override { return "OkLab"; } + std::string const getIcon() const override { return "color-selector-oklab"; } + +protected: + friend class Inkscape::Colors::Color; + + void spaceToProfile(std::vector &output) const override + { + scaleUp(output); + OkLab::toLinearRGB(output); + LinearRGB::toRGB(output); + } + void profileToSpace(std::vector &output) const override + { + LinearRGB::fromRGB(output); + OkLab::fromLinearRGB(output); + scaleDown(output); + } + + std::string toString(std::vector const &values, bool opacity) const override; + +public: + class Parser : public Colors::Parser + { + public: + Parser() + : Colors::Parser("oklab", Type::OKLAB) + {} + bool parse(std::istringstream &input, std::vector &output) const override; + }; + + static void toLinearRGB(std::vector &output); + static void fromLinearRGB(std::vector &output); + + static void scaleUp(std::vector &in_out); + static void scaleDown(std::vector &in_out); +}; + +} // namespace Inkscape::Colors::Space + +#endif // SEEN_COLORS_SPACES_OKLAB_H diff --git a/src/colors/spaces/oklch.cpp b/src/colors/spaces/oklch.cpp new file mode 100644 index 0000000000..0e0d660f17 --- /dev/null +++ b/src/colors/spaces/oklch.cpp @@ -0,0 +1,389 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + *//* + * Authors: + * RafaƂ Siejakowski + * Martin Owens + * + * Copyright (C) 2023 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "oklch.h" + +#include <2geom/angle.h> +#include <2geom/polynomial.h> +#include + +#include "colors/color.h" +#include "colors/printer.h" + +namespace Inkscape::Colors::Space { + +/* Chroma is technically unbounded in the actual calculations but is + * defined between 0.0 and 0.4 by the CSS Color Module specification + * as the reasonable upper and lower limits for display. Our internal model + * always scales from 0 to 1 of this expected range of values. + */ +constexpr double CHROMA_SCALE = 0.4; +constexpr double HUE_SCALE = 360; + +/** + * Convert a color from the the OkLch colorspace to the OKLab colorspace. + * + * @param in_out[in,out] The OkLch color converted to an OKLab color. + */ +void OkLch::toOkLab(std::vector &in_out) +{ + // c and h are polar coordinates; convert to Cartesian a, b coords. + double c = in_out[1]; + Geom::sincos(Geom::Angle::from_degrees(in_out[2] * HUE_SCALE), in_out[2], in_out[1]); + in_out[1] *= c; + in_out[2] *= c; +} + +/** + * Convert a color from the the OKLab colorspace to the OkLch colorspace. + * + * @param in_out[in,out] The OKLab color converted to an OkLch color. + */ +void OkLch::fromOkLab(std::vector &in_out) +{ + // Convert a, b to polar coordinates c, h. + double c = std::hypot(in_out[1], in_out[2]); + if (c > 0.001) { + Geom::Angle const hue_angle = std::atan2(in_out[2], in_out[1]); + in_out[2] = Geom::deg_from_rad(hue_angle.radians0()) / HUE_SCALE; + } else { + in_out[2] = 0; + } + in_out[1] = c; +} + +/** @brief + * Data needed to compute coefficients in the cubic polynomials which express the lines + * of constant luminosity and hue (but varying chroma) as curves in the linear RGB space. + */ +struct ChromaLineCoefficients +{ + // Variable naming: `c%d` contains coefficients of c^%d in the polynomial, where c is + // the OKLch chroma. l refers to the luminosity, cos and sin to the cosine and sine of + // the hue angle. Trailing digits are exponents. For example, + // c2.lcos2 is the coefficient of (l * cos(hue_angle)^2) in the overall coefficient of c^2. + struct + { + double l2cos, l2sin; + } c1; + struct + { + double lcos2, lcossin, lsin2; + } c2; + struct + { + double cos3, cos2sin, cossin2, sin3; + } c3; +}; + +// clang-format off +ChromaLineCoefficients const LAB_BOUNDS[] = { + // Red polynomial + { + .c1 = { + .l2cos = 5.83279532899080641005754476131631984, + .l2sin = 2.3780791275435732378965655753413412 + }, + .c2 = { + .lcos2 = 1.81614129917652075864819542521099165275, + .lcossin = 2.11851258971260413543962953223104329409, + .lsin2 = 1.68484527361538384522450980300698198391 + }, + .c3 = { + .cos3 = 0.257535869797624151773507242289856932594, + .cos2sin = 0.414490345667882332785000888243122224651, + .cossin2 = 0.126596511492002610582126014059213892767, + .sin3 = -0.455702039844046560333204117380816048203 + } + }, + // Green polynomial + { + .c1 = { + .l2cos = -2.243030176177044107983968331289088261, + .l2sin = 0.00129441240977850026657772225608 + }, + .c2 = { + .lcos2 = -0.5187087369791308621879921351291952375, + .lcossin = -0.7820717390897833607054953914674219281, + .lsin2 = -1.8531911425339782749638630868227383795 + }, + .c3 = { + .cos3 = -0.0817959138495637068389017598370049459, + .cos2sin = -0.1239788660641220973883495153116480854, + .cossin2 = 0.0792215342150077349794741576353537047, + .sin3 = 0.7218132301017783162780535454552058572 + } + }, + // Blue polynomial + { + .c1 = { + .l2cos = -0.2406412780923628220925350522352767957, + .l2sin = -6.48404701978782955733370693958213669 + }, + .c2 = { + .lcos2 = 0.015528352128452044798222201797574285162, + .lcossin = 1.153466975472590255156068122829360981648, + .lsin2 = 8.535379923500727607267514499627438513637 + }, + .c3 = { + .cos3 = -0.0006573855374563134769075967180540368, + .cos2sin = -0.0519029179849443823389557527273309386, + .cossin2 = -0.763927972885238036962716856256210617, + .sin3 = -3.67825541507929556013845659620477582 + } + } +}; +// clang-format on + +/** Stores powers of luminance, hue cosine and hue sine angles. */ +struct ConstraintMonomials +{ + double l, l2, l3, c, c2, c3, s, s2, s3; + ConstraintMonomials(double l, double h) + : l{l} + { + l2 = Geom::sqr(l); + l3 = l2 * l; + Geom::sincos(Geom::rad_from_deg(h), s, c); + c2 = Geom::sqr(c); + c3 = c2 * c; + s2 = 1.0 - c2; // Use sin^2 = 1 - cos^2. + s3 = s2 * s; + } +}; + +/** @brief Find the coefficients of the cubic polynomial expressing the linear + * R, G or B component as a function of OKLch chroma. + * + * The returned polynomial gives R(c), G(c) or B(c) for all values of c and fixed + * values of luminance and hue. + * + * @param index The index of the component to evaluate (0 for R, 1 for G, 2 for B). + * @param m The monomials in L, cos(hue) and sin(hue) needed for the calculation. + * @return an array whose i-th element is the coefficient of c^i in the polynomial. + */ +static std::array component_coefficients(unsigned index, ConstraintMonomials const &m) +{ + auto const &coeffs = LAB_BOUNDS[index]; + std::array result; + // Multiply the coefficients by the corresponding monomials. + result[0] = m.l3; // The coefficient of l^3 is always 1 + result[1] = coeffs.c1.l2cos * m.l2 * m.c + coeffs.c1.l2sin * m.l2 * m.s; + result[2] = coeffs.c2.lcos2 * m.l * m.c2 + coeffs.c2.lcossin * m.l * m.c * m.s + coeffs.c2.lsin2 * m.l * m.s2; + result[3] = + coeffs.c3.cos3 * m.c3 + coeffs.c3.cos2sin * m.c2 * m.s + coeffs.c3.cossin2 * m.c * m.s2 + coeffs.c3.sin3 * m.s3; + return result; +} + +/* Compute the maximum Lch chroma for the given luminosity and hue. + * + * Implementation notes: + * The space of Lch colors is a complicated solid with curved faces in the + * (L, c, h)-space. So it is not easy to find the maximum chroma for the given + * luminosity and hue. (By maximum chroma, we mean the maximum value of c such + * that the color oklch(L c h) still fits in the sRGB gamut.) + * + * We consider an abstract ray (L, c, h) where L and h are fixed and c varies + * from 0 to infinity. Conceptually, we transform this ray to the linear RGB space, + * which is the unit cube. The ray thus becomes a 3D cubic curve in the RGB cube + * and the coordinates R(c), G(c) and B(c) are degree 3 polynomials in the chroma + * variable c. The coefficients of c^i in those polynomials will depend on L and h. + * + * To find the smallest positive value of c for which the curve leaves the unit + * cube, we must solve the equations R(c) = 0, R(c) = 1 and similarly for G(c) + * and B(c). The desired value is the smallest positive solution among those 6 + * equations. + * + * The case of very small or very large luminosity is handled separately. + */ +double OkLch::max_chroma(double l, double h) +{ + static double const EPS = 1e-7; + if (l < EPS || l > 1.0 - EPS) { // Black or white allow no chroma. + return 0; + } + + double chroma_bound = Geom::infinity(); + auto const process_root = [&](double root) -> bool { + if (root < EPS) { // Ignore roots less than epsilon + return false; + } + if (chroma_bound > root) { + chroma_bound = root; + } + return true; + }; + + // Check relevant chroma constraints for all three coordinates R, G, B. + auto const monomials = ConstraintMonomials(l, h); + for (unsigned i = 0; i < 3; i++) { + auto const coeffs = component_coefficients(i, monomials); + // The cubic polynomial is coeffs[3]*c^3 + coeffs[2]*c^2 + coeffs[1]*c + coeffs[0] + + // First we solve for the R/G/B component equal to zero. + for (double root : Geom::solve_cubic(coeffs[3], coeffs[2], coeffs[1], coeffs[0])) { + if (process_root(root)) { + break; + } + } + + // Now solve for the component equal to 1 by subtracting 1.0 from coeffs[0]. + for (double root : Geom::solve_cubic(coeffs[3], coeffs[2], coeffs[1], coeffs[0] - 1.0)) { + if (process_root(root)) { + break; + } + } + } + if (chroma_bound == Geom::infinity()) { // No bound was found, so everything was < EPS + return 0; + } + return chroma_bound; +} + +/** @brief How many intervals a color scale should be subdivided into for the chroma bounds probing. + * + * The reason this constant exists is because probing chroma bounds requires solving 6 cubic equations, + * which would not be feasible for all 1024 pixels on a scale without slowing down the UI. + * To speed things up, we subdivide the scale into COLOR_SCALE_INTERVALS intervals and linearly + * interpolate the chroma bound on each interval. Note that the actual color interpolation is still + * done in the OKLab space, but the computed absolute chroma may be slightly off in the middle of + * each interval (hopefully, in an imperceptible way). + * + * @todo Consider rendering the color sliders asynchronously, which might make this + * interpolation unnecessary. We would then get full precision gradients. + */ +unsigned const COLOR_SCALE_INTERVALS = 32; // Must be a power of 2 and less than 1024. + +uint8_t const *render_hue_scale(double s, double l, std::array *map) +{ + auto const data = map->data(); + auto pos = data; + unsigned const interval_length = 1024 / COLOR_SCALE_INTERVALS; + + double h = 0; // Variable hue + double chroma_bound = OkLch::max_chroma(l, h); + double next_chroma_bound; + double const step = 360.0 / 1024.0; + double const interpolation_step = 360.0 / COLOR_SCALE_INTERVALS; + + for (unsigned i = 0; i < COLOR_SCALE_INTERVALS; i++) { + double const initial_chroma = chroma_bound * s; + next_chroma_bound = OkLch::max_chroma(l, h + interpolation_step); + double const final_chroma = next_chroma_bound * s; + + for (unsigned j = 0; j < interval_length; j++) { + double const c = Geom::lerp(static_cast(j) / interval_length, initial_chroma, final_chroma); + auto rgb = *Color(Space::Type::OKLCH, {l, c, h / 360}).converted(Space::Type::RGB); + *pos++ = (uint8_t)SP_COLOR_F_TO_U(rgb[0]); + *pos++ = (uint8_t)SP_COLOR_F_TO_U(rgb[1]); + *pos++ = (uint8_t)SP_COLOR_F_TO_U(rgb[2]); + *pos++ = 0xFF; + h += step; + } + chroma_bound = next_chroma_bound; + } + return data; +} + +uint8_t const *render_saturation_scale(double h, double l, std::array *map) +{ + auto const data = map->data(); + auto pos = data; + auto chromax = OkLch::max_chroma(l, h); + if (chromax == 0.0) { // Render black or white strip. + uint8_t const bw = (l > 0.9) ? 0xFF : 0x00; + for (size_t i = 0; i < 1024; i++) { + *pos++ = bw; // red + *pos++ = bw; // green + *pos++ = bw; // blue + *pos++ = 0xFF; // alpha + } + } else { // Render strip of varying chroma. + double const chroma_step = chromax / 1024.0; + double c = 0.0; + for (size_t i = 0; i < 1024; i++) { + auto rgb = *Color(Space::Type::OKLCH, {l, c, h}).converted(Space::Type::RGB); + *pos++ = (uint8_t)SP_COLOR_F_TO_U(rgb[0]); + *pos++ = (uint8_t)SP_COLOR_F_TO_U(rgb[1]); + *pos++ = (uint8_t)SP_COLOR_F_TO_U(rgb[2]); + *pos++ = 0xFF; + c += chroma_step; + } + } + return data; +} + +uint8_t const *render_lightness_scale(double h, double s, std::array *map) +{ + auto const data = map->data(); + auto pos = data; + unsigned const interval_length = 1024 / COLOR_SCALE_INTERVALS; + + double l = 0; // Variable lightness + + double chroma_bound = OkLch::max_chroma(l, h); + double next_chroma_bound; + double const step = 1.0 / 1024.0; + double const interpolation_step = 1.0 / COLOR_SCALE_INTERVALS; + + for (unsigned i = 0; i < COLOR_SCALE_INTERVALS; i++) { + double const initial_chroma = chroma_bound * s; + next_chroma_bound = OkLch::max_chroma(l + interpolation_step, h); + double const final_chroma = next_chroma_bound * s; + + for (unsigned j = 0; j < interval_length; j++) { + double const c = Geom::lerp(static_cast(j) / interval_length, initial_chroma, final_chroma); + auto rgb = *Color(Space::Type::OKLCH, {l, c, h}).converted(Space::Type::RGB); + *pos++ = (uint8_t)SP_COLOR_F_TO_U(rgb[0]); + *pos++ = (uint8_t)SP_COLOR_F_TO_U(rgb[1]); + *pos++ = (uint8_t)SP_COLOR_F_TO_U(rgb[2]); + *pos++ = 0xFF; + l += step; + } + chroma_bound = next_chroma_bound; + } + return data; +} + +bool OkLch::Parser::parse(std::istringstream &ss, std::vector &output) const +{ + bool end = false; + if (append_css_value(ss, output, end, ',') // Luminance + && append_css_value(ss, output, end, ',', CHROMA_SCALE) // Chroma + && append_css_value(ss, output, end, '/', HUE_SCALE) // Hue + && (append_css_value(ss, output, end) || true) // Optional opacity + && end) { + return true; + } + return false; +} + +/** + * Print the Lab color to a CSS string. + * + * @arg values - A vector of doubles for each channel in the Lch space + * @arg opacity - True if the opacity should be included in the output. + */ +std::string OkLch::toString(std::vector const &values, bool opacity) const +{ + auto os = CssFuncPrinter(3, "oklch"); + + os << values[0] // Luminance + << values[1] * CHROMA_SCALE // Chroma + << values[2] * HUE_SCALE; // Hue + + if (opacity && values.size() == 4) + os << values[3]; // Optional opacity + + return os; +} + +}; // namespace Inkscape::Colors::Space diff --git a/src/colors/spaces/oklch.h b/src/colors/spaces/oklch.h new file mode 100644 index 0000000000..a2529c9340 --- /dev/null +++ b/src/colors/spaces/oklch.h @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Martin Owens + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_COLORS_SPACES_OKLCH_H +#define SEEN_COLORS_SPACES_OKLCH_H + +#include "oklab.h" + +namespace Inkscape::Colors::Space { + +class OkLch : public RGB +{ +public: + OkLch() = default; + ~OkLch() override = default; + + Type getType() const override { return Type::OKLCH; } + std::string const getName() const override { return "OkLch"; } + std::string const getIcon() const override { return "color-selector-oklch"; } + +protected: + friend class Inkscape::Colors::Color; + + void spaceToProfile(std::vector &output) const override + { + OkLch::toOkLab(output); + OkLab::toLinearRGB(output); + LinearRGB::toRGB(output); + } + void profileToSpace(std::vector &output) const override + { + LinearRGB::fromRGB(output); + OkLab::fromLinearRGB(output); + OkLch::fromOkLab(output); + } + + std::string toString(std::vector const &values, bool opacity) const override; + +public: + class Parser : public Colors::Parser + { + public: + Parser() + : Colors::Parser("oklch", Type::OKLCH) + {} + bool parse(std::istringstream &input, std::vector &output) const override; + }; + + static void toOkLab(std::vector &output); + static void fromOkLab(std::vector &output); + static double max_chroma(double l, double h); +}; + +uint8_t const *render_hue_scale(double s, double l, std::array *map); +uint8_t const *render_saturation_scale(double h, double l, std::array *map); +uint8_t const *render_lightness_scale(double h, double s, std::array *map); + +} // namespace Inkscape::Colors::Space + +#endif // SEEN_COLORS_SPACES_OKLCH_H diff --git a/src/colors/spaces/rgb.cpp b/src/colors/spaces/rgb.cpp new file mode 100644 index 0000000000..4ae73b2970 --- /dev/null +++ b/src/colors/spaces/rgb.cpp @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + *//* + * Authors: see git history + * + * Copyright (C) 2023 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "rgb.h" + +#include + +#include "colors/cms/profile.h" +#include "colors/color.h" +#include "colors/utils.h" + +namespace Inkscape::Colors::Space { + +/** + * Return the RGB color profile, this is static for all RGB sub-types + */ +std::shared_ptr const RGB::getProfile() const +{ + static std::shared_ptr srgb_profile; + if (!srgb_profile) { + srgb_profile = Colors::CMS::Profile::create_srgb(); + } + return srgb_profile; +} + +/** + * Print the RGB color to a CSS Hex code of 6 or 8 digits. + * + * @arg values - A vector of doubles for each channel in the RGB space + * @arg opacity - True if the opacity should be included in the output. + */ +std::string RGB::toString(std::vector const &values, bool opacity) const +{ + return rgba_to_hex(toRGBA(values), values.size() == 4 && opacity); +} + +/** + * Convert the color into an RGBA32 for use within Gdk rendering. + */ +uint32_t RGB::toRGBA(std::vector const &values, double opacity) const +{ + if (getType() != Type::RGB) { + std::vector copy = values; + spaceToProfile(copy); + return _to_rgba(copy, opacity); + } + return _to_rgba(values, opacity); +} + +uint32_t RGB::_to_rgba(std::vector const &values, double opacity) const +{ + switch (values.size()) { + case 3: + return SP_RGBA32_F_COMPOSE(values[0], values[1], values[2], opacity); + case 4: + return SP_RGBA32_F_COMPOSE(values[0], values[1], values[2], opacity * values[3]); + default: + throw ColorError("Color values should be size 3 for RGB or 4 for RGBA."); + } + return 0x0; // transparent black +} + +bool RGB::Parser::parse(std::istringstream &ss, std::vector &output) const +{ + bool end = false; + return append_css_value(ss, output, end, ',', 255) // Red + && append_css_value(ss, output, end, ',', 255) // Green + && append_css_value(ss, output, end, !_alpha ? '/' : ',', 255) // Blue + && (append_css_value(ss, output, end) || !_alpha) // Opacity + && end; +} + +}; // namespace Inkscape::Colors::Space diff --git a/src/colors/spaces/rgb.h b/src/colors/spaces/rgb.h new file mode 100644 index 0000000000..3754bea015 --- /dev/null +++ b/src/colors/spaces/rgb.h @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Martin Owens + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_COLORS_SPACES_RGB_H +#define SEEN_COLORS_SPACES_RGB_H + +#include "base.h" + +namespace Inkscape::Colors::Space { + +class RGB : public AnySpace +{ +public: + RGB() = default; + ~RGB() override = default; + + Type getType() const override { return Type::RGB; } + std::string const getName() const override { return "RGB"; } + std::string const getIcon() const override { return "color-selector-rgb"; } + unsigned int getComponentCount() const override { return 3; } + std::shared_ptr const getProfile() const override; + +protected: + friend class Inkscape::Colors::Color; + + std::string toString(std::vector const &values, bool opacity = true) const override; + uint32_t toRGBA(std::vector const &values, double opacity = 1.0) const override; + +public: + class Parser : public LegacyParser + { + public: + Parser(bool alpha) + : LegacyParser("rgb", Type::RGB, alpha) + {} + bool parse(std::istringstream &input, std::vector &output) const override; + }; + +private: + uint32_t _to_rgba(std::vector const &values, double opacity) const; +}; + +} // namespace Inkscape::Colors::Space + +#endif // SEEN_COLORS_SPACES_RGB_H diff --git a/src/colors/spaces/xyz.cpp b/src/colors/spaces/xyz.cpp new file mode 100644 index 0000000000..19150c8e8c --- /dev/null +++ b/src/colors/spaces/xyz.cpp @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + *//* + * Authors: + * 2015 Alexei Boronine (original idea, JavaScript implementation) + * 2015 Roger Tallada (Obj-C implementation) + * 2017 Martin Mitas (C implementation, based on Obj-C implementation) + * 2021 Massinissa Derriche (C++ implementation for Inkscape, based on C implementation) + * 2023 Martin Owens (New Color classes) + * + * Copyright (C) 2023 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "xyz.h" + +#include + +#include "colors/printer.h" + +namespace Inkscape::Colors::Space { + +/** + * Calculate the dot product of the given arrays. + * + * @param t1 The first array. + * @param t2 The second array. + * @return The resulting dot product. + */ +static double dot_product(std::vector const &t1, std::vector const &t2) +{ + return (t1[0] * t2[0] + t1[1] * t2[1] + t1[2] * t2[2]); +} + +/** + * Convert a color from the the XYZ colorspace to the RGB colorspace. + * + * @param in_out[in,out] The XYZ color converted to a RGB color. + */ +void XYZ::toLinearRGB(std::vector &in_out) +{ + std::vector result = in_out; // copy + for (size_t i : {0, 1, 2}) { + result[i] = dot_product(d65[i], in_out); + } + in_out = result; +} + +/** + * Convert from sRGB icc values to XYZ values + * + * @param in_out[in,out] The RGB color converted to a XYZ color. + */ +void XYZ::fromLinearRGB(std::vector &in_out) +{ + std::vector result = in_out; // copy + for (size_t i : {0, 1, 2}) { + result[i] = dot_product(in_out, d65_inv[i]); + } + in_out = result; +} + +/** + * Print the RGB color to a CSS Color module 4 xyz-d65 color. + * + * @arg values - A vector of doubles for each channel in the RGB space + * @arg opacity - True if the opacity should be included in the output. + */ +std::string XYZ::toString(std::vector const &values, bool opacity) const +{ + auto os = CssColorPrinter(3, "xyz"); + os << values; + if (opacity && values.size() == 4) + os << values[3]; + return os; +} + +}; // namespace Inkscape::Colors::Space diff --git a/src/colors/spaces/xyz.h b/src/colors/spaces/xyz.h new file mode 100644 index 0000000000..3b097b7906 --- /dev/null +++ b/src/colors/spaces/xyz.h @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Martin Owens + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_COLORS_SPACES_XYZ_H +#define SEEN_COLORS_SPACES_XYZ_H + +#include "linear-rgb.h" + +namespace Inkscape::Colors::Space { + +// CIE standard illuminant D65, Observer= 2° [0.9504, 1.0000, 1.0888]. +// Simulates noon daylight with correlated color temperature of 6504 K. +inline const std::vector illuminant_d65 = {0.9504, 1.0000, 1.0888}; + +/* for sRGB, reference white D65 */ +inline const std::vector d65[3] = {{3.24096994190452134377, -1.53738317757009345794, -0.49861076029300328366}, + {-0.96924363628087982613, 1.87596750150772066772, 0.04155505740717561247}, + {0.05563007969699360846, -0.20397695888897656435, 1.05697151424287856072}}; + +inline const std::vector d65_inv[3] = {{0.41239079926595949381, 0.35758433938387799725, 0.18048078840183429261}, + {0.21263900587151036595, 0.71516867876775596569, 0.07219231536073371975}, + {0.019330818715591851469, 0.1191947797946259924, 0.9505321522496605464}}; + +class XYZ : public RGB +{ +public: + XYZ() = default; + ~XYZ() override = default; + + Type getType() const override { return Type::XYZ; } + std::string const getName() const override { return "XYZ"; } + std::string const getIcon() const override { return "color-selector-xyz"; } + +protected: + friend class Inkscape::Colors::Color; + + void spaceToProfile(std::vector &output) const override + { + XYZ::toLinearRGB(output); + LinearRGB::toRGB(output); + } + void profileToSpace(std::vector &output) const override + { + LinearRGB::fromRGB(output); + XYZ::fromLinearRGB(output); + } + + std::string toString(std::vector const &values, bool opacity = true) const override; + +public: + static void toLinearRGB(std::vector &output); + static void fromLinearRGB(std::vector &output); +}; + +} // namespace Inkscape::Colors::Space + +#endif // SEEN_COLORS_SPACES_XYZ_H diff --git a/src/colors/utils.cpp b/src/colors/utils.cpp new file mode 100644 index 0000000000..936ee829e0 --- /dev/null +++ b/src/colors/utils.cpp @@ -0,0 +1,202 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2023 AUTHORS + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include +#include +#include +#include + +#include "colors/color.h" +#include "colors/spaces/base.h" +#include "colors/spaces/named.h" +#include "spaces/enum.h" + +namespace Inkscape::Colors { + +/** + * Parse a color directly without any CSS or CMS support. This function is ONLY + * intended to parse values stored in inkscape specific screen-attributes and + * preferences. + * + * DO NOT use this as a common color parser, it does not support any other format + * other than RRGGBBAA and anything else will cause an error. + * + * @arg value - Must be in format #RRGGBBAA only or an empty string. + */ +uint32_t hex_to_rgba(std::string const &value) +{ + if (value.empty()) + return 0x0; + + std::istringstream ss(value); + if (value.size() != 9 || ss.get() != '#') { + throw ColorError("Baddly formatted color, it must be in #RRGGBBAA format"); + } + unsigned int hex; + ss >> std::hex >> hex; + return hex; +} + +/** + * Convert a 32bit unsigned int into a set of 3 or 4 double values for rgba. + * + * @arg rgba - The integer in the format 0xRRGGBBAA + * @arg opacity - Include the opacity channel, if false throws opacity away. + * + * @returns the values of the rgba channels between 0.0 and 1.0 + */ +std::vector rgba_to_values(uint32_t rgba, bool opacity) +{ + std::vector values(3 + opacity); + values[0] = SP_RGBA32_R_F(rgba); + values[1] = SP_RGBA32_G_F(rgba); + values[2] = SP_RGBA32_B_F(rgba); + if (opacity) { + values[3] = SP_RGBA32_A_F(rgba); + } + return values; +} + +/** + * Output the RGBA value as a #RRGGBB hex color, if alpha is true + * then the output will be #RRGGBBAA instead. + */ +std::string rgba_to_hex(uint32_t value, bool alpha) +{ + std::ostringstream oo; + oo.imbue(std::locale("C")); + oo << "#" << std::setfill('0') << std::setw(alpha ? 8 : 6) << std::hex << (alpha ? value : value >> 8); + return oo.str(); +} + +/** + * Create a somewhat unique id for the given color used for palette identification. + */ +std::string color_to_id(std::optional const &color) +{ + if (!color) + return "none"; + + auto name = color->getName(); + if (!name.empty() && name[0] != '#') + return desc_to_id(name); + + std::ostringstream oo; + + // Special case cssname + if (auto cns = std::dynamic_pointer_cast(color->getSpace())) { + auto name = cns->getNameFor(color->toRGBA()); + if (!name.empty()) { + oo << "css-" << color->toString(); + return oo.str(); + } + } + + oo << color->getSpace()->getName() << "-" << std::hex << std::setfill('0'); + for (double const &value : color->getValues()) { + unsigned int diget = value * 0xff; + oo << std::setw(2) << diget; + } + + auto ret = oo.str(); + std::transform(ret.begin(), ret.end(), ret.begin(), ::tolower); + return ret; +} + +/** + * Transform a color name or description into an id used for palette identification. + */ +std::string desc_to_id(std::string const &desc) +{ + auto name = Glib::ustring(desc); + // Convert description to ascii, strip out symbols, remove duplicate dashes and prefixes + static auto const reg1 = Glib::Regex::create("[^[:alnum:]]"); + name = reg1->replace(name, 0, "-", static_cast(0)); + static auto const reg2 = Glib::Regex::create("-{2,}"); + name = reg2->replace(name, 0, "-", static_cast(0)); + static auto const reg3 = Glib::Regex::create("(^-|-$)"); + name = reg3->replace(name, 0, "", static_cast(0)); + // Move important numbers from the start where they are invalid xml, to the end. + static auto const reg4 = Glib::Regex::create("^(\\d+)(-?)([^\\d]*)"); + name = reg4->replace(name, 0, "\\3\\2\\1", static_cast(0)); + return name.lowercase(); +} + +/** + * Make a darker or lighter version of the color, useful for making checkerboards. + */ +Color make_contrasted_color(Color const &orig, double amount) +{ + if (auto color = orig.converted(Space::Type::HSL)) { + auto lightness = (*color)[2]; + color->set(2, lightness + ((lightness < 0.08 ? 0.08 : -0.08) * amount)); + color->convert(orig.getSpace()); + return *color; + } + return orig; +} + +/** + * Make a themed dark or light color based on a previous shade, returns RGB color. + */ +Color make_theme_color(Color const &orig, bool dark) +{ + // color of the image strip to HSL, so we can manipulate its lightness + auto color = *orig.converted(Colors::Space::Type::HSLUV); + + if (dark) { + // limit saturation to improve contrast with some artwork + color.set(1, std::min(color[1], 0.008)); + // make a darker shade and limit to remove extremes + color.set(2, std::min(color[2] * 0.7, 0.003)); + } else { + // make a lighter shade and limit to remove extemes + color.set(2, std::max(color[2] + (0.01 - color[2]) * 0.5, 0.008)); + } + + return *color.converted(Colors::Space::Type::RGB); +} + +double perceptual_lightness(double l) +{ + return l <= 0.885645168 ? l * 0.09032962963 : std::cbrt(l) * 0.249914424 - 0.16; +} + +/** + * Return a value for how the light the color appears to be using HSLUV + */ +double get_perceptual_lightness(Color const &color) +{ + return perceptual_lightness((*color.converted(Space::Type::HSLUV))[2] * 100); +} + +std::pair get_contrasting_color(double l) +{ + double constexpr l_threshold = 0.85; + if (l > l_threshold) { // Draw dark over light. + auto t = (l - l_threshold) / (1.0 - l_threshold); + return {0.0, 0.4 - 0.1 * t}; + } else { // Draw light over dark. + auto t = (l_threshold - l) / l_threshold; + return {1.0, 0.6 + 0.1 * t}; + } +} + +}; // namespace Inkscape::Colors + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/colors/utils.h b/src/colors/utils.h new file mode 100644 index 0000000000..712dc48891 --- /dev/null +++ b/src/colors/utils.h @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2023 AUTHORS + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_COLORS_UTILS_H +#define SEEN_COLORS_UTILS_H + +#include +#include +#include +#include + +/* Useful composition macros */ + +constexpr double SP_COLOR_U_TO_F(uint32_t v) +{ + return v / 255.0; +} +constexpr uint32_t SP_COLOR_F_TO_U(double v) +{ + return (unsigned int)(v * 255. + .5); +} +constexpr uint32_t SP_RGBA32_R_U(uint32_t v) +{ + return (v >> 24) & 0xff; +} +constexpr uint32_t SP_RGBA32_G_U(uint32_t v) +{ + return (v >> 16) & 0xff; +} +constexpr uint32_t SP_RGBA32_B_U(uint32_t v) +{ + return (v >> 8) & 0xff; +} +constexpr uint32_t SP_RGBA32_A_U(uint32_t v) +{ + return v & 0xff; +} +constexpr double SP_RGBA32_R_F(uint32_t v) +{ + return SP_COLOR_U_TO_F(SP_RGBA32_R_U(v)); +} +constexpr double SP_RGBA32_G_F(uint32_t v) +{ + return SP_COLOR_U_TO_F(SP_RGBA32_G_U(v)); +} +constexpr double SP_RGBA32_B_F(uint32_t v) +{ + return SP_COLOR_U_TO_F(SP_RGBA32_B_U(v)); +} +constexpr double SP_RGBA32_A_F(uint32_t v) +{ + return SP_COLOR_U_TO_F(SP_RGBA32_A_U(v)); +} + +constexpr uint32_t SP_RGBA32_U_COMPOSE(uint32_t r, uint32_t g, uint32_t b, uint32_t a) +{ + return ((r & 0xff) << 24) | ((g & 0xff) << 16) | ((b & 0xff) << 8) | (a & 0xff); +} +constexpr uint32_t SP_RGBA32_F_COMPOSE(double r, double g, double b, double a) +{ + return SP_RGBA32_U_COMPOSE(SP_COLOR_F_TO_U(r), SP_COLOR_F_TO_U(g), SP_COLOR_F_TO_U(b), SP_COLOR_F_TO_U(a)); +} +constexpr uint32_t SP_RGBA32_C_COMPOSE(uint32_t c, double o) +{ + return SP_RGBA32_U_COMPOSE(SP_RGBA32_R_U(c), SP_RGBA32_G_U(c), SP_RGBA32_B_U(c), SP_COLOR_F_TO_U(o)); +} + +/** + * A set of useful color modifying functions which do not fit as generic + * methods on the color class itself but which are used in various places. + */ +namespace Inkscape::Colors { + +class Color; + +uint32_t hex_to_rgba(std::string const &value); +std::vector rgba_to_values(uint32_t rgba, bool opacity); +std::string rgba_to_hex(uint32_t value, bool alpha = false); +std::string color_to_id(std::optional const &color); +std::string desc_to_id(std::string const &desc); + +Color make_contrasted_color(Color const &orig, double amount); +Color make_theme_color(Color const &orig, bool dark); + +double lightness(Color color); +double perceptual_lightness(double l); +double get_perceptual_lightness(Color const &color); +std::pair get_contrasting_color(double l); + +} // namespace Inkscape::Colors + +#endif // SEEN_COLORS_UTILS_H diff --git a/src/colors/xml-color.cpp b/src/colors/xml-color.cpp new file mode 100644 index 0000000000..c900f02ced --- /dev/null +++ b/src/colors/xml-color.cpp @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Martin Owens + * + * Copyright (C) 2023 author + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "xml-color.h" + +#include "cms/profile.h" +#include "cms/system.h" +#include "colors/color.h" +#include "document-cms.h" +#include "document.h" +#include "manager.h" +#include "spaces/base.h" +#include "spaces/cms.h" +#include "spaces/components.h" +#include "xml/node-iterators.h" +#include "xml/node.h" +#include "xml/repr.h" +#include "xml/simple-document.h" + +namespace Inkscape::Colors { + +/** + * Turn a color into a color xml document, used for drag and drop. + * + * @arg paint - The color or none to convert into xml + */ +std::string paint_to_xml_string(Paint const &paint) +{ + auto doc = paint_to_xml(paint); + auto ret = sp_repr_save_buf(doc); + GC::release(doc); + return ret; +} + +/** + * Parse an xml document into a color. Usually from a drag and drop. + * + * @arg xmls - A string of a color xml document + * @arg doc - An optional document to match icc profiles + */ +Paint xml_string_to_paint(std::string const &xmls, SPDocument *doc) +{ + auto color_doc = sp_repr_read_buf(xmls, nullptr); + auto ret = xml_to_paint(color_doc, doc); + GC::release(color_doc); + return ret; +} + +XML::Document *paint_to_xml(Paint const &paint) +{ + auto *document = new XML::SimpleDocument(); + auto root = document->createElement("paint"); + document->appendChild(root); + + if (std::holds_alternative(paint)) { + auto node = document->createElement("nocolor"); + root->appendChild(node); + GC::release(node); + GC::release(root); + return document; + } + + auto &color = std::get(paint); + auto space = color.getSpace(); + + // This format is entirely inkscape's creation and doesn't work with anything + // outside of inkscape. It's completely safe to change at any time since the + // data is never saved to a file. + auto node = document->createElement("color"); + node->setAttribute("space", space->getName()); + node->setAttributeOrRemoveIfEmpty("name", color.getName()); + root->appendChild(node); + + if (auto cms = std::dynamic_pointer_cast(space)) { + if (auto profile = cms->getProfile()) { + // Store the unique icc profile id, so we have a chance of matching it + node->setAttribute("icc", profile->getId()); + } + } + + if (color.hasOpacity()) { + node->setAttributeSvgDouble("opacity", color.getOpacity()); + } + + auto components = space->getComponents(); + for (unsigned int i = 0; i < components.size() && i < color.size(); i++) { + node->setAttributeCssDouble(components[i].id, color[i]); + } + + GC::release(node); + GC::release(root); + return document; +} + +Paint xml_to_paint(XML::Document const *xml, SPDocument *doc) +{ + auto get_node = [](XML::Node const *node, std::string const &name) { + XML::NodeConstSiblingIterator iter{node->firstChild()}; + for (; iter; ++iter) { + if (iter->name() && name == iter->name()) { + return &*iter; + } + } + return (const Inkscape::XML::Node *)(nullptr); + }; + + if (auto const paint = get_node(xml, "paint")) { + if (get_node(paint, "nocolor")) { + return NoColor(); + } + if (auto color_xml = get_node(paint, "color")) { + auto space_name = color_xml->attribute("space"); + + if (!space_name) { + throw ColorError("Invalid color data, no space specified."); + } + + auto space = Manager::get().find(space_name); + + if (!space && doc) + space = doc->getDocumentCMS().getSpace(space_name); + + if (auto icc_id = color_xml->attribute("icc")) { + // Make a temporary space for the icc information, if possible + if (!space) + if (auto profile = CMS::System::get().getProfile(icc_id)) { + auto cms = std::make_shared(profile); + cms->setIntent(RenderingIntent::AUTO); + space = cms; + } + + if (auto cms = std::dynamic_pointer_cast(space)) { + // Check named space has a cms profile that is actually the same Id + if (cms->getProfile()->getId() != icc_id) { + g_warning("Mismatched icc profiles in color data: '%s'", space_name); + // Not returning, will still return something + } + } + } + if (!space) { + throw ColorError("No color space available."); + } + + XML::NodeConstSiblingIterator color_iter{color_xml->firstChild()}; + std::vector values; + for (auto &comp : space->getComponents()) { + values.emplace_back(color_xml->getAttributeDouble(comp.id)); + } + auto color = Color(space, values); + + if (color_xml->attribute("opacity")) { + color.setOpacity(color_xml->getAttributeDouble("opacity")); + } + if (auto name = color_xml->attribute("name")) { + color.setName(name); + } + return color; + } + } + throw ColorError("No color data found"); +} + +} // namespace Inkscape::Colors + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 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/xml-color.h b/src/colors/xml-color.h new file mode 100644 index 0000000000..a3dbb346ae --- /dev/null +++ b/src/colors/xml-color.h @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Martin Owens + * + * Copyright (C) 2023 author + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_COLORS_XML_COLOR +#define INKSCAPE_COLORS_XML_COLOR + +#include +#include +#include + +class SPDocument; + +namespace Inkscape::XML { +struct Document; +} + +namespace Inkscape::Colors { + +class Color; +struct NoColor +{ +}; + +// NoColor must go first for default constructor +using Paint = std::variant; + +XML::Document *paint_to_xml(Paint const &paint); +std::string paint_to_xml_string(Paint const &paint); + +Paint xml_string_to_paint(std::string const &xmls, SPDocument *doc); +Paint xml_to_paint(XML::Document const *xml, SPDocument *doc); + +} // namespace Inkscape::Colors + +#endif // INKSCAPE_COLORS_XML_COLOR + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 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/document.cpp b/src/document.cpp index 7ff39fa03c..f0a306f10b 100644 --- a/src/document.cpp +++ b/src/document.cpp @@ -55,6 +55,7 @@ #include "layer-manager.h" #include "page-manager.h" #include "profile-manager.h" +#include "colors/tracker.h" #include "rdf.h" #include "selection.h" @@ -164,6 +165,7 @@ SPDocument::SPDocument() : add_document_actions_effect(this); _page_manager = std::make_unique(this); + _color_tracker = std::make_unique(this); } SPDocument::~SPDocument() { diff --git a/src/document.h b/src/document.h index b1301132c6..913b9fdddf 100644 --- a/src/document.h +++ b/src/document.h @@ -84,6 +84,9 @@ namespace Inkscape { class EventLog; class PageManager; class ProfileManager; + namespace Colors { + class Tracker; + } class Selection; class UndoStackObserver; namespace XML { @@ -168,12 +171,15 @@ public: Inkscape::PageManager& getPageManager() { return *_page_manager; } const Inkscape::PageManager& getPageManager() const { return *_page_manager; } + Inkscape::Colors::Tracker &getColorTracker() { return *_color_tracker; } + const Inkscape::Colors::Tracker &getColorTracker() const { return *_color_tracker; } private: void _importDefsNode(SPDocument *source, Inkscape::XML::Node *defs, Inkscape::XML::Node *target_defs); SPObject *_activexmltree; std::unique_ptr _page_manager; + std::unique_ptr _color_tracker; std::queue pending_resource_changes; diff --git a/src/object/color-profile.cpp b/src/object/color-profile.cpp index 8e5dda9fc5..d8a2d17b01 100644 --- a/src/object/color-profile.cpp +++ b/src/object/color-profile.cpp @@ -11,481 +11,193 @@ #include "color-profile.h" #include - -#include // for g_free, guchar, g_utf8_case... - -#ifdef _WIN32 -#undef NOGDI -#include -#include -#endif - -#include +#include #include "attributes.h" -#include "color.h" +#include "color/color-profile-cms-fns.h" // XXX REMOVE! +#include "colors/cms/profile.h" #include "document.h" #include "inkscape.h" #include "preferences.h" +#include "sp-defs.h" #include "uri.h" - -#include "color/color-profile-cms-fns.h" - #include "xml/document.h" #include "xml/href-attribute-helper.h" -using Inkscape::ColorProfile; -using Inkscape::ColorProfileImpl; - namespace Inkscape { -class ColorProfileImpl { -public: - static cmsHPROFILE _sRGBProf; - static cmsHPROFILE _NullProf; - - ColorProfileImpl(); - - static cmsUInt32Number _getInputFormat( cmsColorSpaceSignature space ); - - static cmsHPROFILE getNULLProfile(); - static cmsHPROFILE getSRGBProfile(); - - void _clearProfile(); - - cmsHPROFILE _profHandle; - cmsProfileClassSignature _profileClass; - cmsColorSpaceSignature _profileSpace; - cmsHTRANSFORM _transf; - cmsHTRANSFORM _revTransf; - cmsHTRANSFORM _gamutTransf; -}; - -cmsColorSpaceSignature asICColorSpaceSig(ColorSpaceSig const & sig) +void ColorProfile::release() { - return ColorSpaceSigWrapper(sig); -} - -cmsProfileClassSignature asICColorProfileClassSig(ColorProfileClassSig const & sig) -{ - return ColorProfileClassSigWrapper(sig); -} - -} // namespace Inkscape - -ColorProfileImpl::ColorProfileImpl() - : - _profHandle(nullptr), - _profileClass(cmsSigInputClass), - _profileSpace(cmsSigRgbData), - _transf(nullptr), - _revTransf(nullptr), - _gamutTransf(nullptr) -{ -} - - -cmsHPROFILE ColorProfileImpl::_sRGBProf = nullptr; - -cmsHPROFILE ColorProfileImpl::getSRGBProfile() { - if ( !_sRGBProf ) { - _sRGBProf = cmsCreate_sRGBProfile(); - } - return ColorProfileImpl::_sRGBProf; -} - -cmsHPROFILE ColorProfileImpl::_NullProf = nullptr; - -cmsHPROFILE ColorProfileImpl::getNULLProfile() { - if ( !_NullProf ) { - _NullProf = cmsCreateNULLProfile(); - } - return _NullProf; -} - -ColorProfile::ColorProfile() : SPObject() { - this->impl = new ColorProfileImpl(); - - this->href = nullptr; - this->local = nullptr; - this->name = nullptr; - this->intentStr = nullptr; - this->rendering_intent = Inkscape::RENDERING_INTENT_UNKNOWN; -} - -ColorProfile::~ColorProfile() = default; - -bool ColorProfile::operator<(ColorProfile const &other) const { - gchar *a_name_casefold = g_utf8_casefold(this->name, -1 ); - gchar *b_name_casefold = g_utf8_casefold(other.name, -1 ); - int result = g_strcmp0(a_name_casefold, b_name_casefold); - g_free(a_name_casefold); - g_free(b_name_casefold); - return result < 0; -} - -/** - * Callback: free object - */ -void ColorProfile::release() { // Unregister ourselves - if ( this->document ) { + if (this->document) { this->document->removeResource("iccprofile", this); } - if ( this->href ) { - g_free( this->href ); - this->href = nullptr; - } - - if ( this->local ) { - g_free( this->local ); - this->local = nullptr; - } - - if ( this->name ) { - g_free( this->name ); - this->name = nullptr; - } - - if ( this->intentStr ) { - g_free( this->intentStr ); - this->intentStr = nullptr; - } - - this->impl->_clearProfile(); - - delete this->impl; - this->impl = nullptr; - SPObject::release(); } -void ColorProfileImpl::_clearProfile() -{ - _profileSpace = cmsSigRgbData; - - if ( _transf ) { - cmsDeleteTransform( _transf ); - _transf = nullptr; - } - if ( _revTransf ) { - cmsDeleteTransform( _revTransf ); - _revTransf = nullptr; - } - if ( _gamutTransf ) { - cmsDeleteTransform( _gamutTransf ); - _gamutTransf = nullptr; - } - if ( _profHandle ) { - cmsCloseProfile( _profHandle ); - _profHandle = nullptr; - } -} - /** * Callback: set attributes from associated repr. */ -void ColorProfile::build(SPDocument *document, Inkscape::XML::Node *repr) { - g_assert(this->href == nullptr); - g_assert(this->local == nullptr); - g_assert(this->name == nullptr); - g_assert(this->intentStr == nullptr); - +void ColorProfile::build(SPDocument *document, Inkscape::XML::Node *repr) +{ SPObject::build(document, repr); this->readAttr(SPAttr::XLINK_HREF); - this->readAttr(SPAttr::ID); this->readAttr(SPAttr::LOCAL); this->readAttr(SPAttr::NAME); this->readAttr(SPAttr::RENDERING_INTENT); // Register - if ( document ) { - document->addResource( "iccprofile", this ); + if (document) { + document->addResource("iccprofile", this); } } - /** * Callback: set attribute. */ -void ColorProfile::set(SPAttr key, gchar const *value) { +void ColorProfile::set(SPAttr key, gchar const *value) +{ switch (key) { case SPAttr::XLINK_HREF: - if ( this->href ) { - g_free( this->href ); - this->href = nullptr; - } - if ( value ) { - this->href = g_strdup( value ); - if ( *this->href ) { - - // TODO open filename and URIs properly - //FILE* fp = fopen_utf8name( filename, "r" ); - //LCMSAPI cmsHPROFILE LCMSEXPORT cmsOpenProfileFromMem(LPVOID MemPtr, cmsUInt32Number dwSize); - - // Try to open relative - SPDocument *doc = this->document; - if (!doc) { - doc = SP_ACTIVE_DOCUMENT; - g_warning("this has no document. using active"); - } - //# 1. Get complete filename of document - gchar const *docbase = doc->getDocumentFilename(); - - Inkscape::URI docUri(""); - if (docbase) { // The file has already been saved - docUri = Inkscape::URI::from_native_filename(docbase); - } - - this->impl->_clearProfile(); - - try { - auto hrefUri = Inkscape::URI(this->href, docUri); - auto contents = hrefUri.getContents(); - this->impl->_profHandle = cmsOpenProfileFromMem(contents.data(), contents.size()); - } catch (...) { - g_warning("Failed to open CMS profile URI '%.100s'", this->href); - } - - if ( this->impl->_profHandle ) { - this->impl->_profileSpace = cmsGetColorSpace( this->impl->_profHandle ); - this->impl->_profileClass = cmsGetDeviceClass( this->impl->_profHandle ); - } - } + // Href is the filename or the data of the icc profile itself and is used before local + if (value) { + auto fn = document->getDocumentFilename(); + _uri = std::make_unique(value, fn ? ("file://" + std::string(fn)).c_str() : nullptr); + } else { + _uri.reset(); } - this->requestModified(SP_OBJECT_MODIFIED_FLAG); break; case SPAttr::LOCAL: - if ( this->local ) { - g_free( this->local ); - this->local = nullptr; - } - this->local = g_strdup( value ); - this->requestModified(SP_OBJECT_MODIFIED_FLAG); + // Local is the ID of the profile as a hex string. Provided by Colors::CMS::Profile::getId() + // it's only used if the href isn't set or isn't found on this system in the specified place + _local = value ? value : ""; break; case SPAttr::NAME: - if ( this->name ) { - g_free( this->name ); - this->name = nullptr; - } - this->name = g_strdup( value ); - this->requestModified(SP_OBJECT_MODIFIED_FLAG); + // Name is used by the icc-color format to match this profile to a color. It over-rides the + // name given in the icc profile if it's provided. + _name = value ? value : ""; break; case SPAttr::RENDERING_INTENT: - if ( this->intentStr ) { - g_free( this->intentStr ); - this->intentStr = nullptr; - } - this->intentStr = g_strdup( value ); - - if ( value ) { - if ( strcmp( value, "auto" ) == 0 ) { - this->rendering_intent = RENDERING_INTENT_AUTO; - } else if ( strcmp( value, "perceptual" ) == 0 ) { - this->rendering_intent = RENDERING_INTENT_PERCEPTUAL; - } else if ( strcmp( value, "relative-colorimetric" ) == 0 ) { - this->rendering_intent = RENDERING_INTENT_RELATIVE_COLORIMETRIC; - } else if ( strcmp( value, "saturation" ) == 0 ) { - this->rendering_intent = RENDERING_INTENT_SATURATION; - } else if ( strcmp( value, "absolute-colorimetric" ) == 0 ) { - this->rendering_intent = RENDERING_INTENT_ABSOLUTE_COLORIMETRIC; - } else { - this->rendering_intent = RENDERING_INTENT_UNKNOWN; + // There is a standard set of rendering intents, the default fallback intent is decided in the + // color CMS system and not here. + _intent = Colors::RenderingIntent::UNKNOWN; + if (value) { + for (auto &pair : Colors::intentIds) { + if (pair.second == value) { + _intent = pair.first; + break; + } } - } else { - this->rendering_intent = RENDERING_INTENT_UNKNOWN; } - - this->requestModified(SP_OBJECT_MODIFIED_FLAG); break; default: - SPObject::set(key, value); - break; + return SPObject::set(key, value); } + this->requestModified(SP_OBJECT_MODIFIED_FLAG); } /** * Callback: write attributes to associated repr. */ -Inkscape::XML::Node* ColorProfile::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { +Inkscape::XML::Node *ColorProfile::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) +{ if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { repr = xml_doc->createElement("svg:color-profile"); } - if ( (flags & SP_OBJECT_WRITE_ALL) || this->href ) { - Inkscape::setHrefAttribute(*repr, this->href ); - } - - if ( (flags & SP_OBJECT_WRITE_ALL) || this->local ) { - repr->setAttribute( "local", this->local ); - } - - if ( (flags & SP_OBJECT_WRITE_ALL) || this->name ) { - repr->setAttribute( "name", this->name ); + if ((flags & SP_OBJECT_WRITE_ALL) || _uri) { + auto fn = document->getDocumentFilename(); + Inkscape::setHrefAttribute(*repr, fn ? _uri->str(("file://" + std::string(fn)).c_str()) : nullptr); } - if ( (flags & SP_OBJECT_WRITE_ALL) || this->intentStr ) { - repr->setAttribute( "rendering-intent", this->intentStr ); - } + repr->setAttributeOrRemoveIfEmpty("local", _local); + repr->setAttributeOrRemoveIfEmpty("name", _name); + repr->setAttributeOrRemoveIfEmpty("rendering-intent", Colors::intentIds[_intent]); SPObject::write(xml_doc, repr, flags); - return repr; } - -struct MapMap { - cmsColorSpaceSignature space; - cmsUInt32Number inForm; -}; - -cmsUInt32Number ColorProfileImpl::_getInputFormat( cmsColorSpaceSignature space ) +/** + * Return the profile data, if any. Returns empty string if none + * is available. + */ +std::string ColorProfile::getProfileData() const { - MapMap possible[] = { - {cmsSigXYZData, TYPE_XYZ_16}, - {cmsSigLabData, TYPE_Lab_16}, - //cmsSigLuvData - {cmsSigYCbCrData, TYPE_YCbCr_16}, - {cmsSigYxyData, TYPE_Yxy_16}, - {cmsSigRgbData, TYPE_RGB_16}, - {cmsSigGrayData, TYPE_GRAY_16}, - {cmsSigHsvData, TYPE_HSV_16}, - {cmsSigHlsData, TYPE_HLS_16}, - {cmsSigCmykData, TYPE_CMYK_16}, - {cmsSigCmyData, TYPE_CMY_16}, - }; - - int index = 0; - for ( guint i = 0; i < G_N_ELEMENTS(possible); i++ ) { - if ( possible[i].space == space ) { - index = i; - break; + // Note: The returned data could be Megabytes in length, but we're + // copying the data. We should find a way to pass the const string back + if (_uri) { + try { + return _uri->getContents(); + } catch (const Gio::Error &e) { + g_warning("Couldn't get color profile: %s", e.what()); } } - - return possible[index].inForm; -} - -static int getLcmsIntent( guint svgIntent ) -{ - int intent = INTENT_PERCEPTUAL; - switch ( svgIntent ) { - case Inkscape::RENDERING_INTENT_RELATIVE_COLORIMETRIC: - intent = INTENT_RELATIVE_COLORIMETRIC; - break; - case Inkscape::RENDERING_INTENT_SATURATION: - intent = INTENT_SATURATION; - break; - case Inkscape::RENDERING_INTENT_ABSOLUTE_COLORIMETRIC: - intent = INTENT_ABSOLUTE_COLORIMETRIC; - break; - case Inkscape::RENDERING_INTENT_PERCEPTUAL: - case Inkscape::RENDERING_INTENT_UNKNOWN: - case Inkscape::RENDERING_INTENT_AUTO: - default: - intent = INTENT_PERCEPTUAL; - } - return intent; -} - -Inkscape::ColorSpaceSig ColorProfile::getColorSpace() const { - return ColorSpaceSigWrapper(impl->_profileSpace); -} - -Inkscape::ColorProfileClassSig ColorProfile::getProfileClass() const { - return ColorProfileClassSigWrapper(impl->_profileClass); + return ""; } -cmsHTRANSFORM ColorProfile::getTransfToSRGB8() +/** + * Set the rendering intent for this color profile. + */ +void ColorProfile::setRenderingIntent(Colors::RenderingIntent intent) { - if ( !impl->_transf && impl->_profHandle ) { - int intent = getLcmsIntent(rendering_intent); - impl->_transf = cmsCreateTransform( impl->_profHandle, ColorProfileImpl::_getInputFormat(impl->_profileSpace), ColorProfileImpl::getSRGBProfile(), TYPE_RGBA_8, intent, 0 ); - } - return impl->_transf; + setAttribute("rendering-intent", Colors::intentIds[intent]); } -cmsHTRANSFORM ColorProfile::getTransfFromSRGB8() +/** + * Create a profile for the given profile in the given document. + * + * @args doc - The SPDocument to add this profile into, creating a new color profile element in it's defs. + * @args profile - The color profile object to use as the data source + * @args name - The name to use, this over-rides the name in the profile + * @args storage - This sets the prefered data source. + * - HREF_DATA - The profile is embeded as a base64 encoded stream. + * - HREF_FILE - The href is a relative or absolute link to the icc profile file. + * the profile MUST be a file. If the document has a file and the path is close + * to the icc profile, it will be relative. + * - LOCAL_ID - The profile's unique id will be stored, no href will be added. + */ +ColorProfile *ColorProfile::createFromProfile(SPDocument *doc, Colors::CMS::Profile const &profile, + std::string const &name, ColorProfileStorage storage) { - if ( !impl->_revTransf && impl->_profHandle ) { - int intent = getLcmsIntent(rendering_intent); - impl->_revTransf = cmsCreateTransform( ColorProfileImpl::getSRGBProfile(), TYPE_RGBA_8, impl->_profHandle, ColorProfileImpl::_getInputFormat(impl->_profileSpace), intent, 0 ); + if (name.empty()) { + g_error("Refusing to create a color profile with an empty name!"); + return nullptr; } - return impl->_revTransf; -} - -cmsHTRANSFORM ColorProfile::getTransfGamutCheck() -{ - if ( !impl->_gamutTransf ) { - impl->_gamutTransf = cmsCreateProofingTransform(ColorProfileImpl::getSRGBProfile(), - TYPE_BGRA_8, - ColorProfileImpl::getNULLProfile(), - TYPE_GRAY_8, - impl->_profHandle, - INTENT_RELATIVE_COLORIMETRIC, - INTENT_RELATIVE_COLORIMETRIC, - (cmsFLAGS_GAMUTCHECK | cmsFLAGS_SOFTPROOFING)); + if (storage == ColorProfileStorage::HREF_FILE && profile.getPath().empty()) { + storage = ColorProfileStorage::HREF_DATA; // fallback to data } - return impl->_gamutTransf; -} - -// Check if a particular color is out of gamut. -bool ColorProfile::GamutCheck(SPColor color) -{ - guint32 val = color.toRGBA32(0); - cmsUInt16Number oldAlarmCodes[cmsMAXCHANNELS] = {0}; - cmsGetAlarmCodes(oldAlarmCodes); - cmsUInt16Number newAlarmCodes[cmsMAXCHANNELS] = {0}; - newAlarmCodes[0] = ~0; - cmsSetAlarmCodes(newAlarmCodes); + // Create new object and attach it to the document + auto repr = doc->getReprDoc()->createElement("svg:color-profile"); - cmsUInt8Number outofgamut = 0; - guchar check_color[4] = { - static_cast(SP_RGBA32_R_U(val)), - static_cast(SP_RGBA32_G_U(val)), - static_cast(SP_RGBA32_B_U(val)), - 255}; + // It's expected that the color manager will hace checked for collisions before this call. + repr->setAttributeOrRemoveIfEmpty("name", name); - cmsHTRANSFORM gamutCheck = ColorProfile::getTransfGamutCheck(); - if (gamutCheck) { - cmsDoTransform(gamutCheck, &check_color, &outofgamut, 1); + switch (storage) { + case ColorProfileStorage::LOCAL_ID: + repr->setAttributeOrRemoveIfEmpty("local", profile.getId()); + break; + case ColorProfileStorage::HREF_DATA: + Inkscape::setHrefAttribute(*repr, "data:application/vnd.iccprofile;base64," + profile.dumpBase64()); + break; + case ColorProfileStorage::HREF_FILE: { + auto uri = Inkscape::URI::from_native_filename(profile.getPath().c_str()); + auto fn = doc->getDocumentFilename(); + Inkscape::setHrefAttribute(*repr, fn ? (uri.str((std::string("file://") + fn).c_str())) : nullptr); + } break; } - - cmsSetAlarmCodes(oldAlarmCodes); - - return (outofgamut != 0); -} - -gint ColorProfile::getChannelCount() const -{ - return cmsChannelsOf(asICColorSpaceSig(getColorSpace())); -} - -bool ColorProfile::isPrintColorSpace() -{ - ColorSpaceSigWrapper colorspace = getColorSpace(); - return (colorspace == cmsSigCmykData) || (colorspace == cmsSigCmyData); -} - -cmsHPROFILE ColorProfile::getHandle() -{ - return impl->_profHandle; -} - -void errorHandlerCB(cmsContext /*contextID*/, cmsUInt32Number errorCode, char const *errorText) -{ - g_message("lcms: Error %d", errorCode); - g_message(" %p", errorText); - //g_message("lcms: Error %d; %s", errorCode, errorText); + // Complete the creation by appending to the defs. This must be done last. + return cast(doc->getDefs()->appendChildRepr(repr)); } +} // namespace Inkscape /* Local Variables: diff --git a/src/object/color-profile.h b/src/object/color-profile.h index 0e93e04ae3..12c4273419 100644 --- a/src/object/color-profile.h +++ b/src/object/color-profile.h @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /** @file - * TODO: insert short description here + * SPObject of the color-profile object found a direct child of defs. *//* * Authors: see git history * @@ -10,72 +10,85 @@ #ifndef SEEN_COLOR_PROFILE_H #define SEEN_COLOR_PROFILE_H -#ifdef HAVE_CONFIG_H -# include "config.h" // only include where actually required! -#endif - -#include -#include - -#include +#include // XXX Remove during refactoring! +#include "color.h" // XXX Remove during refactoring! +#include "color/cms-color-types.h" // XXX Remove as well +#include "colors/cms/system.h" +#include "colors/spaces/enum.h" // RenderingIntent #include "sp-object.h" -#include "color/cms-color-types.h" - -struct SPColor; namespace Inkscape { -enum { - RENDERING_INTENT_UNKNOWN = 0, - RENDERING_INTENT_AUTO = 1, - RENDERING_INTENT_PERCEPTUAL = 2, - RENDERING_INTENT_RELATIVE_COLORIMETRIC = 3, - RENDERING_INTENT_SATURATION = 4, - RENDERING_INTENT_ABSOLUTE_COLORIMETRIC = 5 -}; - -class ColorProfileImpl; +class URI; +enum class ColorProfileStorage +{ + HREF_DATA, + HREF_FILE, + LOCAL_ID, +}; -/** - * Color Profile. - */ -class ColorProfile final : public SPObject { +class ColorProfile final : public SPObject +{ public: - ColorProfile(); - ~ColorProfile() override; + ColorProfile() = default; + ~ColorProfile() override = default; int tag() const override { return tag_of; } - bool operator<(ColorProfile const &other) const; - - ColorSpaceSig getColorSpace() const; - ColorProfileClassSig getProfileClass() const; - cmsHTRANSFORM getTransfToSRGB8(); - cmsHTRANSFORM getTransfFromSRGB8(); - cmsHTRANSFORM getTransfGamutCheck(); - bool GamutCheck(SPColor color); - int getChannelCount() const; - bool isPrintColorSpace(); - cmsHPROFILE getHandle(); + static ColorProfile *createFromProfile(SPDocument *doc, Colors::CMS::Profile const &profile, + std::string const &name, ColorProfileStorage storage); + std::string getName() const { return _name; } + std::string getLocalProfileId() const { return _local; } + std::string getProfileData() const; + Colors::RenderingIntent getRenderingIntent() const { return _intent; } - // TODO: Make private - char* href; - char* local; - char* name; - char* intentStr; - unsigned int rendering_intent; // FIXME: type the enum and hold that instead + // This is the only variable we expect inkscape to modify. Changing the icc + // profile data or ID should instead involve creating a new ColorProfile element. + void setRenderingIntent(Colors::RenderingIntent intent); protected: - ColorProfileImpl *impl; - - void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void build(SPDocument *doc, Inkscape::XML::Node *repr) override; void release() override; - void set(SPAttr key, char const* value) override; + void set(SPAttr key, char const *value) override; + + Inkscape::XML::Node *write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, unsigned int flags) override; + +private: + std::string _name; + std::string _local; + Colors::RenderingIntent _intent; + + std::unique_ptr _uri; + +public: + // XXX Compatability code which will be removed in the main refactoring step, never populated + char *name = nullptr; + char *href = nullptr; + unsigned int rendering_intent; + + ColorSpaceSig getColorSpace() const { return {0}; }; + ColorProfileClassSig getProfileClass() const { return {0}; } + cmsHTRANSFORM getTransfToSRGB8() { return nullptr; } + cmsHTRANSFORM getTransfFromSRGB8() { return nullptr; } + cmsHTRANSFORM getTransfGamutCheck() { return nullptr; }; + bool GamutCheck(SPColor color) { return false; } + int getChannelCount() const { return 4; } + bool isPrintColorSpace() { return false; } + cmsHPROFILE getHandle() { return nullptr; } +}; - Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; +// XXX Delete!!! +enum +{ + RENDERING_INTENT_UNKNOWN = 0, + RENDERING_INTENT_AUTO = 1, + RENDERING_INTENT_PERCEPTUAL = 2, + RENDERING_INTENT_RELATIVE_COLORIMETRIC = 3, + RENDERING_INTENT_SATURATION = 4, + RENDERING_INTENT_ABSOLUTE_COLORIMETRIC = 5 }; } // namespace Inkscape diff --git a/src/ui/widget/color-icc-selector.cpp b/src/ui/widget/color-icc-selector.cpp index edf1000cd0..adc45279ab 100644 --- a/src/ui/widget/color-icc-selector.cpp +++ b/src/ui/widget/color-icc-selector.cpp @@ -33,7 +33,6 @@ #define noDEBUG_LCMS #include "object/color-profile.h" -#include "color/color-profile-cms-fns.h" #ifdef DEBUG_LCMS #include "preferences.h" @@ -195,7 +194,7 @@ std::vector colorspace::getColorSpaceInfo(uint32_t space) std::vector colorspace::getColorSpaceInfo(Inkscape::ColorProfile *prof) { - return getColorSpaceInfo(asICColorSpaceSig(prof->getColorSpace())); + return getColorSpaceInfo((uint32_t)0); // asICColorSpaceSig(prof->getColorSpace())); } namespace Inkscape::UI::Widget { @@ -502,10 +501,10 @@ void ColorICCSelectorImpl::_switchToProfile(gchar const *name) #ifdef DEBUG_LCMS g_message("got on out [%04x] [%04x] [%04x] [%04x]", post[0], post[1], post[2], post[3]); #endif // DEBUG_LCMS - guint count = cmsChannelsOf(asICColorSpaceSig(newProf->getColorSpace())); + guint count = 0; // cmsChannelsOf(asICColorSpaceSig(newProf->getColorSpace())); std::vector things = - colorspace::getColorSpaceInfo(asICColorSpaceSig(newProf->getColorSpace())); + colorspace::getColorSpaceInfo((uint32_t)0); // asICColorSpaceSig(newProf->getColorSpace())); std::vector colors; for (guint i = 0; i < count; i++) { @@ -711,12 +710,13 @@ void ColorICCSelectorImpl::_setProfile(const std::string &profile) if (!profile.empty()) { _prof = SP_ACTIVE_DOCUMENT->getProfileManager().find(profile.c_str()); - if (_prof && (asICColorProfileClassSig(_prof->getProfileClass()) != cmsSigNamedColorClass)) { + // if (_prof && (asICColorProfileClassSig(_prof->getProfileClass()) != cmsSigNamedColorClass)) { + if (false) { _profChannelCount = _prof->getChannelCount(); if (profChanged) { std::vector things = - colorspace::getColorSpaceInfo(asICColorSpaceSig(_prof->getColorSpace())); + colorspace::getColorSpaceInfo((uint32_t)0); // asICColorSpaceSig(_prof->getColorSpace())); for (size_t i = 0; (i < things.size()) && (i < _profChannelCount); ++i) { _compUI[i]._component = things[i]; } @@ -741,8 +741,7 @@ void ColorICCSelectorImpl::_setProfile(const std::string &profile) gtk_widget_set_visible(_compUI[i]._btn, false); } } - } - else { + } else { // Give up for now on named colors _prof = nullptr; } diff --git a/testfiles/CMakeLists.txt b/testfiles/CMakeLists.txt index d83e6fd77c..97293d0487 100644 --- a/testfiles/CMakeLists.txt +++ b/testfiles/CMakeLists.txt @@ -60,13 +60,38 @@ set(TEST_SOURCES async_channel-test async_funclog-test async_progress-test + colors/cms-test + colors/color-test + colors/document-cms-test + colors/dragndrop-test + colors/manager-test + colors/parser-test + colors/printer-test + colors/color-set-test + colors/spaces-cms-test + colors/spaces-cmyk-test + colors/spaces-gray-test + colors/spaces-hsl-test + colors/spaces-hsluv-test + colors/spaces-hsv-test + colors/spaces-lab-test + colors/spaces-lch-test + colors/spaces-linear-rgb-test + colors/spaces-luv-test + colors/spaces-named-test + colors/spaces-okhsl-test + colors/spaces-oklab-test + colors/spaces-oklch-test + colors/spaces-rgb-test + colors/spaces-xyz-test + colors/utils-test + colors/xml-color-test uri-test util-test drag-and-drop-svgz drawing-pattern-test extract-uri-test attributes-test - color-profile-test dir-util-test oklab-color-test sp-item-test @@ -112,6 +137,7 @@ target_link_libraries(cpp_test_static_library PUBLIC ${GTEST_LIBRARIES} inkscape add_custom_target(tests) foreach(test_source ${TEST_SOURCES}) string(REPLACE "-test" "" testname "test_${test_source}") + string(REPLACE "/" "_" testname "${testname}") add_executable(${testname} src/${test_source}.cpp) target_include_directories(${testname} SYSTEM PRIVATE ${GTEST_INCLUDE_DIRS}) target_link_libraries(${testname} cpp_test_static_library 2Geom::2geom) diff --git a/testfiles/data/colors/SwappedRedAndGreen.icc b/testfiles/data/colors/SwappedRedAndGreen.icc new file mode 100644 index 0000000000000000000000000000000000000000..1b8c69a11bc8d3771df8005e98c181350fc0d3e6 GIT binary patch literal 15720 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 Aq5uE@ literal 0 HcmV?d00001 diff --git a/testfiles/data/colors/cms-in-defs.svg b/testfiles/data/colors/cms-in-defs.svg new file mode 100644 index 0000000000..e70c7f3409 --- /dev/null +++ b/testfiles/data/colors/cms-in-defs.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/testfiles/data/colors/cms-in-objs.svg b/testfiles/data/colors/cms-in-objs.svg new file mode 100644 index 0000000000..dcbdfb89ef --- /dev/null +++ b/testfiles/data/colors/cms-in-objs.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/testfiles/data/colors/default_cmyk.icc b/testfiles/data/colors/default_cmyk.icc new file mode 100644 index 0000000000000000000000000000000000000000..fce61bf2bcbc9a821364d7fff85a2a4680af3078 GIT binary patch literal 187484 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

%EmEQkTngjnQ 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 literal 0 HcmV?d00001 diff --git a/testfiles/data/colors/display.icc b/testfiles/data/colors/display.icc new file mode 100644 index 0000000000000000000000000000000000000000..12cb9c8b167b92335474155b4ac07c44b0cdabf6 GIT binary patch literal 864 zcmZQzU`|LZD9B+FU|`72D=7+ccT$Lmj8b5~#K6uV$-u*)&%l(JTwLH75a7eWz`&rO zpr8PvkuW0z!?$Y;j3ANAx>)2v68JDrN@{U30|TQ70|P@sazRlE0|R3T0|P@wN^V{X z0|Vn72s=@7OKh@Avs+kn{RB?To649p-uia^{0b~2l@US@JKm`OdNB((zM zKL+Rgg36-I^o$Y(jbu#)BLf2?1*f9Yg9 zVuhg8;?$zD)D%4hn~ai@0xNy}^73-MB#_1&xQ68XTssCNfAFQGmSrZV`lhA3fCHO) zo?&1>wnqSJPku^j4h?K#V1UFu*!hf*G{?XIrorhClMPDCVATu_OBfhja~T-e!08(z x7SzkYurio|LHZ9uo$^u!20;e~hK+*BrA1{BbqtIk;|cK*W_-H~b`!&8D*(4zafbi^ literal 0 HcmV?d00001 diff --git a/testfiles/src/color-profile-test.cpp b/testfiles/src/color-profile-test.cpp deleted file mode 100644 index 4089920416..0000000000 --- a/testfiles/src/color-profile-test.cpp +++ /dev/null @@ -1,127 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * Unit tests for color profile. - * - * Author: - * Jon A. Cruz - * - * Copyright (C) 2015 Authors - * - * Released under GNU GPL v2+, read the file 'COPYING' for more information. - */ - -#include "gtest/gtest.h" - -#include "attributes.h" -#include "color/cms-system.h" -#include "object/color-profile.h" -#include "doc-per-case-test.h" - -namespace { - -/** - * Test fixture to inherit a shared doc and create a color profile instance per test. - */ -class ProfTest : public DocPerCaseTest -{ -public: - ProfTest() : - DocPerCaseTest(), - _prof(0) - { - } - -protected: - void SetUp() override - { - DocPerCaseTest::SetUp(); - _prof = new Inkscape::ColorProfile(); - ASSERT_TRUE( _prof != NULL ); - _prof->document = _doc.get(); - } - - void TearDown() override - { - if (_prof) { - delete _prof; - _prof = NULL; - } - DocPerCaseTest::TearDown(); - } - - Inkscape::ColorProfile *_prof; -}; - -typedef ProfTest ColorProfileTest; - -TEST_F(ColorProfileTest, SetRenderingIntent) -{ - struct { - gchar const *attr; - guint intVal; - } - const cases[] = { - {"auto", (guint)Inkscape::RENDERING_INTENT_AUTO}, - {"perceptual", (guint)Inkscape::RENDERING_INTENT_PERCEPTUAL}, - {"relative-colorimetric", (guint)Inkscape::RENDERING_INTENT_RELATIVE_COLORIMETRIC}, - {"saturation", (guint)Inkscape::RENDERING_INTENT_SATURATION}, - {"absolute-colorimetric", (guint)Inkscape::RENDERING_INTENT_ABSOLUTE_COLORIMETRIC}, - {"something-else", (guint)Inkscape::RENDERING_INTENT_UNKNOWN}, - {"auto2", (guint)Inkscape::RENDERING_INTENT_UNKNOWN}, - }; - - for (auto i : cases) { - _prof->setKeyValue( SPAttr::RENDERING_INTENT, i.attr); - ASSERT_EQ( (guint)i.intVal, _prof->rendering_intent ) << i.attr; - } -} - -TEST_F(ColorProfileTest, SetLocal) -{ - gchar const* cases[] = { - "local", - "something", - }; - - for (auto & i : cases) { - _prof->setKeyValue( SPAttr::LOCAL, i); - ASSERT_TRUE( _prof->local != NULL ); - if ( _prof->local ) { - ASSERT_EQ( std::string(i), _prof->local ); - } - } - _prof->setKeyValue( SPAttr::LOCAL, NULL); - ASSERT_EQ( (gchar*)0, _prof->local ); -} - -TEST_F(ColorProfileTest, SetName) -{ - gchar const* cases[] = { - "name", - "something", - }; - - for (auto & i : cases) { - _prof->setKeyValue( SPAttr::NAME, i); - ASSERT_TRUE( _prof->name != NULL ); - if ( _prof->name ) { - ASSERT_EQ( std::string(i), _prof->name ); - } - } - _prof->setKeyValue( SPAttr::NAME, NULL ); - ASSERT_EQ( (gchar*)0, _prof->name ); -} - - -} // 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-test.cpp b/testfiles/src/colors/cms-test.cpp new file mode 100644 index 0000000000..d1bfdce59f --- /dev/null +++ b/testfiles/src/colors/cms-test.cpp @@ -0,0 +1,349 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Unit tests for color objects. + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include + +#include "colors/cms/profile.h" +#include "colors/cms/system.h" +#include "colors/cms/transform.h" +#include "colors/color.h" +#include "colors/manager.h" +#include "colors/utils.h" +#include "preferences.h" + +static std::string icc_dir = INKSCAPE_TESTS_DIR "/data/colors"; +static std::string grb_profile = INKSCAPE_TESTS_DIR "/data/colors/SwappedRedAndGreen.icc"; +static std::string cmyk_profile = INKSCAPE_TESTS_DIR "/data/colors/default_cmyk.icc"; +static std::string display_profile = INKSCAPE_TESTS_DIR "/data/colors/display.icc"; +static std::string not_a_profile = INKSCAPE_TESTS_DIR "/data/colors/color-cms.svg"; + +using namespace Inkscape::Colors; + +namespace { + +// ================= CMS::System ================= // + +class ColorCmsSystem : public ::testing::Test +{ +protected: + void SetUp() override + { + cms = &CMS::System::get(); + cms->clearDirectoryPaths(); + cms->addDirectoryPath(icc_dir, false); + cms->refreshProfiles(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString("/options/displayprofile/uri", display_profile); + prefs->setBool("/options/displayprofile/enabled", true); + } + void TearDown() override {} + Inkscape::Colors::CMS::System *cms = nullptr; +}; + +TEST_F(ColorCmsSystem, getDirectoryPaths) +{ + ASSERT_EQ(cms->getDirectoryPaths().size(), 1); + ASSERT_EQ(cms->getDirectoryPaths()[0].first, icc_dir); +} + +TEST_F(ColorCmsSystem, addDirectoryPath) +{ + cms->clearDirectoryPaths(); + cms->addDirectoryPath("nope", false); + cms->addDirectoryPath("yep", true); + ASSERT_EQ(cms->getDirectoryPaths().size(), 2); + ASSERT_EQ(cms->getDirectoryPaths()[0].first, "nope"); + ASSERT_EQ(cms->getDirectoryPaths()[1].first, "yep"); +} + +TEST_F(ColorCmsSystem, clearDirectoryPaths) +{ + cms->clearDirectoryPaths(); + ASSERT_GE(cms->getDirectoryPaths().size(), 2); +} + +TEST_F(ColorCmsSystem, getProfiles) +{ + auto profiles = cms->getProfiles(); + ASSERT_EQ(profiles.size(), 3); + + ASSERT_EQ(profiles[0]->getName(), "Artifex CMYK SWOP Profile"); + ASSERT_EQ(profiles[1]->getName(), "C.icc"); + ASSERT_EQ(profiles[2]->getName(), "Swapped Red and Green"); +} + +TEST_F(ColorCmsSystem, getProfileByName) +{ + auto profile = cms->getProfile("Swapped Red and Green"); + ASSERT_TRUE(profile); + ASSERT_EQ(profile->getPath(), grb_profile); +} + +TEST_F(ColorCmsSystem, getProfileByID) +{ + auto profile = cms->getProfile("f9eda5a42a222a28f0adb82a938eeb0e"); + ASSERT_TRUE(profile); + ASSERT_EQ(profile->getName(), "Swapped Red and Green"); +} + +TEST_F(ColorCmsSystem, getProfileByPath) +{ + auto profile = cms->getProfile(grb_profile); + ASSERT_TRUE(profile); + ASSERT_EQ(profile->getId(), "f9eda5a42a222a28f0adb82a938eeb0e"); +} + +TEST_F(ColorCmsSystem, getDisplayProfiles) +{ + auto profiles = cms->getDisplayProfiles(); + ASSERT_EQ(profiles.size(), 1); + ASSERT_EQ(profiles[0]->getName(), "C.icc"); +} + +TEST_F(ColorCmsSystem, getDisplayProfile) +{ + bool updated = false; + auto profile = cms->getDisplayProfile(updated); + ASSERT_TRUE(updated); + ASSERT_TRUE(profile); + ASSERT_EQ(profile->getName(), "C.icc"); +} + +TEST_F(ColorCmsSystem, getDisplayTransform) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + bool updated = false; + auto profile = cms->getDisplayProfile(updated); + ASSERT_TRUE(profile); + + ASSERT_TRUE(cms->getDisplayTransform()); + prefs->setBool("/options/displayprofile/enabled", false); + ASSERT_FALSE(cms->getDisplayTransform()); + prefs->setBool("/options/displayprofile/enabled", true); + ASSERT_TRUE(cms->getDisplayTransform()); + prefs->setString("/options/displayprofile/uri", ""); + ASSERT_FALSE(cms->getDisplayTransform()); +} + +TEST_F(ColorCmsSystem, getOutputProfiles) +{ + auto profiles = cms->getOutputProfiles(); + ASSERT_EQ(profiles.size(), 1); + ASSERT_EQ(profiles[0]->getName(), "Artifex CMYK SWOP Profile"); +} + +TEST_F(ColorCmsSystem, refreshProfiles) +{ + cms->clearDirectoryPaths(); + cms->refreshProfiles(); + ASSERT_GE(cms->getProfiles().size(), 3); +} + +// ================= CMS::Profile ================= // + +TEST(ColorCmsProfile, create) +{ + auto rgb_profile = cmsCreate_sRGBProfile(); + auto profile = CMS::Profile::create(rgb_profile, "", false); + ASSERT_TRUE(profile); + ASSERT_EQ(profile->getId(), ""); + ASSERT_EQ(profile->getName(), "sRGB built-in"); + ASSERT_EQ(profile->getPath(), ""); + ASSERT_FALSE(profile->inHome()); + ASSERT_EQ(profile->getHandle(), rgb_profile); +} + +TEST(ColorCmsProfile, create_from_uri) +{ + auto profile = CMS::Profile::create_from_uri(grb_profile); + + ASSERT_EQ(profile->getId(), "f9eda5a42a222a28f0adb82a938eeb0e"); + ASSERT_EQ(profile->getName(), "Swapped Red and Green"); + ASSERT_EQ(profile->getName(true), "Swapped-Red-and-Green"); + ASSERT_EQ(profile->getPath(), grb_profile); + ASSERT_EQ(profile->getColorSpace(), cmsSigRgbData); + ASSERT_EQ(profile->getProfileClass(), cmsSigDisplayClass); + + ASSERT_FALSE(profile->inHome()); + ASSERT_FALSE(profile->isForDisplay()); +} + +TEST(ColorCmsProfile, create_from_data) +{ + // Prepare some memory first + cmsUInt32Number len = 0; + auto rgb_profile = cmsCreate_sRGBProfile(); + ASSERT_TRUE(cmsSaveProfileToMem(rgb_profile, nullptr, &len)); + auto buf = std::vector(len); + cmsSaveProfileToMem(rgb_profile, &buf.front(), &len); + std::string data(buf.begin(), buf.end()); + + auto profile = CMS::Profile::create_from_data(data); + ASSERT_TRUE(profile); +} + +TEST(ColorCmsProfile, create_srgb) +{ + auto profile = CMS::Profile::create_srgb(); + ASSERT_TRUE(profile); +} + +TEST(ColorCmsProfile, equalTo) +{ + auto profile1 = CMS::Profile::create_from_uri(grb_profile); + auto profile2 = CMS::Profile::create_from_uri(grb_profile); + auto profile3 = CMS::Profile::create_from_uri(cmyk_profile); + ASSERT_EQ(*profile1, *profile2); + ASSERT_NE(*profile1, *profile3); +} + +TEST(ColorCmsProfile, isIccFile) +{ + ASSERT_TRUE(CMS::Profile::isIccFile(grb_profile)); + ASSERT_FALSE(CMS::Profile::isIccFile(not_a_profile)); + ASSERT_FALSE(CMS::Profile::isIccFile(icc_dir + "not_existing.icc")); +} + +TEST(ColorCmsProfile, cmsDumpBase64) +{ + auto profile = CMS::Profile::create_from_uri(grb_profile); + // First 100 bytes taken from the base64 of the icc profile file on the command line + ASSERT_EQ(profile->dumpBase64().substr(0, 100), + "AAA9aGxjbXMEMAAAbW50clJHQiBYWVogB+YAAgAWAA0AGQAuYWNzcEFQUEwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPbWAAEA"); +} + +// ================= CMS::Transform ================= // + +TEST(ColorCmsTransform, applyTransformColor) +{ + auto srgb = CMS::Profile::create_srgb(); + auto profile = CMS::Profile::create_from_uri(grb_profile); + auto tr = CMS::Transform::create_for_cms(srgb, profile, RenderingIntent::RELATIVE_COLORIMETRIC); + + std::vector output = {0.1, 0.2, 0.3, 1.0}; + tr->do_transform(output); + ASSERT_NEAR(output[0], 0.2, 0.01); + ASSERT_NEAR(output[1], 0.1, 0.01); + ASSERT_NEAR(output[2], 0.3, 0.01); + ASSERT_EQ(output[3], 1.0); +} + +TEST(ColorCmsTransform, gamutCheckColor) +{ + auto srgb = CMS::Profile::create_srgb(); + auto profile = CMS::Profile::create_from_uri(cmyk_profile); + ASSERT_TRUE(srgb); + ASSERT_TRUE(profile); + + auto tr1 = CMS::Transform::create_for_cms_checker(srgb, profile); + ASSERT_TRUE(tr1); + + // An RGB color which is within the cmyk color profile gamut + EXPECT_FALSE(tr1->check_gamut({0.83, 0.19, 0.49})); + + // An RGB color (magenta) which is outside the cmyk color profile + EXPECT_TRUE(tr1->check_gamut({1.0, 0.0, 1.0})); +} + +::testing::AssertionResult CairoPixelIs(Cairo::RefPtr &cs, unsigned expected_color) +{ + auto data = cs->get_data(); + // BGRA8 cairo surfaces, (aka ARGB32) + unsigned found_color = SP_RGBA32_U_COMPOSE(data[2], data[1], data[0], data[3]); + + if (found_color != expected_color) { + return ::testing::AssertionFailure() << "\n" + << Inkscape::Colors::rgba_to_hex(found_color) << "\n != \n" + << Inkscape::Colors::rgba_to_hex(expected_color); + } + return ::testing::AssertionSuccess(); +} + +TEST(ColorCmsTransform, applyTransformCairo) +{ + auto srgb = CMS::Profile::create_srgb(); + auto profile = CMS::Profile::create_from_uri(grb_profile); + auto tr = CMS::Transform::create_for_cairo(srgb, profile); + + auto cs = Cairo::ImageSurface::create(Cairo::Surface::Format::ARGB32, 1, 1); + auto cr = Cairo::Context::create(cs); + + cr->set_source_rgb(0.5, 0, 0); + cr->paint(); + ASSERT_TRUE(CairoPixelIs(cs, 0x800000ff)); + tr->do_transform(cs, cs); + ASSERT_TRUE(CairoPixelIs(cs, 0x008000ff)); +} + +TEST(ColorCmsTransform, applyWithProofing) +{ + auto srgb = CMS::Profile::create_srgb(); + auto profile = CMS::Profile::create_from_uri(cmyk_profile); + auto proofed = CMS::Transform::create_for_cairo(srgb, srgb, profile, RenderingIntent::AUTO, false); + + auto cs = Cairo::ImageSurface::create(Cairo::Surface::Format::ARGB32, 1, 1); + auto cr = Cairo::Context::create(cs); + + cr->set_source_rgb(1.0, 0, 1.0); + cr->paint(); + + ASSERT_TRUE(CairoPixelIs(cs, 0xff00ffff)); + proofed->do_transform(cs, cs); + // Value should be + ASSERT_TRUE(CairoPixelIs(cs, 0xba509dff)); + + cr->set_source_rgb(0.83, 0.19, 0.49); + cr->paint(); + + ASSERT_TRUE(CairoPixelIs(cs, 0xd4307dff)); + proofed->do_transform(cs, cs); + ASSERT_TRUE(CairoPixelIs(cs, 0xd42279ff)); +} + +TEST(ColorCmsTransform, applyWithGamutWarn) +{ + auto srgb = CMS::Profile::create_srgb(); + auto profile = CMS::Profile::create_from_uri(cmyk_profile); + auto warned = CMS::Transform::create_for_cairo(srgb, srgb, profile, RenderingIntent::AUTO, true); + warned->set_gamut_warn({0.0, 1.0, 0.0}); + + auto cs = Cairo::ImageSurface::create(Cairo::Surface::Format::ARGB32, 1, 1); + auto cr = Cairo::Context::create(cs); + + cr->set_source_rgb(1.0, 0, 1.0); + cr->paint(); + + ASSERT_TRUE(CairoPixelIs(cs, 0xff00ffff)); + warned->do_transform(cs, cs); + ASSERT_TRUE(CairoPixelIs(cs, 0x00ff00ff)); + + cr->set_source_rgb(0.83, 0.19, 0.49); + cr->paint(); + + ASSERT_TRUE(CairoPixelIs(cs, 0xd4307dff)); + warned->do_transform(cs, cs); + ASSERT_TRUE(CairoPixelIs(cs, 0xd42279ff)); +} + +} // 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/color-set-test.cpp b/testfiles/src/colors/color-set-test.cpp new file mode 100644 index 0000000000..985dab1c6c --- /dev/null +++ b/testfiles/src/colors/color-set-test.cpp @@ -0,0 +1,286 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Unit tests for testing selected colors + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include + +#include "../test-utils.h" + +#include "colors/color.h" +#include "colors/color-set.h" +#include "colors/manager.h" +#include "colors/spaces/base.h" +#include "colors/spaces/components.h" + +using namespace Inkscape::Colors; + +namespace { + +::testing::AssertionResult signalIsCalled(sigc::signal &signal, std::function then_run) +{ + unsigned was_called = 0; + sigc::connection ret = signal.connect([&was_called]() { was_called++; }); + then_run(); + ret.disconnect(); + if (was_called == 0) + return ::testing::AssertionFailure() << " Signal not called."; + if (was_called > 1) + return ::testing::AssertionFailure() << " Signal called too many times."; + return ::testing::AssertionSuccess(); +} + +TEST(ColorSetTest, setColors) +{ + auto colors = ColorSet(); + ASSERT_TRUE(colors.isEmpty()); + ASSERT_FALSE(colors.getAlphaConstraint()); + ASSERT_FALSE(colors.getSpaceConstraint()); + + colors.set("i1", *Color::parse("red")); + colors.set("i2", *Color::parse("#ff0000")); + ASSERT_FALSE(colors.isEmpty()); + ASSERT_FALSE(colors.isSame()); + + colors.set("i3", *Color::parse("#0000ffff")); + ASSERT_FALSE(colors.isSame()); + + EXPECT_EQ(colors.get("i1")->toString(), "red"); + EXPECT_EQ(colors.get("i2")->toString(), "#ff0000"); + EXPECT_EQ(colors.get("i3")->toString(), "#0000ffff"); + EXPECT_EQ(colors.size(), 3); + EXPECT_FALSE(colors.get("i4")); + + colors.set("i1", *Color::parse("green")); + EXPECT_EQ(colors.size(), 3); + EXPECT_EQ(colors.get("i1")->toString(), "green"); + + ASSERT_FALSE(colors.getAlphaConstraint()); + ASSERT_FALSE(colors.getSpaceConstraint()); +} + +TEST(ColorSetTest, setSingleColor) +{ + auto color = ColorSet(); + ASSERT_FALSE(color.get()); + + color.set(*Color::parse("red")); + EXPECT_FALSE(color.isEmpty()); + ASSERT_EQ(color.size(), 1); + ASSERT_TRUE(color.get()); + ASSERT_EQ(color.get()->toString(), "red"); + ASSERT_EQ(color.getAverage().toString(), "#ff0000ff"); + + color.set(*Color::parse("blue")); + ASSERT_EQ(color.size(), 1); + ASSERT_TRUE(color.get()); + ASSERT_EQ(color.get()->toString(), "blue"); + ASSERT_EQ(color.getAverage().toString(), "#0000ffff"); +} + +TEST(ColorSetTest, setColorsConstrained) +{ + auto space = Manager::get().find(Space::Type::RGB); + auto colors = ColorSet(space, false); + ASSERT_TRUE(colors.getSpaceConstraint()); + ASSERT_FALSE(*colors.getAlphaConstraint()); + + colors.set("i1", *Color::parse("red")); + colors.set("i2", *Color::parse("#ff000080")); + ASSERT_TRUE(colors.isSame()); + colors.set("i3", *Color::parse("#0000ffff")); + ASSERT_FALSE(colors.isSame()); + + EXPECT_EQ(colors.get("i1")->toString(), "#ff0000"); + EXPECT_EQ(colors.get("i2")->toString(), "#ff0000"); + EXPECT_EQ(colors.get("i3")->toString(), "#0000ff"); +} + +TEST(ColorSetTest, setColorsHsl) +{ + auto space = Manager::get().find(Space::Type::HSL); + auto colors = ColorSet(space, true); + ASSERT_TRUE(colors.getSpaceConstraint()); + ASSERT_TRUE(colors.getAlphaConstraint()); + ASSERT_TRUE(*colors.getAlphaConstraint()); + + colors.set("i1", *Color::parse("red")); + ASSERT_EQ(colors.get("i1")->toString(), "hsla(0, 1, 0.5, 1)"); +} + +TEST(ColorSetTest, setAllColors) +{ + auto colors_a = ColorSet(); + colors_a.set("i1", *Color::parse("red")); + colors_a.set("i2", *Color::parse("blue")); + + auto colors_b = ColorSet(); + colors_b.set("i1", *Color::parse("green")); + colors_b.set("i3", *Color::parse("purple")); + + EXPECT_TRUE(signalIsCalled(colors_b.signal_changed, [&colors_a, &colors_b]() { colors_b.setAll(colors_a); })); + + // No change to other object + EXPECT_EQ(colors_a.size(), 2); + EXPECT_EQ(colors_a.get("i1")->toString(), "red"); + EXPECT_EQ(colors_a.get("i2")->toString(), "blue"); + + // Addative changes to B + EXPECT_EQ(colors_b.size(), 3); + EXPECT_EQ(colors_b.get("i1")->toString(), "red"); + EXPECT_EQ(colors_b.get("i2")->toString(), "blue"); + EXPECT_EQ(colors_b.get("i3")->toString(), "purple"); +} + +TEST(ColorSetTest, clearColors) +{ + auto colors = ColorSet(); + colors.set("i1", *Color::parse("red")); + colors.set("i2", *Color::parse("green")); + ASSERT_EQ(colors.size(), 2); + colors.clear(); + ASSERT_EQ(colors.size(), 0); +} + +TEST(ColorSetTest, iterateColors) +{ + auto colors = ColorSet(); + colors.set("i1", *Color::parse("red")); + + for (auto &[id, color] : colors) { + EXPECT_EQ(id, "i1"); + EXPECT_EQ(color.toString(), "red"); + } +} + +TEST(ColorSetTest, signalGrabRelease) +{ + auto space = Manager::get().find(Space::Type::RGB); + auto colors = ColorSet(space, false); + ASSERT_TRUE(colors.getSpaceConstraint()); + ASSERT_TRUE(colors.getAlphaConstraint()); + ASSERT_FALSE(*colors.getAlphaConstraint()); + + EXPECT_TRUE(signalIsCalled(colors.signal_grabbed, [&colors]() { colors.grab(); })); + EXPECT_FALSE(signalIsCalled(colors.signal_grabbed, [&colors]() { colors.grab(); })); + EXPECT_TRUE(signalIsCalled(colors.signal_released, [&colors]() { colors.release(); })); + EXPECT_FALSE(signalIsCalled(colors.signal_released, [&colors]() { colors.release(); })); +} + +TEST(ColorSetTest, signalChanged) +{ + auto space = Manager::get().find(Space::Type::RGB); + auto colors = ColorSet(space); + colors.set("0", Color(0xff0000ff)); + colors.set("1", Color(0x00ff00ff)); + + auto comp = colors.getBestSpace()->getComponents(); + + EXPECT_TRUE(signalIsCalled(colors.signal_changed, [&colors, &comp]() { ASSERT_EQ(colors.setAll(comp[0], 0.5), 2); })); + EXPECT_FALSE(signalIsCalled(colors.signal_changed, [&colors, &comp]() { ASSERT_EQ(colors.setAll(comp[0], 0.5), 0); })); + EXPECT_TRUE(signalIsCalled(colors.signal_changed, [&colors]() { ASSERT_TRUE(colors.set("0", *Color::parse("blue"))); })); + EXPECT_FALSE(signalIsCalled(colors.signal_changed, [&colors]() { ASSERT_FALSE(colors.set("0", *Color::parse("blue"))); })); + EXPECT_TRUE(signalIsCalled(colors.signal_changed, [&colors]() { ASSERT_EQ(colors.setAll(*Color::parse("blue")), 1); })); + EXPECT_FALSE(signalIsCalled(colors.signal_changed, [&colors]() { ASSERT_EQ(colors.setAll(*Color::parse("blue")), 0); })); + EXPECT_TRUE(signalIsCalled(colors.signal_changed, [&colors, &comp]() { + colors.grab(); + colors.setAll(comp[0], 0.75); + ; + })); +} + +TEST(ColorSetTest, signalModified) +{ + auto colors = ColorSet(); + EXPECT_FALSE(signalIsCalled(colors.signal_cleared, [&colors]() { colors.clear(); })); + EXPECT_FALSE(signalIsCalled(colors.signal_cleared, [&colors]() { colors.set("new", *Color::parse("red")); })); + EXPECT_TRUE(signalIsCalled(colors.signal_cleared, [&colors]() { colors.clear(); })); +} + +TEST(ColorSetTest, colorAverages) +{ + auto space = Manager::get().find(Space::Type::RGB); + auto colors = ColorSet(space, false); + colors.set("0", Color(space, {0.4, 0.5, 1.0})); + colors.set("1", Color(space, {0.5, 0.5, 0.5})); + colors.set("2", Color(space, {0.6, 0.5, 0.0})); + auto comp = colors.getBestSpace()->getComponents(); + + // Red starts off in the middle, Green is our control + EXPECT_NEAR(colors.getAverage(comp[0]), 0.5, 0.01); + EXPECT_NEAR(colors.getAverage(comp[1]), 0.5, 0.01); + EXPECT_TRUE(VectorIsNear(colors.getAll(comp[0]), {0.4, 0.5, 0.6}, 0.05)); + + // Move red to 0.75, green doesn't change + EXPECT_TRUE(signalIsCalled(colors.signal_changed, [&colors, &comp]() { colors.setAverage(comp[0], 0.75); })); + + EXPECT_NEAR(colors.getAverage(comp[0]), 0.75, 0.01); + EXPECT_NEAR(colors.getAverage(comp[1]), 0.5, 0.01); + EXPECT_TRUE(VectorIsNear(colors.getAll(comp[0]), {0.65, 0.75, 0.85}, 0.05)); + + // Move red to 1.0 pushing some values out of bounds + colors.setAverage(comp[0], 1.0); + EXPECT_NEAR(colors.getAverage(comp[0]), 1.0, 0.01); + EXPECT_TRUE(VectorIsNear(colors.getAll(comp[0]), {0.9, 1, 1}, 0.05)); + + // Move red to 0.25, green doesn't change, but red remembers it's previous range + colors.setAverage(comp[0], 0.25); + EXPECT_NEAR(colors.getAverage(comp[0]), 0.25, 0.01); + EXPECT_NEAR(colors.getAverage(comp[1]), 0.5, 0.01); + EXPECT_TRUE(VectorIsNear(colors.getAll(comp[0]), {0.15, 0.25, 0.35}, 0.05)); +} + +TEST(ColorSetTest, getAverage) +{ + auto colors = ColorSet({}, false); + colors.set("c1", *Color::parse("black")); + colors.set("c2", *Color::parse("white")); + EXPECT_EQ(colors.getBestSpace()->getName(), "CSSNAME"); + EXPECT_EQ(colors.getAverage().toString(), "gray"); + EXPECT_FALSE(colors.isSame()); + + colors.set("c1", *Color::parse("red")); + colors.set("c2", *Color::parse("red")); + EXPECT_EQ(colors.getBestSpace()->getName(), "CSSNAME"); + EXPECT_EQ(colors.getAverage().toString(), "red"); + EXPECT_TRUE(colors.isSame()); + + colors.clear(); + colors.set("c1", *Color::parse("hsl(180,1,1)")); + colors.set("c2", *Color::parse("hsla(60,0,0, 0.5)")); + EXPECT_EQ(colors.getBestSpace()->getName(), "HSL"); + EXPECT_EQ(colors.getAverage().toString(), "hsl(120, 0.5, 0.5)"); + + colors.set("c1", *Color::parse("hsl(180,1,1)")); + colors.set("c2", *Color::parse("hsl(0,0.5,1)")); + colors.set("c3", *Color::parse("blue")); + EXPECT_EQ(colors.getBestSpace()->getName(), "HSL"); + EXPECT_EQ(colors.getAverage().toString(), "hsl(139, 0.833, 0.833)"); +} + +TEST(ColorSetTest, getCmykAverage) +{ + auto colors = ColorSet({}, false); + colors.set("cmyk1", *Color::parse("device-cmyk(0.5 0.5 0.0 0.2 / 0.5)")); + colors.set("rgb1", *Color::parse("red")); + EXPECT_EQ(colors.getBestSpace()->getName(), "DeviceCMYK"); + EXPECT_EQ(colors.getAverage().toString(), "device-cmyk(0.25 0.75 0.5 0.1)"); +} + +} // 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/color-test.cpp b/testfiles/src/colors/color-test.cpp new file mode 100644 index 0000000000..bcc4e4448e --- /dev/null +++ b/testfiles/src/colors/color-test.cpp @@ -0,0 +1,424 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Unit tests for color objects. + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "colors/color.h" + +#include +#include "../test-utils.h" + +#include "colors/manager.h" +#include "colors/spaces/base.h" + +using namespace Inkscape::Colors; + +namespace { + +TEST(ColorsColor, construct_space_obj) +{ + auto space = Manager::get().find(Space::Type::HSL); + ASSERT_TRUE(space); + + ASSERT_EQ(Color(space, {0, 1, 0.5}).toString(), "hsl(0, 1, 0.5)"); +} + +TEST(ColorsColor, construct_space_type) +{ + ASSERT_EQ(Color(Space::Type::HSL, {0, 1, 0.5}).toString(), "hsl(0, 1, 0.5)"); +} + +TEST(ColorsColor, construct_css_string) +{ + ASSERT_EQ(Color::parse("red")->toString(), "red"); + // document tested in cms tests +} + +TEST(ColorsColor, construct_rgba) +{ + ASSERT_EQ(Color(0xff00ff00, false).toString(), "#ff00ff"); + ASSERT_EQ(Color(0xff00ff00, true).toString(), "#ff00ff00"); +} + +TEST(ColorsColor, construct_other) +{ + auto color = *Color::parse("red"); + auto other = Color(color); + ASSERT_EQ(other.toString(), "red"); +} + +TEST(ColorsColor, parse) +{ + ASSERT_TRUE(Color::parse("red")); + ASSERT_FALSE(Color::parse("none")); + ASSERT_FALSE(Color::parse(nullptr)); +} + +TEST(ColorsColor, setter) +{ + auto color = *Color::parse("purple"); + color = *Color::parse("green"); + ASSERT_EQ(color.toString(), "green"); + + EXPECT_TRUE(color.set(Color(0x0000ffff), true)); + ASSERT_EQ(color.toString(), "blue"); + EXPECT_TRUE(color.set(Color(0x0000ffff, false), false)); + ASSERT_EQ(color.toString(), "#0000ff"); + + EXPECT_TRUE(color.set(1, 1.0)); + EXPECT_FALSE(color.set(1, 1.0)); + ASSERT_EQ(color.toString(), "#00ffff"); + + EXPECT_TRUE(color.set("red", true)); + EXPECT_FALSE(color.set("red", true)); + ASSERT_EQ(color.toString(), "#ff0000"); + EXPECT_TRUE(color.set("red", false)); + EXPECT_FALSE(color.set("red", false)); + ASSERT_EQ(color.toString(), "red"); + + EXPECT_TRUE(color.set(0x0, false)); + EXPECT_FALSE(color.set(0x0, false)); + ASSERT_EQ(color.toString(), "#000000"); + EXPECT_TRUE(color.set(0x00ff00ff, true)); + ASSERT_EQ(color.toString(), "#00ff00ff"); + EXPECT_TRUE(color.set(0x00ff00, false)); + ASSERT_EQ(color.toString(), "#0000ff"); + + color.setValues({0.2, 1.0, 0.5}); + ASSERT_EQ(color.toString(), "#33ff80"); +} + +TEST(ColorsColor, conditionals) +{ + // == + ASSERT_EQ(*Color::parse("red"), *Color::parse("red")); + // != + ASSERT_NE(*Color::parse("green"), Color(0xff0000)); + // bool + ASSERT_TRUE(Color::parse("blue")); +} + +TEST(ColorsColor, getSpace) +{ + auto color = *Color::parse("red"); + ASSERT_TRUE(color.getSpace()); + ASSERT_EQ(color.getSpace()->getName(), "CSSNAME"); +} + +TEST(ColorsColor, values) +{ + auto color = *Color::parse("red"); + EXPECT_TRUE(VectorIsNear(color.getValues(), {1.0, 0.0, 0.0}, 0.01)); +} + +TEST(ColorsColor, Opacity) +{ + auto color = *Color::parse("red"); + ASSERT_FALSE(color.hasOpacity()); + ASSERT_FALSE(color.converted(Space::Type::HSL)->hasOpacity()); + EXPECT_TRUE(color.setOpacity(1.0)); + EXPECT_FALSE(color.setOpacity(1.0)); + EXPECT_FALSE(color.addOpacity(1.0)); + ASSERT_TRUE(color.hasOpacity()); + ASSERT_EQ(color.getOpacity(), 1.0); + ASSERT_EQ(color.toString(), "#ff0000ff"); + EXPECT_TRUE(color.setOpacity(0.5)); + EXPECT_FALSE(color.setOpacity(0.5)); + ASSERT_TRUE(color.hasOpacity()); + ASSERT_EQ(color.getOpacity(), 0.5); + ASSERT_EQ(color.toString(), "#ff000080"); + EXPECT_TRUE(color.addOpacity(0.5)); + ASSERT_EQ(color.getOpacity(), 0.25); + ASSERT_EQ(color.toString(), "#ff000040"); + color.enableOpacity(false); + ASSERT_FALSE(color.hasOpacity()); + ASSERT_EQ(color.toString(), "red"); + EXPECT_TRUE(color.addOpacity(0.5)); + ASSERT_TRUE(color.hasOpacity()); + ASSERT_EQ(color.getOpacity(), 0.5); + ASSERT_EQ(color.stealOpacity(), 0.5); + ASSERT_FALSE(color.hasOpacity()); +} + +TEST(ColorsColor, colorOpacityPin) +{ + auto color = Color::parse("red"); + ASSERT_EQ(color->getOpacityChannel(), 3); + ASSERT_EQ(color->getPin(3), 8); + color->convert(Space::Type::CMYK); + ASSERT_EQ(color->getOpacityChannel(), 4); + ASSERT_EQ(color->getPin(4), 16); +} + +TEST(ColorsColor, difference) +{ + auto color = Color::parse("green"); + ASSERT_NEAR(color->difference(*Color::parse("red")), 1.251, 0.001); + ASSERT_NEAR(color->difference(*Color::parse("blue")), 1.251, 0.001); + ASSERT_NEAR(color->difference(*Color::parse("black")), 0.251, 0.001); +} + +TEST(ColorsColor, similarAndClose) +{ + double one_hex_away = 0.004; + auto c1 = Color(0xff0000ff, false); + auto c2 = Color(0x0000ffff, false); + ASSERT_FALSE(c1.isClose(c2)); + ASSERT_FALSE(c1.isSimilar(c2)); + + ASSERT_TRUE(c1.isClose(c1)); + ASSERT_TRUE(c1.isSimilar(c1)); + + c2 = *Color::parse("red"); + ASSERT_FALSE(c1.isClose(c2)); + ASSERT_TRUE(c1.isSimilar(c2)); + + c2 = Color(0xfe0101ff, false); + ASSERT_TRUE(c1.isClose(c2, one_hex_away)); + ASSERT_TRUE(c1.isSimilar(c2, one_hex_away)); + + c2 = Color(0xfe0102ff, false); + ASSERT_FALSE(c1.isClose(c2, one_hex_away)); + ASSERT_FALSE(c1.isSimilar(c2, one_hex_away)); +} + +TEST(ColorsColor, convert_other) +{ + auto other = *Color::parse("red"); + auto color = Color::parse("hsl(120, 1, 0.251)"); + color->convert(other); + EXPECT_EQ(color->toString(), "green"); + other.addOpacity(); + color->convert(other); + EXPECT_EQ(color->toString(), "#008000ff"); +} + +TEST(ColorsColor, convert_space_obj) +{ + auto space = Manager::get().find(Space::Type::HSL); + ASSERT_TRUE(space); + + auto color = Color(0xff0000ff, false); + color.convert(space); + ASSERT_EQ(color.toString(), "hsl(0, 1, 0.5)"); +} + +TEST(ColorsColor, convert_space_type) +{ + auto color = Color(0xff0000ff, false); + ASSERT_TRUE(color.convert(Space::Type::HSL)); + ASSERT_EQ(color.toString(), "hsl(0, 1, 0.5)"); + ASSERT_FALSE(color.convert(Space::Type::NONE)); + ASSERT_EQ(color.toString(), "hsl(0, 1, 0.5)"); +} + +TEST(ColorsColor, converted_other) +{ + auto other = *Color::parse("red"); + ASSERT_EQ(Color::parse("hsl(120, 1, 0.251)")->converted(other)->toString(), "green"); + other.addOpacity(); + ASSERT_EQ(Color::parse("hsl(120, 1, 0.251)")->converted(other)->toString(), "#008000ff"); +} + +TEST(ColorsColor, converted_space_obj) +{ + auto space = Manager::get().find(Space::Type::HSL); + ASSERT_TRUE(space); + ASSERT_EQ(Color::parse("red")->converted(space)->toString(), "hsl(0, 1, 0.5)"); +} + +TEST(ColorsColor, converted_space_type) +{ + auto color = Color::parse("red"); + ASSERT_EQ(color->converted(Space::Type::HSL)->toString(), "hsl(0, 1, 0.5)"); + + auto none = color->converted(Space::Type::NONE); + ASSERT_FALSE(none); +} + +TEST(ColorsColor, toString) +{ + ASSERT_EQ(Color::parse("red")->toString(), "red"); + ASSERT_EQ(Color::parse("#ff0")->toString(), "#ffff00"); + ASSERT_EQ(Color::parse("rgb(80,90,255 / 128)")->toString(true), "#505aff80"); + ASSERT_EQ(Color::parse("rgb(80,90,255 / 128)")->toString(false), "#505aff"); + // Each type of space tested in it's own testcase here after. +} + +TEST(ColorsColor, toRGBA) +{ + ASSERT_EQ(Color(0x123456cc).toRGBA(1.0), 0x123456cc); + ASSERT_EQ(Color(0x123456cc).toRGBA(0.5), 0x12345666); + // Each type of space tested in it's own testcase here after. +} + +TEST(ColorsColor, toARGB) +{ + ASSERT_EQ(Color(0x123456cc).toARGB(1.0), 0xcc123456); + ASSERT_EQ(Color(0x123456cc).toARGB(0.5), 0x66123456); +} + +TEST(ColorsColor, toABGR) +{ + ASSERT_EQ(Color(0x123456cc).toABGR(1.0), 0xcc563412); + ASSERT_EQ(Color(0x123456cc).toABGR(0.5), 0x66563412); +} + +TEST(ColorsColor, name) +{ + auto color = *Color::parse("red"); + ASSERT_FALSE(color.getName().size()); + color.setName("Rouge"); + ASSERT_EQ(color.getName(), "Rouge"); + + color.setName("Rouge"); + color.convert(Space::Type::HSL); + ASSERT_FALSE(color.getName().size()); +} + +TEST(ColorsColor, normalizeColor) +{ + auto color = *Color::parse("rgb(0, 0, 0)"); + color.set(0, 2.0); + ASSERT_EQ(color[0], 2.0); + color.set(1, 1.0); + color.set(2, -0.5); + color.normalize(); + ASSERT_EQ(color[0], 1.0); + ASSERT_EQ(color[1], 1.0); + ASSERT_EQ(color[2], 0.0); + + color.convert(Space::Type::HSL); + color.set(0, 4.1); + color.normalize(); + ASSERT_NEAR(color[0], 0.1, 0.001); + + color.set(0, -0.2); + color.normalize(); + ASSERT_NEAR(color[0], 0.8, 0.001); + + color.set(0, -2.2); + color.normalize(); + ASSERT_NEAR(color[0], 0.8, 0.001); + + color.setOpacity(4.2); + auto copy = color.normalized(); + ASSERT_NEAR(color[3], 4.2, 0.001); + ASSERT_NEAR(copy[3], 1.0, 0.001); +} + +TEST(ColorsColor, invertColor) +{ + auto color = *Color::parse("red"); + color.invert(); + ASSERT_EQ(color.toString(), "aqua"); + color.invert(); + ASSERT_EQ(color.toString(), "red"); + + color = *Color::parse("hsl(90,0.5,0.1)"); + color.invert(); + ASSERT_EQ(color.toString(), "hsl(270, 0.5, 0.9)"); + + color.invert(2); + ASSERT_EQ(color.toString(), "hsl(90, 0.5, 0.1)"); + + color = *Color::parse("rgb(255 255 255 0.2)"); + ASSERT_NEAR(color[0], 1, 0.001); + color.invert(); + ASSERT_NEAR(color[0], 0, 0.001); + ASSERT_NEAR(color[3], 0.2, 0.001); + + color.invert(0); + ASSERT_NEAR(color[0], 1, 0.001); + ASSERT_NEAR(color[3], 0.8, 0.001); +} + +TEST(ColorsColor, jitterColor) +{ + auto color = *Color::parse("gray"); + + std::srand(1); // fixed random seed + + color.jitter(0.1, 0xff); + EXPECT_EQ(color.toString(), "gray"); + +#ifdef __APPLE__ + // The hasher on llvm works differently so the random results are different + color.jitter(0.1); + EXPECT_EQ(color.toString(), "#737787"); + color.jitter(0.2); + EXPECT_EQ(color.toString(), "#717878"); + color.jitter(0.2, 0x02); + EXPECT_EQ(color.toString(), "#5a7881"); +#else + color.jitter(0.1); + EXPECT_EQ(color.toString(), "#897d87"); + color.jitter(0.2); + EXPECT_EQ(color.toString(), "#989278"); + color.jitter(0.2, 0x02); + EXPECT_EQ(color.toString(), "#8f9285"); +#endif + + color.setOpacity(0.5); + color.jitter(0.5, color.getPin(color.getOpacityChannel())); + EXPECT_EQ(color.getOpacity(), 0.5); +} + +TEST(ColorsColor, average) +{ + auto c1 = *Color::parse("#ff0000"); + auto c2 = *Color::parse("#0000ff"); + ASSERT_EQ(c1.averaged(c2).toString(), "#800080"); + ASSERT_EQ(c2.averaged(c1).toString(), "#800080"); + c1.setOpacity(0.5); + ASSERT_EQ(c1.averaged(c2, 0.25).toString(), "#bf00409f"); + c1.enableOpacity(false); + c2.setOpacity(0.5); + ASSERT_EQ(c1.averaged(c2, 0.75).toString(), "#4000bf"); + + c1 = Color(0x0); + c1.average(Color(0xffffffff), 0.25, 1); + EXPECT_EQ(c1.toString(), "#00404040"); + + c1 = Color(0x0); + c1.average(Color(0xffffffff), 0.25, 2); + EXPECT_EQ(c1.toString(), "#40004040"); + + c1 = Color(0x0); + c1.average(Color(0xffffffff), 0.25, 4 + 2); + EXPECT_EQ(c1.toString(), "#40000040"); + + c1 = Color(0x0); + c1.average(Color(0xffffffff), 0.25, c1.getPin(3)); + EXPECT_EQ(c1.toString(), "#40404000"); + + auto c3 = Color(0x1a1a1a1a); + c3.average(Color(0xffffffff), 0.2, 2); + EXPECT_EQ(c3.toString(), "#481a4848"); + c3.average(Color(0xffffffff), 0.3, 4 + 2); + EXPECT_EQ(c3.toString(), "#7f1a487f"); + c3.average(Color(0xffffffff), 0.5, c3.getPin(3)); + EXPECT_EQ(c3.toString(), "#bf8da37f"); + + c1 = Color(0x00000000); + c1.average(Color(0xffffffff), 0.1, 0); + ASSERT_NEAR(c1[0], 0.1, 0.001); + ASSERT_EQ(c1.toString(), "#1a1a1a1a"); +} + +} // 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/document-cms-test.cpp b/testfiles/src/colors/document-cms-test.cpp new file mode 100644 index 0000000000..096047d23c --- /dev/null +++ b/testfiles/src/colors/document-cms-test.cpp @@ -0,0 +1,268 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Unit tests for tracking icc profiles in a document. + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "colors/document-cms.h" + +#include + +#include "colors/color.h" +#include "colors/manager.h" +#include "colors/spaces/cms.h" +#include "document.h" +#include "inkscape.h" +#include "object/color-profile.h" +#include "object/sp-stop.h" + +static std::string icc_dir = INKSCAPE_TESTS_DIR "/data/colors/"; +static std::string svg_objs_file = INKSCAPE_TESTS_DIR "/data/colors/cms-in-objs.svg"; +static std::string svg_defs_file = INKSCAPE_TESTS_DIR "/data/colors/cms-in-defs.svg"; +static std::string grb_profile = INKSCAPE_TESTS_DIR "/data/colors/SwappedRedAndGreen.icc"; + +using namespace Inkscape; +using namespace Inkscape::Colors; + +namespace { + +class ColorDocumentCMSObjsTest : public ::testing::Test +{ +protected: + void SetUp() override + { + // Setup inkscape dependency + Inkscape::Application::create(false); + + // Allow lookup by ID and name with test icc profiles + auto &cms = Inkscape::Colors::CMS::System::get(); + cms.clearDirectoryPaths(); + cms.addDirectoryPath(icc_dir, false); + cms.refreshProfiles(); + + // Load the test svg file with a bunch of icc profiles + doc = SPDocument::createNewDoc(getDocFilename().c_str(), false); + } + std::unique_ptr doc; + + virtual std::string const &getDocFilename() const + { + return svg_objs_file; + } +}; + +TEST_F(ColorDocumentCMSObjsTest, loadDocument) +{ + auto &cm = Manager::get(); + auto &tr = doc->getDocumentCMS(); + + ASSERT_FALSE(tr.getSpace("nonsense")); + + // Internal spaces + ASSERT_TRUE(cm.find(Space::Type::CSSNAME)); + ASSERT_TRUE(cm.find(Space::Type::RGB)); + ASSERT_TRUE(cm.find(Space::Type::HSL)); + + // Document icc profiles + ASSERT_TRUE(tr.getSpace("grb")); + ASSERT_TRUE(tr.getSpace("cmyk-rcm")); + ASSERT_TRUE(tr.getSpace("cmyk-acm")); + + ASSERT_TRUE(cm.find(Space::Type::CMYK)); + ASSERT_EQ(cm.find(Space::Type::CMYK)->getName(), "DeviceCMYK"); + ASSERT_EQ(cm.find(Space::Type::RGB)->getName(), "RGB"); +} + +TEST_F(ColorDocumentCMSObjsTest, updateIntent) +{ + auto &tr = doc->getDocumentCMS(); + auto space = tr.getSpace("grb"); + ASSERT_TRUE(space); + + auto cp = tr.getColorProfileForSpace(space); + ASSERT_TRUE(cp); + + ASSERT_EQ(space->getIntent(), RenderingIntent::PERCEPTUAL); + ASSERT_EQ(cp->getRenderingIntent(), RenderingIntent::UNKNOWN); + ASSERT_EQ(cp->getAttribute("rendering-intent"), nullptr); + tr.setRenderingIntent("grb", RenderingIntent::PERCEPTUAL); + ASSERT_EQ(space->getIntent(), RenderingIntent::PERCEPTUAL); + ASSERT_EQ(cp->getRenderingIntent(), RenderingIntent::PERCEPTUAL); + ASSERT_EQ(std::string(cp->getAttribute("rendering-intent")), "perceptual"); + + space = tr.getSpace("cmyk-acm"); + ASSERT_EQ(space->getIntent(), RenderingIntent::ABSOLUTE_COLORIMETRIC); + + space = tr.getSpace("cmyk-rcm"); + ASSERT_EQ(space->getIntent(), RenderingIntent::RELATIVE_COLORIMETRIC); +} + +TEST_F(ColorDocumentCMSObjsTest, createColorProfile) +{ + auto &tr = doc->getDocumentCMS(); + ASSERT_FALSE(tr.getSpace("C.icc")); + + tr.attachProfileToDoc("C.icc", ColorProfileStorage::LOCAL_ID, RenderingIntent::AUTO); + ASSERT_TRUE(tr.getSpace("C.icc")); + auto space = tr.getSpace("C.icc"); + + ASSERT_TRUE(space); + ASSERT_EQ(space->getIntent(), RenderingIntent::AUTO); +} + +TEST_F(ColorDocumentCMSObjsTest, deleteColorProfile) +{ + auto &tr = doc->getDocumentCMS(); + auto cp0 = doc->getObjectById("cp2"); + ASSERT_TRUE(cp0); + + ASSERT_TRUE(tr.getSpace("cmyk-rcm")); + auto cp = tr.getColorProfileForSpace("cmyk-rcm"); + ASSERT_TRUE(cp); + cp->deleteObject(); + ASSERT_FALSE(tr.getSpace("cmyk-rcm")); +} + +TEST_F(ColorDocumentCMSObjsTest, cmsAddMultiple) +{ + auto &tr = doc->getDocumentCMS(); + ASSERT_TRUE(tr.getSpace("grb")); + ASSERT_EQ(tr.getSpace("grb")->getType(), Space::Type::RGB); + EXPECT_THROW(tr.addProfileURI(grb_profile, "grb", RenderingIntent::RELATIVE_COLORIMETRIC), ColorError); +} + +TEST_F(ColorDocumentCMSObjsTest, cmsParsingRGB) +{ + auto &tr = doc->getDocumentCMS(); + + auto space = tr.getSpace("grb"); + ASSERT_TRUE(space); + EXPECT_TRUE(space->isValid()); + EXPECT_EQ(space->getType(), Space::Type::RGB); + + auto color = *tr.parse("#000001 icc-color(grb, 1, 0.8, 0.6)"); + EXPECT_EQ(color.toString(), "#ccff99 icc-color(grb, 1, 0.8, 0.6)"); + EXPECT_EQ(color.toRGBA(), 0xccff99ff); + EXPECT_EQ(*color.getSpace(), *space); +} + +TEST_F(ColorDocumentCMSObjsTest, cmsParsingCMYK1) +{ + auto &tr = doc->getDocumentCMS(); + + auto space = tr.getSpace("cmyk-rcm"); + ASSERT_TRUE(space); + EXPECT_TRUE(space->isValid()); + EXPECT_EQ(space->getType(), Space::Type::CMYK); + + auto color = *tr.parse("#000002 icc-color(cmyk-rcm, 0.5, 0, 0, 0)"); + EXPECT_EQ(color.toString(), "#6dcff6 icc-color(cmyk-rcm, 0.5, 0, 0, 0)"); + EXPECT_EQ(color.toRGBA(), 0x6dcff6ff); + EXPECT_EQ(*color.getSpace(), *space); +} + +TEST_F(ColorDocumentCMSObjsTest, cmsParsingCMYK2) +{ + auto &tr = doc->getDocumentCMS(); + + auto space = tr.getSpace("cmyk-acm"); + ASSERT_TRUE(space); + EXPECT_TRUE(space->isValid()); + EXPECT_EQ(space->getType(), Space::Type::CMYK); + + auto color = *tr.parse("#000003 icc-color(cmyk-acm, 0.5, 0, 0, 0)"); + EXPECT_EQ(color.toString(), "#69b6d1 icc-color(cmyk-acm, 0.5, 0, 0, 0)"); + EXPECT_EQ(color.toRGBA(), 0x69b6d1ff); + EXPECT_EQ(*color.getSpace(), *space); +} + +TEST_F(ColorDocumentCMSObjsTest, applyConversion) +{ + auto &tr = doc->getDocumentCMS(); + auto grb = tr.getSpace("grb"); + auto rcm = tr.getSpace("cmyk-rcm"); + auto acm = tr.getSpace("cmyk-acm"); + ASSERT_TRUE(grb); + ASSERT_TRUE(rcm); + ASSERT_TRUE(acm); + + auto color = *Color::parse("red"); + ASSERT_EQ(color.toString(), "red"); + + // Converting an anonymous color fails + auto other = tr.parse("icc-color(bad, 1.0, 0.8, 0.6)"); + color.convert(other->getSpace()); + ASSERT_EQ(color.toString(), "red"); + ASSERT_FALSE(color.converted(other->getSpace())); + + // Specifying the space properly works + EXPECT_EQ(color.converted(grb)->toString(), "#ff0000 icc-color(grb, 0, 1, 0)"); + + // Double conversion does nothing + color.convert(grb); + color.convert(grb); + EXPECT_EQ(color.toString(), "#ff0000 icc-color(grb, 0, 1, 0)"); + + // Converting from one icc profile to another is possible + EXPECT_EQ(color.converted(rcm)->toString(), "#ed1c24 icc-color(cmyk-rcm, 0, 1, 1, 0)"); + // Same icc profile should keep the same cmyk values, but + // because the render intent is different the RGB changes + EXPECT_EQ(color.converted(acm)->toString(), "#cf2c2d icc-color(cmyk-acm, 0, 1, 1, 0)"); +} + +class ColorDocumentCMSDefsTest : public ColorDocumentCMSObjsTest +{ + std::string const &getDocFilename() const override + { + return svg_defs_file; + } +}; + +TEST_F(ColorDocumentCMSDefsTest, loadDocument) +{ + auto &tracker = doc->getDocumentCMS(); + auto spaces = tracker.getSpaces(); + + ASSERT_EQ(spaces.size(), 1); + ASSERT_EQ(spaces[0]->getName(), "Artifex-CMYK-SWOP-Profile"); + ASSERT_TRUE(spaces[0]->isValid()); + + auto objects = tracker.getObjects(); + ASSERT_EQ(objects.size(), 1); + ASSERT_EQ(std::string(objects[0]->getId()), "artefact-cmyk"); + + auto stop = cast(doc->getObjectById("stop2212")); + ASSERT_TRUE(stop); + auto color = stop->getColor(); + + // Test the expected values actually return + std::string expected = "#2c292a icc-color(Artifex-CMYK-SWOP-Profile, 0, 0, 0, 1)"; + EXPECT_TRUE(tracker.parse(expected)->getSpace()->isValid()); + EXPECT_EQ(tracker.parse(expected)->toString(), expected); + EXPECT_EQ(color.toString(), expected); + + auto space = dynamic_pointer_cast(color.getSpace()); + EXPECT_TRUE(space->isValid()); + EXPECT_EQ(space, spaces[0]); + EXPECT_NEAR(color[0], 0, 0.01); + EXPECT_NEAR(color[1], 0, 0.01); + EXPECT_NEAR(color[2], 0, 0.01); + EXPECT_NEAR(color[3], 1, 0.01); +} + +} // 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/dragndrop-test.cpp b/testfiles/src/colors/dragndrop-test.cpp new file mode 100644 index 0000000000..e56135fa0b --- /dev/null +++ b/testfiles/src/colors/dragndrop-test.cpp @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Unit tests for color xml conversions. + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "colors/dragndrop.h" + +#include + +#include "colors/color.h" + +using namespace Inkscape; +using namespace Inkscape::Colors; + +namespace { + +TEST(ColorDragAndDrop, test_getMimeData_none) +{ + auto data = getMIMEData(*Color::parse("red"), "text/bad-format"); + ASSERT_EQ(data.size(), 0); + data = getMIMEData(NoColor(), "text/bad-format"); + ASSERT_EQ(data.size(), 0); +} + +TEST(ColorDragAndDrop, test_getMimeData_oswb) +{ + auto data = getMIMEData(*Color::parse("red"), "application/x-oswb-color"); + ASSERT_EQ(data.size(), 138); + ASSERT_EQ(data[0], '<'); + ASSERT_EQ(data[10], 'i'); + ASSERT_EQ(data[20], 'e'); + ASSERT_EQ(data[30], 'U'); +} + +TEST(ColorDragAndDrop, test_getMimeData_x_color) +{ + auto data = getMIMEData(*Color::parse("red"), "application/x-color"); + ASSERT_EQ(data.size(), 8); + ASSERT_EQ(data[0], '\xFF'); + ASSERT_EQ(data[2], '\x0'); + ASSERT_EQ(data[4], '\x0'); + + data = getMIMEData(NoColor(), "application/x-color"); + ASSERT_EQ(data.size(), 8); + ASSERT_EQ(data[0], '\x0'); + ASSERT_EQ(data[2], '\x0'); + ASSERT_EQ(data[4], '\x0'); +} + +TEST(ColorDragAndDrop, test_getMimeData_text) +{ + auto data = getMIMEData(*Color::parse("red"), "text/plain"); + ASSERT_EQ(data.size(), 3); + ASSERT_EQ(data[0], 'r'); + ASSERT_EQ(data[2], 'd'); + + data = getMIMEData(NoColor(), "text/plain"); + ASSERT_EQ(std::string(data.begin(), data.end()), "none"); +} + +TEST(ColorDragAndDrop, test_fromMimeData) +{ + auto data = getMIMEData(*Color::parse("red"), "application/x-oswb-color"); + auto paint = fromMIMEData(data, "application/x-oswb-color"); + ASSERT_EQ(std::get(paint).toString(), "red"); +} + +} // 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/manager-test.cpp b/testfiles/src/colors/manager-test.cpp new file mode 100644 index 0000000000..35e0e28fbe --- /dev/null +++ b/testfiles/src/colors/manager-test.cpp @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Unit tests for color objects. + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "colors/manager.h" + +#include + +#include "colors/cms/system.h" +#include "colors/color.h" +#include "colors/spaces/base.h" +#include "colors/spaces/cms.h" +#include "colors/spaces/rgb.h" +#include "colors/spaces/components.h" +#include "colors/spaces/enum.h" +#include "document.h" +#include "inkscape.h" +#include "object/color-profile.h" + +using namespace Inkscape; +using namespace Inkscape::Colors; + +namespace { + +class TestManager : public Manager +{ +public: + TestManager() = default; + ~TestManager() = default; + + std::shared_ptr testAddSpace(Space::AnySpace *space) + { + return addSpace(space); + } + + bool testRemoveSpace(std::shared_ptr space) + { + return removeSpace(std::move(space)); + } + +}; + +TEST(ColorManagerTest, spaceComponents) +{ + auto &cm = Manager::get(); + + ASSERT_TRUE(cm.find(Space::Type::RGB)); + auto comp = cm.find(Space::Type::RGB)->getComponents(); + ASSERT_EQ(comp.size(), 3); + ASSERT_EQ(comp[0].name, "_R:"); + ASSERT_EQ(comp[1].name, "_G:"); + ASSERT_EQ(comp[2].name, "_B:"); + + ASSERT_TRUE(cm.find(Space::Type::HSL)); + comp = cm.find(Space::Type::HSL)->getComponents(true); + ASSERT_EQ(comp.size(), 4); + ASSERT_EQ(comp[0].name, "_H:"); + ASSERT_EQ(comp[1].name, "_S:"); + ASSERT_EQ(comp[2].name, "_L:"); + ASSERT_EQ(comp[3].name, "_A:"); + + ASSERT_TRUE(cm.find(Space::Type::CMYK)); + comp = cm.find(Space::Type::CMYK)->getComponents(false); + ASSERT_EQ(comp.size(), 4); + ASSERT_EQ(comp[0].name, "_C:"); + ASSERT_EQ(comp[1].name, "_M:"); + ASSERT_EQ(comp[2].name, "_Y:"); + ASSERT_EQ(comp[3].name, "_K:"); +} + +TEST(ColorManagerTest, addAndRemoveSpaces) +{ + auto cm = TestManager(); + + auto rgb = cm.find(Space::Type::RGB); + ASSERT_TRUE(rgb); + + EXPECT_THROW(cm.testAddSpace(rgb.get()), ColorError); + + ASSERT_TRUE(cm.testRemoveSpace(rgb)); + ASSERT_FALSE(cm.testRemoveSpace(rgb)); + ASSERT_FALSE(cm.find(Space::Type::RGB)); + + cm.testAddSpace(new Space::RGB()); + ASSERT_TRUE(cm.find(Space::Type::RGB)); +} + + +} // 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/parser-test.cpp b/testfiles/src/colors/parser-test.cpp new file mode 100644 index 0000000000..1d59118c14 --- /dev/null +++ b/testfiles/src/colors/parser-test.cpp @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Unit tests for color objects. + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "colors/parser.h" + +#include + +#include "../test-utils.h" + +using namespace Inkscape; +using namespace Inkscape::Colors; + +namespace { + +TEST(ColorsParser, test_prefix_parsing) +{ + std::istringstream tests("#rgb(hsl( color( srgb icc-color(profile"); + ASSERT_EQ(Parser::getCssPrefix(tests), "#"); + ASSERT_EQ(Parser::getCssPrefix(tests), "rgb"); + ASSERT_EQ(Parser::getCssPrefix(tests), "hsl"); + ASSERT_EQ(Parser::getCssPrefix(tests), "srgb"); + ASSERT_EQ(Parser::getCssPrefix(tests), "icc-color"); + + std::istringstream fails("rgb fail"); + ASSERT_EQ(Parser::getCssPrefix(fails), ""); +} + +void testCssNumber(std::istringstream &ss, double in_value, std::string in_unit, bool in_end = false) +{ + double out_value; + std::string out_unit; + bool out_end = false; + + ASSERT_TRUE(Parser::css_number(ss, out_value, out_unit, out_end, ',')) << in_value << in_unit; + EXPECT_NEAR(out_value, in_value, 0.001); + EXPECT_EQ(out_unit, in_unit); + EXPECT_EQ(out_end, in_end) << in_value << in_unit << " '" << ss.peek() << "'"; +} + +TEST(ColorsParser, test_number_parsing) +{ + try { + std::locale::global(std::locale("C")); + } catch (std::runtime_error &e) { + ASSERT_TRUE(false) << "Locale 'C' not available for testing"; + } + + std::istringstream tests("1.2 .2 5turn 120deg 20%,5,5, 2cm ,4 9000) 0.0002 5t) 42 ) "); + + testCssNumber(tests, 1.2, ""); + testCssNumber(tests, 0.2, ""); + testCssNumber(tests, 5, "turn"); + testCssNumber(tests, 120, "deg"); + testCssNumber(tests, 20, "%"); + testCssNumber(tests, 5, ""); + testCssNumber(tests, 5, ""); + testCssNumber(tests, 2, "cm"); + testCssNumber(tests, 4, ""); + testCssNumber(tests, 9000, "", true); + testCssNumber(tests, 0.0002, ""); + testCssNumber(tests, 5, "t", true); + testCssNumber(tests, 42, "", true); +} + +TEST(ColorsParser, test_alt_locale) +{ + std::locale was; + try { + was = std::locale::global(std::locale("de_DE.UTF8")); + } catch (std::runtime_error &e) { + GTEST_SKIP() << "Skipping all locale test, locale not available"; + } + + std::istringstream tests("1.2 .2 5turn 120deg 20%,5,5, 2cm ,4 9000) 0.0002 5t) 42 ) "); + + testCssNumber(tests, 1.2, ""); + testCssNumber(tests, 0.2, ""); + testCssNumber(tests, 5, "turn"); + testCssNumber(tests, 120, "deg"); + testCssNumber(tests, 20, "%"); + testCssNumber(tests, 5, ""); + testCssNumber(tests, 5, ""); + testCssNumber(tests, 2, "cm"); + testCssNumber(tests, 4, ""); + testCssNumber(tests, 9000, "", true); + testCssNumber(tests, 0.0002, ""); + testCssNumber(tests, 5, "t", true); + testCssNumber(tests, 42, "", true); + + std::locale::global(was); +} + +void testCssValue(std::string test) +{ + std::istringstream tests("2.0 200% .3, 20 / 5.0)"); + std::vector output; + bool end = false; + ASSERT_TRUE(Parser::append_css_value(tests, output, end, ',', 2) // Value 1 + && Parser::append_css_value(tests, output, end, ',', 3) // Value 2 + && Parser::append_css_value(tests, output, end, ',', 0.1) // Value 3 + && Parser::append_css_value(tests, output, end, '/', 5) // Value 4 + && Parser::append_css_value(tests, output, end)); // Opacity + ASSERT_TRUE(end); + ASSERT_EQ(output.size(), 5); + for (auto i = 0; i < 5; i++) { + EXPECT_NEAR(output[i], (double)(i + 1), 0.001); + } +} + +TEST(ColorsParser, parse_append_css_value) +{ + testCssValue("2.0 200% .3, 20 / 5.0)"); + testCssValue("2.0 200% .3, 20)"); + testCssValue("360deg 3turn .3, 20)"); +} + +TEST(ColorsParser, parse_hex) +{ + auto parser = HexParser(); + ASSERT_EQ(parser.getPrefix(), "#"); + + bool more = false; + std::vector output; + std::istringstream p("000001 icc-profile(foo"); + + ASSERT_EQ(parser.parseColor(p, output, more), ""); + ASSERT_TRUE(more); +} + +TEST(ColorsParser, parse) +{ + Space::Type space_type; + std::string cms_name; + std::vector values; + std::vector fallback; + ASSERT_TRUE(Parsers::get().parse("rgb(128, 255, 255)", space_type, cms_name, values, fallback)); + + ASSERT_EQ(space_type, Space::Type::RGB); + ASSERT_EQ(cms_name, ""); + ASSERT_EQ(fallback.size(), 0); + EXPECT_TRUE(VectorIsNear(values, {0.5, 1.0, 1.0}, 0.01)); +} + +} // 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/printer-test.cpp b/testfiles/src/colors/printer-test.cpp new file mode 100644 index 0000000000..6e906c0231 --- /dev/null +++ b/testfiles/src/colors/printer-test.cpp @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Unit tests for printer base class + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "colors/printer.h" + +#include + +using namespace Inkscape::Colors; + +namespace { + +TEST(ColorsPrinter, PrinterBasics) +{ + std::stringstream number; + try { + number.imbue(std::locale("C")); + } catch (std::runtime_error &e) { + ASSERT_TRUE(false) << "Locale 'C' not available for testing"; + } + + number.precision(12); + number << std::fixed << 3.1415; + + auto oo = CssPrinter(3, "prefix", "", " "); + oo << (int)1 << (double)3.3 << (double)0.0; + ASSERT_EQ(std::string(oo), "prefix(1 3.3 0)"); +} + +TEST(ColorsPrinter, PrinterLocale) +{ + std::locale was; + try { + was = std::locale::global(std::locale("de_DE.utf8")); + } catch (std::runtime_error &e) { + GTEST_SKIP() << "Skipping all locale test, locale not available"; + } + + auto oo = CssPrinter(3, "prefix", "", " "); + oo << 1.2 << 3.3 << 0.0234; + ASSERT_EQ(std::string(oo), "prefix(1.2 3.3 0.023)"); + std::locale::global(was); +} + +TEST(ColorsPrinter, LegacyPrinter) +{ + auto oo = CssLegacyPrinter(3, "leg", false); + oo << 1.0 << 3.3 << 0.0; + ASSERT_EQ(std::string(oo), "leg(1, 3.3, 0)"); + + oo = CssLegacyPrinter(3, "leg", true); + oo << 1.2 << 3.3 << 0.0 << 0.5; + ASSERT_EQ(std::string(oo), "lega(1.2, 3.3, 0, 0.5)"); +} + +TEST(ColorsPrinter, FuncPrinter) +{ + auto oo = CssFuncPrinter(4, "func"); + oo << 1.0 << 3.3 << 0.0 << 1.2; + ASSERT_EQ(std::string(oo), "func(1 3.3 0 1.2)"); + + oo = CssFuncPrinter(4, "func"); + oo << 1.0 << 3.3 << 0.0 << 1.2 << 0.5; + ASSERT_EQ(std::string(oo), "func(1 3.3 0 1.2 / 50%)"); +} + +TEST(ColorsPrinter, ColorPrinter) +{ + auto oo = CssColorPrinter(3, "ident"); + oo << 1.0 << 3.3 << 0.0 << 0.5; + ASSERT_EQ(std::string(oo), "color(ident 1 3.3 0 / 50%)"); +} + +} // 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 new file mode 100644 index 0000000000..1a8cd6f004 --- /dev/null +++ b/testfiles/src/colors/spaces-cms-test.cpp @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Unit tests for color objects. + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +#include "colors/cms/profile.h" +#include "colors/color.h" +#include "colors/parser.h" +#include "colors/spaces/cms.h" +#include "colors/document-cms.h" + +using namespace Inkscape::Colors; + +static std::string cmyk_icc = INKSCAPE_TESTS_DIR "/data/colors/default_cmyk.icc"; + +namespace { + +// Get access to protected members for testing +class CMS : public Space::CMS +{ +public: + CMS(unsigned size, Space::Type type = Space::Type::CMYK) + : Space::CMS("test-profile", size, type){}; + CMS(std::shared_ptr profile) + : Space::CMS(profile){}; + std::string toString(std::vector const &values, bool opacity = true) const + { + return Space::CMS::toString(values, opacity); + } + bool testOverInk(double black) { return overInk({1.0, 1.0, 1.0, black}); } + bool testOutOfGamut(std::vector values, std::shared_ptr space) + { + return outOfGamut(values, space); + } + void testName(std::string const &name) { setName(name); } +}; + +TEST(ColorsSpacesCms, parseColor) +{ + auto parser = Space::CMS::CmsParser(); + ASSERT_EQ(parser.getPrefix(), "icc-color"); + + bool more = false; + std::vector output; + std::istringstream ss("stress-test, 0.2, 90%,2, .3 5%)"); + auto name = parser.parseColor(ss, output, more); + ASSERT_EQ(name, "stress-test"); + ASSERT_EQ(output.size(), 5); + ASSERT_EQ(output[0], 0.2); + ASSERT_EQ(output[1], 0.9); + ASSERT_EQ(output[2], 2.0); + ASSERT_EQ(output[3], 0.3); + ASSERT_EQ(output[4], 0.05); +} + +TEST(ColorsSpacesCms, realColor) +{ + auto cmyk_profile = Inkscape::Colors::CMS::Profile::create_from_uri(cmyk_icc); + auto cmyk = std::make_shared(cmyk_profile); + auto color = Color(cmyk, {0, 0, 0, 1}); + + EXPECT_EQ(color.toString(), "#2c292a icc-color(Artifex-CMYK-SWOP-Profile, 0, 0, 0, 1)"); + EXPECT_EQ(color.toRGBA(), 0x2c292aff); + EXPECT_EQ(color.toRGBA(0.5), 0x2c292a80); + EXPECT_EQ(color.converted(Space::Type::RGB)->toString(), "#2c292a"); + EXPECT_FALSE(color.hasOpacity()); + EXPECT_EQ(color.getOpacity(), 1.0); + + color.addOpacity(0.5); + // Opacity isn't stored in the icc-color string, because it's not supported. + EXPECT_TRUE(color.hasOpacity()); + EXPECT_EQ(color.getOpacity(), 0.5); + EXPECT_EQ(color.toString(), "#2c292a icc-color(Artifex-CMYK-SWOP-Profile, 0, 0, 0, 1)"); + EXPECT_EQ(color.toRGBA(), 0x2c292a80); + EXPECT_EQ(color.toRGBA(0.5), 0x2c292a40); + EXPECT_EQ(color.converted(Space::Type::RGB)->toString(), "#2c292a80"); + + color = Color(0x2c292aff); + EXPECT_TRUE(color.convert(cmyk)); + EXPECT_EQ(color.toString(), "#201c1c icc-color(Artifex-CMYK-SWOP-Profile, 0.697, 0.696, 0.658, 0.856)"); +} + +TEST(ColorsSpacesCms, fallbackColor) +{ + auto tr = DocumentCMS(nullptr); + auto color = *tr.parse("#0080ff icc-color(missing-profile, 1, 2, 3)"); + EXPECT_EQ(color.toString(), "#0080ff icc-color(missing-profile, 1, 2, 3)"); + EXPECT_EQ(color.toRGBA(), 0x0080ffff); + EXPECT_EQ(color.toRGBA(0.5), 0x0080ff80); + EXPECT_EQ(color.converted(Space::Type::RGB)->toString(), "#0080ff"); + EXPECT_FALSE(color.hasOpacity()); + EXPECT_EQ(color.getOpacity(), 1.0); + + color.addOpacity(0.5); + EXPECT_EQ(color.toString(), "#0080ff icc-color(missing-profile, 1, 2, 3)"); + EXPECT_EQ(color.toRGBA(), 0x0080ff80); + EXPECT_EQ(color.toRGBA(0.5), 0x0080ff40); +} + +TEST(ColorsSpacesCms, renderingIntent) +{ + auto cmyk_profile = Inkscape::Colors::CMS::Profile::create_from_uri(cmyk_icc); + auto cmyk = std::make_shared(cmyk_profile); + cmyk->testName("vals"); + + auto color1 = Color(cmyk, {0, 0, 0, 1}); + auto color2 = Color(cmyk, {0.5, 0, 0, 0}); + EXPECT_EQ(cmyk->getIntent(), RenderingIntent::UNKNOWN); + + cmyk->setIntent(RenderingIntent::PERCEPTUAL); + EXPECT_EQ(cmyk->getIntent(), RenderingIntent::PERCEPTUAL); + EXPECT_EQ(color1.toString(), "#2c292a icc-color(vals, 0, 0, 0, 1)"); + EXPECT_EQ(color1.converted(Space::Type::RGB)->toString(), "#2c292a"); + EXPECT_EQ(color2.toString(), "#70d0f6 icc-color(vals, 0.5, 0, 0, 0)"); + + cmyk->setIntent(RenderingIntent::RELATIVE_COLORIMETRIC); + EXPECT_EQ(cmyk->getIntent(), RenderingIntent::RELATIVE_COLORIMETRIC); + EXPECT_EQ(color1.toString(), "#231f20 icc-color(vals, 0, 0, 0, 1)"); + EXPECT_EQ(color1.converted(Space::Type::RGB)->toString(), "#231f20"); + EXPECT_EQ(color2.toString(), "#6dcff6 icc-color(vals, 0.5, 0, 0, 0)"); + + cmyk->setIntent(RenderingIntent::SATURATION); + EXPECT_EQ(cmyk->getIntent(), RenderingIntent::SATURATION); + EXPECT_EQ(color1.toString(), "#2c292a icc-color(vals, 0, 0, 0, 1)"); + EXPECT_EQ(color1.converted(Space::Type::RGB)->toString(), "#2c292a"); + EXPECT_EQ(color2.toString(), "#70d0f6 icc-color(vals, 0.5, 0, 0, 0)"); + + cmyk->setIntent(RenderingIntent::ABSOLUTE_COLORIMETRIC); + EXPECT_EQ(cmyk->getIntent(), RenderingIntent::ABSOLUTE_COLORIMETRIC); + EXPECT_EQ(color1.toString(), "#2f2d2c icc-color(vals, 0, 0, 0, 1)"); + EXPECT_EQ(color1.converted(Space::Type::RGB)->toString(), "#2f2d2c"); + EXPECT_EQ(color2.toString(), "#69b6d1 icc-color(vals, 0.5, 0, 0, 0)"); + + cmyk->setIntent(RenderingIntent::RELATIVE_COLORIMETRIC_NOBPC); + EXPECT_EQ(cmyk->getIntent(), RenderingIntent::RELATIVE_COLORIMETRIC_NOBPC); + EXPECT_EQ(color1.toString(), "#373535 icc-color(vals, 0, 0, 0, 1)"); + EXPECT_EQ(color1.converted(Space::Type::RGB)->toString(), "#373535"); + EXPECT_EQ(color2.toString(), "#73d1f6 icc-color(vals, 0.5, 0, 0, 0)"); + + // These should be the same as PERCEPTUAL + cmyk->setIntent(RenderingIntent::UNKNOWN); + EXPECT_EQ(cmyk->getIntent(), RenderingIntent::UNKNOWN); + EXPECT_EQ(color1.toString(), "#2c292a icc-color(vals, 0, 0, 0, 1)"); + EXPECT_EQ(color1.converted(Space::Type::RGB)->toString(), "#2c292a"); + EXPECT_EQ(color2.toString(), "#70d0f6 icc-color(vals, 0.5, 0, 0, 0)"); + + cmyk->setIntent(RenderingIntent::AUTO); + EXPECT_EQ(cmyk->getIntent(), RenderingIntent::AUTO); + EXPECT_EQ(color1.toString(), "#2c292a icc-color(vals, 0, 0, 0, 1)"); + EXPECT_EQ(color1.converted(Space::Type::RGB)->toString(), "#2c292a"); + EXPECT_EQ(color2.toString(), "#70d0f6 icc-color(vals, 0.5, 0, 0, 0)"); +} + +TEST(ColorsSpacesCms, printColor) +{ + auto space = CMS(4); + + ASSERT_FALSE(space.isValid()); + ASSERT_EQ(space.toString({}), ""); + ASSERT_EQ(space.toString({1}), ""); + ASSERT_EQ(space.toString({1, 2, 3, 4}), ""); + ASSERT_EQ(space.toString({0, 0.5, 1, 1, 2, 3, 4}), "#0080ff icc-color(test-profile, 1, 2, 3, 4)"); + + space = CMS(2); + ASSERT_FALSE(space.isValid()); + ASSERT_EQ(space.toString({1}), ""); + ASSERT_EQ(space.toString({0, 0.5, 1, 1, 2}), "#0080ff icc-color(test-profile, 1, 2)"); + ASSERT_EQ(space.toString({0, 0, 0, 1, 2, 3}), "#000000 icc-color(test-profile, 1, 2)"); + + auto srgb = Inkscape::Colors::CMS::Profile::create_srgb(); + space = CMS(srgb); + space.setIntent(RenderingIntent::AUTO); + ASSERT_TRUE(space.isValid()); + ASSERT_EQ(space.toString({1}), ""); + ASSERT_EQ(space.toString({0, 0.5001, 1}), "#0080ff icc-color(sRGB-built-in, 0, 0.5, 1)"); +} + +TEST(ColorsSpacesCms, outOfGamut) +{ + auto srgb = Inkscape::Colors::CMS::Profile::create_srgb(); + auto cmyk = Inkscape::Colors::CMS::Profile::create_from_uri(cmyk_icc); + auto space = CMS(srgb); + auto to_space = std::make_shared(cmyk); + + EXPECT_FALSE(space.testOutOfGamut({0.83, 0.19, 0.49}, to_space)); + // An RGB color (magenta) which is outside the cmyk color profile + EXPECT_TRUE(space.testOutOfGamut({1.0, 0.0, 1.0}, to_space)); + + auto from_space = std::make_shared(srgb); + auto pink = Inkscape::Colors::Color(from_space, {0.83, 0.19, 0.49}); + EXPECT_FALSE(pink.isOutOfGamut(to_space)); + + auto magenta = Inkscape::Colors::Color(from_space, {1.0, 0.0, 1.0}); + EXPECT_TRUE(magenta.isOutOfGamut(to_space)); +} + +TEST(ColorsSpacesCms, overInk) +{ + auto space = CMS(4, Space::Type::CMYK); + ASSERT_TRUE(space.testOverInk(0.21)); + ASSERT_FALSE(space.testOverInk(0.19)); + space = CMS(4, Space::Type::RGB); + ASSERT_FALSE(space.testOverInk(0.21)); + ASSERT_FALSE(space.testOverInk(0.19)); +} + +}; // 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-cmyk-test.cpp b/testfiles/src/colors/spaces-cmyk-test.cpp new file mode 100644 index 0000000000..c4744ebd6d --- /dev/null +++ b/testfiles/src/colors/spaces-cmyk-test.cpp @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Unit tests for the device-cmyk css color space + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "colors/spaces/cmyk.h" +#include "spaces-testbase.h" + +namespace { + +using Space::Type::CMYK; +using Space::Type::RGB; + +class ColorsSpacesCmykProtected + : public Inkscape::Colors::Space::DeviceCMYK + , public testing::Test +{ +}; // Access protected + +// clang-format off +INSTANTIATE_TEST_SUITE_P(ColorsSpacesCmyk, fromString, testing::Values( + // Taken from the w3c device-cmyk example chart + _P(in, "device-cmyk(0 0.2 0.2 0.2)", { 0, 0.2, 0.2, 0.2 }, 0xcca3a3ff), + _P(in, "device-cmyk(30% 0.2 0.2 0.0)", { 0.3, 0.2, 0.2, 0 }, 0xb3ccccff), + _P(in, "device-cmyk(0 0.4 0.4 0.3)", { 0, 0.4, 0.4, 0.3 }, 0xb36b6bff), + _P(in, "device-cmyk(0 0.6 60% 0.5)", { 0, 0.6, 0.6, 0.5 }, 0x803333ff), + _P(in, "device-cmyk(0.3 60% 0.6 10%)", { 0.3, 0.6, 0.6, 0.1 }, 0xa15c5cff), + _P(in, " device-cmyk(90% 0.6 0.6 0) ", { 0.9, 0.6, 0.6, 0 }, 0x196666ff), + _P(in, "device-cmyk(0 0.8 0.8 0.2)", { 0.0, 0.8, 0.8, 0.2 }, 0xcc2929ff), + _P(in, "device-cmyk(0 1.0 1.0 0.1 / 0.5)", { 0.0, 1.0, 1.0, 0.1, 0.5 }, 0xe6000080) +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesCmyk, badColorString, testing::Values( + "device-cmyk", "device-cmyk(", "device-cmyk(10%,", + "device-cmyk(1.0, 1.0, 1.0)" +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesCmyk, toString, testing::Values( + _P(out, CMYK, { 0.1, 0.2, 0.8, 0.1 }, "device-cmyk(0.1 0.2 0.8 0.1)"), + _P(out, CMYK, { 0.2, 0.1, 0.2, 0.1 }, "device-cmyk(0.2 0.1 0.2 0.1)"), + _P(out, CMYK, { 0.3, 0.3, 0.0, 0.5 }, "device-cmyk(0.3 0.3 0 0.5)"), + _P(out, CMYK, { 0.9, 0.0, 0.2, 0.6, 0.8 }, "device-cmyk(0.9 0 0.2 0.6 / 80%)"), + _P(out, CMYK, { 0.9, 0.0, 0.2, 0.6, 0.8 }, "device-cmyk(0.9 0 0.2 0.6)", false) +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesCmyk, convertColorSpace, testing::Values( + _P(inb, CMYK, {1.000, 0.000, 0.000, 0.000}, RGB, {0.000, 1.000, 1.000}), + _P(inb, CMYK, {0.000, 1.000, 0.000, 0.000}, RGB, {1.000, 0.000, 1.000}), + _P(inb, CMYK, {0.000, 0.000, 1.000, 0.000}, RGB, {1.000, 1.000, 0.000}), + _P(inb, CMYK, {0.000, 0.000, 0.000, 1.000}, RGB, {0.000, 0.000, 0.000}), + _P(inb, CMYK, {1.000, 1.000, 0.000, 0.000}, RGB, {0.000, 0.000, 1.000}), + _P(inb, CMYK, {0.000, 1.000, 1.000, 0.000}, RGB, {1.000, 0.000, 0.000}), + _P(inb, CMYK, {1.000, 0.000, 1.000, 0.000}, RGB, {0.000, 1.000, 0.000}), + + // No conversion + _P(inb, CMYK, {1.000, 0.400, 0.200, 0.300}, CMYK, {1.000, 0.400, 0.200, 0.300}, false) +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesCmyk, normalize, testing::Values( + _P(inb, CMYK, { 0.5, 0.5, 0.5, 0.5, 0.5 }, CMYK, { 0.5, 0.5, 0.5, 0.5, 0.5 }), + _P(inb, CMYK, { 1.2, 1.2, 1.2, 1.2, 1.2 }, CMYK, { 1.0, 1.0, 1.0, 1.0, 1.0 }), + _P(inb, CMYK, {-0.2, -0.2, -0.2, -0.2, -0.2 }, CMYK, { 0.0, 0.0, 0.0, 0.0, 0.0 }), + _P(inb, CMYK, { 0.0, 0.0, 0.0, 0.0, 0.0 }, CMYK, { 0.0, 0.0, 0.0, 0.0, 0.0 }), + _P(inb, CMYK, { 1.0, 1.0, 1.0, 1.0, 1.0 }, CMYK, { 1.0, 1.0, 1.0, 1.0, 1.0 }) +)); +// clang-format on + +TEST(ColorsSpacesCmyk, randomConversion) +{ + GTEST_SKIP(); // cmyk isn't reflective + EXPECT_TRUE(RandomPassthrough(CMYK, RGB, 1)); +} + +TEST(ColorsSpacesCmyk, components) +{ + auto c = Manager::get().find(CMYK)->getComponents(); + ASSERT_EQ(c.size(), 4); + ASSERT_EQ(c[0].id, "c"); + ASSERT_EQ(c[1].id, "m"); + ASSERT_EQ(c[2].id, "y"); + ASSERT_EQ(c[3].id, "k"); +} + +TEST_F(ColorsSpacesCmykProtected, overInk) +{ + ASSERT_TRUE(overInk({1.0, 1.0, 1.0, 0.21})); + ASSERT_FALSE(overInk({0.0, 1.0, 1.0, 0.19})); +} + +} // 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-gray-test.cpp b/testfiles/src/colors/spaces-gray-test.cpp new file mode 100644 index 0000000000..1a0af29c33 --- /dev/null +++ b/testfiles/src/colors/spaces-gray-test.cpp @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Unit tests for a Grayscale color space + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "spaces-testbase.h" + +namespace { + +using Space::Type::Gray; +using Space::Type::RGB; + +// There is no CSS for Gray, it was remoed from css color module 4 draft in 2018 +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(fromString); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(badColorString); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(toString); + +// clang-format off +INSTANTIATE_TEST_SUITE_P(ColorsSpacesGray, convertColorSpace, testing::Values( + _P(inb, Gray, {0.7}, RGB, {0.7, 0.7, 0.7}), + + // No conversion + _P(inb, Gray, {0.2}, Gray, {0.2}) +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesGray, normalize, testing::Values( + _P(inb, Gray, { 0.5 }, Gray, { 0.5 }), + _P(inb, Gray, { 1.2 }, Gray, { 1.0 }), + _P(inb, Gray, {-0.2 }, Gray, { 0.0 }), + _P(inb, Gray, { 0.0 }, Gray, { 0.0 }), + _P(inb, Gray, { 1.0 }, Gray, { 1.0 }) +)); +// clang-format on + +TEST(ColorsSpacesGray, randomConversion) +{ + EXPECT_TRUE(RandomPassthrough(Gray, RGB, 100)); +} + +TEST(ColorsSpacesGray, components) +{ + auto c = Manager::get().find(Gray)->getComponents(); + ASSERT_EQ(c.size(), 1); + ASSERT_EQ(c[0].id, "gray"); +} + +} // 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-hsl-test.cpp b/testfiles/src/colors/spaces-hsl-test.cpp new file mode 100644 index 0000000000..24581e8935 --- /dev/null +++ b/testfiles/src/colors/spaces-hsl-test.cpp @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Unit tests for the HSL color space + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "spaces-testbase.h" + +namespace { + +using Space::Type::HSL; +using Space::Type::RGB; + +// clang-format off +INSTANTIATE_TEST_SUITE_P(ColorsSpacesHsl, fromString, testing::Values( + _P(in, "hsl(80, 1, 0.5)", { 0.222, 1, 0.5 }, 0xaaff00ff), + _P(in, "hsl(360,0.5,0)", { 1.0, 0.5, 0 }, 0x000000ff), + _P(in, "hsl(180deg, 100%, 50%)", { 0.5, 1, 0.5 }, 0x00ffffff), + _P(in, "hsl(0.5turn 100% 50%)", { 0.5, 1, 0.5 }, 0x00ffffff), + _P(in, " hsl(20, 1, 0.5)", { 0.055, 1, 0.5 }, 0xff5500ff), + _P(in, "hsl(50%, 100%, 50% / 50%)", { 0.5, 1, 0.5, 0.5 }, 0x00ffff80), + _P(in, "hsla(30, 0, 0.5, 0.5)", { 0.083, 0, 0.5, 0.5 }, 0x80808080) +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesHsl, badColorString, testing::Values( + "hsl", "hsl(", "hsl(360," +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesHsl, toString, testing::Values( + _P(out, HSL, { 0.333, 0.2, 0.8 }, "hsl(119, 0.2, 0.8)"), + _P(out, HSL, { 0.333, 0.8, 0.258 }, "hsl(119, 0.8, 0.258)"), + _P(out, HSL, { 1.0, 0.5, 0.004 }, "hsl(360, 0.5, 0.004)"), + _P(out, HSL, { 0, 1, 0.2, 0.8 }, "hsla(0, 1, 0.2, 0.8)", true), + _P(out, HSL, { 0, 1, 0.2, 0.8 }, "hsl(0, 1, 0.2)", false) +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesHsl, convertColorSpace, testing::Values( + // 20 random tests generated by python3 colorsys.hls_to_rgb() + _P(inb, HSL, {0.248, 0.225, 0.453}, RGB, {0.455, 0.554, 0.351}), + _P(inb, HSL, {0.257, 0.011, 0.403}, RGB, {0.403, 0.407, 0.399}, false), + // XXX GOT {0.250, 0.001, 0.403} in inverse (RGB to HSL) + _P(inb, HSL, {0.415, 0.514, 0.565}, RGB, {0.341, 0.789, 0.561}), + _P(inb, HSL, {0.528, 0.949, 0.408}, RGB, {0.021, 0.664, 0.795}), + _P(inb, HSL, {0.182, 0.455, 0.152}, RGB, {0.209, 0.222, 0.083}), + _P(inb, HSL, {0.334, 0.320, 0.265}, RGB, {0.181, 0.350, 0.181}), + _P(inb, HSL, {0.942, 0.401, 0.881}, RGB, {0.929, 0.833, 0.866}), + _P(inb, HSL, {0.845, 0.925, 0.707}, RGB, {0.978, 0.436, 0.942}), + _P(inb, HSL, {0.889, 0.190, 0.973}, RGB, {0.978, 0.968, 0.974}, false), + // XXX GOT {0.900, 0.185, 0.973} in inverse (RGB to HSL) + _P(inb, HSL, {0.182, 0.870, 0.172}, RGB, {0.295, 0.322, 0.022}), + _P(inb, HSL, {0.474, 0.305, 0.388}, RGB, {0.270, 0.507, 0.470}), + _P(inb, HSL, {0.070, 0.507, 0.513}, RGB, {0.760, 0.474, 0.266}), + _P(inb, HSL, {0.087, 0.713, 0.089}, RGB, {0.153, 0.092, 0.026}), + _P(inb, HSL, {0.537, 0.286, 0.749}, RGB, {0.677, 0.789, 0.821}), + _P(inb, HSL, {0.314, 0.688, 0.858}, RGB, {0.783, 0.956, 0.761}), + _P(inb, HSL, {0.385, 0.802, 0.797}, RGB, {0.634, 0.960, 0.736}), + _P(inb, HSL, {0.544, 0.265, 0.126}, RGB, {0.093, 0.142, 0.160}), + _P(inb, HSL, {0.793, 0.659, 0.998}, RGB, {0.999, 0.997, 0.999}, false), + // XXX GOT {0.833, 0.500, 0.998} in inverse (RGB to HSL) + _P(inb, HSL, {0.884, 0.984, 0.538}, RGB, {0.993, 0.084, 0.719}), + _P(inb, HSL, {0.730, 0.175, 0.475}, RGB, {0.455, 0.392, 0.558}), + + // No conversion + _P(inb, HSL, {1.000, 0.400, 0.200}, HSL, {1.000, 0.400, 0.200}) +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesHsl, normalize, testing::Values( + // Note HSL is special in that it's hue component is radial so -0.2 == +0.8 + _P(inb, HSL, { 0.5, 0.5, 0.5, 0.5 }, HSL, { 0.5, 0.5, 0.5, 0.5 }), + _P(inb, HSL, { 1.2, 1.2, 1.2, 1.2 }, HSL, { 0.2, 1.0, 1.0, 1.0 }), + _P(inb, HSL, {-0.2, -0.2, -0.2, -0.2 }, HSL, { 0.8, 0.0, 0.0, 0.0 }), + _P(inb, HSL, { 0.0, 0.0, 0.0, 0.0 }, HSL, { 0.0, 0.0, 0.0, 0.0 }), + _P(inb, HSL, { 1.0, 1.0, 1.0, 1.0 }, HSL, { 1.0, 1.0, 1.0, 1.0 }) +)); +// clang-format on + +TEST(ColorsSpacesHsl, randomConversion) +{ + EXPECT_TRUE(RandomPassthrough(HSL, RGB, 1000)); +} + +TEST(ColorsSpacesHsl, components) +{ + auto c = Manager::get().find(HSL)->getComponents(); + ASSERT_EQ(c.size(), 3); + ASSERT_EQ(c[0].id, "h"); + ASSERT_EQ(c[1].id, "s"); + ASSERT_EQ(c[2].id, "l"); +} + +} // 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-hsluv-test.cpp b/testfiles/src/colors/spaces-hsluv-test.cpp new file mode 100644 index 0000000000..0916eb25af --- /dev/null +++ b/testfiles/src/colors/spaces-hsluv-test.cpp @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Unit tests for the HSLuv color space + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "spaces-testbase.h" + +namespace { + +using Space::Type::HSLUV; +using Space::Type::RGB; +using Space::Type::LUV; + +// clang-format off +// There is no CSS string for HSLuv colors +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(fromString); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(badColorString); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(toString); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesHSLuv, convertColorSpace, testing::Values( + // No conversion + _P(inb, HSLUV, {1.000, 0.400, 0.200}, HSLUV, {1.000, 0.400, 0.200}), + // Generated by hsluv.org a sample of low and high saturation hues + _P(inb, HSLUV, {0.946, 0.182, 0.500}, RGB, {0.565, 0.431, 0.490}), + _P(inb, HSLUV, {0.016, 0.723, 0.500}, RGB, {0.835, 0.247, 0.349}), + _P(inb, HSLUV, {0.760, 0.787, 0.500}, RGB, {0.517, 0.372, 0.878}), + _P(inb, HSLUV, {0.748, 0.768, 0.232}, RGB, {0.207, 0.137, 0.549}), + _P(inb, HSLUV, {0.018, 0.752, 0.232}, RGB, {0.415, 0.094, 0.149}), + _P(inb, HSLUV, {0.358, 0.454, 0.232}, RGB, {0.149, 0.239, 0.152}), + _P(inb, HSLUV, {0.344, 0.821, 0.848}, RGB, {0.458, 0.933, 0.352}), + _P(inb, HSLUV, {0.874, 0.526, 0.848}, RGB, {0.921, 0.792, 0.901}), + _P(inb, HSLUV, {0.205, 0.788, 0.848}, RGB, {0.921, 0.831, 0.384}) +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesHSLuv, normalize, testing::Values( + _P(inb, HSLUV, { 0.5, 0.5, 0.5, 0.5 }, HSLUV, { 0.5, 0.5, 0.5, 0.5 }), + _P(inb, HSLUV, { 1.2, 1.2, 1.2, 1.2 }, HSLUV, { 0.2, 1.0, 1.0, 1.0 }), + _P(inb, HSLUV, {-0.2, -0.2, -0.2, -0.2 }, HSLUV, { 0.8, 0.0, 0.0, 0.0 }), + _P(inb, HSLUV, { 0.0, 0.0, 0.0, 0.0 }, HSLUV, { 0.0, 0.0, 0.0, 0.0 }), + _P(inb, HSLUV, { 1.0, 1.0, 1.0, 1.0 }, HSLUV, { 1.0, 1.0, 1.0, 1.0 }) +)); +// clang-format on + +TEST(ColorsSpacesHSLuv, randomConversion) +{ + EXPECT_TRUE(RandomPassthrough(HSLUV, RGB, 1000)); +} + +TEST(ColorsSpacesHSLuv, components) +{ + auto c = Manager::get().find(HSLUV)->getComponents(); + ASSERT_EQ(c.size(), 3); + ASSERT_EQ(c[0].id, "h"); + ASSERT_EQ(c[1].id, "s"); + ASSERT_EQ(c[2].id, "l"); +} + +} // 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-hsv-test.cpp b/testfiles/src/colors/spaces-hsv-test.cpp new file mode 100644 index 0000000000..6baf0b4c77 --- /dev/null +++ b/testfiles/src/colors/spaces-hsv-test.cpp @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Unit tests for the HSV color space + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "spaces-testbase.h" + +namespace { + +using Space::Type::HSV; +using Space::Type::RGB; + +// clang-format off +INSTANTIATE_TEST_SUITE_P(ColorsSpacesHsv, fromString, testing::Values( + // Taken from the w3c hwb example chart + _P(in, "hwb(0, 0.2, 0.2)", { 0.0, 0.75, 0.8 }, 0xcc3333ff), + _P(in, "hwb(30, 0.2, 0.2)", { 0.083, 0.75, 0.8 }, 0xcc7f33ff), + _P(in, "hwb(90, 0.2, 0.2)", { 0.25, 0.75, 0.8 }, 0x80cc33ff), + _P(in, "hwb(0, 0.4, 0.4)", { 0.0, 0.333, 0.6 }, 0x996666ff), + _P(in, "hwb(30deg, 0.4, 0.4)", { 0.083, 0.333, 0.6 }, 0x997f66ff), + _P(in, "hwb(0.25turn, 0.4, 0.4)", { 0.25, 0.333, 0.6 }, 0x809966ff), + _P(in, "hwb(0, 0.6, 60%)", { 0.0, 0, 0.5 }, 0x808080ff), + _P(in, "hwb(30, 60%, 0.6)", { 0.083, 0, 0.5 }, 0x808080ff), + _P(in, " hwb(90, 0.6, 0.6) ", { 0.25, 0, 0.5 }, 0x808080ff), + _P(in, "hwb(0, 0.8, 0.8)", { 0.0, 0, 0.5 }, 0x808080ff), + _P(in, "hwb(0, 1.0, 1.0 / 0.5)", { 0.0, 0, 0.5, 0.5 }, 0x80808080) +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesHsv, badColorString, testing::Values( + "hwb", "hwb(", "hwb(360," +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesHsv, toString, testing::Values( + _P(out, HSV, { 0.333, 0.2, 0.8 }, "hwb(119, 0.64, 0.2)"), + _P(out, HSV, { 0.333, 0.8, 0.258 }, "hwb(119, 0.052, 0.742)"), + _P(out, HSV, { 1.0, 0.5, 0.004 }, "hwb(360, 0.002, 0.996)"), + _P(out, HSV, { 0, 1, 0.2, 0.8 }, "hwba(0, 0, 0.8, 0.8)"), + _P(out, HSV, { 0, 1, 0.2, 0.8 }, "hwb(0, 0, 0.8)", false) +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesHsv, convertColorSpace, testing::Values( + // 20 random tests generated by python3 colorsys.rgb_to_hsv() + _P(inb, HSV, {0.132, 0.333, 0.633}, RGB, {0.633, 0.590, 0.422}), + _P(inb, HSV, {0.590, 0.814, 0.225}, RGB, {0.042, 0.126, 0.225}), + _P(inb, HSV, {0.351, 0.643, 0.888}, RGB, {0.317, 0.888, 0.379}), + _P(inb, HSV, {0.160, 0.718, 0.993}, RGB, {0.993, 0.966, 0.280}), + _P(inb, HSV, {0.565, 0.905, 0.411}, RGB, {0.039, 0.265, 0.411}), + _P(inb, HSV, {0.264, 0.981, 0.860}, RGB, {0.368, 0.860, 0.016}), + _P(inb, HSV, {0.883, 0.628, 0.817}, RGB, {0.817, 0.304, 0.664}), + _P(inb, HSV, {0.183, 0.676, 0.788}, RGB, {0.737, 0.788, 0.256}), + _P(inb, HSV, {0.685, 0.769, 0.830}, RGB, {0.263, 0.192, 0.830}), + _P(inb, HSV, {0.691, 0.876, 0.976}, RGB, {0.248, 0.121, 0.976}), + _P(inb, HSV, {0.843, 0.118, 0.803}, RGB, {0.803, 0.708, 0.797}), + _P(inb, HSV, {0.393, 0.732, 0.885}, RGB, {0.237, 0.885, 0.467}), + _P(inb, HSV, {0.923, 0.762, 0.654}, RGB, {0.654, 0.155, 0.385}), + _P(inb, HSV, {0.940, 0.294, 0.387}, RGB, {0.387, 0.273, 0.315}), + _P(inb, HSV, {0.707, 0.348, 0.989}, RGB, {0.728, 0.645, 0.989}), + _P(inb, HSV, {0.043, 0.541, 0.907}, RGB, {0.907, 0.542, 0.416}), + _P(inb, HSV, {0.322, 0.639, 0.043}, RGB, {0.017, 0.043, 0.016}, false), + // XXX GOT {0.327, 0.628, 0.043} in inverse (RGB to HSV) + _P(inb, HSV, {0.035, 0.991, 0.422}, RGB, {0.422, 0.092, 0.004}), + _P(inb, HSV, {0.871, 0.910, 0.735}, RGB, {0.735, 0.066, 0.583}), + _P(inb, HSV, {0.625, 0.931, 0.824}, RGB, {0.057, 0.250, 0.824}), + + // No conversion + _P(inb, HSV, {1.000, 0.400, 0.200}, HSV, {1.000, 0.400, 0.200}) +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesHsv, normalize, testing::Values( + // Note HSV is special in that it's hue component is radial so -0.2 == +0.8 + _P(inb, HSV, { 0.5, 0.5, 0.5, 0.5 }, HSV, { 0.5, 0.5, 0.5, 0.5 }), + _P(inb, HSV, { 1.2, 1.2, 1.2, 1.2 }, HSV, { 0.2, 1.0, 1.0, 1.0 }), + _P(inb, HSV, {-0.2, -0.2, -0.2, -0.2 }, HSV, { 0.8, 0.0, 0.0, 0.0 }), + _P(inb, HSV, { 0.0, 0.0, 0.0, 0.0 }, HSV, { 0.0, 0.0, 0.0, 0.0 }), + _P(inb, HSV, { 1.0, 1.0, 1.0, 1.0 }, HSV, { 1.0, 1.0, 1.0, 1.0 }) +)); +// clang-format on + +TEST(ColorsSpacesHsv, randomConversion) +{ + EXPECT_TRUE(RandomPassthrough(HSV, RGB, 1000)); +} + +TEST(ColorsSpacesHsv, components) +{ + auto c = Manager::get().find(HSV)->getComponents(); + ASSERT_EQ(c.size(), 3); + ASSERT_EQ(c[0].id, "h"); + ASSERT_EQ(c[1].id, "s"); + ASSERT_EQ(c[2].id, "v"); + ASSERT_EQ(c[0].index, 0); + ASSERT_EQ(c[1].index, 1); + ASSERT_EQ(c[2].index, 2); +} + +} // 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-lab-test.cpp b/testfiles/src/colors/spaces-lab-test.cpp new file mode 100644 index 0000000000..3eb654cf15 --- /dev/null +++ b/testfiles/src/colors/spaces-lab-test.cpp @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Unit tests for the Lab color space + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "colors/spaces/lab.h" +#include "spaces-testbase.h" + +namespace { + +using Space::Type::LAB; +using Space::Type::RGB; + +// clang-format off +INSTANTIATE_TEST_SUITE_P(ColorsSpacesLAB, fromString, testing::Values( + _P(in, "lab(50% -20 0.5)", { 0.5, 0.42, 0.502 }, 0x4c8175ff), + _P(in, "lab(75 -125 125)", { 0.75, 0.0, 1.0 }, 0x4ce3d9ff), + _P(in, "lab(0 0 0)", { 0.0, 0.5, 0.5 }, 0x000000ff), + _P(in, "lab(20% 20 20 / 20%)", { 0.2, 0.58, 0.58, 0.2 }, 0x51231333) +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesLAB, badColorString, testing::Values( + "lab", "lab(", "lab(100" +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesLAB, toString, testing::Values( + _P(out, LAB, { 0.3, 0.2, 0.8 }, "lab(30 -75 75)"), + _P(out, LAB, { 0.3, 0.8, 0.258 }, "lab(30 75 -60.5)"), + _P(out, LAB, { 1.0, 0.5, 0.004 }, "lab(100 0 -124)"), + _P(out, LAB, { 0, 1, 0.2, 0.8 }, "lab(0 125 -75 / 80%)", true), + _P(out, LAB, { 0, 1, 0.2, 0.8 }, "lab(0 125 -75)", false) +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesLAB, convertColorSpace, testing::Values( + // Example from w3c css-color-4 documentation + _P(inb, LAB, {0.462, 0.309, 0.694}, RGB, {0.097, 0.499, 0.006}), + // No conversion + _P(inb, LAB, {1.000, 0.400, 0.200}, LAB, {1.000, 0.400, 0.200}) +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesLAB, normalize, testing::Values( + _P(inb, LAB, { 0.5, 0.5, 0.5, 0.5 }, LAB, { 0.5, 0.5, 0.5, 0.5 }), + _P(inb, LAB, { 1.2, 1.2, 1.2, 1.2 }, LAB, { 1.0, 1.0, 1.0, 1.0 }), + _P(inb, LAB, {-0.2, -0.2, -0.2, -0.2 }, LAB, { 0.0, 0.0, 0.0, 0.0 }), + _P(inb, LAB, { 0.0, 0.0, 0.0, 0.0 }, LAB, { 0.0, 0.0, 0.0, 0.0 }), + _P(inb, LAB, { 1.0, 1.0, 1.0, 1.0 }, LAB, { 1.0, 1.0, 1.0, 1.0 }) +)); +// clang-format on + +TEST(ColorsSpacesLAB, randomConversion) +{ + // Isolate conversion functions + EXPECT_TRUE(RandomPassFunc(Space::Lab::fromXYZ, Space::Lab::toXYZ, 1000)); + + // Full stack conversion + EXPECT_TRUE(RandomPassthrough(LAB, RGB, 1000)); +} + +TEST(ColorsSpacesLAB, components) +{ + auto c = Manager::get().find(LAB)->getComponents(); + ASSERT_EQ(c.size(), 3); + EXPECT_EQ(c[0].id, "l"); + EXPECT_EQ(c[1].id, "a"); + EXPECT_EQ(c[2].id, "b"); +} + +} // 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-lch-test.cpp b/testfiles/src/colors/spaces-lch-test.cpp new file mode 100644 index 0000000000..c429b7f4be --- /dev/null +++ b/testfiles/src/colors/spaces-lch-test.cpp @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Unit tests for the LCH color space + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "colors/spaces/lch.h" +#include "spaces-testbase.h" + +namespace { + +using Space::Type::LCH; +using Space::Type::RGB; + +// clang-format off +INSTANTIATE_TEST_SUITE_P(ColorsSpacesLCH, fromString, testing::Values( + _P(in, "lch(50% 20 180)", { 0.5, 0.133, 0.5 }, 0x557f79ff), + _P(in, "lch(100 150 360)", { 1.0, 1.0, 1.0 }, 0x95b4ecff), + _P(in, "lch(0 0 0)", { 0.0, 0.0, 0.0 }, 0x000000ff), + _P(in, "lch(20% 20 72 / 20%)", { 0.2, 0.133, 0.2, 0.2 }, 0x38300933) +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesLCH, badColorString, testing::Values( + "lch", "lch(", "lch(100" +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesLCH, toString, testing::Values( + _P(out, LCH, { 0.0, 0.667, 0.945 }, "lch(0 100.05 340.2)"), + _P(out, LCH, { 0.3, 0.8, 0.258 }, "lch(30 120 92.88)"), + _P(out, LCH, { 1.0, 0.5, 0.004 }, "lch(100 75 1.44)"), + _P(out, LCH, { 0, 1, 0.2, 0.8 }, "lch(0 150 72 / 80%)", true), + _P(out, LCH, { 0, 1, 0.2, 0.8 }, "lch(0 150 72)", false) +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesLCH, convertColorSpace, testing::Values( + // Example from w3c css-color-4 documentation + // None of these conversions match, so a manual comparison was done between + // the old hsluv conversion and the new code, these match ok. So our lch code + // never matched the expected output in css land and this might be a future bug. + //_P(inb, LCH, { 0.0, 0.667, 0.945 }, RGB, { 0.0, 0.14, 0.5 }), + //_P(inb, LCH, { 1.0, 0.667, 0.945 }, RGB, { 0.0, 1.0, 1.0 }), + //_P(inb, LCH, { 0.5, 0.867, 0.055 }, RGB, { 1.0, 0.0, 0.230 }), + //_P(inb, LCH, { 1.0, 0.2, 0.055 }, RGB, { 1.0, 0.918, 0.926 }), + //_P(inb, LCH, { 0.5, 0.88, 0.361 }, RGB, { 0.0, 0.574, 0.0 }), + //_P(inb, LCH, { 0.5, 0.88, 0.5 }, RGB, { 0.0, 0.609, 0.453 }), + // No conversion + _P(inb, LCH, { 1.0, 0.400, 0.200 }, LCH, { 1.0, 0.400, 0.200 }) +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesLCH, normalize, testing::Values( + _P(inb, LCH, { 0.5, 0.5, 0.5, 0.5 }, LCH, { 0.5, 0.5, 0.5, 0.5 }), + _P(inb, LCH, { 1.2, 1.2, 1.2, 1.2 }, LCH, { 1.0, 1.0, 0.2, 1.0 }), + _P(inb, LCH, {-0.2, -0.2, -0.2, -0.2 }, LCH, { 0.0, 0.0, 0.8, 0.0 }), + _P(inb, LCH, { 0.0, 0.0, 0.0, 0.0 }, LCH, { 0.0, 0.0, 0.0, 0.0 }), + _P(inb, LCH, { 1.0, 1.0, 1.0, 1.0 }, LCH, { 1.0, 1.0, 1.0, 1.0 }) +)); +// clang-format on + +TEST(ColorsSpacesLCH, randomConversion) +{ + // Isolate conversion functions + EXPECT_TRUE(RandomPassFunc(Space::Lch::fromLuv, Space::Lch::toLuv, 1000)); + + // Full stack conversion + EXPECT_TRUE(RandomPassthrough(LCH, RGB, 1000)); +} + +TEST(ColorsSpacesLCH, components) +{ + auto c = Manager::get().find(LCH)->getComponents(); + ASSERT_EQ(c.size(), 3); + EXPECT_EQ(c[0].id, "l"); + EXPECT_EQ(c[1].id, "c"); + EXPECT_EQ(c[2].id, "h"); +} + +} // 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-linear-rgb-test.cpp b/testfiles/src/colors/spaces-linear-rgb-test.cpp new file mode 100644 index 0000000000..3da08f1662 --- /dev/null +++ b/testfiles/src/colors/spaces-linear-rgb-test.cpp @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Unit tests for the Linear RGB color space + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "colors/spaces/linear-rgb.h" +#include "spaces-testbase.h" + +namespace { + +using Space::Type::linearRGB; +using Space::Type::RGB; + +// clang-format off +INSTANTIATE_TEST_SUITE_P(ColorsSpacesLinearRGB, fromString, testing::Values( + _P(in, "color(srgb-linear 0.1 1 0.5)", { 0.1, 1, 0.5 }, 0x59ffbcff) +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesLinearRGB, badColorString, testing::Values( + "color(srgb-linear", "color(srgb-linear", "color(srgb-linear 360" +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesLinearRGB, toString, testing::Values( + _P(out, linearRGB, { 0.3, 0.2, 0.8 }, "color(srgb-linear 0.3 0.2 0.8)"), + _P(out, linearRGB, { 0.3, 0.8, 0.258 }, "color(srgb-linear 0.3 0.8 0.258)"), + _P(out, linearRGB, { 1.0, 0.5, 0.004 }, "color(srgb-linear 1 0.5 0.004)"), + _P(out, linearRGB, { 0, 1, 0.2, 0.8 }, "color(srgb-linear 0 1 0.2 / 80%)", true), + _P(out, linearRGB, { 0, 1, 0.2, 0.8 }, "color(srgb-linear 0 1 0.2)", false) +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesLinearRGB, convertColorSpace, testing::Values( + // Example from w3c css-color-4 documentation + _P(inb, linearRGB, {0.435, 0.017, 0.055}, RGB, {0.691, 0.139, 0.259}), + // No conversion + _P(inb, linearRGB, {1.000, 0.400, 0.200}, linearRGB, {1.000, 0.400, 0.200}) +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesLinearRGB, normalize, testing::Values( + _P(inb, linearRGB, { 0.5, 0.5, 0.5, 0.5 }, linearRGB, { 0.5, 0.5, 0.5, 0.5 }), + _P(inb, linearRGB, { 1.2, 1.2, 1.2, 1.2 }, linearRGB, { 1.0, 1.0, 1.0, 1.0 }), + _P(inb, linearRGB, {-0.2, -0.2, -0.2, -0.2 }, linearRGB, { 0.0, 0.0, 0.0, 0.0 }), + _P(inb, linearRGB, { 0.0, 0.0, 0.0, 0.0 }, linearRGB, { 0.0, 0.0, 0.0, 0.0 }), + _P(inb, linearRGB, { 1.0, 1.0, 1.0, 1.0 }, linearRGB, { 1.0, 1.0, 1.0, 1.0 }) +)); +// clang-format on + +TEST(ColorsSpacesLinearRGB, randomConversion) +{ + // Using the functions directly + EXPECT_TRUE(RandomPassFunc(Space::LinearRGB::fromRGB, Space::LinearRGB::toRGB, 1000)); + + // Using the color conversion stack + EXPECT_TRUE(RandomPassthrough(linearRGB, RGB, 1000)); +} + +TEST(ColorsSpacesLinearRGB, components) +{ + auto c = Manager::get().find(linearRGB)->getComponents(); + ASSERT_EQ(c.size(), 3); + EXPECT_EQ(c[0].id, "r"); + EXPECT_EQ(c[1].id, "g"); + EXPECT_EQ(c[2].id, "b"); +} + +} // 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-luv-test.cpp b/testfiles/src/colors/spaces-luv-test.cpp new file mode 100644 index 0000000000..60c942278b --- /dev/null +++ b/testfiles/src/colors/spaces-luv-test.cpp @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Unit tests for the LUV color space + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "colors/spaces/luv.h" +#include "spaces-testbase.h" + +namespace { + +using Space::Type::LUV; +using Space::Type::RGB; + +// clang-format off +// There is no CSS string for Luv colors +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(fromString); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(badColorString); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(toString); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesLuv, convertColorSpace, testing::Values( + // No conversion + _P(inb, LUV, {1.000, 0.400, 0.200}, LUV, {1.000, 0.400, 0.200}) +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesLuv, normalize, testing::Values( + _P(inb, LUV, { 0.5, 0.5, 0.5, 0.5 }, LUV, { 0.5, 0.5, 0.5, 0.5 }), + _P(inb, LUV, { 1.2, 1.2, 1.2, 1.2 }, LUV, { 1.0, 1.0, 1.0, 1.0 }), + _P(inb, LUV, {-0.2, -0.2, -0.2, -0.2 }, LUV, { 0.0, 0.0, 0.0, 0.0 }), + _P(inb, LUV, { 0.0, 0.0, 0.0, 0.0 }, LUV, { 0.0, 0.0, 0.0, 0.0 }), + _P(inb, LUV, { 1.0, 1.0, 1.0, 1.0 }, LUV, { 1.0, 1.0, 1.0, 1.0 }) +)); +// clang-format on + +TEST(ColorsSpacesLuv, manualConversion) +{ + // This output is unscaled, so Luv values are between L:0..100 and etc. + EXPECT_TRUE(ManualPassFunc(Space::Luv::fromXYZ, {0.5, 0.2, 0.4}, Space::Luv::toXYZ, {51.837, 153.445, -57.51})); +} + +TEST(ColorsSpacesLuv, randomConversion) +{ + // Isolate conversion functions + EXPECT_TRUE(RandomPassFunc(Space::Luv::fromXYZ, Space::Luv::toXYZ, 1000)); + + // Full stack conversion + EXPECT_TRUE(RandomPassthrough(LUV, RGB, 1000)); +} + +TEST(ColorsSpacesLuv, components) +{ + auto c = Manager::get().find(LUV)->getComponents(); + ASSERT_EQ(c.size(), 3); + ASSERT_EQ(c[0].id, "l"); + ASSERT_EQ(c[1].id, "u"); + ASSERT_EQ(c[2].id, "v"); +} + +} // 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-named-test.cpp b/testfiles/src/colors/spaces-named-test.cpp new file mode 100644 index 0000000000..f346c372e8 --- /dev/null +++ b/testfiles/src/colors/spaces-named-test.cpp @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Unit tests for color objects. + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +#include "colors/color.h" + +using namespace Inkscape::Colors; + +namespace { + +TEST(ColorsSpacesRgb, fromString) +{ + ASSERT_EQ(Color::parse(" red ")->toRGBA(), 0xff0000ff); + ASSERT_EQ(Color::parse("BLUE ")->toRGBA(0.5), 0x0000ff80); +} + +TEST(ColorsSpaceRgb, fromStringFailures) +{ + ASSERT_FALSE(Color::parse("rĂ©d")); +} + +TEST(ColorsSpaceRgb, toString) +{ + ASSERT_EQ(Color(0xff000000, false).converted(Space::Type::CSSNAME)->toString(), "red"); + ASSERT_EQ(Color::parse("mediumpurple")->toString(), "mediumpurple"); +} + +} // 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-okhsl-test.cpp b/testfiles/src/colors/spaces-okhsl-test.cpp new file mode 100644 index 0000000000..4224f103ae --- /dev/null +++ b/testfiles/src/colors/spaces-okhsl-test.cpp @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Unit tests for the OkHsl color space + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "colors/spaces/okhsl.h" +#include "spaces-testbase.h" + +namespace { + +using Space::Type::RGB; +using Space::Type::OKHSL; + +// clang-format off +// There is no CSS for OkHsl +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(fromString); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(badColorString); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(toString); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesOkHSL, convertColorSpace, testing::Values( + // No conversion + _P(inb, OKHSL, { 1.0, 0.400, 0.200 }, OKHSL, { 1.0, 0.400, 0.200 }) +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesOkHSL, normalize, testing::Values( + _P(inb, OKHSL, { 0.5, 0.5, 0.5, 0.5 }, OKHSL, { 0.5, 0.5, 0.5, 0.5 }), + _P(inb, OKHSL, { 1.2, 1.2, 1.2, 1.2 }, OKHSL, { 0.2, 1.0, 1.0, 1.0 }), + _P(inb, OKHSL, {-0.2, -0.2, -0.2, -0.2 }, OKHSL, { 0.8, 0.0, 0.0, 0.0 }), + _P(inb, OKHSL, { 0.0, 0.0, 0.0, 0.0 }, OKHSL, { 0.0, 0.0, 0.0, 0.0 }), + _P(inb, OKHSL, { 1.0, 1.0, 1.0, 1.0 }, OKHSL, { 1.0, 1.0, 1.0, 1.0 }) +)); +// clang-format on + +TEST(ColorsSpacesOkHSL, randomConversion) +{ + // Isolate conversion functions + // EXPECT_TRUE(RandomPassFunc(Space::OkHsl::toOkLab, Space::OkHsl::toOkLab, 1000)); + + // Full stack conversion + EXPECT_TRUE(RandomPassthrough(OKHSL, RGB, 1000, true)); +} + +TEST(ColorsSpacesOkHSL, components) +{ + auto c = Manager::get().find(OKHSL)->getComponents(); + ASSERT_EQ(c.size(), 3); + EXPECT_EQ(c[0].id, "h"); + EXPECT_EQ(c[1].id, "s"); + EXPECT_EQ(c[2].id, "l"); +} + +} // 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-oklab-test.cpp b/testfiles/src/colors/spaces-oklab-test.cpp new file mode 100644 index 0000000000..cfcc8373bd --- /dev/null +++ b/testfiles/src/colors/spaces-oklab-test.cpp @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Unit tests for the LAB color space + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "colors/spaces/oklab.h" +#include "spaces-testbase.h" + +namespace { + +using Space::Type::OKLAB; + +// clang-format off +INSTANTIATE_TEST_SUITE_P(ColorsSpacesOkLAB, fromString, testing::Values( + _P(in, "oklab(50% -0.4 -0.4)", { 0.5, 0.0, 0.0 }, 0x0045ffff), + _P(in, "oklab(1 0.4 0.4)", { 1.0, 1.0, 1.0 }, 0xff0000ff), + _P(in, "oklab(0 0 0)", { 0.0, 0.5, 0.5 }, 0x000000ff), + _P(in, "oklab(20% 0.2 0.2 / 20%)", { 0.2, 0.75, 0.75, 0.2 }, 0x62000033) +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesOkLAB, badColorString, testing::Values( + "oklab", "oklab(", "oklab(100" +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesOkLAB, toString, testing::Values( + _P(out, OKLAB, { 0.0, 0.667, 0.945 }, "oklab(0 0.134 0.356)"), + _P(out, OKLAB, { 0.3, 0.8, 0.258 }, "oklab(0.3 0.24 -0.194)"), + _P(out, OKLAB, { 1.0, 0.5, 0.004 }, "oklab(1 0 -0.397)"), + _P(out, OKLAB, { 0, 1, 0.2, 0.8 }, "oklab(0 0.4 -0.24 / 80%)", true), + _P(out, OKLAB, { 0, 1, 0.2, 0.8 }, "oklab(0 0.4 -0.24)", false) +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesOkLAB, convertColorSpace, testing::Values( + //_P(inb, OKLAB, { 0.6, 0.125, 0.0 }, RGB, { 0.0, 0.196, 1.0 }), + // No conversion + _P(inb, OKLAB, { 1.0, 0.400, 0.200 }, OKLAB, { 1.0, 0.400, 0.200 }) +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesOkLAB, normalize, testing::Values( + _P(inb, OKLAB, { 0.5, 0.5, 0.5, 0.5 }, OKLAB, { 0.5, 0.5, 0.5, 0.5 }), + _P(inb, OKLAB, { 1.2, 1.2, 1.2, 1.2 }, OKLAB, { 1.0, 1.0, 1.0, 1.0 }), + _P(inb, OKLAB, {-0.2, -0.2, -0.2, -0.2 }, OKLAB, { 0.0, 0.0, 0.0, 0.0 }), + _P(inb, OKLAB, { 0.0, 0.0, 0.0, 0.0 }, OKLAB, { 0.0, 0.0, 0.0, 0.0 }), + _P(inb, OKLAB, { 1.0, 1.0, 1.0, 1.0 }, OKLAB, { 1.0, 1.0, 1.0, 1.0 }) +)); +// clang-format on + +TEST(ColorsSpacesOkLAB, randomConversion) +{ + // Isolate conversion functions + EXPECT_TRUE(RandomPassFunc(Space::OkLab::fromLinearRGB, Space::OkLab::toLinearRGB, 1000)); + + // Full stack conversion + // EXPECT_TRUE(RandomPassthrough(OKLAB, RGB, 1)); +} + +TEST(ColorsSpacesOkLAB, components) +{ + auto c = Manager::get().find(OKLAB)->getComponents(); + ASSERT_EQ(c.size(), 3); + EXPECT_EQ(c[0].id, "h"); + EXPECT_EQ(c[1].id, "s"); + EXPECT_EQ(c[2].id, "l"); +} + +} // 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-oklch-test.cpp b/testfiles/src/colors/spaces-oklch-test.cpp new file mode 100644 index 0000000000..f446f592f0 --- /dev/null +++ b/testfiles/src/colors/spaces-oklch-test.cpp @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Unit tests for the LCH color space + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "colors/spaces/oklch.h" +#include "spaces-testbase.h" + +namespace { + +using Space::Type::OKLCH; +using Space::Type::RGB; + +// clang-format off +// Run out of time before the rest of the features could be done +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(fromString); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(badColorString); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(toString); + +/*INSTANTIATE_TEST_SUITE_P(ColorsSpacesOkLCH, fromString, testing::Values( + _P(in, "oklch(50% 0.1 180)", { 0.5, 0.133, 0.5 }, 0x557f79ff), + _P(in, "oklch(100 0.4 360)", { 1.0, 1.0, 1.0 }, 0x95b4ecff), + _P(in, "oklch(0 0 0)", { 0.0, 0.0, 0.0 }, 0x000000ff), + _P(in, "oklch(20% 0.2 72 / 20%)", { 0.2, 0.133, 0.2, 0.2 }, 0x38300933) +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesOkLCH, badColorString, testing::Values( + "oklch", "oklch(", "oklch(100" +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesOkLCH, toString, testing::Values( + _P(out, OKLCH, { }, "", true), + _P(out, OKLCH, { }, "", false), + _P(out, OKLCH, { 0.0, 0.667, 0.945 }, "oklch(0 100.05 340.2)"), + _P(out, OKLCH, { 0.3, 0.8, 0.258 }, "oklch(30 120 92.88)"), + _P(out, OKLCH, { 1.0, 0.5, 0.004 }, "oklch(100 75 1.44)"), + _P(out, OKLCH, { 0, 1, 0.2, 0.8 }, "oklch(0 150 72 / 80%)", true), + _P(out, OKLCH, { 0, 1, 0.2, 0.8 }, "oklch(0 150 72)", false) +));*/ + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesOkLCH, convertColorSpace, testing::Values( + // No conversion + _P(inb, OKLCH, { 1.0, 0.400, 0.200 }, OKLCH, { 1.0, 0.400, 0.200 }) +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesOkLCH, normalize, testing::Values( + _P(inb, OKLCH, { 0.5, 0.5, 0.5, 0.5 }, OKLCH, { 0.5, 0.5, 0.5, 0.5 }), + _P(inb, OKLCH, { 1.2, 1.2, 1.2, 1.2 }, OKLCH, { 1.0, 1.0, 0.2, 1.0 }), + _P(inb, OKLCH, {-0.2, -0.2, -0.2, -0.2 }, OKLCH, { 0.0, 0.0, 0.8, 0.0 }), + _P(inb, OKLCH, { 0.0, 0.0, 0.0, 0.0 }, OKLCH, { 0.0, 0.0, 0.0, 0.0 }), + _P(inb, OKLCH, { 1.0, 1.0, 1.0, 1.0 }, OKLCH, { 1.0, 1.0, 1.0, 1.0 }) +)); +// clang-format on + +TEST(ColorsSpacesOkLCH, randomConversion) +{ + // Isolate conversion functions + // EXPECT_TRUE(RandomPassFunc(Space::OkLch::toOkLab, Space::OkLch::toOkLab, 1000)); + + // Full stack conversion + // EXPECT_TRUE(RandomPassthrough(OKLCH, RGB, 1000)); +} + +TEST(ColorsSpacesOkLCH, components) +{ + auto c = Manager::get().find(OKLCH)->getComponents(); + ASSERT_EQ(c.size(), 3); + EXPECT_EQ(c[0].id, "l"); + EXPECT_EQ(c[1].id, "c"); + EXPECT_EQ(c[2].id, "h"); +} + +} // 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-rgb-test.cpp b/testfiles/src/colors/spaces-rgb-test.cpp new file mode 100644 index 0000000000..65bcd9ad7b --- /dev/null +++ b/testfiles/src/colors/spaces-rgb-test.cpp @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Unit tests for the RGB color space + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "colors/spaces/components.h" +#include "spaces-testbase.h" + +namespace { + +using Space::Type::RGB; +using Space::Type::HSL; + +// clang-format off +INSTANTIATE_TEST_SUITE_P(ColorsSpacesRgb, fromString, testing::Values( + _P(in, "#f0f", { 1, 0, 1 }, 0xff00ffff), + _P(in, "#FFC", { 1, 1, 0.8 }, 0xffffccff), + _P(in, "#0F3c", { 0, 1, 0.2, 0.8 }, 0x00ff33cc), + _P(in, "#5533Cc", { 0.333, 0.2, 0.8 }, 0x5533ccff), + _P(in, "#5533Cc66", { 0.333, 0.2, 0.8, 0.4 }, 0x5533cc66), + _P(in, " #55Cc42 ", { 0.333, 0.8, 0.258 }, 0x55cc42ff), + _P(in, "rgb(100%, 50%, 1)", { 1.0, 0.5, 0.004 }, 0xff8001ff), + _P(in, "rgb(100% 50% 51)", { 1.0, 0.5, 0.2 }, 0xff8033ff), + _P(in, "rgb(100% ,50% , 51 )", { 1.0, 0.5, 0.2 }, 0xff8033ff), + _P(in, "rgb(100% ,50% , 102 / 50%)", { 1.0, 0.5, 0.4, 0.5 }, 0xff806680), + _P(in, " rgb(128, 128, 128)", { 0.501, 0.501, 0.501 }, 0x808080ff), + _P(in, "rgba(255, 255, 128, 0.5) ", { 1.0, 1.0, 0.501, 0.5 }, 0xffff8080), + _P(in, "color(srgb 1 0.5 0.4 / 50%)", { 1.0, 0.5, 0.4, 0.5 }, 0xff806680) +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesRgb, badColorString, testing::Values( + "", "#", "#1", "#12", + "rgb", "rgb(", "rgb(255,", "rgb(1 2 3", "rgb(1 2 3 4", + "rgba(1 2 3)", + "color(srgb 3" +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesRgb, toString, testing::Values( + _P(out, RGB, { 0.333, 0.2, 0.8 }, "#5533cc"), + _P(out, RGB, { 0.333, 0.8, 0.258 }, "#55cc42"), + _P(out, RGB, { 1.0, 0.5, 0.004 }, "#ff8001"), + _P(out, RGB, { 0, 1, 0.2, 0.8 }, "#00ff33cc") +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesRgb, convertColorSpace, testing::Values( + _P(inb, RGB, { 1.0, 0.0, 0.0 }, RGB, { 1.0, 0.0, 0.0 }, false), + _P(inb, RGB, { 1.0, 0.0, 0.0, 0.5 }, RGB, { 1.0, 0.0, 0.0, 0.5 }, false), + // All other tests are in their respective color space test, for example spoaces-hsl-test.cpp + _P(inb, RGB, { 1.0, 0.0, 0.0 }, HSL, { 0.0, 1.0, 0.5 }), + _P(inb, RGB, { 1.0, 0.0, 0.0, 0.5 }, HSL, { 0.0, 1.0, 0.5, 0.5 }) +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesRgb, normalize, testing::Values( + _P(inb, RGB, { 0.5, 0.5, 0.5, 0.5 }, RGB, { 0.5, 0.5, 0.5, 0.5 }), + _P(inb, RGB, { 1.2, 1.2, 1.2, 1.2 }, RGB, { 1.0, 1.0, 1.0, 1.0 }), + _P(inb, RGB, {-0.2, -0.2, -0.2, -0.2 }, RGB, { 0.0, 0.0, 0.0, 0.0 }), + _P(inb, RGB, { 0.0, 0.0, 0.0, 0.0 }, RGB, { 0.0, 0.0, 0.0, 0.0 }), + _P(inb, RGB, { 1.0, 1.0, 1.0, 1.0 }, RGB, { 1.0, 1.0, 1.0, 1.0 }) +)); +// clang-format on + +TEST(ColorsSpacesRgb, randomConversion) +{ + EXPECT_TRUE(RandomPassthrough(RGB, RGB, 1)); // Not really needed +} + +TEST(ColorsSpacesRgb, components) +{ + auto c = Manager::get().find(RGB)->getComponents(); + ASSERT_EQ(c.size(), 3); + ASSERT_EQ(c[0].id, "r"); + ASSERT_EQ(c[1].id, "g"); + ASSERT_EQ(c[2].id, "b"); + ASSERT_EQ(c[0].index, 0); + ASSERT_EQ(c[1].index, 1); + ASSERT_EQ(c[2].index, 2); + + auto c2 = Manager::get().find(RGB)->getComponents(true); + ASSERT_EQ(c2.size(), 4); + ASSERT_EQ(c2[3].id, "a"); + ASSERT_EQ(c2[3].index, 3); +} + +/*TEST(ColorsSpacesRgb, colorVarFallback) +{ + auto &cm = Manager::get(); + ASSERT_EQ(Color("var(--foo, white)").toString(), "white"); + ASSERT_EQ(Color("var(--foo, black)").toString(), "white"); + ASSERT_EQ(Color("var(--foo, #00ff00)").toString(), "#00ff00"); +}*/ + +} // 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-testbase.h b/testfiles/src/colors/spaces-testbase.h new file mode 100644 index 0000000000..b97ef7de9f --- /dev/null +++ b/testfiles/src/colors/spaces-testbase.h @@ -0,0 +1,370 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Shared test header for testing color spaces + * + * Copyright (C) 2024 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include + +#include "../test-utils.h" + +#include "colors/color.h" +#include "colors/manager.h" +#include "colors/spaces/base.h" +#include "colors/spaces/components.h" + +using namespace Inkscape::Colors; + +namespace { + +/** + * Test that a color space actually exists, to catch test writing mistakes instead of crashing. + */ +void testSpaceName(Space::Type const &type) +{ + auto &manager = Manager::get(); + ASSERT_TRUE(manager.find(type)) << "Unknown Color Space"; +} + +// Allow numbers to be printed as hex in failures +// see https://github.com/google/googletest/issues/222 +class Hex +{ +public: + explicit Hex(unsigned int n) + : _number(n) + {} + operator unsigned int() { return _number; } + bool operator==(Hex const &other) const { return other._number == _number; } + bool operator!=(Hex const &other) const { return other._number != _number; } + bool operator==(unsigned int const &other) const { return other == _number; } + bool operator!=(unsigned int const &other) const { return other != _number; } + unsigned int _number; +}; +void PrintTo(const Hex &obj, std::ostream *oo) +{ + oo->imbue(std::locale("C")); + *oo << "0x" << std::setfill('0') << std::setw(8) << std::hex << obj._number << "'"; +} + +/* ===== In test ===== */ + +struct in : traced_data +{ + const std::string val; + const std::vector out; + const unsigned int rgba; +}; +void PrintTo(const in &obj, std::ostream *oo) +{ + *oo << "'" << obj.val << "'"; +} + +class fromString : public testing::TestWithParam +{ +}; + +TEST_P(fromString, hasValues) +{ + in test = GetParam(); + auto color = Color::parse(test.val); + ASSERT_TRUE(color); + auto scope = test.enable_scope(); + EXPECT_TRUE(VectorIsNear(color->getValues(), test.out, 0.001)); +} + +TEST_P(fromString, hasRGBA) +{ + in test = GetParam(); + auto scope = test.enable_scope(); + auto color = Color::parse(test.val); + ASSERT_TRUE(color); + EXPECT_EQ(Hex(color->toRGBA(true)), Hex(test.rgba)); +} + +class badColorString : public testing::TestWithParam +{ +}; + +TEST_P(badColorString, returnsNone) +{ + EXPECT_FALSE(Color::parse(GetParam())); +} + +/* ===== Out test ===== */ + +struct out : traced_data +{ + const Space::Type space; + const std::vector val; + const std::string out; + const bool opacity = true; +}; +void PrintTo(const out &obj, std::ostream *oo) +{ + *oo << "'" << obj.out << "'"; +} +class toString : public testing::TestWithParam +{ +}; +TEST_P(toString, hasValue) +{ + out test = GetParam(); + auto scope = test.enable_scope(); + testSpaceName(test.space); + EXPECT_EQ(Color(test.space, test.val).toString(test.opacity), test.out); +} + +/* ====== Convert test ===== */ + +struct norm +{ + double count = 0.0; + std::vector min; + std::vector max; +}; + +struct inb : traced_data +{ + const Space::Type space_in; + const std::vector in; + const Space::Type space_out; + const std::vector out; + bool both_directions = true; + + Color do_conversion(bool inplace, norm *notnorm = nullptr) const + { + auto result = Color(space_in, in); + if (inplace) { + result.convert(space_out); + count_notnorm(result, notnorm); + return result; + } + if (auto color = result.converted(space_out)) { + count_notnorm(*color, notnorm); + return *color; + } + throw ColorError("Bad conversion in test"); + } + + void count_notnorm(Color const &c, norm *out = nullptr) const + { + if (!out) + return; + + for (unsigned int i = 0; i < c.size(); i++) { + double v = c[i]; + + // Count the out of bounds results from conversions + if (v < 0.0) + out->count += std::abs(v); + else if (v > 1.0) + out->count += (v - 1.0); + + // Record the min and max ranges + if (out->min.size() <= i) + out->min.emplace_back(0.0); + if (out->max.size() <= i) + out->max.emplace_back(0.0); + + out->min[i] = std::min(out->min[i], v); + out->max[i] = std::max(out->max[i], v); + } + } + + ::testing::AssertionResult forward_test(bool inplace, norm *notnorm = nullptr) const + { + auto result = do_conversion(inplace, notnorm); + return VectorIsNear(result.getValues(), out, 0.005); + } + + ::testing::AssertionResult backward_test(bool inplace, norm *notnorm = nullptr) const + { + return inb{_file, _line, space_out, out, space_in, in}.forward_test(inplace, notnorm); + } + + // Send the results back to be tested for a pass-through test + ::testing::AssertionResult through_test(bool inplace, norm *notnorm = nullptr) const + { + auto result = do_conversion(inplace, notnorm); + return inb{_file, _line, space_out, result.getValues(), space_in, in}.forward_test(inplace, notnorm); + } +}; +void PrintTo(const inb &obj, std::ostream *oo) +{ + *oo << (int)obj.space_in << print_values(obj.in); + *oo << "<->"; + *oo << (int)obj.space_out << print_values(obj.out); +} + +class convertColorSpace : public testing::TestWithParam +{ +}; +TEST_P(convertColorSpace, copy) +{ + auto test = GetParam(); + auto scope = test.enable_scope(); + testSpaceName(test.space_in); + testSpaceName(test.space_out); + EXPECT_TRUE(test.forward_test(false)) << " " << (int)test.space_in << " copy to " << (int)test.space_out; + if (test.both_directions) { + EXPECT_TRUE(test.backward_test(false)) << " " << (int)test.space_in << " copy from " << (int)test.space_out; + } +} +TEST_P(convertColorSpace, inPlace) +{ + auto test = GetParam(); + auto scope = test.enable_scope(); + testSpaceName(test.space_in); + testSpaceName(test.space_out); + EXPECT_TRUE(test.forward_test(true)) << " in place " << (int)test.space_in << " to " << (int)test.space_out; + if (test.both_directions) { + EXPECT_TRUE(test.backward_test(true)) << " in place " << (int)test.space_in << " from " << (int)test.space_out; + } +} + +/** + * Manually test a conversion function, both ways. + * + * @arg from_func - A conversion function in one direction + * @arg from_values - The values to pass into the from_func and to compare to the output from to_func + * @arg to_func - The reverse function + * @arg to_values - The values to pass to to_func and to compare to the output from from_func + */ +::testing::AssertionResult ManualPassFunc(std::function &)> from_func, + std::vector from_values, + std::function &)> to_func, + std::vector to_values, double epsilon = 0.005) +{ + (void)&ManualPassFunc; // Avoid compile warning + auto copy = from_values; + from_func(copy); + auto ret = VectorIsNear(copy, to_values, epsilon); + + if (ret) { + to_func(to_values); + ret = VectorIsNear(to_values, from_values, epsilon); + } + return ret; +} + +/** + * Create many random tests of the conversion functions, outputs and fed to inputs + * to guarentee stability in both directions. + * + * @arg from_func - A conversion function in one direction + * @arg to_func - The reverse function + * @arg count - The number of tests to create + */ +::testing::AssertionResult RandomPassFunc(std::function &)> from_func, + std::function &)> to_func, unsigned count = 1000) +{ + (void)&RandomPassFunc; // Avoid compile warning + std::srand(13375336); // We always seed for tests' repeatability + + std::vector range = {1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0}; + + for (unsigned i = 0; i < count; i++) { + auto values = random_values(3); + auto expected = values; + + from_func(values); + for (int x = 0; x < 3; x++) { + range[x + 0] = std::min(range[x + 0], values[x]); + range[x + 3] = std::max(range[x + 3], values[x]); + } + + to_func(values); + for (int x = 6; x < 9; x++) { + range[x + 0] = std::min(range[x + 0], values[x - 6]); + range[x + 3] = std::max(range[x + 3], values[x - 6]); + } + + auto ret = VectorIsNear(values, expected, 0.005); + if (!ret) { + return ret; + } + } + /*auto ret = VectorIsNear(range, {0,0,0,1,1,1,0,0,0,1,1,1}, 0.01); + if (!ret) { + return ret << " values ranges in random functions calls."; + }*/ + return ::testing::AssertionSuccess(); +} + +/** + * Create many random tests of the conversion stack, outputs and fed to inputs + * to guarentee stability in both directions. + * + * @arg from - A color space to convert in one direction + * @arg to_func - A color space to convert in the oposite direction + * @arg count - The number of tests to create + */ +::testing::AssertionResult RandomPassthrough( + Space::Type const &from, + Space::Type const &to, + unsigned count = 1000, + bool normal_check = false +) +{ + (void)&RandomPassthrough; // Avoid compile warning + std::srand(13375336); // We always seed for tests' repeatability + norm notnorm; // Count the out of bounds + + testSpaceName(from); + testSpaceName(to); + + auto space = Manager::get().find(from); + if (!space) + return ::testing::AssertionFailure() << "can't find space " << (int)from; + + auto ccount = space->getComponentCount(); + for (unsigned i = 0; i < count; i++) { + auto ret = inb{"", 0, from, random_values(ccount), to, {}}.through_test(true, normal_check ? ¬norm : nullptr); + if (!ret) { + return ret << " | " << (int)from << "->" << (int)to; + } + } + if (normal_check && notnorm.count > 1.0) { + return ::testing::AssertionFailure() << " values went above or below the normal expected range of 0.0 and 1.0 by " << notnorm.count << " in " << count << " conversions\n" + << " - Minimal ranges: " << print_values(notnorm.min) << "\n" + << " + Maximal ranges: " << print_values(notnorm.max) << "\n"; + } + return ::testing::AssertionSuccess(); +} + +/* ===== Normalization tests ===== */ + +class normalize : public testing::TestWithParam +{ +}; + +/** + * Test that the normalization functions as expected for this color space. + */ +TEST_P(normalize, values) +{ + inb test = GetParam(); + testSpaceName(test.space_in); + auto color = Color(test.space_in, test.in); + color.normalize(); + auto scope = test.enable_scope(); + EXPECT_TRUE(VectorIsNear(color.getValues(), test.out, 0.001)); +} + +} // 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-xyz-test.cpp b/testfiles/src/colors/spaces-xyz-test.cpp new file mode 100644 index 0000000000..b26193101f --- /dev/null +++ b/testfiles/src/colors/spaces-xyz-test.cpp @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Unit tests for the Linear RGB color space + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "colors/spaces/xyz.h" +#include "spaces-testbase.h" + +namespace { + +using Space::Type::XYZ; +using Space::Type::RGB; + +// clang-format off +INSTANTIATE_TEST_SUITE_P(ColorsSpacesXYZ, fromString, testing::Values( + _P(in, "color(xyz 0.1 1 0.5)", { 0.1, 1, 0.5 }, 0x2e4a9bff) +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesXYZ, badColorString, testing::Values( + "color(xyz", "color(xyz", "color(xyz 360" +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesXYZ, toString, testing::Values( + _P(out, XYZ, { 0.3, 0.2, 0.8 }, "color(xyz 0.3 0.2 0.8)"), + _P(out, XYZ, { 0.3, 0.8, 0.258 }, "color(xyz 0.3 0.8 0.258)"), + _P(out, XYZ, { 1.0, 0.5, 0.004 }, "color(xyz 1 0.5 0.004)"), + _P(out, XYZ, { 0, 1, 0.2, 0.8 }, "color(xyz 0 1 0.2 / 80%)", true), + _P(out, XYZ, { 0, 1, 0.2, 0.8 }, "color(xyz 0 1 0.2)", false) +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesXYZ, convertColorSpace, testing::Values( + // Example from w3c css-color-4 documentation + _P(inb, XYZ, {0.217, 0.146, 0.594}, RGB, {0.463, 0.329, 0.804}), + //_P(inb, XYZ, {0.217, 0.146, 0.594}, "Lab", {0.444, 0.644, 0.264}), + // No conversion + _P(inb, XYZ, {1.000, 0.400, 0.200}, XYZ, {1.000, 0.400, 0.200}) +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesXYZ, normalize, testing::Values( + _P(inb, XYZ, { 0.5, 0.5, 0.5, 0.5 }, XYZ, { 0.5, 0.5, 0.5, 0.5 }), + _P(inb, XYZ, { 1.2, 1.2, 1.2, 1.2 }, XYZ, { 1.0, 1.0, 1.0, 1.0 }), + _P(inb, XYZ, {-0.2, -0.2, -0.2, -0.2 }, XYZ, { 0.0, 0.0, 0.0, 0.0 }), + _P(inb, XYZ, { 0.0, 0.0, 0.0, 0.0 }, XYZ, { 0.0, 0.0, 0.0, 0.0 }), + _P(inb, XYZ, { 1.0, 1.0, 1.0, 1.0 }, XYZ, { 1.0, 1.0, 1.0, 1.0 }) +)); +// clang-format on + +TEST(ColorsSpacesXYZ, randomConversion) +{ + // Isolate conversion functions + EXPECT_TRUE(RandomPassFunc(Space::XYZ::fromLinearRGB, Space::XYZ::toLinearRGB, 1000)); + + // Full stack conversion + EXPECT_TRUE(RandomPassthrough(XYZ, RGB, 1000)); +} + +TEST(ColorsSpacesXYZ, components) +{ + auto c = Manager::get().find(XYZ)->getComponents(); + ASSERT_EQ(c.size(), 3); + EXPECT_EQ(c[0].id, "x"); + EXPECT_EQ(c[1].id, "y"); + EXPECT_EQ(c[2].id, "z"); +} + +} // 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/utils-test.cpp b/testfiles/src/colors/utils-test.cpp new file mode 100644 index 0000000000..db7e110754 --- /dev/null +++ b/testfiles/src/colors/utils-test.cpp @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Unit tests for color objects. + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "colors/utils.h" + +#include + +#include "colors/color.h" +#include "colors/manager.h" + +using namespace Inkscape; +using namespace Inkscape::Colors; + +namespace { + +TEST(ColorUtils, test_hex_to_rgba) +{ + EXPECT_EQ(hex_to_rgba("#ff00ffff"), 0xff00ffff); +} + +TEST(ColorUtils, test_rgba_to_hex) +{ + EXPECT_EQ(rgba_to_hex(0xff00ff00, false), "#ff00ff"); + EXPECT_EQ(rgba_to_hex(0xff00ffff, true), "#ff00ffff"); +} + +TEST(ColorUtils, test_color_to_id) +{ + EXPECT_EQ(color_to_id({}), "none"); + EXPECT_EQ(color_to_id(Color::parse("not-a-color")), "none"); + EXPECT_EQ(color_to_id(Color::parse("red")), "css-red"); + EXPECT_EQ(color_to_id(Color::parse("#0000ff")), "rgb-0000ff"); + + auto color = Color::parse("hsl(0.5, 0.5, 1.0)"); + EXPECT_EQ(color_to_id(color), "hsl-007fff"); + + color->setName("Huey // Dewy_! Lewy"); + EXPECT_EQ(color_to_id(color), "huey-dewy-lewy"); + + color->convert(Space::Type::RGB); + EXPECT_EQ(color_to_id(color), "rgb-ffffff"); +} + +TEST(ColorUtils, test_desc_to_id) +{ + EXPECT_EQ(desc_to_id("thing"), "thing"); + EXPECT_EQ(desc_to_id("Thing Two"), "thing-two"); + EXPECT_EQ(desc_to_id(" Thing ThreĂ© "), "thing-threĂ©"); + EXPECT_EQ(desc_to_id(" Wobble blink CAPLINK!"), "wobble-blink-caplink"); +} + +TEST(ColorUtils, test_make_contrasted_color) +{ + EXPECT_EQ(make_contrasted_color(Color(0x000000ff), 0.2).toRGBA(), 0x040404ff); + EXPECT_EQ(make_contrasted_color(Color(0x000000ff), 0.4).toRGBA(), 0x080808ff); + EXPECT_EQ(make_contrasted_color(Color(0x000000ff), 0.6).toRGBA(), 0x0c0c0cff); + EXPECT_EQ(make_contrasted_color(Color(0xffffffff), 0.2).toRGBA(), 0xfbfbfbff); + EXPECT_EQ(make_contrasted_color(Color(0xffffffff), 0.4).toRGBA(), 0xf7f7f7ff); + EXPECT_EQ(make_contrasted_color(Color(0xffffffff), 0.6).toRGBA(), 0xf3f3f3ff); + EXPECT_EQ(make_contrasted_color(Color(0xa1a1a1ff), 0.2).toRGBA(), 0x9d9d9dff); + EXPECT_EQ(make_contrasted_color(Color(0x1a1a1aff), 0.4).toRGBA(), 0x121212ff); + EXPECT_EQ(make_contrasted_color(Color(0x808080ff), 0.6).toRGBA(), 0x747474ff); +} + +TEST(ColorUtils, test_get_perceptual_lightness) +{ + EXPECT_NEAR(get_perceptual_lightness(*Color::parse("red")), 0.780, 0.001); + EXPECT_NEAR(get_perceptual_lightness(*Color::parse("black")), 0.0, 0.001); + EXPECT_NEAR(get_perceptual_lightness(*Color::parse("white")), 1.0, 0.001); + EXPECT_NEAR(get_perceptual_lightness(*Color::parse("device-cmyk(0.2 0.1 1.0 0.0)")), 0.945, 0.001); +} + +TEST(ColorUtils, test_contrasting_color) +{ + auto a = get_contrasting_color(0.1); + EXPECT_EQ(a.first, 1.0); + EXPECT_NEAR(a.second, 0.688, 0.001); + + auto b = get_contrasting_color(0.9); + EXPECT_EQ(b.first, 0.0); + EXPECT_NEAR(b.second, 0.366, 0.001); +} + +TEST(ColorUtils, make_theme_color) +{ + EXPECT_EQ(make_theme_color(*Color::parse("red"), false).toRGBA(), 0x870000ff); + EXPECT_EQ(make_theme_color(*Color::parse("red"), true).toRGBA(), 0x010101ff); + EXPECT_EQ(make_theme_color(*Color::parse("white"), false).toRGBA(), 0x787878ff); + EXPECT_EQ(make_theme_color(*Color::parse("white"), true).toRGBA(), 0x010101ff); + EXPECT_EQ(make_theme_color(*Color::parse("black"), false).toRGBA(), 0x030303ff); + EXPECT_EQ(make_theme_color(*Color::parse("black"), true).toRGBA(), 0x000000ff); +} + +} // 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/xml-color-test.cpp b/testfiles/src/colors/xml-color-test.cpp new file mode 100644 index 0000000000..9d11195954 --- /dev/null +++ b/testfiles/src/colors/xml-color-test.cpp @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Unit tests for color xml conversions. + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "colors/xml-color.h" + +#include + +#include "colors/cms/profile.h" +#include "colors/cms/system.h" +#include "colors/color.h" +#include "colors/manager.h" +#include "colors/spaces/cms.h" +#include "preferences.h" + +using namespace Inkscape; +using namespace Inkscape::Colors; + +static std::string cmyk_profile = INKSCAPE_TESTS_DIR "/data/colors/default_cmyk.icc"; + +namespace { + +class ColorXmlColor : public ::testing::Test +{ + void SetUp() override + { + auto prefs = Inkscape::Preferences::get(); + prefs->setBool("/options/svgoutput/inlineattrs", false); + } +}; + +TEST_F(ColorXmlColor, test_paint_to_xml_string) +{ + ASSERT_EQ(paint_to_xml_string({}), R"( + + + +)"); + ASSERT_EQ(paint_to_xml_string(NoColor()), R"( + + + +)"); + ASSERT_EQ(paint_to_xml_string(Color(0xcf321244)), R"( + + + +)"); + ASSERT_EQ(paint_to_xml_string(*Color::parse("hsl(180,1,1)")), R"( + + + +)"); +} + +TEST_F(ColorXmlColor, test_icc_paint_xml) +{ + auto profile = CMS::Profile::create_from_uri(cmyk_profile); + CMS::System::get().addProfile(profile); + auto space = std::make_shared(profile); + space->setIntent(RenderingIntent::AUTO); + std::vector vals = {0.5, 0.2, 0.1, 0.23}; + auto color = Color(space, vals); + auto str = paint_to_xml_string(color); + + ASSERT_EQ(str, R"( + + + +)"); + + auto reverse = xml_string_to_paint(str, nullptr); + ASSERT_EQ(std::get(reverse).toString(), color.toString()); +} + +TEST_F(ColorXmlColor, test_xml_string_to_paint) +{ + ASSERT_TRUE(std::holds_alternative(xml_string_to_paint(R"( + + + +)", + nullptr))); + ASSERT_EQ(std::get(xml_string_to_paint(R"( + + + +)", + nullptr) + ).toString(), + "#cf321244"); + ASSERT_EQ(std::get(xml_string_to_paint(R"( + + + +)", + nullptr) + ).toString(), + "hsl(180, 1, 1)"); +} + +} // 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/style-test.cpp b/testfiles/src/style-test.cpp index f0f427f1cd..886d837cb3 100644 --- a/testfiles/src/style-test.cpp +++ b/testfiles/src/style-test.cpp @@ -61,8 +61,8 @@ std::vector getStyleData() // StyleRead("fill:url(#painter) rgb(255, 0, 255)", // "fill:url(#painter) #ff00ff", "#painter"), - - StyleRead("fill:#ff00ff icc-color(colorChange, 0.1, 0.5, 0.1)"), + // Requires a documentCMS to be available to the SPStyle + // StyleRead("fill:#ff00ff icc-color(colorChange, 0.1, 0.5, 0.1)"), // StyleRead("fill:url(#painter)", "", "#painter"), // StyleRead("fill:url(#painter) none", "", "#painter"), diff --git a/testfiles/src/test-utils.h b/testfiles/src/test-utils.h new file mode 100644 index 0000000000..f5920db816 --- /dev/null +++ b/testfiles/src/test-utils.h @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Shared test header for testing colors + * + * Copyright (C) 2024 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include + +namespace { + +/** + * Allow the correct tracing of the file and line where data came from when using P_TESTs. + */ +struct traced_data +{ + const char *_file; + const int _line; + + ::testing::ScopedTrace enable_scope() const { return ::testing::ScopedTrace(_file, _line, ""); } +}; +// Macro for the above tracing in P Tests +#define _P(type, ...) \ + type \ + { \ + __FILE__, __LINE__, __VA_ARGS__ \ + } + + +/** + * Print a vector of doubles for debugging + */ +std::string print_values(const std::vector &v) +{ + std::ostringstream oo; + oo << "{"; + bool first = true; + for (double const &item : v) { + if (!first) { + oo << ", "; + } + first = false; + oo << std::setprecision(3) << item; + } + oo << "}"; + return oo.str(); +} + +/** + * 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) +{ + 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); + } + if (!is_same) { + return ::testing::AssertionFailure() << "\n" << print_values(A) << "\n != \n" << print_values(B); + } + return ::testing::AssertionSuccess(); +} + + +/** + * Generate a count of random doubles between 0 and 1. + * + * Randomly appends an extra value for optional opacity. + */ +inline static std::vector random_values(unsigned ccount) +{ + std::vector values; + for (unsigned j = 0; j < ccount; j++) { + values.emplace_back(static_cast(std::rand()) / RAND_MAX); + } + // randomly add opacity + if (std::rand() > (RAND_MAX / 2)) { + values.emplace_back(static_cast(std::rand()) / RAND_MAX); + } + return values; +} + +} // 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 : -- GitLab From 0b4d3151ad419e0503b7d2a736a78aa9ad5940ff Mon Sep 17 00:00:00 2001 From: Martin Owens Date: Mon, 29 Apr 2024 21:58:16 -0400 Subject: [PATCH 2/2] Reactor Inkscape to use the new color model framework Main movement of the previous color handling code: * Removal of color.cpp, which is now replaced by the class in colors/color * Removal of svg/svg-color.cpp, which is now replaced by colors/color for full color handling and colors/parser for parsing. * Removal of color-rgba.h which is now replaced by colors/color and ui/utils for Gdk conversions * Removal of color/color-conv.cpp into colors/parser * Removal of color/color-profile-cms-fns.h as all the cms encapsulation is already done above. * Removal of widgets/paintdef.cpp, which was a duplicate color structure, xml functionality moved to colors/xml-color.cpp * Removal of svg/svg-renderer.cpp color code, Gdk coversions moved to ui/utils and css code is now an Inkscape::Color object. * Removal of svg-color and oklab tests which are entirely replaced by colors/*tests * Refactor global-palettes.cpp to remove all custom rgb building and instead build Color objects directly. * Refactor color-icc-selector removing most of it's own code in favor of the cms and color space API * Moving of color/cmyk-conv.cpp into colors/spaces/cmyk and regularised into a color space class. * Moving of hsluv.cpp and oklab.cpp which are split and regularised into many colors/spaces/hsluv and ok color space classes * Moving of color/cms-* into colors/cms/* with some of the code from object/colorprofile.cpp to restrict the use of lcms2 to just this directory and make the accessing of cms profiles and transforms predictable and less error prone. * Moving of profile-manager.cpp functionality colors/tracker.cpp rewritten to create the new icc color space objects. * Moving of ui/selected-color.cpp functionality to colors/color-set rewritten to be a collection of trackable color objects. Refactoring actions: * Refactor style color to remove SPColor and Href values, add a private Inkscape::Color and getter/setter functions. * Refactor style alpha so it can be used as a double transparently without conversion. * Refactor SP-Stop color to bundle opacity and simplify how color is used. * Refactor filter constructors to stop constructing their own rgb css color formats * Remove extract color functions from preferences and replace with getColor and setColor which take the new color object. * Remove use of SPColor from anywhere it's being used and replace with Inkscape::Color object * Remove SP_RGBA32_G_F and similar macros to construct colors, or convert them from preferences. * Remove custom checkerboard darken and use new color utils and cairo utils * Remove custom HSL tweaking code from selected-style * Remove SPIPaint::read function used in sp-stop, it was basically just a color parser * Add cairo color functions into display/cairo-utils.cpp which centralise conversion to RGBA; to make it easier to replace later. * Use colors/utils `make_contrasted_color` for finding consistant contrasting colors, replacing copy pasta calculations. * Removal of icc profile specific code in display/nr-filter-* code * Use Color Tracker object in document.cpp instead of the now deleted Profile Manager * Use Color utils hex parsing for Gtk/css handling in themes instead of `sp_svg_write_color` * Use Color in lpe color values, removing duplicated color parsing and printing. * Use Color for item highlight color and plug into various gtk widgets. * Use average color util in GrDrag instead of internal rgb calculation. * Drag and drop colors using new xml and mime type code in colors/dragndrop.cpp * Convert SP Grid to use color objects instead of RGBA ints * Use color/cms framework instead of lcms2 directly in sp-image * Use getColor to handle opacity and color in gradients and mesh gradients * Use Color object in extensions param color. * Parse colors using new parsers in internal extensions. * Replace color average or blending with colors/utils average color methods * Use color module in swatches and palettes for parsing out any color needed * Use of HSL space inside color object instead of converting to and fro in color manipulation code * Use average_color_between for all color dividers such as gradient-dragging and desktop-style * Focus all uses of cairo context setting through display/cairo-utils.cpp color setting functions * Use a standard toColorRef function in emf/wmf extensions to convert colors. * Generate base64 from icc profiles found in pdf files using the cms object * Document properties to use the internal ColorProfile generator instead of it's own xml code * Convert tool and desktop prefs color from guint32 to optional Color objects for preference setting/getting * Use util Colors::lightness instead of SP_RGBA32_LUMINANCE * Fix TODO issue with selected color not calling update when dragging had finished * Move color wheels to use color objects and tie in where possible. * ColorPicker is now a derived widget instead of being a button in a button Other things: * Add `sp_repr_css_set_property_string` to xml/repr.h to handle setting std::string * Add gtk adjustment functions to ui/util.cpp to replace color-scale specific code * Give SPIScale24 a setter and getter for double instead of requiring the use of SP_SCALE24_TO/FROM_FLOAT at every use. * Removed a useless _cmp method in document-properties --- po/POTFILES.src.in | 3 - src/CMakeLists.txt | 11 - src/attributes.cpp | 2 +- src/attributes.h | 2 +- src/color-rgba.h | 173 ---- src/color.cpp | 568 ----------- src/color.h | 116 --- src/color/CMakeLists.txt | 19 - src/color/cms-color-types.h | 72 -- src/color/cms-system.cpp | 470 --------- src/color/cms-system.h | 132 --- src/color/cms-util.cpp | 123 --- src/color/cms-util.h | 56 -- src/color/cmyk-conv.cpp | 93 -- src/color/cmyk-conv.h | 41 - src/color/color-conv.cpp | 30 - src/color/color-conv.h | 24 - src/color/color-profile-cms-fns.h | 58 -- src/colorspace.h | 51 - src/desktop-style.cpp | 136 +-- src/desktop-style.h | 15 +- src/desktop.cpp | 1 - src/display/cairo-utils.cpp | 110 +-- src/display/cairo-utils.h | 18 +- src/display/control/canvas-item-bpath.cpp | 1 - src/display/control/canvas-item-curve.cpp | 5 +- src/display/control/canvas-item-grid.cpp | 6 +- src/display/control/canvas-item-grid.h | 6 - src/display/control/canvas-item-quad.cpp | 8 +- src/display/control/canvas-item-rect.cpp | 1 - src/display/control/canvas-item-text.cpp | 10 +- src/display/control/canvas-page.cpp | 24 +- src/display/control/canvas-page.h | 15 +- src/display/control/ctrl-handle-rendering.cpp | 15 +- src/display/drawing-paintserver.cpp | 13 +- src/display/drawing-paintserver.h | 11 +- src/display/drawing.cpp | 6 +- src/display/nr-filter-diffuselighting.cpp | 9 - src/display/nr-filter-diffuselighting.h | 6 - src/display/nr-filter-flood.cpp | 18 +- src/display/nr-filter-flood.h | 4 - src/display/nr-filter-specularlighting.cpp | 10 - src/display/nr-filter-specularlighting.h | 4 - src/display/nr-light.cpp | 2 +- src/display/nr-style.cpp | 18 +- src/display/nr-style.h | 7 +- src/document.cpp | 9 +- src/document.h | 12 +- src/doxygen-main.dox | 1 - src/extension/extension.cpp | 8 +- src/extension/extension.h | 8 +- src/extension/internal/bitmap/colorize.cpp | 22 +- src/extension/internal/bitmap/colorize.h | 11 +- .../internal/cairo-render-context.cpp | 22 +- src/extension/internal/emf-inout.cpp | 100 +- src/extension/internal/emf-print.cpp | 58 +- src/extension/internal/filter/bevels.h | 51 +- src/extension/internal/filter/blurs.h | 14 +- src/extension/internal/filter/bumps.h | 55 +- src/extension/internal/filter/color.h | 131 +-- src/extension/internal/filter/morphology.h | 14 +- src/extension/internal/filter/overlays.h | 15 +- src/extension/internal/filter/paint.h | 63 +- src/extension/internal/filter/shadows.h | 12 +- src/extension/internal/filter/transparency.h | 14 +- src/extension/internal/gimpgrad.cpp | 26 +- src/extension/internal/latex-pstricks.cpp | 10 +- .../internal/latex-text-renderer.cpp | 37 +- src/extension/internal/metafile-inout.h | 10 + src/extension/internal/metafile-print.cpp | 24 +- src/extension/internal/metafile-print.h | 4 +- src/extension/internal/odf.cpp | 13 +- .../internal/pdfinput/svg-builder.cpp | 20 +- src/extension/internal/pov-out.cpp | 30 +- src/extension/internal/wmf-inout.cpp | 81 +- src/extension/internal/wmf-print.cpp | 49 +- src/extension/prefdialog/parameter-color.cpp | 73 +- src/extension/prefdialog/parameter-color.h | 16 +- src/extension/prefdialog/parameter.cpp | 6 +- src/extension/prefdialog/parameter.h | 8 +- src/gradient-chemistry.cpp | 142 +-- src/gradient-chemistry.h | 12 +- src/gradient-drag.cpp | 72 +- src/gradient-drag.h | 5 +- src/hsluv.cpp | 506 ---------- src/hsluv.h | 174 ---- src/id-clash.cpp | 4 +- src/inkscape.cpp | 2 - src/inkscape.h | 2 - src/io/file-export-cmd.cpp | 22 +- src/layer-manager.cpp | 2 + src/live_effects/fill-conversion.cpp | 22 +- src/live_effects/lpe-measure-segments.cpp | 59 +- src/live_effects/lpe-measure-segments.h | 2 +- src/live_effects/lpe-powermask.cpp | 28 +- src/live_effects/lpe-powermask.h | 2 +- src/live_effects/parameter/colorpicker.cpp | 49 +- src/live_effects/parameter/colorpicker.h | 10 +- src/object/color-profile.cpp | 3 +- src/object/color-profile.h | 31 - src/object/filters/diffuselighting.cpp | 37 +- src/object/filters/diffuselighting.h | 8 +- src/object/filters/flood.cpp | 38 +- src/object/filters/flood.h | 5 +- src/object/filters/sp-filter-primitive.h | 3 + src/object/filters/specularlighting.cpp | 33 +- src/object/filters/specularlighting.h | 8 +- src/object/sp-gradient-vector.h | 5 +- src/object/sp-gradient.cpp | 30 +- src/object/sp-grid.cpp | 61 +- src/object/sp-grid.h | 13 +- src/object/sp-guide.cpp | 9 +- src/object/sp-image.cpp | 81 +- src/object/sp-item-group.cpp | 4 +- src/object/sp-item-group.h | 2 +- src/object/sp-item.cpp | 26 +- src/object/sp-item.h | 8 +- src/object/sp-mesh-array.cpp | 194 +--- src/object/sp-mesh-array.h | 13 +- src/object/sp-mesh-gradient.cpp | 8 +- src/object/sp-namedview.cpp | 107 +- src/object/sp-namedview.h | 21 +- src/object/sp-object.cpp | 4 +- src/object/sp-pattern.cpp | 2 +- src/object/sp-solid-color.cpp | 4 +- src/object/sp-solid-color.h | 1 - src/object/sp-stop.cpp | 33 +- src/object/sp-stop.h | 13 +- src/oklab.cpp | 466 --------- src/oklab.h | 150 --- src/page-manager.cpp | 47 +- src/page-manager.h | 21 +- src/pattern-manager.cpp | 2 +- src/pattern-manipulation.cpp | 8 +- src/pattern-manipulation.h | 6 +- src/preferences.cpp | 45 +- src/preferences.h | 24 +- src/profile-manager.cpp | 102 -- src/profile-manager.h | 55 -- src/selection-chemistry.cpp | 19 +- src/style-internal.cpp | 195 ++-- src/style-internal.h | 118 +-- src/style.cpp | 42 +- src/style.h | 12 +- src/svg/CMakeLists.txt | 3 - src/svg/svg-color.cpp | 669 ------------- src/svg/svg-color.h | 26 - src/trace/depixelize/inkscape-depixelize.cpp | 14 +- src/ui/CMakeLists.txt | 4 - src/ui/builder-utils.cpp | 10 + src/ui/builder-utils.h | 2 + src/ui/clipboard.cpp | 42 +- src/ui/cursor-utils.cpp | 42 +- src/ui/cursor-utils.h | 15 +- src/ui/dialog/about.cpp | 17 +- src/ui/dialog/clonetiler.cpp | 93 +- src/ui/dialog/clonetiler.h | 2 +- src/ui/dialog/color-item.cpp | 84 +- src/ui/dialog/color-item.h | 16 +- src/ui/dialog/document-properties.cpp | 132 +-- src/ui/dialog/document-properties.h | 4 +- src/ui/dialog/document-resources.cpp | 51 +- src/ui/dialog/export-batch.cpp | 15 +- src/ui/dialog/export-batch.h | 3 +- src/ui/dialog/export-single.cpp | 16 +- src/ui/dialog/export-single.h | 3 +- src/ui/dialog/export.cpp | 15 +- src/ui/dialog/export.h | 7 +- src/ui/dialog/filter-effects-dialog.cpp | 28 +- src/ui/dialog/global-palettes.cpp | 218 ++-- src/ui/dialog/global-palettes.h | 54 +- src/ui/dialog/inkscape-preferences.cpp | 150 ++- src/ui/dialog/layer-properties.cpp | 3 +- src/ui/dialog/livepatheffect-editor.cpp | 9 +- src/ui/dialog/object-properties.cpp | 8 +- src/ui/dialog/object-properties.h | 2 +- src/ui/dialog/objects.cpp | 13 +- src/ui/dialog/print.cpp | 8 +- src/ui/dialog/startup.cpp | 24 +- src/ui/dialog/styledialog.cpp | 12 +- src/ui/dialog/swatches.cpp | 40 +- src/ui/dialog/swatches.h | 10 +- src/ui/drag-and-drop.cpp | 49 +- src/ui/icon-loader.cpp | 18 +- src/ui/selected-color.cpp | 155 --- src/ui/selected-color.h | 100 -- src/ui/svg-renderer.cpp | 27 - src/ui/svg-renderer.h | 11 +- src/ui/syntax.cpp | 1 - src/ui/syntax.h | 2 - src/ui/themes.cpp | 22 +- src/ui/tool/multi-path-manipulator.cpp | 12 +- src/ui/tool/multi-path-manipulator.h | 2 +- src/ui/toolbar/text-toolbar.cpp | 8 +- src/ui/tools/booleans-builder.cpp | 3 +- src/ui/tools/calligraphic-tool.cpp | 6 +- src/ui/tools/dropper-tool.cpp | 101 +- src/ui/tools/dropper-tool.h | 19 +- src/ui/tools/eraser-tool.cpp | 7 +- src/ui/tools/flood-tool.cpp | 25 +- src/ui/tools/freehand-base.cpp | 23 +- src/ui/tools/gradient-tool.cpp | 15 +- src/ui/tools/measure-tool.cpp | 16 +- src/ui/tools/node-tool.cpp | 2 +- src/ui/tools/pen-tool.cpp | 10 +- src/ui/tools/pencil-tool.cpp | 11 +- src/ui/tools/spray-tool.cpp | 73 +- src/ui/tools/tool-base.cpp | 18 +- src/ui/tools/tweak-tool.cpp | 150 +-- src/ui/util.cpp | 38 + src/ui/util.h | 10 + src/ui/widget/canvas-grid.cpp | 2 +- src/ui/widget/canvas.cpp | 22 +- src/ui/widget/canvas.h | 7 +- src/ui/widget/canvas/graphics.cpp | 10 +- src/ui/widget/canvas/util.cpp | 12 +- src/ui/widget/canvas/util.h | 11 +- src/ui/widget/color-entry.cpp | 88 +- src/ui/widget/color-entry.h | 14 +- src/ui/widget/color-icc-selector.cpp | 933 ------------------ src/ui/widget/color-icc-selector.h | 72 -- src/ui/widget/color-notebook.cpp | 42 +- src/ui/widget/color-notebook.h | 12 +- src/ui/widget/color-picker.cpp | 119 +-- src/ui/widget/color-picker.h | 53 +- src/ui/widget/color-preview.cpp | 14 +- src/ui/widget/color-scales.cpp | 537 +++------- src/ui/widget/color-scales.h | 26 +- src/ui/widget/color-slider.cpp | 9 +- src/ui/widget/fill-style.cpp | 31 +- src/ui/widget/fill-style.h | 1 + src/ui/widget/gradient-editor.cpp | 36 +- src/ui/widget/gradient-editor.h | 6 +- src/ui/widget/gradient-image.cpp | 6 +- src/ui/widget/gradient-vector-selector.cpp | 10 +- src/ui/widget/gradient-with-stops.cpp | 15 +- src/ui/widget/gradient-with-stops.h | 3 +- src/ui/widget/ink-color-wheel.cpp | 421 ++------ src/ui/widget/ink-color-wheel.h | 80 +- src/ui/widget/layer-selector.cpp | 6 +- src/ui/widget/objects-dialog-cells.cpp | 7 +- src/ui/widget/oklab-color-wheel.cpp | 96 +- src/ui/widget/oklab-color-wheel.h | 7 +- src/ui/widget/page-properties.cpp | 47 +- src/ui/widget/page-properties.h | 5 +- src/ui/widget/paint-selector.cpp | 109 +- src/ui/widget/paint-selector.h | 20 +- src/ui/widget/pattern-editor.cpp | 24 +- src/ui/widget/pattern-editor.h | 12 +- src/ui/widget/pattern-store.h | 4 +- src/ui/widget/preferences-widget.cpp | 11 +- src/ui/widget/preferences-widget.h | 6 +- src/ui/widget/registered-widget.cpp | 27 +- src/ui/widget/registered-widget.h | 8 +- src/ui/widget/selected-style.cpp | 218 +--- src/ui/widget/selected-style.h | 10 +- src/ui/widget/shapeicon.cpp | 1 - src/ui/widget/stroke-style.cpp | 1 - src/ui/widget/style-swatch.cpp | 9 +- src/ui/widget/swatch-selector.cpp | 14 +- src/ui/widget/swatch-selector.h | 6 +- src/util/object-renderer.cpp | 10 +- src/util/object-renderer.h | 6 + src/widgets/CMakeLists.txt | 2 - src/widgets/paintdef.cpp | 191 ---- src/widgets/paintdef.h | 98 -- src/xml/repr-css.cpp | 8 + src/xml/repr.h | 1 + testfiles/CMakeLists.txt | 3 +- .../export-area-drawing_expected.eps | 631 ++++++------ .../export-area-drawing_expected.pdf | Bin 1463 -> 1475 bytes .../export-area-drawing_expected.png | Bin 13995 -> 13918 bytes .../testcases/export-area-drawing_expected.ps | 635 ++++++------ .../testcases/export-area-page_expected.pdf | Bin 1350 -> 1484 bytes .../testcases/export-area-page_expected.png | Bin 15117 -> 15343 bytes .../testcases/export-area-page_expected.ps | 632 ++++++------ .../testcases/export-area_expected.png | Bin 5778 -> 5863 bytes .../testcases/export-dpi_expected.eps | 359 +++---- .../testcases/export-dpi_expected.png | Bin 763 -> 771 bytes .../testcases/export-dpi_expected.ps | 369 +++---- .../testcases/export-id_expected.svg | 151 ++- .../export-id_export-id-only_expected.svg | 61 +- ...t-id-only_export-area-drawing_expected.svg | 61 +- .../export-png-color-mode-gray-8_expected.png | Bin 6347 -> 6388 bytes .../export-png-color-mode-rgb-8_expected.png | Bin 11485 -> 11590 bytes .../export-png-color-mode-rgba-8_expected.png | Bin 15278 -> 15343 bytes testfiles/src/object-style-test.cpp | 9 +- testfiles/src/oklab-color-test.cpp | 171 ---- testfiles/src/style-elem-test.cpp | 4 +- testfiles/src/style-internal-test.cpp | 10 + testfiles/src/style-test.cpp | 4 +- testfiles/src/svg-color-test.cpp | 112 --- testfiles/src/ui-util-test.cpp | 44 + 293 files changed, 4140 insertions(+), 12252 deletions(-) delete mode 100644 src/color-rgba.h delete mode 100644 src/color.cpp delete mode 100644 src/color.h delete mode 100644 src/color/CMakeLists.txt delete mode 100644 src/color/cms-color-types.h delete mode 100644 src/color/cms-system.cpp delete mode 100644 src/color/cms-system.h delete mode 100644 src/color/cms-util.cpp delete mode 100644 src/color/cms-util.h delete mode 100644 src/color/cmyk-conv.cpp delete mode 100644 src/color/cmyk-conv.h delete mode 100644 src/color/color-conv.cpp delete mode 100644 src/color/color-conv.h delete mode 100644 src/color/color-profile-cms-fns.h delete mode 100644 src/colorspace.h delete mode 100644 src/hsluv.cpp delete mode 100644 src/hsluv.h delete mode 100644 src/oklab.cpp delete mode 100644 src/oklab.h delete mode 100644 src/profile-manager.cpp delete mode 100644 src/profile-manager.h delete mode 100644 src/svg/svg-color.cpp delete mode 100644 src/svg/svg-color.h delete mode 100644 src/ui/selected-color.cpp delete mode 100644 src/ui/selected-color.h delete mode 100644 src/ui/widget/color-icc-selector.cpp delete mode 100644 src/ui/widget/color-icc-selector.h delete mode 100644 src/widgets/paintdef.cpp delete mode 100644 src/widgets/paintdef.h delete mode 100644 testfiles/src/oklab-color-test.cpp delete mode 100644 testfiles/src/svg-color-test.cpp create mode 100644 testfiles/src/ui-util-test.cpp diff --git a/po/POTFILES.src.in b/po/POTFILES.src.in index 8a8393a76a..61123a4e66 100644 --- a/po/POTFILES.src.in +++ b/po/POTFILES.src.in @@ -41,7 +41,6 @@ ${_build_dir}/share/templates/templates.h ../src/actions/actions-view-window.cpp ../src/actions/actions-window.cpp ../src/auto-save.cpp -../src/color/cms-util.cpp ../src/colors/spaces/components.cpp ../src/context-fns.cpp ../src/desktop-events.cpp @@ -392,7 +391,6 @@ ${_build_dir}/share/templates/templates.h ../src/ui/util.cpp ../src/ui/widget/canvas-grid.cpp ../src/ui/widget/color-entry.cpp -../src/ui/widget/color-icc-selector.cpp ../src/ui/widget/color-notebook.cpp ../src/ui/widget/color-palette.cpp ../src/ui/widget/color-scales.cpp @@ -432,5 +430,4 @@ ${_build_dir}/share/templates/templates.h ../src/util/font-collections.h ../src/util/paper.cpp ../src/vanishing-point.cpp -../src/widgets/paintdef.cpp ../src/widgets/sp-attribute-widget.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5a84fb6664..a721d0e302 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -12,8 +12,6 @@ set(inkscape_SRC attributes.cpp auto-save.cpp axis-manip.cpp - hsluv.cpp - color.cpp composite-undo-stack-observer.cpp conditions.cpp conn-avoid-ref.cpp @@ -50,14 +48,12 @@ set(inkscape_SRC mod360.cpp object-hierarchy.cpp object-snapper.cpp - oklab.cpp page-manager.cpp path-chemistry.cpp path-prefix.cpp perspective-line.cpp preferences.cpp print.cpp - profile-manager.cpp proj_pt.cpp pure-transform.cpp rdf.cpp @@ -94,10 +90,6 @@ set(inkscape_SRC auto-save.h axis-manip.h bad-uri-exception.h - color-rgba.h - hsluv.h - color.h - colorspace.h composite-undo-stack-observer.h conditions.h conn-avoid-ref.h @@ -139,7 +131,6 @@ set(inkscape_SRC number-opt-number.h object-hierarchy.h object-snapper.h - oklab.h page-manager.h path-chemistry.h path-prefix.h @@ -151,7 +142,6 @@ set(inkscape_SRC preferences-skeleton.h preferences.h print.h - profile-manager.h proj_pt.h pure-transform.h rdf.h @@ -309,7 +299,6 @@ list(APPEND inkscape_SRC # these call add_inkscape_source add_subdirectory(actions) add_subdirectory(async) -add_subdirectory(color) add_subdirectory(colors) add_subdirectory(debug) add_subdirectory(display) diff --git a/src/attributes.cpp b/src/attributes.cpp index 55d0746213..99657120d1 100644 --- a/src/attributes.cpp +++ b/src/attributes.cpp @@ -130,7 +130,7 @@ static SPStyleProp const props[] = { {SPAttr::INKSCAPE_DOCUMENT_UNITS, "inkscape:document-units"}, // This setting sets the Display units, *not* the units used in SVG {SPAttr::INKSCAPE_LOCKGUIDES, "inkscape:lockguides"}, {SPAttr::UNITS, "units"}, - /* SPColorProfile */ + /* ColorProfile */ {SPAttr::LOCAL, "local"}, {SPAttr::NAME, "name"}, {SPAttr::RENDERING_INTENT, "rendering-intent"}, diff --git a/src/attributes.h b/src/attributes.h index 4c204b98fe..f42ab8a935 100644 --- a/src/attributes.h +++ b/src/attributes.h @@ -129,7 +129,7 @@ enum class SPAttr { INKSCAPE_DOCUMENT_UNITS, INKSCAPE_LOCKGUIDES, UNITS, - /* SPColorProfile */ + /* ColorProfile */ LOCAL, NAME, RENDERING_INTENT, diff --git a/src/color-rgba.h b/src/color-rgba.h deleted file mode 100644 index a48d1e4e6d..0000000000 --- a/src/color-rgba.h +++ /dev/null @@ -1,173 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - Authors: - bulia byak - - Copyright (C) 2004 Authors - - Released under GNU GPL v2+, read the file 'COPYING' for more information. -*/ -#ifndef SEEN_COLOR_RGBA_H -#define SEEN_COLOR_RGBA_H - -#include -#include - -/** - * A class to contain a floating point RGBA color as one unit. - */ -class ColorRGBA { -public: - - /** - * A constructor to create the color from four floating point values. - * Load the values into the array of floats in this object. - * - * @param c0 Red - * @param c1 Green - * @param c2 Blue - * @param c3 Alpha - */ - ColorRGBA(float c0, float c1, float c2, float c3) - { - _c[0] = c0; _c[1] = c1; - _c[2] = c2; _c[3] = c3; - } - - /** - * Create a quick ColorRGBA with all zeros. - */ - ColorRGBA() : _c{0.0,0.0,0.0,0.0} - { - } - - /** - * A constructor to create the color from an unsigned int, as found everywhere when dealing with colors. - * - * Separate the values and load them into the array of floats in this object. - * @param intcolor rgba32 "unsigned int representation (0xRRGGBBAA) - */ - ColorRGBA(unsigned int intcolor) - { - _c[0] = ((intcolor & 0xff000000) >> 24) / 255.0; - _c[1] = ((intcolor & 0x00ff0000) >> 16) / 255.0; - _c[2] = ((intcolor & 0x0000ff00) >> 8) / 255.0; - _c[3] = ((intcolor & 0x000000ff) >> 0) / 255.0; - - } - - /** - * Create a ColorRGBA using an array of floats. - * - * Go through each entry in the array and put it into \c _c. - * - * @param in_array The values to be placed into the object - */ - ColorRGBA(float in_array[4]) - { - for (int i = 0; i < 4; i++) - _c[i] = in_array[i]; - } - - /** - * Overwrite the values in this object with another \c ColorRGBA. - * Copy all the values from \c m into \c this. - * - * @param m Values to use for the array. - * @return This ColorRGBA object. - */ - ColorRGBA &operator=(ColorRGBA const &m) { - for (unsigned i = 0 ; i < 4 ; ++i) { - _c[i] = m._c[i]; - } - return *this; - } - - /** - * Grab a particular value from the ColorRGBA object. - * First checks to make sure that the value is within the array, - * and then return the value if it is. - * - * @param i Which value to grab. - * @return The requested value. - */ - float operator[](unsigned int const i) const { - assert( unsigned(i) < 4 ); - return _c[i]; - } - - /** - * Check to ensure that two \c ColorRGBA's are equal. - * Check each value to see if they are equal. If they all are, - * return true. - * - * @param other The guy to check against. - * @return Whether or not they are equal. - */ - bool operator== (const ColorRGBA &other) const { - for (int i = 0; i < 4; i++) { - if (_c[i] != other[i]) - return false; - } - return true; - } - - bool operator!=(ColorRGBA const &o) const { - return !(*this == o); - } - - /** - * Average two \c ColorRGBAs to create another one. - * This function goes through all the points in the two objects and - * merges them together based on the weighting. The current objects - * value are multiplied by 1.0 - weight and the second object by weight. - * This means that they should always be balanced by the parameter. - * - * @param second The second RGBA, with this being the first - * @param weight How much of each should be used. Zero is all - * this while one is all the second. Default is - * half and half. - */ - ColorRGBA average (const ColorRGBA &second, const float weight = 0.5) const { - float returnval[4]; - - for (int i = 0; i < 4; i++) { - returnval[i] = _c[i] * (1.0 - weight) + second[i] * weight; - } - - return ColorRGBA(returnval[0], returnval[1], returnval[2], returnval[3]); - } - - /** - * Give the rgba32 "unsigned int" representation of the color. - * round each components*255 and combine them (RRGGBBAA). - * WARNING : this reduces color precision (from float to 0->255 int per component) - * but it should be expected since we request this kind of output - */ - unsigned int getIntValue() const { - - return - (int(round(_c[0]*255)) << 24) | - (int(round(_c[1]*255)) << 16) | - (int(round(_c[2]*255)) << 8) | - (int(round(_c[3]*255))); - } - -private: - /** Array of values that are stored. */ - float _c[4]; -}; - - -#endif // SEEN_COLOR_RGBA_H - -/* - Local Variables: - mode:c++ - c-file-style:"stroustrup" - c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) - indent-tabs-mode:nil - fill-column:99 - End: -*/ -// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/color.cpp b/src/color.cpp deleted file mode 100644 index f516161a8a..0000000000 --- a/src/color.cpp +++ /dev/null @@ -1,568 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * Colors. - * - * Author: - * Lauris Kaplinski - * bulia byak - * Jon A. Cruz - * - * Copyright (C) 2001-2002 Lauris Kaplinski - * Copyright (C) 2001 Ximian, Inc. - * - * Released under GNU GPL v2+, read the file 'COPYING' for more information. - */ - -#include -#include -#include -#include -#include - -#include "color.h" -#include "hsluv.h" -#include "strneq.h" -#include "svg/svg-color.h" - -#include "svg/css-ostringstream.h" -#include "object/color-profile.h" - -#define return_if_fail(x) if (!(x)) { printf("assertion failed: " #x); return; } -#define return_val_if_fail(x, val) if (!(x)) { printf("assertion failed: " #x); return val; } - -using Inkscape::CSSOStringStream; - -static bool profileMatches(SVGICCColor const &first, SVGICCColor const &second); - -static constexpr double PROFILE_EPSILON = 1e-8; - -SPColor::SPColor(SPColor const &other) -{ - *this = other; -} - -SPColor::SPColor(float r, float g, float b) -{ - set( r, g, b ); -} - -SPColor::SPColor(guint32 value) -{ - set( value ); -} - -SPColor &SPColor::operator=(SPColor const &other) -{ - if (this == &other){ - return *this; - } - - set(other.v.c[0], other.v.c[1], other.v.c[2]); - copyColors(other); - return *this; -} - -/** - * Returns true if colors match. - */ -bool SPColor::operator == (SPColor const& other) const -{ - bool match = - (v.c[0] == other.v.c[0]) && - (v.c[1] == other.v.c[1]) && - (v.c[2] == other.v.c[2]); - - match &= profileMatches(_icc, other._icc); - - return match; -} - -/** - * Returns true if no RGB value differs epsilon or more in both colors, - * false otherwise. - */ -bool SPColor::isClose(SPColor const& other, float epsilon) const -{ - bool match = (fabs((v.c[0]) - (other.v.c[0])) < epsilon) - && (fabs((v.c[1]) - (other.v.c[1])) < epsilon) - && (fabs((v.c[2]) - (other.v.c[2])) < epsilon); - - match &= profileMatches(_icc, other._icc); - - return match; -} - -/** - * Matches two profile colors within PROFILE_EPSILON distance. - */ -static bool profileMatches(SVGICCColor const &first, SVGICCColor const &second) -{ - if (first.colorProfile != second.colorProfile || first.colors.size() != second.colors.size()) { - return false; - } - - for (unsigned i = 0; i < first.colors.size(); i++) { - if (fabs(first.colors[i] - second.colors[i]) > PROFILE_EPSILON) { - return false; - } - } - return true; -} - -/** - * Sets RGB values and colorspace in color. - * \pre 0 <={r,g,b}<=1 - */ -void SPColor::set(float r, float g, float b) -{ - return_if_fail(r >= 0.0); - return_if_fail(r <= 1.0); - return_if_fail(g >= 0.0); - return_if_fail(g <= 1.0); - return_if_fail(b >= 0.0); - return_if_fail(b <= 1.0); - - v.c[0] = r; - v.c[1] = g; - v.c[2] = b; - - // Remove icc colors, but not the profile - unsetColors(); -} - -/** - * Converts 32bit value to RGB floats and sets color. - */ -void SPColor::set(guint32 value) -{ - v.c[0] = (value >> 24) / 255.0F; - v.c[1] = ((value >> 16) & 0xff) / 255.0F; - v.c[2] = ((value >> 8) & 0xff) / 255.0F; - // Remove icc colors, but not the profile - unsetColors(); -} - -/** - * Returns true if this color has a color profile set. - */ -bool SPColor::hasColorProfile() const -{ - return !_icc.colorProfile.empty(); -} - -/** - * Returns true if there is a defined color for the set profile. - */ -bool SPColor::hasColors() const -{ - return hasColorProfile() && !_icc.colors.empty() && _icc.colors[0] != -1.0; -} - -void SPColor::setColorProfile(Inkscape::ColorProfile *profile) -{ - unsetColorProfile(); - if (profile) { - _icc.colorProfile = profile->name; - for (int i = 0; i < profile->getChannelCount(); i++) { - _icc.colors.emplace_back(-1.0); - } - } -} - -void SPColor::setColors(std::vector &&values) -{ - if (values.size() != _icc.colors.size()) { - g_error("Can't set profile-based color, wrong number of colors."); - unsetColors(); - return; - } - _icc.colors = std::move(values); -} - -void SPColor::copyColors(const SPColor &other) -{ - if (!profileMatches(_icc, other._icc)) { - _icc = other._icc; // copy - } -} - -void SPColor::setColor(unsigned int index, double value) -{ - if (index < 0 || index > _icc.colors.size()) { - g_warning("Can't set profile-based color, index out of range."); - } - _icc.colors[index] = value; -} - -/** - * Clears the saved profile color, but retains the color profile for imediate reuse. - */ -void SPColor::unsetColors() -{ - for (double &color : _icc.colors) { - color = -1.0; - } -} - -/** - * Remove the color profile and save color, reverting to sRGB only. - */ -void SPColor::unsetColorProfile() -{ - _icc.colorProfile = ""; - _icc.colors.clear(); -} - -/** - * Convert SPColor with integer alpha value to 32bit RGBA value. - * \pre alpha < 256 - */ -guint32 SPColor::toRGBA32( int alpha ) const -{ - return_val_if_fail (alpha <= 0xff, 0x0); - - if (!is_set()) { - return SP_RGBA32_U_COMPOSE(0, 0, 0, alpha); - } - - guint32 rgba = SP_RGBA32_U_COMPOSE( SP_COLOR_F_TO_U(v.c[0]), - SP_COLOR_F_TO_U(v.c[1]), - SP_COLOR_F_TO_U(v.c[2]), - alpha ); - return rgba; -} - -/** - * Convert SPColor with float alpha value to 32bit RGBA value. - * \pre color != NULL && 0 <= alpha <= 1 - */ -guint32 SPColor::toRGBA32( double alpha ) const -{ - return_val_if_fail(alpha >= 0.0, 0x0); - return_val_if_fail(alpha <= 1.0, 0x0); - - return toRGBA32( static_cast(SP_COLOR_F_TO_U(alpha)) ); -} - -std::string SPColor::toString() const -{ - CSSOStringStream css; - char tmp[64] = {0}; - - sp_svg_write_color(tmp, sizeof(tmp), toRGBA32(0x0ff)); - css << tmp; - - if (hasColors()) { - if ( !css.str().empty() ) { - css << " "; - } - css << "icc-color(" << _icc.colorProfile; - for (double color : _icc.colors) { - css << ", " << color; - } - css << ')'; - } - - return css.str(); -} - -/** - * Set the color from the given string. - * - * @param str - The CSS3 formatted string input (see toString) - * @returns true if the color was parsed correctly. - */ -bool SPColor::fromString(char const *str) -{ - guint32 const rgb0 = sp_svg_read_color(str, &str, 0xff); - if (rgb0 == 0xff) { - return false; - } - set(rgb0); - while (g_ascii_isspace(*str)) { - ++str; - } - if (strneq(str, "icc-color(", 10)) { - if (!sp_svg_read_icc_color(str, &str, &_icc)) { - g_warning("Couldn't parse icc-color format in css."); - unsetColorProfile(); - } - } - return true; -} - -/** - * Fill rgb float array with values from SPColor. - * \pre color != NULL && rgb != NULL && rgb[0-2] is meaningful - */ -void -SPColor::get_rgb_floatv(float *rgb) const -{ - return_if_fail (rgb != nullptr); - if (!is_set()) { - return; - } - - rgb[0] = v.c[0]; - rgb[1] = v.c[1]; - rgb[2] = v.c[2]; -} - -/** - * Fill cmyk float array with values from SPColor. - * \pre color != NULL && cmyk != NULL && cmyk[0-3] is meaningful - */ -void -SPColor::get_cmyk_floatv(float *cmyk) const -{ - return_if_fail (cmyk != nullptr); - if (!is_set()) { - return; - } - - SPColor::rgb_to_cmyk_floatv( cmyk, - v.c[0], - v.c[1], - v.c[2] ); -} - -/* Plain mode helpers */ - -/** - * Fill hsv float array from r,g,b float values. - */ -void -SPColor::rgb_to_hsv_floatv (float *hsv, float r, float g, float b) -{ - float max, min, delta; - - max = MAX (MAX (r, g), b); - min = MIN (MIN (r, g), b); - delta = max - min; - - hsv[2] = max; - - if (max > 0) { - hsv[1] = delta / max; - } else { - hsv[1] = 0.0; - } - - if (hsv[1] != 0.0) { - if (r == max) { - hsv[0] = (g - b) / delta; - } else if (g == max) { - hsv[0] = 2.0 + (b - r) / delta; - } else { - hsv[0] = 4.0 + (r - g) / delta; - } - - hsv[0] = hsv[0] / 6.0; - - if (hsv[0] < 0) hsv[0] += 1.0; - } - else - hsv[0] = 0.0; -} - -/** - * Fill rgb float array from h,s,v float values. - */ -void -SPColor::hsv_to_rgb_floatv (float *rgb, float h, float s, float v) -{ - double f, w, q, t, d; - - d = h * 5.99999999; - f = d - floor (d); - w = v * (1.0 - s); - q = v * (1.0 - (s * f)); - t = v * (1.0 - (s * (1.0 - f))); - - if (d < 1.0) { - *rgb++ = v; - *rgb++ = t; - *rgb++ = w; - } else if (d < 2.0) { - *rgb++ = q; - *rgb++ = v; - *rgb++ = w; - } else if (d < 3.0) { - *rgb++ = w; - *rgb++ = v; - *rgb++ = t; - } else if (d < 4.0) { - *rgb++ = w; - *rgb++ = q; - *rgb++ = v; - } else if (d < 5.0) { - *rgb++ = t; - *rgb++ = w; - *rgb++ = v; - } else { - *rgb++ = v; - *rgb++ = w; - *rgb++ = q; - } -} - -/** - * Fill hsl float array from r,g,b float values. - */ -void -SPColor::rgb_to_hsl_floatv (float *hsl, float r, float g, float b) -{ - float max = MAX (MAX (r, g), b); - float min = MIN (MIN (r, g), b); - float delta = max - min; - - hsl[2] = (max + min)/2.0; - - if (delta == 0) { - hsl[0] = 0; - hsl[1] = 0; - } else { - if (hsl[2] <= 0.5) - hsl[1] = delta / (max + min); - else - hsl[1] = delta / (2 - max - min); - - if (r == max) hsl[0] = (g - b) / delta; - else if (g == max) hsl[0] = 2.0 + (b - r) / delta; - else if (b == max) hsl[0] = 4.0 + (r - g) / delta; - - hsl[0] = hsl[0] / 6.0; - - if (hsl[0] < 0) hsl[0] += 1; - if (hsl[0] > 1) hsl[0] -= 1; - } -} - -static float -hue_2_rgb (float v1, float v2, float h) -{ - if (h < 0) h += 6.0; - if (h > 6) h -= 6.0; - - if (h < 1) return v1 + (v2 - v1) * h; - if (h < 3) return v2; - if (h < 4) return v1 + (v2 - v1) * (4 - h); - return v1; -} - -/** - * Fill rgb float array from h,s,l float values. - */ -void -SPColor::hsl_to_rgb_floatv (float *rgb, float h, float s, float l) -{ - if (s == 0) { - rgb[0] = l; - rgb[1] = l; - rgb[2] = l; - } else { - float v2; - if (l < 0.5) { - v2 = l * (1 + s); - } else { - v2 = l + s - l*s; - } - float v1 = 2*l - v2; - - rgb[0] = hue_2_rgb (v1, v2, h*6 + 2.0); - rgb[1] = hue_2_rgb (v1, v2, h*6); - rgb[2] = hue_2_rgb (v1, v2, h*6 - 2.0); - } -} - -/** - * Fill cmyk float array from r,g,b float values. - */ -void -SPColor::rgb_to_cmyk_floatv (float *cmyk, float r, float g, float b) -{ - float c, m, y, k, kd; - - c = 1.0 - r; - m = 1.0 - g; - y = 1.0 - b; - k = MIN (MIN (c, m), y); - - c = c - k; - m = m - k; - y = y - k; - - kd = 1.0 - k; - - if (kd > 1e-9) { - c = c / kd; - m = m / kd; - y = y / kd; - } - - cmyk[0] = c; - cmyk[1] = m; - cmyk[2] = y; - cmyk[3] = k; -} - -/** - * Fill rgb float array from c,m,y,k float values. - */ -void -SPColor::cmyk_to_rgb_floatv (float *rgb, float c, float m, float y, float k) -{ - float kd; - - kd = 1.0 - k; - - c = c * kd; - m = m * kd; - y = y * kd; - - c = c + k; - m = m + k; - y = y + k; - - rgb[0] = 1.0 - c; - rgb[1] = 1.0 - m; - rgb[2] = 1.0 - y; -} - -/** - * Fill hsluv float array from r,g,b float values. - */ -void -SPColor::rgb_to_hsluv_floatv(float *hsluv, float r, float g, float b) -{ - auto tmp = Hsluv::rgb_to_hsluv(r, g, b); - tmp[0] /= 360.0; - tmp[1] /= 100.0; - tmp[2] /= 100.0; - for (size_t i : {0, 1, 2}) { - hsluv[i] = std::clamp(tmp[i], 0.0, 1.0); - } -} - -/** - * Fill rgb float array from h,s,l float values. - */ -void -SPColor::hsluv_to_rgb_floatv(float *rgb, float h, float s, float l) -{ - auto tmp = Hsluv::hsluv_to_rgb(h * 360, s * 100, l * 100); - for (size_t i : {0, 1, 2}) { - rgb[i] = tmp[i]; - } -} - -/* - Local Variables: - mode:c++ - c-file-style:"stroustrup" - c-file-offsets:((innamespace . 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/color.h b/src/color.h deleted file mode 100644 index 9a3fa133ff..0000000000 --- a/src/color.h +++ /dev/null @@ -1,116 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -#ifndef SEEN_SP_COLOR_H -#define SEEN_SP_COLOR_H - -/* - * Author: - * Lauris Kaplinski - * bulia byak - * Jon A. Cruz - * Martin Owens - * - * Copyright (C) 2001-2023 AUTHORS - * Copyright (C) 2001 Ximian, Inc. - * - * Released under GNU GPL v2+, read the file 'COPYING' for more information. - */ - -#include - -#include "svg/svg-icc-color.h" - -typedef unsigned int guint32; // uint is guaranteed to hold up to 2^32 − 1 - -/* Useful composition macros */ - -#define SP_RGBA32_R_U(v) (((v) >> 24) & 0xff) -#define SP_RGBA32_G_U(v) (((v) >> 16) & 0xff) -#define SP_RGBA32_B_U(v) (((v) >> 8) & 0xff) -#define SP_RGBA32_A_U(v) ((v) & 0xff) -#define SP_COLOR_U_TO_F(v) ((v) / 255.0) -#define SP_COLOR_F_TO_U(v) ((unsigned int) ((v) * 255. + .5)) -#define SP_RGBA32_R_F(v) SP_COLOR_U_TO_F (SP_RGBA32_R_U (v)) -#define SP_RGBA32_G_F(v) SP_COLOR_U_TO_F (SP_RGBA32_G_U (v)) -#define SP_RGBA32_B_F(v) SP_COLOR_U_TO_F (SP_RGBA32_B_U (v)) -#define SP_RGBA32_A_F(v) SP_COLOR_U_TO_F (SP_RGBA32_A_U (v)) -#define SP_RGBA32_U_COMPOSE(r,g,b,a) ((((r) & 0xff) << 24) | (((g) & 0xff) << 16) | (((b) & 0xff) << 8) | ((a) & 0xff)) -#define SP_RGBA32_F_COMPOSE(r,g,b,a) SP_RGBA32_U_COMPOSE (SP_COLOR_F_TO_U (r), SP_COLOR_F_TO_U (g), SP_COLOR_F_TO_U (b), SP_COLOR_F_TO_U (a)) -#define SP_RGBA32_C_COMPOSE(c,o) SP_RGBA32_U_COMPOSE(SP_RGBA32_R_U(c),SP_RGBA32_G_U(c),SP_RGBA32_B_U(c),SP_COLOR_F_TO_U(o)) -#define SP_RGBA32_LUMINANCE(v) (SP_RGBA32_R_U(v) * 0.30 + SP_RGBA32_G_U(v) * 0.59 + SP_RGBA32_B_U(v) * 0.11 + 0.5) - -struct SVGICCColor; - -namespace Inkscape { - class ColorProfile; -}; - -/** - * An RGB color with optional icc-color part - */ -class SPColor final -{ -public: - SPColor() = default; - SPColor(SPColor const &other); - SPColor(float r, float g, float b); - SPColor(guint32 value); - - SPColor& operator= (SPColor const& other); - - bool operator == ( SPColor const& other ) const; - bool operator != ( SPColor const& other ) const { return !(*this == other); }; - operator bool() const { return is_set(); } - - bool isClose( SPColor const& other, float epsilon ) const; - - void set(float r, float g, float b); - void set(guint32 value); - - bool hasColorProfile() const; - void unsetColorProfile(); - void setColorProfile(Inkscape::ColorProfile *profile); - const std::string &getColorProfile() const { return _icc.colorProfile; } - - bool hasColors() const; - void unsetColors(); - void setColors(std::vector &&values); - void setColor(unsigned int index, double value); - void copyColors(const SPColor &other); - const std::vector &getColors() const { return _icc.colors; } - - guint32 toRGBA32( int alpha ) const; - guint32 toRGBA32( double alpha ) const; - - std::string toString() const; - bool fromString(const char *str); - - union { - float c[3] = { -1, 0, 0 }; - } v; - - guint32 get_rgba32_ualpha (guint32 alpha) const; - guint32 get_rgba32_falpha (float alpha) const; - - bool is_set() const { return v.c[0] > -1; } - void get_rgb_floatv (float *rgb) const; - void get_cmyk_floatv (float *cmyk) const; - - /* Plain mode helpers */ - - static void rgb_to_hsv_floatv (float *hsv, float r, float g, float b); - static void hsv_to_rgb_floatv (float *rgb, float h, float s, float v); - - static void rgb_to_hsl_floatv (float *hsl, float r, float g, float b); - static void hsl_to_rgb_floatv (float *rgb, float h, float s, float l); - - static void rgb_to_cmyk_floatv (float *cmyk, float r, float g, float b); - static void cmyk_to_rgb_floatv (float *rgb, float c, float m, float y, float k); - - static void rgb_to_hsluv_floatv (float *hsluv, float r, float g, float b); - static void hsluv_to_rgb_floatv (float *rgb, float h, float s, float l); - -private: - SVGICCColor _icc; -}; - -#endif // SEEN_SP_COLOR_H diff --git a/src/color/CMakeLists.txt b/src/color/CMakeLists.txt deleted file mode 100644 index 3cdda4a9d8..0000000000 --- a/src/color/CMakeLists.txt +++ /dev/null @@ -1,19 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0-or-later - -set(color_SRC - cms-system.cpp - cms-util.cpp - cmyk-conv.cpp - color-conv.cpp - - # ------- - # Headers - color-profile-cms-fns.h - cms-color-types.h - cms-system.h - cms-util.h - cmyk-conv.h - color-conv.h -) - -add_inkscape_source("${color_SRC}") diff --git a/src/color/cms-color-types.h b/src/color/cms-color-types.h deleted file mode 100644 index a9ee3d880d..0000000000 --- a/src/color/cms-color-types.h +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/** @file - * TODO: insert short description here - *//* - * Authors: see git history - * - * Copyright (C) 2018 Authors - * Released under GNU GPL v2+, read the file 'COPYING' for more information. - */ -#ifndef SEEN_CMS_COLOR_TYPES_H -#define SEEN_CMS_COLOR_TYPES_H - -/** - * @file - * A simple abstraction to provide opaque compatibility with either lcms or lcms2. - */ - -#ifdef HAVE_CONFIG_H -# include "config.h" // only include where actually required! -#endif - -#include // uint8_t, etc - -typedef unsigned int guint32; - -typedef void * cmsHPROFILE; -typedef void * cmsHTRANSFORM; - - -namespace Inkscape { - -/** - * Opaque holder of a 32-bit signature type. - */ -class FourCCSig { -public: - FourCCSig( FourCCSig const &other ) = default; - - FourCCSig( guint32 value ) : value(value) {}; - - guint32 value; -}; - -class ColorSpaceSig : public FourCCSig { -public: - ColorSpaceSig( ColorSpaceSig const &other ) = default; - - ColorSpaceSig( guint32 value ) : FourCCSig(value) {}; -}; - -class ColorProfileClassSig : public FourCCSig { -public: - ColorProfileClassSig( ColorProfileClassSig const &other ) = default; - - ColorProfileClassSig( guint32 value ) : FourCCSig(value) {}; -}; - -} // namespace Inkscape - - -#endif // SEEN_CMS_COLOR_TYPES_H - -/* - Local Variables: - mode:c++ - c-file-style:"stroustrup" - c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) - indent-tabs-mode:nil - fill-column:99 - End: -*/ -// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/color/cms-system.cpp b/src/color/cms-system.cpp deleted file mode 100644 index 2dfb064976..0000000000 --- a/src/color/cms-system.cpp +++ /dev/null @@ -1,470 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/** @file - * A class to provide access to system/user ICC color profiles. - *//* - * Authors: see git history - * - * Copyright (C) 2018 Authors - * Released under GNU GPL v2+, read the file 'COPYING' for more information. - */ -#ifdef HAVE_CONFIG_H -#include "config.h" // only include where actually required! -#endif - -#include "cms-system.h" - -#include - -#include - -#include "cms-util.h" -#include "document.h" -#include "preferences.h" - -#include "io/resource.h" -#include "object/color-profile.h" - -#ifdef _WIN32 -#undef NOGDI -#include -#include -#endif - -using Inkscape::ColorProfile; - -namespace Inkscape { - -/** - * Holds information about one ICC profile. - */ -class ProfileInfo -{ -public: - ProfileInfo(cmsHPROFILE prof, Glib::ustring &&path); - - Glib::ustring const &getName() const { return _name; } - Glib::ustring const &getPath() const { return _path; } - cmsColorSpaceSignature getSpace() const { return _profileSpace; } - cmsProfileClassSignature getClass() const { return _profileClass; } - -private: - Glib::ustring _path; - Glib::ustring _name; - cmsColorSpaceSignature _profileSpace; - cmsProfileClassSignature _profileClass; -}; - -ProfileInfo::ProfileInfo(cmsHPROFILE prof, Glib::ustring &&path) - : _path(std::move(path)) - , _name(get_color_profile_name(prof)) - , _profileSpace(cmsGetColorSpace(prof)) - , _profileClass(cmsGetDeviceClass(prof)) -{ -} - -CMSSystem::CMSSystem() -{ - // Read in profiles (move to refresh()?). - load_profiles(); - - // Create generic sRGB profile. - sRGB_profile = cmsCreate_sRGBProfile(); -} - -CMSSystem::~CMSSystem() -{ - if (current_monitor_profile) { - cmsCloseProfile(current_monitor_profile); - } - - if (current_proof_profile) { - cmsCloseProfile(current_proof_profile); - } - - if (sRGB_profile) { - cmsCloseProfile(sRGB_profile); - } -} - -/* - * We track last transform created so we can delete it later. - * - * This is OK since we only have one transform for all montiors/canvases. If we choose to allow the - * user to assign different profiles to different monitors or have CMS preferences that are not - * global, we'll need to have either one transform per monitor or one transform per canvas. - */ - -// Search for system ICC profile files and add them to list. -void CMSSystem::load_profiles() -{ - system_profile_infos.clear(); // Allows us to refresh list if necessary. - - // Get list of all possible file directories, with flag if they are "home" directories or not. - auto directory_paths = get_directory_paths(); - - // Look for icc files in specified directories. - for (auto const &directory_path : directory_paths) { - using Inkscape::IO::Resource::get_filenames; - for (auto &&filename : get_filenames(directory_path.first, {".icc", ".icm"})) { - // Check if files are ICC files and extract out basic information, add to list. - if (!is_icc_file(filename)) { - std::cerr << "CMSSystem::load_profiles: " << filename << " is not an ICC file!" << std::endl; - continue; - } - - cmsHPROFILE profile = cmsOpenProfileFromFile(filename.c_str(), "r"); - if (!profile) { - std::cerr << "CMSSystem::load_profiles: failed to load " << filename << std::endl; - continue; - } - - ICCProfileInfo info{profile, std::move(filename), directory_path.second}; - cmsCloseProfile(profile); - profile = nullptr; - - bool same_name = false; - for (auto const &profile_info : system_profile_infos) { - if (profile_info.get_name() == info.get_name() ) { - same_name = true; - std::cerr << "CMSSystem::load_profiles: ICC profile with duplicate name: " << profile_info.get_name() << ":" << std::endl; - std::cerr << " " << profile_info.get_path() << std::endl; - std::cerr << " " << info.get_path() << std::endl; - break; - } - } - - if (!same_name) { - system_profile_infos.emplace_back(std::move(info)); - } - } - } -} - -// Create list of all directories where ICC profiles are expected to be found. -std::vector> CMSSystem::get_directory_paths() -{ - std::vector> paths; - - // First try user's local directory. - paths.emplace_back(Glib::build_filename(Glib::get_user_data_dir(), "color", "icc"), true); - - // See https://github.com/hughsie/colord/blob/fe10f76536bb27614ced04e0ff944dc6fb4625c0/lib/colord/cd-icc-store.c#L590 - - // User store - paths.emplace_back(Glib::build_filename(Glib::get_user_data_dir(), "icc"), true); - - paths.emplace_back(Glib::build_filename(Glib::get_home_dir(), ".color", "icc"), true); - - // System store - paths.emplace_back("/var/lib/color/icc", false); - paths.emplace_back("/var/lib/colord/icc", false); - - auto data_directories = Glib::get_system_data_dirs(); - for (auto const &data_directory : data_directories) { - paths.emplace_back(Glib::build_filename(data_directory, "color", "icc"), false); - } - -#ifdef __APPLE__ - paths.emplace_back("/System/Library/ColorSync/Profiles", false); - paths.emplace_back("/Library/ColorSync/Profiles", false); - - paths.emplace_back(Glib::build_filename(Glib::get_home_dir(), "Library", "ColorSync", "Profiles"), true); -#endif // __APPLE__ - -#ifdef _WIN32 - wchar_t pathBuf[MAX_PATH + 1]; - pathBuf[0] = 0; - DWORD pathSize = sizeof(pathBuf); - g_assert(sizeof(wchar_t) == sizeof(gunichar2)); - if (GetColorDirectoryW(NULL, pathBuf, &pathSize)) { - auto utf8Path = g_utf16_to_utf8((gunichar2*)(&pathBuf[0]), -1, NULL, NULL, NULL); - if (!g_utf8_validate(utf8Path, -1, NULL)) { - g_warning( "GetColorDirectoryW() resulted in invalid UTF-8" ); - } else { - paths.emplace_back(utf8Path, false); - } - g_free(utf8Path); - } -#endif // _WIN32 - - return paths; -} - -// Get the user set monitor profile. -cmsHPROFILE CMSSystem::get_monitor_profile() -{ - static Glib::ustring current_monitor_uri; - static bool use_user_monitor_profile_old = false; - - Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - bool use_user_monitor_profile = prefs->getBool("/options/displayprofile/use_user_profile", false); - - if (use_user_monitor_profile_old != use_user_monitor_profile) { - use_user_monitor_profile_old = use_user_monitor_profile; - current_monitor_profile_changed = true; - } - - if (!use_user_monitor_profile) { - if (current_monitor_profile) { - cmsCloseProfile(current_monitor_profile); - current_monitor_profile = nullptr; - current_monitor_uri.clear(); - } - return current_monitor_profile; - } - - Glib::ustring new_uri = prefs->getString("/options/displayprofile/uri"); - - if (!new_uri.empty()) { - - // User defined monitor profile. - if (new_uri != current_monitor_uri) { - - // Monitor profile changed - current_monitor_profile_changed = true; - current_monitor_uri.clear(); - - // Delete old profile - if (current_monitor_profile) { - cmsCloseProfile(current_monitor_profile); - } - - // Open new profile - current_monitor_profile = cmsOpenProfileFromFile(new_uri.data(), "r"); - if (current_monitor_profile) { - - // A display profile must be of the right type. - cmsColorSpaceSignature space = cmsGetColorSpace(current_monitor_profile); - cmsProfileClassSignature profClass = cmsGetDeviceClass(current_monitor_profile); - - if (profClass != cmsSigDisplayClass) { - std::cerr << "CMSSystem::get_monitor_profile: Not a display (monitor) profile: " << new_uri << std::endl; - cmsCloseProfile(current_monitor_profile); - current_monitor_profile = nullptr; - } else if (space != cmsSigRgbData) { - std::cerr << "CMSSystem::get_monitor_profile: Not an RGB profile: " << new_uri << std::endl; - cmsCloseProfile(current_monitor_profile); - current_monitor_profile = nullptr; - } else { - current_monitor_uri = new_uri; - } - } - } - } else if (current_monitor_profile) { - cmsCloseProfile(current_monitor_profile); - current_monitor_profile = nullptr; - current_monitor_uri.clear(); - current_monitor_profile_changed = true; - } - - return current_monitor_profile; -} - -// Get the user set proof profile. -cmsHPROFILE CMSSystem::get_proof_profile() -{ - static Glib::ustring current_proof_uri; - - Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - Glib::ustring new_uri = prefs->getString("/options/softproof/uri"); - - if (!new_uri.empty()) { - - // User defined proof profile. - if (new_uri != current_proof_uri) { - - // Proof profile changed - current_proof_profile_changed = true; - current_proof_uri.clear(); - - // Delete old profile - if (current_proof_profile) { - cmsCloseProfile(current_proof_profile); - } - - // Open new profile - current_proof_profile = cmsOpenProfileFromFile(new_uri.data(), "r"); - if (current_proof_profile) { - - // We don't check validity of proof profile! - current_proof_uri = new_uri; - } - } - } else if (current_proof_profile) { - cmsCloseProfile(current_proof_profile); - current_proof_profile = nullptr; - current_proof_uri.clear(); - current_proof_profile_changed = true; - } - - return current_proof_profile; -} - -// Get a color profile handle corresponding to "name" from the document. Also, optionally, get intent. -cmsHPROFILE CMSSystem::get_document_profile(SPDocument *document, unsigned *intent, char const *name) -{ - cmsHPROFILE profile_handle = nullptr; - - // Search through elements for one with matching name. - ColorProfile *color_profile = nullptr; - auto color_profiles = document->getResourceList("iccprofile"); - for (auto *object : color_profiles) { - if (auto color_profile_test = cast(object)) { - if (color_profile_test->name && (strcmp(color_profile_test->name, name) == 0)) { - color_profile = color_profile_test; - } - } - } - - // If found, set profile_handle pointer. - if (color_profile) { - profile_handle = color_profile->getHandle(); - } - - // If requested, fill "RENDERING_INTENT" value. - if (intent) { - *intent = color_profile ? color_profile->rendering_intent : (guint)RENDERING_INTENT_UNKNOWN; - } - - return profile_handle; -} - -// Returns vector of names to list in Preferences dialog: display (monitor) profiles. -std::vector CMSSystem::get_monitor_profile_names() const -{ - std::vector result; - - for (auto const &profile_info : system_profile_infos) { - if (profile_info.get_profileclass() == cmsSigDisplayClass && - profile_info.get_colorspace() == cmsSigRgbData) - { - result.emplace_back(profile_info.get_name()); - } - } - std::sort(result.begin(), result.end()); - - return result; -} - -// Returns vector of names to list in Preferences dialog: proofing profiles. -std::vector CMSSystem::get_softproof_profile_names() const -{ - std::vector result; - - for (auto const &profile_info : system_profile_infos) { - if (profile_info.get_profileclass() == cmsSigOutputClass) { - result.emplace_back(profile_info.get_name()); - } - } - std::sort(result.begin(), result.end()); - - return result; -} - -// Returns location of a profile. -std::string CMSSystem::get_path_for_profile(Glib::ustring const &name) const -{ - std::string result; - - for (auto const &profile_info : system_profile_infos) { - if (name.raw() == profile_info.get_name()) { - result = profile_info.get_path(); - break; - } - } - - return result; -} - -// Static, doesn't rely on class. Simply calls lcms' cmsDoTransform. -// Called from Canvas and icc_color_to_sRGB in sgv-color.cpp. -void CMSSystem::do_transform(cmsHTRANSFORM transform, unsigned char *inBuf, unsigned char *outBuf, unsigned size) -{ - cmsDoTransform(transform, inBuf, outBuf, size); -} - -// Called by Canvas to obtain transform. -// Currently there is one transform for all monitors. -// Transform immutably shared between CMSSystem and Canvas. -std::shared_ptr const &CMSSystem::get_cms_transform() -{ - bool preferences_changed = false; - - Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - bool warn = prefs->getBool( "/options/softproof/gamutwarn"); - int intent = prefs->getIntLimited("/options/displayprofile/intent", 0, 0, 3); - int proofIntent = prefs->getIntLimited("/options/softproof/intent", 0, 0, 3); - bool bpc = prefs->getBool( "/options/softproof/bpc"); - Glib::ustring colorStr = prefs->getString( "/options/softproof/gamutcolor"); - Gdk::RGBA gamutColor(colorStr.empty() ? "#808080" : colorStr); - - if (gamutWarn != warn || - lastIntent != intent || - lastProofIntent != proofIntent || - lastBPC != bpc || - lastGamutColor != gamutColor ) - { - preferences_changed = true; - - gamutWarn = warn; - lastIntent = intent; - lastProofIntent = proofIntent; - lastBPC = bpc; - lastGamutColor = gamutColor; - } - - auto monitor_profile = get_monitor_profile(); - auto proof_profile = get_proof_profile(); - - bool need_to_update = preferences_changed || current_monitor_profile_changed || current_proof_profile_changed; - - if (need_to_update) { - if (proof_profile) { - cmsUInt32Number dwFlags = cmsFLAGS_SOFTPROOFING; - - if (gamutWarn) { - dwFlags |= cmsFLAGS_GAMUTCHECK; - - auto gamutColor_r = gamutColor.get_red_u(); - auto gamutColor_g = gamutColor.get_green_u(); - auto gamutColor_b = gamutColor.get_blue_u(); - - cmsUInt16Number newAlarmCodes[cmsMAXCHANNELS] = {0}; - newAlarmCodes[0] = gamutColor_r; - newAlarmCodes[1] = gamutColor_g; - newAlarmCodes[2] = gamutColor_b; - newAlarmCodes[3] = ~0; - cmsSetAlarmCodes(newAlarmCodes); - } - - if (bpc) { - dwFlags |= cmsFLAGS_BLACKPOINTCOMPENSATION; - } - - current_transform = CMSTransform::create( - cmsCreateProofingTransform(sRGB_profile, TYPE_BGRA_8, monitor_profile, TYPE_BGRA_8, - proof_profile, intent, proofIntent, dwFlags)); - - } else if (monitor_profile) { - current_transform = CMSTransform::create( - cmsCreateTransform(sRGB_profile, TYPE_BGRA_8, monitor_profile, TYPE_BGRA_8, intent, 0)); - } - } - - return current_transform; -} - -CMSSystem *CMSSystem::_instance = nullptr; - -} // namespace Inkscape - -/* - Local Variables: - mode:c++ - c-file-style:"stroustrup" - c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) - indent-tabs-mode:nil - fill-column:99 - End: -*/ -// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/color/cms-system.h b/src/color/cms-system.h deleted file mode 100644 index a040146eca..0000000000 --- a/src/color/cms-system.h +++ /dev/null @@ -1,132 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/** @file - * TODO: insert short description here - *//* - * Authors: see git history - * - * Copyright (C) 2017 Authors - * Released under GNU GPL v2+, read the file 'COPYING' for more information. - */ -#ifndef INKSCAPE_COLOR_CMS_SYSTEM_H -#define INKSCAPE_COLOR_CMS_SYSTEM_H - -/** \file - * Access to ICC profiles provided by system. - * Track which profile to use for proofing.(?) - * Track which profile to use on which monitor. - */ - -#include -#include -#include - -#include -#include - -#include // cmsHTRANSFORM - -#include "cms-color-types.h" // cmsColorSpaceSignature, cmsProfileClassSignature -#include "cms-util.h" - -class SPDocument; -class ProfileInfo; - -namespace Inkscape { - -class ColorProfile; - -class CMSTransform -{ -public: - explicit CMSTransform(cmsHTRANSFORM handle) : _handle(handle) { assert(_handle); } - CMSTransform(CMSTransform const &) = delete; - CMSTransform &operator=(CMSTransform const &) = delete; - ~CMSTransform() { cmsDeleteTransform(_handle); } - - cmsHTRANSFORM getHandle() const { return _handle; } - - static std::shared_ptr create(cmsHTRANSFORM handle) - { - return handle ? std::make_shared(handle) : nullptr; - } - -private: - cmsHTRANSFORM _handle; -}; - -class CMSSystem -{ -public: - /** - * Access the singleton CMSSystem object. - */ - static CMSSystem *get() { - if (!_instance) { - _instance = new CMSSystem(); - } - return _instance; - } - - static void unload() { - if (_instance) { - delete _instance; - _instance = nullptr; - } - } - - static std::vector> get_directory_paths(); - std::vector const &get_system_profile_infos() const { return system_profile_infos; } - std::vector get_monitor_profile_names() const; - std::vector get_softproof_profile_names() const; - std::string get_path_for_profile(Glib::ustring const &name) const; - std::shared_ptr const &get_cms_transform(); - static cmsHPROFILE get_document_profile(SPDocument *document, unsigned *intent, char const *name); - - static void do_transform(cmsHTRANSFORM transform, unsigned char *inBuf, unsigned char *outBuf, unsigned size); - -private: - CMSSystem(); - ~CMSSystem(); - - void load_profiles(); // Should this be public (e.g., if a new ColorProfile is created). - cmsHPROFILE get_monitor_profile(); // Get the user set monitor profile. - cmsHPROFILE get_proof_profile(); // Get the user set proof profile. - void clear_transform(); // Clears current_transform. - - static CMSSystem* _instance; - - // List of ICC profiles on system - std::vector system_profile_infos; - - // We track last transform settings. If there is a change, we delete create new transform. - bool gamutWarn = false; - Gdk::RGBA lastGamutColor = Gdk::RGBA("#808080"); - bool lastBPC = false; - int lastIntent = INTENT_PERCEPTUAL; - int lastProofIntent = INTENT_PERCEPTUAL; - bool current_monitor_profile_changed = true; // Force at least one update. - bool current_proof_profile_changed = true; - - // Shared immutably with all canvases. - std::shared_ptr current_transform; - - // So we can delete them later. - cmsHPROFILE current_monitor_profile = nullptr; - cmsHPROFILE current_proof_profile = nullptr; - cmsHPROFILE sRGB_profile = nullptr; // Genric sRGB profile, find it once on inititialization. -}; - -} // namespace Inkscape - -#endif // INKSCAPE_COLOR_CMS_SYSTEM_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/color/cms-util.cpp b/src/color/cms-util.cpp deleted file mode 100644 index 36546820b3..0000000000 --- a/src/color/cms-util.cpp +++ /dev/null @@ -1,123 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/** @file - * Utilities to for working with ICC profiles. Used by CMSSystem and ColorProfile classes. - *//* - * Authors: see git history - * - * Copyright (C) 2018 Authors - * Released under GNU GPL v2+, read the file 'COPYING' for more information. - */ - -#include -#include // Debug output -#include -#include // File open flags -#include // Posix read, close. - -#include -#include - -#include "cms-util.h" - -namespace Inkscape { - -ICCProfileInfo::ICCProfileInfo(cmsHPROFILE profile, std::string &&path, bool in_home) - : _path(std::move(path)) - , _in_home(in_home) -{ - assert(profile); - - _name = get_color_profile_name(profile); - _colorspace = cmsGetColorSpace(profile); - _profileclass = cmsGetDeviceClass(profile); -} - -bool is_icc_file(std::string const &filepath) -{ - bool is_icc_file = false; - GStatBuf st; - if (g_stat(filepath.c_str(), &st) == 0 && st.st_size > 128) { - // 0-3 == size - // 36-39 == 'acsp' 0x61637370 - int fd = g_open(filepath.c_str(), O_RDONLY, S_IRWXU); - if (fd != -1) { - guchar scratch[40] = {0}; - size_t len = sizeof(scratch); - - ssize_t got = read(fd, scratch, len); - if (got != -1) { - size_t calcSize = - (scratch[0] << 24) | - (scratch[1] << 16) | - (scratch[2] << 8) | - (scratch[3]); - if ( calcSize > 128 && calcSize <= static_cast(st.st_size) ) { - is_icc_file = - (scratch[36] == 'a') && - (scratch[37] == 'c') && - (scratch[38] == 's') && - (scratch[39] == 'p'); - } - } - close(fd); - - if (is_icc_file) { - cmsHPROFILE profile = cmsOpenProfileFromFile(filepath.c_str(), "r"); - if (profile) { - cmsProfileClassSignature profClass = cmsGetDeviceClass(profile); - if (profClass == cmsSigNamedColorClass) { - is_icc_file = false; // Ignore named color profiles for now. - } - cmsCloseProfile(profile); - } - } - } - } - - return is_icc_file; -} - -std::string get_color_profile_name(cmsHPROFILE profile) -{ - std::string name; - - if (profile) { - cmsUInt32Number byteLen = cmsGetProfileInfoASCII(profile, cmsInfoDescription, "en", "US", nullptr, 0); - if (byteLen > 0) { - std::vector data(byteLen); - cmsUInt32Number readLen = cmsGetProfileInfoASCII(profile, cmsInfoDescription, - "en", "US", - data.data(), data.size()); - if (readLen < data.size()) { - std::cerr << "get_color_profile_name(): read less than expected!" << std::endl; - data.resize(readLen); - } - - // Remove nulls at end which will cause an invalid utf8 string. - while (!data.empty() && data.back() == 0) { - data.pop_back(); - } - - name = std::string(data.begin(), data.end()); - } - - if (name.empty()) { - name = _("(Unnamed)"); - } - } - - return name; -} - -} // namespace Inkscape - -/* - Local Variables: - mode:c++ - c-file-style:"stroustrup" - c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) - indent-tabs-mode:nil - fill-column:99 - End: -*/ -// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/color/cms-util.h b/src/color/cms-util.h deleted file mode 100644 index e9262d9fa6..0000000000 --- a/src/color/cms-util.h +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/** @file - * Utilities to for working with ICC profiles. Used by CMSSystem and ColorProfile classes. - *//* - * Authors: see git history - * - * Copyright (C) 2018 Authors - * Released under GNU GPL v2+, read the file 'COPYING' for more information. - */ - -#ifndef INKSCAPE_COLOR_CMS_UTIL_H -#define INKSCAPE_COLOR_CMS_UTIL_H - -#include - -#include - -namespace Inkscape { - -// Helper class to store info -class ICCProfileInfo -{ -public: - ICCProfileInfo(cmsHPROFILE profile, std::string &&path, bool in_home); - bool operator<(ICCProfileInfo const &other) const; - std::string const &get_path() const { return _path; } - std::string const &get_name() const { return _name; } - cmsColorSpaceSignature get_colorspace() const { return _colorspace; } - cmsProfileClassSignature get_profileclass() const { return _profileclass; } - bool in_home() const { return _in_home; } - -private: - std::string _path; - std::string _name; - bool _in_home; - cmsColorSpaceSignature _colorspace; - cmsProfileClassSignature _profileclass; -}; - -bool is_icc_file(std::string const &filepath); -std::string get_color_profile_name(cmsHPROFILE profile); // Read as ASCII from profile. - -} // namespace Inkscape - -#endif // INKSCAPE_COLOR_CMS_UTIL_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/color/cmyk-conv.cpp b/src/color/cmyk-conv.cpp deleted file mode 100644 index e565bd37f9..0000000000 --- a/src/color/cmyk-conv.cpp +++ /dev/null @@ -1,93 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/** - * CMYK to sRGB conversion routines - * - * Author: - * Michael Kowalski - * - * Copyright (C) 2023 Michael Kowalski - * - * Released under GNU GPL v2+, read the file 'COPYING' for more information. - */ - -#include "cmyk-conv.h" -#include -#include -#include - -namespace Inkscape { - -CmykConverter::CmykConverter(cmsHPROFILE profile, int intent) { - _intent = intent; - auto color_space = cmsGetColorSpace(profile); - if (color_space != cmsSigCmykData && color_space != cmsSigCmyData) { - g_warning("Select CMYK ICC profile to convert CMYK to sRGB"); - return; - } - //todo - if (cmsIsIntentSupported(_profile, intent, LCMS_USED_AS_OUTPUT)) { - } - _profile = profile; - _srgb = cmsCreate_sRGBProfile(); - auto fmt = color_space == cmsSigCmykData ? TYPE_CMYK_16 : TYPE_CMY_16; - _cmy = color_space == cmsSigCmyData; - _transform = cmsCreateTransform(profile, fmt, _srgb, TYPE_RGBA_8, intent, 0); -} - -CmykConverter::~CmykConverter() { - if (_transform) cmsDeleteTransform(_transform); - // _srgb is virtual and probably not something that can be freed -} - -// Simple CMYK to sRGB conversion using interpolation from plain cyan, magenta and yellow colors -std::array simple_cmyk_to_rgb(float c, float m, float y, float k) { - auto invlerp = [](float f1, float p) { - // CSS Color module 5 allows for a wide range of CMYK values, but clamps them to 0%-100% - p = std::clamp(p, 0.0f, 100.0f); - f1 = (255 - f1) / 255.0f; - return 1.0f - (f1 * p / 100.0f); - }; - - // interpolate cyan - auto cr = invlerp(0x00, c); - auto cg = invlerp(0xa4, c); - auto cb = invlerp(0xdb, c); - // magenta - auto mr = invlerp(0xd7, m); - auto mg = invlerp(0x15, m); - auto mb = invlerp(0x7e, m); - // and yellow - auto yr = invlerp(0xff, y); - auto yg = invlerp(0xf1, y); - auto yb = invlerp(0x08, y); - // and combine them - auto bk = 1 - k / 100.0f; - uint8_t r = (cr * mr * yr) * bk * 255; - uint8_t g = (cg * mg * yg) * bk * 255; - uint8_t b = (cb * mb * yb) * bk * 255; - return {r, g, b}; -} - -std::array CmykConverter::cmyk_to_rgb(float c, float m, float y, float k) { - if (_profile) { - cmsUInt16Number tmp[4] = { cmsUInt16Number(c / 100.0f * 0xffff), cmsUInt16Number(m / 100.0f * 0xffff), cmsUInt16Number(y / 100.0f * 0xffff), cmsUInt16Number(k / 100.0f * 0xffff) }; - - uint8_t post[4] = { 0, 0, 0, 0 }; - cmsDoTransform(_transform, tmp, post, 1); - - if (_cmy && k > 0) { - // if profile cannot transform black, then this is only a crude approximation - auto black = 1 - k / 100.0f; - post[0] *= black; - post[1] *= black; - post[2] *= black; - } - return { post[0], post[1], post[2] }; - } - else { - // no ICC profile available - return simple_cmyk_to_rgb(c, m, y, k); - } -} - -} // namespace diff --git a/src/color/cmyk-conv.h b/src/color/cmyk-conv.h deleted file mode 100644 index 524c9bddf2..0000000000 --- a/src/color/cmyk-conv.h +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -#ifndef INKSCAPE_CMYK_H -#define INKSCAPE_CMYK_H - -#include -#include -#include "color/cms-color-types.h" - -namespace Inkscape { - -// Convert CMYK to sRGB -class CmykConverter { -public: - // Conversion using ICC profile gives the best results and should always be used - // whenever there is a profile selected/available - CmykConverter(cmsHPROFILE profile, int intent = INTENT_PERCEPTUAL); - - // Simple (but not simplistic) CMYK to sRGB conversion to show approximately what - // CMYK colors may look like on an sRGB device (without ICC profile) - CmykConverter() {} - - ~CmykConverter(); - - // if profile has been selected and can decode cmy/k, then return true - bool profile_used() const { return _profile != nullptr; } - - // CMYK channels from 0 to 100 (percentage) to sRGB (channels 0..255) - std::array cmyk_to_rgb(float c, float m, float y, float k); - -private: - cmsHPROFILE _profile = nullptr; - cmsHTRANSFORM _transform = nullptr; - cmsHPROFILE _srgb = nullptr; - bool _cmy = false; - int _intent = 0; -}; - -} // namespace - -#endif diff --git a/src/color/color-conv.cpp b/src/color/color-conv.cpp deleted file mode 100644 index 8ce3bcbac4..0000000000 --- a/src/color/color-conv.cpp +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -#include -#include - -#include "color-conv.h" - -namespace Inkscape { -namespace Util { - -std::string rgba_color_to_string(unsigned int rgba) { - std::ostringstream ost; - ost << "#" << std::setfill ('0') << std::setw(8) << std::hex << rgba; - return ost.str(); -} - -std::optional string_to_rgba_color(const char* str) { - if (!str || *str != '#') { - return std::optional(); - } - try { - return static_cast(std::stoul(str + 1, nullptr, 16)); - } - catch (...) { - return std::optional(); - } -} - -} -} \ No newline at end of file diff --git a/src/color/color-conv.h b/src/color/color-conv.h deleted file mode 100644 index 0b87704c6d..0000000000 --- a/src/color/color-conv.h +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * Copyright (C) 2022 Authors - * - * Released under GNU GPL v2+, read the file 'COPYING' for more information. - */ -#ifndef INKSCAPE_UTIL_COLOR_CONV_H -#define INKSCAPE_UTIL_COLOR_CONV_H - -#include -#include - -namespace Inkscape { -namespace Util { - -// Convert RGBA color to '#rrggbbaa' hex string -std::string rgba_color_to_string(unsigned int rgba); - -// Parse hex string '#rrgbbaa' and return RGBA color -std::optional string_to_rgba_color(const char* str); - -} } // namespace - -#endif diff --git a/src/color/color-profile-cms-fns.h b/src/color/color-profile-cms-fns.h deleted file mode 100644 index 06258c73e1..0000000000 --- a/src/color/color-profile-cms-fns.h +++ /dev/null @@ -1,58 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/** @file - * TODO: insert short description here - *//* - * Authors: see git history - * - * Copyright (C) 2012 Authors - * Released under GNU GPL v2+, read the file 'COPYING' for more information. - */ -#ifndef SEEN_COLOR_PROFILE_CMS_FNS_H -#define SEEN_COLOR_PROFILE_CMS_FNS_H - -#ifdef HAVE_CONFIG_H -# include "config.h" // only include where actually required! -#endif - -#include - -#include "cms-color-types.h" - -namespace Inkscape { - -// Note: these can later be adjusted to adapt for lcms2: - -class ColorSpaceSigWrapper : public ColorSpaceSig { -public : - ColorSpaceSigWrapper( cmsColorSpaceSignature sig ) : ColorSpaceSig( static_cast(sig) ) {} - ColorSpaceSigWrapper( ColorSpaceSig const &other ) : ColorSpaceSig( other ) {} - - operator cmsColorSpaceSignature() const { return static_cast(value); } -}; - -class ColorProfileClassSigWrapper : public ColorProfileClassSig { -public : - ColorProfileClassSigWrapper( cmsProfileClassSignature sig ) : ColorProfileClassSig( static_cast(sig) ) {} - ColorProfileClassSigWrapper( ColorProfileClassSig const &other ) : ColorProfileClassSig( other ) {} - - operator cmsProfileClassSignature() const { return static_cast(value); } -}; - -cmsColorSpaceSignature asICColorSpaceSig(ColorSpaceSig const & sig); -cmsProfileClassSignature asICColorProfileClassSig(ColorProfileClassSig const & sig); - -} // namespace Inkscape - - -#endif // !SEEN_COLOR_PROFILE_CMS_FNS_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/colorspace.h b/src/colorspace.h deleted file mode 100644 index 1903fe0269..0000000000 --- a/src/colorspace.h +++ /dev/null @@ -1,51 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -#ifndef SEEN_COLOR_SPACE_H -#define SEEN_COLOR_SPACE_H - -/* - * Authors: - * Jon A. Cruz - * - * Copyright (C) 2013 Authors - * - * Released under GNU GPL v2+, read the file 'COPYING' for more information. - */ - -#ifdef HAVE_CONFIG_H -# include "config.h" // only include where actually required! -#endif - -# include - -#include -#include - - -namespace Inkscape -{ - -class ColorProfile; - -} // namespace Inkscape - -namespace colorspace -{ - -class Component -{ -public: - Component(); - Component(std::string name, std::string tip, guint scale); - - std::string name; - std::string tip; - guint scale; -}; - -std::vector getColorSpaceInfo( uint32_t space ); - -std::vector getColorSpaceInfo( Inkscape::ColorProfile *prof ); - -} // namespace colorspace - -#endif // SEEN_COLOR_SPACE_H diff --git a/src/desktop-style.cpp b/src/desktop-style.cpp index 1dbfb863cb..74008e627f 100644 --- a/src/desktop-style.cpp +++ b/src/desktop-style.cpp @@ -20,7 +20,8 @@ #include "desktop-style.h" -#include "color-rgba.h" +#include "colors/color.h" +#include "colors/color-set.h" #include "desktop.h" #include "filter-chemistry.h" #include "inkscape.h" @@ -47,7 +48,6 @@ #include "style.h" #include "svg/css-ostringstream.h" -#include "svg/svg-color.h" #include "svg/svg.h" #include "ui/tools/tool-base.h" @@ -72,7 +72,7 @@ static bool isTextualItem(SPObject const *obj) * Set color on selection on desktop. */ void -sp_desktop_set_color(SPDesktop *desktop, ColorRGBA const &color, bool is_relative, bool fill) +sp_desktop_set_color(SPDesktop *desktop, Color const &color, bool is_relative, bool fill) { /// \todo relative color setting if (is_relative) { @@ -80,22 +80,9 @@ sp_desktop_set_color(SPDesktop *desktop, ColorRGBA const &color, bool is_relativ return; } - guint32 rgba = SP_RGBA32_F_COMPOSE(color[0], color[1], color[2], color[3]); - gchar b[64]; - sp_svg_write_color(b, sizeof(b), rgba); SPCSSAttr *css = sp_repr_css_attr_new(); - if (fill) { - sp_repr_css_set_property(css, "fill", b); - Inkscape::CSSOStringStream osalpha; - osalpha << color[3]; - sp_repr_css_set_property(css, "fill-opacity", osalpha.str().c_str()); - } else { - sp_repr_css_set_property(css, "stroke", b); - Inkscape::CSSOStringStream osalpha; - osalpha << color[3]; - sp_repr_css_set_property(css, "stroke-opacity", osalpha.str().c_str()); - } - + sp_repr_css_set_property_string(css, fill ? "fill" : "stroke", color.toString(false)); + sp_repr_css_set_property_double(css, fill ? "fill-opacity" : "stroke-opacity", color.getOpacity()); sp_desktop_set_style(desktop, css); sp_repr_css_attr_unref(css); @@ -286,22 +273,15 @@ sp_desktop_get_style(SPDesktop *desktop, bool with_text) /** * Return the desktop's current color. */ -guint32 +std::optional sp_desktop_get_color(SPDesktop *desktop, bool is_fill) { - guint32 r = 0; // if there's no color, return black gchar const *property = sp_repr_css_property(desktop->current, is_fill ? "fill" : "stroke", "#000"); - if (desktop->current && property) { // if there is style and the property in it, - if (strncmp(property, "url", 3)) { // and if it's not url, - // read it - r = sp_svg_read_color(property, r); - } - } - - return r; + // if there is style and the property in it, + return Color::parse(desktop->current ? property : nullptr); } double @@ -362,14 +342,11 @@ sp_desktop_get_opacity_tool(SPDesktop *desktop, Glib::ustring const &tool, bool return value; } -guint32 -sp_desktop_get_color_tool(SPDesktop *desktop, Glib::ustring const &tool, bool is_fill, bool *has_color) +std::optional +sp_desktop_get_color_tool(SPDesktop *desktop, Glib::ustring const &tool, bool is_fill) { Inkscape::Preferences *prefs = Inkscape::Preferences::get(); SPCSSAttr *css = nullptr; - guint32 r = 0; // if there's no color, return black - if (has_color) - *has_color = false; bool styleFromCurrent = prefs->getBool(tool + "/usecurrent"); if (styleFromCurrent) { css = sp_desktop_get_style(desktop, true); @@ -378,22 +355,14 @@ sp_desktop_get_color_tool(SPDesktop *desktop, Glib::ustring const &tool, bool is Inkscape::GC::anchor(css); } + std::optional ret; if (css) { gchar const *property = sp_repr_css_property(css, is_fill ? "fill" : "stroke", "#000"); - - if (desktop->current && property) { // if there is style and the property in it, - if (strncmp(property, "url", 3) && strncmp(property, "none", 4)) { // and if it's not url or none, - // read it - r = sp_svg_read_color(property, r); - if (has_color) - *has_color = true; - } - } - + // if there is style and the property in it, + ret = Color::parse(desktop->current ? property : nullptr); sp_repr_css_attr_unref(css); } - - return r | 0xff; + return ret; } /** @@ -496,19 +465,10 @@ objects_query_fillstroke (const std::vector &objects, SPStyle *style_re } SPIPaint *paint_res = style_res->getFillOrStroke(isfill); - bool paintImpossible = true; paint_res->set = true; - std::optional iccColor; - - bool iccSeen = false; - gfloat c[4]; - c[0] = c[1] = c[2] = c[3] = 0.0; - gint num = 0; - - gfloat prev[3]; - prev[0] = prev[1] = prev[2] = 0.0; - bool same_color = true; + bool paintImpossible = true; + Colors::ColorSet colors; for (auto obj : objects) { if (!obj) { @@ -598,44 +558,18 @@ objects_query_fillstroke (const std::vector &objects, SPStyle *style_re } // 2. Sum color, copy server from paint to paint_res + if (paint_res->set && paint->isColor()) { + auto copy = paint->getColor(); + copy.addOpacity(isfill ? style->fill_opacity : style->stroke_opacity); - if (paint_res->set && paint_effectively_set && paint->isColor()) { - gfloat d[3]; - paint->value.color.get_rgb_floatv(d); - - // Check if this color is the same as previous - if (paintImpossible) { - prev[0] = d[0]; - prev[1] = d[1]; - prev[2] = d[2]; - paint_res->setColor(d[0], d[1], d[2]); - if (paint->value.color.hasColors()) { - iccColor.emplace(paint->value.color); - iccSeen = true; - } - } else { - if (same_color && (prev[0] != d[0] || prev[1] != d[1] || prev[2] != d[2])) { - same_color = false; - iccColor.reset(); - } - if (iccSeen && iccColor && *iccColor != paint->value.color) { - // We can't yet blend together two icc based colors. - same_color = false; - iccColor.reset(); - } + if (colors.isEmpty()) { + paint_res->setColor(copy); } - - // average color - c[0] += d[0]; - c[1] += d[1]; - c[2] += d[2]; - c[3] += SP_SCALE24_TO_FLOAT (isfill? style->fill_opacity.value : style->stroke_opacity.value); - - num ++; + // Remove colors from this list somehow? + colors.set(obj->getId(), copy); } paintImpossible = false; - paint_res->colorSet = paint->colorSet; paint_res->paintOrigin = paint->paintOrigin; if (paint_res->set && paint_effectively_set && paint->isPaintserver()) { // copy the server if (isfill) { @@ -648,27 +582,17 @@ objects_query_fillstroke (const std::vector &objects, SPStyle *style_re style_res->fill_rule.computed = style->fill_rule.computed; // no averaging on this, just use the last one } - // After all objects processed, divide the color if necessary and return - if (paint_res->set && paint_res->isColor()) { // set the color - g_assert (num >= 1); - - c[0] /= num; - c[1] /= num; - c[2] /= num; - c[3] /= num; - paint_res->setColor(c[0], c[1], c[2]); + if (paint_res->set && paint_res->isColor() && !colors.isEmpty()) { + auto color = colors.getAverage(); if (isfill) { - style_res->fill_opacity.value = SP_SCALE24_FROM_FLOAT (c[3]); + style_res->fill_opacity.set_double(color.stealOpacity()); } else { - style_res->stroke_opacity.value = SP_SCALE24_FROM_FLOAT (c[3]); - } - - if (iccSeen && iccColor) { - paint_res->value.color.copyColors(*iccColor); + style_res->stroke_opacity.set_double(color.stealOpacity()); } + paint_res->setColor(color); - if (num > 1) { - if (same_color) + if (colors.size() > 1) { + if (colors.isSame()) return QUERY_STYLE_MULTIPLE_SAME; else return QUERY_STYLE_MULTIPLE_AVERAGED; diff --git a/src/desktop-style.h b/src/desktop-style.h index a9d357942d..fa1f4dff15 100644 --- a/src/desktop-style.h +++ b/src/desktop-style.h @@ -14,11 +14,12 @@ * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ +#include #include +#include #include -class ColorRGBA; class SPCSSAttr; class SPDesktop; class SPObject; @@ -29,9 +30,15 @@ class ObjectSet; namespace XML { class Node; } +namespace Colors { +class Color; +class ColorSet; +} } namespace Glib { class ustring; } +using Inkscape::Colors::Color; + enum { // what kind of a style the query is returning QUERY_STYLE_NOTHING, // nothing was queried - e.g. no selection QUERY_STYLE_SINGLE, // single object was queried @@ -65,14 +72,14 @@ enum { // which property was queried (add when you need more) }; void sp_desktop_apply_css_recursive(SPObject *o, SPCSSAttr *css, bool skip_lines); -void sp_desktop_set_color(SPDesktop *desktop, ColorRGBA const &color, bool is_relative, bool fill); +void sp_desktop_set_color(SPDesktop *desktop, Color const &color, bool is_relative, bool fill); void sp_desktop_set_style(Inkscape::ObjectSet *set, SPDesktop *desktop, SPCSSAttr *css, bool change = true, bool write_current = true, bool switch_style = false); void sp_desktop_set_style(SPDesktop *desktop, SPCSSAttr *css, bool change = true, bool write_current = true, bool switch_style = false); SPCSSAttr *sp_desktop_get_style(SPDesktop *desktop, bool with_text); -guint32 sp_desktop_get_color (SPDesktop *desktop, bool is_fill); double sp_desktop_get_master_opacity_tool(SPDesktop *desktop, Glib::ustring const &tool, bool* has_opacity = nullptr); double sp_desktop_get_opacity_tool(SPDesktop *desktop, Glib::ustring const &tool, bool is_fill); -guint32 sp_desktop_get_color_tool(SPDesktop *desktop, Glib::ustring const &tool, bool is_fill, bool* has_color = nullptr); +std::optional sp_desktop_get_color (SPDesktop *desktop, bool is_fill); +std::optional sp_desktop_get_color_tool(SPDesktop *desktop, Glib::ustring const &tool, bool is_fill); double sp_desktop_get_font_size_tool (SPDesktop *desktop); void sp_desktop_apply_style_tool(SPDesktop *desktop, Inkscape::XML::Node *repr, Glib::ustring const &tool, bool with_text); diff --git a/src/desktop.cpp b/src/desktop.cpp index a51218a275..e05e111f52 100644 --- a/src/desktop.cpp +++ b/src/desktop.cpp @@ -30,7 +30,6 @@ #include #include "desktop.h" -#include "color.h" #include "desktop-events.h" #include "desktop-style.h" #include "document-undo.h" diff --git a/src/display/cairo-utils.cpp b/src/display/cairo-utils.cpp index 807fb80fc9..89c26ee606 100644 --- a/src/display/cairo-utils.cpp +++ b/src/display/cairo-utils.cpp @@ -29,7 +29,8 @@ #include #include "cairo-templates.h" -#include "color.h" +#include "colors/manager.h" +#include "colors/utils.h" #include "document.h" #include "helper/pixbuf-ops.h" #include "preferences.h" @@ -1015,13 +1016,39 @@ ink_cairo_set_source_rgba32(cairo_t *ct, guint32 rgba) { cairo_set_source_rgba(ct, SP_RGBA32_R_F(rgba), SP_RGBA32_G_F(rgba), SP_RGBA32_B_F(rgba), SP_RGBA32_A_F(rgba)); } +void ink_cairo_set_source_rgba32(Cairo::RefPtr ctx, guint32 rgba) +{ + ctx->set_source_rgba(SP_RGBA32_R_F(rgba), SP_RGBA32_G_F(rgba), SP_RGBA32_B_F(rgba), SP_RGBA32_A_F(rgba)); +} +void ink_cairo_set_source_rgba32(Cairo::Context &ctx, guint32 rgba) +{ + ctx.set_source_rgba(SP_RGBA32_R_F(rgba), SP_RGBA32_G_F(rgba), SP_RGBA32_B_F(rgba), SP_RGBA32_A_F(rgba)); +} -void -ink_cairo_set_source_color(cairo_t *ct, SPColor const &c, double opacity) +/** + * The following functions interact between Inkscape color model, and cairo surface rendering. + * for compatability we only convert to RGB and use that directly. + */ +void ink_cairo_set_source_color(Cairo::RefPtr ctx, Inkscape::Colors::Color const &color, double opacity) +{ + ink_cairo_set_source_rgba32(ctx, color.toRGBA(opacity)); +} +void ink_cairo_set_source_color(cairo_t *ctx, Inkscape::Colors::Color const &color, double opacity) +{ + ink_cairo_set_source_rgba32(ctx, color.toRGBA(opacity)); +} +void ink_cairo_pattern_add_color_stop(cairo_pattern_t *ptn, double offset, Inkscape::Colors::Color const &color, double opacity) +{ + auto c = color.toRGBA(opacity); + cairo_pattern_add_color_stop_rgba(ptn, offset, SP_RGBA32_R_F(c), SP_RGBA32_G_F(c), SP_RGBA32_B_F(c), SP_RGBA32_A_F(c)); +} +cairo_pattern_t *ink_cairo_pattern_create(Inkscape::Colors::Color const &color, double opacity) { - cairo_set_source_rgba(ct, c.v.c[0], c.v.c[1], c.v.c[2], opacity); + auto c = color.toRGBA(opacity); + return cairo_pattern_create_rgba(SP_RGBA32_R_F(c), SP_RGBA32_G_F(c), SP_RGBA32_B_F(c), SP_RGBA32_A_F(c)); } + void ink_matrix_to_2geom(Geom::Affine &m, cairo_matrix_t const &cm) { m[0] = cm.xx; @@ -1270,17 +1297,6 @@ static int ink_cairo_surface_average_color_internal(cairo_surface_t *surface, do return width * height; } -guint32 ink_cairo_surface_average_color(cairo_surface_t *surface) -{ - double rf,gf,bf,af; - ink_cairo_surface_average_color_premul(surface, rf,gf,bf,af); - guint32 r = round(rf * 255); - guint32 g = round(gf * 255); - guint32 b = round(bf * 255); - guint32 a = round(af * 255); - ASSEMBLE_ARGB32(px, a,r,g,b); - return px; -} // We extract colors from pattern background, if we need to extract sometimes from a gradient we can add // a extra parameter with the spot number and use cairo_pattern_get_color_stop_rgba // also if the pattern is a image we can pass a boolean like solid = false to get the color by image average ink_cairo_surface_average_color @@ -1306,34 +1322,22 @@ guint32 ink_cairo_pattern_get_argb32(cairo_pattern_t *pattern) return 0; } -void ink_cairo_surface_average_color(cairo_surface_t *surface, double &r, double &g, double &b, double &a) +Colors::Color ink_cairo_surface_average_color(cairo_surface_t *surface) { - int count = ink_cairo_surface_average_color_internal(surface, r,g,b,a); - - r /= a; - g /= a; - b /= a; - a /= count; - - r = CLAMP(r, 0.0, 1.0); - g = CLAMP(g, 0.0, 1.0); - b = CLAMP(b, 0.0, 1.0); - a = CLAMP(a, 0.0, 1.0); + double r, g, b, a = 0.0; + int count = ink_cairo_surface_average_color_internal(surface, r, g, b, a); + auto color = Colors::Color(Colors::Space::Type::RGB, {r / a, g / a, b / a, a / count}); + color.normalize(); + return color; } -void ink_cairo_surface_average_color_premul(cairo_surface_t *surface, double &r, double &g, double &b, double &a) +Colors::Color ink_cairo_surface_average_color_premul(cairo_surface_t *surface) { - int count = ink_cairo_surface_average_color_internal(surface, r,g,b,a); - - r /= count; - g /= count; - b /= count; - a /= count; - - r = CLAMP(r, 0.0, 1.0); - g = CLAMP(g, 0.0, 1.0); - b = CLAMP(b, 0.0, 1.0); - a = CLAMP(a, 0.0, 1.0); + double r, g, b, a = 0.0; + int count = ink_cairo_surface_average_color_internal(surface, r, g, b, a); + auto color = Colors::Color(Colors::Space::Type::RGB, {r / count, g / count, b / count, a / count}); + color.normalize(); + return color; } static guint32 srgb_to_linear( const guint32 c, const guint32 a ) { @@ -1513,37 +1517,21 @@ ink_cairo_pattern_create_checkerboard(guint32 rgba, bool use_alpha) int const w = 6; int const h = 6; - double r = SP_RGBA32_R_F(rgba); - double g = SP_RGBA32_G_F(rgba); - double b = SP_RGBA32_B_F(rgba); - - float hsl[3]; - SPColor::rgb_to_hsl_floatv(hsl, r, g, b); - hsl[2] += hsl[2] < 0.08 ? 0.08 : -0.08; // 0.08 = 0.77-0.69, the original checkerboard colors. - - float rgb2[3]; - SPColor::hsl_to_rgb_floatv(rgb2, hsl[0], hsl[1], hsl[2]); + auto color_a = Colors::Color(rgba, use_alpha); + auto color_b = Inkscape::Colors::make_contrasted_color(color_a, 1.0); + // Once the second color is generated, the original doesn't need alpha + color_a.enableOpacity(false); cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 2*w, 2*h); cairo_t *ct = cairo_create(s); cairo_set_operator(ct, CAIRO_OPERATOR_SOURCE); - cairo_set_source_rgb(ct, r, g, b); + ink_cairo_set_source_color(ct, color_a); cairo_paint(ct); - cairo_set_source_rgb(ct, rgb2[0], rgb2[1], rgb2[2]); + ink_cairo_set_source_color(ct, color_b); cairo_rectangle(ct, 0, 0, w, h); cairo_rectangle(ct, w, h, w, h); cairo_fill(ct); - if (use_alpha) { - // use alpha to show opacity cover checkerboard - double a = SP_RGBA32_A_F(rgba); - if (a > 0.0) { - cairo_set_operator(ct, CAIRO_OPERATOR_OVER); - cairo_rectangle(ct, 0, 0, 2 * w, 2 * h); - cairo_set_source_rgba(ct, r, g, b, a); - cairo_fill(ct); - } - } cairo_destroy(ct); cairo_pattern_t *p = cairo_pattern_create_for_surface(s); diff --git a/src/display/cairo-utils.h b/src/display/cairo-utils.h index 2a3eb4dffe..c44a76674c 100644 --- a/src/display/cairo-utils.h +++ b/src/display/cairo-utils.h @@ -17,12 +17,14 @@ #include #include "style.h" -struct SPColor; typedef struct _GdkPixbuf GdkPixbuf; void ink_cairo_pixbuf_cleanup(unsigned char *, void *); namespace Inkscape { +namespace Colors { +class Color; +} /** Class to hold image data for raster images. * Allows easy interoperation with GdkPixbuf and Cairo. */ @@ -98,8 +100,13 @@ void set_cairo_surface_ci(cairo_surface_t *surface, SPColorInterpolation cif); void copy_cairo_surface_ci(cairo_surface_t *in, cairo_surface_t *out); void convert_cairo_surface_ci(cairo_surface_t *surface, SPColorInterpolation cif); -void ink_cairo_set_source_color(cairo_t *ct, SPColor const &color, double opacity); -void ink_cairo_set_source_rgba32(cairo_t *ct, guint32 rgba); +cairo_pattern_t *ink_cairo_pattern_create(Inkscape::Colors::Color const &color, double opacity = 1.0); +void ink_cairo_pattern_add_color_stop(cairo_pattern_t *ptn, double offset, Inkscape::Colors::Color const &color, double opacity = 1.0); +void ink_cairo_set_source_color(Cairo::RefPtr ctx, Inkscape::Colors::Color const &color, double opacity = 1.0); +void ink_cairo_set_source_color(cairo_t *ctx, Inkscape::Colors::Color const &c, double opacity = 1.0); +void ink_cairo_set_source_rgba32(Cairo::RefPtr ctx, guint32 rgba); +void ink_cairo_set_source_rgba32(Cairo::Context &ctx, guint32 rgba); +void ink_cairo_set_source_rgba32(cairo_t *ctx, guint32 rgba); void ink_cairo_transform(cairo_t *ct, Geom::Affine const &m); void ink_cairo_pattern_set_matrix(cairo_pattern_t *cp, Geom::Affine const &m); void ink_cairo_set_hairline(cairo_t *ct); @@ -119,10 +126,9 @@ cairo_surface_t *ink_cairo_surface_create_output(cairo_surface_t *image, cairo_s void ink_cairo_surface_blit(cairo_surface_t *src, cairo_surface_t *dest); int ink_cairo_surface_get_width(cairo_surface_t *surface); int ink_cairo_surface_get_height(cairo_surface_t *surface); -guint32 ink_cairo_surface_average_color(cairo_surface_t *surface); guint32 ink_cairo_pattern_get_argb32(cairo_pattern_t *pattern); -void ink_cairo_surface_average_color(cairo_surface_t *surface, double &r, double &g, double &b, double &a); -void ink_cairo_surface_average_color_premul(cairo_surface_t *surface, double &r, double &g, double &b, double &a); +Colors::Color ink_cairo_surface_average_color(cairo_surface_t *surface); +Colors::Color ink_cairo_surface_average_color_premul(cairo_surface_t *surface); double srgb_to_linear( const double c ); int ink_cairo_surface_srgb_to_linear(cairo_surface_t *surface); diff --git a/src/display/control/canvas-item-bpath.cpp b/src/display/control/canvas-item-bpath.cpp index 7c7ddf99fd..757e8e2d81 100644 --- a/src/display/control/canvas-item-bpath.cpp +++ b/src/display/control/canvas-item-bpath.cpp @@ -16,7 +16,6 @@ #include "canvas-item-bpath.h" -#include "color.h" // SP_RGBA_x_F #include "display/curve.h" #include "display/cairo-utils.h" #include "helper/geom.h" // bounds_exact_transformed() diff --git a/src/display/control/canvas-item-curve.cpp b/src/display/control/canvas-item-curve.cpp index 7369fdb775..9ee52425ad 100644 --- a/src/display/control/canvas-item-curve.cpp +++ b/src/display/control/canvas-item-curve.cpp @@ -18,8 +18,7 @@ #include "canvas-item-curve.h" -#include "color.h" // SP_RGBA_x_F - +#include "display/cairo-utils.h" #include "helper/geom.h" #include "ui/widget/canvas.h" @@ -178,7 +177,7 @@ void CanvasItemCurve::_render(Inkscape::CanvasItemBuffer &buf) const buf.cr->set_line_width(background_width); buf.cr->stroke_preserve(); - buf.cr->set_source_rgba(SP_RGBA32_R_F(_stroke), SP_RGBA32_G_F(_stroke), SP_RGBA32_B_F(_stroke), SP_RGBA32_A_F(_stroke)); + ink_cairo_set_source_rgba32(buf.cr, _stroke); buf.cr->set_line_width(_width); buf.cr->stroke(); diff --git a/src/display/control/canvas-item-grid.cpp b/src/display/control/canvas-item-grid.cpp index 9b9696d675..c36920b51b 100644 --- a/src/display/control/canvas-item-grid.cpp +++ b/src/display/control/canvas-item-grid.cpp @@ -14,8 +14,8 @@ #include #include +#include "colors/color.h" #include "canvas-item-grid.h" -#include "color.h" #include "helper/geom.h" enum Dim3 { X, Y, Z }; @@ -45,8 +45,8 @@ CanvasItemGrid::CanvasItemGrid(CanvasItemGroup *group) : CanvasItem(group) , _origin(0, 0) , _spacing(1, 1) - , _minor_color(GRID_DEFAULT_MINOR_COLOR) - , _major_color(GRID_DEFAULT_MAJOR_COLOR) + , _minor_color(0x0) + , _major_color(0x0) , _major_line_interval(5) , _dotted(false) { diff --git a/src/display/control/canvas-item-grid.h b/src/display/control/canvas-item-grid.h index 9acacca0cf..7670e33b75 100644 --- a/src/display/control/canvas-item-grid.h +++ b/src/display/control/canvas-item-grid.h @@ -18,12 +18,6 @@ #include "canvas-item.h" #include "preferences.h" -// default colors in RGBA -uint32_t constexpr GRID_DEFAULT_MAJOR_COLOR = 0x0099e5'4d; -uint32_t constexpr GRID_DEFAULT_MINOR_COLOR = 0x0099e5'26; -uint32_t constexpr GRID_DEFAULT_BLOCK_COLOR = 0x0047cb'4d; -uint32_t constexpr GRID_DEFAULT_MINOR_BLOCK_COLOR = 0x0047cb'26; - namespace Inkscape { class CanvasItemGrid : public CanvasItem diff --git a/src/display/control/canvas-item-quad.cpp b/src/display/control/canvas-item-quad.cpp index 3b4946cfb6..b6427a6dd5 100644 --- a/src/display/control/canvas-item-quad.cpp +++ b/src/display/control/canvas-item-quad.cpp @@ -18,7 +18,7 @@ #include "canvas-item-quad.h" -#include "color.h" // SP_RGBA_x_F +#include "display/cairo-utils.h" #include "helper/geom.h" namespace Inkscape { @@ -134,13 +134,11 @@ void CanvasItemQuad::_render(Inkscape::CanvasItemBuffer &buf) const cairo_set_operator(buf.cr->cobj(), CAIRO_OPERATOR_DIFFERENCE); } - buf.cr->set_source_rgba(SP_RGBA32_R_F(_fill), SP_RGBA32_G_F(_fill), - SP_RGBA32_B_F(_fill), SP_RGBA32_A_F(_fill)); + ink_cairo_set_source_rgba32(buf.cr, _fill); buf.cr->fill_preserve(); buf.cr->set_line_width(1); - buf.cr->set_source_rgba(SP_RGBA32_R_F(_stroke), SP_RGBA32_G_F(_stroke), - SP_RGBA32_B_F(_stroke), SP_RGBA32_A_F(_stroke)); + ink_cairo_set_source_rgba32(buf.cr, _stroke); buf.cr->stroke_preserve(); buf.cr->begin_new_path(); diff --git a/src/display/control/canvas-item-rect.cpp b/src/display/control/canvas-item-rect.cpp index ac502d487e..7c3cbdeeae 100644 --- a/src/display/control/canvas-item-rect.cpp +++ b/src/display/control/canvas-item-rect.cpp @@ -18,7 +18,6 @@ #include -#include "color.h" // SP_RGBA_x_F #include "desktop.h" #include "display/cairo-utils.h" #include "helper/geom.h" diff --git a/src/display/control/canvas-item-text.cpp b/src/display/control/canvas-item-text.cpp index e026fc1efe..694f064647 100644 --- a/src/display/control/canvas-item-text.cpp +++ b/src/display/control/canvas-item-text.cpp @@ -20,8 +20,7 @@ #include // std::move #include -#include "color.h" // SP_RGBA_x_F - +#include "display/cairo-utils.h" #include "ui/util.h" namespace Inkscape { @@ -150,8 +149,7 @@ void CanvasItemText::_render(Inkscape::CanvasItemBuffer &buf) const buf.cr->arc(x + radius, y + radius, radius, M_PI, 3*M_PI_2); } buf.cr->set_line_width(2); - buf.cr->set_source_rgba(SP_RGBA32_R_F(_background), SP_RGBA32_G_F(_background), - SP_RGBA32_B_F(_background), SP_RGBA32_A_F(_background)); + ink_cairo_set_source_rgba32(buf.cr, _background); buf.cr->fill(); } @@ -164,8 +162,8 @@ void CanvasItemText::_render(Inkscape::CanvasItemBuffer &buf) const buf.cr->select_font_face(_fontname, Cairo::ToyFontFace::Slant::NORMAL, Cairo::ToyFontFace::Weight::NORMAL); buf.cr->set_font_size(_fontsize); buf.cr->text_path(_text); - buf.cr->set_source_rgba(SP_RGBA32_R_F(_fill), SP_RGBA32_G_F(_fill), - SP_RGBA32_B_F(_fill), SP_RGBA32_A_F(_fill)); + + ink_cairo_set_source_rgba32(buf.cr, _fill); buf.cr->fill(); buf.cr->restore(); } diff --git a/src/display/control/canvas-page.cpp b/src/display/control/canvas-page.cpp index 81f6569884..5426e9ed77 100644 --- a/src/display/control/canvas-page.cpp +++ b/src/display/control/canvas-page.cpp @@ -13,12 +13,11 @@ #include "canvas-page.h" #include "canvas-item-rect.h" #include "canvas-item-text.h" -#include "color.h" +#include "colors/utils.h" namespace Inkscape { CanvasPage::CanvasPage() = default; - CanvasPage::~CanvasPage() = default; /** @@ -47,7 +46,7 @@ void CanvasPage::add(Geom::Rect size, CanvasItemGroup *background_group, CanvasI item->set_name("margin"); item->set_dashed(false); item->set_inverted(false); - item->set_stroke(_margin_color); + item->set_stroke(_margin_color.toRGBA()); canvas_items.emplace_back(item); } @@ -55,7 +54,7 @@ void CanvasPage::add(Geom::Rect size, CanvasItemGroup *background_group, CanvasI item->set_name("bleed"); item->set_dashed(false); item->set_inverted(false); - item->set_stroke(_bleed_color); + item->set_stroke(_bleed_color.toRGBA()); canvas_items.emplace_back(item); } @@ -112,11 +111,11 @@ void CanvasPage::update(Geom::Rect size, Geom::OptRect margin, Geom::OptRect ble { // Put these in the preferences? bool border_on_top = _border_on_top; - guint32 shadow_color = _border_color; // there's no separate shadow color in the UI, border color is used + guint32 shadow_color = _border_color.toRGBA(); // there's no separate shadow color in the UI, border color is used guint32 select_color = 0x000000cc; - guint32 border_color = _border_color; - guint32 margin_color = _margin_color; - guint32 bleed_color = _bleed_color; + guint32 border_color = _border_color.toRGBA(); + guint32 margin_color = _margin_color.toRGBA(); + guint32 bleed_color = _bleed_color.toRGBA(); // This is used when showing the viewport as *not a page* it's mostly // never used as the first page is normally the viewport too. @@ -171,7 +170,7 @@ void CanvasPage::update(Geom::Rect size, Geom::OptRect margin, Geom::OptRect ble rect->set_background(_background_color | 0xff); } */ - rect->set_fill(_background_color); + rect->set_fill(_background_color.toRGBA()); rect->set_shadow(shadow_color, _shadow_size); } else { rect->set_fill(0x0); @@ -198,8 +197,7 @@ void CanvasPage::_updateTextItem(CanvasItemText *label, Geom::Rect page, std::st double radius = 0.2; // Change the colors for whiter/lighter backgrounds - unsigned char luminance = SP_RGBA32_LUMINANCE(_canvas_color); - if (luminance < 0x88) { + if (Colors::get_perceptual_lightness(_canvas_color) < 0.5) { foreground = 0x000000ff; background = 0xffffff99; selected = 0x50afe7ff; @@ -246,9 +244,9 @@ bool CanvasPage::setShadow(int shadow) return false; } -bool CanvasPage::setPageColor(uint32_t border, uint32_t bg, uint32_t canvas, uint32_t margin, uint32_t bleed) +bool CanvasPage::setPageColor(Color const &border, Color const &bg, Color const &canvas, Color const &margin, Color const &bleed) { - if (border != _border_color || bg != _background_color || canvas != _canvas_color) { + if (std::tie(border, bg, canvas, margin, bleed) != std::tie(_border_color, _background_color, _canvas_color, _margin_color, _bleed_color)) { _border_color = border; _background_color = bg; _canvas_color = canvas; diff --git a/src/display/control/canvas-page.h b/src/display/control/canvas-page.h index e3227bfc2a..6d7bac308a 100644 --- a/src/display/control/canvas-page.h +++ b/src/display/control/canvas-page.h @@ -17,10 +17,13 @@ #include #include "canvas-item-ptr.h" +#include "colors/color.h" namespace Inkscape { namespace UI::Widget { class Canvas; } +using Colors::Color; + class CanvasItemGroup; class CanvasItemText; @@ -39,7 +42,7 @@ public: bool setOnTop(bool on_top); bool setShadow(int shadow); - bool setPageColor(uint32_t border, uint32_t bg, uint32_t canvas, uint32_t margin, uint32_t bleed); + bool setPageColor(Color const &border, Color const &bg, Color const &canvas, Color const &margin, Color const &bleed); bool setLabelStyle(const std::string &style); bool is_selected = false; @@ -53,11 +56,11 @@ private: int _shadow_size = 0; bool _border_on_top = true; - uint32_t _background_color = 0xffffffff; - uint32_t _border_color = 0x00000040; - uint32_t _canvas_color = 0xffffffff; - uint32_t _margin_color = 0x1699d771; // Blue'ish - uint32_t _bleed_color = 0xbe310e62; // Red'ish + Color _background_color {0xffffffff}; + Color _border_color {0x00000040}; + Color _canvas_color {0xffffffff}; + Color _margin_color {0x1699d771}; + Color _bleed_color {0xbe310e62}; std::string _label_style = "default"; }; diff --git a/src/display/control/ctrl-handle-rendering.cpp b/src/display/control/ctrl-handle-rendering.cpp index c2deb8fc7f..7d42d95946 100644 --- a/src/display/control/ctrl-handle-rendering.cpp +++ b/src/display/control/ctrl-handle-rendering.cpp @@ -10,8 +10,8 @@ #include #include <2geom/point.h> -#include "color.h" #include "display/control/canvas-item-enums.h" +#include "display/cairo-utils.h" namespace Inkscape::Handles { namespace { @@ -424,13 +424,6 @@ void draw_cairo_path(CanvasItemCtrlShape shape, Cairo::Context &cr, double size, std::unordered_map> cache; std::mutex mutex; -void set_source_rgba32(Cairo::Context &cr, uint32_t rgba) -{ - cr.set_source_rgba(SP_RGBA32_R_F(rgba), - SP_RGBA32_G_F(rgba), - SP_RGBA32_B_F(rgba), - SP_RGBA32_A_F(rgba)); -} std::shared_ptr draw_uncached(RenderParams const &p) { @@ -470,16 +463,16 @@ std::shared_ptr draw_uncached(RenderParams const &p) draw_cairo_path(p.shape, cr, p.size * scale, grid_fit); // Outline. - set_source_rgba32(cr, p.outline); + ink_cairo_set_source_rgba32(cr, p.outline); cr.set_line_width(effective_outline * scale); cr.stroke_preserve(); // Fill. - set_source_rgba32(cr, p.fill);; + ink_cairo_set_source_rgba32(cr, p.fill);; cr.fill_preserve(); // Stroke. - set_source_rgba32(cr, p.stroke); + ink_cairo_set_source_rgba32(cr, p.stroke); cr.set_line_width(p.stroke_width * scale); cr.stroke(); diff --git a/src/display/drawing-paintserver.cpp b/src/display/drawing-paintserver.cpp index e018d8b940..9f3b8f8edf 100644 --- a/src/display/drawing-paintserver.cpp +++ b/src/display/drawing-paintserver.cpp @@ -1,14 +1,21 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "drawing-paintserver.h" + +#include + #include "cairo-utils.h" +#include "colors/color.h" namespace Inkscape { DrawingPaintServer::~DrawingPaintServer() = default; +DrawingSolidColor::DrawingSolidColor(Colors::Color color) + : color(std::move(color)) {} + cairo_pattern_t *DrawingSolidColor::create_pattern(cairo_t *, Geom::OptRect const &, double opacity) const { - return cairo_pattern_create_rgba(c[0], c[1], c[2], alpha * opacity); + return ink_cairo_pattern_create(color, opacity); } void DrawingGradient::common_setup(cairo_pattern_t *pat, Geom::OptRect const &bbox, double opacity) const @@ -45,7 +52,7 @@ cairo_pattern_t *DrawingLinearGradient::create_pattern(cairo_t *, Geom::OptRect // add stops for (auto &stop : stops) { // multiply stop opacity by paint opacity - cairo_pattern_add_color_stop_rgba(pat, stop.offset, stop.color.v.c[0], stop.color.v.c[1], stop.color.v.c[2], stop.opacity * opacity); + ink_cairo_pattern_add_color_stop(pat, stop.offset, *stop.color, opacity); } return pat; @@ -103,7 +110,7 @@ cairo_pattern_t *DrawingRadialGradient::create_pattern(cairo_t *ct, Geom::OptRec // add stops for (auto &stop : stops) { // multiply stop opacity by paint opacity - cairo_pattern_add_color_stop_rgba(pat, stop.offset, stop.color.v.c[0], stop.color.v.c[1], stop.color.v.c[2], stop.opacity * opacity); + ink_cairo_pattern_add_color_stop(pat, stop.offset, *stop.color, opacity); } return pat; diff --git a/src/display/drawing-paintserver.h b/src/display/drawing-paintserver.h index eb68f6b672..7a9a22edaf 100644 --- a/src/display/drawing-paintserver.h +++ b/src/display/drawing-paintserver.h @@ -23,6 +23,9 @@ class SPGradient; namespace Inkscape { +namespace Colors { +class Color; +} /** * A DrawingPaintServer is a lightweight copy of the resources needed to paint using a paint server. @@ -56,15 +59,11 @@ class DrawingSolidColor final : public DrawingPaintServer { public: - DrawingSolidColor(float *c, double alpha) - : c({c[0], c[1], c[2]}) - , alpha(alpha) {} - + DrawingSolidColor(Colors::Color color); cairo_pattern_t *create_pattern(cairo_t *, Geom::OptRect const &, double opacity) const override; private: - std::array c; ///< RGB color components. - double alpha; + Colors::Color color; }; /** diff --git a/src/display/drawing.cpp b/src/display/drawing.cpp index 3070adf04c..5dc7c24801 100644 --- a/src/display/drawing.cpp +++ b/src/display/drawing.cpp @@ -373,7 +373,11 @@ void Drawing::averageColor(Geom::IntRect const &area, double &R, double &G, doub auto dc = Inkscape::DrawingContext(surface->cobj(), area.min()); render(dc, area); - ink_cairo_surface_average_color_premul(surface->cobj(), R, G, B, A); + auto color = ink_cairo_surface_average_color_premul(surface->cobj()); + R = color[0]; + G = color[1]; + B = color[2]; + A = color.getOpacity(); } /* diff --git a/src/display/nr-filter-diffuselighting.cpp b/src/display/nr-filter-diffuselighting.cpp index 8f80998ba9..6905023ed9 100644 --- a/src/display/nr-filter-diffuselighting.cpp +++ b/src/display/nr-filter-diffuselighting.cpp @@ -26,7 +26,6 @@ #include "display/nr-filter-units.h" #include "display/nr-filter-utils.h" #include "display/nr-light.h" -#include "svg/svg-color.h" namespace Inkscape { namespace Filters { @@ -141,14 +140,6 @@ void FilterDiffuseLighting::render_cairo(FilterSlot &slot) const double g = SP_RGBA32_G_F(lighting_color); double b = SP_RGBA32_B_F(lighting_color); - if (icc) { - unsigned char ru, gu, bu; - icc_color_to_sRGB(&*icc, &ru, &gu, &bu); - r = SP_COLOR_U_TO_F(ru); - g = SP_COLOR_U_TO_F(gu); - b = SP_COLOR_U_TO_F(bu); - } - // Only alpha channel of input is used, no need to check input color_interpolation_filter value. // Lighting color is always defined in terms of sRGB, preconvert to linearRGB // if color_interpolation_filters set to linearRGB (for efficiency assuming diff --git a/src/display/nr-filter-diffuselighting.h b/src/display/nr-filter-diffuselighting.h index 69577b5d14..a50d90939a 100644 --- a/src/display/nr-filter-diffuselighting.h +++ b/src/display/nr-filter-diffuselighting.h @@ -19,12 +19,10 @@ #include "display/nr-filter-primitive.h" #include "display/nr-filter-slot.h" #include "display/nr-filter-units.h" -#include "svg/svg-icc-color.h" class SPFeDistantLight; class SPFePointLight; class SPFeSpotLight; -struct SVGICCColor; namespace Inkscape { namespace Filters { @@ -36,7 +34,6 @@ public: ~FilterDiffuseLighting() override; void render_cairo(FilterSlot &slot) const override; - void set_icc(SVGICCColor const &icc_) { icc = icc_; } void area_enlarge(Geom::IntRect &area, Geom::Affine const &trans) const override; double complexity(Geom::Affine const &ctm) const override; @@ -51,9 +48,6 @@ public: guint32 lighting_color; Glib::ustring name() const override { return "Diffuse Lighting"; } - -private: - std::optional icc; }; } // namespace Filters diff --git a/src/display/nr-filter-flood.cpp b/src/display/nr-filter-flood.cpp index cc7fcb2ce6..b39f284c65 100644 --- a/src/display/nr-filter-flood.cpp +++ b/src/display/nr-filter-flood.cpp @@ -18,9 +18,6 @@ #include "display/cairo-utils.h" #include "display/nr-filter-flood.h" #include "display/nr-filter-slot.h" -#include "svg/svg-icc-color.h" -#include "svg/svg-color.h" -#include "color.h" namespace Inkscape { namespace Filters { @@ -36,15 +33,7 @@ void FilterFlood::render_cairo(FilterSlot &slot) const double r = SP_RGBA32_R_F(color); double g = SP_RGBA32_G_F(color); double b = SP_RGBA32_B_F(color); - double a = opacity; - - if (icc) { - unsigned char ru, gu, bu; - icc_color_to_sRGB(&*icc, &ru, &gu, &bu); - r = SP_COLOR_U_TO_F(ru); - g = SP_COLOR_U_TO_F(gu); - b = SP_COLOR_U_TO_F(bu); - } + double a = SP_RGBA32_A_F(color); cairo_surface_t *out = ink_cairo_surface_create_same_size(input, CAIRO_CONTENT_COLOR_ALPHA); @@ -99,11 +88,6 @@ void FilterFlood::set_color(guint32 c) color = c; } -void FilterFlood::set_opacity(double o) -{ - opacity = o; -} - double FilterFlood::complexity(Geom::Affine const &) const { // flood is actually less expensive than normal rendering, diff --git a/src/display/nr-filter-flood.h b/src/display/nr-filter-flood.h index 2228e00787..31504bbf91 100644 --- a/src/display/nr-filter-flood.h +++ b/src/display/nr-filter-flood.h @@ -32,16 +32,12 @@ public: double complexity(Geom::Affine const &ctm) const override; bool uses_background() const override { return false; } - void set_opacity(double o); void set_color(guint32 c); - void set_icc(SVGICCColor const &icc_) { icc = icc_; } Glib::ustring name() const override { return Glib::ustring("Flood"); } private: - double opacity; guint32 color; - std::optional icc; }; } // namespace Filters diff --git a/src/display/nr-filter-specularlighting.cpp b/src/display/nr-filter-specularlighting.cpp index 64402bb151..7b8399d455 100644 --- a/src/display/nr-filter-specularlighting.cpp +++ b/src/display/nr-filter-specularlighting.cpp @@ -26,8 +26,6 @@ #include "display/nr-filter-units.h" #include "display/nr-filter-utils.h" #include "display/nr-light.h" -#include "svg/svg-icc-color.h" -#include "svg/svg-color.h" namespace Inkscape { namespace Filters { @@ -154,14 +152,6 @@ void FilterSpecularLighting::render_cairo(FilterSlot &slot) const double g = SP_RGBA32_G_F(lighting_color); double b = SP_RGBA32_B_F(lighting_color); - if (icc) { - unsigned char ru, gu, bu; - icc_color_to_sRGB(&*icc, &ru, &gu, &bu); - r = SP_COLOR_U_TO_F(ru); - g = SP_COLOR_U_TO_F(gu); - b = SP_COLOR_U_TO_F(bu); - } - // Only alpha channel of input is used, no need to check input color_interpolation_filter value. // Lighting color is always defined in terms of sRGB, preconvert to linearRGB // if color_interpolation_filters set to linearRGB (for efficiency assuming diff --git a/src/display/nr-filter-specularlighting.h b/src/display/nr-filter-specularlighting.h index fdc7eb9a97..43d7e0167b 100644 --- a/src/display/nr-filter-specularlighting.h +++ b/src/display/nr-filter-specularlighting.h @@ -34,7 +34,6 @@ public: ~FilterSpecularLighting() override; void render_cairo(FilterSlot &slot) const override; - void set_icc(SVGICCColor const &icc_) { icc = icc_; } void area_enlarge(Geom::IntRect &area, Geom::Affine const &trans) const override; double complexity(Geom::Affine const &ctm) const override; @@ -51,9 +50,6 @@ public: guint32 lighting_color; Glib::ustring name() const override { return Glib::ustring("Specular Lighting"); } - -private: - std::optional icc; }; } // namespace Filters diff --git a/src/display/nr-light.cpp b/src/display/nr-light.cpp index 00404fdc17..9562ea779d 100644 --- a/src/display/nr-light.cpp +++ b/src/display/nr-light.cpp @@ -17,7 +17,7 @@ #include "object/filters/distantlight.h" #include "object/filters/pointlight.h" #include "object/filters/spotlight.h" -#include "color.h" +#include "colors/color.h" namespace Inkscape { namespace Filters { diff --git a/src/display/nr-style.cpp b/src/display/nr-style.cpp index 8d21061aef..3b1652307a 100644 --- a/src/display/nr-style.cpp +++ b/src/display/nr-style.cpp @@ -13,6 +13,7 @@ #include "display/nr-style.h" #include "style.h" +#include "colors/manager.h" #include "display/cairo-utils.h" #include "display/drawing-context.h" #include "display/drawing-pattern.h" @@ -28,7 +29,7 @@ void NRStyleData::Paint::clear() type = PaintType::NONE; } -void NRStyleData::Paint::set(SPColor const &c) +void NRStyleData::Paint::set(Colors::Color const &c) { clear(); type = PaintType::COLOR; @@ -47,16 +48,16 @@ void NRStyleData::Paint::set(SPPaintServer *ps) void NRStyleData::Paint::set(SPIPaint const *paint) { if (paint->isPaintserver()) { - SPPaintServer* server = paint->value.href->getObject(); + SPPaintServer* server = paint->href->getObject(); if (server && server->isValid()) { set(server); - } else if (paint->colorSet) { - set(paint->value.color); + } else if (paint->isColor()) { + set(paint->getColor()); } else { clear(); } } else if (paint->isColor()) { - set(paint->value.color); + set(paint->getColor()); } else if (paint->isNone()) { clear(); } else if (paint->paintOrigin == SP_CSS_PAINT_ORIGIN_CONTEXT_FILL || @@ -259,7 +260,7 @@ NRStyleData::NRStyleData(SPStyle const *style, SPStyle const *context_style) } else if (style_td->text_decoration_color.set) { if(style->fill.isPaintserver() || style->fill.isColor()) { // SVG sets color specifically - text_decoration_fill.set(style->text_decoration_color.value.color); + text_decoration_fill.set(style->text_decoration_color.getColor()); } else { // No decoration fill because no text fill text_decoration_fill.clear(); @@ -274,7 +275,7 @@ NRStyleData::NRStyleData(SPStyle const *style, SPStyle const *context_style) } else if (style_td->text_decoration_color.set) { if(style->stroke.isPaintserver() || style->stroke.isColor()) { // SVG sets color specifically - text_decoration_stroke.set(style->text_decoration_color.value.color); + text_decoration_stroke.set(style->text_decoration_color.getColor()); } else { // No decoration stroke because no text stroke text_decoration_stroke.clear(); @@ -323,8 +324,7 @@ auto NRStyle::preparePaint(Inkscape::DrawingContext &dc, Inkscape::RenderContext } break; case NRStyleData::PaintType::COLOR: { - auto const &c = paint.color.v.c; - cp.pattern = CairoPatternUniqPtr(cairo_pattern_create_rgba(c[0], c[1], c[2], paint.opacity)); + cp.pattern = CairoPatternUniqPtr(ink_cairo_pattern_create(*paint.color, paint.opacity)); break; } default: diff --git a/src/display/nr-style.h b/src/display/nr-style.h index c3d889e612..ec81f93c90 100644 --- a/src/display/nr-style.h +++ b/src/display/nr-style.h @@ -18,7 +18,6 @@ #include #include #include <2geom/rect.h> -#include "color.h" #include "drawing-paintserver.h" #include "initlock.h" @@ -55,12 +54,12 @@ struct NRStyleData struct Paint { PaintType type = PaintType::NONE; - SPColor color = 0; + std::optional color; std::unique_ptr server; float opacity = 1.0; void clear(); - void set(SPColor const &c); + void set(Colors::Color const &c); void set(SPPaintServer *ps); void set(SPIPaint const *paint); bool ditherable() const; @@ -147,7 +146,7 @@ public: void applyTextDecorationStroke(DrawingContext &dc, CairoPatternUniqPtr const &cp) const; void invalidate(); - NRStyleData data; + NRStyleData data; private: struct CachedPattern diff --git a/src/document.cpp b/src/document.cpp index f0a306f10b..0f38893754 100644 --- a/src/document.cpp +++ b/src/document.cpp @@ -54,8 +54,7 @@ #include "inkscape.h" #include "layer-manager.h" #include "page-manager.h" -#include "profile-manager.h" -#include "colors/tracker.h" +#include "colors/document-cms.h" #include "rdf.h" #include "selection.h" @@ -147,9 +146,6 @@ SPDocument::SPDocument() : history_size = 0; seeking = false; - // Once things are set, hook in the manager - _profileManager = std::make_unique(this); - // For undo/redo undoStackObservers.add(*_event_log); @@ -165,14 +161,13 @@ SPDocument::SPDocument() : add_document_actions_effect(this); _page_manager = std::make_unique(this); - _color_tracker = std::make_unique(this); + _cms_manager = std::make_unique(this); } SPDocument::~SPDocument() { destroySignal.emit(); // kill/unhook this first - _profileManager.reset(); _desktop_activated_connection.disconnect(); if (partial) { diff --git a/src/document.h b/src/document.h index 913b9fdddf..bf8c2b68ff 100644 --- a/src/document.h +++ b/src/document.h @@ -83,9 +83,8 @@ namespace Inkscape { class Event; class EventLog; class PageManager; - class ProfileManager; namespace Colors { - class Tracker; + class DocumentCMS; } class Selection; class UndoStackObserver; @@ -171,15 +170,15 @@ public: Inkscape::PageManager& getPageManager() { return *_page_manager; } const Inkscape::PageManager& getPageManager() const { return *_page_manager; } - Inkscape::Colors::Tracker &getColorTracker() { return *_color_tracker; } - const Inkscape::Colors::Tracker &getColorTracker() const { return *_color_tracker; } + Inkscape::Colors::DocumentCMS &getDocumentCMS() { return *_cms_manager; } + const Inkscape::Colors::DocumentCMS &getDocumentCMS() const { return *_cms_manager; } private: void _importDefsNode(SPDocument *source, Inkscape::XML::Node *defs, Inkscape::XML::Node *target_defs); SPObject *_activexmltree; std::unique_ptr _page_manager; - std::unique_ptr _color_tracker; + std::unique_ptr _cms_manager; std::queue pending_resource_changes; @@ -197,9 +196,7 @@ public: /******** Getters and Setters **********/ // Document structure ----------------- - Inkscape::ProfileManager &getProfileManager() const { return *_profileManager; } Avoid::Router* getRouter() const { return _router.get(); } - /** Returns our SPRoot */ SPRoot *getRoot() { return root; } @@ -373,7 +370,6 @@ public: private: // Document ------------------------------ - std::unique_ptr _profileManager; // Color profile. std::unique_ptr _router; // Instance of the connector router std::unique_ptr _selection; diff --git a/src/doxygen-main.dox b/src/doxygen-main.dox index 9f51b0eae5..6da5449cf1 100644 --- a/src/doxygen-main.dox +++ b/src/doxygen-main.dox @@ -320,7 +320,6 @@ namespace XML {} */ /** \page Rendering Rendering Related Classes and Files * - * SPColor [\ref color.cpp, \ref color.h, \ref color-rgba.h] * [\ref geom.cpp] [\ref mod360.cpp] */ /** \page OtherServices Classes and Files From Other Services diff --git a/src/extension/extension.cpp b/src/extension/extension.cpp index daefbc257b..642d39d6ae 100644 --- a/src/extension/extension.cpp +++ b/src/extension/extension.cpp @@ -784,7 +784,7 @@ Extension::get_param_optiongroup_contains(char const *name, char const *value) c Look up in the parameters list, const then execute the function on that found parameter. */ -std::uint32_t +Colors::Color Extension::get_param_color(char const *name) const { const InxParameter *param; @@ -880,12 +880,12 @@ Extension::set_param_optiongroup(char const *name, char const *value) Look up in the parameters list, const then execute the function on that found parameter. */ -std::uint32_t -Extension::set_param_color(char const *name, const std::uint32_t color) +void +Extension::set_param_color(char const *name, Inkscape::Colors::Color const &color) { InxParameter *param; param = get_param(name); - return param->set_color(color); + param->set_color(color); } /** diff --git a/src/extension/extension.h b/src/extension/extension.h index ae11554ecd..3de7e2cbb2 100644 --- a/src/extension/extension.h +++ b/src/extension/extension.h @@ -29,6 +29,9 @@ #include "util/hybrid-pointer.h" +// Push color headers to all extensions +#include "colors/color.h" + namespace Glib { class ustring; } // namespace Glib @@ -262,7 +265,8 @@ public: char const *get_param_string (char const *name) const; char const *get_param_optiongroup (char const *name, char const *alt) const; char const *get_param_optiongroup (char const *name) const; - std::uint32_t get_param_color (char const *name) const; + + Colors::Color get_param_color(char const *name) const; bool get_param_optiongroup_contains (char const *name, char const *value) const; @@ -271,7 +275,7 @@ public: double set_param_float (char const *name, double value); char const *set_param_string (char const *name, char const *value); char const *set_param_optiongroup (char const *name, char const *value); - std::uint32_t set_param_color (char const *name, std::uint32_t color); + void set_param_color (char const *name, Colors::Color const &color); void set_param_any(char const *name, std::string const &value); void set_param_hidden(char const *name, bool hidden); diff --git a/src/extension/internal/bitmap/colorize.cpp b/src/extension/internal/bitmap/colorize.cpp index 2e208afbc4..03c2fa888e 100644 --- a/src/extension/internal/bitmap/colorize.cpp +++ b/src/extension/internal/bitmap/colorize.cpp @@ -14,8 +14,6 @@ #include "colorize.h" -#include "color.h" - #include #include @@ -23,22 +21,22 @@ namespace Inkscape { namespace Extension { namespace Internal { namespace Bitmap { - + +Colorize::Colorize() + : _color(0x000000ff) +{} + void Colorize::applyEffect(Magick::Image *image) { - float r = ((_color >> 24) & 0xff) / 255.0F; - float g = ((_color >> 16) & 0xff) / 255.0F; - float b = ((_color >> 8) & 0xff) / 255.0F; - float a = ((_color ) & 0xff) / 255.0F; - - Magick::ColorRGB mc(r,g,b); - - image->colorize(a * 100, mc); + if (auto color = _color.converted(Colors::Space::Type::RGB)) { + Magick::ColorRGB mc((*color)[0], (*color)[1], (*color)[2]); + image->colorize(color->getOpacity() * 100, mc); + } } void Colorize::refreshParameters(Inkscape::Extension::Effect *module) { - _color = module->get_param_color("color"); + _color = module->get_param_color("color"); } #include "../clear-n_.h" diff --git a/src/extension/internal/bitmap/colorize.h b/src/extension/internal/bitmap/colorize.h index e2db5791d8..f3e21e1614 100644 --- a/src/extension/internal/bitmap/colorize.h +++ b/src/extension/internal/bitmap/colorize.h @@ -11,20 +11,25 @@ #include "imagemagick.h" +#include "colors/color.h" + namespace Inkscape { namespace Extension { namespace Internal { namespace Bitmap { -class Colorize : public ImageMagick { -private: - guint32 _color; +class Colorize : public ImageMagick { public: + Colorize(); + void applyEffect(Magick::Image *image) override; void refreshParameters(Inkscape::Extension::Effect *module) override; static void init (); + +private: + Colors::Color _color; }; }; /* namespace Bitmap */ diff --git a/src/extension/internal/cairo-render-context.cpp b/src/extension/internal/cairo-render-context.cpp index bec401b8c7..209b96306a 100644 --- a/src/extension/internal/cairo-render-context.cpp +++ b/src/extension/internal/cairo-render-context.cpp @@ -34,6 +34,7 @@ #include #include +#include "colors/color.h" #include "display/drawing.h" #include "display/cairo-utils.h" #include "display/drawing-paintserver.h" @@ -1262,9 +1263,7 @@ CairoRenderContext::_createPatternForPaintServer(SPPaintServer const *const pain // add stops for (gint i = 0; unsigned(i) < lg->vector.stops.size(); i++) { - float rgb[3]; - lg->vector.stops[i].color.get_rgb_floatv(rgb); - cairo_pattern_add_color_stop_rgba(pattern, lg->vector.stops[i].offset, rgb[0], rgb[1], rgb[2], lg->vector.stops[i].opacity * alpha); + ink_cairo_pattern_add_color_stop(pattern, lg->vector.stops[i].offset, *lg->vector.stops[i].color, alpha); } } else if (auto rg = cast(paintserver_mutable)) { @@ -1282,9 +1281,7 @@ CairoRenderContext::_createPatternForPaintServer(SPPaintServer const *const pain // add stops for (gint i = 0; unsigned(i) < rg->vector.stops.size(); i++) { - float rgb[3]; - rg->vector.stops[i].color.get_rgb_floatv(rgb); - cairo_pattern_add_color_stop_rgba(pattern, rg->vector.stops[i].offset, rgb[0], rgb[1], rgb[2], rg->vector.stops[i].opacity * alpha); + ink_cairo_pattern_add_color_stop(pattern, rg->vector.stops[i].offset, *rg->vector.stops[i].color, alpha); } } else if (auto mg = cast(paintserver_mutable)) { pattern = mg->create_drawing_paintserver()->create_pattern(_cr, pbox, 1.0); @@ -1368,12 +1365,8 @@ void CairoRenderContext::_setFillStyle(SPStyle const *const style, Geom::OptRect cairo_set_source(_cr, pattern); cairo_pattern_destroy(pattern); } - } else if (style->fill.colorSet) { - float rgb[3]; - style->fill.value.color.get_rgb_floatv(rgb); - - cairo_set_source_rgba(_cr, rgb[0], rgb[1], rgb[2], alpha); - + } else if (style->fill.isColor()) { + ink_cairo_set_source_color(_cr, style->fill.getColor(), alpha); } else { // unset fill is black g_assert(!style->fill.set || (paint_server && !paint_server->isValid())); @@ -1386,10 +1379,7 @@ void CairoRenderContext::_setStrokeStyle(SPStyle const *style, Geom::OptRect con { float const alpha = _mergedOpacity(SP_SCALE24_TO_FLOAT(style->stroke_opacity.value)); if (style->stroke.isColor() || (style->stroke.isPaintserver() && !style->getStrokePaintServer()->isValid())) { - float rgb[3]; - style->stroke.value.color.get_rgb_floatv(rgb); - - cairo_set_source_rgba(_cr, rgb[0], rgb[1], rgb[2], alpha); + ink_cairo_set_source_color(_cr, style->stroke.getColor(), alpha); } else { g_assert( style->stroke.isPaintserver() || is(SP_STYLE_STROKE_SERVER(style)) diff --git a/src/extension/internal/emf-inout.cpp b/src/extension/internal/emf-inout.cpp index 687a5326ad..dd3e443572 100644 --- a/src/extension/internal/emf-inout.cpp +++ b/src/extension/internal/emf-inout.cpp @@ -31,6 +31,7 @@ #include "emf-inout.h" #include "clear-n_.h" +#include "colors/color.h" #include "display/drawing-item.h" #include "display/drawing.h" #include "document.h" @@ -773,31 +774,27 @@ Emf::output_style(PEMF_CALLBACK_DATA d, int iType) SVGOStringStream tmp_style; char tmp[1024] = {0}; - float fill_rgb[3]; - d->dc[d->level].style.fill.value.color.get_rgb_floatv(fill_rgb); - float stroke_rgb[3]; - d->dc[d->level].style.stroke.value.color.get_rgb_floatv(stroke_rgb); + auto fill = d->dc[d->level].style.fill.getColor(); + auto stroke = d->dc[d->level].style.stroke.getColor(); // for U_EMR_BITBLT with no image, try to approximate some of these operations/ // Assume src color is "white" if(d->dwRop3){ switch(d->dwRop3){ case U_PATINVERT: // invert pattern - fill_rgb[0] = 1.0 - fill_rgb[0]; - fill_rgb[1] = 1.0 - fill_rgb[1]; - fill_rgb[2] = 1.0 - fill_rgb[2]; + fill.set("black"); break; case U_SRCINVERT: // treat all of these as black case U_DSTINVERT: case U_BLACKNESS: case U_SRCERASE: case U_NOTSRCCOPY: - fill_rgb[0]=fill_rgb[1]=fill_rgb[2]=0.0; + fill.set("black"); break; case U_SRCCOPY: // treat all of these as white case U_NOTSRCERASE: case U_WHITENESS: - fill_rgb[0]=fill_rgb[1]=fill_rgb[2]=1.0; + fill.set("white"); break; case U_SRCPAINT: // use the existing color case U_SRCAND: @@ -817,19 +814,15 @@ Emf::output_style(PEMF_CALLBACK_DATA d, int iType) // pen color through. switch(d->dwRop2){ case U_R2_BLACK: - fill_rgb[0] = fill_rgb[1] = fill_rgb[2] = 0.0; - stroke_rgb[0]= stroke_rgb[1]= stroke_rgb[2] = 0.0; + fill.set("black"); + stroke.set("black"); break; case U_R2_NOTMERGEPEN: case U_R2_MASKNOTPEN: break; case U_R2_NOTCOPYPEN: - fill_rgb[0] = 1.0 - fill_rgb[0]; - fill_rgb[1] = 1.0 - fill_rgb[1]; - fill_rgb[2] = 1.0 - fill_rgb[2]; - stroke_rgb[0] = 1.0 - stroke_rgb[0]; - stroke_rgb[1] = 1.0 - stroke_rgb[1]; - stroke_rgb[2] = 1.0 - stroke_rgb[2]; + fill.invert(); + stroke.invert(); break; case U_R2_MASKPENNOT: case U_R2_NOT: @@ -844,8 +837,8 @@ Emf::output_style(PEMF_CALLBACK_DATA d, int iType) case U_R2_MERGEPEN: break; case U_R2_WHITE: - fill_rgb[0] = fill_rgb[1] = fill_rgb[2] = 1.0; - stroke_rgb[0]= stroke_rgb[1]= stroke_rgb[2] = 1.0; + fill.set("white"); + stroke.set("white"); break; default: break; @@ -871,14 +864,8 @@ Emf::output_style(PEMF_CALLBACK_DATA d, int iType) case DRAW_LINEAR_GRADIENT: case DRAW_PAINT: default: // <-- this should never happen, but just in case... - snprintf( - tmp, 1023, - "fill:#%02x%02x%02x;", - SP_COLOR_F_TO_U(fill_rgb[0]), - SP_COLOR_F_TO_U(fill_rgb[1]), - SP_COLOR_F_TO_U(fill_rgb[2]) - ); - tmp_style << tmp; + fill.convert(Colors::Space::Type::RGB); + tmp_style << "fill:" << fill.toString(false).c_str() << ";"; break; } snprintf( @@ -897,11 +884,7 @@ Emf::output_style(PEMF_CALLBACK_DATA d, int iType) (d->dc[d->level].fill_mode == d->dc[d->level].stroke_mode) && ( (d->dc[d->level].fill_mode != DRAW_PAINT) || - ( - (fill_rgb[0]==stroke_rgb[0]) && - (fill_rgb[1]==stroke_rgb[1]) && - (fill_rgb[2]==stroke_rgb[2]) - ) + fill == stroke ) ){ d->dc[d->level].stroke_set = false; @@ -924,14 +907,8 @@ Emf::output_style(PEMF_CALLBACK_DATA d, int iType) case DRAW_LINEAR_GRADIENT: case DRAW_PAINT: default: // <-- this should never happen, but just in case... - snprintf( - tmp, 1023, - "stroke:#%02x%02x%02x;", - SP_COLOR_F_TO_U(stroke_rgb[0]), - SP_COLOR_F_TO_U(stroke_rgb[1]), - SP_COLOR_F_TO_U(stroke_rgb[2]) - ); - tmp_style << tmp; + stroke.convert(Colors::Space::Type::RGB); + tmp_style << "stroke:" << stroke.toString(false).c_str() << ";"; break; } tmp_style << "stroke-width:" << @@ -1140,15 +1117,9 @@ Emf::select_pen(PEMF_CALLBACK_DATA d, int index) d->level = cur_level; d->dc[d->level].style.stroke_width.value = pen_width; } - - double r, g, b; - r = SP_COLOR_U_TO_F( U_RGBAGetR(pEmr->lopn.lopnColor) ); - g = SP_COLOR_U_TO_F( U_RGBAGetG(pEmr->lopn.lopnColor) ); - b = SP_COLOR_U_TO_F( U_RGBAGetB(pEmr->lopn.lopnColor) ); - d->dc[d->level].style.stroke.value.color.set( r, g, b ); + d->dc[d->level].style.stroke.setColor(Colors::Color(U_RGB_COMPOSE(pEmr->lopn.lopnColor), false)); } - void Emf::select_extpen(PEMF_CALLBACK_DATA d, int index) { @@ -1267,11 +1238,7 @@ Emf::select_extpen(PEMF_CALLBACK_DATA d, int index) d->dc[d->level].stroke_set = true; if (pEmr->elp.elpPenStyle == U_PS_NULL) { // draw nothing, but fill out all the values with something - double r, g, b; - r = SP_COLOR_U_TO_F( U_RGBAGetR(d->dc[d->level].textColor)); - g = SP_COLOR_U_TO_F( U_RGBAGetG(d->dc[d->level].textColor)); - b = SP_COLOR_U_TO_F( U_RGBAGetB(d->dc[d->level].textColor)); - d->dc[d->level].style.stroke.value.color.set( r, g, b ); + d->dc[d->level].style.stroke.setColor(Colors::Color(U_RGB_COMPOSE(d->dc[d->level].textColor), false)); d->dc[d->level].style.stroke_width.value = 0; d->dc[d->level].stroke_set = false; d->dc[d->level].stroke_mode = DRAW_PAINT; @@ -1293,11 +1260,7 @@ Emf::select_extpen(PEMF_CALLBACK_DATA d, int index) } if( pEmr->elp.elpBrushStyle == U_BS_SOLID){ - double r, g, b; - r = SP_COLOR_U_TO_F( U_RGBAGetR(pEmr->elp.elpColor) ); - g = SP_COLOR_U_TO_F( U_RGBAGetG(pEmr->elp.elpColor) ); - b = SP_COLOR_U_TO_F( U_RGBAGetB(pEmr->elp.elpColor) ); - d->dc[d->level].style.stroke.value.color.set( r, g, b ); + d->dc[d->level].style.stroke.setColor(Colors::Color(U_RGB_COMPOSE(pEmr->elp.elpColor), false)); d->dc[d->level].stroke_mode = DRAW_PAINT; d->dc[d->level].stroke_set = true; } @@ -1313,11 +1276,7 @@ Emf::select_extpen(PEMF_CALLBACK_DATA d, int index) d->dc[d->level].stroke_set = true; } else { // U_BS_PATTERN and anything strange that falls in, stroke is solid textColor - double r, g, b; - r = SP_COLOR_U_TO_F( U_RGBAGetR(d->dc[d->level].textColor)); - g = SP_COLOR_U_TO_F( U_RGBAGetG(d->dc[d->level].textColor)); - b = SP_COLOR_U_TO_F( U_RGBAGetB(d->dc[d->level].textColor)); - d->dc[d->level].style.stroke.value.color.set( r, g, b ); + d->dc[d->level].style.stroke.setColor(Colors::Color(U_RGB_COMPOSE(d->dc[d->level].textColor))); d->dc[d->level].stroke_mode = DRAW_PAINT; d->dc[d->level].stroke_set = true; } @@ -1336,11 +1295,7 @@ Emf::select_brush(PEMF_CALLBACK_DATA d, int index) if(iType == U_EMR_CREATEBRUSHINDIRECT){ PU_EMRCREATEBRUSHINDIRECT pEmr = (PU_EMRCREATEBRUSHINDIRECT) d->emf_obj[index].lpEMFR; if( pEmr->lb.lbStyle == U_BS_SOLID){ - double r, g, b; - r = SP_COLOR_U_TO_F( U_RGBAGetR(pEmr->lb.lbColor) ); - g = SP_COLOR_U_TO_F( U_RGBAGetG(pEmr->lb.lbColor) ); - b = SP_COLOR_U_TO_F( U_RGBAGetB(pEmr->lb.lbColor) ); - d->dc[d->level].style.fill.value.color.set( r, g, b ); + d->dc[d->level].style.fill.setColor(Colors::Color(U_RGB_COMPOSE(pEmr->lb.lbColor), false)); d->dc[d->level].fill_mode = DRAW_PAINT; d->dc[d->level].fill_set = true; } @@ -1355,11 +1310,7 @@ Emf::select_brush(PEMF_CALLBACK_DATA d, int index) PU_EMRCREATEDIBPATTERNBRUSHPT pEmr = (PU_EMRCREATEDIBPATTERNBRUSHPT) d->emf_obj[index].lpEMFR; tidx = add_image(d, (void *) pEmr, pEmr->cbBits, pEmr->cbBmi, pEmr->iUsage, pEmr->offBits, pEmr->offBmi); if(tidx == U_EMR_INVALID){ // This happens if createmonobrush has a DIB that isn't monochrome - double r, g, b; - r = SP_COLOR_U_TO_F( U_RGBAGetR(d->dc[d->level].textColor)); - g = SP_COLOR_U_TO_F( U_RGBAGetG(d->dc[d->level].textColor)); - b = SP_COLOR_U_TO_F( U_RGBAGetB(d->dc[d->level].textColor)); - d->dc[d->level].style.fill.value.color.set( r, g, b ); + d->dc[d->level].style.fill.setColor(Colors::Color(U_RGB_COMPOSE(d->dc[d->level].textColor), false)); d->dc[d->level].fill_mode = DRAW_PAINT; } else { @@ -2509,8 +2460,7 @@ std::cout << "BEFORE DRAW" val = 255.0 / 255.0; break; } - d->dc[d->level].style.fill.value.color.set( val, val, val ); - + d->dc[d->level].style.fill.setColor(Colors::Color(SP_RGBA32_F_COMPOSE(val, val, val, 1.0), false)); d->dc[d->level].fill_mode = DRAW_PAINT; d->dc[d->level].fill_set = true; break; @@ -2525,7 +2475,7 @@ std::cout << "BEFORE DRAW" float val = index == U_BLACK_PEN ? 0 : 1; d->dc[d->level].style.stroke_dasharray.set = false; d->dc[d->level].style.stroke_width.value = 1.0; - d->dc[d->level].style.stroke.value.color.set( val, val, val ); + d->dc[d->level].style.stroke.setColor(Colors::Color(SP_RGBA32_F_COMPOSE(val, val, val, 1.0), false)); d->dc[d->level].stroke_mode = DRAW_PAINT; d->dc[d->level].stroke_set = true; diff --git a/src/extension/internal/emf-print.cpp b/src/extension/internal/emf-print.cpp index 6db324b4b5..2954d45d1c 100644 --- a/src/extension/internal/emf-print.cpp +++ b/src/extension/internal/emf-print.cpp @@ -65,6 +65,8 @@ #include "util/units.h" +using namespace Inkscape::Colors; + namespace Inkscape { namespace Extension { namespace Internal { @@ -280,7 +282,7 @@ unsigned int PrintEmf::begin(Inkscape::Extension::Print *mod, SPDocument *doc) g_error("Fatal programming error in PrintEmf::begin at U_EMRSETTEXTALIGN_set"); } - htextcolor_rgb[0] = htextcolor_rgb[1] = htextcolor_rgb[2] = 0.0; + htextcolor_rgb = Color(0x000000ff); rec = U_EMRSETTEXTCOLOR_set(U_RGB(0, 0, 0)); if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { g_error("Fatal programming error in PrintEmf::begin at U_EMRSETTEXTCOLOR_set"); @@ -322,7 +324,6 @@ unsigned int PrintEmf::finish(Inkscape::Extension::Print * /*mod*/) // fcolor is defined when gradients are being expanded, it is the color of one stripe or ring. int PrintEmf::create_brush(SPStyle const *style, PU_COLORREF fcolor) { - float rgb[3]; char *rec; U_LOGBRUSH lb; uint32_t brush, fmode; @@ -361,12 +362,12 @@ int PrintEmf::create_brush(SPStyle const *style, PU_COLORREF fcolor) opacity = 0.0; // basically the same as no fill } #endif - style->fill.value.color.get_rgb_floatv(rgb); + auto rgb = *style->fill.getColor().converted(Space::Type::RGB); hatchColor = U_RGB(255 * rgb[0], 255 * rgb[1], 255 * rgb[2]); fmode = style->fill_rule.computed == 0 ? U_WINDING : (style->fill_rule.computed == 2 ? U_ALTERNATE : U_ALTERNATE); } else if (is(SP_STYLE_FILL_SERVER(style))) { // must be paint-server - SPPaintServer *paintserver = style->fill.value.href->getObject(); + SPPaintServer *paintserver = style->fill.href->getObject(); auto pat = cast(paintserver); double dwidth = pat->width(); double dheight = pat->height(); @@ -392,7 +393,7 @@ int PrintEmf::create_brush(SPStyle const *style, PU_COLORREF fcolor) brushStyle = U_BS_HATCHED; } else if (is(SP_STYLE_FILL_SERVER(style))) { // must be a gradient // currently we do not do anything with gradients, the code below just sets the color to the average of the stops - SPPaintServer *paintserver = style->fill.value.href->getObject(); + SPPaintServer *paintserver = style->fill.href->getObject(); SPLinearGradient *lg = nullptr; SPRadialGradient *rg = nullptr; @@ -548,10 +549,8 @@ int PrintEmf::create_pen(SPStyle const *style, const Geom::Affine &transform) bkColor = U_RGB(0, 0, 0); if (style) { - float rgb[3]; - if (is(SP_STYLE_STROKE_SERVER(style))) { // must be paint-server - SPPaintServer *paintserver = style->stroke.value.href->getObject(); + SPPaintServer *paintserver = style->stroke.href->getObject(); auto pat = cast(paintserver); double dwidth = pat->width(); double dheight = pat->height(); @@ -594,7 +593,7 @@ int PrintEmf::create_pen(SPStyle const *style, const Geom::Affine &transform) } else if (is(SP_STYLE_STROKE_SERVER(style))) { // must be a gradient // currently we do not do anything with gradients, the code below has no net effect. - SPPaintServer *paintserver = style->stroke.value.href->getObject(); + SPPaintServer *paintserver = style->stroke.href->getObject(); if (is(paintserver)) { auto lg = cast(paintserver); @@ -629,7 +628,7 @@ int PrintEmf::create_pen(SPStyle const *style, const Geom::Affine &transform) // default fill } } else if (style->stroke.isColor()) { // test last, always seems to be set, even for other types above - style->stroke.value.color.get_rgb_floatv(rgb); + auto rgb = *style->stroke.getColor().converted(Space::Type::RGB); brushStyle = U_BS_SOLID; hatchColor = U_RGB(255 * rgb[0], 255 * rgb[1], 255 * rgb[2]); hatchType = U_HS_SOLIDCLR; @@ -1151,23 +1150,17 @@ unsigned int PrintEmf::fill( */ destroy_pen(); //this sets the NULL_PEN, otherwise gradient slices may display with boundaries, see longer explanation below Geom::Path cutter; - float rgb[3]; U_COLORREF wc, c1, c2; FillRule frb = SPWR_to_LVFR((SPWindRule) style->fill_rule.computed); double doff, doff_base, doff_range; double divisions = 128.0; int nstops; int istop = 1; - float opa; // opacity at stop SPRadialGradient *tg = (SPRadialGradient *)(gv.grad); // linear/radial are the same here nstops = tg->vector.stops.size(); - tg->vector.stops[0].color.get_rgb_floatv(rgb); - opa = tg->vector.stops[0].opacity; // first stop - c1 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); - tg->vector.stops[nstops - 1].color.get_rgb_floatv(rgb); - opa = tg->vector.stops[nstops - 1].opacity; // last stop - c2 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); + c1 = toColorRef(tg->vector.stops[0].color); + c2 = toColorRef(tg->vector.stops[nstops - 1].color); doff = 0.0; doff_base = 0.0; @@ -1195,9 +1188,7 @@ unsigned int PrintEmf::fill( (void) create_brush(style, &wc); print_pathv(pathvr, fill_transform); - tg->vector.stops[istop].color.get_rgb_floatv(rgb); - opa = tg->vector.stops[istop].opacity; - c2 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); + c2 = toColorRef(tg->vector.stops[istop].color); for (start = 0.0; start < range; start += step, doff += 1. / divisions) { stop = start + step + overlap; @@ -1221,9 +1212,7 @@ unsigned int PrintEmf::fill( doff_base = doff_range; doff_range = tg->vector.stops[istop].offset; // next or last stop c1 = c2; - tg->vector.stops[istop].color.get_rgb_floatv(rgb); - opa = tg->vector.stops[istop].opacity; - c2 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); + c2 = toColorRef(tg->vector.stops[istop].color); } } } else if (gv.mode == DRAW_LINEAR_GRADIENT) { @@ -1287,9 +1276,7 @@ unsigned int PrintEmf::fill( rcb.top = round(outUL[Y]); rcb.right = round(outLR[X]); rcb.bottom = round(outLR[Y]); - tg->vector.stops[istop].color.get_rgb_floatv(rgb); - opa = tg->vector.stops[istop].opacity; - c2 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); + c2 = toColorRef(tg->vector.stops[istop].color); if(rectDir == 2 || rectDir == 4){ // gradient is reversed, so swap colors ut[0] = make_trivertex(outUL, c2); @@ -1337,9 +1324,7 @@ unsigned int PrintEmf::fill( pathvr = sp_pathvector_boolop(pathvc, pathv, bool_op_inters, (FillRule) fill_nonZero, frb); print_pathv(pathvr, fill_transform); - tg->vector.stops[istop].color.get_rgb_floatv(rgb); - opa = tg->vector.stops[istop].opacity; - c2 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); + c2 = toColorRef(tg->vector.stops[istop].color); for (start = 0.0; start < range; start += step, doff += 1. / divisions) { stop = start + step + overlap; @@ -1362,9 +1347,7 @@ unsigned int PrintEmf::fill( doff_base = doff_range; doff_range = tg->vector.stops[istop].offset; // next or last stop c1 = c2; - tg->vector.stops[istop].color.get_rgb_floatv(rgb); - opa = tg->vector.stops[istop].opacity; - c2 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); + c2 = toColorRef(tg->vector.stops[istop].color); } } } @@ -2082,12 +2065,11 @@ unsigned int PrintEmf::text(Inkscape::Extension::Print * /*mod*/, char const *te g_error("Fatal programming error in PrintEmf::text at selectobject_set"); } - float rgb[3]; - style->fill.value.color.get_rgb_floatv(rgb); + auto rgb = style->fill.getColor(); // only change the text color when it needs to be changed - if (memcmp(htextcolor_rgb, rgb, 3 * sizeof(float))) { - memcpy(htextcolor_rgb, rgb, 3 * sizeof(float)); - rec = U_EMRSETTEXTCOLOR_set(U_RGB(255 * rgb[0], 255 * rgb[1], 255 * rgb[2])); + if (htextcolor_rgb != rgb) { + htextcolor_rgb = rgb; + rec = U_EMRSETTEXTCOLOR_set(toColorRef(rgb)); if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { g_error("Fatal programming error in PrintEmf::text at U_EMRSETTEXTCOLOR_set"); } diff --git a/src/extension/internal/filter/bevels.h b/src/extension/internal/filter/bevels.h index 6eaa5dcedb..80cf5ab9cb 100644 --- a/src/extension/internal/filter/bevels.h +++ b/src/extension/internal/filter/bevels.h @@ -81,30 +81,21 @@ DiffuseLight::get_filter_text (Inkscape::Extension::Extension * ext) std::ostringstream smooth; std::ostringstream elevation; std::ostringstream azimuth; - std::ostringstream r; - std::ostringstream g; - std::ostringstream b; - std::ostringstream a; smooth << ext->get_param_float("smooth"); elevation << ext->get_param_int("elevation"); azimuth << ext->get_param_int("azimuth"); - guint32 color = ext->get_param_color("color"); - - r << ((color >> 24) & 0xff); - g << ((color >> 16) & 0xff); - b << ((color >> 8) & 0xff); - a << (color & 0xff) / 255.0F; + auto color = ext->get_param_color("color"); _filter = g_strdup_printf( "\n" "\n" - "\n" + "\n" "\n" "\n" "\n" - "\n" - "\n", smooth.str().c_str(), r.str().c_str(), g.str().c_str(), b.str().c_str(), elevation.str().c_str(), azimuth.str().c_str(), a.str().c_str()); + "\n" + "\n", smooth.str().c_str(), color.toString(false).c_str(), elevation.str().c_str(), azimuth.str().c_str(), color.getOpacity()); return _filter; }; /* DiffuseLight filter */ @@ -165,32 +156,23 @@ MatteJelly::get_filter_text (Inkscape::Extension::Extension * ext) std::ostringstream bright; std::ostringstream elevation; std::ostringstream azimuth; - std::ostringstream r; - std::ostringstream g; - std::ostringstream b; - std::ostringstream a; smooth << ext->get_param_float("smooth"); bright << ext->get_param_float("bright"); elevation << ext->get_param_int("elevation"); azimuth << ext->get_param_int("azimuth"); - guint32 color = ext->get_param_color("color"); - - r << ((color >> 24) & 0xff); - g << ((color >> 16) & 0xff); - b << ((color >> 8) & 0xff); - a << (color & 0xff) / 255.0F; + auto color = ext->get_param_color("color"); _filter = g_strdup_printf( "\n" "\n" "\n" - "\n" + "\n" "\n" "\n" - "\n" + "\n" "\n" - "\n", smooth.str().c_str(), bright.str().c_str(), r.str().c_str(), g.str().c_str(), b.str().c_str(), elevation.str().c_str(), azimuth.str().c_str(), a.str().c_str()); + "\n", smooth.str().c_str(), bright.str().c_str(), color.toString().c_str(), elevation.str().c_str(), azimuth.str().c_str(), color.getOpacity()); return _filter; }; /* MatteJelly filter */ @@ -251,31 +233,22 @@ SpecularLight::get_filter_text (Inkscape::Extension::Extension * ext) std::ostringstream bright; std::ostringstream elevation; std::ostringstream azimuth; - std::ostringstream r; - std::ostringstream g; - std::ostringstream b; - std::ostringstream a; smooth << ext->get_param_float("smooth"); bright << ext->get_param_float("bright"); elevation << ext->get_param_int("elevation"); azimuth << ext->get_param_int("azimuth"); - guint32 color = ext->get_param_color("color"); + auto color = ext->get_param_color("color"); - r << ((color >> 24) & 0xff); - g << ((color >> 16) & 0xff); - b << ((color >> 8) & 0xff); - a << (color & 0xff) / 255.0F; - _filter = g_strdup_printf( "\n" "\n" - "\n" + "\n" "\n" "\n" - "\n" + "\n" "\n" - "\n", smooth.str().c_str(), bright.str().c_str(), r.str().c_str(), g.str().c_str(), b.str().c_str(), elevation.str().c_str(), azimuth.str().c_str(), a.str().c_str()); + "\n", smooth.str().c_str(), bright.str().c_str(), color.toString(false).c_str(), elevation.str().c_str(), azimuth.str().c_str(), color.getOpacity()); return _filter; }; /* SpecularLight filter */ diff --git a/src/extension/internal/filter/blurs.h b/src/extension/internal/filter/blurs.h index 0d9966b06a..8d56776d7f 100644 --- a/src/extension/internal/filter/blurs.h +++ b/src/extension/internal/filter/blurs.h @@ -387,10 +387,6 @@ ImageBlur::get_filter_text (Inkscape::Extension::Extension * ext) std::ostringstream dilat; std::ostringstream erosion; std::ostringstream opacity; - std::ostringstream r; - std::ostringstream g; - std::ostringstream b; - std::ostringstream a; std::ostringstream blend; std::ostringstream background; @@ -400,11 +396,7 @@ ImageBlur::get_filter_text (Inkscape::Extension::Extension * ext) erosion << -ext->get_param_float("erosion"); opacity << ext->get_param_float("opacity"); - guint32 color = ext->get_param_color("color"); - r << ((color >> 24) & 0xff); - g << ((color >> 16) & 0xff); - b << ((color >> 8) & 0xff); - a << (color & 0xff) / 255.0F; + auto color = ext->get_param_color("color"); blend << ext->get_param_optiongroup("blend"); if (ext->get_param_bool("background")) { @@ -416,14 +408,14 @@ ImageBlur::get_filter_text (Inkscape::Extension::Extension * ext) // clang-format off _filter = g_strdup_printf( "\n" - "\n" + "\n" "\n" "\n" "\n" "\n" "\n" "\n" - "\n", a.str().c_str(), r.str().c_str(), g.str().c_str(), b.str().c_str(), + "\n", color.getOpacity(), color.toString(false).c_str(), hblur.str().c_str(), vblur.str().c_str(), dilat.str().c_str(), erosion.str().c_str(), background.str().c_str(), blend.str().c_str(), opacity.str().c_str()); // clang-format on diff --git a/src/extension/internal/filter/bumps.h b/src/extension/internal/filter/bumps.h index 85808b0fe8..3d191993b8 100644 --- a/src/extension/internal/filter/bumps.h +++ b/src/extension/internal/filter/bumps.h @@ -169,13 +169,7 @@ Bump::get_filter_text (Inkscape::Extension::Extension * ext) std::ostringstream lightStart; std::ostringstream lightOptions; std::ostringstream lightEnd; - - std::ostringstream floodRed; - std::ostringstream floodGreen; - std::ostringstream floodBlue; - std::ostringstream floodAlpha; std::ostringstream colorize; - simplifyImage << ext->get_param_float("simplifyImage"); simplifyBump << ext->get_param_float("simplifyBump"); @@ -185,8 +179,8 @@ Bump::get_filter_text (Inkscape::Extension::Extension * ext) crop << ext->get_param_float("crop"); blend << ext->get_param_optiongroup("blend"); - guint32 lightingColor = ext->get_param_color("lightingColor"); - guint32 imageColor = ext->get_param_color("imageColor"); + auto lightingColor = ext->get_param_color("lightingColor"); + auto imageColor = ext->get_param_color("imageColor"); if (ext->get_param_bool("background")) { bumpSource << "BackgroundImage" ; @@ -197,15 +191,13 @@ Bump::get_filter_text (Inkscape::Extension::Extension * ext) const gchar *lightType = ext->get_param_optiongroup("lightType"); if ((g_ascii_strcasecmp("specular", lightType) == 0)) { // Specular - lightStart << "> 24) & 0xff) << "," - << ((lightingColor >> 16) & 0xff) << "," << ((lightingColor >> 8) & 0xff) << ")\" surfaceScale=\"" + lightStart << "get_param_float("height") << "\" specularConstant=\"" << ext->get_param_float("lightness") << "\" specularExponent=\"" << ext->get_param_int("precision") << "\" result=\"lighting\">"; lightEnd << ""; } else { // Diffuse - lightStart << "> 24) & 0xff) << "," - << ((lightingColor >> 16) & 0xff) << "," << ((lightingColor >> 8) & 0xff) << ")\" surfaceScale=\"" + lightStart << "get_param_float("height") << "\" diffuseConstant=\"" << ext->get_param_float("lightness") << "\" result=\"lighting\">"; lightEnd << ""; @@ -230,11 +222,6 @@ Bump::get_filter_text (Inkscape::Extension::Extension * ext) << "\" />"; } - floodRed << ((imageColor >> 24) & 0xff); - floodGreen << ((imageColor >> 16) & 0xff); - floodBlue << ((imageColor >> 8) & 0xff); - floodAlpha << (imageColor & 0xff) / 255.0F; - if (ext->get_param_bool("colorize")) { colorize << "flood" ; } else { @@ -252,14 +239,14 @@ Bump::get_filter_text (Inkscape::Extension::Extension * ext) "%s\n" "%s\n" "%s\n" - "\n" + "\n" "\n" "\n" "\n" "\n", simplifyImage.str().c_str(), bumpSource.str().c_str(), red.str().c_str(), green.str().c_str(), blue.str().c_str(), crop.str().c_str(), simplifyBump.str().c_str(), lightStart.str().c_str(), lightOptions.str().c_str(), lightEnd.str().c_str(), - floodRed.str().c_str(), floodGreen.str().c_str(), floodBlue.str().c_str(), floodAlpha.str().c_str(), + imageColor.toString(false).c_str(), imageColor.getOpacity(), colorize.str().c_str(), blend.str().c_str()); // clang-format on @@ -397,16 +384,6 @@ WaxBump::get_filter_text (Inkscape::Extension::Extension * ext) std::ostringstream precision; std::ostringstream distantAzimuth; std::ostringstream distantElevation; - - std::ostringstream lightRed; - std::ostringstream lightGreen; - std::ostringstream lightBlue; - - std::ostringstream floodRed; - std::ostringstream floodGreen; - std::ostringstream floodBlue; - std::ostringstream floodAlpha; - std::ostringstream revert; std::ostringstream lightingblend; std::ostringstream highlightblend; @@ -429,16 +406,8 @@ WaxBump::get_filter_text (Inkscape::Extension::Extension * ext) distantAzimuth << ext->get_param_int("distantAzimuth"); distantElevation << ext->get_param_int("distantElevation"); - guint32 lightingColor = ext->get_param_color("lightingColor"); - lightRed << ((lightingColor >> 24) & 0xff); - lightGreen << ((lightingColor >> 16) & 0xff); - lightBlue << ((lightingColor >> 8) & 0xff); - - guint32 imageColor = ext->get_param_color("imageColor"); - floodRed << ((imageColor >> 24) & 0xff); - floodGreen << ((imageColor >> 16) & 0xff); - floodBlue << ((imageColor >> 8) & 0xff); - floodAlpha << (imageColor & 0xff) / 255.0F; + auto lightingColor = ext->get_param_color("lightingColor"); + auto imageColor = ext->get_param_color("imageColor"); if (ext->get_param_bool("revert")) { revert << "in" ; @@ -457,10 +426,10 @@ WaxBump::get_filter_text (Inkscape::Extension::Extension * ext) "\n" "\n" "\n" - "\n" + "\n" "\n" "\n" - "\n" + "\n" "\n" "\n" "\n" @@ -473,9 +442,9 @@ WaxBump::get_filter_text (Inkscape::Extension::Extension * ext) "\n" "\n", simplifyImage.str().c_str(), background.str().c_str(), bgopacity.str().c_str(), red.str().c_str(), green.str().c_str(), blue.str().c_str(), crop.str().c_str(), - floodRed.str().c_str(), floodGreen.str().c_str(), floodBlue.str().c_str(), floodAlpha.str().c_str(), + imageColor.toString(false).c_str(), imageColor.getOpacity(), revert.str().c_str(), simplifyBump.str().c_str(), - lightRed.str().c_str(), lightGreen.str().c_str(), lightBlue.str().c_str(), + lightingColor.toString(false).c_str(), lightness.str().c_str(), height.str().c_str(), precision.str().c_str(), distantElevation.str().c_str(), distantAzimuth.str().c_str(), lightingblend.str().c_str(), transparency.str().c_str(), highlightblend.str().c_str() ); diff --git a/src/extension/internal/filter/color.h b/src/extension/internal/filter/color.h index 099ceac758..44bd0f28d4 100644 --- a/src/extension/internal/filter/color.h +++ b/src/extension/internal/filter/color.h @@ -197,10 +197,6 @@ ChannelPaint::get_filter_text (Inkscape::Extension::Extension * ext) std::ostringstream blue; std::ostringstream alpha; std::ostringstream invert; - std::ostringstream floodRed; - std::ostringstream floodGreen; - std::ostringstream floodBlue; - std::ostringstream floodAlpha; saturation << ext->get_param_float("saturation"); red << ext->get_param_float("red"); @@ -208,11 +204,7 @@ ChannelPaint::get_filter_text (Inkscape::Extension::Extension * ext) blue << ext->get_param_float("blue"); alpha << ext->get_param_float("alpha"); - guint32 color = ext->get_param_color("color"); - floodRed << ((color >> 24) & 0xff); - floodGreen << ((color >> 16) & 0xff); - floodBlue << ((color >> 8) & 0xff); - floodAlpha << (color & 0xff) / 255.0F; + auto flood = ext->get_param_color("color"); if (ext->get_param_bool("invert")) { invert << "in"; @@ -225,7 +217,7 @@ ChannelPaint::get_filter_text (Inkscape::Extension::Extension * ext) "\n" "\n" "\n" - "\n" + "\n" "\n" "\n" "\n" @@ -233,8 +225,8 @@ ChannelPaint::get_filter_text (Inkscape::Extension::Extension * ext) "\n" "\n" "\n", saturation.str().c_str(), red.str().c_str(), green.str().c_str(), - blue.str().c_str(), alpha.str().c_str(), floodRed.str().c_str(), - floodGreen.str().c_str(), floodBlue.str().c_str(), floodAlpha.str().c_str(), + blue.str().c_str(), alpha.str().c_str(), + flood.toString(false).c_str(), flood.getOpacity(), invert.str().c_str() ); // clang-format on @@ -454,22 +446,13 @@ Colorize::get_filter_text (Inkscape::Extension::Extension * ext) { if (_filter != nullptr) g_free((void *)_filter); - std::ostringstream a; - std::ostringstream r; - std::ostringstream g; - std::ostringstream b; std::ostringstream hlight; std::ostringstream nlight; std::ostringstream duotone; std::ostringstream blend1; std::ostringstream blend2; - guint32 color = ext->get_param_color("color"); - r << ((color >> 24) & 0xff); - g << ((color >> 16) & 0xff); - b << ((color >> 8) & 0xff); - a << (color & 0xff) / 255.0F; - + auto color = ext->get_param_color("color"); hlight << ext->get_param_float("hlight"); nlight << ext->get_param_float("nlight"); blend1 << ext->get_param_optiongroup("blend1"); @@ -485,13 +468,13 @@ Colorize::get_filter_text (Inkscape::Extension::Extension * ext) "\n" "\n" "\n" - "\n" + "\n" "\n" "\n" "\n" "\n" "\n", hlight.str().c_str(), nlight.str().c_str(), duotone.str().c_str(), - a.str().c_str(), r.str().c_str(), g.str().c_str(), b.str().c_str(), + color.getOpacity(), color.toString(false).c_str(), blend1.str().c_str(), blend2.str().c_str() ); // clang-format on @@ -647,67 +630,51 @@ Duochrome::get_filter_text (Inkscape::Extension::Extension * ext) { if (_filter != nullptr) g_free((void *)_filter); - std::ostringstream a1; - std::ostringstream r1; - std::ostringstream g1; - std::ostringstream b1; - std::ostringstream a2; - std::ostringstream r2; - std::ostringstream g2; - std::ostringstream b2; + bool swapa = false; std::ostringstream fluo; std::ostringstream swap1; std::ostringstream swap2; - guint32 color1 = ext->get_param_color("color1"); - guint32 color2 = ext->get_param_color("color2"); + auto color1 = ext->get_param_color("color1"); + auto color2 = ext->get_param_color("color2"); double fluorescence = ext->get_param_float("fluo"); const gchar *swaptype = ext->get_param_optiongroup("swap"); - r1 << ((color1 >> 24) & 0xff); - g1 << ((color1 >> 16) & 0xff); - b1 << ((color1 >> 8) & 0xff); - r2 << ((color2 >> 24) & 0xff); - g2 << ((color2 >> 16) & 0xff); - b2 << ((color2 >> 8) & 0xff); fluo << fluorescence; if ((g_ascii_strcasecmp("full", swaptype) == 0)) { swap1 << "in"; swap2 << "out"; - a1 << (color1 & 0xff) / 255.0F; - a2 << (color2 & 0xff) / 255.0F; } else if ((g_ascii_strcasecmp("color", swaptype) == 0)) { swap1 << "in"; swap2 << "out"; - a1 << (color2 & 0xff) / 255.0F; - a2 << (color1 & 0xff) / 255.0F; + swapa = true; } else if ((g_ascii_strcasecmp("alpha", swaptype) == 0)) { swap1 << "out"; swap2 << "in"; - a1 << (color2 & 0xff) / 255.0F; - a2 << (color1 & 0xff) / 255.0F; + swapa = true; } else { swap1 << "out"; swap2 << "in"; - a1 << (color1 & 0xff) / 255.0F; - a2 << (color2 & 0xff) / 255.0F; } + double a1 = swapa ? color2.getOpacity() : color1.getOpacity(); + double a2 = swapa ? color1.getOpacity() : color2.getOpacity(); + // clang-format off _filter = g_strdup_printf( "\n" "\n" - "\n" + "\n" "\n" - "\n" + "\n" "\n" "\n" "\n" "\n" "\n" "\n" - "\n", a1.str().c_str(), r1.str().c_str(), g1.str().c_str(), b1.str().c_str(), swap1.str().c_str(), - a2.str().c_str(), r2.str().c_str(), g2.str().c_str(), b2.str().c_str(), swap2.str().c_str(), + "\n", a1, color1.toString(false).c_str(), swap1.str().c_str(), + a2, color2.toString(false).c_str(), swap2.str().c_str(), fluo.str().c_str() ); // clang-format on @@ -1362,11 +1329,6 @@ NudgeRGB::get_filter_text (Inkscape::Extension::Extension * ext) std::ostringstream bx; std::ostringstream by; - std::ostringstream a; - std::ostringstream r; - std::ostringstream g; - std::ostringstream b; - rx << ext->get_param_float("rx"); ry << ext->get_param_float("ry"); gx << ext->get_param_float("gx"); @@ -1374,16 +1336,12 @@ NudgeRGB::get_filter_text (Inkscape::Extension::Extension * ext) bx << ext->get_param_float("bx"); by << ext->get_param_float("by"); - guint32 color = ext->get_param_color("color"); - r << ((color >> 24) & 0xff); - g << ((color >> 16) & 0xff); - b << ((color >> 8) & 0xff); - a << (color & 0xff) / 255.0F; + auto color = ext->get_param_color("color"); // clang-format off _filter = g_strdup_printf( "\n" - "\n" + "\n" "\n" "\n" "\n" @@ -1393,7 +1351,7 @@ NudgeRGB::get_filter_text (Inkscape::Extension::Extension * ext) "\n" "\n" "\n" - "\n", a.str().c_str(), r.str().c_str(), g.str().c_str(), b.str().c_str(), + "\n", color.getOpacity(), color.toString(false).c_str(), rx.str().c_str(), ry.str().c_str(), gx.str().c_str(), gy.str().c_str(), bx.str().c_str(), by.str().c_str() ); @@ -1478,11 +1436,6 @@ NudgeCMY::get_filter_text (Inkscape::Extension::Extension * ext) std::ostringstream yx; std::ostringstream yy; - std::ostringstream a; - std::ostringstream r; - std::ostringstream g; - std::ostringstream b; - cx << ext->get_param_float("cx"); cy << ext->get_param_float("cy"); mx << ext->get_param_float("mx"); @@ -1490,16 +1443,12 @@ NudgeCMY::get_filter_text (Inkscape::Extension::Extension * ext) yx << ext->get_param_float("yx"); yy << ext->get_param_float("yy"); - guint32 color = ext->get_param_color("color"); - r << ((color >> 24) & 0xff); - g << ((color >> 16) & 0xff); - b << ((color >> 8) & 0xff); - a << (color & 0xff) / 255.0F; + auto color = ext->get_param_color("color"); // clang-format off _filter = g_strdup_printf( "\n" - "\n" + "\n" "\n" "\n" "\n" @@ -1509,7 +1458,7 @@ NudgeCMY::get_filter_text (Inkscape::Extension::Extension * ext) "\n" "\n" "\n" - "\n", a.str().c_str(), r.str().c_str(), g.str().c_str(), b.str().c_str(), + "\n", color.getOpacity(), color.toString(false).c_str(), cx.str().c_str(), cy.str().c_str(), mx.str().c_str(), my.str().c_str(), yx.str().c_str(), yy.str().c_str() ); @@ -1670,27 +1619,19 @@ SimpleBlend::get_filter_text (Inkscape::Extension::Extension * ext) { if (_filter != nullptr) g_free((void *)_filter); - std::ostringstream a; - std::ostringstream r; - std::ostringstream g; - std::ostringstream b; std::ostringstream blend; - guint32 color = ext->get_param_color("color"); - r << ((color >> 24) & 0xff); - g << ((color >> 16) & 0xff); - b << ((color >> 8) & 0xff); - a << (color & 0xff) / 255.0F; + auto color = ext->get_param_color("color"); blend << ext->get_param_optiongroup("blendmode"); // clang-format off _filter = g_strdup_printf( "\n" - "\n" + "\n" "\n" "\n" - "\n", r.str().c_str(), g.str().c_str(), b.str().c_str(), - a.str().c_str(), blend.str().c_str()); + "\n", color.toString(false).c_str(), + color.getOpacity(), blend.str().c_str()); // clang-format on return _filter; @@ -1864,10 +1805,6 @@ Tritone::get_filter_text (Inkscape::Extension::Extension * ext) if (_filter != nullptr) g_free((void *)_filter); std::ostringstream dist; - std::ostringstream a; - std::ostringstream r; - std::ostringstream g; - std::ostringstream b; std::ostringstream globalblend; std::ostringstream glow; std::ostringstream glowblend; @@ -1878,11 +1815,7 @@ Tritone::get_filter_text (Inkscape::Extension::Extension * ext) std::ostringstream c2in2; std::ostringstream b6in2; - guint32 color = ext->get_param_color("color"); - r << ((color >> 24) & 0xff); - g << ((color >> 16) & 0xff); - b << ((color >> 8) & 0xff); - a << (color & 0xff) / 255.0F; + auto color = ext->get_param_color("color"); globalblend << ext->get_param_optiongroup("globalblend"); dist << ext->get_param_int("dist"); glow << ext->get_param_float("glow"); @@ -1939,14 +1872,14 @@ Tritone::get_filter_text (Inkscape::Extension::Extension * ext) "\n" "\n" "\n" - "\n" + "\n" "\n" "\n" "\n" "\n" "\n" "\n", dist.str().c_str(), globalblend.str().c_str(), - a.str().c_str(), r.str().c_str(), g.str().c_str(), b.str().c_str(), + color.getOpacity(), color.toString(false).c_str(), c1in2.str().c_str(), glow.str().c_str(), b6in2.str().c_str(), glowblend.str().c_str(), c2in.str().c_str(), c2in2.str().c_str(), llight.str().c_str(), glight.str().c_str() ); // clang-format on diff --git a/src/extension/internal/filter/morphology.h b/src/extension/internal/filter/morphology.h index 5701d2c075..6294ee046d 100644 --- a/src/extension/internal/filter/morphology.h +++ b/src/extension/internal/filter/morphology.h @@ -229,10 +229,6 @@ Outline::get_filter_text (Inkscape::Extension::Extension * ext) std::ostringstream dilat2; std::ostringstream erosion2; std::ostringstream antialias; - std::ostringstream r; - std::ostringstream g; - std::ostringstream b; - std::ostringstream a; std::ostringstream fopacity; std::ostringstream sopacity; std::ostringstream smooth; @@ -252,11 +248,7 @@ Outline::get_filter_text (Inkscape::Extension::Extension * ext) dilat2 << ext->get_param_float("dilat2"); erosion2 << (- ext->get_param_float("erosion2")); antialias << ext->get_param_float("antialias"); - guint32 color = ext->get_param_color("color"); - r << ((color >> 24) & 0xff); - g << ((color >> 16) & 0xff); - b << ((color >> 8) & 0xff); - a << (color & 0xff) / 255.0F; + auto color = ext->get_param_color("color"); fopacity << ext->get_param_float("fopacity"); sopacity << ext->get_param_float("sopacity"); @@ -310,7 +302,7 @@ Outline::get_filter_text (Inkscape::Extension::Extension * ext) "\n" "\n" "\n" - "\n" + "\n" "\n" "\n" "\n" @@ -318,7 +310,7 @@ Outline::get_filter_text (Inkscape::Extension::Extension * ext) dilat1.str().c_str(), erosion1.str().c_str(), width2.str().c_str(), c2in.str().c_str(), c2op.str().c_str(), dilat2.str().c_str(), erosion2.str().c_str(), antialias.str().c_str(), smooth.str().c_str(), - a.str().c_str(), r.str().c_str(), g.str().c_str(), b.str().c_str(), + color.getOpacity(), color.toString(false).c_str(), c4in.str().c_str(), fopacity.str().c_str(), sopacity.str().c_str() ); // clang-format on diff --git a/src/extension/internal/filter/overlays.h b/src/extension/internal/filter/overlays.h index 74a703c8f5..1f6b49e604 100644 --- a/src/extension/internal/filter/overlays.h +++ b/src/extension/internal/filter/overlays.h @@ -102,10 +102,6 @@ NoiseFill::get_filter_text (Inkscape::Extension::Extension * ext) std::ostringstream variation; std::ostringstream dilat; std::ostringstream erosion; - std::ostringstream r; - std::ostringstream g; - std::ostringstream b; - std::ostringstream a; std::ostringstream inverted; type << ext->get_param_optiongroup("type"); @@ -115,11 +111,7 @@ NoiseFill::get_filter_text (Inkscape::Extension::Extension * ext) variation << ext->get_param_int("variation"); dilat << ext->get_param_float("dilat"); erosion << (- ext->get_param_float("erosion")); - guint32 color = ext->get_param_color("color"); - r << ((color >> 24) & 0xff); - g << ((color >> 16) & 0xff); - b << ((color >> 8) & 0xff); - a << (color & 0xff) / 255.0F; + auto color = ext->get_param_color("color"); if (ext->get_param_bool("inverted")) inverted << "out"; else @@ -130,13 +122,14 @@ NoiseFill::get_filter_text (Inkscape::Extension::Extension * ext) "\n" "\n" "\n" - "\n" + "\n" "\n" "\n" "\n" "\n" "\n" - "\n", type.str().c_str(), hfreq.str().c_str(), vfreq.str().c_str(), complexity.str().c_str(), variation.str().c_str(), inverted.str().c_str(), dilat.str().c_str(), erosion.str().c_str(), a.str().c_str(), r.str().c_str(), g.str().c_str(), b.str().c_str()); + "\n", type.str().c_str(), hfreq.str().c_str(), vfreq.str().c_str(), complexity.str().c_str(), variation.str().c_str(), inverted.str().c_str(), dilat.str().c_str(), erosion.str().c_str(), + color.getOpacity(), color.toString(false).c_str()); return _filter; }; /* NoiseFill filter */ diff --git a/src/extension/internal/filter/paint.h b/src/extension/internal/filter/paint.h index 8f4eda31e5..e12d75465b 100644 --- a/src/extension/internal/filter/paint.h +++ b/src/extension/internal/filter/paint.h @@ -396,15 +396,7 @@ Drawing::get_filter_text (Inkscape::Extension::Extension * ext) std::ostringstream blur; std::ostringstream bdilat; std::ostringstream berosion; - std::ostringstream strokea; - std::ostringstream stroker; - std::ostringstream strokeg; - std::ostringstream strokeb; std::ostringstream ios; - std::ostringstream filla; - std::ostringstream fillr; - std::ostringstream fillg; - std::ostringstream fillb; std::ostringstream iof; simply << ext->get_param_float("simply"); @@ -423,21 +415,13 @@ Drawing::get_filter_text (Inkscape::Extension::Extension * ext) bdilat << ext->get_param_float("bdilat"); berosion << (- ext->get_param_float("berosion")); - guint32 fcolor = ext->get_param_color("fcolor"); - fillr << ((fcolor >> 24) & 0xff); - fillg << ((fcolor >> 16) & 0xff); - fillb << ((fcolor >> 8) & 0xff); - filla << (fcolor & 0xff) / 255.0F; + auto fcolor = ext->get_param_color("fcolor"); if (ext->get_param_bool("iof")) iof << "SourceGraphic"; else iof << "flood3"; - guint32 scolor = ext->get_param_color("scolor"); - stroker << ((scolor >> 24) & 0xff); - strokeg << ((scolor >> 16) & 0xff); - strokeb << ((scolor >> 8) & 0xff); - strokea << (scolor & 0xff) / 255.0F; + auto scolor = ext->get_param_color("scolor"); if (ext->get_param_bool("ios")) ios << "SourceGraphic"; else @@ -462,20 +446,20 @@ Drawing::get_filter_text (Inkscape::Extension::Extension * ext) "\n" "\n" "\n" - "\n" + "\n" "\n" - "\n" + "\n" "\n" - "\n" + "\n" "\n" - "\n" + "\n" "\n" "\n" "\n" "\n" "\n" "\n" - "\n", simply.str().c_str(), clean.str().c_str(), erase.str().c_str(), smooth.str().c_str(), dilat.str().c_str(), erosion.str().c_str(), blur.str().c_str(), bdilat.str().c_str(), berosion.str().c_str(), stroker.str().c_str(), strokeg.str().c_str(), strokeb.str().c_str(), ios.str().c_str(), strokea.str().c_str(), offset.str().c_str(), offset.str().c_str(), fillr.str().c_str(), fillg.str().c_str(), fillb.str().c_str(), iof.str().c_str(), filla.str().c_str(), translucent.str().c_str()); + "\n", simply.str().c_str(), clean.str().c_str(), erase.str().c_str(), smooth.str().c_str(), dilat.str().c_str(), erosion.str().c_str(), blur.str().c_str(), bdilat.str().c_str(), berosion.str().c_str(), scolor.toString(false).c_str(), ios.str().c_str(), scolor.getOpacity(), offset.str().c_str(), offset.str().c_str(), fcolor.toString(false).c_str(), iof.str().c_str(), fcolor.getOpacity(), translucent.str().c_str()); // clang-format on return _filter; @@ -771,14 +755,6 @@ PointEngraving::get_filter_text (Inkscape::Extension::Extension * ext) std::ostringstream grain; std::ostringstream erase; std::ostringstream blur; - std::ostringstream r; - std::ostringstream g; - std::ostringstream b; - std::ostringstream a; - std::ostringstream br; - std::ostringstream bg; - std::ostringstream bb; - std::ostringstream ba; std::ostringstream iof; std::ostringstream iop; @@ -794,17 +770,8 @@ PointEngraving::get_filter_text (Inkscape::Extension::Extension * ext) erase << ext->get_param_float("erase"); blur << ext->get_param_float("blur"); - guint32 fcolor = ext->get_param_color("fcolor"); - r << ((fcolor >> 24) & 0xff); - g << ((fcolor >> 16) & 0xff); - b << ((fcolor >> 8) & 0xff); - a << (fcolor & 0xff) / 255.0F; - - guint32 pcolor = ext->get_param_color("pcolor"); - br << ((pcolor >> 24) & 0xff); - bg << ((pcolor >> 16) & 0xff); - bb << ((pcolor >> 8) & 0xff); - ba << (pcolor & 0xff) / 255.0F; + auto fcolor = ext->get_param_color("fcolor"); + auto pcolor = ext->get_param_color("pcolor"); if (ext->get_param_bool("iof")) iof << "SourceGraphic"; @@ -826,18 +793,18 @@ PointEngraving::get_filter_text (Inkscape::Extension::Extension * ext) "\n" "\n" "\n" - "\n" + "\n" "\n" - "\n" + "\n" "\n" - "\n" + "\n" "\n" "\n", reduction.str().c_str(), blend.str().c_str(), type.str().c_str(), hfreq.str().c_str(), vfreq.str().c_str(), complexity.str().c_str(), variation.str().c_str(), lightness.str().c_str(), grain.str().c_str(), erase.str().c_str(), blur.str().c_str(), - br.str().c_str(), bg.str().c_str(), bb.str().c_str(), ba.str().c_str(), iop.str().c_str(), - r.str().c_str(), g.str().c_str(), b.str().c_str(), a.str().c_str(), iof.str().c_str(), - a.str().c_str(), ba.str().c_str() ); + pcolor.toString(false).c_str(), pcolor.getOpacity(), iop.str().c_str(), + fcolor.toString(false).c_str(), fcolor.getOpacity(), iof.str().c_str(), + fcolor.getOpacity(), pcolor.getOpacity()); // clang-format on return _filter; diff --git a/src/extension/internal/filter/shadows.h b/src/extension/internal/filter/shadows.h index a41654c934..1d55107251 100644 --- a/src/extension/internal/filter/shadows.h +++ b/src/extension/internal/filter/shadows.h @@ -104,14 +104,8 @@ gchar const *ColorizableDropShadow::get_filter_text(Inkscape::Extension::Extensi // Style parameters + auto color = ext->get_param_color("color"); float blur_std = ext->get_param_float("blur"); - - guint32 color = ext->get_param_color("color"); - float flood_a = (color & 0xff) / 255.0F; - int flood_r = ((color >> 24) & 0xff); - int flood_g = ((color >> 16) & 0xff); - int flood_b = ((color >> 8) & 0xff); - float offset_x = ext->get_param_float("xoffset"); float offset_y = ext->get_param_float("yoffset"); @@ -168,14 +162,14 @@ gchar const *ColorizableDropShadow::get_filter_text(Inkscape::Extension::Extensi auto old = std::locale::global(std::locale::classic()); _filter = g_strdup_printf( "\n" - "\n" + "\n" "\n" "\n" "\n" "\n" "\n", - flood_a, flood_r, flood_g, flood_b, + color.getOpacity(), color.toString(false).c_str(), blur_std, offset_x, offset_y, diff --git a/src/extension/internal/filter/transparency.h b/src/extension/internal/filter/transparency.h index 25e4d82228..776cf0fe99 100644 --- a/src/extension/internal/filter/transparency.h +++ b/src/extension/internal/filter/transparency.h @@ -381,18 +381,10 @@ Silhouette::get_filter_text (Inkscape::Extension::Extension * ext) { if (_filter != nullptr) g_free((void *)_filter); - std::ostringstream a; - std::ostringstream r; - std::ostringstream g; - std::ostringstream b; std::ostringstream cutout; std::ostringstream blur; - guint32 color = ext->get_param_color("color"); - r << ((color >> 24) & 0xff); - g << ((color >> 16) & 0xff); - b << ((color >> 8) & 0xff); - a << (color & 0xff) / 255.0F; + auto color = ext->get_param_color("color"); if (ext->get_param_bool("cutout")) cutout << "out"; else @@ -402,10 +394,10 @@ Silhouette::get_filter_text (Inkscape::Extension::Extension * ext) // clang-format off _filter = g_strdup_printf( "\n" - "\n" + "\n" "\n" "\n" - "\n", a.str().c_str(), r.str().c_str(), g.str().c_str(), b.str().c_str(), cutout.str().c_str(), blur.str().c_str()); + "\n", color.getOpacity(), color.toString(false).c_str(), cutout.str().c_str(), blur.str().c_str()); // clang-format on return _filter; diff --git a/src/extension/internal/gimpgrad.cpp b/src/extension/internal/gimpgrad.cpp index 036b0c7b4d..4d42faf1dc 100644 --- a/src/extension/internal/gimpgrad.cpp +++ b/src/extension/internal/gimpgrad.cpp @@ -13,18 +13,20 @@ * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ -#include #include "io/sys.h" #include "extension/system.h" #include "svg/css-ostringstream.h" -#include "svg/svg-color.h" +#include "colors/color.h" +#include "colors/manager.h" #include "gimpgrad.h" #include "streq.h" #include "strneq.h" #include "document.h" #include "extension/extension.h" +using namespace Inkscape::Colors; + namespace Inkscape::Extension::Internal { bool GimpGrad::load(Inkscape::Extension::Extension *) @@ -50,24 +52,22 @@ static void append_css_num(Glib::ustring &str, double const num) \param location Where the stop is placed in the gradient \return The text that is the stop. Full SVG containing the element. - This function encapsulates all of the translation of the ColorRGBA + This function encapsulates all of the translation of the Color and the location into the gradient. It is really pretty simple except - that the ColorRGBA is in floats that are 0 to 1 and the SVG wants + that the Color is in floats that are 0 to 1 and the SVG wants hex values from 0 to 255 for color. Otherwise mostly this is just turning the values into strings and returning it. */ -static Glib::ustring stop_svg(ColorRGBA const in_color, double const location) +static Glib::ustring stop_svg(Color const &in_color, double const location) { Glib::ustring ret("toString(false); ret += '"'; if (in_color[3] != 1) { ret += " stop-opacity=\""; - append_css_num(ret, in_color[3]); + append_css_num(ret, in_color.getOpacity()); ret += '"'; } ret += " offset=\""; @@ -161,7 +161,7 @@ std::unique_ptr GimpGrad::open(Inkscape::Extension::Input *, char co goto error; } - ColorRGBA prev_color(-1.0, -1.0, -1.0, -1.0); + Color prev_color(0x0); Glib::ustring outsvg("\n"); long n_segs_found = 0; double prev_right = 0.0; @@ -194,9 +194,9 @@ std::unique_ptr GimpGrad::open(Inkscape::Extension::Input *, char co goto error; } - ColorRGBA const leftcolor(dbls[3], dbls[4], dbls[5], dbls[6]); - ColorRGBA const rightcolor(dbls[7], dbls[8], dbls[9], dbls[10]); g_assert(11 == G_N_ELEMENTS(dbls)); + auto leftcolor = Color(Space::Type::RGB, {dbls[3], dbls[4], dbls[5], dbls[6]}); + auto rightcolor = Color(Space::Type::RGB, {dbls[7], dbls[8], dbls[9], dbls[10]}); /* Interpolation enums: curve shape and colour space. */ { @@ -215,7 +215,7 @@ std::unique_ptr GimpGrad::open(Inkscape::Extension::Input *, char co outsvg += stop_svg(leftcolor, left); } if (fabs(middle - .5 * (left + right)) > 1e-4) { - outsvg += stop_svg(leftcolor.average(rightcolor), middle); + outsvg += stop_svg(leftcolor.averaged(rightcolor), middle); } outsvg += stop_svg(rightcolor, right); diff --git a/src/extension/internal/latex-pstricks.cpp b/src/extension/internal/latex-pstricks.cpp index 3654e952c6..3e42f97cd2 100644 --- a/src/extension/internal/latex-pstricks.cpp +++ b/src/extension/internal/latex-pstricks.cpp @@ -19,6 +19,7 @@ #include "util/units.h" #include "helper/geom-curves.h" +#include "colors/color.h" #include "extension/print.h" #include "extension/system.h" #include "inkscape-version.h" @@ -173,13 +174,12 @@ unsigned int PrintLatex::fill(Inkscape::Extension::Print * /*mod*/, if (style->fill.isColor()) { Inkscape::SVGOStringStream os; - float rgb[3]; float fill_opacity; os.setf(std::ios::fixed); fill_opacity=SP_SCALE24_TO_FLOAT(style->fill_opacity.value); - style->fill.value.color.get_rgb_floatv(rgb); + auto rgb = *style->fill.getColor().converted(Colors::Space::Type::RGB); os << "{\n\\newrgbcolor{curcolor}{" << rgb[0] << " " << rgb[1] << " " << rgb[2] << "}\n"; os << "\\pscustom[linestyle=none,fillstyle=solid,fillcolor=curcolor"; if (fill_opacity!=1.0) { @@ -208,14 +208,12 @@ unsigned int PrintLatex::stroke(Inkscape::Extension::Print * /*mod*/, if (style->stroke.isColor()) { Inkscape::SVGOStringStream os; - float rgb[3]; - float stroke_opacity; Geom::Affine tr_stack = m_tr_stack.top(); double const scale = tr_stack.descrim(); os.setf(std::ios::fixed); - stroke_opacity=SP_SCALE24_TO_FLOAT(style->stroke_opacity.value); - style->stroke.value.color.get_rgb_floatv(rgb); + double stroke_opacity = style->stroke_opacity; + auto rgb = *style->stroke.getColor().converted(Colors::Space::Type::RGB); os << "{\n\\newrgbcolor{curcolor}{" << rgb[0] << " " << rgb[1] << " " << rgb[2] << "}\n"; os << "\\pscustom[linewidth=" << style->stroke_width.computed*scale<< ",linecolor=curcolor"; diff --git a/src/extension/internal/latex-text-renderer.cpp b/src/extension/internal/latex-text-renderer.cpp index 29aa127114..aba66c656a 100644 --- a/src/extension/internal/latex-text-renderer.cpp +++ b/src/extension/internal/latex-text-renderer.cpp @@ -28,6 +28,8 @@ #include <2geom/transforms.h> #include <2geom/rect.h> +#include "colors/color.h" +#include "colors/manager.h" #include "object/sp-item.h" #include "object/sp-item-group.h" #include "object/sp-root.h" @@ -281,25 +283,15 @@ void LaTeXTextRenderer::sp_text_render(SPText *textobj) g_warning("LaTeXTextRenderer::sp_text_render: baselineAnchorPoint unset, text position will be wrong. Please report the issue."); } - // determine color and transparency (for now, use rgb color model as it is most native to Inkscape) - bool has_color = false; // if the item has no color set, don't force black color - bool has_transparency = false; - // TODO: how to handle ICC colors? - // give priority to fill color - guint32 rgba = 0; - float opacity = SP_SCALE24_TO_FLOAT(style->opacity.value); + Inkscape::Colors::Color color(0x0); if (style->fill.set && style->fill.isColor()) { - has_color = true; - rgba = style->fill.value.color.toRGBA32(1.); - opacity *= SP_SCALE24_TO_FLOAT(style->fill_opacity.value); + color = style->fill.getColor(); + color.addOpacity(style->fill_opacity); } else if (style->stroke.set && style->stroke.isColor()) { - has_color = true; - rgba = style->stroke.value.color.toRGBA32(1.); - opacity *= SP_SCALE24_TO_FLOAT(style->stroke_opacity.value); - } - if (opacity < 1.0) { - has_transparency = true; + color = style->stroke.getColor(); + color.addOpacity(style->stroke_opacity); } + color.addOpacity(style->opacity); // get rotation Geom::Affine i2doc = textobj->i2doc_affine(); @@ -322,11 +314,10 @@ void LaTeXTextRenderer::sp_text_render(SPText *textobj) os.setf(std::ios::fixed); // don't use scientific notation os << " \\put(" << anchor[Geom::X] << "," << anchor[Geom::Y] << "){"; - if (has_color) { - os << "\\color[rgb]{" << SP_RGBA32_R_F(rgba) << "," << SP_RGBA32_G_F(rgba) << "," << SP_RGBA32_B_F(rgba) << "}"; - } - if (_pdflatex && has_transparency) { - os << "\\transparent{" << opacity << "}"; + color.convert(Colors::Space::Type::RGB); + os << "\\color[rgb]{" << color[0] << "," << color[1] << "," << color[2] << "}"; + if (_pdflatex && color.getOpacity() < 1.0) { + os << "\\transparent{" << color.getOpacity() << "}"; } if (has_rotation) { os << "\\rotatebox{" << degrees << "}{"; @@ -475,11 +466,11 @@ Flowing in rectangle is possible, not in arb shape. float opacity = SP_SCALE24_TO_FLOAT(style->opacity.value); if (style->fill.set && style->fill.isColor()) { has_color = true; - rgba = style->fill.value.color.toRGBA32(1.); + rgba = style->fill.getColor().toRGBA(); opacity *= SP_SCALE24_TO_FLOAT(style->fill_opacity.value); } else if (style->stroke.set && style->stroke.isColor()) { has_color = true; - rgba = style->stroke.value.color.toRGBA32(1.); + rgba = style->stroke.getColor().toRGBA(); opacity *= SP_SCALE24_TO_FLOAT(style->stroke_opacity.value); } if (opacity < 1.0) { diff --git a/src/extension/internal/metafile-inout.h b/src/extension/internal/metafile-inout.h index c742a64dfc..c3494e512c 100644 --- a/src/extension/internal/metafile-inout.h +++ b/src/extension/internal/metafile-inout.h @@ -25,6 +25,16 @@ #include <2geom/pathvector.h> #include "extension/implementation/implementation.h" +#include "colors/utils.h" + +constexpr uint32_t U_RGB_COMPOSE(U_COLORREF c) +{ + return SP_RGBA32_U_COMPOSE(U_RGBAGetR(c), U_RGBAGetG(c), U_RGBAGetB(c), 0xff); +} +constexpr uint32_t U_RGBA_COMPOSE(U_COLORREF c) +{ + return SP_RGBA32_U_COMPOSE(U_RGBAGetR(c), U_RGBAGetG(c), U_RGBAGetB(c), U_RGBAGetA(c)); +} class SPObject; diff --git a/src/extension/internal/metafile-print.cpp b/src/extension/internal/metafile-print.cpp index 41775daeac..d9471cb12b 100644 --- a/src/extension/internal/metafile-print.cpp +++ b/src/extension/internal/metafile-print.cpp @@ -31,6 +31,14 @@ namespace Inkscape { namespace Extension { namespace Internal { +U_COLORREF toColorRef(std::optional color) +{ + // Make sure it's RGB first, if wmf can support other color formats, this can be changed. + auto c = color ? *color->converted(Colors::Space::Type::RGB) : Colors::Color(0x000000ff); + return U_RGBA(255 * c[0], 255 * c[1], 255 * c[2], 255 * c[3]); +} + + PrintMetafile::~PrintMetafile() { #ifndef G_OS_WIN32 @@ -146,20 +154,14 @@ U_COLORREF PrintMetafile::avg_stop_color(SPGradient *gr) U_COLORREF cr; int last = gr->vector.stops.size() - 1; if (last >= 1) { - float rgbs[3]; - float rgbe[3]; - float ops, ope; - - ops = gr->vector.stops[0 ].opacity; - ope = gr->vector.stops[last].opacity; - gr->vector.stops[0 ].color.get_rgb_floatv(rgbs); - gr->vector.stops[last].color.get_rgb_floatv(rgbe); + auto rgbs = *gr->vector.stops[0 ].color->converted(Colors::Space::Type::RGB); + auto rgbe = *gr->vector.stops[last].color->converted(Colors::Space::Type::RGB); /* Replace opacity at start & stop with that fraction background color, then average those two for final color. */ cr = U_RGB( - 255 * ((opweight(rgbs[0], gv.rgb[0], ops) + opweight(rgbe[0], gv.rgb[0], ope)) / 2.0), - 255 * ((opweight(rgbs[1], gv.rgb[1], ops) + opweight(rgbe[1], gv.rgb[1], ope)) / 2.0), - 255 * ((opweight(rgbs[2], gv.rgb[2], ops) + opweight(rgbe[2], gv.rgb[2], ope)) / 2.0) + 255 * ((opweight(rgbs[0], gv.rgb[0], rgbs[3]) + opweight(rgbe[0], gv.rgb[0], rgbe[3])) / 2.0), + 255 * ((opweight(rgbs[1], gv.rgb[1], rgbs[3]) + opweight(rgbe[1], gv.rgb[1], rgbe[3])) / 2.0), + 255 * ((opweight(rgbs[2], gv.rgb[2], rgbs[3]) + opweight(rgbe[2], gv.rgb[2], rgbe[3])) / 2.0) ); } else { cr = U_RGB(0, 0, 0); // The default fill diff --git a/src/extension/internal/metafile-print.h b/src/extension/internal/metafile-print.h index 2266ba5b95..f7f691797f 100644 --- a/src/extension/internal/metafile-print.h +++ b/src/extension/internal/metafile-print.h @@ -20,6 +20,7 @@ #include <2geom/affine.h> #include <2geom/pathvector.h> +#include "colors/color.h" #include "extension/implementation/implementation.h" #include "style-enums.h" // Fill rule @@ -34,6 +35,7 @@ class Pixbuf; namespace Extension { namespace Internal { +U_COLORREF toColorRef(std::optional color); enum MFDrawMode {DRAW_PAINT, DRAW_PATTERN, DRAW_IMAGE, DRAW_LINEAR_GRADIENT, DRAW_RADIAL_GRADIENT}; struct FontfixParams { @@ -73,7 +75,7 @@ protected: uint32_t htextalignment; uint32_t hpolyfillmode; // used to minimize redundant records that set this - float htextcolor_rgb[3]; // used to minimize redundant records that set this + std::optional htextcolor_rgb; // used to minimize redundant records that set this std::stack m_tr_stack; Geom::PathVector fill_pathv; diff --git a/src/extension/internal/odf.cpp b/src/extension/internal/odf.cpp index 5ed491e644..26872b1de6 100644 --- a/src/extension/internal/odf.cpp +++ b/src/extension/internal/odf.cpp @@ -45,7 +45,6 @@ //# Inkscape includes #include "attributes.h" // for SPAttr -#include "color.h" // for SPColor #include "document.h" // for SPDocument #include "preferences.h" // for guint32 #include "style-internal.h" // for SPIPaint, SPIScale24, SPILe... @@ -1291,7 +1290,7 @@ bool OdfOutput::processStyle(SPItem *item, const Glib::ustring &id, const Glib:: // FILL if (style->fill.isColor()) { - guint32 fillCol = style->fill.value.color.toRGBA32( 0 ); + guint32 fillCol = style->fill.getColor().toRGBA(); char buf[16]; int r = (fillCol >> 24) & 0xff; int g = (fillCol >> 16) & 0xff; @@ -1316,7 +1315,7 @@ bool OdfOutput::processStyle(SPItem *item, const Glib::ustring &id, const Glib:: // STROKE if (style->stroke.isColor()) { - guint32 strokeCol = style->stroke.value.color.toRGBA32( 0 ); + guint32 strokeCol = style->stroke.getColor().toRGBA(); char buf[16]; int r = (strokeCol >> 24) & 0xff; int g = (strokeCol >> 16) & 0xff; @@ -1431,10 +1430,10 @@ bool OdfOutput::processGradient(SPItem *item, for (SPStop *stop = grvec->getFirstStop(); stop ; stop = stop->getNextStop()) { - unsigned long rgba = stop->get_rgba32(); - unsigned long rgb = (rgba >> 8) & 0xffffff; - double opacity = (static_cast(rgba & 0xff)) / 256.0; - GradientStop gs(rgb, opacity); + // TODO: Replace these with a color object to support more than RGB + auto color = stop->getColor().toRGBA(false); + auto opacity = stop->getColor().getOpacity(); + GradientStop gs(color, opacity); gi.stops.push_back(gs); } diff --git a/src/extension/internal/pdfinput/svg-builder.cpp b/src/extension/internal/pdfinput/svg-builder.cpp index f0a9c1ac69..8a015a91bb 100644 --- a/src/extension/internal/pdfinput/svg-builder.cpp +++ b/src/extension/internal/pdfinput/svg-builder.cpp @@ -29,16 +29,15 @@ #include "GfxState.h" #include "Page.h" #include "Stream.h" -#include "color.h" #include "document.h" #include "extract-uri.h" #include "pdf-parser.h" #include "pdf-utils.h" #include "png.h" #include "poppler-cairo-font-engine.h" -#include "profile-manager.h" -#include "color/cms-util.h" +#include "colors/cms/profile.h" +#include "colors/document-cms.h" #include "display/cairo-utils.h" #include "display/nr-filter-utils.h" #include "object/sp-defs.h" @@ -920,30 +919,23 @@ std::string SvgBuilder::_getColorProfile(cmsHPROFILE hp) if (_icc_profiles.find(hp) != _icc_profiles.end()) return _icc_profiles[hp]; - std::string name = get_color_profile_name(hp); + auto profile = Colors::CMS::Profile::create(hp); + std::string name = profile->getName(); // Find the named profile in the document (if already added) - if (_doc->getProfileManager().find(name.c_str())) + if (_doc->getDocumentCMS().getSpace(name)) return name; // Add the profile, we've never seen it before. - cmsUInt32Number len = 0; - cmsSaveProfileToMem(hp, nullptr, &len); - auto buf = (unsigned char *)malloc(len * sizeof(unsigned char)); - cmsSaveProfileToMem(hp, buf, &len); - Inkscape::XML::Node *icc_node = _xml_doc->createElement("svg:color-profile"); icc_node->setAttribute("inkscape:label", name); icc_node->setAttribute("name", name); - auto *base64String = g_base64_encode(buf, len); - auto icc_data = std::string("data:application/vnd.iccprofile;base64,") + base64String; - g_free(base64String); + auto icc_data = std::string("data:application/vnd.iccprofile;base64,") + profile->dumpBase64(); icc_node->setAttributeOrRemoveIfEmpty("xlink:href", icc_data); _doc->getDefs()->getRepr()->appendChild(icc_node); Inkscape::GC::release(icc_node); - free(buf); _icc_profiles[hp] = name; return name; } diff --git a/src/extension/internal/pov-out.cpp b/src/extension/internal/pov-out.cpp index 963a9d3dcf..9c8358324f 100644 --- a/src/extension/internal/pov-out.cpp +++ b/src/extension/internal/pov-out.cpp @@ -290,25 +290,17 @@ bool PovOutput::doCurve(SPItem *item, const String &id) SPStyle *style = shape->style; /* fixme: Handle other fill types, even if this means translating gradients to a single flat colour. */ - if (style) - { - if (style->fill.isColor()) - { - // see color.h for how to parse SPColor - float rgb[3]; - style->fill.value.color.get_rgb_floatv(rgb); - double const dopacity = ( SP_SCALE24_TO_FLOAT(style->fill_opacity.value) - * effective_opacity(shape) ); - //gchar *str = g_strdup_printf("rgbf < %1.3f, %1.3f, %1.3f %1.3f>", - // rgb[0], rgb[1], rgb[2], 1.0 - dopacity); - String rgbf = "rgbf <"; - rgbf.append(dstr(rgb[0])); rgbf.append(", "); - rgbf.append(dstr(rgb[1])); rgbf.append(", "); - rgbf.append(dstr(rgb[2])); rgbf.append(", "); - rgbf.append(dstr(1.0 - dopacity)); rgbf.append(">"); - shapeInfo.color += rgbf; - } - } + if (style && style->fill.isColor()) { + auto rgba = *style->fill.getColor().converted(Colors::Space::Type::RGB); + rgba.addOpacity(style->fill_opacity); + rgba.addOpacity(effective_opacity(shape)); + String rgbf = "rgbf <"; + rgbf.append(dstr(rgba[0])); rgbf.append(", "); + rgbf.append(dstr(rgba[1])); rgbf.append(", "); + rgbf.append(dstr(rgba[2])); rgbf.append(", "); + rgbf.append(dstr(1.0 - rgba[3])); rgbf.append(">"); + shapeInfo.color += rgbf; + } povShapes.push_back(shapeInfo); //passed all tests. save the info diff --git a/src/extension/internal/wmf-inout.cpp b/src/extension/internal/wmf-inout.cpp index bc9b554266..998daa4213 100644 --- a/src/extension/internal/wmf-inout.cpp +++ b/src/extension/internal/wmf-inout.cpp @@ -680,10 +680,8 @@ Wmf::output_style(PWMF_CALLBACK_DATA d) SVGOStringStream tmp_style; char tmp[1024] = {0}; - float fill_rgb[3]; - d->dc[d->level].style.fill.value.color.get_rgb_floatv(fill_rgb); - float stroke_rgb[3]; - d->dc[d->level].style.stroke.value.color.get_rgb_floatv(stroke_rgb); + auto fill = d->dc[d->level].style.fill.getColor(); + auto stroke = d->dc[d->level].style.stroke.getColor(); // for U_WMR_BITBLT with no image, try to approximate some of these operations/ // Assume src color is "white" @@ -695,13 +693,13 @@ Wmf::output_style(PWMF_CALLBACK_DATA d) case U_BLACKNESS: case U_SRCERASE: case U_NOTSRCCOPY: - fill_rgb[0]=fill_rgb[1]=fill_rgb[2]=0.0; + fill.set("black"); break; case U_SRCCOPY: // treat all of these as white case U_NOTSRCERASE: case U_PATCOPY: case U_WHITENESS: - fill_rgb[0]=fill_rgb[1]=fill_rgb[2]=1.0; + fill.set("white"); break; case U_SRCPAINT: // use the existing color case U_SRCAND: @@ -720,19 +718,15 @@ Wmf::output_style(PWMF_CALLBACK_DATA d) // pen color through. switch(d->dwRop2){ case U_R2_BLACK: - fill_rgb[0] = fill_rgb[1] = fill_rgb[2] = 0.0; - stroke_rgb[0]= stroke_rgb[1]= stroke_rgb[2] = 0.0; + fill.set("black"); + stroke.set("black"); break; case U_R2_NOTMERGEPEN: case U_R2_MASKNOTPEN: break; case U_R2_NOTCOPYPEN: - fill_rgb[0] = 1.0 - fill_rgb[0]; - fill_rgb[1] = 1.0 - fill_rgb[1]; - fill_rgb[2] = 1.0 - fill_rgb[2]; - stroke_rgb[0] = 1.0 - stroke_rgb[0]; - stroke_rgb[1] = 1.0 - stroke_rgb[1]; - stroke_rgb[2] = 1.0 - stroke_rgb[2]; + fill.invert(); + stroke.invert(); break; case U_R2_MASKPENNOT: case U_R2_NOT: @@ -747,8 +741,8 @@ Wmf::output_style(PWMF_CALLBACK_DATA d) case U_R2_MERGEPEN: break; case U_R2_WHITE: - fill_rgb[0] = fill_rgb[1] = fill_rgb[2] = 1.0; - stroke_rgb[0]= stroke_rgb[1]= stroke_rgb[2] = 1.0; + fill.set("white"); + stroke.set("white"); break; default: break; @@ -773,14 +767,8 @@ Wmf::output_style(PWMF_CALLBACK_DATA d) break; case DRAW_PAINT: default: // <-- this should never happen, but just in case... - snprintf( - tmp, 1023, - "fill:#%02x%02x%02x;", - SP_COLOR_F_TO_U(fill_rgb[0]), - SP_COLOR_F_TO_U(fill_rgb[1]), - SP_COLOR_F_TO_U(fill_rgb[2]) - ); - tmp_style << tmp; + fill.convert(Colors::Space::Type::RGB); + tmp_style << "fill:" << fill.toString(false).c_str() << ";"; break; } snprintf( @@ -799,11 +787,7 @@ Wmf::output_style(PWMF_CALLBACK_DATA d) (d->dc[d->level].fill_mode == d->dc[d->level].stroke_mode) && ( (d->dc[d->level].fill_mode != DRAW_PAINT) || - ( - (fill_rgb[0]==stroke_rgb[0]) && - (fill_rgb[1]==stroke_rgb[1]) && - (fill_rgb[2]==stroke_rgb[2]) - ) + fill == stroke ) ){ d->dc[d->level].stroke_set = false; @@ -825,14 +809,8 @@ Wmf::output_style(PWMF_CALLBACK_DATA d) break; case DRAW_PAINT: default: // <-- this should never happen, but just in case... - snprintf( - tmp, 1023, - "stroke:#%02x%02x%02x;", - SP_COLOR_F_TO_U(stroke_rgb[0]), - SP_COLOR_F_TO_U(stroke_rgb[1]), - SP_COLOR_F_TO_U(stroke_rgb[2]) - ); - tmp_style << tmp; + stroke.convert(Colors::Space::Type::RGB); + tmp_style << "stroke:" << stroke.toString(false).c_str() << ";"; break; } if(d->dc[d->level].style.stroke_width.value){ @@ -1028,12 +1006,7 @@ Wmf::select_pen(PWMF_CALLBACK_DATA d, int index) d->level = cur_level; } d->dc[d->level].style.stroke_width.value = pen_width; - - double r, g, b; - r = SP_COLOR_U_TO_F( U_RGBAGetR(up.Color) ); - g = SP_COLOR_U_TO_F( U_RGBAGetG(up.Color) ); - b = SP_COLOR_U_TO_F( U_RGBAGetB(up.Color) ); - d->dc[d->level].style.stroke.value.color.set( r, g, b ); + d->dc[d->level].style.stroke.setColor(Colors::Color(U_RGB_COMPOSE(up.Color), false)); } @@ -1055,11 +1028,7 @@ Wmf::select_brush(PWMF_CALLBACK_DATA d, int index) (void) U_WMRCREATEBRUSHINDIRECT_get(record, &membrush); memcpy(&lb, membrush, U_SIZE_WLOGBRUSH); if(lb.Style == U_BS_SOLID){ - double r, g, b; - r = SP_COLOR_U_TO_F( U_RGBAGetR(lb.Color) ); - g = SP_COLOR_U_TO_F( U_RGBAGetG(lb.Color) ); - b = SP_COLOR_U_TO_F( U_RGBAGetB(lb.Color) ); - d->dc[d->level].style.fill.value.color.set( r, g, b ); + d->dc[d->level].style.fill.setColor(Colors::Color(U_RGB_COMPOSE(lb.Color))); d->dc[d->level].fill_mode = DRAW_PAINT; d->dc[d->level].fill_set = true; } @@ -1091,11 +1060,7 @@ Wmf::select_brush(PWMF_CALLBACK_DATA d, int index) tidx = add_bm16_image(d, Bm16, px); } if(tidx == U_WMR_INVALID){ // Problem with the image, for instance, an unsupported bitmap16 type - double r, g, b; - r = SP_COLOR_U_TO_F( U_RGBAGetR(d->dc[d->level].textColor)); - g = SP_COLOR_U_TO_F( U_RGBAGetG(d->dc[d->level].textColor)); - b = SP_COLOR_U_TO_F( U_RGBAGetB(d->dc[d->level].textColor)); - d->dc[d->level].style.fill.value.color.set( r, g, b ); + d->dc[d->level].style.fill.setColor(Colors::Color(U_RGB_COMPOSE(d->dc[d->level].textColor), false)); d->dc[d->level].fill_mode = DRAW_PAINT; } else { @@ -1116,11 +1081,7 @@ Wmf::select_brush(PWMF_CALLBACK_DATA d, int index) if(U_WMRCREATEPATTERNBRUSH_get(record, &Bm16h, &cbPx, &px)){ tidx = add_bm16_image(d, Bm16h, px); if(tidx == 0xFFFFFFFF){ // Problem with the image, for instance, an unsupported bitmap16 type - double r, g, b; - r = SP_COLOR_U_TO_F( U_RGBAGetR(d->dc[d->level].textColor)); - g = SP_COLOR_U_TO_F( U_RGBAGetG(d->dc[d->level].textColor)); - b = SP_COLOR_U_TO_F( U_RGBAGetB(d->dc[d->level].textColor)); - d->dc[d->level].style.fill.value.color.set( r, g, b ); + d->dc[d->level].style.fill.setColor(Colors::Color(U_RGB_COMPOSE(d->dc[d->level].textColor), false)); d->dc[d->level].fill_mode = DRAW_PAINT; } else { @@ -1222,7 +1183,7 @@ Wmf::delete_object(PWMF_CALLBACK_DATA d, int index) d->dc[d->level].style.stroke_linejoin.computed = SP_STROKE_LINEJOIN_MITER; // U_PS_JOIN_MITER; d->dc[d->level].stroke_set = true; d->dc[d->level].style.stroke_width.value = 1.0; - d->dc[d->level].style.stroke.value.color.set( 0, 0, 0 ); + d->dc[d->level].style.stroke.setColor(Colors::Color(0x000000ff)); } else if(index == d->dc[d->level].active_brush){ d->dc[d->level].active_brush = -1; @@ -3111,7 +3072,7 @@ std::unique_ptr Wmf::open(Inkscape::Extension::Input *, char const * d.dc[0].style.stroke_linecap.computed = SP_STROKE_LINECAP_SQUARE; // U_PS_ENDCAP_SQUARE; d.dc[0].style.stroke_linejoin.computed = SP_STROKE_LINEJOIN_MITER; // U_PS_JOIN_MITER; d.dc[0].style.stroke_width.value = 1.0; // will be reset to something reasonable once WMF drawing size is known - d.dc[0].style.stroke.value.color.set( 0, 0, 0 ); + d.dc[0].style.stroke.setColor(Colors::Color(0x000000ff)); d.dc[0].stroke_set = true; // Default brush is none - no fill. WMF files that do not specify a brush are unlikely to look very good! diff --git a/src/extension/internal/wmf-print.cpp b/src/extension/internal/wmf-print.cpp index 6780065fb5..f0c20ef159 100644 --- a/src/extension/internal/wmf-print.cpp +++ b/src/extension/internal/wmf-print.cpp @@ -235,7 +235,7 @@ unsigned int PrintWmf::begin(Inkscape::Extension::Print *mod, SPDocument *doc) return -1; } - htextcolor_rgb[0] = htextcolor_rgb[1] = htextcolor_rgb[2] = 0.0; + htextcolor_rgb.reset(); rec = U_WMRSETTEXTCOLOR_set(U_RGB(0, 0, 0)); if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { g_warning("Failed in PrintWmf::begin at U_WMRSETTEXTCOLOR_set"); @@ -327,7 +327,6 @@ unsigned int PrintWmf::finish(Inkscape::Extension::Print * /*mod*/) // fcolor is defined when gradients are being expanded, it is the color of one stripe or ring. int PrintWmf::create_brush(SPStyle const *style, U_COLORREF *fcolor) { - float rgb[3]; char *rec; U_WLOGBRUSH lb; uint32_t brush, fmode; @@ -365,12 +364,11 @@ int PrintWmf::create_brush(SPStyle const *style, U_COLORREF *fcolor) opacity = 0.0; // basically the same as no fill } */ - style->fill.value.color.get_rgb_floatv(rgb); - hatchColor = U_RGB(255 * rgb[0], 255 * rgb[1], 255 * rgb[2]); + hatchColor = toColorRef(style->fill.getColor()); fmode = style->fill_rule.computed == 0 ? U_WINDING : (style->fill_rule.computed == 2 ? U_ALTERNATE : U_ALTERNATE); } else if (is(SP_STYLE_FILL_SERVER(style))) { // must be paint-server - SPPaintServer *paintserver = style->fill.value.href->getObject(); + SPPaintServer *paintserver = style->fill.href->getObject(); auto pat = cast(paintserver); double dwidth = pat->width(); double dheight = pat->height(); @@ -396,7 +394,7 @@ int PrintWmf::create_brush(SPStyle const *style, U_COLORREF *fcolor) brushStyle = U_BS_HATCHED; } else if (is(SP_STYLE_FILL_SERVER(style))) { // must be a gradient // currently we do not do anything with gradients, the code below just sets the color to the average of the stops - SPPaintServer *paintserver = style->fill.value.href->getObject(); + SPPaintServer *paintserver = style->fill.href->getObject(); SPLinearGradient *lg = nullptr; SPRadialGradient *rg = nullptr; @@ -536,11 +534,9 @@ int PrintWmf::create_pen(SPStyle const *style, const Geom::Affine &transform) uint32_t linewidth = 1; if (style) { // override some or all of the preceding - float rgb[3]; // WMF does not support hatched, bitmap, or gradient pens, just set the color. - style->stroke.value.color.get_rgb_floatv(rgb); - penColor = U_RGB(255 * rgb[0], 255 * rgb[1], 255 * rgb[2]); + penColor = toColorRef(style->stroke.getColor()); using Geom::X; using Geom::Y; @@ -685,23 +681,17 @@ unsigned int PrintWmf::fill( */ destroy_pen(); //this sets the NULL_PEN, otherwise gradient slices may display with boundaries, see longer explanation below Geom::Path cutter; - float rgb[3]; U_COLORREF wc, c1, c2; FillRule frb = SPWR_to_LVFR((SPWindRule) style->fill_rule.computed); double doff, doff_base, doff_range; double divisions = 128.0; int nstops; int istop = 1; - float opa; // opacity at stop SPRadialGradient *tg = (SPRadialGradient *)(gv.grad); // linear/radial are the same here nstops = tg->vector.stops.size(); - tg->vector.stops[0].color.get_rgb_floatv(rgb); - opa = tg->vector.stops[0].opacity; - c1 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); - tg->vector.stops[nstops - 1].color.get_rgb_floatv(rgb); - opa = tg->vector.stops[nstops - 1].opacity; - c2 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); + c1 = toColorRef(tg->vector.stops[0].color); + c2 = toColorRef(tg->vector.stops[nstops - 1].color); doff = 0.0; doff_base = 0.0; @@ -729,9 +719,7 @@ unsigned int PrintWmf::fill( (void) create_brush(style, &wc); print_pathv(pathvr, fill_transform); - tg->vector.stops[istop].color.get_rgb_floatv(rgb); - opa = tg->vector.stops[istop].opacity; - c2 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); + c2 = toColorRef(tg->vector.stops[istop].color); for (start = 0.0; start < range; start += step, doff += 1. / divisions) { stop = start + step + overlap; @@ -754,9 +742,7 @@ unsigned int PrintWmf::fill( doff_base = doff_range; doff_range = tg->vector.stops[istop].offset; // next or last stop c1 = c2; - tg->vector.stops[istop].color.get_rgb_floatv(rgb); - opa = tg->vector.stops[istop].opacity; - c2 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); + c2 = toColorRef(tg->vector.stops[istop].color); } } } else if (gv.mode == DRAW_LINEAR_GRADIENT) { @@ -783,9 +769,7 @@ unsigned int PrintWmf::fill( pathvr = sp_pathvector_boolop(pathvc, pathv, bool_op_inters, (FillRule) fill_nonZero, frb); print_pathv(pathvr, fill_transform); - tg->vector.stops[istop].color.get_rgb_floatv(rgb); - opa = tg->vector.stops[istop].opacity; - c2 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); + c2 = toColorRef(tg->vector.stops[istop].color); for (start = 0.0; start < range; start += step, doff += 1. / divisions) { stop = start + step + overlap; @@ -807,9 +791,7 @@ unsigned int PrintWmf::fill( doff_base = doff_range; doff_range = tg->vector.stops[istop].offset; // next or last stop c1 = c2; - tg->vector.stops[istop].color.get_rgb_floatv(rgb); - opa = tg->vector.stops[istop].opacity; - c2 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); + c2 = toColorRef(tg->vector.stops[istop].color); } } } else { @@ -1476,12 +1458,11 @@ unsigned int PrintWmf::text(Inkscape::Extension::Print * /*mod*/, char const *te g_error("Fatal programming error in PrintWmf::text at wselectobject_set"); } - float rgb[3]; - style->fill.value.color.get_rgb_floatv(rgb); + auto rgb = style->fill.getColor(); // only change the text color when it needs to be changed - if (memcmp(htextcolor_rgb, rgb, 3 * sizeof(float))) { - memcpy(htextcolor_rgb, rgb, 3 * sizeof(float)); - rec = U_WMRSETTEXTCOLOR_set(U_RGB(255 * rgb[0], 255 * rgb[1], 255 * rgb[2])); + if (htextcolor_rgb != rgb) { + htextcolor_rgb = rgb; + rec = U_WMRSETTEXTCOLOR_set(toColorRef(rgb)); if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { g_error("Fatal programming error in PrintWmf::text at U_WMRSETTEXTCOLOR_set"); } diff --git a/src/extension/prefdialog/parameter-color.cpp b/src/extension/prefdialog/parameter-color.cpp index 4e77c1d411..86d06e0ea7 100644 --- a/src/extension/prefdialog/parameter-color.cpp +++ b/src/extension/prefdialog/parameter-color.cpp @@ -17,10 +17,11 @@ #include "parameter-color.h" -#include "color.h" +#include "colors/manager.h" #include "extension/extension.h" #include "preferences.h" #include "ui/pack.h" +#include "ui/util.h" #include "ui/widget/color-notebook.h" #include "xml/node.h" @@ -28,24 +29,19 @@ namespace Inkscape::Extension { ParamColor::ParamColor(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext) : InxParameter(xml, ext) + , _colors(std::make_shared()) { - // get value - unsigned int _value = 0x000000ff; // default to black if (xml->firstChild()) { - const char *value = xml->firstChild()->content(); - if (value) - string_to_value(value); - _value = _color.value(); + if (auto parsed = Colors::Color::parse(xml->firstChild()->content())) { + _colors->set(*parsed); + } + } + if (_colors->isEmpty()) { + auto prefs = Preferences::get(); + _colors->set(prefs->getColor(pref_name())); } - Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - _value = prefs->getUInt(pref_name(), _value); - - _color.setValue(_value); - - _color_changed = _color.signal_changed.connect(sigc::mem_fun(*this, &ParamColor::_onColorChanged)); - // TODO: SelectedColor does not properly emit signal_changed after dragging, so we also need the following - _color_released = _color.signal_released.connect(sigc::mem_fun(*this, &ParamColor::_onColorChanged)); + _color_changed = _colors->signal_changed.connect(sigc::mem_fun(*this, &ParamColor::_onColorChanged)); // parse appearance if (_appearance) { @@ -61,14 +57,6 @@ ParamColor::ParamColor(Inkscape::XML::Node *xml, Inkscape::Extension::Extension ParamColor::~ParamColor() { _color_changed.disconnect(); - _color_released.disconnect(); -} - -unsigned int ParamColor::set(unsigned int in) -{ - _color.setValue(in); - - return in; } Gtk::Widget *ParamColor::get_widget(sigc::signal *changeSignal) @@ -87,15 +75,12 @@ Gtk::Widget *ParamColor::get_widget(sigc::signal *changeSignal) label->set_visible(true); UI::pack_start(*hbox, *label, true, true); - Gdk::RGBA rgba; - rgba.set_red_u (((_color.value() >> 24) & 255) << 8); - rgba.set_green_u(((_color.value() >> 16) & 255) << 8); - rgba.set_blue_u (((_color.value() >> 8) & 255) << 8); - rgba.set_alpha_u(((_color.value() >> 0) & 255) << 8); - // TODO: It would be nicer to have a custom Gtk::ColorButton() implementation here, // that wraps an Inkscape::UI::Widget::ColorNotebook into a new dialog - _color_button = Gtk::make_managed(rgba); + auto color = _colors->get(); + if (!color) + color = Colors::Color::parse("magenta"); + _color_button = Gtk::make_managed(color_to_rgba(*color)); _color_button->set_title(_text); _color_button->set_use_alpha(); _color_button->set_visible(true); @@ -103,7 +88,7 @@ Gtk::Widget *ParamColor::get_widget(sigc::signal *changeSignal) _color_button->signal_color_set().connect(sigc::mem_fun(*this, &ParamColor::_onColorButtonChanged)); } else { - Gtk::Widget *selector = Gtk::make_managed(_color); + Gtk::Widget *selector = Gtk::make_managed(_colors); UI::pack_start(*hbox, *selector, true, true); selector->set_visible(true); } @@ -116,31 +101,17 @@ Gtk::Widget *ParamColor::get_widget(sigc::signal *changeSignal) void ParamColor::_onColorChanged() { Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - prefs->setUInt(pref_name(), _color.value()); + if (auto color = _colors->get()) { + prefs->setColor(pref_name(), *color); - if (_changeSignal) - _changeSignal->emit(); + if (_changeSignal) + _changeSignal->emit(); + } } void ParamColor::_onColorButtonChanged() { - Gdk::RGBA rgba = _color_button->get_rgba(); - unsigned int value = ((rgba.get_red_u() >> 8) << 24) + - ((rgba.get_green_u() >> 8) << 16) + - ((rgba.get_blue_u() >> 8) << 8) + - ((rgba.get_alpha_u() >> 8) << 0); - set(value); -} - -std::string ParamColor::value_to_string() const -{ - return std::to_string(_color.value()); -} - -void ParamColor::string_to_value(const std::string &in) -{ - // If we canÊŒt convert this will return 0 or ULONG_MAX - _color.setValue(std::strtoul(in.c_str(), nullptr, 0)); + set(Colors::Color(to_guint32(_color_button->get_rgba()))); } } // namespace Inkscape::Extension diff --git a/src/extension/prefdialog/parameter-color.h b/src/extension/prefdialog/parameter-color.h index 106dd4422b..2b94301574 100644 --- a/src/extension/prefdialog/parameter-color.h +++ b/src/extension/prefdialog/parameter-color.h @@ -13,8 +13,11 @@ #include #include #include +#include + +#include "colors/color.h" +#include "colors/color-set.h" #include "parameter.h" -#include "ui/selected-color.h" namespace Gtk { class ColorButton; @@ -38,15 +41,11 @@ public: ParamColor(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext); ~ParamColor() override; - /** Returns \c _value, with a \i const to protect it. */ - unsigned int get() const { return _color.value(); } - unsigned int set(unsigned int in); + Colors::Color get() const { return _colors->getAverage(); } // copy + void set(Colors::Color const &color) { _colors->set(color); } Gtk::Widget *get_widget(sigc::signal *changeSignal) override; - std::string value_to_string() const override; - void string_to_value(const std::string &in) override; - std::unique_ptr> _changeSignal; private: @@ -54,10 +53,9 @@ private: void _onColorButtonChanged(); /** Internal value of this parameter */ - Inkscape::UI::SelectedColor _color; + std::shared_ptr _colors; sigc::connection _color_changed; - sigc::connection _color_released; Gtk::ColorButton *_color_button; diff --git a/src/extension/prefdialog/parameter.cpp b/src/extension/prefdialog/parameter.cpp index c3594df966..4d2dd458fe 100644 --- a/src/extension/prefdialog/parameter.cpp +++ b/src/extension/prefdialog/parameter.cpp @@ -163,7 +163,7 @@ bool InxParameter::get_optiongroup_contains(const char *value) const return param->contains(value); } -unsigned int InxParameter::get_color() const +Inkscape::Colors::Color InxParameter::get_color() const { ParamColor const *param = dynamic_cast(this); if (!param) { @@ -214,12 +214,12 @@ const char *InxParameter::set_optiongroup(const char *in) return param->set(in).c_str(); } -unsigned int InxParameter::set_color(unsigned int in) +void InxParameter::set_color(Inkscape::Colors::Color const &in) { ParamColor*param = dynamic_cast(this); if (param == nullptr) throw param_not_color_param(); - return param->set(in); + param->set(in); } diff --git a/src/extension/prefdialog/parameter.h b/src/extension/prefdialog/parameter.h index 3b37951507..ac70db8c75 100644 --- a/src/extension/prefdialog/parameter.h +++ b/src/extension/prefdialog/parameter.h @@ -21,6 +21,10 @@ namespace Glib { class ustring; } // namespace Glib +namespace Inkscape::Colors { +class Color; +} + namespace Inkscape::Extension { /** @@ -55,7 +59,7 @@ public: bool get_optiongroup_contains(const char *value) const; /** Wrapper to cast to the object and use it's function. */ - unsigned int get_color() const; + Inkscape::Colors::Color get_color() const; /** Wrapper to cast to the object and use it's function. */ bool set_bool(bool in); @@ -73,7 +77,7 @@ public: const char *set_optiongroup(const char *in); /** Wrapper to cast to the object and use it's function. */ - unsigned int set_color(unsigned int in); + void set_color(Inkscape::Colors::Color const &in); char const *name() const { return _name; } diff --git a/src/gradient-chemistry.cpp b/src/gradient-chemistry.cpp index 5af41778a0..1f9a868f4c 100644 --- a/src/gradient-chemistry.cpp +++ b/src/gradient-chemistry.cpp @@ -46,7 +46,6 @@ #include "style.h" #include "svg/svg.h" -#include "svg/svg-color.h" #include "svg/css-ostringstream.h" #include "ui/icon-names.h" @@ -54,6 +53,8 @@ #include "ui/widget/gradient-vector-selector.h" #include "xml/href-attribute-helper.h" +#include "colors/color.h" + #define noSP_GR_VERBOSE using Inkscape::DocumentUndo; @@ -705,16 +706,6 @@ SPStop *sp_get_stop_i(SPGradient *gradient, guint stop_i) return stop; } -guint32 average_color(guint32 c1, guint32 c2, gdouble p) -{ - guint32 r = static_cast(SP_RGBA32_R_U (c1) * (1 - p) + SP_RGBA32_R_U (c2) * p); - guint32 g = static_cast(SP_RGBA32_G_U (c1) * (1 - p) + SP_RGBA32_G_U (c2) * p); - guint32 b = static_cast(SP_RGBA32_B_U (c1) * (1 - p) + SP_RGBA32_B_U (c2) * p); - guint32 a = static_cast(SP_RGBA32_A_U (c1) * (1 - p) + SP_RGBA32_A_U (c2) * p); - - return SP_RGBA32_U_COMPOSE(r, g, b, a); -} - void sp_repr_set_css_double(Inkscape::XML::Node* node, const char* key, double value) { if (node) { node->setAttributeCssDouble(key, value); @@ -731,7 +722,7 @@ SPStop *sp_vector_add_stop(SPGradient *vector, SPStop* prev_stop, SPStop* next_s if (!prev_stop && !next_stop) return newstop; // This function completely breaks CMYK gradients. - guint cnew = 0; // new color + Color cnew(0x000000ff); // new color Inkscape::XML::Node *new_stop_repr = nullptr; if (!prev_stop || !next_stop) { @@ -741,23 +732,19 @@ SPStop *sp_vector_add_stop(SPGradient *vector, SPStop* prev_stop, SPStop* next_s new_stop_repr = repr->duplicate(vector->getRepr()->document()); vector->getRepr()->addChild(new_stop_repr, prev_stop ? repr : nullptr); - cnew = stop->get_rgba32(); + cnew = stop->getColor(); } else { auto repr = prev_stop->getRepr(); new_stop_repr = repr->duplicate(vector->getRepr()->document()); vector->getRepr()->addChild(new_stop_repr, repr); - - guint32 const c1 = prev_stop->get_rgba32(); - guint32 const c2 = next_stop->get_rgba32(); - cnew = average_color (c1, c2, (offset - prev_stop->offset) / (next_stop->offset - prev_stop->offset)); + cnew = prev_stop->getColor().averaged(next_stop->getColor(), (offset - prev_stop->offset) / (next_stop->offset - prev_stop->offset)); } newstop = reinterpret_cast(vector->document->getObjectByRepr(new_stop_repr)); newstop->offset = offset; newstop->getRepr()->setAttributeCssDouble("offset", (double)offset); - // XXX This is removing icc color information - newstop->setColor({cnew}, SP_RGBA32_A_F(cnew)); + newstop->setColor(cnew); Inkscape::GC::release(new_stop_repr); return newstop; @@ -797,13 +784,13 @@ static bool verify_grad(SPGradient* gradient) { child = xml_doc->createElement("svg:stop"); sp_repr_set_css_double(child, "offset", 0.0); - SPStop::setColorRepr(child, {0, 0, 0}, 1.0); + SPStop::setColorRepr(child, Colors::Color(0x000000ff)); gradient->getRepr()->addChild(child, nullptr); Inkscape::GC::release(child); child = xml_doc->createElement("svg:stop"); sp_repr_set_css_double(child, "offset", 1.0); - SPStop::setColorRepr(child, {0, 0, 0}, 1.0); + SPStop::setColorRepr(child, Colors::Color(0x000000ff)); gradient->getRepr()->addChild(child, nullptr); Inkscape::GC::release(child); modified = true; @@ -854,11 +841,7 @@ SPStop* sp_gradient_add_stop(SPGradient* gradient, SPStop* current) { newstop->offset = (stop->offset + next->offset) * 0.5 ; - guint32 const c1 = stop->get_rgba32(); - guint32 const c2 = next->get_rgba32(); - guint32 cnew = average_color(c1, c2); - - newstop->setColor({cnew}, SP_RGBA32_A_F(cnew)); + newstop->setColor(stop->getColor().averaged(next->getColor())); sp_repr_set_css_double(newstop->getRepr(), "offset", (double)newstop->offset); Inkscape::GC::release(new_stop_repr); DocumentUndo::done(gradient->document, _("Add gradient stop"), INKSCAPE_ICON("color-gradient")); @@ -887,9 +870,9 @@ SPStop* sp_gradient_add_stop_at(SPGradient* gradient, double offset) { } } -void sp_set_gradient_stop_color(SPDocument* document, SPStop* stop, SPColor color, double opacity) { +void sp_set_gradient_stop_color(SPDocument* document, SPStop* stop, Color const &color) { sp_repr_set_css_double(stop->getRepr(), "offset", stop->offset); - stop->setColor(color, opacity); + stop->setColor(color); DocumentUndo::done(document, _("Change gradient stop color"), INKSCAPE_ICON("color-gradient")); } @@ -931,20 +914,19 @@ SPStop* sp_item_gradient_get_stop(SPItem *item, GrPointType point_type, guint po return nullptr; } -guint32 sp_item_gradient_stop_query_style(SPItem *item, GrPointType point_type, guint point_i, Inkscape::PaintTarget fill_or_stroke) +Color sp_item_gradient_stop_query_style(SPItem *item, GrPointType point_type, guint point_i, Inkscape::PaintTarget fill_or_stroke) { SPGradient *gradient = getGradient(item, fill_or_stroke); - if (!gradient) { - return 0; - } + if (!gradient) + return Colors::Color(0x000000ff); if (is(gradient) || is(gradient) ) { SPGradient *vector = gradient->getVector(); if (!vector) // orphan! - return 0; // what else to do? + return Colors::Color(0x000000ff); switch (point_type) { case POINT_LG_BEGIN: @@ -953,7 +935,7 @@ guint32 sp_item_gradient_stop_query_style(SPItem *item, GrPointType point_type, { SPStop *first = vector->getFirstStop(); if (first) { - return first->get_rgba32(); + return first->getColor(); } } break; @@ -964,7 +946,7 @@ guint32 sp_item_gradient_stop_query_style(SPItem *item, GrPointType point_type, { SPStop *last = sp_last_stop (vector); if (last) { - return last->get_rgba32(); + return last->getColor(); } } break; @@ -975,7 +957,7 @@ guint32 sp_item_gradient_stop_query_style(SPItem *item, GrPointType point_type, { SPStop *stopi = sp_get_stop_i (vector, point_i); if (stopi) { - return stopi->get_rgba32(); + return stopi->getColor(); } } break; @@ -984,7 +966,6 @@ guint32 sp_item_gradient_stop_query_style(SPItem *item, GrPointType point_type, g_warning( "Bad linear/radial gradient handle type" ); break; } - return 0; } else if (is(gradient)) { // Mesh gradient @@ -993,16 +974,12 @@ guint32 sp_item_gradient_stop_query_style(SPItem *item, GrPointType point_type, switch (point_type) { case POINT_MG_CORNER: { if (point_i >= mg->array.corners.size()) { - return 0; + return Colors::Color(0x000000ff); } SPMeshNode const* cornerpoint = mg->array.corners[ point_i ]; if (cornerpoint) { - SPColor color = cornerpoint->color; - double opacity = cornerpoint->opacity; - return color.toRGBA32( opacity ); - } else { - return 0; + return *cornerpoint->color; } break; } @@ -1017,10 +994,8 @@ guint32 sp_item_gradient_stop_query_style(SPItem *item, GrPointType point_type, default: g_warning( "Bad mesh handle type" ); } - return 0; } - - return 0; + return Colors::Color(0x000000ff); } void sp_item_gradient_stop_set_style(SPItem *item, GrPointType point_type, guint point_i, Inkscape::PaintTarget fill_or_stroke, SPCSSAttr *stop) @@ -1088,37 +1063,22 @@ void sp_item_gradient_stop_set_style(SPItem *item, GrPointType point_type, guint // Mesh gradient auto mg = cast(gradient); - bool changed = false; switch (point_type) { case POINT_MG_CORNER: { // Update mesh array (which is not updated automatically when stop is changed?) - gchar const* color_str = sp_repr_css_property( stop, "stop-color", nullptr ); - if( color_str ) { - SPColor color( 0 ); - SPIPaint paint; - paint.read( color_str ); - if( paint.isColor() ) { - color = paint.value.color; - } - mg->array.corners[ point_i ]->color = color; - changed = true; - } - gchar const* opacity_str = sp_repr_css_property( stop, "stop-opacity", nullptr ); - if( opacity_str ) { - std::stringstream os( opacity_str ); - double opacity = 1.0; - os >> opacity; - mg->array.corners[ point_i ]->opacity = opacity; - changed = true; - } - // Update stop - if( changed ) { - SPStop *stopi = mg->array.corners[ point_i ]->stop; - if (stopi) { - sp_repr_css_change(stopi->getRepr(), stop, "style"); - } else { - std::cerr << "sp_item_gradient_stop_set_style: null stopi" << std::endl; + gchar const* color_str = sp_repr_css_property(stop, "stop-color", nullptr); + if (auto color = Color::parse(color_str)) { + color->addOpacity(sp_repr_css_double_property(stop, "stop-opacity", 1.0)); + if (*color != mg->array.corners[point_i]->color) { + mg->array.corners[ point_i ]->color = *color; + // Update stop + SPStop *stopi = mg->array.corners[ point_i ]->stop; + if (stopi) { + sp_repr_css_change(stopi->getRepr(), stop, "style"); + } else { + std::cerr << "sp_item_gradient_stop_set_style: null stopi" << std::endl; + } } } break; @@ -1209,16 +1169,9 @@ void sp_item_gradient_invert_vector_color(SPItem *item, Inkscape::PaintTarget fi for (auto &child: vector->children) { if (auto stop = cast(&child)) { - // XXX This breaks icc / cmyk colors! - guint32 color = stop->get_rgba32(); - //g_message("Stop color %d", color); - color = SP_RGBA32_U_COMPOSE( - (255 - SP_RGBA32_R_U(color)), - (255 - SP_RGBA32_G_U(color)), - (255 - SP_RGBA32_B_U(color)), - SP_RGBA32_A_U(color) - ); - stop->setColor({color}, SP_RGBA32_A_U(color)); + auto color = stop->getColor(); + color.invert(); + stop->setColor(color); } } } @@ -1723,14 +1676,17 @@ static void sp_gradient_repr_set_link(Inkscape::XML::Node *repr, SPGradient *lin } -static void addStop(Inkscape::XML::Node *parent, SPColor color, double opacity, gchar const *offset) +static void addStop(Inkscape::XML::Node *parent, Color const &color, double opacity, gchar const *offset) { #ifdef SP_GR_VERBOSE g_message("addStop(%p, %s, %d, %s)", parent, color.c_str(), opacity, offset); #endif auto doc = parent->document(); Inkscape::XML::Node *repr = doc->createElement("svg:stop"); - SPStop::setColorRepr(repr, color, opacity); + + Color copy = color; + copy.addOpacity(opacity); + SPStop::setColorRepr(repr, copy); repr->setAttribute( "offset", offset ); parent->appendChild(repr); Inkscape::GC::release(repr); @@ -1739,7 +1695,7 @@ static void addStop(Inkscape::XML::Node *parent, SPColor color, double opacity, /* * Get default normalized gradient vector of document, create if there is none */ -SPGradient *sp_document_default_gradient_vector( SPDocument *document, SPColor const &color, double opacity, bool singleStop ) +SPGradient *sp_document_default_gradient_vector( SPDocument *document, Color const &color, double opacity, bool singleStop ) { SPDefs *defs = document->getDefs(); @@ -1783,10 +1739,14 @@ SPGradient *sp_document_default_gradient_vector( SPDocument *document, SPColor c SPGradient *sp_gradient_vector_for_object( SPDocument *const doc, SPDesktop *const desktop, SPObject *const o, Inkscape::PaintTarget const fill_or_stroke, bool singleStop ) { - SPColor color; + Color color(0x000000ff); double opacity = 1.0; bool for_fill = fill_or_stroke == Inkscape::FOR_FILL; + // if not o or o doesn't use flat color, then take current color of the desktop. + if (auto tmp = sp_desktop_get_color(desktop, for_fill)) + color = *tmp; + if (o && o->style) { // take the color of the object SPStyle const &style = *(o->style); @@ -1797,15 +1757,11 @@ SPGradient *sp_gradient_vector_for_object( SPDocument *const doc, SPDesktop *con return cast(server)->getVector(true); } } else if (paint.isColor()) { - color = paint.value.color; - opacity = SP_SCALE24_TO_FLOAT(for_fill ? style.fill_opacity.value : style.stroke_opacity.value); + color = paint.getColor(); + opacity = for_fill ? style.fill_opacity : style.stroke_opacity; } } - if (!color) { - // if not o or o doesn't use flat color, then take current color of the desktop. - color = sp_desktop_get_color(desktop, for_fill); - } return sp_document_default_gradient_vector(doc, color, opacity, singleStop); } diff --git a/src/gradient-chemistry.h b/src/gradient-chemistry.h index 5d3a71ffa3..1af443a679 100644 --- a/src/gradient-chemistry.h +++ b/src/gradient-chemistry.h @@ -26,6 +26,10 @@ class SPItem; class SPGradient; class SPDesktop; +namespace Inkscape::Colors { +class Color; +} +using Inkscape::Colors::Color; /** * Either normalizes given gradient to vector, or returns fresh normalized @@ -46,7 +50,7 @@ SPGradient *sp_item_set_gradient(SPItem *item, SPGradient *gr, SPGradientType ty /* * Get default normalized gradient vector of document, create if there is none */ -SPGradient *sp_document_default_gradient_vector( SPDocument *document, SPColor const &color, double opacity, bool singleStop ); +SPGradient *sp_document_default_gradient_vector( SPDocument *document, Color const &color, double opacity, bool singleStop ); /** * Return the preferred vector for \a o, made from (in order of preference) its current vector, @@ -72,14 +76,12 @@ std::pair sp_get_before_after_stops(SPStop* stop); unsigned int sp_number_of_stops(SPGradient const *gradient); unsigned int sp_number_of_stops_before_stop(SPGradient* gradient, SPStop *target); -guint32 average_color(guint32 c1, guint32 c2, double p = 0.5); - SPStop *sp_vector_add_stop(SPGradient *vector, SPStop* prev_stop, SPStop* next_stop, gfloat offset); void sp_gradient_delete_stop(SPGradient* gradient, SPStop* stop); SPStop* sp_gradient_add_stop(SPGradient* gradient, SPStop* current); SPStop* sp_gradient_add_stop_at(SPGradient* gradient, double offset); -void sp_set_gradient_stop_color(SPDocument* document, SPStop* stop, SPColor color, double opacity); +void sp_set_gradient_stop_color(SPDocument* document, SPStop* stop, Color const &color); void sp_gradient_transform_multiply(SPGradient *gradient, Geom::Affine postmul, bool set); @@ -120,7 +122,7 @@ SPGradientSpread sp_item_gradient_get_spread(SPItem *item, Inkscape::PaintTarget SPStop* sp_item_gradient_get_stop(SPItem *item, GrPointType point_type, guint point_i, Inkscape::PaintTarget fill_or_stroke); void sp_item_gradient_stop_set_style(SPItem *item, GrPointType point_type, unsigned int point_i, Inkscape::PaintTarget fill_or_stroke, SPCSSAttr *stop); -guint32 sp_item_gradient_stop_query_style(SPItem *item, GrPointType point_type, unsigned int point_i, Inkscape::PaintTarget fill_or_stroke); +Color sp_item_gradient_stop_query_style(SPItem *item, GrPointType point_type, unsigned int point_i, Inkscape::PaintTarget fill_or_stroke); void sp_item_gradient_reverse_vector(SPItem *item, Inkscape::PaintTarget fill_or_stroke); void sp_item_gradient_invert_vector_color(SPItem *item, Inkscape::PaintTarget fill_or_stroke); diff --git a/src/gradient-drag.cpp b/src/gradient-drag.cpp index 55b108e204..8bb3799df4 100644 --- a/src/gradient-drag.cpp +++ b/src/gradient-drag.cpp @@ -20,6 +20,9 @@ #include +#include "colors/utils.h" +#include "colors/color-set.h" + #include "desktop-style.h" #include "desktop.h" #include "document-undo.h" @@ -125,12 +128,9 @@ static int gr_drag_style_query(SPStyle *style, int property, gpointer data) return QUERY_STYLE_NOTHING; } else { int ret = QUERY_STYLE_NOTHING; - - float cf[4]; - cf[0] = cf[1] = cf[2] = cf[3] = 0; + auto colors = Colors::ColorSet(); SPStop* selected = nullptr; - int count = 0; for(auto d : drag->selected) { //for all selected draggers for(auto draggable : d->draggables) { //for all draggables of dragger if (ret == QUERY_STYLE_NOTHING) { @@ -140,39 +140,32 @@ static int gr_drag_style_query(SPStyle *style, int property, gpointer data) ret = QUERY_STYLE_MULTIPLE_AVERAGED; } - guint32 c = sp_item_gradient_stop_query_style (draggable->item, draggable->point_type, draggable->point_i, draggable->fill_or_stroke); - cf[0] += SP_RGBA32_R_F (c); - cf[1] += SP_RGBA32_G_F (c); - cf[2] += SP_RGBA32_B_F (c); - cf[3] += SP_RGBA32_A_F (c); - - count ++; + colors.set( + draggable->item->getId(), + sp_item_gradient_stop_query_style (draggable->item, draggable->point_type, draggable->point_i, draggable->fill_or_stroke) + ); } } - if (count) { - cf[0] /= count; - cf[1] /= count; - cf[2] /= count; - cf[3] /= count; + if (!colors.isEmpty()) { + auto avg = colors.getAverage(); + auto opacity = avg.stealOpacity(); // set both fill and stroke with our stop-color and stop-opacity style->fill.clear(); - style->fill.setColor( cf[0], cf[1], cf[2] ); + style->fill.setColor(avg); style->fill.set = TRUE; style->fill.setTag(selected); style->stroke.clear(); - style->stroke.setColor( cf[0], cf[1], cf[2] ); + style->stroke.setColor(avg); style->stroke.set = TRUE; style->stroke.setTag(selected); - style->fill_opacity.value = SP_SCALE24_FROM_FLOAT (cf[3]); - style->fill_opacity.set = TRUE; - style->stroke_opacity.value = SP_SCALE24_FROM_FLOAT (cf[3]); - style->stroke_opacity.set = TRUE; + style->fill_opacity.set_double(opacity); + style->stroke_opacity.set_double(opacity); - style->opacity.value = SP_SCALE24_FROM_FLOAT (cf[3]); - style->opacity.set = TRUE; + // This seems wrong, it duplicates the opacity + style->opacity.set_double(opacity); } return ret; @@ -304,35 +297,20 @@ bool GrDrag::styleSet( const SPCSSAttr *css, bool switch_style) return local_change; // true if handled } -guint32 GrDrag::getColor() +Inkscape::Colors::Color GrDrag::getColor() { - if (selected.empty()) return 0; - - float cf[4]; - cf[0] = cf[1] = cf[2] = cf[3] = 0; - - int count = 0; + if (selected.empty()) + return Color(0x000000ff); + Inkscape::Colors::ColorSet colors; for(auto d : selected) { //for all selected draggers for(auto draggable : d->draggables) { //for all draggables of dragger - guint32 c = sp_item_gradient_stop_query_style (draggable->item, draggable->point_type, draggable->point_i, draggable->fill_or_stroke); - cf[0] += SP_RGBA32_R_F (c); - cf[1] += SP_RGBA32_G_F (c); - cf[2] += SP_RGBA32_B_F (c); - cf[3] += SP_RGBA32_A_F (c); - - count ++; + colors.set( + draggable->item->getId(), + sp_item_gradient_stop_query_style (draggable->item, draggable->point_type, draggable->point_i, draggable->fill_or_stroke)); } } - - if (count) { - cf[0] /= count; - cf[1] /= count; - cf[2] /= count; - cf[3] /= count; - } - - return SP_RGBA32_F_COMPOSE(cf[0], cf[1], cf[2], cf[3]); + return colors.getAverage(); } // TODO refactor early returns diff --git a/src/gradient-drag.h b/src/gradient-drag.h index 98e8957244..86fef80ca3 100644 --- a/src/gradient-drag.h +++ b/src/gradient-drag.h @@ -44,6 +44,9 @@ class SPRadialGradient; class SPStop; namespace Inkscape { +namespace Colors { +class Color; +} class Selection; class CanvasItemCurve; struct KeyPressEvent; @@ -177,7 +180,7 @@ public: // FIXME: make more of this private! void deleteSelected(bool just_one = false); - guint32 getColor(); + Inkscape::Colors::Color getColor(); bool keep_selection; diff --git a/src/hsluv.cpp b/src/hsluv.cpp deleted file mode 100644 index 64f30e74cb..0000000000 --- a/src/hsluv.cpp +++ /dev/null @@ -1,506 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * HSLuv-C: Human-friendly HSL - * - * Authors: - * 2015 Alexei Boronine (original idea, JavaScript implementation) - * 2015 Roger Tallada (Obj-C implementation) - * 2017 Martin Mitas (C implementation, based on Obj-C implementation) - * 2021 Massinissa Derriche (C++ implementation for Inkscape, based on C implementation) - * - * Copyright (C) 2021 Authors - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -#include "hsluv.h" - -#include -#include -#include -#include -#include <2geom/line.h> -#include <2geom/ray.h> - -namespace Hsluv { - -/* for sRGB, reference white D65 */ -static const Triplet m[3] = { - { 3.24096994190452134377, -1.53738317757009345794, -0.49861076029300328366 }, - { -0.96924363628087982613, 1.87596750150772066772, 0.04155505740717561247 }, - { 0.05563007969699360846, -0.20397695888897656435, 1.05697151424287856072 } -}; - -/* for XYZ */ -static const Triplet m_inv[3] = { - { 0.41239079926595948129, 0.35758433938387796373, 0.18048078840183428751 }, - { 0.21263900587151035754, 0.71516867876775592746, 0.07219231536073371500 }, - { 0.01933081871559185069, 0.11919477979462598791, 0.95053215224966058086 } -}; - -static const double REF_U = 0.19783000664283680764; -static const double REF_V = 0.46831999493879100370; - -// CIE LUV constants -static const double KAPPA = 903.29629629629629629630; -static const double EPSILON = 0.00885645167903563082; - -/** - * Calculate the bounds of the Luv colors in RGB gamut. - * - * @param l Lightness. Between 0.0 and 100.0. - * @return Bounds of Luv colors in RGB gamut. - */ -std::array get_bounds(double l) -{ - std::array bounds; - - double tl = l + 16.0; - double sub1 = (tl * tl * tl) / 1560896.0; - double sub2 = (sub1 > EPSILON ? sub1 : (l / KAPPA)); - int channel; - int t; - - for(channel = 0; channel < 3; channel++) { - double m1 = m[channel][0]; - double m2 = m[channel][1]; - double m3 = m[channel][2]; - - for (t = 0; t < 2; t++) { - double top1 = (284517.0 * m1 - 94839.0 * m3) * sub2; - double top2 = (838422.0 * m3 + 769860.0 * m2 + 731718.0 * m1) * l * sub2 - 769860.0 * t * l; - double bottom = (632260.0 * m3 - 126452.0 * m2) * sub2 + 126452.0 * t; - - bounds[channel * 2 + t].setCoefficients(top1, -bottom, top2); - } - } - - return bounds; -} - -/** - * Calculate the maximum in gamut chromaticity for the given luminance and hue. - * - * @param l Luminance. - * @param h Hue. - * @return The maximum chromaticity. - */ -static double max_chroma_for_lh(double l, double h) -{ - double min_len = std::numeric_limits::max(); - auto const ray = Geom::Ray(Geom::Point(0, 0), Geom::rad_from_deg(h)); - - for (auto const &line : get_bounds(l)) { - auto intersections = line.intersect(ray); - if (intersections.empty()) { - continue; - } - double len = intersections[0].point().length(); - - if (len >= 0 && len < min_len) { - min_len = len; - } - } - - return min_len; -} - -/** - * Calculate the dot product of the given arrays. - * - * @param t1 The first array. - * @param t2 The second array. - * @return The resulting dot product. - */ -static double dot_product(const Triplet &t1, const Triplet &t2) -{ - return (t1[0] * t2[0] + t1[1] * t2[1] + t1[2] * t2[2]); -} - -/** - * Convenience function used for RGB conversions. - * - * @param c Value. - * @return RGB color component. - */ -double from_linear(double c) -{ - if (c <= 0.0031308) { - return 12.92 * c; - } else { - return 1.055 * std::pow(c, 1.0 / 2.4) - 0.055; - } -} - -/** - * Convenience function used for RGB conversions. - * - * @param c Value. - * @return XYZ color component. - */ -double to_linear(double c) -{ - if (c > 0.04045) { - return std::pow((c + 0.055) / 1.055, 2.4); - } else { - return c / 12.92; - } -} - -/** - * @overload - * @param t RGB color components. - * @return XYZ color components. - */ -static Triplet to_linear(const Triplet &t) -{ - return { - to_linear(t[0]), - to_linear(t[1]), - to_linear(t[2]) - }; -} - -/** - * Convert a color from the the XYZ colorspace to the RGB colorspace. - * - * @param in_out[in,out] The XYZ color converted to a RGB color. - */ -static void xyz2rgb(Triplet &in_out) -{ - Triplet result; - for (size_t i : {0, 1, 2}) { - result[i] = from_linear(dot_product(m[i], in_out)); - } - in_out = result; -} - -/** - * Convert a color from the the RGB colorspace to the XYZ colorspace. - * - * @param in_out[in,out] The RGB color converted to a XYZ color. - */ -static void rgb2xyz(Triplet &in_out) -{ - Triplet rgbl = to_linear(in_out); - for (size_t i : {0, 1, 2}) { - in_out[i] = dot_product(m_inv[i], rgbl); - } -} - -/** - * Utility function used to convert from the XYZ colorspace to CIELuv. - * https://en.wikipedia.org/wiki/CIELUV - * - * @param y Y component of the XYZ color. - * @return Luminance component of Luv color. - */ -static double y2l(double y) -{ - if (y <= EPSILON) - return y * KAPPA; - else - return 116.0 * std::cbrt(y) - 16.0; -} - -/** - * Utility function used to convert from CIELuv colorspace to XYZ. - * - * @param l Luminance component of Luv color. - * @return Y component of the XYZ color. - */ -static double l2y(double l) -{ - if (l <= 8.0) { - return l / KAPPA; - } else { - double x = (l + 16.0) / 116.0; - return (x * x * x); - } -} - -/** - * Convert a color from the the XYZ colorspace to the Luv colorspace. - * - * @param in_out[in,out] The XYZ color converted to a Luv color. - */ -static void xyz2luv(Triplet &in_out) -{ - double const denominator = in_out[0] + (15.0 * in_out[1]) + (3.0 * in_out[2]); - double var_u = 4.0 * in_out[0] / denominator; - double var_v = 9.0 * in_out[1] / denominator; - double l = y2l(in_out[1]); - double u = 13.0 * l * (var_u - REF_U); - double v = 13.0 * l * (var_v - REF_V); - - in_out[0] = l; - if (l < 0.00000001) { - in_out[1] = 0.0; - in_out[2] = 0.0; - } else { - in_out[1] = u; - in_out[2] = v; - } -} - -/** - * Convert a color from the the Luv colorspace to the XYZ colorspace. - * - * @param in_out[in,out] The Luv color converted to a XYZ color. - */ -static void luv2xyz(Triplet &in_out) -{ - if (in_out[0] <= 0.00000001) { - /* Black would create a divide-by-zero error. */ - in_out[0] = 0.0; - in_out[1] = 0.0; - in_out[2] = 0.0; - return; - } - - double var_u = in_out[1] / (13.0 * in_out[0]) + REF_U; - double var_v = in_out[2] / (13.0 * in_out[0]) + REF_V; - double y = l2y(in_out[0]); - double x = -(9.0 * y * var_u) / ((var_u - 4.0) * var_v - var_u * var_v); - double z = (9.0 * y - (15.0 * var_v * y) - (var_v * x)) / (3.0 * var_v); - - in_out[0] = x; - in_out[1] = y; - in_out[2] = z; -} - -/** - * Convert a color from the the Luv colorspace to the LCH colorspace. - * - * @param in_out[in,out] The Luv color converted to a LCH color. - */ -static void luv2lch(Triplet &in_out) -{ - double l = in_out[0]; - auto uv = Geom::Point(in_out[1], in_out[2]); - double h; - double const c = uv.length(); - - /* Grays: disambiguate hue */ - if (c < 0.00000001) { - h = 0; - } else { - h = Geom::deg_from_rad(Geom::atan2(uv)); - if (h < 0.0) { - h += 360.0; - } - } - - in_out[0] = l; - in_out[1] = c; - in_out[2] = h; -} - -/** - * Convert a color from the the LCH colorspace to the Luv colorspace. - * - * @param in_out[in,out] The LCH color converted to a Luv color. - */ -static void lch2luv(Triplet &in_out) -{ - double sinhrad, coshrad; - Geom::sincos(Geom::rad_from_deg(in_out[2]), sinhrad, coshrad); - double u = coshrad * in_out[1]; - double v = sinhrad * in_out[1]; - - in_out[1] = u; - in_out[2] = v; -} - -/** - * Convert a color from the the HSLuv colorspace to the LCH colorspace. - * - * @param in_out[in,out] The HSLuv color converted to a LCH color. - */ -static void hsluv2lch(Triplet &in_out) -{ - double h = in_out[0]; - double s = in_out[1]; - double l = in_out[2]; - double c; - - /* White and black: disambiguate chroma */ - if(l > 99.9999999 || l < 0.00000001) { - c = 0.0; - } else { - c = max_chroma_for_lh(l, h) / 100.0 * s; - } - - /* Grays: disambiguate hue */ - if (s < 0.00000001) { - h = 0.0; - } - - in_out[0] = l; - in_out[1] = c; - in_out[2] = h; -} - -/** - * Convert a color from the the LCH colorspace to the HSLuv colorspace. - * - * @param in_out[in,out] The LCH color converted to a HSLuv color. - */ -static void lch2hsluv(Triplet &in_out) -{ - double l = in_out[0]; - double c = in_out[1]; - double h = in_out[2]; - double s; - - /* White and black: disambiguate saturation */ - if (l > 99.9999999 || l < 0.00000001) { - s = 0.0; - } else { - s = c / max_chroma_for_lh(l, h) * 100.0; - } - - /* Grays: disambiguate hue */ - if (c < 0.00000001) { - h = 0.0; - } - - in_out[0] = h; - in_out[1] = s; - in_out[2] = l; -} - -// Interface functions -Triplet luv_to_rgb(double l, double u, double v) -{ - Triplet result{l, u, v}; - luv2xyz(result); - xyz2rgb(result); - - for (size_t i : {0, 1, 2}) { - result[i] = std::clamp(result[i], 0.0, 1.0); - } - return result; -} - -Triplet hsluv_to_luv(double h, double s, double l) -{ - Triplet result{h, s, l}; - hsluv2lch(result); - lch2luv(result); - return result; -} - -Triplet luv_to_hsluv(double l, double u, double v) -{ - Triplet result{l, u, v}; - luv2lch(result); - lch2hsluv(result); - return result; -} - -Triplet rgb_to_hsluv(double r, double g, double b) -{ - Triplet result{r, g, b}; - rgb2xyz(result); - xyz2luv(result); - luv2lch(result); - lch2hsluv(result); - return result; -} - -Triplet hsluv_to_rgb(double h, double s, double l) -{ - Triplet result{h, s, l}; - hsluv2lch(result); - lch2luv(result); - luv2xyz(result); - xyz2rgb(result); - - for (size_t i : {0, 1, 2}) { - result[i] = std::clamp(result[i], 0.0, 1.0); - } - return result; -} - -Triplet hsluv_to_luv(double *hsl) -{ - return hsluv_to_luv(hsl[0], hsl[1], hsl[2]); -} - -double perceptual_lightness(double l) -{ - return l <= 0.885645168 ? l * 0.09032962963 : std::cbrt(l) * 0.249914424 - 0.16; -} - -double rgb_to_perceptual_lightness(Triplet const &rgb) -{ - return perceptual_lightness(rgb_to_hsluv(rgb[0], rgb[1], rgb[2])[2]); -} - -std::pair get_contrasting_color(double l) -{ - double constexpr l_threshold = 0.85; - if (l > l_threshold) { // Draw dark over light. - auto t = (l - l_threshold) / (1.0 - l_threshold); - return {0.0, 0.4 - 0.1 * t}; - } else { // Draw light over dark. - auto t = (l_threshold - l) / l_threshold; - return {1.0, 0.6 + 0.1 * t}; - } -} - -// http://www.brucelindbloom.com/index.html?Eqn_Lab_to_XYZ.html -Triplet cielab_to_xyz(double L, double a, double b, const Triplet& ref_white_xyz) { - auto fy = (L + 16) / 116; - auto fx = a / 500 + fy; - auto fz = fy - b / 200; - - constexpr auto e = 0.008856; - constexpr auto k = 903.3; - - auto fx3 = std::pow(fx, 3); - auto fy3 = std::pow(fy, 3); - auto fz3 = std::pow(fz, 3); - - auto xr = fx3 > e ? fx3 : (116 * fx - 16) / k; - auto yr = L > k * e ? fy3 : L / k; - auto zr = fz3 > e ? fz3 : (116 * fz - 16) / k; - - return {xr * ref_white_xyz[0], yr * ref_white_xyz[1], zr * ref_white_xyz[2]}; -} - -Triplet lab_to_rgb(double L, double a, double b, const Triplet& ref_white_xyz) { - auto result = cielab_to_xyz(L, a, b, ref_white_xyz); - xyz2rgb(result); - for (size_t i : {0, 1, 2}) { - result[i] = std::clamp(result[i], 0.0, 1.0); - } - return result; -} - -// CIE standard illuminant D65, Observer= 2° [0.9504, 1.0000, 1.0888]. -// Simulates noon daylight with correlated color temperature of 6504 K. -constexpr Triplet illuminant_d65{ 0.9504, 1.0000, 1.0888 }; - -Triplet lab_to_rgb(double L, double a, double b) { - return lab_to_rgb(L, a, b, illuminant_d65); -} - -} // namespace Hsluv diff --git a/src/hsluv.h b/src/hsluv.h deleted file mode 100644 index b8d814d9b8..0000000000 --- a/src/hsluv.h +++ /dev/null @@ -1,174 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * HSLuv-C: Human-friendly HSL - * - * Authors: - * 2015 Alexei Boronine (original idea, JavaScript implementation) - * 2015 Roger Tallada (Obj-C implementation) - * 2017 Martin Mitas (C implementation, based on Obj-C implementation) - * 2021 Massinissa Derriche (C++ implementation for Inkscape, based on C implementation) - * - * Copyright (C) 2021 Authors - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -#ifndef SEEN_HSLUV_H -#define SEEN_HSLUV_H - -#include -#include <2geom/line.h> - -namespace Hsluv { - -// Types -using Triplet = std::array; - -/** - * Used to represent the in RGB gamut colors polygon of the HSLuv color wheel. - */ -struct PickerGeometry { - std::vector vertices; ///< Vertices, in counter-clockwise order. - double outer_circle_radius; ///< Smallest circle with center at origin such that polygon fits inside. - double inner_circle_radius; ///< Largest circle with center at origin such that it fits inside polygon. -}; - -// Functions -/** Apply sRGB gamma compression to a linear RGB color component. */ -double from_linear(double c); - -/** Convert an sRGB color component to linear RGB (de-gamma). */ -double to_linear(double c); - -/** - * Return the bounds of the Luv colors in RGB gamut. - * - * @param l Lightness. Between 0.0 and 100.0. - * @return Bounds of Luv colors in RGB gamut. - */ -std::array get_bounds(double l); - -/** - * Convert Luv to RGB. - * - * @param l Luminance. Between 0.0 and 100.0. - * @param u U coordinate. - * @param v V coordinate. - * @return An RGB triplet, with all components between 0.0 and 1.0. - */ -Triplet luv_to_rgb(double l, double u, double v); - -/** - * Convert Lab to sRGB. - * - * @param L Luminance. Between 0.0 and 100.0. - * @param a A coordinate. - * @param b B coordinate. - * @param ref_white_xyz White reference in normalized XYZ color space. - * @return An sRGB triplet, with all components between 0.0 and 1.0. - */ -Triplet lab_to_rgb(double L, double a, double b, const Triplet& ref_white_xyz); -/** - * Convert Lab to sRGB using D65 illuminant reference point. - * - * @param L Luminance. Between 0.0 and 100.0. - * @param a A coordinate. - * @param b B coordinate. - * @return An sRGB triplet, with all components between 0.0 and 1.0. - */ -Triplet lab_to_rgb(double L, double a, double b); - -Triplet cielab_to_xyz(double L, double a, double b, const Triplet& ref_white_xyz); - -/** - * Convert HSLuv to Luv. - * - * @param hsl A pointer to a buffer of length 3 containing an HSLuv color: - * [0]: Hue between 0.0 and 360.0. - * [1]: Saturation between 0.0 and 100.0. - * [2]: Lightness between 0.0 and 100.0. - * @return An LUV triplet, with luminance between 0.0 and 100.0. - */ -Triplet hsluv_to_luv(double *hsl); - -/** - * Convert Luv to HSLuv. - * - * @param l Luminance. Between 0.0 and 100.0. - * @param u U coordinate. - * @param v V coordinate. - * @return An HSLuv triplet containing: - * [0]: Hue between 0.0 and 360.0; - * [1]: Saturation between 0.0 and 100.0; - * [2]: Lightness between 0.0 and 100.0. - */ -Triplet luv_to_hsluv(double l, double u, double v); - -/** - * Convert RGB to HSLuv. - * - * @param r Red. Between 0.0 and 1.0. - * @param g Green. Between 0.0 and 1.0. - * @param b Blue. Between 0.0 and 1.0. - * @return An HSLuv triplet containing: - * [0]: Hue between 0.0 and 360.0; - * [1]: Saturation between 0.0 and 100.0; - * [2]: Lightness between 0.0 and 100.0. - */ -Triplet rgb_to_hsluv(double r, double g, double b); - -/** - * Convert HSLuv to RGB. - * - * @param h Hue. Between 0.0 and 360.0. - * @param s Saturation. Between 0.0 and 100.0. - * @param l Lightness. Between 0.0 and 100.0. - * @return An RGB triplet, with all components between 0.0 and 1.0. - */ -Triplet hsluv_to_rgb(double h, double s, double l); - -/** - * Calculate the perceptual lightness of an HSLuv color. - * - * @param l The lightness component in HSLuv coordinates, between 0.0 and 100.0. - * @return The perceptual lightness between 0.0 (black) and 1.0 (white). - */ -double perceptual_lightness(double l); - -/** - * Calculate the perceptual lightness of an RGB color. - * - * @param rgb A triplet of RGB components between 0.0 and 1.0. - * @return The perceptual lightness between 0.0 (black) and 1.0 (white). - */ -double rgb_to_perceptual_lightness(Triplet const &rgb); - -/** - * Get a contrasting grayscale color suitable for UI elements shown against - * a background color with the specified perceptual lightness. - * - * @param perceptual_lightness The perceptual lightness of the background, between 0.0 and 1.0. - * @return A pair consisting of grayscale and alpha components representing a color which will - * be easy to spot against the background. Both components are between 0.0 and 1.0. - */ -std::pair get_contrasting_color(double perceptual_lightness); - -} // namespace Hsluv - -#endif // SEEN_HSLUV_H diff --git a/src/id-clash.cpp b/src/id-clash.cpp index 4474050928..3c14433514 100644 --- a/src/id-clash.cpp +++ b/src/id-clash.cpp @@ -306,8 +306,8 @@ static void find_references(SPObject *elem, refmap_type &refmap, bool from_clipb for (unsigned i = 0; i < NUM_SPIPAINT_PROPERTIES; ++i) { const SPIPaint SPStyle::*prop = SPIPaint_members[i]; const SPIPaint *paint = &(style->*prop); - if (paint->isPaintserver() && paint->value.href) { - const SPObject *obj = paint->value.href->getObject(); + if (paint->isPaintserver() && paint->href) { + const SPObject *obj = paint->href->getObject(); if (obj) { const gchar *id = obj->getId(); IdReference idref = { REF_STYLE, elem, SPIPaint_properties[i] }; diff --git a/src/inkscape.cpp b/src/inkscape.cpp index 8be02304e7..7b23590b6b 100644 --- a/src/inkscape.cpp +++ b/src/inkscape.cpp @@ -43,7 +43,6 @@ #include "path-prefix.h" #include "selection.h" -#include "color/cms-system.h" #include "debug/simple-event.h" #include "debug/event-tracker.h" #include "io/resource.h" @@ -296,7 +295,6 @@ Application::~Application() } Inkscape::Preferences::unload(); - Inkscape::CMSSystem::unload(); _S_inst = nullptr; // this will probably break things diff --git a/src/inkscape.h b/src/inkscape.h index 361f57b31c..a5f0cf0698 100644 --- a/src/inkscape.h +++ b/src/inkscape.h @@ -25,7 +25,6 @@ class SPDesktop; class SPDocument; -struct SPColor; namespace Inkscape { @@ -139,7 +138,6 @@ public: // these are orphaned signals (nothing emits them and nothing connects to them) sigc::signal signal_destroy_document; - sigc::signal signal_color_set; // inkscape is quitting sigc::signal signal_shut_down; diff --git a/src/io/file-export-cmd.cpp b/src/io/file-export-cmd.cpp index aeb1706560..34a8e1e2cb 100644 --- a/src/io/file-export-cmd.cpp +++ b/src/io/file-export-cmd.cpp @@ -24,6 +24,8 @@ #include #include // PNG export +#include "colors/color.h" +#include "colors/manager.h" #include "document.h" #include "extension/db.h" #include "extension/extension.h" @@ -42,7 +44,6 @@ #include "page-manager.h" #include "path-chemistry.h" // sp_item_list_to_curves #include "selection-chemistry.h" // fit_canvas_to_drawing -#include "svg/svg-color.h" // Background color #include "text-editing.h" // te_update_layout_now_recursive #include "util/units.h" #include "util/parse-int-range.h" @@ -439,10 +440,12 @@ int InkFileExportCmd::do_export_vector(SPDocument *doc, std::string const &expor } guint32 InkFileExportCmd::get_bgcolor(SPDocument *doc) { - guint32 bgcolor = 0x00000000; + Inkscape::Colors::Color bgcolor(0xffffffff); if (!export_background.empty()) { // override the page color - bgcolor = sp_svg_read_color(export_background.c_str(), 0xffffff00); + if (auto c = Color::parse(export_background)) { + bgcolor = *c; + } // default is opaque if a color is given on commandline if (export_background_opacity < -.5 ) { export_background_opacity = 255; @@ -451,26 +454,27 @@ guint32 InkFileExportCmd::get_bgcolor(SPDocument *doc) { // read from namedview Inkscape::XML::Node *nv = doc->getReprNamedView(); if (nv && nv->attribute("pagecolor")){ - bgcolor = sp_svg_read_color(nv->attribute("pagecolor"), 0xffffff00); + if (auto c = Color::parse(nv->attribute("pagecolor"))) { + bgcolor = *c; + } } } if (export_background_opacity > -.5) { // if the value is manually set if (export_background_opacity > 1.0) { float value = CLAMP (export_background_opacity, 1.0f, 255.0f); - bgcolor |= (guint32) floor(value); + bgcolor.addOpacity(floor(value) / 255.0); } else { float value = CLAMP (export_background_opacity, 0.0f, 1.0f); - bgcolor |= SP_COLOR_F_TO_U(value); + bgcolor.addOpacity(value); } } else { Inkscape::XML::Node *nv = doc->getReprNamedView(); if (nv && nv->attribute("inkscape:pageopacity")){ - double opacity = nv->getAttributeDouble("inkscape:pageopacity", 1.0); - bgcolor |= SP_COLOR_F_TO_U(opacity); + bgcolor.addOpacity(nv->getAttributeDouble("inkscape:pageopacity", 1.0)); } // else it's transparent } - return bgcolor; + return bgcolor.toRGBA(); } /** diff --git a/src/layer-manager.cpp b/src/layer-manager.cpp index 1ecd0bc5f3..2fd4e174d5 100644 --- a/src/layer-manager.cpp +++ b/src/layer-manager.cpp @@ -85,6 +85,8 @@ void LayerManager::_layer_activated(SPObject *layer) void LayerManager::_layer_deactivated(SPObject *layer) { + if (!_document) + return; // happens on destruction if (auto group = cast(layer)) { group->setLayerDisplayMode(_desktop->dkey, SPGroup::GROUP); } diff --git a/src/live_effects/fill-conversion.cpp b/src/live_effects/fill-conversion.cpp index 4d8c11560f..99b46a1188 100644 --- a/src/live_effects/fill-conversion.cpp +++ b/src/live_effects/fill-conversion.cpp @@ -19,7 +19,6 @@ #include "object/sp-shape.h" #include "object/sp-defs.h" #include "object/sp-paint-server.h" -#include "svg/svg-color.h" #include "svg/css-ostringstream.h" #include "style.h" #include "util/units.h" @@ -81,26 +80,23 @@ static SPObject *get_linked_fill(SPObject *source) static void convert_fill_color(SPCSSAttr *css, SPObject *source) { - char c[64]; - - sp_svg_write_color(c, sizeof(c), source->style->fill.value.color.toRGBA32(SP_SCALE24_TO_FLOAT(source->style->fill_opacity.value))); - sp_repr_css_set_property(css, "fill", c); + auto color = source->style->fill.getColor(); + color.addOpacity(source->style->fill_opacity.value); + sp_repr_css_set_property_string(css, "fill", color.toString()); } static void convert_stroke_color(SPCSSAttr *css, SPObject *source) { - char c[64]; - - sp_svg_write_color(c, sizeof(c), source->style->stroke.value.color.toRGBA32(SP_SCALE24_TO_FLOAT(source->style->stroke_opacity.value))); - sp_repr_css_set_property(css, "fill", c); + auto color = source->style->stroke.getColor(); + color.addOpacity(source->style->stroke_opacity.value); + sp_repr_css_set_property_string(css, "fill", color.toString()); } static void revert_stroke_color(SPCSSAttr *css, SPObject *source) { - char c[64]; - - sp_svg_write_color(c, sizeof(c), source->style->fill.value.color.toRGBA32(SP_SCALE24_TO_FLOAT(source->style->fill_opacity.value))); - sp_repr_css_set_property(css, "stroke", c); + auto color = source->style->fill.getColor(); + color.addOpacity(source->style->fill_opacity.value); + sp_repr_css_set_property_string(css, "stroke", color.toString()); } static void convert_fill_server(SPCSSAttr *css, SPObject *source) diff --git a/src/live_effects/lpe-measure-segments.cpp b/src/live_effects/lpe-measure-segments.cpp index 2926a88802..bc073d299d 100644 --- a/src/live_effects/lpe-measure-segments.cpp +++ b/src/live_effects/lpe-measure-segments.cpp @@ -46,7 +46,6 @@ #include "object/sp-shape.h" #include "object/sp-text.h" #include "svg/stringstream.h" -#include "svg/svg-color.h" #include "svg/svg.h" #include "text-editing.h" #include "ui/pack.h" @@ -73,7 +72,7 @@ LPEMeasureSegments::LPEMeasureSegments(LivePathEffectObject *lpeobject) : Effect(lpeobject), unit(_("Unit"), _("Unit of measurement"), "unit", &wr, this, "mm"), orientation(_("Orientation"), _("Orientation of the line and labels"), "orientation", OMConverter, &wr, this, OM_PARALLEL, false), - coloropacity(_("Color and opacity"), _("Set color and opacity of the dimensions"), "coloropacity", &wr, this, 0x000000ff), + coloropacity(_("Color and opacity"), _("Set color and opacity of the dimensions"), "coloropacity", &wr, this, Colors::Color(0x000000ff)), fontbutton(_("Font"), _("Select font for labels"), "fontbutton", &wr, this), precision(_("Precision"), _("Number of digits after the decimal point"), "precision", &wr, this, 2), fix_overlaps(_("Merge overlaps °"), _("Minimum angle at which overlapping dimension lines are merged into one, use 180° to disable merging"), "fix_overlaps", &wr, this, 0), @@ -108,6 +107,7 @@ LPEMeasureSegments::LPEMeasureSegments(LivePathEffectObject *lpeobject) : centers(_("Add object center"), _("Add the projected object center"), "centers", &wr, this, false), maxmin(_("Only max and min"), _("Compute only max/min projection values"), "maxmin", &wr, this, false), helpdata(_("Help"), _("Measure segments help"), "helpdata", &wr, this, "", "") + , color(0x000000ff) { //set to true the parameters you want to be changed his default values registerParameter(&unit); @@ -192,7 +192,6 @@ LPEMeasureSegments::LPEMeasureSegments(LivePathEffectObject *lpeobject) : pagenumber = 0; anotation_width = 0; fontsize = 0; - rgb32 = 0; arrow_gap = 0; } @@ -290,12 +289,12 @@ LPEMeasureSegments::createArrowMarker(Glib::ustring mode) } Glib::ustring lpobjid = this->lpeobj->getId(); Glib::ustring itemid = sp_lpe_item->getId(); - Glib::ustring style; - style = Glib::ustring("fill:context-stroke;"); - Inkscape::SVGOStringStream os; - os << SP_RGBA32_A_F(coloropacity.get_value()); - style = style + Glib::ustring(";fill-opacity:") + Glib::ustring(os.str()); - style = style + Glib::ustring(";stroke:none"); + + auto const css = sp_repr_css_attr_new(); + sp_repr_css_set_property_string(css, "fill", "context-stroke"); + sp_repr_css_set_property_double(css, "fill-opacity", coloropacity.get_value()->getOpacity()); + sp_repr_css_set_property_string(css, "stroke", "none"); + Inkscape::XML::Document *xml_doc = document->getReprDoc(); SPObject *elemref = nullptr; Inkscape::XML::Node *arrow = nullptr; @@ -307,7 +306,7 @@ LPEMeasureSegments::createArrowMarker(Glib::ustring mode) Inkscape::XML::Node *arrow_data = arrow->firstChild(); if (arrow_data) { arrow_data->removeAttribute("transform"); - arrow_data->setAttribute("style", style); + sp_repr_css_change(arrow_data, css, "style"); } } } else { @@ -342,12 +341,13 @@ LPEMeasureSegments::createArrowMarker(Glib::ustring mode) arrow_path->setAttributeOrRemoveIfEmpty("class", classarrowpath); Glib::ustring arrowpath = mode + Glib::ustring("_path"); arrow_path->setAttribute("id", arrowpath); - arrow_path->setAttribute("style", style); + sp_repr_css_change(arrow_path, css, "style"); arrow->addChild(arrow_path, nullptr); Inkscape::GC::release(arrow_path); elemref = document->getDefs()->appendChildRepr(arrow); Inkscape::GC::release(arrow); } + sp_repr_css_attr_unref(css); items.push_back(mode); } @@ -418,21 +418,15 @@ LPEMeasureSegments::createTextLabel(Geom::Point &pos, size_t counter, double len setlocale (LC_NUMERIC, "C"); font_size << fontsize << "px"; setlocale (LC_NUMERIC, locale_base); - gchar c[32]; - safeprintf(c, "#%06x", rgb32 >> 8); - sp_repr_css_set_property (css, "fill",c); - Inkscape::SVGOStringStream os; - os << SP_RGBA32_A_F(coloropacity.get_value()); - sp_repr_css_set_property (css, "fill-opacity",os.str().c_str()); + sp_repr_css_set_property_string(css, "fill", color.toString(false)); + sp_repr_css_set_property_double(css, "fill-opacity", color.getOpacity()); sp_repr_css_set_property (css, "font-size",font_size.str().c_str()); sp_repr_css_unset_property (css, "-inkscape-font-specification"); if (remove) { sp_repr_css_set_property (css, "display","none"); } - Glib::ustring css_str; - sp_repr_css_write_string(css,css_str); - rtext->setAttributeOrRemoveIfEmpty("style", css_str); - rtspan->setAttributeOrRemoveIfEmpty("style", css_str); + sp_repr_css_change(rtext, css, "style"); + sp_repr_css_change(rtspan, css, "style"); rtspan->removeAttribute("transform"); sp_repr_css_attr_unref (css); if (legacy) { @@ -605,23 +599,18 @@ LPEMeasureSegments::createLine(Geom::Point start,Geom::Point end, Glib::ustring setlocale (LC_NUMERIC, locale_base); style += "stroke-width:"; style += stroke_w.str(); - gchar c[32]; - safeprintf(c, "#%06x", rgb32 >> 8); - style += ";stroke:"; - style += Glib::ustring(c); - Inkscape::SVGOStringStream os; - os << SP_RGBA32_A_F(coloropacity.get_value()); - style = style + Glib::ustring(";stroke-opacity:") + Glib::ustring(os.str()); + SPCSSAttr *css = sp_repr_css_attr_new(); sp_repr_css_attr_add_from_string(css, style.c_str()); - Glib::ustring css_str; - sp_repr_css_write_string(css,css_str); - line->setAttributeOrRemoveIfEmpty("style", css_str); + sp_repr_css_set_property_string(css, "stroke", color.toString(false)); + sp_repr_css_set_property_double(css, "stroke-opacity", color.getOpacity()); + sp_repr_css_change(line, css, "style"); + sp_repr_css_attr_unref (css); + if (!elemref) { elemref = document->getRoot()->appendChildRepr(line); Inkscape::GC::release(line); } - sp_repr_css_attr_unref (css); } void @@ -965,12 +954,12 @@ LPEMeasureSegments::doBeforeEffect (SPLPEItem const* lpeitem) //end projection prepare auto shape = cast(splpeitem); if (shape) { - guint32 color32 = coloropacity.get_value(); + auto opacity = coloropacity.get_value(); bool colorchanged = false; - if (color32 != rgb32) { + if (opacity != color) { colorchanged = true; } - rgb32 = std::move(color32); + color = *opacity; bool unitchanged = false; Glib::ustring currentunit = unit.get_abbreviation(); if (currentunit != prevunit) { diff --git a/src/live_effects/lpe-measure-segments.h b/src/live_effects/lpe-measure-segments.h index b0d9ccc398..575f9186a5 100644 --- a/src/live_effects/lpe-measure-segments.h +++ b/src/live_effects/lpe-measure-segments.h @@ -93,12 +93,12 @@ private: ScalarParam angle_projection; BoolParam avoid_overlapping; MessageParam helpdata; + Colors::Color color; bool legacy = false; double fontsize; double prevfontsize = 0; double anotation_width; double previous_size; - guint32 rgb32; Glib::ustring prevunit = ""; double arrow_gap; guint pagenumber; diff --git a/src/live_effects/lpe-powermask.cpp b/src/live_effects/lpe-powermask.cpp index 63e748abeb..a069559c25 100644 --- a/src/live_effects/lpe-powermask.cpp +++ b/src/live_effects/lpe-powermask.cpp @@ -35,7 +35,7 @@ LPEPowerMask::LPEPowerMask(LivePathEffectObject *lpeobject) //wrap(_("Wrap mask data"), _("Wrap mask data allowing previous filters"), "wrap", &wr, this, false), hide_mask(_("Hide mask"), _("Hide mask"), "hide_mask", &wr, this, false), background(_("Add background to mask"), _("Add background to mask"), "background", &wr, this, false), - background_color(_("Background color and opacity"), _("Set color and opacity of the background"), "background_color", &wr, this, 0xffffffff) + background_color(_("Background color and opacity"), _("Set color and opacity of the background"), "background_color", &wr, this, Colors::Color(0xffffffff)) { registerParameter(&uri); registerParameter(&invert); @@ -115,8 +115,8 @@ LPEPowerMask::doBeforeEffect (SPLPEItem const* lpeitem){ } mask = sp_lpe_item->getMaskObject(); if (mask) { - if (previous_color != background_color.get_value()) { - previous_color = background_color.get_value(); + if (previous_color != *background_color.get_value()) { + previous_color = *background_color.get_value(); setMask(); } else { uri.param_setValue(Glib::ustring(extract_uri(sp_lpe_item->getAttribute("mask"))), true); @@ -237,16 +237,12 @@ LPEPowerMask::setMask(){ box->setAttribute("id", box_id); exist = false; } - Glib::ustring style; - gchar c[32]; - unsigned const rgb24 = background_color.get_value() >> 8; - safeprintf(c, "#%06x", rgb24); - style = Glib::ustring("fill:") + Glib::ustring(c); - Inkscape::SVGOStringStream os; - os << SP_RGBA32_A_F(background_color.get_value()); - style = style + Glib::ustring(";fill-opacity:") + Glib::ustring(os.str()); - SPCSSAttr *css = sp_repr_css_attr_new(); - sp_repr_css_attr_add_from_string(css, style.c_str()); + + auto const css = sp_repr_css_attr_new(); + sp_repr_css_set_property_string(css, "fill", background_color.get_value()->toString(false)); + sp_repr_css_set_property_double(css, "fill-opacity", background_color.get_value()->getOpacity()); + sp_repr_css_set_property_string(css, "stroke", "none"); + char const* filter = sp_repr_css_property (css, "filter", nullptr); if(!filter || !strcmp(filter, filter_uri.c_str())) { if (invert && is_visible) { @@ -254,11 +250,9 @@ LPEPowerMask::setMask(){ } else { sp_repr_css_set_property (css, "filter", nullptr); } - } - Glib::ustring css_str; - sp_repr_css_write_string(css, css_str); - box->setAttributeOrRemoveIfEmpty("style", css_str); + sp_repr_css_change(box, css, "style"); + sp_repr_css_attr_unref(css); box->setAttribute("d", sp_svg_write_path(mask_box)); if (!exist) { elemref = mask->appendChildRepr(box); diff --git a/src/live_effects/lpe-powermask.h b/src/live_effects/lpe-powermask.h index aed32fc994..6bd72f79d5 100644 --- a/src/live_effects/lpe-powermask.h +++ b/src/live_effects/lpe-powermask.h @@ -36,8 +36,8 @@ private: BoolParam hide_mask; BoolParam background; ColorPickerParam background_color; + std::optional previous_color; Geom::Path mask_box; - guint32 previous_color; }; void sp_remove_powermask(Inkscape::Selection *sel); diff --git a/src/live_effects/parameter/colorpicker.cpp b/src/live_effects/parameter/colorpicker.cpp index 16c7e20f1b..ae53315d54 100644 --- a/src/live_effects/parameter/colorpicker.cpp +++ b/src/live_effects/parameter/colorpicker.cpp @@ -13,7 +13,6 @@ #include "document-undo.h" // for DocumentUndo #include "live_effects/effect.h" // for Effect #include "live_effects/parameter/colorpicker.h" // for ColorPickerParam -#include "svg/svg-color.h" // for guint32 #include "ui/icon-names.h" // for INKSCAPE_ICON #include "ui/pack.h" // for pack_start #include "ui/widget/registered-widget.h" // for RegisteredColorPicker @@ -26,7 +25,7 @@ namespace LivePathEffect { ColorPickerParam::ColorPickerParam( const Glib::ustring& label, const Glib::ustring& tip, const Glib::ustring& key, Inkscape::UI::Widget::Registry* wr, - Effect* effect, const guint32 default_color ) + Effect* effect, std::optional default_color) : Parameter(label, tip, key, wr, effect), value(default_color), defvalue(default_color) @@ -39,61 +38,29 @@ ColorPickerParam::param_set_default() param_setValue(defvalue); } -static guint32 sp_read_color_alpha(gchar const *str, guint32 def) -{ - guint32 val = 0; - if (str == nullptr) return def; - while ((*str <= ' ') && *str) str++; - if (!*str) return def; - - if (str[0] == '#') { - gint i; - for (i = 1; str[i]; i++) { - int hexval; - if (str[i] >= '0' && str[i] <= '9') - hexval = str[i] - '0'; - else if (str[i] >= 'A' && str[i] <= 'F') - hexval = str[i] - 'A' + 10; - else if (str[i] >= 'a' && str[i] <= 'f') - hexval = str[i] - 'a' + 10; - else - break; - val = (val << 4) + hexval; - } - if (i != 1 + 8) { - return def; - } - } - return val; -} - void ColorPickerParam::param_update_default(const gchar * default_value) { - defvalue = sp_read_color_alpha(default_value, 0x000000ff); + defvalue->set(default_value ? default_value : ""); } bool -ColorPickerParam::param_readSVGValue(const gchar * strvalue) +ColorPickerParam::param_readSVGValue(const gchar *val) { - param_setValue(sp_read_color_alpha(strvalue, 0x000000ff)); + param_setValue(Colors::Color::parse(val)); return true; } Glib::ustring ColorPickerParam::param_getSVGValue() const { - gchar c[32]; - safeprintf(c, "#%08x", value); - return c; + return value->toString(); } Glib::ustring ColorPickerParam::param_getDefaultSVGValue() const { - gchar c[32]; - safeprintf(c, "#%08x", defvalue); - return c; + return defvalue->toString(); } Gtk::Widget * @@ -114,7 +81,7 @@ ColorPickerParam::param_newWidget() { SPDocument *document = param_effect->getSPDoc(); DocumentUndo::ScopedInsensitive _no_undo(document); - colorpickerwdg->setRgba32(value); + colorpickerwdg->setColor(*value); } colorpickerwdg->set_undo_parameters(_("Change color button parameter"), INKSCAPE_ICON("dialog-path-effects")); @@ -124,7 +91,7 @@ ColorPickerParam::param_newWidget() } void -ColorPickerParam::param_setValue(const guint32 newvalue) +ColorPickerParam::param_setValue(std::optional newvalue) { value = newvalue; } diff --git a/src/live_effects/parameter/colorpicker.h b/src/live_effects/parameter/colorpicker.h index ff7abe0e5d..3f0416b015 100644 --- a/src/live_effects/parameter/colorpicker.h +++ b/src/live_effects/parameter/colorpicker.h @@ -22,7 +22,7 @@ public: const Glib::ustring& key, Inkscape::UI::Widget::Registry* wr, Effect* effect, - const guint32 default_color = 0x000000ff); + std::optional = {}); ~ColorPickerParam() override = default; Gtk::Widget * param_newWidget() override; @@ -31,18 +31,18 @@ public: Glib::ustring param_getSVGValue() const override; Glib::ustring param_getDefaultSVGValue() const override; - void param_setValue(guint32 newvalue); + void param_setValue(std::optional newvalue); void param_set_default() override; - guint32 get_value() const { return value; }; + std::optional get_value() const { return value; }; ParamType paramType() const override { return ParamType::COLOR_PICKER; }; private: ColorPickerParam(const ColorPickerParam&) = delete; ColorPickerParam& operator=(const ColorPickerParam&) = delete; - guint32 value; - guint32 defvalue; + std::optional value; + std::optional defvalue; }; } //namespace LivePathEffect diff --git a/src/object/color-profile.cpp b/src/object/color-profile.cpp index d8a2d17b01..79996837f5 100644 --- a/src/object/color-profile.cpp +++ b/src/object/color-profile.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /** @file - * TODO: insert short description here + * The color profile tag in an svg document *//* * Authors: see git history * @@ -14,7 +14,6 @@ #include #include "attributes.h" -#include "color/color-profile-cms-fns.h" // XXX REMOVE! #include "colors/cms/profile.h" #include "document.h" #include "inkscape.h" diff --git a/src/object/color-profile.h b/src/object/color-profile.h index 12c4273419..96c6f54b04 100644 --- a/src/object/color-profile.h +++ b/src/object/color-profile.h @@ -10,10 +10,6 @@ #ifndef SEEN_COLOR_PROFILE_H #define SEEN_COLOR_PROFILE_H -#include // XXX Remove during refactoring! - -#include "color.h" // XXX Remove during refactoring! -#include "color/cms-color-types.h" // XXX Remove as well #include "colors/cms/system.h" #include "colors/spaces/enum.h" // RenderingIntent #include "sp-object.h" @@ -62,33 +58,6 @@ private: Colors::RenderingIntent _intent; std::unique_ptr _uri; - -public: - // XXX Compatability code which will be removed in the main refactoring step, never populated - char *name = nullptr; - char *href = nullptr; - unsigned int rendering_intent; - - ColorSpaceSig getColorSpace() const { return {0}; }; - ColorProfileClassSig getProfileClass() const { return {0}; } - cmsHTRANSFORM getTransfToSRGB8() { return nullptr; } - cmsHTRANSFORM getTransfFromSRGB8() { return nullptr; } - cmsHTRANSFORM getTransfGamutCheck() { return nullptr; }; - bool GamutCheck(SPColor color) { return false; } - int getChannelCount() const { return 4; } - bool isPrintColorSpace() { return false; } - cmsHPROFILE getHandle() { return nullptr; } -}; - -// XXX Delete!!! -enum -{ - RENDERING_INTENT_UNKNOWN = 0, - RENDERING_INTENT_AUTO = 1, - RENDERING_INTENT_PERCEPTUAL = 2, - RENDERING_INTENT_RELATIVE_COLORIMETRIC = 3, - RENDERING_INTENT_SATURATION = 4, - RENDERING_INTENT_ABSOLUTE_COLORIMETRIC = 5 }; } // namespace Inkscape diff --git a/src/object/filters/diffuselighting.cpp b/src/object/filters/diffuselighting.cpp index 4d462b2fc6..b6a0793c7e 100644 --- a/src/object/filters/diffuselighting.cpp +++ b/src/object/filters/diffuselighting.cpp @@ -28,7 +28,6 @@ #include "display/nr-light-types.h" // for SpotLightData, Light... #include "object/filters/sp-filter-primitive.h" // for SPFilterPrimitive #include "object/sp-object.h" // for SP_OBJECT_MODIFIED_FLAG -#include "svg/svg-color.h" // for sp_svg_read_color #include "xml/node.h" // for Node class SPDocument; @@ -104,28 +103,7 @@ void SPFeDiffuseLighting::set(SPAttr key, char const *value) requestModified(SP_OBJECT_MODIFIED_FLAG); break; case SPAttr::LIGHTING_COLOR: { - char const *end_ptr = nullptr; - lighting_color = sp_svg_read_color(value, &end_ptr, 0xffffffff); - - // if a value was read - if (end_ptr) { - while (g_ascii_isspace(*end_ptr)) { - ++end_ptr; - } - - if (std::strncmp(end_ptr, "icc-color(", 10) == 0) { - icc.emplace(); - if (!sp_svg_read_icc_color(end_ptr, &*icc)) { - icc.reset(); - } - } - - lighting_color_set = true; - } else { - // lighting_color already contains the default value - lighting_color_set = false; - } - + lighting_color = Inkscape::Colors::Color::parse(value); requestModified(SP_OBJECT_MODIFIED_FLAG); break; } @@ -168,12 +146,8 @@ Inkscape::XML::Node *SPFeDiffuseLighting::write(Inkscape::XML::Document *doc, In } /*TODO kernelUnits */ - if (lighting_color_set) { - char c[64]; - sp_svg_write_color(c, sizeof(c), lighting_color); - repr->setAttribute("lighting-color", c); - } else { - repr->removeAttribute("lighting-color"); + if (lighting_color) { + repr->setAttributeOrRemoveIfEmpty("lighting-color", lighting_color->toString()); } SPFilterPrimitive::write(doc, repr, flags); @@ -206,10 +180,7 @@ std::unique_ptr SPFeDiffuseLighting::build_r diffuselighting->diffuseConstant = diffuseConstant; diffuselighting->surfaceScale = surfaceScale; - diffuselighting->lighting_color = lighting_color; - if (icc) { - diffuselighting->set_icc(*icc); - } + diffuselighting->lighting_color = lighting_color ? lighting_color->toRGBA() : 0x0; // We assume there is at most one child diffuselighting->light_type = Inkscape::Filters::NO_LIGHT; diff --git a/src/object/filters/diffuselighting.h b/src/object/filters/diffuselighting.h index 221b38553b..b09fe3ff68 100644 --- a/src/object/filters/diffuselighting.h +++ b/src/object/filters/diffuselighting.h @@ -16,11 +16,8 @@ #include #include #include "sp-filter-primitive.h" -#include "svg/svg-icc-color.h" #include "number-opt-number.h" -struct SVGICCColor; - namespace Inkscape { namespace Filters { class FilterDiffuseLighting; @@ -36,14 +33,13 @@ public: private: float surfaceScale = 1.0f; float diffuseConstant = 1.0f; - uint32_t lighting_color = 0xffffffff; + + std::optional lighting_color; bool surfaceScale_set = false; bool diffuseConstant_set = false; - bool lighting_color_set = false; NumberOptNumber kernelUnitLength; // TODO - std::optional icc; protected: void build(SPDocument *doc, Inkscape::XML::Node *repr) override; diff --git a/src/object/filters/flood.cpp b/src/object/filters/flood.cpp index 2bae7ca9ab..50b27c5e93 100644 --- a/src/object/filters/flood.cpp +++ b/src/object/filters/flood.cpp @@ -21,7 +21,6 @@ #include "display/nr-filter-flood.h" // for FilterFlood #include "object/filters/sp-filter-primitive.h" // for SPFilterPrimitive #include "object/sp-object.h" // for SP_OBJECT_MODIFIED_FLAG -#include "svg/svg-color.h" // for sp_svg_read_color class SPDocument; @@ -47,34 +46,9 @@ void SPFeFlood::set(SPAttr key, char const *value) { switch (key) { case SPAttr::FLOOD_COLOR: { - char const *end_ptr = nullptr; - uint32_t n_color = sp_svg_read_color(value, &end_ptr, 0x0); - bool modified = false; - if (n_color != color) { - color = n_color; - modified = true; - } - - if (end_ptr) { - while (g_ascii_isspace(*end_ptr)) { - ++end_ptr; - } - - if (std::strncmp(end_ptr, "icc-color(", 10) == 0) { - icc.emplace(); - - if (!sp_svg_read_icc_color(end_ptr, &*icc)) { - icc.reset(); - } - - modified = true; - } - } - - if (modified) { - requestModified(SP_OBJECT_MODIFIED_FLAG); - } + flood_color = Inkscape::Colors::Color::parse(value); + requestModified(SP_OBJECT_MODIFIED_FLAG); break; } case SPAttr::FLOOD_OPACITY: { @@ -107,13 +81,7 @@ std::unique_ptr SPFeFlood::build_renderer(In { auto flood = std::make_unique(); build_renderer_common(flood.get()); - - flood->set_opacity(opacity); - flood->set_color(color); - if (icc) { - flood->set_icc(*icc); - } - + flood->set_color(flood_color ? flood_color->toRGBA(opacity) : 0x0); return flood; } diff --git a/src/object/filters/flood.h b/src/object/filters/flood.h index 99cc4cfc76..f67183937c 100644 --- a/src/object/filters/flood.h +++ b/src/object/filters/flood.h @@ -16,7 +16,6 @@ #include #include #include "sp-filter-primitive.h" -#include "svg/svg-icc-color.h" class SPFeFlood final : public SPFilterPrimitive @@ -25,9 +24,9 @@ public: int tag() const override { return tag_of; } private: - uint32_t color = 0x0; + + std::optional flood_color; double opacity = 1.0; - std::optional icc; protected: void build(SPDocument *doc, Inkscape::XML::Node *repr) override; diff --git a/src/object/filters/sp-filter-primitive.h b/src/object/filters/sp-filter-primitive.h index 4935170b10..d15ba9e753 100644 --- a/src/object/filters/sp-filter-primitive.h +++ b/src/object/filters/sp-filter-primitive.h @@ -19,6 +19,9 @@ #include #include #include "2geom/rect.h" +#include "colors/color.h" +#include "colors/manager.h" +#include "document.h" #include "object/sp-object.h" #include "object/sp-dimensions.h" #include "display/nr-filter-types.h" diff --git a/src/object/filters/specularlighting.cpp b/src/object/filters/specularlighting.cpp index 10a9d61fdd..c59490c15a 100644 --- a/src/object/filters/specularlighting.cpp +++ b/src/object/filters/specularlighting.cpp @@ -26,7 +26,6 @@ #include "display/nr-light-types.h" // for SpotLightData, Light... #include "object/filters/sp-filter-primitive.h" // for SPFilterPrimitive #include "object/sp-object.h" // for SP_OBJECT_MODIFIED_FLAG -#include "svg/svg-color.h" // for sp_svg_read_color #include "xml/node.h" // for Node class SPDocument; @@ -116,24 +115,7 @@ void SPFeSpecularLighting::set(SPAttr key, char const *value) requestModified(SP_OBJECT_MODIFIED_FLAG); break; case SPAttr::LIGHTING_COLOR: { - char const *end_ptr = nullptr; - lighting_color = sp_svg_read_color(value, &end_ptr, 0xffffffff); - // if a value was read - if (end_ptr) { - while (g_ascii_isspace(*end_ptr)) { - ++end_ptr; - } - if (strneq(end_ptr, "icc-color(", 10)) { - if (!icc) icc.emplace(); - if (!sp_svg_read_icc_color(end_ptr, &*icc)) { - icc.reset(); - } - } - lighting_color_set = true; - } else { - // lighting_color already contains the default value - lighting_color_set = false; - } + lighting_color = Inkscape::Colors::Color::parse(value); requestModified(SP_OBJECT_MODIFIED_FLAG); break; } @@ -177,12 +159,10 @@ Inkscape::XML::Node *SPFeSpecularLighting::write(Inkscape::XML::Document *doc, I } // TODO kernelUnits - if (lighting_color_set) { - char c[64]; - sp_svg_write_color(c, sizeof(c), lighting_color); - repr->setAttribute("lighting-color", c); - } + if (lighting_color) { + repr->setAttributeOrRemoveIfEmpty("lighting-color", lighting_color->toString()); + } SPFilterPrimitive::write(doc, repr, flags); return repr; @@ -214,10 +194,7 @@ std::unique_ptr SPFeSpecularLighting::build_ specularlighting->specularConstant = specularConstant; specularlighting->specularExponent = specularExponent; specularlighting->surfaceScale = surfaceScale; - specularlighting->lighting_color = lighting_color; - if (icc) { - specularlighting->set_icc(*icc); - } + specularlighting->lighting_color = lighting_color ? lighting_color->toRGBA() : 0x0; // We assume there is at most one child specularlighting->light_type = Inkscape::Filters::NO_LIGHT; diff --git a/src/object/filters/specularlighting.h b/src/object/filters/specularlighting.h index 34d6c36c51..b47428b89b 100644 --- a/src/object/filters/specularlighting.h +++ b/src/object/filters/specularlighting.h @@ -17,12 +17,9 @@ #include #include -#include "svg/svg-icc-color.h" #include "sp-filter-primitive.h" #include "number-opt-number.h" -struct SVGICCColor; - namespace Inkscape { namespace Filters { class FilterSpecularLighting; @@ -39,15 +36,14 @@ private: float surfaceScale = 1.0f; float specularConstant = 1.0f; float specularExponent = 1.0f; - uint32_t lighting_color = 0xffffffff; + + std::optional lighting_color; bool surfaceScale_set = false; bool specularConstant_set = false; bool specularExponent_set = false; - bool lighting_color_set = false; NumberOptNumber kernelUnitLength; // TODO - std::optional icc; protected: void build(SPDocument *doc, Inkscape::XML::Node *repr) override; diff --git a/src/object/sp-gradient-vector.h b/src/object/sp-gradient-vector.h index 893d41e5fe..49a113e0f7 100644 --- a/src/object/sp-gradient-vector.h +++ b/src/object/sp-gradient-vector.h @@ -11,7 +11,7 @@ #define SEEN_SP_GRADIENT_VECTOR_H #include -#include "color.h" +#include "colors/color.h" /** * Differs from SPStop in that SPStop mirrors the \ element in the document, whereas @@ -22,9 +22,8 @@ * copying from SPStop to SPGradientStop. */ struct SPGradientStop { + std::optional color; double offset; - SPColor color; - float opacity; }; /** diff --git a/src/object/sp-gradient.cpp b/src/object/sp-gradient.cpp index 7b1406fec4..6723bfadfc 100644 --- a/src/object/sp-gradient.cpp +++ b/src/object/sp-gradient.cpp @@ -138,8 +138,7 @@ bool SPGradient::isEquivalent(SPGradient *that) bool effective = true; while (effective && (as && bs)) { - if (!as->getColor().isClose(bs->getColor(), 0.001) || - as->offset != bs->offset || as->getOpacity() != bs->getOpacity() ) { + if (!as->getColor().isClose(bs->getColor(), 0.001) || as->offset != bs->offset) { effective = false; break; } @@ -922,7 +921,8 @@ SPGradient::repr_write_vector() /* strictly speaking, offset an SVG rather than a CSS one, but exponents make no * sense for offset proportions. */ auto obj = cast(document->getObjectByRepr(child)); - obj->setColor(stop.color, stop.opacity); + if (auto color = stop.color) + obj->setColor(*color); /* Order will be reversed here */ l.push_back(child); } @@ -1020,10 +1020,7 @@ void SPGradient::rebuildVector() // 0%. Gradient offset values greater than 1 (or greater than 100%) are rounded // down to 100%." gstop.offset = CLAMP(gstop.offset, 0, 1); - gstop.color = stop->getColor(); - gstop.opacity = stop->getOpacity(); - vector.stops.push_back(gstop); } } @@ -1036,15 +1033,11 @@ void SPGradient::rebuildVector() { SPGradientStop gstop; gstop.offset = 0.0; - gstop.color.set( 0x00000000 ); - gstop.opacity = 0.0; vector.stops.push_back(gstop); } { SPGradientStop gstop; gstop.offset = 1.0; - gstop.color.set( 0x00000000 ); - gstop.opacity = 0.0; vector.stops.push_back(gstop); } } else { @@ -1056,7 +1049,6 @@ void SPGradient::rebuildVector() SPGradientStop gstop; gstop.offset = 0.0; gstop.color = vector.stops.front().color; - gstop.opacity = vector.stops.front().opacity; vector.stops.insert(vector.stops.begin(), gstop); } if (vector.stops.back().offset < 1.0) { @@ -1064,7 +1056,6 @@ void SPGradient::rebuildVector() SPGradientStop gstop; gstop.offset = 1.0; gstop.color = vector.stops.back().color; - gstop.opacity = vector.stops.back().opacity; vector.stops.push_back(gstop); } } @@ -1153,11 +1144,9 @@ sp_gradient_pattern_common_setup(cairo_pattern_t *cp, // add stops if (!is(gr)) { - for (auto & stop : gr->vector.stops) - { + for (auto & stop : gr->vector.stops) { // multiply stop opacity by paint opacity - cairo_pattern_add_color_stop_rgba(cp, stop.offset, - stop.color.v.c[0], stop.color.v.c[1], stop.color.v.c[2], stop.opacity * opacity); + ink_cairo_pattern_add_color_stop(cp, stop.offset, *stop.color, opacity); } } @@ -1180,10 +1169,8 @@ SPGradient::create_preview_pattern(double width) pat = cairo_pattern_create_linear(0, 0, width, 0); - for (auto & stop : vector.stops) - { - cairo_pattern_add_color_stop_rgba(pat, stop.offset, - stop.color.v.c[0], stop.color.v.c[1], stop.color.v.c[2], stop.opacity); + for (auto & stop : vector.stops) { + ink_cairo_pattern_add_color_stop(pat, stop.offset, *stop.color); } } else if (unsigned const num_columns = array.patch_columns()) { // For the moment, use the top row of nodes for preview. @@ -1193,8 +1180,7 @@ SPGradient::create_preview_pattern(double width) for (unsigned i = 0; i < num_columns + 1; ++i) { SPMeshNode* node = array.node( 0, i*3 ); - cairo_pattern_add_color_stop_rgba(pat, i*offset, - node->color.v.c[0], node->color.v.c[1], node->color.v.c[2], node->opacity); + ink_cairo_pattern_add_color_stop(pat, i * offset, *node->color); } } diff --git a/src/object/sp-grid.cpp b/src/object/sp-grid.cpp index 9069e6e858..f25db106d6 100644 --- a/src/object/sp-grid.cpp +++ b/src/object/sp-grid.cpp @@ -19,6 +19,7 @@ #include "sp-grid.h" #include "sp-namedview.h" +#include "colors/manager.h" #include "display/control/canvas-item-grid.h" #include "display/control/canvas-item-ptr.h" @@ -28,7 +29,6 @@ #include "grid-snapper.h" #include "page-manager.h" #include "snapper.h" -#include "svg/svg-color.h" #include "svg/svg-length.h" #include "util/units.h" @@ -38,12 +38,18 @@ using Inkscape::Util::UnitTable; +// default colors +static auto const GRID_DEFAULT_MAJOR_COLOR = Inkscape::Colors::Color{0x0099e54d}; +static auto const GRID_DEFAULT_MINOR_COLOR = Inkscape::Colors::Color{0x0099e526}; + SPGrid::SPGrid() : _visible(true) , _enabled(true) , _dotted(false) , _snap_to_visible_only(true) , _legacy(false) + , _major_color{GRID_DEFAULT_MAJOR_COLOR} + , _minor_color{GRID_DEFAULT_MINOR_COLOR} , _pixel(true) , _grid_type(GridType::RECTANGULAR) { } @@ -95,8 +101,6 @@ void SPGrid::build(SPDocument *doc, Inkscape::XML::Node *repr) readAttr(SPAttr::EMPCOLOR); readAttr(SPAttr::VISIBLE); readAttr(SPAttr::ENABLED); - readAttr(SPAttr::OPACITY); - readAttr(SPAttr::EMPOPACITY); readAttr(SPAttr::MAJOR_LINE_INTERVAL); readAttr(SPAttr::DOTTED); readAttr(SPAttr::SNAP_TO_VISIBLE_ONLY); @@ -199,14 +203,20 @@ void SPGrid::set(SPAttr key, const gchar* value) _margin_y.read(value); requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); break; - case SPAttr::COLOR: - _minor_color = (_minor_color & 0xff) | sp_svg_read_color(value, GRID_DEFAULT_MINOR_COLOR); + case SPAttr::COLOR: { + auto const old_opacity = _minor_color.getOpacity(); + _minor_color = Inkscape::Colors::Color::parse(value).value_or(GRID_DEFAULT_MINOR_COLOR); + _minor_color.setOpacity(old_opacity); requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); break; - case SPAttr::EMPCOLOR: - _major_color = (_major_color & 0xff) | sp_svg_read_color(value, GRID_DEFAULT_MAJOR_COLOR); + } + case SPAttr::EMPCOLOR: { + auto const old_opacity = _major_color.getOpacity(); + _major_color = Inkscape::Colors::Color::parse(value).value_or(GRID_DEFAULT_MAJOR_COLOR); + _major_color.setOpacity(old_opacity); requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); break; + } case SPAttr::VISIBLE: _visible.read(value); requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); @@ -217,11 +227,11 @@ void SPGrid::set(SPAttr key, const gchar* value) requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); break; case SPAttr::OPACITY: - sp_ink_read_opacity(value, &_minor_color, GRID_DEFAULT_MINOR_COLOR); + _minor_color.setOpacity(value ? g_ascii_strtod(value, nullptr) : 1.0); requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); break; case SPAttr::EMPOPACITY: - sp_ink_read_opacity(value, &_major_color, GRID_DEFAULT_MAJOR_COLOR); + _major_color.setOpacity(value ? g_ascii_strtod(value, nullptr) : 1.0); requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); break; case SPAttr::MAJOR_LINE_INTERVAL: @@ -409,8 +419,8 @@ void SPGrid::setPrefValues() Quantity::convert(prefs->getDouble(prefix + "/spacing_x", default_spacing), _display_unit, "px"), Quantity::convert(prefs->getDouble(prefix + "/spacing_y", default_spacing), _display_unit, "px")) * scale); - setMajorColor(prefs->getColor(prefix + "/empcolor", modular ? GRID_DEFAULT_BLOCK_COLOR : GRID_DEFAULT_MAJOR_COLOR)); - setMinorColor(prefs->getColor(prefix + "/color", modular ? GRID_DEFAULT_MINOR_BLOCK_COLOR : GRID_DEFAULT_MINOR_COLOR)); + setMajorColor(prefs->getColor(prefix + "/empcolor", modular ? "#0047cb4d" : "#0099e54d")); + setMinorColor(prefs->getColor(prefix + "/color", modular ? "#0047cb26" : "#0099e526")); setMajorLineInterval(prefs->getInt(prefix + "/empspacing")); // these prefs are bound specifically to one type of grid @@ -478,8 +488,8 @@ void SPGrid::update(SPCtx *ctx, unsigned int flags) if (_enabled) { view->set_origin(origin); view->set_spacing(spacing); - view->set_major_color(_major_color); - view->set_minor_color(_minor_color); + view->set_major_color(getMajorColor().toRGBA()); + view->set_minor_color(getMinorColor().toRGBA()); view->set_dotted(_dotted); view->set_major_line_interval(_major_line_interval); @@ -687,30 +697,17 @@ void SPGrid::setOrigin(Geom::Point const &new_origin) requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); } -void SPGrid::setMajorColor(const guint32 color) +void SPGrid::setMajorColor(Inkscape::Colors::Color const &color) { - char color_str[16]; - sp_svg_write_color(color_str, 16, color); - - getRepr()->setAttribute("empcolor", color_str); - - double opacity = (color & 0xff) / 255.0; // convert to value between [0.0, 1.0] - getRepr()->setAttributeSvgDouble("empopacity", opacity); - + getRepr()->setAttribute("empcolor", color.toString(false)); + getRepr()->setAttributeSvgDouble("empopacity", color.getOpacity()); requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); } -void SPGrid::setMinorColor(const guint32 color) +void SPGrid::setMinorColor(Inkscape::Colors::Color const &color) { - char color_str[16]; - sp_svg_write_color(color_str, 16, color); - - - getRepr()->setAttribute("color", color_str); - - double opacity = (color & 0xff) / 255.0; // convert to value between [0.0, 1.0] - getRepr()->setAttributeSvgDouble("opacity", opacity); - + getRepr()->setAttribute("color", color.toString(false)); + getRepr()->setAttributeSvgDouble("opacity", color.getOpacity()); requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); } diff --git a/src/object/sp-grid.h b/src/object/sp-grid.h index a254cd38ce..848af5e0aa 100644 --- a/src/object/sp-grid.h +++ b/src/object/sp-grid.h @@ -18,6 +18,7 @@ #ifndef SEEN_SP_GRID_H_ #define SEEN_SP_GRID_H_ +#include "colors/color.h" #include "display/control/canvas-item-ptr.h" #include "object/sp-object.h" #include "svg/svg-bool.h" @@ -69,11 +70,11 @@ public: bool getSnapToVisibleOnly() const { return _snap_to_visible_only; } void setSnapToVisibleOnly(bool v); - guint32 getMajorColor() const { return _major_color; } - void setMajorColor(const guint32 color); + Inkscape::Colors::Color const &getMajorColor() const { return _major_color; } + void setMajorColor(Inkscape::Colors::Color const &color); - guint32 getMinorColor() const { return _minor_color; } - void setMinorColor(const guint32 color); + Inkscape::Colors::Color const &getMinorColor() const { return _minor_color; } + void setMinorColor(Inkscape::Colors::Color const &color); Geom::Point getOrigin() const; void setOrigin(Geom::Point const &new_origin); @@ -144,8 +145,8 @@ private: guint32 _major_line_interval = 0; - guint32 _major_color = 0; - guint32 _minor_color = 0; + Inkscape::Colors::Color _major_color; + Inkscape::Colors::Color _minor_color; bool _pixel; // is in user units bool _legacy; // a grid from versions prior to inkscape 0.98 diff --git a/src/object/sp-guide.cpp b/src/object/sp-guide.cpp index 1ecbfc7151..3726f92b32 100644 --- a/src/object/sp-guide.cpp +++ b/src/object/sp-guide.cpp @@ -30,9 +30,10 @@ #include "sp-namedview.h" #include "sp-root.h" +#include "colors/color.h" +#include "colors/manager.h" #include "display/control/canvas-item-guideline.h" #include "object/sp-page.h" -#include "svg/svg-color.h" #include "svg/svg.h" #include "util/numeric/converters.h" #include "util/units.h" @@ -88,8 +89,10 @@ void SPGuide::release() void SPGuide::set(SPAttr key, const gchar *value) { switch (key) { case SPAttr::INKSCAPE_COLOR: - if (value) { - this->setColor(sp_svg_read_color(value, 0x0000ff00) | 0x7f); + if (auto c = Inkscape::Colors::Color::parse(value)) { + if (!c->hasOpacity()) + c->addOpacity(0.5); + color = c->toRGBA(); } break; case SPAttr::INKSCAPE_LABEL: diff --git a/src/object/sp-image.cpp b/src/object/sp-image.cpp index bc5c2afe37..a0647655e1 100644 --- a/src/object/sp-image.cpp +++ b/src/object/sp-image.cpp @@ -24,7 +24,6 @@ #include #include -#include #include <2geom/rect.h> #include <2geom/transforms.h> @@ -42,8 +41,9 @@ #include "xml/quote.h" #include "xml/href-attribute-helper.h" -#include "color/cms-system.h" -#include "color-profile.h" +#include "colors/cms/system.h" +#include "colors/manager.h" +#include "colors/document-cms.h" //#define DEBUG_LCMS #ifdef DEBUG_LCMS @@ -238,74 +238,6 @@ void SPImage::set(SPAttr key, const gchar* value) { } // BLIP -void SPImage::apply_profile(Inkscape::Pixbuf *pixbuf) { - - // TODO: this will prevent using MIME data when exporting. - // Integrate color correction into loading. - pixbuf->ensurePixelFormat(Inkscape::Pixbuf::PF_GDK); - int imagewidth = pixbuf->width(); - int imageheight = pixbuf->height(); - int rowstride = pixbuf->rowstride(); - guchar* px = pixbuf->pixels(); - - if ( px ) { - DEBUG_MESSAGE( lcmsFive, "in 's sp_image_update. About to call get_document_profile()"); - - guint profIntent = Inkscape::RENDERING_INTENT_UNKNOWN; - cmsHPROFILE prof = Inkscape::CMSSystem::get_document_profile(document, - &profIntent, - color_profile ); - if ( prof ) { - cmsProfileClassSignature profileClass = cmsGetDeviceClass( prof ); - if ( profileClass != cmsSigNamedColorClass ) { - int intent = INTENT_PERCEPTUAL; - - switch ( profIntent ) { - case Inkscape::RENDERING_INTENT_RELATIVE_COLORIMETRIC: - intent = INTENT_RELATIVE_COLORIMETRIC; - break; - case Inkscape::RENDERING_INTENT_SATURATION: - intent = INTENT_SATURATION; - break; - case Inkscape::RENDERING_INTENT_ABSOLUTE_COLORIMETRIC: - intent = INTENT_ABSOLUTE_COLORIMETRIC; - break; - case Inkscape::RENDERING_INTENT_PERCEPTUAL: - case Inkscape::RENDERING_INTENT_UNKNOWN: - case Inkscape::RENDERING_INTENT_AUTO: - default: - intent = INTENT_PERCEPTUAL; - } - - cmsHPROFILE destProf = cmsCreate_sRGBProfile(); - cmsHTRANSFORM transf = cmsCreateTransform( prof, - TYPE_RGBA_8, - destProf, - TYPE_RGBA_8, - intent, 0 ); - if ( transf ) { - guchar* currLine = px; - for ( int y = 0; y < imageheight; y++ ) { - // Since the types are the same size, we can do the transformation in-place - cmsDoTransform( transf, currLine, currLine, imagewidth ); - currLine += rowstride; - } - - cmsDeleteTransform( transf ); - } else { - DEBUG_MESSAGE( lcmsSix, "in 's sp_image_update. Unable to create LCMS transform." ); - } - - cmsCloseProfile( destProf ); - } else { - DEBUG_MESSAGE( lcmsSeven, "in 's sp_image_update. Profile type is named color. Can't transform." ); - } - } else { - DEBUG_MESSAGE( lcmsEight, "in 's sp_image_update. No profile found." ); - } - } -} - void SPImage::update(SPCtx *ctx, unsigned int flags) { SPItem::update(ctx, flags); @@ -333,7 +265,12 @@ void SPImage::update(SPCtx *ctx, unsigned int flags) { } if (pb) { - if (color_profile) apply_profile(pb); + if (color_profile) { + if (auto cp = document->getDocumentCMS().getSpace(color_profile)) { + pb->ensurePixelFormat(Inkscape::Pixbuf::PF_GDK); + // XXX TODO cp->transformToRGB(pb); + } + } pb->ensurePixelFormat(Inkscape::Pixbuf::PF_CAIRO); // Expected by rendering code, so convert now before making immutable. pixbuf = std::shared_ptr(pb); } diff --git a/src/object/sp-item-group.cpp b/src/object/sp-item-group.cpp index f2fa557a9f..390488d825 100644 --- a/src/object/sp-item-group.cpp +++ b/src/object/sp-item-group.cpp @@ -1095,13 +1095,13 @@ std::vector default_highlights; /** * Generate a highlight colour if one isn't set and return it. */ -guint32 SPGroup::highlight_color() const { +Inkscape::Colors::Color SPGroup::highlight_color() const { // Parent must not be a layer (root, or similar) and this group must also be a layer if (!_highlightColor && !SP_IS_LAYER(parent) && this->_layer_mode == SPGroup::LAYER && !default_highlights.empty()) { char const * oid = defaultLabel(); if (oid && *oid) { // Color based on the last few bits of the label or object id. - return default_highlights[oid[(strlen(oid) - 1)] % default_highlights.size()]; + return Inkscape::Colors::Color(default_highlights[oid[(strlen(oid) - 1)] % default_highlights.size()]); } } return SPItem::highlight_color(); diff --git a/src/object/sp-item-group.h b/src/object/sp-item-group.h index 252985cc0b..9de52f7342 100644 --- a/src/object/sp-item-group.h +++ b/src/object/sp-item-group.h @@ -91,7 +91,7 @@ public: void update_patheffect(bool write) override; - guint32 highlight_color() const override; + Inkscape::Colors::Color highlight_color() const override; void removeTransformsRecursively(SPObject const *root) override; diff --git a/src/object/sp-item.cpp b/src/object/sp-item.cpp index d0c44e9268..07c2415a1f 100644 --- a/src/object/sp-item.cpp +++ b/src/object/sp-item.cpp @@ -18,10 +18,10 @@ #include #include +#include "colors/manager.h" #include "helper/geom.h" #include "path/path-util.h" #include "svg/svg.h" -#include "svg/svg-color.h" #include "print.h" #include "display/drawing-item.h" #include "attributes.h" @@ -59,6 +59,7 @@ #include "live_effects/effect.h" #include "live_effects/lpeobject-reference.h" +#include "ui/util.h" #include "util/units.h" #define noSP_ITEM_DEBUG_IDLE @@ -75,7 +76,6 @@ SPItem::SPItem() sensitive = TRUE; bbox_valid = FALSE; - _highlightColor = 0; transform_center_x = 0; transform_center_y = 0; @@ -251,27 +251,26 @@ bool SPItem::isHidden(unsigned display_key) const return true; } -void SPItem::setHighlight(guint32 color) { - _highlightColor = color; +void SPItem::setHighlight(Inkscape::Colors::Color color) { + _highlightColor = std::move(color); updateRepr(); } bool SPItem::isHighlightSet() const { - return _highlightColor != 0; + return _highlightColor.has_value(); } -guint32 SPItem::highlight_color() const { +Inkscape::Colors::Color SPItem::highlight_color() const { if (isHighlightSet()) { - return _highlightColor; + return *_highlightColor; } SPItem const *item = cast(parent); if (parent && (parent != this) && item) { return item->highlight_color(); - } else { - static Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - return prefs->getInt("/tools/nodes/highlight_color", 0xaaaaaaff); } + auto prefs = Inkscape::Preferences::get(); + return prefs->getColor("/tools/nodes/highlight_color", "#aaaaaaff"); } void SPItem::setEvaluated(bool evaluated) { @@ -573,10 +572,7 @@ void SPItem::set(SPAttr key, gchar const* value) { } case SPAttr::INKSCAPE_HIGHLIGHT_COLOR: { - item->_highlightColor = 0; - if (value) { - item->_highlightColor = sp_svg_read_color(value, 0x0) | 0xff; - } + item->_highlightColor = Inkscape::Colors::Color::parse(value); break; } case SPAttr::CONNECTOR_AVOID: @@ -878,7 +874,7 @@ Inkscape::XML::Node* SPItem::write(Inkscape::XML::Document *xml_doc, Inkscape::X repr->setAttributeOrRemoveIfEmpty("mask", value); } if (item->isHighlightSet()) { - repr->setAttribute("inkscape:highlight-color", SPColor(item->_highlightColor).toString()); + repr->setAttribute("inkscape:highlight-color", item->_highlightColor->toString()); } else { repr->removeAttribute("inkscape:highlight-color"); } diff --git a/src/object/sp-item.h b/src/object/sp-item.h index 1bbcb96d0c..2ffbcc23a8 100644 --- a/src/object/sp-item.h +++ b/src/object/sp-item.h @@ -27,6 +27,8 @@ #include <2geom/affine.h> #include <2geom/rect.h> +#include "colors/color.h" + #include "sp-object.h" #include "sp-marker-loc.h" #include "display/drawing-item-ptr.h" @@ -172,9 +174,9 @@ public: return sensitive; }; - void setHighlight(uint32_t color); + void setHighlight(Inkscape::Colors::Color color); bool isHighlightSet() const; - virtual uint32_t highlight_color() const; + virtual Inkscape::Colors::Color highlight_color() const; //==================== @@ -402,7 +404,7 @@ public: */ Geom::Affine dt2i_affine() const; - uint32_t _highlightColor; + std::optional _highlightColor; bool isExpanded() const { return _is_expanded; } void setExpanded(bool expand) { _is_expanded = expand; } diff --git a/src/object/sp-mesh-array.cpp b/src/object/sp-mesh-array.cpp index e040e98382..062b4d8441 100644 --- a/src/object/sp-mesh-array.cpp +++ b/src/object/sp-mesh-array.cpp @@ -49,11 +49,12 @@ #include <2geom/line.h> // For color picking +#include "colors/color.h" +#include "colors/utils.h" #include "display/drawing.h" #include "display/drawing-context.h" #include "display/cairo-utils.h" #include "document.h" -#include "gradient-chemistry.h" // average_color() #include "sp-root.h" #include "sp-mesh-gradient.h" #include "sp-mesh-row.h" @@ -66,11 +67,9 @@ #include "sp-ellipse.h" #include "sp-star.h" -// For writing color/opacity to style -#include "svg/css-ostringstream.h" - // For default color #include "style.h" +#include "svg/css-ostringstream.h" #include "xml/document.h" // for Document #include "xml/node.h" // for Node @@ -470,34 +469,27 @@ void SPMeshPatchI::updateNodes() { /** Return color for corner of patch. */ -SPColor SPMeshPatchI::getColor(unsigned const i) +std::optional SPMeshPatchI::getColor(unsigned const i) { assert( i < 4 ); - SPColor color; switch ( i ) { case 0: - color = (*nodes)[ row ][ col ]->color; - break; + return (*nodes)[ row ][ col ]->color; case 1: - color = (*nodes)[ row ][ col+3 ]->color; - break; + return (*nodes)[ row ][ col+3 ]->color; case 2: - color = (*nodes)[ row+3 ][ col+3 ]->color; - break; + return (*nodes)[ row+3 ][ col+3 ]->color; case 3: - color = (*nodes)[ row+3 ][ col ]->color; - break; - + return (*nodes)[ row+3 ][ col ]->color; } - - return color; + return {}; } /** Set color for corner of patch. */ -void SPMeshPatchI::setColor(unsigned const i, SPColor const color) +void SPMeshPatchI::setColor(unsigned const i, Inkscape::Colors::Color const &color) { assert( i < 4 ); @@ -517,57 +509,6 @@ void SPMeshPatchI::setColor(unsigned const i, SPColor const color) } } -/** - Return opacity for corner of patch. -*/ -double SPMeshPatchI::getOpacity(unsigned const i) -{ - assert( i < 4 ); - - double opacity = 0.0; - switch ( i ) { - case 0: - opacity = (*nodes)[ row ][ col ]->opacity; - break; - case 1: - opacity = (*nodes)[ row ][ col+3 ]->opacity; - break; - case 2: - opacity = (*nodes)[ row+3 ][ col+3 ]->opacity; - break; - case 3: - opacity = (*nodes)[ row+3 ][ col ]->opacity; - break; - } - - return opacity; -} - -/** - Set opacity for corner of patch. -*/ -void SPMeshPatchI::setOpacity(unsigned const i, double const opacity) -{ - - assert( i < 4 ); - - switch ( i ) { - case 0: - (*nodes)[ row ][ col ]->opacity = opacity; - break; - case 1: - (*nodes)[ row ][ col+3 ]->opacity = opacity; - break; - case 2: - (*nodes)[ row+3 ][ col+3 ]->opacity = opacity; - break; - case 3: - (*nodes)[ row+3 ][ col ]->opacity = opacity; - break; - - } -} - /** Return stop pointer for corner of patch. */ @@ -836,10 +777,8 @@ bool SPMeshNodeArray::read(SPMeshGradient *mg_in) if( (istop == 0 && irow == 0 && icolumn > 0) || (istop == 1 && irow > 0 ) ) { // skip } else { - SPColor color = stop->getColor(); - double opacity = stop->getOpacity(); + auto color = stop->getColor(); new_patch.setColor( istop, color ); - new_patch.setOpacity( istop, opacity ); new_patch.setStopPtr( istop, stop ); } ++istop; @@ -1038,13 +977,12 @@ void SPMeshNodeArray::write( SPMeshGradient *mg ) ( k == 2 ) || ( k == 3 && j == 0 ) ) { - // Why are we setting attribute and not style? - //stop->setAttribute("stop-color", patchi.getColor(k).toString() ); - //stop->setAttribute("stop-opacity", patchi.getOpacity(k) ); - - Inkscape::CSSOStringStream os; - os << "stop-color:" << patchi.getColor(k).toString() << ";stop-opacity:" << patchi.getOpacity(k); - stop->setAttribute("style", os.str()); + auto color = patchi.getColor(k); + SPCSSAttr *color_css = sp_repr_css_attr_new(); + sp_repr_css_set_property_string(color_css, "stop-color", color->toString(false)); + sp_repr_css_set_property_double(color_css, "stop-opacity", color->getOpacity()); + sp_repr_css_set(stop, color_css, "style"); + sp_repr_css_attr_unref(color_css); } patch->appendChild( stop ); } @@ -1055,38 +993,33 @@ void SPMeshNodeArray::write( SPMeshGradient *mg ) /** * Find default color based on colors in existing fill. */ -static SPColor default_color( SPItem *item ) { - - SPColor color( 0.5, 0.0, 0.5 ); - +static Inkscape::Colors::Color default_color(SPItem *item) +{ if ( item->style ) { SPIPaint const &paint = ( item->style->fill ); // Could pick between style.fill/style.stroke - if ( paint.isColor() ) { - color = paint.value.color; + if (paint.isColor()) { + return paint.getColor(); } else if ( paint.isPaintserver() ) { auto *server = item->style->getFillPaintServer(); auto gradient = cast(server); if (gradient && gradient->getVector()) { SPStop *firstStop = gradient->getVector()->getFirstStop(); if ( firstStop ) { - color = firstStop->getColor(); + return firstStop->getColor(); } } } } else { std::cerr << " SPMeshNodeArray: default_color(): No style" << std::endl; } - - return color; + return Inkscape::Colors::Color(0x800080ff); } /** Create a default mesh. */ -void SPMeshNodeArray::create( SPMeshGradient *mg, SPItem *item, Geom::OptRect bbox ) { - - // std::cout << "SPMeshNodeArray::create: Entrance" << std::endl; - +void SPMeshNodeArray::create( SPMeshGradient *mg, SPItem *item, Geom::OptRect bbox ) +{ if( !bbox ) { // Set default size to bounding box if size not given. std::cerr << "SPMeshNodeArray::create(): bbox empty" << std::endl; @@ -1119,13 +1052,13 @@ void SPMeshNodeArray::create( SPMeshGradient *mg, SPItem *item, Geom::OptRect bb repr->setAttribute("gradientUnits", "userSpaceOnUse"); // Get default color - SPColor color = default_color( item ); + Inkscape::Colors::Color color = default_color( item ); // Set some corners to white so we can see the mesh. - SPColor white( 1.0, 1.0, 1.0 ); + auto white = Inkscape::Colors::Color(0xffffffff); if (color == white) { // If default color is white, set other color to black. - white = SPColor( 0.0, 0.0, 0.0 ); + white.set("black"); } // Get preferences @@ -1214,7 +1147,6 @@ void SPMeshNodeArray::create( SPMeshGradient *mg, SPItem *item, Geom::OptRect bb for( unsigned k = 0; k < 4; ++k ) { patch.setPathType( k, 'l' ); patch.setColor( k, (i+k)%2 ? color : white ); - patch.setOpacity( k, 1.0 ); } patch.setPathType( 0, 'c' ); @@ -1270,7 +1202,6 @@ void SPMeshNodeArray::create( SPMeshGradient *mg, SPItem *item, Geom::OptRect bb patch.setPathType( i, 'c' ); patch.setColor( i, i%2 ? color : white ); - patch.setOpacity( i, 1.0 ); } // Fill out tensor points @@ -1311,7 +1242,6 @@ void SPMeshNodeArray::create( SPMeshGradient *mg, SPItem *item, Geom::OptRect bb for( unsigned s = 0; s < 4; ++s ) { patch.setPathType( s, 'l' ); patch.setColor( s, (i+s)%2 ? color : white ); - patch.setOpacity( s, 1.0 ); } // Set handle and tensor nodes. @@ -1339,10 +1269,8 @@ void SPMeshNodeArray::create( SPMeshGradient *mg, SPItem *item, Geom::OptRect bb for( unsigned s = 0; s < 4; ++s ) { patch0.setPathType( s, 'l' ); patch0.setColor( s, s%2 ? color : white ); - patch0.setOpacity( s, 1.0 ); patch1.setPathType( s, 'l' ); patch1.setColor( s, s%2 ? white : color ); - patch1.setOpacity( s, 1.0 ); } // Set handle and tensor nodes. @@ -1392,7 +1320,6 @@ void SPMeshNodeArray::create( SPMeshGradient *mg, SPItem *item, Geom::OptRect bb node->node_type = MG_NODE_TYPE_CORNER; node->set = true; node->color = (i+j)%2 ? color : white; - node->opacity = 1.0; } else { // Side @@ -1633,11 +1560,11 @@ void SPMeshNodeArray::bicubic(SPMeshNodeArray * const smooth, SPMeshType const t for( unsigned i = 0; i < d.size(); ++i ) { d[i].resize( smooth->patch_columns() + 1 ); for( unsigned j = 0; j < d[i].size(); ++j ) { - float rgb_color[3]; - this->nodes[ i*3 ][ j*3 ]->color.get_rgb_floatv(rgb_color); - d[i][j].g[0][0] = rgb_color[ 0 ]; - d[i][j].g[1][0] = rgb_color[ 1 ]; - d[i][j].g[2][0] = rgb_color[ 2 ]; + // Note: Conversion to RGB happens here + auto rgb_color = this->nodes[i * 3][j * 3]->color->converted(Colors::Space::Type::RGB); + d[i][j].g[0][0] = rgb_color->get(0); + d[i][j].g[1][0] = rgb_color->get(1); + d[i][j].g[2][0] = rgb_color->get(2); d[i][j].p = this->nodes[ i*3 ][ j*3 ]->p; } } @@ -1784,8 +1711,12 @@ void SPMeshNodeArray::bicubic(SPMeshNodeArray * const smooth, SPMeshType const t for( unsigned k = 0; k < 9; ++k ) { for( unsigned l = 0; l < 9; ++l ) { + // We're not sure why opacity isn't smoothed, it's just sort of, retained without explaination + auto op = smooth->nodes[(i*8+k)*3 ][(j*8+l)*3]->color->getOpacity(); // Every third node is a corner node - smooth->nodes[ (i*8+k)*3 ][(j*8+l)*3 ]->color.set( r[0][k][l], r[1][k][l], r[2][k][l] ); + smooth->nodes[(i*8+k)*3 ][(j*8+l)*3]->color->set( + Colors::Color(SP_RGBA32_F_COMPOSE(r[0][k][l], r[1][k][l], r[2][k][l], op)) + ); } } } @@ -2158,9 +2089,9 @@ unsigned SPMeshNodeArray::color_smooth(std::vector const &corners) double slope_diff[3]; // Color of corners - SPColor color0 = n[0]->color; - SPColor color3 = n[3]->color; - SPColor color6 = n[6]->color; + auto &color0 = n[0]->color; + auto &color3 = n[3]->color; + auto &color6 = n[6]->color; // Distance nodes from selected corner Geom::Point d[7]; @@ -2173,17 +2104,17 @@ unsigned SPMeshNodeArray::color_smooth(std::vector const &corners) unsigned cdm = 0; // Color Diff Max (Which color has the maximum difference in slopes) for( unsigned c = 0; c < 3; ++c ) { if( d[2].length() != 0.0 ) { - slope[0][c] = (color3.v.c[c] - color0.v.c[c]) / d[2].length(); + slope[0][c] = (color3->get(c) - color0->get(c)) / d[2].length(); } if( d[4].length() != 0.0 ) { - slope[1][c] = (color6.v.c[c] - color3.v.c[c]) / d[4].length(); + slope[1][c] = (color6->get(c) - color3->get(c)) / d[4].length(); } slope_ave[c] = (slope[0][c]+slope[1][c]) / 2.0; slope_diff[c] = (slope[0][c]-slope[1][c]); // std::cout << " color: " << c << " :" - // << color0.v.c[c] << " " - // << color3.v.c[c] << " " - // << color6.v.c[c] + // << color0[c] << " " + // << color3[c] << " " + // << color6[c] // << " slope: " // << slope[0][c] << " " // << slope[1][c] @@ -2203,8 +2134,8 @@ unsigned SPMeshNodeArray::color_smooth(std::vector const &corners) double length_left = d[0].length(); double length_right = d[6].length(); if( slope_ave[ cdm ] != 0.0 ) { - length_left = std::abs( (color3.v.c[cdm] - color0.v.c[cdm]) / slope_ave[ cdm ] ); - length_right = std::abs( (color6.v.c[cdm] - color3.v.c[cdm]) / slope_ave[ cdm ] ); + length_left = std::abs( (color3->get(cdm) - color0->get(cdm)) / slope_ave[ cdm ] ); + length_right = std::abs( (color6->get(cdm) - color3->get(cdm)) / slope_ave[ cdm ] ); } // Move closest handle a maximum of mid point... but don't shorten @@ -2342,17 +2273,8 @@ unsigned SPMeshNodeArray::color_pick(std::vector const &icorners, SPIt /* Render copy and pick color */ pick_drawing->render(dc, ibox); - double R = 0, G = 0, B = 0, A = 0; - ink_cairo_surface_average_color(s, R, G, B, A); + n->color = ink_cairo_surface_average_color(s); cairo_surface_destroy(s); - - // std::cout << " p: " << p - // << " box: " << ibox - // << " R: " << R - // << " G: " << G - // << " B: " << B - // << std::endl; - n->color.set( R, G, B ); } pick_doc->getRoot()->invoke_hide(pick_visionkey); @@ -2878,14 +2800,7 @@ void SPMeshNodeArray::split_row( unsigned int row, double coord ) { nodes[i+5][j]->node_type = MG_NODE_TYPE_HANDLE; // Color stored in corners - unsigned c0 = nodes[i ][j]->color.toRGBA32( 1.0 ); - unsigned c1 = nodes[i+6][j]->color.toRGBA32( 1.0 ); - double o0 = nodes[i ][j]->opacity; - double o1 = nodes[i+6][j]->opacity; - unsigned cnew = average_color( c0, c1, coord ); - double onew = o0 * (1.0 - coord) + o1 * coord; - nodes[i+3][j]->color.set( cnew ); - nodes[i+3][j]->opacity = onew; + nodes[i+3][j]->color = nodes[i][j]->color->averaged(*nodes[i+6][j]->color, coord); nodes[i+3][j]->node_type = MG_NODE_TYPE_CORNER; nodes[i+3][j]->set = true; @@ -2991,14 +2906,7 @@ void SPMeshNodeArray::split_column( unsigned int col, double coord ) { nodes[i][j+5]->node_type = MG_NODE_TYPE_HANDLE; // Color stored in corners - unsigned c0 = nodes[i][j ]->color.toRGBA32( 1.0 ); - unsigned c1 = nodes[i][j+6]->color.toRGBA32( 1.0 ); - double o0 = nodes[i][j ]->opacity; - double o1 = nodes[i][j+6]->opacity; - unsigned cnew = average_color( c0, c1, coord ); - double onew = o0 * (1.0 - coord) + o1 * coord; - nodes[i][j+3]->color.set( cnew ); - nodes[i][j+3]->opacity = onew; + nodes[i][j+3]->color = nodes[i][j]->color->averaged(*nodes[i][j+6]->color, coord); nodes[i][j+3]->node_type = MG_NODE_TYPE_CORNER; nodes[i][j+3]->set = true; diff --git a/src/object/sp-mesh-array.h b/src/object/sp-mesh-array.h index 66ccf7e6f4..02a5a82186 100644 --- a/src/object/sp-mesh-array.h +++ b/src/object/sp-mesh-array.h @@ -45,7 +45,7 @@ #include #include <2geom/point.h> -#include "color.h" +#include "colors/color.h" // For color picking #include "sp-item.h" @@ -94,7 +94,7 @@ class SPStop; class SPMeshNode { public: - SPMeshNode() {} + SPMeshNode() = default; NodeType node_type = MG_NODE_TYPE_UNKNOWN; unsigned node_edge = MG_NODE_EDGE_NONE; @@ -102,8 +102,7 @@ public: Geom::Point p; unsigned draggable = -1; // index of on-screen node char path_type = 'u'; - SPColor color{0, 0, 0}; // Default black - double opacity = 0.0; + std::optional color; SPStop *stop = nullptr; // Stop corresponding to node. }; @@ -128,10 +127,8 @@ public: bool tensorIsSet( unsigned i ); Geom::Point coonsTensorPoint( unsigned i ); void updateNodes(); - SPColor getColor( unsigned i ); - void setColor( unsigned i, SPColor c ); - double getOpacity( unsigned i ); - void setOpacity( unsigned i, double o ); + std::optional getColor(unsigned i); + void setColor(unsigned i, Inkscape::Colors::Color const &c); SPStop* getStopPtr( unsigned i ); void setStopPtr( unsigned i, SPStop* ); }; diff --git a/src/object/sp-mesh-gradient.cpp b/src/object/sp-mesh-gradient.cpp index 4bbc455534..394360497e 100644 --- a/src/object/sp-mesh-gradient.cpp +++ b/src/object/sp-mesh-gradient.cpp @@ -216,12 +216,12 @@ std::unique_ptr SPMeshGradient::create_drawing_pai // << " calculated as " << t << "." << std::endl; } - auto color = patch.getColor(k); + // Does this data really need RGB at this stage? + auto color = *patch.getColor(k)->converted(Colors::Space::Type::RGB); for (int r = 0; r < 3; r++) { - data.color[k][r] = color.v.c[r]; + data.color[k][r] = color[r]; } - - data.opacity[k] = patch.getOpacity(k); + data.opacity[k] = color.getOpacity(); } } } diff --git a/src/object/sp-namedview.cpp b/src/object/sp-namedview.cpp index 680c48a25d..a0bac8e772 100644 --- a/src/object/sp-namedview.cpp +++ b/src/object/sp-namedview.cpp @@ -25,6 +25,7 @@ #include #include "attributes.h" +#include "colors/manager.h" #include "conn-avoid-ref.h" // for defaultConnSpacing. #include "desktop.h" #include "document-undo.h" @@ -42,7 +43,6 @@ #include "actions/actions-canvas-snapping.h" #include "display/control/canvas-page.h" -#include "svg/svg-color.h" #include "ui/monitor.h" #include "ui/widget/canvas.h" #include "ui/widget/canvas-grid.h" @@ -52,10 +52,6 @@ using Inkscape::DocumentUndo; using Inkscape::Util::UnitTable; -#define DEFAULTGUIDECOLOR 0x0086e599 -#define DEFAULTGUIDEHICOLOR 0xff00007f -#define DEFAULTDESKCOLOR 0xd1d1d1ff - SPNamedView::SPNamedView() : SPObjectGroup() , snap_manager(this, get_snapping_preferences()) @@ -66,8 +62,6 @@ SPNamedView::SPNamedView() , desk_checkerboard(false) { this->zoom = 0; - this->guidecolor = 0; - this->guidehicolor = 0; this->views.clear(); // this->page_size_units = nullptr; this->window_x = 0; @@ -80,7 +74,6 @@ SPNamedView::SPNamedView() this->window_width = 0; this->window_height = 0; this->window_maximized = 0; - this->desk_color = DEFAULTDESKCOLOR; this->editable = TRUE; this->viewcount = 0; @@ -144,9 +137,8 @@ void SPNamedView::build(SPDocument *document, Inkscape::XML::Node *repr) { for (auto &child : children) { if (auto guide = cast(&child)) { this->guides.push_back(guide); - //g_object_set(G_OBJECT(g), "color", nv->guidecolor, "hicolor", nv->guidehicolor, NULL); - guide->setColor(this->guidecolor); - guide->setHiColor(this->guidehicolor); + guide->setColor(getGuideColor().toRGBA()); + guide->setHiColor(getGuideHiColor().toRGBA()); guide->readAttr(SPAttr::INKSCAPE_COLOR); } if (auto page = cast(&child)) { @@ -171,14 +163,35 @@ void SPNamedView::set_clip_to_page(SPDesktop* desktop, bool enable) { } } +static auto const default_desk_color = Inkscape::Colors::Color{0xd1d1d1, false}; +static auto const default_guide_color = Inkscape::Colors::Color{0x0086e5, false}; +static auto const default_guide_hi_color = Inkscape::Colors::Color{0xff0000, false}; + +Inkscape::Colors::Color SPNamedView::getDeskColor() const +{ + return _desk_color.value_or(default_desk_color); +} + +Inkscape::Colors::Color SPNamedView::getGuideColor() const +{ + auto copy = _guide_color.value_or(default_guide_color); + copy.addOpacity(_guide_opacity); + return copy; +} + +Inkscape::Colors::Color SPNamedView::getGuideHiColor() const +{ + auto copy = _guide_hi_color.value_or(default_guide_hi_color); + copy.addOpacity(_guide_hi_opacity); + return copy; +} + void SPNamedView::set_desk_color(SPDesktop* desktop) { if (desktop) { - if (desk_checkerboard) { - desktop->getCanvas()->set_desk(desk_color); - } else { - desktop->getCanvas()->set_desk(desk_color | 0xff); - } - // Update pages, who's colours sometimes change whe the desk color changes. + auto dkcolor = getDeskColor(); + dkcolor.setOpacity(desk_checkerboard ? 0.0 : 1.0); + desktop->getCanvas()->set_desk(dkcolor.toRGBA()); + // Update pages, whose colours sometimes change whe the desk color changes. document->getPageManager().setDefaultAttributes(_viewport); } } @@ -275,6 +288,14 @@ void SPNamedView::set(SPAttr key, const gchar* value) { return; } + auto update_guides = [this]() { + for(auto guide : guides) { + guide->setColor(getGuideColor().toRGBA()); + guide->setHiColor(getGuideHiColor().toRGBA()); + guide->readAttr(SPAttr::INKSCAPE_COLOR); + } + }; + switch (key) { case SPAttr::VIEWONLY: this->editable = (!value); @@ -305,41 +326,23 @@ void SPNamedView::set(SPAttr key, const gchar* value) { this->snap_manager.snapprefs.setDistributionTolerance(value ? g_ascii_strtod(value, nullptr) : 5); break; case SPAttr::GUIDECOLOR: - this->guidecolor = (this->guidecolor & 0xff) | (DEFAULTGUIDECOLOR & 0xffffff00); - if (value) { - this->guidecolor = (this->guidecolor & 0xff) | sp_svg_read_color(value, this->guidecolor); - } - for(auto guide : this->guides) { - guide->setColor(this->guidecolor); - guide->readAttr(SPAttr::INKSCAPE_COLOR); - } + _guide_color = Inkscape::Colors::Color::parse(value); + update_guides(); break; case SPAttr::GUIDEOPACITY: - sp_ink_read_opacity(value, &this->guidecolor, DEFAULTGUIDECOLOR); - for (auto guide : this->guides) { - guide->setColor(this->guidecolor); - guide->readAttr(SPAttr::INKSCAPE_COLOR); - } + _guide_opacity = value ? g_ascii_strtod(value, nullptr) : 0.6; + update_guides(); break; case SPAttr::GUIDEHICOLOR: - this->guidehicolor = (this->guidehicolor & 0xff) | (DEFAULTGUIDEHICOLOR & 0xffffff00); - if (value) { - this->guidehicolor = (this->guidehicolor & 0xff) | sp_svg_read_color(value, this->guidehicolor); - } - for(auto guide : this->guides) { - guide->setHiColor(this->guidehicolor); - } + _guide_hi_color = Inkscape::Colors::Color::parse(value); + update_guides(); break; case SPAttr::GUIDEHIOPACITY: - sp_ink_read_opacity(value, &this->guidehicolor, DEFAULTGUIDEHICOLOR); - for (auto guide : this->guides) { - guide->setHiColor(this->guidehicolor); - } + _guide_hi_opacity = value ? g_ascii_strtod(value, nullptr) : 0.5; + update_guides(); break; case SPAttr::INKSCAPE_DESK_COLOR: - if (value) { - desk_color = sp_svg_read_color(value, desk_color); - } + _desk_color = Inkscape::Colors::Color::parse(value); break; case SPAttr::INKSCAPE_DESK_CHECKERBOARD: this->desk_checkerboard.readOrUnset(value); @@ -460,8 +463,8 @@ void SPNamedView::child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *r this->guides.push_back(g); //g_object_set(G_OBJECT(g), "color", this->guidecolor, "hicolor", this->guidehicolor, NULL); - g->setColor(this->guidecolor); - g->setHiColor(this->guidehicolor); + g->setColor(getGuideColor().toRGBA()); + g->setHiColor(getGuideHiColor().toRGBA()); g->readAttr(SPAttr::INKSCAPE_COLOR); if (this->editable) { @@ -978,14 +981,10 @@ void SPNamedView::scrollAllDesktops(double dx, double dy) { } } -void SPNamedView::change_color(unsigned int rgba, SPAttr color_key, SPAttr opacity_key /*= SPAttr::INVALID*/) { - gchar buf[32]; - sp_svg_write_color(buf, sizeof(buf), rgba); - getRepr()->setAttribute(sp_attribute_name(color_key), buf); - - if (opacity_key != SPAttr::INVALID) { - getRepr()->setAttributeCssDouble(sp_attribute_name(opacity_key), (rgba & 0xff) / 255.0); - } +void SPNamedView::change_color(SPAttr color_key, SPAttr opacity_key, Inkscape::Colors::Color const &color) { + if (color.hasOpacity()) + getRepr()->setAttributeCssDouble(sp_attribute_name(opacity_key), color.getOpacity()); + getRepr()->setAttribute(sp_attribute_name(color_key), color.toString(false)); } void SPNamedView::change_bool_setting(SPAttr key, bool value) { diff --git a/src/object/sp-namedview.h b/src/object/sp-namedview.h index 98ff9d5b18..32533975b3 100644 --- a/src/object/sp-namedview.h +++ b/src/object/sp-namedview.h @@ -21,6 +21,7 @@ #include "snap.h" #include "sp-object-group.h" +#include "colors/color.h" #include "svg/svg-bool.h" namespace Inkscape { @@ -28,8 +29,13 @@ namespace Inkscape { namespace Util { class Unit; } + namespace Colors { + class Color; + } } +using namespace Inkscape; + class SPGrid; typedef unsigned int guint32; @@ -53,7 +59,6 @@ public: SVGBool grids_visible; SVGBool clip_to_page; // if true, clip rendered content to pages' boundaries SVGBool antialias_rendering = true; - guint32 desk_color; SVGBool desk_checkerboard; double zoom; @@ -75,9 +80,6 @@ public: double connector_spacing; - guint32 guidecolor; - guint32 guidehicolor; - std::vector guides; std::vector grids; std::vector views; @@ -115,7 +117,7 @@ public: void newGridCreated(); // page background, border, desk colors - void change_color(unsigned int rgba, SPAttr color_key, SPAttr opacity_key = SPAttr::INVALID); + void change_color(SPAttr color_key, SPAttr opacity_key,Colors::Color const &color); // show border, border on top, anti-aliasing, ... void change_bool_setting(SPAttr key, bool value); // sync desk colors @@ -127,6 +129,9 @@ public: SPGrid *getFirstEnabledGrid(); + Colors::Color getDeskColor() const; + Colors::Color getGuideColor() const; + Colors::Color getGuideHiColor() const; private: void updateGuides(); void updateGrids(); @@ -138,6 +143,12 @@ private: Inkscape::CanvasPage *_viewport = nullptr; bool _sync_grids = true; + std::optional _desk_color; + std::optional _guide_color; + std::optional _guide_hi_color; + double _guide_opacity = 0.6; + double _guide_hi_opacity = 0.5; + protected: void build(SPDocument *document, Inkscape::XML::Node *repr) override; void release() override; diff --git a/src/object/sp-object.cpp b/src/object/sp-object.cpp index 14bb97e7bf..abdde8080a 100644 --- a/src/object/sp-object.cpp +++ b/src/object/sp-object.cpp @@ -734,8 +734,8 @@ void SPObject::release() { debug("id=%p, typename=%s", object, g_type_name_from_instance((GTypeInstance*)object)); style->filter.clear(); - style->fill.value.href.reset(); - style->stroke.value.href.reset(); + style->fill.href.reset(); + style->stroke.href.reset(); style->shape_inside.clear(); style->shape_subtract.clear(); diff --git a/src/object/sp-pattern.cpp b/src/object/sp-pattern.cpp index 88857527c2..32b0b6fd2d 100644 --- a/src/object/sp-pattern.cpp +++ b/src/object/sp-pattern.cpp @@ -541,7 +541,7 @@ char const *SPPattern::produce(std::vector const &reprs, G // if some elements have undefined color or solid black, then their fill color is customizable if (copy->style && copy->style->isSet(SPAttr::FILL)) { if (auto paint = copy->style->getFillOrStroke(true)) { - if (paint->isColor() && paint->value.color.toRGBA32(255) == 255) { // black color set? + if (paint->isColor() && paint->getColor().toRGBA() == 255) { // black color set? can_colorize = true; // remove black fill, it will be inherited from pattern paint->clear(); diff --git a/src/object/sp-solid-color.cpp b/src/object/sp-solid-color.cpp index b7d520a357..40a8fa872f 100644 --- a/src/object/sp-solid-color.cpp +++ b/src/object/sp-solid-color.cpp @@ -63,7 +63,9 @@ Inkscape::XML::Node* SPSolidColor::write(Inkscape::XML::Document* xml_doc, Inksc std::unique_ptr SPSolidColor::create_drawing_paintserver() { - return std::make_unique(style->solid_color.value.color.v.c, SP_SCALE24_TO_FLOAT(style->solid_opacity.value)); + auto color = style->solid_color.getColor(); + color.addOpacity(style->solid_opacity); + return std::make_unique(color); } /* diff --git a/src/object/sp-solid-color.h b/src/object/sp-solid-color.h index 6b5b4726ad..f49ef15fdc 100644 --- a/src/object/sp-solid-color.h +++ b/src/object/sp-solid-color.h @@ -12,7 +12,6 @@ * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ -#include "color.h" #include "sp-paint-server.h" typedef struct _cairo cairo_t; diff --git a/src/object/sp-stop.cpp b/src/object/sp-stop.cpp index 2886688695..4a644bb50c 100644 --- a/src/object/sp-stop.cpp +++ b/src/object/sp-stop.cpp @@ -20,7 +20,6 @@ #include "attributes.h" #include "streq.h" #include "svg/svg.h" -#include "svg/svg-color.h" #include "svg/css-ostringstream.h" SPStop::SPStop() : SPObject() { @@ -128,45 +127,33 @@ SPStop* SPStop::getPrevStop() { return result; } -SPColor SPStop::getColor() const +Inkscape::Colors::Color SPStop::getColor() const { - if (style->stop_color.currentcolor) { - return style->color.value.color; - } - return style->stop_color.value.color; -} - -gfloat SPStop::getOpacity() const -{ - return SP_SCALE24_TO_FLOAT(style->stop_opacity.value); + // Copy color from the right place + Inkscape::Colors::Color color = style->stop_color.currentcolor ? style->color.getColor() : style->stop_color.getColor(); + // Bundle stop opacity into the color + color.addOpacity(style->stop_opacity); + return color; } /** * Sets the stop color and stop opacity in the style attribute. */ -void SPStop::setColor(SPColor color, double opacity) +void SPStop::setColor(Inkscape::Colors::Color const &color) { - setColorRepr(getRepr(), color, opacity); + setColorRepr(getRepr(), color); } /** * Set the color and opacity directly into the given xml repr. */ -void SPStop::setColorRepr(Inkscape::XML::Node *node, SPColor color, double opacity) +void SPStop::setColorRepr(Inkscape::XML::Node *node, Inkscape::Colors::Color const &color) { Inkscape::CSSOStringStream os; - os << "stop-color:" << color.toString() << ";stop-opacity:" << opacity <<";"; + os << "stop-color:" << color.toString(false) << ";stop-opacity:" << color.getOpacity() <<";"; node->setAttribute("style", os.str()); } -/** - * Return stop's color as 32bit value. - */ -guint32 SPStop::get_rgba32() const -{ - return getColor().toRGBA32(getOpacity()); -} - /* Local Variables: mode:c++ diff --git a/src/object/sp-stop.h b/src/object/sp-stop.h index f22b708f06..a823053971 100644 --- a/src/object/sp-stop.h +++ b/src/object/sp-stop.h @@ -19,11 +19,14 @@ #include -#include "color.h" #include "sp-object.h" typedef unsigned int guint32; +namespace Inkscape::Colors { + class Color; +} + /** Gradient stop. */ class SPStop final : public SPObject { public: @@ -39,12 +42,10 @@ public: SPStop* getNextStop(); SPStop* getPrevStop(); - SPColor getColor() const; - gfloat getOpacity() const; - guint32 get_rgba32() const; - void setColor(SPColor color, double opacity); + Inkscape::Colors::Color getColor() const; + void setColor(Inkscape::Colors::Color const &color); - static void setColorRepr(Inkscape::XML::Node *node, SPColor color, double opacity); + static void setColorRepr(Inkscape::XML::Node *node, Inkscape::Colors::Color const &color); protected: void build(SPDocument* doc, Inkscape::XML::Node* repr) override; diff --git a/src/oklab.cpp b/src/oklab.cpp deleted file mode 100644 index 80f994e02b..0000000000 --- a/src/oklab.cpp +++ /dev/null @@ -1,466 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/** @file Implementation of the OKLab/OKLch perceptual color space. - */ -/* - * Authors: - * RafaƂ Siejakowski - * - * Copyright (C) 2022 Authors - * - * Released under GNU GPL v2+, read the file 'COPYING' for more information. - */ - -#include "oklab.h" - -#include - -#include <2geom/angle.h> -#include <2geom/math-utils.h> -#include <2geom/polynomial.h> -#include "color.h" - -namespace Oklab { - -/** Two-dimensional array to store a constant 3x3 matrix. */ -using Matrix = const double[3][3]; - -/** Matrix of the linear transformation from linear RGB space to linear - * cone responses, used in the first step of RGB to OKLab conversion. - */ -Matrix LRGB2CONE = { - { 0.4122214708, 0.5363325363, 0.0514459929 }, - { 0.2119034982, 0.6806995451, 0.1073969566 }, - { 0.0883024619, 0.2817188376, 0.6299787005 } -}; - -/** The inverse of the matrix LRGB2CONE. */ -Matrix CONE2LRGB = { - { 4.0767416613479942676681908333711298900607278264432, -3.30771159040819331315866078424893188865618253342, 0.230969928729427886449650619561935920170561518112 }, - { -1.2684380040921760691815055595117506020901414005992, 2.60975740066337143024050095284233623056192338553, -0.341319396310219620992658250306535533187548361872 }, - { -0.0041960865418371092973767821251846315637521173374, -0.70341861445944960601310996913659932654899822384, 1.707614700930944853864541790660472961199090408527 } -}; - -/** The matrix M2 used in the second step of RGB to OKLab conversion. - * Taken from https://bottosson.github.io/posts/oklab/ (retrieved 2022). - */ -Matrix M2 = { - { 0.2104542553, 0.793617785, -0.0040720468 }, - { 1.9779984951, -2.428592205, 0.4505937099 }, - { 0.0259040371, 0.7827717662, -0.808675766 } -}; - -/** The inverse of the matrix M2. The first column looks like it wants to be 1 but - * this form is closer to the actual inverse (due to numerics). */ -Matrix M2_INVERSE = { - { 0.99999999845051981426207542502031373637162589278552, 0.39633779217376785682345989261573192476766903603, 0.215803758060758803423141461830037892590617787467 }, - { 1.00000000888176077671607524567047071276183677410134, -0.10556134232365634941095687705472233997368274024, -0.063854174771705903405254198817795633810975771082 }, - { 1.00000005467241091770129286515344610721841028698942, -0.08948418209496575968905274586339134130669669716, -1.291485537864091739948928752914772401878545675371 } -}; - -/** Compute the dot-product between two 3D-vectors. */ -template -inline constexpr double dot3(const A1 &a1, const A2 &a2) -{ - return a1[0] * a2[0] + a1[1] * a2[1] + a1[2] * a2[2]; -} - -Triplet oklab_to_oklch(Triplet const &ok_lab_color) -{ - Triplet result; - result[0] = ok_lab_color[0]; // copy the L component - // Convert a, b to polar coordinates c, h. - result[1] = std::hypot(ok_lab_color[1], ok_lab_color[2]); - if (result[1] > 0.001) { - Geom::Angle const hue_angle = std::atan2(ok_lab_color[2], ok_lab_color[1]); - result[2] = Geom::deg_from_rad(hue_angle.radians0()); - } else { - result[2] = 0; - } - return result; -} - -Triplet oklch_to_oklab(Triplet const &ok_lch_color) -{ - return oklch_radians_to_oklab({ ok_lch_color[0], - ok_lch_color[1], - Geom::Angle::from_degrees(ok_lch_color[2]) }); -} - -Triplet oklch_radians_to_oklab(Triplet const &oklch_rad) -{ - Triplet result; - result[0] = oklch_rad[0]; // Copy the L component - // c and h are polar coordinates; convert to Cartesian a, b coords. - Geom::sincos(oklch_rad[2], result[2], result[1]); - result[1] *= oklch_rad[1]; - result[2] *= oklch_rad[1]; - return result; -} - -Triplet oklab_to_linear_rgb(Triplet const &oklab_color) -{ - Triplet cones; - for (unsigned i = 0; i < 3; i++) { - cones[i] = Geom::cube(dot3(M2_INVERSE[i], oklab_color)); - } - Triplet result; - for (unsigned i = 0; i < 3; i++) { - result[i] = std::clamp(dot3(CONE2LRGB[i], cones), 0.0, 1.0); - } - return result; -} - -Triplet linear_rgb_to_oklab(Triplet const &linear_rgb_color) -{ - Triplet cones; - for (unsigned i = 0; i < 3; i++) { - cones[i] = std::cbrt(dot3(LRGB2CONE[i], linear_rgb_color)); - } - Triplet result; - for (unsigned i = 0; i < 3; i++) { - result[i] = dot3(M2[i], cones); - } - return result; -} - -Triplet oklab_to_okhsl(Triplet const &ok_lab_color) -{ - Triplet result; - result[2] = std::clamp(ok_lab_color[0], 0.0, 1.0); // Copy the L component. - - // Compute the chroma. - double const absolute_chroma = std::hypot(ok_lab_color[1], ok_lab_color[2]); - if (absolute_chroma < 1e-7) { - // It would be numerically unstable to calculate the hue for this - // color, so we set the hue and saturation to zero (grayscale color). - result[0] = 0.0; - result[1] = 0.0; - return result; - } - - // Compute the hue (in the unit interval). - Geom::Angle const hue_angle = std::atan2(ok_lab_color[2], ok_lab_color[1]); - result[0] = hue_angle.radians0() / (2.0 * M_PI); - - // Compute the linear saturation. - double const hue_degrees = Geom::deg_from_rad(hue_angle.radians0()); - double const chromax = max_chroma(result[2], hue_degrees); - result[1] = (chromax == 0.0) ? 0.0 : std::clamp(absolute_chroma / chromax, 0.0, 1.0); - return result; -} - -Triplet okhsl_to_oklab(Triplet const &ok_hsl_color) -{ - Triplet result; - result[0] = std::clamp(ok_hsl_color[2], 0.0, 1.0); // Copy the L component. - - // Get max chroma for this hue and lightness and compute the absolute chroma. - double const chromax = max_chroma(result[0], ok_hsl_color[0] * 360.0); - double const absolute_chroma = ok_hsl_color[1] * chromax; - - // Convert hue and chroma to the Cartesian a, b coordinates. - Geom::sincos(ok_hsl_color[0] * 2.0 * M_PI, result[2], result[1]); - result[1] *= absolute_chroma; - result[2] *= absolute_chroma; - return result; -} - -/** @brief - * Data needed to compute coefficients in the cubic polynomials which express the lines - * of constant luminosity and hue (but varying chroma) as curves in the linear RGB space. - */ -struct ChromaLineCoefficients { - // Variable naming: `c%d` contains coefficients of c^%d in the polynomial, where c is - // the OKLch chroma. l refers to the luminosity, cos and sin to the cosine and sine of - // the hue angle. Trailing digits are exponents. For example, - // c2.lcos2 is the coefficient of (l * cos(hue_angle)^2) in the overall coefficient of c^2. - struct { - double l2cos, l2sin; - } c1; - struct { - double lcos2, lcossin, lsin2; - } c2; - struct { - double cos3, cos2sin, cossin2, sin3; - } c3; -}; - -ChromaLineCoefficients const LAB_BOUNDS[] = { - // Red polynomial - { - .c1 = { - .l2cos = 5.83279532899080641005754476131631984, - .l2sin = 2.3780791275435732378965655753413412 - }, - .c2 = { - .lcos2 = 1.81614129917652075864819542521099165275, - .lcossin = 2.11851258971260413543962953223104329409, - .lsin2 = 1.68484527361538384522450980300698198391 - }, - .c3 = { - .cos3 = 0.257535869797624151773507242289856932594, - .cos2sin = 0.414490345667882332785000888243122224651, - .cossin2 = 0.126596511492002610582126014059213892767, - .sin3 = -0.455702039844046560333204117380816048203 - } - }, - // Green polynomial - { - .c1 = { - .l2cos = -2.243030176177044107983968331289088261, - .l2sin = 0.00129441240977850026657772225608 - }, - .c2 = { - .lcos2 = -0.5187087369791308621879921351291952375, - .lcossin = -0.7820717390897833607054953914674219281, - .lsin2 = -1.8531911425339782749638630868227383795 - }, - .c3 = { - .cos3 = -0.0817959138495637068389017598370049459, - .cos2sin = -0.1239788660641220973883495153116480854, - .cossin2 = 0.0792215342150077349794741576353537047, - .sin3 = 0.7218132301017783162780535454552058572 - } - }, - // Blue polynomial - { - .c1 = { - .l2cos = -0.2406412780923628220925350522352767957, - .l2sin = -6.48404701978782955733370693958213669 - }, - .c2 = { - .lcos2 = 0.015528352128452044798222201797574285162, - .lcossin = 1.153466975472590255156068122829360981648, - .lsin2 = 8.535379923500727607267514499627438513637 - }, - .c3 = { - .cos3 = -0.0006573855374563134769075967180540368, - .cos2sin = -0.0519029179849443823389557527273309386, - .cossin2 = -0.763927972885238036962716856256210617, - .sin3 = -3.67825541507929556013845659620477582 - } - } -}; - -/** Stores powers of luminance, hue cosine and hue sine angles. */ -struct ConstraintMonomials -{ - double l, l2, l3, c, c2, c3, s, s2, s3; - ConstraintMonomials(double l, double h) - : l{l} - { - l2 = Geom::sqr(l); - l3 = l2 * l; - Geom::sincos(Geom::rad_from_deg(h), s, c); - c2 = Geom::sqr(c); - c3 = c2 * c; - s2 = 1.0 - c2; // Use sin^2 = 1 - cos^2. - s3 = s2 * s; - } -}; - -/** @brief Find the coefficients of the cubic polynomial expressing the linear - * R, G or B component as a function of OKLch chroma. - * - * The returned polynomial gives R(c), G(c) or B(c) for all values of c and fixed - * values of luminance and hue. - * - * @param index The index of the component to evaluate (0 for R, 1 for G, 2 for B). - * @param m The monomials in L, cos(hue) and sin(hue) needed for the calculation. - * @return an array whose i-th element is the coefficient of c^i in the polynomial. - */ -static std::array component_coefficients(unsigned index, ConstraintMonomials const &m) -{ - auto const &coeffs = LAB_BOUNDS[index]; - std::array result; - // Multiply the coefficients by the corresponding monomials. - result[0] = m.l3; // The coefficient of l^3 is always 1 - result[1] = coeffs.c1.l2cos * m.l2 * m.c + coeffs.c1.l2sin * m.l2 * m.s; - result[2] = coeffs.c2.lcos2 * m.l * m.c2 + coeffs.c2.lcossin * m.l * m.c * m.s + coeffs.c2.lsin2 * m.l * m.s2; - result[3] = coeffs.c3.cos3 * m.c3 + coeffs.c3.cos2sin * m.c2 * m.s - + coeffs.c3.cossin2 * m.c * m.s2 + coeffs.c3.sin3 * m.s3; - return result; -} - -/* Compute the maximum Lch chroma for the given luminosity and hue. - * - * Implementation notes: - * The space of Lch colors is a complicated solid with curved faces in the - * (L, c, h)-space. So it is not easy to find the maximum chroma for the given - * luminosity and hue. (By maximum chroma, we mean the maximum value of c such - * that the color oklch(L c h) still fits in the sRGB gamut.) - * - * We consider an abstract ray (L, c, h) where L and h are fixed and c varies - * from 0 to infinity. Conceptually, we transform this ray to the linear RGB space, - * which is the unit cube. The ray thus becomes a 3D cubic curve in the RGB cube - * and the coordinates R(c), G(c) and B(c) are degree 3 polynomials in the chroma - * variable c. The coefficients of c^i in those polynomials will depend on L and h. - * - * To find the smallest positive value of c for which the curve leaves the unit - * cube, we must solve the equations R(c) = 0, R(c) = 1 and similarly for G(c) - * and B(c). The desired value is the smallest positive solution among those 6 - * equations. - * - * The case of very small or very large luminosity is handled separately. - */ -double max_chroma(double l, double h) -{ - static double const EPS = 1e-7; - if (l < EPS || l > 1.0 - EPS) { // Black or white allow no chroma. - return 0; - } - - double chroma_bound = Geom::infinity(); - auto const process_root = [&](double root) -> bool { - if (root < EPS) { // Ignore roots less than epsilon - return false; - } - if (chroma_bound > root) { - chroma_bound = root; - } - return true; - }; - - // Check relevant chroma constraints for all three coordinates R, G, B. - auto const monomials = ConstraintMonomials(l, h); - for (unsigned i = 0; i < 3; i++) { - auto const coeffs = component_coefficients(i, monomials); - // The cubic polynomial is coeffs[3]*c^3 + coeffs[2]*c^2 + coeffs[1]*c + coeffs[0] - - // First we solve for the R/G/B component equal to zero. - for (double root : Geom::solve_cubic(coeffs[3], coeffs[2], coeffs[1], coeffs[0])) { - if (process_root(root)) { - break; - } - } - - // Now solve for the component equal to 1 by subtracting 1.0 from coeffs[0]. - for (double root : Geom::solve_cubic(coeffs[3], coeffs[2], coeffs[1], coeffs[0] - 1.0)) { - if (process_root(root)) { - break; - } - } - } - if (chroma_bound == Geom::infinity()) { // No bound was found, so everything was < EPS - return 0; - } - return chroma_bound; -} - -/** @brief How many intervals a color scale should be subdivided into for the chroma bounds probing. - * - * The reason this constant exists is because probing chroma bounds requires solving 6 cubic equations, - * which would not be feasible for all 1024 pixels on a scale without slowing down the UI. - * To speed things up, we subdivide the scale into COLOR_SCALE_INTERVALS intervals and linearly - * interpolate the chroma bound on each interval. Note that the actual color interpolation is still - * done in the OKLab space, but the computed absolute chroma may be slightly off in the middle of - * each interval (hopefully, in an imperceptible way). - * - * @todo Consider rendering the color sliders asynchronously, which might make this - * interpolation unnecessary. We would then get full precision gradients. - */ -unsigned const COLOR_SCALE_INTERVALS = 32; // Must be a power of 2 and less than 1024. - -uint8_t const *render_hue_scale(double s, double l, std::array *map) -{ - auto const data = map->data(); - auto pos = data; - unsigned const interval_length = 1024 / COLOR_SCALE_INTERVALS; - - double h = 0; // Variable hue - double chroma_bound = max_chroma(l, h); - double next_chroma_bound; - double const step = 360.0 / 1024.0; - double const interpolation_step = 360.0 / COLOR_SCALE_INTERVALS; - - for (unsigned i = 0; i < COLOR_SCALE_INTERVALS; i++) { - double const initial_chroma = chroma_bound * s; - next_chroma_bound = max_chroma(l, h + interpolation_step); - double const final_chroma = next_chroma_bound * s; - - for (unsigned j = 0; j < interval_length; j++) { - double const c = Geom::lerp(static_cast(j) / interval_length, initial_chroma, final_chroma); - auto [r, g, b] = oklab_to_rgb(oklch_to_oklab({l, c, h})); - *pos++ = (uint8_t)SP_COLOR_F_TO_U(r); - *pos++ = (uint8_t)SP_COLOR_F_TO_U(g); - *pos++ = (uint8_t)SP_COLOR_F_TO_U(b); - *pos++ = 0xFF; - h += step; - } - chroma_bound = next_chroma_bound; - } - return data; -} - -uint8_t const *render_saturation_scale(double h, double l, std::array *map) -{ - auto const data = map->data(); - auto pos = data; - auto chromax = max_chroma(l, h); - if (chromax == 0.0) { // Render black or white strip. - uint8_t const bw = (l > 0.9) ? 0xFF : 0x00; - for (size_t i = 0; i < 1024; i++) { - *pos++ = bw; // red - *pos++ = bw; // green - *pos++ = bw; // blue - *pos++ = 0xFF; // alpha - } - } else { // Render strip of varying chroma. - double const chroma_step = chromax / 1024.0; - double c = 0.0; - for (size_t i = 0; i < 1024; i++) { - auto [r, g, b] = oklab_to_rgb(oklch_to_oklab({l, c, h})); - *pos++ = (uint8_t)SP_COLOR_F_TO_U(r); - *pos++ = (uint8_t)SP_COLOR_F_TO_U(g); - *pos++ = (uint8_t)SP_COLOR_F_TO_U(b); - *pos++ = 0xFF; - c += chroma_step; - } - } - return data; -} - -uint8_t const *render_lightness_scale(double h, double s, std::array *map) -{ - auto const data = map->data(); - auto pos = data; - unsigned const interval_length = 1024 / COLOR_SCALE_INTERVALS; - - double l = 0; // Variable lightness - - double chroma_bound = max_chroma(l, h); - double next_chroma_bound; - double const step = 1.0 / 1024.0; - double const interpolation_step = 1.0 / COLOR_SCALE_INTERVALS; - - for (unsigned i = 0; i < COLOR_SCALE_INTERVALS; i++) { - double const initial_chroma = chroma_bound * s; - next_chroma_bound = max_chroma(l + interpolation_step, h); - double const final_chroma = next_chroma_bound * s; - - for (unsigned j = 0; j < interval_length; j++) { - double const c = Geom::lerp(static_cast(j) / interval_length, initial_chroma, final_chroma); - auto [r, g, b] = oklab_to_rgb(oklch_to_oklab({l, c, h})); - *pos++ = (uint8_t)SP_COLOR_F_TO_U(r); - *pos++ = (uint8_t)SP_COLOR_F_TO_U(g); - *pos++ = (uint8_t)SP_COLOR_F_TO_U(b); - *pos++ = 0xFF; - l += step; - } - chroma_bound = next_chroma_bound; - } - return data; -} - -} // namespace Oklab - -/* - Local Variables: - mode:c++ - c-file-style:"stroustrup" - c-file-offsets:((innamespace . 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 : \ No newline at end of file diff --git a/src/oklab.h b/src/oklab.h deleted file mode 100644 index 65c0bd1458..0000000000 --- a/src/oklab.h +++ /dev/null @@ -1,150 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/** @file Support for the OKLab/OKLch perceptual color space. - */ -/* - * Authors: - * RafaƂ Siejakowski - * - * Copyright (C) 2022 Authors - * - * Released under GNU GPL v2+, read the file 'COPYING' for more information. - */ - -#ifndef SEEN_OKLAB_H -#define SEEN_OKLAB_H - -#include - -#include "hsluv.h" - -namespace Oklab { - -using Triplet = Hsluv::Triplet; -using Hsluv::to_linear, Hsluv::from_linear; - -/** @brief Convert an OKLab color to the linear RGB color space. - * - * @param oklab_color A triplet containing the L, a, b components. - * @return a triplet containing linear (de-gamma'd) RGB components, all in [0, 1]. - */ -Triplet oklab_to_linear_rgb(Triplet const &oklab_color); - -/** @brief Convert a linear RGB color to OKLab coordinates. - * - * @param linear_rgb_color A triplet containing degamma'd (linearized) R, G, B components. - * @return a triplet containing the (L, a, b) coordinates of the color in the OKLab space. - */ -Triplet linear_rgb_to_oklab(Triplet const &linear_rgb_color); - -/** Convert an OKLab color to a gamma-compressed sRGB color. */ -inline Triplet oklab_to_rgb(Triplet const &oklab_color) -{ - auto rgb = oklab_to_linear_rgb(oklab_color); - for (auto &component : rgb) { - component = from_linear(component); - } - return rgb; -} - -/** Convert a gamma-compressed sRGB color to an OKLab color. */ -inline Triplet rgb_to_oklab(Triplet const &rgb_color) -{ - Triplet linear; - for (size_t i = 0; i < 3; i++) { - linear[i] = to_linear(rgb_color[i]); - } - return linear_rgb_to_oklab(linear); -} - -/** @brief Convert an OKLab color to the OKLch coordinates. - * - * The OKLch coordinates are more closely aligned with the perceptual properties of a color - * and therefore are more convenient for the end user. They consist of: - * L – luminance of the color, in the interval [0, 1] (this is the same as the L in (L, a, b)). - * c – chroma; how far the color is from grayscale. The range of c-values is [0, cmax] but - * cmax depends on L and h; @see Oklab::max_chroma(). - * h – hue. A number in the interval [0, 360), interpreted as a hue angle on the color wheel. - * - * @param ok_lab_color A color in the OKLab color space. - * @return The same color expressed in the Lch coordinates. - */ -Triplet oklab_to_oklch(Triplet const &ok_lab_color); - -/** @brief Convert an OKLch color to the OKLab coordinates. - * - * For the meaning of the Lch color coordinates, @see oklab_to_oklch(). - */ -Triplet oklch_to_oklab(Triplet const &ok_lch_color); -Triplet oklch_radians_to_oklab(Triplet const &ok_lch_with_hue_in_radians); - -/** @brief Convert an OKLab color to an OKHSL representation. - * - * As of late 2022, OK-HSL (hue, saturation, lightness) is not a fully standardized color - * space. The version used here stores colors as triples (h, s, L) of doubles, all in - * the interval [0, 1]. These coordinates have the following meaning: - * - * \li \c h is the hue angle, scaled to the interval [0, 1]. - * \li \c s is the linear saturation. For a given OKLch color (L, c, h), linear saturation - * is the ratio of c to the maximum possible chroma c_max such that (L, c_max, h) - * fits in the sRGB gamut. Therefore, s=1 always corresponds to a maximally saturated - * color and s=0 is always a grayscale color. - * \li \c L is the lightness; it is the same coordinate as the L in OKLab or OKLch. - * - * Note that Björn Ottosson proposes a new, non-standard way of compressing the saturation - * (similar to gamma compression) which results in a different saturation scale. He further - * suggests varying this compression depending on the hue, although he does not standardize - * this proposed OKHSL space or the saturation transfer functions. Instead, he uses a somewhat - * ad-hoc piecewise-linear saturation transfer function in his own picker, which unfortunately - * breaks differentiability of the parametrization, a great advantage of the OKLch color space. - * If an OKHSL space is ever standardized, the behaviour of this function may change. - * See https://bottosson.github.io/posts/colorpicker/ for more details. - * - * @param ok_lab_color A color in the OKLab color space. - * @return The same color expressed in the (h, s, L) coordinates. - */ -Triplet oklab_to_okhsl(Triplet const &ok_lab_color); - -/** @brief Convert an OKHSL color to the OKLab coordinates. - * - * For a description of the OKHSL color scale used here, see oklab_to_okhsl(). - * If an OKHSL space is ever standardized, the behaviour of this function may change. - * - * @param ok_hsl_color A color in the OKHSL coordinates, all normalized to [0, 1]. - * @return The same color expressed in the (L, a, b) coordinates. - */ -Triplet okhsl_to_oklab(Triplet const &ok_hsl_color); - -/** @brief Find the maximum OKLch chroma for the given luminosity and hue. - * - * @param l OKLab/OKLch luminosity, in the interval [0, 1]. - * @param h OKLch hue angle in degrees (interval [0, 360]). - * @return The maximum chroma c such that the color oklch(l, c, h) fits in the sRGB gamut. - */ -double max_chroma(double l, double h); - -/** @brief Helper functions for rendering color strips used in color sliders. - * - * @param h Hue angle in degrees (range 0-360). - * @param s Saturation (percentage of allowed chroma) in the range 0-1. - * @param l OKLab/OKLch lightness in the range 0-1. - * @param[out] map Working store and RGB output showing the range of one of the color coordinates. - * @return A pointer to the raw data of the map. - */ -uint8_t const *render_hue_scale(double s, double l, std::array *map); -uint8_t const *render_saturation_scale(double h, double l, std::array *map); -uint8_t const *render_lightness_scale(double h, double s, std::array *map); - -} // namespace Oklab - -#endif // SEEN_OKLAB_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 : \ No newline at end of file diff --git a/src/page-manager.cpp b/src/page-manager.cpp index d91ed03a98..3324d3ef88 100644 --- a/src/page-manager.cpp +++ b/src/page-manager.cpp @@ -11,6 +11,7 @@ #include #include "attributes.h" +#include "colors/manager.h" #include "desktop.h" #include "display/control/canvas-page.h" #include "document.h" @@ -21,12 +22,16 @@ #include "object/sp-page.h" #include "object/sp-root.h" #include "selection-chemistry.h" -#include "svg/svg-color.h" #include "util/parse-int-range.h" #include "util/numeric/converters.h" namespace Inkscape { +static auto const default_background_color = Colors::Color{0xffffff00}; +static auto const default_margin_color = Colors::Color{0x1699d751}; +static auto const default_bleed_color = Colors::Color{0xbe310e31}; +static auto const default_border_color = Colors::Color{0x0000003f}; + bool PageManager::move_objects() { Inkscape::Preferences *prefs = Inkscape::Preferences::get(); @@ -38,6 +43,10 @@ PageManager::PageManager(SPDocument *document) , border_on_top(true) , shadow_show(true) , checkerboard(false) + , background_color{default_background_color} + , margin_color{default_margin_color} + , bleed_color{default_bleed_color} + , border_color{default_border_color} { _document = document; } @@ -675,19 +684,17 @@ bool PageManager::subset(SPAttr key, const gchar *value) case SPAttr::BORDERLAYER: this->border_on_top.readOrUnset(value); break; - case SPAttr::BORDERCOLOR: - this->border_color = this->border_color & 0xff; - if (value) { - this->border_color = this->border_color | sp_svg_read_color(value, this->border_color); - } + case SPAttr::BORDERCOLOR: { + auto const old_opacity = border_color.getOpacity(); + border_color = Colors::Color::parse(value).value_or(default_border_color); + border_color.setOpacity(old_opacity); break; + } case SPAttr::BORDEROPACITY: - sp_ink_read_opacity(value, &this->border_color, 0x000000ff); + border_color.setOpacity(value ? g_ascii_strtod(value, nullptr) : 1.0); break; case SPAttr::PAGECOLOR: - if (value) { - this->background_color = sp_svg_read_color(value, this->background_color) | 0xff; - } + background_color = Colors::Color::parse(value).value_or(default_background_color); break; case SPAttr::SHOWPAGESHADOW: // Deprecated this->shadow_show.readOrUnset(value); @@ -714,14 +721,28 @@ bool PageManager::subset(SPAttr key, const gchar *value) */ bool PageManager::setDefaultAttributes(Inkscape::CanvasPage *item) { + auto bdcolor = getBorderColor(); + + if (!border_show) { + bdcolor.setOpacity(0.0); + } + + auto bgcolor = getBackgroundColor(); + // note: page background color doesn't have configurable transparency; it is considered to be opaque; // here alpha gets manipulated to reveal checkerboard pattern, if needed - auto bgcolor = checkerboard ? background_color & ~0xff : background_color | 0xff; - auto dkcolor = _document->getNamedView()->desk_color; + if (checkerboard) { + bgcolor.setOpacity(0.0); + } else { + bgcolor.setOpacity(1.0); + } + + auto dkcolor = _document->getNamedView()->getDeskColor(); + bool ret = item->setOnTop(border_on_top); // fixed shadow size, not configurable; shadow changes size with zoom ret |= item->setShadow(border_show && shadow_show ? 2 : 0); - ret |= item->setPageColor(border_show ? border_color : 0x0, bgcolor, dkcolor, margin_color, bleed_color); + ret |= item->setPageColor(bdcolor, bgcolor, dkcolor, getMarginColor(), getBleedColor()); ret |= item->setLabelStyle(label_style); return ret; } diff --git a/src/page-manager.h b/src/page-manager.h index 36d154293d..e239cdefd3 100644 --- a/src/page-manager.h +++ b/src/page-manager.h @@ -11,8 +11,8 @@ #define SEEN_INKSCAPE_PAGE_MANAGER_H #include +#include -#include "color-rgba.h" #include "document.h" #include "object/sp-namedview.h" #include "svg/svg-bool.h" @@ -29,6 +29,9 @@ namespace Dialog { class DocumentProperties; } } // namespace UI +namespace Colors { +class Color; +} class PageManager { @@ -75,7 +78,7 @@ public: bool hasNextPage() const { return getSelectedPageIndex() + 1 < pages.size(); } bool hasPrevPage() const { return getSelectedPageIndex() - 1 >= 0; } - ColorRGBA getDefaultBackgroundColor() const { return ColorRGBA(background_color); } + Colors::Color const &getDefaultBackgroundColor() const { return background_color; } // TODO: move these functions out of here and into the Canvas or InkscapeWindow void zoomToPage(SPDesktop *desktop, SPPage *page, bool width_only = false); @@ -121,10 +124,10 @@ public: return _pages_changed_signal.connect(slot); } - // Access from export.cpp and others for the guint32 - guint32 background_color = 0xffffff00; - guint32 margin_color = 0x1699d751; - guint32 bleed_color = 0xbe310e31; + Colors::Color const &getBackgroundColor() const { return background_color; } + Colors::Color const &getMarginColor() const { return margin_color; } + Colors::Color const &getBleedColor() const { return bleed_color; } + Colors::Color const &getBorderColor() const { return border_color; } void movePages(Geom::Affine tr); std::vector getOverlappingItems(SPDesktop *desktop, SPPage *page, bool hidden = true, bool in_bleed = false, bool in_layers = true); @@ -138,7 +141,6 @@ protected: SVGBool shadow_show; SVGBool checkerboard; - guint32 border_color = 0x0000003f; std::string label_style = "default"; private: @@ -151,6 +153,11 @@ private: sigc::signal _pages_changed_signal; sigc::connection _page_modified_connection; + + Colors::Color background_color; + Colors::Color margin_color; + Colors::Color bleed_color; + Colors::Color border_color; }; } // namespace Inkscape diff --git a/src/pattern-manager.cpp b/src/pattern-manager.cpp index 4a08f0d31c..be8f0a3898 100644 --- a/src/pattern-manager.cpp +++ b/src/pattern-manager.cpp @@ -179,7 +179,7 @@ Glib::RefPtr create_pattern_item(SPDocument *sandbox, SPPattern* pa // reading color style form "root" pattern; linked one won't have any effect, as it's not a parrent if (root_pattern->style && root_pattern->style->isSet(SPAttr::FILL) && root_pattern->style->fill.isColor()) { - item->color.emplace(SPColor(root_pattern->style->fill.value.color)); + item->color.emplace(root_pattern->style->fill.getColor()); } // uniform scaling? if (link_pattern->aspect_set) { diff --git a/src/pattern-manipulation.cpp b/src/pattern-manipulation.cpp index 29b54ff21f..6d4b2b2813 100644 --- a/src/pattern-manipulation.cpp +++ b/src/pattern-manipulation.cpp @@ -3,8 +3,8 @@ #include #include #include "pattern-manipulation.h" +#include "colors/color.h" #include "document.h" -#include "color.h" #include "helper/stock-items.h" #include "object/sp-pattern.h" #include "xml/repr.h" @@ -34,12 +34,12 @@ std::vector sp_get_pattern_list(SPDocument* source) { return list; } -void sp_pattern_set_color(SPPattern* pattern, unsigned int color) { +void sp_pattern_set_color(SPPattern* pattern, Inkscape::Colors::Color const &c) +{ if (!pattern) return; - SPColor c(color); SPCSSAttr* css = sp_repr_css_attr_new(); - sp_repr_css_set_property(css, "fill", c.toString().c_str()); + sp_repr_css_set_property_string(css, "fill", c.toString()); pattern->changeCSS(css, "style"); sp_repr_css_attr_unref(css); } diff --git a/src/pattern-manipulation.h b/src/pattern-manipulation.h index 45d1e26c8b..5d55c5d746 100644 --- a/src/pattern-manipulation.h +++ b/src/pattern-manipulation.h @@ -10,6 +10,10 @@ class SPDocument; class SPPattern; +namespace Inkscape::Colors { +class Color; +} + // Find and load stock patterns if not yet loaded and return them. // Their lifetime is bound to StockPaintDocuments. std::vector sp_get_stock_patterns(); @@ -22,7 +26,7 @@ std::vector sp_get_pattern_list(SPDocument* source); // Set fill color for a pattern. // If elements comprising pattern have no fill, they will inherit it. // Some patterns may not be affected at all if not designed to support color change. -void sp_pattern_set_color(SPPattern* pattern, unsigned int color); +void sp_pattern_set_color(SPPattern* pattern, Inkscape::Colors::Color const &color); // Set 'patternTransform' attribute void sp_pattern_set_transform(SPPattern* pattern, const Geom::Affine& transform); diff --git a/src/preferences.cpp b/src/preferences.cpp index e16720a5a2..4ccb41aa91 100644 --- a/src/preferences.cpp +++ b/src/preferences.cpp @@ -21,6 +21,8 @@ #include #include "attribute-rel-util.h" +#include "colors/color.h" +#include "colors/manager.h" #include "preferences.h" #include "preferences-skeleton.h" #include "io/resource.h" @@ -423,11 +425,9 @@ void Preferences::setDoubleUnit(Glib::ustring const &pref_path, double value, Gl _setRawValue(pref_path, str); } -void Preferences::setColor(Glib::ustring const &pref_path, guint32 value) +void Preferences::setColor(Glib::ustring const &pref_path, Colors::Color const &color) { - gchar buf[16]; - g_snprintf(buf, 16, "#%08x", value); - _setRawValue(pref_path, buf); + _setRawValue(pref_path, color.toString()); } /** @@ -898,23 +898,6 @@ Glib::ustring Preferences::_extractUnit(Entry const &v) } } -guint32 Preferences::_extractColor(Entry const &v) -{ - if (v.cached_color) return v.value_color; - v.cached_color = true; - gchar const *s = static_cast(v._value); - std::istringstream hr(s); - guint32 color; - if (s[0] == '#') { - hr.ignore(1); - hr >> std::hex >> color; - } else { - hr >> color; - } - v.value_color = color; - return color; -} - SPCSSAttr *Preferences::_extractStyle(Entry const &v) { if (v.cached_style) return v.value_style; @@ -1017,6 +1000,26 @@ PrefObserver Preferences::createObserver(Glib::ustring path, std::function(_value))) { + return *res; + } + } + if (auto res = Colors::Color::parse(def)) { + return *res; + } + // Transparent black is the default's default + return Colors::Color(0x00000000); +} + } // namespace Inkscape /* diff --git a/src/preferences.h b/src/preferences.h index 40a1f75c22..285dcb4732 100644 --- a/src/preferences.h +++ b/src/preferences.h @@ -32,6 +32,9 @@ class SPCSSAttr; typedef unsigned int guint32; namespace Inkscape { +namespace Colors { +class Color; +} class ErrorReporter { public: @@ -230,9 +233,9 @@ public: inline Glib::ustring getUnit() const; /** - * Interpret the preference as an RGBA color value. + * Interpret the preference as a css color value. */ - inline guint32 getColor(guint32 def) const; + inline Colors::Color getColor(std::string const &def) const; /** * Interpret the preference as a CSS style. @@ -276,7 +279,6 @@ public: mutable unsigned int value_uint = 0; mutable double value_double = 0.; mutable Glib::ustring value_unit; - mutable guint32 value_color = 0; mutable SPCSSAttr* value_style = nullptr; mutable bool cached_bool = false; @@ -458,9 +460,7 @@ public: return getEntry(pref_path).getUnit(); } - guint32 getColor(Glib::ustring const &pref_path, guint32 def=0x000000ff) { - return getEntry(pref_path).getColor(def); - } + Colors::Color getColor(Glib::ustring const &pref_path, std::string const &def = "black"); /** * Retrieve a CSS style. @@ -541,7 +541,7 @@ public: /** * Set an RGBA color value. */ - void setColor(Glib::ustring const &pref_path, guint32 value); + void setColor(Glib::ustring const &pref_path, Colors::Color const &color); /** * Set a CSS style. @@ -655,7 +655,6 @@ protected: double _extractDouble(Entry const &v, Glib::ustring const &requested_unit); Glib::ustring _extractString(Entry const &v); Glib::ustring _extractUnit(Entry const &v); - guint32 _extractColor(Entry const &v); SPCSSAttr *_extractStyle(Entry const &v); SPCSSAttr *_extractInheritedStyle(Entry const &v); @@ -792,15 +791,6 @@ inline Glib::ustring Preferences::Entry::getUnit() const } } -inline guint32 Preferences::Entry::getColor(guint32 def) const -{ - if (!this->isValid()) { - return def; - } else { - return Inkscape::Preferences::get()->_extractColor(*this); - } -} - inline SPCSSAttr *Preferences::Entry::getStyle() const { if (!this->isValid()) { diff --git a/src/profile-manager.cpp b/src/profile-manager.cpp deleted file mode 100644 index b092980ec8..0000000000 --- a/src/profile-manager.cpp +++ /dev/null @@ -1,102 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * Inkscape::ProfileManager - a view of a document's color profiles. - * - * Copyright 2007 Jon A. Cruz - * Abhishek Sharma - * - * Released under GNU GPL v2+, read the file 'COPYING' for more information. - */ - -#include -#include - -#include "profile-manager.h" - -#include "document.h" - -#include "object/color-profile.h" - - -namespace Inkscape { - -ProfileManager::ProfileManager(SPDocument *document) : - _doc(document), - _knownProfiles() -{ - _resource_connection = _doc->connectResourcesChanged( "iccprofile", sigc::mem_fun(*this, &ProfileManager::_resourcesChanged) ); -} - -ProfileManager::~ProfileManager() -{ - _resource_connection.disconnect(); - _doc = nullptr; -} - -void ProfileManager::_resourcesChanged() -{ - std::vector newList; - if (_doc) { - std::vector current = _doc->getResourceList( "iccprofile" ); - newList = current; - } - sort( newList.begin(), newList.end() ); - - std::vector diff1; - std::set_difference( _knownProfiles.begin(), _knownProfiles.end(), newList.begin(), newList.end(), - std::insert_iterator >(diff1, diff1.begin()) ); - - std::vector diff2; - std::set_difference( newList.begin(), newList.end(), _knownProfiles.begin(), _knownProfiles.end(), - std::insert_iterator >(diff2, diff2.begin()) ); - - if ( !diff1.empty() ) { - for ( std::vector::iterator it = diff1.begin(); it < diff1.end(); ++it ) { - SPObject* tmp = *it; - _knownProfiles.erase( remove(_knownProfiles.begin(), _knownProfiles.end(), tmp), _knownProfiles.end() ); - if ( includes(tmp) ) { - _removeOne(tmp); - } - } - } - - if ( !diff2.empty() ) { - for ( std::vector::iterator it = diff2.begin(); it < diff2.end(); ++it ) { - SPObject* tmp = *it; - _knownProfiles.push_back(tmp); - _addOne(tmp); - } - sort( _knownProfiles.begin(), _knownProfiles.end() ); - } -} - -ColorProfile* ProfileManager::find(gchar const* name) -{ - ColorProfile* match = nullptr; - if ( name ) { - unsigned int howMany = childCount(nullptr); - for ( unsigned int index = 0; index < howMany; index++ ) { - SPObject *obj = nthChildOf(nullptr, index); - ColorProfile* prof = reinterpret_cast(obj); - if (prof && (prof->name && !strcmp(name, prof->name))) { - match = prof; - break; - } - } - } - return match; -} - -} - - -/* - Local Variables: - mode:c++ - c-file-style:"stroustrup" - c-file-offsets:((innamespace . 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/profile-manager.h b/src/profile-manager.h deleted file mode 100644 index d9ad037e09..0000000000 --- a/src/profile-manager.h +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * Inkscape::ProfileManager - a view of a document's color profiles. - * - * Copyright 2007 Jon A. Cruz - * - * Released under GNU GPL v2+, read the file 'COPYING' for more information. - */ - -#ifndef SEEN_INKSCAPE_PROFILE_MANAGER_H -#define SEEN_INKSCAPE_PROFILE_MANAGER_H - -#include - -#include "document-subset.h" - -class SPDocument; - -namespace Inkscape { - -class ColorProfile; - -class ProfileManager : public DocumentSubset -{ -public: - ProfileManager(SPDocument *document); - ~ProfileManager(); - - ColorProfile* find(char const* name); - -private: - ProfileManager(ProfileManager const &) = delete; // no copy - void operator=(ProfileManager const &) = delete; // no assign - - void _resourcesChanged(); - - SPDocument* _doc; - sigc::connection _resource_connection; - std::vector _knownProfiles; -}; - -} - -#endif // SEEN_INKSCAPE_PROFILE_MANAGER_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/selection-chemistry.cpp b/src/selection-chemistry.cpp index e5f3f4f840..9c5e98272c 100644 --- a/src/selection-chemistry.cpp +++ b/src/selection-chemistry.cpp @@ -83,7 +83,6 @@ #include "path-chemistry.h" #include "selection.h" #include "style.h" -#include "svg/svg-color.h" #include "svg/svg.h" #include "text-chemistry.h" #include "text-editing.h" @@ -2085,8 +2084,8 @@ std::vector sp_get_same_fill_or_stroke_color(SPItem *sel, std::vectorstyle->getFillOrStroke(type == SP_FILL_COLOR); match = false; - if (sel_paint->isColor() && iter_paint->isColor() // color == color comparison doesn't seem to work here. - && (sel_paint->value.color.toRGBA32(1.0) == iter_paint->value.color.toRGBA32(1.0))) { + if (sel_paint->isColor() && iter_paint->isColor() + && (sel_paint->getColor().isSimilar(iter_paint->getColor()))) { match = true; } else if (sel_paint->isPaintserver() && iter_paint->isPaintserver()) { @@ -4168,10 +4167,9 @@ void ObjectSet::swapFillStroke() if (paint->set && paint->isNone()) sp_repr_css_set_property (css, "stroke", "none"); else if (paint->set && paint->isColor()) { - guint32 color = paint->value.color.toRGBA32(SP_SCALE24_TO_FLOAT (item->style->fill_opacity.value)); - gchar c[64]; - sp_svg_write_color (c, sizeof(c), color); - sp_repr_css_set_property (css, "stroke", c); + auto color = paint->getColor(); + color.addOpacity(item->style->fill_opacity); + sp_repr_css_set_property_string(css, "stroke", color.toString()); } else if (!paint->set) sp_repr_css_unset_property (css, "stroke"); @@ -4191,10 +4189,9 @@ void ObjectSet::swapFillStroke() if (paint->set && paint->isNone()) sp_repr_css_set_property (css, "fill", "none"); else if (paint->set && paint->isColor()) { - guint32 color = paint->value.color.toRGBA32(SP_SCALE24_TO_FLOAT (item->style->stroke_opacity.value)); - gchar c[64]; - sp_svg_write_color (c, sizeof(c), color); - sp_repr_css_set_property (css, "fill", c); + auto color = paint->getColor(); + color.addOpacity(item->style->stroke_opacity); + sp_repr_css_set_property_string(css, "fill", color.toString()); } else if (!paint->set) sp_repr_css_unset_property (css, "fill"); diff --git a/src/style-internal.cpp b/src/style-internal.cpp index c0945784ab..b5deabf1ed 100644 --- a/src/style-internal.cpp +++ b/src/style-internal.cpp @@ -31,6 +31,11 @@ #include "style-internal.h" #include "style.h" +#include "colors/color.h" +#include "colors/document-cms.h" + +#include "document.h" + #include "bad-uri-exception.h" #include "extract-uri.h" #include "preferences.h" @@ -1387,13 +1392,57 @@ void SPIShapes::hrefs_clear() // SPIColor ------------------------------------------------------------- +SPIColor::SPIColor(bool inherits) + : SPIBase(inherits) +{} + +bool SPIColor::canHaveCMS() const +{ + return style && style->document; +} + +Colors::DocumentCMS const &SPIColor::getCMS() const +{ + if (!style || !style->document) { + g_error("Can not get Document CMS manager."); + } + return style->document->getDocumentCMS(); +} + +SPIColor& SPIColor::operator=(const SPIColor& rhs) +{ + SPIBase::operator=(rhs); + currentcolor = rhs.currentcolor; + _color = rhs._color; + return *this; +} + +SPIColor& SPIColor::operator=(const Colors::Color &rhs) +{ + auto copy = rhs; + setColor(copy); + return *this; +} + +void SPIColor::setColor(Colors::Color const &other) +{ + currentcolor = false; + _color = other; + set = true; +} + +Colors::Color const &SPIColor::getColor() const +{ + static auto default_color = Colors::Color(0x0); // Transparent RGB black + return _color ? *_color : default_color; +} + // Used for 'color', 'text-decoration-color', 'flood-color', 'lighting-color', and 'stop-color'. -// (The last three have yet to be implemented.) // CSS3: 'currentcolor' is allowed value and is equal to inherit for the 'color' property. -// FIXME: We should preserve named colors, hsl colors, etc. -void SPIColor::read( gchar const *str ) { - if( !str ) return; +void SPIColor::read(gchar const *str) +{ + if(!str) return; set = false; inherit = false; @@ -1407,12 +1456,22 @@ void SPIColor::read( gchar const *str ) { if (id() == SPAttr::COLOR) { inherit = true; // CSS3 } else if (style) { - setColor( style->color.value.color ); + _color = style->color._color; } else { std::cerr << "SPIColor::read(): value is 'currentColor' but 'color' not available." << std::endl; } } else { - set = value.color.fromString(str); + if (canHaveCMS()) { + _color = getCMS().parse(str); + } else { + if (str) { // DEBUG + if (std::string(str).find("icc") != std::string::npos) { + g_error("CMS color '%s' not parsed, no CMS document available.", str); + } + } + _color = Colors::Color::parse(str); + } + set = (bool)_color; } } @@ -1421,15 +1480,16 @@ const Glib::ustring SPIColor::get_value() const // currentcolor goes first to handle special case for 'color' property if (this->currentcolor) return Glib::ustring("currentColor"); if (this->inherit) return Glib::ustring("inherit"); - return this->value.color.toString(); + return _color ? _color->toString() : ""; } void SPIColor::cascade( const SPIBase* const parent ) { if( const SPIColor* p = dynamic_cast(parent) ) { if( (inherits && !set) || inherit) { // FIXME verify for 'color' - if( !(inherit && currentcolor) ) currentcolor = p->currentcolor; - setColor( p->value.color ); + if (!(inherit && currentcolor)) + currentcolor = p->currentcolor; + _color = p->_color; } else { // Add CSS4 Color: Lighter, Darker } @@ -1447,7 +1507,7 @@ SPIColor::merge( const SPIBase* const parent ) { set = p->set; inherit = p->inherit; currentcolor = p->currentcolor; - value.color = p->value.color; + _color = p->_color; } } } @@ -1457,11 +1517,15 @@ bool SPIColor::equals(const SPIBase& rhs) const { if( const SPIColor* r = dynamic_cast(&rhs) ) { - // ICC support is handled by SPColor== - if (currentcolor != r->currentcolor || value.color != r->value.color) { + if (currentcolor != r->currentcolor) { + return false; + } + if ((_color && !r->_color) || (!_color && r->_color)) { + return false; + } + if (_color && r->_color && *_color != *r->_color) { return false; } - return SPIBase::equals(rhs); } else { @@ -1479,6 +1543,24 @@ SPIColor::equals(const SPIBase& rhs) const { // find the object for creating an href (this is done through document but should be done // directly so document not needed.. FIXME). +SPIPaint::SPIPaint() +{ + clear(); +} + +bool SPIPaint::canHaveCMS() const +{ + return style && style->document; +} + +Colors::DocumentCMS const &SPIPaint::getCMS() const +{ + if (!style || !style->document) { + g_error("Can not get Document CMS manager."); + } + return style->document->getDocumentCMS(); +} + /** * Set SPIPaint object from string. * @@ -1529,21 +1611,21 @@ SPIPaint::read( gchar const *str ) { SPDocument *document = (style->object) ? style->object->document : nullptr; // Create href if not done already - if (!value.href) { + if (!href) { if (style->object) { - value.href = std::make_shared(style->object); + href = std::make_shared(style->object); } else if (document) { - value.href = std::make_shared(document); + href = std::make_shared(document); } else { std::cerr << "SPIPaint::read: No valid object or document!" << std::endl; return; } if (this == &style->fill) { - style->fill_ps_changed_connection = value.href->changedSignal().connect(sigc::bind(sigc::ptr_fun(sp_style_fill_paint_server_ref_changed), style)); + style->fill_ps_changed_connection = href->changedSignal().connect(sigc::bind(sigc::ptr_fun(sp_style_fill_paint_server_ref_changed), style)); } else { - style->stroke_ps_changed_connection = value.href->changedSignal().connect(sigc::bind(sigc::ptr_fun(sp_style_stroke_paint_server_ref_changed), style)); + style->stroke_ps_changed_connection = href->changedSignal().connect(sigc::bind(sigc::ptr_fun(sp_style_stroke_paint_server_ref_changed), style)); } } @@ -1552,6 +1634,7 @@ SPIPaint::read( gchar const *str ) { } } + // Continue to parse after url for fallback colors. while ( g_ascii_isspace(*str) ) { ++str; } @@ -1560,14 +1643,14 @@ SPIPaint::read( gchar const *str ) { set = true; paintOrigin = SP_CSS_PAINT_ORIGIN_CURRENT_COLOR; if (style) { - setColor( style->color.value.color ); + setColor(style->color.getColor()); } else { // Normally an SPIPaint is part of an SPStyle and the value of 'color' is // available. SPIPaint can be used 'stand-alone' (e.g. to parse color values) in // which case a value of 'currentColor' is meaningless, thus we shouldn't reach // here. std::cerr << "SPIPaint::read(): value is 'currentColor' but 'color' not available." << std::endl; - setColor( 0 ); + setColor(Colors::Color(0x000000ff)); } } else if (streq(str, "context-fill")) { set = true; @@ -1578,30 +1661,21 @@ SPIPaint::read( gchar const *str ) { } else if (streq(str, "none")) { set = true; noneSet = true; - } else if (value.color.fromString(str)) { + } else if (auto color = canHaveCMS() ? getCMS().parse(str) : Colors::Color::parse(str)) { set = true; - colorSet = true; + _color = color; } } } -// Stand-alone read (Legacy read()), used multiple places, e.g. sp-stop.cpp -// This function should not be necessary. FIXME -void -SPIPaint::read( gchar const *str, SPStyle &style_in, SPDocument *document_in ) { - style = &style_in; - style->document = document_in; - read( str ); -} - const Glib::ustring SPIPaint::get_value() const { if (this->inherit) return Glib::ustring("inherit"); if (this->noneSet) return Glib::ustring("none"); // url must go first as other values can serve as fallbacks auto ret = Glib::ustring(""); - if (this->value.href && this->value.href->getURI()) { - ret += this->value.href->getURI()->cssStr(); + if (this->href && this->href->getURI()) { + ret += this->href->getURI()->cssStr(); } switch(this->paintOrigin) { case SP_CSS_PAINT_ORIGIN_CURRENT_COLOR: @@ -1617,15 +1691,29 @@ const Glib::ustring SPIPaint::get_value() const ret += "context-stroke"; break; case SP_CSS_PAINT_ORIGIN_NORMAL: - if (this->colorSet) { + if (_color) { if (!ret.empty()) ret += " "; - ret += value.color.toString(); + ret += _color->toString(); } break; } return ret; } +void SPIPaint::setColor(Colors::Color const &other) +{ + _color = other; + set = true; +} + +Colors::Color const &SPIPaint::getColor() const +{ + static auto default_color = Colors::Color(0x0); // Transparent RGB black + if (_color) + return *_color; + return default_color; +} + void SPIPaint::clear() { // std::cout << "SPIPaint::clear(): " << name << std::endl; @@ -1638,15 +1726,13 @@ SPIPaint::reset( bool init ) { // std::cout << "SPIPaint::reset(): " << name << " " << init << std::endl; SPIBase::clear(); paintOrigin = SP_CSS_PAINT_ORIGIN_NORMAL; - colorSet = false; noneSet = false; - value.color.set(0x0); - value.color.unsetColorProfile(); + _color.reset(); tag = nullptr; - value.href.reset(); + href.reset(); if (init && id() == SPAttr::FILL) { - setColor(0.0, 0.0, 0.0); // 'black' is default for 'fill' + _color = Inkscape::Colors::Color(0x000000ff); // 'black' is default for 'fill' } } @@ -1660,19 +1746,19 @@ SPIPaint::cascade( const SPIBase* const parent ) { reset( false ); // Do not init if( p->isPaintserver() ) { - if( p->value.href) { + if( p->href) { // Why can we use p->document ? - sp_style_set_ipaint_to_uri( style, this, p->value.href->getURI(), p->value.href->getOwnerDocument()); + sp_style_set_ipaint_to_uri( style, this, p->href->getURI(), p->href->getOwnerDocument()); } else { std::cerr << "SPIPaint::cascade: Expected paint server not found." << std::endl; } } else if( p->isColor() ) { - setColor( p->value.color ); + _color = p->_color; } else if( p->isNoneSet() ) { noneSet = true; } else if( p->paintOrigin == SP_CSS_PAINT_ORIGIN_CURRENT_COLOR ) { paintOrigin = SP_CSS_PAINT_ORIGIN_CURRENT_COLOR; - setColor( style->color.value.color ); + _color = style->color.getColor(); } else if( isNone() ) { // } else { @@ -1681,7 +1767,7 @@ SPIPaint::cascade( const SPIBase* const parent ) { } else { if( paintOrigin == SP_CSS_PAINT_ORIGIN_CURRENT_COLOR ) { // Update in case color value changed. - setColor( style->color.value.color ); + _color = style->color.getColor(); } } @@ -1715,17 +1801,14 @@ SPIPaint::equals(const SPIBase& rhs) const { } if ( this->isPaintserver() ) { - if( this->value.href == nullptr || r->value.href == nullptr || - this->value.href->getObject() != r->value.href->getObject() ) { + if( this->href == nullptr || r->href == nullptr || + this->href->getObject() != r->href->getObject() ) { return false; } } - if ( this->isColor() ) { - // ICC handled by SPColor== - if (value.color != r->value.color) { - return false; - } + if (this->isColor() && *_color != *r->_color) { + return false; } return SPIBase::equals(rhs); @@ -1957,7 +2040,7 @@ SPIFilter::read( gchar const *str ) { const Glib::ustring SPIFilter::get_value() const { if (this->inherit) return Glib::ustring("inherit"); - if (this->href) return this->href->getURI()->cssStr(); + if (this->href && this->href->getURI()) return this->href->getURI()->cssStr(); return Glib::ustring(""); } @@ -2003,11 +2086,11 @@ SPIFilter::merge( const SPIBase* const parent ) { } } else { // If we don't have an href, create it - if( style->document ) { // FIXME + if (style->object) { + href = new SPFilterReference(style->object); + } else if (style->document) { // FIXME href = new SPFilterReference(style->document); //href->changedSignal().connect(sigc::bind(sigc::ptr_fun(sp_style_filter_ref_changed), style)); - } else if (style->object) { - href = new SPFilterReference(style->object); } } if( href ) { diff --git a/src/style-internal.h b/src/style-internal.h index 6c26873280..4574e2b55b 100644 --- a/src/style-internal.h +++ b/src/style-internal.h @@ -25,8 +25,6 @@ #include "attributes.h" #include "style-enums.h" -#include "color.h" - #include "object/sp-marker-loc.h" #include "object/sp-filter.h" #include "object/sp-filter-reference.h" @@ -35,14 +33,21 @@ #include "object/uri.h" -#include "svg/svg-icc-color.h" +// TODO: Remove include when we figure out how to store Color without the full class +#include "colors/color.h" #include "xml/repr.h" namespace Inkscape { class ObjectSet; +namespace Colors { +class Color; +class DocumentCMS; +} }; +using namespace Inkscape; + static const unsigned SP_STYLE_FLAG_ALWAYS (1 << 2); static const unsigned SP_STYLE_FLAG_IFSET (1 << 0); static const unsigned SP_STYLE_FLAG_IFDIFF (1 << 1); @@ -307,12 +312,18 @@ public: ~SPIScale24() override = default; + operator double() const { return SP_SCALE24_TO_FLOAT(value); } + void read( gchar const *str ) override; const Glib::ustring get_value() const override; void clear() override { SPIBase::clear(); value = get_default(); } + void set_double(const double& other) { + value = SP_SCALE24_FROM_FLOAT(other); + set = true; + } void cascade( const SPIBase* const parent ) override; void merge( const SPIBase* const parent ) override; @@ -321,7 +332,6 @@ public: bool equals(const SPIBase& rhs) const override; - // To do: make private public: unsigned value : 24; @@ -661,58 +671,39 @@ public: bool containsAnyShape(Inkscape::ObjectSet *set); }; -/// Color type internal to SPStyle, FIXME Add string value to store SVG named color. class SPIColor : public SPIBase { public: - SPIColor(bool inherits = true) - : SPIBase(inherits) - , currentcolor(false) - { - value.color.set(0); - } + SPIColor(bool inherits = true); + ~SPIColor() override = default; - ~SPIColor() override - = default; + void read(gchar const *str) override; + void read(gchar const *str, SPStyle &style); - void read( gchar const *str ) override; const Glib::ustring get_value() const override; void clear() override { SPIBase::clear(); - value.color.set(0); + _color.reset(); } void cascade( const SPIBase* const parent ) override; void merge( const SPIBase* const parent ) override; - SPIColor& operator=(const SPIColor& rhs) { - SPIBase::operator=(rhs); - currentcolor = rhs.currentcolor; - value.color = rhs.value.color; - return *this; - } + SPIColor& operator=(const SPIColor& rhs); + SPIColor& operator=(const Colors::Color &rhs); bool equals(const SPIBase& rhs) const override; - void setColor( float r, float g, float b ) { - value.color.set( r, g, b ); - } - - void setColor( guint32 val ) { - value.color.set( val ); - } - - void setColor( SPColor const& color ) { - value.color = color; - } + void setColor(Colors::Color const &other); + Colors::Color const &getColor() const; + bool canHaveCMS() const; + Colors::DocumentCMS const &getCMS() const; public: - bool currentcolor : 1; - // FIXME: remove structure and derive SPIPaint from this class. - struct { - SPColor color; - } value; + bool currentcolor = false; +private: + std::optional _color; }; @@ -734,11 +725,9 @@ class SPIPaint : public SPIBase { public: - SPIPaint() { clear(); } - + SPIPaint(); ~SPIPaint() override = default; - void read( gchar const *str ) override; - virtual void read( gchar const *str, SPStyle &style, SPDocument *document = nullptr); + void read(gchar const *str) override; const Glib::ustring get_value() const override; void clear() override; virtual void reset( bool init ); // Used internally when reading or cascading @@ -747,18 +736,17 @@ public: SPIPaint& operator=(const SPIPaint& rhs) { SPIBase::operator=(rhs); - paintOrigin = rhs.paintOrigin; - colorSet = rhs.colorSet; - noneSet = rhs.noneSet; - value.color = rhs.value.color; - value.href = rhs.value.href; + paintOrigin = rhs.paintOrigin; + noneSet = rhs.noneSet; + _color = rhs._color; + href = rhs.href; return *this; } bool equals(const SPIBase& rhs) const override; bool isSameType( SPIPaint const & other ) const { - return (isPaintserver() == other.isPaintserver()) && (colorSet == other.colorSet) && (paintOrigin == other.paintOrigin); + return (isPaintserver() == other.isPaintserver()) && (isColor() == other.isColor()) && (paintOrigin == other.paintOrigin); } bool isNoneSet() const { @@ -766,44 +754,38 @@ public: } bool isNone() const { - return !colorSet && !isPaintserver() && (paintOrigin == SP_CSS_PAINT_ORIGIN_NORMAL); - } // TODO refine + return !_color && !isPaintserver() && (paintOrigin == SP_CSS_PAINT_ORIGIN_NORMAL); + } bool isColor() const { - return colorSet && !isPaintserver(); + return _color && !isPaintserver(); } bool isPaintserver() const { - return value.href && value.href->getObject() != nullptr; + return href && href->getObject() != nullptr; } - void setColor( float r, float g, float b ) { - value.color.set( r, g, b ); colorSet = true; + void setNone() { + noneSet = true; + _color.reset(); } - void setColor( guint32 val ) { - value.color.set( val ); colorSet = true; - } - - void setColor( SPColor const& color ) { - value.color = color; colorSet = true; - } - - void setNone() {noneSet = true; colorSet=false;} - void setTag(SPObject* tag) { this->tag = tag; } SPObject* getTag() { return tag; } + void setColor(Colors::Color const &other); + Colors::Color const &getColor() const; + + bool canHaveCMS() const; + Colors::DocumentCMS const &getCMS() const; // To do: make private public: SPPaintOrigin paintOrigin : 2; - bool colorSet : 1; bool noneSet : 1; - struct { - std::shared_ptr href; - SPColor color; - } value; + std::shared_ptr href; SPObject *tag = nullptr; +private: + std::optional _color; }; diff --git a/src/style.cpp b/src/style.cpp index bc37f4d73c..99f3697943 100644 --- a/src/style.cpp +++ b/src/style.cpp @@ -31,11 +31,11 @@ #include "attributes.h" #include "bad-uri-exception.h" +#include "colors/manager.h" #include "document.h" #include "preferences.h" #include "3rdparty/libcroco/src/cr-sel-eng.h" - #include "object/sp-paint-server.h" #include "object/uri.h" @@ -400,7 +400,7 @@ SPStyle::SPStyle(SPDocument *document_in, SPObject *object_in) : // std::cout << " SPIPaint: " << sizeof(SPIPaint) << std::endl; // std::cout << " SPITextDecorationLine" << sizeof(SPITextDecorationLine) << std::endl; // std::cout << " Glib::ustring:" << sizeof(Glib::ustring) << std::endl; - // std::cout << " SPColor: " << sizeof(SPColor) << std::endl; + // std::cout << " Color: " << sizeof(Color) << std::endl; // first = false; // } @@ -476,11 +476,11 @@ SPStyle::~SPStyle() { filter_changed_connection.disconnect(); // The following should be moved into SPIPaint and SPIFilter - if (fill.value.href) { + if (fill.href) { fill_ps_modified_connection.disconnect(); } - if (stroke.value.href) { + if (stroke.href) { stroke_ps_modified_connection.disconnect(); } @@ -525,11 +525,11 @@ SPStyle::clear() { filter.href = new SPFilterReference(document); filter_changed_connection = filter.href->changedSignal().connect(sigc::bind(sigc::ptr_fun(sp_style_filter_ref_changed), this)); - fill.value.href = std::make_shared(document); - fill_ps_changed_connection = fill.value.href->changedSignal().connect(sigc::bind(sigc::ptr_fun(sp_style_fill_paint_server_ref_changed), this)); + fill.href = std::make_shared(document); + fill_ps_changed_connection = fill.href->changedSignal().connect(sigc::bind(sigc::ptr_fun(sp_style_fill_paint_server_ref_changed), this)); - stroke.value.href = std::make_shared(document); - stroke_ps_changed_connection = stroke.value.href->changedSignal().connect(sigc::bind(sigc::ptr_fun(sp_style_stroke_paint_server_ref_changed), this)); + stroke.href = std::make_shared(document); + stroke_ps_changed_connection = stroke.href->changedSignal().connect(sigc::bind(sigc::ptr_fun(sp_style_stroke_paint_server_ref_changed), this)); } cloned = false; @@ -555,6 +555,10 @@ SPStyle::read( SPObject *object, Inkscape::XML::Node *repr ) { clear(); // FIXME, If this isn't here, EVERYTHING stops working! Why? + if (!document && object) { + document = object->document; + } + if (object && object->cloned) { cloned = true; } @@ -588,7 +592,7 @@ SPStyle::read( SPObject *object, Inkscape::XML::Node *repr ) { } } else if( repr->parent() ) { // When does this happen? // std::cout << "SPStyle::read(): reading via repr->parent()" << std::endl; - SPStyle *parent = new SPStyle(); + SPStyle *parent = new SPStyle(document); parent->read( nullptr, repr->parent() ); cascade( parent ); delete parent; @@ -1246,36 +1250,36 @@ sp_repr_sel_eng() void sp_style_set_ipaint_to_uri(SPStyle *style, SPIPaint *paint, const Inkscape::URI *uri, SPDocument *document) { - if (!paint->value.href) { + if (!paint->href) { if (style->object) { // Should not happen as href should have been created in SPIPaint. (TODO: Removed code duplication.) - paint->value.href = std::make_shared(style->object); + paint->href = std::make_shared(style->object); } else if (document || style->document) { // Used by desktop style (no object to attach to!). - paint->value.href = std::make_shared(document ? document : style->document); + paint->href = std::make_shared(document ? document : style->document); } else { std::cerr << "sp_style_set_ipaint_to_uri: No valid object or document!" << std::endl; return; } if (paint == &style->fill) { - style->fill_ps_changed_connection = paint->value.href->changedSignal().connect(sigc::bind(sigc::ptr_fun(sp_style_fill_paint_server_ref_changed), style)); + style->fill_ps_changed_connection = paint->href->changedSignal().connect(sigc::bind(sigc::ptr_fun(sp_style_fill_paint_server_ref_changed), style)); } else { - style->stroke_ps_changed_connection = paint->value.href->changedSignal().connect(sigc::bind(sigc::ptr_fun(sp_style_stroke_paint_server_ref_changed), style)); + style->stroke_ps_changed_connection = paint->href->changedSignal().connect(sigc::bind(sigc::ptr_fun(sp_style_stroke_paint_server_ref_changed), style)); } } - if (paint->value.href){ - if (paint->value.href->getObject()){ - paint->value.href->detach(); + if (paint->href){ + if (paint->href->getObject()){ + paint->href->detach(); } try { - paint->value.href->attach(*uri); + paint->href->attach(*uri); } catch (Inkscape::BadURIException &e) { g_warning("%s", e.what()); - paint->value.href->detach(); + paint->href->detach(); } } } diff --git a/src/style.h b/src/style.h index d94d8c9e70..0746b748b1 100644 --- a/src/style.h +++ b/src/style.h @@ -336,13 +336,13 @@ public: SPFilter const *getFilter() const { return (filter.href) ? filter.href->getObject() : nullptr; } Inkscape::URI const *getFilterURI() const { return (filter.href) ? filter.href->getURI() : nullptr; } - SPPaintServer *getFillPaintServer() { return (fill.value.href) ? fill.value.href->getObject() : nullptr; } - SPPaintServer const *getFillPaintServer() const { return (fill.value.href) ? fill.value.href->getObject() : nullptr; } - Inkscape::URI const *getFillURI() const { return (fill.value.href) ? fill.value.href->getURI() : nullptr; } + SPPaintServer *getFillPaintServer() { return (fill.href) ? fill.href->getObject() : nullptr; } + SPPaintServer const *getFillPaintServer() const { return (fill.href) ? fill.href->getObject() : nullptr; } + Inkscape::URI const *getFillURI() const { return (fill.href) ? fill.href->getURI() : nullptr; } - SPPaintServer *getStrokePaintServer() { return (stroke.value.href) ? stroke.value.href->getObject() : nullptr; } - SPPaintServer const *getStrokePaintServer() const { return (stroke.value.href) ? stroke.value.href->getObject() : nullptr; } - Inkscape::URI const *getStrokeURI() const { return (stroke.value.href) ? stroke.value.href->getURI() : nullptr; } + SPPaintServer *getStrokePaintServer() { return (stroke.href) ? stroke.href->getObject() : nullptr; } + SPPaintServer const *getStrokePaintServer() const { return (stroke.href) ? stroke.href->getObject() : nullptr; } + Inkscape::URI const *getStrokeURI() const { return (stroke.href) ? stroke.href->getURI() : nullptr; } /** * Return a font feature string useful for Pango. diff --git a/src/svg/CMakeLists.txt b/src/svg/CMakeLists.txt index 6a66a6291a..335eeffcab 100644 --- a/src/svg/CMakeLists.txt +++ b/src/svg/CMakeLists.txt @@ -9,7 +9,6 @@ set(svg_SRC svg-affine.cpp svg-affine-parser.cpp svg-box.cpp - svg-color.cpp svg-angle.cpp svg-length.cpp svg-bool.cpp @@ -23,8 +22,6 @@ set(svg_SRC stringstream.h strip-trailing-zeros.h svg-box.h - svg-color.h - svg-icc-color.h svg-angle.h svg-length.h svg-bool.h diff --git a/src/svg/svg-color.cpp b/src/svg/svg-color.cpp deleted file mode 100644 index 66ae587c12..0000000000 --- a/src/svg/svg-color.cpp +++ /dev/null @@ -1,669 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/** - * \file - * Reading \& writing of SVG/CSS colors. - */ -/* - * Authors: - * Lauris Kaplinski - * - * Copyright (C) 1999-2002 Lauris Kaplinski - * - * Released under GNU GPL v2+, read the file 'COPYING' for more information. - */ - -#ifdef HAVE_CONFIG_H -# include "config.h" // only include where actually required! -#endif - -#include "svg-color.h" - -#include -#include // sprintf -#include -#include -#include -#include -#include -#include - -#include // g_assert - -#include "color.h" -#include "colorspace.h" -#include "document.h" -#include "inkscape.h" -#include "preferences.h" -#include "profile-manager.h" -#include "strneq.h" -#include "svg-icc-color.h" - -#include "color/cms-system.h" -#include "object/color-profile.h" - -struct SPSVGColor { - unsigned long rgb; - const std::string name; -}; - -/* - * These are the colors defined in the SVG standard - */ -static SPSVGColor const sp_svg_color_named[] = { - { 0xF0F8FF, "aliceblue" }, - { 0xFAEBD7, "antiquewhite" }, - { 0x00FFFF, "aqua" }, - { 0x7FFFD4, "aquamarine" }, - { 0xF0FFFF, "azure" }, - { 0xF5F5DC, "beige" }, - { 0xFFE4C4, "bisque" }, - { 0x000000, "black" }, - { 0xFFEBCD, "blanchedalmond" }, - { 0x0000FF, "blue" }, - { 0x8A2BE2, "blueviolet" }, - { 0xA52A2A, "brown" }, - { 0xDEB887, "burlywood" }, - { 0x5F9EA0, "cadetblue" }, - { 0x7FFF00, "chartreuse" }, - { 0xD2691E, "chocolate" }, - { 0xFF7F50, "coral" }, - { 0x6495ED, "cornflowerblue" }, - { 0xFFF8DC, "cornsilk" }, - { 0xDC143C, "crimson" }, - { 0x00FFFF, "cyan" }, - { 0x00008B, "darkblue" }, - { 0x008B8B, "darkcyan" }, - { 0xB8860B, "darkgoldenrod" }, - { 0xA9A9A9, "darkgray" }, - { 0x006400, "darkgreen" }, - { 0xA9A9A9, "darkgrey" }, - { 0xBDB76B, "darkkhaki" }, - { 0x8B008B, "darkmagenta" }, - { 0x556B2F, "darkolivegreen" }, - { 0xFF8C00, "darkorange" }, - { 0x9932CC, "darkorchid" }, - { 0x8B0000, "darkred" }, - { 0xE9967A, "darksalmon" }, - { 0x8FBC8F, "darkseagreen" }, - { 0x483D8B, "darkslateblue" }, - { 0x2F4F4F, "darkslategray" }, - { 0x2F4F4F, "darkslategrey" }, - { 0x00CED1, "darkturquoise" }, - { 0x9400D3, "darkviolet" }, - { 0xFF1493, "deeppink" }, - { 0x00BFFF, "deepskyblue" }, - { 0x696969, "dimgray" }, - { 0x696969, "dimgrey" }, - { 0x1E90FF, "dodgerblue" }, - { 0xB22222, "firebrick" }, - { 0xFFFAF0, "floralwhite" }, - { 0x228B22, "forestgreen" }, - { 0xFF00FF, "fuchsia" }, - { 0xDCDCDC, "gainsboro" }, - { 0xF8F8FF, "ghostwhite" }, - { 0xFFD700, "gold" }, - { 0xDAA520, "goldenrod" }, - { 0x808080, "gray" }, - { 0x808080, "grey" }, - { 0x008000, "green" }, - { 0xADFF2F, "greenyellow" }, - { 0xF0FFF0, "honeydew" }, - { 0xFF69B4, "hotpink" }, - { 0xCD5C5C, "indianred" }, - { 0x4B0082, "indigo" }, - { 0xFFFFF0, "ivory" }, - { 0xF0E68C, "khaki" }, - { 0xE6E6FA, "lavender" }, - { 0xFFF0F5, "lavenderblush" }, - { 0x7CFC00, "lawngreen" }, - { 0xFFFACD, "lemonchiffon" }, - { 0xADD8E6, "lightblue" }, - { 0xF08080, "lightcoral" }, - { 0xE0FFFF, "lightcyan" }, - { 0xFAFAD2, "lightgoldenrodyellow" }, - { 0xD3D3D3, "lightgray" }, - { 0x90EE90, "lightgreen" }, - { 0xD3D3D3, "lightgrey" }, - { 0xFFB6C1, "lightpink" }, - { 0xFFA07A, "lightsalmon" }, - { 0x20B2AA, "lightseagreen" }, - { 0x87CEFA, "lightskyblue" }, - { 0x778899, "lightslategray" }, - { 0x778899, "lightslategrey" }, - { 0xB0C4DE, "lightsteelblue" }, - { 0xFFFFE0, "lightyellow" }, - { 0x00FF00, "lime" }, - { 0x32CD32, "limegreen" }, - { 0xFAF0E6, "linen" }, - { 0xFF00FF, "magenta" }, - { 0x800000, "maroon" }, - { 0x66CDAA, "mediumaquamarine" }, - { 0x0000CD, "mediumblue" }, - { 0xBA55D3, "mediumorchid" }, - { 0x9370DB, "mediumpurple" }, - { 0x3CB371, "mediumseagreen" }, - { 0x7B68EE, "mediumslateblue" }, - { 0x00FA9A, "mediumspringgreen" }, - { 0x48D1CC, "mediumturquoise" }, - { 0xC71585, "mediumvioletred" }, - { 0x191970, "midnightblue" }, - { 0xF5FFFA, "mintcream" }, - { 0xFFE4E1, "mistyrose" }, - { 0xFFE4B5, "moccasin" }, - { 0xFFDEAD, "navajowhite" }, - { 0x000080, "navy" }, - { 0xFDF5E6, "oldlace" }, - { 0x808000, "olive" }, - { 0x6B8E23, "olivedrab" }, - { 0xFFA500, "orange" }, - { 0xFF4500, "orangered" }, - { 0xDA70D6, "orchid" }, - { 0xEEE8AA, "palegoldenrod" }, - { 0x98FB98, "palegreen" }, - { 0xAFEEEE, "paleturquoise" }, - { 0xDB7093, "palevioletred" }, - { 0xFFEFD5, "papayawhip" }, - { 0xFFDAB9, "peachpuff" }, - { 0xCD853F, "peru" }, - { 0xFFC0CB, "pink" }, - { 0xDDA0DD, "plum" }, - { 0xB0E0E6, "powderblue" }, - { 0x800080, "purple" }, - { 0x663399, "rebeccapurple" }, - { 0xFF0000, "red" }, - { 0xBC8F8F, "rosybrown" }, - { 0x4169E1, "royalblue" }, - { 0x8B4513, "saddlebrown" }, - { 0xFA8072, "salmon" }, - { 0xF4A460, "sandybrown" }, - { 0x2E8B57, "seagreen" }, - { 0xFFF5EE, "seashell" }, - { 0xA0522D, "sienna" }, - { 0xC0C0C0, "silver" }, - { 0x87CEEB, "skyblue" }, - { 0x6A5ACD, "slateblue" }, - { 0x708090, "slategray" }, - { 0x708090, "slategrey" }, - { 0xFFFAFA, "snow" }, - { 0x00FF7F, "springgreen" }, - { 0x4682B4, "steelblue" }, - { 0xD2B48C, "tan" }, - { 0x008080, "teal" }, - { 0xD8BFD8, "thistle" }, - { 0xFF6347, "tomato" }, - { 0x40E0D0, "turquoise" }, - { 0xEE82EE, "violet" }, - { 0xF5DEB3, "wheat" }, - { 0xFFFFFF, "white" }, - { 0xF5F5F5, "whitesmoke" }, - { 0xFFFF00, "yellow" }, - { 0x9ACD32, "yellowgreen" } -}; - -static std::map sp_svg_create_color_hash(); - -guint32 sp_svg_read_color(gchar const *str, guint32 const dfl) -{ - return sp_svg_read_color(str, nullptr, dfl); -} - -static guint32 internal_sp_svg_read_color(gchar const *str, gchar const **end_ptr, guint32 def) -{ - static std::map colors; - guint32 val = 0; - - if (str == nullptr) return def; - while ((*str <= ' ') && *str) str++; - if (!*str) return def; - - if (str[0] == '#') { - gint i; - for (i = 1; str[i]; i++) { - int hexval; - if (str[i] >= '0' && str[i] <= '9') - hexval = str[i] - '0'; - else if (str[i] >= 'A' && str[i] <= 'F') - hexval = str[i] - 'A' + 10; - else if (str[i] >= 'a' && str[i] <= 'f') - hexval = str[i] - 'a' + 10; - else - break; - val = (val << 4) + hexval; - } - /* handle #rgb case */ - if (i == 1 + 3) { - val = ((val & 0xf00) << 8) | - ((val & 0x0f0) << 4) | - (val & 0x00f); - val |= val << 4; - } else if (i != 1 + 6) { - /* must be either 3 or 6 digits. */ - return def; - } - if (end_ptr) { - *end_ptr = str + i; - } - } else if (strneq(str, "rgb(", 4)) { - bool hasp, hasd; - gchar *s, *e; - gdouble r, g, b; - - s = (gchar *) str + 4; - hasp = false; - hasd = false; - - r = g_ascii_strtod(s, &e); - if (s == e) return def; - s = e; - if (*s == '%') { - hasp = true; - s += 1; - } else { - hasd = true; - } - while (*s && g_ascii_isspace(*s)) s += 1; - if (*s != ',') return def; - s += 1; - while (*s && g_ascii_isspace(*s)) s += 1; - g = g_ascii_strtod(s, &e); - if (s == e) return def; - s = e; - if (*s == '%') { - hasp = true; - s += 1; - } else { - hasd = true; - } - while (*s && g_ascii_isspace(*s)) s += 1; - if (*s != ',') return def; - s += 1; - while (*s && g_ascii_isspace(*s)) s += 1; - b = g_ascii_strtod(s, &e); - if (s == e) return def; - s = e; - if (*s == '%') { - hasp = true; - s += 1; - } else { - hasd = true; - } - while(*s && g_ascii_isspace(*s)) s += 1; - if (*s != ')') { - return def; - } - ++s; - if (hasp && hasd) return def; - if (hasp) { - val = static_cast(floor(CLAMP(r, 0.0, 100.0) * 2.559999)) << 24; - val |= (static_cast(floor(CLAMP(g, 0.0, 100.0) * 2.559999)) << 16); - val |= (static_cast(floor(CLAMP(b, 0.0, 100.0) * 2.559999)) << 8); - } else { - val = static_cast(CLAMP(r, 0, 255)) << 24; - val |= (static_cast(CLAMP(g, 0, 255)) << 16); - val |= (static_cast(CLAMP(b, 0, 255)) << 8); - } - if (end_ptr) { - *end_ptr = s; - } - return val; - } else if (strneq(str, "hsl(", 4)) { - - gchar *ptr = (gchar *) str + 4; - - gchar *e; // ptr after read - - double h = g_ascii_strtod(ptr, &e); // Read h (0-360) - if (ptr == e) return def; // Read failed - ptr = e; - - while (*ptr && g_ascii_isspace(*ptr)) ptr += 1; // Remove any white space - if (*ptr != ',') return def; // Need comma - ptr += 1; - while (*ptr && g_ascii_isspace(*ptr)) ptr += 1; // Remove any white space - - double s = g_ascii_strtod(ptr, &e); // Read s (percent) - if (ptr == e) return def; // Read failed - ptr = e; - while (*ptr && g_ascii_isspace(*ptr)) ptr += 1; // Remove any white space - if (*ptr != '%') return def; // Need % - ptr += 1; - - while (*ptr && g_ascii_isspace(*ptr)) ptr += 1; // Remove any white space - if (*ptr != ',') return def; // Need comma - ptr += 1; - while (*ptr && g_ascii_isspace(*ptr)) ptr += 1; // Remove any white space - - double l = g_ascii_strtod(ptr, &e); // Read l (percent) - if (ptr == e) return def; // Read failed - ptr = e; - while (*ptr && g_ascii_isspace(*ptr)) ptr += 1; // Remove any white space - if (*ptr != '%') return def; // Need % - ptr += 1; - - if (end_ptr) { - *end_ptr = ptr; - } - - - // Normalize to 0..1 - h /= 360.0; - s /= 100.0; - l /= 100.0; - - gfloat rgb[3]; - - SPColor::hsl_to_rgb_floatv( rgb, h, s, l ); - - val = static_cast(floor(CLAMP(rgb[0], 0.0, 1.0) * 255.9999)) << 24; - val |= (static_cast(floor(CLAMP(rgb[1], 0.0, 1.0) * 255.9999)) << 16); - val |= (static_cast(floor(CLAMP(rgb[2], 0.0, 1.0) * 255.9999)) << 8); - return val; - - } else { - gint i; - if (colors.empty()) { - colors = sp_svg_create_color_hash(); - } - gchar c[32]; - for (i = 0; i < 31; i++) { - if (str[i] == ';' || g_ascii_isspace(str[i])) { - c[i] = '\0'; - break; - } - c[i] = g_ascii_tolower(str[i]); - if (!str[i]) break; - } - c[31] = '\0'; - - if (colors.count(std::string(c))) { - val = colors[std::string(c)]; - } - else { - return def; - } - if (end_ptr) { - *end_ptr = str + i; - } - } - - return (val << 8); -} - -guint32 sp_svg_read_color(gchar const *str, gchar const **end_ptr, guint32 dfl) -{ - /* I've been rather hurried in editing the above to add support for end_ptr, so I'm adding - * this check wrapper. */ - gchar const *end = str; - guint32 const ret = internal_sp_svg_read_color(str, &end, dfl); - g_assert(((ret == dfl) && (end == str)) - || (((ret & 0xff) == 0) - && (str < end))); - if (str < end) { - gchar *buf = (gchar *) g_malloc(end + 1 - str); - memcpy(buf, str, end - str); - buf[end - str] = '\0'; - gchar const *buf_end = buf; - guint32 const check = internal_sp_svg_read_color(buf, &buf_end, 1); - g_assert(check == ret - && buf_end - buf == end - str); - g_free(buf); - - if ( end_ptr ) { - *end_ptr = end; - } - } - return ret; -} - - -/** - * Converts an RGB colour expressed in form 0x00rrggbb to a CSS/SVG representation of that colour. - * The result is valid even in SVG Tiny or non-SVG CSS. - */ -static void rgb24_to_css(char *const buf, size_t buflen, unsigned const rgb24) -{ - g_assert(rgb24 < (1u << 24)); - - /* SVG 1.1 Full allows additional colour names not supported by SVG Tiny, but we don't bother - * with them: it's good for these colours to be copyable to non-SVG CSS stylesheets and for - * documents to be more viewable in SVG Tiny/Basic viewers; and some of the SVG Full names are - * less meaningful than hex equivalents anyway. And it's easier for a person to map from the - * restricted set because the only component values are {00,80,ff}, other than silver 0xc0c0c0. - */ - - char const *src = nullptr; - switch (rgb24) { - /* Extracted mechanically from the table at - * http://www.w3.org/TR/REC-html40/types.html#h-6.5 .*/ - case 0x000000: src = "black"; break; - case 0xc0c0c0: src = "silver"; break; - case 0x808080: src = "gray"; break; - case 0xffffff: src = "white"; break; - case 0x800000: src = "maroon"; break; - case 0xff0000: src = "red"; break; - case 0x800080: src = "purple"; break; - case 0xff00ff: src = "fuchsia"; break; - case 0x008000: src = "green"; break; - case 0x00ff00: src = "lime"; break; - case 0x808000: src = "olive"; break; - case 0xffff00: src = "yellow"; break; - case 0x000080: src = "navy"; break; - case 0x0000ff: src = "blue"; break; - case 0x008080: src = "teal"; break; - case 0x00ffff: src = "aqua"; break; - - default: { - if ((rgb24 & 0xf0f0f) * 0x11 == rgb24) { - /* Can use the shorter three-digit form #rgb instead of #rrggbb. */ - std::snprintf(buf, buflen, "#%x%x%x", - (rgb24 >> 16) & 0xf, - (rgb24 >> 8) & 0xf, - rgb24 & 0xf); - } else { - std::snprintf(buf, buflen, "#%06x", rgb24); - } - break; - } - } - if (src) { - strcpy(buf, src); - } - - // assert(sp_svg_read_color(buf, 0xff) == (rgb24 << 8)); -} - -/** - * Converts an RGBA32 colour to a CSS/SVG representation of the RGB portion of that colour. The - * result is valid even in SVG Tiny or non-SVG CSS. - * - * \param rgba32 Colour expressed in form 0xrrggbbaa. - * \pre buflen \>= 8. - */ -void sp_svg_write_color(gchar *buf, unsigned const buflen, guint32 const rgba32) -{ - g_assert(8 <= buflen); - - Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - unsigned const rgb24 = rgba32 >> 8; - if ( prefs->getBool("/options/svgoutput/usenamedcolors") && - !prefs->getBool("/options/svgoutput/disable_optimizations" )) { - rgb24_to_css(buf, buflen, rgb24); - } else { - g_snprintf(buf, buflen, "#%06x", rgb24); - } -} - -static std::map -sp_svg_create_color_hash() -{ - std::map colors; - - for (const auto & i : sp_svg_color_named) { - colors[i.name] = i.rgb; - } - return colors; -} - - -void icc_color_to_sRGB(SVGICCColor const* icc, guchar* r, guchar* g, guchar* b) -{ - if (icc) { - g_message("profile name: %s", icc->colorProfile.c_str()); - if (auto prof = SP_ACTIVE_DOCUMENT->getProfileManager().find(icc->colorProfile.c_str())) { - guchar color_out[4] = {0,0,0,0}; - cmsHTRANSFORM trans = prof->getTransfToSRGB8(); - if ( trans ) { - std::vector comps = colorspace::getColorSpaceInfo( prof ); - - size_t count = prof->getChannelCount(); - size_t cap = std::min(count, comps.size()); - guchar color_in[4]; - for (size_t i = 0; i < cap; i++) { - color_in[i] = static_cast((((gdouble)icc->colors[i]) * 256.0) * (gdouble)comps[i].scale); - g_message("input[%d]: %d", (int)i, (int)color_in[i]); - } - - Inkscape::CMSSystem::do_transform( trans, color_in, color_out, 1 ); - g_message("transform to sRGB done"); - } - *r = color_out[0]; - *g = color_out[1]; - *b = color_out[2]; - } - } -} - -/* - * Some discussion at http://markmail.org/message/bhfvdfptt25kgtmj - * Allowed ASCII first characters: ':', 'A'-'Z', '_', 'a'-'z' - * Allowed ASCII remaining chars add: '-', '.', '0'-'9', - */ -bool sp_svg_read_icc_color( gchar const *str, gchar const **end_ptr, SVGICCColor* dest ) -{ - bool good = true; - - if ( end_ptr ) { - *end_ptr = str; - } - if ( dest ) { - dest->colorProfile.clear(); - dest->colors.clear(); - } - - if ( !str ) { - // invalid input - good = false; - } else { - while ( g_ascii_isspace(*str) ) { - str++; - } - - good = strneq( str, "icc-color(", 10 ); - - if ( good ) { - str += 10; - while ( g_ascii_isspace(*str) ) { - str++; - } - - if ( !g_ascii_isalpha(*str) - && ( !(0x080 & *str) ) - && (*str != '_') - && (*str != ':') ) { - // Name must start with a certain type of character - good = false; - } else { - while ( g_ascii_isdigit(*str) || g_ascii_isalpha(*str) - || (*str == '-') || (*str == ':') || (*str == '_') || (*str == '.') ) { - if ( dest ) { - dest->colorProfile += *str; - } - str++; - } - while ( g_ascii_isspace(*str) || *str == ',' ) { - str++; - } - } - } - - if ( good ) { - while ( *str && *str != ')' ) { - if ( g_ascii_isdigit(*str) || *str == '.' || *str == '-' || *str == '+') { - gchar* endPtr = nullptr; - gdouble dbl = g_ascii_strtod( str, &endPtr ); - if ( !errno ) { - if ( dest ) { - dest->colors.push_back( dbl ); - } - str = endPtr; - } else { - good = false; - break; - } - - while ( g_ascii_isspace(*str) || *str == ',' ) { - str++; - } - } else { - break; - } - } - } - - // We need to have ended on a closing parenthesis - if ( good ) { - while ( g_ascii_isspace(*str) ) { - str++; - } - good &= (*str == ')'); - } - } - - if ( good ) { - if ( end_ptr ) { - *end_ptr = str; - } - } else { - if ( dest ) { - dest->colorProfile.clear(); - dest->colors.clear(); - } - } - - return good; -} - - -bool sp_svg_read_icc_color( gchar const *str, SVGICCColor* dest ) -{ - return sp_svg_read_icc_color(str, nullptr, dest); -} - -/** - * Reading inkscape colors, for things like namedviews, guides, etc. - * Non-CSS / SVG specification formatted. Usually just a number. - */ -bool sp_ink_read_opacity(char const *str, guint32 *color, guint32 default_color) -{ - *color = (*color & 0xffffff00) | (default_color & 0xff); - if (!str) return false; - - gchar *check; - gdouble value = g_ascii_strtod(str, &check); - if (!check) return false; - - value = CLAMP(value, 0.0, 1.0); - *color = (*color & 0xffffff00) | (guint32) floor(value * 255.9999); - return 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/src/svg/svg-color.h b/src/svg/svg-color.h deleted file mode 100644 index b2c2f79737..0000000000 --- a/src/svg/svg-color.h +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/** @file - * TODO: insert short description here - *//* - * Authors: see git history - * - * Copyright (C) 2014 Authors - * Released under GNU GPL v2+, read the file 'COPYING' for more information. - */ -#ifndef SVG_SVG_COLOR_H_SEEN -#define SVG_SVG_COLOR_H_SEEN - -typedef unsigned int guint32; -struct SVGICCColor; - -guint32 sp_svg_read_color(char const *str, unsigned int dfl); -guint32 sp_svg_read_color(char const *str, char const **end_ptr, guint32 def); -void sp_svg_write_color(char *buf, unsigned int buflen, unsigned int rgba32); - -bool sp_svg_read_icc_color( char const *str, char const **end_ptr, SVGICCColor* dest ); -bool sp_svg_read_icc_color( char const *str, SVGICCColor* dest ); -void icc_color_to_sRGB(SVGICCColor const *dest, unsigned char *r, unsigned char *g, unsigned char *b); - -bool sp_ink_read_opacity(char const *str, guint32 *color, guint32 default_color); - -#endif /* !SVG_SVG_COLOR_H_SEEN */ diff --git a/src/trace/depixelize/inkscape-depixelize.cpp b/src/trace/depixelize/inkscape-depixelize.cpp index ae3c783a88..8b2daf9cb8 100644 --- a/src/trace/depixelize/inkscape-depixelize.cpp +++ b/src/trace/depixelize/inkscape-depixelize.cpp @@ -20,10 +20,9 @@ #include "inkscape-depixelize.h" -#include "color.h" +#include "colors/utils.h" #include "preferences.h" #include "async/progress.h" -#include "svg/svg-color.h" #include "svg/css-ostringstream.h" namespace Inkscape { @@ -65,17 +64,14 @@ TraceResult DepixelizeTracingEngine::trace(Glib::RefPtr const &pixb throttled.report_or_throw((double)i / num_splines); i++; - char b[64]; - sp_svg_write_color(b, sizeof(b), + auto hex = Inkscape::Colors::rgba_to_hex( SP_RGBA32_U_COMPOSE(unsigned(it.rgba[0]), unsigned(it.rgba[1]), unsigned(it.rgba[2]), unsigned(it.rgba[3]))); - Inkscape::CSSOStringStream osalpha; - osalpha << it.rgba[3] / 255.0f; - char *style = g_strdup_printf("fill:%s;fill-opacity:%s;", b, osalpha.str().c_str()); - res.emplace_back(style, std::move(it.pathVector)); - g_free(style); + Inkscape::CSSOStringStream ss; + ss << "fill:" << hex.c_str() << ";fill-opacity:" << (it.rgba[3] / 255.0f) << ";"; + res.emplace_back(ss.str(), std::move(it.pathVector)); } return res; diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt index a05fe57eb2..6210690b50 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -14,7 +14,6 @@ set(ui_SRC monitor.cpp pack.cpp popup-menu.cpp - selected-color.cpp shape-editor.cpp shape-editor-knotholders.cpp simple-pref-pusher.cpp @@ -192,7 +191,6 @@ set(ui_SRC widget/canvas-grid.cpp widget/canvas-notice.cpp widget/color-entry.cpp - widget/color-icc-selector.cpp widget/color-notebook.cpp widget/color-palette.cpp widget/color-palette-preview.cpp @@ -296,7 +294,6 @@ set(ui_SRC monitor.h pack.h popup-menu.h - selected-color.h shape-editor.h simple-pref-pusher.h shortcuts.h @@ -482,7 +479,6 @@ set(ui_SRC widget/canvas-notice.h widget/completion-popup.h widget/color-entry.h - widget/color-icc-selector.h widget/color-notebook.h widget/color-palette.h widget/color-palette-preview.h diff --git a/src/ui/builder-utils.cpp b/src/ui/builder-utils.cpp index a0620a9dde..21c24a19bd 100644 --- a/src/ui/builder-utils.cpp +++ b/src/ui/builder-utils.cpp @@ -45,6 +45,16 @@ Glib::RefPtr create_builder(const char* filename) { } } +bool hide_widget(const Glib::RefPtr &builder, std::string const &id) +{ + auto widget = builder->get_widget(id); + if (widget) { + widget->set_visible(false); + return true; + } + return false; +} + } } // namespace Inkscape::UI /* diff --git a/src/ui/builder-utils.h b/src/ui/builder-utils.h index 31bcbea411..dfe7875434 100644 --- a/src/ui/builder-utils.h +++ b/src/ui/builder-utils.h @@ -55,6 +55,8 @@ template Glib::RefPtr get_object(Glib::RefPtr& build return object; } +bool hide_widget(const Glib::RefPtr& builder, std::string const &id); + /** * This version of get_object is needed for Gtk::CellRenderer objects which can not be * put into Glib::RefPtr by the compiler, but are somehow passed to us as RefPtrs anyway. diff --git a/src/ui/clipboard.cpp b/src/ui/clipboard.cpp index 855ff4353e..87a801a3d5 100644 --- a/src/ui/clipboard.cpp +++ b/src/ui/clipboard.cpp @@ -34,6 +34,7 @@ // TODO: reduce header bloat if possible +#include "colors/manager.h" #include "context-fns.h" #include "desktop-style.h" // for sp_desktop_set_style, used in _pasteStyle #include "desktop.h" @@ -82,7 +83,6 @@ #include "object/sp-textpath.h" #include "object/sp-use.h" #include "svg/css-ostringstream.h" // used in copy -#include "svg/svg-color.h" #include "svg/svg.h" // for sp_svg_transform_write, used in _copySelection #include "text-chemistry.h" #include "ui/tool/control-point-selection.h" @@ -234,7 +234,7 @@ private: Glib::ustring _getBestTarget(SPDesktop *desktop = nullptr); void _registerSerializers(); void _setClipboardTargets(); - void _setClipboardColor(uint32_t); + void _setClipboardColor(Colors::Color const &color); void _userWarn(SPDesktop *, char const *); // private properties @@ -281,7 +281,7 @@ void ClipboardManagerImpl::copy(ObjectSet *set) if (auto const drag = desktop->getTool()->get_drag(); drag && drag->hasSelection()) { - guint32 col = drag->getColor(); + Color col = drag->getColor(); // set the color as clipboard content (text in RRGGBBAA format) _setClipboardColor(col); @@ -294,16 +294,8 @@ void ClipboardManagerImpl::copy(ObjectSet *set) } _text_style = sp_repr_css_attr_new(); // print and set properties - gchar color_str[16]; - g_snprintf(color_str, 16, "#%06x", col >> 8); - sp_repr_css_set_property(_text_style, "fill", color_str); - float opacity = SP_RGBA32_A_F(col); - if (opacity > 1.0) { - opacity = 1.0; // safeguard - } - Inkscape::CSSOStringStream opcss; - opcss << opacity; - sp_repr_css_set_property(_text_style, "opacity", opcss.str().data()); + sp_repr_css_set_property_string(_text_style, "fill", col.toString(false)); + sp_repr_css_set_property_double(_text_style, "opacity", col.getOpacity()); _discardInternalClipboard(); return; @@ -311,7 +303,7 @@ void ClipboardManagerImpl::copy(ObjectSet *set) // Special case for when the color picker ("dropper") is active - copies color under cursor if (auto const dt = dynamic_cast(desktop->getTool())) { - _setClipboardColor(dt->get_color(false, true)); + _setClipboardColor(*dt->get_color(false, true)); _discardInternalClipboard(); return; } @@ -1547,12 +1539,10 @@ bool ClipboardManagerImpl::_pasteText(SPDesktop *desktop) if (clip_text.length() < 30) { // Zero makes it impossible to paste a 100% transparent black, but it's useful. - auto const rgb0 = sp_svg_read_color(clip_text.c_str(), 0x0); - if (rgb0) { + if (auto color = Colors::Color::parse(clip_text)) { auto color_css = sp_repr_css_attr_new(); - sp_repr_css_set_property(color_css, "fill", SPColor(rgb0).toString().c_str()); - // In the future this could parse opacity, but sp_svg_read_color lacks this. - sp_repr_css_set_property(color_css, "fill-opacity", "1.0"); + sp_repr_css_set_property_string(color_css, "fill", color->toString(false)); + sp_repr_css_set_property_double(color_css, "fill-opacity", color->getOpacity()); sp_desktop_set_style(desktop, color_css); sp_repr_css_attr_unref(color_css); return true; @@ -1769,14 +1759,6 @@ void ClipboardManagerImpl::_onGet(char const *mime_type, Glib::RefPtr(area.height() + 0.5); // read from namedview - Inkscape::XML::Node *nv = _clipboardSPDoc->getReprNamedView(); - if (nv && nv->attribute("pagecolor")) { - bgcolor = sp_svg_read_color(nv->attribute("pagecolor"), 0xffffff00); - } - if (nv && nv->attribute("inkscape:pageopacity")) { - double opacity = nv->getAttributeDouble("inkscape:pageopacity", 1.0); - bgcolor |= SP_COLOR_F_TO_U(opacity); - } auto const raster_file = get_tmp_filename("inkscape-clipboard-export-raster"); sp_export_png_file(_clipboardSPDoc.get(), raster_file.c_str(), area, width, height, dpi, dpi, bgcolor, nullptr, nullptr, true, {}); (*out)->export_raster(_clipboardSPDoc.get(), raster_file.c_str(), filename.c_str(), true); @@ -2034,11 +2016,9 @@ void ClipboardManagerImpl::_setClipboardTargets() /** * Set the string representation of a 32-bit RGBA color as the clipboard contents. */ -void ClipboardManagerImpl::_setClipboardColor(uint32_t color) +void ClipboardManagerImpl::_setClipboardColor(Colors::Color const &color) { - char colorstr[16]; - g_snprintf(colorstr, 16, "%08x", color); - _clipboard->set_text(colorstr); + _clipboard->set_text(color.toString()); } /** diff --git a/src/ui/cursor-utils.cpp b/src/ui/cursor-utils.cpp index c2b65d0b56..c09d0baad0 100644 --- a/src/ui/cursor-utils.cpp +++ b/src/ui/cursor-utils.cpp @@ -44,7 +44,7 @@ using Inkscape::IO::Resource::ICONS; namespace Inkscape { // SVG cursor unique ID/key -typedef std::tuple Key; +typedef std::tuple Key; struct KeyHasher { std::size_t operator () (const Key& k) const { return boost::hash_value(k); } @@ -58,16 +58,16 @@ struct KeyHasher { Glib::RefPtr load_svg_cursor(Gtk::Widget &widget, std::string const &file_name, - std::uint32_t const fill, - std::uint32_t const stroke, - double fill_opacity, - double stroke_opacity) + std::optional maybe_fill, + std::optional maybe_stroke) { // GTK puts cursors in a "cursors" subdirectory of icon themes. We'll do the same... but // note that we cannot use the normal GTK method for loading cursors as GTK knows nothing // about scalable SVG cursors. We must locate and load the files ourselves. (Even if // GTK could handle scalable cursors, we would need to load the files ourselves inorder // to modify CSS 'fill' and 'stroke' properties.) + Colors::Color fill = maybe_fill.value_or(Colors::Color(0xffffffff)); + Colors::Color stroke = maybe_stroke.value_or(Colors::Color(0x000000ff)); Glib::RefPtr cursor; @@ -89,8 +89,8 @@ load_svg_cursor(Gtk::Widget &widget, theme_names.emplace_back("hicolor"); // quantize opacity to limit number of cursor variations we generate - fill_opacity = std::floor(std::clamp(fill_opacity, 0.0, 1.0) * 100) / 100; - stroke_opacity = std::floor(std::clamp(stroke_opacity, 0.0, 1.0) * 100) / 100; + fill.setOpacity(std::floor(std::clamp(fill.getOpacity(), 0.0, 1.0) * 100) / 100); + stroke.setOpacity(std::floor(std::clamp(stroke.getOpacity(), 0.0, 1.0) * 100) / 100); const auto enable_drop_shadow = prefs->getBool("/options/cursor-drop-shadow", true); @@ -109,7 +109,7 @@ load_svg_cursor(Gtk::Widget &widget, if (cache_enabled) { // construct a key cursor_key = std::tuple{theme_names[0], theme_names[1], file_name, - fill, stroke, fill_opacity, stroke_opacity, + fill.toRGBA(), stroke.toRGBA(), enable_drop_shadow, scale}; if (auto const it = cursor_cache.find(cursor_key); it != cursor_cache.end()) { return it->second; @@ -155,20 +155,10 @@ load_svg_cursor(Gtk::Widget &widget, // Set the CSS 'fill' and 'stroke' properties on the SVG element (for cascading). SPCSSAttr *css = sp_repr_css_attr(root->getRepr(), "style"); - - std::stringstream fill_stream; - fill_stream << "#" - << std::setfill ('0') << std::setw(6) - << std::hex << (fill >> 8); - std::stringstream stroke_stream; - stroke_stream << "#" - << std::setfill ('0') << std::setw(6) - << std::hex << (stroke >> 8); - - sp_repr_css_set_property(css, "fill", fill_stream.str().c_str()); - sp_repr_css_set_property(css, "stroke", stroke_stream.str().c_str()); - sp_repr_css_set_property_double(css, "fill-opacity", fill_opacity); - sp_repr_css_set_property_double(css, "stroke-opacity", stroke_opacity); + sp_repr_css_set_property_string(css, "fill", fill.toString(false)); + sp_repr_css_set_property_string(css, "stroke", stroke.toString(false)); + sp_repr_css_set_property_double(css, "fill-opacity", fill.getOpacity()); + sp_repr_css_set_property_double(css, "stroke-opacity", stroke.getOpacity()); root->changeCSS(css, "style"); sp_repr_css_attr_unref(css); @@ -235,12 +225,10 @@ load_svg_cursor(Gtk::Widget &widget, void set_svg_cursor(Gtk::Widget &widget, std::string const &file_name, - std::uint32_t const fill, - std::uint32_t const stroke, - double fill_opacity, - double stroke_opacity) + std::optional fill, + std::optional stroke) { - auto cursor = load_svg_cursor(widget, file_name, fill, stroke, fill_opacity, stroke_opacity); + auto cursor = load_svg_cursor(widget, file_name, fill, stroke); widget.set_cursor(std::move(cursor)); } diff --git a/src/ui/cursor-utils.h b/src/ui/cursor-utils.h index 98a535c74f..603cb0db16 100644 --- a/src/ui/cursor-utils.h +++ b/src/ui/cursor-utils.h @@ -14,6 +14,9 @@ #include #include #include +#include + +#include "colors/color.h" namespace Gdk { class Cursor; @@ -27,17 +30,13 @@ namespace Inkscape { Glib::RefPtr load_svg_cursor(Gtk::Widget &widget, std::string const &file_name, - std::uint32_t fill = 0xffffffff, - std::uint32_t stroke = 0x000000ff, - double fill_opacity = 1.0, - double stroke_opacity = 1.0); + std::optional fill = {}, + std::optional stroke = {}); void set_svg_cursor(Gtk::Widget &widget, std::string const &file_name, - std::uint32_t fill = 0xffffffff, - std::uint32_t stroke = 0x000000ff, - double fill_opacity = 1.0, - double stroke_opacity = 1.0); + std::optional fill = {}, + std::optional stroke = {}); } // namespace Inkscape diff --git a/src/ui/dialog/about.cpp b/src/ui/dialog/about.cpp index 134f7ebce3..76ba3927d2 100644 --- a/src/ui/dialog/about.cpp +++ b/src/ui/dialog/about.cpp @@ -42,7 +42,6 @@ #include "desktop.h" #include "display/cairo-utils.h" #include "helper/auto-connection.h" -#include "hsluv.h" #include "inkscape-version-info.h" #include "inkscape.h" #include "inkscape-window.h" @@ -147,24 +146,14 @@ private: auto ctx = Cairo::Context::create(surface); ctx->set_source(image, 0, -y); ctx->paint(); - double r,g,b,a; - ink_cairo_surface_average_color_premul(surface->cobj(), r, g, b, a); // calculate footer color: light/dark depending on a theme bool dark = INKSCAPE.themecontext->isCurrentThemeDark(this); - // color of the image strip to HSL, so we can manipulate its lightness - auto [h, s, l] = Hsluv::rgb_to_hsluv(r, g, b); - // for a dark theme come up with a darker shade, for a light time - with a lighter one - l = dark ? l * 0.7 : l + (100 - l) * 0.5; - // clip them to remove extremes - l = dark ? std::min(l, 30.0) : std::max(l, 80.0); - // limit saturation to improve contrast with some artwork - s = dark ? std::min(s, 80.0) : s; - auto rgb = Hsluv::hsluv_to_rgb(h, s, l); + auto foot = Colors::make_theme_color(ink_cairo_surface_average_color_premul(surface->cobj()), dark); + auto style_context = _footer->get_style_context(); _footer_style = Gtk::CssProvider::create(); - auto color_str = rgba_to_css_color(rgb[0], rgb[1], rgb[2]); - _footer_style->load_from_data("box {background-color:" + color_str + ";}"); + _footer_style->load_from_data("box {background-color:" + foot.toString() + ";}"); if (_footer_style) style_context->remove_provider(_footer_style); style_context->add_provider(_footer_style, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); } diff --git a/src/ui/dialog/clonetiler.cpp b/src/ui/dialog/clonetiler.cpp index d84cc6dd28..2aa1bda374 100644 --- a/src/ui/dialog/clonetiler.cpp +++ b/src/ui/dialog/clonetiler.cpp @@ -44,7 +44,6 @@ #include "object/sp-namedview.h" #include "object/sp-root.h" #include "object/sp-use.h" -#include "svg/svg-color.h" #include "svg/svg.h" #include "ui/icon-loader.h" #include "ui/icon-names.h" @@ -671,10 +670,10 @@ CloneTiler::CloneTiler() auto const l = Gtk::make_managed(_("Initial color: ")); UI::pack_start(*hb, *l, false, false); - guint32 rgba = 0x000000ff | sp_svg_read_color (prefs->getString(prefs_path + "initial_color").data(), 0x000000ff); + auto color = prefs->getColor(prefs_path + "initial_color", "#000000ff"); color_picker = Gtk::make_managed(_("Initial color of tiled clones"), _("Initial color for clones (works only if the original has unset fill or stroke or on spray tool in copy mode)"), - rgba, false); + color, false); color_changed_connection = color_picker->connectChanged(sigc::mem_fun(*this, &CloneTiler::on_picker_color_changed)); UI::pack_start(*hb, *color_picker, false, false); @@ -1218,19 +1217,15 @@ CloneTiler::~CloneTiler () color_changed_connection.disconnect(); } -void CloneTiler::on_picker_color_changed(guint rgba) +void CloneTiler::on_picker_color_changed(Colors::Color const &color) { static bool is_updating = false; if (is_updating || !SP_ACTIVE_DESKTOP) return; is_updating = true; - - gchar c[32]; - sp_svg_write_color(c, sizeof(c), rgba); Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - prefs->setString(prefs_path + "initial_color", c); - + prefs->setColor(prefs_path + "initial_color", color); is_updating = false; } @@ -1947,11 +1942,9 @@ guint32 CloneTiler::trace_pick(Geom::Rect box) Inkscape::DrawingContext dc(s, ibox.min()); /* Render */ trace_drawing->render(dc, ibox); - double R = 0, G = 0, B = 0, A = 0; - ink_cairo_surface_average_color(s, R, G, B, A); + auto color = ink_cairo_surface_average_color(s); cairo_surface_destroy(s); - - return SP_RGBA32_F_COMPOSE (R, G, B, A); + return color.toRGBA(); } void CloneTiler::trace_finish() @@ -2166,7 +2159,7 @@ void CloneTiler::apply() bool opacity_alternatej = prefs->getBool(prefs_path + "opacity_alternatej"); double opacity_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "opacity_rand", 0, 0, 100); - Glib::ustring initial_color = prefs->getString(prefs_path + "initial_color"); + auto initial_color = prefs->getColor(prefs_path + "initial_color"); double hue_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "hue_per_j", 0, -100, 100); double hue_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "hue_per_i", 0, -100, 100); double hue_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "hue_rand", 0, 0, 100); @@ -2298,29 +2291,20 @@ void CloneTiler::apply() } } - gchar color_string[32]; *color_string = 0; + std::string color_string; // Color tab - if (!initial_color.empty()) { - guint32 rgba = sp_svg_read_color (initial_color.data(), 0x000000ff); - float hsl[3]; - SPColor::rgb_to_hsl_floatv (hsl, SP_RGBA32_R_F(rgba), SP_RGBA32_G_F(rgba), SP_RGBA32_B_F(rgba)); + double eff_i = (color_alternatei? (i%2) : (i)); + double eff_j = (color_alternatej? (j%2) : (j)); - double eff_i = (color_alternatei? (i%2) : (i)); - double eff_j = (color_alternatej? (j%2) : (j)); + auto hsl = *initial_color.converted(Colors::Space::Type::HSL); + hsl.set(0, hsl[0] + hue_per_i * eff_i + hue_per_j * eff_j + hue_rand * g_random_double_range (-1, 1)); + hsl.set(1, hsl[1] + saturation_per_i * eff_i + saturation_per_j * eff_j + saturation_rand * g_random_double_range (-1, 1)); + hsl.set(2, hsl[2] + lightness_per_i * eff_i + lightness_per_j * eff_j + lightness_rand * g_random_double_range (-1, 1)); + hsl.normalize(); - hsl[0] += hue_per_i * eff_i + hue_per_j * eff_j + hue_rand * g_random_double_range (-1, 1); - double notused; - hsl[0] = modf( hsl[0], ¬used ); // Restrict to 0-1 - hsl[1] += saturation_per_i * eff_i + saturation_per_j * eff_j + saturation_rand * g_random_double_range (-1, 1); - hsl[1] = CLAMP (hsl[1], 0, 1); - hsl[2] += lightness_per_i * eff_i + lightness_per_j * eff_j + lightness_rand * g_random_double_range (-1, 1); - hsl[2] = CLAMP (hsl[2], 0, 1); - - float rgb[3]; - SPColor::hsl_to_rgb_floatv (rgb, hsl[0], hsl[1], hsl[2]); - sp_svg_write_color(color_string, sizeof(color_string), SP_RGBA32_F_COMPOSE(rgb[0], rgb[1], rgb[2], 1.0)); - } + // We could convert to RGB here, but we shouldn't actually need to. + color_string = hsl.toString(); // Blur double blur = 0.0; @@ -2344,14 +2328,8 @@ void CloneTiler::apply() if (dotrace) { Geom::Rect bbox_t = transform_rect (bbox_original, t*Geom::Scale(1.0/scale_units)); - guint32 rgba = trace_pick (bbox_t); - float r = SP_RGBA32_R_F(rgba); - float g = SP_RGBA32_G_F(rgba); - float b = SP_RGBA32_B_F(rgba); - float a = SP_RGBA32_A_F(rgba); - - float hsl[3]; - SPColor::rgb_to_hsl_floatv (hsl, r, g, b); + auto rgba = Colors::Color(trace_pick(bbox_t)); + auto hsl = *rgba.converted(Colors::Space::Type::HSL); gdouble val = 0; switch (pick) { @@ -2359,16 +2337,16 @@ void CloneTiler::apply() val = 1 - hsl[2]; // inverse lightness; to match other picks where black = max break; case PICK_OPACITY: - val = a; + val = rgba.getOpacity(); break; case PICK_R: - val = r; + val = rgba[0]; break; case PICK_G: - val = g; + val = rgba[1]; break; case PICK_B: - val = b; + val = rgba[2]; break; case PICK_H: val = hsl[0]; @@ -2385,9 +2363,8 @@ void CloneTiler::apply() if (rand_picked > 0) { val = randomize01 (val, rand_picked); - r = randomize01 (r, rand_picked); - g = randomize01 (g, rand_picked); - b = randomize01 (b, rand_picked); + for (auto i = 0; i < 3; i++) + rgba.set(i, randomize01(rgba[i], rand_picked)); } if (gamma_picked != 0) { @@ -2398,25 +2375,17 @@ void CloneTiler::apply() power = 1 + fabs(gamma_picked); val = pow (val, power); - r = pow (r, power); - g = pow (g, power); - b = pow (b, power); + for (auto i = 0; i < 3; i++) + rgba.set(i, pow(rgba[i], power)); } if (invert_picked) { val = 1 - val; - r = 1 - r; - g = 1 - g; - b = 1 - b; + rgba.invert(); } val = CLAMP (val, 0, 1); - r = CLAMP (r, 0, 1); - g = CLAMP (g, 0, 1); - b = CLAMP (b, 0, 1); - - // recompose tweaked color - rgba = SP_RGBA32_F_COMPOSE(r, g, b, a); + rgba.normalize(); if (pick_to_presence) { if (g_random_double_range (0, 1) > val) { @@ -2432,7 +2401,7 @@ void CloneTiler::apply() opacity *= val; } if (pick_to_color) { - sp_svg_write_color(color_string, sizeof(color_string), rgba); + color_string = rgba.toString(); } } @@ -2464,7 +2433,7 @@ void CloneTiler::apply() clone->setAttributeCssDouble("opacity", opacity); } - if (*color_string) { + if (!color_string.empty()) { clone->setAttribute("fill", color_string); clone->setAttribute("stroke", color_string); } diff --git a/src/ui/dialog/clonetiler.h b/src/ui/dialog/clonetiler.h index bfbbfccd47..4417efa472 100644 --- a/src/ui/dialog/clonetiler.h +++ b/src/ui/dialog/clonetiler.h @@ -90,7 +90,7 @@ protected: void table_attach(Gtk::Grid *table, Gtk::Widget *widget, float align, int row, int col); void symgroup_changed(Gtk::ComboBox *cb); - void on_picker_color_changed(guint rgba); + void on_picker_color_changed(Colors::Color const &rgba); void trace_hide_tiled_clones_recursively(SPObject *from); guint number_of_clones(SPObject *obj); void trace_setup(SPDocument *doc, gdouble zoom, SPItem *original); diff --git a/src/ui/dialog/color-item.cpp b/src/ui/dialog/color-item.cpp index 42de70f29c..834d32c68f 100644 --- a/src/ui/dialog/color-item.cpp +++ b/src/ui/dialog/color-item.cpp @@ -35,10 +35,10 @@ #include #include +#include "colors/dragndrop.h" #include "desktop-style.h" #include "document.h" #include "document-undo.h" -#include "hsluv.h" #include "message-context.h" #include "preferences.h" #include "selection.h" @@ -48,7 +48,6 @@ #include "io/resource.h" #include "object/sp-gradient.h" #include "object/tags.h" -#include "svg/svg-color.h" #include "ui/containerize.h" #include "ui/controller.h" #include "ui/dialog/dialog-base.h" @@ -82,21 +81,23 @@ Glib::RefPtr get_removecolor() } // namespace -ColorItem::ColorItem(PaintDef const &paintdef, DialogBase *dialog) +ColorItem::ColorItem(DialogBase *dialog) : dialog(dialog) { - if (paintdef.get_type() == PaintDef::RGB) { - pinned_default = false; - data = RGBData{paintdef.get_rgb()}; - } else { - pinned_default = true; - data = PaintNone{}; - add_css_class("paint-none"); - } - description = paintdef.get_description(); - color_id = paintdef.get_color_id(); - tooltip = paintdef.get_tooltip(); + data = PaintNone(); + pinned_default = true; + add_css_class("paint-none"); + description = C_("Paint", "None"); + color_id = "none"; + common_setup(); +} +ColorItem::ColorItem(Colors::Color color, DialogBase *dialog) + : dialog(dialog) +{ + description = color.getName(); + color_id = color_to_id(color); + data = std::move(color); common_setup(); } @@ -202,9 +203,8 @@ void ColorItem::draw_color(Cairo::RefPtr const &cr, int w, int h cr->paint(); cr->restore(); } - } else if (auto const rgbdata = std::get_if(&data)) { - auto [r, g, b] = rgbdata->rgb; - cr->set_source_rgb(r / 255.0, g / 255.0, b / 255.0); + } else if (auto const color = std::get_if(&data)) { + ink_cairo_set_source_color(cr, *color); cr->paint(); // there's no way to query background color to check if color item stands out, // so we apply faint outline to let users make out color shapes blending with background @@ -256,8 +256,8 @@ void ColorItem::draw_func(Cairo::RefPtr const &cr, int const w, // Draw fill/stroke indicators. if (is_fill || is_stroke) { - double const lightness = Hsluv::rgb_to_perceptual_lightness(average_color()); - auto [gray, alpha] = Hsluv::get_contrasting_color(lightness); + double const lightness = Colors::get_perceptual_lightness(getColor()); + auto [gray, alpha] = Colors::get_contrasting_color(lightness); cr->set_source_rgba(gray, gray, gray, alpha); // Scale so that the square -1...1 is the biggest possible square centred in the widget. @@ -351,12 +351,8 @@ void ColorItem::on_click(bool stroke) if (is_paint_none()) { sp_repr_css_set_property(css.get(), attr_name, "none"); descr = stroke ? _("Set stroke color to none") : _("Set fill color to none"); - } else if (auto const rgbdata = std::get_if(&data)) { - auto [r, g, b] = rgbdata->rgb; - std::uint32_t rgba = (r << 24) | (g << 16) | (b << 8) | 0xff; - char buf[64]; - sp_svg_write_color(buf, sizeof(buf), rgba); - sp_repr_css_set_property(css.get(), attr_name, buf); + } else if (auto const color = std::get_if(&data)) { + sp_repr_css_set_property_string(css.get(), attr_name, color->toString()); descr = stroke ? _("Set stroke color from swatch") : _("Set fill color from swatch"); } else if (auto const graddata = std::get_if(&data)) { auto grad = graddata->gradient; @@ -511,30 +507,13 @@ void ColorItem::action_convert(Glib::ustring const &name) DocumentUndo::done(doc, _("Add gradient stop"), INKSCAPE_ICON("color-gradient")); } -PaintDef ColorItem::to_paintdef() const -{ - if (is_paint_none()) { - return PaintDef(); - } else if (auto const rgbdata = std::get_if(&data)) { - return PaintDef(rgbdata->rgb, description, ""); - } else if (auto const graddata = std::get_if(&data)) { - auto const grad = graddata->gradient; - assert(grad != nullptr); - return PaintDef({0, 0, 0}, grad->getId(), ""); - } - - // unreachable - assert(false); - return {}; -} - Glib::RefPtr ColorItem::on_drag_prepare(Gtk::DragSource const &, double, double) { if (!dialog) return {}; - Glib::Value value; + Glib::Value> value; value.init(value.value_type()); - value.set(to_paintdef()); + value.set(getColor()); return Gdk::ContentProvider::create(value); } @@ -572,13 +551,16 @@ bool ColorItem::is_pinned() const } } -std::array ColorItem::average_color() const +/** + * Return the average color for this color item. If none, returns whit + * but if a gradient an average of the gradient in RGB is returned. + */ +Colors::Color ColorItem::getColor() const { if (is_paint_none()) { - return {1.0, 1.0, 1.0}; - } else if (auto const rgbdata = std::get_if(&data)) { - auto [r, g, b] = rgbdata->rgb; - return {r / 255.0, g / 255.0, b / 255.0}; + return Colors::Color(0xffffffff); + } else if (auto const color = std::get_if(&data)) { + return *color; } else if (auto const graddata = std::get_if(&data)) { auto grad = graddata->gradient; auto pat = Cairo::RefPtr(new Cairo::Pattern(grad->create_preview_pattern(1), true)); @@ -589,12 +571,12 @@ std::array ColorItem::average_color() const cr->set_source(pat); cr->paint(); auto rgb = img->get_data(); - return {rgb[0] / 255.0, rgb[1] / 255.0, rgb[2] / 255.0}; + return Colors::Color(Colors::Space::Type::RGB, {rgb[0] / 255.0, rgb[1] / 255.0, rgb[2] / 255.0}); } // unreachable assert(false); - return {1.0, 1.0, 1.0}; + return Colors::Color(0xffffffff); } bool ColorItem::is_paint_none() const { diff --git a/src/ui/dialog/color-item.h b/src/ui/dialog/color-item.h index 61d1078c54..77086040d9 100644 --- a/src/ui/dialog/color-item.h +++ b/src/ui/dialog/color-item.h @@ -20,7 +20,7 @@ #include #include // Gtk::EventSequenceState -#include "widgets/paintdef.h" +#include "colors/color.h" namespace Cairo { class Context; @@ -51,8 +51,10 @@ class DialogBase; class ColorItem : public Gtk::DrawingArea { public: - /// Create a static color from a paintdef. - ColorItem(PaintDef const&, DialogBase*); + // No fill option + ColorItem(DialogBase*); + /// Create a static color + ColorItem(Colors::Color, DialogBase*); /// Add new group or filler element. ColorItem(Glib::ustring name); ~ColorItem() override; @@ -118,11 +120,8 @@ private: // Draw the color only (i.e. no indicators) to a Cairo context. Used for drawing both the widget and the drag/drop icon. void draw_color(Cairo::RefPtr const &cr, int w, int h) const; - // Construct an equivalent paintdef for use during drag/drop. - PaintDef to_paintdef() const; - // Return the color (or average if a gradient), for choosing the color of the fill/stroke indicators. - std::array average_color() const; + Colors::Color getColor() const; // Description of the color, shown in help text. Glib::ustring description; @@ -136,9 +135,8 @@ private: // The color. struct Undefined {}; struct PaintNone {}; - struct RGBData { std::array rgb; }; struct GradientData { SPGradient *gradient; }; - std::variant data; + std::variant data; // The dialog this widget belongs to. Used for determining what desktop to take action on. DialogBase *dialog = nullptr; diff --git a/src/ui/dialog/document-properties.cpp b/src/ui/dialog/document-properties.cpp index 638962f1d1..c482b18555 100644 --- a/src/ui/dialog/document-properties.cpp +++ b/src/ui/dialog/document-properties.cpp @@ -57,7 +57,9 @@ #include "rdf.h" #include "page-manager.h" #include "selection.h" -#include "color/cms-system.h" + +#include "colors/cms/profile.h" +#include "colors/document-cms.h" #include "helper/auto-connection.h" #include "io/sys.h" #include "object/color-profile.h" @@ -65,7 +67,6 @@ #include "object/sp-root.h" #include "object/sp-script.h" #include "streq.h" -#include "svg/svg-color.h" #include "ui/dialog/filedialog.h" #include "ui/icon-loader.h" #include "ui/icon-names.h" @@ -295,11 +296,10 @@ void set_namedview_bool(SPDesktop* desktop, const Glib::ustring& operation, SPAt DocumentUndo::done(desktop->getDocument(), operation, ""); } -void set_color(SPDesktop* desktop, Glib::ustring operation, unsigned int rgba, SPAttr color_key, SPAttr opacity_key = SPAttr::INVALID) { +void set_color(SPDesktop* desktop, Glib::ustring operation, SPAttr color_key, SPAttr opacity_key, Colors::Color const &color) { if (!desktop || !desktop->getDocument()) return; - desktop->getNamedView()->change_color(rgba, color_key, opacity_key); - + desktop->getNamedView()->change_color(color_key, opacity_key, color); desktop->getDocument()->setModifiedSinceSave(); DocumentUndo::maybeDone(desktop->getDocument(), ("document-color-" + operation).c_str(), operation, ""); } @@ -476,19 +476,19 @@ void DocumentProperties::build_page() _page = Gtk::manage(PageProperties::create()); _page_page->table().attach(*_page, 0, 0); - _page->signal_color_changed().connect([this](unsigned const color, PageProperties::Color const element){ + _page->signal_color_changed().connect([this](Colors::Color const &color, PageProperties::Color const element){ if (_wr.isUpdating() || !_wr.desktop()) return; _wr.setUpdating(true); switch (element) { case PageProperties::Color::Desk: - set_color(_wr.desktop(), _("Desk color"), color, SPAttr::INKSCAPE_DESK_COLOR); + set_color(_wr.desktop(), _("Desk color"), SPAttr::INKSCAPE_DESK_COLOR, SPAttr::INKSCAPE_DESK_OPACITY, color); break; case PageProperties::Color::Background: - set_color(_wr.desktop(), _("Background color"), color, SPAttr::PAGECOLOR); + set_color(_wr.desktop(), _("Background color"), SPAttr::PAGECOLOR, SPAttr::INKSCAPE_PAGEOPACITY, color); break; case PageProperties::Color::Border: - set_color(_wr.desktop(), _("Border color"), color, SPAttr::BORDERCOLOR, SPAttr::BORDEROPACITY); + set_color(_wr.desktop(), _("Border color"), SPAttr::BORDERCOLOR, SPAttr::BORDEROPACITY, color); break; } _wr.setUpdating(false); @@ -621,24 +621,24 @@ void DocumentProperties::populate_available_profiles(){ // Iterate through the list of profiles and add the name to the combo box. bool home = true; // initial value doesn't matter, it's just to avoid a compiler warning bool first = true; - auto cms_system = Inkscape::CMSSystem::get(); - for (auto const &info: cms_system->get_system_profile_infos()) { + auto &cms_system = Inkscape::Colors::CMS::System::get(); + for (auto const &profile: cms_system.getProfiles()) { Gtk::TreeModel::Row row; // add a separator between profiles from the user's home directory and system profiles - if (!first && info.in_home() != home) + if (!first && profile->inHome() != home) { row = *(_AvailableProfilesListStore->append()); row[_AvailableProfilesListColumns.fileColumn] = ""; row[_AvailableProfilesListColumns.nameColumn] = ""; row[_AvailableProfilesListColumns.separatorColumn] = true; } - home = info.in_home(); + home = profile->inHome(); first = false; row = *(_AvailableProfilesListStore->append()); - row[_AvailableProfilesListColumns.fileColumn] = info.get_path(); - row[_AvailableProfilesListColumns.nameColumn] = info.get_name(); + row[_AvailableProfilesListColumns.fileColumn] = profile->getPath(); + row[_AvailableProfilesListColumns.nameColumn] = profile->getName(); row[_AvailableProfilesListColumns.separatorColumn] = false; } } @@ -694,33 +694,8 @@ void DocumentProperties::linkSelectedProfile() Glib::ustring file = (*iter)[_AvailableProfilesListColumns.fileColumn]; Glib::ustring name = (*iter)[_AvailableProfilesListColumns.nameColumn]; - std::vector current = document->getResourceList( "iccprofile" ); - for (auto obj : current) { - Inkscape::ColorProfile* prof = reinterpret_cast(obj); - if (!strcmp(prof->href, file.c_str())) - return; - } - Inkscape::XML::Document *xml_doc = document->getReprDoc(); - Inkscape::XML::Node *cprofRepr = xml_doc->createElement("svg:color-profile"); - std::string nameStr = name.empty() ? "profile" : name; // TODO add some auto-numbering to avoid collisions - sanitizeName(nameStr); - cprofRepr->setAttribute("name", nameStr); - cprofRepr->setAttribute("xlink:href", Glib::filename_to_uri(Glib::filename_from_utf8(file))); - cprofRepr->setAttribute("id", file); - - // Checks whether there is a defs element. Creates it when needed - Inkscape::XML::Node *defsRepr = sp_repr_lookup_name(xml_doc, "svg:defs"); - if (!defsRepr) { - defsRepr = xml_doc->createElement("svg:defs"); - xml_doc->root()->addChild(defsRepr, nullptr); - } - - g_assert(document->getDefs()); - defsRepr->addChild(cprofRepr, nullptr); - - // TODO check if this next line was sometimes needed. It being there caused an assertion. - //Inkscape::GC::release(defsRepr); - + auto filename = Glib::filename_to_uri(Glib::filename_from_utf8(file)); + document->getDocumentCMS().attachProfileToDoc(filename, ColorProfileStorage::HREF_FILE, Colors::RenderingIntent::AUTO, name); // inform the document, so we can undo DocumentUndo::done(document, _("Link Color Profile"), ""); @@ -728,20 +703,6 @@ void DocumentProperties::linkSelectedProfile() } } -struct _cmp { - bool operator()(const SPObject * const & a, const SPObject * const & b) - { - const Inkscape::ColorProfile &a_prof = reinterpret_cast(*a); - const Inkscape::ColorProfile &b_prof = reinterpret_cast(*b); - auto const a_name_casefold = g_utf8_casefold(a_prof.name, -1); - auto const b_name_casefold = g_utf8_casefold(b_prof.name, -1); - int result = g_strcmp0(a_name_casefold, b_name_casefold); - g_free(a_name_casefold); - g_free(b_name_casefold); - return result < 0; - } -}; - template struct static_caster { To * operator () (From * value) const { return static_cast(value); } }; @@ -750,9 +711,6 @@ void DocumentProperties::populate_linked_profiles_box() _LinkedProfilesListStore->clear(); if (auto document = getDocument()) { std::vector current = document->getResourceList( "iccprofile" ); - if (! current.empty()) { - _emb_profiles_observer.set((*(current.begin()))->parent); - } std::set _current; std::transform(current.begin(), @@ -762,7 +720,7 @@ void DocumentProperties::populate_linked_profiles_box() for (auto const &profile: _current) { Gtk::TreeModel::Row row = *(_LinkedProfilesListStore->append()); - row[_LinkedProfilesListColumns.nameColumn] = profile->name; + row[_LinkedProfilesListColumns.nameColumn] = profile->getName(); } } } @@ -787,14 +745,9 @@ void DocumentProperties::removeSelectedProfile(){ } } if (auto document = getDocument()) { - std::vector current = document->getResourceList( "iccprofile" ); - for (auto obj : current) { - Inkscape::ColorProfile* prof = reinterpret_cast(obj); - if (!name.compare(prof->name)){ - prof->deleteObject(true, false); - DocumentUndo::done(document, _("Remove linked color profile"), ""); - break; // removing the color profile likely invalidates part of the traversed list, stop traversing here. - } + if (auto colorprofile = document->getDocumentCMS().getColorProfileForSpace(name)) { + colorprofile->deleteObject(true, false); + DocumentUndo::done(document, _("Remove linked color profile"), ""); } } @@ -885,15 +838,6 @@ void DocumentProperties::build_cms() _LinkedProfilesList.get_selection()->signal_changed().connect( sigc::mem_fun(*this, &DocumentProperties::onColorProfileSelectRow) ); connect_remove_popup_menu(_LinkedProfilesList, _popoverbin, sigc::mem_fun(*this, &DocumentProperties::removeSelectedProfile)); - - if (auto document = getDocument()) { - std::vector current = document->getResourceList( "defs" ); - if (!current.empty()) { - _emb_profiles_observer.set((*(current.begin()))->parent); - } - _emb_profiles_observer.signal_changed().connect(sigc::mem_fun(*this, &DocumentProperties::populate_linked_profiles_box)); - onColorProfileSelectRow(); - } } void DocumentProperties::build_scripting() @@ -1574,11 +1518,11 @@ void DocumentProperties::update_widgets() _page->set_unit(PageProperties::Units::Display, nv->display_units->abbr); } _page->set_check(PageProperties::Check::Checkerboard, nv->desk_checkerboard); - _page->set_color(PageProperties::Color::Desk, nv->desk_color); - _page->set_color(PageProperties::Color::Background, page_manager.background_color); + _page->set_color(PageProperties::Color::Desk, nv->getDeskColor()); + _page->set_color(PageProperties::Color::Background, page_manager.getBackgroundColor()); _page->set_check(PageProperties::Check::Border, page_manager.border_show); _page->set_check(PageProperties::Check::BorderOnTop, page_manager.border_on_top); - _page->set_color(PageProperties::Color::Border, page_manager.border_color); + _page->set_color(PageProperties::Color::Border, page_manager.getBorderColor()); _page->set_check(PageProperties::Check::Shadow, page_manager.shadow_show); _page->set_check(PageProperties::Check::PageLabelStyle, page_manager.label_style != "default"); _page->set_check(PageProperties::Check::AntiAlias, nv->antialias_rendering); @@ -1588,12 +1532,8 @@ void DocumentProperties::update_widgets() _rcb_sgui.setActive (nv->getShowGuides()); _rcb_lgui.setActive (nv->getLockGuides()); - _rcp_gui.setRgba32 (nv->guidecolor); - _rcp_hgui.setRgba32 (nv->guidehicolor); - - //------------------------------------------------Color Management page - - populate_linked_profiles_box(); + _rcp_gui.setColor(nv->getGuideColor()); + _rcp_hgui.setColor(nv->getGuideHiColor()); //-----------------------------------------------------------meta pages // update the RDF entities; note that this may modify document, maybe doc-undo should be called? @@ -1676,12 +1616,14 @@ void DocumentProperties::documentReplaced() { _root_connection.disconnect(); _namedview_connection.disconnect(); + _cms_connection.disconnect(); if (auto desktop = getDesktop()) { _wr.setDesktop(desktop); _namedview_connection.connect(desktop->getNamedView()->getRepr()); if (auto document = desktop->getDocument()) { _root_connection.connect(document->getRoot()->getRepr()); + _cms_connection = document->getDocumentCMS().connectChanged(sigc::mem_fun(*this, &DocumentProperties::populate_linked_profiles_box)); } populate_linked_profiles_box(); update_widgets(); @@ -1899,17 +1841,15 @@ GridWidget::GridWidget(SPGrid *grid) "", _("Grid color"), _("Color of the grid lines"), "empcolor", "empopacity", _wr, repr, doc); - _grid_color->setCustomSetter([](Inkscape::XML::Node* node, std::uint32_t rgba) { - char buf[32]; + _grid_color->setCustomSetter([](Inkscape::XML::Node* node, Colors::Color color) { // major color - sp_svg_write_color(buf, sizeof(buf), rgba); - node->setAttribute("empcolor", buf); - node->setAttributeCssDouble("empopacity", (rgba & 0xff) / 255.0); + node->setAttribute("empcolor", color.toString(false)); + node->setAttributeSvgDouble("empopacity", color.getOpacity()); + // minor color at half opacity - rgba = (rgba & ~0xff) | ((rgba & 0xff) / 2); - sp_svg_write_color(buf, sizeof(buf), rgba); - node->setAttribute("color", buf); - node->setAttributeCssDouble("opacity", (rgba & 0xff) / 255.0); + color.addOpacity(0.5); + node->setAttribute("color", color.toString(false)); + node->setAttributeCssDouble("opacity", color.getOpacity()); }); _grid_color->set_spacing(0); _no_of_lines = Gtk::make_managed( @@ -2119,7 +2059,7 @@ void GridWidget::update() _margin_y->setValueKeepUnit(margin.y(), "px"); } - _grid_color->setRgba32 (_grid->getMajorColor()); + _grid_color->setColor(_grid->getMajorColor()); show(_no_of_lines, !modular); _no_of_lines->setValue(_grid->getMajorLineInterval()); diff --git a/src/ui/dialog/document-properties.h b/src/ui/dialog/document-properties.h index ed51a3e430..dc9c0a9d9f 100644 --- a/src/ui/dialog/document-properties.h +++ b/src/ui/dialog/document-properties.h @@ -127,7 +127,7 @@ private: void set_viewbox_pos(SPDesktop* desktop, double x, double y); void set_viewbox_size(SPDesktop* desktop, double width, double height); - Inkscape::XML::SignalObserver _emb_profiles_observer, _scripts_observer; + Inkscape::XML::SignalObserver _scripts_observer; UI::Widget::PopoverBin _popoverbin; Gtk::Notebook _notebook; @@ -265,6 +265,8 @@ private: // nodes connected to listeners WatchConnection _namedview_connection; WatchConnection _root_connection; + + auto_connection _cms_connection; }; } // namespace Dialog diff --git a/src/ui/dialog/document-resources.cpp b/src/ui/dialog/document-resources.cpp index 0b9cf3ebcd..698e57f481 100644 --- a/src/ui/dialog/document-resources.cpp +++ b/src/ui/dialog/document-resources.cpp @@ -60,7 +60,8 @@ #include #include -#include "color.h" +#include "colors/color.h" +#include "colors/color-set.h" #include "desktop.h" #include "document-undo.h" #include "helper/choose-file.h" @@ -629,27 +630,24 @@ Cairo::RefPtr render_color(uint32_t rgb, double size, double rad return add_background_to_image(nul, rgb, size / 2, radius, device_scale, 0x7f7f7f00); } -void collect_object_colors(SPObject& obj, std::map& colors) { +void collect_object_colors(SPObject& obj, Colors::ColorSet &colors) { auto style = obj.style; - if (style->stroke.set && style->stroke.colorSet) { - const auto& c = style->stroke.value.color; - colors[c.toString()] = c; - } + auto add = [&colors](Colors::Color const &c) { + colors.set(c.toString(), c); + }; + if (style->stroke.set && style->stroke.isColor()) { + add(style->stroke.getColor()); + } if (style->color.set) { - const auto& c = style->color.value.color; - colors[c.toString()] = c; + add(style->color.getColor()); } - if (style->fill.set) { - const auto& c = style->fill.value.color; - colors[c.toString()] = c; + add(style->fill.getColor()); } - if (style->solid_color.set) { - const auto& c = style->solid_color.value.color; - colors[c.toString()] = c; + add(style->solid_color.getColor()); } } @@ -666,12 +664,10 @@ void apply_visitor(SPObject& object, V&& visitor) { } } -std::map collect_colors(SPObject* object) { - std::map colors; +void collect_colors(SPObject* object, Colors::ColorSet &colors) { if (object) { apply_visitor(*object, [&](SPObject& obj){ collect_object_colors(obj, colors); }); } - return colors; } void collect_used_fonts(SPObject& object, std::set& fonts) { @@ -758,7 +754,7 @@ details::Statistics collect_statistics(SPObject* root) { return stats; } - std::map colors; + Colors::ColorSet colors; std::set fonts; apply_visitor(*root, [&](SPObject& obj){ @@ -929,12 +925,12 @@ void DocumentResources::selectionModified(Inkscape::Selection* selection, unsign auto get_id = [](const SPObject* object) { auto id = object->getId(); return id ? id : ""; }; auto label_fmt = [](const char* label, const Glib::ustring& id) { return label && *label ? label : '#' + id; }; -void add_colors(Glib::RefPtr& item_store, const std::map& colors, int device_scale) { +void add_colors(Glib::RefPtr& item_store, Colors::ColorSet const &colors, int device_scale) { for (auto&& it : colors) { const auto& color = it.second; Glib::ustring name = color.toString(); - auto rgba32 = color.toRGBA32(0xff); + auto rgba32 = color.toRGBA(0xff); auto rgb24 = rgba32 >> 8; int size = 20; @@ -1124,12 +1120,15 @@ void DocumentResources::refresh_page(const Glib::ustring& id) { switch (rsrc) { case Colors: - add_colors(_item_store, collect_colors(root), device_scale); - item_width = 70; - items_selectable = false; // to do: make selectable? - can_extract = true; - break; - + { + Colors::ColorSet colors; + collect_colors(root, colors); + add_colors(_item_store, colors, device_scale); + item_width = 70; + items_selectable = false; // to do: make selectable? + can_extract = true; + break; + } case Symbols: { auto opt = object_renderer::options(); diff --git a/src/ui/dialog/export-batch.cpp b/src/ui/dialog/export-batch.cpp index 915a060f11..6c49046471 100644 --- a/src/ui/dialog/export-batch.cpp +++ b/src/ui/dialog/export-batch.cpp @@ -268,6 +268,7 @@ BatchExport::BatchExport(BaseObjectType * const cobject, Glib::RefPtr (builder, "b_progress")) , _prog_batch (get_widget (builder, "b_progress_batch")) , export_list (get_derived_widget(builder, "b_export_list")) + , _background_color(get_derived_widget(builder, "b_backgnd", _("Background color"), true)) { prefs = Inkscape::Preferences::get(); @@ -279,10 +280,6 @@ BatchExport::BatchExport(BaseObjectType * const cobject, Glib::RefPtr(builder, "b_s_layers"); selection_buttons[SELECTION_PAGE] = &get_widget(builder, "b_s_pages"); - auto &button = get_widget(builder, "b_backgnd"); - _bgnd_color_picker = std::make_unique( - _("Background color"), _("Color used to fill the image background"), 0xffffff00, true, &button); - path_chooser.signal_clicked().connect([this] { pickBatchPath(); }); setup(); @@ -363,7 +360,7 @@ void BatchExport::setup() export_conn = export_btn.signal_clicked().connect(sigc::mem_fun(*this, &BatchExport::onExport)); cancel_conn = cancel_btn.signal_clicked().connect(sigc::mem_fun(*this, &BatchExport::onCancel)); hide_all.signal_toggled().connect(sigc::mem_fun(*this, &BatchExport::refreshPreview)); - _bgnd_color_picker->connectChanged([=, this](guint32 color){ + _background_color.connectChanged([=, this](Colors::Color const &color){ if (_desktop) { Inkscape::UI::Dialog::set_export_bg_color(_desktop->getNamedView(), color); } @@ -501,7 +498,7 @@ void BatchExport::refreshPreview() _preview_drawing->set_shown_items(std::move(selected)); for (auto &[key, val] : current_items) { - val->refresh(!preview, _bgnd_color_picker->get_current_color()); + val->refresh(!preview, _background_color.get_current_color().toRGBA()); } } } @@ -772,7 +769,7 @@ void BatchExport::onExport() unsigned long int height = (int)(area.height() * dpi / DPI_BASE + 0.5); Export::exportRaster( - area, width, height, dpi, _bgnd_color_picker->get_current_color(), + area, width, height, dpi, _background_color.get_current_color().toRGBA(), item_filename, true, onProgressCallback, this, ext, hide ? &show_only : nullptr); } else { auto copy_doc = _document->copy(); @@ -866,9 +863,7 @@ void BatchExport::setDocument(SPDocument *document) if (document) { // when the page selected is changed, update the export area _pages_changed_connection = document->getPageManager().connectPagesChanged([this]() { pagesChanged(); }); - - auto bg_color = get_export_bg_color(document->getNamedView(), 0xffffff00); - _bgnd_color_picker->setRgba32(bg_color); + _background_color.setColor(get_export_bg_color(document->getNamedView(), Colors::Color(0xffffff00))); _preview_drawing = std::make_shared(document); } else { _preview_drawing.reset(); diff --git a/src/ui/dialog/export-batch.h b/src/ui/dialog/export-batch.h index 41ea57db03..b4d6a5d989 100644 --- a/src/ui/dialog/export-batch.h +++ b/src/ui/dialog/export-batch.h @@ -141,6 +141,7 @@ private: Gtk::ProgressBar &_prog_batch; ExportList &export_list; Gtk::Box &progress_box; + Inkscape::UI::Widget::ColorPicker &_background_color; // Store all items to be displayed in flowbox std::map> current_items; @@ -183,8 +184,6 @@ private: auto_connection refresh_items_conn; // SVG Signals auto_connection _pages_changed_connection; - - std::unique_ptr _bgnd_color_picker; }; } // namespace Dialog diff --git a/src/ui/dialog/export-single.cpp b/src/ui/dialog/export-single.cpp index 0720840e3f..2d81d1fc1b 100644 --- a/src/ui/dialog/export-single.cpp +++ b/src/ui/dialog/export-single.cpp @@ -80,6 +80,7 @@ SingleExport::SingleExport(BaseObjectType *cobject, const Glib::RefPtr (builder, "si_progress")) , cancel_button (get_widget (builder, "si_cancel")) , progress_box (get_widget (builder, "si_inprogress")) + , _background_color (get_derived_widget(builder, "si_backgnd", _("Background color"), true)) { prefs = Inkscape::Preferences::get(); @@ -113,10 +114,6 @@ SingleExport::SingleExport(BaseObjectType *cobject, const Glib::RefPtr(builder, "si_prefs"); pref_button_box.append(*si_extension_cb.getPrefButton()); - auto &button = get_widget(builder, "si_backgnd"); - _bgnd_color_picker = std::make_unique( - _("Background color"), _("Color used to fill background"), 0xffffff00, true, &button); - setup(); } @@ -206,7 +203,7 @@ void SingleExport::setup() si_filename_entry.signal_activate().connect(sigc::mem_fun(*this, &SingleExport::onExport)); si_show_preview.signal_toggled().connect(sigc::mem_fun(*this, &SingleExport::refreshPreview)); si_hide_all.signal_toggled().connect(sigc::mem_fun(*this, &SingleExport::refreshPreview)); - _bgnd_color_picker->connectChanged([=, this](guint32 color){ + _background_color.connectChanged([=, this](Colors::Color const &color){ if (_desktop) { Inkscape::UI::Dialog::set_export_bg_color(_desktop->getNamedView(), color); } @@ -647,7 +644,7 @@ void SingleExport::onExport() std::vector selected(selection->items().begin(), selection->items().end()); exportSuccessful = Export::exportRaster( - area, width, height, dpi, _bgnd_color_picker->get_current_color(), + area, width, height, dpi, _background_color.get_current_color().toRGBA(), filename, false, onProgressCallback, this, omod, selected_only ? &selected : nullptr); @@ -1006,7 +1003,7 @@ void SingleExport::refreshPreview() bool have_pages = false; for (auto const child : UI::get_children(pages_list)) { if (auto bi = dynamic_cast(child)) { - bi->refresh(!show, _bgnd_color_picker->get_current_color()); + bi->refresh(!show, _background_color.get_current_color().toRGBA()); have_pages = true; } } @@ -1023,7 +1020,7 @@ void SingleExport::refreshPreview() float y0 = unit->convert(spin_buttons[SPIN_Y0]->get_value(), "px"); float y1 = unit->convert(spin_buttons[SPIN_Y1]->get_value(), "px"); preview.setBox(Geom::Rect(x0, y0, x1, y1) * _document->dt2doc()); - preview.setBackgroundColor(_bgnd_color_picker->get_current_color()); + preview.setBackgroundColor(_background_color.get_current_color().toRGBA()); preview.queueRefresh(); } @@ -1051,8 +1048,7 @@ void SingleExport::setDocument(SPDocument *document) _page_selected_connection = pm.connectPageSelected(sigc::mem_fun(*this, &SingleExport::onPagesSelected)); _page_modified_connection = pm.connectPageModified(sigc::mem_fun(*this, &SingleExport::onPagesModified)); _page_changed_connection = pm.connectPagesChanged(sigc::mem_fun(*this, &SingleExport::onPagesChanged)); - auto bg_color = get_export_bg_color(document->getNamedView(), 0xffffff00); - _bgnd_color_picker->setRgba32(bg_color); + _background_color.setColor(get_export_bg_color(document->getNamedView(), Colors::Color(0xffffff00))); _preview_drawing = std::make_shared(document); preview.setDrawing(_preview_drawing); diff --git a/src/ui/dialog/export-single.h b/src/ui/dialog/export-single.h index 180f2eda44..ef0e2fe601 100644 --- a/src/ui/dialog/export-single.h +++ b/src/ui/dialog/export-single.h @@ -132,6 +132,7 @@ private: Gtk::ProgressBar &progress_bar; Gtk::Widget &progress_box; Gtk::Button &cancel_button; + UI::Widget::ColorPicker &_background_color; bool filename_modified = false; Glib::ustring original_name; @@ -210,8 +211,6 @@ private: auto_connection _page_selected_connection; auto_connection _page_modified_connection; auto_connection _page_changed_connection; - - std::unique_ptr _bgnd_color_picker; }; } // namespace Dialog diff --git a/src/ui/dialog/export.cpp b/src/ui/dialog/export.cpp index beb9ac2baf..6f811d3384 100644 --- a/src/ui/dialog/export.cpp +++ b/src/ui/dialog/export.cpp @@ -34,7 +34,10 @@ #include "message.h" // for MessageType #include "message-stack.h" -#include "color/color-conv.h" +#include "colors/utils.h" +#include "colors/color.h" +#include "colors/manager.h" + #include "extension/output.h" #include "helper/png-write.h" #include "io/resource.h" @@ -479,16 +482,16 @@ std::string Export::defaultFilename(SPDocument *doc, std::string &filename_entry return filename; } -void set_export_bg_color(SPObject* object, guint32 color) { +void set_export_bg_color(SPObject* object, Inkscape::Colors::Color const &color) { if (object) { - object->setAttribute("inkscape:export-bgcolor", Inkscape::Util::rgba_color_to_string(color).c_str()); + object->setAttribute("inkscape:export-bgcolor", color.toString()); } } -guint32 get_export_bg_color(SPObject* object, guint32 default_color) { +Inkscape::Colors::Color get_export_bg_color(SPObject* object, Inkscape::Colors::Color const &default_color) { if (object) { - if (auto color = Inkscape::Util::string_to_rgba_color(object->getAttribute("inkscape:export-bgcolor"))) { - return *color; + if (auto c = Inkscape::Colors::Color::parse(object->getAttribute("inkscape:export-bgcolor"))) { + return *c; } } return default_color; diff --git a/src/ui/dialog/export.h b/src/ui/dialog/export.h index ae6a31d7aa..815f8a3e15 100644 --- a/src/ui/dialog/export.h +++ b/src/ui/dialog/export.h @@ -35,6 +35,9 @@ class SPPage; namespace Inkscape { class Preferences; +namespace Colors { +class Color; +} namespace Extension { class Output; @@ -51,8 +54,8 @@ enum notebook_page BATCH_EXPORT }; -void set_export_bg_color(SPObject* object, guint32 color); -guint32 get_export_bg_color(SPObject* object, guint32 default_color); +void set_export_bg_color(SPObject* object, Inkscape::Colors::Color const &color); +Inkscape::Colors::Color get_export_bg_color(SPObject* object, Inkscape::Colors::Color const &default_color); class Export final : public DialogBase { diff --git a/src/ui/dialog/filter-effects-dialog.cpp b/src/ui/dialog/filter-effects-dialog.cpp index 0129b5c5f5..d80be8a4a1 100644 --- a/src/ui/dialog/filter-effects-dialog.cpp +++ b/src/ui/dialog/filter-effects-dialog.cpp @@ -74,7 +74,6 @@ #include "object/filters/pointlight.h" #include "object/filters/spotlight.h" #include "selection-chemistry.h" -#include "svg/svg-color.h" #include "ui/builder-utils.h" #include "ui/column-menu-builder.h" #include "ui/controller.h" @@ -312,42 +311,29 @@ class ColorButton : public Widget::ColorPicker, public AttrWidget { public: ColorButton(unsigned int def, const SPAttr a, char* tip_text) - : ColorPicker(_("Select color"), tip_text ? tip_text : "", 0x000000ff, false), + : ColorPicker(_("Select color"), tip_text ? tip_text : "", Colors::Color(0x000000ff), false, false), AttrWidget(a, def) { - use_transparency(false); - connectChanged([this](unsigned const rgba){ signal_attr_changed().emit(); }); + connectChanged([this](Colors::Color const &color){ signal_attr_changed().emit(); }); if (tip_text) { set_tooltip_text(tip_text); } - setRgba32(0xffffffff); + setColor(Colors::Color(0xffffffff)); } - // Returns the color in 'rgb(r,g,b)' form. Glib::ustring get_as_attribute() const override { - // no doubles here, so we can use the standard string stream. - std::ostringstream os; - - const auto c = get_current_color(); - int r = SP_RGBA32_R_U(c); - int g = SP_RGBA32_G_U(c); - int b = SP_RGBA32_B_U(c); - os << "rgb(" << r << "," << g << "," << b << ")"; - return os.str(); + return get_current_color().toString(false); } - void set_from_attribute(SPObject* o) override { const gchar* val = attribute_value(o); - guint32 i = 0; - if(val) { - i = sp_svg_read_color(val, 0xFFFFFFFF); + if (auto color = Colors::Color::parse(val)) { + setColor(*color); } else { - i = (guint32) get_default()->as_uint(); + setColor(Colors::Color(get_default()->as_uint())); } - setRgba32(i); } }; diff --git a/src/ui/dialog/global-palettes.cpp b/src/ui/dialog/global-palettes.cpp index 7e68a363bb..11725239e5 100644 --- a/src/ui/dialog/global-palettes.cpp +++ b/src/ui/dialog/global-palettes.cpp @@ -33,14 +33,17 @@ #include #include -#include "color/cmyk-conv.h" +#include "colors/manager.h" +#include "colors/spaces/enum.h" + #include "helper/choose-file.h" -#include "hsluv.h" +#include "colors/spaces/lab.h" #include "io/resource.h" #include "io/sys.h" #include "util/delete-with.h" -using Inkscape::Util::delete_with; #include "util-string/ustring-format.h" +using Inkscape::Util::delete_with; +using namespace Inkscape::Colors; namespace { @@ -143,29 +146,6 @@ void load_acb_palette(PaletteFileData& palette, std::string const &fname) { palette.columns = read_value(stream); palette.page_offset = read_value(stream); auto cs = read_value(stream); - int components = 0; - PaletteFileData::ColorSpace color_space; - - switch (cs) { - case 0: // RGB - components = 3; - color_space = PaletteFileData::Rgb255; - break; - case 2: // CMYK - components = 4; - color_space = PaletteFileData::Cmyk100; - break; - case 7: // LAB - components = 3; - color_space = PaletteFileData::Lab100; - break; - case 8: // Grayscale - components = 1; - color_space = PaletteFileData::Rgb255; // using RGB for grayscale - break; - default: - throw std::runtime_error(_("ACB file color space not supported.")); - } auto ext = get_extension(ttl); if (ext == ".acb") { @@ -183,73 +163,46 @@ void load_acb_palette(PaletteFileData& palette, std::string const &fname) { } palette.colors.reserve(color_count); - // simple CMYK converter here; original palette colors are kept for later use - Inkscape::CmykConverter convert; + + auto &cm = Manager::get(); + std::shared_ptr space; + + switch (cs) { + case 0: // RGB + space = cm.find(Space::Type::RGB); + break; + case 2: // CMYK + space = cm.find(Space::Type::CMYK); + break; + case 7: // LAB + space = cm.find(Space::Type::LAB); + break; + case 8: // Grayscale + space = cm.find(Space::Type::Gray); + break; + default: + throw std::runtime_error(_("ACB file color space not supported.")); + } for (int index = 0; index < color_count; ++index) { auto name = read_pstring(stream); if (name.substr(0, 3) == "$$$") name = extract(name); auto code = read_string(stream, 6); - auto channels = read_data(stream, components); - PaletteFileData::Color color; std::ostringstream ost; ost.precision(3); - switch (color_space) { - case PaletteFileData::Lab100: { - auto l = std::floor(channels[0] / 2.55f + 0.5f); - auto a = channels[1] - 128.0f; - auto b = channels[2] - 128.0f; - auto rgb = Hsluv::lab_to_rgb(l, a, b); - unsigned ur = rgb[0] * 255; - unsigned ug = rgb[1] * 255; - unsigned ub = rgb[2] * 255; - color = {{l, a, b, 0}, color_space, {ur, ug, ub}}; - ost << "L: " << l << " a: " << a << " b: " << b; - } - break; - - case PaletteFileData::Cmyk100: { - // 0% - 100% - auto c = std::floor((255 - channels[0]) / 2.55f + 0.5f); - auto m = std::floor((255 - channels[1]) / 2.55f + 0.5f); - auto y = std::floor((255 - channels[2]) / 2.55f + 0.5f); - auto k = std::floor((255 - channels[3]) / 2.55f + 0.5f); - auto [r, g, b] = convert.cmyk_to_rgb(c, m, y, k); - color = {{c, m, y, k}, color_space, {r, g, b}}; - ost << "C: " << c << "% M: " << m << "% Y: " << y << "% K: " << k << '%'; - } - break; - - case PaletteFileData::Rgb255: { - float r = channels[0]; - float g = cs == 1 ? r : channels[1]; - float b = cs == 1 ? r : channels[2]; - unsigned ur = r; - unsigned ug = g; - unsigned ub = b; - color = {{r, g, b, 0}, color_space, {ur, ug, ub}}; - if (cs == 1) { - // grayscale - ost << "R: " << ur << " G: " << ug << " B: " << ub; - } - else { - ost << "R: " << ur << " G: " << ug << " B: " << ub; - } - } - break; - - default: - throw std::runtime_error(_("Palette color space unexpected.")); + std::vector data; + for (auto channel : read_data(stream, space->getComponentCount())) { + data.emplace_back(channel / 255.0); } + auto color = Color(space, data); if (name.empty()) { palette.colors.emplace_back(PaletteFileData::SpacerItem()); } else { - color.name = prefix + name + suffix; - color.definition = ost.str(); + color.setName(prefix + name + suffix); palette.colors.emplace_back(std::move(color)); } } @@ -269,13 +222,13 @@ void load_ase_swatches(PaletteFileData& palette, std::string const &fname) { } auto block_count = read_value(stream); - Inkscape::CmykConverter convert; - auto to_mode = [](int type){ - switch (type) { - case 0: return PaletteFileData::Global; - case 1: return PaletteFileData::Spot; - default: return PaletteFileData::Normal; - } + auto &cm = Manager::get(); + + static std::map name_map = { + {"RGB ", Space::Type::RGB}, + {"LAB ", Space::Type::LAB}, + {"CMYK", Space::Type::CMYK}, + {"GRAY", Space::Type::Gray} }; for (uint32_t block = 0; block < block_count; ++block) { @@ -289,60 +242,26 @@ void load_ase_swatches(PaletteFileData& palette, std::string const &fname) { } else if (block_type == 0x0001) { // color entry auto color_name = read_pstring(stream, true); - auto mode = read_string(stream, 4); - if (mode == "CMYK") { - auto c = read_float(stream) * 100; - auto m = read_float(stream) * 100; - auto y = read_float(stream) * 100; - auto k = read_float(stream) * 100; - auto type = read_value(stream); - auto mode = to_mode(type); - auto [r, g, b] = convert.cmyk_to_rgb(c, m, y, k); - ost << "C: " << c << "% M: " << m << "% Y: " << y << "% K: " << k << '%'; - palette.colors.emplace_back( - PaletteFileData::Color {{c, m, y, k}, PaletteFileData::Cmyk100, {r, g, b}, color_name, ost.str(), mode} - ); - } - else if (mode == "RGB ") { - auto r = read_float(stream) * 255; - auto g = read_float(stream) * 255; - auto b = read_float(stream) * 255; - auto type = read_value(stream); - auto mode = to_mode(type); - palette.colors.emplace_back( - PaletteFileData::Color {{r, g, b, 0}, PaletteFileData::Rgb255, {(unsigned)r, (unsigned)g, (unsigned)b}, color_name, "", mode} - ); - } - else if (mode == "LAB ") { - //TODO - verify scale - auto l = read_float(stream) * 100; - auto a = read_float(stream) * 100; - auto b = read_float(stream) * 100; - auto type = read_value(stream); - auto mode = to_mode(type); - auto rgb = Hsluv::lab_to_rgb(l, a, b); - unsigned ur = rgb[0] * 255; - unsigned ug = rgb[1] * 255; - unsigned ub = rgb[2] * 255; - ost << "L: " << l << " a: " << a << " b: " << b; - palette.colors.emplace_back( - PaletteFileData::Color {{l, a, b, 0}, PaletteFileData::Lab100, {ur, ug, ub}, color_name, ost.str(), mode} - ); - } - else if (mode == "Gray") { - auto g = read_float(stream) * 255; - auto type = read_value(stream); - auto mode = to_mode(type); - palette.colors.emplace_back( - PaletteFileData::Color {{g, g, g, 0}, PaletteFileData::Rgb255, {(unsigned)g, (unsigned)g, (unsigned)g}, color_name, "", mode} - ); - } - else { + auto space_name = read_string(stream, 4); + + auto it = name_map.find(space_name); + if (it == name_map.end()) { std::ostringstream ost; - ost << _("ASE color mode not recognized:") << " '" << mode << "'."; + ost << _("ASE color mode not recognized:") << " '" << space_name << "'."; throw std::runtime_error(ost.str()); } - + auto space = cm.find(it->second); + std::vector data; + for (unsigned i = 0; i < space->getComponentCount(); i++) { + data.emplace_back(read_float(stream)); + } + auto color = Color(space, data); + + read_value(stream); // type uint16, ignored for now + // auto mode = to_mode(type); Is this used? 0, Global, 1, Spot, else Normal + + color.setName(color_name); + palette.colors.emplace_back(color); } else if (block_type == 0xc002) { // group end } @@ -375,30 +294,25 @@ void load_gimp_palette(PaletteFileData& palette, std::string const &path) static auto const regex_cols = Glib::Regex::create("\\s*Columns:\\s*(.*\\S)", Glib::Regex::CompileFlags::OPTIMIZE | Glib::Regex::CompileFlags::ANCHORED); static auto const regex_blank = Glib::Regex::create("\\s*(?:$|#)", Glib::Regex::CompileFlags::OPTIMIZE | Glib::Regex::CompileFlags::ANCHORED); + auto &cm = Manager::get(); + auto space = cm.find(Space::Type::RGB); + while (std::fgets(buf, sizeof(buf), f.get())) { auto line = Glib::ustring(buf); // Unnecessary copy required until using a glibmm with support for string views. TODO: Fix when possible. Glib::MatchInfo match; if (regex_rgb->match(line, match)) { // ::regex_match(line, match, boost::regex(), boost::regex_constants::match_continuous)) { // RGB color, followed by an optional name. - PaletteFileData::Color color; - for (int i = 0; i < 3; i++) { - color.rgb[i] = std::clamp(std::stoi(match.fetch(i + 1)), 0, 255); - color.channels[i] = color.rgb[i]; + + std::vector data; + for (unsigned i = 0; i < space->getComponentCount(); i++) { + data.emplace_back(std::stoi(match.fetch(i+1)) / 255.0); } - color.channels[3] = 0; - color.space = PaletteFileData::Rgb255; - color.name = match.fetch(4); + auto color = Color(space, data); + color.setName(match.fetch(4)); - if (!color.name.empty()) { + if (!color.getName().empty()) { // Translate the name if present. - color.name = g_dpgettext2(nullptr, "Palette", color.name.c_str()); - } else { - // Otherwise, set the name to be the hex value. - color.name = Glib::ustring::compose("#%1%2%3", - Inkscape::ustring::format_classic(std::hex, std::setw(2), std::setfill('0'), color.rgb[0]), - Inkscape::ustring::format_classic(std::hex, std::setw(2), std::setfill('0'), color.rgb[1]), - Inkscape::ustring::format_classic(std::hex, std::setw(2), std::setfill('0'), color.rgb[2]) - ).uppercase(); + color.setName(g_dpgettext2(nullptr, "Palette", color.getName().c_str())); } palette.colors.emplace_back(std::move(color)); diff --git a/src/ui/dialog/global-palettes.h b/src/ui/dialog/global-palettes.h index 65387cc785..017961ff6c 100644 --- a/src/ui/dialog/global-palettes.h +++ b/src/ui/dialog/global-palettes.h @@ -19,6 +19,8 @@ #include #include +#include "colors/color.h" + namespace Gtk { class Window; } // namespace Gtk @@ -42,56 +44,6 @@ struct PaletteFileData /// We can use this info to organize colors in columns in multiples of this value. int columns; - /// Color space of all colors in this palette. Original definitions are kept in "Color.channels" - /// for use with ICC profiles. Preview sRGB colors are inside "Color.rgb" - enum ColorSpace { - Undefined, // not a valid color definition - Rgb255, // RGB 0..255 - Lab100, // Cie*Lab, L 0..100, a, b -128..127 - Cmyk100 // CMYK 0%..100% - }; - - enum ColorMode: uint8_t { - Normal, - Global, - Spot, - }; - - struct Color - { - /// Original color definition (Lab, Cmyk, Rgb); unused channels 0.0 - std::array channels; - - /// Color space of this color. - ColorSpace space = Undefined; - - /// RGB color. - std::array rgb; - - /// Name of the color, either specified in the file or generated from the rgb. - Glib::ustring name; - - /// Color as defined in a palette, for informational purposes. - Glib::ustring definition; - - /// Mode (not used currently, for informational purposes only) - ColorMode mode = Normal; - -#ifdef false // not currently used - bool operator < (const Color& c) const { - for (int i = 0; i < rgb.size(); ++i) { - if (rgb[i] < c.rgb[i]) return true; - if (rgb[i] > c.rgb[i]) return false; - } - for (int i = 0; i < channels.size(); ++i) { - if (channels[i] < c.channels[i]) return true; - if (channels[i] > c.channels[i]) return false; - } - return name < c.name; - } -#endif - }; - // dummy item used for aligning color tiles in a palette enum SpacerItem {}; @@ -100,7 +52,7 @@ struct PaletteFileData Glib::ustring name; }; - using ColorItem = std::variant; + using ColorItem = std::variant; /// The list of colors in the palette. std::vector colors; diff --git a/src/ui/dialog/inkscape-preferences.cpp b/src/ui/dialog/inkscape-preferences.cpp index 91dc437692..6b7afe05f8 100644 --- a/src/ui/dialog/inkscape-preferences.cpp +++ b/src/ui/dialog/inkscape-preferences.cpp @@ -82,13 +82,15 @@ #include "selection.h" #include "style.h" -#include "color/cms-system.h" +#include "colors/cms/system.h" +#include "colors/cms/profile.h" +#include "colors/manager.h" +#include "colors/spaces/base.h" #include "display/control/canvas-item-grid.h" #include "display/nr-filter-gaussian.h" #include "extension/internal/gdkpixbuf-input.h" #include "helper/auto-connection.h" #include "io/resource.h" -#include "svg/svg-color.h" #include "ui/builder-utils.h" #include "ui/controller.h" #include "ui/desktop/menubar.h" @@ -118,7 +120,6 @@ using Inkscape::UI::Widget::PrefItem; using Inkscape::UI::Widget::PrefRadioButtons; using Inkscape::UI::Widget::PrefSpinButton; using Inkscape::UI::Widget::StyleSwatch; -using Inkscape::CMSSystem; using Inkscape::IO::Resource::get_filename; using Inkscape::IO::Resource::UIS; @@ -985,7 +986,7 @@ void InkscapePreferences::initPageTools() AddLayerChangeCheckbox(_page_node, "/tools/nodes", false); _page_node.add_group_header( _("Path outline")); - _t_node_pathoutline_color.init(_("Path outline color"), "/tools/nodes/highlight_color", 0xff0000ff); + _t_node_pathoutline_color.init(_("Path outline color"), "/tools/nodes/highlight_color", "#ff0000ff"); _page_node.add_line( false, "", _t_node_pathoutline_color, "", _("Selects the color used for showing the path outline"), false); _t_node_show_outline.init(_("Always show outline"), "/tools/nodes/show_outline", false); _page_node.add_line( true, "", _t_node_show_outline, "", _("Show outlines for all paths, not only invisible paths")); @@ -1172,9 +1173,7 @@ void InkscapePreferences::get_highlight_colors(guint32 &colorsetbase, guint32 &c size_t endposin = result.find(";"); result = result.substr(startposin + 5, endposin - (startposin + 5)); Util::trim(result); - Gdk::RGBA base_color = Gdk::RGBA(result); - SPColor base_color_sp(base_color.get_red(), base_color.get_green(), base_color.get_blue()); - colorsetbase = base_color_sp.toRGBA32(base_color.get_alpha()); + colorsetbase = to_guint32(Gdk::RGBA(result)); } content.erase(0, endpos + 1); startpos = content.find(prefix + ".success"); @@ -1185,9 +1184,7 @@ void InkscapePreferences::get_highlight_colors(guint32 &colorsetbase, guint32 &c size_t endposin = result.find(";"); result = result.substr(startposin + 5, endposin - (startposin + 5)); Util::trim(result); - Gdk::RGBA success_color = Gdk::RGBA(result); - SPColor success_color_sp(success_color.get_red(), success_color.get_green(), success_color.get_blue()); - colorsetsuccess = success_color_sp.toRGBA32(success_color.get_alpha()); + colorsetsuccess = to_guint32(Gdk::RGBA(result)); } content.erase(0, endpos + 1); startpos = content.find(prefix + ".warning"); @@ -1198,9 +1195,7 @@ void InkscapePreferences::get_highlight_colors(guint32 &colorsetbase, guint32 &c size_t endposin = result.find(";"); result = result.substr(startposin + 5, endposin - (startposin + 5)); Util::trim(result); - Gdk::RGBA warning_color = Gdk::RGBA(result); - SPColor warning_color_sp(warning_color.get_red(), warning_color.get_green(), warning_color.get_blue()); - colorsetwarning = warning_color_sp.toRGBA32(warning_color.get_alpha()); + colorsetwarning = to_guint32(Gdk::RGBA(result)); } content.erase(0, endpos + 1); startpos = content.find(prefix + ".error"); @@ -1211,9 +1206,7 @@ void InkscapePreferences::get_highlight_colors(guint32 &colorsetbase, guint32 &c size_t endposin = result.find(";"); result = result.substr(startposin + 5, endposin - (startposin + 5)); Util::trim(result); - Gdk::RGBA error_color = Gdk::RGBA(result); - SPColor error_color_sp(error_color.get_red(), error_color.get_green(), error_color.get_blue()); - colorseterror = error_color_sp.toRGBA32(error_color.get_alpha()); + colorseterror = to_guint32(Gdk::RGBA(result)); } } } @@ -1227,10 +1220,10 @@ void InkscapePreferences::resetIconsColors(bool themechange) if (!prefs->getBool("/theme/symbolicIcons", false)) { _symbolic_base_colors.set_sensitive(false); _symbolic_highlight_colors.set_sensitive(false); - _symbolic_base_color.setSensitive(false); - _symbolic_success_color.setSensitive(false); - _symbolic_warning_color.setSensitive(false); - _symbolic_error_color.setSensitive(false); + _symbolic_base_color.set_sensitive(false); + _symbolic_success_color.set_sensitive(false); + _symbolic_warning_color.set_sensitive(false); + _symbolic_error_color.set_sensitive(false); return; } @@ -1246,22 +1239,20 @@ void InkscapePreferences::resetIconsColors(bool themechange) // This is a hack to fix a problematic style which isn't updated fast enough on // change from dark to bright themes if (themechange) { - base_color = to_rgba(_symbolic_base_color.get_current_color()); + base_color = to_rgba(_symbolic_base_color.get_current_color().toRGBA()); } - // This colors are set on style.css of inkscape - SPColor base_color_sp(base_color.get_red(), base_color.get_green(), base_color.get_blue()); - //we copy highlight to not use - guint32 colorsetbase = base_color_sp.toRGBA32(base_color.get_alpha()); + // This colors are set on style.css of inkscape, we copy highlight to not use + guint32 colorsetbase = to_guint32(base_color); guint32 colorsetsuccess = colorsetbase; guint32 colorsetwarning = colorsetbase; guint32 colorseterror = colorsetbase; get_highlight_colors(colorsetbase, colorsetsuccess, colorsetwarning, colorseterror); - _symbolic_base_color.setRgba32(colorsetbase); + _symbolic_base_color.setColor(Colors::Color(colorsetbase)); prefs->setUInt("/theme/" + themeiconname + "/symbolicBaseColor", colorsetbase); - _symbolic_base_color.setSensitive(false); + _symbolic_base_color.set_sensitive(false); doChangeIconsColors = true; } else { - _symbolic_base_color.setSensitive(true); + _symbolic_base_color.set_sensitive(true); } if (prefs->getBool("/theme/symbolicDefaultHighColors", true)) { @@ -1272,29 +1263,26 @@ void InkscapePreferences::resetIconsColors(bool themechange) auto const success_color = _symbolic_success_color.get_color(); auto const warning_color = _symbolic_warning_color.get_color(); auto const error_color = _symbolic_error_color .get_color(); - SPColor success_color_sp(success_color.get_red(), success_color.get_green(), success_color.get_blue()); - SPColor warning_color_sp(warning_color.get_red(), warning_color.get_green(), warning_color.get_blue()); - SPColor error_color_sp(error_color.get_red(), error_color.get_green(), error_color.get_blue()); //we copy base to not use - guint32 colorsetbase = success_color_sp.toRGBA32(success_color.get_alpha()); - guint32 colorsetsuccess = success_color_sp.toRGBA32(success_color.get_alpha()); - guint32 colorsetwarning = warning_color_sp.toRGBA32(warning_color.get_alpha()); - guint32 colorseterror = error_color_sp.toRGBA32(error_color.get_alpha()); + guint32 colorsetbase = to_guint32(success_color); + guint32 colorsetsuccess = to_guint32(success_color); + guint32 colorsetwarning = to_guint32(warning_color); + guint32 colorseterror = to_guint32(error_color); get_highlight_colors(colorsetbase, colorsetsuccess, colorsetwarning, colorseterror); - _symbolic_success_color.setRgba32(colorsetsuccess); - _symbolic_warning_color.setRgba32(colorsetwarning); - _symbolic_error_color.setRgba32(colorseterror); + _symbolic_success_color.setColor(Colors::Color(colorsetsuccess)); + _symbolic_warning_color.setColor(Colors::Color(colorsetwarning)); + _symbolic_error_color.setColor(Colors::Color(colorseterror)); prefs->setUInt("/theme/" + themeiconname + "/symbolicSuccessColor", colorsetsuccess); prefs->setUInt("/theme/" + themeiconname + "/symbolicWarningColor", colorsetwarning); prefs->setUInt("/theme/" + themeiconname + "/symbolicErrorColor", colorseterror); - _symbolic_success_color.setSensitive(false); - _symbolic_warning_color.setSensitive(false); - _symbolic_error_color.setSensitive(false); + _symbolic_success_color.set_sensitive(false); + _symbolic_warning_color.set_sensitive(false); + _symbolic_error_color.set_sensitive(false); doChangeIconsColors = true; } else { - _symbolic_success_color.setSensitive(true); - _symbolic_warning_color.setSensitive(true); - _symbolic_error_color.setSensitive(true); + _symbolic_success_color.set_sensitive(true); + _symbolic_warning_color.set_sensitive(true); + _symbolic_error_color.set_sensitive(true); } if (doChangeIconsColors) { @@ -1312,10 +1300,10 @@ void InkscapePreferences::changeIconsColors() guint32 colorsetsuccess = prefs->getUInt("/theme/" + themeiconname + "/symbolicSuccessColor", 0x4AD589ff); guint32 colorsetwarning = prefs->getUInt("/theme/" + themeiconname + "/symbolicWarningColor", 0xF57900ff); guint32 colorseterror = prefs->getUInt("/theme/" + themeiconname + "/symbolicErrorColor", 0xCC0000ff); - _symbolic_base_color.setRgba32(colorsetbase); - _symbolic_success_color.setRgba32(colorsetsuccess); - _symbolic_warning_color.setRgba32(colorsetwarning); - _symbolic_error_color.setRgba32(colorseterror); + _symbolic_base_color.setColor(Colors::Color(colorsetbase)); + _symbolic_success_color.setColor(Colors::Color(colorsetsuccess)); + _symbolic_warning_color.setColor(Colors::Color(colorsetwarning)); + _symbolic_error_color.setColor(Colors::Color(colorseterror)); auto const &colorize_provider = INKSCAPE.themecontext->getColorizeProvider(); if (!colorize_provider) return; @@ -1507,18 +1495,18 @@ void InkscapePreferences::symbolicThemeCheck() } else { changeIconsColors(); } - guint32 colorsetbase = prefs->getUInt("/theme/" + themeiconname + "/symbolicBaseColor", 0x2E3436ff); - guint32 colorsetsuccess = prefs->getUInt("/theme/" + themeiconname + "/symbolicSuccessColor", 0x4AD589ff); - guint32 colorsetwarning = prefs->getUInt("/theme/" + themeiconname + "/symbolicWarningColor", 0xF57900ff); - guint32 colorseterror = prefs->getUInt("/theme/" + themeiconname + "/symbolicErrorColor", 0xCC0000ff); + auto colorsetbase = prefs->getColor("/theme/" + themeiconname + "/symbolicBaseColor", "#2E3436ff"); + auto colorsetsuccess = prefs->getColor("/theme/" + themeiconname + "/symbolicSuccessColor", "#4AD589ff"); + auto colorsetwarning = prefs->getColor("/theme/" + themeiconname + "/symbolicWarningColor", "#F57900ff"); + auto colorseterror = prefs->getColor("/theme/" + themeiconname + "/symbolicErrorColor", "#CC0000ff"); _symbolic_base_color.init(_("Color for symbolic icons:"), "/theme/" + themeiconname + "/symbolicBaseColor", - colorsetbase); + colorsetbase.toString()); _symbolic_success_color.init(_("Color for symbolic success icons:"), - "/theme/" + themeiconname + "/symbolicSuccessColor", colorsetsuccess); + "/theme/" + themeiconname + "/symbolicSuccessColor", colorsetsuccess.toString()); _symbolic_warning_color.init(_("Color for symbolic warning icons:"), - "/theme/" + themeiconname + "/symbolicWarningColor", colorsetwarning); + "/theme/" + themeiconname + "/symbolicWarningColor", colorsetwarning.toString()); _symbolic_error_color.init(_("Color for symbolic error icons:"), - "/theme/" + themeiconname + "/symbolicErrorColor", colorseterror); + "/theme/" + themeiconname + "/symbolicErrorColor", colorseterror.toString()); } } @@ -1846,13 +1834,13 @@ void InkscapePreferences::initPageUI() _symbolic_highlight_colors.signal_toggled().connect(sigc::mem_fun(*this, &InkscapePreferences::resetIconsColorsWrapper)); _page_theme.add_line(true, "", _symbolic_highlight_colors, "", "", true); _symbolic_base_color.init(_("Color for symbolic icons:"), "/theme/" + themeiconname + "/symbolicBaseColor", - 0x2E3436ff); + "#2E3436ff"); _symbolic_success_color.init(_("Color for symbolic success icons:"), - "/theme/" + themeiconname + "/symbolicSuccessColor", 0x4AD589ff); + "/theme/" + themeiconname + "/symbolicSuccessColor", "#4AD589ff"); _symbolic_warning_color.init(_("Color for symbolic warning icons:"), - "/theme/" + themeiconname + "/symbolicWarningColor", 0xF57900ff); + "/theme/" + themeiconname + "/symbolicWarningColor", "#F57900ff"); _symbolic_error_color.init(_("Color for symbolic error icons:"), "/theme/" + themeiconname + "/symbolicErrorColor", - 0xCC0000ff); + "#CC0000ff"); _symbolic_base_color.add_css_class("system_base_color"); _symbolic_success_color.add_css_class("system_success_color"); _symbolic_warning_color.add_css_class("system_warning_color"); @@ -2110,6 +2098,11 @@ void InkscapePreferences::initPageUI() this->AddPage(_page_windows, _("Windows"), iter_ui, PREFS_PAGE_UI_WINDOWS); + // default colors in RGBA + static auto GRID_DEFAULT_MAJOR_COLOR = "#0099e54d"; + static auto GRID_DEFAULT_MINOR_COLOR = "#0099e526"; + static auto GRID_DEFAULT_BLOCK_COLOR = "#0047cb4d"; + // Color pickers _compact_colorselector.init(_("Use compact color selector mode switch"), "/colorselector/switcher", true); _page_color_pickers.add_line(false, "", _compact_colorselector, "", _("Use compact combo box for selecting color modes"), false); @@ -2275,10 +2268,9 @@ static void profileComboChanged( Gtk::ComboBoxText* combo ) } else { Glib::ustring active = combo->get_active_text(); - auto cms_system = Inkscape::CMSSystem::get(); - Glib::ustring path = cms_system->get_path_for_profile(active); - if ( !path.empty() ) { - prefs->setString("/options/displayprofile/uri", path); + auto &cms_system = Inkscape::Colors::CMS::System::get(); + if (auto profile = cms_system.getProfile(active)) { + prefs->setString("/options/displayprofile/uri", profile->getPath()); } } } @@ -2286,12 +2278,10 @@ static void profileComboChanged( Gtk::ComboBoxText* combo ) static void proofComboChanged( Gtk::ComboBoxText* combo ) { Glib::ustring active = combo->get_active_text(); - auto cms_system = Inkscape::CMSSystem::get(); - Glib::ustring path = cms_system->get_path_for_profile(active); - - if ( !path.empty() ) { + auto &cms_system = Inkscape::Colors::CMS::System::get(); + if (auto profile = cms_system.getProfile(active)) { Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - prefs->setString("/options/softproof/uri", path); + prefs->setString("/options/softproof/uri", profile->getPath()); } } @@ -2442,7 +2432,7 @@ void InkscapePreferences::initPageIO() _page_cms.add_group_header( _("Display adjustment")); Glib::ustring tmpStr; - for (auto const &path : Inkscape::CMSSystem::get_directory_paths()) { + for (auto const &path : Inkscape::Colors::CMS::System::get().getDirectoryPaths()) { tmpStr += "\n"; tmpStr += path.first; } @@ -2491,16 +2481,14 @@ void InkscapePreferences::initPageIO() _("Enables black point compensation"), false); { - auto cms_system = Inkscape::CMSSystem::get(); - std::vector names = cms_system->get_monitor_profile_names(); - Glib::ustring current = prefs->getString( "/options/displayprofile/uri" ); + auto &cms_system = Inkscape::Colors::CMS::System::get(); + std::string current = prefs->getString( "/options/displayprofile/uri" ); gint index = 0; _cms_display_profile.append(_("")); index++; - for (auto const &name : names) { - _cms_display_profile.append( name ); - Glib::ustring path = cms_system->get_path_for_profile(name); - if ( !path.empty() && path == current ) { + for (auto profile : cms_system.getDisplayProfiles()) { + _cms_display_profile.append(profile->getName()); + if (profile->getPath() == current) { _cms_display_profile.set_active(index); } index++; @@ -2509,13 +2497,11 @@ void InkscapePreferences::initPageIO() _cms_display_profile.set_active(0); } - names = cms_system->get_softproof_profile_names(); current = prefs->getString("/options/softproof/uri"); index = 0; - for (auto const &name : names) { - _cms_proof_profile.append( name ); - Glib::ustring path = cms_system->get_path_for_profile(name); - if ( !path.empty() && path == current ) { + for (auto profile : cms_system.getOutputProfiles()) { + _cms_proof_profile.append(profile->getName()); + if (profile->getPath() == current) { _cms_proof_profile.set_active(index); } index++; diff --git a/src/ui/dialog/layer-properties.cpp b/src/ui/dialog/layer-properties.cpp index 8f2b6069c1..b838cfbb31 100644 --- a/src/ui/dialog/layer-properties.cpp +++ b/src/ui/dialog/layer-properties.cpp @@ -183,8 +183,7 @@ void LayerPropertiesDialog::_doRename() SPGroup *activeLayer = layman.currentLayer(); if (activeLayer && !activeLayer->isHighlightSet()) { - guint32 color = activeLayer->highlight_color(); - activeLayer->setHighlight(color); + activeLayer->setHighlight(activeLayer->highlight_color()); } layman.renameLayer(layman.currentLayer(), name.c_str(), false); diff --git a/src/ui/dialog/livepatheffect-editor.cpp b/src/ui/dialog/livepatheffect-editor.cpp index 5d7678a174..d4feb6a703 100644 --- a/src/ui/dialog/livepatheffect-editor.cpp +++ b/src/ui/dialog/livepatheffect-editor.cpp @@ -481,6 +481,7 @@ LivePathEffectEditor::selection_info() SPItem * selected = nullptr; _LPESelectionInfo.set_visible(false); if (selection && (selected = selection->singleItem()) ) { + auto highlight = selected->highlight_color().toRGBA(); if (is(selected) || is(selected)) { _LPESelectionInfo.set_text(_("Text objects do not support Live Path Effects")); _LPESelectionInfo.set_visible(true); @@ -489,7 +490,7 @@ LivePathEffectEditor::selection_info() auto const selectbutton = Gtk::make_managed(); auto const boxc = Gtk::make_managed(); auto const lbl = Gtk::make_managed(labeltext); - auto const type = get_shape_image("group", selected->highlight_color()); + auto const type = get_shape_image("group", highlight); UI::pack_start(*boxc, *type, false, false); UI::pack_start(*boxc, *lbl, false, false); type->set_margin_start(4); @@ -504,7 +505,7 @@ LivePathEffectEditor::selection_info() auto const selectbutton2 = Gtk::make_managed(); auto const boxc2 = Gtk::make_managed(); auto const lbl2 = Gtk::make_managed(labeltext2); - auto const type2 = get_shape_image("clone", selected->highlight_color()); + auto const type2 = get_shape_image("clone", highlight); UI::pack_start(*boxc2, *type2, false, false); UI::pack_start(*boxc2, *lbl2, false, false); type2->set_margin_start(4); @@ -523,7 +524,7 @@ LivePathEffectEditor::selection_info() auto const boxc = Gtk::make_managed(); auto const lbl = Gtk::make_managed(labeltext); lbl->set_ellipsize(Pango::EllipsizeMode::END); - auto const type = get_shape_image(selected->typeName(), selected->highlight_color()); + auto const type = get_shape_image(selected->typeName(), highlight); UI::pack_start(*boxc, *type, false, false); UI::pack_start(*boxc, *lbl, false, false); _LPECurrentItem.append(*boxc); @@ -546,7 +547,7 @@ LivePathEffectEditor::selection_info() auto const selectbutton = Gtk::make_managed(); auto const boxc = Gtk::make_managed(); auto const lbl = Gtk::make_managed(labeltext); - auto const type = get_shape_image(selected->typeName(), selected->highlight_color()); + auto const type = get_shape_image(selected->typeName(), highlight); UI::pack_start(*boxc, *type, false, false); UI::pack_start(*boxc, *lbl, false, false); type->set_margin_start(4); diff --git a/src/ui/dialog/object-properties.cpp b/src/ui/dialog/object-properties.cpp index b1ab365afb..f70af2ce65 100644 --- a/src/ui/dialog/object-properties.cpp +++ b/src/ui/dialog/object-properties.cpp @@ -65,7 +65,7 @@ ObjectProperties::ObjectProperties() , _label_title(_("_Title:"), true) , _label_dpi(_("_DPI SVG:"), true) , _label_color(_("Highlight Color:"), true) - , _highlight_color(_("Highlight Color"), "", 0xff0000ff, true) + , _highlight_color(_("Highlight Color"), "", Colors::Color(0xff0000ff), true) , _cb_hide(_("_Hide"), true) , _cb_lock(_("L_ock"), true) , _cb_aspect_ratio(_("Preserve Ratio"), true) @@ -334,7 +334,7 @@ void ObjectProperties::update_entries() _cb_aspect_ratio.set_active(item && g_strcmp0(item->getAttribute("preserveAspectRatio"), "none") != 0); _cb_lock.set_active(item && item->isLocked()); /* Sensitive */ _cb_hide.set_active(item && item->isExplicitlyHidden()); /* Hidden */ - _highlight_color.setRgba32(item ? item->highlight_color() : 0x0); + _highlight_color.setColor(item ? item->highlight_color() : Colors::Color(0x000000ff)); _highlight_color.closeWindow(); // hide aspect ratio checkbox for an image element, images have their own panel in object attributes; // apart from only , , and support this attribute, but we don't handle them @@ -471,13 +471,13 @@ void ObjectProperties::_labelChanged() _blocked = false; } -void ObjectProperties::_highlightChanged(guint rgba) +void ObjectProperties::_highlightChanged(Colors::Color const &color) { if (_blocked) return; if (auto item = getSelection()->singleItem()) { - item->setHighlight(rgba); + item->setHighlight(color); DocumentUndo::done(getDocument(), _("Set item highlight color"), INKSCAPE_ICON("dialog-object-properties")); } } diff --git a/src/ui/dialog/object-properties.h b/src/ui/dialog/object-properties.h index 82b9b3933e..830394972f 100644 --- a/src/ui/dialog/object-properties.h +++ b/src/ui/dialog/object-properties.h @@ -105,7 +105,7 @@ private: void _labelChanged(); // Callback for highlight color - void _highlightChanged(guint rgba); + void _highlightChanged(Colors::Color const &color); /// Callback for checkbox Lock. void _sensitivityToggled(); diff --git a/src/ui/dialog/objects.cpp b/src/ui/dialog/objects.cpp index 93df690002..72800e5713 100644 --- a/src/ui/dialog/objects.cpp +++ b/src/ui/dialog/objects.cpp @@ -353,7 +353,7 @@ void ObjectWatcher::updateRowHighlight() { if (auto item = cast(panel->getObject(node))) { auto row = *panel->_store->get_iter(row_ref.get_path()); - auto new_color = item->highlight_color(); + auto new_color = item->highlight_color().toRGBA(); if (new_color != row[panel->_model->_colIconColor]) { row[panel->_model->_colIconColor] = new_color; updateRowBg(new_color); @@ -695,7 +695,7 @@ ObjectsPanel::ObjectsPanel() , _layer(nullptr) , _is_editing(false) , _page(Gtk::Orientation::VERTICAL) - , _color_picker(_("Highlight color"), "", 0, true) + , _color_picker(_("Highlight color"), "", Colors::Color(0x000000ff), true) , _builder(create_builder("dialog-objects.glade")) , _settings_menu(get_widget(_builder, "settings-menu")) , _object_menu(get_widget(_builder, "object-menu")) @@ -905,14 +905,14 @@ ObjectsPanel::ObjectsPanel() _clicked_item_row = *_store->get_iter(path); if (auto item = getItem(_clicked_item_row)) { // find object's color - _color_picker.setRgba32(item->highlight_color()); + _color_picker.setColor(item->highlight_color()); _color_picker.open(); } }); - _color_picker.connectChanged([this](guint rgba) { + _color_picker.connectChanged([this](Colors::Color const &color) { if (auto item = getItem(_clicked_item_row)) { - item->setHighlight(rgba); + item->setHighlight(color); DocumentUndo::maybeDone(getDocument(), "highlight-color", _("Set item highlight color"), INKSCAPE_ICON("dialog-object-properties")); } }); @@ -1706,8 +1706,7 @@ void ObjectsPanel::_handleEdited(const Glib::ustring& path, const Glib::ustring& if (!new_text.empty() && (!item->label() || new_text != item->label())) { auto obj = cast(item); if (obj && obj->layerMode() == SPGroup::LAYER && !obj->isHighlightSet()) { - guint32 color = obj->highlight_color(); - obj->setHighlight(color); + obj->setHighlight(obj->highlight_color()); } item->setLabel(new_text.c_str()); DocumentUndo::done(getDocument(), _("Rename object"), ""); diff --git a/src/ui/dialog/print.cpp b/src/ui/dialog/print.cpp index 084b218d32..d8322c649f 100644 --- a/src/ui/dialog/print.cpp +++ b/src/ui/dialog/print.cpp @@ -26,10 +26,11 @@ #include "document.h" #include "object/sp-page.h" +#include "colors/manager.h" +#include "colors/color.h" #include "util/units.h" #include "helper/png-write.h" #include "page-manager.h" -#include "svg/svg-color.h" #include @@ -181,7 +182,10 @@ void Print::draw_page(const Glib::RefPtr& context, int page_n guint32 bgcolor = 0x00000000; Inkscape::XML::Node *nv = _workaround._doc->getReprNamedView(); if (nv && nv->attribute("pagecolor")){ - bgcolor = sp_svg_read_color(nv->attribute("pagecolor"), 0xffffff00); + if (auto c = Colors::Color::parse(nv->attribute("pagecolor"))) { + // TODO allow page color to be any color space, not just RGB + bgcolor = c->toRGBA(); + } } if (nv && nv->attribute("inkscape:pageopacity")){ double opacity = nv->getAttributeDouble("inkscape:pageopacity", 1.0); diff --git a/src/ui/dialog/startup.cpp b/src/ui/dialog/startup.cpp index d62993b346..26e7899ae6 100644 --- a/src/ui/dialog/startup.cpp +++ b/src/ui/dialog/startup.cpp @@ -34,8 +34,6 @@ #include #include -#include "color.h" -#include "color-rgba.h" #include "inkscape-application.h" #include "inkscape.h" #include "inkscape-version.h" @@ -136,19 +134,6 @@ class ThemeCols: public Gtk::TreeModel::ColumnRecord { Gtk::TreeModelColumn enabled; }; -/** - * Color is store as a string in the form #RRGGBBAA, '0' means "unset" - * - * @param color - The string color from glade. - */ -unsigned int get_color_value(const Glib::ustring color) -{ - Gdk::RGBA gdk_color = Gdk::RGBA(color); - ColorRGBA sp_color(gdk_color.get_red(), gdk_color.get_green(), - gdk_color.get_blue(), gdk_color.get_alpha()); - return sp_color.getIntValue(); -} - using Inkscape::UI::Widget::TemplateList; StartScreen::StartScreen() @@ -622,21 +607,18 @@ StartScreen::canvas_changed() prefs->setString("/options/boot/canvas", row[cols.id]); Gdk::RGBA gdk_color = Gdk::RGBA(row[cols.pagecolor]); - SPColor sp_color(gdk_color.get_red(), gdk_color.get_green(), gdk_color.get_blue()); - prefs->setString("/template/base/pagecolor", sp_color.toString()); + prefs->setString("/template/base/pagecolor", gdk_to_css_color(gdk_color)); prefs->setDouble("/template/base/pageopacity", gdk_color.get_alpha()); Gdk::RGBA gdk_border = Gdk::RGBA(row[cols.bordercolor]); - SPColor sp_border(gdk_border.get_red(), gdk_border.get_green(), gdk_border.get_blue()); - prefs->setString("/template/base/bordercolor", sp_border.toString()); + prefs->setString("/template/base/bordercolor", gdk_to_css_color(gdk_border)); prefs->setDouble("/template/base/borderopacity", gdk_border.get_alpha()); prefs->setBool("/template/base/pagecheckerboard", row[cols.checkered]); prefs->setInt("/template/base/pageshadow", row[cols.shadow] ? 2 : 0); Gdk::RGBA gdk_desk = Gdk::RGBA(row[cols.deskcolor]); - SPColor sp_desk(gdk_desk.get_red(), gdk_desk.get_green(), gdk_desk.get_blue()); - prefs->setString("/template/base/deskcolor", sp_desk.toString()); + prefs->setString("/template/base/deskcolor", gdk_to_css_color(gdk_desk)); } catch(int e) { g_warning("Couldn't find canvas value."); } diff --git a/src/ui/dialog/styledialog.cpp b/src/ui/dialog/styledialog.cpp index 0bd9d72b57..821056927e 100644 --- a/src/ui/dialog/styledialog.cpp +++ b/src/ui/dialog/styledialog.cpp @@ -42,6 +42,8 @@ #include #include +#include "colors/manager.h" +#include "colors/color.h" #include "attribute-rel-svg.h" #include "attributes.h" #include "document.h" @@ -54,7 +56,6 @@ #include "io/resource.h" #include "object/sp-object.h" -#include "svg/svg-color.h" #include "ui/builder-utils.h" #include "ui/controller.h" #include "ui/icon-loader.h" @@ -735,11 +736,10 @@ void StyleDialog::readStyleElement() break; } } - guint32 r1 = 0; // if there's no color, return black - r1 = sp_svg_read_color(value.c_str(), r1); - guint32 r2 = 0; // if there's no color, return black - r2 = sp_svg_read_color(val.c_str(), r2); - if ((r1 == 0 || r1 != r2) && value != val || attr_prop.count(name)) { + + auto r1 = Colors::Color::parse(value); + auto r2 = Colors::Color::parse(val); + if (((!r1 || *r1 != *r2) && value != val) || attr_prop.count(name)) { row[_mColumns._colStrike] = true; } else { row[_mColumns._colOwner] = _("Current value"); diff --git a/src/ui/dialog/swatches.cpp b/src/ui/dialog/swatches.cpp index a8cd719c38..1aec3aa09c 100644 --- a/src/ui/dialog/swatches.cpp +++ b/src/ui/dialog/swatches.cpp @@ -57,7 +57,14 @@ #include "ui/widget/color-palette-preview.h" #include "ui/widget/popover-menu-item.h" #include "util/variant-visitor.h" -#include "widgets/paintdef.h" + +namespace Inkscape::Colors { + std::size_t hash_value(Color const& b) + { + boost::hash hasher; + return hasher(b.toRGBA()); + } +} namespace Inkscape::UI::Dialog { @@ -228,7 +235,7 @@ void SwatchesPanel::set_palette(const Glib::ustring& id) { select_palette(id); } -const PaletteFileData* SwatchesPanel::get_palette(const Glib::ustring& id) { +const PaletteFileData *SwatchesPanel::get_palette(const Glib::ustring& id) { if (auto p = GlobalPalettes::get().find_palette(id)) return p; if (_loaded_palette.id == id) return &_loaded_palette; @@ -375,19 +382,6 @@ bool SwatchesPanel::update_isswatch() return modified; } -static auto spcolor_to_rgb(SPColor const &color) -{ - float rgbf[3]; - color.get_rgb_floatv(rgbf); - - std::array rgb; - for (int i = 0; i < 3; i++) { - rgb[i] = SP_COLOR_F_TO_U(rgbf[i]); - }; - - return rgb; -} - void SwatchesPanel::update_fillstroke_indicators() { auto doc = getDocument(); @@ -413,7 +407,7 @@ void SwatchesPanel::update_fillstroke_indicators() if (attr->isNone()) { return std::monostate{}; } else if (attr->isColor()) { - return spcolor_to_rgb(attr->value.color); + return attr->getColor(); } else if (attr->isPaintserver()) { if (auto grad = cast(fill ? style.getFillPaintServer() : style.getStrokePaintServer())) { if (grad->isSwatch()) { @@ -460,9 +454,9 @@ void SwatchesPanel::update_fillstroke_indicators() palette.id = p.id; for (auto const &c : p.colors) { std::visit(VariantVisitor { - [&](const PaletteFileData::Color& c) { - auto [r, g, b] = c.rgb; - palette.colors.push_back({r / 255.0, g / 255.0, b / 255.0}); + [&](const Colors::Color& c) { + auto rgb = *c.converted(Colors::Space::Type::RGB); + palette.colors.push_back({rgb[0], rgb[1], rgb[2]}); }, [](const PaletteFileData::SpacerItem&) {}, [](const PaletteFileData::GroupStart&) {} @@ -510,7 +504,7 @@ void SwatchesPanel::rebuild() current_stroke.clear(); // Add the "remove-color" color. - auto const w = Gtk::make_managed(PaintDef(), this); + auto const w = Gtk::make_managed(this); w->set_pinned_pref(_prefs_path); palette.emplace_back(w); widgetmap.emplace(std::monostate{}, w); @@ -527,10 +521,10 @@ void SwatchesPanel::rebuild() [](const PaletteFileData::GroupStart& g) { return Gtk::make_managed(g.name); }, - [=, this](const PaletteFileData::Color& c) { - auto w = Gtk::make_managed(PaintDef(c.rgb, c.name, c.definition), dialog); + [=, this](const Colors::Color& c) { + auto w = Gtk::make_managed(c, dialog); w->set_pinned_pref(_prefs_path); - widgetmap.emplace(c.rgb, w); + widgetmap.emplace(c, w); return w; }, }, c); diff --git a/src/ui/dialog/swatches.h b/src/ui/dialog/swatches.h index dba066f837..8f81da54d8 100644 --- a/src/ui/dialog/swatches.h +++ b/src/ui/dialog/swatches.h @@ -23,6 +23,7 @@ #include #include +#include "colors/color.h" #include "preferences.h" // PrefObserver #include "ui/dialog/dialog-base.h" #include "ui/dialog/global-palettes.h" @@ -38,6 +39,11 @@ class ToggleButton; class SPGradient; +// Allow boost to map with colors +namespace Inkscape::Colors { + std::size_t hash_value(Color const& b); +} // namespace Inkscape::Colors + namespace Inkscape::UI { namespace Widget { @@ -90,7 +96,7 @@ private: Glib::ustring _current_palette_id; void set_palette(const Glib::ustring& id); void select_palette(const Glib::ustring& id); - const PaletteFileData* get_palette(const Glib::ustring& id); + const PaletteFileData *get_palette(const Glib::ustring& id); // Asynchronous update mechanism. sigc::connection conn_gradients; @@ -108,7 +114,7 @@ private: // A map from colors to their respective widgets. Used to quickly find the widgets corresponding // to the current fill/stroke color, in order to update their fill/stroke indicators. - using ColorKey = std::variant, SPGradient *>; + using ColorKey = std::variant; boost::unordered_multimap widgetmap; // need boost for array hash std::vector current_fill; std::vector current_stroke; diff --git a/src/ui/drag-and-drop.cpp b/src/ui/drag-and-drop.cpp index 0ded741b57..149973df93 100644 --- a/src/ui/drag-and-drop.cpp +++ b/src/ui/drag-and-drop.cpp @@ -13,6 +13,7 @@ #include "drag-and-drop.h" +#include #include #include #include @@ -29,6 +30,7 @@ #include "style.h" #include "layer-manager.h" +#include "colors/dragndrop.h" #include "extension/find_extension_by_mime.h" #include "object/sp-shape.h" @@ -37,16 +39,12 @@ #include "path/path-util.h" -#include "svg/svg-color.h" // write color - #include "ui/clipboard.h" #include "ui/interface.h" #include "ui/tools/tool-base.h" #include "ui/widget/canvas.h" // Target, canvas to world transform. #include "ui/widget/desktop-widget.h" -#include "widgets/paintdef.h" - using Inkscape::DocumentUndo; namespace { @@ -196,19 +194,19 @@ GValue from_bytes(Glib::RefPtr &&bytes, char const *) } template <> -GValue from_bytes(Glib::RefPtr &&bytes, char const *mime_type) +GValue from_bytes(Glib::RefPtr &&bytes, char const *mime_type) { - PaintDef result; - if (!result.fromMIMEData(mime_type, get_span(bytes))) { - throw Glib::Error(G_FILE_ERROR, 0, "Failed to parse colour"); + try { + return make_value(Colors::fromMIMEData(get_span(bytes), mime_type)); + } catch (Colors::ColorError const &c) { + throw Glib::Error(G_FILE_ERROR, 0, c.what()); } - return make_value(std::move(result)); } template <> -Glib::RefPtr to_bytes(PaintDef const &paintdef, char const *mime_type) +Glib::RefPtr to_bytes(Colors::Paint const &paint, char const *mime_type) { - return make_bytes(paintdef.getMIMEData(mime_type)); + return make_bytes(getMIMEData(paint, mime_type)); } template <> @@ -224,18 +222,18 @@ std::vector const &get_drop_types() register_deserializer(mime_type); } - for (auto mime_type : {mimeOSWB_COLOR, mimeX_COLOR}) { - register_deserializer(mime_type); + for (auto mime_type : {Colors::mimeOSWB_COLOR, Colors::mimeX_COLOR}) { + register_deserializer(mime_type); } - for (auto mime_type : {mimeOSWB_COLOR, mimeX_COLOR, mimeTEXT}) { - register_serializer(mime_type); + for (auto mime_type : {Colors::mimeOSWB_COLOR, Colors::mimeX_COLOR, Colors::mimeTEXT}) { + register_serializer(mime_type); } register_serializer("text/plain"); return { - Glib::Value::value_type(), + Glib::Value::value_type(), Glib::Value::value_type(), GDK_TYPE_FILE_LIST, Glib::Value::value_type(), @@ -257,16 +255,17 @@ bool on_drop(Glib::ValueBase const &value, double x, double y, SPDesktopWidget * auto const world_pos = canvas->canvas_to_world(canvas_pos); auto const dt_pos = desktop->w2d(world_pos); - if (auto const paintdef = get(value)) { + if (auto const ptr = get(value)) { + auto paint = *ptr; auto const item = desktop->getItemAtPoint(world_pos, true); if (!item) { return false; } - auto find_gradient = [&] () -> SPGradient * { + auto find_gradient = [&] (Colors::Color const &color) -> SPGradient * { for (auto obj : doc->getResourceList("gradient")) { auto const grad = cast_unsafe(obj); - if (grad->hasStops() && grad->getId() == paintdef->get_description()) { + if (grad->hasStops() && grad->getId() == color.getName()) { return grad; } } @@ -274,16 +273,14 @@ bool on_drop(Glib::ValueBase const &value, double x, double y, SPDesktopWidget * }; std::string colorspec; - if (paintdef->get_type() == PaintDef::NONE) { + if (std::holds_alternative(paint)) { colorspec = "none"; } else { - if (auto const grad = find_gradient()) { + auto &color = std::get(paint); + if (auto const grad = find_gradient(color)) { colorspec = std::string{"url(#"} + grad->getId() + ")"; } else { - auto const [r, g, b] = paintdef->get_rgb(); - colorspec.resize(63); - sp_svg_write_color(colorspec.data(), colorspec.size() + 1, SP_RGBA32_U_COMPOSE(r, g, b, 0xff)); - colorspec.resize(std::strlen(colorspec.c_str())); + colorspec.resize(std::strlen(color.toString().c_str())); } } @@ -324,7 +321,7 @@ bool on_drop(Glib::ValueBase const &value, double x, double y, SPDesktopWidget * } auto const css = sp_repr_css_attr_new(); - sp_repr_css_set_property(css, fillnotstroke ? "fill" : "stroke", colorspec.c_str()); + sp_repr_css_set_property_string(css, fillnotstroke ? "fill" : "stroke", colorspec); sp_desktop_apply_css_recursive(item, css, true); sp_repr_css_attr_unref(css); diff --git a/src/ui/icon-loader.cpp b/src/ui/icon-loader.cpp index 1a327af8ac..5bd73400f7 100644 --- a/src/ui/icon-loader.cpp +++ b/src/ui/icon-loader.cpp @@ -21,6 +21,7 @@ #include #include +#include "colors/color.h" #include "desktop.h" #include "inkscape.h" @@ -57,18 +58,19 @@ namespace Inkscape::UI { auto &color_class = color_classes[rgba_color]; if (!color_class.empty()) return color_class; + auto color = Colors::Color(rgba_color); + // The CSS class is .icon-color-RRGGBBAA - std::string hex(9, '\0'); - std::snprintf(hex.data(), 9, "%08X", rgba_color); - color_class = Glib::ustring::compose("icon-color-%1", hex); - // GTK CSS does not support #RRGGBBAA, so we split it - hex.resize(6); - auto const opacity = (rgba_color & 0xFF) / 255.0; + auto rgba_str = color.toString(); + rgba_str.erase(0, 1); + color_class = Glib::ustring::compose("icon-color-%1", rgba_str); + // Add a persistent CSS provider for that class+color auto const css_provider = Gtk::CssProvider::create(); auto const data = Glib::ustring::compose( - ".symbolic .%1, .regular .%1 { -gtk-icon-style: symbolic; color: #%2; opacity: %3; }", - color_class, hex, opacity); + ".symbolic .%1, .regular .%1 { -gtk-icon-style: symbolic; color: %2; opacity: %3; }", + color_class, color.toString(false), color.getOpacity()); + css_provider->load_from_data(data); // Add it with the needed priority = higher than themes.cpp _colorizeprovider static constexpr auto priority = GTK_STYLE_PROVIDER_PRIORITY_APPLICATION + 1; diff --git a/src/ui/selected-color.cpp b/src/ui/selected-color.cpp deleted file mode 100644 index b43976f0cc..0000000000 --- a/src/ui/selected-color.cpp +++ /dev/null @@ -1,155 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/** @file - * Color selected in color selector widget. - * This file was created during the refactoring of SPColorSelector - *//* - * Authors: - * bulia byak - * Jon A. Cruz - * Tomasz Boczkowski - * - * Copyright (C) 2014 Authors - * Released under GNU GPL v2+, read the file 'COPYING' for more information. - */ - -#include "selected-color.h" - -#include - -#include - -namespace Inkscape { -namespace UI { - -double const SelectedColor::_EPSILON = 1e-4; - -SelectedColor::SelectedColor() - : _color(0) - , _alpha(1.0) - , _held(false) - , _virgin(true) - , _updating(false) -{ - -} - -SelectedColor::~SelectedColor() = default; - -void SelectedColor::setColor(SPColor const &color) -{ - setColorAlpha( color, _alpha); -} - -SPColor SelectedColor::color() const -{ - return _color; -} - -void SelectedColor::setAlpha(gfloat alpha) -{ - g_return_if_fail( ( 0.0 <= alpha ) && ( alpha <= 1.0 ) ); - setColorAlpha( _color, alpha); -} - -gfloat SelectedColor::alpha() const -{ - return _alpha; -} - -void SelectedColor::setValue(guint32 value) -{ - SPColor color(value); - gfloat alpha = SP_RGBA32_A_F(value); - setColorAlpha(color, alpha); -} - -guint32 SelectedColor::value() const -{ - return color().toRGBA32(_alpha); -} - -void SelectedColor::setColorAlpha(SPColor const &color, gfloat alpha, bool emit_signal) -{ -#ifdef DUMP_CHANGE_INFO - g_message("SelectedColor::setColorAlpha( this=%p, %f, %f, %f, %s, %f, %s)", this, color.v.c[0], color.v.c[1], color.v.c[2], (color.icc?color.icc->colorProfile.c_str():""), alpha, (emit_signal?"YES":"no")); -#endif - g_return_if_fail( ( 0.0 <= alpha ) && ( alpha <= 1.0 ) ); - - if (_updating) { - return; - } - -#ifdef DUMP_CHANGE_INFO - g_message("---- SelectedColor::setColorAlpha virgin:%s !close:%s alpha is:%s", - (_virgin?"YES":"no"), - (!color.isClose( _color, _EPSILON )?"YES":"no"), - ((fabs((_alpha) - (alpha)) >= _EPSILON )?"YES":"no") - ); -#endif - - if ( _virgin || !color.isClose( _color, _EPSILON ) || - (fabs((_alpha) - (alpha)) >= _EPSILON )) { - - _virgin = false; - - _color = color; - _alpha = alpha; - - if (emit_signal) - { - _updating = true; - if (_held) { - signal_dragged.emit(); - } else { - signal_changed.emit(); - } - _updating = false; - } - -#ifdef DUMP_CHANGE_INFO - } else { - g_message("++++ SelectedColor::setColorAlpha color:%08x ==> _color:%08X isClose:%s", color.toRGBA32(alpha), _color.toRGBA32(_alpha), - (color.isClose( _color, _EPSILON )?"YES":"no")); -#endif - } -} - -void SelectedColor::colorAlpha(SPColor &color, gfloat &alpha) const { - color = _color; - alpha = _alpha; -} - -void SelectedColor::setHeld(bool held) { - if (_updating) { - return; - } - bool grabbed = held && !_held; - bool released = !held && _held; - - _held = held; - - _updating = true; - if (grabbed) { - signal_grabbed.emit(); - } - - if (released) { - signal_released.emit(); - // signal_changed.emit(); // TODO: signal_changed isn't emitted after dragging! - } - _updating = false; -} - -} -} - -/* - Local Variables: - mode:c++ - c-file-style:"stroustrup" - c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) - indent-tabs-mode:nil - fill-column:99 - End: -*/ -// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/selected-color.h b/src/ui/selected-color.h deleted file mode 100644 index 6010b76045..0000000000 --- a/src/ui/selected-color.h +++ /dev/null @@ -1,100 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/** @file - * Color selected in color selector widget. - * This file was created during the refactoring of SPColorSelector - *//* - * Authors: - * bulia byak - * Jon A. Cruz - * Tomasz Boczkowski - * - * Copyright (C) 2014 Authors - * Released under GNU GPL v2+, read the file 'COPYING' for more information. - */ -#ifndef SEEN_SELECTED_COLOR -#define SEEN_SELECTED_COLOR - -#include -#include -#include - -#include "color.h" - -namespace Gtk -{ - class Widget; -} - -namespace Inkscape { -namespace UI { - -class SelectedColor { -public: - SelectedColor(); - virtual ~SelectedColor(); - - // By default, disallow copy constructor and assignment operator - SelectedColor(SelectedColor const &obj) = delete; - SelectedColor& operator=(SelectedColor const &obj) = delete; - - void setColor(SPColor const &color); - SPColor color() const; - - void setAlpha(gfloat alpha); - gfloat alpha() const; - - void setValue(guint32 value); - guint32 value() const; - - void setColorAlpha(SPColor const &color, gfloat alpha, bool emit_signal = true); - void colorAlpha(SPColor &color, gfloat &alpha) const; - void emitIccChanged() { signal_icc_changed.emit(); } - - void setHeld(bool held); - - sigc::signal signal_grabbed; - sigc::signal signal_dragged; - sigc::signal signal_released; - sigc::signal signal_changed; - sigc::signal signal_icc_changed; - -private: - SPColor _color; - /** - * Color alpha value guaranteed to be in [0, 1]. - */ - gfloat _alpha; - - bool _held; - /** - * This flag is true if no color is set yet - */ - bool _virgin; - - bool _updating; - - static double const _EPSILON; -}; - -class ColorSelectorFactory { -public: - virtual ~ColorSelectorFactory() = default; - - virtual Gtk::Widget* createWidget(SelectedColor &color, bool no_alpha) const = 0; - virtual Glib::ustring modeName() const = 0; -}; - -} -} - -#endif -/* - Local Variables: - mode:c++ - c-file-style:"stroustrup" - c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) - indent-tabs-mode:nil - fill-column:99 - End: -*/ -// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/svg-renderer.cpp b/src/ui/svg-renderer.cpp index d851f7a4de..a62acb832c 100644 --- a/src/ui/svg-renderer.cpp +++ b/src/ui/svg-renderer.cpp @@ -33,33 +33,6 @@ namespace Inkscape { -Glib::ustring rgba_to_css_color(double r, double g, double b) { - char buffer[16]; - safeprintf(buffer, "#%02x%02x%02x", - static_cast(r * 0xff + 0.5), - static_cast(g * 0xff + 0.5), - static_cast(b * 0xff + 0.5) - ); - return Glib::ustring(buffer); -} - -Glib::ustring rgba_to_css_color(const Gdk::RGBA& color) { - return rgba_to_css_color(color.get_red(), color.get_green(), color.get_blue()); -} - -Glib::ustring rgba_to_css_color(const SPColor& color) { - float rgb[3]; - color.get_rgb_floatv(rgb); - return rgba_to_css_color(rgb[0], rgb[1], rgb[2]); -} - -Glib::ustring double_to_css_value(double value) { - char buffer[32]; - // arbitrarily chosen precision - safeprintf(buffer, "%.4f", value); - return Glib::ustring(buffer); -} - template static auto ensure_nonnull(T &&t, char const *message) { diff --git a/src/ui/svg-renderer.h b/src/ui/svg-renderer.h index f7c0e352ea..3a2d391099 100644 --- a/src/ui/svg-renderer.h +++ b/src/ui/svg-renderer.h @@ -10,7 +10,7 @@ #include #include -#include "color.h" +#include "colors/color.h" class SPDocument; class SPRoot; @@ -25,19 +25,10 @@ class ustring; namespace Gdk { class Pixbuf; -class RGBA; } // namespace Gdk namespace Inkscape { -// utilities for set_style: -// Gdk color to CSS rgb (no alpha) -Glib::ustring rgba_to_css_color(const Gdk::RGBA& color); -Glib::ustring rgba_to_css_color(const SPColor& color); -Glib::ustring rgba_to_css_color(double r, double g, double b); -// double to low precision string -Glib::ustring double_to_css_value(double value); - class Pixbuf; class svg_renderer diff --git a/src/ui/syntax.cpp b/src/ui/syntax.cpp index b38f902ad2..205c830d8a 100644 --- a/src/ui/syntax.cpp +++ b/src/ui/syntax.cpp @@ -18,7 +18,6 @@ #include #include -#include "color.h" #include "config.h" #include "io/resource.h" #include "object/sp-factory.h" diff --git a/src/ui/syntax.h b/src/ui/syntax.h index 35917351e1..c9cd280be1 100644 --- a/src/ui/syntax.h +++ b/src/ui/syntax.h @@ -19,8 +19,6 @@ #include #include -#include "color.h" - namespace Inkscape::UI::Syntax { /** The style of a single element in a (Pango markup)-enabled widget. */ diff --git a/src/ui/themes.cpp b/src/ui/themes.cpp index d1182910d2..1a3dc0681c 100644 --- a/src/ui/themes.cpp +++ b/src/ui/themes.cpp @@ -36,10 +36,10 @@ #include "desktop.h" #include "inkscape.h" #include "preferences.h" +#include "colors/utils.h" #include "io/resource.h" #include "object/sp-item-group.h" // set_default_highlight_colors #include "svg/css-ostringstream.h" -#include "svg/svg-color.h" #include "ui/dialog/dialog-manager.h" #include "ui/dialog/dialog-window.h" #include "ui/util.h" @@ -149,11 +149,6 @@ Glib::ustring ThemeContext::get_symbolic_colors() { Glib::ustring css_str; - gchar colornamed[64]; - gchar colornamedsuccess[64]; - gchar colornamedwarning[64]; - gchar colornamederror[64]; - gchar colornamed_inverse[64]; Inkscape::Preferences *prefs = Inkscape::Preferences::get(); Glib::ustring themeiconname = prefs->getString("/theme/iconTheme", prefs->getString("/theme/defaultIconTheme", "")); guint32 colorsetbase = 0x2E3436ff; @@ -165,15 +160,10 @@ ThemeContext::get_symbolic_colors() colorsetsuccess = prefs->getUInt("/theme/" + themeiconname + "/symbolicSuccessColor", colorsetsuccess); colorsetwarning = prefs->getUInt("/theme/" + themeiconname + "/symbolicWarningColor", colorsetwarning); colorseterror = prefs->getUInt("/theme/" + themeiconname + "/symbolicErrorColor", colorseterror); - sp_svg_write_color(colornamed, sizeof(colornamed), colorsetbase); - sp_svg_write_color(colornamedsuccess, sizeof(colornamedsuccess), colorsetsuccess); - sp_svg_write_color(colornamedwarning, sizeof(colornamedwarning), colorsetwarning); - sp_svg_write_color(colornamederror, sizeof(colornamederror), colorseterror); colorsetbase_inverse = colorsetbase ^ 0xffffff00; - sp_svg_write_color(colornamed_inverse, sizeof(colornamed_inverse), colorsetbase_inverse); - css_str += "@define-color warning_color " + Glib::ustring(colornamedwarning) + ";\n"; - css_str += "@define-color error_color " + Glib::ustring(colornamederror) + ";\n"; - css_str += "@define-color success_color " + Glib::ustring(colornamedsuccess) + ";\n"; + css_str += "@define-color warning_color " + Inkscape::Colors::rgba_to_hex(colorsetwarning) + ";\n"; + css_str += "@define-color error_color " + Inkscape::Colors::rgba_to_hex(colorseterror) + ";\n"; + css_str += "@define-color success_color " + Inkscape::Colors::rgba_to_hex(colorsetsuccess) + ";\n"; /* ":not(.rawstyle) > image" works only on images in first level of widget container if in the future we use a complex widget with more levels and we dont want to tweak the color here, retaining default we can add more lines like ":not(.rawstyle) > > image" @@ -184,7 +174,7 @@ ThemeContext::get_symbolic_colors() css_str += ":not(.rawstyle) > image:not(.arrow),"; css_str += ":not(.rawstyle) treeview.image"; css_str += "{color:"; - css_str += colornamed; + css_str += Inkscape::Colors::rgba_to_hex(colorsetbase); css_str += ";}"; } css_str += ".dark .forcebright :not(.rawstyle) > image,"; @@ -199,7 +189,7 @@ ThemeContext::get_symbolic_colors() css_str += ".inverse image:not(.rawstyle)"; css_str += "{color:"; if (overridebasecolor) { - css_str += colornamed_inverse; + css_str += Inkscape::Colors::rgba_to_hex(colorsetbase_inverse); } else { // we override base color in this special cases using inverse color css_str += "@theme_bg_color"; diff --git a/src/ui/tool/multi-path-manipulator.cpp b/src/ui/tool/multi-path-manipulator.cpp index c4d7938f09..edb83e7d37 100644 --- a/src/ui/tool/multi-path-manipulator.cpp +++ b/src/ui/tool/multi-path-manipulator.cpp @@ -182,7 +182,7 @@ void MultiPathManipulator::setItems(std::set const &s) auto lpobj = cast(r.object); if (!is(r.object) && !lpobj) continue; std::shared_ptr newpm(new PathManipulator(*this, (SPPath*) r.object, - r.edit_transform, _getOutlineColor(r.role, r.object), r.lpe_key)); + r.edit_transform, _getOutlineColor(r.role, r.object).toRGBA(), r.lpe_key)); newpm->showHandles(_show_handles); // always show outlines for clips and masks newpm->showOutline(_show_outline || r.role != SHAPE_ROLE_NORMAL); @@ -889,19 +889,19 @@ void MultiPathManipulator::_doneWithCleanup(gchar const *reason, bool alert_LPE) } /** Get an outline color based on the shape's role (normal, mask, LPE parameter, etc.). */ -guint32 MultiPathManipulator::_getOutlineColor(ShapeRole role, SPObject *object) +Colors::Color MultiPathManipulator::_getOutlineColor(ShapeRole role, SPObject *object) { Inkscape::Preferences *prefs = Inkscape::Preferences::get(); switch(role) { case SHAPE_ROLE_CLIPPING_PATH: - return prefs->getColor("/tools/nodes/clipping_path_color", 0x00ff00ff); + return prefs->getColor("/tools/nodes/clipping_path_color", "#00ff00ff"); case SHAPE_ROLE_MASK: - return prefs->getColor("/tools/nodes/mask_color", 0x0000ffff); + return prefs->getColor("/tools/nodes/mask_color", "#0000ffff"); case SHAPE_ROLE_LPE_PARAM: - return prefs->getColor("/tools/nodes/lpe_param_color", 0x009000ff); + return prefs->getColor("/tools/nodes/lpe_param_color", "#009000ff"); case SHAPE_ROLE_NORMAL: default: - return prefs->getColor("/tools/nodes/highlight_color", 0xff0000ff);; + return prefs->getColor("/tools/nodes/highlight_color", "#ff0000ff");; } } diff --git a/src/ui/tool/multi-path-manipulator.h b/src/ui/tool/multi-path-manipulator.h index d48234e696..615999268c 100644 --- a/src/ui/tool/multi-path-manipulator.h +++ b/src/ui/tool/multi-path-manipulator.h @@ -125,7 +125,7 @@ private: void _commit(CommitEvent cps); void _done(gchar const *reason, bool alert_LPE = true); void _doneWithCleanup(gchar const *reason, bool alert_LPE = false); - guint32 _getOutlineColor(ShapeRole role, SPObject *object); + Colors::Color _getOutlineColor(ShapeRole role, SPObject *object); MapType _mmap; diff --git a/src/ui/toolbar/text-toolbar.cpp b/src/ui/toolbar/text-toolbar.cpp index d26c15b8be..2563431cbf 100644 --- a/src/ui/toolbar/text-toolbar.cpp +++ b/src/ui/toolbar/text-toolbar.cpp @@ -1568,12 +1568,10 @@ void TextToolbar::selection_changed(Inkscape::Selection *selection) // don't bot std::vector qactive{ this->_sub_active_item }; auto parent = cast(this->_sub_active_item->parent); std::vector qparent{ parent }; - result_numbers = - sp_desktop_query_style_from_list(qactive, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS); - result_numbers_fallback = - sp_desktop_query_style_from_list(qparent, &query_fallback, QUERY_STYLE_PROPERTY_FONTNUMBERS); + result_numbers = objects_query_fontnumbers(qactive, &query); + result_numbers_fallback = objects_query_fontnumbers(qparent, &query_fallback); } else if (_outer) { - result_numbers = sp_desktop_query_style_from_list(to_work, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS); + result_numbers = objects_query_fontnumbers(to_work, &query); } else { result_numbers = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS); } diff --git a/src/ui/tools/booleans-builder.cpp b/src/ui/tools/booleans-builder.cpp index e06bc5d524..dada329649 100644 --- a/src/ui/tools/booleans-builder.cpp +++ b/src/ui/tools/booleans-builder.cpp @@ -89,8 +89,7 @@ void BooleanBuilder::redraw_items() return; } - auto nv = _set->desktop()->getNamedView(); - _dark = SP_RGBA32_LUMINANCE(nv->desk_color) < 100; + _dark = Colors::get_perceptual_lightness(_set->desktop()->getNamedView()->getDeskColor()) < 0.5; _screen_items.clear(); diff --git a/src/ui/tools/calligraphic-tool.cpp b/src/ui/tools/calligraphic-tool.cpp index e1733611f2..8e20d405b4 100644 --- a/src/ui/tools/calligraphic-tool.cpp +++ b/src/ui/tools/calligraphic-tool.cpp @@ -1042,13 +1042,13 @@ void CalligraphicTool::fit_and_split(bool release) if (!release) { g_assert(!currentcurve.is_empty()); - guint32 fillColor = sp_desktop_get_color_tool(_desktop, "/tools/calligraphic", true); + auto fillColor = sp_desktop_get_color_tool(_desktop, "/tools/calligraphic", true); double opacity = sp_desktop_get_master_opacity_tool(_desktop, "/tools/calligraphic"); double fillOpacity = sp_desktop_get_opacity_tool(_desktop, "/tools/calligraphic", true); - guint fill = (fillColor & 0xffffff00) | SP_COLOR_F_TO_U(opacity*fillOpacity); + // TODO: This removes color space information. auto cbp = new Inkscape::CanvasItemBpath(_desktop->getCanvasSketch(), currentcurve.get_pathvector(), true); - cbp->set_fill(fill, SP_WIND_RULE_EVENODD); + cbp->set_fill(fillColor ? fillColor->toRGBA(opacity * fillOpacity) : 0x0, SP_WIND_RULE_EVENODD); cbp->set_stroke(0x0); /* fixme: Cannot we cascade it to root more clearly? */ diff --git a/src/ui/tools/dropper-tool.cpp b/src/ui/tools/dropper-tool.cpp index a78bc8adf0..b086231775 100644 --- a/src/ui/tools/dropper-tool.cpp +++ b/src/ui/tools/dropper-tool.cpp @@ -34,8 +34,6 @@ #include "object/sp-namedview.h" -#include "svg/svg-color.h" - #include "ui/cursor-utils.h" #include "ui/icon-names.h" #include "ui/tools/dropper-tool.h" @@ -89,7 +87,7 @@ DropperTool::~DropperTool() * @param invert If true, invert the rgb value * @param non_dropping If true, use color from canvas, even in dropping mode. */ -uint32_t DropperTool::get_color(bool invert, bool non_dropping) const +std::optional DropperTool::get_color(bool invert, bool non_dropping) const { auto prefs = Preferences::get(); @@ -98,16 +96,15 @@ uint32_t DropperTool::get_color(bool invert, bool non_dropping) const // non_dropping ignores dropping mode and always uses color from canvas. // Used by the clipboard - double r = non_dropping ? non_dropping_R : R; - double g = non_dropping ? non_dropping_G : G; - double b = non_dropping ? non_dropping_B : B; - double a = non_dropping ? non_dropping_A : alpha; - - return SP_RGBA32_F_COMPOSE( - std::abs(invert - r), - std::abs(invert - g), - std::abs(invert - b), - (pick == PICK_ACTUAL && setalpha) ? a : 1.0); + auto color = non_dropping ? non_dropping_color : stored_color; + + if (color && invert) + color->invert(); + + if (color && (pick != PICK_ACTUAL || !setalpha)) + color->enableOpacity(false); + + return color; } bool DropperTool::root_handler(CanvasEvent const &event) @@ -128,29 +125,21 @@ bool DropperTool::root_handler(CanvasEvent const &event) auto selection = _desktop->getSelection(); g_assert(selection); - std::optional apply_color; + std::optional apply_color; for (auto const &obj: selection->objects()) { if (obj->style) { - double opacity = 1.0; if (!stroke && obj->style->fill.set) { - if (obj->style->fill_opacity.set) { - opacity = SP_SCALE24_TO_FLOAT(obj->style->fill_opacity.value); - } - apply_color = obj->style->fill.value.color.toRGBA32(opacity); + apply_color = obj->style->fill.getColor(); + apply_color->addOpacity(obj->style->fill_opacity); } else if (stroke && obj->style->stroke.set) { - if (obj->style->stroke_opacity.set) { - opacity = SP_SCALE24_TO_FLOAT(obj->style->stroke_opacity.value); - } - apply_color = obj->style->stroke.value.color.toRGBA32(opacity); + apply_color = obj->style->stroke.getColor(); + apply_color->addOpacity(obj->style->stroke_opacity); } } } if (apply_color) { - R = SP_RGBA32_R_F(*apply_color); - G = SP_RGBA32_G_F(*apply_color); - B = SP_RGBA32_B_F(*apply_color); - alpha = SP_RGBA32_A_F(*apply_color); + stored_color = apply_color; } else { // This means that having no selection or some other error // we will default back to normal dropper mode. @@ -223,7 +212,7 @@ bool DropperTool::root_handler(CanvasEvent const &event) auto canvas_item_drawing = _desktop->getCanvasDrawing(); auto drawing = canvas_item_drawing->get_drawing(); - // Get average color + // Get average color of on screen pixels (sRGB) double R2, G2, B2, A2; drawing->averageColor(pick_area, R2, G2, B2, A2); @@ -249,19 +238,12 @@ bool DropperTool::root_handler(CanvasEvent const &event) // Remember color if (!dropping) { - R = R2; - G = G2; - B = B2; - alpha = A2; + stored_color = Colors::Color(SP_RGBA32_F_COMPOSE(R2, G2, B2, A2)); } // Remember color from canvas, even in dropping mode. // These values are used by the clipboard. - non_dropping_R = R2; - non_dropping_G = G2; - non_dropping_B = B2; - non_dropping_A = A2; - + non_dropping_color = Colors::Color(SP_RGBA32_F_COMPOSE(R2, G2, B2, A2)); ret = true; }, @@ -291,11 +273,11 @@ bool DropperTool::root_handler(CanvasEvent const &event) } } - auto picked_color = ColorRGBA(get_color(invert)); + auto picked_color = get_color(invert); // One time pick has active signal, call them all and clear. if (!onetimepick_signal.empty()) { - onetimepick_signal.emit(&picked_color); + onetimepick_signal.emit(*picked_color); onetimepick_signal.clear(); // Do this last as it destroys the picker tool. sp_toggle_dropper(_desktop); @@ -304,7 +286,7 @@ bool DropperTool::root_handler(CanvasEvent const &event) } // do the actual color setting - sp_desktop_set_color(_desktop, picked_color, false, !stroke); + sp_desktop_set_color(_desktop, *picked_color, false, !stroke); // REJON: set aux. toolbar input to hex color! if (!_desktop->getSelection()->isEmpty()) { @@ -345,31 +327,32 @@ bool DropperTool::root_handler(CanvasEvent const &event) } // set the status message to the right text. - char c[64]; - sp_svg_write_color(c, sizeof(c), get_color(invert)); - - // alpha of color under cursor, to show in the statusbar - // locale-sensitive printf is OK, since this goes to the UI, not into SVG - auto alphastr = g_strdup_printf(_(" alpha %.3g"), alpha); - // where the color is picked, to show in the statusbar - auto where = dragging ? g_strdup_printf(_(", averaged with radius %d"), (int)radius) : g_strdup_printf("%s", _(" under cursor")); - // message, to show in the statusbar - auto message = dragging ? _("Release mouse to set color.") : _("Click to set fill, Shift+click to set stroke; drag to average color in area; with Alt to pick inverse color; Ctrl+C to copy the color under mouse to clipboard"); - - defaultMessageContext()->setF( - Inkscape::NORMAL_MESSAGE, - "%s%s%s. %s", c, - pick == PICK_VISIBLE ? "" : alphastr, where, message); - - g_free(where); - g_free(alphastr); + auto color = get_color(invert); + + if (color) { + // alpha of color under cursor, to show in the statusbar + // locale-sensitive printf is OK, since this goes to the UI, not into SVG + auto alphastr = g_strdup_printf(_(" alpha %.3g"), color->getOpacity()); + // where the color is picked, to show in the statusbar + auto where = dragging ? g_strdup_printf(_(", averaged with radius %d"), (int)radius) : g_strdup_printf("%s", _(" under cursor")); + // message, to show in the statusbar + auto message = dragging ? _("Release mouse to set color.") : _("Click to set fill, Shift+click to set stroke; drag to average color in area; with Alt to pick inverse color; Ctrl+C to copy the color under mouse to clipboard"); + + defaultMessageContext()->setF( + Inkscape::NORMAL_MESSAGE, + "%s%s%s. %s", color->toString(false).c_str(), + pick == PICK_VISIBLE ? "" : alphastr, where, message); + + g_free(where); + g_free(alphastr); + } // Set the right cursor for the mode and apply the special Fill color _cursor_filename = dropping ? (stroke ? "dropper-drop-stroke.svg" : "dropper-drop-fill.svg") : (stroke ? "dropper-pick-stroke.svg" : "dropper-pick-fill.svg"); // We do this ourselves to get color correct. - set_svg_cursor(*_desktop->getCanvas(), _cursor_filename, get_color(invert)); + set_svg_cursor(*_desktop->getCanvas(), _cursor_filename, color); if (!ret) { ret = ToolBase::root_handler(event); diff --git a/src/ui/tools/dropper-tool.h b/src/ui/tools/dropper-tool.h index ef6848b748..70b17c6373 100644 --- a/src/ui/tools/dropper-tool.h +++ b/src/ui/tools/dropper-tool.h @@ -15,7 +15,7 @@ #include <2geom/point.h> -#include "color-rgba.h" +#include "colors/color.h" #include "display/control/canvas-item-ptr.h" #include "ui/tools/tool-base.h" @@ -28,26 +28,19 @@ public: DropperTool(SPDesktop *desktop); ~DropperTool() override; - uint32_t get_color(bool invert = false, bool non_dropping = false) const; - - sigc::signal onetimepick_signal; + std::optional get_color(bool invert = false, bool non_dropping = false) const; + sigc::signal onetimepick_signal; protected: bool root_handler(CanvasEvent const &event) override; private: // Stored color. - double R = 0.0; - double G = 0.0; - double B = 0.0; - double alpha = 0.0; + std::optional stored_color; // Stored color taken from canvas. Used by clipboard. - // Identical to R, G, B, alpha if dropping disabled. - double non_dropping_R = 0.0; - double non_dropping_G = 0.0; - double non_dropping_B = 0.0; - double non_dropping_A = 0.0; + // Identical to stored_color if dropping disabled. + std::optional non_dropping_color; bool invert = false; ///< Set color to inverse rgb value bool stroke = false; ///< Set to stroke color. In dropping mode, set from stroke color diff --git a/src/ui/tools/eraser-tool.cpp b/src/ui/tools/eraser-tool.cpp index b604c9f544..0852505402 100644 --- a/src/ui/tools/eraser-tool.cpp +++ b/src/ui/tools/eraser-tool.cpp @@ -1334,14 +1334,13 @@ void EraserTool::_fitDrawLastPoint() { g_assert(!currentcurve.is_empty()); - guint32 fillColor = sp_desktop_get_color_tool(_desktop, "/tools/eraser", true); + auto fillColor = sp_desktop_get_color_tool(_desktop, "/tools/eraser", true); double opacity = sp_desktop_get_master_opacity_tool(_desktop, "/tools/eraser"); double fillOpacity = sp_desktop_get_opacity_tool(_desktop, "/tools/eraser", true); - guint fill = (fillColor & 0xffffff00) | SP_COLOR_F_TO_U(opacity * fillOpacity); - + // TODO This removes color space information from the color auto cbp = new Inkscape::CanvasItemBpath(_desktop->getCanvasSketch(), currentcurve.get_pathvector(), true); - cbp->set_fill(fill, trace_wind_rule); + cbp->set_fill(fillColor ? fillColor->toRGBA(opacity * fillOpacity) : 0x0, trace_wind_rule); cbp->set_stroke(0x0); /* fixme: Cannot we cascade it to root more clearly? */ diff --git a/src/ui/tools/flood-tool.cpp b/src/ui/tools/flood-tool.cpp index 8cc4c6f4a5..ba51af714c 100644 --- a/src/ui/tools/flood-tool.cpp +++ b/src/ui/tools/flood-tool.cpp @@ -30,7 +30,6 @@ #include <2geom/pathvector.h> #include "async/progress.h" -#include "color.h" #include "context-fns.h" #include "desktop-style.h" #include "desktop.h" @@ -70,6 +69,8 @@ using Inkscape::Display::ExtractARGB32; using Inkscape::Display::ExtractRGB32; using Inkscape::Display::AssembleARGB32; +using namespace Inkscape::Colors; + namespace Inkscape::UI::Tools { // Must match PaintBucketChannels enum @@ -192,8 +193,6 @@ static bool compare_uint32(uint32_t a, uint32_t b, uint32_t d) */ static bool compare_pixels(uint32_t check, uint32_t orig, uint32_t merged_orig_pixel, uint32_t dtc, int threshold, PaintBucketChannels method) { - float hsl_check[3] = {0,0,0}, hsl_orig[3] = {0,0,0}; - uint32_t ac = 0, rc = 0, gc = 0, bc = 0; ExtractARGB32(check, ac, rc, gc, bc); @@ -206,13 +205,16 @@ static bool compare_pixels(uint32_t check, uint32_t orig, uint32_t merged_orig_p uint32_t amop = 0, rmop = 0, gmop = 0, bmop = 0; ExtractARGB32(merged_orig_pixel, amop, rmop, gmop, bmop); + auto hsl_orig = Color(Space::Type::HSL, {0, 0, 0}); + auto hsl_check = hsl_orig; + if ((method == FLOOD_CHANNELS_H) || (method == FLOOD_CHANNELS_S) || (method == FLOOD_CHANNELS_L)) { - double dac = ac; double dao = ao; - SPColor::rgb_to_hsl_floatv(hsl_check, rc / dac, gc / dac, bc / dac); - SPColor::rgb_to_hsl_floatv(hsl_orig, ro / dao, go / dao, bo / dao); + double dac = ac; + hsl_orig.set(Color(Space::Type::RGB, {ro / dao, go / dao, bo / dao}), true); + hsl_check.set(Color(Space::Type::RGB, {rc / dac, gc / dac, bc / dac}), true); } switch (method) { @@ -720,7 +722,7 @@ static void sp_flood_do_flood_fill(SPDesktop *desktop, Geom::Point const &cursor auto const stride = Cairo::ImageSurface::format_stride_for_width(Cairo::Surface::Format::ARGB32, width); // TODO: C++20: *once* Apple+AppImage support it: Use std::make_unique_for_overwrite() auto const px = std::make_unique(stride * height); - uint32_t bgcolor, dtc; + uint32_t dtc; // Draw image into data block px { // this block limits the lifetime of Drawing and DrawingContext @@ -738,12 +740,11 @@ static void sp_flood_do_flood_fill(SPDesktop *desktop, Geom::Point const &cursor auto dc = DrawingContext(surf->cobj(), Geom::Point()); // cairo_translate not necessary here - surface origin is at 0,0 - bgcolor = document->getPageManager().background_color; - bgcolor &= 0xffffff00; // make color transparent for 'alpha' flood mode to work - // bgcolor is 0xrrggbbaa, we need 0xaarrggbb - dtc = bgcolor >> 8; // keep color transparent; page color doesn't support transparency anymore + // make color transparent for 'alpha' flood mode to work + auto bgcolor = document->getPageManager().getBackgroundColor(); + bgcolor.setOpacity(0.0); - dc.setSource(bgcolor); + dc.setSource(bgcolor.toARGB()); dc.setOperator(CAIRO_OPERATOR_SOURCE); dc.paint(); dc.setOperator(CAIRO_OPERATOR_OVER); diff --git a/src/ui/tools/freehand-base.cpp b/src/ui/tools/freehand-base.cpp index 6ca80a1375..c731258917 100644 --- a/src/ui/tools/freehand-base.cpp +++ b/src/ui/tools/freehand-base.cpp @@ -36,7 +36,6 @@ #include "object/sp-rect.h" #include "object/sp-use.h" -#include "svg/svg-color.h" #include "svg/svg.h" #include "ui/clipboard.h" @@ -169,11 +168,9 @@ void spdc_apply_style(SPObject *obj) sp_repr_css_set_property(css, "fill", str.c_str()); } } else if (obj->style->stroke.isColor()) { - gchar c[64]; - sp_svg_write_color( - c, sizeof(c), - obj->style->stroke.value.color.toRGBA32(SP_SCALE24_TO_FLOAT(obj->style->stroke_opacity.value))); - sp_repr_css_set_property(css, "fill", c); + auto color = obj->style->stroke.getColor(); + color.addOpacity(obj->style->stroke_opacity); + sp_repr_css_set_property_string(css, "fill", color.toString()); } else { sp_repr_css_set_property(css, "fill", "none"); } @@ -899,11 +896,15 @@ void spdc_create_single_dot(ToolBase *tool, Geom::Point const &pt, char const *p } // unset stroke and set fill color to former stroke color - auto str = strcmp(path, "/tools/calligraphic") - ? g_strdup_printf("fill:#%06x;stroke:none;", sp_desktop_get_color_tool(desktop, path, false) >> 8) - : g_strdup_printf("fill:#%06x;stroke:#%06x;", sp_desktop_get_color_tool(desktop, path, true) >> 8, sp_desktop_get_color_tool(desktop, path, false) >> 8); - repr->setAttribute("style", str); - g_free(str); + bool cali = strcmp(path, "/tools/calligraphic"); + auto fill = sp_desktop_get_color_tool(desktop, path, cali); + auto stroke = sp_desktop_get_color_tool(desktop, path, false); + + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property_string(css, "fill", fill ? fill->toString() : "none"); + sp_repr_css_set_property_string(css, "stroke", !cali && stroke ? stroke->toString() : "none"); + sp_repr_css_set(repr, css, "style"); + sp_repr_css_attr_unref(css); // put the circle where the mouse click occurred and set the diameter to the // current stroke width, multiplied by the amount specified in the preferences diff --git a/src/ui/tools/gradient-tool.cpp b/src/ui/tools/gradient-tool.cpp index da3cdc20f9..7db42a4bce 100644 --- a/src/ui/tools/gradient-tool.cpp +++ b/src/ui/tools/gradient-tool.cpp @@ -346,18 +346,9 @@ void GradientTool::simplify(double tolerance) } // compare color of stop1 to the average color of stop0 and stop2 - uint32_t const c0 = stop0->get_rgba32(); - uint32_t const c2 = stop2->get_rgba32(); - uint32_t const c1r = stop1->get_rgba32(); - uint32_t const c1 = average_color(c0, c2, (stop1->offset - stop0->offset) / (stop2->offset - stop0->offset)); - - double diff = - Geom::sqr(SP_RGBA32_R_F(c1) - SP_RGBA32_R_F(c1r)) + - Geom::sqr(SP_RGBA32_G_F(c1) - SP_RGBA32_G_F(c1r)) + - Geom::sqr(SP_RGBA32_B_F(c1) - SP_RGBA32_B_F(c1r)) + - Geom::sqr(SP_RGBA32_A_F(c1) - SP_RGBA32_A_F(c1r)); - - if (diff < tolerance) { + auto coord = (stop1->offset - stop0->offset) / (stop2->offset - stop0->offset); + auto avg = stop0->getColor().averaged(stop2->getColor(), coord); + if (avg.difference(stop1->getColor()) < tolerance) { todel.emplace(stop1); } } diff --git a/src/ui/tools/measure-tool.cpp b/src/ui/tools/measure-tool.cpp index da47557393..7af6215f75 100644 --- a/src/ui/tools/measure-tool.cpp +++ b/src/ui/tools/measure-tool.cpp @@ -22,6 +22,7 @@ #include <2geom/line.h> #include <2geom/path-intersection.h> +#include "colors/utils.h" #include "desktop-style.h" #include "desktop.h" #include "document-undo.h" @@ -44,7 +45,6 @@ #include "object/sp-shape.h" #include "object/sp-text.h" -#include "svg/svg-color.h" #include "svg/svg.h" #include "ui/dialog/knot-properties.h" @@ -854,10 +854,8 @@ void MeasureTool::setLabelText(Glib::ustring const &value, Geom::Point pos, doub /* Create */ Inkscape::XML::Node *rrect = xml_doc->createElement("svg:rect"); SPCSSAttr *css = sp_repr_css_attr_new (); - gchar color_line[64]; - sp_svg_write_color (color_line, sizeof(color_line), background); - sp_repr_css_set_property (css, "fill", color_line); - sp_repr_css_set_property (css, "fill-opacity", "0.5"); + sp_repr_css_set_property_string(css, "fill", Inkscape::Colors::rgba_to_hex(background)); + sp_repr_css_set_property_double(css, "fill-opacity", 0.5); sp_repr_css_set_property (css, "stroke-width", "0"); Glib::ustring css_str; sp_repr_css_write_string(css,css_str); @@ -1331,13 +1329,7 @@ void MeasureTool::setMeasureItem(Geom::PathVector pathv, bool is_curve, bool mar } sp_repr_css_set_property (css, "stroke-width", stroke_width.str().c_str()); sp_repr_css_set_property (css, "fill", "none"); - if(color) { - char color_line[64]; - sp_svg_write_color (color_line, sizeof(color_line), color); - sp_repr_css_set_property (css, "stroke", color_line); - } else { - sp_repr_css_set_property (css, "stroke", "#ff0000"); - } + sp_repr_css_set_property_string(css, "stroke", color ? Inkscape::Colors::rgba_to_hex(color) : "#ff0000"); char const * stroke_linecap = is_curve ? "butt" : "square"; sp_repr_css_set_property (css, "stroke-linecap", stroke_linecap); sp_repr_css_set_property (css, "stroke-linejoin", "miter"); diff --git a/src/ui/tools/node-tool.cpp b/src/ui/tools/node-tool.cpp index 0780530cfb..02d8edf784 100644 --- a/src/ui/tools/node-tool.cpp +++ b/src/ui/tools/node-tool.cpp @@ -491,7 +491,7 @@ bool NodeTool::root_handler(CanvasEvent const &event) auto c = shape->curveForEdit()->transformed(over_item->i2dt_affine()); auto flash = new Inkscape::CanvasItemBpath(_desktop->getCanvasTemp(), c.get_pathvector(), true); - flash->set_stroke(over_item->highlight_color()); + flash->set_stroke(over_item->highlight_color().toRGBA()); flash->set_fill(0x0, SP_WIND_RULE_NONZERO); // No fill. flash_tempitem = _desktop->add_temporary_canvasitem(flash, prefs->getInt("/tools/nodes/pathflash_timeout", 500)); diff --git a/src/ui/tools/pen-tool.cpp b/src/ui/tools/pen-tool.cpp index a648ea2f81..e90afec817 100644 --- a/src/ui/tools/pen-tool.cpp +++ b/src/ui/tools/pen-tool.cpp @@ -1222,12 +1222,14 @@ void PenTool::_setAngleDistanceStatusMessage(Geom::Point const p, int pc_point_t void PenTool::_bsplineSpiroColor() { static Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + auto highlight = currentLayer()->highlight_color(); + auto other = prefs->getColor("/tools/nodes/highlight_color", "#ff0000ff"); if (this->spiro){ this->red_color = 0xff000000; this->green_color = 0x00ff0000; } else if(this->bspline) { - this->highlight_color = currentLayer()->highlight_color(); - if((unsigned int)prefs->getInt("/tools/nodes/highlight_color", 0xff0000ff) == this->highlight_color){ + highlight_color = highlight.toRGBA(); + if(other == highlight) { this->green_color = 0xff00007f; this->red_color = 0xff00007f; } else { @@ -1235,9 +1237,9 @@ void PenTool::_bsplineSpiroColor() this->red_color = this->highlight_color; } } else { - this->highlight_color = currentLayer()->highlight_color(); + highlight_color = highlight.toRGBA(); this->red_color = 0xff00007f; - if((unsigned int)prefs->getInt("/tools/nodes/highlight_color", 0xff0000ff) == this->highlight_color){ + if(other == highlight) { this->green_color = 0x00ff007f; } else { this->green_color = this->highlight_color; diff --git a/src/ui/tools/pencil-tool.cpp b/src/ui/tools/pencil-tool.cpp index eeaefee65c..c3813ed1c5 100644 --- a/src/ui/tools/pencil-tool.cpp +++ b/src/ui/tools/pencil-tool.cpp @@ -1139,12 +1139,15 @@ void PencilTool::_fitAndSplit() { /// \todo fixme: auto layer = _desktop->layerManager().currentLayer(); - this->highlight_color = layer->highlight_color(); - if((unsigned int)prefs->getInt("/tools/nodes/highlight_color", 0xff0000ff) == this->highlight_color){ - this->green_color = 0x00ff007f; + auto highlight = layer->highlight_color(); + auto other = prefs->getColor("/tools/nodes/highlight_color", "#ff0000ff"); + + if(other == highlight) { + green_color = 0x00ff007f; } else { - this->green_color = this->highlight_color; + green_color = highlight.toRGBA(); } + highlight_color = highlight.toRGBA(); auto cshape = new Inkscape::CanvasItemBpath(_desktop->getCanvasSketch(), red_curve.get_pathvector(), true); cshape->set_stroke(green_color); diff --git a/src/ui/tools/spray-tool.cpp b/src/ui/tools/spray-tool.cpp index 037b94f8cb..5f4d3ca6db 100644 --- a/src/ui/tools/spray-tool.cpp +++ b/src/ui/tools/spray-tool.cpp @@ -30,6 +30,7 @@ #include <2geom/circle.h> +#include "colors/utils.h" #include "context-fns.h" #include "desktop-style.h" #include "desktop.h" @@ -47,8 +48,6 @@ #include "object/sp-shape.h" #include "object/sp-use.h" -#include "svg/svg-color.h" - #include "ui/icon-names.h" #include "ui/toolbar/spray-toolbar.h" #include "ui/widget/events/canvas-event.h" @@ -602,17 +601,16 @@ static bool fit_item(SPDesktop *desktop, } double opacity = 1.0; gchar color_string[32]; *color_string = 0; - float r = SP_RGBA32_R_F(rgba); - float g = SP_RGBA32_G_F(rgba); - float b = SP_RGBA32_B_F(rgba); - float a = SP_RGBA32_A_F(rgba); - if(!over_transparent && (a == 0 || a < 1e-6)){ + auto color = Colors::Color(rgba); + bool invisible = color.getOpacity() < 1e-6; + + if(!over_transparent && invisible){ if(!no_overlap && (picker || over_transparent || over_no_transparent)){ showHidden(items_down); } return false; } - if(!over_no_transparent && a > 0){ + if(!over_no_transparent && !invisible){ if(!no_overlap && (picker || over_transparent || over_no_transparent)){ showHidden(items_down); } @@ -620,8 +618,7 @@ static bool fit_item(SPDesktop *desktop, } if(picker && do_trace){ - float hsl[3]; - SPColor::rgb_to_hsl_floatv (hsl, r, g, b); + auto hsl = *color.converted(Colors::Space::Type::HSL); gdouble val = 0; switch (pick) { @@ -629,16 +626,16 @@ static bool fit_item(SPDesktop *desktop, val = 1 - hsl[2]; // inverse lightness; to match other picks where black = max break; case PICK_OPACITY: - val = a; + val = color.getOpacity(); break; case PICK_R: - val = r; + val = color[0]; break; case PICK_G: - val = g; + val = color[1]; break; case PICK_B: - val = b; + val = color[2]; break; case PICK_H: val = hsl[0]; @@ -655,9 +652,9 @@ static bool fit_item(SPDesktop *desktop, if (rand_picked > 0) { val = randomize01 (val, rand_picked); - r = randomize01 (r, rand_picked); - g = randomize01 (g, rand_picked); - b = randomize01 (b, rand_picked); + for (auto i = 0; i < 3; i++) { + color.set(i, randomize01(color[i], rand_picked)); + } } if (gamma_picked != 0) { @@ -668,25 +665,19 @@ static bool fit_item(SPDesktop *desktop, power = 1 + fabs(gamma_picked); val = pow (val, power); - r = pow ((double)r, (double)power); - g = pow ((double)g, (double)power); - b = pow ((double)b, (double)power); + for (auto i = 0; i < 3; i++) { + color.set(i, pow(color[i], (double)power)); + } } if (invert_picked) { val = 1 - val; - r = 1 - r; - g = 1 - g; - b = 1 - b; + color.invert(); } val = CLAMP (val, 0, 1); - r = CLAMP (r, 0, 1); - g = CLAMP (g, 0, 1); - b = CLAMP (b, 0, 1); + color.normalize(); - // recompose tweaked color - rgba = SP_RGBA32_F_COMPOSE(r, g, b, a); if (pick_to_size) { if(!trace_scale){ if(pick_inverse_value) { @@ -760,13 +751,7 @@ static bool fit_item(SPDesktop *desktop, } } if (pick_to_color) { - sp_svg_write_color(color_string, sizeof(color_string), rgba); - if(pick_fill){ - sp_repr_css_set_property(css, "fill", color_string); - } - if(pick_stroke){ - sp_repr_css_set_property(css, "stroke", color_string); - } + sp_repr_css_set_property_string(css, pick_fill ? "fill" : "stroke", Inkscape::Colors::rgba_to_hex(rgba)); } if (opacity < 1e-6) { // invisibly transparent, skip if(!no_overlap && (picker || over_transparent || over_no_transparent)){ @@ -779,23 +764,11 @@ static bool fit_item(SPDesktop *desktop, if(!pick_center){ rgba = rgba2; } + auto color = Colors::Color(rgba); if (pick_inverse_value) { - r = 1 - SP_RGBA32_R_F(rgba); - g = 1 - SP_RGBA32_G_F(rgba); - b = 1 - SP_RGBA32_B_F(rgba); - } else { - r = SP_RGBA32_R_F(rgba); - g = SP_RGBA32_G_F(rgba); - b = SP_RGBA32_B_F(rgba); - } - rgba = SP_RGBA32_F_COMPOSE(r, g, b, a); - sp_svg_write_color(color_string, sizeof(color_string), rgba); - if(pick_fill){ - sp_repr_css_set_property(css, "fill", color_string); - } - if(pick_stroke){ - sp_repr_css_set_property(css, "stroke", color_string); + color.invert(); } + sp_repr_css_set_property_string(css, pick_fill ? "fill" : "stroke", color.toString()); } if(!no_overlap && (picker || over_transparent || over_no_transparent)){ showHidden(items_down); diff --git a/src/ui/tools/tool-base.cpp b/src/ui/tools/tool-base.cpp index 27efd7c2bc..59ec50c90f 100644 --- a/src/ui/tools/tool-base.cpp +++ b/src/ui/tools/tool-base.cpp @@ -162,14 +162,16 @@ void ToolBase::set_cursor(std::string filename) */ Glib::RefPtr ToolBase::get_cursor(Gtk::Widget &widget, std::string const &filename) const { - bool fillHasColor = false; - bool strokeHasColor = false; - guint32 fillColor = sp_desktop_get_color_tool(_desktop, getPrefsPath(), true, &fillHasColor); - guint32 strokeColor = sp_desktop_get_color_tool(_desktop, getPrefsPath(), false, &strokeHasColor); - double fillOpacity = fillHasColor ? sp_desktop_get_opacity_tool(_desktop, getPrefsPath(), true) : 1.0; - double strokeOpacity = strokeHasColor ? sp_desktop_get_opacity_tool(_desktop, getPrefsPath(), false) : 1.0; - return load_svg_cursor(widget, filename, - fillColor, strokeColor, fillOpacity, strokeOpacity); + auto fillColor = sp_desktop_get_color_tool(_desktop, getPrefsPath(), true); + if (fillColor) { + fillColor->addOpacity(sp_desktop_get_opacity_tool(_desktop, getPrefsPath(), true)); + } + + auto strokeColor = sp_desktop_get_color_tool(_desktop, getPrefsPath(), false); + if (strokeColor) { + strokeColor->addOpacity(sp_desktop_get_opacity_tool(_desktop, getPrefsPath(), false)); + } + return load_svg_cursor(widget, filename, fillColor, strokeColor); } /** diff --git a/src/ui/tools/tweak-tool.cpp b/src/ui/tools/tweak-tool.cpp index 604612d354..84f21202b0 100644 --- a/src/ui/tools/tweak-tool.cpp +++ b/src/ui/tools/tweak-tool.cpp @@ -585,71 +585,27 @@ sp_tweak_dilate_recursive (Inkscape::Selection *selection, SPItem *item, Geom::P return did; } - static void -tweak_colorpaint (float *color, guint32 goal, double force, bool do_h, bool do_s, bool do_l) -{ - float rgb_g[3]; - - if (!do_h || !do_s || !do_l) { - float hsl_g[3]; - SPColor::rgb_to_hsl_floatv (hsl_g, SP_RGBA32_R_F(goal), SP_RGBA32_G_F(goal), SP_RGBA32_B_F(goal)); - float hsl_c[3]; - SPColor::rgb_to_hsl_floatv (hsl_c, color[0], color[1], color[2]); - if (!do_h) { - hsl_g[0] = hsl_c[0]; - } - if (!do_s) { - hsl_g[1] = hsl_c[1]; - } - if (!do_l) { - hsl_g[2] = hsl_c[2]; - } - SPColor::hsl_to_rgb_floatv (rgb_g, hsl_g[0], hsl_g[1], hsl_g[2]); - } else { - rgb_g[0] = SP_RGBA32_R_F(goal); - rgb_g[1] = SP_RGBA32_G_F(goal); - rgb_g[2] = SP_RGBA32_B_F(goal); - } - - for (int i = 0; i < 3; i++) { - double d = rgb_g[i] - color[i]; - color[i] += d * force; - } -} - - static void -tweak_colorjitter (float *color, double force, bool do_h, bool do_s, bool do_l) +static Color tweak_color(guint mode, Color const &color, Color const &goal, double force, bool do_h, bool do_s, bool do_l) { - float hsl_c[3]; - SPColor::rgb_to_hsl_floatv (hsl_c, color[0], color[1], color[2]); - - if (do_h) { - hsl_c[0] += g_random_double_range(-0.5, 0.5) * force; - if (hsl_c[0] > 1) { - hsl_c[0] -= 1; - } - if (hsl_c[0] < 0) { - hsl_c[0] += 1; + // Tweak colors are entirely based on HSL values; + if (auto hsl = color.converted(Colors::Space::Type::HSL)) { + unsigned int pin = (do_h * 1) + (do_s * 2) + (do_l * 4); + if (mode == TWEAK_MODE_COLORPAINT) { + hsl->average(goal, force, pin); + } else if (mode == TWEAK_MODE_COLORJITTER) { + hsl->jitter(force, pin); } + if (auto copy = hsl->converted(color.getSpace())) + return *copy; } - if (do_s) { - hsl_c[1] += g_random_double_range(-hsl_c[1], 1 - hsl_c[1]) * force; - } - if (do_l) { - hsl_c[2] += g_random_double_range(-hsl_c[2], 1 - hsl_c[2]) * force; - } - - SPColor::hsl_to_rgb_floatv (color, hsl_c[0], hsl_c[1], hsl_c[2]); + return color; // Bad conversion } - static void -tweak_color (guint mode, float *color, guint32 goal, double force, bool do_h, bool do_s, bool do_l) +static void tweak_stop_color(guint mode, SPStop *stop, Color const &goal, double force, bool do_h, bool do_s, bool do_l) { - if (mode == TWEAK_MODE_COLORPAINT) { - tweak_colorpaint (color, goal, force, do_h, do_s, do_l); - } else if (mode == TWEAK_MODE_COLORJITTER) { - tweak_colorjitter (color, force, do_h, do_s, do_l); - } + auto copy = stop->getColor(); + tweak_color(mode, copy, goal, force, do_h, do_s, do_l); + stop->setColor(copy); } static void @@ -686,7 +642,7 @@ tweak_profile (double dist, double radius) } static void tweak_colors_in_gradient(SPItem *item, Inkscape::PaintTarget fill_or_stroke, - guint32 const rgb_goal, Geom::Point p_w, double radius, double force, guint mode, + Color const &goal, Geom::Point p_w, double radius, double force, guint mode, bool do_h, bool do_s, bool do_l, bool /*do_o*/) { SPGradient *gradient = getGradient(item, fill_or_stroke); @@ -778,10 +734,10 @@ static void tweak_colors_in_gradient(SPItem *item, Inkscape::PaintTarget fill_or // so it only affects the ends of this interstop; // distribute the force between the two endstops so that they // get all the painting even if they are not touched by the brush - tweak_color (mode, stop->getColor().v.c, rgb_goal, + tweak_stop_color(mode, stop, goal, force * (pos_e - offset_l) / (offset_h - offset_l), do_h, do_s, do_l); - tweak_color(mode, prevStop->getColor().v.c, rgb_goal, + tweak_stop_color(mode, prevStop, goal, force * (offset_h - pos_e) / (offset_h - offset_l), do_h, do_s, do_l); stop->updateRepr(); @@ -791,14 +747,14 @@ static void tweak_colors_in_gradient(SPItem *item, Inkscape::PaintTarget fill_or // wide brush, may affect more than 2 stops, // paint each stop by the force from the profile curve if (offset_l <= pos_e && offset_l > pos_e - r) { - tweak_color(mode, prevStop->getColor().v.c, rgb_goal, + tweak_stop_color(mode, prevStop, goal, force * tweak_profile (fabs (pos_e - offset_l), r), do_h, do_s, do_l); child_prev->updateRepr(); } if (offset_h >= pos_e && offset_h < pos_e + r) { - tweak_color (mode, stop->getColor().v.c, rgb_goal, + tweak_stop_color(mode, prevStop, goal, force * tweak_profile (fabs (pos_e - offset_h), r), do_h, do_s, do_l); stop->updateRepr(); @@ -820,7 +776,7 @@ static void tweak_colors_in_gradient(SPItem *item, Inkscape::PaintTarget fill_or for( unsigned j=0; j < array->nodes[i].size(); j+=3 ) { SPStop *stop = array->nodes[i][j]->stop; double distance = Geom::L2(Geom::Point(p - array->nodes[i][j]->p)); - tweak_color (mode, stop->getColor().v.c, rgb_goal, + tweak_stop_color(mode, stop, goal, force * tweak_profile (distance, radius), do_h, do_s, do_l); stop->updateRepr(); } @@ -829,10 +785,9 @@ static void tweak_colors_in_gradient(SPItem *item, Inkscape::PaintTarget fill_or } } - static bool +static bool sp_tweak_color_recursive (guint mode, SPItem *item, SPItem *item_at_point, - guint32 fill_goal, bool do_fill, - guint32 stroke_goal, bool do_stroke, + std::optional &fill_goal, std::optional &stroke_goal, float opacity_goal, bool do_opacity, bool do_blur, bool reverse, Geom::Point p, double radius, double force, @@ -845,8 +800,7 @@ sp_tweak_color_recursive (guint mode, SPItem *item, SPItem *item_at_point, auto childItem = cast(&child); if (childItem) { if (sp_tweak_color_recursive (mode, childItem, item_at_point, - fill_goal, do_fill, - stroke_goal, do_stroke, + fill_goal, stroke_goal, opacity_goal, do_opacity, do_blur, reverse, p, radius, force, do_h, do_s, do_l, do_o)) { @@ -932,22 +886,22 @@ sp_tweak_color_recursive (guint mode, SPItem *item, SPItem *item_at_point, return true; // do not do colors, blur is a separate mode } - if (do_fill) { + if (fill_goal) { if (style->fill.isPaintserver()) { - tweak_colors_in_gradient(item, Inkscape::FOR_FILL, fill_goal, p, radius, this_force, mode, do_h, do_s, do_l, do_o); + tweak_colors_in_gradient(item, Inkscape::FOR_FILL, *fill_goal, p, radius, this_force, mode, do_h, do_s, do_l, do_o); did = true; } else if (style->fill.isColor()) { - tweak_color (mode, style->fill.value.color.v.c, fill_goal, this_force, do_h, do_s, do_l); + style->fill.setColor(tweak_color(mode, style->fill.getColor(), *fill_goal, this_force, do_h, do_s, do_l)); item->updateRepr(); did = true; } } - if (do_stroke) { + if (stroke_goal) { if (style->stroke.isPaintserver()) { - tweak_colors_in_gradient(item, Inkscape::FOR_STROKE, stroke_goal, p, radius, this_force, mode, do_h, do_s, do_l, do_o); + tweak_colors_in_gradient(item, Inkscape::FOR_STROKE, *stroke_goal, p, radius, this_force, mode, do_h, do_s, do_l, do_o); did = true; } else if (style->stroke.isColor()) { - tweak_color (mode, style->stroke.value.color.v.c, stroke_goal, this_force, do_h, do_s, do_l); + style->stroke.setColor(tweak_color(mode, style->stroke.getColor(), *stroke_goal, this_force, do_h, do_s, do_l)); item->updateRepr(); did = true; } @@ -977,40 +931,15 @@ sp_tweak_dilate (TweakTool *tc, Geom::Point event_p, Geom::Point p, Geom::Point SPItem *item_at_point = tc->getDesktop()->getItemAtPoint(event_p, true); - bool do_fill = false, do_stroke = false, do_opacity = false; - guint32 fill_goal = sp_desktop_get_color_tool(desktop, "/tools/tweak", true, &do_fill); - guint32 stroke_goal = sp_desktop_get_color_tool(desktop, "/tools/tweak", false, &do_stroke); + bool do_opacity = false; + auto fill_goal = sp_desktop_get_color_tool(desktop, "/tools/tweak", true); + auto stroke_goal = sp_desktop_get_color_tool(desktop, "/tools/tweak", false); double opacity_goal = sp_desktop_get_master_opacity_tool(desktop, "/tools/tweak", &do_opacity); if (reverse) { -#if 0 - // HSL inversion - float hsv[3]; - float rgb[3]; - SPColor::rgb_to_hsv_floatv (hsv, - SP_RGBA32_R_F(fill_goal), - SP_RGBA32_G_F(fill_goal), - SP_RGBA32_B_F(fill_goal)); - SPColor::hsv_to_rgb_floatv (rgb, hsv[0]<.5? hsv[0]+.5 : hsv[0]-.5, 1 - hsv[1], 1 - hsv[2]); - fill_goal = SP_RGBA32_F_COMPOSE(rgb[0], rgb[1], rgb[2], 1); - SPColor::rgb_to_hsv_floatv (hsv, - SP_RGBA32_R_F(stroke_goal), - SP_RGBA32_G_F(stroke_goal), - SP_RGBA32_B_F(stroke_goal)); - SPColor::hsv_to_rgb_floatv (rgb, hsv[0]<.5? hsv[0]+.5 : hsv[0]-.5, 1 - hsv[1], 1 - hsv[2]); - stroke_goal = SP_RGBA32_F_COMPOSE(rgb[0], rgb[1], rgb[2], 1); -#else - // RGB inversion - fill_goal = SP_RGBA32_U_COMPOSE( - (255 - SP_RGBA32_R_U(fill_goal)), - (255 - SP_RGBA32_G_U(fill_goal)), - (255 - SP_RGBA32_B_U(fill_goal)), - (255 - SP_RGBA32_A_U(fill_goal))); - stroke_goal = SP_RGBA32_U_COMPOSE( - (255 - SP_RGBA32_R_U(stroke_goal)), - (255 - SP_RGBA32_G_U(stroke_goal)), - (255 - SP_RGBA32_B_U(stroke_goal)), - (255 - SP_RGBA32_A_U(stroke_goal))); -#endif + if (fill_goal) + fill_goal->invert(); + if (stroke_goal) + stroke_goal->invert(); opacity_goal = 1 - opacity_goal; } @@ -1025,10 +954,9 @@ sp_tweak_dilate (TweakTool *tc, Geom::Point event_p, Geom::Point p, Geom::Point std::vector items(selection->items().begin(), selection->items().end()); for(auto item : items){ if (is_color_mode (tc->mode)) { - if (do_fill || do_stroke || do_opacity) { + if (fill_goal || stroke_goal || do_opacity) { if (sp_tweak_color_recursive (tc->mode, item, item_at_point, - fill_goal, do_fill, - stroke_goal, do_stroke, + fill_goal, stroke_goal, opacity_goal, do_opacity, tc->mode == TWEAK_MODE_BLUR, reverse, p, radius, color_force, tc->do_h, tc->do_s, tc->do_l, tc->do_o)) { diff --git a/src/ui/util.cpp b/src/ui/util.cpp index 0e6f7f3395..ac99180ac7 100644 --- a/src/ui/util.cpp +++ b/src/ui/util.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -29,6 +30,9 @@ #include #include #include + +#include "colors/color.h" +#include "colors/utils.h" // color to hex string #include "util/numeric/converters.h" #if (defined (_WIN32) || defined (_WIN64)) @@ -254,6 +258,19 @@ void ellipsize(Gtk::Label &label, int const max_width_chars, Pango::EllipsizeMod } // namespace Inkscape::UI +/** + * Color is store as a string in the form #RRGGBBAA, '0' means "unset" + * + * @param color - The string color from glade. + */ +unsigned int get_color_value(const Glib::ustring color) +{ + Gdk::RGBA gdk_color = Gdk::RGBA(color); + return SP_RGBA32_F_COMPOSE(gdk_color.get_red(), gdk_color.get_green(), + gdk_color.get_blue(), gdk_color.get_alpha()); +} + + Gdk::RGBA mix_colors(const Gdk::RGBA& a, const Gdk::RGBA& b, float ratio) { auto lerp = [](double v0, double v1, double t){ return (1.0 - t) * v0 + t * v1; }; Gdk::RGBA result; @@ -291,6 +308,11 @@ guint32 to_guint32(Gdk::RGBA const &rgba) static_cast(0xFF * rgba.get_alpha() + 0.5); } +Gdk::RGBA color_to_rgba(Inkscape::Colors::Color const &color) +{ + return to_rgba(color.toRGBA()); +} + Gdk::RGBA to_rgba(guint32 const u32) { auto rgba = Gdk::RGBA{}; @@ -301,6 +323,22 @@ Gdk::RGBA to_rgba(guint32 const u32) return rgba; } +/** + * These GUI related color conversions allow us to convert from + * SVG xml attributes to Gdk colors, without needing the entire CMS + * framework, which would be excessive for widget painting. + */ +Glib::ustring gdk_to_css_color(const Gdk::RGBA& color) { + return Inkscape::Colors::rgba_to_hex(to_guint32(color), true); +} +Gdk::RGBA css_color_to_gdk(const char *value) { + try { + return to_rgba(value ? Inkscape::Colors::hex_to_rgba(value) : 0x0); + } catch (Inkscape::Colors::ColorError &e) { + return {}; + } +} + // 2Geom <-> Cairo Cairo::RectangleInt geom_to_cairo(const Geom::IntRect &rect) diff --git a/src/ui/util.h b/src/ui/util.h index 9dfaf8b334..cecc32ac7b 100644 --- a/src/ui/util.h +++ b/src/ui/util.h @@ -52,6 +52,10 @@ class TextBuffer; class Widget; } // namespace Gtk +namespace Inkscape::Colors { +class Color; +} // namespace Inkscape::Colors + Glib::ustring ink_ellipsize_text(Glib::ustring const &src, std::size_t maxlen); void reveal_widget(Gtk::Widget *widget, bool show); @@ -190,7 +194,12 @@ double get_luminance(const Gdk::RGBA &color); Gdk::RGBA get_color_with_class(Gtk::Widget &widget, Glib::ustring const &css_class); +// Convert a Gdk color to a hex code for css injection. +Glib::ustring gdk_to_css_color(const Gdk::RGBA& color); +Gdk::RGBA css_color_to_gdk(const char *value); + guint32 to_guint32(Gdk::RGBA const &rgba); +Gdk::RGBA color_to_rgba(Inkscape::Colors::Color const &color); Gdk::RGBA to_rgba(guint32 const u32); // convert Gdk::RGBA into 32-bit rrggbbaa color, optionally replacing alpha, if specified @@ -216,6 +225,7 @@ Cairo::RefPtr create_cubic_gradient( // If on Windows, get the native window & set it to DWMA_USE_IMMERSIVE_DARK_MODE void set_dark_titlebar(Glib::RefPtr const &surface, bool is_dark); +unsigned int get_color_value(const Glib::ustring color); // Cover for Glib::wrap not passing through const. template diff --git a/src/ui/widget/canvas-grid.cpp b/src/ui/widget/canvas-grid.cpp index 4880b5f493..ec56885c53 100644 --- a/src/ui/widget/canvas-grid.cpp +++ b/src/ui/widget/canvas-grid.cpp @@ -447,7 +447,7 @@ void CanvasGrid::_createGuideItem(Geom::Point const &pos, bool horiz) } _active_guide = make_canvasitem(desktop->getCanvasGuides(), Glib::ustring(), Geom::Point(), Geom::Point()); - _active_guide->set_stroke(desktop->getNamedView()->guidehicolor); + _active_guide->set_stroke(desktop->getNamedView()->getGuideHiColor().toRGBA()); } void CanvasGrid::_rulerMotion(GtkEventControllerMotion const *controller_c, double x, double y, bool horiz) diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp index 96d7959c7a..79a2d21fe5 100644 --- a/src/ui/widget/canvas.cpp +++ b/src/ui/widget/canvas.cpp @@ -42,8 +42,8 @@ #include "canvas/stores.h" #include "canvas/synchronizer.h" #include "canvas/util.h" -#include "color/cms-system.h" // Color correction -#include "color.h" // Background color +#include "colors/cms/transform.h" +#include "colors/cms/system.h" #include "desktop.h" #include "display/control/canvas-item-drawing.h" #include "display/control/canvas-item-group.h" @@ -156,7 +156,7 @@ struct RedrawData bool decoupled_mode; Cairo::RefPtr snapshot_drawn; Geom::OptIntRect grabbed; - std::shared_ptr cms_transform; + std::shared_ptr cms_transform; // Saved prefs int coarsener_min_size; @@ -1830,8 +1830,7 @@ void Canvas::set_cms_transform() // auto surface = get_surface(); // auto the_monitor = display->get_monitor_at_surface(surface); - auto cms_system = CMSSystem::get(); - _cms_transform = cms_system->get_cms_transform( /* monitor */ ); + _cms_transform = Colors::CMS::System::get().getDisplayTransform(); } // Change cursor @@ -2431,16 +2430,11 @@ void CanvasPrivate::paint_single_buffer(Cairo::RefPtr const auto buf = CanvasItemBuffer{ rect, scale_factor, cr, outline_pass }; canvasitem_ctx->root()->render(buf); - // Apply CMS transform. + // 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) { - surface->flush(); - auto px = surface->get_data(); - int stride = surface->get_stride(); - for (int i = 0; i < surface->get_height(); i++) { - auto row = px + i * stride; - Inkscape::CMSSystem::do_transform(rd.cms_transform->getHandle(), row, row, surface->get_width()); - } - surface->mark_dirty(); + rd.cms_transform->do_transform(surface->cobj(), surface->cobj()); } // Paint over newly drawn content with a translucent random colour. diff --git a/src/ui/widget/canvas.h b/src/ui/widget/canvas.h index 3928bcc4c3..37d83c5dfe 100644 --- a/src/ui/widget/canvas.h +++ b/src/ui/widget/canvas.h @@ -42,9 +42,12 @@ namespace Inkscape { class CanvasItem; class CanvasItemGroup; -class CMSTransform; class Drawing; +namespace Colors::CMS { + class Transform; +} + namespace UI::Widget { class CanvasPrivate; @@ -197,7 +200,7 @@ private: // CMS bool _cms_active = false; - std::shared_ptr _cms_transform; ///< The lcms transform to apply to canvas. + std::shared_ptr _cms_transform; ///< The lcms transform to apply to canvas. void set_cms_transform(); ///< Set the lcms transform. /* Internal state */ diff --git a/src/ui/widget/canvas/graphics.cpp b/src/ui/widget/canvas/graphics.cpp index a1eafc2d04..1184b89734 100644 --- a/src/ui/widget/canvas/graphics.cpp +++ b/src/ui/widget/canvas/graphics.cpp @@ -6,6 +6,8 @@ #include #include +#include "colors/color.h" +#include "display/cairo-utils.h" #include "helper/geom.h" #include "ui/util.h" #include "util.h" @@ -24,15 +26,17 @@ Cairo::RefPtr rgba_to_pattern(std::uint32_t const rgba) int constexpr w = 6; int constexpr h = 6; - auto dark = checkerboard_darken(rgba); + auto color = Colors::Color(rgba); + auto dark = checkerboard_darken(color); + color.enableOpacity(false); auto surface = Cairo::ImageSurface::create(Cairo::Surface::Format::ARGB32, 2 * w, 2 * h); auto cr = Cairo::Context::create(surface); cr->set_operator(Cairo::Context::Operator::SOURCE); - cr->set_source_rgb(SP_RGBA32_R_F(rgba), SP_RGBA32_G_F(rgba), SP_RGBA32_B_F(rgba)); + ink_cairo_set_source_color(cr, color); cr->paint(); - cr->set_source_rgb(dark[0], dark[1], dark[2]); + ink_cairo_set_source_color(cr, dark); cr->rectangle(0, 0, w, h); cr->rectangle(w, h, w, h); cr->fill(); diff --git a/src/ui/widget/canvas/util.cpp b/src/ui/widget/canvas/util.cpp index 3d9d59bc1c..3a1def52ed 100644 --- a/src/ui/widget/canvas/util.cpp +++ b/src/ui/widget/canvas/util.cpp @@ -42,16 +42,10 @@ Cairo::RefPtr shrink_region(Cairo::RefPtr const &r return reg2; } -std::array checkerboard_darken(std::array const &rgb, float amount) +Colors::Color checkerboard_darken(Colors::Color color) { - std::array hsl; - SPColor::rgb_to_hsl_floatv(&hsl[0], rgb[0], rgb[1], rgb[2]); - hsl[2] += (hsl[2] < 0.08 ? 0.08 : -0.08) * amount; - - std::array rgb2; - SPColor::hsl_to_rgb_floatv(&rgb2[0], hsl[0], hsl[1], hsl[2]); - - return rgb2; + auto opacity = color.stealOpacity(); + return Colors::make_contrasted_color(color, 1.0 - opacity); } } // namespace Widget diff --git a/src/ui/widget/canvas/util.h b/src/ui/widget/canvas/util.h index c2c1ad3a6b..e83d07c5c7 100644 --- a/src/ui/widget/canvas/util.h +++ b/src/ui/widget/canvas/util.h @@ -6,7 +6,9 @@ #include <2geom/int-rect.h> #include <2geom/affine.h> #include -#include "color.h" + +#include "colors/color.h" +#include "colors/utils.h" namespace Inkscape { namespace UI { @@ -50,11 +52,12 @@ inline auto premultiplied(std::array arr) return arr; } -std::array checkerboard_darken(std::array const &rgb, float amount = 1.0f); +Colors::Color checkerboard_darken(Colors::Color color); -inline auto checkerboard_darken(uint32_t rgba) +inline std::array checkerboard_darken(uint32_t rgba) { - return checkerboard_darken(rgb_to_array(rgba), 1.0f - SP_RGBA32_A_U(rgba) / 255.0f); + auto const color = checkerboard_darken(Colors::Color{rgba}); + return {(float)color[0], (float)color[1], (float)color[2]}; } } // namespace Widget diff --git a/src/ui/widget/color-entry.cpp b/src/ui/widget/color-entry.cpp index a4f7f76e31..f8b7fb4823 100644 --- a/src/ui/widget/color-entry.cpp +++ b/src/ui/widget/color-entry.cpp @@ -20,17 +20,16 @@ namespace Inkscape { namespace UI { namespace Widget { -ColorEntry::ColorEntry(SelectedColor &color) - : _color(color) +ColorEntry::ColorEntry(std::shared_ptr colors) + : _colors(std::move(colors)) , _updating(false) , _updatingrgba(false) , _prevpos(0) - , _lastcolor(0) + , _lastcolor() { set_name("ColorEntry"); - _color_changed_connection = color.signal_changed.connect(sigc::mem_fun(*this, &ColorEntry::_onColorChanged)); - _color_dragged_connection = color.signal_dragged.connect(sigc::mem_fun(*this, &ColorEntry::_onColorChanged)); + _color_changed_connection = _colors->signal_changed.connect(sigc::mem_fun(*this, &ColorEntry::_onColorChanged)); signal_activate().connect(sigc::mem_fun(*this, &ColorEntry::_onColorChanged)); get_buffer()->signal_inserted_text().connect(sigc::mem_fun(*this, &ColorEntry::_inputCheck)); _onColorChanged(); @@ -44,7 +43,6 @@ ColorEntry::ColorEntry(SelectedColor &color) ColorEntry::~ColorEntry() { _color_changed_connection.disconnect(); - _color_dragged_connection.disconnect(); } void ColorEntry::_inputCheck(guint pos, const gchar * /*chars*/, guint n_chars) @@ -56,74 +54,17 @@ void ColorEntry::_inputCheck(guint pos, const gchar * /*chars*/, guint n_chars) void ColorEntry::on_changed() { - if (_updating) { + if (_updating || _updatingrgba) { return; } - if (_updatingrgba) { - return; // Typing text into entry box - } - - Glib::ustring text = get_text(); - bool changed = false; - - // Coerce the value format to hexadecimal - for (auto it = text.begin(); it != text.end(); /*++it*/) { - if (!g_ascii_isxdigit(*it)) { - text.erase(it); - changed = true; - } else { - ++it; - } - } - - if (text.size() > 8) { - text.erase(_prevpos, 1); - changed = true; - } - // autofill rules - gchar *str = g_strdup(text.c_str()); - gchar *end = nullptr; - guint64 rgba = g_ascii_strtoull(str, &end, 16); - ptrdiff_t len = end - str; - if (len < 8) { - if (len == 0) { - rgba = _lastcolor; - } else if (len <= 2) { - if (len == 1) { - rgba *= 17; - } - rgba = (rgba << 24) + (rgba << 16) + (rgba << 8); - } else if (len <= 4) { - // display as rrggbbaa - rgba = rgba << (4 * (4 - len)); - guint64 r = rgba & 0xf000; - guint64 g = rgba & 0x0f00; - guint64 b = rgba & 0x00f0; - guint64 a = rgba & 0x000f; - rgba = 17 * ((r << 12) + (g << 8) + (b << 4) + a); - } else { - rgba = rgba << (4 * (8 - len)); - } - - if (len == 7) { - rgba = (rgba & 0xfffffff0) + (_lastcolor & 0x00f); - } else if (len == 5) { - rgba = (rgba & 0xfffff000) + (_lastcolor & 0xfff); - } else if (len != 4 && len != 8) { - rgba = (rgba & 0xffffff00) + (_lastcolor & 0x0ff); - } - } + auto new_color = Colors::Color::parse(get_text()); _updatingrgba = true; - if (changed) { - set_text(str); + if (new_color) { + _colors->setAll(*new_color); } - SPColor color(rgba); - _color.setColorAlpha(color, SP_RGBA32_A_F(rgba)); _updatingrgba = false; - - g_free(str); } @@ -133,14 +74,15 @@ void ColorEntry::_onColorChanged() return; } - SPColor color = _color.color(); - gdouble alpha = _color.alpha(); - - _lastcolor = color.toRGBA32(alpha); + if (_colors->isEmpty()) { + set_text(_("N/A")); + return; + } - auto text = Inkscape::ustring::format_classic(std::hex, std::setw(8), std::setfill('0'), _lastcolor); + _lastcolor = _colors->getAverage();; + auto text = _lastcolor->toString(); - Glib::ustring old_text = get_text(); + std::string old_text = get_text(); if (old_text != text) { _updating = true; set_text(text); diff --git a/src/ui/widget/color-entry.h b/src/ui/widget/color-entry.h index 4df80def9c..4a487403a2 100644 --- a/src/ui/widget/color-entry.h +++ b/src/ui/widget/color-entry.h @@ -13,7 +13,9 @@ #define SEEN_COLOR_ENTRY_H #include -#include "ui/selected-color.h" + +#include "colors/color.h" +#include "colors/color-set.h" namespace Inkscape { namespace UI { @@ -22,7 +24,7 @@ namespace Widget { class ColorEntry : public Gtk::Entry { public: - ColorEntry(SelectedColor &color); + ColorEntry(std::shared_ptr color); ~ColorEntry() override; protected: @@ -32,13 +34,13 @@ private: void _onColorChanged(); void _inputCheck(guint pos, const gchar * /*chars*/, guint /*n_chars*/); - SelectedColor &_color; - sigc::connection _color_changed_connection; - sigc::connection _color_dragged_connection; + std::shared_ptr _colors; bool _updating; bool _updatingrgba; - guint32 _lastcolor; int _prevpos; + std::optional _lastcolor; + + sigc::connection _color_changed_connection; }; } diff --git a/src/ui/widget/color-icc-selector.cpp b/src/ui/widget/color-icc-selector.cpp deleted file mode 100644 index adc45279ab..0000000000 --- a/src/ui/widget/color-icc-selector.cpp +++ /dev/null @@ -1,933 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/** @file - * TODO: insert short description here - *//* - * Authors: see git history - * - * Copyright (C) 2018 Authors - * Released under GNU GPL v2+, read the file 'COPYING' for more information. - */ - -#include "color-icc-selector.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "colorspace.h" -#include "document.h" -#include "inkscape.h" -#include "profile-manager.h" -#include "ui/dialog-events.h" -#include "ui/util.h" -#include "ui/widget/color-scales.h" -#include "ui/widget/color-slider.h" - -#define noDEBUG_LCMS - -#include "object/color-profile.h" - -#ifdef DEBUG_LCMS -#include "preferences.h" -#endif // DEBUG_LCMS - -#ifdef DEBUG_LCMS -extern guint update_in_progress; -#define DEBUG_MESSAGE(key, ...) \ - { \ - Inkscape::Preferences *prefs = Inkscape::Preferences::get(); \ - bool dump = prefs->getBool("/options/scislac/" #key); \ - bool dumpD = prefs->getBool("/options/scislac/" #key "D"); \ - bool dumpD2 = prefs->getBool("/options/scislac/" #key "D2"); \ - dumpD && = ((update_in_progress == 0) || dumpD2); \ - if (dump) { \ - g_message(__VA_ARGS__); \ - } \ - if (dumpD) { \ - GtkWidget *dialog = gtk_message_dialog_new(NULL, GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_INFO, \ - GTK_BUTTONS_OK, __VA_ARGS__); \ - g_signal_connect_swapped(dialog, "response", G_CALLBACK(gtk_widget_destroy), dialog); \ - gtk_widget_set_visible(dialog, true); \ - } \ - } -#endif // DEBUG_LCMS - -static constexpr int XPAD = 4; -static constexpr int YPAD = 1; - -namespace { - -GtkWidget *ink_combo_box_new_with_model(GtkTreeModel *model) -{ - auto combobox = Gtk::make_managed(); - gtk_combo_box_set_model(combobox->gobj(), model); - return combobox->Gtk::Widget::gobj(); -} - -size_t maxColorspaceComponentCount = 0; - -/** - * Internal variable to track all known colorspaces. - */ -std::set knownColorspaces; - -/** - * Helper function to handle GTK2/GTK3 attachment #ifdef code. - */ -void attachToGridOrTable(GtkWidget *parent, GtkWidget *child, guint left, guint top, guint width, guint height, - bool hexpand = false, bool centered = false, guint xpadding = XPAD, guint ypadding = YPAD) -{ - gtk_widget_set_margin_start(child, xpadding); - gtk_widget_set_margin_end(child, xpadding); - gtk_widget_set_margin_top(child, ypadding); - gtk_widget_set_margin_bottom(child, ypadding); - - if (hexpand) { - gtk_widget_set_hexpand(child, TRUE); - } - - if (centered) { - gtk_widget_set_halign(child, GTK_ALIGN_CENTER); - gtk_widget_set_valign(child, GTK_ALIGN_CENTER); - } - - gtk_grid_attach(GTK_GRID(parent), child, left, top, width, height); -} - -} // namespace - -/* -icSigRgbData -icSigCmykData -icSigCmyData -*/ -#define SPACE_ID_RGB 0 -#define SPACE_ID_CMY 1 -#define SPACE_ID_CMYK 2 - -colorspace::Component::Component() - : name() - , tip() - , scale(1) -{ -} - -colorspace::Component::Component(std::string name, std::string tip, guint scale) - : name(std::move(name)) - , tip(std::move(tip)) - , scale(scale) -{ -} - -static cmsUInt16Number *getScratch() -{ - // bytes per pixel * input channels * width - static std::array scritch; - return scritch.data(); -} - -std::vector colorspace::getColorSpaceInfo(uint32_t space) -{ - static std::map > sets; - if (sets.empty()) { - sets[cmsSigXYZData].emplace_back("_X", "X", 2); // TYPE_XYZ_16 - sets[cmsSigXYZData].emplace_back("_Y", "Y", 1); - sets[cmsSigXYZData].emplace_back("_Z", "Z", 2); - - sets[cmsSigLabData].emplace_back("_L", "L", 100); // TYPE_Lab_16 - sets[cmsSigLabData].emplace_back("_a", "a", 256); - sets[cmsSigLabData].emplace_back("_b", "b", 256); - - // cmsSigLuvData - - sets[cmsSigYCbCrData].emplace_back("_Y", "Y", 1); // TYPE_YCbCr_16 - sets[cmsSigYCbCrData].emplace_back("C_b", "Cb", 1); - sets[cmsSigYCbCrData].emplace_back("C_r", "Cr", 1); - - sets[cmsSigYxyData].emplace_back("_Y", "Y", 1); // TYPE_Yxy_16 - sets[cmsSigYxyData].emplace_back("_x", "x", 1); - sets[cmsSigYxyData].emplace_back("y", "y", 1); - - sets[cmsSigRgbData].emplace_back(_("_R:"), _("Red"), 1); // TYPE_RGB_16 - sets[cmsSigRgbData].emplace_back(_("_G:"), _("Green"), 1); - sets[cmsSigRgbData].emplace_back(_("_B:"), _("Blue"), 1); - - sets[cmsSigGrayData].emplace_back(_("G:"), _("Gray"), 1); // TYPE_GRAY_16 - - sets[cmsSigHsvData].emplace_back(_("_H:"), _("Hue"), 360); // TYPE_HSV_16 - sets[cmsSigHsvData].emplace_back(_("_S:"), _("Saturation"), 1); - sets[cmsSigHsvData].emplace_back("_V:", "Value", 1); - - sets[cmsSigHlsData].emplace_back(_("_H:"), _("Hue"), 360); // TYPE_HLS_16 - sets[cmsSigHlsData].emplace_back(_("_L:"), _("Lightness"), 1); - sets[cmsSigHlsData].emplace_back(_("_S:"), _("Saturation"), 1); - - sets[cmsSigCmykData].emplace_back(_("_C:"), _("Cyan"), 1); // TYPE_CMYK_16 - sets[cmsSigCmykData].emplace_back(_("_M:"), _("Magenta"), 1); - sets[cmsSigCmykData].emplace_back(_("_Y:"), _("Yellow"), 1); - sets[cmsSigCmykData].emplace_back(_("_K:"), _("Black"), 1); - - sets[cmsSigCmyData].emplace_back(_("_C:"), _("Cyan"), 1); // TYPE_CMY_16 - sets[cmsSigCmyData].emplace_back(_("_M:"), _("Magenta"), 1); - sets[cmsSigCmyData].emplace_back(_("_Y:"), _("Yellow"), 1); - - for (auto & set : sets) { - knownColorspaces.insert(set.first); - maxColorspaceComponentCount = std::max(maxColorspaceComponentCount, set.second.size()); - } - } - - std::vector target; - - if (sets.find(space) != sets.end()) { - target = sets[space]; - } - return target; -} - -std::vector colorspace::getColorSpaceInfo(Inkscape::ColorProfile *prof) -{ - return getColorSpaceInfo((uint32_t)0); // asICColorSpaceSig(prof->getColorSpace())); -} - -namespace Inkscape::UI::Widget { - -/** - * Class containing the parts for a single color component's UI presence. - */ -class ComponentUI final { -public: - explicit ComponentUI(colorspace::Component component = {}) - : _component(std::move(component)) - , _adj(nullptr) - , _slider(nullptr) - , _btn(nullptr) - , _label(nullptr) - , _map(4 * 1024, 0xFF) - { - } - - colorspace::Component _component; - Glib::RefPtr _adj; // Component adjustment - Inkscape::UI::Widget::ColorSlider *_slider; - GtkWidget *_btn; // spinbutton - GtkWidget *_label; // Label - std::vector _map; -}; - -/** - * Class that implements the internals of the selector. - */ -class ColorICCSelectorImpl final { -public: - ColorICCSelectorImpl(ColorICCSelector *owner, SelectedColor &color); - - void _adjustmentChanged(Glib::RefPtr const &adjustment); - - void _sliderGrabbed(); - void _sliderReleased(); - void _sliderChanged(); - - static void _profileSelected(GtkWidget *src, gpointer data); - static void _fixupHit(GtkWidget *src, gpointer data); - - void _setProfile(const std::string &profile); - void _switchToProfile(gchar const *name); - - void _updateSliders(gint ignore); - void _profilesChanged(std::string const &name); - - ColorICCSelector *_owner; - SelectedColor &_color; - - gboolean _updating : 1; - gboolean _dragging : 1; - - guint32 _fixupNeeded; - GtkWidget *_fixupBtn; - GtkWidget *_profileSel; - - std::vector _compUI; - - Glib::RefPtr _adj; // Channel adjustment - Inkscape::UI::Widget::ColorSlider *_slider; - GtkWidget *_sbtn; // Spinbutton - GtkWidget *_label; // Label - - std::string _profileName; - Inkscape::ColorProfile *_prof; - guint _profChannelCount; - gulong _profChangedID; -}; - -ColorICCSelector::ColorICCSelector(SelectedColor &color, bool no_alpha) - : _impl{std::make_unique(this, color)} -{ - init(no_alpha); - color.signal_changed.connect(sigc::mem_fun(*this, &ColorICCSelector::_colorChanged)); - color.signal_icc_changed.connect(sigc::mem_fun(*this, &ColorICCSelector::_colorChanged)); -} - -ColorICCSelector::~ColorICCSelector() = default; - -ColorICCSelectorImpl::ColorICCSelectorImpl(ColorICCSelector *owner, SelectedColor &color) - : _owner(owner) - , _color(color) - , _updating(FALSE) - , _dragging(FALSE) - , _fixupNeeded(0) - , _fixupBtn(nullptr) - , _profileSel(nullptr) - , _compUI() - , _adj(nullptr) - , _slider(nullptr) - , _sbtn(nullptr) - , _label(nullptr) - , _profileName() - , _prof(nullptr) - , _profChannelCount(0) - , _profChangedID(0) -{ -} - -void ColorICCSelector::init(bool no_alpha) -{ - gint row = 0; - - _impl->_updating = FALSE; - _impl->_dragging = FALSE; - - GtkWidget *t = GTK_WIDGET(gobj()); - - _impl->_compUI.clear(); - - // Create components - row = 0; - - _impl->_fixupBtn = gtk_button_new_with_label(_("Fix")); - g_signal_connect(G_OBJECT(_impl->_fixupBtn), "clicked", G_CALLBACK(ColorICCSelectorImpl::_fixupHit), - _impl.get()); - gtk_widget_set_sensitive(_impl->_fixupBtn, FALSE); - gtk_widget_set_tooltip_text(_impl->_fixupBtn, _("Fix RGB fallback to match icc-color() value.")); - gtk_widget_set_visible(_impl->_fixupBtn, true); - - attachToGridOrTable(t, _impl->_fixupBtn, 0, row, 1, 1); - - // Combobox and store with 2 columns : label (0) and full name (1) - GtkListStore *store = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_STRING); - _impl->_profileSel = ink_combo_box_new_with_model(GTK_TREE_MODEL(store)); - - GtkCellRenderer *renderer = gtk_cell_renderer_text_new(); - gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(_impl->_profileSel), renderer, TRUE); - gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(_impl->_profileSel), renderer, "text", 0, nullptr); - - GtkTreeIter iter; - gtk_list_store_append(store, &iter); - gtk_list_store_set(store, &iter, 0, _(""), 1, "null", -1); - - gtk_widget_set_visible(_impl->_profileSel, true); - gtk_combo_box_set_active(GTK_COMBO_BOX(_impl->_profileSel), 0); - - attachToGridOrTable(t, _impl->_profileSel, 1, row, 1, 1); - - _impl->_profChangedID = g_signal_connect(G_OBJECT(_impl->_profileSel), "changed", - G_CALLBACK(ColorICCSelectorImpl::_profileSelected), _impl.get()); - - row++; - - // populate the data for colorspaces and channels: - std::vector things = colorspace::getColorSpaceInfo(cmsSigRgbData); - - for (size_t i = 0; i < maxColorspaceComponentCount; i++) { - if (i < things.size()) { - _impl->_compUI.emplace_back(things[i]); - } - else { - _impl->_compUI.emplace_back(); - } - - auto const labelStr = i < things.size() ? things[i].name.c_str() : ""; - _impl->_compUI[i]._label = gtk_label_new_with_mnemonic(labelStr); - - gtk_widget_set_halign(_impl->_compUI[i]._label, GTK_ALIGN_END); - gtk_widget_set_visible(_impl->_compUI[i]._label, true); - - attachToGridOrTable(t, _impl->_compUI[i]._label, 0, row, 1, 1); - - // Adjustment - guint scaleValue = _impl->_compUI[i]._component.scale; - gdouble step = static_cast(scaleValue) / 100.0; - gdouble page = static_cast(scaleValue) / 10.0; - gint digits = (step > 0.9) ? 0 : 2; - _impl->_compUI[i]._adj = Gtk::Adjustment::create(0.0, 0.0, scaleValue, step, page, page); - - // Slider - _impl->_compUI[i]._slider = - Gtk::make_managed(_impl->_compUI[i]._adj); - _impl->_compUI[i]._slider->set_tooltip_text((i < things.size()) ? things[i].tip.c_str() : ""); - _impl->_compUI[i]._slider->set_visible(true); - - attachToGridOrTable(t, _impl->_compUI[i]._slider->Gtk::Widget::gobj(), 1, row, 1, 1, true); - - auto const spinbutton = Gtk::make_managed(_impl->_compUI[i]._adj, step, digits); - _impl->_compUI[i]._btn = spinbutton->Gtk::Widget::gobj(); - spinbutton->set_tooltip_text(i < things.size() ? things[i].tip.c_str() : ""); - sp_dialog_defocus_on_enter(*spinbutton); - gtk_label_set_mnemonic_widget(GTK_LABEL(_impl->_compUI[i]._label), _impl->_compUI[i]._btn); - gtk_widget_set_visible(_impl->_compUI[i]._btn, true); - - attachToGridOrTable(t, _impl->_compUI[i]._btn, 2, row, 1, 1, false, true); - - // Signals - _impl->_compUI[i]._adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*_impl, &ColorICCSelectorImpl::_adjustmentChanged), _impl->_compUI[i]._adj)); - - _impl->_compUI[i]._slider->signal_grabbed.connect(sigc::mem_fun(*_impl, &ColorICCSelectorImpl::_sliderGrabbed)); - _impl->_compUI[i]._slider->signal_released.connect( - sigc::mem_fun(*_impl, &ColorICCSelectorImpl::_sliderReleased)); - _impl->_compUI[i]._slider->signal_value_changed.connect( - sigc::mem_fun(*_impl, &ColorICCSelectorImpl::_sliderChanged)); - - row++; - } - - // Label - _impl->_label = gtk_label_new_with_mnemonic(_("_A:")); - - gtk_widget_set_halign(_impl->_label, GTK_ALIGN_END); - gtk_widget_set_visible(_impl->_label, true); - - attachToGridOrTable(t, _impl->_label, 0, row, 1, 1); - - // Adjustment - _impl->_adj = Gtk::Adjustment::create(0.0, 0.0, 100.0, 1.0, 10.0, 10.0); - - // Slider - _impl->_slider = Gtk::make_managed(_impl->_adj); - _impl->_slider->set_tooltip_text(_("Alpha (opacity)")); - _impl->_slider->set_visible(true); - - attachToGridOrTable(t, _impl->_slider->Gtk::Widget::gobj(), 1, row, 1, 1, true); - - _impl->_slider->setColors(SP_RGBA32_F_COMPOSE(1.0, 1.0, 1.0, 0.0), SP_RGBA32_F_COMPOSE(1.0, 1.0, 1.0, 0.5), - SP_RGBA32_F_COMPOSE(1.0, 1.0, 1.0, 1.0)); - - // Spinbutton - auto const spinbuttonalpha = Gtk::make_managed(_impl->_adj, 1.0); - _impl->_sbtn = spinbuttonalpha->Gtk::Widget::gobj(); - spinbuttonalpha->set_tooltip_text(_("Alpha (opacity)")); - sp_dialog_defocus_on_enter(*spinbuttonalpha); - gtk_label_set_mnemonic_widget(GTK_LABEL(_impl->_label), _impl->_sbtn); - gtk_widget_set_visible(_impl->_sbtn, true); - - if (no_alpha) { - _impl->_slider->set_visible(false); - gtk_widget_set_visible(_impl->_label, false); - gtk_widget_set_visible(_impl->_sbtn, false); - } - - attachToGridOrTable(t, _impl->_sbtn, 2, row, 1, 1, false, true); - - // Signals - _impl->_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*_impl, &ColorICCSelectorImpl::_adjustmentChanged), _impl->_adj)); - - _impl->_slider->signal_grabbed.connect(sigc::mem_fun(*_impl, &ColorICCSelectorImpl::_sliderGrabbed)); - _impl->_slider->signal_released.connect(sigc::mem_fun(*_impl, &ColorICCSelectorImpl::_sliderReleased)); - _impl->_slider->signal_value_changed.connect(sigc::mem_fun(*_impl, &ColorICCSelectorImpl::_sliderChanged)); - - gtk_widget_set_visible(t, true); -} - -void ColorICCSelectorImpl::_fixupHit(GtkWidget * /*src*/, gpointer data) -{ - ColorICCSelectorImpl *self = reinterpret_cast(data); - gtk_widget_set_sensitive(self->_fixupBtn, FALSE); - self->_adjustmentChanged(self->_compUI[0]._adj); -} - -void ColorICCSelectorImpl::_profileSelected(GtkWidget * /*src*/, gpointer data) -{ - ColorICCSelectorImpl *self = reinterpret_cast(data); - - GtkTreeIter iter; - if (gtk_combo_box_get_active_iter(GTK_COMBO_BOX(self->_profileSel), &iter)) { - GtkTreeModel *store = gtk_combo_box_get_model(GTK_COMBO_BOX(self->_profileSel)); - gchar *name = nullptr; - - gtk_tree_model_get(store, &iter, 1, &name, -1); - self->_switchToProfile(name); - gtk_widget_set_tooltip_text(self->_profileSel, name); - - g_free(name); - } -} - -void ColorICCSelectorImpl::_switchToProfile(gchar const *name) -{ - bool dirty = false; - SPColor tmp(_color.color()); - - if (name && std::string(name) != "null") { - if (tmp.getColorProfile() == name) { -#ifdef DEBUG_LCMS - g_message("Already at name [%s]", name); -#endif // DEBUG_LCMS - } - else { -#ifdef DEBUG_LCMS - g_message("Need to switch to profile [%s]", name); -#endif // DEBUG_LCMS - - if (auto newProf = SP_ACTIVE_DOCUMENT->getProfileManager().find(name)) { - cmsHTRANSFORM trans = newProf->getTransfFromSRGB8(); - if (trans) { - guint32 val = _color.color().toRGBA32(0); - guchar pre[4] = { - static_cast(SP_RGBA32_R_U(val)), - static_cast(SP_RGBA32_G_U(val)), - static_cast(SP_RGBA32_B_U(val)), - 255}; -#ifdef DEBUG_LCMS - g_message("Shoving in [%02x] [%02x] [%02x]", pre[0], pre[1], pre[2]); -#endif // DEBUG_LCMS - cmsUInt16Number post[4] = { 0, 0, 0, 0 }; - cmsDoTransform(trans, pre, post, 1); -#ifdef DEBUG_LCMS - g_message("got on out [%04x] [%04x] [%04x] [%04x]", post[0], post[1], post[2], post[3]); -#endif // DEBUG_LCMS - guint count = 0; // cmsChannelsOf(asICColorSpaceSig(newProf->getColorSpace())); - - std::vector things = - colorspace::getColorSpaceInfo((uint32_t)0); // asICColorSpaceSig(newProf->getColorSpace())); - - std::vector colors; - for (guint i = 0; i < count; i++) { - gdouble val = - (((gdouble)post[i]) / 65535.0) * (gdouble)((i < things.size()) ? things[i].scale : 1); -#ifdef DEBUG_LCMS - g_message(" scaled %d by %d to be %f", i, ((i < things.size()) ? things[i].scale : 1), val); -#endif // DEBUG_LCMS - colors.push_back(val); - } - - cmsHTRANSFORM retrans = newProf->getTransfToSRGB8(); - if (retrans) { - cmsDoTransform(retrans, post, pre, 1); -#ifdef DEBUG_LCMS - g_message(" back out [%02x] [%02x] [%02x]", pre[0], pre[1], pre[2]); -#endif // DEBUG_LCMS - tmp.set(SP_RGBA32_U_COMPOSE(pre[0], pre[1], pre[2], 0xff)); - tmp.setColorProfile(newProf); - tmp.setColors(std::move(colors)); - } else { - g_warning("Couldn't get sRGB from color profile."); - } - - dirty = true; - } - } - } - } - else { -#ifdef DEBUG_LCMS - g_message("NUKE THE ICC"); -#endif // DEBUG_LCMS - if (tmp.hasColorProfile()) { - tmp.unsetColorProfile(); - dirty = true; - _fixupHit(nullptr, this); - } - else { -#ifdef DEBUG_LCMS - g_message("No icc to nuke"); -#endif // DEBUG_LCMS - } - } - - if (dirty) { -#ifdef DEBUG_LCMS - g_message("+----------------"); - g_message("+ new color is [%s]", tmp.toString().c_str()); -#endif // DEBUG_LCMS - _setProfile(tmp.getColorProfile()); - _color.setColor(tmp); -#ifdef DEBUG_LCMS - g_message("+_________________"); -#endif // DEBUG_LCMS - } -} - -struct _cmp { - bool operator()(const SPObject * const & a, const SPObject * const & b) - { - const Inkscape::ColorProfile &a_prof = reinterpret_cast(*a); - const Inkscape::ColorProfile &b_prof = reinterpret_cast(*b); - gchar *a_name_casefold = g_utf8_casefold(a_prof.name, -1 ); - gchar *b_name_casefold = g_utf8_casefold(b_prof.name, -1 ); - int result = g_strcmp0(a_name_casefold, b_name_casefold); - g_free(a_name_casefold); - g_free(b_name_casefold); - return result < 0; - } -}; - -template -struct static_caster { To * operator () (From * value) const { return static_cast(value); } }; - -void ColorICCSelectorImpl::_profilesChanged(std::string const &name) -{ - GtkComboBox *combo = GTK_COMBO_BOX(_profileSel); - - g_signal_handler_block(G_OBJECT(_profileSel), _profChangedID); - - GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(combo)); - gtk_list_store_clear(store); - - GtkTreeIter iter; - gtk_list_store_append(store, &iter); - gtk_list_store_set(store, &iter, 0, _(""), 1, "null", -1); - - gtk_combo_box_set_active(combo, 0); - - int index = 1; - std::vector current = SP_ACTIVE_DOCUMENT->getResourceList("iccprofile"); - - std::set _current; - std::transform(current.begin(), - current.end(), - std::inserter(_current, _current.begin()), - static_caster()); - - for (auto &it: _current) { - Inkscape::ColorProfile *prof = it; - - gtk_list_store_append(store, &iter); - gtk_list_store_set(store, &iter, 0, ink_ellipsize_text(prof->name, 25).c_str(), 1, prof->name, -1); - - if (name == prof->name) { - gtk_combo_box_set_active(combo, index); - gtk_widget_set_tooltip_text(_profileSel, prof->name); - } - - index++; - } - - g_signal_handler_unblock(G_OBJECT(_profileSel), _profChangedID); -} - -void ColorICCSelector::on_show() -{ - Gtk::Grid::on_show(); - _colorChanged(); -} - -// Helpers for setting color value - -void ColorICCSelector::_colorChanged() -{ - _impl->_updating = TRUE; - auto color = _impl->_color.color(); - auto name = color.getColorProfile(); - -#ifdef DEBUG_LCMS - g_message("/^^^^^^^^^ %p::_colorChanged(%08x:%s)", this, color.toRGBA32(_impl->_color.alpha()), name.c_str()); -#endif // DEBUG_LCMS - - _impl->_profilesChanged(name); - ColorScales<>::setScaled(_impl->_adj, _impl->_color.alpha()); - - _impl->_setProfile(name); - _impl->_fixupNeeded = 0; - gtk_widget_set_sensitive(_impl->_fixupBtn, FALSE); - - if (_impl->_prof) { - if (_impl->_prof->getTransfToSRGB8()) { - cmsUInt16Number tmp[4]; - for (guint i = 0; i < _impl->_profChannelCount; i++) { - auto colors = color.getColors(); - gdouble val = 0.0; - if (colors.size() > i) { - auto scale = static_cast(_impl->_compUI[i]._component.scale); - if (_impl->_compUI[i]._component.scale == 256) { - val = (colors[i] + 128.0) / scale; - } - else { - val = colors[i] / scale; - } - } - tmp[i] = val * 0x0ffff; - } - guchar post[4] = { 0, 0, 0, 0 }; - cmsHTRANSFORM trans = _impl->_prof->getTransfToSRGB8(); - if (trans) { - cmsDoTransform(trans, tmp, post, 1); - guint32 other = SP_RGBA32_U_COMPOSE(post[0], post[1], post[2], 255); - if (other != color.toRGBA32(255)) { - _impl->_fixupNeeded = other; - gtk_widget_set_sensitive(_impl->_fixupBtn, TRUE); -#ifdef DEBUG_LCMS - g_message("Color needs to change 0x%06x to 0x%06x", color.toRGBA32(255) >> 8, other >> 8); -#endif // DEBUG_LCMS - } - } - } - } - _impl->_updateSliders(-1); - - _impl->_updating = FALSE; -#ifdef DEBUG_LCMS - g_message("\\_________ %p::_colorChanged()", this); -#endif // DEBUG_LCMS -} - -void ColorICCSelectorImpl::_setProfile(const std::string &profile) -{ -#ifdef DEBUG_LCMS - g_message("/^^^^^^^^^ %p::_setProfile(%s)", this, profile.c_str()); -#endif // DEBUG_LCMS - bool profChanged = false; - if (_prof && _profileName != profile) { - // Need to clear out the prior one - profChanged = true; - _profileName.clear(); - _prof = nullptr; - _profChannelCount = 0; - } else if (!_prof && !profile.empty()) { - profChanged = true; - } - - for (auto & i : _compUI) { - gtk_widget_set_visible(i._label, false); - i._slider->set_visible(false); - gtk_widget_set_visible(i._btn, false); - } - - if (!profile.empty()) { - _prof = SP_ACTIVE_DOCUMENT->getProfileManager().find(profile.c_str()); - // if (_prof && (asICColorProfileClassSig(_prof->getProfileClass()) != cmsSigNamedColorClass)) { - if (false) { - _profChannelCount = _prof->getChannelCount(); - - if (profChanged) { - std::vector things = - colorspace::getColorSpaceInfo((uint32_t)0); // asICColorSpaceSig(_prof->getColorSpace())); - for (size_t i = 0; (i < things.size()) && (i < _profChannelCount); ++i) { - _compUI[i]._component = things[i]; - } - - for (guint i = 0; i < _profChannelCount; i++) { - gtk_label_set_text_with_mnemonic(GTK_LABEL(_compUI[i]._label), - (i < things.size()) ? things[i].name.c_str() : ""); - - _compUI[i]._slider->set_tooltip_text((i < things.size()) ? things[i].tip.c_str() : ""); - gtk_widget_set_tooltip_text(_compUI[i]._btn, (i < things.size()) ? things[i].tip.c_str() : ""); - - _compUI[i]._slider->setColors(SPColor(0.0, 0.0, 0.0).toRGBA32(0xff), - SPColor(0.5, 0.5, 0.5).toRGBA32(0xff), - SPColor(1.0, 1.0, 1.0).toRGBA32(0xff)); - gtk_widget_set_visible(_compUI[i]._label, true); - _compUI[i]._slider->set_visible(true); - gtk_widget_set_visible(_compUI[i]._btn, true); - } - for (size_t i = _profChannelCount; i < _compUI.size(); i++) { - gtk_widget_set_visible(_compUI[i]._label, false); - _compUI[i]._slider->set_visible(false); - gtk_widget_set_visible(_compUI[i]._btn, false); - } - } - } else { - // Give up for now on named colors - _prof = nullptr; - } - } - -#ifdef DEBUG_LCMS - g_message("\\_________ %p::_setProfile()", this); -#endif // DEBUG_LCMS -} - -void ColorICCSelectorImpl::_updateSliders(gint ignore) -{ - _slider->set_sensitive(false); - - if (_color.color().hasColorProfile()) { - auto colors = _color.color().getColors(); - if (colors.size() != _profChannelCount) { - g_warning("Can't set profile with %d colors to %d channels", (int)colors.size(), _profChannelCount); - } - for (guint i = 0; i < _profChannelCount; i++) { - double val = 0.0; - auto scale = static_cast(_compUI[i]._component.scale); - if (_compUI[i]._component.scale == 256) { - val = (colors[i] + 128.0) / scale; - } else { - val = colors[i] / scale; - } - _compUI[i]._adj->set_value(val); - } - - if (_prof) { - _slider->set_sensitive(true); - - if (_prof->getTransfToSRGB8()) { - for (guint i = 0; i < _profChannelCount; i++) { - if (static_cast(i) != ignore) { - cmsUInt16Number *scratch = getScratch(); - cmsUInt16Number filler[4] = { 0, 0, 0, 0 }; - for (guint j = 0; j < _profChannelCount; j++) { - filler[j] = 0x0ffff * ColorScales<>::getScaled(_compUI[j]._adj); - } - - cmsUInt16Number *p = scratch; - for (guint x = 0; x < 1024; x++) { - for (guint j = 0; j < _profChannelCount; j++) { - if (j == i) { - *p++ = x * 0x0ffff / 1024; - } - else { - *p++ = filler[j]; - } - } - } - - cmsHTRANSFORM trans = _prof->getTransfToSRGB8(); - if (trans) { - cmsDoTransform(trans, scratch, _compUI[i]._map.data(), 1024); - if (_compUI[i]._slider) { - _compUI[i]._slider->setMap(_compUI[i]._map.data()); - } - } - } - } - } - } - } - - guint32 start = _color.color().toRGBA32(0x00); - guint32 mid = _color.color().toRGBA32(0x7f); - guint32 end = _color.color().toRGBA32(0xff); - - _slider->setColors(start, mid, end); -} - -void ColorICCSelectorImpl::_adjustmentChanged(Glib::RefPtr const &adjustment) -{ -#ifdef DEBUG_LCMS - g_message("/^^^^^^^^^ %p::_adjustmentChanged()", this); -#endif // DEBUG_LCMS - - ColorICCSelector *iccSelector = _owner; - if (iccSelector->_impl->_updating) { - return; - } - - iccSelector->_impl->_updating = TRUE; - - gint match = -1; - - SPColor newColor(iccSelector->_impl->_color.color()); - gfloat scaled = ColorScales<>::getScaled(iccSelector->_impl->_adj); - if (iccSelector->_impl->_adj == adjustment) { -#ifdef DEBUG_LCMS - g_message("ALPHA"); -#endif // DEBUG_LCMS - } - else { - for (size_t i = 0; i < iccSelector->_impl->_compUI.size(); i++) { - if (iccSelector->_impl->_compUI[i]._adj == adjustment) { - match = i; - break; - } - } - if (match >= 0) { -#ifdef DEBUG_LCMS - g_message(" channel %d", match); -#endif // DEBUG_LCMS - } - - cmsUInt16Number tmp[4]; - for (guint i = 0; i < 4; i++) { - tmp[i] = ColorScales<>::getScaled(iccSelector->_impl->_compUI[i]._adj) * 0x0ffff; - } - guchar post[4] = { 0, 0, 0, 0 }; - - cmsHTRANSFORM trans = iccSelector->_impl->_prof->getTransfToSRGB8(); - if (trans) { - cmsDoTransform(trans, tmp, post, 1); - } - - // Set the sRGB version of the color first. - guint32 prior = iccSelector->_impl->_color.color().toRGBA32(255); - guint32 newer = SP_RGBA32_U_COMPOSE(post[0], post[1], post[2], 255); - - if (prior != newer) { -#ifdef DEBUG_LCMS - g_message("Transformed color from 0x%08x to 0x%08x", prior, newer); - g_message(" ~~~~ FLIP"); -#endif // DEBUG_LCMS - - // Be careful to always set() and then setColors() to retain ICC data. - newColor.set(newer); - if (iccSelector->_impl->_color.color().hasColorProfile()) { - std::vector colors; - for (guint i = 0; i < iccSelector->_impl->_profChannelCount; i++) { - double val = ColorScales<>::getScaled(iccSelector->_impl->_compUI[i]._adj); - val *= iccSelector->_impl->_compUI[i]._component.scale; - if (iccSelector->_impl->_compUI[i]._component.scale == 256) { - val -= 128; - } - colors.push_back(val); - } - newColor.setColors(std::move(colors)); - } - } - } - iccSelector->_impl->_color.setColorAlpha(newColor, scaled); - iccSelector->_impl->_updateSliders(match); - - iccSelector->_impl->_updating = FALSE; -#ifdef DEBUG_LCMS - g_message("\\_________ %p::_adjustmentChanged()", this); -#endif // DEBUG_LCMS -} - -void ColorICCSelectorImpl::_sliderGrabbed() -{ -} - -void ColorICCSelectorImpl::_sliderReleased() -{ -} - -void ColorICCSelectorImpl::_sliderChanged() -{ -} - -Gtk::Widget *ColorICCSelectorFactory::createWidget(Inkscape::UI::SelectedColor &color, bool no_alpha) const -{ - return Gtk::make_managed(color, no_alpha); -} - -Glib::ustring ColorICCSelectorFactory::modeName() const -{ - return _("CMS"); -} - -} // namespace Inkscape::UI::widget - -/* - Local Variables: - mode:c++ - c-file-style:"stroustrup" - c-file-offsets:((innamespace . 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/color-icc-selector.h b/src/ui/widget/color-icc-selector.h deleted file mode 100644 index 9f8afee259..0000000000 --- a/src/ui/widget/color-icc-selector.h +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/** @file - * TODO: insert short description here - *//* - * Authors: see git history - * - * Copyright (C) 2018 Authors - * Released under GNU GPL v2+, read the file 'COPYING' for more information. - */ -#ifndef SEEN_SP_COLOR_ICC_SELECTOR_H -#define SEEN_SP_COLOR_ICC_SELECTOR_H - -#include -#include -#include -#include "ui/selected-color.h" - -namespace Inkscape { - -class ColorProfile; - -namespace UI::Widget { - -class ColorICCSelectorImpl; - -class ColorICCSelector final - : public Gtk::Grid - { -public: - ColorICCSelector(SelectedColor &color, bool no_alpha); - ~ColorICCSelector() final; - - ColorICCSelector(const ColorICCSelector &obj) = delete; - ColorICCSelector &operator=(const ColorICCSelector &obj) = delete; - - void init(bool no_alpha); - -protected: - void on_show() final; - - virtual void _colorChanged(); - - void _recalcColor(bool changing); - -private: - friend class ColorICCSelectorImpl; - std::unique_ptr _impl; -}; - - -class ColorICCSelectorFactory final : public ColorSelectorFactory { -public: - Gtk::Widget *createWidget(SelectedColor &color, bool no_alpha) const final; - Glib::ustring modeName() const final; -}; - -} // namespace UI::Widget - -} // namespace Inkscape - -#endif // SEEN_SP_COLOR_ICC_SELECTOR_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/color-notebook.cpp b/src/ui/widget/color-notebook.cpp index b1884015e5..3a833f2477 100644 --- a/src/ui/widget/color-notebook.cpp +++ b/src/ui/widget/color-notebook.cpp @@ -26,8 +26,6 @@ #include "document.h" #include "inkscape.h" #include "preferences.h" -#include "profile-manager.h" -#include "color/cms-system.h" #include "object/color-profile.h" #include "ui/dialog-events.h" #include "ui/icon-loader.h" @@ -38,22 +36,20 @@ #include "ui/widget/color-scales.h" #include "ui/widget/icon-combobox.h" -using Inkscape::CMSSystem; - static constexpr int XPAD = 2; static constexpr int YPAD = 1; namespace Inkscape::UI::Widget { -ColorNotebook::ColorNotebook(SelectedColor &color, bool no_alpha) - : _selected_color(color) +ColorNotebook::ColorNotebook(SelectedColor color) + : _selected_color(std::move(color)) { + assert(_selected_color); set_name("ColorNotebook"); - _initUI(no_alpha); + _initUI(!_selected_color->getAlphaConstraint().value_or(true)); - _selected_color.signal_changed.connect(sigc::mem_fun(*this, &ColorNotebook::_onSelectedColorChanged)); - _selected_color.signal_dragged.connect(sigc::mem_fun(*this, &ColorNotebook::_onSelectedColorChanged)); + //_selected_color.signal_changed.connect(sigc::mem_fun(*this, &ColorNotebook::_onSelectedColorChanged)); auto desktop = SP_ACTIVE_DESKTOP; _doc_replaced_connection = desktop->connectDocumentReplaced(sigc::hide<0>(sigc::mem_fun(*this, &ColorNotebook::setDocument))); @@ -76,12 +72,12 @@ ColorNotebook::Page::Page(std::unique_ptr se void ColorNotebook::setDocument(SPDocument *document) { _document = document; - _icc_changed_connection.disconnect(); + /*_icc_changed_connection.disconnect(); if (document) { _icc_changed_connection = document->connectResourcesChanged("iccprofile", [this]() { _selected_color.emitIccChanged(); }); - } + }*/ } void ColorNotebook::set_label(const Glib::ustring& label) { @@ -230,20 +226,18 @@ void ColorNotebook::_onPickerClicked(GtkWidget * /*widget*/, ColorNotebook *colo Inkscape::UI::Tools::sp_toggle_dropper(SP_ACTIVE_DESKTOP); auto tool = dynamic_cast(SP_ACTIVE_DESKTOP->getTool()); if (tool) { - colorbook->_onetimepick = tool->onetimepick_signal.connect(sigc::mem_fun(*colorbook, &ColorNotebook::_pickColor)); + colorbook->_onetimepick = tool->onetimepick_signal.connect([colorbook](Colors::Color const &color) { + // Set color to color notebook here. + colorbook->_selected_color->setAll(color); + //_onSelectedColorChanged(); + }); } } } -void ColorNotebook::_pickColor(ColorRGBA *color) { - // Set color to color notebook here. - _selected_color.setValue(color->getIntValue()); - _onSelectedColorChanged(); -} +//void ColorNotebook::_onSelectedColorChanged() { _updateICCButtons(); } -void ColorNotebook::_onSelectedColorChanged() { _updateICCButtons(); } - -void ColorNotebook::_updateICCButtons() +/*void ColorNotebook::_updateICCButtons() { if (!_document) { return; @@ -254,7 +248,6 @@ void ColorNotebook::_updateICCButtons() g_return_if_fail((0.0 <= alpha) && (alpha <= 1.0)); - /* update color management icon*/ gtk_widget_set_sensitive(_colormanaged, color.hasColorProfile()); gtk_widget_set_sensitive(_toomuchink, false); gtk_widget_set_sensitive(_outofgamut, false); @@ -265,13 +258,11 @@ void ColorNotebook::_updateICCButtons() // Set notebook page to cms if icc profile being used. _setCurrentPage(getPageIndex("CMS"), true); - /* update out-of-gamut icon */ Inkscape::ColorProfile *target_profile = _document->getProfileManager().find(name.c_str()); if (target_profile) gtk_widget_set_sensitive(_outofgamut, target_profile->GamutCheck(color)); - /* update too-much-ink icon */ Inkscape::ColorProfile *prof = _document->getProfileManager().find(name.c_str()); if (prof && prof->isPrintColorSpace()) { gtk_widget_set_visible(_toomuchink, true); @@ -280,9 +271,6 @@ void ColorNotebook::_updateICCButtons() ink_sum += i; } - /* Some literature states that when the sum of paint values exceed 320%, it is considered to be a satured color, - which means the paper can get too wet due to an excessive amount of ink. This may lead to several issues - such as misalignment and poor quality of printing in general.*/ if (ink_sum > 3.2) gtk_widget_set_sensitive(_toomuchink, true); } @@ -294,7 +282,7 @@ void ColorNotebook::_updateICCButtons() auto page = prefs->getString("/colorselector/page"); _setCurrentPage(getPageIndex(page), true); } -} +}*/ int ColorNotebook::getPageIndex(const Glib::ustring &name) { diff --git a/src/ui/widget/color-notebook.h b/src/ui/widget/color-notebook.h index b0e14f092a..831dd6ef5f 100644 --- a/src/ui/widget/color-notebook.h +++ b/src/ui/widget/color-notebook.h @@ -21,10 +21,10 @@ #include // for GtkWidget, Widget (ptr only) #include // for connection +#include "colors/color-set.h" +#include "color-scales.h" #include "preferences.h" // for PrefObserver -#include "ui/selected-color.h" // for ColorSelectorFactory, SelectedColor (... -class ColorRGBA; class SPDocument; namespace Gtk { @@ -42,7 +42,7 @@ class ColorNotebook : public Gtk::Grid { public: - ColorNotebook(SelectedColor &color, bool no_alpha = false); + ColorNotebook(SelectedColor color); ~ColorNotebook() override; void set_label(const Glib::ustring& label); @@ -59,16 +59,14 @@ protected: void _addPage(Page &page, bool no_alpha, const Glib::ustring vpath); void setDocument(SPDocument *document); - void _pickColor(ColorRGBA *color); static void _onPickerClicked(GtkWidget *widget, ColorNotebook *colorbook); - virtual void _onSelectedColorChanged(); + //virtual void _onSelectedColorChanged(); int getPageIndex(const Glib::ustring &name); int getPageIndex(Gtk::Widget *widget); void _updateICCButtons(); void _setCurrentPage(int i, bool sync_combo); - Inkscape::UI::SelectedColor &_selected_color; unsigned long _entryId = 0; Gtk::Stack* _book = nullptr; Gtk::StackSwitcher* _switcher = nullptr; @@ -89,7 +87,7 @@ private: SPDocument *_document = nullptr; sigc::connection _doc_replaced_connection; - sigc::connection _icc_changed_connection; + SelectedColor _selected_color; }; } // namespace Inkscape::UI::Widget diff --git a/src/ui/widget/color-picker.cpp b/src/ui/widget/color-picker.cpp index 4544dd6970..cc14d0a598 100644 --- a/src/ui/widget/color-picker.cpp +++ b/src/ui/widget/color-picker.cpp @@ -13,10 +13,11 @@ #include "color-picker.h" +#include + #include "desktop.h" #include "document-undo.h" #include "inkscape.h" - #include "ui/dialog-events.h" #include "ui/widget/color-notebook.h" @@ -24,32 +25,48 @@ static bool _in_use = false; namespace Inkscape::UI::Widget { -ColorPicker::ColorPicker(Glib::ustring const &title, +ColorPicker::ColorPicker(Glib::ustring title, Glib::ustring const &tip, - std::uint32_t const rgba, + Colors::Color const &initial, bool const undo, - Gtk::Button * const external_button) - : _preview(Gtk::make_managed(rgba)) - , _title(title) - , _rgba(rgba) + bool use_transparency) + : _preview(Gtk::make_managed(initial.toRGBA())) + , _title(std::move(title)) , _undo(undo) + , _colors(std::make_shared(nullptr, use_transparency)) { - auto button = external_button ? external_button : this; - button->set_child(*_preview); // set tooltip if given, otherwise leave original tooltip in place (from external button) if (!tip.empty()) { - button->set_tooltip_text(tip); + set_tooltip_text(tip); } - _selected_color.signal_changed.connect(sigc::mem_fun(*this, &ColorPicker::_onSelectedColorChanged)); - _selected_color.signal_dragged.connect(sigc::mem_fun(*this, &ColorPicker::_onSelectedColorChanged)); - _selected_color.signal_released.connect(sigc::mem_fun(*this, &ColorPicker::_onSelectedColorChanged)); + _colors->set(initial); + _construct(); +} + +ColorPicker::ColorPicker(BaseObjectType *cobject, Glib::RefPtr const &, + Glib::ustring title, bool use_transparency) + : Gtk::Button(cobject) + , _preview(Gtk::make_managed(0x0)) + , _title(std::move(title)) + , _colors(std::make_shared(nullptr, use_transparency)) +{ + _construct(); +} + +void ColorPicker::_construct() +{ + set_child(*_preview); + + _colors->signal_changed.connect(sigc::mem_fun(*this, &ColorPicker::_onSelectedColorChanged)); + _colors->signal_released.connect(sigc::mem_fun(*this, &ColorPicker::_onSelectedColorChanged)); + + auto color_selector = Gtk::make_managed(_colors); + color_selector->set_label(_title); + color_selector->set_margin(4); - if (external_button) { - external_button->signal_clicked().connect([this] { on_clicked(); }); - } - sp_transientize(_colorSelectorDialog); - _colorSelectorDialog.set_title(title); + _colorSelectorDialog.set_child(*color_selector); + _colorSelectorDialog.set_title(_title); _colorSelectorDialog.set_hide_on_close(); property_sensitive().signal_changed().connect([this](){ @@ -59,19 +76,14 @@ ColorPicker::ColorPicker(Glib::ustring const &title, ColorPicker::~ColorPicker() = default; -void ColorPicker::setRgba32(std::uint32_t const rgba) +void ColorPicker::setColor(Colors::Color const &color) { if (_in_use) return; - set_preview(rgba); - _rgba = rgba; - - if (_color_selector) - { - _updating = true; - _selected_color.setValue(rgba); - _updating = false; - } + _updating = true; + set_preview(color.toRGBA()); + _colors->set(color); + _updating = false; } void ColorPicker::closeWindow() @@ -85,61 +97,36 @@ void ColorPicker::open() { void ColorPicker::on_clicked() { - if (!_color_selector) { - _color_selector = Gtk::make_managed(_selected_color, _ignore_transparency); - _color_selector->set_label(_title); - _color_selector->set_margin(4); - _colorSelectorDialog.set_child(*_color_selector); - } - - _updating = true; - _selected_color.setValue(_rgba); - _updating = false; - _colorSelectorDialog.present(); } -void ColorPicker::on_changed(std::uint32_t) +void ColorPicker::on_changed(Colors::Color const &color) { } -void ColorPicker::_onSelectedColorChanged() { - if (_updating) { - return; - } - - if (_in_use) { +void ColorPicker::_onSelectedColorChanged() +{ + if (_updating || _in_use) return; - } else { - _in_use = true; - } - - auto const rgba = _selected_color.value(); - set_preview(rgba); if (_undo && SP_ACTIVE_DESKTOP) { DocumentUndo::done(SP_ACTIVE_DESKTOP->getDocument(), /* TODO: annotate */ "color-picker.cpp:129", ""); } - on_changed(rgba); + _in_use = true; + if (auto color = _colors->get()) { + set_preview(color->toRGBA()); + on_changed(*color); + _changed_signal.emit(*color); + } _in_use = false; - _rgba = rgba; - _changed_signal.emit(rgba); -} - -void ColorPicker::set_preview(std::uint32_t const rgba) -{ - _preview->setRgba32(_ignore_transparency ? rgba | 0xff : rgba); -} -void ColorPicker::use_transparency(bool enable) { - _ignore_transparency = !enable; - set_preview(_rgba); } -std::uint32_t ColorPicker::get_current_color() const +void ColorPicker::set_preview(std::uint32_t const rgba) { - return _rgba; + bool has_alpha = _colors->getAlphaConstraint().value_or(true); + _preview->setRgba32(has_alpha ? rgba : rgba | 0xff); } } // namespace Inkscape::UI::Widget diff --git a/src/ui/widget/color-picker.h b/src/ui/widget/color-picker.h index b4799b4238..8ab68493b8 100644 --- a/src/ui/widget/color-picker.h +++ b/src/ui/widget/color-picker.h @@ -19,13 +19,16 @@ #include #include -#include "ui/selected-color.h" +#include "colors/color.h" +#include "colors/color-set.h" #include "ui/widget/color-preview.h" #include #include #include -struct SPColorSelector; +namespace Gtk { +class Builder; +} namespace Inkscape::UI::Widget { @@ -33,49 +36,52 @@ class ColorNotebook; class ColorPicker : public Gtk::Button { public: - [[nodiscard]] ColorPicker(Glib::ustring const &title, + [[nodiscard]] ColorPicker(Glib::ustring title, Glib::ustring const &tip, - std::uint32_t rgba, + Colors::Color const &initial, bool undo, - Gtk::Button *external_button = nullptr); + bool use_transparency = true); + + ColorPicker(BaseObjectType *cobject, Glib::RefPtr const &, + Glib::ustring title, bool use_transparency = true); ~ColorPicker() override; - void setRgba32(std::uint32_t rgba); - void setSensitive(bool sensitive) { set_sensitive(sensitive); } + void setColor(Colors::Color const &); void open(); void closeWindow(); - sigc::connection connectChanged(sigc::slot slot) + sigc::connection connectChanged(sigc::slot slot) { return _changed_signal.connect(std::move(slot)); } - void use_transparency(bool enable); - [[nodiscard]] std::uint32_t get_current_color() const; + [[nodiscard]] Colors::Color get_current_color() const { + if (_colors->isEmpty()) + return Colors::Color(0x0); + return _colors->getAverage(); + } protected: void _onSelectedColorChanged(); void on_clicked() override; - virtual void on_changed(std::uint32_t); + virtual void on_changed(Colors::Color const &); ColorPreview *_preview = nullptr; Glib::ustring _title; - sigc::signal _changed_signal; - std::uint32_t _rgba = 0 ; + sigc::signal _changed_signal; bool _undo = false; bool _updating = false; - Gtk::Window _colorSelectorDialog; + void setupDialog(Glib::ustring const &title); + Gtk::Dialog _colorSelectorDialog; - SelectedColor _selected_color; + std::shared_ptr _colors; private: + void _construct(); void set_preview(std::uint32_t rgba); - - ColorNotebook *_color_selector = nullptr; - bool _ignore_transparency = false; }; @@ -84,18 +90,17 @@ public: [[nodiscard]] LabelledColorPicker(Glib::ustring const &label, Glib::ustring const &title, Glib::ustring const &tip, - std::uint32_t rgba, + Colors::Color const &initial, bool undo) - : Labelled(label, tip, new ColorPicker(title, tip, rgba, undo)) { - + : Labelled(label, tip, new ColorPicker(title, tip, initial, undo)) { property_sensitive().signal_changed().connect([this](){ getWidget()->set_sensitive(is_sensitive()); }); } - void setRgba32(std::uint32_t const rgba) + void setColor(Colors::Color const &color) { - static_cast(getWidget())->setRgba32(rgba); + static_cast(getWidget())->setColor(color); } void closeWindow() @@ -103,7 +108,7 @@ public: static_cast(getWidget())->closeWindow(); } - sigc::connection connectChanged(sigc::slot slot) + sigc::connection connectChanged(sigc::slot slot) { return static_cast(getWidget())->connectChanged(std::move(slot)); } diff --git a/src/ui/widget/color-preview.cpp b/src/ui/widget/color-preview.cpp index 23a940c4c7..3d88806fba 100644 --- a/src/ui/widget/color-preview.cpp +++ b/src/ui/widget/color-preview.cpp @@ -17,7 +17,8 @@ #include #include "display/cairo-utils.h" -#include "hsluv.h" +#include "colors/color.h" +#include "colors/spaces/enum.h" namespace Inkscape::UI::Widget { @@ -62,14 +63,12 @@ void ColorPreview::draw_func(Cairo::RefPtr const &cr, cairo_pattern_t *checkers = ink_cairo_pattern_create_checkerboard(); cairo_set_source(cr->cobj(), checkers); cr->fill_preserve(); - auto color = _rgba; + auto color = Colors::Color(_rgba); // if preview is disabled render colors in gray if (!_enabled) { - auto l = Hsluv::rgb_to_perceptual_lightness(Hsluv::Triplet{SP_RGBA32_R_F(color), SP_RGBA32_G_F(color), SP_RGBA32_B_F(color)}); - l = Hsluv::from_linear(l); - color = SP_RGBA32_F_COMPOSE(l, l, l, (color & 0xff) / 255.0); + color.convert(Colors::Space::Type::Gray); } - ink_cairo_set_source_rgba32(cr->cobj(), color); + ink_cairo_set_source_color(cr, color); cr->fill(); cairo_pattern_destroy(checkers); @@ -81,7 +80,8 @@ void ColorPreview::draw_func(Cairo::RefPtr const &cr, cairo_line_to(cr->cobj(), x, height); cairo_line_to(cr->cobj(), x, y); cairo_close_path (cr->cobj()); - ink_cairo_set_source_rgba32(cr->cobj(), color | 0xff); + color.enableOpacity(false); + ink_cairo_set_source_color(cr, color); cr->fill(); } diff --git a/src/ui/widget/color-scales.cpp b/src/ui/widget/color-scales.cpp index 8a7b608e65..74af7a62de 100644 --- a/src/ui/widget/color-scales.cpp +++ b/src/ui/widget/color-scales.cpp @@ -25,31 +25,21 @@ #include #include -#include "oklab.h" +#include "colors/spaces/components.h" +#include "colors/spaces/enum.h" +#include "colors/spaces/oklch.h" #include "preferences.h" #include "ui/dialog-events.h" #include "ui/icon-loader.h" #include "ui/pack.h" -#include "ui/selected-color.h" -#include "ui/widget/color-icc-selector.h" #include "ui/widget/color-slider.h" #include "ui/widget/ink-color-wheel.h" #include "ui/widget/oklab-color-wheel.h" -constexpr static int CSC_CHANNEL_R = (1 << 0); -constexpr static int CSC_CHANNEL_G = (1 << 1); -constexpr static int CSC_CHANNEL_B = (1 << 2); -constexpr static int CSC_CHANNEL_A = (1 << 3); -constexpr static int CSC_CHANNEL_H = (1 << 0); -constexpr static int CSC_CHANNEL_S = (1 << 1); -constexpr static int CSC_CHANNEL_V = (1 << 2); -constexpr static int CSC_CHANNEL_C = (1 << 0); -constexpr static int CSC_CHANNEL_M = (1 << 1); -constexpr static int CSC_CHANNEL_Y = (1 << 2); -constexpr static int CSC_CHANNEL_K = (1 << 3); -constexpr static int CSC_CHANNEL_CMYKA = (1 << 4); - -constexpr static int CSC_CHANNELS_ALL = 0; +constexpr static int CSC_CHANNEL_H = 0; +constexpr static int CSC_CHANNEL_S = 1; +constexpr static int CSC_CHANNEL_V = 2; +constexpr static int CSC_CHANNELS_ALL = 10; constexpr static int XPAD = 2; constexpr static int YPAD = 2; @@ -73,7 +63,7 @@ static const char* color_mode_icons[] = { }; const char* color_mode_name[] = { - N_("None"), N_("RGB"), N_("HSL"), N_("CMYK"), N_("HSV"), N_("HSLuv"), N_("OKHSL"), N_("CMS"), nullptr + N_("None"), N_("RGB"), N_("HSL"), N_("CMYK"), N_("HSV"), N_("HSLuv"), N_("OKHSL"), nullptr }; const char* get_color_mode_icon(SPColorScalesMode mode) { @@ -96,7 +86,7 @@ std::unique_ptr get_factory(SPColorScalesMod case SPColorScalesMode::CMYK: return std::make_unique>(); case SPColorScalesMode::HSLUV: return std::make_unique>(); case SPColorScalesMode::OKLAB: return std::make_unique>(); - case SPColorScalesMode::CMS: return std::make_unique(); + //case SPColorScalesMode::CMS: return std::make_unique(); default: throw std::invalid_argument("There's no factory for the requested color mode"); } @@ -112,7 +102,7 @@ std::vector get_color_pickers() { SPColorScalesMode::CMYK, SPColorScalesMode::OKLAB, SPColorScalesMode::HSLUV, - SPColorScalesMode::CMS + //SPColorScalesMode::CMS }) { auto label = get_color_mode_label(mode); @@ -151,14 +141,16 @@ gchar const * const ColorScales::_pref_wheel_visibilit "/wheel_vis_okhsl"; template -ColorScales::ColorScales(SelectedColor &color, bool no_alpha) +ColorScales::ColorScales(SelectedColor color, bool no_alpha) : Gtk::Box() - , _color(color) + , _color(std::move(color)) , _range_limit(255.0) , _updating(false) , _dragging(false) , _wheel(nullptr) { + assert(_color); + for (gint i = 0; i < 5; i++) { _l[i] = nullptr; _s[i] = nullptr; @@ -167,8 +159,7 @@ ColorScales::ColorScales(SelectedColor &color, bool no_alpha) _initUI(no_alpha); - _color_changed = _color.signal_changed.connect([this](){ _onColorChanged(); }); - _color_dragged = _color.signal_dragged.connect([this](){ _onColorChanged(); }); + _color_changed = _color->signal_changed.connect([this](){ _onColorChanged(); }); } template @@ -319,102 +310,52 @@ void ColorScales::_initUI(bool no_alpha) template void ColorScales::_recalcColor() { - SPColor color; - gfloat alpha = 1.0; - gfloat c[5]; - - if constexpr ( - MODE == SPColorScalesMode::RGB || - MODE == SPColorScalesMode::HSL || - MODE == SPColorScalesMode::HSV || - MODE == SPColorScalesMode::HSLUV || - MODE == SPColorScalesMode::OKLAB) - { - _getRgbaFloatv(c); - color.set(c[0], c[1], c[2]); - alpha = c[3]; - } else if constexpr (MODE == SPColorScalesMode::CMYK) { - _getCmykaFloatv(c); - - float rgb[3]; - SPColor::cmyk_to_rgb_floatv(rgb, c[0], c[1], c[2], c[3]); - color.set(rgb[0], rgb[1], rgb[2]); - alpha = c[4]; - } else { - g_warning("file %s: line %d: Illegal color selector mode NONE", __FILE__, __LINE__); - } - - _color.setColorAlpha(color, alpha); + if (_color->isEmpty()) + g_warning("Color setter can't do anything, nothing to set to."); + _color->setAll(_getColor()); } template void ColorScales::_updateDisplay(bool update_wheel) { -#ifdef DUMP_CHANGE_INFO - g_message("ColorScales::_onColorChanged( this=%p, %f, %f, %f, %f) %d", this, - _color.color().v.c[0], - _color.color().v.c[1], _color.color().v.c[2], _color.alpha(), int(update_wheel); -#endif - - gfloat tmp[3]; - gfloat c[5] = { 0.0, 0.0, 0.0, 0.0 }; + if (_color->isEmpty()) { + g_warning("Empty ColorSet for ColorScale..."); + return; + } - SPColor color = _color.color(); + auto color = _color->getAverage(); if constexpr (MODE == SPColorScalesMode::RGB) { - color.get_rgb_floatv(c); - c[3] = _color.alpha(); - c[4] = 0.0; + color.convert(Colors::Space::Type::RGB); } else if constexpr (MODE == SPColorScalesMode::HSL) { - color.get_rgb_floatv(tmp); - SPColor::rgb_to_hsl_floatv(c, tmp[0], tmp[1], tmp[2]); - c[3] = _color.alpha(); - c[4] = 0.0; - // N.B. We setRgb() with emit = false, to avoid a warning from PaintSelector. - if (update_wheel) { _wheel->setRgb(tmp[0], tmp[1], tmp[2], true, false); } + color.convert(Colors::Space::Type::HSL); } else if constexpr (MODE == SPColorScalesMode::HSV) { - color.get_rgb_floatv(tmp); - SPColor::rgb_to_hsv_floatv(c, tmp[0], tmp[1], tmp[2]); - c[3] = _color.alpha(); - c[4] = 0.0; - if (update_wheel) { _wheel->setRgb(tmp[0], tmp[1], tmp[2], true, false); } + color.convert(Colors::Space::Type::HSV); } else if constexpr (MODE == SPColorScalesMode::CMYK) { - color.get_cmyk_floatv(c); - c[4] = _color.alpha(); + color.convert(Colors::Space::Type::CMYK); } else if constexpr (MODE == SPColorScalesMode::HSLUV) { - color.get_rgb_floatv(tmp); - SPColor::rgb_to_hsluv_floatv(c, tmp[0], tmp[1], tmp[2]); - c[3] = _color.alpha(); - c[4] = 0.0; - if (update_wheel) { _wheel->setRgb(tmp[0], tmp[1], tmp[2], true, false); } + color.convert(Colors::Space::Type::HSLUV); } else if constexpr (MODE == SPColorScalesMode::OKLAB) { - color.get_rgb_floatv(tmp); - // OKLab color space is more sensitive to numerical errors; use doubles. - auto const hsl = Oklab::oklab_to_okhsl(Oklab::rgb_to_oklab({tmp[0], tmp[1], tmp[2]})); - _updating = true; - for (size_t i : {0, 1, 2}) { - setScaled(_a[i], hsl[i]); - } - setScaled(_a[3], _color.alpha()); - setScaled(_a[4], 0.0); - _updateSliders(CSC_CHANNELS_ALL); - _updating = false; - if (update_wheel) { - _wheel->setRgb(tmp[0], tmp[1], tmp[2], true, false); - } - return; + color.convert(Colors::Space::Type::OKHSL); } else { g_warning("file %s: line %d: Illegal color selector mode NONE", __FILE__, __LINE__); } _updating = true; - setScaled(_a[0], c[0]); - setScaled(_a[1], c[1]); - setScaled(_a[2], c[2]); - setScaled(_a[3], c[3]); - setScaled(_a[4], c[4]); + for (size_t i = 0; i < 5; i++) { + if (i < color.size()) { + setScaled(_a[i], color[i]); + } else { + setScaled(_a[i], 0.0); + } + } _updateSliders(CSC_CHANNELS_ALL); _updating = false; + + if (_wheel && update_wheel) { + // N.B. We setColor() with emit = false, to avoid a warning from PaintSelector. + _wheel->setColor(color, true, false); + } } /* Helpers for setting color value */ @@ -466,108 +407,46 @@ void ColorScales::on_show() } template -void ColorScales::_getRgbaFloatv(gfloat *rgba) +Colors::Color ColorScales::_getColor() { - g_return_if_fail(rgba != nullptr); + Colors::Color color(0x0); if constexpr (MODE == SPColorScalesMode::RGB) { - rgba[0] = getScaled(_a[0]); - rgba[1] = getScaled(_a[1]); - rgba[2] = getScaled(_a[2]); - rgba[3] = getScaled(_a[3]); + color.convert(Colors::Space::Type::RGB); } else if constexpr (MODE == SPColorScalesMode::HSL) { - SPColor::hsl_to_rgb_floatv(rgba, getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2])); - rgba[3] = getScaled(_a[3]); + color.convert(Colors::Space::Type::HSL); } else if constexpr (MODE == SPColorScalesMode::HSV) { - SPColor::hsv_to_rgb_floatv(rgba, getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2])); - rgba[3] = getScaled(_a[3]); + color.convert(Colors::Space::Type::HSV); } else if constexpr (MODE == SPColorScalesMode::CMYK) { - SPColor::cmyk_to_rgb_floatv(rgba, getScaled(_a[0]), getScaled(_a[1]), - getScaled(_a[2]), getScaled(_a[3])); - rgba[3] = getScaled(_a[4]); + color.convert(Colors::Space::Type::CMYK); } else if constexpr (MODE == SPColorScalesMode::HSLUV) { - SPColor::hsluv_to_rgb_floatv(rgba, getScaled(_a[0]), getScaled(_a[1]), - getScaled(_a[2])); - rgba[3] = getScaled(_a[3]); + color.convert(Colors::Space::Type::HSLUV); } else if constexpr (MODE == SPColorScalesMode::OKLAB) { - auto const tmp = Oklab::oklab_to_rgb( - Oklab::okhsl_to_oklab({ getScaled(_a[0]), - getScaled(_a[1]), - getScaled(_a[2]) })); - for (size_t i : {0, 1, 2}) { - rgba[i] = static_cast(tmp[i]); - } - rgba[3] = getScaled(_a[3]); + color.convert(Colors::Space::Type::OKHSL); } else { g_warning("file %s: line %d: Illegal color selector mode", __FILE__, __LINE__); } -} - -template -void ColorScales::_getCmykaFloatv(gfloat *cmyka) -{ - gfloat rgb[3]; - - g_return_if_fail(cmyka != nullptr); - - if constexpr (MODE == SPColorScalesMode::RGB) { - SPColor::rgb_to_cmyk_floatv(cmyka, getScaled(_a[0]), getScaled(_a[1]), - getScaled(_a[2])); - cmyka[4] = getScaled(_a[3]); - } else if constexpr (MODE == SPColorScalesMode::HSL) { - SPColor::hsl_to_rgb_floatv(rgb, getScaled(_a[0]), getScaled(_a[1]), - getScaled(_a[2])); - SPColor::rgb_to_cmyk_floatv(cmyka, rgb[0], rgb[1], rgb[2]); - cmyka[4] = getScaled(_a[3]); - } else if constexpr (MODE == SPColorScalesMode::HSLUV) { - SPColor::hsluv_to_rgb_floatv(rgb, getScaled(_a[0]), getScaled(_a[1]), - getScaled(_a[2])); - SPColor::rgb_to_cmyk_floatv(cmyka, rgb[0], rgb[1], rgb[2]); - cmyka[4] = getScaled(_a[3]); - } else if constexpr (MODE == SPColorScalesMode::OKLAB) { - auto const tmp = Oklab::oklab_to_rgb( - Oklab::okhsl_to_oklab({ getScaled(_a[0]), - getScaled(_a[1]), - getScaled(_a[2]) })); - SPColor::rgb_to_cmyk_floatv(cmyka, (float)tmp[0], (float)tmp[1], (float)tmp[2]); - cmyka[4] = getScaled(_a[3]); - } else if constexpr (MODE == SPColorScalesMode::CMYK) { - cmyka[0] = getScaled(_a[0]); - cmyka[1] = getScaled(_a[1]); - cmyka[2] = getScaled(_a[2]); - cmyka[3] = getScaled(_a[3]); - cmyka[4] = getScaled(_a[4]); - } else { - g_warning("file %s: line %d: Illegal color selector mode", __FILE__, __LINE__); + for (size_t i = 0; i < color.size(); i++) { + color.set(i, getScaled(_a[i])); } + return color; } template guint32 ColorScales::_getRgba32() { - gfloat c[4]; - guint32 rgba; - - _getRgbaFloatv(c); - - rgba = SP_RGBA32_F_COMPOSE(c[0], c[1], c[2], c[3]); - - return rgba; + return _getColor().toRGBA(); } template void ColorScales::setupMode(bool no_alpha) { - gfloat rgba[4]; - gfloat c[4]; - int alpha_index = 0; - if constexpr (MODE == SPColorScalesMode::NONE) { - rgba[0] = rgba[1] = rgba[2] = rgba[3] = 1.0; - } else { - _getRgbaFloatv(rgba); + return; } + Colors::Color c = _getColor(); + if constexpr (MODE == SPColorScalesMode::RGB) { _setRangeLimit(255.0); _a[3]->set_upper(100.0); @@ -580,7 +459,6 @@ void ColorScales::setupMode(bool no_alpha) _l[2]->set_markup_with_mnemonic(_("_B:")); _s[2]->set_tooltip_text(_("Blue")); _b[2]->set_tooltip_text(_("Blue")); - alpha_index = 3; _l[3]->set_markup_with_mnemonic(_("_A:")); _s[3]->set_tooltip_text(_("Alpha (opacity)")); _b[3]->set_tooltip_text(_("Alpha (opacity)")); @@ -589,10 +467,10 @@ void ColorScales::setupMode(bool no_alpha) _s[4]->set_visible(false); _b[4]->set_visible(false); _updating = true; - setScaled(_a[0], rgba[0]); - setScaled(_a[1], rgba[1]); - setScaled(_a[2], rgba[2]); - setScaled(_a[3], rgba[3]); + setScaled(_a[0], c[0]); + setScaled(_a[1], c[1]); + setScaled(_a[2], c[2]); + setScaled(_a[3], c[3]); _updateSliders(CSC_CHANNELS_ALL); _updating = false; } else if constexpr (MODE == SPColorScalesMode::HSL) { @@ -611,7 +489,6 @@ void ColorScales::setupMode(bool no_alpha) _s[2]->set_tooltip_text(_("Lightness")); _b[2]->set_tooltip_text(_("Lightness")); - alpha_index = 3; _l[3]->set_markup_with_mnemonic(_("_A:")); _s[3]->set_tooltip_text(_("Alpha (opacity)")); _b[3]->set_tooltip_text(_("Alpha (opacity)")); @@ -620,14 +497,11 @@ void ColorScales::setupMode(bool no_alpha) _s[4]->set_visible(false); _b[4]->set_visible(false); _updating = true; - c[0] = 0.0; - - SPColor::rgb_to_hsl_floatv(c, rgba[0], rgba[1], rgba[2]); setScaled(_a[0], c[0]); setScaled(_a[1], c[1]); setScaled(_a[2], c[2]); - setScaled(_a[3], rgba[3]); + setScaled(_a[3], c[3]); _updateSliders(CSC_CHANNELS_ALL); _updating = false; @@ -647,7 +521,6 @@ void ColorScales::setupMode(bool no_alpha) _s[2]->set_tooltip_text(_("Value")); _b[2]->set_tooltip_text(_("Value")); - alpha_index = 3; _l[3]->set_markup_with_mnemonic(_("_A:")); _s[3]->set_tooltip_text(_("Alpha (opacity)")); _b[3]->set_tooltip_text(_("Alpha (opacity)")); @@ -656,14 +529,11 @@ void ColorScales::setupMode(bool no_alpha) _s[4]->set_visible(false); _b[4]->set_visible(false); _updating = true; - c[0] = 0.0; - - SPColor::rgb_to_hsv_floatv(c, rgba[0], rgba[1], rgba[2]); setScaled(_a[0], c[0]); setScaled(_a[1], c[1]); setScaled(_a[2], c[2]); - setScaled(_a[3], rgba[3]); + setScaled(_a[3], c[3]); _updateSliders(CSC_CHANNELS_ALL); _updating = false; @@ -685,7 +555,6 @@ void ColorScales::setupMode(bool no_alpha) _s[3]->set_tooltip_text(_("Black")); _b[3]->set_tooltip_text(_("Black")); - alpha_index = 4; _l[4]->set_markup_with_mnemonic(_("_A:")); _s[4]->set_tooltip_text(_("Alpha (opacity)")); _b[4]->set_tooltip_text(_("Alpha (opacity)")); @@ -696,13 +565,12 @@ void ColorScales::setupMode(bool no_alpha) _b[4]->set_visible(true); _updating = true; - SPColor::rgb_to_cmyk_floatv(c, rgba[0], rgba[1], rgba[2]); setScaled(_a[0], c[0]); setScaled(_a[1], c[1]); setScaled(_a[2], c[2]); setScaled(_a[3], c[3]); + setScaled(_a[4], c[4]); - setScaled(_a[4], rgba[3]); _updateSliders(CSC_CHANNELS_ALL); _updating = false; } else if constexpr (MODE == SPColorScalesMode::HSLUV) { @@ -721,7 +589,6 @@ void ColorScales::setupMode(bool no_alpha) _s[2]->set_tooltip_text(_("Lightness")); _b[2]->set_tooltip_text(_("Lightness")); - alpha_index = 3; _l[3]->set_markup_with_mnemonic(_("_A:")); _s[3]->set_tooltip_text(_("Alpha (opacity)")); _b[3]->set_tooltip_text(_("Alpha (opacity)")); @@ -734,14 +601,11 @@ void ColorScales::setupMode(bool no_alpha) _s[4]->set_visible(false); _b[4]->set_visible(false); _updating = true; - c[0] = 0.0; - - SPColor::rgb_to_hsluv_floatv(c, rgba[0], rgba[1], rgba[2]); setScaled(_a[0], c[0]); setScaled(_a[1], c[1]); setScaled(_a[2], c[2]); - setScaled(_a[3], rgba[3]); + setScaled(_a[3], c[3]); _updateSliders(CSC_CHANNELS_ALL); _updating = false; @@ -761,7 +625,6 @@ void ColorScales::setupMode(bool no_alpha) _s[2]->set_tooltip_text(_("Lightness")); _b[2]->set_tooltip_text(_("Lightness")); - alpha_index = 3; _l[3]->set_markup_with_mnemonic(_("_A:")); _s[3]->set_tooltip_text(_("Alpha (opacity)")); _b[3]->set_tooltip_text(_("Alpha (opacity)")); @@ -771,11 +634,10 @@ void ColorScales::setupMode(bool no_alpha) _b[4]->set_visible(false); _updating = true; - auto const tmp = Oklab::oklab_to_okhsl(Oklab::rgb_to_oklab({rgba[0], rgba[1], rgba[2]})); for (size_t i : {0, 1, 2}) { - setScaled(_a[i], tmp[i]); + setScaled(_a[i], c[i]); } - setScaled(_a[3], rgba[3]); + setScaled(_a[3], c.getOpacity()); _updateSliders(CSC_CHANNELS_ALL); _updating = false; @@ -783,7 +645,8 @@ void ColorScales::setupMode(bool no_alpha) g_warning("file %s: line %d: Illegal color selector mode", __FILE__, __LINE__); } - if (no_alpha && alpha_index > 0) { + if (no_alpha) { + auto alpha_index = c.getOpacityChannel(); _l[alpha_index]->set_visible(false); _s[alpha_index]->set_visible(false); _b[alpha_index]->set_visible(false); @@ -800,7 +663,7 @@ void ColorScales::_sliderAnyGrabbed() if (!_dragging) { _dragging = true; - _color.setHeld(true); + _color->grab(); } } @@ -811,7 +674,7 @@ void ColorScales::_sliderAnyReleased() if (_dragging) { _dragging = false; - _color.setHeld(false); + _color->release(); } } @@ -828,7 +691,7 @@ void ColorScales::_adjustmentChanged(int channel) { if (_updating) { return; } - _updateSliders((1 << channel)); + _updateSliders(channel); _recalcColor(); } @@ -846,214 +709,74 @@ void ColorScales::_wheelChanged() if (_updating) { return; } _updating = true; - - double rgb[3]; - _wheel->getRgbV(rgb); - SPColor color(rgb[0], rgb[1], rgb[2]); - _color_changed.block(); - _color_dragged.block(); // Color - _color.setHeld(_wheel->isAdjusting()); - _color.setColor(color); + if (_wheel->isAdjusting()) { + _color->grab(); + } else { + _color->release(); + } + _color->setAll(_wheel->getColor()); // Sliders _updateDisplay(false); _color_changed.unblock(); - _color_dragged.unblock(); - _updating = false; } template -void ColorScales::_updateSliders(guint channels) +void ColorScales::_updateSliders(guint channel_pin) { - gfloat rgb0[3], rgbm[3], rgb1[3]; + auto color = _getColor(); -#ifdef SPCS_PREVIEW - guint32 rgba; -#endif - - std::array const adj = [this]() -> std::array { - if constexpr (MODE == SPColorScalesMode::CMYK) { - return { getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2]), getScaled(_a[3]) }; - } else { - return { getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2]), 0.0 }; - } - }(); + // Opacity is not shown in color sliders + if (channel_pin == color.getOpacityChannel()) + return; + color.enableOpacity(false); - if constexpr (MODE == SPColorScalesMode::RGB) { - if ((channels != CSC_CHANNEL_R) && (channels != CSC_CHANNEL_A)) { - /* Update red */ - _s[0]->setColors(SP_RGBA32_F_COMPOSE(0.0, adj[1], adj[2], 1.0), - SP_RGBA32_F_COMPOSE(0.5, adj[1], adj[2], 1.0), - SP_RGBA32_F_COMPOSE(1.0, adj[1], adj[2], 1.0)); - } - if ((channels != CSC_CHANNEL_G) && (channels != CSC_CHANNEL_A)) { - /* Update green */ - _s[1]->setColors(SP_RGBA32_F_COMPOSE(adj[0], 0.0, adj[2], 1.0), - SP_RGBA32_F_COMPOSE(adj[0], 0.5, adj[2], 1.0), - SP_RGBA32_F_COMPOSE(adj[0], 1.0, adj[2], 1.0)); - } - if ((channels != CSC_CHANNEL_B) && (channels != CSC_CHANNEL_A)) { - /* Update blue */ - _s[2]->setColors(SP_RGBA32_F_COMPOSE(adj[0], adj[1], 0.0, 1.0), - SP_RGBA32_F_COMPOSE(adj[0], adj[1], 0.5, 1.0), - SP_RGBA32_F_COMPOSE(adj[0], adj[1], 1.0, 1.0)); - } - if (channels != CSC_CHANNEL_A) { - /* Update alpha */ - _s[3]->setColors(SP_RGBA32_F_COMPOSE(adj[0], adj[1], adj[2], 0.0), - SP_RGBA32_F_COMPOSE(adj[0], adj[1], adj[2], 0.5), - SP_RGBA32_F_COMPOSE(adj[0], adj[1], adj[2], 1.0)); - } - } else if constexpr (MODE == SPColorScalesMode::HSL) { - /* Hue is never updated */ - if ((channels != CSC_CHANNEL_S) && (channels != CSC_CHANNEL_A)) { - /* Update saturation */ - SPColor::hsl_to_rgb_floatv(rgb0, adj[0], 0.0, adj[2]); - SPColor::hsl_to_rgb_floatv(rgbm, adj[0], 0.5, adj[2]); - SPColor::hsl_to_rgb_floatv(rgb1, adj[0], 1.0, adj[2]); - _s[1]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0), - SP_RGBA32_F_COMPOSE(rgbm[0], rgbm[1], rgbm[2], 1.0), - SP_RGBA32_F_COMPOSE(rgb1[0], rgb1[1], rgb1[2], 1.0)); - } - if ((channels != CSC_CHANNEL_V) && (channels != CSC_CHANNEL_A)) { - /* Update value */ - SPColor::hsl_to_rgb_floatv(rgb0, adj[0], adj[1], 0.0); - SPColor::hsl_to_rgb_floatv(rgbm, adj[0], adj[1], 0.5); - SPColor::hsl_to_rgb_floatv(rgb1, adj[0], adj[1], 1.0); - _s[2]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0), - SP_RGBA32_F_COMPOSE(rgbm[0], rgbm[1], rgbm[2], 1.0), - SP_RGBA32_F_COMPOSE(rgb1[0], rgb1[1], rgb1[2], 1.0)); - } - if (channels != CSC_CHANNEL_A) { - /* Update alpha */ - SPColor::hsl_to_rgb_floatv(rgb0, adj[0], adj[1], adj[2]); - _s[3]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 0.0), - SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 0.5), - SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0)); - } - } else if constexpr (MODE == SPColorScalesMode::HSV) { - /* Hue is never updated */ - if ((channels != CSC_CHANNEL_S) && (channels != CSC_CHANNEL_A)) { - /* Update saturation */ - SPColor::hsv_to_rgb_floatv(rgb0, adj[0], 0.0, adj[2]); - SPColor::hsv_to_rgb_floatv(rgbm, adj[0], 0.5, adj[2]); - SPColor::hsv_to_rgb_floatv(rgb1, adj[0], 1.0, adj[2]); - _s[1]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0), - SP_RGBA32_F_COMPOSE(rgbm[0], rgbm[1], rgbm[2], 1.0), - SP_RGBA32_F_COMPOSE(rgb1[0], rgb1[1], rgb1[2], 1.0)); - } - if ((channels != CSC_CHANNEL_V) && (channels != CSC_CHANNEL_A)) { - /* Update value */ - SPColor::hsv_to_rgb_floatv(rgb0, adj[0], adj[1], 0.0); - SPColor::hsv_to_rgb_floatv(rgbm, adj[0], adj[1], 0.5); - SPColor::hsv_to_rgb_floatv(rgb1, adj[0], adj[1], 1.0); - _s[2]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0), - SP_RGBA32_F_COMPOSE(rgbm[0], rgbm[1], rgbm[2], 1.0), - SP_RGBA32_F_COMPOSE(rgb1[0], rgb1[1], rgb1[2], 1.0)); - } - if (channels != CSC_CHANNEL_A) { - /* Update alpha */ - SPColor::hsv_to_rgb_floatv(rgb0, adj[0], adj[1], adj[2]); - _s[3]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 0.0), - SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 0.5), - SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0)); - } - } else if constexpr (MODE == SPColorScalesMode::CMYK) { - if ((channels != CSC_CHANNEL_C) && (channels != CSC_CHANNEL_CMYKA)) { - /* Update C */ - SPColor::cmyk_to_rgb_floatv(rgb0, 0.0, adj[1], adj[2], adj[3]); - SPColor::cmyk_to_rgb_floatv(rgbm, 0.5, adj[1], adj[2], adj[3]); - SPColor::cmyk_to_rgb_floatv(rgb1, 1.0, adj[1], adj[2], adj[3]); - _s[0]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0), - SP_RGBA32_F_COMPOSE(rgbm[0], rgbm[1], rgbm[2], 1.0), - SP_RGBA32_F_COMPOSE(rgb1[0], rgb1[1], rgb1[2], 1.0)); - } - if ((channels != CSC_CHANNEL_M) && (channels != CSC_CHANNEL_CMYKA)) { - /* Update M */ - SPColor::cmyk_to_rgb_floatv(rgb0, adj[0], 0.0, adj[2], adj[3]); - SPColor::cmyk_to_rgb_floatv(rgbm, adj[0], 0.5, adj[2], adj[3]); - SPColor::cmyk_to_rgb_floatv(rgb1, adj[0], 1.0, adj[2], adj[3]); - _s[1]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0), - SP_RGBA32_F_COMPOSE(rgbm[0], rgbm[1], rgbm[2], 1.0), - SP_RGBA32_F_COMPOSE(rgb1[0], rgb1[1], rgb1[2], 1.0)); - } - if ((channels != CSC_CHANNEL_Y) && (channels != CSC_CHANNEL_CMYKA)) { - /* Update Y */ - SPColor::cmyk_to_rgb_floatv(rgb0, adj[0], adj[1], 0.0, adj[3]); - SPColor::cmyk_to_rgb_floatv(rgbm, adj[0], adj[1], 0.5, adj[3]); - SPColor::cmyk_to_rgb_floatv(rgb1, adj[0], adj[1], 1.0, adj[3]); - _s[2]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0), - SP_RGBA32_F_COMPOSE(rgbm[0], rgbm[1], rgbm[2], 1.0), - SP_RGBA32_F_COMPOSE(rgb1[0], rgb1[1], rgb1[2], 1.0)); - } - if ((channels != CSC_CHANNEL_K) && (channels != CSC_CHANNEL_CMYKA)) { - /* Update K */ - SPColor::cmyk_to_rgb_floatv(rgb0, adj[0], adj[1], adj[2], 0.0); - SPColor::cmyk_to_rgb_floatv(rgbm, adj[0], adj[1], adj[2], 0.5); - SPColor::cmyk_to_rgb_floatv(rgb1, adj[0], adj[1], adj[2], 1.0); - _s[3]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0), - SP_RGBA32_F_COMPOSE(rgbm[0], rgbm[1], rgbm[2], 1.0), - SP_RGBA32_F_COMPOSE(rgb1[0], rgb1[1], rgb1[2], 1.0)); - } - if (channels != CSC_CHANNEL_CMYKA) { - /* Update alpha */ - SPColor::cmyk_to_rgb_floatv(rgb0, adj[0], adj[1], adj[2], adj[3]); - _s[4]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 0.0), - SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 0.5), - SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0)); - } - } else if constexpr (MODE == SPColorScalesMode::HSLUV) { - if ((channels != CSC_CHANNEL_H) && (channels != CSC_CHANNEL_A)) { + if constexpr (MODE == SPColorScalesMode::HSLUV) { + if (channel_pin != CSC_CHANNEL_H) { /* Update hue */ - _s[0]->setMap(hsluvHueMap(adj[1], adj[2], &_sliders_maps[0])); + _s[0]->setMap(hsluvHueMap(color[1], color[2], &_sliders_maps[0])); } - if ((channels != CSC_CHANNEL_S) && (channels != CSC_CHANNEL_A)) { + if (channel_pin != CSC_CHANNEL_S) { /* Update saturation (scaled chroma) */ - _s[1]->setMap(hsluvSaturationMap(adj[0], adj[2], &_sliders_maps[1])); + _s[1]->setMap(hsluvSaturationMap(color[0], color[2], &_sliders_maps[1])); } - if ((channels != CSC_CHANNEL_V) && (channels != CSC_CHANNEL_A)) { + if (channel_pin != CSC_CHANNEL_V) { /* Update lightness */ - _s[2]->setMap(hsluvLightnessMap(adj[0], adj[1], &_sliders_maps[2])); - } - if (channels != CSC_CHANNEL_A) { - /* Update alpha */ - SPColor::hsluv_to_rgb_floatv(rgb0, adj[0], adj[1], adj[2]); - _s[3]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 0.0), - SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 0.5), - SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0)); + _s[2]->setMap(hsluvLightnessMap(color[0], color[1], &_sliders_maps[2])); } } else if constexpr (MODE == SPColorScalesMode::OKLAB) { - if (channels != CSC_CHANNEL_H && channels != CSC_CHANNEL_A) { - _s[0]->setMap(Oklab::render_hue_scale(adj[1], adj[2], &_sliders_maps[0])); - } - if ((channels != CSC_CHANNEL_S) && (channels != CSC_CHANNEL_A)) { - _s[1]->setMap(Oklab::render_saturation_scale(360.0 * adj[0], adj[2], &_sliders_maps[1])); + if (channel_pin != CSC_CHANNEL_H) { + _s[0]->setMap(Inkscape::Colors::Space::render_hue_scale(color[1], color[2], &_sliders_maps[0])); } - if ((channels != CSC_CHANNEL_V) && (channels != CSC_CHANNEL_A)) { - _s[2]->setMap(Oklab::render_lightness_scale(360.0 * adj[0], adj[1], &_sliders_maps[2])); + if (channel_pin != CSC_CHANNEL_S) { + _s[1]->setMap(Inkscape::Colors::Space::render_saturation_scale(360.0 * color[0], color[2], &_sliders_maps[1])); } - if (channels != CSC_CHANNEL_A) { // Update the alpha gradient. - auto const rgb = Oklab::oklab_to_rgb( - Oklab::okhsl_to_oklab({ getScaled(_a[0]), - getScaled(_a[1]), - getScaled(_a[2]) })); - _s[3]->setColors(SP_RGBA32_F_COMPOSE(rgb[0], rgb[1], rgb[2], 0.0), - SP_RGBA32_F_COMPOSE(rgb[0], rgb[1], rgb[2], 0.5), - SP_RGBA32_F_COMPOSE(rgb[0], rgb[1], rgb[2], 1.0)); + if (channel_pin != CSC_CHANNEL_V) { + _s[2]->setMap(Inkscape::Colors::Space::render_lightness_scale(360.0 * color[0], color[1], &_sliders_maps[2])); } - } else { - g_warning("file %s: line %d: Illegal color selector mode", __FILE__, __LINE__); } -#ifdef SPCS_PREVIEW - rgba = sp_color_scales_get_rgba32(cs); - sp_color_preview_set_rgba32(SP_COLOR_PREVIEW(_p), rgba); -#endif + // We request the opacity channel even though color has no opacity, this is ok as Color::set will handle it. + for (auto &channel : color.getSpace()->getComponents(true)) { + // Ignore any hue, any mapped and any updated channels + if (channel.id != "a" && (MODE == SPColorScalesMode::HSLUV || MODE == SPColorScalesMode::OKLAB)) + continue; + if (channel.scale == 360 || channel.index == channel_pin) + continue; + + auto low = color; + auto mid = color; + auto high = color; + low.set(channel.index, 0.0); + mid.set(channel.index, 0.5); + high.set(channel.index, 1.0); + _s[channel.index]->setColors(low.toRGBA(), mid.toRGBA(), high.toRGBA()); + } } static guchar const *sp_color_scales_hue_map() @@ -1063,9 +786,10 @@ static guchar const *sp_color_scales_hue_map() guchar *p; p = m.data(); + auto color = Colors::Color(Colors::Space::Type::HSL, {0, 1.0, 0.5}); for (gint h = 0; h < 1024; h++) { - gfloat rgb[3]; - SPColor::hsl_to_rgb_floatv(rgb, h / 1024.0, 1.0, 0.5); + color.set(0, h / 1024.0); + auto rgb = *color.converted(Colors::Space::Type::RGB); *p++ = SP_COLOR_F_TO_U(rgb[0]); *p++ = SP_COLOR_F_TO_U(rgb[1]); *p++ = SP_COLOR_F_TO_U(rgb[2]); @@ -1139,8 +863,13 @@ template guchar const *ColorScales::hsluvHueMap(gfloat s, gfloat l, std::array *map) { - return sp_color_scales_hsluv_map(map->data(), [s, l] (float *colors, float h) { - SPColor::hsluv_to_rgb_floatv(colors, h, s, l); + auto color = Colors::Color(Colors::Space::Type::HSLUV, {0, s, l}); + return sp_color_scales_hsluv_map(map->data(), [&color] (float *colors, float h) { + color.set(0, (double)h); + auto rgb = *color.converted(Colors::Space::Type::RGB); + *colors++ = rgb[0]; + *colors++ = rgb[1]; + *colors++ = rgb[2]; }); } @@ -1148,8 +877,13 @@ template guchar const *ColorScales::hsluvSaturationMap(gfloat h, gfloat l, std::array *map) { - return sp_color_scales_hsluv_map(map->data(), [h, l] (float *colors, float s) { - SPColor::hsluv_to_rgb_floatv(colors, h, s, l); + auto color = Colors::Color(Colors::Space::Type::HSLUV, {h, 0, l}); + return sp_color_scales_hsluv_map(map->data(), [&color] (float *colors, float s) { + color.set(1, (double)s); + auto rgb = *color.converted(Colors::Space::Type::RGB); + *colors++ = rgb[0]; + *colors++ = rgb[1]; + *colors++ = rgb[2]; }); } @@ -1157,8 +891,13 @@ template guchar const *ColorScales::hsluvLightnessMap(gfloat h, gfloat s, std::array *map) { - return sp_color_scales_hsluv_map(map->data(), [h, s] (float *colors, float l) { - SPColor::hsluv_to_rgb_floatv(colors, h, s, l); + auto color = Colors::Color(Colors::Space::Type::HSLUV, {h, s, 0}); + return sp_color_scales_hsluv_map(map->data(), [&color] (float *colors, float l) { + color.set(2, (double)l); + auto rgb = *color.converted(Colors::Space::Type::RGB); + *colors++ = rgb[0]; + *colors++ = rgb[1]; + *colors++ = rgb[2]; }); } @@ -1167,9 +906,9 @@ ColorScalesFactory::ColorScalesFactory() {} template -Gtk::Widget *ColorScalesFactory::createWidget(Inkscape::UI::SelectedColor &color, bool no_alpha) const +Gtk::Widget *ColorScalesFactory::createWidget(Inkscape::UI::SelectedColor color, bool no_alpha) const { - Gtk::Widget *w = Gtk::make_managed>(color, no_alpha); + Gtk::Widget *w = Gtk::make_managed>(std::move(color), no_alpha); return w; } diff --git a/src/ui/widget/color-scales.h b/src/ui/widget/color-scales.h index b533ff73e1..b3e2afc04a 100644 --- a/src/ui/widget/color-scales.h +++ b/src/ui/widget/color-scales.h @@ -19,14 +19,29 @@ #include #include +#include "colors/color-set.h" #include "helper/auto-connection.h" -#include "ui/selected-color.h" namespace Gtk { class Label; class SpinButton; } // namespace Gtk + +// TODO: Replace this later, originally ui/selected-color.h +namespace Inkscape::UI { + +typedef std::shared_ptr SelectedColor; + +class ColorSelectorFactory { +public: + virtual ~ColorSelectorFactory() = default; + + virtual Gtk::Widget* createWidget(SelectedColor colors, bool no_alpha) const = 0; + virtual Glib::ustring modeName() const = 0; +}; +} + namespace Inkscape::UI::Widget { class ColorSlider; @@ -53,7 +68,7 @@ public: static double getScaled(Glib::RefPtr const &a); static void setScaled(Glib::RefPtr &a, double v, bool constrained = false); - ColorScales(SelectedColor &color, bool no_alpha); + ColorScales(SelectedColor color, bool no_alpha); void setupMode(bool no_alpha); SPColorScalesMode getMode() const; @@ -77,8 +92,7 @@ protected: void _adjustmentChanged(int channel); void _wheelChanged(); - void _getRgbaFloatv(gfloat *rgba); - void _getCmykaFloatv(gfloat *cmyka); + Colors::Color _getColor(); guint32 _getRgba32(); void _updateSliders(guint channels); void _recalcColor(); @@ -86,7 +100,7 @@ protected: void _setRangeLimit(gdouble upper); - SelectedColor &_color; + SelectedColor _color; gdouble _range_limit; gboolean _updating : 1; gboolean _dragging : 1; @@ -115,7 +129,7 @@ class ColorScalesFactory : public Inkscape::UI::ColorSelectorFactory public: ColorScalesFactory(); - Gtk::Widget *createWidget(Inkscape::UI::SelectedColor &color, bool no_alpha) const override; + Gtk::Widget *createWidget(Inkscape::UI::SelectedColor color, bool no_alpha) const override; Glib::ustring modeName() const override; }; diff --git a/src/ui/widget/color-slider.cpp b/src/ui/widget/color-slider.cpp index e75fc9fb49..541575cd35 100644 --- a/src/ui/widget/color-slider.cpp +++ b/src/ui/widget/color-slider.cpp @@ -18,6 +18,7 @@ #include #include +#include "colors/color.h" #include "ui/widget/color-slider.h" #include "preferences.h" #include "ui/controller.h" @@ -52,10 +53,10 @@ ColorSlider::ColorSlider(Glib::RefPtr adjustment) _cm[2] = 0x00; _cm[3] = 0xff; - _c0[0] = 0xff; - _c0[1] = 0xff; - _c0[2] = 0xff; - _c0[3] = 0xff; + _c1[0] = 0xff; + _c1[1] = 0xff; + _c1[2] = 0xff; + _c1[3] = 0xff; _b0 = 0x5f; _b1 = 0xa0; diff --git a/src/ui/widget/fill-style.cpp b/src/ui/widget/fill-style.cpp index 503f8cf6f5..477ede501e 100644 --- a/src/ui/widget/fill-style.cpp +++ b/src/ui/widget/fill-style.cpp @@ -58,11 +58,15 @@ namespace Widget { FillNStroke::FillNStroke(FillOrStroke k) : Gtk::Box(Gtk::Orientation::VERTICAL) , kind(k) + , _solid_colors(std::make_shared()) , subselChangedConn() , eventContextConn() { + // Single fallback color + _solid_colors->set(Color(0x000000ff)); + // Add and connect up the paint selector widget: - _psel = Gtk::make_managed(kind); + _psel = Gtk::make_managed(kind, _solid_colors); _psel->set_visible(true); append(*_psel); _psel->signal_mode_changed().connect(sigc::mem_fun(*this, &FillNStroke::paintModeChangeCB)); @@ -189,7 +193,7 @@ void FillNStroke::performUpdate() } } SPIPaint &targPaint = *query.getFillOrStroke(kind == FILL); - SPIScale24 &targOpacity = *(kind == FILL ? query.fill_opacity.upcast() : query.stroke_opacity.upcast()); + double targOpacity = kind == FILL ? query.fill_opacity : query.stroke_opacity; switch (result) { case QUERY_STYLE_NOTHING: { @@ -212,7 +216,10 @@ void FillNStroke::performUpdate() } if (targPaint.set && targPaint.isColor()) { - _psel->setColorAlpha(targPaint.value.color, SP_SCALE24_TO_FLOAT(targOpacity.value)); + // This is terrible, future refactoring has been written + auto color = targPaint.getColor(); + color.addOpacity(targOpacity); + _solid_colors->set(color); } else if (targPaint.set && targPaint.isPaintserver()) { SPPaintServer* server = (kind == FILL) ? query.getFillPaintServer() : query.getStrokePaintServer(); @@ -349,9 +356,9 @@ void FillNStroke::dragFromPaint() case UI::Widget::PaintSelector::MODE_SOLID_COLOR: { // local change, do not update from selection _drag_id = g_timeout_add_full(G_PRIORITY_DEFAULT, 100, dragDelayCB, this, nullptr); - _psel->setFlatColor(_desktop, - (kind == FILL) ? "fill" : "stroke", - (kind == FILL) ? "fill-opacity" : "stroke-opacity"); + + sp_desktop_set_color(_desktop, _solid_colors->getAverage(), false, kind == FILL); + DocumentUndo::maybeDone(_desktop->doc(), (kind == FILL) ? undo_F_label : undo_S_label, (kind == FILL) ? _("Set fill color") : _("Set stroke color"), INKSCAPE_ICON("dialog-fill-and-stroke")); @@ -428,8 +435,9 @@ void FillNStroke::updateFromPaint(bool switch_style) } case UI::Widget::PaintSelector::MODE_SOLID_COLOR: { - _psel->setFlatColor(_desktop, (kind == FILL) ? "fill" : "stroke", - (kind == FILL) ? "fill-opacity" : "stroke-opacity"); + + sp_desktop_set_color(_desktop, _solid_colors->getAverage(), false, kind == FILL); + DocumentUndo::maybeDone(_desktop->getDocument(), (kind == FILL) ? undo_F_label : undo_S_label, (kind == FILL) ? _("Set fill color") : _("Set stroke color"), INKSCAPE_ICON("dialog-fill-and-stroke")); @@ -462,11 +470,12 @@ void FillNStroke::updateFromPaint(bool switch_style) int result = objects_query_fillstroke(items, &query, kind == FILL); if (result == QUERY_STYLE_MULTIPLE_SAME) { SPIPaint &targPaint = *query.getFillOrStroke(kind == FILL); - SPColor common; + Inkscape::Colors::Color common(0x000000ff); if (!targPaint.isColor()) { - common = sp_desktop_get_color(_desktop, kind == FILL); + if (auto color = sp_desktop_get_color(_desktop, kind == FILL)) + common = *color; } else { - common = targPaint.value.color; + common = targPaint.getColor(); } vector = sp_document_default_gradient_vector(document, common, 1.0, createSwatch); } diff --git a/src/ui/widget/fill-style.h b/src/ui/widget/fill-style.h index 165aec791b..8cd12a5540 100644 --- a/src/ui/widget/fill-style.h +++ b/src/ui/widget/fill-style.h @@ -36,6 +36,7 @@ namespace Widget { class FillNStroke : public Gtk::Box { private: FillOrStroke kind; + std::shared_ptr _solid_colors; SPDesktop *_desktop = nullptr; PaintSelector *_psel = nullptr; guint32 _last_drag = 0; diff --git a/src/ui/widget/gradient-editor.cpp b/src/ui/widget/gradient-editor.cpp index 405bfad34a..bcc4d39d88 100644 --- a/src/ui/widget/gradient-editor.cpp +++ b/src/ui/widget/gradient-editor.cpp @@ -77,7 +77,7 @@ void set_icon(Gtk::Button &btn, char const *pixmap) } // draw solid color circle with black outline; right side is to show checkerboard if color's alpha is > 0 -Glib::RefPtr draw_circle(int size, guint32 rgba) { +Glib::RefPtr draw_circle(int size, Colors::Color color) { int width = size; int height = size; gint w2 = width / 2; @@ -104,7 +104,8 @@ Glib::RefPtr draw_circle(int size, guint32 rgba) { cairo_close_path(cr); // solid part - ink_cairo_set_source_rgba32(cr, rgba | 0xff); + auto opacity = color.stealOpacity(); + ink_cairo_set_source_color(cr, color); cairo_fill(cr); x = w2; @@ -115,13 +116,14 @@ Glib::RefPtr draw_circle(int size, guint32 rgba) { cairo_close_path(cr); // (semi)transparent part - if ((rgba & 0xff) != 0xff) { + if (opacity < 1.0) { cairo_pattern_t* checkers = ink_cairo_pattern_create_checkerboard(); cairo_set_source(cr, checkers); cairo_fill_preserve(cr); cairo_pattern_destroy(checkers); } - ink_cairo_set_source_rgba32(cr, rgba); + color.addOpacity(opacity); + ink_cairo_set_source_color(cr, color); cairo_fill(cr); cairo_destroy(cr); @@ -134,7 +136,7 @@ Glib::RefPtr draw_circle(int size, guint32 rgba) { Glib::RefPtr get_stop_pixmap(SPStop* stop) { const int size = 30; - return draw_circle(size, stop->getColor().toRGBA32(stop->getOpacity())); + return draw_circle(size, stop->getColor()); } Glib::ustring get_repeat_icon(SPGradientSpread mode) { @@ -156,9 +158,10 @@ Glib::ustring get_repeat_icon(SPGradientSpread mode) { return ico; } -GradientEditor::GradientEditor(const char* prefs) : +GradientEditor::GradientEditor(const char* prefs): _builder(Inkscape::UI::create_builder("gradient-edit.glade")), _selector(Gtk::make_managed()), + _colors(new Colors::ColorSet()), _repeat_popover{std::make_unique(Gtk::PositionType::BOTTOM)}, _repeat_icon(get_widget(_builder, "repeatIco")), _stop_tree(get_widget(_builder, "stopList")), @@ -211,7 +214,7 @@ GradientEditor::GradientEditor(const char* prefs) : gradBox.append(_gradient_image); // add color selector - auto const color_selector = Gtk::make_managed(_selected_color); + auto const color_selector = Gtk::make_managed(_colors); color_selector->set_label(_("Stop color")); color_selector->set_visible(true); _colors_box.append(*color_selector); @@ -289,11 +292,8 @@ GradientEditor::GradientEditor(const char* prefs) : get_widget(_builder, "repeatMode").set_popover(*_repeat_popover); set_repeat_icon(SP_GRADIENT_SPREAD_PAD); - _selected_color.signal_changed.connect([=]() { - set_stop_color(_selected_color.color(), _selected_color.alpha()); - }); - _selected_color.signal_dragged.connect([=]() { - set_stop_color(_selected_color.color(), _selected_color.alpha()); + _colors->signal_changed.connect([=]() { + set_stop_color(_colors->getAverage()); }); _offset_btn.signal_changed().connect([=]() { @@ -315,7 +315,8 @@ GradientEditor::GradientEditor(const char* prefs) : GradientEditor::~GradientEditor() noexcept { } -void GradientEditor::set_stop_color(SPColor color, float opacity) { +void GradientEditor::set_stop_color(Inkscape::Colors::Color const &color) +{ if (_update.pending()) return; SPGradient* vector = get_gradient_vector(); @@ -330,7 +331,7 @@ void GradientEditor::set_stop_color(SPColor color, float opacity) { // update list view too row->set_value(_stop_color, get_stop_pixmap(stop)); - sp_set_gradient_stop_color(_document, stop, color, opacity); + sp_set_gradient_stop_color(_document, stop, color); } } } @@ -355,13 +356,13 @@ SPStop* GradientEditor::get_nth_stop(size_t index) { // stop has been selected in a list view void GradientEditor::stop_selected() { + _colors->clear(); if (auto row = current_stop()) { SPStop* stop = row->get_value(_stopObj); if (stop) { auto scoped(_update.block()); - _selected_color.setColor(stop->getColor()); - _selected_color.setAlpha(stop->getOpacity()); + _colors->set(stop->getId(), stop->getColor()); auto stops = sp_get_before_after_stops(stop); if (stops.first && stops.second) { @@ -380,9 +381,6 @@ void GradientEditor::stop_selected() { else { // no selection auto scoped(_update.block()); - - _selected_color.setColor(SPColor()); - _offset_btn.set_range(0, 0); _offset_btn.set_value(0); _offset_btn.set_sensitive(false); diff --git a/src/ui/widget/gradient-editor.h b/src/ui/widget/gradient-editor.h index af5ce636bc..21ef0cbde1 100644 --- a/src/ui/widget/gradient-editor.h +++ b/src/ui/widget/gradient-editor.h @@ -19,9 +19,9 @@ #include #include +#include "colors/color-set.h" #include "object/sp-gradient.h" #include "object/sp-stop.h" -#include "ui/selected-color.h" #include "spin-scale.h" #include "gradient-with-stops.h" #include "gradient-selector-interface.h" @@ -86,7 +86,7 @@ private: void set_repeat_icon(SPGradientSpread mode); void reverse_gradient(); void turn_gradient(double angle, bool relative); - void set_stop_color(SPColor color, float opacity); + void set_stop_color(Colors::Color const &color); std::optional current_stop(); SPStop* get_nth_stop(size_t index); SPStop* get_current_stop(); @@ -97,7 +97,7 @@ private: Glib::RefPtr _builder; GradientSelector* _selector; - Inkscape::UI::SelectedColor _selected_color; + std::shared_ptr _colors; std::unique_ptr _repeat_popover; Gtk::Image& _repeat_icon; GradientWithStops _gradient_image; diff --git a/src/ui/widget/gradient-image.cpp b/src/ui/widget/gradient-image.cpp index 6f573789d2..f39edd0f8c 100644 --- a/src/ui/widget/gradient-image.cpp +++ b/src/ui/widget/gradient-image.cpp @@ -129,12 +129,14 @@ sp_gradstop_to_pixbuf_ref (SPStop *stop, int width, int height) if (stop) { /* Alpha area */ cairo_rectangle(ct, 0, 0, width/2, height); - ink_cairo_set_source_rgba32(ct, stop->get_rgba32()); + ink_cairo_set_source_color(ct, stop->getColor()); cairo_fill(ct); /* Solid area */ + auto no_alpha = stop->getColor(); + no_alpha.enableOpacity(false); cairo_rectangle(ct, width/2, 0, width, height); - ink_cairo_set_source_rgba32(ct, stop->get_rgba32() | 0xff); + ink_cairo_set_source_color(ct, no_alpha); cairo_fill(ct); } diff --git a/src/ui/widget/gradient-vector-selector.cpp b/src/ui/widget/gradient-vector-selector.cpp index 5fa9f0f5bb..f40bba773b 100644 --- a/src/ui/widget/gradient-vector-selector.cpp +++ b/src/ui/widget/gradient-vector-selector.cpp @@ -35,11 +35,8 @@ #include "object/sp-defs.h" #include "object/sp-stop.h" -#include "ui/selected-color.h" #include "ui/widget/gradient-image.h" -using Inkscape::UI::SelectedColor; - void gr_get_usage_counts(SPDocument *doc, std::map *mapUsageCount ); unsigned long sp_gradient_to_hhssll(SPGradient *gr); @@ -251,11 +248,8 @@ Glib::ustring gr_ellipsize_text(Glib::ustring const &src, size_t maxlen) */ unsigned long sp_gradient_to_hhssll(SPGradient *gr) { - SPStop *stop = gr->getFirstStop(); - unsigned long rgba = stop->get_rgba32(); - float hsl[3]; - SPColor::rgb_to_hsl_floatv (hsl, SP_RGBA32_R_F(rgba), SP_RGBA32_G_F(rgba), SP_RGBA32_B_F(rgba)); - + auto hsl = gr->getFirstStop()->getColor(); + hsl.convert(Inkscape::Colors::Space::Type::HSL); return ((int)(hsl[0]*100 * 10000)) + ((int)(hsl[1]*100 * 100)) + ((int)(hsl[2]*100 * 1)); } diff --git a/src/ui/widget/gradient-with-stops.cpp b/src/ui/widget/gradient-with-stops.cpp index 56cf8c0047..281c485c52 100644 --- a/src/ui/widget/gradient-with-stops.cpp +++ b/src/ui/widget/gradient-with-stops.cpp @@ -27,6 +27,7 @@ #include "ui/controller.h" #include "ui/cursor-utils.h" #include "ui/util.h" +#include "util/numeric/converters.h" #include "util/object-renderer.h" // c.f. share/ui/style.css @@ -92,7 +93,7 @@ void GradientWithStops::modified() { SPStop* stop = _gradient->getFirstStop(); while (stop) { _stops.push_back(stop_t { - .offset = stop->offset, .color = stop->getColor(), .opacity = stop->getOpacity() + .offset = stop->offset, .color = stop->getColor() }); stop = stop->getNextStop(); } @@ -451,9 +452,9 @@ void GradientWithStops::draw_func(Cairo::RefPtr const &cr, auto const &bg = _background_color; // stop handle outlines and selection indicator use theme colors: - _template.set_style(".outer", "fill", rgba_to_css_color(fg)); - _template.set_style(".inner", "stroke", rgba_to_css_color(bg)); - _template.set_style(".hole", "fill", rgba_to_css_color(bg)); + _template.set_style(".outer", "fill", gdk_to_css_color(fg)); + _template.set_style(".inner", "stroke", gdk_to_css_color(bg)); + _template.set_style(".hole", "fill", gdk_to_css_color(bg)); auto tip = _tip_template.render(scale); @@ -461,12 +462,12 @@ void GradientWithStops::draw_func(Cairo::RefPtr const &cr, const auto& stop = _stops[i]; // stop handle shows stop color and opacity: - _template.set_style(".color", "fill", rgba_to_css_color(stop.color)); - _template.set_style(".opacity", "opacity", double_to_css_value(stop.opacity)); + _template.set_style(".color", "fill", stop.color.toString(false)); + _template.set_style(".opacity", "opacity", Util::format_number(stop.opacity)); // show/hide selection indicator const auto is_selected = _focused_stop == static_cast(i); - _template.set_style(".selected", "opacity", double_to_css_value(is_selected ? 1 : 0)); + _template.set_style(".selected", "opacity", Util::format_number(is_selected ? 1 : 0)); // render stop handle auto pix = _template.render(scale); diff --git a/src/ui/widget/gradient-with-stops.h b/src/ui/widget/gradient-with-stops.h index 991ff081b7..5d63688a5c 100644 --- a/src/ui/widget/gradient-with-stops.h +++ b/src/ui/widget/gradient-with-stops.h @@ -21,6 +21,7 @@ #include // Gtk::EventSequenceState #include +#include "colors/color.h" #include "helper/auto-connection.h" #include "ui/svg-renderer.h" #include "ui/widget/widget-vfuncs-class-init.h" @@ -116,7 +117,7 @@ private: struct stop_t { double offset; - SPColor color; + Inkscape::Colors::Color color; double opacity; }; std::vector _stops; diff --git a/src/ui/widget/ink-color-wheel.cpp b/src/ui/widget/ink-color-wheel.cpp index 9c73b17ead..c98e090b2f 100644 --- a/src/ui/widget/ink-color-wheel.cpp +++ b/src/ui/widget/ink-color-wheel.cpp @@ -13,20 +13,29 @@ * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ -#include -#include +#include "ui/widget/ink-color-wheel.h" + #include <2geom/angle.h> #include <2geom/coord.h> -#include +#include +#include #include #include #include +#include +#include +#include "colors/spaces/enum.h" +#include "colors/spaces/hsluv.h" +#include "colors/utils.h" #include "ui/controller.h" #include "ui/dialog/color-item.h" #include "ui/util.h" #include "ui/widget/bin.h" -#include "ui/widget/ink-color-wheel.h" + +using namespace Inkscape::Colors; +using Inkscape::Colors::Space::Luv; +using Inkscape::Colors::Space::Type; namespace Inkscape::UI::Widget { @@ -73,8 +82,7 @@ struct Intersection final static double lerp(double v0, double v1, double t0, double t1, double t); static ColorPoint lerp(ColorPoint const &v0, ColorPoint const &v1, double t0, double t1, double t); -static guint32 hsv_to_rgb(double h, double s, double v); -static double luminance(guint32 color); +static double luminance(Color const &color); static Geom::Point to_pixel_coordinate(Geom::Point const &point, double scale, double resize); static Geom::Point from_pixel_coordinate(Geom::Point const &point, double scale, double resize); static std::vector to_pixel_coordinate(std::vector const &points, double scale, @@ -84,9 +92,10 @@ static void draw_vertical_padding(ColorPoint p0, ColorPoint p1, int padding, boo /* Base Color Wheel */ -ColorWheel::ColorWheel() +ColorWheel::ColorWheel(Type type, std::vector initial_color) : Gtk::AspectFrame(0.5, 0.5, 1.0, false) , _bin{Gtk::make_managed()} + , _values{type, std::move(initial_color)} , _drawing_area{Gtk::make_managed()} { set_name("ColorWheel"); @@ -108,37 +117,6 @@ ColorWheel::ColorWheel() (*_drawing_area, *this); } -bool ColorWheel::setHue(double h, bool const emit) -{ - h = std::clamp(h, MIN_HUE, MAX_HUE); - bool const changed = std::exchange(_values[0], h) != h; - if (changed && emit) color_changed(); - return changed; -} - -bool ColorWheel::setSaturation(double s, bool const emit) -{ - s = std::clamp(s, MIN_SATURATION, MAX_SATURATION); - bool const changed = std::exchange(_values[1], s) != s; - if (changed && emit) color_changed(); - return changed; -} - -bool ColorWheel::setLightness(double l, bool const emit) -{ - l = std::clamp(l, MIN_LIGHTNESS, MAX_LIGHTNESS); - bool const changed = std::exchange(_values[2], l) != l; - if (changed && emit) color_changed(); - return changed; -} - -void ColorWheel::getValues(double *a, double *b, double *c) const -{ - if (a) *a = _values[0]; - if (b) *b = _values[1]; - if (c) *c = _values[2]; -} - sigc::connection ColorWheel::connect_color_changed(sigc::slot slot) { return _signal_color_changed.connect(std::move(slot)); @@ -202,99 +180,19 @@ bool ColorWheel::on_key_released(GtkEventControllerKey const * /*controller*/, /* HSL Color Wheel */ -bool ColorWheelHSL::setRgb(double r, double g, double b, - bool const overrideHue, bool const emit) +bool ColorWheelHSL::setColor(Color const &color, + bool const /*overrideHue*/, bool const emit) { - auto const old_values = _values; - auto const [min, max] = std::minmax({r, g, b}); - - _values[2] = max; - - if (min == max) { - if (overrideHue) { - _values[0] = 0.0; - } - } else { - if (max == r) { - _values[0] = ((g - b) / (max - min) ) / 6.0; - } else if (max == g) { - _values[0] = ((b - r) / (max - min) + 2) / 6.0; - } else { - _values[0] = ((r - g) / (max - min) + 4) / 6.0; - } - - if (_values[0] < 0.0) { - _values[0] += 1.0; - } - } - - if (max == 0) { - _values[1] = 0; - } else { - _values[1] = (max - min) / max; - } - - bool changed = false; - - if (_values[0] != old_values[0]) { - changed = true; + if (_values.set(color, true)) { _triangle_corners.reset(); - } - - if (_values[1] != old_values[1] || _values[2] != old_values[2]) { - changed = true; _marker_point.reset(); + if (emit) + color_changed(); + else + queue_drawing_area_draw(); + return true; } - - if (changed && emit) color_changed(); - return changed; -} - -void ColorWheelHSL::getRgb(double *r, double *g, double *b) const -{ - guint32 color = getRgb(); - *r = ((color & 0x00ff0000) >> 16) / 255.0; - *g = ((color & 0x0000ff00) >> 8) / 255.0; - *b = ((color & 0x000000ff) ) / 255.0; -} - -void ColorWheelHSL::getRgbV(double *rgb) const -{ - guint32 color = getRgb(); - rgb[0] = ((color & 0x00ff0000) >> 16) / 255.0; - rgb[1] = ((color & 0x0000ff00) >> 8) / 255.0; - rgb[2] = ((color & 0x000000ff) ) / 255.0; -} - -guint32 ColorWheelHSL::getRgb() const -{ - return hsv_to_rgb(_values[0], _values[1], _values[2]); -} - -bool ColorWheelHSL::setHue(double const h, bool const emit) -{ - bool const changed = ColorWheel::setHue(h, emit); - if (changed) _triangle_corners.reset(); - return changed; -} - -bool ColorWheelHSL::setSaturation(double const s, bool const emit) -{ - bool const changed = ColorWheel::setSaturation(s, emit); - if (changed) _marker_point.reset(); - return changed; -} - -bool ColorWheelHSL::setLightness(double const l, bool const emit) -{ - bool const changed = ColorWheel::setLightness(l, emit); - if (changed) _marker_point.reset(); - return changed; -} - -void ColorWheelHSL::getHsl(double *h, double *s, double *l) const -{ - getValues(h, s, l); + return false; } void ColorWheelHSL::update_ring_source() @@ -327,7 +225,7 @@ void ColorWheelHSL::update_ring_source() angle += 2.0 * M_PI; } double hue = angle/(2.0 * M_PI); - *p++ = hsv_to_rgb(hue, 1.0, 1.0); + *p++ = Color(Type::HSV, {hue, 1.0, 1.0}).toARGB(); } } } @@ -391,13 +289,13 @@ ColorWheelHSL::update_triangle_source() auto p = _buffer_triangle.data() + y * (stride / 4); int x = 0; for (; x <= x_start; ++x) { - *p++ = side0.get_color(); + *p++ = side0.color.toARGB(); } for (; x < x_end; ++x) { - *p++ = lerp(side0, side1, side0.x, side1.x, x).get_color(); + *p++ = lerp(side0, side1, side0.x, side1.x, x).color.toARGB(); } for (; x < width; ++x) { - *p++ = side1.get_color(); + *p++ = side1.color.toARGB(); } } } @@ -454,9 +352,8 @@ void ColorWheelHSL::on_drawing_area_draw(Cairo::RefPtr const &cr cr->stroke(); cr->restore(); // Paint line on ring - double l = 0.0; - guint32 color_on_ring = hsv_to_rgb(_values[0], 1.0, 1.0); - if (luminance(color_on_ring) < 0.5) l = 1.0; + auto color_on_ring = Color(Type::HSV, {_values[0], 1.0, 1.0}); + double l = luminance(color_on_ring) < 0.5 ? 1.0 : 0.0; cr->save(); cr->set_source_rgb(l, l, l); cr->move_to(cx + cos(_values[0] * M_PI * 2.0) * (r_min + 1), @@ -478,9 +375,7 @@ void ColorWheelHSL::on_drawing_area_draw(Cairo::RefPtr const &cr // Draw marker auto const &[mx, my] = get_marker_point(); - double a = 0.0; - guint32 color_at_marker = getRgb(); - if (luminance(color_at_marker) < 0.5) a = 1.0; + double a = luminance(getColor()) < 0.5 ? 1.0 : 0.0; cr->set_source_rgb(a, a, a); cr->begin_new_path(); cr->arc(mx, my, marker_radius, 0, 2 * M_PI); @@ -567,10 +462,11 @@ bool ColorWheelHSL::_set_from_xy(double const x, double const y) double yt = lerp(0.0, 1.0, -dy, dy, yp); yt = std::clamp(yt, 0.0, 1.0); - ColorPoint c0(0, 0, yt, yt, yt); // Grey point along base. - ColorPoint c1(0, 0, hsv_to_rgb(_values[0], 1, 1)); // Hue point at apex + ColorPoint c0(0, 0, Color(Type::RGB, {yt, yt, yt})); // Grey point along base. + ColorPoint c1(0, 0, Color(Type::HSV, {_values[0], 1, 1})); // Hue point at apex ColorPoint c = lerp(c0, c1, 0, 1, xt); - return setRgb(c.r, c.g, c.b, false); // Don't override previous hue. + c.color.setOpacity(_values.getOpacity()); // Remember opacity + return setColor(c.color, false); // Don't override previous hue. } bool ColorWheelHSL::set_from_xy_delta(double const dx, double const dy) @@ -625,8 +521,7 @@ void ColorWheelHSL::_update_ring_color(double x, double y) } angle /= 2.0 * M_PI; - bool const changed = std::exchange(_values[0], angle) != angle; - if (changed) { + if (_values.set(0, angle)) { _triangle_corners.reset(); color_changed(); } @@ -689,7 +584,6 @@ bool ColorWheelHSL::on_key_pressed(GtkEventControllerKey const * /*controller*/, nullptr); static constexpr double delta_hue = 2.0 / MAX_HUE; - auto const old_hue = _values[0]; auto dx = 0.0, dy = 0.0; switch (key) { @@ -717,21 +611,12 @@ bool ColorWheelHSL::on_key_pressed(GtkEventControllerKey const * /*controller*/, bool changed = false; if (_focus_on_ring) { - _values[0] += -(dx != 0 ? dx : dy) * delta_hue; + changed = _values.set(0, _values[0] - ((dx != 0 ? dx : dy) * delta_hue)); } else { changed = set_from_xy_delta(dx, dy); } - if (_values[0] >= 1.0) { - _values[0] -= 1.0; - } else if (_values[0] < 0.0) { - _values[0] += 1.0; - } - - if (_values[0] != old_hue) { - _triangle_corners.reset(); - changed = true; - } + _values.normalize(); if (changed) { color_changed(); @@ -780,9 +665,9 @@ std::array const &ColorWheelHSL::get_triangle_corners() auto const y1 = cy - std::sin(angle2) * r_min; auto const x2 = cx + std::cos(angle4) * r_min; auto const y2 = cy - std::sin(angle4) * r_min; - p0 = {x0, y0, hsv_to_rgb(_values[0], 1.0, 1.0)}; - p1 = {x1, y1, hsv_to_rgb(_values[0], 1.0, 0.0)}; - p2 = {x2, y2, hsv_to_rgb(_values[0], 0.0, 1.0)}; + p0 = {x0, y0, Color(Type::HSV, {_values[0], 1.0, 1.0})}; + p1 = {x1, y1, Color(Type::HSV, {_values[0], 1.0, 0.0})}; + p2 = {x2, y2, Color(Type::HSV, {_values[0], 0.0, 1.0})}; return *_triangle_corners; } @@ -803,61 +688,36 @@ Geom::Point const &ColorWheelHSL::get_marker_point() return *_marker_point; } +ColorWheelHSL::ColorWheelHSL() + : Glib::ObjectBase{"ColorWheelHSL"} + , WidgetVfuncsClassInit{} + // All the calculations are based on HSV, not HSL + , ColorWheel(Type::HSV, {0, 0, 0, 1}) +{} /* HSLuv Color Wheel */ ColorWheelHSLuv::ColorWheelHSLuv() + : ColorWheel(Type::HSLUV, {0, 1, 0.5, 1}) { - _picker_geometry = std::make_unique(); - setHsluv(MIN_HUE, MAX_SATURATION, 0.5 * MAX_LIGHTNESS); + _picker_geometry = std::make_unique(); } -bool ColorWheelHSLuv::setRgb(double r, double g, double b, +bool ColorWheelHSLuv::setColor(Color const &color, bool /*overrideHue*/, bool const emit) { - auto hsl = Hsluv::rgb_to_hsluv(r, g, b); - bool changed = false; - changed |= setHue (hsl[0], false); - changed |= setSaturation(hsl[1], false); - changed |= setLightness (hsl[2], false); - if (changed && emit) color_changed(); - return changed; -} - -void ColorWheelHSLuv::getRgb(double *r, double *g, double *b) const -{ - auto rgb = Hsluv::hsluv_to_rgb(_values[0], _values[1], _values[2]); - *r = rgb[0]; - *g = rgb[1]; - *b = rgb[2]; -} - -void ColorWheelHSLuv::getRgbV(double *rgb) const -{ - auto converted = Hsluv::hsluv_to_rgb(_values[0], _values[1], _values[2]); - for (size_t i : {0, 1, 2}) { - rgb[i] = converted[i]; + if (_values.set(color, true)) { + assert(_values.getSpace()->getType() == Type::HSLUV); + updateGeometry(); + _scale = OUTER_CIRCLE_RADIUS / _picker_geometry->outer_circle_radius; + _updatePolygon(); + if (emit) + color_changed(); + else + queue_drawing_area_draw(); + return true; } -} - -guint32 ColorWheelHSLuv::getRgb() const -{ - auto rgb = Hsluv::hsluv_to_rgb(_values[0], _values[1], _values[2]); - return ( - (static_cast(rgb[0] * 255.0) << 16) | - (static_cast(rgb[1] * 255.0) << 8) | - (static_cast(rgb[2] * 255.0) ) - ); -} - -bool ColorWheelHSLuv::setHsluv(double h, double s, double l) -{ - bool changed = false; - changed |= setHue (h, false); - changed |= setSaturation(s, false); - changed |= setLightness (l, false); - if (changed) color_changed(); - return changed; + return false; } /** @@ -866,10 +726,10 @@ bool ColorWheelHSLuv::setHsluv(double h, double s, double l) void ColorWheelHSLuv::updateGeometry() { // Separate from the extremes to avoid overlapping intersections - double lightness = std::clamp(_values[2] + 0.01, 0.1, 99.9); + double lightness = std::clamp((_values[2] * 100) + 0.01, 0.1, 99.9); // Find the lines bounding the gamut polygon - auto const lines = Hsluv::get_bounds(lightness); + auto const lines = Space::HSLuv::get_bounds(lightness); // Find the line closest to origin Geom::Line const *closest_line = nullptr; @@ -929,22 +789,6 @@ void ColorWheelHSLuv::updateGeometry() _picker_geometry->inner_circle_radius = closest_distance; } -bool ColorWheelHSLuv::setLightness(double const l, bool const emit) -{ - bool const changed = ColorWheel::setLightness(l, emit); - if (changed) { - updateGeometry(); - _scale = OUTER_CIRCLE_RADIUS / _picker_geometry->outer_circle_radius; - _updatePolygon(); - } - return changed; -} - -void ColorWheelHSLuv::getHsluv(double *h, double *s, double *l) const -{ - getValues(h, s, l); -} - static Geom::IntPoint _getMargin(Gtk::Allocation const &allocation) { int const width = allocation.get_width(); @@ -967,7 +811,7 @@ inline static int _getAllocationSize(Gtk::Allocation const &allocation) /// Detect whether we're at the top or bottom vertex of the color space. bool ColorWheelHSLuv::_vertex() const { - return _values[2] < VERTEX_EPSILON || _values[2] > MAX_LIGHTNESS - VERTEX_EPSILON; + return _values[2] < VERTEX_EPSILON || _values[2] > 1.0 - VERTEX_EPSILON; } void ColorWheelHSLuv::on_drawing_area_draw(::Cairo::RefPtr<::Cairo::Context> const &cr, int, int) @@ -1027,7 +871,7 @@ void ColorWheelHSLuv::on_drawing_area_draw(::Cairo::RefPtr<::Cairo::Context> con cr->unset_dash(); // Contrast - auto [gray, alpha] = Hsluv::get_contrasting_color(Hsluv::perceptual_lightness(_values[2])); + auto [gray, alpha] = get_contrasting_color(perceptual_lightness(_values[2])); cr->set_source_rgba(gray, gray, gray, alpha); // Draw inscribed circle @@ -1044,7 +888,7 @@ void ColorWheelHSLuv::on_drawing_area_draw(::Cairo::RefPtr<::Cairo::Context> con cr->fill(); // Draw marker - auto luv = Hsluv::hsluv_to_luv(_values.data()); + auto luv = Luv::toCoordinates(_values.converted(Type::LUV)->getValues()); auto mp = to_pixel_coordinate({luv[1], luv[2]}, _scale, resize) + margin; cr->set_line_width(inner_stroke_width); @@ -1072,12 +916,11 @@ bool ColorWheelHSLuv::_set_from_xy(double const x, double const y) double const resize = std::min(width, height) / static_cast(SIZE); auto const p = from_pixel_coordinate(Geom::Point(x, y) - _getMargin(allocation), _scale, resize); - auto hsluv = Hsluv::luv_to_hsluv(_values[2], p[Geom::X], p[Geom::Y]); - bool changed = false; - changed |= setHue (hsluv[0], false); - changed |= setSaturation(hsluv[1], false); - if (changed) color_changed(); - return changed; + if (_values.set(Color(Type::LUV, Luv::fromCoordinates({_values[2] * 100, p[Geom::X], p[Geom::Y]})), true)) { + color_changed(); + return true; + } + return false; } void ColorWheelHSLuv::_updatePolygon() @@ -1115,21 +958,21 @@ void ColorWheelHSLuv::_updatePolygon() _buffer_polygon.resize(_cache_size.y() * stride / 4); std::vector buffer_line(stride / 4); - ColorPoint clr; auto const square_center = Geom::IntPoint(_square_size / 2, _square_size / 2); + std::vector color_vals = {_values[2] * 100, 0, 0}; // Set the color of each pixel/square for (int y = bounding_min[Geom::Y]; y < bounding_max[Geom::Y]; y++) { for (int x = bounding_min[Geom::X]; x < bounding_max[Geom::X]; x++) { auto pos = Geom::IntPoint(x * _square_size, y * _square_size); auto point = from_pixel_coordinate(pos + square_center - margin, _scale, resize); + color_vals[1] = point[Geom::X]; + color_vals[2] = point[Geom::Y]; - auto rgb = Hsluv::luv_to_rgb(_values[2], point[Geom::X], point[Geom::Y]); // safe with _values[2] == 0 - clr.set_color(rgb); - + auto color = Color(Type::LUV, Luv::fromCoordinates(color_vals)); guint32 *p = buffer_line.data() + (x * _square_size); for (int i = 0; i < _square_size; i++) { - p[i] = clr.get_color(); + p[i] = color.toARGB(); } } @@ -1195,29 +1038,29 @@ bool ColorWheelHSLuv::on_key_pressed(GtkEventControllerKey const * /*controller* nullptr); // Get current point - auto luv = Hsluv::hsluv_to_luv(_values.data()); + auto luv = *_values.converted(Type::LUV); double const marker_move = 1.0 / _scale; switch (key) { case GDK_KEY_Up: case GDK_KEY_KP_Up: - luv[2] += marker_move; + luv.set(2, luv[2] + marker_move); consumed = true; break; case GDK_KEY_Down: case GDK_KEY_KP_Down: - luv[2] -= marker_move; + luv.set(2, luv[2] - marker_move); consumed = true; break; case GDK_KEY_Left: case GDK_KEY_KP_Left: - luv[1] -= marker_move; + luv.set(1, luv[1] - marker_move); consumed = true; break; case GDK_KEY_Right: case GDK_KEY_KP_Right: - luv[1] += marker_move; + luv.set(1, luv[1] + marker_move); consumed = true; } @@ -1225,45 +1068,31 @@ bool ColorWheelHSLuv::on_key_pressed(GtkEventControllerKey const * /*controller* _adjusting = true; - auto const hsluv = Hsluv::luv_to_hsluv(luv[0], luv[1], luv[2]); - bool changed = false; - changed |= setHue (hsluv[0], false); - changed |= setSaturation(hsluv[1], false); - if (changed) color_changed(); + if (_values.set(luv, true)) + color_changed(); return true; } /* ColorPoint */ ColorPoint::ColorPoint() - : x(0), y(0), r(0), g(0), b(0) + : x(0) + , y(0) + , color(0x0) {} -ColorPoint::ColorPoint(double x, double y, double r, double g, double b) - : x(x), y(y), r(r), g(g), b(b) +ColorPoint::ColorPoint(double x, double y, Color c) + : x(x) + , y(y) + , color(std::move(c)) {} -ColorPoint::ColorPoint(double x, double y, guint color) +ColorPoint::ColorPoint(double x, double y, guint c) : x(x) , y(y) - , r(((color & 0xff0000) >> 16) / 255.0) - , g(((color & 0x00ff00) >> 8) / 255.0) - , b(((color & 0x0000ff) ) / 255.0) + , color(c) {} -guint32 ColorPoint::get_color() const -{ - return (static_cast(r * 255) << 16 | - static_cast(g * 255) << 8 | - static_cast(b * 255) - ); -}; - -std::pair ColorPoint::get_xy() const -{ - return {x, y}; -} - static double lerp(double v0, double v1, double t0, double t1, double t) { double const s = (t0 != t1) ? (t - t0) / (t1 - t0) : 0.0; @@ -1275,61 +1104,21 @@ static ColorPoint lerp(ColorPoint const &v0, ColorPoint const &v1, double t0, do { double x = lerp(v0.x, v1.x, t0, t1, t); double y = lerp(v0.y, v1.y, t0, t1, t); - double r = lerp(v0.r, v1.r, t0, t1, t); - double g = lerp(v0.g, v1.g, t0, t1, t); - double b = lerp(v0.b, v1.b, t0, t1, t); - - return ColorPoint(x, y, r, g, b); -} -/** - * @param h Hue. Between 0 and 1. - * @param s Saturation. Between 0 and 1. - * @param v Value. Between 0 and 1. - */ -static guint32 hsv_to_rgb(double h, double s, double v) -{ - h = std::clamp(h, 0.0, 1.0); - s = std::clamp(s, 0.0, 1.0); - v = std::clamp(v, 0.0, 1.0); - - double r = v; - double g = v; - double b = v; - - if (s != 0.0) { - if (h == 1.0) h = 0.0; - h *= 6.0; - - double f = h - (int)h; - double p = v * (1.0 - s); - double q = v * (1.0 - s * f); - double t = v * (1.0 - s * (1.0 - f)); - - switch (static_cast(h)) { - case 0: r = v; g = t; b = p; break; - case 1: r = q; g = v; b = p; break; - case 2: r = p; g = v; b = t; break; - case 3: r = p; g = q; b = v; break; - case 4: r = t; g = p; b = v; break; - case 5: r = v; g = p; b = q; break; - default: g_assert_not_reached(); - } - } + auto r0 = *v0.color.converted(Type::RGB); + auto r1 = *v1.color.converted(Type::RGB); + double r = lerp(r0[0], r1[0], t0, t1, t); + double g = lerp(r0[1], r1[1], t0, t1, t); + double b = lerp(r0[2], r1[2], t0, t1, t); - guint32 rgb = (static_cast(floor(r * 255 + 0.5)) << 16) | - (static_cast(floor(g * 255 + 0.5)) << 8) | - (static_cast(floor(b * 255 + 0.5)) ); - return rgb; + return ColorPoint(x, y, Color(Type::RGB, {r, g, b})); } -// N.B. We also have util:get_luminance(), but that uses different weightings..! -double luminance(guint32 color) +// N.B. We also have Color:get_perceptual_lightness(), but that uses different weightings..! +double luminance(Color const &color) { - double r = ((color & 0xff0000) >> 16) / 255.0; - double g = ((color & 0xff00) >> 8) / 255.0; - double b = ((color & 0xff) ) / 255.0; - return (r * 0.2125 + g * 0.7154 + b * 0.0721); + auto c = *color.converted(Type::RGB); + return (c[0] * 0.2125 + c[1] * 0.7154 + c[2] * 0.0721); } /** @@ -1427,9 +1216,9 @@ void draw_vertical_padding(ColorPoint p0, ColorPoint p1, int padding, bool pad_u // paint the padding vertically above or below this point for (int offset = 0; offset <= padding; ++offset) { if (pad_upwards && (point.y - offset) >= 0) { - *(p - (offset * stride)) = point.get_color(); + *(p - (offset * stride)) = point.color.toARGB(); } else if (!pad_upwards && (point.y + offset) < height) { - *(p + (offset * stride)) = point.get_color(); + *(p + (offset * stride)) = point.color.toARGB(); } } ++p; diff --git a/src/ui/widget/ink-color-wheel.h b/src/ui/widget/ink-color-wheel.h index 57aa0135ed..3949e96ab5 100644 --- a/src/ui/widget/ink-color-wheel.h +++ b/src/ui/widget/ink-color-wheel.h @@ -28,8 +28,7 @@ #include #include // Gtk::EventSequenceState -#include "hsluv.h" - +#include "colors/color.h" #include "ui/widget/widget-vfuncs-class-init.h" // for focus namespace Gtk { @@ -37,6 +36,10 @@ class DrawingArea; class GestureClick; } // namespace Gtk +namespace Inkscape::Colors { +class Color; +} // namespace Inkscape::Colors + namespace Inkscape::UI::Widget { class Bin; @@ -44,25 +47,15 @@ class Bin; struct ColorPoint final { ColorPoint(); - ColorPoint(double x, double y, double r, double g, double b); + ColorPoint(double x, double y, Colors::Color color); ColorPoint(double x, double y, guint color); - guint32 get_color() const; - std::pair get_xy() const; - - void set_color(Hsluv::Triplet const &rgb) - { - r = rgb[0]; - g = rgb[1]; - b = rgb[2]; - } + std::pair get_xy() const { return {x, y}; } // eurgh! double x; double y; - double r; - double g; - double b; + Colors::Color color; }; /** @@ -72,38 +65,23 @@ struct ColorPoint final class ColorWheel : public Gtk::AspectFrame { public: - ColorWheel(); + ColorWheel(Colors::Space::Type type, std::vector initial_color); /// Set the RGB of the wheel. If @a emit is true & hue changes, we call color_changed() for you - /// @param r the red component, from 0.0 to 1.0 - /// @param g the green component, from 0.0 to 1.0 - /// @param b the blue component, from 0.0 to 1.0 /// @param overrideHue whether to set hue to 0 if min==max(r,g,b) – only used by ColorwheelHSL
 /// @param emit false if you want to manually call color_changed() e.g. to avoid multiple emits /// @return whether or not the value actually changed, to enable avoiding redraw if it does not - virtual bool setRgb(double r, double g, double b, + virtual bool setColor(Colors::Color const &color, bool overrideHue = true, bool emit = true) = 0; - virtual void getRgb(double *r, double *g, double *b) const = 0; - virtual void getRgbV(double *rgb) const = 0; - virtual guint32 getRgb() const = 0; - - /// Set the hue of the wheel. If @a emit is true & hue changes, we call color_changed() for you - /// @param emit false if you want to manually call color_changed() e.g. to avoid multiple emits - /// @return whether or not the value actually changed, to enable avoiding redraw if it does not - virtual bool setHue (double h, bool emit = true); - /// Ditto setHue(), but changes the saturation instead. - virtual bool setSaturation(double s, bool emit = true); - /// Ditto setHue(), but changes the lightness instead. - virtual bool setLightness (double l, bool emit = true); + virtual Colors::Color getColor() const { return _values; } - void getValues(double *a, double *b, double *c) const; bool isAdjusting() const { return _adjusting; } /// Connect a slot to be called after the color has changed. sigc::connection connect_color_changed(sigc::slot); protected: - std::array _values = {}; + Colors::Color _values; bool _adjusting = false; /// Call when color has changed! Emits signal_color_changed & calls _drawing_area->queue_draw() @@ -143,18 +121,9 @@ class ColorWheelHSL , public ColorWheel { public: - ColorWheelHSL() - : Glib::ObjectBase{"ColorWheelHSL"} - , WidgetVfuncsClassInit{} {} - bool setHue (double h, bool emit = true) final; - bool setSaturation(double s, bool emit = true) final; - bool setLightness (double l, bool emit = true) final; - bool setRgb(double r, double g, double b, + ColorWheelHSL(); + bool setColor(Colors::Color const &color, bool overrideHue = true, bool emit = true) override; - void getRgb(double *r, double *g, double *b) const override; - void getRgbV(double *rgb) const override; - guint32 getRgb() const override; - void getHsl(double *h, double *s, double *l) const; private: void on_drawing_area_size(int width, int height, int baseline) override; @@ -201,6 +170,16 @@ private: [[nodiscard]] TriangleCorners update_triangle_source(); }; +/** + * Used to represent the in RGB gamut colors polygon of the HSLuv color wheel. + */ +struct PickerGeometry { + std::vector vertices; ///< Vertices, in counter-clockwise order. + double outer_circle_radius; ///< Smallest circle with center at origin such that polygon fits inside. + double inner_circle_radius; ///< Largest circle with center at origin such that it fits inside polygon. +}; + + /** * @class ColorWheelHSLuv */ @@ -210,16 +189,9 @@ public: ColorWheelHSLuv(); /// See base doc & N.B. that overrideHue is unused by this class - bool setRgb(double r, double g, double b, + bool setColor(Colors::Color const &color, bool overrideHue = true, bool emit = true) override; - void getRgb(double *r, double *g, double *b) const override; - void getRgbV(double *rgb) const override; - guint32 getRgb() const override; - - bool setHsluv(double h, double s, double l); - bool setLightness(double l, bool emit) final; - void getHsluv(double *h, double *s, double *l) const; void updateGeometry(); private: @@ -239,7 +211,7 @@ private: unsigned keyval, unsigned keycode, GdkModifierType state) final; double _scale = 1.0; - std::unique_ptr _picker_geometry; + std::unique_ptr _picker_geometry; std::vector _buffer_polygon; Cairo::RefPtr<::Cairo::ImageSurface> _surface_polygon; Geom::IntPoint _cache_size; diff --git a/src/ui/widget/layer-selector.cpp b/src/ui/widget/layer-selector.cpp index f96d680c41..fbe6092d34 100644 --- a/src/ui/widget/layer-selector.cpp +++ b/src/ui/widget/layer-selector.cpp @@ -157,14 +157,14 @@ void LayerSelector::_layerModified() if (active) { _layer_label.set_text(_layer->defaultLabel()); - color_str = SPColor(_layer->highlight_color()).toString(); + color_str = _layer->highlight_color().converted(Colors::Space::Type::RGB)->toString(false); } else { _layer_label.set_markup(_layer ? "[root]" : "nothing"); } + auto css = Glib::ustring::compose("#%1.%2 label { border-color: %3; }", cssName, getThisCssClass(), color_str); // Other border properties are set in share/ui/style.css - _label_style->load_from_data(Glib::ustring::compose("#%1.%2 label { border-color: %3; }", - cssName, getThisCssClass(), color_str)); + _label_style->load_from_data(css); _hide_layer_connection.block(); _lock_layer_connection.block(); diff --git a/src/ui/widget/objects-dialog-cells.cpp b/src/ui/widget/objects-dialog-cells.cpp index 7e6e921cab..abeb49ec12 100644 --- a/src/ui/widget/objects-dialog-cells.cpp +++ b/src/ui/widget/objects-dialog-cells.cpp @@ -9,11 +9,12 @@ * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ +#include "colors/color.h" +#include "display/cairo-utils.h" #include "ui/widget/objects-dialog-cells.h" #include -#include "color-rgba.h" #include "preferences.h" namespace Inkscape::UI::Widget { @@ -46,8 +47,8 @@ void ColorTagRenderer::snapshot_vfunc(Glib::RefPtr const &snapsho { auto const cr = snapshot->append_cairo(cell_area); cr->rectangle(cell_area.get_x(), cell_area.get_y(), cell_area.get_width(), cell_area.get_height()); - ColorRGBA color(_property_color.get_value()); - cr->set_source_rgb(color[0], color[1], color[2]); + auto color = Colors::Color(_property_color.get_value()); // RGBA + ink_cairo_set_source_color(cr, color); cr->fill(); if (_property_hover.get_value()) { diff --git a/src/ui/widget/oklab-color-wheel.cpp b/src/ui/widget/oklab-color-wheel.cpp index d4b1538391..81174e5d60 100644 --- a/src/ui/widget/oklab-color-wheel.cpp +++ b/src/ui/widget/oklab-color-wheel.cpp @@ -17,66 +17,36 @@ #include #include +#include "colors/spaces/enum.h" +#include "colors/spaces/oklch.h" +#include "colors/utils.h" #include "display/cairo-utils.h" -#include "oklab.h" + +using namespace Inkscape::Colors; namespace Inkscape::UI::Widget { OKWheel::OKWheel() -{ - // Set to black - _values[H] = 0; - _values[S] = 0; - _values[L] = 0; -} + : ColorWheel(Space::Type::OKHSL, {0, 0, 0, 1}) +{} -bool OKWheel::setRgb(double r, double g, double b, +bool OKWheel::setColor(Color const &color, bool /*overrideHue*/, bool const emit) { - using namespace Oklab; - - auto [h, s, l] = oklab_to_okhsl(rgb_to_oklab({ r, g, b })); - - auto const h2 = h * 2.0 * M_PI; - bool const changed_hue = _values[H] != h2; - _values[H] = h2; - - bool const changed_saturation = _values[S] != s; - _values[S] = s; - - bool const changed_lightness = _values[L] != l; - _values[L] = l; - - if (changed_lightness) { + if (_values.set(color, true)) { _updateChromaBounds(); _redrawDisc(); queue_drawing_area_draw(); + if (emit) + color_changed(); + return true; } - - bool const changed = changed_hue || changed_saturation || changed_lightness; - if (changed && emit) color_changed(); - return changed; + return false; } -void OKWheel::getRgb(double *red, double *green, double *blue) const +Color OKWheel::getColor() const { - using namespace Oklab; - auto [r, g, b] = oklab_to_rgb(okhsl_to_oklab({ _values[H] / (2.0 * M_PI), _values[S], _values[L] })); - *red = r; - *green = g; - *blue = b; -} - -guint32 OKWheel::getRgb() const -{ - guint32 result = 0x0; - double rgb[3]; - getRgbV(rgb); - for (auto component : rgb) { - result <<= 8; - result |= SP_COLOR_F_TO_U(component); - } - return result; + return _values; } /** @brief Compute the chroma bounds around the picker disc. @@ -90,7 +60,7 @@ void OKWheel::_updateChromaBounds() double const angle_step = 360.0 / CHROMA_BOUND_SAMPLES; double hue_angle_deg = 0.0; for (unsigned i = 0; i < CHROMA_BOUND_SAMPLES; i++) { - _bounds[i] = Oklab::max_chroma(_values[L], hue_angle_deg); + _bounds[i] = Space::OkLch::max_chroma(_values[L], hue_angle_deg); hue_angle_deg += angle_step; } } @@ -127,13 +97,9 @@ bool OKWheel::_updateDimensions() */ uint32_t OKWheel::_discColor(Geom::Point const &point) const { - using namespace Oklab; - using Display::AssembleARGB32; - double saturation = point.length(); if (saturation == 0.0) { - auto [r, g, b] = oklab_to_rgb({ _values[L], 0.0, 0.0 }); - return AssembleARGB32(0xFF, (guint)(r * 255.5), (guint)(g * 255.5), (guint)(b * 255.5)); + return Color(Space::Type::OKLCH, {_values[L], 0, 0}).toARGB(); } else if (saturation > 1.0) { saturation = 1.0; } @@ -152,8 +118,7 @@ uint32_t OKWheel::_discColor(Geom::Point const &point) const double const chroma_bound_estimate = Geom::lerp(t, _bounds[previous_sample], _bounds[next_sample]); double const absolute_chroma = chroma_bound_estimate * saturation; - auto [r, g, b] = oklab_to_rgb(oklch_radians_to_oklab({ _values[L], absolute_chroma, hue_radians })); - return AssembleARGB32(0xFF, (guint)(r * 255.5), (guint)(g * 255.5), (guint)(b * 255.5)); + return Color(Space::Type::OKLCH, {_values[L], absolute_chroma, Geom::deg_from_rad(hue_radians) / 360}).toARGB(); } /** @brief Returns the position of the current color in the coordinates @@ -165,7 +130,7 @@ uint32_t OKWheel::_discColor(Geom::Point const &point) const Geom::Point OKWheel::_curColorWheelCoords() const { Geom::Point result; - Geom::sincos(_values[H], result.y(), result.x()); + Geom::sincos(Geom::Angle::from_degrees(_values[H] * 360), result.y(), result.x()); result *= _values[S]; return result * Geom::Scale(_disc_radius, -_disc_radius); } @@ -198,15 +163,13 @@ void OKWheel::on_drawing_area_draw(Cairo::RefPtr const &cr, int, cr->close_path(); // Fill the halo with the current color. { - double r, g, b; - getRgb(&r, &g, &b); - cr->set_source_rgba(r, g, b, 1.0); + ink_cairo_set_source_color(cr, getColor()); } cr->fill_preserve(); // Stroke the border of the halo. { - auto [gray, alpha] = Hsluv::get_contrasting_color(_values[L]); + auto [gray, alpha] = get_contrasting_color(_values[L]); cr->set_source_rgba(gray, gray, gray, alpha); } cr->set_line_width(HALO_STROKE); @@ -257,18 +220,17 @@ Geom::Point OKWheel::_event2abstract(Geom::Point const &event_pt) const */ bool OKWheel::_setColor(Geom::Point const &pt, bool const emit) { - auto const s = std::clamp(pt.length(), 0.0, 1.0); - Geom::Angle clicked_hue = _values[S] ? Geom::atan2(pt) : 0.0; - auto const h = clicked_hue.radians0(); - bool const changed = _values[S] != s || _values[H] != h; - if (changed) { - _values[S] = s; - _values[H] = h; - if (emit) color_changed(); + bool s = _values.set(S, pt.length()); + bool h = _values.set(H, Geom::deg_from_rad(clicked_hue.radians0()) / 360); + if (s || h) { + _values.normalize(); + if (emit) + color_changed(); + return true; } - return changed; + return false; } /** @brief Handle a left mouse click on the widget. diff --git a/src/ui/widget/oklab-color-wheel.h b/src/ui/widget/oklab-color-wheel.h index db91196d69..0662e58380 100644 --- a/src/ui/widget/oklab-color-wheel.h +++ b/src/ui/widget/oklab-color-wheel.h @@ -29,13 +29,10 @@ public: /** @brief Set the displayed color to the specified gamma-compressed sRGB color. */ /// See base doc & N.B. that overrideHue is unused by this class - bool setRgb(double r, double g, double b, + bool setColor(Colors::Color const &color, bool overrideHue = true, bool emit = true) override; - /** @brief Get the gamma-compressed sRGB color from the picker wheel. */ - void getRgb(double *r, double *g, double *b) const override; - void getRgbV(double *rgb) const override { getRgb(rgb, rgb + 1, rgb + 2); } - guint32 getRgb() const override; + Colors::Color getColor() const override; private: void on_drawing_area_draw(Cairo::RefPtr const &cr, int, int) override; diff --git a/src/ui/widget/page-properties.cpp b/src/ui/widget/page-properties.cpp index d896889708..e473677025 100644 --- a/src/ui/widget/page-properties.cpp +++ b/src/ui/widget/page-properties.cpp @@ -157,27 +157,16 @@ public: , _linked_viewbox_scale (get_widget (_builder, "linked-scale-img")) , _display_units (get_derived_widget (_builder, "display-units")) , _page_units (get_derived_widget (_builder, "page-units")) + , _backgnd_color_picker (get_derived_widget (_builder, "background-color", _("Background color"), false)) + , _border_color_picker (get_derived_widget (_builder, "border-color", _("Border and shadow color"), true)) + , _desk_color_picker (get_derived_widget (_builder, "desk-color", _("Desk color"), false)) // clang-format-on { - _backgnd_color_picker = std::make_unique( - _("Background color"), "", 0xffffff00, true, - &get_widget(_builder, "background-color")); - _backgnd_color_picker->use_transparency(false); - - _border_color_picker = std::make_unique( - _("Border and shadow color"), "", 0x0000001f, true, - &get_widget(_builder, "border-color")); - - _desk_color_picker = std::make_unique( - _("Desk color"), "", 0xd0d0d0ff, true, - &get_widget(_builder, "desk-color")); - _desk_color_picker->use_transparency(false); - for (auto element : {Color::Background, Color::Border, Color::Desk}) { - get_color_picker(element).connectChanged([=](unsigned const rgba) { - update_preview_color(element, rgba); + get_color_picker(element).connectChanged([=](Colors::Color const &color) { + update_preview_color(element, color); if (_update.pending()) return; - _signal_color_changed.emit(rgba, element); + _signal_color_changed.emit(color, element); }); } @@ -275,11 +264,11 @@ private: } } - void update_preview_color(Color const element, unsigned const rgba) { + void update_preview_color(Color const element, Colors::Color const &color) { switch (element) { - case Color::Desk: _preview->set_desk_color(rgba); break; - case Color::Border: _preview->set_border_color(rgba); break; - case Color::Background: _preview->set_page_color(rgba); break; + case Color::Desk: _preview->set_desk_color(color.toRGBA()); break; + case Color::Border: _preview->set_border_color(color.toRGBA()); break; + case Color::Background: _preview->set_page_color(color.toRGBA()); break; } } @@ -423,10 +412,10 @@ private: _signal_unit_changed.emit(new_unit, Units::Document); } - void set_color(Color element, unsigned int color) override { + void set_color(Color element, Colors::Color const &color) override { auto scoped(_update.block()); - get_color_picker(element).setRgba32(color); + get_color_picker(element).setColor(color); update_preview_color(element, color); } @@ -481,9 +470,9 @@ private: ColorPicker& get_color_picker(Color element) { switch (element) { - case Color::Background: return *_backgnd_color_picker; - case Color::Desk: return *_desk_color_picker; - case Color::Border: return *_border_color_picker; + case Color::Background: return _backgnd_color_picker; + case Color::Desk: return _desk_color_picker; + case Color::Border: return _border_color_picker; default: throw std::runtime_error("missing case in get_color_picker"); @@ -566,9 +555,9 @@ private: MathSpinButton &_viewbox_y; MathSpinButton &_viewbox_width; MathSpinButton &_viewbox_height; - std::unique_ptr _backgnd_color_picker; - std::unique_ptr _border_color_picker; - std::unique_ptr _desk_color_picker; + ColorPicker &_backgnd_color_picker; + ColorPicker &_border_color_picker; + ColorPicker &_desk_color_picker; std::vector _page_sizes; Glib::RefPtr _template_action; Gtk::MenuButton &_templates_menu_button; diff --git a/src/ui/widget/page-properties.h b/src/ui/widget/page-properties.h index ed9a3cbe0f..0b7c0595ea 100644 --- a/src/ui/widget/page-properties.h +++ b/src/ui/widget/page-properties.h @@ -20,6 +20,7 @@ namespace Inkscape { +namespace Colors { class Color; } namespace Util { class Unit; } namespace UI::Widget { @@ -29,7 +30,7 @@ public: static PageProperties* create(); enum class Color { Background, Desk, Border }; - virtual void set_color(Color element, unsigned int rgba) = 0; + virtual void set_color(Color element, Colors::Color const &) = 0; enum class Check { Checkerboard, Border, Shadow, BorderOnTop, AntiAlias, NonuniformScale, DisabledScale, UnsupportedSize, ClipToPage, PageLabelStyle }; @@ -48,7 +49,7 @@ public: auto signal_resize_to_fit () { return _signal_resize_to_fit ; } protected: - sigc::signal _signal_color_changed; + sigc::signal _signal_color_changed; sigc::signal _signal_check_toggled; sigc::signal _signal_dimension_changed; sigc::signal _signal_unit_changed; diff --git a/src/ui/widget/paint-selector.cpp b/src/ui/widget/paint-selector.cpp index 8f9577d151..ef1c6a89c8 100644 --- a/src/ui/widget/paint-selector.cpp +++ b/src/ui/widget/paint-selector.cpp @@ -61,12 +61,6 @@ #include "widgets/widget-sizes.h" #include "xml/repr.h" -#ifdef SP_PS_VERBOSE -#include "svg/svg-icc-color.h" -#endif // SP_PS_VERBOSE - -using Inkscape::UI::SelectedColor; - #ifdef SP_PS_VERBOSE static gchar const *modeStrings[] = { "MODE_EMPTY", @@ -139,7 +133,8 @@ GradientSelectorInterface *PaintSelector::getGradientFromData() const #define XPAD 4 #define YPAD 1 -PaintSelector::PaintSelector(FillOrStroke kind) +PaintSelector::PaintSelector(FillOrStroke kind, std::shared_ptr colors) + : _selected_colors(std::move(colors)) { set_orientation(Gtk::Orientation::VERTICAL); @@ -206,14 +201,9 @@ PaintSelector::PaintSelector(FillOrStroke kind) _frame->set_visible(true); UI::pack_start(*this, *_frame, true, true); - /* Last used color */ - _selected_color = std::make_unique(); - _updating_color = false; - - _selected_color->signal_grabbed.connect(sigc::mem_fun(*this, &PaintSelector::onSelectedColorGrabbed)); - _selected_color->signal_dragged.connect(sigc::mem_fun(*this, &PaintSelector::onSelectedColorDragged)); - _selected_color->signal_released.connect(sigc::mem_fun(*this, &PaintSelector::onSelectedColorReleased)); - _selected_color->signal_changed.connect(sigc::mem_fun(*this, &PaintSelector::onSelectedColorChanged)); + _selected_colors->signal_grabbed.connect(sigc::mem_fun(*this, &PaintSelector::onSelectedColorGrabbed)); + _selected_colors->signal_released.connect(sigc::mem_fun(*this, &PaintSelector::onSelectedColorReleased)); + _selected_colors->signal_changed.connect(sigc::mem_fun(*this, &PaintSelector::onSelectedColorChanged)); // from _new function setMode(PaintSelector::MODE_MULTIPLE); @@ -275,7 +265,7 @@ void PaintSelector::set_mode_ex(Mode mode, bool switch_style) { set_mode_none(); break; case MODE_SOLID_COLOR: - set_mode_color(mode); + set_mode_color(); break; case MODE_GRADIENT_LINEAR: case MODE_GRADIENT_RADIAL: @@ -317,20 +307,6 @@ void PaintSelector::setFillrule(FillRule fillrule) } } -void PaintSelector::setColorAlpha(SPColor const &color, float alpha) -{ - g_return_if_fail((0.0 <= alpha) && (alpha <= 1.0)); - { -#ifdef SP_PS_VERBOSE - g_print("PaintSelector set RGBA\n"); -#endif - setMode(MODE_SOLID_COLOR); - } - _updating_color = true; - _selected_color->setColorAlpha(color, alpha); - _updating_color = false; -} - void PaintSelector::setSwatch(SPGradient *vector) { #ifdef SP_PS_VERBOSE @@ -407,16 +383,6 @@ void PaintSelector::getGradientProperties(SPGradientUnits &units, SPGradientSpre } -/** - * \post (alpha == NULL) || (*alpha in [0.0, 1.0]). - */ -void PaintSelector::getColorAlpha(SPColor &color, gfloat &alpha) const -{ - _selected_color->colorAlpha(color, alpha); - - g_assert((0.0 <= alpha) && (alpha <= 1.0)); -} - SPGradient *PaintSelector::getGradientVector() { SPGradient *vect = nullptr; @@ -494,32 +460,25 @@ void PaintSelector::set_mode_none() /* Color paint */ void PaintSelector::onSelectedColorGrabbed() { _signal_grabbed.emit(); } - -void PaintSelector::onSelectedColorDragged() -{ - if (_updating_color) { - return; - } - - _signal_dragged.emit(); -} - void PaintSelector::onSelectedColorReleased() { _signal_released.emit(); } void PaintSelector::onSelectedColorChanged() { - if (_updating_color) { + if (_updating_color) return; - } if (_mode == MODE_SOLID_COLOR) { - _signal_changed.emit(); + if (_selected_colors->isGrabbed()) { + _signal_dragged.emit(); + } else { + _signal_changed.emit(); + } } else { g_warning("PaintSelector::onSelectedColorChanged(): selected color changed while not in color selection mode"); } } -void PaintSelector::set_mode_color(PaintSelector::Mode /*mode*/) +void PaintSelector::set_mode_color() { using Inkscape::UI::Widget::ColorNotebook; @@ -530,9 +489,10 @@ void PaintSelector::set_mode_color(PaintSelector::Mode /*mode*/) // Gradient can be null if object paint is changed externally (ie. with a color picker tool) if (gradient) { - SPColor color = gradient->getFirstStop()->getColor(); - float alpha = gradient->getFirstStop()->getOpacity(); - _selected_color->setColorAlpha(color, alpha, false); + _selected_colors->block(); + _selected_colors->clear(); + _selected_colors->set(gradient->getFirstStop()->getId(), gradient->getFirstStop()->getColor()); + _selected_colors->unblock(); } } } @@ -552,7 +512,7 @@ void PaintSelector::set_mode_color(PaintSelector::Mode /*mode*/) _selector_solid_color = Gtk::make_managed(Gtk::Orientation::VERTICAL, 4); /* Color selector */ - auto const color_selector = Gtk::make_managed(*_selected_color); + auto const color_selector = Gtk::make_managed(_selected_colors); color_selector->set_visible(true); UI::pack_start(*_selector_solid_color, *color_selector, true, true); /* Pack everything to frame */ @@ -989,7 +949,7 @@ void PaintSelector::set_mode_pattern(PaintSelector::Mode mode) if (!_selector_pattern) { _selector_pattern = Gtk::make_managed("/pattern-edit", PatternManager::get()); _selector_pattern->signal_changed().connect([=](){ _signal_changed.emit(); }); - _selector_pattern->signal_color_changed().connect([=](unsigned){ _signal_changed.emit(); }); + _selector_pattern->signal_color_changed().connect([=](Colors::Color const &){ _signal_changed.emit(); }); _selector_pattern->signal_edit().connect([=](){ _signal_edit_pattern.emit(); }); _frame->append(*_selector_pattern); } @@ -1031,8 +991,8 @@ gboolean PaintSelector::isSeparator(GtkTreeModel *model, GtkTreeIter *iter, gpoi return sep; } -std::optional PaintSelector::get_pattern_color() { - if (!_selector_pattern) return 0; +std::optional PaintSelector::get_pattern_color() { + if (!_selector_pattern) return Colors::Color(0x000000ff); return _selector_pattern->get_selected_color(); } @@ -1133,33 +1093,6 @@ void PaintSelector::set_mode_swatch(PaintSelector::Mode mode) #endif } -// TODO this seems very bad to be taking in a desktop pointer to muck with. Logic probably belongs elsewhere -void PaintSelector::setFlatColor(SPDesktop *desktop, gchar const *color_property, gchar const *opacity_property) -{ - SPCSSAttr *css = sp_repr_css_attr_new(); - - SPColor color; - gfloat alpha = 0; - getColorAlpha(color, alpha); - - std::string colorStr = color.toString(); - -#ifdef SP_PS_VERBOSE - guint32 rgba = color.toRGBA32(alpha); - g_message("sp_paint_selector_set_flat_color() to '%s' from 0x%08x::%s", colorStr.c_str(), rgba, - (color.icc ? color.icc->colorProfile.c_str() : "")); -#endif // SP_PS_VERBOSE - - sp_repr_css_set_property(css, color_property, colorStr.c_str()); - Inkscape::CSSOStringStream osalpha; - osalpha << alpha; - sp_repr_css_set_property(css, opacity_property, osalpha.str().c_str()); - - sp_desktop_set_style(desktop, css); - - sp_repr_css_attr_unref(css); -} - PaintSelector::Mode PaintSelector::getModeForStyle(SPStyle const &style, FillOrStroke kind) { Mode mode = MODE_UNSET; diff --git a/src/ui/widget/paint-selector.h b/src/ui/widget/paint-selector.h index 285002f678..f76eda68f6 100644 --- a/src/ui/widget/paint-selector.h +++ b/src/ui/widget/paint-selector.h @@ -21,12 +21,11 @@ #include <2geom/forward.h> #include -#include "color.h" +#include "colors/color-set.h" #include "fill-or-stroke.h" #include "gradient-selector-interface.h" #include "object/sp-gradient-spread.h" #include "object/sp-gradient-units.h" -#include "ui/selected-color.h" #include "ui/widget/gradient-selector.h" #include "ui/widget/swatch-selector.h" @@ -112,10 +111,8 @@ class PaintSelector : public Gtk::Box { bool _meshmenu_update = false; #endif - std::unique_ptr _selected_color; - bool _updating_color; - - void getColorAlpha(SPColor &color, gfloat &alpha) const; + std::shared_ptr _selected_colors; + bool _updating_color = false; static gboolean isSeparator(GtkTreeModel *model, GtkTreeIter *iter, gpointer data); @@ -133,7 +130,6 @@ class PaintSelector : public Gtk::Box { void style_button_toggled(StyleToggleButton *tb); void fillrule_toggled(FillRuleRadioButton *tb); void onSelectedColorGrabbed(); - void onSelectedColorDragged(); void onSelectedColorReleased(); void onSelectedColorChanged(); void set_mode_empty(); @@ -143,7 +139,7 @@ class PaintSelector : public Gtk::Box { GradientSelectorInterface *getGradientFromData() const; void clear_frame(); void set_mode_unset(); - void set_mode_color(PaintSelector::Mode mode); + void set_mode_color(); void set_mode_gradient(PaintSelector::Mode mode); #ifdef WITH_MESH void set_mode_mesh(PaintSelector::Mode mode); @@ -165,7 +161,7 @@ class PaintSelector : public Gtk::Box { static void pattern_destroy(GtkWidget *widget, PaintSelector *psel); public: - PaintSelector(FillOrStroke kind); + PaintSelector(FillOrStroke kind, std::shared_ptr colors); inline decltype(_signal_fillrule_changed) signal_fillrule_changed() const { return _signal_fillrule_changed; } inline decltype(_signal_dragged) signal_dragged() const { return _signal_dragged; } @@ -179,7 +175,6 @@ class PaintSelector : public Gtk::Box { void setMode(Mode mode); static Mode getModeForStyle(SPStyle const &style, FillOrStroke kind); void setFillrule(FillRule fillrule); - void setColorAlpha(SPColor const &color, float alpha); void setSwatch(SPGradient *vector); void setGradientLinear(SPGradient *vector, SPLinearGradient* gradient, SPStop* selected); void setGradientRadial(SPGradient *vector, SPRadialGradient* gradient, SPStop* selected); @@ -197,13 +192,10 @@ class PaintSelector : public Gtk::Box { void updatePatternList(SPPattern *pat); inline decltype(_mode) get_mode() const { return _mode; } - // TODO move this elsewhere: - void setFlatColor(SPDesktop *desktop, const gchar *color_property, const gchar *opacity_property); - SPGradient *getGradientVector(); void pushAttrsToGradient(SPGradient *gr) const; SPPattern *getPattern(); - std::optional get_pattern_color(); + std::optional get_pattern_color(); Geom::Affine get_pattern_transform(); Geom::Point get_pattern_offset(); Geom::Scale get_pattern_gap(); diff --git a/src/ui/widget/pattern-editor.cpp b/src/ui/widget/pattern-editor.cpp index 385fbc4069..c312cca902 100644 --- a/src/ui/widget/pattern-editor.cpp +++ b/src/ui/widget/pattern-editor.cpp @@ -91,7 +91,6 @@ PatternEditor::PatternEditor(const char* prefs, Inkscape::PatternManager& manage _edit_btn(get_widget(_builder, "edit-pattern")), _preview_img(get_widget(_builder, "preview")), _preview(get_widget(_builder, "preview-box")), - _color_btn(get_widget(_builder, "color-btn")), _color_label(get_widget(_builder, "color-label")), _paned(get_widget(_builder, "paned")), _main_grid(get_widget(_builder, "main-box")), @@ -104,13 +103,10 @@ PatternEditor::PatternEditor(const char* prefs, Inkscape::PatternManager& manage _search_box(get_widget(_builder, "search")), _tile_slider(get_widget(_builder, "tile-slider")), _show_names(get_widget(_builder, "show-names")), + _color_picker(get_derived_widget(_builder, "color-btn", _("Pattern color"), false)), _prefs(prefs) { - _color_picker = std::make_unique( - _("Pattern color"), "", 0x7f7f7f00, true, - &get_widget(_builder, "color-btn")); - _color_picker->use_transparency(false); - _color_picker->connectChanged([=](guint color){ + _color_picker.connectChanged([=](Colors::Color const &color){ if (_update.pending()) return; _signal_color_changed.emit(color); }); @@ -396,15 +392,15 @@ void PatternEditor::update_widgets_from_pattern(Glib::RefPtr& patte _gap_y_spin.set_value(item.gap[Geom::Y]); if (item.color.has_value()) { - _color_picker->setRgba32(item.color->toRGBA32(1.0)); - _color_btn.set_sensitive(); + _color_picker.setColor(*item.color); + _color_picker.set_sensitive(); _color_label.set_opacity(1.0); // hack: sensitivity doesn't change appearance, so using opacity directly } else { - _color_picker->setRgba32(0); - _color_btn.set_sensitive(false); + _color_picker.setColor(Colors::Color(0x0)); + _color_picker.set_sensitive(false); _color_label.set_opacity(0.6); - _color_picker->closeWindow(); + _color_picker.closeWindow(); } } @@ -656,12 +652,12 @@ std::pair PatternEditor::get_selected() { } } -std::optional PatternEditor::get_selected_color() { +std::optional PatternEditor::get_selected_color() { auto pat = get_active(); if (pat.first && pat.first->color.has_value()) { - return _color_picker->get_current_color(); + return _color_picker.get_current_color(); } - return std::optional(); // color not supported + return {}; // color not supported } Geom::Point PatternEditor::get_selected_offset() { diff --git a/src/ui/widget/pattern-editor.h b/src/ui/widget/pattern-editor.h index 45581d5d71..08a06a2a44 100644 --- a/src/ui/widget/pattern-editor.h +++ b/src/ui/widget/pattern-editor.h @@ -18,7 +18,6 @@ #include <2geom/point.h> #include <2geom/transforms.h> -#include "color.h" #include "object/sp-pattern.h" #include "pattern-manager.h" #include "spin-scale.h" @@ -47,6 +46,10 @@ class Viewport; class SPDocument; class ColorPicker; +namespace Inkscape::Colors { +class Color; +} + namespace Inkscape::UI::Widget { class PatternEditor : public Gtk::Box { @@ -60,7 +63,7 @@ public: // selected pattern ID if any plus stock pattern collection document (or null) std::pair get_selected(); // and its color - std::optional get_selected_color(); + std::optional get_selected_color(); // return combined scale and rotation Geom::Affine get_selected_transform(); // return pattern offset @@ -74,7 +77,7 @@ public: private: sigc::signal _signal_changed; - sigc::signal _signal_color_changed; + sigc::signal _signal_color_changed; sigc::signal _signal_edit; public: @@ -114,7 +117,6 @@ private: bool _precise_gap_control = false; Gtk::Button& _edit_btn; Gtk::Label& _color_label; - Gtk::Button& _color_btn; Gtk::Button& _link_scale; Gtk::Picture& _preview_img; Gtk::Viewport& _preview; @@ -130,7 +132,7 @@ private: Glib::ustring _prefs; PatternStore _doc_pattern_store; PatternStore _stock_pattern_store; - std::unique_ptr _color_picker; + ColorPicker& _color_picker; OperationBlocker _update; std::unordered_map> _cached_items; // cached current document patterns Inkscape::PatternManager& _manager; diff --git a/src/ui/widget/pattern-store.h b/src/ui/widget/pattern-store.h index 501f893b81..62ac04457c 100644 --- a/src/ui/widget/pattern-store.h +++ b/src/ui/widget/pattern-store.h @@ -10,9 +10,9 @@ #include #include <2geom/transforms.h> +#include "colors/color.h" #include #include -#include "color.h" #include "ui/filtered-store.h" class SPDocument; @@ -32,7 +32,7 @@ public: bool uniform_scale = false; Geom::Affine transform; Geom::Point offset; - std::optional color; + std::optional color; Geom::Scale gap; SPDocument* collection = nullptr; diff --git a/src/ui/widget/preferences-widget.cpp b/src/ui/widget/preferences-widget.cpp index 7d0ecf0d12..dbe390ee52 100644 --- a/src/ui/widget/preferences-widget.cpp +++ b/src/ui/widget/preferences-widget.cpp @@ -918,20 +918,19 @@ void PrefMultiEntry::on_changed() } void PrefColorPicker::init(Glib::ustring const &label, Glib::ustring const &prefs_path, - std::uint32_t const default_rgba) + std::string const &default_color) { _prefs_path = prefs_path; _title = label; Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - this->setRgba32( prefs->getInt(_prefs_path, (int)default_rgba) ); + this->setColor(prefs->getColor(_prefs_path, default_color)); } -void PrefColorPicker::on_changed(std::uint32_t const rgba) +void PrefColorPicker::on_changed(Inkscape::Colors::Color const &color) { - if (this->get_visible()) //only take action if the user toggled it - { + if (this->get_visible()) { //only take action if the user toggled it Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - prefs->setInt(_prefs_path, static_cast(rgba)); + prefs->setColor(_prefs_path, color); } } diff --git a/src/ui/widget/preferences-widget.h b/src/ui/widget/preferences-widget.h index 6b565e6388..99df6d1ab4 100644 --- a/src/ui/widget/preferences-widget.h +++ b/src/ui/widget/preferences-widget.h @@ -284,15 +284,15 @@ private: class PrefColorPicker : public ColorPicker { public: - PrefColorPicker() : ColorPicker("", "", 0, false) {}; + PrefColorPicker() : ColorPicker("", "", Colors::Color(0x000000ff), false) {}; ~PrefColorPicker() override = default;; void init(Glib::ustring const &abel, Glib::ustring const &prefs_path, - std::uint32_t default_rgba); + std::string const &default_color); private: Glib::ustring _prefs_path; - void on_changed(std::uint32_t rgba) override; + void on_changed(Inkscape::Colors::Color const &color) override; }; class PrefUnit : public UnitMenu diff --git a/src/ui/widget/registered-widget.cpp b/src/ui/widget/registered-widget.cpp index 1e41e67ccd..5a1f84f5e3 100644 --- a/src/ui/widget/registered-widget.cpp +++ b/src/ui/widget/registered-widget.cpp @@ -21,7 +21,6 @@ #include #include "object/sp-root.h" -#include "svg/svg-color.h" #include "svg/stringstream.h" #include "util/safe-printf.h" @@ -403,7 +402,7 @@ RegisteredColorPicker::RegisteredColorPicker(Glib::ustring const &label, Registry &wr, Inkscape::XML::Node *repr_in, SPDocument *doc_in) - : RegisteredWidget{label, title, tip, 0u, true} + : RegisteredWidget{label, title, tip, Colors::Color(0x000000ff), true} { init_parent("", wr, repr_in, doc_in); @@ -413,9 +412,9 @@ RegisteredColorPicker::RegisteredColorPicker(Glib::ustring const &label, } void -RegisteredColorPicker::setRgba32(std::uint32_t const rgba) +RegisteredColorPicker::setColor(Colors::Color const &color) { - LabelledColorPicker::setRgba32(rgba); + LabelledColorPicker::setColor(color); } void @@ -425,7 +424,7 @@ RegisteredColorPicker::closeWindow() } void -RegisteredColorPicker::on_changed(std::uint32_t const rgba) +RegisteredColorPicker::on_changed(Colors::Color const &color) { if (_wr->isUpdating()) return; @@ -445,21 +444,13 @@ RegisteredColorPicker::on_changed(std::uint32_t const rgba) local_repr = dt->getNamedView()->getRepr(); local_doc = dt->getDocument(); } - gchar c[32]; - if (_akey == _ckey + "_opacity_LPE") { //For LPE parameter we want stored with alpha - safeprintf(c, "#%08x", rgba); - } else { - sp_svg_write_color(c, sizeof(c), rgba); - } + bool alpha = _akey == _ckey + "_opacity_LPE"; //For LPE parameter we want stored with alpha + auto c = color.toString(alpha); + { DocumentUndo::ScopedInsensitive _no_undo(local_doc); - if (_setter) { - _setter(local_repr, rgba); - } - else { - local_repr->setAttribute(_ckey, c); - local_repr->setAttributeCssDouble(_akey.c_str(), (rgba & 0xff) / 255.0); - } + local_repr->setAttribute(_ckey, c); + local_repr->setAttributeCssDouble(_akey.c_str(), color.getOpacity()); } local_doc->setModifiedSinceSave(); DocumentUndo::done(local_doc, "registered-widget.cpp: RegisteredColorPicker::on_changed", ""); // TODO Fix description. diff --git a/src/ui/widget/registered-widget.h b/src/ui/widget/registered-widget.h index 40ade53d21..2de4958cf1 100644 --- a/src/ui/widget/registered-widget.h +++ b/src/ui/widget/registered-widget.h @@ -287,15 +287,15 @@ public: Inkscape::XML::Node *repr_in = nullptr, SPDocument *doc_in = nullptr); - void setRgba32(std::uint32_t); + void setColor(Colors::Color const &); void closeWindow(); - void setCustomSetter(std::function setter) { _setter = std::move(setter); } + void setCustomSetter(std::function setter) { _setter = std::move(setter); } private: Glib::ustring _ckey, _akey; auto_connection _changed_connection; - std::function _setter; - void on_changed(std::uint32_t); + std::function _setter; + void on_changed(Colors::Color const &); }; class RegisteredInteger : public RegisteredWidget { diff --git a/src/ui/widget/selected-style.cpp b/src/ui/widget/selected-style.cpp index e0781d1005..49b881bfaa 100644 --- a/src/ui/widget/selected-style.cpp +++ b/src/ui/widget/selected-style.cpp @@ -24,6 +24,8 @@ #include "selected-style.h" +#include "colors/dragndrop.h" +#include "colors/manager.h" #include "desktop-style.h" #include "document-undo.h" #include "gradient-chemistry.h" @@ -38,7 +40,6 @@ #include "object/sp-pattern.h" #include "object/sp-radial-gradient.h" #include "svg/css-ostringstream.h" -#include "svg/svg-color.h" #include "ui/controller.h" #include "ui/clipboard.h" #include "ui/cursor-utils.h" @@ -54,7 +55,6 @@ #include "util/safe-printf.h" #include "util/units.cpp" #include "util-string/ustring-format.h" -#include "widgets/paintdef.h" static constexpr int SELECTED_STYLE_SB_WIDTH = 48; static constexpr int SELECTED_STYLE_PLACE_WIDTH = 50; @@ -171,28 +171,18 @@ SelectedStyle::SelectedStyle() drop[i] = std::make_unique(); drop[i]->parent = this; drop[i]->item = i; - auto target = Gtk::DropTarget::create(Glib::Value::value_type(), Gdk::DragAction::COPY | Gdk::DragAction::MOVE); + auto target = Gtk::DropTarget::create(Glib::Value>::value_type(), Gdk::DragAction::COPY | Gdk::DragAction::MOVE); target->signal_drop().connect([this, i] (Glib::ValueBase const &value, double, double) { if (!dropEnabled[i]) { return false; } auto const &tracker = *drop[i]; - auto const &paintdef = *reinterpret_cast(g_value_get_boxed(value.gobj())); - - // copied from drag-and-drop.cpp, case PaintDef - std::string colorspec; - if (paintdef.get_type() == PaintDef::NONE) { - colorspec = "none"; - } else { - auto const [r, g, b] = paintdef.get_rgb(); - colorspec.resize(63); - sp_svg_write_color(colorspec.data(), colorspec.size() + 1, SP_RGBA32_U_COMPOSE(r, g, b, 0xff)); - colorspec.resize(std::strlen(colorspec.c_str())); - } + auto const &color = *reinterpret_cast*>(g_value_get_boxed(value.gobj())); + std::string colorspec = color ? color->toString(false) : "none"; auto const css = sp_repr_css_attr_new(); - sp_repr_css_set_property(css, tracker.item == SS_FILL ? "fill" : "stroke", colorspec.c_str()); + sp_repr_css_set_property_string(css, tracker.item == SS_FILL ? "fill" : "stroke", colorspec); sp_desktop_set_style(tracker.parent->_desktop, css); sp_repr_css_attr_unref(css); @@ -325,10 +315,8 @@ void SelectedStyle::on_stroke_opaque() { void SelectedStyle::on_fill_lastused() { SPCSSAttr *css = sp_repr_css_attr_new (); - guint32 color = sp_desktop_get_color(_desktop, true); - gchar c[64]; - sp_svg_write_color (c, sizeof(c), color); - sp_repr_css_set_property (css, "fill", c); + auto color = sp_desktop_get_color(_desktop, true); + sp_repr_css_set_property_string(css, "fill", color ? color->toString() : "none"); sp_desktop_set_style (_desktop, css); sp_repr_css_attr_unref (css); DocumentUndo::done(_desktop->getDocument(), _("Apply last set color to fill"), INKSCAPE_ICON("dialog-fill-and-stroke")); @@ -336,10 +324,8 @@ void SelectedStyle::on_fill_lastused() { void SelectedStyle::on_stroke_lastused() { SPCSSAttr *css = sp_repr_css_attr_new (); - guint32 color = sp_desktop_get_color(_desktop, false); - gchar c[64]; - sp_svg_write_color (c, sizeof(c), color); - sp_repr_css_set_property (css, "stroke", c); + auto color = sp_desktop_get_color(_desktop, false); + sp_repr_css_set_property_string(css, "fill", color ? color->toString() : "none"); sp_desktop_set_style (_desktop, css); sp_repr_css_attr_unref (css); DocumentUndo::done(_desktop->getDocument(), _("Apply last set color to stroke"), INKSCAPE_ICON("dialog-fill-and-stroke")); @@ -347,9 +333,7 @@ void SelectedStyle::on_stroke_lastused() { void SelectedStyle::on_fill_lastselected() { SPCSSAttr *css = sp_repr_css_attr_new (); - gchar c[64]; - sp_svg_write_color (c, sizeof(c), _lastselected[SS_FILL]); - sp_repr_css_set_property (css, "fill", c); + sp_repr_css_set_property_string(css, "fill", _lastselected[SS_FILL] ? _lastselected[SS_FILL]->toString() : ""); sp_desktop_set_style (_desktop, css); sp_repr_css_attr_unref (css); DocumentUndo::done(_desktop->getDocument(), _("Apply last selected color to fill"), INKSCAPE_ICON("dialog-fill-and-stroke")); @@ -357,9 +341,7 @@ void SelectedStyle::on_fill_lastselected() { void SelectedStyle::on_stroke_lastselected() { SPCSSAttr *css = sp_repr_css_attr_new (); - gchar c[64]; - sp_svg_write_color (c, sizeof(c), _lastselected[SS_STROKE]); - sp_repr_css_set_property (css, "stroke", c); + sp_repr_css_set_property_string(css, "stroke", _lastselected[SS_STROKE] ? _lastselected[SS_STROKE]->toString() : ""); sp_desktop_set_style (_desktop, css); sp_repr_css_attr_unref (css); DocumentUndo::done(_desktop->getDocument(), _("Apply last selected color to stroke"), INKSCAPE_ICON("dialog-fill-and-stroke")); @@ -367,8 +349,7 @@ void SelectedStyle::on_stroke_lastselected() { void SelectedStyle::on_fill_invert() { SPCSSAttr *css = sp_repr_css_attr_new (); - guint32 color = _thisselected[SS_FILL]; - gchar c[64]; + auto color = _thisselected[SS_FILL]; if (_mode[SS_FILL] == SS_LGRADIENT || _mode[SS_FILL] == SS_RGRADIENT) { sp_gradient_invert_selected_gradients(_desktop, Inkscape::FOR_FILL); return; @@ -376,15 +357,8 @@ void SelectedStyle::on_fill_invert() { } if (_mode[SS_FILL] != SS_COLOR) return; - sp_svg_write_color (c, sizeof(c), - SP_RGBA32_U_COMPOSE( - (255 - SP_RGBA32_R_U(color)), - (255 - SP_RGBA32_G_U(color)), - (255 - SP_RGBA32_B_U(color)), - SP_RGBA32_A_U(color) - ) - ); - sp_repr_css_set_property (css, "fill", c); + color->invert(); + sp_repr_css_set_property_string(css, "fill", color->toString()); sp_desktop_set_style (_desktop, css); sp_repr_css_attr_unref (css); DocumentUndo::done(_desktop->getDocument(), _("Invert fill"), INKSCAPE_ICON("dialog-fill-and-stroke")); @@ -392,22 +366,14 @@ void SelectedStyle::on_fill_invert() { void SelectedStyle::on_stroke_invert() { SPCSSAttr *css = sp_repr_css_attr_new (); - guint32 color = _thisselected[SS_STROKE]; - gchar c[64]; + auto color = _thisselected[SS_STROKE]; if (_mode[SS_STROKE] == SS_LGRADIENT || _mode[SS_STROKE] == SS_RGRADIENT) { sp_gradient_invert_selected_gradients(_desktop, Inkscape::FOR_STROKE); return; } if (_mode[SS_STROKE] != SS_COLOR) return; - sp_svg_write_color (c, sizeof(c), - SP_RGBA32_U_COMPOSE( - (255 - SP_RGBA32_R_U(color)), - (255 - SP_RGBA32_G_U(color)), - (255 - SP_RGBA32_B_U(color)), - SP_RGBA32_A_U(color) - ) - ); - sp_repr_css_set_property (css, "stroke", c); + color->invert(); + sp_repr_css_set_property_string(css, "stroke", color->toString()); sp_desktop_set_style (_desktop, css); sp_repr_css_attr_unref (css); DocumentUndo::done(_desktop->getDocument(), _("Invert stroke"), INKSCAPE_ICON("dialog-fill-and-stroke")); @@ -415,9 +381,7 @@ void SelectedStyle::on_stroke_invert() { void SelectedStyle::on_fill_white() { SPCSSAttr *css = sp_repr_css_attr_new (); - gchar c[64]; - sp_svg_write_color (c, sizeof(c), 0xffffffff); - sp_repr_css_set_property (css, "fill", c); + sp_repr_css_set_property (css, "fill", "#ffffff"); sp_repr_css_set_property (css, "fill-opacity", "1"); sp_desktop_set_style (_desktop, css); sp_repr_css_attr_unref (css); @@ -426,9 +390,7 @@ void SelectedStyle::on_fill_white() { void SelectedStyle::on_stroke_white() { SPCSSAttr *css = sp_repr_css_attr_new (); - gchar c[64]; - sp_svg_write_color (c, sizeof(c), 0xffffffff); - sp_repr_css_set_property (css, "stroke", c); + sp_repr_css_set_property (css, "stroke", "#ffffff"); sp_repr_css_set_property (css, "stroke-opacity", "1"); sp_desktop_set_style (_desktop, css); sp_repr_css_attr_unref (css); @@ -437,9 +399,7 @@ void SelectedStyle::on_stroke_white() { void SelectedStyle::on_fill_black() { SPCSSAttr *css = sp_repr_css_attr_new (); - gchar c[64]; - sp_svg_write_color (c, sizeof(c), 0x000000ff); - sp_repr_css_set_property (css, "fill", c); + sp_repr_css_set_property (css, "fill", "#000000"); sp_repr_css_set_property (css, "fill-opacity", "1.0"); sp_desktop_set_style (_desktop, css); sp_repr_css_attr_unref (css); @@ -448,9 +408,7 @@ void SelectedStyle::on_fill_black() { void SelectedStyle::on_stroke_black() { SPCSSAttr *css = sp_repr_css_attr_new (); - gchar c[64]; - sp_svg_write_color (c, sizeof(c), 0x000000ff); - sp_repr_css_set_property (css, "stroke", c); + sp_repr_css_set_property (css, "stroke", "#000000"); sp_repr_css_set_property (css, "stroke-opacity", "1.0"); sp_desktop_set_style (_desktop, css); sp_repr_css_attr_unref (css); @@ -459,10 +417,7 @@ void SelectedStyle::on_stroke_black() { void SelectedStyle::on_fill_copy() { if (_mode[SS_FILL] == SS_COLOR) { - gchar c[64]; - sp_svg_write_color (c, sizeof(c), _thisselected[SS_FILL]); - Glib::ustring text; - text += c; + auto text = _thisselected[SS_FILL]->toString(); if (!text.empty()) { auto const display = Gdk::Display::get_default(); display->get_primary_clipboard()->set_text(text); @@ -472,10 +427,7 @@ void SelectedStyle::on_fill_copy() { void SelectedStyle::on_stroke_copy() { if (_mode[SS_STROKE] == SS_COLOR) { - gchar c[64]; - sp_svg_write_color (c, sizeof(c), _thisselected[SS_STROKE]); - Glib::ustring text; - text += c; + auto text = _thisselected[SS_STROKE]->toString(); if (!text.empty()) { auto const display = Gdk::Display::get_default(); display->get_primary_clipboard()->set_text(text); @@ -495,13 +447,9 @@ void SelectedStyle::_on_paste_callback(Glib::RefPtr& result, G std::cout << "Pasting text failed: " << err.what() << std::endl; return; } - if (!text.empty()) { - guint32 color = sp_svg_read_color(text.c_str(), 0x000000ff); // impossible value, as SVG color cannot have opacity - if (color == 0x000000ff) // failed to parse color string - return; - + if (auto color = Inkscape::Colors::Color::parse(text)) { SPCSSAttr *css = sp_repr_css_attr_new (); - sp_repr_css_set_property (css, typepaste.c_str(), text.c_str()); + sp_repr_css_set_property_string(css, "fill", color->toString()); sp_desktop_set_style (_desktop, css); sp_repr_css_attr_unref (css); DocumentUndo::done(_desktop->getDocument(), typepaste == "fill" ? _("Paste fill") : _("Paste stroke"), INKSCAPE_ICON("dialog-fill-and-stroke")); @@ -785,6 +733,7 @@ SelectedStyle::update() dropEnabled[i] = true; auto paint = i == SS_FILL ? query.fill.upcast() : query.stroke.upcast(); + double opacity = i == SS_FILL ? query.fill_opacity : query.stroke_opacity; if (paint->set && paint->isPaintserver()) { SPPaintServer *server = (i == SS_FILL)? SP_STYLE_FILL_SERVER (&query) : SP_STYLE_STROKE_SERVER (&query); if ( server ) { @@ -835,21 +784,17 @@ SelectedStyle::update() g_warning ("file %s: line %d: Unknown paint server", __FILE__, __LINE__); } } else if (paint->set && paint->isColor()) { - guint32 color = paint->value.color.toRGBA32( - SP_SCALE24_TO_FLOAT ((i == SS_FILL) ? - query.fill_opacity.value : - query.stroke_opacity.value)); + auto color = paint->getColor(); + color.addOpacity(opacity); + _lastselected[i] = _thisselected[i]; _thisselected[i] = color; // include opacity - gchar c_string[64]; - safeprintf (c_string, "%06x/%.3g", color >> 8, SP_RGBA32_A_F(color)); - // No type_label. - swatch[i]->set_tooltip_text(type_strings[SS_COLOR][i][1] + ": " + c_string + + swatch[i]->set_tooltip_text(type_strings[SS_COLOR][i][1] + ": " + color.toString() + _(", drag to adjust, middle-click to remove")); type_label[i]->set_visible(false); - color_preview[i]->setRgba32(color); + color_preview[i]->setRgba32(color.toRGBA()); color_preview[i]->show(); _mode[i] = SS_COLOR; @@ -1021,72 +966,27 @@ RotateableSwatch::RotateableSwatch(SelectedStyle *parent, guint mode) RotateableSwatch::~RotateableSwatch() = default; -double -RotateableSwatch::color_adjust(float *hsla, double by, guint32 cc, guint modifier) +std::pair RotateableSwatch::color_adjust(Colors::Color const &cc, double by, guint modifier) { - SPColor::rgb_to_hsl_floatv (hsla, SP_RGBA32_R_F(cc), SP_RGBA32_G_F(cc), SP_RGBA32_B_F(cc)); - hsla[3] = SP_RGBA32_A_F(cc); - double diff = 0; - if (modifier == 2) { // saturation - double old = hsla[1]; - if (by > 0) { - hsla[1] += by * (1 - hsla[1]); - } else { - hsla[1] += by * (hsla[1]); - } - diff = hsla[1] - old; - } else if (modifier == 1) { // lightness - double old = hsla[2]; - if (by > 0) { - hsla[2] += by * (1 - hsla[2]); - } else { - hsla[2] += by * (hsla[2]); - } - diff = hsla[2] - old; - } else if (modifier == 3) { // alpha - double old = hsla[3]; - hsla[3] += by/2; - if (hsla[3] < 0) { - hsla[3] = 0; - } else if (hsla[3] > 1) { - hsla[3] = 1; - } - diff = hsla[3] - old; - } else { // hue - double old = hsla[0]; - hsla[0] += by/2; - while (hsla[0] < 0) - hsla[0] += 1; - while (hsla[0] > 1) - hsla[0] -= 1; - diff = hsla[0] - old; - } + static int map[4] = {0,2,1,3}; + auto hsl = *cc.converted(Colors::Space::Type::HSL); + int ch = map[modifier]; + double old = hsl[ch]; - float rgb[3]; - SPColor::hsl_to_rgb_floatv (rgb, hsla[0], hsla[1], hsla[2]); - - gchar c[64]; - sp_svg_write_color (c, sizeof(c), - SP_RGBA32_U_COMPOSE( - (SP_COLOR_F_TO_U(rgb[0])), - (SP_COLOR_F_TO_U(rgb[1])), - (SP_COLOR_F_TO_U(rgb[2])), - 0xff - ) - ); + hsl.set(ch, old + by * (by > 0 ? (1 - hsl[ch]) : hsl[ch])); + hsl.normalize(); + double diff = hsl[ch] - old; + hsl.convert(cc.getSpace()); SPCSSAttr *css = sp_repr_css_attr_new (); - if (modifier == 3) { // alpha - Inkscape::CSSOStringStream osalpha; - osalpha << hsla[3]; - sp_repr_css_set_property(css, (fillstroke == SS_FILL) ? "fill-opacity" : "stroke-opacity", osalpha.str().c_str()); + sp_repr_css_set_property_double(css, (fillstroke == SS_FILL) ? "fill-opacity" : "stroke-opacity", hsl.getOpacity()); } else { - sp_repr_css_set_property (css, (fillstroke == SS_FILL) ? "fill" : "stroke", c); + sp_repr_css_set_property_string(css, (fillstroke == SS_FILL) ? "fill" : "stroke", hsl.toString(false)); } sp_desktop_set_style (parent->getDesktop(), css); sp_repr_css_attr_unref (css); - return diff; + return {old, diff}; } void RotateableSwatch::do_motion(double by, guint modifier) @@ -1110,50 +1010,39 @@ void RotateableSwatch::do_motion(double by, guint modifier) cursor_state = modifier; } - guint32 cc; - if (!startcolor_set) { - cc = startcolor = parent->_thisselected[fillstroke]; - startcolor_set = true; - } else { - cc = startcolor; + if (!startcolor) { + startcolor = parent->_thisselected[fillstroke]; } - float hsla[4]; - double diff = 0; - - diff = color_adjust(hsla, by, cc, modifier); + auto ret = color_adjust(*startcolor, by, modifier); if (modifier == 3) { // alpha DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey, (_("Adjust alpha")), INKSCAPE_ICON("dialog-fill-and-stroke")); - double ch = hsla[3]; parent->getDesktop()->getTool()->message_context->setF( Inkscape::IMMEDIATE_MESSAGE, _("Adjusting alpha: was %.3g, now %.3g (diff %.3g); with Ctrl to adjust lightness, with Shift to adjust saturation, without modifiers to adjust hue"), - ch - diff, ch, diff); + ret.first - ret.second, ret.first, ret.second); } else if (modifier == 2) { // saturation DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey, (_("Adjust saturation")), INKSCAPE_ICON("dialog-fill-and-stroke")); - double ch = hsla[1]; parent->getDesktop()->getTool()->message_context->setF( Inkscape::IMMEDIATE_MESSAGE, _("Adjusting saturation: was %.3g, now %.3g (diff %.3g); with Ctrl to adjust lightness, with Alt to adjust alpha, without modifiers to adjust hue"), - ch - diff, ch, diff); + ret.first - ret.second, ret.first, ret.second); } else if (modifier == 1) { // lightness DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey, (_("Adjust lightness")), INKSCAPE_ICON("dialog-fill-and-stroke")); - double ch = hsla[2]; parent->getDesktop()->getTool()->message_context->setF( Inkscape::IMMEDIATE_MESSAGE, _("Adjusting lightness: was %.3g, now %.3g (diff %.3g); with Shift to adjust saturation, with Alt to adjust alpha, without modifiers to adjust hue"), - ch - diff, ch, diff); + ret.first - ret.second, ret.first, ret.second); } else { // hue DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey, (_("Adjust hue")), INKSCAPE_ICON("dialog-fill-and-stroke")); - double ch = hsla[0]; parent->getDesktop()->getTool()->message_context->setF( Inkscape::IMMEDIATE_MESSAGE, _("Adjusting hue: was %.3g, now %.3g (diff %.3g); with Shift to adjust saturation, with Alt to adjust alpha, with Ctrl to adjust lightness"), - ch - diff, ch, diff); + ret.first - ret.second, ret.first, ret.second); } } @@ -1169,8 +1058,7 @@ void RotateableSwatch::do_release(double by, guint modifier) return; parent->dragging = false; - float hsla[4]; - color_adjust(hsla, by, startcolor, modifier); + color_adjust(*startcolor, by, modifier); if (cursor_state != -1) { if (auto window = dynamic_cast(get_root())) { @@ -1199,7 +1087,7 @@ void RotateableSwatch::do_release(double by, guint modifier) } parent->getDesktop()->getTool()->message_context->clear(); - startcolor_set = false; + startcolor.reset(); } /* ============================================= RotateableStrokeWidth */ diff --git a/src/ui/widget/selected-style.h b/src/ui/widget/selected-style.h index 2b08cf5197..15bcad5283 100644 --- a/src/ui/widget/selected-style.h +++ b/src/ui/widget/selected-style.h @@ -21,6 +21,7 @@ #include #include +#include "colors/color.h" #include "helper/auto-connection.h" #include "rotateable.h" #include "ui/popup-menu.h" @@ -74,7 +75,7 @@ class RotateableSwatch : public Rotateable { RotateableSwatch(SelectedStyle *parent, guint mode); ~RotateableSwatch() override; - double color_adjust (float *hsl, double by, guint32 cc, guint state); + std::pair color_adjust(Colors::Color const &cc, double by, guint state); void do_motion (double by, guint state) override; void do_release (double by, guint state) override; @@ -85,8 +86,7 @@ private: SelectedStyle *parent; - guint32 startcolor = 0; - bool startcolor_set = false; + std::optional startcolor; gchar const *undokey = "ssrot1"; @@ -125,8 +125,8 @@ public: SPDesktop *getDesktop() {return _desktop;} void update(); - guint32 _lastselected[2]; - guint32 _thisselected[2]; + std::optional _lastselected[2]; + std::optional _thisselected[2]; guint _mode[2]; diff --git a/src/ui/widget/shapeicon.cpp b/src/ui/widget/shapeicon.cpp index 401c695b3f..d371fa997b 100644 --- a/src/ui/widget/shapeicon.cpp +++ b/src/ui/widget/shapeicon.cpp @@ -18,7 +18,6 @@ #include #include -#include "color.h" #include "ui/icon-loader.h" #include "ui/util.h" diff --git a/src/ui/widget/stroke-style.cpp b/src/ui/widget/stroke-style.cpp index f493d93b99..c7a7604fc9 100644 --- a/src/ui/widget/stroke-style.cpp +++ b/src/ui/widget/stroke-style.cpp @@ -40,7 +40,6 @@ #include "object/sp-stop.h" #include "object/sp-text.h" #include "svg/css-ostringstream.h" -#include "svg/svg-color.h" #include "ui/icon-loader.h" #include "ui/icon-names.h" #include "ui/pack.h" diff --git a/src/ui/widget/style-swatch.cpp b/src/ui/widget/style-swatch.cpp index 49958e0b76..9de9ecef34 100644 --- a/src/ui/widget/style-swatch.cpp +++ b/src/ui/widget/style-swatch.cpp @@ -265,14 +265,15 @@ void StyleSwatch::setStyle(SPStyle *query) place->set_tooltip_text((i == SS_FILL)? (_("Pattern (fill)")) : (_("Pattern (stroke)"))); } } else if (paint->set && paint->isColor()) { - guint32 color = paint->value.color.toRGBA32( SP_SCALE24_TO_FLOAT ((i == SS_FILL)? query->fill_opacity.value : query->stroke_opacity.value) ); - _color_preview[i]->setRgba32(color); + auto color = paint->getColor(); + color.addOpacity(i == SS_FILL ? query->fill_opacity : query->stroke_opacity); + _color_preview[i]->setRgba32(color.toRGBA()); place->append(*_color_preview[i]); gchar *tip; if (i == SS_FILL) { - tip = g_strdup_printf (_("Fill: %06x/%.3g"), color >> 8, SP_RGBA32_A_F(color)); + tip = g_strdup_printf (_("Fill: %s"), color.toString().c_str()); } else { - tip = g_strdup_printf (_("Stroke: %06x/%.3g"), color >> 8, SP_RGBA32_A_F(color)); + tip = g_strdup_printf (_("Stroke: %s"), color.toString().c_str()); } place->set_tooltip_text(tip); g_free (tip); diff --git a/src/ui/widget/swatch-selector.cpp b/src/ui/widget/swatch-selector.cpp index 16878ed388..c3404be01e 100644 --- a/src/ui/widget/swatch-selector.cpp +++ b/src/ui/widget/swatch-selector.cpp @@ -27,21 +27,20 @@ namespace Widget { SwatchSelector::SwatchSelector() : Gtk::Box(Gtk::Orientation::VERTICAL) + , _colors(new Colors::ColorSet()) { _gsel = Gtk::make_managed(); _gsel->setMode(GradientSelector::MODE_SWATCH); _gsel->set_visible(true); UI::pack_start(*this, *_gsel); - auto const color_selector = Gtk::make_managed(_selected_color); + auto const color_selector = Gtk::make_managed(_colors); color_selector->set_label(_("Swatch color")); color_selector->set_visible(true); UI::pack_start(*this, *color_selector); - _selected_color.signal_dragged.connect(sigc::mem_fun(*this, &SwatchSelector::_changedCb)); - _selected_color.signal_released.connect(sigc::mem_fun(*this, &SwatchSelector::_changedCb)); - // signal_changed doesn't get called if updating shape with colour. - _selected_color.signal_changed.connect(sigc::mem_fun(*this, &SwatchSelector::_changedCb)); + _colors->signal_released.connect(sigc::mem_fun(*this, &SwatchSelector::_changedCb)); + _colors->signal_changed.connect(sigc::mem_fun(*this, &SwatchSelector::_changedCb)); } void SwatchSelector::_changedCb() @@ -62,7 +61,7 @@ void SwatchSelector::_changedCb() ngr->ensureVector(); if (auto stop = ngr->getFirstStop()) { - stop->setColor(_selected_color.color(), _selected_color.alpha()); + stop->setColor(_colors->getAverage()); DocumentUndo::done(ngr->document, _("Change swatch color"), INKSCAPE_ICON("color-gradient")); } } @@ -71,11 +70,12 @@ void SwatchSelector::_changedCb() void SwatchSelector::setVector(SPDocument */*doc*/, SPGradient *vector) { _gsel->setVector(vector ? vector->document : nullptr, vector); + _colors->clear(); if (vector && vector->isSolid()) { _updating_color = true; auto stop = vector->getFirstStop(); - _selected_color.setColorAlpha(stop->getColor(), stop->getOpacity(), true); + _colors->set(stop->getId(), stop->getColor()); _updating_color = false; } } diff --git a/src/ui/widget/swatch-selector.h b/src/ui/widget/swatch-selector.h index c67b0132cf..082383087e 100644 --- a/src/ui/widget/swatch-selector.h +++ b/src/ui/widget/swatch-selector.h @@ -11,11 +11,11 @@ #define SEEN_SP_SWATCH_SELECTOR_H #include -#include "ui/selected-color.h" + +#include "colors/color-set.h" class SPDocument; class SPGradient; -struct SPColorSelector; namespace Inkscape { namespace UI { @@ -35,7 +35,7 @@ private: void _changedCb(); GradientSelector *_gsel = nullptr; - Inkscape::UI::SelectedColor _selected_color; + std::shared_ptr _colors; bool _updating_color = false; }; diff --git a/src/util/object-renderer.cpp b/src/util/object-renderer.cpp index a827fa0922..bef875d287 100644 --- a/src/util/object-renderer.cpp +++ b/src/util/object-renderer.cpp @@ -15,7 +15,7 @@ #include #include #include -#include "color.h" +#include "colors/color.h" #include "display/cairo-utils.h" #include "document.h" #include "gradient-chemistry.h" @@ -36,6 +36,7 @@ #include "display/drawing.h" #include "util/scope_exit.h" #include "ui/cache/svg_preview_cache.h" +#include "ui/util.h" #include "xml/href-attribute-helper.h" using namespace std::literals; @@ -229,7 +230,7 @@ Cairo::RefPtr draw_gradient(SPGradient* gradient, double width, double py = h + 2 * radius; double px = std::round(stop.offset * width); ctx->arc(px, py, radius, 0, 2 * M_PI); - ctx->set_source_rgba(stop.color.v.c[0], stop.color.v.c[1], stop.color.v.c[2], stop.opacity); + ink_cairo_set_source_color(ctx, *stop.color); ctx->fill_preserve(); ctx->set_source_rgb(0.5, 0.5, 0.5); ctx->stroke(); @@ -239,7 +240,6 @@ Cairo::RefPtr draw_gradient(SPGradient* gradient, double width, return surface; } - std::unique_ptr ink_markers_preview_doc(const Glib::ustring& group_id) { constexpr auto buffer = R"A( @@ -406,11 +406,11 @@ Cairo::RefPtr create_marker_image( } Gdk::RGBA fg = marker_color; - auto fgcolor = rgba_to_css_color(fg); + auto fgcolor = gdk_to_css_color(fg); fg.set_red(1 - fg.get_red()); fg.set_green(1 - fg.get_green()); fg.set_blue(1 - fg.get_blue()); - auto bgcolor = rgba_to_css_color(fg); + auto bgcolor = gdk_to_css_color(fg); auto objects = _sandbox->getObjectsBySelector(".colors"); for (auto el : objects) { if (SPCSSAttr* css = sp_repr_css_attr(el->getRepr(), "style")) { diff --git a/src/util/object-renderer.h b/src/util/object-renderer.h index 9317c4dc01..a1173d179b 100644 --- a/src/util/object-renderer.h +++ b/src/util/object-renderer.h @@ -2,6 +2,7 @@ #ifndef SEEN_OBJECT_RENDERER_H #define SEEN_OBJECT_RENDERER_H +#include #include #include #include @@ -14,6 +15,9 @@ #include "document.h" namespace Inkscape { + namespace Colors { + class Color; + }; class object_renderer { public: @@ -117,4 +121,6 @@ void draw_gradient(const Cairo::RefPtr& cr, SPGradient* gradient } // namespace +void set_source_inkscape_color(Cairo::RefPtr context, Inkscape::Colors::Color const &color, double opacity); + #endif // SEEN_OBJECT_RENDERER_H diff --git a/src/widgets/CMakeLists.txt b/src/widgets/CMakeLists.txt index 1718cc469f..3a7d6e7e4a 100644 --- a/src/widgets/CMakeLists.txt +++ b/src/widgets/CMakeLists.txt @@ -1,13 +1,11 @@ # SPDX-License-Identifier: GPL-2.0-or-later set(widgets_SRC - paintdef.cpp sp-attribute-widget.cpp spw-utilities.cpp # ------- # Headers - paintdef.h sp-attribute-widget.h spw-utilities.h widget-sizes.h diff --git a/src/widgets/paintdef.cpp b/src/widgets/paintdef.cpp deleted file mode 100644 index 3685da6bb8..0000000000 --- a/src/widgets/paintdef.cpp +++ /dev/null @@ -1,191 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later OR MPL-1.1 OR LGPL-2.1-or-later -/* ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1/GPL 2.0/LGPL 2.1 - * - * The contents of this file are subject to the Mozilla Public License Version - * 1.1 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * http://www.mozilla.org/MPL/ - * - * Software distributed under the License is distributed on an "AS IS" basis, - * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License - * for the specific language governing rights and limitations under the - * License. - * - * The Original Code is Eek Color Definition. - * - * The Initial Developer of the Original Code is - * Jon A. Cruz. - * Portions created by the Initial Developer are Copyright (C) 2006 - * the Initial Developer. All Rights Reserved. - * - * Contributor(s): - * - * Alternatively, the contents of this file may be used under the terms of - * either the GNU General Public License Version 2 or later (the "GPL"), or - * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), - * in which case the provisions of the GPL or the LGPL are applicable instead - * of those above. If you wish to allow use of your version of this file only - * under the terms of either the GPL or the LGPL, and not to allow others to - * use your version of this file under the terms of the MPL, indicate your - * decision by deleting the provisions above and replace them with the notice - * and other provisions required by the GPL or the LGPL. If you do not delete - * the provisions above, a recipient may use your version of this file under - * the terms of any one of the MPL, the GPL or the LGPL. - * - * ***** END LICENSE BLOCK ***** */ - -#include "paintdef.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -PaintDef::PaintDef() - : description(C_("Paint", "None")) - , type(NONE) - , rgb({0, 0, 0}) -{ -} - -PaintDef::PaintDef(std::array const &rgb, std::string description, Glib::ustring tooltip) - : description(std::move(description)), tooltip(std::move(tooltip)) - , type(RGB) - , rgb(rgb) -{ -} - -std::string PaintDef::get_color_id() const -{ - if (type == NONE) { - return "none"; - } - if (!description.empty() && description[0] != '#') { - auto name = Glib::ustring(std::move(description)); - // Convert description to ascii, strip out symbols, remove duplicate dashes and prefixes - static auto const reg1 = Glib::Regex::create("[^[:alnum:]]"); - name = reg1->replace(name, 0, "-", static_cast(0)); - static auto const reg2 = Glib::Regex::create("-{2,}"); - name = reg2->replace(name, 0, "-", static_cast(0)); - static auto const reg3 = Glib::Regex::create("(^-|-$)"); - name = reg3->replace(name, 0, "", static_cast(0)); - // Move important numbers from the start where they are invalid xml, to the end. - static auto const reg4 = Glib::Regex::create("^(\\d+)(-?)([^\\d]*)"); - name = reg4->replace(name, 0, "\\3\\2\\1", static_cast(0)); - return name.lowercase(); - } - auto [r, g, b] = rgb; - char buf[12]; - std::snprintf(buf, 12, "rgb%02x%02x%02x", r, g, b); - return std::string(buf); -} - -const Glib::ustring& PaintDef::get_tooltip() const { - return tooltip; -} - -std::vector PaintDef::getMIMEData(char const *mime_type) const -{ - auto from_data = [] (void const *p, int len) { - std::vector v(len); - std::memcpy(v.data(), p, len); - return v; - }; - - auto const [r, g, b] = rgb; - - if (std::strcmp(mime_type, mimeTEXT) == 0) { - std::array tmp; - std::snprintf(tmp.data(), 8, "#%02x%02x%02x", r, g, b); - return from_data(tmp.data(), tmp.size()); - } else if (std::strcmp(mime_type, mimeX_COLOR) == 0) { - auto const tmp = std::to_array({(uint16_t)((r << 8) | r), (uint16_t)((g << 8) | g), (uint16_t)((b << 8) | b), uint16_t{0xffff}}); - return from_data(tmp.data(), tmp.size() * sizeof(decltype(tmp)::value_type)); - } else if (std::strcmp(mime_type, mimeOSWB_COLOR) == 0) { - std::string tmp(""); - switch (get_type()) { - case PaintDef::NONE: - tmp += ""; - break; - default: - tmp += std::string(""; - tmp += ""; - tmp += ""; - } - tmp += ""; - return from_data(tmp.c_str(), tmp.size()); - } else { - return {}; - } -} - -bool PaintDef::fromMIMEData(char const *mime_type, std::span data) -{ - if (std::strcmp(mime_type, mimeX_COLOR) == 0) { - if (data.size() == 8) { - // Careful about endian issues. - type = PaintDef::RGB; - auto const vals = reinterpret_cast(data.data()); - rgb[0] = 0x0ff & (vals[0] >> 8); - rgb[1] = 0x0ff & (vals[1] >> 8); - rgb[2] = 0x0ff & (vals[2] >> 8); - return true; - } - } else if (std::strcmp(mime_type, mimeOSWB_COLOR) == 0) { - std::string xml(data.data(), data.size()); - if (xml.find("") != std::string::npos) { - type = PaintDef::NONE; - rgb = {0, 0, 0}; - return true; - } else if (auto pos = xml.find("", pos)); - type = PaintDef::RGB; - if (auto numPos = srgb.find("r="); numPos != std::string::npos) { - double dbl = Glib::Ascii::strtod(srgb.substr(numPos + 3)); - rgb[0] = static_cast(255 * dbl); - } - if (auto numPos = srgb.find("g="); numPos != std::string::npos) { - double dbl = Glib::Ascii::strtod(srgb.substr(numPos + 3)); - rgb[1] = static_cast(255 * dbl); - } - if (auto numPos = srgb.find("b="); numPos != std::string::npos) { - double dbl = Glib::Ascii::strtod(srgb.substr(numPos + 3)); - rgb[2] = static_cast(255 * dbl); - } - if (auto pos = xml.find("", pos)); - if (auto namePos = colorTag.find("name="); namePos != std::string::npos) { - char quote = colorTag[namePos + 5]; - auto endPos = colorTag.find(quote, namePos + 6); - description = colorTag.substr(namePos + 6, endPos - (namePos + 6)); - } - } - return true; - } - } - - return false; -} - -/* - Local Variables: - mode:c++ - c-file-style:"stroustrup" - c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) - indent-tabs-mode:nil - fill-column:99 - End: -*/ -// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/widgets/paintdef.h b/src/widgets/paintdef.h deleted file mode 100644 index 68da249943..0000000000 --- a/src/widgets/paintdef.h +++ /dev/null @@ -1,98 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later OR MPL-1.1 OR LGPL-2.1-or-later -/* ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1/GPL 2.0/LGPL 2.1 - * - * The contents of this file are subject to the Mozilla Public License Version - * 1.1 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * http://www.mozilla.org/MPL/ - * - * Software distributed under the License is distributed on an "AS IS" basis, - * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License - * for the specific language governing rights and limitations under the - * License. - * - * The Original Code is Eek Color Definition. - * - * The Initial Developer of the Original Code is - * Jon A. Cruz. - * Portions created by the Initial Developer are Copyright (C) 2006 - * the Initial Developer. All Rights Reserved. - * - * Contributor(s): - * - * Alternatively, the contents of this file may be used under the terms of - * either the GNU General Public License Version 2 or later (the "GPL"), or - * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), - * in which case the provisions of the GPL or the LGPL are applicable instead - * of those above. If you wish to allow use of your version of this file only - * under the terms of either the GPL or the LGPL, and not to allow others to - * use your version of this file under the terms of the MPL, indicate your - * decision by deleting the provisions above and replace them with the notice - * and other provisions required by the GPL or the LGPL. If you do not delete - * the provisions above, a recipient may use your version of this file under - * the terms of any one of the MPL, the GPL or the LGPL. - * - * ***** END LICENSE BLOCK ***** */ - -#ifndef INKSCAPE_WIDGETS_PAINTDEF_H -#define INKSCAPE_WIDGETS_PAINTDEF_H - -#include -#include -#include -#include -#include -#include - -inline constexpr auto mimeOSWB_COLOR = "application/x-oswb-color"; -inline constexpr auto mimeX_COLOR = "application/x-color"; -inline constexpr auto mimeTEXT = "text/plain"; - -/** - * Pure data representation of a color definition. - */ -class PaintDef -{ -public: - enum ColorType - { - NONE, - RGB - }; - - /// Create a color of type NONE - PaintDef(); - - /// Create a color of type RGB - PaintDef(std::array const &rgb, std::string description, Glib::ustring tooltip); - - std::string get_color_id() const; - const Glib::ustring& get_tooltip() const; - - std::string const &get_description() const { return description; } - ColorType get_type() const { return type; } - std::array const &get_rgb() const { return rgb; } - - std::vector getMIMEData(char const *mime_type) const; - bool fromMIMEData(char const *mime_type, std::span data); - -protected: - std::string description; - Glib::ustring tooltip; - ColorType type; - std::array rgb; -}; - -#endif // INKSCAPE_WIDGETS_PAINTDEF_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/xml/repr-css.cpp b/src/xml/repr-css.cpp index 5043db44fb..b19be97a4e 100644 --- a/src/xml/repr-css.cpp +++ b/src/xml/repr-css.cpp @@ -229,6 +229,14 @@ void sp_repr_css_set_property_double(SPCSSAttr *css, gchar const *name, double v ((Node *) css)->setAttributeCssDouble(name, value); } +/** + * Set a style property to a standard string. + */ +void sp_repr_css_set_property_string(SPCSSAttr *css, char const *name, std::string const &value) +{ + sp_repr_css_set_property(css, name, value.c_str()); +} + /** * Write a style attribute string from a list of properties stored in an SPCSAttr object. */ diff --git a/src/xml/repr.h b/src/xml/repr.h index 9cfc68791a..9b11c248e4 100644 --- a/src/xml/repr.h +++ b/src/xml/repr.h @@ -89,6 +89,7 @@ void sp_repr_css_unset_property(SPCSSAttr *css, char const *name); bool sp_repr_css_property_is_unset(SPCSSAttr *css, char const *name); double sp_repr_css_double_property(SPCSSAttr *css, char const *name, double defval); void sp_repr_css_set_property_double(SPCSSAttr *css, char const *name, double value); +void sp_repr_css_set_property_string(SPCSSAttr *css, char const *name, std::string const &value); void sp_repr_css_write_string(SPCSSAttr *css, Glib::ustring &str); void sp_repr_css_set(Inkscape::XML::Node *repr, SPCSSAttr *css, char const *key); diff --git a/testfiles/CMakeLists.txt b/testfiles/CMakeLists.txt index 97293d0487..a9a975e44c 100644 --- a/testfiles/CMakeLists.txt +++ b/testfiles/CMakeLists.txt @@ -93,7 +93,6 @@ set(TEST_SOURCES extract-uri-test attributes-test dir-util-test - oklab-color-test sp-item-test sp-object-test sp-object-tags-test @@ -110,7 +109,6 @@ set(TEST_SOURCES style-test svg-affine-test svg-box-test - svg-color-test svg-length-test svg-stringstream-test sp-gradient-test @@ -128,6 +126,7 @@ set(TEST_SOURCES sp-item-group-test store-test lpe-test + ui-util-test ${LPE_TESTS_64bit} ) diff --git a/testfiles/cli_tests/testcases/export-area-drawing_expected.eps b/testfiles/cli_tests/testcases/export-area-drawing_expected.eps index f22557757c..7da25894b3 100644 --- a/testfiles/cli_tests/testcases/export-area-drawing_expected.eps +++ b/testfiles/cli_tests/testcases/export-area-drawing_expected.eps @@ -1,6 +1,6 @@ %!PS-Adobe-3.0 EPSF-3.0 -%%Creator: cairo 1.16.0 (https://cairographics.org) -%%CreationDate: Thu Feb 27 23:52:53 2020 +%%Creator: cairo 1.18.0 (https://cairographics.org) +%%CreationDate: Tue May 7 07:52:50 2024 %%Pages: 1 %%DocumentData: Clean7Bit %%LanguageLevel: 3 @@ -122,319 +122,320 @@ q /ImageMatrix [ 630 0 0 -630 0 630 ] >> cairo_image - Gb"0WGFVoNIH^]DMs@>:hKuG,0.X(r,f,]L#qUH]rg8Q\:I]joW(", - UT5GOfW;#Y*)i!>1tE8h,\O.4@G&jg$c)%$beRNlaq3MXV8gH@>\(UFekStW@gX? - $.>Co=LtPM<#'4hP%tkD9FtKkDeb!,IV_T%-_@a8<&7bW2mqr'uX$_SS)C(4\9_G0atVltr - 63+5F1fr249KKnX.f/-'IGmJ?4e7NHbiNqa.9"ptaa:fm_K*.NCV]nLU!fK!g;d=C*N6t,$ - l$4FY%u=\1nfi>Dt1^ - m\2+-c]BBi2i`IYj`'T"^oUP>:.?sfTqd:L\ArYrL#)c4@H":H/kFSG,bE^)@6Kk - Deb!,G>JSmYCI?`g+`OtH5\;cn:!_?-t(bbjoBKtY0_@`tR;K*!PP-nIJJ@0'J?SC\OS2iU - &+DkYKA1PLNq-nG?[p)IAsZEPp.?:dFki>Dt1^lH)uZE4i7N5$kl-nIJJ@0,#cp[E3-Klrp - @k0lp0$4G0V=9*O241*4@@F%>C;XE;Uf]sWK_SS)C(4[Dc#;$bc[S>cm^$"cT$470e:fm^@ - j;_9:^T^SUG0f_s,mB!VB.,;248/,Mi_a,)-['aBP(LQf1DCUT*W'mnHWSiU^KO46eG>5#@ - Gr(2$c%c)q\FH\fJC!<;G9'SfCDN@$4G0V=9*M(mIRon/WRJ$a5W.S@Gr(2$c%dTHPZBT=N - Bs`Xf\ClYj`'T"^oBEpZD?R=N@CXPP6*'n>$$k$dVXO!-T?.GFkF\Fe%<)@3l)2KkDeb!': - \(qiK-X]ku5CVsJ;MC^LQr:fm^@*.DP'5[gGCXJDMnn-k!S@)nD=k"$6$/iB>:/JP=H_m/9juuu$4FY0UMnGD/aSnh8rA6-Y,%_pn-k!S@)oP.ZOE:I;%Knf - fctF]NW7%rYj`'T"^oBU2eEuXYYf%>f3S"?(qYnn^2nUj.k)kt4@G?o*NU5oS'DCV-=@keei>Dt1^lB'iV\ - qnleuoJIE,&U[qq6tqR8)]P^[k8@5;n4LB.,;24&5.0RV;\@p"7j.[]7d3Sc?CZc5t_'F3h - ,gIk46$i>Dt1^lFW5V^*1p4?_i`_E)FSVgLY]DIairP(LQf1DCUT*N-=l>Z`AKpFWJA;.g[ - /@Dt1^lJ.rS![mRcKX:B$gXbB=k6YPSCfi=Dfd9/K - nX.f/-%b,Dk<&(k)/f8GREce8rBa[=&MjsG_X':YiGAQnVK0,=:^CtYhlLX\6Q=EH4D0&;- - 3gA(^/,5B)I@"=\3U9pTY[jgeHh4jtDl=`CNT8(4ZR8Nik(7>H&[6:gq%A8TE_9m,fYo\eo - Tf=9'-'2(Iqr4ij;#/Lr7WCp0W_IV3t*h)aegN+>7`!EcT#8@Ch60'WILL%T+*7DY!U2_.s - %O(BRG$4FY$I>=+-'Wk('2_Jpia0O8LA]Mq6j@&PjFsgeA0,1E.c"/YTW,E&qc'AkWW#Sl - _rG&cfu*c,?qZnErD!JdSh(C7:?4!Y^c[^K7mOX]keG7dB(eiojGCoc.b"I\h%'3:a;ba/- - $(Ok:ACPf2^YU@UZ$68V,fmYQ-46Y]$4$]IJ%7gP*?.]'a<_-,i/N;BSgN(4ZPnpF\/@A/3bSVFRRH9#4?n+)^m6;$]_k_f-!u!\s#@4FYr<*p/b/)5.UYR. - k0s1'Gc3&8)B\E:oKkpUMkk"'jXCN?4X=\ft8mMDHpF!4in:0'Gc3&8)KhROJb.q_9^!,W6 - i6F7b1FcC7b&O7fZCg!#?R14j+sA%D'7QKtS4,8Wh%c$@%TJ!jLV8)`%r'e>P/l$@=;7P+k - d6Zl@\>Dbb/J@3l(\;-3f6*8. - jSM"$s>LA]MA_'p\C`6Z+G-U[U7hAL!!#?R33R&[9:>fXjgEO4l_XbaQXJ>iM"^o<+*@?IT - DelCWh9#LcMW/iX2qG*r*qBk1@0'JLEr4?S"-f_(LkkXu%D(@UlROE-:;0l=;L6VU$AAXk_ - 9$Om'!749bpZoK]3RRI5kY^>2]rrrtQef`SM6o')CLSH;FHtapF3XM=9*7+3OZdI0r93#GWOmJd7T:n\olF9J9NCm - !4VYA2(@m;4\35T$#.nW7=(?Wa;4-0<*-J"3X"pXKa$DchBU\XEOi"QMFTI"&J%o?jDH#g] - l]Za$fD0q$c&mXLR/sEV+_C20;]Uf;BS.;(4]Wd%P9JMasGk>]TpL:FdD]c'W(\=ZB?EB:f - m^8jW%N?En.&aMMKUh031sbm[@C1A$pCp?jhYli)\-S,mH6;XS[fTdNpPI;WM\c^e0Jp6"' - f([=A3cK9T@!hVsB60;?pc?PVosQB`0+K#.S.COaVRK8s3a]u9e]M\dlm/f^>9gr.7=TgiG - k\922O8RX,l:[p2DjN1#OG?@EKC(qG6>6jmT1rtGnh'\d_]0'7gEO0\^F=CUP_?+]Bi1:)V - mC9%*M)WhnLebR\#M>#PmcO&k8sW)i!X:&(!4NHO2r>_tf6(0b+F:tiPnYV.h27"9!st[*! - 8KX-mQ?,4gsB?F;s55T<`51KS^Gs&a0HjT@0*l\pmm0KH;UHD`&m1^HJSS%*aq"5XJ?+:$4 - FYO,B(mcLdeH6/07*\21)Tk@`sGiZ>AcK+A>!I_dVMNgPbo]l#JE5!M'Zlih^"h8gAF26EQqNH@\[+;>T - A>K:p51oNcoDC8aRT_W=\02j'Gc4'a'YHId"Q)&0&AZ= - d3f+MN3&f@kZh.d":Ttfi7@GMV]->d\XF0bSY0_gQ#D!q7HrZsbmiJ*'H@f&J6OCE:,BtBG - iQ_"k]YNY;OlQ#V>`M+.0>+Jc(Gd%eRl(+@dWf0!)3,T@'u^o^O>-RU - tQ$h['\W!,ndmSISD-,mDRV`*,/Pn$O2U9FXQH@O,V6mr";4[Qep!S!o$MY0fXOj2cAZ*rY - #l.2XW$4FYOe#t'&fQ2AKGg:miU=77I=@l@iZpZr6.>YhBE9m/Y$&Qt*,!AW4AO.);8R_F) - =]SEs=9,%9B%-NCVgYfk.UKB1X3mL0ok:t"/_MqTWICC[G>\[""lP]_W%Z\R`=j#7?EBqNg - c>1B'i,10YQ.!tcdFCkEm\5n=Zj=,:,/^$J5>;1 - <7d,dK_I6-5,'!cV`>iT1:(rLt>USulsgY,&<"Uk&%sZ^Mh98O6bc"Ot.Qb7&!0RI2`bXO) - g,U_mf)ZKL)7g/,+F\jXYt_?+[&kFbCTbB71^@"hIAc7%(-lp`m_gVfQ(/#6[>PUJ'_=)nd - %243ZqFW^-WKiCIpoF4KUjJ>R-nG@"p?u?t0[_InDt-e\RR\kW,0ep?6t$*! - `VuCH#/g@rmNipRYcHZIPE^I-=@mN+N@FNZ<_K - >?UaRboK$4FWs - 8)KhR0W=;[Hgq*4!fpA[goOa0*Nr;4YQ.og[X4=VP]"k8`9WU2,!,+c*agq4\XUWH.p4@XY - Mc1[!*hPX`_;!sm9EkPV^Jr)nDF-Fl\O%u_F@O4IbhAK#sHn - 7p!!I3W!ldH:iF$up_(FKC:lh4ua#eQ3NgbkLb'GM=5(Ja=eAh]Nh;-5!5XJFK(Tt2dZ3bJ - I]@0*lY@`Qtd#BHsg.it//`_$RF$QoRIDP**]-'')lSJ\:(XWJ^u%9) - \G4>Y.4i!OuSmE_H$9nW_rtRWWqU*c*d@[oZI48thLa9b$EkT3nH[ql^\LJ?_7-=.&p1TcP - cA&[][Z8d\)-R0DU;o2J*%R``Ld-$s7bDSH&c4+Gr^b7k_BZ+WV\]q>U%(Bh!Kf3RuaYGA[ - IE-@DHUi?cFl6GZ$*6o)-q88 - =0%4ES3J\XDp&7OoV8Vjgt6ga\\X4Ve,@8?JN,OON++s"DIi@J9rtH&iC]!K`nDpRQ_0^jm - ]F!BEDos>DX9Hlb[A4mdf%,%JbVZU>+#UG.@b0=q.RdAZ#8s:P,3#&Y#8@%f&[lVC+rr:F^ - OZR+Al.M]CdNT"El([,dO,HC=Ul47E+_a4T:_b!cTgU^_2:A;ka)EpAtP6Ff0UMg2:GlKK0 - j*i^n"7gp$dtL#l)($aqFq$^AL0Y\X:<`h)!V9*&PI3rQ]4A2gf>>:/.8\D5]ZeR*gLVJp1b%u6m`!.8`nha2t; - ;lP,"B*'\kdClV/WVk8Y87[@N@K"`cCiX-q/;R"1'4HQ - 6R>_hJi@4p!gWq]AraR%>=rs":+\F2jc_ZF>r_N^l%>JMNN)"FRIDQcBY:(YVt%61]*IIK` - r"jLYeM50R91(+7V2'Q6r/"ik\4okGluVXHAKb"R+@\*c2a/N%CD0$CuB'OliOV%==V5[YUXCQ?6l - \Z%ZJ[f@I`d-]-CiHA."Y]E_8Vi?LMciI`)\Mc - 1VclM9A>*a,ZCf'(BkSbAO;!=B4MKk%`,(lY4:SKU2pi=@SB;?TCj*`oFu?I?IO*ct`iKa$ - DOn^gL,T%U3o`@!lt'4"0a6p-:6jQk20omlA>C[@BBKa$DOnX\,;B%1bt9c?o[o5FQ4mNWc - C2i@73$JkT1Q`KjL@0*lW@Y[N``JAMA,iT.Er#1j%eMct$c%Hp> - :1dZapRbhnKAEGcF)U74]&ek/XNO4q3I!l'Gc3,Vm[k`)n&S('VFk#Jh+F!86'a(fFg?cq/ - $j`C(R$)#`*5djkEB7k?#8Nh*8&\g"8W"!P!)pF2EV_d_j$/4+bA\OE> - s9nh.!jhm0M@_?+]8oVR9`G[7D+M,/6sMY35K/`3kEVRm0'2PI>T>W@rBUVr[*.R)8m4;Ls - q[!^%D5YiK!YQ.Aa3OZe4%$p1pntQ_HFBs)`Fa\pJ"'R?ZrVi+B/JJtNWI9Pf)&2e-mO(6q&-b(8(XJF7G`'?>0U6]uWB - jt9F^Osl#P`hd!\fm\@#70X0C(gmUCZhpK`tk!s8KtAKrr_8j[UEN!t#FOeG^GO_HtX'j@: - n0J(E+!r8p?UDI=MhV/-&TRpFJ!Hlo!TnIe3Y)TWeD - i*I;Y<6"-t_Y4A=un,\4H@-Z+No*btQ6[+Y7I=MhV/-&V(hg*;9YVeI7Ie3Y)TWeDi*I3"7 - 6"+POBDKp[pjg3_0QlHY3.3Kl%+=j0JcQ-nG?sa**jb[S^\[O$:>a'Gc4uj0-c_-u?4bQQLX+%k&t/;o - `7;DG\uA:6QcJJ.F%Z!)k7g:,=<"r7hDP6"'e]3k8a8U3Y#6A*)kpi"$/&^spKn;\CZu^)e - L`pAf\>=9,3nD+hCC"a[QRg]RI?@0)`V[<6MaKe:e!jl[]C%gi][eGEGs-/GtVVh`IA4hr-R(4`C5RjTIbfHNIPIdR79TWeDiS_5h`dD?[bU%378n-=XN?n0R: - 3_K'Qq7-b*K>I[DZKLEYK>O53KB22:i"$/&^rY@Wf`m/DTEiWc4hr-R(4_O\LR/qop7`TN5 - PG/-:fm]ep)R=K:tRPZ?[hDDrh-IqN8ts8.;ZQ_.mNb.Io'5I7WJti2(7g&h+)q^!WJar!0 - $:%^VleL'tUmPg]RI?@0%4uTI[DEa[cKK>LsF4ltiJi"$/&^rV)?$Om - GIWI)_44hr-R(4_O"HPRG4[Rtb1T6l)Q-nG?CLYeM=:tY*FX8L`Tr'=`l(ch6V4G1N-cs>_ - q*oIP9$c'iWG98RgCphQOT6l)Q-nG?CLYnM8:tQ]B=aoc>rh-IqN#[+YN^7)hFXR!q#P`qg - !\eZEW3CEUKDaL,r'=`l(chHTCkKUUlh9mj&+BgX"^pEDmTBaJD:%\2k8rOl$4FYjYr?85' - XgkOb0N5d*u#K@OlO&c(_jDMB?8C^^`O7+JnBbYDs5Y7"4>#^7hb2'"@&f$A63]lG<$Y - &+C$^"d$7-7YSOK#K[K>^c)rC5iVJYaYkR676NnbkQ/DkYQ,=tI\ - :'-u)&2e-mO(6k!8gUT!/\lZ\C@1UjgqYnci\Y_@0%W+kJ.^__H,kanj=tI\Gc:@)\ - i"'mNk*i!8gUT!/]I&ZcJfD=*7=7Ta1@I_?*9`JUIn8$@m$4@trLYag9luh,aIcpgHl)t$^hl\sUrsG^MT]b?6+1"KDa'uHp_@m$4Ge$eR!Orll/mi[^AOlVo]H4^c)rC^ooUSXtN!o06`WA9@m - 58'+aRGK`r$1j5TY-mB40^e$_A%7Xj=tI\O&3b%n"]GOb/Bn(R%dVh`m/D??el_?-^]r>pg - (jm'*PFK\nr!fIa\@=_BhCQCk&MY5>B[S.fT;-4qE75e0^0$\miX#laiq0G`"mOg``!'q;7 - 828]i8DL7F%q^5'5[f5n&S^+[`?'jt($JLRFBA:["p=s\"]BtsHR)d#e-"?]#lFm\"d5G.H - R'SbH=W$'/3hL35bYDI;S,f7]Q!%En(O52a-Gt>X@!-YK>I[]MEMT&0FA15UDk#8ZUY'.7U - ^l/dSR9IFRMn!04!6U!Q9]/!_FWJ7j;lp"Rk'e*XNqi'Ga4Hm#_8LLVa*9kK=p'dF[%r!@3 - Q>!b'(i#p?`+/.bSCOT9un_?(l[I4\_X8.\+,6jGZD"LQ2"jH?abdk9EnDT;,Voc88 - FU8Q7&B<#5@f;/7:?5SYt"jd#o"0EP:q8.Y3BipZNU>_"oNE4lH41[Vg<[RarUj35U\2A^# - l3bjep`U!MgP>$[@Un&jV>KY^gAT)qL>rSa5NI$aW>)#VHGlG_8-l5cUT`J8G`QCJ+O`6QLrlsel!5aZ2!XW=s"G"!Y=+:db#!0AlA$abaR&c`>;=PuKt,6>Rh$4GW"HR&U$7:Cc)`Z>EdG#6gn)B(M=] - XXu7@)[s9b75Loin1YS_2]Q-[r.c2lDq635PicdJ,"e5NdpuIBeB@S?i4Ano,hr9Z`;)V^GOFE/2qEldj.L6HjXU[]:@e&*Ps#aFW^uNo2E,P&.9ZK?gC&N-e(NXUhUFdioC: - -h1\.fkL9+b>Dr/,JUITOreh'g'4WQSZ%iWr;lI;u[QhULXh]MXHcOPHp`NfI?Io$'1J!mQ - eo0Z9kCMMFWs08@Hp.,1f/7f08p$*IYEmhM6r78>(54QRqN>qc@T%uO[/&Q6iXdktGgGqLQ - UQ5n4qWX-f:[@r&c;2aYYGZd#,B6l\@?1PQ>*?7sV55]OCmk%bDbp;p7gDhZeu`H&FQmVQ4 - _\VFaH2Etf(=CY\^CHGs33ZHA8-JjHLX5_,etH-!+&6S_E+eZEFRU^IPg)=(IgJ&Vu?L:OR - Aea-SmPqe>U[<5OZcKO)qIFSNhZr7/l(4\?JI)J;6Hqf^n9-3a$b)l&gl>okN)29fdKL38M - /WY$BZ#$1hgZL$r7_7V9I/8)hQ/rr4\oZ'1f - =](=0[B7.=Yh+4I0'WK(OT9?)+:DC[^A?A]B0Yq)YQ4i(iY,5W==:17s7r/qIs=dL_O@/t=/Ga - B2+":!8ki?$G18lO/WO#.eB4^#7'6F\s0s?Ns1@dPs7tp,s4aeN%9BL,;BbjPH.K(bhu<>f - _SR&ITet+m];B]-9M[QJ.=6F0KLj?>J-lK-*m&5mR",O86jbPjkmBE_SUJ) - :S(hbbVLJjpMTHoh!Z2qelrshHe0(0;s9:G8$Bl2+SqMp.&Hk?,BnT\>-XgJ:OY(Bq/&_dY - ;`L_(;2TMdnA:#H6@P[!$oQ^@;k"-s+;Ce?b!>fC*;V - _u]PB;#=hkn-WKi(bB*rE?8,"qp;$6XFq(\[F]e_g/A\:4C(q4ol@W^mVC3'#Wo$Yk+TESgS[:+`3A7\LL'&MWb@70HR- - U;dCjF;kmG4l=G:U0^4"fuIXDr-L2UFqQ7MW@dYU8JQ_HEN:\SD'@nUIZY%R^e,LK9crD5:mli4NNH.?&hkM0=C#n.H?=n*-LMoWd++Rfm#B(Ok4q* - WcWI\G9VRuo74^GfJ.mLm-^1[CWGZ+XIkX##NGKCkg&6tb3mBob'Gfpn`f(TgJ*f)`=m'`DA*-tVr@lIG;ahjJlTpLe9FYX\a[;MZ*sU8$ - XCs7B6XRJ+AP3[4DpOQ>''O*a$de9m`E6qs6+Fm%'G54"CkX%1\)cLUEc!3)X'qX/RBgT+C - /4+f:OH@*jLddnR[pi:J/V7hrU="ijIiu^d;e5l96!:ZJ[n^p&%W46K9B0Q7fEqDFW6^3k" - R@3d=&+hjNmpk_H27c;PKDaI55Y?$'fIkS?['JEtO"3(_=ZJXVB"[mdI8Bgi6rd797H"E$>J3\q*Yha"DN;RC3f''>Sr,`$fA5]?,NINc)"=32Z:=s]Yd@]g:=4j>d8no - jKMo^QbHl54eQpGC(,Jaj-p].=3G/=ETbrNl..#l - sDS'_\kF@Ge'gHQrV/D;2\Z8!l7P5!J\*nVo(>i!kHSi'qd7ci.fdp1(4l+P;N& - fO5g:4LcLNp6!5q`:"P.4M)Q:kTT5JE@T1cr0!2MR]M]hnc;V?fN_TU/"s5T:mo&%l60`WY - \_BScHE%XbJg%h1aWp8K@6\4KHt6AT0EQeT[XII_,VO.23Pko5aK&!L_*g4T,c-+q)15$B&Hf-'N\JSm8IfH/K\ - C.jus3fN4=VV\2-7b>sV;OFC[@8-0J@amJCdbu'=hO"=jj0`]fR/5j;fV:&=0kHSk&\eorZ - 'G`.WObCKgo@/($+gM*,;-9WSO!OYI]4ZRoqAfnc]I5=/asWEgY7`nRpo*hS'L52d=JVB]. - b-g2FM@^oddL*7@74OdJt>f=Z\ndYV5`oH.K*9N&3:L@WTbk.\uPOd'ArJYV:PX+@Ig9ggF.$6:Cl+-nGqY,: - `]aa?gX;Ds@!RJ0:Mh5Wf\Trm(Ok>l&jX+:'AH&T.AFl>j - =mr8>*-nLIYQ]rDii]O8So&Ul7sTs0&oT3&%FMW]*-;Wl_!!Mfu.$Wupb($ - BjaZ1PXLJ3PG5@*JE;U6h,`Wo?&+1Y%o^9>CE+G`'?>G!-`3%Z9/3-iN`Di$f!@=LV8KH+&/hH3+*9%YY( - fHV(3N.\8G1W+u6aoFM#\&IJnjiKGb:o63u&%flYOTs0&;VQ4r:YHFnba3o#/J/9Ubi/q[4 - ?)@>g#GgM_!&%/N^cN(-GYYkEFm=>%UB#ge'Gg2`91nkTnXL+@aGf=>q1/O]+pn^&l9d%mP - Er(pp:l?Hl2J*%PPeM_?j25f]J`*qF-b.s3hs_U$MDK`\kdrr00^NY\3XO(YC0HNF;<-tO1 - uUa'%n$hH<@$3fq^Fb#6b)0'Gg2SC_b9!1o'Z5Kl'haLKreJtLq:E - A$HJce)o(8<-H9en+_ECa]3)E_m+3RVGB@b!r%k;Cr!^3PgpZ%hn+Qt4)i/]NAeOd`$IdlM - f@RdfF=ZrKGpMS@FjN6"'edX]=n]%kR23^9Dh<'6Qb*0i(8.g) - L'9Pf:_Ph@inc]/R(/!q65E"oPi9`H*+#?r-*BD7q=^Jf`#($G]j[(@^4>ESXTaeL>qH^Q7 - n5Z7YY?BHH@>G_O!9,2uhso\:hkHrioahMM/3P@kQ=p - S@0%hI4`KS6IW9n1Kl,5C]H[2;+$QV,9MnnWb3,!];_cV*E?5!2Co`j,qQRU60L9+J6/ts- - k83$`Y^OBbQ3[99kmm8_$O@+I$OHT=(!l^.`ZF,nD8eY85eJKr+DZdjH+IUa+8J]>mrsWob - :A)[-nHM,%ob[Sn,I.hff,da=7ol^cj+e<$4FrORSm9McmQ@8;mS@mI*GY]$s/ - !/g?/.`@gWd)f"IP5aPQBeK_6 - \?B%!EIV&ZOUTd"VHpGY5)&[.T0!2E#fbR&R - 8X>Xg@)D-P-rF"TiA-MKqDbnKB*1D:Pr)6KE\]jgqYnn-!3pYWr3eSr.gip[?Ja]KnH]/aB - LBnp,N"3B^VrGQKTs=ENrC[C=cE#/jB$l0U1i]>m]IYn1;6=G - Ef>HD;*c[9?7GTts">7#FPpQV;E^k5Hc+NQaH@\,#I\r-70]s=DS%16=>#Kon?i'AbB&7f. - .]fn/&9nhS2Z#.+%I[NK1oCU[k:6Qcq^jT'^+Q3*qQtQ$QLVskj;H*NTZcD%Tl;$o&'Y#e' - $O#6)(%ZhGX(0']l(6SVV[D6aIZOX*7:Catqk/glQ7_DShFI=_:X1aECp>$.5=gA&U9HnuI - 9slTSN:?3f<5udUeB7)Z4./1X2d*`FST&Hgl'#N_:M]+4T"h((/L`!OerS"&%nso=n8m5\/ - 0i&Jo?g0H2crEVMHVJ4gRZ7Q0Z0TZP9ncKa!'HNJ`"QG[d%b+5NtWZbW6HdA:fnk8?AuRA+tG1=\TrgP,2J,m1jc%SFkp&HS-!o - .YWsW/<\[C!IcqlZj!`_Be"EpVH@YoQ"n"]cJP_0_AuD:"ClW`^h7HjpaIobkHBe=e"n"]c - JP]IO@\.LcA"7cch5e/rb3YnU#2N%'U&oP5YWsW7To+-pi"$$LpX+kMm?7qc>G\sT:K67I' - G^t.VcCqsDHrKP*3H_qBT>Ae0Bb^m',A"jK`sg%AYn+][SM3q>'9L!nNONcDXu6))[SD%.+0%RV$4DO+C_cR>gV:uIn"UVOY>7&)lkS>LI]* - =M-nGoFlVu+"c#8UYO/!GVN]goLfMXk#I]*=M-nGoFJ#$PF:ObDC[M(`?>Zbl?fK;;TU=82 - d.aWda!b(Vs.csmEZB'5MX_=_AQb1B#8Tlu&Zi"&3>#8P3-ltl/R[_mkDDq;=p4huZ5,]&_=* - ;gor7X';5`[t,7hk9_-$i+]*dgfq.+eDIFRMiXo:#)j?$EF%?#"M[HJP:bc7W$t+@I!GR-0 - 5hS08qD>>!bA@8PTL\tAc!7d)g[F@p<;HFpAngJK$jrU-;?*uY.=Zr+e)YIp#,:Y9B$-$Q' - !E-&0>`5aaPV+G;\=/iY!g?L?'Ip%56!/T*6.d$QF-\dGDd?^8'=.2jl7\(tlZk4&cSQ;hI - >0e^Q"[_@t<6V[\9jdEG=qfSV[q10-A@n_NM9g$l7ChjP7:%Do&2ITm1!c@M4lje"chX)6# - *\GK*[!YU&ILI=(Q\,Z@=do^VqOe%"14Gst\3&KAt\=M6(`+M%2bP;=clb$+t+FGN:R-3,,A`?k"kZ[LSKAi- - "a5#ZGG.=06\td7+&el*P*ck?*o>V^W`3X,Sj3=uDo?A/Kikla%e"ec,ogg1q8n!;?=fPr& - TDir@GS]Zrgbmbm^0T."`90.4VGn)r-a+4"#bSLSk5##NVi!F\Y-MPY%H^lO*[3eo&L`ug+ - 5cW]@=don7)_:Gr8r7o-JEV&ic9\IQE/';IJOeD8@=f-,q(=+$pL\#T!*NQN-H$'C2-k;]? - 0,*Fu?k;pg?T'2!B,YH8L*0loF'^:H&3c - /UR-/O_)^-7jlcN#J1Dk+#HC4JPLU`7gJBM!UD4TTUoIj)j/.aRdU10]ASerdq7-eS?Y:uH - *brS4>_F%.bmd;j6"ktC^hH\h+3c&\=n,0rdDlgZU\^GathRKf&WbBN)JjgDe(6kG]=*A]R - q#drqpWb==BDT0p;u_%#?cZ"O!:frfMKtBL&[AG^bo:[^RsYos6['+anrQbI41JNJiEWimQ - Y2f%DQDhG#(ZJs:;o=;rpV'#+F9_lcO>F4GtI`DTCWWACh(#oebhig?F1C^#aN`:R-0T%#) - LmX$Wlm7)GJ5YHA)'lLPShkfO\C8kKIu@U8>(ZW-hI]4<$3N'87H6TXLDsIOY;0bl['-I#=;iU5319Yu,9t4?6de/N - a_?-G%'c$Fo)J!RpX]Vl'kc[>Mb;.mII/oe"(WJst1K`sg),o-$qTANR0Kc[3U - ]@^Lp(?eWT@it]+/sdF;19[%)5@j!n94@L+=N_'FbZVpN/+9(c`2;QMg?LtX#CoCWLn)[/MJP^Vu`B - 4Yqgdf#0hRRlUNksd=Y6Tp1Is@tgGK=j=>b\*CL"2M?K`sg)9.K(,[k9\UXjc95a\mcMRA\ - *p,eG>QXOGuYQ`.u2@/K%X@=_7#7`@L)pNt#mEM>n^c*_0To;:q(*WgTI]H6,hYGM,.7O#9 - )I\j863;o8&Gs:kojP63VlKfHg$qBFqh+Kj5n:L7Men&.qG"ueP?g#7@Q,3Trnt_.g*d]FE - 2uhe"dd`)$VJ(f-.c(5/kC0s0mcO$tQ:k%V+T.g-1!ca^SV0eB]@2atb=r2?*ZNMki^JAdN - B4D]ol>nhU*ZDE<[fR2.\BTncVmUci=jl4eC:]Td:ZmsE3=E'UB"LB3iuKpFd^jeUN=+rX\ - r7D<'I!bShGJ"P^!>E.AXZ2p3Kq/.d8/P]8KZY - DPo2Jfgp6T1(cL7B:0P3!'!TLdX6ED`)\9M\Imk7NF$\V+_eb87[0jQnh^8X6,fO3.ild=_ - 65XNV9?^H%$]6fFUb9\Pf&:PX!gLp$6NoK$h9geRlZ[#cQ4,?r?EOqjU\BJaFQ3m4C<_L(n - .4c#O?W/]*o]b2M_rmqLSWaIODPcbM*Y--+c>naL4tN%nYX$9X;8kZNftGA*kGPU1Et(TVW - ?*VI%`B'dZ!?;Zqt&oUT+i%HO2k8EPOsoC^WZ;uAL;nDW5h;JRWq-F[YoH^bIj.RkM("`$4 - DOB#?B">r:A%FpA[l^FmI7`DlrFQ?4aK;;YP-uf7NCRS - e>15RVcM'%ld'B&RD[Q99=uU]b@TMMAR<]s0u\E0LqbJi/.a.8="7L`I0L2J+?`V5'YcH[? - kesn^>_GpSXeS_>imn^bCmImX2JA,'G^tBc`3d-^C00: - 01tI[Tu3Ohq1Yo*H0'r6ZSqQ6%J1C+C>`)ot`76bFt[(#A7aUkcS+C\c)Fh<%49JWTY4(%Z - hGX(.qqMMt*tL>:D#Rp>OJY2XH;/K;kZMVjii]F;_cMSG.f![4g6`B=1m:Xc+0MO9R?1CV9 - DRI2^L(KBYe0(W8NI8]+M[GijVHVdfe,Y"i%kOmF1=0N%DR_QpO1)K5%cg'&/V<<]>o*8kFN9 - CXqTQk-J;haZ,19ZDD-3n.n`-kKb+:M;OR-3.8;sUDDpSp?bfO.>7ld=^o8O55FZcg#D]f+ - h]eOo@5?(35n&-apibk%SOc&eME_Wn1'S'C4Zh*J\5.A]mS3ks=W/F9d-V;a/===>^3"p7l - LF80_D#EIsS?_;-gX0:f[imC=it_-nV>%iCiQ,P5i@&rWHP_@#:P\5iD<81!aX=L#U)_5Eq - e1B/$oTohb/_-+EJlONY;.c^>N)K];%Do2'G^t_5>j5^Zt\?l-V8hF2m,BOa^g--lR_Q=,piF2_K`VC3u,?sei*CVi)Ec\CZ\$\#%fo!$e$G:pDDX- - ?:1l$b8VHLB%Wbt[/&V*\UA!;i"qafc]I(rE,`8%Ge]mVMFT//C3?FZQb32TO2"i.;Z+LD$&&P7WgATgrq - :9.8C*CUW[_1]0]DWrB^niO<0PTYd%aiP^fI%2/H3esLVa+DoQ_1taj9.>YRf5cC\T0e3o* - *77:C6W5M>O$ - O5Tl%d1!f#Is4FGXVpL/W^Abg\SbT+>dM"iaCSF/n$7:cgpiF0u5L0)AeUi"a?SJoGa\NNL - GBlcZp+#4XZ=V32:7LFb^Roa>]oo>/:bO`+bZd[_*Yk^L?`L:Hr&OdHg+0*#l]h%"`KMN29 - EZA`D6Qs`6/l&or@uJWM^1(K^Z;>.m'S7.:+%j@0 - 5@>ZW,/TG3rJti=V+Ar>FLB-1RD<94a%%.hmH05%sH+H`%rts00&==o)^hebT4G@3)gQ~> + Gb"0WH$!;8H2.E#,k(WEmu+jINgJDg*q*u>=t!U*"pcKN+d=O>";8GFVsnJK$3s0rQNOSuq + @2=KH0j1?*@FVA7,DIt9>"rC,[4Sc9=?CpZE.X8)iqcO>P?9gmdf-poDY93+'mftR$m->la + cWB`K-9:I]W/P^3nk`4_k'@h,I\Zj6P;]"F/kQio\*A[K!oBb5`[@<7e4cIXN6pJLV!7+Iu + A3G_X':88AnTStW@gdMdgnB.,<]ZpnK_)%$`oX\i2FL;F!_CD7PUTs+OF)MNlX-nKnU7Ub6 + n$4@uY;-8't_?*8D'Gd:6YQ3)iKnX.f/:b)s@Gr(2$QuQK=EoS;!_@)@(7q5(!7Dd>KkDeb + J@%dSi>Dt1+K\LCG_X':88AnTStW@gdMdgnB.,<]ZpnK_)%$`oX\i2FL;F!_CD7PUTs+OF) + MNlX-nGAmlsHdm1F.go_D;lAJ8=kl1Fl)ra%-PkKkDeb!8$Ralb6jaB)e@L6Zb%U@Gr(2$c + "WFgL`n_)g1AP8NHY`X8b;=TWmnoKa$D']PTHn$QF6LQ1;J002V\2ihE[H%#"lCRf./"^KK + 3fU[,t_^,K-lVn+eo$dVXO!3S>dFkWsO]b+BbPPDt1^blA*:;*d6bmnW + ZP*<@CrPr[cKnX.f/-+9AI(eXY7NL>B@;#!poq1_K'Gd:6YQ.pLBZlXP(&#Y\:E.=@hDnsA + LV+(MKkDeb!7r4E4S^%PD>mWqBIbM[/3H:.!P%:jf-]=cYg!YoSX,HUPa)ps(7q5(!*o-pW + tQ-qD"C8V*PkiL6"/^i$4FX,LGqpuYctrH8O6eD)%$be2+KQb:V=l)Dq]$tJ2MHit-nG@Bm!WN4s+P=PA0hD + 5KkDeb!-ddVq`_.!*kS\?@WKaXS!=[%(7q5(!8N_2Q=aQdh,m6'Icp,&ao9p,(7q5(!8N^G + D5M2ZO#=,:a@U7q"bhYC-nG@Bm!j)272+!<^LNGoAn5Js^:3Q(r5Y\=n-k!Si/[bS9Xb::K + =%YEcM96Lh2Bs=>7u?^!/6,>-nG@B*8!F^7\3Zn7FQmRHoi8rDF!.k)kt4@kmsR7jS!=[>5:M-"_D;lAJ3+7S2C[>Kjicj + L$QE(rm0dL$]9/*PGV-(oTWmnoKa$CppKsJYD"XM?D6^W,9+\l\mm#iKnX.f + /-'n,^M-$?aX`MdG:RK/P@U8J!U`GNkk_Qpn'/Q=a/6*1%#"lCNl)gV-el8n1Ai2ID,r)K> + [L'bpX[QX:C:\I0i;4@Rop.Rq*Ts+N%OL*91Y\0cH?"'4(;O#<8G9^>!H,A*i%r./: + U^RS<^]cgn0"*1^=m9R)f]J6+@h3O&1U>%ZN2\kC5^P722,IBT0%cT(a,C=h(`+t]@BMW>q(PN%NJmGu0YXhlMoQ7.P+soHFR)0(fjNW3%-,ra`mVR!N`(^ngH$h([T8 + Sh]c9)daa8m7U;:&7;b==T]E5s%CN:55iO1C!#O_12qOd,d>L_[$@CU8Pn^VBQ>+Z^KE[&5 + i.Ba.k?%kf=uq"3f^%CrUaTe#"VO+0!OuP+p=U4hWf`=5(:IA2FEN!X`S(3r,#*G9MgM5'! + *lc&SQQ`!]&H@kYC!U=iag?AVYW1(1E8\M@0*lGhqRm.7&j;Aqbu.GH>-Y9_q^7k%`i#t\L]a[l*#[]!)MGd3DU'&d?RF@0I;\DZkrU^MNS7]=B"DS0 + (C;7:Ol;Zff&81PAiJ.%CM1-XCN'[&3'P2:g-.k+*!9kG\?3h`bX>]=5k\>40j6e*u=D]Z$ + V\8m%tp>gun-nG?7*8N;,g")D1&G:dt'p4Gql/QYlO0'L&=YNWupqm=N(4ZdQY"q)8-Y9N. + 5SM(.$^(U9,)GG)/?SD'0:;qNSaVOg5#2+XcG)<,OE=\03;'Gc3t\N0Cg[,[HZ:fm]mmG0tH+r + Q`DPBmSL'3e$_b2(.)m?l0kMGU"N!1_cTIC;QtU((#hTVnqaRpm9'DIcmA&LnJ1J5c@%Sn/ + Ll>3V\$6*E.kZ'&SV\=0b*U^.,2$c&`iDr#&'MQ>0#]n!h),t6O^YQ139gG[M)'6V`(]7'[ + h,[KusISp.r.0>RI!cTW-gC0!*>a%)2iU^"?RbdY%'I@ZlDQ$j;h%AEOKa$Dar\p`Ze=g2P + X&mV[?EBqNg,u)&_D:MCTLBP1J5Z]]::!JY1cO^Ao:JN'.5%/nW*L(lM8B=5lG1uM(0tU:9 + _I7VWKOEeYG=.iIhjO`!f"PnE9cBI2MI,sc)I%)OV]Ms:?G5cWaDNl(#22c!B>48bEV(/OK + IA!FnO1o=GF@*fc)FjW`_n*&/5n1;hm8,:,W7N10Qh[6-pmFAMVWga;/S3GPTHlmC6OEKa$ + Diql`^d\cOWZ]l(Y`+[+Z[=SA\(T0GA<].]h]!1\B8eVVBpTMS+XG:cXkV6g%L.JC!/K#.Q + p\&4[^,Wkp^A;#9YJu2;s7fQTGNGS-n8Z\_\YQ2Q+aN!aA:OX1a?F0]IPcPNRf\OUa^,j!: + Js@#a(-j8gH"C6dA%tEdWokq#]N@af^L:qYklU8-U`g'Q^o=(J`[3e=94`p*bC[s5Pn]?-5 + *i%L!5dg\qP:%f#oTgNZ%0G1*Sb!nn_UXmCTFkTP!06.$/[^u;AMR)q?_PglRAXSG]9+\t33N,M,456&!t#FOOds'1(.8).oN.)TLf? + tWpK(64SfWKPu4%:ZU_?+[ef,gEuSs/$r[0imh + A41o3EAt2DerQd]R&Ac8e3dXF33m>o!fN1I+[+@S'l'hG#QkDA^l_p7*j?LDQ-r#_;INWWm + 0dL$]9,7]I;374.k)ktJ3_c#F2#U=^ED@_$^Cc3UihM&FZ6U[S&\mLpn2f";?MUVi5hK1o. + 7oV7Hp\@VpsYD8r@*t``tR8:chH%JAMA,A5CML(,NTq\uN4gQ*?DqO=2X[7rmLVCGEUX8Jh + %LV3i1c=Q&mFMN^-!2:hB(/;48#,PU"S(8Lc4FAbn4lLA=07I_=\Y$GA-SZ7dGYQ,Zr]7%- + ,gl>][WV7ee2+lFk)<>[G$T=!F3O_Dg%t0HU=<3i?1ZlY8T?e8Au(Q9T-u5jTB*[s;qS/!:;&qBc\n&&V=.)d,mAu+i + /bNf1G&h(2//g,TQJO'X\*/\]9-YN"bd-_Ock*l8:kclD8X.%!+A52c`gUVrpYZ$4FW]S*L + t1Un/4S9p9+RaZt%p>Hi,&jg0lQHfq$@qUO,o(9RW7Io0;Jp@^/IR((HDY20cGPPZjZN_9$ + @UN=u?kGe;..k_U]5FF;Z5PELHhfSf(K`tj9-Z=j\YWX97ZjVK\Whm&f.k+6\R?:r/.AHi% + 2m[;-3fnjQs3%ll#&kSe!*@:>-H)#^7Lt]PUORXJCYYY$NZ)i:NV/"WQoV0 + 0P!mi5CYhM_/Q-=m;j@%O@uMZ;/JKG9^>elM0(j&?_gbXK/I.G5h!dP.)(X`D'bA33a]PTu + DO6QTj]n5iTFY8WgbrCfT^$rFdONTl/DWP7);6Y&6l"X5=U,J.k$=fG+%LoN&."P#Wf6]i[ + iaS%iN5[WD;M%A$Oa_?+]"St,[0-;TnK<=]5&o40/dDYI417%/!uY"&"gr8o9.X%He$\0 + ^m_\JCClP&sR)"'e\ND-^L?;6k%ZoLdSX=<$R,s5mNDG@(\C@0$L3m^mNk-;X9f7m.<#Mao + 9HY+/j6'@t79\uOlTO50mRN0UO!a7U53?E9-9K.[=1e\_7W/h=0LG + )QHe[-8M>djYQ+`?mlRj,-I3D6Ru7,N8TF?+2UNMFpQWMrE7_h5n$ABD[F/AE#Om<].k+50 + TA!DVqY.^8F*>ZbV%B,%=:.?&!,t64I$(&sj^/-fh`4Js#H+?OU! + Osb#gJ!OI;^ZG46NHSn<`0?&&"L4\n+p`*$c#s&RklTB:VF,4Y5l49+['-2=>osFATX=KXH + K^qde\8k6X^!tg],98M^`aXph8!'pu&H"Ed]6&0HHM=jjL%r3^tCTGCKZ.(4ZJl[SF.G7u` + \.55a@M031sR7Wta>WK\HJ]rut'!c[)5lVfB);j1al:O`'cq?thF[oZHU.[6'.UgLNONrLl + 3YQ,Y&\7LQ:N;C6&8OFDPr\o:$HE5*6Ja=!e=s6%m]+(@K!c[)Ek/eCV8q#Vb7?qu>&(S?h + "Z#A-%#E`JeB6nqRDK#Ei6Z725HH,;7]VSrB^oOXL$OV-=S.LJMKEh6QRrmeJ-58T3mlru5 + ^gpF;=o1=o[_A:6aocXiO9p0c_(b7%mA[jggX"C'M!$ + !u2EC-remC8*OTC<,<9'N%TOs54_H;Xr9s3n/s^%m>u!c[)=.nOYd0:$&3ZQ0'D,97/&PWr + S.OqD?'DaN?eE=7I(i4tHa8O&6A7#%]PFp(>-6F'^JHPTY_DqVXS_?+]7k_?@a%_YaXU-@* + >.']$OK3Sh#@I.2VQ_]Q1DA*?b^k&9&5>(:<$s"J7#Tad_;D`mH-9t?KVQ^'%m9aT_#0UPM + roc]l7B\b9I8YSChm.50K`tj7-Z=i1@8FuFrR$Cn\sh8DMW6Id>l&^DI9<(i87;POg9Gk,7:FM55Q/ub@#J?Z7UIOf0,TC]fslaQ!j + K'WGEG=&UXN+joH=Vq9./&P6?nC)!jK(RcU*Q,Ss/%-UCZhpK`tk"fBreXKfq*&oLC"]!t# + FOeui9h=;821HKpPT^]tPhJ?@Whk?AY/;L7Pf5/7C;(4`OC46BZ[p4@D<++8YM-nG@&HZEp + 0>:&+t7j$Xk$4FXMDU/1rKtSa;oLC"]!t#FOa'Gc5$I;dH;-u?#-cl[[d%k&t/>Pm9gc_@XFV4IVL"+3@B! + SGQL2UeD)Fja_U6=9,er$JQhoqP+Sj!JL\bleq)-/IEGZ%/nQYpjg3_Y]XX&PiN4AXZuO[Io'5I[LR + /GS8;X*A[MHl"8kGn!SD_u[7SIU[Qm=HcTT&c'Gc5$pKsJYCgd]_V#K-Or'=`lfK-ZZ6F): + sOqIP3&+BgX"^pZ;`pq.d[TJ:McTT&c'Gc5$,Hm&![]h&5<.F<:rh-IqC`V=c2VGs5aq4N^ + #P`qg!jHfhIBm47*HOOOld%`0*u#K@X^q#AF]&G.cTT&c'Gc4aS@k[6Ke8L>qK!2:#DE=h, + q!Vf7:AtbJ,Qn_pk-EbYd.cr@Z/*pOad.J&+BgX"^kn3mNRS+Ss@%dEt@aFK`tjqBC8*E_N + mh:rOE1.K>I\oZ>5Lq@F%M`rOE1.K>I\oZ>919Mo&Lc:]7g/pk-EbYd/&r!u?TmPTK[B&+B + gX"^kp9cM860]cc/(k8rOl$4FYlSfRDR$@>X]l-DN.*u#K@Ps%/%>84M<]'@U)^`O7+J;:lfOW'C"VIo'5IGkU<[Sa@&k8rOl$4FYl_: + PSg'XgRgp#dCS%gi][b/#Z./.-`01A'`Ni"$/&^ri@qPERY9[f4VUHp_.d"Y_=i1J*6pITs+Nu]YA/IZ@sln&c+j*n-t'T@--mtn];R?\.$t2H@To + ^/-%1j?5])G@>e]arm1ap6X^!t3O3(0Z@slq+o4P:n-t'T@--VKU^MN9#3ud_^c)rCJ=tI]\P[4>84MDHKYl&^c)rC5k9*eQtK + :KMpb=LkQ/DkYQ0iq-g,r:_H'60rLjJkL;F!_?LE/]/IHhr]''Yf^c)rC5k8sQSRYOLC?_f + aoDmcj21PYTb,BgfIt^=&.fgb)"oE-8QFKCFl46%n2dO]$hjhjTf@L$g+taS`ge3$K_BQ*Y + =9(pm3P2)+o6?21ek1;fTa1@I_?*81A)6EK]4Y6Q[FnPoVa'eRhb[DTM7I_K@H"7FQ``9t8 + R).e#Pa%j!f3cRb0#o.e"!eQ=hV2i+*rHU-nKmqLodX.R$[iq=QashC67$C6P&YqK`uE3Fd + JD\]KOr5[Phoc/H)7IpkHWe\3m'3mJ3Muo/F1#l$?9lAEj[g^c)rC_!e>+>ZUdB,^8"s/0Y + u.6P&YqKa$sMNV0?q&'dAIhYjn&AF!#R^eYX[5YUk877E[nGgk4_nn#3-_uop*@=bf;=L\/ + !hO36.26''!Z>K_XLVa,Wc1rE;\i-cGV_T.B+Kb;!)k`,+!bin-jN/I">8><%bL0OCZ2m6V@ + =e(/h&_F1b!%'KfS^%5%YY'aS]?(&DE'8dY=JosJHc5aK`tB.jg0l'[Wo;P,'rKW/q$h]%( + (Q%!\'!i&qD_E$`'N^;iB9Q!@X_I5\a_g":u7](.]=J4V@o;a*H9;p.,0TTWg[R@#kF%3#! + ,63Ig`n<3o'r8/?m__MgVj5]BS&+Iq(/!QHpnC1Jk + 49!5f0/$Va,6<%M_?*$Oe?Hsi4VB!l,6@Rj_?,9c;ndoJ&jY`TYWr1Fe?/:jO2NaG8-$AuY + ^eY*8-#WG_?,&9,6=Sq$4CP:B90t7l)m6]J@%aR!T.1`?;^RE'MgAffcpO]UTf90r7N<78\ + 4-3('#$an4RW[ZJmejH$SYs*XNr[-nO;i8E8FqLZbs5!_=sY(@W/f,_.^?@0+\2eZV!FJ!Q + F + mgYcLEfcpNN%tt1WnU?P;Qr>bj4VC$T:ftN!%soRZDXS;l!;r=VR7dI55Z_lWS;^c^Id:4u*dr.og9l?AbimccA!X_-Fld`6hE(M>IIMjkSp4^ho + V[]JbgE&QlX*G5]gj8ml$?*NZUY'f0.C)WZEa]oD4:FjDiaeJ`s,;1gne;g9^QXW4W,,Ld% + GP4)s*7t!0fs]P)uUG+7.gMk#T*2*!:Wfu$[_0gK@-b_n\jpBo6Rc]*VrE.=&,)7Xd1R + )*=_G&/T\($jU1`gl>'FlG^$0cQ`amTYb!uj5WT9K@2g-9mb[oXGJ5ll6Q[4#E*tcpRK`Ys + CR5q;h;ao97fE28pROO`6N+[5i*_TK!1sD+Y`4WXF)pD50?P%`mM3sXP/;7ZNpZJ[,=?qRZ + fr2>BjKKI$d63>,B63LYt"l2%=p(s\T99-IX/r.Nt1R/0.f@bp%)^+B$?Xip"FPj6"VP&ZpS,Lsk;Tm+LNR*usdf+&5"Bl9Y(EHAXiu!#0 + )_[\d[0c[S,A3p4+tCTSVIoKCT/BkGZWp5O!L3HAZ8H4A]\]LUt[A:'YEp.,0sR8RIs?CC6`iG7I:]l[hH"G",T\$48arp!SZIJC\^_\(HN + @lbRXa-^;TN&,'t0f*9N))FKhM)!1_f#=`gmJm#Jm/MG4Du]K/@fP<4`XF]$].kK)CjGmLY + 2B6P\DtVrWe]"h6g^nZS^Y8g.siNA`8aL!lg&IoZ$uW2K3VdTlh#:X>a0.&DdEDEkAar%o9VT2bUEE99@7AEMd;+`NKs.2Z+<*]AOE#5d?lR)o4Q'j+BI?5Z&@h + +0)q%ZHa9q4G0a)67l(VSa#i6U!-aiqt2\$#2.D>3pqf@l*na1"h9o$Wdd&c`nj:tWY%,u6WX]L)pb + msNA:AjG]doo[lY5VF:+d;/Xj`]h*'I#gL*GL!R0?%;F/BjU7MgV=_:CV(2PT`%,Y&duE\E0k37i144EfZcJNKcqY"<]b%e1H\s1t*Lf-+GtS+42SLPHLtnLSaT7^p%u's.[IA1!^J5k't0j2im7Z&e#cf + ap\iZOZ#>5i`kHlVl#C4q\9UHkUe+iuIsnI1HR+Wpn65(Ls1SSs%_A+tIeUH;HFU=X3&>`) + bbNn`+9)%-CkJR2+4i0&dm$:9j8pZJ,qO(mAfA3Sc`/4oa2!L]>t[[^&C)J/Tehm;'p54Q + ?X\n+#%_HJM\L4CM4$?[ZRSPJ!4'+SJ>+:h*SZhQ_"VYWRR;kcY1iEFf*Bf"_-r_<:G"9> + og(A;7ldhQQiFR5>WgXEQ_q:S^#D^YUqWN5_h"`>"PQ*6[^4qB;)[Js&"D$9o+@1c&V)`-B + #%Fk$4VO5+Q#,%!n,-X7D9RgPrH?Cs3s,ZlC$UC/UYR%P<)7.ud=a7'Sbl@Dd'D]'0>7@"/'+p + 3R66\tQ>6Q`#c[D(JF_VYTXm[GPj%dL([ske%aZC-X:ZELp:QIRO6cTs+g%cPt?^=I_UGD6 + Ee'qgA\l*Dfjkf2mqLD_01uiMPs3g2B+S.?D=ie6XtDREbH'JNZ`aGI?=V!)abi71/gc0c` + uoVfR3kkp>^U+Fk.q@?DJBBX;0Q9e.:*"2H],(+J8IA#>`+:M,@Wf.UgP%0<7M$B#V)3pD] + :1G\a*/-$2W=JVNa(t6bUH_11Gm5H8?'LQ;kOrcP/qV"^8&6T!NU9I0g?adN>l*46cM$Ml< + E!:qa$@C'cCm45]p`e2V/+#8BJ.t7WOkRNebY45eq8`Wg(B\W8_BL#%1a^*cFWOl5?P`f`L + ;G-/4I#KIGTL4/,"*2s2%#96nWBW>H2$AABYQn8KGT>2>5=sKD+mjj)^!UZP6)"=$B'#tB? + %;98Y'dL9)F#D+H.^"da4c/F>/QDN"#<1a0aa(!()IX,!A),9P9RaM+73rr^-M<$6WS%/CN + )cN9Oda5HV68L9GC-@0)#eKge%L,.T5W%W0J9^XIYX!XK/@M_t0QEhCJImqtbt?2JIH+;?4 + Tn9JNtS&EYJ_<0b'[GSLpJrdbM;4gVOHa8PVR;9ReIe*UH3!p$Mqa>HDT,q.4rT47pE"j(t + kp+2YRDdE15Hata"V>2Y(:_nMP'B5W7Y^`"0581T!WVUQ#2`6,9TWg + [d&"1A^6ZYu_f@o\/!'rkYJOCdTrLL_%1[XooJ0:Mh5U*p[bXdrUBq8gYrOqo/)h9sV$RFq + MA%J$1ca^gRatL-Zh#MOo=hjp$(e% + YT`*^Pi&(iL\EH@k#+J"URo\\(5A@n2!I0L=!q(@cd2`pN0MD<1&&;*X(3&(2PMg`Z-1)!= + ^cN5GE8:oAoJVPrnUjO<:BUYq'nlh(n7O)ao8fNq*Wc=(6=BnU9JMBo*h?$E:UT?c!&%/N^ + `uW$\k;LLFkUm:J/9Ubi16\kOjcA7*<&Iq;#nCS'Gg2`$c_fmM4A^0>=Bf)Iin`1&VUBNoc + ]O?aiH19oCmm`9&EfdY)^S.,:]R^b@0%Io0Suf\ah8.>'6EQR9i(T,7P@7puE?4It[ + pGQ-oHL9qZJ)[^-J):N"M-OL,!$4HXkX2]68M + 4B'BBi$R=rJc1B>p]ZSV)'4C!eEUF!C&_\&ik"[GH`L^/b_Ym3[=U+8-f)""/I^[$i&sH@l + g#Gc7f3+W0>!\#>5V_VTT7<2K5#OB236#=opN(@]ub`cNqT:KC + N9S,Y[h`Q6oK-`TeMZFg8.",-pV\(W*J*r2QPDAW"jpI(R8!A(DS]L?60s#m"$9uS.-BC>c + pBTF@lX?]8F5Tdcl'GaN-fSZT)5LnN;_VXiLm;S@m!%t%NJ;U7aBl2k$^-,[@qIR*"O_r)? + I/qkq=?oMg&)@!8^[1E%@@K,-YMasqJ0PaDd..?M1/-IZ3P1j^KtW?)'nAFS$Rcp'&ip!hS + RID4mdELZ]Ge2I8PJG@G + h$E'"C7W4Uq4EXj0t*0"c6,dOkeEnT.EZO/UN;\/2bRNL`lcNARq$4Eg0$^8YWlD]ojKtSq + `\@27`U9^?=o/DV.&*ocg?@d2g@=]Otgk^"UudXgp$hbo3df^0'sAM="6lX4ia'P;7sU + 'hCh/('oMI9HrZ]QY3[-fUK$L??,u.guDTdC0.eH4O==ILVa*Yn:EC^:#*'uD;T[Vp85F$j + lH_[(VC[1%r+V#9-M5Xh&'%u0t'3-'b-gL@Ih@mj1I?5X$/.`ds)j>! + qSq$^j.F&0/o + P]\>@ai_aS044-nA:JYre@!gCO$?J;c4orO[NrIY#'*:nd%IU.TKEl>W/5m#l(9;L<7mZYC + 0+CNbRUWT[*o^X?S^r6r6u.B`?As4]to*Y1 + umEKZL`nErfEMOj/bnKEHo3K'"QirC-8&Im2]?_Z&-2#Poi$YGdSDqSNu&Mo#MrP\oEO:OH + W58muXQ?='>r5bE,G>CUl!ED]g\JH!X.4am49D/*7T)qehGe]WAI?;Vn6onOn+(PN"okS[k + c9\bi$]0JN_2r.SB;6fJ(hQ2<'4tNnYAr/6/JAMB(1r.$mhu)V=ortld>$mRs<(gm&q$Ggq + Ka!'BTo"-YcgRRadsGc.?ZWLIJJ6e9*gLQn$[YqNl-E/`hC'od9V44_:KCLc;NHaddSP;NS + uCXcP?]YTOc7YZT>8-;=U9K>,8q2KqpHW/<+R#bsPBQe$'Y#(h$O#.1'DHnBF$nI + kl($R%W(Q[S-ni:Jp%f-nGp??\uN]0dFShi`i\^ + G,];O7:Ge!F@7nV#:\"D$4?uA*u"C]o#beA6orhBD7/3[n7nf5rZDAJ6t(XsRq^lRkFNWpS + k/&2Q9J;>0E"0D["uR#!VZU.pFKS%;n\7;PolH3;:nbaU-D2"(/\0Vi4`maT(_QXY$G8,Sk + +WOb*Q4YB.(;_1A,9/E$ku#"CGdTp[$]2J9iC[MDVKD>OGH8*VF%oE$ku#"Q+G;pO)FV'@d + c\>j:r2(54n!IIq/$E$ku#"Q'P"pH8:c.dW_)3-N&*@>e]ar[\6@6t(Z)qi$AYk03(mhFHa + D/!nq;'\ab4oZ&D")[ZrUht-C4:3>iie^Us,4crK.f%E47mZM\>+/cnk-nGo:4\a^1=L^a` + OcG^oS8M3V1X]'KS-$0nYWsT^E3n"HIsIZ4+.^#?1uG%375@2ZS-$0nYWsTf13(W!^+i*jO + 3Z9]>&O"1m$N*5XZo[6>>\KF'`QV#4b*#MrRLf/MSdub>86e*FOQk\^o]"9&.24#\0jAMUO + !ncEB8<@7:B!6!<%`]gkrrMU%"d2D9T[hAnFM,c"co83`f=m^nS%:1.m%j!k4o.5ZeFMER1 + dnU#2CXePNm#m\AF8dS>:j!]NtY+=q,l\EBbSSZ^g>#bNOamkg*8%)2/72im1?S + ZYWc_of0OWn_38QpQn=gC+d0VWG#"Q0VB/6/>WK:Cl8q)\9p2J?=qOLedd + `3:\16n2V+m_lME#u8;-4r/-CVI-Xgd_"O/"3LNGFSlYk]^@Z?/tde-MfWdc54hckX^E@Mr + 2r6Vbds1)Y=+qNX^G$L)h%\mLUQe^Cut3n6k"CXTZM7[JUO],cS/6=dfeH[bnQWTnJFbDCG + @=qikUqUB.867:L-A!1^%f-Y^$niq?sSGpk8OJ'!.mWn&n<-Z8TZ\i8e>7A")[>+Of7^t9I + X!/GGQdIs-4/Z:V[PQ?-I;U8D/k + [Q.5CTH55e+3[pHHKLF\pDYM_5^g\!Uu#1.$D2,P^Q#9nA8o.'F=##bNa3hPYGX,/]VEVm0A>c9=Vg5,C5kka + HER3mu&#s9i7Ak3>=q%l=1-g8B,HrdmGH^U1@=dp9@,da[#8X=Pd7'Bs/@673"Z#@(:U3>\ + Y"H'i0r0mL0:,=.=AiE;I + qLjWu.d?4PYD'0>Dn*BYq/t?>YU932?bUZ%JEU<\q6aP,$brWONk + /j--V=.),IWNs3a8NFF_uPn/c^VElPo^Na;l"-pYmiVOnZm77+7dcV-Z/Hd[%kL@+*6Q!;V + Tf?Q2\]%OLQs2\ECaO*iL,J0:[E.R`0q2QTj\U*\*W6=>!HR0[IRnQGN]snjO5d4S2I"obr + .M,6*+M$!AgseOMc%PfZ1GGs/QNL9h[JK`sg)02:6.AkcY*)LA+N_D8_`fc[EHV`fE.+[$k + .p%[*pNV0>$/m2K@&q&,'&-TdM&MDVWR+r2@7R?.9*Cr,b@g3*\(us2s!*Wo]7JZkO8] + Q*6XdB>K1kqDIobWEh;2VWA][V`s_2RsJdSk]jX+eVOHI]OE<@&1T?#\7a*Wmcl7naG&[G/ + Z**AN)Unrn4,l%8nEgr]TsY,mF!MN_?-G%2&#`>G@*<%'RJiojV;qpo:!$`+9(cG=m>6+Y9 + e*_N\ml7YWt6qcGVm;kP%eKTm$i&O1,sq(T"b?n;#UJ8Jd/-/UV\g6gZAt?EOtX#aK$rgp; + *:0D,g$kOdW!(6c'u#--"!Gfl<5&iJcD>^a.\=@Pd!3goEX<'\K\p8dmWnTd60]'S< + ;FA`(/sjY$s%9I>j$LF`3Bb]gfQ0p@=dopeAOMBq90EC#^Ye,RDfqo2#kRZln[+L!#IL`X$ + R]gn,g%ue[H1oogl36h6q'0r + 4->JAGVM?5fSF5d+HR^XR#$AiE>JH6(qj2/2)i + \\q@e^3jOtpFfJQ$\ZZ;'DHnCHP=p;kd)_%UdTJ^EE2#oZMe/9n$eU?3p\XMJV + 6E4`/2hLoc[IcX]mr:+R5<)s[%7 + plAX&$u8k],Q+aP?u$9!mM(pfR)X*Q=YX@?o(jF"L+M9iqa;HP&@#+2ouVT-*["B0JYpC)s + 7HWp9HpB!pC/XI=@0X%BqBDVT?V6E4`F>NYTmFl-OLX.mqM]]0j["U854W-f53U,obY9hpM + Hblc?+[a9j!@<+JaiH'M0l*nl + (T4_58A%'?nl[T89a`G+;*5$9nA7DP1LYr,u4?A>_9fo+nq4YP/79/no\Ftc+QpQrb8`Wi`;oE9X,GRkG + 5S5:iNK!ml\m^9@Qrp=+VoF1Z4K'HQ@tbdEhX,r/H5.Cc?`>!#tA&QO6oOnu + ,@_?/]f8JCiGoV2Mr7uE>\MIffj3P/.oeBKQ\bW]V>g%t$OR8p_,D]K8_^H61lFcQ(;;id@Q + mjb=7sq9XYLdX0X%Bt"_>/3M^U+f"qR35BDM,Tg@I_ckrggS7eCEZY9'/)<$Appk4SN#M2h + 0o?Elh*JM:2Dih!'>)Q>kqPZ0RDd=^lCm[$]A[Uq:qp + dRQ\\8f]RO3.#&!qMQp+'G^teY8bsEh#"HnQ#ZJ35dm5!3h&(O@U,8A5kXQ@O0d9d@9uU`S + fGE)&hTi^5i@(,`'*/$GLdf^JP]X\aC8WAbNS/DQ$Rc)_'S@*0WtYL`mnF!WW#L_%#"idmu + L/toAL4Xksj[e1t8WgC3BPMXgD05:Wd`i\\sbS9@[B]$)=^ii(Z.McX6_F=RBN0'IDK\>E% + tWl)>_seCGn5a(;cu$eWW"H0R&eA5HANn9US6BB.")4]OmEq4ZIWU\f]Q;/]9G4!atKk-]$ + goLL5A+2?Z#@=_g5esHkemus0-EUdL?5+1usjg0lI#'b=`,"k'dWN&jB3m;BeQ3iQ&K"P?_ + 72-LC[Z?leBCb+9g63dIRMsc^^0K[ML,"6d0$tjM$O#43'DFW_fJ)\a& + hGE=:UM&"kG5S5N)EHhNTCct*F?N^Pm2`K7#NHE&8X^-h"+nQ2-kQl"O$V9F"PCVM%!>O;2 + U2^QPe^9Y@mFo;h%6hU9Hn5I4bH$M>4]6)55oa'[S+n`A5Zr + "ml&[F4FVdN_rn$W1HfK6eFPK6@,,t\*>Rt[i=],dTX=\@HW$K`/45c?B=8.XZ*G`GD`@Y3L]J[7]]B0X'G?0$tjM$O#!r'DJS6gK/`V"P9V91G$QCBc + 3QbWUQpQpeTp*qMWB?'G^tS*[^=Nrq@dSc]I(b.U?F:iBG*=ZKjhaLdR+JOX6bRgS[;>!Rd[r5VX>H9nOJpqGbC;5o,NL + ,g#q'BX?HFZB$RVDASk&H;DiEO"pe^q39.:+%n@05@FdnSW(Eofp\i=V+AcVsl>V;u:g)eQ + @$]L(SDjoZBK]XRb-iZC*:1hj]u]K6'W?Q Q Q Q showpage diff --git a/testfiles/cli_tests/testcases/export-area-drawing_expected.pdf b/testfiles/cli_tests/testcases/export-area-drawing_expected.pdf index 9662cf1f7cf6848de78bb7ec0c8a5091a8a07099..3a72577eee967d0a4458d27a6d552afa1ed02c8a 100644 GIT binary patch delta 386 zcmdnaeVBVg43n^#rG zG$lGRdp?81w=6Aw`3cO2`0RdXWbRgD@P77r-VLkhQZH)>&CEHh(>7&x8CbPl_!T+r zYv$XOYLipTQ?Ffp{PC&O^3)A2xwG`wt87hOIV0p3OS^RTx@bY`JH^M$v?Wb?!Zynk z_c3wnxSWeuJ0UN4{BS|N)dvS<_d4yleV^SI+f`hiI+6Rg^DGbT=dM;yj~a7TNl)ZD zw(N^tbCod1w*&c;qgagO{kPAnW(wWWQSs|^{U&e8C)?MqU-zXyJk;t7x1)b^&E(ZA zK1^l?ll_?`CkwH^a}04L_1NdN!< delta 400 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 Ib4q9e03nK7Qvd(} literal 13995 zcmeAS@N?(olHy`uVBq!ia0y~yVDw^OU=-qDV_;yI{bkO41_lO}VkgfK4h{~E8jh3> z1_lPs0*}aI1_r((Aj~*bn@^g7fkCpwHKHUqKdq!Zu_%?Hyu4g5GcUV1Ik6yBFTW^# z_B$IX1_lKNPZ!6KiaBrZmd}wcpZVkC`4wxneb1}DD`eg0J?GxZWsICB6Qi7VGWTgO ze5Mfdld*Fx1N-#HdUp=WYO<>oOlRO!RopV+k!qok$7b2%d)HsTT|ITnq-WoczgegH zwr5*!n%kw%b$h>wF1eT`C;jtc`M%FJ^Vc7e`T2L%>z~^mi@V)fw>l>4i%n?YIftce z8fh=&9;5zf3gbS+^>- z8gd$DuR7$t;Euh>yP6kUf1j7$ZY?))W}43w^MAS7|2nk4xP}JWu|J6xm@M$QFs8A- z;@iBX{kLR0PS|(~KMj?)FKk@pQqQtxgNB@Fxf9d0#n6~Sx$CppbeoueD*;-)Yy+@NCUY(y~XTNII z?Fkc?oRE0c7+_FvkNduT@`-mIF282&`m*mwvHGV)t9~V`cy{$HT65~;+23t<13SK$ zP1UGn{MvouZtld+kblok`On>P;hllYuOh~nzbmJ4v3}iL#;)wrwWGu3|IK#_An_Gh zPmCQFp54eX(TXcP{_n|WpWpI1FHsj?(Y5MR;%;U^rK^jy zjT{rJUj25My2;!yU2oCF)o=KmIJ!g3>;4>x+TASh^7zsPr(Us6w2cYe`CIqdnWc-? zY_jH1T(fA!sjS_}0xyrVUb!jTA|SSEt2M*Kx)_hh2`ALFj+V76T&v{D#ZZ-fXkxc^KgOvpMzrs+gp588a>|uJt!% zU3Q(^;Sw2i<23vIZ}-dE6=haw)EB6JVBP(AL!kPH@G|y^c~34yb10@MbOsSaTFlE& znR4f$;O@f$kv>983v5)SmM&O6{m?zT6nXYUcF!q?K8l309xylYDRfs~mvQA;hs(B! zE}C=ll>(V6m>j!qTYc#1zb!H0gxMsG*2kKLok9m>`GW6U^E~%rhOb~}j&f(v5>?mU zpet8>?;TihcVUN)LT8Xg+@lVou)6;YuCcbB@ybV>JQs=dnFkAZSU z(ru4LBInGzFD_CSS+ry;bG|`_qXSs5&;0i3ELEw6I~I4}S@=-IU1d|Rir2%M;+gd! zppxXQ)`&C&Bw>055TsASDfOTD`;w{ofkavcvYS*9VnNcex~%B6F&c1`-T zFKo`d`}Px73AnaRcF~-3KB3PjOtV(!kxE|AbCu6*Y#--HXa*k6+y2t9t4FOf$YS2b zvvXWndoDN_Tek4rKW!@WO}2X7!%Gh)=-R|1f(|Vz}S!>{9WsZ`Mbox%DV*_5=l1 zyI|@W{XkFi(k+tv!a@V9_`=p?*KVz2UZwQ)<)H~GUJomN6;Ii$t)idQ!8YrofLYDi zg6hP>AD>I>$;p5H@l1BfiHu1at&1hsex6t9QrXq7z-ywe>&MUc>y-bV4PQ2GeAhI| zl3mkio{HDQ47clti#?|tSkra+uw!}LY3;A#{4)A$XJ4M(-J{+aWHFml`P7a3Ggn%~ z_G--Qi@o}M+xr!fi&tF=cg|1=SSrNc@IRkeR}+zVLIdW-35Q& zr%Q$EKU|BcIzC&+@BM-{$F9@H$-*3`hEJO@u5NT`BWE8 znX`R@scVdWoltbKE_LB~SNly~KUP%co9OlVw|5<$D{S~~pW?Hl{q?d@NqgjWOu6}d zZTg=d{WArdRlFYNn5lcs(DAcB=P4$r*|u`)@$IHDyI%ac7GC3eFz|HKX@~OS z@_P!+(-)mJ-X|PkHUE2Hz`pb_A5glwX}f67lhiqEvoe=YOXMnz7nb`fHn$@2WU8sO zRh<4R`B&Ra4wyVYzMtpqq+hcf+14{R?|*Xhbzq=;d_jURDAg2uEn-<~v3H)?u9Zus z8{NI`a(LaGZEv@Vx3v|Og|B&MnU>q~^0D2!&)Y7RSr*8C;7jN|xxC^;|NR~7@5z8N zNLqy4lv}eckA-NxJmfy1EbBt9!4fU6^9Q?14=TnN-o2Tdua$RoU4rb<{{7QBcOEml zwT`uUQd8t713!uN7ZWe*?cQ3<`7psxD)5+A(gc~NEt@(%&g{B;KD+kpWP7q;OYx-e|=@6 z`_7XuuKURPHmP_$Ebx;J^eYlhG}J8#I_$9N(HHxx@lmB|-_KooTC1+hXZZBbk8lkQ zb3c{i+94r_Uq8I5kRuO; zRxX?ybL_>EsPcP3p`zi>->urr%xrnoG`;jq@SHE~GZwden(OP(ysN>!FYM0!V_luz zuT)LW--xPXPZzxCA8-Hh@%#DZ25jIs`_*9-#>reExxCj(X-8?i@r|HOMkbH6qe}jr zyncD5#b523)XJ65n5VX zz9)U!c@0RR^yO)oe0|-^)UVIC?VA-XC9%!oNMvP;`mOc5>IAH~JNE zlre1M{8X9aNB>Mq*uJ4fuOwVg?!)8yL`O%d2`-v4z857etO`vev%`+QOrCd0xIU&P z^KDz-^*KRv{+aFz6ICjaEjTRqZI5Wgg=>4`jzu2g3@dCfeX~@e_+9GWJcC&w0sFcW z%NtrJg3Nk<>GsP`A+_$y(JvP^$!V3uFK&yy_w8+|INR^y@1gTXEn8kJn6~1b zuiV<=ssxSY6&EYlS_j9@k(2uP>)n%Y-&d}<&S37=sh7cDoU6^dwkBQl_e!r#q8lX3 zHunkG?RECA>C|7k=I&fj$@9``QBUX*0n3(*rSl4#r>ZQDW$m7|UH5E!$-k4~XZ8H} z`F?(C_ZPd`Q}Xos7db8c@Id=56TWxr-?wbxf1UMma!lFsCu-+ZKxOWwH)(x>Yc2L# zRu=vW@Vogjmv{R0xp%*1)m?qD$2&$T@u=;s$OyAbff2^H=Eh6&?ON`)VpZ|}O<^5D z-qx!v4rK8Kc)vch?)>wWv%R7s)|9_^B6aPL-qh#QK;fAGWk#OzDbLNie)c~8^gN{9ACxU-Rej&trI& z1<0E$?yfWf6+4<%&Bsf8rx^Tx^ktoGsC09)rEA;Km#Y@>l#5N_-7b2i=j+qZzWygV zFZfIyL}S+^*=t*DTJULZNXLU`yk;+CgBsa`YwJKhHcIFV^5AJWz7BVl(Dr#{pHd*d0OIs>hhF=J2y5=+_FZfW9!`B6~A(Imq=ve$NVD?K5?H9{K-e1VB`(wSP%UNh?!J3k+jcLnHKKf$zO0rke@4TXm z;^8O8UUJEj|GzVQTYJP)r8CGPY@3?b1-G8=jD^#SY+5uESo%-$s?Evksrgpe_D)gs zt9#fnrOqIWa4~hS3;*Vbez_srmU}OuIZ*ub{AG)}D~-6Lo~gKKz5KeU6;|JKNlVqFz5#M-6~8{OjGGJ(%ySAis{KK*awOuVUxOcvu8_Tz0R$|u|zP|_i>*rtnc%x7*A4wN~`smTVEI z>UpMQys~h;#ZgUOgD)AZ&sY7PZFXGoNJ*e%U|P3gm*tVns&=-JEAt~|{guQIZ2qoS zDOqvpSwu@m@)UumX@1XcCf+&vC(*v{cJ|)oO5HB8^BZ}xdUY<%KNFb7Dykd!YU5(Y zws|XEe%`6|=3BJk=DCEHj-@A-mWNp!E2`4GZvEw0n%VxeQk$wr$96^Uzx;tWRU|JRnK7+m_B zzTjRj@80_@3?=!`PRPDLv*|8l^u=>+iI4AHe`lXx^V;^b-TSUjW~_Wsl2Zf}lu!9w zce8za<)y3@mr10wZ;^4CMg6mVOZ|V^Ed0uKO8vq=2B!#Tp;R~TZ9>QU?*~rj+kA`b z(f0dxwm;t3@_vsCyj(ELJ8*r_!?*zcB~}&_@20rS+S;D`8uvm|l_lx!`VLM{8i{l#P?` znPweI4-raDyKz{G>B&pkUw7Z$sXO$lB5l)x+P$;?C0E*i*yOo!#-qross~cbjw?RX zzc4-g%D&CdbWCmje%8Eu*7)#iwyl%7PL=EOrG|-3(P%Z8EEO0NTfNFpIz!LWH&pa) zrlMrvvi)(VEYAh)b(YD(Qi5rVTk4At{ot)(Yvv@T`qn|`hI?b;Z5-rYr$ z_MUT|k!7Wsa%0s6moDDm6OMoNo*h2mugjP2rxM8J-P2!DeEa^2So3R>!>uo8drK_n z($i!;Wng{7LuC8Ch3$WT$o}os=nPsC^Zwwcwt3E?>8k`)Up_y3+QPJCLjT*j|6RG> zn7l~pJFLIkQD~{h!UGNK7bK^jZF`WA=ep^-ri0|(kPFIB?ImAXoPMz^=>3J+b$|Mz zwk&cNT52$VmB!s_?eH(My0N(-7Ir7yCq!9&5Zs_;mDsGpYap{zNPVJ;SK_LT_x3#M zo4Yl=)oqXJ$++bUf*kpueo@r*H;$eV^Q~yvy@eCR*C+3qp|0E+G$To`^>JlSpfkH= z%Nj2K8yVuSdw0$F|ERlWb6)=Qb+aNx`@{{Lcx5X%q6KFabnspu%D?DFf9*ra9hP+@)Xx{C|n-#g-)x%rNl(9#)`KdiWZ zec$G?yBYrT-leH;?2=s4r59N@W9gxa8%L}5ItTL4laG)+@8q#aWb?K1B{_xP8@sk< z-B@aEZ12f+ob^E5HG^gMkN)|-z4&+G>c^2Bo>P+C4l2gi9q*gP+t?U$x9g!NSAduO z)RNQNHYw#wC%l;GqS>>u>(b`^Ns;d-UNXM2!n=IpI~94ytQQL5T*p~2_)8v-SXRDb z`J7McI(+E|RRYfidUIR;{x-4vgTl|_{gba>Yg@m*f6hL;A|}Q=&CL}IszQ#1ySIA1 znj^Dl=S~+dZ_cSx1-10k_w1W@;MCNfb#Z5J%rfl}6S2tVFuOK8{La43&(?gLI7gEE z`01BR=fp+UX)N1w?xw_)%l{sPn5dK)oQ!8F7?Br?ic&NMg7v!Ub}v2XNze0`FpFT{ynR@e6d!Lh2AqC zSHm|+?+$&f?LKVida0h{rN`B>#6Rb1>dN28eX9FrylP%$+p4+Nn(tp9PA{t1{H$o9 zUi`TWKc8PN{`<8g_m+!)U|?3hJpXp_(2~88TN7;_ct%;gQ}UWB(zkrM--Go*)m_bV z&K<7tcz;3u?+<;yw>N{8U%OAay)E#|Vfmo{SAws|SueZ4xGwHf1IL;24Whk^WbAxC zK0GY)|HpBi^CuOH&RKrRx#e=(-Oi~xS9e+NA(QEcK039nTo%lAymg_9dEvZmMtsJp zPdh$+NG#g_Rd<@rH;L|9vo+^@o&B8I)WI^q>SM6U#~Qn+B9mQD1)1E`>)aX~7t}vm zp8Y6&+GE%L_7|+;7rs8^FMhOGe(B-ov;RMN;85(D`+eW3Qe)A+-JJott&d-xZxVdU zT+@?QvcLFuR9#refrC8{UG7aGau5=Gm0_t9!Ods<|C8u zC(gL5(d@yIBmUyXX4f=zRe!82zxe6r`Qng2#;O{#>7wrn}h4F0ilf(XVUUBTDYqu9@-i(k?LzUJKa>CY85K zG8zLP7!(Ltn0&m^aJ#qaYUq}~m+cFGyjnfkU+!GleJM?u^A2JvUJdp|g8rY%?k01c zQs2Je>AWlMiSv4Vr!+FGTVFr7?R`!As5m8N(VN%FFZX``^uv7r$yMk4{(cR9!X({Ld_gF67bo-YhU<;W$MknU5wd!> ztGjl3+^K^53qPrDzPbIFdxzAmr3E&@kyA7ne`!9Ivvc&=U-9IqvE^somzN}zFAF^t zwUhrZ%d;u$dj@NPVS(5S|M^Q6%01{mz;|Ic|GToq``+(5bRpe7;IX#0SzOHG1wn4} z-$YM-spB(+VSSSn&;Ve96;Tk}tO>YYE^goI9?_{F7;?^&Iwc_KmcWbjgKgD~8^$*wEL%*g( zJy_qky{T4vi|h)yw?VISC)_yqc>DT;$D$uv9zQS8>GOfF&Xw_SRl7~#BKt3o%in~T zS@mkHZaCg(uU@w11=Bi)d#ozC9Mg`9Y!UoswPmWk;nNp~8Gr0o(>T}MeQtxQRif%_ zwBT1}c#mIF4P3`M%VWR5l9R2UEc_d*>0liE zJ%5pw%}*e8_e-Fk`ytkcE#A_ZHQPFurLBHE%hT%5pN%ePyCQ%my|@^~|Yx`Pc9PW772tPZ-`iM_0`_TfXJ&Bj?>B@(SyER%A2C{TJTz*-69q z)}invva|K~s!zRm+~DYz!1d)nn%t!3XD@m=<7lE}(W{P6!3WAaeV@1y|C+XLEbdIsIYnbf<}rm)$rz$MVUg>LAU(EGHJT-u-v+ zx!I*3Uc$cHwkH=~>iG2bK!1ntz0d>kOh?UJr}SQ9e6RR!a^VWzb^C?eCVe>|lC*E# zGf6FjJvlr7zplCa&2OWh=alS5dEwnRBP~B9I^?NEy!Y8sd3wg_53=qLp6yncBX*bB z)#{_cIW;-wi}^x(H9S7t4m&>O1n2a7r3d6M%!Y;d5bdpuNL}lvJR|jRB!kieE!D;w@FjY zTvhxcTdw)qrp-N~&7SBfI%o5xbqkI(S(pSenOrzicP_!DKKY8abmkvNVc%^!?Hhhy z`knXS(@J-}2X~h||Hd=(RaVmeV+=pO?ESFn?d8`>mv?l2(5p+_aywnLVE<7?-;KtB z_gHGI-p$OOXyv&)u)$vW-Dz!srrGZW`oOKl|-Q^E}H`Pq{k$ zDVzHGLX{`i^N)*iH%+X6wQ{lShnc_MaCxL`I(_4qQdAG$|LfsDS3Q-Ld*d-h??HX% z-CYa0a<3@OSuL`5YHZK{yIlOzbw9Zc?e!MrJTkPY+WpkgbH<178!7}h%XWSW&al3j zp*H=(Q;EGX7RO#5X#MD4`$;Zyf#R8PPxJ7NS6_VCTKA_uIn^di{S@1S`H$!AlzQtF zv!p${^R=$M0n_~7^0yZWO1@EwOVT{v!*~Df@%mYz8zbgwym!scUzq9eO!aav`&%xB zXOlmlF#GXzKFHZxtW$k${P-OA9%enzT=!?%69aAGr)m#&t#p@LW|}CFxu`EQzF_`7 zako*Y?hv@`C@}W4rbD95+W8F4AF$igvHz9m{HY=ggH~{Zg%SwidjcAC&u^a z*8PzVDt#Byui<_1*7CWxZv3(J2w&h6#PG#n>vKO9uEvBZ+WHzQO|uO@y30k}OM;FHh80Y5_}-Og_(9DHFJF{tZ)eUZ2~}Q7IEzwA$qVmNjf_$UFW@ zQ!s`n$cMYz@(|aq6>1u@w{}NXuh=Rf=F`r(Qfi>Ex+c`9-G#nL$iXU-`&9XX|XVE#vo7w6088=iO&e%2;_ zmC#eRq}%FK7*_|Cs6CVsKWbm|W97PomzMrzx7qjMMa(@l7rwh8MJ=Lp5;&Qs2yESZ zd`rQssWQ)Uwn@wWdo%s#C(*RI+M)CJ{WvF=zWjsFAG8!W z*?)ho)fcUVT%@{@~>yzP;m(xp8C8s;~jC%7^&h{=G~0yG|(vi-C5sSCOB zPrHO_ALzZ)x@2t;zH-OR$Bc3bn-1J*EfaQMHqU526N|#ahu#&J)piweIVZb*b(Vdq z{j2YZNXu{K#Oc~HA4HmrPOn)maOk*zD^I{F3(Fl#SJb{UdXVO)V=lPrWr}+G!pjzm zbbNNK>0B1C|HtOpZm}JyuP(kkP^oFrU9ikZVcFz>Xz88v|J%5$Utajg>FZP$cduzY z+otNt+C2N_=BE5OFJI-w%&$*&cXcc5$e>{)&Ok_B293l7X)?>05EzUolLN5Lb@@9z(AIHu-h9w>J47@ygqV$L|8 zE9Mu!-1u|57z-2L$&%Hd=KiuXM{t zF`nq)ec%1w{W%bG|MD4^O2LiWbEnPvaZc&4j@mo5dwsH?iC0!r;%&fMH5@|H>OHZ|Ybv{7-pxz$^-?O{qv ztIIdd?ACby$k66j>`Jf3_r~q~oijT>-Pz$@eCoQAvT9AF-S;^^CmZfu+L*cc6o($u zCJ~+2p_h(MHIbCvcdh^b`n2D1X=m_wYW6`0tPXZy8=+tq} zCb|pAKd$IpR&zKsJ-9)-g6aO3dpZn14>b3G=y(`EDe|q>qt$AvfnA=dANjw(QLH~H z^p|t-PpuV_?*i|w3A@^o{WiyY%Ht;&4n10O_rdWK&okE@^OoRGERt02KX9eqw&$^; z%=ef_8=cxbH+5av=q2&stLJ=z~h8$zv*ujR zJaufBu(0X@jmpT2zAKugyvsN6JWiakh*dR~|KZAQ6?RNdggPo~Ua3!K*lBe>`}i{5 z@AEHy{WB|B=j!X&m)%0OAMD=kej%3ExjMw$KJc01@5dd%LU!G@wkuyoObj=@WP2gV zX04iE_OsbHJBu2E*v_us<7N5w(fz|Gj;u5MymY!ly4RlP8|I4d2zmeZT;}4pWeMJI zCa0;M3f?@)XqL!EA)(ax_$;?6tfq(eb}e!X4ek6r>)e;hQw%#x^wt;#-5U-91` z%#;0RJPffq|4xf#qQ*mEF|MU~Now0`wpbemHkqx`NizstzSY;dv`6}nuwMMl-OE4q z%v)^LZdH2CLOcHX>!prOsz=l>BrS+=)C>+>ZrfkBdY#F$LN(nD_QF57KJX{_Eeh$7 zb@y5%vQfy6|6sx^q142$5#|3kZo1leK0RsCn}tn+&6%x#PF)OZR>^ev%M|~D`Inf~ z^}BcGnx9CR$sHa!C1Yk&5ZjYgD=LLKxAUyc$oZr$o_i?oQz5Ixvd^D#g{MC_KXa-^ z>jm?_mnUvI?_GD*WB;SD!&xeI&YOH@FPB-^AlLjf|K6Vu0e3BP^CM&~O}V`zmq&s* z>iKWEx<9Qida{z`kM|S^yKhbNQ{U0>zP33($3!{Mt&0BBZZ468ko+QJo(Vx z6w%PLx}4v7zCJs)_3?~5a)%ZeIz@3lUTV8h_cF`TwQbqw^J?$T750418EOA7F<}O) zN_2<(U8bFZi-H&5YPp#EXocCgpO@VvcAj2ic}iiDM(ezm^Udd{-}2r5`Lz3PVPVyb z#ivUAr%ZNzD%kvU?JCpQ{3lac+jRI&Z#*M$x2`+?XVptxBke%8ghdisYxkdSdAUX= za8WM*#o~-#>x36S3HGYj@}FBYH$>PV;zr1xiVq$yH~nMnJ#eICdr`Ik$1Ky0b_QSf zKa11QH@cbe=kTRVSNL8Roj%qUbI;>gF2A9NAv0TcbFJ&beY3vGe!jIn>P%l`=ckbG z(+oWhfBmEsDkPMey7Zr#TG}aX-n1*<1RQHh)XE&^J^1F<{Pp?V8sEaznzAd?tv3s$ zp1<~X&QgFF6odmC;RN_D@p z_>5MhF!#g{kr8`lWKKVtc6!NM#ak;=?{)pm1g=3r@P6OPaFGA8SmzcmI=CMq#Y>NU9Hj?o-QF&tI{)2_WicD~?0h)!*Jr=H?G{Vcq=hZA5&FL3_=8ZT-7cNKFRfXb z7_u?LVfwk-&MhA=%)7u{J!#R3i?4cfGvh<%<(=`Gviq@Isl*)DQ&x}R9;oB@6#Y5XJGrN`^K7QK&wRK{l2UBtN#QC?dxAOB8XOWBf8;s-q5?AamOTL|Vw^`}w8J{WZkK0aqZ#(gWvI(t*j ztoNEtL_%b$_PiGrcX#Hl_YT}yd&Xyq{t??rE;H{gI&drH?;PzEzyFs9DO1ZEu#h)GvN9eYrPVEL0{QIOB4Y`_0U%D}0jse$Mq! zTPX5tQTL0j$~||A(&OvC2OgW?*njiRB!}{^yVf;}wX2lO_`CI2b%O4KUR{3X^qDnm z6Xk^dRp#Gvu{JBwKD%D#^&9IL!`$i)L9U$pa!EZlyH;F_)qQe~v;XS+`@gulvdZ=@ zDe0NZzWH?hdROmhv%Vgv_OWX&*k|}aTHtc!Cyfn<`U3v^u+6x;_EFUoQNtt8f0W-A zGwBOBQTs>N`Pc74_Qumm+r34<8C5S{{=6YLJBMGPw`TK2JCBU>zoV;}814TV++~D-6*yEJ@KvLrjybt_w;_e+>)-dZRXkP z3tZD4$)rkVz1k)@f7$e+x`Vq7c91MYu+q~4cq_quxhW%*;dz0($L=19*} zEUf)5_rNte$ur@*&C~T2?TN2V9>o9oQJ?3jkQ5g*Rb#tgbj>}-r0n>Ko&QYU&U}AU zpjA&a)I2cC(Elgb^Uafc-P*S^%?_4piI@=Lcj3tWoyA9XI4wED&{@9wc!T^cp>96= ze`(969z4uBO~CBNh%}b2}>*nA1ca5FlCYxsQ--$nNuA0l!A|`Eq^C{Z(;qyK1b*FmbK7F5k^Ut1_>SCG`U)}1+7u|I5rh9m8HR?MR`%iuS*G+ts8jk7T;F*FQS`U-z=VR*>4&&QB^E7ki23 zzKTleV^pZy7%Q^l^0bJw2gmPs`!NPfZ(UYqYk$aV-t(fxhks98^l3}7>(;l|6_+Kx zj(fsap;kHT$k#vp58ix@xU9E7G)5+Qre?mK1E>9CTesFXhY#%j`y+Mn6q97vMLF#M zwBOBCpLq7T(Lqijap4~3I%}ty%~Q?>yM6e;VXpqV^e3nNKU+8U*5b&mb+6}l>bmAB z7q-hz%v<#0E6cC^E#BVhx7)6*Q|*&%eZK$Ut`zOse~0`3{}-zfO5|QG^i-js`i`-` zqP6L=&Gv%39St*N-|bKG)+sjFIPKTcBhRZ>q!{c=eLCk&5;JI0?x8)?f0Os-;@dW_ zQ)aMnc(bLRL3+1H7wb#+_sj15XRh8>J%hdZ^!uN^TGlHba^{&>-FbQNbDaIZ2^YCr zK1a^fP(Bd9_-u0KHzi)3U-NmyfK$#v2(1tJFi-F*?Ysf7SZBg&KdV5 z-!JJey8D>x)bwo|p051asxjxtD;LfWsY$2ZnKGW9+_ZJ~=dFv%Pu5!B+Uj^~Mntsj z+L<2=TuaU;R(IsaoU^+oAzPQk9sRGgPLON)M&&@ZhVR0+&uy1%?pAy#k$hmW%C-l8 znG3edKAe%@nCf&q;#}A|6RR4I9|kW?PltjV&M%tbRCjE5Z`Ubiy%_VKA5XUgv2D~2Wcs7Naqb1j-JS}2L*_i! z6f*DZW~y)8=3=UwoAx=cpRKC)y1Z90?%F*|Xgtj?b+UbVgB+vXRYF%IW69PJJ9E2 zu3D>_%&~VaoVPlPgf~vDe0$*k#E8}EYp(Qct55pF^Q7zn^W}NI!FhM~U$2hKpSP*~ zREq1<+PpjO_x0XR*?*DeX^RVI)Wws_AI-~Ttn*1qmr86E+p<^q@%|Nl4CV`BSyNg% zojx#ko>$M~7gTDWUtsK5Q{zvuwlG6+J1v&owB(^Q(P|aG8L!O;NTXwFm@2q~N z@qsyMwVvxL8E5ZFA3q;%&hRu+?%tku>)5ycm@|3Tr2hW8oE2-&P>z@P_b~prBDT$D;-d{M%@!XoSzilp+^@3DG@2v# z>__1jF$SHHuU&W7n(aM%SMfp71Kw`sO}!UwmNtUwr};lql_p!*1~!>lS2A}RPg%e4 z?vsh4TZ^}I=jNa3%XI(85Yu41eU`7J=-0}$?MD*DH#H}P6@6M-zUJ@&{l@HuQ!CTi z8)O|g1CqUCUaEGO3El6Ct9@mg;j8DihWAvRuSu(P<{qg@Wk)2K?!7v$f0tuZfK`u% z`dh|x?$P>1UJPqDpFMmc_G1bk(|yP6xib0<*DC)_PANS4if8(Ltq+0r|7ydID$h}! zE2Nrn|CKr8eASOZFBzhgDnws0Nn~@pUHw$?%dFlvY~za-L|HRLV%T|L>Y(|1;u$x0(D*Q}f(OmXnR+ z9qW%(Yj;|Abgh}JS3IR^*M+<{%+=3d^Wah!Bj!f2>Cg=i!$Qn`xmL6csZ5P zycMLxGZduv=H>_=%o>b-RaiX{R z(L10`8Yd%K%daihNJ#VK2t1|{wCkqJ~=#h%BS=jh%3 ztL8wap}k+mo`s844?AZ(^*7Fcx>fdv`2)cvPd5rIJK5%9xsv0D#D~HMJ(BUU&1TDY zraW4%Gj*Xa$Ku@PLZ-Ey;l`UI)rHyWcx$Aa_pWLc;GXQ;X>{#r#_GU(ydvjbnb%~T z+$^=iL3;MIMN9A3q{UB^Vsg9Kwg2}8z2&4uNc(>qfbvoG8#)OEi-BdO?G(Wm3RW>$08JbdFWE!(%~tkbszYbCQ>Z(F_9 mxTX4)F~xJCK;M`Dj7zNpTc20F5Mp3pVDNPHb6Mw<&;$VgI1$?b diff --git a/testfiles/cli_tests/testcases/export-area-drawing_expected.ps b/testfiles/cli_tests/testcases/export-area-drawing_expected.ps index 130815e96c..8551790606 100644 --- a/testfiles/cli_tests/testcases/export-area-drawing_expected.ps +++ b/testfiles/cli_tests/testcases/export-area-drawing_expected.ps @@ -1,10 +1,10 @@ %!PS-Adobe-3.0 -%%Creator: cairo 1.16.0 (https://cairographics.org) -%%CreationDate: Thu Feb 27 23:52:48 2020 +%%Creator: cairo 1.18.0 (https://cairographics.org) +%%CreationDate: Tue May 7 07:52:49 2024 %%Pages: 1 %%DocumentData: Clean7Bit %%LanguageLevel: 3 -%%DocumentMedia: 87x72mm 248 206 0 () () +%%DocumentMedia: 87x73mm 248 206 0 () () %%BoundingBox: 0 0 248 206 %%EndComments %%BeginProlog @@ -103,7 +103,7 @@ %%EndSetup %%Page: 1 1 %%BeginPageSetup -%%PageMedia: 87x72mm +%%PageMedia: 87x73mm %%PageBoundingBox: 0 0 248 206 248 206 cairo_set_page_size %%EndPageSetup @@ -160,319 +160,320 @@ q /ImageMatrix [ 630 0 0 -630 0 630 ] >> cairo_image - Gb"0WGFVoNIH^]DMs@>:hKuG,0.X(r,f,]L#qUH]rg8Q\:I]joW(", - UT5GOfW;#Y*)i!>1tE8h,\O.4@G&jg$c)%$beRNlaq3MXV8gH@>\(UFekStW@gX? - $.>Co=LtPM<#'4hP%tkD9FtKkDeb!,IV_T%-_@a8<&7bW2mqr'uX$_SS)C(4\9_G0atVltr - 63+5F1fr249KKnX.f/-'IGmJ?4e7NHbiNqa.9"ptaa:fm_K*.NCV]nLU!fK!g;d=C*N6t,$ - l$4FY%u=\1nfi>Dt1^ - m\2+-c]BBi2i`IYj`'T"^oUP>:.?sfTqd:L\ArYrL#)c4@H":H/kFSG,bE^)@6Kk - Deb!,G>JSmYCI?`g+`OtH5\;cn:!_?-t(bbjoBKtY0_@`tR;K*!PP-nIJJ@0'J?SC\OS2iU - &+DkYKA1PLNq-nG?[p)IAsZEPp.?:dFki>Dt1^lH)uZE4i7N5$kl-nIJJ@0,#cp[E3-Klrp - @k0lp0$4G0V=9*O241*4@@F%>C;XE;Uf]sWK_SS)C(4[Dc#;$bc[S>cm^$"cT$470e:fm^@ - j;_9:^T^SUG0f_s,mB!VB.,;248/,Mi_a,)-['aBP(LQf1DCUT*W'mnHWSiU^KO46eG>5#@ - Gr(2$c%c)q\FH\fJC!<;G9'SfCDN@$4G0V=9*M(mIRon/WRJ$a5W.S@Gr(2$c%dTHPZBT=N - Bs`Xf\ClYj`'T"^oBEpZD?R=N@CXPP6*'n>$$k$dVXO!-T?.GFkF\Fe%<)@3l)2KkDeb!': - \(qiK-X]ku5CVsJ;MC^LQr:fm^@*.DP'5[gGCXJDMnn-k!S@)nD=k"$6$/iB>:/JP=H_m/9juuu$4FY0UMnGD/aSnh8rA6-Y,%_pn-k!S@)oP.ZOE:I;%Knf - fctF]NW7%rYj`'T"^oBU2eEuXYYf%>f3S"?(qYnn^2nUj.k)kt4@G?o*NU5oS'DCV-=@keei>Dt1^lB'iV\ - qnleuoJIE,&U[qq6tqR8)]P^[k8@5;n4LB.,;24&5.0RV;\@p"7j.[]7d3Sc?CZc5t_'F3h - ,gIk46$i>Dt1^lFW5V^*1p4?_i`_E)FSVgLY]DIairP(LQf1DCUT*N-=l>Z`AKpFWJA;.g[ - /@Dt1^lJ.rS![mRcKX:B$gXbB=k6YPSCfi=Dfd9/K - nX.f/-%b,Dk<&(k)/f8GREce8rBa[=&MjsG_X':YiGAQnVK0,=:^CtYhlLX\6Q=EH4D0&;- - 3gA(^/,5B)I@"=\3U9pTY[jgeHh4jtDl=`CNT8(4ZR8Nik(7>H&[6:gq%A8TE_9m,fYo\eo - Tf=9'-'2(Iqr4ij;#/Lr7WCp0W_IV3t*h)aegN+>7`!EcT#8@Ch60'WILL%T+*7DY!U2_.s - %O(BRG$4FY$I>=+-'Wk('2_Jpia0O8LA]Mq6j@&PjFsgeA0,1E.c"/YTW,E&qc'AkWW#Sl - _rG&cfu*c,?qZnErD!JdSh(C7:?4!Y^c[^K7mOX]keG7dB(eiojGCoc.b"I\h%'3:a;ba/- - $(Ok:ACPf2^YU@UZ$68V,fmYQ-46Y]$4$]IJ%7gP*?.]'a<_-,i/N;BSgN(4ZPnpF\/@A/3bSVFRRH9#4?n+)^m6;$]_k_f-!u!\s#@4FYr<*p/b/)5.UYR. - k0s1'Gc3&8)B\E:oKkpUMkk"'jXCN?4X=\ft8mMDHpF!4in:0'Gc3&8)KhROJb.q_9^!,W6 - i6F7b1FcC7b&O7fZCg!#?R14j+sA%D'7QKtS4,8Wh%c$@%TJ!jLV8)`%r'e>P/l$@=;7P+k - d6Zl@\>Dbb/J@3l(\;-3f6*8. - jSM"$s>LA]MA_'p\C`6Z+G-U[U7hAL!!#?R33R&[9:>fXjgEO4l_XbaQXJ>iM"^o<+*@?IT - DelCWh9#LcMW/iX2qG*r*qBk1@0'JLEr4?S"-f_(LkkXu%D(@UlROE-:;0l=;L6VU$AAXk_ - 9$Om'!749bpZoK]3RRI5kY^>2]rrrtQef`SM6o')CLSH;FHtapF3XM=9*7+3OZdI0r93#GWOmJd7T:n\olF9J9NCm - !4VYA2(@m;4\35T$#.nW7=(?Wa;4-0<*-J"3X"pXKa$DchBU\XEOi"QMFTI"&J%o?jDH#g] - l]Za$fD0q$c&mXLR/sEV+_C20;]Uf;BS.;(4]Wd%P9JMasGk>]TpL:FdD]c'W(\=ZB?EB:f - m^8jW%N?En.&aMMKUh031sbm[@C1A$pCp?jhYli)\-S,mH6;XS[fTdNpPI;WM\c^e0Jp6"' - f([=A3cK9T@!hVsB60;?pc?PVosQB`0+K#.S.COaVRK8s3a]u9e]M\dlm/f^>9gr.7=TgiG - k\922O8RX,l:[p2DjN1#OG?@EKC(qG6>6jmT1rtGnh'\d_]0'7gEO0\^F=CUP_?+]Bi1:)V - mC9%*M)WhnLebR\#M>#PmcO&k8sW)i!X:&(!4NHO2r>_tf6(0b+F:tiPnYV.h27"9!st[*! - 8KX-mQ?,4gsB?F;s55T<`51KS^Gs&a0HjT@0*l\pmm0KH;UHD`&m1^HJSS%*aq"5XJ?+:$4 - FYO,B(mcLdeH6/07*\21)Tk@`sGiZ>AcK+A>!I_dVMNgPbo]l#JE5!M'Zlih^"h8gAF26EQqNH@\[+;>T - A>K:p51oNcoDC8aRT_W=\02j'Gc4'a'YHId"Q)&0&AZ= - d3f+MN3&f@kZh.d":Ttfi7@GMV]->d\XF0bSY0_gQ#D!q7HrZsbmiJ*'H@f&J6OCE:,BtBG - iQ_"k]YNY;OlQ#V>`M+.0>+Jc(Gd%eRl(+@dWf0!)3,T@'u^o^O>-RU - tQ$h['\W!,ndmSISD-,mDRV`*,/Pn$O2U9FXQH@O,V6mr";4[Qep!S!o$MY0fXOj2cAZ*rY - #l.2XW$4FYOe#t'&fQ2AKGg:miU=77I=@l@iZpZr6.>YhBE9m/Y$&Qt*,!AW4AO.);8R_F) - =]SEs=9,%9B%-NCVgYfk.UKB1X3mL0ok:t"/_MqTWICC[G>\[""lP]_W%Z\R`=j#7?EBqNg - c>1B'i,10YQ.!tcdFCkEm\5n=Zj=,:,/^$J5>;1 - <7d,dK_I6-5,'!cV`>iT1:(rLt>USulsgY,&<"Uk&%sZ^Mh98O6bc"Ot.Qb7&!0RI2`bXO) - g,U_mf)ZKL)7g/,+F\jXYt_?+[&kFbCTbB71^@"hIAc7%(-lp`m_gVfQ(/#6[>PUJ'_=)nd - %243ZqFW^-WKiCIpoF4KUjJ>R-nG@"p?u?t0[_InDt-e\RR\kW,0ep?6t$*! - `VuCH#/g@rmNipRYcHZIPE^I-=@mN+N@FNZ<_K - >?UaRboK$4FWs - 8)KhR0W=;[Hgq*4!fpA[goOa0*Nr;4YQ.og[X4=VP]"k8`9WU2,!,+c*agq4\XUWH.p4@XY - Mc1[!*hPX`_;!sm9EkPV^Jr)nDF-Fl\O%u_F@O4IbhAK#sHn - 7p!!I3W!ldH:iF$up_(FKC:lh4ua#eQ3NgbkLb'GM=5(Ja=eAh]Nh;-5!5XJFK(Tt2dZ3bJ - I]@0*lY@`Qtd#BHsg.it//`_$RF$QoRIDP**]-'')lSJ\:(XWJ^u%9) - \G4>Y.4i!OuSmE_H$9nW_rtRWWqU*c*d@[oZI48thLa9b$EkT3nH[ql^\LJ?_7-=.&p1TcP - cA&[][Z8d\)-R0DU;o2J*%R``Ld-$s7bDSH&c4+Gr^b7k_BZ+WV\]q>U%(Bh!Kf3RuaYGA[ - IE-@DHUi?cFl6GZ$*6o)-q88 - =0%4ES3J\XDp&7OoV8Vjgt6ga\\X4Ve,@8?JN,OON++s"DIi@J9rtH&iC]!K`nDpRQ_0^jm - ]F!BEDos>DX9Hlb[A4mdf%,%JbVZU>+#UG.@b0=q.RdAZ#8s:P,3#&Y#8@%f&[lVC+rr:F^ - OZR+Al.M]CdNT"El([,dO,HC=Ul47E+_a4T:_b!cTgU^_2:A;ka)EpAtP6Ff0UMg2:GlKK0 - j*i^n"7gp$dtL#l)($aqFq$^AL0Y\X:<`h)!V9*&PI3rQ]4A2gf>>:/.8\D5]ZeR*gLVJp1b%u6m`!.8`nha2t; - ;lP,"B*'\kdClV/WVk8Y87[@N@K"`cCiX-q/;R"1'4HQ - 6R>_hJi@4p!gWq]AraR%>=rs":+\F2jc_ZF>r_N^l%>JMNN)"FRIDQcBY:(YVt%61]*IIK` - r"jLYeM50R91(+7V2'Q6r/"ik\4okGluVXHAKb"R+@\*c2a/N%CD0$CuB'OliOV%==V5[YUXCQ?6l - \Z%ZJ[f@I`d-]-CiHA."Y]E_8Vi?LMciI`)\Mc - 1VclM9A>*a,ZCf'(BkSbAO;!=B4MKk%`,(lY4:SKU2pi=@SB;?TCj*`oFu?I?IO*ct`iKa$ - DOn^gL,T%U3o`@!lt'4"0a6p-:6jQk20omlA>C[@BBKa$DOnX\,;B%1bt9c?o[o5FQ4mNWc - C2i@73$JkT1Q`KjL@0*lW@Y[N``JAMA,iT.Er#1j%eMct$c%Hp> - :1dZapRbhnKAEGcF)U74]&ek/XNO4q3I!l'Gc3,Vm[k`)n&S('VFk#Jh+F!86'a(fFg?cq/ - $j`C(R$)#`*5djkEB7k?#8Nh*8&\g"8W"!P!)pF2EV_d_j$/4+bA\OE> - s9nh.!jhm0M@_?+]8oVR9`G[7D+M,/6sMY35K/`3kEVRm0'2PI>T>W@rBUVr[*.R)8m4;Ls - q[!^%D5YiK!YQ.Aa3OZe4%$p1pntQ_HFBs)`Fa\pJ"'R?ZrVi+B/JJtNWI9Pf)&2e-mO(6q&-b(8(XJF7G`'?>0U6]uWB - jt9F^Osl#P`hd!\fm\@#70X0C(gmUCZhpK`tk!s8KtAKrr_8j[UEN!t#FOeG^GO_HtX'j@: - n0J(E+!r8p?UDI=MhV/-&TRpFJ!Hlo!TnIe3Y)TWeD - i*I;Y<6"-t_Y4A=un,\4H@-Z+No*btQ6[+Y7I=MhV/-&V(hg*;9YVeI7Ie3Y)TWeDi*I3"7 - 6"+POBDKp[pjg3_0QlHY3.3Kl%+=j0JcQ-nG?sa**jb[S^\[O$:>a'Gc4uj0-c_-u?4bQQLX+%k&t/;o - `7;DG\uA:6QcJJ.F%Z!)k7g:,=<"r7hDP6"'e]3k8a8U3Y#6A*)kpi"$/&^spKn;\CZu^)e - L`pAf\>=9,3nD+hCC"a[QRg]RI?@0)`V[<6MaKe:e!jl[]C%gi][eGEGs-/GtVVh`IA4hr-R(4`C5RjTIbfHNIPIdR79TWeDiS_5h`dD?[bU%378n-=XN?n0R: - 3_K'Qq7-b*K>I[DZKLEYK>O53KB22:i"$/&^rY@Wf`m/DTEiWc4hr-R(4_O\LR/qop7`TN5 - PG/-:fm]ep)R=K:tRPZ?[hDDrh-IqN8ts8.;ZQ_.mNb.Io'5I7WJti2(7g&h+)q^!WJar!0 - $:%^VleL'tUmPg]RI?@0%4uTI[DEa[cKK>LsF4ltiJi"$/&^rV)?$Om - GIWI)_44hr-R(4_O"HPRG4[Rtb1T6l)Q-nG?CLYeM=:tY*FX8L`Tr'=`l(ch6V4G1N-cs>_ - q*oIP9$c'iWG98RgCphQOT6l)Q-nG?CLYnM8:tQ]B=aoc>rh-IqN#[+YN^7)hFXR!q#P`qg - !\eZEW3CEUKDaL,r'=`l(chHTCkKUUlh9mj&+BgX"^pEDmTBaJD:%\2k8rOl$4FYjYr?85' - XgkOb0N5d*u#K@OlO&c(_jDMB?8C^^`O7+JnBbYDs5Y7"4>#^7hb2'"@&f$A63]lG<$Y - &+C$^"d$7-7YSOK#K[K>^c)rC5iVJYaYkR676NnbkQ/DkYQ,=tI\ - :'-u)&2e-mO(6k!8gUT!/\lZ\C@1UjgqYnci\Y_@0%W+kJ.^__H,kanj=tI\Gc:@)\ - i"'mNk*i!8gUT!/]I&ZcJfD=*7=7Ta1@I_?*9`JUIn8$@m$4@trLYag9luh,aIcpgHl)t$^hl\sUrsG^MT]b?6+1"KDa'uHp_@m$4Ge$eR!Orll/mi[^AOlVo]H4^c)rC^ooUSXtN!o06`WA9@m - 58'+aRGK`r$1j5TY-mB40^e$_A%7Xj=tI\O&3b%n"]GOb/Bn(R%dVh`m/D??el_?-^]r>pg - (jm'*PFK\nr!fIa\@=_BhCQCk&MY5>B[S.fT;-4qE75e0^0$\miX#laiq0G`"mOg``!'q;7 - 828]i8DL7F%q^5'5[f5n&S^+[`?'jt($JLRFBA:["p=s\"]BtsHR)d#e-"?]#lFm\"d5G.H - R'SbH=W$'/3hL35bYDI;S,f7]Q!%En(O52a-Gt>X@!-YK>I[]MEMT&0FA15UDk#8ZUY'.7U - ^l/dSR9IFRMn!04!6U!Q9]/!_FWJ7j;lp"Rk'e*XNqi'Ga4Hm#_8LLVa*9kK=p'dF[%r!@3 - Q>!b'(i#p?`+/.bSCOT9un_?(l[I4\_X8.\+,6jGZD"LQ2"jH?abdk9EnDT;,Voc88 - FU8Q7&B<#5@f;/7:?5SYt"jd#o"0EP:q8.Y3BipZNU>_"oNE4lH41[Vg<[RarUj35U\2A^# - l3bjep`U!MgP>$[@Un&jV>KY^gAT)qL>rSa5NI$aW>)#VHGlG_8-l5cUT`J8G`QCJ+O`6QLrlsel!5aZ2!XW=s"G"!Y=+:db#!0AlA$abaR&c`>;=PuKt,6>Rh$4GW"HR&U$7:Cc)`Z>EdG#6gn)B(M=] - XXu7@)[s9b75Loin1YS_2]Q-[r.c2lDq635PicdJ,"e5NdpuIBeB@S?i4Ano,hr9Z`;)V^GOFE/2qEldj.L6HjXU[]:@e&*Ps#aFW^uNo2E,P&.9ZK?gC&N-e(NXUhUFdioC: - -h1\.fkL9+b>Dr/,JUITOreh'g'4WQSZ%iWr;lI;u[QhULXh]MXHcOPHp`NfI?Io$'1J!mQ - eo0Z9kCMMFWs08@Hp.,1f/7f08p$*IYEmhM6r78>(54QRqN>qc@T%uO[/&Q6iXdktGgGqLQ - UQ5n4qWX-f:[@r&c;2aYYGZd#,B6l\@?1PQ>*?7sV55]OCmk%bDbp;p7gDhZeu`H&FQmVQ4 - _\VFaH2Etf(=CY\^CHGs33ZHA8-JjHLX5_,etH-!+&6S_E+eZEFRU^IPg)=(IgJ&Vu?L:OR - Aea-SmPqe>U[<5OZcKO)qIFSNhZr7/l(4\?JI)J;6Hqf^n9-3a$b)l&gl>okN)29fdKL38M - /WY$BZ#$1hgZL$r7_7V9I/8)hQ/rr4\oZ'1f - =](=0[B7.=Yh+4I0'WK(OT9?)+:DC[^A?A]B0Yq)YQ4i(iY,5W==:17s7r/qIs=dL_O@/t=/Ga - B2+":!8ki?$G18lO/WO#.eB4^#7'6F\s0s?Ns1@dPs7tp,s4aeN%9BL,;BbjPH.K(bhu<>f - _SR&ITet+m];B]-9M[QJ.=6F0KLj?>J-lK-*m&5mR",O86jbPjkmBE_SUJ) - :S(hbbVLJjpMTHoh!Z2qelrshHe0(0;s9:G8$Bl2+SqMp.&Hk?,BnT\>-XgJ:OY(Bq/&_dY - ;`L_(;2TMdnA:#H6@P[!$oQ^@;k"-s+;Ce?b!>fC*;V - _u]PB;#=hkn-WKi(bB*rE?8,"qp;$6XFq(\[F]e_g/A\:4C(q4ol@W^mVC3'#Wo$Yk+TESgS[:+`3A7\LL'&MWb@70HR- - U;dCjF;kmG4l=G:U0^4"fuIXDr-L2UFqQ7MW@dYU8JQ_HEN:\SD'@nUIZY%R^e,LK9crD5:mli4NNH.?&hkM0=C#n.H?=n*-LMoWd++Rfm#B(Ok4q* - WcWI\G9VRuo74^GfJ.mLm-^1[CWGZ+XIkX##NGKCkg&6tb3mBob'Gfpn`f(TgJ*f)`=m'`DA*-tVr@lIG;ahjJlTpLe9FYX\a[;MZ*sU8$ - XCs7B6XRJ+AP3[4DpOQ>''O*a$de9m`E6qs6+Fm%'G54"CkX%1\)cLUEc!3)X'qX/RBgT+C - /4+f:OH@*jLddnR[pi:J/V7hrU="ijIiu^d;e5l96!:ZJ[n^p&%W46K9B0Q7fEqDFW6^3k" - R@3d=&+hjNmpk_H27c;PKDaI55Y?$'fIkS?['JEtO"3(_=ZJXVB"[mdI8Bgi6rd797H"E$>J3\q*Yha"DN;RC3f''>Sr,`$fA5]?,NINc)"=32Z:=s]Yd@]g:=4j>d8no - jKMo^QbHl54eQpGC(,Jaj-p].=3G/=ETbrNl..#l - sDS'_\kF@Ge'gHQrV/D;2\Z8!l7P5!J\*nVo(>i!kHSi'qd7ci.fdp1(4l+P;N& - fO5g:4LcLNp6!5q`:"P.4M)Q:kTT5JE@T1cr0!2MR]M]hnc;V?fN_TU/"s5T:mo&%l60`WY - \_BScHE%XbJg%h1aWp8K@6\4KHt6AT0EQeT[XII_,VO.23Pko5aK&!L_*g4T,c-+q)15$B&Hf-'N\JSm8IfH/K\ - C.jus3fN4=VV\2-7b>sV;OFC[@8-0J@amJCdbu'=hO"=jj0`]fR/5j;fV:&=0kHSk&\eorZ - 'G`.WObCKgo@/($+gM*,;-9WSO!OYI]4ZRoqAfnc]I5=/asWEgY7`nRpo*hS'L52d=JVB]. - b-g2FM@^oddL*7@74OdJt>f=Z\ndYV5`oH.K*9N&3:L@WTbk.\uPOd'ArJYV:PX+@Ig9ggF.$6:Cl+-nGqY,: - `]aa?gX;Ds@!RJ0:Mh5Wf\Trm(Ok>l&jX+:'AH&T.AFl>j - =mr8>*-nLIYQ]rDii]O8So&Ul7sTs0&oT3&%FMW]*-;Wl_!!Mfu.$Wupb($ - BjaZ1PXLJ3PG5@*JE;U6h,`Wo?&+1Y%o^9>CE+G`'?>G!-`3%Z9/3-iN`Di$f!@=LV8KH+&/hH3+*9%YY( - fHV(3N.\8G1W+u6aoFM#\&IJnjiKGb:o63u&%flYOTs0&;VQ4r:YHFnba3o#/J/9Ubi/q[4 - ?)@>g#GgM_!&%/N^cN(-GYYkEFm=>%UB#ge'Gg2`91nkTnXL+@aGf=>q1/O]+pn^&l9d%mP - Er(pp:l?Hl2J*%PPeM_?j25f]J`*qF-b.s3hs_U$MDK`\kdrr00^NY\3XO(YC0HNF;<-tO1 - uUa'%n$hH<@$3fq^Fb#6b)0'Gg2SC_b9!1o'Z5Kl'haLKreJtLq:E - A$HJce)o(8<-H9en+_ECa]3)E_m+3RVGB@b!r%k;Cr!^3PgpZ%hn+Qt4)i/]NAeOd`$IdlM - f@RdfF=ZrKGpMS@FjN6"'edX]=n]%kR23^9Dh<'6Qb*0i(8.g) - L'9Pf:_Ph@inc]/R(/!q65E"oPi9`H*+#?r-*BD7q=^Jf`#($G]j[(@^4>ESXTaeL>qH^Q7 - n5Z7YY?BHH@>G_O!9,2uhso\:hkHrioahMM/3P@kQ=p - S@0%hI4`KS6IW9n1Kl,5C]H[2;+$QV,9MnnWb3,!];_cV*E?5!2Co`j,qQRU60L9+J6/ts- - k83$`Y^OBbQ3[99kmm8_$O@+I$OHT=(!l^.`ZF,nD8eY85eJKr+DZdjH+IUa+8J]>mrsWob - :A)[-nHM,%ob[Sn,I.hff,da=7ol^cj+e<$4FrORSm9McmQ@8;mS@mI*GY]$s/ - !/g?/.`@gWd)f"IP5aPQBeK_6 - \?B%!EIV&ZOUTd"VHpGY5)&[.T0!2E#fbR&R - 8X>Xg@)D-P-rF"TiA-MKqDbnKB*1D:Pr)6KE\]jgqYnn-!3pYWr3eSr.gip[?Ja]KnH]/aB - LBnp,N"3B^VrGQKTs=ENrC[C=cE#/jB$l0U1i]>m]IYn1;6=G - Ef>HD;*c[9?7GTts">7#FPpQV;E^k5Hc+NQaH@\,#I\r-70]s=DS%16=>#Kon?i'AbB&7f. - .]fn/&9nhS2Z#.+%I[NK1oCU[k:6Qcq^jT'^+Q3*qQtQ$QLVskj;H*NTZcD%Tl;$o&'Y#e' - $O#6)(%ZhGX(0']l(6SVV[D6aIZOX*7:Catqk/glQ7_DShFI=_:X1aECp>$.5=gA&U9HnuI - 9slTSN:?3f<5udUeB7)Z4./1X2d*`FST&Hgl'#N_:M]+4T"h((/L`!OerS"&%nso=n8m5\/ - 0i&Jo?g0H2crEVMHVJ4gRZ7Q0Z0TZP9ncKa!'HNJ`"QG[d%b+5NtWZbW6HdA:fnk8?AuRA+tG1=\TrgP,2J,m1jc%SFkp&HS-!o - .YWsW/<\[C!IcqlZj!`_Be"EpVH@YoQ"n"]cJP_0_AuD:"ClW`^h7HjpaIobkHBe=e"n"]c - JP]IO@\.LcA"7cch5e/rb3YnU#2N%'U&oP5YWsW7To+-pi"$$LpX+kMm?7qc>G\sT:K67I' - G^t.VcCqsDHrKP*3H_qBT>Ae0Bb^m',A"jK`sg%AYn+][SM3q>'9L!nNONcDXu6))[SD%.+0%RV$4DO+C_cR>gV:uIn"UVOY>7&)lkS>LI]* - =M-nGoFlVu+"c#8UYO/!GVN]goLfMXk#I]*=M-nGoFJ#$PF:ObDC[M(`?>Zbl?fK;;TU=82 - d.aWda!b(Vs.csmEZB'5MX_=_AQb1B#8Tlu&Zi"&3>#8P3-ltl/R[_mkDDq;=p4huZ5,]&_=* - ;gor7X';5`[t,7hk9_-$i+]*dgfq.+eDIFRMiXo:#)j?$EF%?#"M[HJP:bc7W$t+@I!GR-0 - 5hS08qD>>!bA@8PTL\tAc!7d)g[F@p<;HFpAngJK$jrU-;?*uY.=Zr+e)YIp#,:Y9B$-$Q' - !E-&0>`5aaPV+G;\=/iY!g?L?'Ip%56!/T*6.d$QF-\dGDd?^8'=.2jl7\(tlZk4&cSQ;hI - >0e^Q"[_@t<6V[\9jdEG=qfSV[q10-A@n_NM9g$l7ChjP7:%Do&2ITm1!c@M4lje"chX)6# - *\GK*[!YU&ILI=(Q\,Z@=do^VqOe%"14Gst\3&KAt\=M6(`+M%2bP;=clb$+t+FGN:R-3,,A`?k"kZ[LSKAi- - "a5#ZGG.=06\td7+&el*P*ck?*o>V^W`3X,Sj3=uDo?A/Kikla%e"ec,ogg1q8n!;?=fPr& - TDir@GS]Zrgbmbm^0T."`90.4VGn)r-a+4"#bSLSk5##NVi!F\Y-MPY%H^lO*[3eo&L`ug+ - 5cW]@=don7)_:Gr8r7o-JEV&ic9\IQE/';IJOeD8@=f-,q(=+$pL\#T!*NQN-H$'C2-k;]? - 0,*Fu?k;pg?T'2!B,YH8L*0loF'^:H&3c - /UR-/O_)^-7jlcN#J1Dk+#HC4JPLU`7gJBM!UD4TTUoIj)j/.aRdU10]ASerdq7-eS?Y:uH - *brS4>_F%.bmd;j6"ktC^hH\h+3c&\=n,0rdDlgZU\^GathRKf&WbBN)JjgDe(6kG]=*A]R - q#drqpWb==BDT0p;u_%#?cZ"O!:frfMKtBL&[AG^bo:[^RsYos6['+anrQbI41JNJiEWimQ - Y2f%DQDhG#(ZJs:;o=;rpV'#+F9_lcO>F4GtI`DTCWWACh(#oebhig?F1C^#aN`:R-0T%#) - LmX$Wlm7)GJ5YHA)'lLPShkfO\C8kKIu@U8>(ZW-hI]4<$3N'87H6TXLDsIOY;0bl['-I#=;iU5319Yu,9t4?6de/N - a_?-G%'c$Fo)J!RpX]Vl'kc[>Mb;.mII/oe"(WJst1K`sg),o-$qTANR0Kc[3U - ]@^Lp(?eWT@it]+/sdF;19[%)5@j!n94@L+=N_'FbZVpN/+9(c`2;QMg?LtX#CoCWLn)[/MJP^Vu`B - 4Yqgdf#0hRRlUNksd=Y6Tp1Is@tgGK=j=>b\*CL"2M?K`sg)9.K(,[k9\UXjc95a\mcMRA\ - *p,eG>QXOGuYQ`.u2@/K%X@=_7#7`@L)pNt#mEM>n^c*_0To;:q(*WgTI]H6,hYGM,.7O#9 - )I\j863;o8&Gs:kojP63VlKfHg$qBFqh+Kj5n:L7Men&.qG"ueP?g#7@Q,3Trnt_.g*d]FE - 2uhe"dd`)$VJ(f-.c(5/kC0s0mcO$tQ:k%V+T.g-1!ca^SV0eB]@2atb=r2?*ZNMki^JAdN - B4D]ol>nhU*ZDE<[fR2.\BTncVmUci=jl4eC:]Td:ZmsE3=E'UB"LB3iuKpFd^jeUN=+rX\ - r7D<'I!bShGJ"P^!>E.AXZ2p3Kq/.d8/P]8KZY - DPo2Jfgp6T1(cL7B:0P3!'!TLdX6ED`)\9M\Imk7NF$\V+_eb87[0jQnh^8X6,fO3.ild=_ - 65XNV9?^H%$]6fFUb9\Pf&:PX!gLp$6NoK$h9geRlZ[#cQ4,?r?EOqjU\BJaFQ3m4C<_L(n - .4c#O?W/]*o]b2M_rmqLSWaIODPcbM*Y--+c>naL4tN%nYX$9X;8kZNftGA*kGPU1Et(TVW - ?*VI%`B'dZ!?;Zqt&oUT+i%HO2k8EPOsoC^WZ;uAL;nDW5h;JRWq-F[YoH^bIj.RkM("`$4 - DOB#?B">r:A%FpA[l^FmI7`DlrFQ?4aK;;YP-uf7NCRS - e>15RVcM'%ld'B&RD[Q99=uU]b@TMMAR<]s0u\E0LqbJi/.a.8="7L`I0L2J+?`V5'YcH[? - kesn^>_GpSXeS_>imn^bCmImX2JA,'G^tBc`3d-^C00: - 01tI[Tu3Ohq1Yo*H0'r6ZSqQ6%J1C+C>`)ot`76bFt[(#A7aUkcS+C\c)Fh<%49JWTY4(%Z - hGX(.qqMMt*tL>:D#Rp>OJY2XH;/K;kZMVjii]F;_cMSG.f![4g6`B=1m:Xc+0MO9R?1CV9 - DRI2^L(KBYe0(W8NI8]+M[GijVHVdfe,Y"i%kOmF1=0N%DR_QpO1)K5%cg'&/V<<]>o*8kFN9 - CXqTQk-J;haZ,19ZDD-3n.n`-kKb+:M;OR-3.8;sUDDpSp?bfO.>7ld=^o8O55FZcg#D]f+ - h]eOo@5?(35n&-apibk%SOc&eME_Wn1'S'C4Zh*J\5.A]mS3ks=W/F9d-V;a/===>^3"p7l - LF80_D#EIsS?_;-gX0:f[imC=it_-nV>%iCiQ,P5i@&rWHP_@#:P\5iD<81!aX=L#U)_5Eq - e1B/$oTohb/_-+EJlONY;.c^>N)K];%Do2'G^t_5>j5^Zt\?l-V8hF2m,BOa^g--lR_Q=,piF2_K`VC3u,?sei*CVi)Ec\CZ\$\#%fo!$e$G:pDDX- - ?:1l$b8VHLB%Wbt[/&V*\UA!;i"qafc]I(rE,`8%Ge]mVMFT//C3?FZQb32TO2"i.;Z+LD$&&P7WgATgrq - :9.8C*CUW[_1]0]DWrB^niO<0PTYd%aiP^fI%2/H3esLVa+DoQ_1taj9.>YRf5cC\T0e3o* - *77:C6W5M>O$ - O5Tl%d1!f#Is4FGXVpL/W^Abg\SbT+>dM"iaCSF/n$7:cgpiF0u5L0)AeUi"a?SJoGa\NNL - GBlcZp+#4XZ=V32:7LFb^Roa>]oo>/:bO`+bZd[_*Yk^L?`L:Hr&OdHg+0*#l]h%"`KMN29 - EZA`D6Qs`6/l&or@uJWM^1(K^Z;>.m'S7.:+%j@0 - 5@>ZW,/TG3rJti=V+Ar>FLB-1RD<94a%%.hmH05%sH+H`%rts00&==o)^hebT4G@3)gQ~> + Gb"0WH$!;8H2.E#,k(WEmu+jINgJDg*q*u>=t!U*"pcKN+d=O>";8GFVsnJK$3s0rQNOSuq + @2=KH0j1?*@FVA7,DIt9>"rC,[4Sc9=?CpZE.X8)iqcO>P?9gmdf-poDY93+'mftR$m->la + cWB`K-9:I]W/P^3nk`4_k'@h,I\Zj6P;]"F/kQio\*A[K!oBb5`[@<7e4cIXN6pJLV!7+Iu + A3G_X':88AnTStW@gdMdgnB.,<]ZpnK_)%$`oX\i2FL;F!_CD7PUTs+OF)MNlX-nKnU7Ub6 + n$4@uY;-8't_?*8D'Gd:6YQ3)iKnX.f/:b)s@Gr(2$QuQK=EoS;!_@)@(7q5(!7Dd>KkDeb + J@%dSi>Dt1+K\LCG_X':88AnTStW@gdMdgnB.,<]ZpnK_)%$`oX\i2FL;F!_CD7PUTs+OF) + MNlX-nGAmlsHdm1F.go_D;lAJ8=kl1Fl)ra%-PkKkDeb!8$Ralb6jaB)e@L6Zb%U@Gr(2$c + "WFgL`n_)g1AP8NHY`X8b;=TWmnoKa$D']PTHn$QF6LQ1;J002V\2ihE[H%#"lCRf./"^KK + 3fU[,t_^,K-lVn+eo$dVXO!3S>dFkWsO]b+BbPPDt1^blA*:;*d6bmnW + ZP*<@CrPr[cKnX.f/-+9AI(eXY7NL>B@;#!poq1_K'Gd:6YQ.pLBZlXP(&#Y\:E.=@hDnsA + LV+(MKkDeb!7r4E4S^%PD>mWqBIbM[/3H:.!P%:jf-]=cYg!YoSX,HUPa)ps(7q5(!*o-pW + tQ-qD"C8V*PkiL6"/^i$4FX,LGqpuYctrH8O6eD)%$be2+KQb:V=l)Dq]$tJ2MHit-nG@Bm!WN4s+P=PA0hD + 5KkDeb!-ddVq`_.!*kS\?@WKaXS!=[%(7q5(!8N_2Q=aQdh,m6'Icp,&ao9p,(7q5(!8N^G + D5M2ZO#=,:a@U7q"bhYC-nG@Bm!j)272+!<^LNGoAn5Js^:3Q(r5Y\=n-k!Si/[bS9Xb::K + =%YEcM96Lh2Bs=>7u?^!/6,>-nG@B*8!F^7\3Zn7FQmRHoi8rDF!.k)kt4@kmsR7jS!=[>5:M-"_D;lAJ3+7S2C[>Kjicj + L$QE(rm0dL$]9/*PGV-(oTWmnoKa$CppKsJYD"XM?D6^W,9+\l\mm#iKnX.f + /-'n,^M-$?aX`MdG:RK/P@U8J!U`GNkk_Qpn'/Q=a/6*1%#"lCNl)gV-el8n1Ai2ID,r)K> + [L'bpX[QX:C:\I0i;4@Rop.Rq*Ts+N%OL*91Y\0cH?"'4(;O#<8G9^>!H,A*i%r./: + U^RS<^]cgn0"*1^=m9R)f]J6+@h3O&1U>%ZN2\kC5^P722,IBT0%cT(a,C=h(`+t]@BMW>q(PN%NJmGu0YXhlMoQ7.P+soHFR)0(fjNW3%-,ra`mVR!N`(^ngH$h([T8 + Sh]c9)daa8m7U;:&7;b==T]E5s%CN:55iO1C!#O_12qOd,d>L_[$@CU8Pn^VBQ>+Z^KE[&5 + i.Ba.k?%kf=uq"3f^%CrUaTe#"VO+0!OuP+p=U4hWf`=5(:IA2FEN!X`S(3r,#*G9MgM5'! + *lc&SQQ`!]&H@kYC!U=iag?AVYW1(1E8\M@0*lGhqRm.7&j;Aqbu.GH>-Y9_q^7k%`i#t\L]a[l*#[]!)MGd3DU'&d?RF@0I;\DZkrU^MNS7]=B"DS0 + (C;7:Ol;Zff&81PAiJ.%CM1-XCN'[&3'P2:g-.k+*!9kG\?3h`bX>]=5k\>40j6e*u=D]Z$ + V\8m%tp>gun-nG?7*8N;,g")D1&G:dt'p4Gql/QYlO0'L&=YNWupqm=N(4ZdQY"q)8-Y9N. + 5SM(.$^(U9,)GG)/?SD'0:;qNSaVOg5#2+XcG)<,OE=\03;'Gc3t\N0Cg[,[HZ:fm]mmG0tH+r + Q`DPBmSL'3e$_b2(.)m?l0kMGU"N!1_cTIC;QtU((#hTVnqaRpm9'DIcmA&LnJ1J5c@%Sn/ + Ll>3V\$6*E.kZ'&SV\=0b*U^.,2$c&`iDr#&'MQ>0#]n!h),t6O^YQ139gG[M)'6V`(]7'[ + h,[KusISp.r.0>RI!cTW-gC0!*>a%)2iU^"?RbdY%'I@ZlDQ$j;h%AEOKa$Dar\p`Ze=g2P + X&mV[?EBqNg,u)&_D:MCTLBP1J5Z]]::!JY1cO^Ao:JN'.5%/nW*L(lM8B=5lG1uM(0tU:9 + _I7VWKOEeYG=.iIhjO`!f"PnE9cBI2MI,sc)I%)OV]Ms:?G5cWaDNl(#22c!B>48bEV(/OK + IA!FnO1o=GF@*fc)FjW`_n*&/5n1;hm8,:,W7N10Qh[6-pmFAMVWga;/S3GPTHlmC6OEKa$ + Diql`^d\cOWZ]l(Y`+[+Z[=SA\(T0GA<].]h]!1\B8eVVBpTMS+XG:cXkV6g%L.JC!/K#.Q + p\&4[^,Wkp^A;#9YJu2;s7fQTGNGS-n8Z\_\YQ2Q+aN!aA:OX1a?F0]IPcPNRf\OUa^,j!: + Js@#a(-j8gH"C6dA%tEdWokq#]N@af^L:qYklU8-U`g'Q^o=(J`[3e=94`p*bC[s5Pn]?-5 + *i%L!5dg\qP:%f#oTgNZ%0G1*Sb!nn_UXmCTFkTP!06.$/[^u;AMR)q?_PglRAXSG]9+\t33N,M,456&!t#FOOds'1(.8).oN.)TLf? + tWpK(64SfWKPu4%:ZU_?+[ef,gEuSs/$r[0imh + A41o3EAt2DerQd]R&Ac8e3dXF33m>o!fN1I+[+@S'l'hG#QkDA^l_p7*j?LDQ-r#_;INWWm + 0dL$]9,7]I;374.k)ktJ3_c#F2#U=^ED@_$^Cc3UihM&FZ6U[S&\mLpn2f";?MUVi5hK1o. + 7oV7Hp\@VpsYD8r@*t``tR8:chH%JAMA,A5CML(,NTq\uN4gQ*?DqO=2X[7rmLVCGEUX8Jh + %LV3i1c=Q&mFMN^-!2:hB(/;48#,PU"S(8Lc4FAbn4lLA=07I_=\Y$GA-SZ7dGYQ,Zr]7%- + ,gl>][WV7ee2+lFk)<>[G$T=!F3O_Dg%t0HU=<3i?1ZlY8T?e8Au(Q9T-u5jTB*[s;qS/!:;&qBc\n&&V=.)d,mAu+i + /bNf1G&h(2//g,TQJO'X\*/\]9-YN"bd-_Ock*l8:kclD8X.%!+A52c`gUVrpYZ$4FW]S*L + t1Un/4S9p9+RaZt%p>Hi,&jg0lQHfq$@qUO,o(9RW7Io0;Jp@^/IR((HDY20cGPPZjZN_9$ + @UN=u?kGe;..k_U]5FF;Z5PELHhfSf(K`tj9-Z=j\YWX97ZjVK\Whm&f.k+6\R?:r/.AHi% + 2m[;-3fnjQs3%ll#&kSe!*@:>-H)#^7Lt]PUORXJCYYY$NZ)i:NV/"WQoV0 + 0P!mi5CYhM_/Q-=m;j@%O@uMZ;/JKG9^>elM0(j&?_gbXK/I.G5h!dP.)(X`D'bA33a]PTu + DO6QTj]n5iTFY8WgbrCfT^$rFdONTl/DWP7);6Y&6l"X5=U,J.k$=fG+%LoN&."P#Wf6]i[ + iaS%iN5[WD;M%A$Oa_?+]"St,[0-;TnK<=]5&o40/dDYI417%/!uY"&"gr8o9.X%He$\0 + ^m_\JCClP&sR)"'e\ND-^L?;6k%ZoLdSX=<$R,s5mNDG@(\C@0$L3m^mNk-;X9f7m.<#Mao + 9HY+/j6'@t79\uOlTO50mRN0UO!a7U53?E9-9K.[=1e\_7W/h=0LG + )QHe[-8M>djYQ+`?mlRj,-I3D6Ru7,N8TF?+2UNMFpQWMrE7_h5n$ABD[F/AE#Om<].k+50 + TA!DVqY.^8F*>ZbV%B,%=:.?&!,t64I$(&sj^/-fh`4Js#H+?OU! + Osb#gJ!OI;^ZG46NHSn<`0?&&"L4\n+p`*$c#s&RklTB:VF,4Y5l49+['-2=>osFATX=KXH + K^qde\8k6X^!tg],98M^`aXph8!'pu&H"Ed]6&0HHM=jjL%r3^tCTGCKZ.(4ZJl[SF.G7u` + \.55a@M031sR7Wta>WK\HJ]rut'!c[)5lVfB);j1al:O`'cq?thF[oZHU.[6'.UgLNONrLl + 3YQ,Y&\7LQ:N;C6&8OFDPr\o:$HE5*6Ja=!e=s6%m]+(@K!c[)Ek/eCV8q#Vb7?qu>&(S?h + "Z#A-%#E`JeB6nqRDK#Ei6Z725HH,;7]VSrB^oOXL$OV-=S.LJMKEh6QRrmeJ-58T3mlru5 + ^gpF;=o1=o[_A:6aocXiO9p0c_(b7%mA[jggX"C'M!$ + !u2EC-remC8*OTC<,<9'N%TOs54_H;Xr9s3n/s^%m>u!c[)=.nOYd0:$&3ZQ0'D,97/&PWr + S.OqD?'DaN?eE=7I(i4tHa8O&6A7#%]PFp(>-6F'^JHPTY_DqVXS_?+]7k_?@a%_YaXU-@* + >.']$OK3Sh#@I.2VQ_]Q1DA*?b^k&9&5>(:<$s"J7#Tad_;D`mH-9t?KVQ^'%m9aT_#0UPM + roc]l7B\b9I8YSChm.50K`tj7-Z=i1@8FuFrR$Cn\sh8DMW6Id>l&^DI9<(i87;POg9Gk,7:FM55Q/ub@#J?Z7UIOf0,TC]fslaQ!j + K'WGEG=&UXN+joH=Vq9./&P6?nC)!jK(RcU*Q,Ss/%-UCZhpK`tk"fBreXKfq*&oLC"]!t# + FOeui9h=;821HKpPT^]tPhJ?@Whk?AY/;L7Pf5/7C;(4`OC46BZ[p4@D<++8YM-nG@&HZEp + 0>:&+t7j$Xk$4FXMDU/1rKtSa;oLC"]!t#FOa'Gc5$I;dH;-u?#-cl[[d%k&t/>Pm9gc_@XFV4IVL"+3@B! + SGQL2UeD)Fja_U6=9,er$JQhoqP+Sj!JL\bleq)-/IEGZ%/nQYpjg3_Y]XX&PiN4AXZuO[Io'5I[LR + /GS8;X*A[MHl"8kGn!SD_u[7SIU[Qm=HcTT&c'Gc5$pKsJYCgd]_V#K-Or'=`lfK-ZZ6F): + sOqIP3&+BgX"^pZ;`pq.d[TJ:McTT&c'Gc5$,Hm&![]h&5<.F<:rh-IqC`V=c2VGs5aq4N^ + #P`qg!jHfhIBm47*HOOOld%`0*u#K@X^q#AF]&G.cTT&c'Gc4aS@k[6Ke8L>qK!2:#DE=h, + q!Vf7:AtbJ,Qn_pk-EbYd.cr@Z/*pOad.J&+BgX"^kn3mNRS+Ss@%dEt@aFK`tjqBC8*E_N + mh:rOE1.K>I\oZ>5Lq@F%M`rOE1.K>I\oZ>919Mo&Lc:]7g/pk-EbYd/&r!u?TmPTK[B&+B + gX"^kp9cM860]cc/(k8rOl$4FYlSfRDR$@>X]l-DN.*u#K@Ps%/%>84M<]'@U)^`O7+J;:lfOW'C"VIo'5IGkU<[Sa@&k8rOl$4FYl_: + PSg'XgRgp#dCS%gi][b/#Z./.-`01A'`Ni"$/&^ri@qPERY9[f4VUHp_.d"Y_=i1J*6pITs+Nu]YA/IZ@sln&c+j*n-t'T@--mtn];R?\.$t2H@To + ^/-%1j?5])G@>e]arm1ap6X^!t3O3(0Z@slq+o4P:n-t'T@--VKU^MN9#3ud_^c)rCJ=tI]\P[4>84MDHKYl&^c)rC5k9*eQtK + :KMpb=LkQ/DkYQ0iq-g,r:_H'60rLjJkL;F!_?LE/]/IHhr]''Yf^c)rC5k8sQSRYOLC?_f + aoDmcj21PYTb,BgfIt^=&.fgb)"oE-8QFKCFl46%n2dO]$hjhjTf@L$g+taS`ge3$K_BQ*Y + =9(pm3P2)+o6?21ek1;fTa1@I_?*81A)6EK]4Y6Q[FnPoVa'eRhb[DTM7I_K@H"7FQ``9t8 + R).e#Pa%j!f3cRb0#o.e"!eQ=hV2i+*rHU-nKmqLodX.R$[iq=QashC67$C6P&YqK`uE3Fd + JD\]KOr5[Phoc/H)7IpkHWe\3m'3mJ3Muo/F1#l$?9lAEj[g^c)rC_!e>+>ZUdB,^8"s/0Y + u.6P&YqKa$sMNV0?q&'dAIhYjn&AF!#R^eYX[5YUk877E[nGgk4_nn#3-_uop*@=bf;=L\/ + !hO36.26''!Z>K_XLVa,Wc1rE;\i-cGV_T.B+Kb;!)k`,+!bin-jN/I">8><%bL0OCZ2m6V@ + =e(/h&_F1b!%'KfS^%5%YY'aS]?(&DE'8dY=JosJHc5aK`tB.jg0l'[Wo;P,'rKW/q$h]%( + (Q%!\'!i&qD_E$`'N^;iB9Q!@X_I5\a_g":u7](.]=J4V@o;a*H9;p.,0TTWg[R@#kF%3#! + ,63Ig`n<3o'r8/?m__MgVj5]BS&+Iq(/!QHpnC1Jk + 49!5f0/$Va,6<%M_?*$Oe?Hsi4VB!l,6@Rj_?,9c;ndoJ&jY`TYWr1Fe?/:jO2NaG8-$AuY + ^eY*8-#WG_?,&9,6=Sq$4CP:B90t7l)m6]J@%aR!T.1`?;^RE'MgAffcpO]UTf90r7N<78\ + 4-3('#$an4RW[ZJmejH$SYs*XNr[-nO;i8E8FqLZbs5!_=sY(@W/f,_.^?@0+\2eZV!FJ!Q + F + mgYcLEfcpNN%tt1WnU?P;Qr>bj4VC$T:ftN!%soRZDXS;l!;r=VR7dI55Z_lWS;^c^Id:4u*dr.og9l?AbimccA!X_-Fld`6hE(M>IIMjkSp4^ho + V[]JbgE&QlX*G5]gj8ml$?*NZUY'f0.C)WZEa]oD4:FjDiaeJ`s,;1gne;g9^QXW4W,,Ld% + GP4)s*7t!0fs]P)uUG+7.gMk#T*2*!:Wfu$[_0gK@-b_n\jpBo6Rc]*VrE.=&,)7Xd1R + )*=_G&/T\($jU1`gl>'FlG^$0cQ`amTYb!uj5WT9K@2g-9mb[oXGJ5ll6Q[4#E*tcpRK`Ys + CR5q;h;ao97fE28pROO`6N+[5i*_TK!1sD+Y`4WXF)pD50?P%`mM3sXP/;7ZNpZJ[,=?qRZ + fr2>BjKKI$d63>,B63LYt"l2%=p(s\T99-IX/r.Nt1R/0.f@bp%)^+B$?Xip"FPj6"VP&ZpS,Lsk;Tm+LNR*usdf+&5"Bl9Y(EHAXiu!#0 + )_[\d[0c[S,A3p4+tCTSVIoKCT/BkGZWp5O!L3HAZ8H4A]\]LUt[A:'YEp.,0sR8RIs?CC6`iG7I:]l[hH"G",T\$48arp!SZIJC\^_\(HN + @lbRXa-^;TN&,'t0f*9N))FKhM)!1_f#=`gmJm#Jm/MG4Du]K/@fP<4`XF]$].kK)CjGmLY + 2B6P\DtVrWe]"h6g^nZS^Y8g.siNA`8aL!lg&IoZ$uW2K3VdTlh#:X>a0.&DdEDEkAar%o9VT2bUEE99@7AEMd;+`NKs.2Z+<*]AOE#5d?lR)o4Q'j+BI?5Z&@h + +0)q%ZHa9q4G0a)67l(VSa#i6U!-aiqt2\$#2.D>3pqf@l*na1"h9o$Wdd&c`nj:tWY%,u6WX]L)pb + msNA:AjG]doo[lY5VF:+d;/Xj`]h*'I#gL*GL!R0?%;F/BjU7MgV=_:CV(2PT`%,Y&duE\E0k37i144EfZcJNKcqY"<]b%e1H\s1t*Lf-+GtS+42SLPHLtnLSaT7^p%u's.[IA1!^J5k't0j2im7Z&e#cf + ap\iZOZ#>5i`kHlVl#C4q\9UHkUe+iuIsnI1HR+Wpn65(Ls1SSs%_A+tIeUH;HFU=X3&>`) + bbNn`+9)%-CkJR2+4i0&dm$:9j8pZJ,qO(mAfA3Sc`/4oa2!L]>t[[^&C)J/Tehm;'p54Q + ?X\n+#%_HJM\L4CM4$?[ZRSPJ!4'+SJ>+:h*SZhQ_"VYWRR;kcY1iEFf*Bf"_-r_<:G"9> + og(A;7ldhQQiFR5>WgXEQ_q:S^#D^YUqWN5_h"`>"PQ*6[^4qB;)[Js&"D$9o+@1c&V)`-B + #%Fk$4VO5+Q#,%!n,-X7D9RgPrH?Cs3s,ZlC$UC/UYR%P<)7.ud=a7'Sbl@Dd'D]'0>7@"/'+p + 3R66\tQ>6Q`#c[D(JF_VYTXm[GPj%dL([ske%aZC-X:ZELp:QIRO6cTs+g%cPt?^=I_UGD6 + Ee'qgA\l*Dfjkf2mqLD_01uiMPs3g2B+S.?D=ie6XtDREbH'JNZ`aGI?=V!)abi71/gc0c` + uoVfR3kkp>^U+Fk.q@?DJBBX;0Q9e.:*"2H],(+J8IA#>`+:M,@Wf.UgP%0<7M$B#V)3pD] + :1G\a*/-$2W=JVNa(t6bUH_11Gm5H8?'LQ;kOrcP/qV"^8&6T!NU9I0g?adN>l*46cM$Ml< + E!:qa$@C'cCm45]p`e2V/+#8BJ.t7WOkRNebY45eq8`Wg(B\W8_BL#%1a^*cFWOl5?P`f`L + ;G-/4I#KIGTL4/,"*2s2%#96nWBW>H2$AABYQn8KGT>2>5=sKD+mjj)^!UZP6)"=$B'#tB? + %;98Y'dL9)F#D+H.^"da4c/F>/QDN"#<1a0aa(!()IX,!A),9P9RaM+73rr^-M<$6WS%/CN + )cN9Oda5HV68L9GC-@0)#eKge%L,.T5W%W0J9^XIYX!XK/@M_t0QEhCJImqtbt?2JIH+;?4 + Tn9JNtS&EYJ_<0b'[GSLpJrdbM;4gVOHa8PVR;9ReIe*UH3!p$Mqa>HDT,q.4rT47pE"j(t + kp+2YRDdE15Hata"V>2Y(:_nMP'B5W7Y^`"0581T!WVUQ#2`6,9TWg + [d&"1A^6ZYu_f@o\/!'rkYJOCdTrLL_%1[XooJ0:Mh5U*p[bXdrUBq8gYrOqo/)h9sV$RFq + MA%J$1ca^gRatL-Zh#MOo=hjp$(e% + YT`*^Pi&(iL\EH@k#+J"URo\\(5A@n2!I0L=!q(@cd2`pN0MD<1&&;*X(3&(2PMg`Z-1)!= + ^cN5GE8:oAoJVPrnUjO<:BUYq'nlh(n7O)ao8fNq*Wc=(6=BnU9JMBo*h?$E:UT?c!&%/N^ + `uW$\k;LLFkUm:J/9Ubi16\kOjcA7*<&Iq;#nCS'Gg2`$c_fmM4A^0>=Bf)Iin`1&VUBNoc + ]O?aiH19oCmm`9&EfdY)^S.,:]R^b@0%Io0Suf\ah8.>'6EQR9i(T,7P@7puE?4It[ + pGQ-oHL9qZJ)[^-J):N"M-OL,!$4HXkX2]68M + 4B'BBi$R=rJc1B>p]ZSV)'4C!eEUF!C&_\&ik"[GH`L^/b_Ym3[=U+8-f)""/I^[$i&sH@l + g#Gc7f3+W0>!\#>5V_VTT7<2K5#OB236#=opN(@]ub`cNqT:KC + N9S,Y[h`Q6oK-`TeMZFg8.",-pV\(W*J*r2QPDAW"jpI(R8!A(DS]L?60s#m"$9uS.-BC>c + pBTF@lX?]8F5Tdcl'GaN-fSZT)5LnN;_VXiLm;S@m!%t%NJ;U7aBl2k$^-,[@qIR*"O_r)? + I/qkq=?oMg&)@!8^[1E%@@K,-YMasqJ0PaDd..?M1/-IZ3P1j^KtW?)'nAFS$Rcp'&ip!hS + RID4mdELZ]Ge2I8PJG@G + h$E'"C7W4Uq4EXj0t*0"c6,dOkeEnT.EZO/UN;\/2bRNL`lcNARq$4Eg0$^8YWlD]ojKtSq + `\@27`U9^?=o/DV.&*ocg?@d2g@=]Otgk^"UudXgp$hbo3df^0'sAM="6lX4ia'P;7sU + 'hCh/('oMI9HrZ]QY3[-fUK$L??,u.guDTdC0.eH4O==ILVa*Yn:EC^:#*'uD;T[Vp85F$j + lH_[(VC[1%r+V#9-M5Xh&'%u0t'3-'b-gL@Ih@mj1I?5X$/.`ds)j>! + qSq$^j.F&0/o + P]\>@ai_aS044-nA:JYre@!gCO$?J;c4orO[NrIY#'*:nd%IU.TKEl>W/5m#l(9;L<7mZYC + 0+CNbRUWT[*o^X?S^r6r6u.B`?As4]to*Y1 + umEKZL`nErfEMOj/bnKEHo3K'"QirC-8&Im2]?_Z&-2#Poi$YGdSDqSNu&Mo#MrP\oEO:OH + W58muXQ?='>r5bE,G>CUl!ED]g\JH!X.4am49D/*7T)qehGe]WAI?;Vn6onOn+(PN"okS[k + c9\bi$]0JN_2r.SB;6fJ(hQ2<'4tNnYAr/6/JAMB(1r.$mhu)V=ortld>$mRs<(gm&q$Ggq + Ka!'BTo"-YcgRRadsGc.?ZWLIJJ6e9*gLQn$[YqNl-E/`hC'od9V44_:KCLc;NHaddSP;NS + uCXcP?]YTOc7YZT>8-;=U9K>,8q2KqpHW/<+R#bsPBQe$'Y#(h$O#.1'DHnBF$nI + kl($R%W(Q[S-ni:Jp%f-nGp??\uN]0dFShi`i\^ + G,];O7:Ge!F@7nV#:\"D$4?uA*u"C]o#beA6orhBD7/3[n7nf5rZDAJ6t(XsRq^lRkFNWpS + k/&2Q9J;>0E"0D["uR#!VZU.pFKS%;n\7;PolH3;:nbaU-D2"(/\0Vi4`maT(_QXY$G8,Sk + +WOb*Q4YB.(;_1A,9/E$ku#"CGdTp[$]2J9iC[MDVKD>OGH8*VF%oE$ku#"Q+G;pO)FV'@d + c\>j:r2(54n!IIq/$E$ku#"Q'P"pH8:c.dW_)3-N&*@>e]ar[\6@6t(Z)qi$AYk03(mhFHa + D/!nq;'\ab4oZ&D")[ZrUht-C4:3>iie^Us,4crK.f%E47mZM\>+/cnk-nGo:4\a^1=L^a` + OcG^oS8M3V1X]'KS-$0nYWsT^E3n"HIsIZ4+.^#?1uG%375@2ZS-$0nYWsTf13(W!^+i*jO + 3Z9]>&O"1m$N*5XZo[6>>\KF'`QV#4b*#MrRLf/MSdub>86e*FOQk\^o]"9&.24#\0jAMUO + !ncEB8<@7:B!6!<%`]gkrrMU%"d2D9T[hAnFM,c"co83`f=m^nS%:1.m%j!k4o.5ZeFMER1 + dnU#2CXePNm#m\AF8dS>:j!]NtY+=q,l\EBbSSZ^g>#bNOamkg*8%)2/72im1?S + ZYWc_of0OWn_38QpQn=gC+d0VWG#"Q0VB/6/>WK:Cl8q)\9p2J?=qOLedd + `3:\16n2V+m_lME#u8;-4r/-CVI-Xgd_"O/"3LNGFSlYk]^@Z?/tde-MfWdc54hckX^E@Mr + 2r6Vbds1)Y=+qNX^G$L)h%\mLUQe^Cut3n6k"CXTZM7[JUO],cS/6=dfeH[bnQWTnJFbDCG + @=qikUqUB.867:L-A!1^%f-Y^$niq?sSGpk8OJ'!.mWn&n<-Z8TZ\i8e>7A")[>+Of7^t9I + X!/GGQdIs-4/Z:V[PQ?-I;U8D/k + [Q.5CTH55e+3[pHHKLF\pDYM_5^g\!Uu#1.$D2,P^Q#9nA8o.'F=##bNa3hPYGX,/]VEVm0A>c9=Vg5,C5kka + HER3mu&#s9i7Ak3>=q%l=1-g8B,HrdmGH^U1@=dp9@,da[#8X=Pd7'Bs/@673"Z#@(:U3>\ + Y"H'i0r0mL0:,=.=AiE;I + qLjWu.d?4PYD'0>Dn*BYq/t?>YU932?bUZ%JEU<\q6aP,$brWONk + /j--V=.),IWNs3a8NFF_uPn/c^VElPo^Na;l"-pYmiVOnZm77+7dcV-Z/Hd[%kL@+*6Q!;V + Tf?Q2\]%OLQs2\ECaO*iL,J0:[E.R`0q2QTj\U*\*W6=>!HR0[IRnQGN]snjO5d4S2I"obr + .M,6*+M$!AgseOMc%PfZ1GGs/QNL9h[JK`sg)02:6.AkcY*)LA+N_D8_`fc[EHV`fE.+[$k + .p%[*pNV0>$/m2K@&q&,'&-TdM&MDVWR+r2@7R?.9*Cr,b@g3*\(us2s!*Wo]7JZkO8] + Q*6XdB>K1kqDIobWEh;2VWA][V`s_2RsJdSk]jX+eVOHI]OE<@&1T?#\7a*Wmcl7naG&[G/ + Z**AN)Unrn4,l%8nEgr]TsY,mF!MN_?-G%2&#`>G@*<%'RJiojV;qpo:!$`+9(cG=m>6+Y9 + e*_N\ml7YWt6qcGVm;kP%eKTm$i&O1,sq(T"b?n;#UJ8Jd/-/UV\g6gZAt?EOtX#aK$rgp; + *:0D,g$kOdW!(6c'u#--"!Gfl<5&iJcD>^a.\=@Pd!3goEX<'\K\p8dmWnTd60]'S< + ;FA`(/sjY$s%9I>j$LF`3Bb]gfQ0p@=dopeAOMBq90EC#^Ye,RDfqo2#kRZln[+L!#IL`X$ + R]gn,g%ue[H1oogl36h6q'0r + 4->JAGVM?5fSF5d+HR^XR#$AiE>JH6(qj2/2)i + \\q@e^3jOtpFfJQ$\ZZ;'DHnCHP=p;kd)_%UdTJ^EE2#oZMe/9n$eU?3p\XMJV + 6E4`/2hLoc[IcX]mr:+R5<)s[%7 + plAX&$u8k],Q+aP?u$9!mM(pfR)X*Q=YX@?o(jF"L+M9iqa;HP&@#+2ouVT-*["B0JYpC)s + 7HWp9HpB!pC/XI=@0X%BqBDVT?V6E4`F>NYTmFl-OLX.mqM]]0j["U854W-f53U,obY9hpM + Hblc?+[a9j!@<+JaiH'M0l*nl + (T4_58A%'?nl[T89a`G+;*5$9nA7DP1LYr,u4?A>_9fo+nq4YP/79/no\Ftc+QpQrb8`Wi`;oE9X,GRkG + 5S5:iNK!ml\m^9@Qrp=+VoF1Z4K'HQ@tbdEhX,r/H5.Cc?`>!#tA&QO6oOnu + ,@_?/]f8JCiGoV2Mr7uE>\MIffj3P/.oeBKQ\bW]V>g%t$OR8p_,D]K8_^H61lFcQ(;;id@Q + mjb=7sq9XYLdX0X%Bt"_>/3M^U+f"qR35BDM,Tg@I_ckrggS7eCEZY9'/)<$Appk4SN#M2h + 0o?Elh*JM:2Dih!'>)Q>kqPZ0RDd=^lCm[$]A[Uq:qp + dRQ\\8f]RO3.#&!qMQp+'G^teY8bsEh#"HnQ#ZJ35dm5!3h&(O@U,8A5kXQ@O0d9d@9uU`S + fGE)&hTi^5i@(,`'*/$GLdf^JP]X\aC8WAbNS/DQ$Rc)_'S@*0WtYL`mnF!WW#L_%#"idmu + L/toAL4Xksj[e1t8WgC3BPMXgD05:Wd`i\\sbS9@[B]$)=^ii(Z.McX6_F=RBN0'IDK\>E% + tWl)>_seCGn5a(;cu$eWW"H0R&eA5HANn9US6BB.")4]OmEq4ZIWU\f]Q;/]9G4!atKk-]$ + goLL5A+2?Z#@=_g5esHkemus0-EUdL?5+1usjg0lI#'b=`,"k'dWN&jB3m;BeQ3iQ&K"P?_ + 72-LC[Z?leBCb+9g63dIRMsc^^0K[ML,"6d0$tjM$O#43'DFW_fJ)\a& + hGE=:UM&"kG5S5N)EHhNTCct*F?N^Pm2`K7#NHE&8X^-h"+nQ2-kQl"O$V9F"PCVM%!>O;2 + U2^QPe^9Y@mFo;h%6hU9Hn5I4bH$M>4]6)55oa'[S+n`A5Zr + "ml&[F4FVdN_rn$W1HfK6eFPK6@,,t\*>Rt[i=],dTX=\@HW$K`/45c?B=8.XZ*G`GD`@Y3L]J[7]]B0X'G?0$tjM$O#!r'DJS6gK/`V"P9V91G$QCBc + 3QbWUQpQpeTp*qMWB?'G^tS*[^=Nrq@dSc]I(b.U?F:iBG*=ZKjhaLdR+JOX6bRgS[;>!Rd[r5VX>H9nOJpqGbC;5o,NL + ,g#q'BX?HFZB$RVDASk&H;DiEO"pe^q39.:+%n@05@FdnSW(Eofp\i=V+AcVsl>V;u:g)eQ + @$]L(SDjoZBK]XRb-iZC*:1hj]u]K6'W?Q Q Q Q showpage diff --git a/testfiles/cli_tests/testcases/export-area-page_expected.pdf b/testfiles/cli_tests/testcases/export-area-page_expected.pdf index 4a510aa30861c99e592292ee9ce7416c8e36d642..ad6ddf46d7a16f0cf0055415b643cd3e6c80e894 100644 GIT binary patch literal 1484 zcmY!laB_9zf5X5ds;yG;i6ls zk}vO0J^$sGhdIAf|Nlwh>zr3g?7nE6DLn0(bNmA7ewm=^nbSh7zHhqna6(43;MFrb zw^#n{nX&0<-K~|TW%Am$S8h)&bJI)Tbm8NK4cwwHw_OOl^mKxzOw!+qM*`BewaY%J zzw8s8(Gpv7C3gPp_wkw2TK_KCu2RzeKL6K8VfB`}rB3%>wDPqlyIQXazt$Jz{_xp} z#1$O!OkLl;aB^Ea_O8fTZuBx-?BG&8n`3X>gPhIPPj(%i_TAaTTJ6Zjb(;Lfw_b{| z_!vq~4Ur0Z+Zbl?+ruQG#^p}MwZ%s-E`0f^3CTE~$W@%wyYzbBZ=Rj22*@4m!)E(w<8x{0J zDhpB-^!<~vf=hBiM#9pg1w1|aDHwt!+%k)bOB76?Z2W1|IN0Dp*R!stObl!mjIkh} zqJ{*>bdcW_6ch~M0bxmmvkZ-p+y!$B$X!OpL^&*UqGR?U1CitJ|BAdfTE>2Rx>;mM z7rUs-F^4;O#qAT?q|9bbT4J>S@1(5Vy>ssAzn}j7Ukig8OJm|B=RO-n77g7=n_Awy zy`^=6Z&8>YW5b)saK3wr>@CVQUow(+9b@(kTYb+WBDgdta)!skwTzP{RdyLzR^Q*1 zI_>M0ZHsKPUj`+=e);8FPw1i93*OH5S9g{04l-MPh1GBI>i6M+)_=cl$=r5Gdq&Q5 z%V#2zMNf2==d^thEVR=3F3m1?=+s4fj@6I;83yvlg)BXKtEPR1grB3KYR_fMC*1Sw zMCa(=n6c;M!BPkJ%5>lBPLaoyu5E9$mA#O?Ak1W4Z}ZdA1Jxh41v_u$jEye67$jT% zn!~x_ME|Lo=e(XSkVxOh7WgZgPki5}_-nl%9-Y4OeZ@oR_nyv|`L8@`j>e2Ya6CX` z(AdxjqyWS-L?mWd(uxR5O#?>_te~?5^RSh2prjt0S(U0_2#MKng=j+sBL$;au#TYo z{1OF2LwNjq=B4E;7#e{2$mL^lLc)*#^E(?SH!`{y7#f{4FfcIsz|5&;HbKI=<0`w# z3|5C0Ef-nkj)b(7og5;`l?M+nFzm`@Wd?ZyHJKKdBo>ua6s4wd85)_IajB}h`nz!f E078;KD*ylh literal 1350 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 diff --git a/testfiles/cli_tests/testcases/export-area-page_expected.ps b/testfiles/cli_tests/testcases/export-area-page_expected.ps index ce348bea70..a3b3f6b3cf 100644 --- a/testfiles/cli_tests/testcases/export-area-page_expected.ps +++ b/testfiles/cli_tests/testcases/export-area-page_expected.ps @@ -1,6 +1,6 @@ %!PS-Adobe-3.0 -%%Creator: cairo 1.16.0 (https://cairographics.org) -%%CreationDate: Thu Feb 27 23:59:28 2020 +%%Creator: cairo 1.18.0 (https://cairographics.org) +%%CreationDate: Tue May 7 07:52:46 2024 %%Pages: 1 %%DocumentData: Clean7Bit %%LanguageLevel: 3 @@ -161,320 +161,320 @@ q /ImageMatrix [ 630 0 0 -630 0 630 ] >> cairo_image - Gb"0W#BX56H/SW;NF2j8'mgT%fW-Fo?>[`Kjj-sYf3!(&ekM#QR6&W*m:?0DCN%d!log@ng - IYG3j4uA7=i;XaU*TF6OePBG!['c?"A'.-5h*V/*(Vb8Js_u#^@shccMZSk4#tuX3"&?"T0 - Fu@k.*"V<4i84P0\\XFfcXDRn*BF%KI\$/B@.ak%je-g\s&182_86Sn`+j#&qE/[_f3IqRJ - QB=p4oJk'H^?f%/)r$d'kKJ7M,Xi>N%2+K84?\;%j%888hSY+`'"OrB%.CFC`aUdeeO)@?i - pWDQd-L;F!_C(qGdTs+N[)MNl\-nKnE7Ub6o$4@uU;-:>__?*8C'GdjFYQ0h)KnX:j/:aNc - @Gr+3$QuEG=La+&!_@&?(9X@8!0S7SKki(fJ>>YCi>N%2+K84?\;%j%888hSY+`'"OrB%.C - FC`aUdeeO)@?ipWDQd-L;F!_C(qGdTs+N[)MNl\-nG@6NZD5HjR?XK+!WBP(9X@8!3A3NqY - YD=-\hZ%peMMa$4GH^=9-pBfK\hp^-CtdIBsEr[8QqcM$8TeCFC_6CJMT%\#llCDss-__?-t(o^:t7(Uf=,<`2ar\ - ;%j%Yib=YdpabBfVt3M*q75)Z4n(WTs+NuH$e/m(^h9c8MSa+.k)kt>XMp#=EA(eG3jqUa[ - umI$eJ3W!7m[n0dX!>[]Q>broO<75;S"ICFC_6CYm`!3UXbuqkSbq;GRXmY](&*"^oS:gC4 - PfmRiA#/%knM.4e^k@0'JOG3_QV_M4W71$-ceV&oZ\'GdjFYQ1abkH(oN+tG+&*qFD!"bhY - G-nG?[%YP5U/?eHRHr9ruKki(f!':\OXL)![EuqK[TWiAEK`tl)_36B#(A@IVZ9,a%8O6eD - )@?kfNc5Hp(A;!D-,kukMd?:d.4e^k@0,##kkHq!KtYamPnXoJY+`'"lo9*'W@N[^LodCkj - i=7&E".L)@)q6$/f$A3oPR`2;G^K*L;F"rj2d?/Xqp_9A0o3'gFF^$/6kPN!SDDQn7&j7)g - 6DjUn\/:[.n-0=La+&!jL_KTg^":[Z/7k'GM=cTs+O`#iAg3=sna72)1n@5^cDEfRXnr^hM<`2ar\;%j%YiGKt$bS.ZD-JB - Bs1^JkYph5/6X^#JET6Ze151]A?:dFki>N%2^lB**k6Vn^m-S*-$s$8&=La+&!jL_GCNF%p - )tqR;abPp0MhH9,SQVo3fS - s\u.k)kt>XMp#fQ$3d:mHO%;XO/V0]Z-aI_3GWUlAh'GdjFYQ4#L[p#B.d`=La+ - &!jL_WmV22l0:^E"$QE-E]B/?uFsD?\-,k`&n,'@=/6kPN!SHr9Y,Uq<02IYD=(0"8FF(25 - BfE?dCk)IW$470m:fm^@SCnd(@Dd;gSJ4!Q[VT]H^<^\-cY]4tm(gL3+.L-a2&$gV*G8^$N - ]2-8H>MbuD<'nR$470m:fm^@]X)*?]U::uULP`h=H2`WI3lks4jsr(XJE(>%>=sna#i$'a4 - n,qH;oiW%;X:;Y;&NCI?J-AFR0/CW"2;Ql<_VS=qZ`L@+-p'> - BPn]#ZK`tl#d=VrJ2!_a@j&0='l?-^4*c!^?`NN!SXBR&8[h0;g(4ZS/%PBRD%.;kA$Q(Hk%57.e6K&^c%0M'_gS`P$'g - OsL2F0.niX?:,:.k/go$4FY$'5s6P[RU#\VQaL:[oZJ[IT!s#1R]7b8V)5^YQ-46nbRWc6I - kAhSe\^,p.C2NlROCkpLZ?`Z]YA1U9FVK34XO16([@`2_PQn)HfXO;;Z"FZ?\MYh7U+"HC2 - U*-nG?+:;/X>S"7^g"`\CY88dV*q'Y1@0'JLC@;R`' - "-q&n\V_kXJE)u[O`pXOrf;\@);(jdg[30^4-i8CV)X`c&Iu&:%dhKk<,P:=9',,hJH.mnZ - 'd\@j--nG?+hVL+AhXomnn`!-RRp.Blb#h5tKIAAO$dS+/)K0ijof2 - K5hTJpTcYd:>793<>6CXc`5.j9l!Ea='V3X;mL_?+]Ai5l]<=h1>`M!*2'.k)#3$4F - YK6RS0="Jn6%Kak*14fki_\8Z*s.k)):Ka$DaU#X<)C3!Gf'!J)uDQKW^Ho\gtk$%"+^^:D - V^o>5lQC@_L<"B[=UiUFTUr[ms;GG+;krQU?$4FYK_ag%1FQA<#+PoU=696FjB<`CY<`11G - $4FYK,=`Lg7&3`T(QY=[Y,nEYa\%FUUTa`g3c]J-5sDI:Ic=M;X%mjhYeH:[F]MBC(PN%Ne - BpVo)RSP9mN^RVlWdUbEU3I9kub_$Ka$DaZ%FBWXjXo%KBS&O::[0UfSqF4.>!Dn!c[GWJV - =I\jsL%Pn^&"B/V^>[VS - c:=9)):Iq+$q/8jaEiXp9:KC2X58)'"S.k-(&_?+]A`9["(8l;Y$M\j_B/NWQV*bI@:GrmA - 'Pd?Ois!TDuVbZTNB97t]j="gI.k-VpKa$Daak,Yn1uahh.NL]K2[Tr.WOMTG1?_&gc4KH8#e*i4Pd$=fD^[.:VquaQUp/*)>s*:^,#8QM);27bS9 - _33n.Q-:^o:7ZB,]j(7Al#U:6lhL.4uU1e^DNOT6u7-!c[GShAHML#jE&n`eeK\)>7-0m*0 - FHAgN-Q:fm]mS_cNc+9^\Ho82tMr'c%+h8naGQuqrJEp:;g_?+]Am9S3#9gGkQ+J(JuWRUn - 67Q.@-bQ@%I,WQ2>^o=ee9oXAlkNd-I;=g8lSX%5fMM+<)!B>46B6LqO^OX1EUg@[Ll7mH, - HiDQ0NOA)J>EbR*/-)Tnm$JFSfFN0o4D4ZCXOBf6#eQ54]oo+QPnXoJ5k)Fm@rf4<^IXD$D - jbNtF@s(bCcAZ1>:3`1=tEsLA4'c+3-BCkgk@np7u<#FkH=,7elPaS4uJst'GMc=&SD,^/_RE\j" - 5d_?+]!@)u^%_o1RM'Q1%K-t=P,Ka$CM#BEi`9_`Q^`6/lH=*6a>OZaL`8J);HP0Ql*UTa_ - <3I#Ru_/(dR6cGN030BI?QCek1naPn2-<=#@=9*elnAK@"_1"rd&J%nR\ZqE.rC+KcXb`Z - FO0e?HZ\$SYQBU@0&?.pui?CH%&0m&FWTF%lf8WgW%@eGFddY/#h$4FY3UB_kt(uSE*O*I$)$nEe)LVa+3jOf$;#,D'MmOb$ - +`)EFX=C4X>iQs,V;G^Hq,*'G6JAL$Delguq8k,Zp%\e*]12N+02VcYl'*]EJDJ;PP(LOPnCWTro*iW]X - mRg5TnMUK8rBq7F'>#X>F*,-nA<Ip;KJ,`e$^\YP#5NGS$kM)S%n8ghjG-nG?gEq[a9R*U - aQ6bGM&k.9tr2mQj,V\c^p`'`Zk'Gc3DDnRpL?ns\*FRe?9Tc2`3htB["dXs?F)gPYR_?-t - 1eB9WW`cAC\'@0BnC=$7DoOtk!AL.M6"lP,6HY42>jH*+KDb\YpdA[rS0YK;Hu1@X/.O25PoQ7Yg!X%'Gc4eZeHhG4*$8P8F_r&?5ds@rKS(DZQ#p.CF)D - JrVFtrm!dpP(Ok$6LD1MSR<)R[`]J*3oEauITG&nG5^*,& - h#:2-P#4=1`3P8oJ#f_+;WKg/!#r0pWuaM2LN,?H=5mGXW@F5.4bHH%N6(j9FhkV#@^Z91C - Njm]9,8VlEnRb5d$mgs4s@Ab0#Uj$OV&Irs1cA"lR>8P:I%Glm4e_4<0/_ZVE$u,O@lXJ*5 - n(b.t*S#lf=GWd)Y)_m=En`UTo0$4FY+6cYN49\4;J=M,Q.91YtF>@0t,FlN(g`T$R'QVbl - &gUF4r()4FppXV),$c';?a-RZ;T$)GAGVWYLZBE)qgTl948^UeXiC<([MaqFCm>1XTBG;54 - 7>HpU24-K@HD#L6@&?\CbLbe@7^hB;),9ei-,UYT>[On9o%_+RYJpI5-jg?t,tEtH0pk(g] - "`J=@'!4k31bm#*\5=mOR-"3#DR`P4^;)S66C1D6dKu;"LQQneJX#6H&@F)[Z - tg%uq]YcsZELf6tU'!%T6D.4d\4i8W_K/PUCh;Dct-]3`"WoYA\\^1BZ?!VjL9K*r,8:R1? - "Sd*j,`_),_I8c,,;Z(5X`#eAb=",[#l]*!1^gQ`gd(RB0!/m:g>,`Q\2c'62-\k&,fBqkj - V%>Q!AOBl,pqcJZ(Doi;C7QXl"kXR<.2@/jh*U4@STl`9;.j;HD6CL^WoD5(]AKofYQ4UZp - _tQ6XjK;MMZ>]G[#uW_nn4SCC2U8a'S=1'n?!A-cSq4R[CWtp$fDZ,<`7^VJ*3S."+e;jE8 - osXGm9H[4G:[@!;Al^;N.nc/%<:*lA#=ckVrNEI;Zf'9ORr=3;M95K?81-G87:]>iistMP( - 74_I(_YWc_A@d&sKCKi(qP#'+%@@k`N"Ff'BbhuPc;;YX:T*dpHfW7B]k3p#1I!^d]V"^il - ohe[pJe>P1ALHiFKUO-*M8`:cM5M?9(X@i5)dcheMnL6!h#MscI;V<`4/n=&0,%&[(DJ%D#36IrG`&^k#F29ti[9U - /T$D5!0Uph7KBdLkd`lD7?Rr3m6SQ947i\E,*^@K;fUIGAB`SDQKXQ6a7\8WVb:/r6RNZ"l - R2\h'RgjTCWUnVn3\r?:eQo^)g@QLYgCkXSn05_?+]7m9O`#K?_jY`.pHc2/G'(Xab!oFS"%7er@0/JJtN"(tr_Nd%uJkcs.-*BrHP8r>quVEtg - iZ_\59U9FV;33sjtLKoV4SG6KIJ%31TU?u/Cf6(GODdcRH!"QZJI<)t6Y=O]iTBbp2#2LU' - Wj$ra)UhRi=&E'"%\2>jpB*I7ognV8%_l`*W>G1\a1&,DQfo#_F[)e]ZE],#!c[(6r6H'-T - s3aJiC]"$BA*&tP6lpHi./X+/ChZ"7j%XY*6K+";V5+Zi@50B^siC[_HtU,j@:;>XBoNTkc - sln>qOMm?2p4M/JJtNW<5.2%17GZh^:Ybi,=aC'I2@>1EAT(2%V/e!)j+uXtV8?p`;hPoYL - :USeMa'gXQd2=9,3.Slfl,fQ',CIe3X^TWeDi*BLpYTWjdU+SjCiG_!X40Qm0C;M2hek^i< - "&+BRQ"^pWbd3T!N]qPbQO$:>]'Gc4u,=`Lg'Xgp6QQLX+#:M,';b%na9br$p9/O)HT)mG.8!\fo2l5YGnf'?$*d0FoH_?)EVZZ!9<@2j;)q9]FlJAM - @Ap"$i[#)-pB)=jO5n,S.G@-Xk\9a]XVe)a(AhuO/m^so?s1CX]GU/U/D+5d/,$c(8k]e^' - ()q$2+UCZhoK`tlLo^:rJKpA9WJ2\l-!)mh)RMKgJF7f\3\HdA3_?)Fap9(Cb(,fN\Q_/VT - 2^g3W;eL9n?.]3A:4DU3hOq(Q9V$GX45Rgil7qjN&+BdW"^pVd*L#M8Clobj5PG.r:fm^pc - >J]TYik[-q7-`TK>I[DF`uN8GRI`W8'9.@^`*t'J>mTo-cTBC;E[?qmK)1;YQ.A9S(FkPHC - 4Wc"2$p.!>mhYZdPMN(\N4IEt@aEK`tlFi5l]?^KcVX,pAf,.=9(f#+'iVXfIM8ur7hCe6"'d23HAY - e6")9dJ,-V[G_HP6mk8rOj$4FYjnM_4#'_\El.6mP,D - bsO97S^T[hk722\qg4="2$p.!>mieqE0DIf';\oEt@aEK`tlFjD/fG$7 - I:H[n%'/-$om?Ckk-fOg[$5PG.r:fm]e:@4T\:tUk)Oe6uN2^g3W,H(902^iJmKgkR*YQ0W - PhEmig;Kf^, - /^S"hLE>@k$4FYjR2o5c/SpOj/O/k-cVR^D7c)f^3SqWEf;%D^*oInC$c'j:D3Ns2Cp>$15 - P+s%:fm]ec>K0iDdc<2#l1/!G_X':0G8;1_b0T@q4S%HS4S%m@]a'G`t3%NSng:tW - s;OSmH-4@QlhLrJ,h9F?,6pQ?VEJ7m^hChmflC[5R4Ldb=Ka_-I2FdDsP"$AqZ!Q:t:DLKVG1 - COUpUO1,o4hrif(-k"!GHZ9RidUTIRp+WRIcpg(Ts+N+do`rZoZ`e9D/E4L]?(RH^bZZ?5Z - FH%fCBa>DO]i8Y@4n^=8ANpG_X':kgZ_MX3^7(k-1t)l?0!%ciZBt@0,.N9@Ln"%Hp*MQkob7K=rX7( - `&:1qW^;5G>;H'"D)Q]O7)f'o7(VeJ#+L!IW]oG6EpF'SLr'4MW1dg?adY(k]ldnI":t`j"VQbAHR,V5Du&o,p`+tk&dWh0!$@o - =5XOsJO$nO?HW0['T3=KV:fnk8fHUFl&VUC?XLW>'gNt3'fHUEaTs-dieWd6cfZbHD^oo.; - &3q<'OXRrW7j:l\7KL=o@=b)^;7\UQ-2'k0^.2U_i+T2t=*e8OX1,6o4VAY'UBsHk'G^rWp - .,176X^#QX1tq%r+jbd]ldnI"CN=*"al7l&jUtE=MV-*Y-LP!#Vl_ppl@@\,\hIGq%3`T0F - A3_%!s7M"@6Gshe91M:j-r@X1,7uOT<$]@0(jcNs(PX-nHL#4VC"hU9K0@d,sYJMfZA67fl - 4L'G_7s*XNt*;-:VTm#_:2!egfjFY^cF.B8;q2B"Q4J< - (9iZZ&cgcW@0'#)RQ"9=>Bk%dMf!$d":ugm(9i?Q&cgdL_?/#i1i9r5C4Z+TVAZ@OllKL(QZjbp".(IOT9um_?08>;7\T6k@?Mi?.gG'"l,=J/TdQ;*Y-(?TLUn^`sg&jTp#Ye[ - qtYXd]9*l87P+>H_qTHbNEOXOb$"H_7a/9$rFjic=%!Pg,k)o#3_oT:c(gT2#:#%jlbc)e2)-a^H,H%%9Dqc^6HCqtA8?bK*4OnXlnao^I?RnDMM>;mM]?l - 1Z!D1$%qu+8ODbj+%!e.DPlTN#,ikpVn/1_KES=Fe/:lMBmDPCcQhf!*GFqKalpCL2StWG[ - \mYIPgl*a%!4<4^dX/mV-A^h/q[fAu:<4T(#Mjrn5.Q@JK!os0NRqXqq'#QLNNO!PlFf/Sn - G;qYg:5r;"[:4oY5TjidO^*o/tBh(L(6p[mmE5%@'c%tAiF1XA`#B/b4UgYBG5r1@q[M!U8 - FgG-g`[^&C)85b:BfPkg;$V#U8t5!,tes$[_mNWdji2=^V1!kW*^1GO:hK*Bi@;FD0gVNJ%5mD^Oo,[u_,'7HN&@M - (JI<*.X8='t1)UdubkJIe&]hO+$B/@DfZK2pK)bI"se5I\/!Z)"WMIbUG(&p@eI;,6>Rf^+ - \iT>8qeR>4Cmb'3;3uhkN]1FLqU"`f00]kbH&qnK4[jk,o9q(FlJ^Bk)G64BL9@Uds&$DXs - ,P!-*H>Cgl;F?i4C0_[d:FeLOod:^M9aNpZhu@j>?*@EsiOlc&lUHgGA?o"mQCm^4oRhd<& - ^Klj?mFUc+sr)D=[\dc8q5Od`tj6"SM/KLS(5O%D]M\c!_d>oUr4h=&Z?bP4,fctKp4V@b_ - jFb=8PcX`G:UAZV+5!\7^BBi_UITP.iYP+%gGZg?@AA&VXn-.c"=k#a]UrG;Y?Y6-MdK-gh - aSpcf4on[qXhmSo[2VN^,W=$0bM-2*^uke>5e?("RK'ZB - XXT<.Cb4M\rGCfSCtKALfW4WEGA`-LCa+r^XjMs-lA"s1tpWdXR.!:B1?E$aZt[9T[?8j0!"-^B! - OW@*pV>,g[9fqQu?Pkp88T2Y%88ps702+fHXe9RcdK9aUOm#N1Q))Nc5b?l=(Dr8EtQGB<) - rl1DuY>'*2f!S3/`uYNe;7Kl`n/[VAo02rc_7nQ((qpM$&LlrWV*:R%UIor/WFtT%e;E]ab - 5O,lejbN=^*=\p`a@anu#S3q%MR#q8Kb[8PMo^]m@I%0?itFB;.N@,>kTK\+!rK!&: - cbfZ(p*J,J?^p$2hi]ZiK&WMrLl;X0CcDg.gHB[9(aFoOe-Kou+W4^W.cBMdi8^,,343FAk - lBPH1&EI'q=j/WP41.m92JXA7)l!F;:EEh(12Z=T("!+0eDfVYN?s7UB;]\'u`U"&kY>EE3W/lrU40C - l[;#9poOGPQ@JRekZH,L;remQk/BfHX7mqm/coJ2sCA<`cL)a\T34NH4h2_L3ZDo6aZ>^Qs - mZ_Y?U]6e'fWIOuro7BKlZi5HMq\^X[R8,rpM3"&qo])"MG):uKE\m*WDI(MF*WJsMu>rkH - 3m?u&!fcP>1:"ssbqY^?eZRtUH#_b0:'ed\`iSko84?`\TmONcq!_<;G(H*pb`MYmW4Lo5F@[<3In15Majof*cmAHR&Sn'162dMcW(8f - LT(E7\iO<[e+!W&*sg6Nhk6s[qeKom.Q?E9Q+Z$\t9FK^6ZNi4V@_rVUNDJQEL5`f>i"#RV - I-%VFEc0\j#mn+;0rTW=@b2L-$@+mu;6k>OD,*M+`.cZVH_!(]bnLHPH2%V)>E_F`?D)egI - :%Jf&nZnmO?/+,cRC-0p7$k!WKs*,=>0EJ?Uh!!;Jh_H'^?h6Q/AXK6N5,;hXTq:/84pB"r - uM0u%EXKm2YZYt%G'^@Z]m#_8ZG]skqeuar3q*.[Afe_IH(`aCfH3eGq-AF0B*_'O[6f\+? - IGT?md@G,kWaF'n#loh?]u%MQ_S=aYdN9OIo9B*GF:mGcmmTF12H&3h.4OPE7u!:phVJLV* - XMh'j1W<54'@[VaZLkT!I3O\OU(i\.4_rm#'"PF?gZ)>n[V - nV6:1\.SfAQYmJ+oRiS:/e*>M15%5_,-:NfECnIh#':Y9RDhZF$BQg4ZWTs48BkhjB?BktF(G=2+f^WHO%-aU[e7G@Sil$3crqGcP - ']oGnWXL''sA[#T03[sE'2@78):F - igunj.@&kPiksqB&237fn4EF]d:6Omb2W?[NZH\u`p^HMkro4m\lNdLYem@!)8l&V - 9Z_s$kW)TcP4"2WD9,gmu^Iqp.!-"k0_MZZAi//=f,s5+FRf%!/6+k[=&+V].uH(`i/fB8mbg'OrT10T7'A3a - i`uBmA[O2OhSO0,Voc8s*H`2"XX/3P+nQGW+pTTqC2T/S1#oiiM.C6@#Y.F>AUb=P)8V - k5-.))n7El7@iNZY8rr=A..%7%4mYR$OJ.+M*2S=X$:f'c7=D?,h,KgjqtF_#iBr$O'rrVT - =:A1,r'Z9SN9.'*&P>=9R7-,#9qRs*SpNWr&R`V$A.%;-3tTdn3>7#%um - ?W=0Nn8P<'uIRUPi]TO$9Bcft5Q[U_89iH12+,?0rq94RCD/t[;-5)r/P7TlnXcBDSh'jh-nL - P%C),>l8Q!7tMY%Vj+LigNP00Zs^+,V@UAji?dMF?_J>08=2BN6\;oO46;Ul.N8meIrJr%8 - f$%4YEBCU(8B<m'Qu$4Ff3Br)$5(f0TAWp!O4QSnh`'GcMPf9GR#@Q]PqX60GD^:F?dU9HoL5+uFMJ+H7rW - uHR?$4Fd`M2Li!Vl9b_QG1-8#bnOg>dc%%L#9 - \`;!X9^(A(T0;:J,!V!(9Xc^s"hSj6>#[(&tW4Pn(TV'G`C/7B(8V`j99S$G@j9YljJ4Ka! - E`>H+2D`q03o+apTtn/-i_SG9UOnat"&jhI"nh>qkX_?(4l;)+YT^?P`DK>I[jXB%'IYATZ?l2c-^_?*bc5CA*`@QCD: - CB*0_?haU`5[a[t^(fo%MC/m4\,Ie(m/q;HK`s#bq8k]_r3Z+(&&<#r(9m!?V$toe:?%*15iSNBJ/ - ^\L@Q_td=*b,PE!_4%>]RT:jbt1XlM0KOOH:kAi0(8oQ*+RV5PRn93<6i('GbZ665@].ejU - >RZh,XG?laq)h"-b.4]Euqe#QF/VZ?e0-nO;Vbpo$CQE"9eVkI,%GDdfU5bapUJ?-,sqF#i - E/FR^Vhk-qs>gUBgF]M&2aNk(k:/G'.D4a\1SCA8Gi]iA/8%).+*`oKLfBI2D;Z_Ie$4D)d - ZARbF>EmZEfQMspbS<:6;0S-E(BEG]@0$md,GoDIUnjshn#)rKNbk1o4n?2PgT8lck6?H=: - ftLP$WJZijas)u.Vu^p4oC'&(#h#HJBq(p^`203.RV0q?MO'3W0(ro9N7E$>,-H8R4,kK-n - O;E#?<X&H:k0>.dBSs;;6Rag.>gX?3/-BY^jc[X'!ZffCsG - jfQR(:r$=M%@X<18\j7bu\XSJPQbCc`^%"5$"M!@cJs/E^[']?dh#[UXRd?ibrbUcbM`3V - 0.kpV*m!hKtn+b:A,<;-6*1(_t56^4!kl[cJ6d#Q!pZ/J']F$OD&H`H(tC,CO,2gFb.b]V7 - miCaHJS<"p)ZZpgsF.dH9Q/+(jdSfjWV40cF"LlT=RZ9G3r3PYL(Z+8U9MMLkBHmT31-OL7 - &qHcig'\BH+f\F!q7d8@L"#Iosg*)b`!:NEd!HI^2ei;H1$/[<6OmA)4`^Ko=9l3_d8e - GN46<^n4G*5hq]*/Eb]!-aXVs>sn;=MDGVIG"iPZlePZL!Dln6(P\ofB@sGkc-;a7(YQ#hB - N*a;&*r$Yp"1d@5_JJK5OJN4;-:UE5+l@F>0jgmYjGV_2r6)iIdupoF'IbDHF - LnoUTf8EbZ7 - S:f<2)3DQJmFZL>M)HZO&c\V3I*:DL(uh0>S'&g)f2I,YM`QLh<[3\0p@[]?T8;fR+Lrm&X - F@D,n"nd7PoK`jeP!WO"2U7MmEE3MW:h\FjMHY)<8'6EcRqscI>/?_=5Ts0&sXB/7pAnGdX - >[73\F - RcBfND<%)B;2DEIjV17Dmjmmn!5&@TMM]X/jD%@\56;C=#R(W-R - 0ktlIC%.,Pu+leG=94`L'7\TS[U:+t+iji%*\c.TJo#^\ZIh4$oaImh5Rm - <.uAA5Dt[?9PcefX*SW5m!*:S;bC(@\ - 6j/(I?_4b*Ci>k*QgW4?ocNRFZ"#A2\I/C1kZhbdLk\#-+&k6kW6.A"h02:enA?F\Y`B;2D - JDtstPFFq@2^@`Waoo'a89tZk5g6Ph$mkT9c[S.3s+/(qI-nO:\hbp_LJqB9IX`Phq\anNB - K>Qd:EUk=gkQHSCr+rTpH@G`qCCasBdTF-,lo):YB?F"Ii*If_i+IsJeFWIqUVmi1SD!XLY - N`dl;\Z_X$4HX^?`L&;lI7BT4uuc:1XD_u(\BEW.14iFJu0f^"HVpFh4(?+9aeo@0*!iX.l2`T,oYW8E\ - 42+^:rCb7@LD6PL=*\IYa>a3P-N@or3o!e\@UkRmB<'^-*^"ao_W.dH9L/Zk^TUNdt'Wk#d - ZTWl!'rYD%S5>-R>;-:X6Z;f=pkg3f/6oriG[.2pPPIG":F:\1M$J^WX!ra"RK&WF#,[3Bn - cBZ$s_k=4KHD(0e"n"ig!WQB%U7T\\5'W+g\=;1J`:J;>r%&$>7UZ>tqgs]Ok0<3:hFHboY - -'PWc;'QuQQN-@qbUD1s.3(;(nm[q - !s]TupX-OK3l<>uea#p)TaJ0%YWu%'n//cEs*BEE+0?sHRr)!SCV$/9:^,QM=$15?ECo'G^t9;(CkQcX/+B7k8=)TleGZT:^/_c=%Ge'SpC.'l)K]ST:2eNo*W4fjYX!^i;c1joIlUdsNjb1d3RaY*i - @u`EWt@>M8qrOEn9V_,pEpXFosd=O-62,F4oD%gDu7ljgXO5+C6e_\!"d\9<7LO3e)SEjBM - 7HtKa*/^I,?)2Xq<3.SJ&Y6Fct>drHRP8Sl+$O"Ze.dC0FXM8utXU/jigG-fXZJEm@`.p7&ihl/.7U@Mp&2$f@G0%rh=!,sB2[]M_iSG_W8tVlD3YT#9D;J(J4`iDufPL - _"J"flc,ke\"d^+F*V4sR=rcY/`(@Wj7K>HrEKUX$r)D7Ke'"VSYLOW*0CqeEJ`->S(%j]gpaGYZ>'KtRBa,gMMB3m(gYiBT0 - :/?95U(GX76R:Alj3,(>'K)P!sU!Q=H=/gm1]U'sB=Y+hMC*Z?LL7-e$F2s,!6Gq8TcmO:?\+f/>>($[L`Dagkk0/@gl2Jp*0QANU#aH9#356Z*[Er4+m":sjtNtT&'oFgU+L/?*;R,G+;Wj3 - ;:!t("3#'>LJhFftbmO?dO`F*Lgf6tSm5MT;C1G(/EW*!Ksj[rs45l&-O - HKNN?,Mk`\>WOUk?ufT'pPI4G\>huq3aXL:RCgi2bE]pGJb$1*H58*t-f./`gaHHC0KJatp - fW"Tmf?.dC0O4u%7B[4?Q7WkerM%q4u>^^[]_]>M;DDOXa8[DmpP.YS$--&0+O+8X - =qnHc_knu;Eh;m]L7(N0>UZa3o2`;+E[$4@!of2U8knA>S-Cn3ZD]X6JT5"/6QnQ'I8$3(A - bd3BXn[6/)nCTg_F$O"V=#k@IM_$\06'sacl(+Fa*@(^nT"uOMe`&Is"pfZ]j5E - 'Ga4^O/o5BF.XBp*^R7Mn"4JO:9Wj^i_b4XeN1l^?EGiHTWg]NI9o?*?i&2!nH-;hau^+N9 - [mN7;S3CAU0_Kpl1%O>H@*3@AW+_X-nGpWO/f/AM'pU/R!BBS - SONQ07KdQ_:QJn3*SASfdleR+`M?a1mPrW/';0lV^CWM,E.YS$-AV5 - kdIeL+IiW\s>GXra3f#rB$GTbq_293ld$MS)CYZuds!l;?N76Ss]5?g7"*k7[5TYY_%d\Fc - #!Fp8`S($3mgEtdg^=oVh.>7p,A\O%Jp?[&i4F\CXcUkG\gS?XD9*#.>3k[e>Fa+"8rVU+8 - ?_nGP#aD;7p,AXnX(8kV>E-((op7 - qS6L=Zo0c(Pl.H*p>;]7Jisn\Qja'!l6fp76Pd9hL(2JhY!V^#*UrPNt^scic7ulFI:[m*K - qCZY4_]=8n!99l)d!2h7KSALX.d8R;"Qt)g%s%2%ZMeh8S>nM6NJ9Q:nkY.4c#Y>u^!"jN) - 5uiU]t+Sns%)HX6@X%gQPUZE[Zoau`LnrM-S^E-tGM4YpaE'!Imn/tJAF0QJi3@H-Z>/2$0 - 1%eElpjpY%O92J_5i4c4cT%0kHLf_nq2bKDT9\hfoSgW4ehf[c,@9s=b9kgaHr)a#S"bPbf - IplQjV$abhHtJLQ0@Ot_?.QY8OYeaYa"p2MI)]c0Rg4jI - c:sU]eo%3$d0iR]Nd'T@=`r5UnXIDa7=>!HX]ghp28R[1#l)m'3?+Qi?Zttns.KYM%:H'-0'mcNt:iN= - %SWWAg\O;k,EX<'\"PI._W+Wu#*mu9%7ciiPDo+b0G#LVa)nnHfs'4SIDul&d/74BHU5S%KJ?X - g>shK!6`\a'i9^G"l\^[9id-pua%V&.kmBeFUR":6HYI@P/XjAMBFnojI="_"i#-goV7U.V - 4r>FGRd8!ARkK4jQ>W>C**$\X("O?nrP_1#dUQEm6YI:lfU@"+..XOn^H2,8:n!LqtS+3H] - ))oLKQm?a[mi3&g+2n]E,`$[S2X]O!_[gV6ig=!_-nGo40mo(-`f/u)DVa-kNm9?rd:IX&G\E7=XmYU2YIWC8 - 2H6jpJ)T3&Ka!(d,:_u(o[2XGo0c^2q'?5D3a[p]DbUjZ]"M?XSAW5#-s\>%Wp:@Q"bL-eM - L,:$mjCcS'4HkXlc]QU^!7MpRY>]:VV>rd_j!>W\5nZR+Q\1Hq:/kV^EA3lc_K3b9^mQ93f^LckDKi/DtluqXMJ$4HXc3#2,al;od;LP]AC5!@e2FI22'iW2Yf=*6`_GqIg@h;#5#q - umiH$4HX`pQ`m#;:!q%rd!e9YaY+t#1GY8:MA.TiYb?f(7%U0Rs:t`E1c6eY^jJOY+hKg"1 - j.Pikr\f>+.@@gM/L]CNL*W&ga>#SShlH7#B6V96?!,!rfVo76Mahck,G5i\)U]cS1aF'mc - NtN"SoWVj-BW%eEkq1Jdp;AV@o%r\dbF4`#1cT.=Or0Grj5odK>O-(`;+OMe`&Is"pfZYt' - bGE0NArYEDSM)X;\kJK$ne5`Y,)s5C+>s(]8UfqCkb\Q'.P6/$pU-fV1n?F??e\5RB - ;&?KKOqlI&sf7H;";H$4HXZ)AVqhR*eQQLA4j'YrN$K]hAqOO&U' - Ul1>$?f&7'SU'3Ea['5cd3/i)psNC^omHPP$X-d7*M#1\o;@';d)Y/=g-\M:+&[o#og - ;m^qk9&fr1e0(_FOX0$O;/2rdqDWL;F"@e5o(%^\m1H> + Gb"0WH$!'\H0G4!#3O%OR!G99YV9Z1bY6G1F`3G_8Wu&UP;J-R-E/$+%USDe:#=a^e[3#(D + R/-S[cq`OkGG;*Qu"H72R^S`0Gon),`i.b5p1h+1`j>c,F]5H3p6G7nEbSKpU<>9FKIB&_E + R/YZ?>kdp9H?B>m:;n-hg_K.u30H;1F]1J-lY,nbPBEe^M:cqDA7D2enA*S__QN + ]1b/l5XMl&,/=k%#"kHegN(36X^"k2$s`;:fm^3NP`O + g'G`r;UTj4rKa!Rh-nIJJ@0*/]$4G0V=9*&p_SS)C(-l&uYj`'T"d%:`/3H:.!MhR[$dVXO + !,7G0_D;lA5['qen-k!SOObg24@,/=k%#"kHegN(36X^"k2 + $s`;:fm^3NP`Og'G`r;UTj4rKa!Rh-nIJJ@0*/]$4G0V=9,KaR/=sUI1+STN209PG_X':E6 + [?)o1j.oE+Db)qG386$4G0V=9,Kann(Y29Sbb+qWrEdY#,.D&j]sb)%$be20j+&e5k5#j0) + /"4gIc[7uE5A_D;lAJ8=_!cb3W:W0:F5mm"$YrMOBPKnX.f/-+9AI=9D3KoJ.$H/bNAk,\M + im@^]Q%#"lCR\R(2r_R$A791br]eT8PU\[434],=+L;F"2c+CT"h-JYka*,bLc!.@M'Ae_c + _D;lAJ8=#t2=l9NNB4=mb.^59e:5OMTs4"pKa$D'k_3b5KtVWte!=h+hi>Dt1^bs/V` + $oNWa0L8"8O6eD)%$be290o7O'C6pn*ZX0RhpmG'GM=STs+N%oA2"H&hIkR8rC:_=@keei> + Dt1^btSo4>lZG#%T"`KnX.f/-+:lfKLU\_T#VS9[I':6"/^i$4FX,-al`E[]oS]:COGs/UY^?C + lJBFMYG@hqn-k!Si/ZU1bM4]9XcI&&:>3I4Of(E%KnX.f/-'n\4g``3_WH3[P(LQf1DCUT4 + 8'[ui^I;1`)]c\nmbU#G_X':E6%s1*Yp[!p^U)#;G^J?L;F"2a8Z$q4=tC!M/?2b*HZQQ"b + hYC-nG@Bm,,S)lb_NT=E_m+K6nEEpg[6)G_X':E6!NX$[a@=IV^qcqeO>>GTF\rqZ + ^_D;lAJ3+7SrNQ#)7TWmnoKa$Cp,B#cA>M/jX.k)h65Hm,H+.L-a1DCUT47_6qCXL'V1;_kghHEL5S + &@o+$4G0V=9.d1gHES$L^he;;YoItGW-l%[ZVi>O(Bak4@/ac7,pWa7_$a_5lCHYXqHTf=PO-nIJJ@0*muh-I?<,1n + )BD2!saQE3t%k&+^*DOGTShnjPcC^LQr:fm_c3;2W2X28K*/o4KT1+p]LG9_HLo7S?8Epi[ + 3;cn:!_?+\sZ(3[%non_QDl]%-"g6_ZPlL_TML.H7JAT/Z'Gc3\Npi@,D"XM?]k:cKlD2U; + )<5UFO2!G%pW0WNrR_f"StW@gg_&pTZH4V]EVCu"<0Lk)rb^>P>Dt1^`=f2:J!Hf75DIQ_RAuV,mB!VB.,;2GF + [;D3SDgqbG[f9)c\-=$>$%Dh%)nk[R>k+ + cd*(Hs&YQ3HuEm)C3"*[e#-XPH6Pn[dbeD81]CnF[RBnc7/7S9DZ!8MR/F4,aoQ!m6!2C@I + IoQN7"q3ra2(,!t*`^i6,(4\JA-(hX0"_mO]^#citFT"c3k7>gs1iLa4a[d]l(4[?aY-<^c + \I>_nX@FcicL.+JFE=SX=@UH+YQ.piS%%1IB]ip1F^\Ka.k/cdE-EO2Zt%8'CY\di=&H<"$hbXM.;X\HFBI0R=/EC<.k.2F*h + 1b";'40k2$s`,"oAc][AfN%E2Be@hQ3M$bFR2=a?Br5UTa`'Yj^K_MhGB!rb`H/W_kgC\@n + '"Gh:H")lp@O2+!2;!cZ!T",ec-F]nRO=P23WP6/mH1gt!IYXtJ-ukT?!u']C\!!G + KoNYmSVq$c]9,8J`q6,h<`1&+'Gc3,%d#ra&ZdJHj]u;!]idp(q!t67.m,6\M4h'Q>]Antq + %gBL/j2?LCo_b1.F,5^!c_(>!%p8'p;7ZF\fj5L/o7$@`Nne[.4UA?6sB@f(@@d!!*jL_ksXsV6eL%D0&/B])/c!Xd'SJ=@n&STs+N%OLKS + >7@(mV.ZgCU==.!eS8j + E5BrRAm=o+8[9'=)q?KBV)$9NaI[j+Ep[F=;IC`'"lP8^GKmFL;)E7'Kl-Kr-,o!Qf8:AYO + *@OtYQ.pIm+%L>-#/Q9otn.G,m?Cr@0*mrfZk?M%>nIVWjhZ03pCZg`jcu@d^NgHeaoSeZp + eC(\D[.Bl,a0D]R!RB`/Blp76@Es=09mloh[q_!P%(_/(9YU:P"I':[6b>,kIN7FuQ_9<`5 + H#,t/`HYQ.qthTJi]mZ*6+n*0dM8fo]e;FD^V/=TZg/-$KVfKLVG?_>JSoocc$Mj5"gB$buqD[S0B'11t1.H9DpC-+sZ@?:f,[m + ib_0UbiDd^]gX!mn9&W(>ea#q"Rl\N"lS%\Pnn?gQQbYA9NI?fT90`R7eZJ7aZ + $CdWnd`$J\hJ-.Q;:"79RpX@.du4n7!\4rg!lBbEci)!3W*$!1_1pZJ6WSkN,#&>ZtV`"VO + (/!B@L]n1op!>`_8:Tld"u\eW@iG;!ROp1[YDIa-a:TD+H8r=AuO,XJAAn'Gc3tqtDbLLr^=qWuabIH*WUm#V.>jNPYa9lG1,f(AXnn53c9mQ + PV/$cL.+JFE=R-.:@qJ!c[G?h?=\^\152BS*7F1aZ_5mh2Q6uN[_'*#U]re^o;ssO'%(18p + [HAd0gQ.\0-FZNGST[$!!g[[WP-[LL7mRiA8MoXtf7b"Ji`)0 + V::*"ZX@>eGk9+U\*HHMG:'Y1<@#S@CO^o9\>fbF.7FB=Nrf,CGpM2ZI6ZE0i-%gi][WPa) + F=d1+VM5OZF+1+RN\I1*RAc=PlM:#Z.k/m[Ka$C6hAZu. + 1ubBSede=UWhj?ZN,uJ7k7>gs1p`>hYUD%&Vt,#W[AZI+FAf<6c_>og-usY\`L/Z_Z]ZN2- + nG@rnoi?a84/#O<)9+c]N@a&q!t67.m,7'J1)rKViJJTgS,(8dQtQj._JqVEL)k(5so&]3; + )^B=VakJQ>H6H.B8P;Y-4Y"31@aK!eQ*B!c[G\>r+'Qcm"nCRFC9F8rAqSp"EtC)[ZtgWFN + -WY\60WlN3=!q9K]D\OUZZ"*%WolqS'G`qnSD'M*S1g.9lMV%n031 + sbm@7'][7\%pl%$VPE9?@VM]lF[6e133(+k\p:'SY + _8fo]e;FD^V>F*,-#cQOkjVHnT]KE.LXO-KH,t7XDfe#o\.=f8:#bp+lb:Ekp"W=*WX>'3S + -,l;.dof7.@0$MXhEspfVL!q)<3K:mH*ia/p@8RX*i0nb$c%mQLR)&$oSYIi4]UGUOc8Kg5 + ^+HB^Ni.Wi<&BlV&0OI^la$M(0f#S9@@1\;di`R]aEP$8T>pBTh#Rc5#GD^JZsY(,)]$A7J + a##.Kt(%P+qTNREbEj[+gf`-nG?glj?rGIFRkQ)O_rKN/V^7ZDVn?^3T*Cm6%3F$4FY3"8` + j;bj_[]>J3`[_i'coq-,1N03b/VW3E&$!$"#/GCd^5^EsJiLFL3HOs5-!/-&0Mk!!6/]rt' + ^od$B*)X19NA)4hc/_`@f\WMqa.4bH&LYeG;0t43[Vg=D%;S@0%G'bN(nZJi#2ca_G.Z>c, + !(RO4rNO8f^*2=_A!F!^-*'^o\f9>>m?;9UYd;D.-5`ZP2cD5\WLLlIA3qGr,C`ALF`Z&:$JAnTQK,28rBs%Ai/=KWu + \B?TWeD9aa7O#`K[NlcKb'o'2=X<P&Wbl\Js + 6"/JG-V(MbBLi$^,V7uk;IgL;/a]:& + i)Y.>`7JYfnXJDMn!5.ghY#uNM\QQ5fF-E16Ugo7?E]:;*UnFpl]Q!5fR2.Th75ECtPI@4c + nB5C!]9+]#2X/58A_KQ\)@?kfO^gIc=:F7ka\WLBH=J6)K`r$0a*13QFqbb6GjW6'Q>5Ye+ + QM8E/1W.t/-*.!Iq;g3^JU58l>]+ZS'njgm3`(-H"2/!*@iQe/-)U)H[0A3'DSqn;(i;po4 + TF=h:7eEJQ#'I!B>+/qimnsYDcN.$L[!*8rBrh^64)b!c[5=[`QY:hI7A!hA"bI8p\%^P+p + rL:8B1Q5F/.M!$"#0F],MD[mW]cf*(j/`Cc#ngfZ4gla:6#E=miACqg;4,@0&?MS%%0^`h + &C1lKpiglD9^1.k+)6:M[%;URk!adD$XM>ZuUHfY#Zpinm'u@0&?MHc2ec(M&VR30:X^aXW + QamRHpBbEM7pKOuN!o^;n6c7CI&2`lL^&q<@X$c%V4LR2.;f*WCo?4A1!PEciO-X860W*[g + I?EU`l\m2pr@Xdi_8T-2UYQ13JGA*tpjLHZ@e<9n@l*->F\UNB-Z6eZEc7Rh;oG>1i/-$LR + 41N4A.$h53e,XF4#Ju?EBrY7,7fEp=?]h.[F-UbphA/oV8 + '-/-$CipF`\;4P/WpofBl'?EBqNh`&0Dadjd_1tJh?>-tN)V%==V5[YVVIDfUc<(Sb91r(\ + LXJ@h^Hb/lBMVCcIMK\i?\Op92\F.^*o.;<8Fm5m^"h;XKPn^Wm=!Vq!HL8p#=2&65@0$MI + Tsl,PQorpI6Tkcq=%"eraoIbQ[Ml_`mjWqoi3't9c*Nd"2:>44# + k3E^.k*+34nPG5'[7(//;;d025S+plP@H=mC8ItW$fDUM<`50r27)Q0g,gCiWa8I.$c + $OJhR"_JF9nq&!8_FS%VOR'8`B>d=2")f#6"At2IG*H(/e67(@p=,aC=_qPg8P/J>^%^8J) + `c9DAlMW*4Ms&*hqb9GE0a?m0I_>`01n0:gT+!$A_bOs7t>AE5'$?RoD,P4AN9JO,%-!jHk + ?of3eoW)5US%tFAL7_0>G]ojR!$=mDP%&oCr9p0pc5Mq5^[X&sWa8.BU6hq.5=24=Bon@3rkU$7! + !eOt3n%P?[7I1kb_ON=Q[>'p^Nq;l<><&MXd1"d03S\j^k#uWB37j0358ZblDg"R"X1DMC& + AhdECK^b^$2*)!c[)m%Z>1Hj43:V-lW$f"?msA\Y\-nY(pI&Ej3:PeJ;j3bdKec\W_F%Ts1+pWjXg#h@QAo)s/-#q3gRe$ + ^3/_=SqKPLqpq2ca=@QY^!PbrT_G_-aJ-.1--!f/4)5TOL&KJ'@'Os;>>V2/5=/ECCSeDG= + ^k%\R-RV"G8n=ao<(ET<8B2>\jne(R6;&Ia7:?3VE,nV"&*Q,#,Lf4EJu1slb@QLiYhls.W + D`Qqjj=\T(4ZLb[S0A<$q=m&roc]\=g"R1r@FGRD).fh!$!t`r&cNo@JZ`06$6IZ(*rOJ!1 + 0!Y]b,b`>(nR%+-/F#d@u!Msg24J3k-#:'Q(E/K;/q=9,dMo9NSp2EE#S7j!=7)UP + fGr:Xpg*fjV:"UYXQ:7d0KGs_?-s + LpWufr_HrIXqP+Sj!JL\bCYurS(59FIrUiZ&hua;o^t/JeM_,9_=o.BD5/7C;(4`Ocq\NBL + p58Lm++8YM-nG@&#PKaV-u>aPd3!de%k&t/>6jjSYUJj$>!4EC"5%7CA.d_U6=9,c>]eeQ?Yh,_LroaH35[a]G3qTjT@B65[roaH35[a]G3rIRH(59FFh=X8[hua;o^t + .JRk#iD,7_7iF5/7C;(4`M3Drt.9[SFF!O$:>a'Gc5$,B#\T;:n,u1)k0EK`tj7DtdLp_U_ + @-rQtlFJAMAlQ+)+#7:@.2U]0HZHpD*@fK/m*Cl?.g8dG=B#P`hd!jM@cY#uNoD3f#:d0KG + s_?-t7m]MrnKl)rGrQtlFJAMAlFmRWr(57`=LApB=HpD*@fK-aaU9N\Nk8rOl$4FZ#nK$)B + /Souse":u;5:A&_>OT6\/_RuVb#nVQ#P`qg!jM@Sb>XTVltI?45PG/-:fm_+:2HPcCgi64[ + /Sh_r'=`lfK-Q[EjCBNO\tsr&+BgX"^pZWek*t!p5FsWT6l)Q-nG@&Vm]8m[]jCU\Gk7cr' + =`lfK-E#fe:6<.$\6;Et@aFK`tlIkfRH-)HACWp#dCS%gi][:>Q>Y_@FtcH[n=//-%32H-7 + cHHX7cCT6l)Q-nG?KH]K_a.-q*TUd+SRIo'5I%1@]M_[P + ImK*<[YQ1ck5A4!J9@@0smK*<[YQ1ck5Hre7YZIX(rnIVR6"'drftW$!7:@^CO8do:r'=`l + =?3U(Tg`Cn8oO[=#P`qg!jH]amfJEa?BYWdk8rOl$4FYl_;%O3$@?')l-DN.*u#K@Psb#a[ + jZ(#o[KL[J3PG5!%Wi]Ib:C!2l+(bg]RI?@0'Ijr73ug_U_7:rOE1.K>I\oEd1B?7:Bu.X8 + ^lVr'=`l=?5s76PbBXP:l]Q&+BgX"^iYF%@b5kPg9,H&+BgX"^iWppKWWn2Fp@:\Hhn^_?- + r^o^t3[Kl*%aqK!2:#DE=hjlgKd/IL5Xg%ob#n-=XN@--3MZRcII\oEdYq"j9n%UJ*6pITs+Nu*.MO'Z@sllEV`$5 + n-t'T@-,(gV^>X#dkXCBkQ/DkYQ1c;[s^lSKe8J8qEke_%>=snji"ULaVM=sn-2B43:+ + 2="qEke_%>=sniTLW,_Nr@VrLjJkL;F!_?>eV?Mo)'L_>6M0n-t'TOM'?n2MJgeKCjp?i#< + "2+F/;E0=*'C"bfBKTs+N[\*q[AMo&eb!W#.on-t'TOM#uHQWOsqP4[?JH@To^/:cgK5Hre + 7Ya;2Yrm1ap6X^!@Du]f*(59FGIJ&@:i#<"2+F+4c/[QpBV=`.WH@To^/:ce%fXnqnYh.q: + rLjJkL;F!_hSIu;/.,T-^%@($i0t5aqe,rBjClB)!0E#s#ApVPY=h[iTa1ATK>P(*f!Vr(" + FrmF4(E?0;ml8E+*rHU-nKlo2:\f\I=%h[@;D[Nrm1ap6X^!@,C9#d\8@Bt?DD83X8^HJHp + _MI + Y#-iK_DpkHWe\;urQobW\6bMVYQ>MCu,8,]+dpkHWe\55DbQZ;#"O\G_I#Pa%j!f1R%\[HncCld<(-6a?( + 6P&YqKa#n2,dQBDnNuP)&rA$$EBI>.BnsP,l)gXk[DP-?EL"_/7db7_)PXH$\oMSI6p"/e( + nRXXoL32>ZrCD?lHC3r4hYr#-\JsT(hnuH!bj"0]]&F?)Q5j/o^jYt4hYsN,lRPM/+> + VK/fTD.]#okQ`e+e;-4qU2:S][HY8YBQ`f\;!90Z + U5`1D7oQlQdTo?+:?M`[i#WN/!U_*5j!!,DpJX#*9*f2U1Uq?WU!lBQ+!o_0@#p>$P/.cF< + NU,4A!!jDSaHR++QU9Hn5Yt"j$0a\9kY4j22pp@M + r/r?1g]!OMWCs&2&jT'_YWsnH7fl4='Gf'Xb8uM6m#_;G7UZ6=b!"C\.aQ"]Ykit+FR*h^`*_?X%V?JP44+D4PQYg;p";#Sf(.d'K16!aMt8 + ^f0iV]'&U!a0P(NuUT/sA2qL9:tYA!f/CMVP$s/8K>XZ[;PfcpO5%2?6n@me01ZEd!l + ns8Xr2?1W3kPY+qe%YRi$"*`/kg+Wgkh+81fR + 2q[3C!;TUaM`NZ#XFUdA+gK%T.TmQ7Xr_I:ODNbU[%m7;BC01!4X + \)$X6W=B$[%Ps6pQJSpYRsd\V@^NpN4**L)^6H[>AM0OmSuO*tikF3fi(AiG%PgYBG-rL\% + dM!^u?TrqOk(V/Gp$ + IGr$mom[-R`#]3b*XZssc^\/6YQ$Z\b?l;NB25YUSS^P69r7I>K:UVr)t92D3H=f]8%?Yj0 + #.*No[&)K"4.[\_DX8g3,i9QO+&qI&,ou+O<1MTn@`ucE"PFkN%-s3YO02u"oj1:,C3N$E#GBk + d+;0C;Us/c_q>u1o5>eK'o5Fqf+@tT.%bgm8'8tFA)]0>c'LmMG:F8br=omb.K]2>i7B]g( + *03p6RJ/68VIpW9`Jfkf:Y&<(@SI[s`o[l0FH>/-_ohTaS`e"ABIdht]#7iFfs&\YHp.,0[ + &:hW`QEL/?^R_rJc%%EPpa*/i)*,"gTDn)2X]ssG1osc^U$MX0,Ja^T!$A(>fZTU_s7E5@E_Zi4DW.K0%jd + Z[uC-?0jcWsPK&""Qq*-=&?GC*@g3k#agXKXU1_8l:1+"kaPFHMBk[lJC?r(,3rA%J:G6#" + a^Fg5!/SPQ_FC_Z2K-n:a@?F<@Uij=3cnK*Ul12bB[#MVI+=VIh$^/3rZYD:5;6\LXSC0ni + W7&@nDKp.P%VjtcHe^>^8,:nH9g6DcQ7pXp.,0SKP,uj07WU!5F=9JA66/aFGg$[G(q_eN; + 7:k>VCc`2e%8o26fBni7K]c2,7aq`laSF?[?E]!:Pd8@?3g1%-5Sk!#"C:@2g0;s8(>gf;o + j_n1e]CA#:0N_O+->I_5JD8p`16WV_%^b`@27lhc:7!X"FraY:tVYB%KHFrZi/OkeA7NWZ&^%-9Y4PB!Sg+c3:cc(@XreS$*g-5/U"2Fc + q*#Eg"6m'D'm%!?eK3.dWENX#Fl6hkN]MRj0dPG5pi?[9*c9[q;l8@ + 8Ca=&i][g=mc5m8liH@_"Dr+b!?iTH1mbP5**2>l1?Ou+*f$s2cfY[u"h9_OQo>!HV/A/2rVo(Si2PVDgfF,>!)VTG@7SB3beJZbp?gMaY?]dY4`+He(]Sfk>Bse@"99 + H;$C`(5.L4`'ESL*)I,((oNUr@%@6Us%f=l_Rl_LY\A^#;]WW+RQOIkeGRh]`)2mha.!pd1 + t8,#&.]m=4;mjeM-np4%8ZUY(IVIU(('t_$js#KY!H + "E$>J7+4+$.H>\`4qdb4["HgN46*B6l?E.j`o#HMp.,/hU36l*OA3U]^pD$3MV + X*Vi/Ac*C%L5sIkc;94V@aHl(3Ur')QS,GI9pq;V7V^DOGQb3d&[Af&um9q$hZr!!Li-$JY + 83*$3iAJ,G-X6]I;9EeZUK:YK_2!4\pg$Tjf<>iE:hZ"9E\NV2PnbP^K_mIq1mh!K8n5GLA + UOj9OQB"g!f`pRAJQnPR![D1.=2Z^^bDAQ?%`WCYTBG8n6U#s_ZBYU5@i&1q#5^D4R(%uhi + GK_sps.o@8$38dV=9R"&@Wr".Skh!o2(r[YZN,Nm,+.*PJEgn9cAYW_oZso%>QE1lKgh)O_ + jF$8me#pb[XM-\$SpJZ8O]g2B+S.?D=ie=JO0Wj+P + T-^m^.UAaK^OP2*6;S^<)UQ(I"@g8?"5LFo^!_?`6(9-8RN9Or;4oK_q]g("q%g#:%'G`.X + c]&fSP!.V^S"5&.rAFe?OshkZa`B@IjQY+?Ctkca:'?Z)-nJ9B)IkNr;Q"$5q:3sJDaj^UL + ;G,\IF=Jm-E32oC&[Ygg@%N3WJ&,4^f6mMTH05 + /*N!Mfo,Mq%I-0dBDukBh]WrPeu%![oTthXeb20Zfp$HPgPk+=noldbNm)rOR6rE-(FHBYSBbKE?i25+13^`QP-HLB#VLk + 5koQ$4FKXq[-+Shin#U9'pgTJDd3,5b!h'Us=JeXS/,"5ep6!i3p$?nWT)DroMJ6hnK/CN5 + >X?>%SC3*OF"+J#3)Z"qXlN(AO!e-LoQoLFN(fp:D/,0Mts16T,f4O#48ZdXEDC#Ts + H^h!I2C$ohuaZ/LhjOmr&jWam5HnQJ=s,3>ap\? + C_ArcIS9Qd%3Pp)/Z+:4MA=pP5e>TXURN9K`r+]-*V=o6UJ//J",jT/SHC + G&`k>uE5`rTkIY^hETP1]I"daQogEr[[bOuTfFI9o;T^_mrnb + \U9K/8h_D"#8auZ?3UO:k-Y6"fb/TA7h2l5L=l>"7Q>F"l/[F7"89#@>MJiJ$n22!5(d0!>d'[jIPahPb=Eea^I.bE#jW9]^q(:h+T$UF"9'>VZ?dA;-:V4)b_G'/(;01P6\.a=,W/ + D+B;6?^f-^8I3QJHV*4E]6@k(GQQao-,9e$%o)X@T'Gg2l+.5MlAqlG_)]=WORmIus+Tq&+ + MuJq@@#)T#5+#jn3NEtg\6[bAMIc?*l/*m0ou@5dUek1/#@C9jRl_K*k!+>Po'XT-3?ocO9 + (-B6et3l]&j]q,J'9^*SE2cemoS-PN%^Bgf;0kh6ifSD@0(k*e!LUlOn7!38&U>PKXQ6%cH + 4u_l-;Fh5X81K^c.=A7*_O[h%D1'*?Wdk\L7Qi-OL,!$4HXk>K1bAM4>C^H?`>MD,VW'Y=R + P?l(C7C!!n8^r3aTkE`UDBqe9kn+4QmIiN[a(ZLfLQONo7*i2:4Hl"o@NN"!&[a.jVBFooi + ;!.>OsnC^^'aSFnc#94NQ[R(jY5mGk3=8d[PU_O4Epag_/gVq)o*BRfErQf!/MX1TB7#eo/ + "6u_Hro=+.!JL^T\&'D2rp3R3h(@uapI&"RT-fk*!1ophJH*?Sd&4.^H=Z-N1sVl;k'q>G' + G'g:]L$$-'o>)DrIlm6oWk^i@sJ79#mq'=piBrN9\_F5E6"0Wn[u*8nN\239b\Ai1'9p9qEk'fFFQQ$]6U\0ro$BTk1L:#+40B@a=pO&sZnS_BB2gm$+\6OX5 + O$4H(oLe6\*lj'9n[S0X2leQfY%rbki(=;`FKU+$h?JWB,ES^;n-"XWn:._[mB4G%M!SiCG + !pXf"gln.o;,X]o[<5tY;r-0.712b3:XcK34n(]Yi3t*cT^GH"On8MBj4aof>=8)Kb9uGEI + V#g2=RX9#$@otpJM>$\4[XsGDJn>#Tu]\l+rfes=&p5i)sPNR^hMoT6RT2_VWkl\Gk + 3M7AXqI%-B]MAMTM3'hA]H('o`L-LU-R3V3+?[>@K\PB]J3^(WqXlK]]-NpN,VQ,6AiV]JMP*\4mU]1FK;X\]dKg@G#-Vg6+ + eSFm=@I/dT(d)!_K)i5bP27EDM?"R?a%4Io5"H#IPA.4;?H0'pkb('Ao`W=AW*B:qgX;VMT + YKrpq#!'_VWkQ45\dbKh9#)o1PM=/<&!j9-)]@=b'hA`B<3X8@6If\6J;]V8A[ktb%s&JPu + dRLLV3aj/2AHI\Kl']2DZh:g#g"[Q6s;'=oP#DI,iYX$S577+a;roGfrpfZ8(K()$t)X4+` + 4Ft!j/.b'V9\f78cOWr'm/&tK?D2C8e0X@_5kNguW9o."A\#/&>$f$dmLXIS('q" + `-LU]h(ItF%1KdFp[iXbnr19i:l(pU2JTajl+L"%EigsSr\u6Fo>2/V*U)d2!$@?)#e/tAL + 4")Wk=PkfhS>)_mlE,.RZLLQd,8Ptr/S"EC.*rG2$O#-J'DMFa(Too_?0u+0B!pH+CqXr=T + ;'.":fnj](7MbraX>"#;=*-*^-?sEqWL^Pi-m(*&7g9(ih!,JH`6\d\Ve$>Mo(e'&H+$bg^ + f)BK'X@b1/.Qm,K!J]#5qc2EE/>Y[4#jBN/L5]rZDC4M8B;p2o*?p>IVmV:S^?PokWs/-X. + s(oEQga:D=QM\@EsSH1op2lA9WNbjO^2KtS[Yo`lpb:D=QM\/E&S+3"CD3C&LJV(q'W.8-k?#EeX56 + A0XqiUr-u>$)efUg*LJV(q'UG5ukI]UlYMMe4lE@CsqC$MRUNFkTW5+rP/.`/\1i^j^4uPs + FO01J@b%-FI.]i)Db=[@TaKRE@=bXRi%]ETp + l@5]qrips=/T49[Sltc%oqOG$4DO+H5$$hg6;rR'1Z%Bc*8[5R1.UfWk`Hc('oi=:#,?NcO + Eg6UFW@j/[[!CJW,LXLAkE]g/(lo/A>`UT#HUdiDJr=bmj"02qr]o>:X"Pk:4i5_?+09]J1 + ;$rA/7l(\Kc!V.B?m6qK+4Rs,X1n[)2)i46'Nj;h).uS=1bIc.U*JtOqDQ*PJ#c$%gROS"nP'Froa + =nf,pEjkL6%EU0^VXVU=UaW!uq>c5 + mDV$hF(i + JMIYkebu@,7'm"lA%j6t(WX944mN^3tphZfn-`Z\pDi>G7!mj[jX*QY+>hP0mV!Q,3U]1Z- + ,6gU>@*c_?>dlIO\6\*.slqCkD>Q,uiEnk*>An6'2ZSmVD)Q?j>&%_B!T6[9;5 + kt86M0khAEeJ`P?$GFjknYN3ZLh(fm0EcsQV1.W+;I"Qih"JGI[GEm?$H+_I#AS\;dKAiRX1bmWCEq=sQMWKgTC[.s$eQc9aI=> + m1l/HKtM^f)M>^GktMPE2p&'cca41k-eR3f%4/)!"[E;Lmf$\Zs[9\f5r:HYQs7+Xkr2rnn + ^)36_P]D@VE(0?ep>WodIiYC4r[/S)@_Fs?'.a'EmT,RbCnc+udBB.!_l9Y&/ffQMk`oBK* + TK_P4V-Ja7bj2h`jIFo3/eBC + 3/Dr$4EYfB2%*eFD5+T)LA+Fl:SZKfd,89diL\o^0IC?lbrYcX.6k:.4c#[h_B,sr;"%VCZ + pVr`nU6L,iuW0<$1o);knFom?5<34FBt`MgElK@FNtHkV + X$Z]:AF<4n^\`[d@l!b%:,g-8N6p/"PY8$3R[ZC$qH1.H]Y_`&;ccs^8,]K&.'Z@TjokpY; + nIjRqWY+@t@e5riZS!a`6;18#Q#M5fM_n6b&T@?uT`hc`fKa#=?_mFoj:[b9S6LPWUQsqd. + YK6n3_]N3cQVPhm9@R9OiSMbC_?-G%>nctVSbmq`#JG3Xn6CmnGts*mc[U2>75Bd4Pf!m]b + (9f"^0E*<&1Z"p\7`X053a0\:0$OfRpZ-9fN+SSQdX1$@#'"Zh`!V0ETQ4^$4DO9djFKc[] + H*I#JJT.F049r]IBE%.fbW08md@!/_W4]\\ZC.#mq&nh`"9bZY%G^oaZ;JjL&\[^AsK^l61 + ZoC8IuKjV%An-Ic*W>V8%moN.(Tg + eo/ojUr7C#erS^ER0uc1fD#;?FtT*^;WK'.jNQU!*?\9>b^B?EfXS+'G^t>a%b@\e5N-m$g + >"Pb:B[VlM+u5;Qg0BC6?^Q)3YhS2l9/.a;'0QGG5^,J?kJ6n/@Cadpsd:098ORD + CbJ4%VK99d.HZSga[$^JR1TsE7f4[8r92"^qH+?Cc"7dEJ\W#c4(;(6o5k1,s_&el)e^SN: + FH1Aj6lM:%fQ%DLWVJEGq;Ju]>eO^S?b9_%4Hdo6#.>7pl--*qeYJ0;>#>FugF4+sG2G+5I + ;SYW*A/@75/oen+jUr7C#f!>cmQ#;uM0f1!&D:0qI?MR.]e2Iu@tA/Z,bn=n3R$gb6Z`3SP + u:^,<5t<8Vl\+O?\0^Wh`ok)b[-A9eU)jX?'>0so6P$CF>&tR:fnk<*].k4?(F3(ETk#8ZH + -M'SsD+r]g,J589]h2^D:^Ve8Sp*mk7NF$\V,*e,JUc2+p)[^5:-/]6W-RS^a]k">r`j6>+ + If:-nGoNh_B,sbaCW + EX66Ag7inrQ\`GS7]cBY^"W?ATY@[1Hnn#?7K`sg2cV3-#Eb0anlM9[jfb04B)gd`RL3c&1 + ^8o]:@9uaE1:cZ3>.rN8Ka#?R%o(iC%M..$;fmJ[ZFU;'VK>Zg#hj8s3Yscf];]2]`4F*VW + io2*$\ZQH'DJk1QHCoSNo3)Vb%.Hg]Uh2%K\`=9('osM-LTQnB7]bUn_9$2bA;W].-"MW;`c;8#Ns + 8V8m\"D80H"]dT(;Go=m0s3F5sk0br0XQ7_5SN"_/*5,BN%gqaS-liY+XHUsiU;OT^%\c-X-(7#iU\?/pT.P`?=/H#XnM;mX+^+5">1;HP%MSi8" + >[!t,[b\Uqu_RJlS6mu"*6"_oQb^Y4Z)E&Y*VoK`CVS_/=3kkCnCJr/f + k?-KY]V]pMA_.r.3n=i`BugA9V%"X%+P>UuHEOH_R`0)"]Y)cae+2\+BW;(QLUg0s8]2LjF + 0ka*$Y20X%B]T%mG[7fC;l!"'):hUl-lP_p+Qf+lLLbe'>$NL73k5`4mAVG3]UP";1!# + 2qC=-P'G^tea%b?qFD5+L@1ijjp$5@Uh4@dt?.A2(nld<>f^X!=.g;!4^W[<[@=c3uC>thg + Isn]t0FB5G+W49S29PnV8\cGGu!L% + o@#!rZ0!e?Ai0]g>!7ljV%;U@=_h`XtcJ@giMc11@8K_'K$WK/Z*`CN,OhPoaI[s>3Y;kXPkl:Qf*7I;>_!%)4(deROhi,*HqHL-*Up`R,[u(R5^B$O#3`'DJ`UksMV + DHGj9OT90`N4\@jP1=0H996O*h1^AN@`'$t"+P7=Z2=^Cp$a_9QYgcr]Bf4,T2bUT*P#=lZ + j5go-gF%\c\\pX.=#@7gt\P$&tR*F?NfaUcd>M0ku$5_.*6ER/"i`E;L^&k + #es0=2S\75A&[9r[*A%!J#iZJ8TbTt`sd_?'bhbJZOf+hc.K>P2!sliqg3FAU:^fMtNcLNt + K*3@/Lb-16W,!^ZKj'DH$JQqNcHCQFU..XZN)`GD`@?L!4O2-6):gq8pk%uR*c5YsUYigq# + DH>:cfi&Z@,Y/PWM0+Y'Q._M2qg\GlHCM+E^^Na!M@=b'iA`B:oT&E&hqhbkuMt(+%A(ua6"lfs.&Pble,Rt0,>)Erss8UCr-~> Q Q Q showpage diff --git a/testfiles/cli_tests/testcases/export-area_expected.png b/testfiles/cli_tests/testcases/export-area_expected.png index c9ded26144f53abfe47042bf9069d16986eef90e..e5029aa6255754184b7c181453ffdcebf6175bc4 100644 GIT binary patch delta 5821 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 delta 5735 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 diff --git a/testfiles/cli_tests/testcases/export-dpi_expected.eps b/testfiles/cli_tests/testcases/export-dpi_expected.eps index 0b2157441d..51587dce23 100644 --- a/testfiles/cli_tests/testcases/export-dpi_expected.eps +++ b/testfiles/cli_tests/testcases/export-dpi_expected.eps @@ -1,6 +1,6 @@ %!PS-Adobe-3.0 EPSF-3.0 -%%Creator: cairo 1.16.0 (https://cairographics.org) -%%CreationDate: Tue Mar 28 19:36:46 2023 +%%Creator: cairo 1.18.0 (https://cairographics.org) +%%CreationDate: Tue May 7 07:52:54 2024 %%Pages: 1 %%DocumentData: Clean7Bit %%LanguageLevel: 3 @@ -71,7 +71,7 @@ %%EndPageSetup q 0 1 173 90 rectclip 1 0 0 -1 0 91 cm q -0.9 0.950196 0.9 rg +0.898039 0.94922 0.898039 rg 90 45 m 90 69.852 69.852 90 45 90 c 20.148 90 0 69.852 0 45 c 0 20.148 20.148 0 45 0 c 69.852 0 90 20.148 90 45 c h 90 45 m f @@ -98,182 +98,183 @@ q /ImageMatrix [ 384 0 0 -375 0 375 ] >> cairo_image - Gb"0WGB=Pn\c6Z%%44*ud^u(.KMYC`,R3VR5&gnFp9BgFm_na4)sko\Vu^)(,`76nLgk[36 - P9Z$5sk2j#t_(uHIo;7Valj8d58Y[Ur..*%#:"!A&,^uC(f:Bbi",b%L`6$jJ)Ci,eA*1$"+N#!#6)C[pk'/9 - n-As.+,m.L5O\pW?pkJ%JNrW/,AQ-Ir..'L;$2=+&,a8")YaDai"&<,^`W=RIk[T$rkKWNK - +U!1#QDL\N2V:ln-Ar#6k0'F5O\n&C>]X#JNjuK"+T5>r5"1-p]H$(%n>'U4DH$&RNFCCUJ - %AZ^8AogM?k[i'UuSi@73U.K$8?#o!iE*$?QHqo!S9>L6B]jb)4m[rHF^qM'-E9hue - JqHlgBaNQmIF;jpcTF$ir5(&.P[(I))PMnUD&CZP%VDpgH?1"I;M.^SQ0ga@]@*DJT+pW - d(H6ur1nmLQrN89Fe,r4i8[:3q#8AWb/"$2S15cn&56Zf["d>o:/7g&-s-KIZ5[8l0N]'#3 - OMl=n],;U-T1?-L#Rj]:QQ)Bu^Y>p9$md*r&FCi6>sTBrTEUs^Mkr7Q;GCl%/d^A%;l4*t# - "?V,ce=rF_gH/ruIA9RHYB!d@t'XihNK$X\e0!01jFEb@V`lU*Soh='WVE_1s[$otmQ9VM; - +8B?("+T5>r..),#6)C[p]H"6%K(cAn-ArK)YaDai",_t2=LkM^`W=rC>]X#JNrWoeA*1$" - +T6iW.@2&#QDL\;$2=+&,^uCU'CV4+8>qf6k0'F5O\pWL`6$jJ)Ck8&N0%_rkKWN,AQ-Ir. - .*%8(>r"1flpd> - )Ic%%Vf3LTX!WA8891c*#qB*OhhfX8cI:pDpYVr!QCbh#e54l!#M5S[q")B4l-](# - Kc(o,Y-G.@[TZn5j]#`i8M*TK;/kPrd4JCQ[g4(j)h%gs!i1#r^ur9>/R:Q$pmfg=aCS#i0 - )>:d`ugr9Flq^[Jj,SA4EkZKX4]eF=%'1CFEs'kS22P5=N;Z.7j9`6__]/tAQ38TW\q?;\X - Wp[;klp=bq#8%6sl1e1%MDMRN&X,>$Zc@PG-BC^[utWH<4*S=O*;/(4$2H*d_65K>4%5?:qb - +n2PuX,.roqF:G>P.SPE_*ej4aGe1Tk?%TGpn.P/l>P*4Bl:$@fO8+(L3jGtT%@pBK)(!uo - jQEUe[t^EmehC@p?:bpPFJRbJ+rpqGePla@kafH*ePg7:7/J:g=4qW3PE#ieb,9[X5[,`/o - Qm-k0'5W'0T:;;[F"1F9Gc>_4UF[>jJ"sP77HG/%la^(39--=\e-8c`K=Eo$8]_9M6L?j*\ - n0+X=WiE2"TL;tlJea[(%<,r..),#6)C[p]H"6%K(cA - n-ArK)YaDai",_t2=LkM^`W=rC>]X#JNrWoeA*1$"+T6iW.@2&#QDL\;$2=+&,^uCU'CV4+ - 8>qf6k0'F5O\pWL`6$jJ)Ck8&N0%_rkKWN,AQ-Ir..*%8(>>=%07fbK7Nsj7GR3GKS]c^[]4T=T4973Kf - Dq4Fa:1mK7"iB9N:jf)fe10)h(<159 - sO^L,+DOsBZE@1>e-X&efO'M[>@t1-t&@4)T@tsCg3m;@V-TZj)"!'=YBKl%dk:fP\ba0XQT,eZ==;3N`]B8LF;$M"^0N)!q^(tm.l0a-r - ]Z';]8s;'dE"OCbG1BW``d!H.VWuCHKr&&I=Y=e-B?B&h];XN#=gu9o\n1.l)LX#FC0/tAm - Lt>53qk-Z]BPAjIU-C7gNaZ7@#qVU - oue/.8tjDi>mk#U&6df!I;709*>mn^4&\?VlmGn>9!Bs+?M.O$\.I+ER@5/X[rnMi",a:5=5;4cmKIb&,^uCn4-f7b)Bm7 - $iGQ?n-AsjnGZ6Aoel4DrkKWN,K(\(4Wao\5O\pW^`Q5Ee"/4(^`W=RJ)HA6r1QXcp]H$(& - ,[%!:RRoXm=-eeJNrW/r`I76Vg^96rkKWN"+Mlb:Plf]NVDGk&,^uCnAfSK'Y!m">2^"iJN - rYEUAhOi?G)6ommLbW7V,)0MUfX+`&PDXi9cqfB6&=unG>Rs%#$q>k0G](r*kmLf:N@u'Bg - ]`NcjLN,IZRZ#2^B6o'F+iBE"_C[dHq6?1jEZ0f0`+NCH[?-:2FS`tNo]U\6(Q3m+;bO#R%4Q%2r5"AG5h96q9M-[p=46b4M7:-HkdGf - oPl"kVr^2:NS42)]OM$=s!3Brq6IrmWgTOg.:cVgMd!E*Db4TNUkRk&X^HG0.,,Bg&osi,= - lUkL)/H1>JOV?W3p%d5m-jiq/c4F*-T>8$GSaP:$-#PT29LhE1YlRFF#?+.VD62#6@cJua! - U1@`b]oYUP]07fSW+6-.n%/ph2jNiG)"e8cpTr/qY55j"0Sp,JZ)*&`LKO7ZVrQ3uYdrRb- - oHRPQO?]8Tq2Th%aSS8;61F(7<>[7E&,]:>nAi]MU$,VlrkKUE#(N`3TC4Fai",`_;Xha'r - V?F*#l_U],mAd7_Op;R#_PA27nLN=S+)2JBPN&^n4-p'+'\95]!`OV^`S76Ir*)rU7.gRjs - Kj*J!:ZFYn_be)H.+CJNla6rn*;s`APX,#R5e:O(*]*2WPB[CbW^JpnO57O)=\kAgi9rn4, - 4L+5?@aW2<*eVh75'7iB,bS+EOG?NP`8O"u;O2WmkRFmd/)L[YQJ-m8:pF8k6"jWp*:_&rH - IJDcJ?qK'@.3rFOn$4-X`U-FK[M7^_cW'=ATY&\t!4o@Us - &H&e1J#JU;'UCst+gZf50+;(+Bkd@jl$K;Ni/\iTZ,^DHa)BtuJB;&G%\*DWQ$S>:Z/fDPf - mi2Tr`&&+BG.K/?+F*[1SZ]m=rOMt?L!$3ro`(p\XXFi/t[>/OY5,pf:qR5h,"+TYg=o - @OHdVZYcGZk/Cn=(Q20g9)Z*hGq2Qt%Sc3$#S[kXti>]#Hd7^i6;.eMK*?aQnmVVod?KE2" - R&QR*_F3c[(A.!706h"f1c&rOqnb9ZA/6=Qg[^JBbD:V"Ap?, - +[hEFU3!%fa))m^20,553)2lg5kfhO!mke?)>leKU=6Xt)fB%(J-PT_c=h-aE;bfsV-RLcabN1=N]LUAdPnX']_P>S@M%A-8eW]p)"jCW&&r'>KCr36HU3qP$er'?2Hr`Fe;a-)M"+(U/C - +)CA$MTCU(5KM]55OJ(j[63!6rj;\q5k#"--MZ%bJ+haTCJfRV5<'+%OM(gIg?HquK)T=YI - uYluLHil:Jj8Ypk9jJ`@SqX/s!])cK+o!sBr=2A3'J"H)f8H%_U??[&$i7o:](*1N-/\-P? - aE$39fprr+Y=1_.%HC%oPr=q@g^nqelESqGVDt>^HnR)ZG$#k=a,RjYT24I_a3Y&q*?C2NO%Jp>;R - qWh6k0C3ci?d`^)Qo+O('D-"WV*h-AcS)6rTV\XM=E,!8=6(W^`lkmV(V&_5^;"Yr4Z#;'( - 'qIJ54$VWIV&QiGMDS)-'rb2UF2"p - 4nqdo^qdO]WZ\Zh!f$i?+4u/5.c'4kJ3^-s!aihgP9lpm1+W-^fp\F7fN%$lhA\UGMYOQ2Q - ,1$^:kb:"$NYQg5CYcf2iIO=p$,TQKBsu\dij31G`T)-59>Y&n-@WBn>AYQFEZqs^$rVpr,GAKK0RG - +Jj1:HLY?rrTB6!EG$a&4Iup3(Ilcl@g.Q35lAsOS0DL1d,l#hcJ,X]jNu%D$qpk*'#MD]< - Vka6Mo,hUBIlnB-i=E5Xi(llAl/pDGV(BT+r]Kj*0_g87'E%*n5?)0*UuhMO-`Gu7ZBO4-" - Fmjeh/56&<:MI5KBslY,Jg"KiCbcccVr_QLHe5>B8>9rj;V9+S^R''E"!N%i(ZJDE3 - 4#rWj)56&H!J8FpKGm9Q%LVsTNF_&oJ?i/]^E2F_VlS3Llj((U+l'GLD,k0MhE\X)ibm>iC - ,+/G:^O#2@cE;U64o>``F(&f>&#l[BdkKWsD>0`bK56=RY/.#YbIuj_`5LTIln.EV-p>=OC - \,!"K?1T7l*h.HTST&lJhgn!_CbkkYc@L2rqYDd=ea=R+=P`]5M9\h - l)e_RELRc$-@:R+GD*$qT"'_Tb?o//Efklr'>Kfsu$iEa<#ptdejE]'``aA%q5`mV"q@g`_O_QitUYZIulerJ#)m-%_s1]K:'IsC&Y%EeaNhC+ - D1+5RQmC#T(!!a+D1+5RQpTC&,\`ki/]^Er,;C8d%=G8KBsfW,L)joGPC+qGPHfX_&mCj*5 - $dJErP"gccAk55LTHirnod6JNnr.r/p+P^Ae[m&3oq7c(FLLE6J'0huA>q,krV/TD6'Dp]H - !W1\`32T?b`Qn!2b9TKi5MccA;%58*nYWTr.[HogO'p:ik9P$J"=ec$@P[L\MY&,-#ikWb5 - MrM/HP^]*s\L'Yl?f_6-Nm3a0,,cs6/**QLi-ShXsJ8[/.-( - X:4D:pk+5)J#)lr-Y/8!r)(G6i"*,Ki/]^EejlpQS3LltDX[na6h!U75CPQi/E`'T5n/=*4 - Tp81q%K2$3#T@%'uHgDXhRlp^mEI,mAcZl;(LEY">H]QLi-Ad0'@j3skn.e=Y, - PUF"jIIXg)Nn.5F_8dbQ>e=Ve^GPIZr*V-,q&-'EeUnlpE8,ZR*[E>6CG[9om5KHTO599DE - n4oWFrFt:NO22p6Yi&<4r1TH-YoqG7rj;Wn2rG3E=R`4UrX@U;<#RsfoV_(p^;D%gYi&<4r - ;"fu5?ZL.X]N!K+(RUPT+:\'-%:b@rRlXB48_D<'E%*V5NafJ1&]c>[n6-L:KoXD^E\Lu6a - dNimb/]\+G%YucT;Bg@JXKaqsQL8r3\chj%oK[l^t]3Wd*'(F%^KD]BT(>!cJ;rNEA6_EtA - Ah$0rZY]N0Nn\THk"rq':dT5eNK/V,mpZ2Y8g(l)@5DL,kTVppT'9<3TUgjuS&Pqdlc6YenPkV-H^>h4JoURtgI:-^.cMS,"4#qg6,UGMMHE2AdEU,uA - 9!3qbNtL4nQChC\=i=sC(5=ErI=$1;Tep,CcRDYVVm"^qma"t^?SVLN/k\BuFSJDTOiJ6%) - FVnq:G!nP`q?Y'B^a=^Hm`m%j>Ue7q]PZd"@f:2iPVapWoqHo,JQA8jg)s=Sm5`3l-E^4rmdbB*)AcT*dllV;H!"^Sc8EAoXCb`V]VF`5KM]55OJ(jW?fT[re.)&^`R* - *_4PoIPH2s9W'B5O_r2-3\)<<)piA:VBKPa85=eA^+)CB/e:U]h#MEPT`u=kirssV;%oQ

@2a:;W.re.,'_&m3[_&or]*)/I4RF/1@I - DI#$]\niQIld5I48_AO$iYtTVaKpc_]P\Yi/^ui3qU]Zr':YtrWr/Z7.Q1piWF[lP(Gn"r< - Z4,L-NbNK0KG9OmqiDIuohF5GJ(mD$Qiq-s'VgIn#-%?9pKq.^VnumNM - FbS%sC5R&3EtU;9=7WNL.(k[>R3=/A`(&GP"c*iC#%Y=6Ha43 - +*h.qhitVjRK,t-S[9:8hNi`WlgJ?ko-!33[]0tU9fb;OnR8u9^h6"PBloekLT*IQfjul5B`>Drqp - HYO!a]tS/a5.P5cfLNUn1b_3#6pdR2+_jAO+NuJ#MA$Ypj5EURoX$?%pI\/5"=or\)SoXa4_6pk/3[R%t"L;WhmM+Q@ - N">hP8;1I"DdbkZ6Z1[%9/c'N?ru)+apk,Xnpg]6]gb62KJ)H@hJj6^-:O7:Ir..'># - l`JH5K,M!R?a30#Q?+opg\CEBPY$F#Q?+rpg[P-PT!0YY6KL7O"u;O[cJ%uLVY)-UR9u#%J - `qu.r'@Z!e9-h@Y-g&jH$H=pZU!Lp]H!['E#*YIi=dpi=Gi`;Xh_-r2H]uC2%C55O_07_&l - nOVnTDWrkKV0"b1@*T*f'sbI7S'i/a%Q5;qI2rH7$/r)$eia85``1R,ip_=XXDli&u+nAi8+_&m=]r]:ek-BKAar?b-SXRFg9O1ZR)((>\a[.nA.L]/FtP$OX'W - M$+2ls\QEd.p>[0&a/gEnTc*?LP#]h-]!1dfB+CF;Bu[SXhet'7NZLp=Iik/)>DO\TIf?VB - 26T,Z?;>VQVsLGMEaq,3':RVG - rN/_BG^l<\+8Jji=Nde[WEu@MSBF$=+o;>=3),o1[):W'hO]";-7=-3*kGip&(_l9<5"[8T - -(ijHLTnj6iCK701L5J)X0C_El8bWHo]2-]#;-=a#Qe1A+W7*Rsmd`7JXNB:7>olV6Q3IBO - QreJ$^QiP7Adt5L&l3g>'c.aJc0g:9h@7oKVcGYM]3TU#0p;n*k[G.tg.\k0MEA6V$NSq*R - dto#DonJIHOUFctP"R#Sg=Hja]sBaM_7DX3Rq[d?kITqo5q)?.Dnp]H"pG^oAYqDJfbJ)Ck - 8JNjq@Vp&9:kaT$[^$l:+^`W>]+b/bqle4N.r..*%#Q@iB+8RX?l9X>=r..*%`tJA.[QOL% - 8+6c_^`W=r]0G#\-beaRJ)Ck8JNoIlVpnO8YeV@U"+T7T&+@hCJ*[Sni",a:5F1f:4[4+E5 - O\pW^`V8.0n7pQd1\b3%"I1Gr..(uo>fKkm*J&V&,^uCn4+7D.V^IpH4]iRJ)Ck8Oi8CK^L - @KJ^`W=RJ,'#MrB4*S"+T5>r3;>J&tZbo&,^uCn;$I.M8#dbV$[.:+8>qfIX$?5!6GIs5O\ - pW^`Nm3MGJA#2uVh>!!!*\l5ilkl?LOrM)UoOS!DF:rK*]QmH2-HQj,!Oi.*qs - P(^U.F!c.$:(W3*#"X5p'6Bl=dD4IL4F-oHfZBNjU9O^J%VfH:-6i\/O>S>j()gWgb62[ - Q6$h30S35&B%6+EeUKaXf8i^W/A5!EkgH+]3Sdd`2fe*&iSMKLHh?dihN&Nd6ruOdSuSC[T - 8ZVLg^!aflVCjZI$)kc*\U1sR/Hg;f3d;0RcL@7R8,c\3k/O:-U8rT8l0B>[QV;J4gQUoUG - >2W%kAaal11:3Rtr2!D/>f?N02;UlFL^SLm,P=J$7oSX^7Cl37F)LWq39)p%"/Bs569PRJl - Yfq.&7n=Gd%(&kF'n4C=K:5O#R=s3D6O)ko9iYGfPSagib.DS*>/XWf?lb%umpdOWd(=q1C - DcbLXt"+T6iW.@2&#QDL\;$2=+&,^uCU'CV4+8>qf6k0'F5O\pWL`6$jJ)Ck8&N0%_rkKWN - ,AQ-Ir..*%8(>r - "1gU#QDL\p`jP3&,^uCn42!D+8>qfi/agf5O\pW^`QDUJ)Ck8JNok5rkKWN"+N^u/H4-Xe] - [&M86X*foWt(5AXltMIc[2_s7u+2s3?H,)Ws%sR+F"n7!`1Wkm4D;0XIeXG6kJQlM`Gc]7C - -`_&e4VqG/A"VX9ekBklSU%5AgW'&6aP>pZs+I`pKIrs54GI;,M2c^bMI_=?a!6T9d2h - H1EnhbaL\VDi]>NJP;AhMRM@YpEkpb`5,2KYH!,/tI)jSG#'g[skV8/4*FXK=JDP1Wq=San - e(:]T1HGfhn^=k4Qjq_,jHmK-q<%Z8hq7s`jF4H(2j@7BP@pVfq\==sdIPEF\ek(tZ+2$3> - 1f<$dR!.:la1ent - k;b)ZdO4g&]3OHl_*4OTIeqK;dN7628UQLt,i>LUuke_8o1S%IY/tWpNF?a - >+FGRXZm;(cc7l<.@l4(`;,H'7Mg0AK48m+c$Gq--6.49qEKD,jrg372eRYtr^?!+Ka8p[N - kXT%eGQZGOVc4/nN13Z%JJgFhuQn8YN[/SL"cRYUT$1=>=in]48K&\\-0tB$/U3!&cN`&5Y - M7uB(L6=<%=]7slo:opoP7uM!5OcbR&)DR(-9VsZa]NK*Y0'Kl9/c75bNi9YZ2Jn@'1%dCE - ::U!4*q9qPu0%l&RVo$RN45S(*:]^LWj*F[[>abd"Ph4r6"&WLH7`G8JSEiPBqr`\Wh/l)6 - I5dOCEVm-PDB(/kgXr"VZ;Qg=O+^p]H$(O/RUon-As.+&N,ii",a:5GAA]^`W=RImabDJNr - W/reMKf"+T5>r"1gU#QDL\p`jP3&,^uCn42!D+8>qfi/agf5O\pW^`QDUJ)Ck8JNok5rkKW - N"+N]Jr..*%#QBMtp]H$(&,["sn-As.+8@'qi",a:5O_1m^`W=RJ)HBdJNrW/rkKUQ"+T5> - r..),#6)C[p]H"6NOASG<15(b5KUt>h:]:;c@3kI6$mM<+[&joX]TRLH:6-NaQ9a[Ae@>1J - nNC;RC=Sd&[MWa$eATgkC1C_JH[FQbH>L+\GfaW^.=[j0sLR)W[WNa2EGmS_o.>1D5b^Yc6 - 5=lPWdrh>+U$qjlD!)#<84A*4g?M'O#]ph3n^e33Bm(Q3a4]bC\(EbSHgVr)rf-ba+i+\hS - Nk[]A.FYBY).:N2Y3ruERbRqLT-a&!tjf(BOq+8B?X_fq&oDIKj;,$.cG?LF.sn8^&fuH - 5BI[KhLh3C=^=s5;e':51[0c+`C9VI:3;.UI3Ca_jQZ6FC%LPgB9oaG0@?+celFPG-Jj^k7 - _2aP2ficj6C0p2:L9")&)G<;=[hZBoqVgK]"\n!r!I0GEj`IUb_U!)psIOt/a\pj)9N51d_ - jRF@R`9spk,i+JNrW/ra8,'r..*%#:"!A&,^uC(f:Bbi",b%L`6$jJ)Ci,eA*1$"+N#!#6) - C[pk'/9n-As.+,m.L5O\pW?pkJ%JNrW/,AQ-Ir..'L;$2=+&,a8")YaDai"&<,^`W=RIk[T - $rkKWNK+U!1#QDL\N2V:ln-Ar#6k0'F5O\n&C>]X#JNjuK"+T5>r5"1-Abr12,(DH")B&M4 - iracbehbn;:JJNJruL\P3X0IiO@H/,Q`&$MfB"e5JkGY]k0;:u&OFE+,M#(;2ua=Q<)sJ~> + Gb"0WGB=Pn\c6Z%%44*ud^u(.KMYC`,R3VR5&gnKp2Q7Z`&D[F0HFt^*I:WdZt^9],XaScL + oN'NJJs^89a14HlgALhd]2?KYb?*knl:OVYAefG'qBe?I+8CICT2DI + M[:\J1-)3\,s'V'Jn-As.+,m.L5O\pW?pkJ%JNrW/,AQ-Ir..'L;$2=+&,a8")YaDai"&<, + ^`W=RIk[T$rkKWNK+U!1#QDL\N2V:ln-Ar#6k0'F5O\n&C>]X#JNjuK"+T5>r5"1-p]H$(% + n>'a+8>qf0VSdN^`W?(&N0%_rkKV7W.@2&#QA+!%K(cAn-:1Qi",a:58[8Hp8EuCBRSCQN% + U+D`nFSu.jCok?Y\AJLCkPX#Vsi%/`9!l-$(GWiPEc=>a%qpYT\!,_DE*Je. + danp)M$1q,@^:MF-0X\CY^2;IMuY,\e11.Rn!UZ&&pa]<&Pb2>c_.9RF/Zp#a + eLjE[X/^;:XDef_.3eHQG!$c(MB[B'/Wn02pRFO7HkGiTebEGlI;`HqAG^)..lo4[RnO`A8 + h?0CFpPs.$Z=.%W](MEAF'9TuH==V+7=gAa/Ic%_H-#0mQ3T)#'")<6NLd6(>WFfOF7rE(O + 3N`FcAQ;#YKefMQ-iO(j@X/[kQl;%i^_11d9!,dNElJH$p(7MFcCOse_,djM*qo]dn-Hugc + IiQ8FG./@u'In,!,]@rm-(#OrbWPA_>.$rB?e82ij7(]EJMqTh9X0i`2Ll`9k + 'rH@b^Eb`.kBQ(0@(SiX(2&BPZ&MohTgQ6uB:EFW&KjOE69Ppa]5cdDVZ6R;ENK5V7W41ln + f%0==[9uNK#@B0Zqq>TuRd3<=>cr+J6!uGm7gure'`@8W[P@M8c`O(OsJ(i)tjHnH'r+TO< + ^`Q2hp]H$(&,["sn-As.+8@'qi",a:5O_1m^`W=RJ)HBdJNrW/rkKUQ"+T5>r..),#6)C[p + ]H"6%K(cAn-ArK)YaDai",_t2=LkM^`W=rC>]X#JNrWoeA*1$"+T6iW.@2&#QDL\;$2=+&, + ^uCU'CV4+8>qf6k0'F5O\pWL`6$jJ)Ck8&N0%_rkKWN,AQ-Ir..*%8(>_"Ji#WPB$Vn)WUfY%kA\V4-P&H$p"=F32\"qNi]k*NW:5V,e#[@m8oXgqCLR2chr%lI'tG + BdB]EVba&AafbD!=ot%2FJhncS]LnLgGG9,Q$8_q+rHE!LV5Ggqf9`u4_=a]@r]Ik;4k^sb + 0r'@X1cMB!eX?[OVclH)C.7EiU0Dh6m]\1T6hko^o('NT/\ToqqekhsV9>4!?ZK$PilLRdW1bFi>p@(]lI" + .8a_p$]Sh2nqP:IJQ"ujeh<)fOaO[DD)EB*R8Adl`_h0;e@&?2=oa":9P^`F`ZVUA&efD8!SOMROojUVF@!eN4[3F0e=ZmL1FT?mM)Q9`Y@Gto + PN`d!I$6q8*T4@kba#LUAKkier0a,C:up"50a?R14_d3<>*f@8IYB+mSTa<+U$36!sNDF`K + BS_&(K*ZA&EJ)Ck8JNok5rkKWN"+N]Jr..*%#QBMtp]H$(&,["sn-As. + +8@'qi",a:5O_1m^`W=RJ)HBdJNrW/rkKUQ"+T5>r..),#6)C[p]H"6%K(cAn-ArK)YaDai + ",_t2=LkM^`W=rC>]X#JNrWoeA*1$"+T6iW.@2&#QDL\;$2=+&,^uCU'CV4+8>qf6k0'F5O + \pWL`6$jJ)Ck8&N+V)UQK9jS\Xm5OGj`GiE>,T,M)kUggC&(iG[.+Deu`2jO*BHJ&^ms91M + gM5L^72+gUl!D?$up%_Xdtctph3qm//+X)kq[.S_9M,&#U$?.aYiBhfUoL`f'CTrOfdg/-% + N"M7r[qeXF.Ml@LQd0aVgfW%12rdgA#bU2\G0QPL\;=:2=d1M.YkeH<#SlG0S_rR1S0lRoA^Lioq[Z'1\pN%HJH2aQKlgD/.0/5+3ec6>FHq + \Ydh50j!O4oZXAO4rlgl^?/$Pcr?0Vk2'lQT<%Ia3m!;4.kDYGj7Xk+.`CeY'RZF%s/l6XJ + ^KmBC2:=es9N!gMVnM]3TW0oHO$Mkf&V6R<62gH1/q>)o$UpFRSI9&oNW`jq-Pq3SS6%rO; + M;l\Z'b$TR6^nuD#Vl2EES(hp1b + [_eJOq\NejI1Kl9tn++fX4kIrU>Beiibau(b7ZkfE(qn!Y&>pRm4XaKHNqF5N`dNg,82,AL + h@dAQ!3qBdOkf*?c22DG/(,0[I@4$:-F^"hauoVOd?R\\;Fg,-$EH&#MSYNSH[;gtm$=OQ=E+N1'#mR8-*/ZW + OT:jQJKOMa)L@4EL4OdPbi##D#,/#QDL\$.J;Qe^3(oq&o/3rkKWN,@S=XVbCGEq+WpF"+T + 5>r8IE<qfi/\G!-u!V?_]SXUJ)CjpT2L*:KT@V!+8>qfi"+]ej&9&IQ_2.Q&,^u + Cn4+7D.+/+p^`W=RIt[UGeb?r=&,^uCn4,*\.()>MJ)Ck8JNlL!TaWLWp]H$(&,[%!:O-5+ + _#f,E&,^uC:Z!8SpVI?0qHb$:JNrW/rlFLs,<'[IIsM!a + )ggeGS>4BgPZ.;Z'BNW8S9b;-<>.;Z%uH/T,9>JN + rWo8\lKAnV>ONB]0L";,Zo(K)@'s/YPZgp]H!['E#*XJ#KrU3qrf,'GLDL/:Yh8jmWNS-mn + _!=oK,e(2Fkrn-Aq@,PYbgrfl!"_B8QJN8Tc9q3.l)c)Ad85O_/D_&nU,-`8:=WB:Ld^`N^ + _Ik9!>Rmf9p&,]9inAc1?8LR,#X%J"/nAit058NP1^!_8s#Q@hOnAdlo\Q3[/o3:G;%l/pu + O!XXDXmd6o>b911-S8`?hu"cd9/s$Z=Y]gCC`3.jJj7hRD#SZRSaJY3ld&.t5NbtkF7N'!< + QtG(O7u/8':0dW?P%^fpf;!LVR5nOq=ISbrPIG.pb1R4,]LlL6SnU!p)0MA^hI3Rr]BVib* + '`9?4OXqHVUFackNZ4P+/[o_VC>rB`[7`ilgRSie`?*KSJUn>/5*W2:OOl1s,&ZSi/5lPJ` + 6;OAq?a1@aG)`)ERt:1)/T'V6$lTpWJ0VY[:gNS?P_l`dQoKrs@A4F$[q]8+Q%j[HRGS-h(V\jd^6nVB?cq3.l$jnk6DT'5d] + LBsUUdK>:J#te;P0e5UmI + NJDI-[HsrE9@5s;lD;/6r,5<">Y%JcDI+sk"g)*oT#t6`d*NGBq84:bc+tKQr^_A=K0Mm]_&n7-8:(Jl5=d + NF+0kf9GS^ef+N4J5`;4QSiHb3NX>]huet:F5Vo(:%p':kPOCEf+@fa$0)Z%]rZK/_Ilci?_hs3rod;F<&H3ef$iY7rT-O)kR+.`9W>FJ'ZF8[e- + iNjrh`jg"c^C$CIL6A[+N4J53pFsdG5fFUrcJ-`EPNZlR,CafrHZEVRE3$3PPPrW&HmTF<< + [k5muL2M%F^rFMPLoB-[s./:4gM=%qRGN_iEol9=Sg3rU[TjOr.?GU>Q^`r7%hIX: + (!_i\C;s_7dYLOcY5n>If8hRe,rE@UVm8pcti`2&a.Gi24[au0SFSF.>ckXIRL5jhAK%!A> + ?L"L8I,Bf.3mFWBW+G$rt_i + 4L2j>?(kj6sX/r,#34+*i9-e342Q8o&YO@-/aEiG&?1MWe`u($:j-&=%Ius=+J%F]:GJ!9 + %95^3D+'"g%QA0o'aY50s%CSFE!EP(?;IuoouJ%Fo@YALt9/!65=dNF*sqpC_3r-:o;CW0!5TbQR,F#^J%Y5pS'E,="D@L^X>@"IS?l1cKZn-:Bhp`n%U5FhP%l2775&$grJO#2@C4SGXDrTW5bd,`ODOQGs:.3SC1 + bo[6662YX#SaBLf5;DmOmR5E31[`!k,1$6Mn-=eE7Xs8Trm"a*]Z&!opYW7Q2h,/frDHN^; + O=-XWn;pU%?VSf7rNicNq&.Tf:3r;XrG"8#L":N3b\<6iLF6,q8.VArYPJu@cMCAgYK;b?c + Tme%aCGfBI5*1<31pYPHE,*WD)8=(OK/1PtCr=kL<:Z5;Dsgl`[DI'Xd1^D3*H>Pi22dG$a + &d045k!6-;_NIqBalqF;`&2X)+bo`Q'+4L$q + +);C:O>R/jV"o)m_%@@WEu"ZsWsJ\qo4S*jrMK2,0%LO3+DV8^8Fsm^Q3[>=5DU3TOlh_ZF + Z9'tIh4F2,L%=)^i]S.`n8-MA.??sXFK.=&f(>,jpuq?"+LkHp`n$*YM3CQ#Q>iXn4/p3q" + t[#62]Z%1k1N5*]KcWq"uqRNo5sTIsb1B:$3&$s+RO"p`n$*I/b5d+F`fMRQnVk_=.^/+D1 + +5RQkeGq#-!;#B>A\')_!3hu/$"IJIulMjJ#)nH.<:X:r,IU\+LIf_D-:$7R1Jd_KBee%bAk0KUH4/VVim@#!l6.QVFOl4PNkCHpXVb@$U]" + SRO+DV8^8FU8Tf-HRVZhsT:pk,Xkp`mVI3@EsVe/sa@*q0D;6h!Tt5DoU[S(+&?55Yh=OHP + 2[')Za]k%6Z(*9/##^^'i],mAcDp`nbBpWiZbnV7Y6%oQ5=eA^T.]q,0,GL83e(+K*W>+Bpk'7rr99A/,PR.]L/02qH/b<#n-:Bkp + sUO=rcIj0-M?PYDgoMEUnlpEQi&!N9^5Eoh`QTW87B?,0)60H'uomd+iZh=2c&0am_:!%59 + k?4kj@"ArZHd6kg)?2hdQ@kf)+pg8Gok[s50HKoP6nI[U7jnMH;sT30i*4Y:"MWm\(Ah.u( + SD,="4srI]dTmS>Kk?C]*/_9R\rSk/B(lpDP0!4sooF"DeJsIChHa$a^?DoI:<1khi2KN";Xd2@5LLUeW2SK"nn4Ki\B4UpF!?*g3b/ + 11,dQn4hr3)!s3=]4^A9VW927s+m`FCJ.JDXB4o#"%(O?&-l8d)3R&1\cTu7Ao$.kCE`+=S5Vc)n1%HlL)Sh2)J + =cHtTp3^PKT?H0*$pJ7@>2+;O!p.^p$@d\s"$YLQQj`E!:e@!//%F#>CK8 + rs,Y,S8^>?OH;Iqj#f01[CU-YM%A98dm$9Fq'Fs0JQ<90&..ZObCMle%PLpDN$Kk]qsHT7K + 4-Q7W%5B\STFH;0su4al&`Bp+tH3U+F[*sSMG>YMcNl"nMqS&I;qbm&TkSlp,MZ[)D2:lms + Acn^@9&&$KarT>g?>/FuBa9m4jEJ?^/Qu)cgT?>!Jh4G3QIR"\Q/V,mP8NCjekXIp%?r`WuJc1C=%_W'@8HWUL/l=F0JW')f@IE1MUlr'=p-r38Y>'$CpH&,]9inAc1?8 + ZtgC5O_/D_&nU,VkgaHX#^Xf^`N^_Ik8keFhJ^m^`R+jIk8d810kdre_gu9;+C&qZM`*-lJ + N3N;,$K"K)@(K(:U`[_B8QJR,F#/q4hZN4F6W*rkKVp"FhuATAnSak*BWS"+QOLr36rcC?f + Tgg=kHqr)$A]a&;hLCi4njn4-p'+,fWd;IA,NYCSq-7nLN=Mt!X<\H2]F+6.bl+Nf=1!Boi",`_;Xh`NrLpt3r..()%K;a$5F$AVD>ug" + j%kYNr[\Oab_c7cF_FgJ*IX=S$uFgN`tnYV=#0P\DqohTMUrOtA?+6\a\DDtpq`p?r6tT&c + Z^7q%KGZ'rBgW38"IPsW4"*&,A9s4U?0573NW]oq>0(%qlBQ(FrnkALs$>jMb&osG28Pm^S + Xijlflc[mQ7KiHN&0DO'hF+AVTr<)$cs-/#\M(Imb*,?^`(7o;Unk(#LdmrHCkU-(V6N^A> + )>Bm\@,Dpm5pf5WRQ(eC@9oY?QI&5JRu05la'o]FO[ht;8]di=u?5MQ.ChN#;6;2uOM;S1\ + ,k9p2]1e%?(AUF%nF5=(hfu'u5cZfD:N5q=5m\;'eb'hdXI1nPGfsi>C$`=G82RqX`0@1Eo + WcL2^eE"td$`t(Gg%50E9:rp_c#&NnO8PtqY%Q>7Is^V*\ + X_m2HfUc\I2r.V;tXW)J"D6Vo\dX]L\eu]?]=Rn2qDK?jG5I8pp,maFbk1EO0t>0eu5]W\% + PCK9+&(\SFY5+eTKD17iQdXqP.q)s3O'mnY`cL:QlT\)&A>T_'p]aLAo6,p!$_0STp4Eq^: + :!>9WpL2Le3a*]\*kb9'mhOZtbO2IqVLN1K%<7W7t4j]Phm9M6L'FBbXj(Q8!b<+"AUW"o< + I+8>qfT"[G#-=$i]JNrW/rkKThq,:U$roi*srk7e+rkKWN@j6C@=rKFc#lK68#X6 + f+;i",a:5O_/YIjFrL+nu.hi",bEqpY*09]6N[JNrYE7/jRnOu2q9J)Ck8H-6,Nl:P:7>,3 + /ArkKWN"+N<0O8tFS!e9,=r..'FVB):tL)S?'i",a:5O_1[[/YHW:4COTLBdan+8>qf,RjL + tf[@i'Ln+HTEW(a>N@'.(HgJ4_I-,C:HXa.:qsSX5kluW28e?LPSK;r;W0)ErOs+>ja%26g + RHuf!:-p_S2VM2P>GARIi_Q.,cYWQZD62h@7)o1L_R%+_jBUYc2?0\)N;`3mIeVg#%lsUFp + \1H<8($U5T^tELs4U!9=hA==[H/ae;Sh.O@<#u4SR1A`%__XsnJF(0F5hk$'-oMcn+c/EVp + e5IrTa)nZ.gNoI!OW>k3p&P"I\ZV4*$<3Il0TOTrPi,\tPRV`h9kJI/@0k3PSNX:4[,1?\N + q$8)HQ1jm6?Ad5adB*Pn\B$SU[o".TVghU.f74dkkBL3/Nnc@]JCHXXGME)+;UL=)/ + ok+S3H*dX+#Y7CPHrK['1Y&<:dgLV1:k7mBL<0+8%nD53D\^.Fr!Q5Gi-'UfWV.3^9O?,X=8c[G`5KhnLghABN:ZmM[iX5q:g*BVtbo + O-:eK3SU8,D,-ZkLWecQiE7(C5C&pTBuKtMf(1gHGT9.l/'lr5P"Yd7p_D7m)-.YRi!*2"; + _,-EiOB7mMideX4"=hZ8/I:-'qK";4oLSBl,#FR'jAFiToQj3-+hiDGV!4S9.K:#A"EZoto + M)R@XQ5L9IH(bMUVFN>!aOoZ)UGO)[Zt-2Ku%+F3m)HXYY-Bs/QV+%&#)M;$sZi(Skrc8"0 + qf8X'MF;`i",_t2=LkM^`W=rC>]X#JNrWoeA*1$"+T6iW.@2& + #QDL\;$2=+&,^uCU'CV4+8>qf6k0'F5O\pWL`6$jJ)Ck8&N0%_rkKWN,AQ-Ir..*%8(>r"1gU#QDL\p`jP3&, + ^uCn42!D+8>qfi/ah!`Lk/97_/F'8.,\u&*@s_o?s)s21Ig]kJ*Vc.jlNSP1QL,m\:%!l+) + kP,IHdQbcLNq+iFRH%t=%U)d%%tmQ,a\ri)^6XM_A(brdh(9Cs!tGULNk8'1.SgTgAH:@a0 + m&,b\?aL2#pYkR&&2F];"t^/87h<6(uPWBO+AQVrs/>+*P"Cs#m + )aQJ*bVka!HY.q8kmH(`k,G_g:o")"XID$b2_#f*m!=KYJ/m]U+SCu?+V4/\g]-Xtt`4Ku/ + MKA>ao8h.WNR!IE'U9V+FHl+7bB.=De!F(aIY1['W;:3DP=3 + Aih=(!4I$FF![/q&[.tN!PU%nH[*qgcIiQXGL&sM>3f6e9o%,[.%J3-t(H.TOl?5">8ALAW + pn]X)kp0-WQ]%@d6A`,Vh1^P3p]H$(O/RUon-As.+&N + ,ii",a:5GAA]^`W=RImabDJNrW/reMKf"+T5>r"1gU#QDL\p`jP3&,^uCn42!D+8>qfi/ag + f5O\pW^`QDUJ)Ck8JNok5rkKWN"+N]Jr..*%#QBMtp]H$(&,["sn-As.+8@'qi",a:5O_1m + ^`W=RJ)HBdJNrW/rkKUQ"+T5>r..),#6)C[p]H"6NOekK<1Y@f5KUW0mi=Q7n!N0lTYksaP + 9-bL>7nMWCXgp@f6=bO\8cPGGEHJ:*NDA'@,-6%?RE>Woa`,dpg^kPGDq8 + o@C#5D=49uEb2AVac7?%LAlb]ZdMOf`c#rk0 + 4gUI*QF9;#X_k21kQmod!0"l-SOMr?+D,^rf/D42)H^\_g/]X#JNjuK"+T5 + >r5"1-p]H$(%n>'a+8>qf0VSdN^`W?(&N0%_rkKV7W.@2&#QA+!%K(cAn-:1Qi",a:58[6" + J)Ck8_2'"@qP,hkJ+D.c1A\o/#QA,TB?\Dgg.(-%42sF*o\>7qHu2'^!c:5s[ZD@=<2M0%D + N;_p#c_[C_tqF5k5PG^TeWJ~> Q Q Q showpage diff --git a/testfiles/cli_tests/testcases/export-dpi_expected.png b/testfiles/cli_tests/testcases/export-dpi_expected.png index cf1781ea307e095cfddb4762fab2b46360079980..b2c28479995e7b7a741a4511a6fb831ede3cd6ca 100644 GIT binary patch delta 689 zcmey(+RQc~u6}~2i(`mI@6@Y~*>_R}j(^{gM*W+1otedWNs2+DGr;!cJ$T8=}p_(qgHUVg{;+8nwl_4ZT69~Y5#xM+o-5z z&c5-ayo%5G^W4gL>1j&Ob9O)an_;nH;j*?R%Gs}1WLvJC`o7Ge-lgSo)~#Qv>@BNS z_3aWh*O?uV^S%53IYUX&aNd;?0mWWrLWg(g?p*W5H0^GYb^R>s*^I{)uR6ZqP+)7i z%3`nDNvscNypaQ7}d;Z@cQi4H|>u~*mVC)xbl z_RMi_+{8Vti(k0jsA_Obj40x`QY$UU{m_>8Fkkj>x%P6#Jk99P&rOGXMg28;9^T}A zus6#8q5M+rO@+pFK73gj)usLizAEhH%J*_BvFShF^6S5fP(+!vigy9tnj4nAIzT*Ei!1IeTcP2$#Q+J z;qy7~=j;t;yULqkow`@ot;O=;rUJj~cWgJ88)?-UFELs5n#C=(c9ZMPEpNK|?thIs zle{zK_JR9aJVtVREjH{-)zH=cwly_NdUN@7-Ou_DH;YUvcU+dlz`(%Z>FVdQ&MBb@ E060ugjQ{`u delta 681 zcmZo>`^`EbuD-+5#WBR9ck0!%*)gFq$3C9lSsrKPt7h04)*2+x8|EVN+O2uU){d@4 z+}HC&TCVl(nEl_OVCGb5bLF+W!VdbzZV_}bTyiU_B`P!bQp%;mxOZo4_0kRNWIHtYGRYv^IL08-O7rS%sE=#?+ZoZDj%&VVQ%u81ld$LT$jCH!}^ewSp zd)l^talf$ZO8>jiPfB(N|FXMWKHxs<-0e+yk<0YHt`^=EC$>kgA|ga|$qU}|YhJB> z{X6QUuW2RMJcIQKob!2=Z9P)!_bhp{YnAr2Eq`9xtu+78_aO7kV+pmk`;rGa4|*PK z>$0y|J=tn?(xp9jp3jhEmRYJCHOWf-l~!fY{oNH@Q~xi?J}xi9T0CRQ&zqXtg+fx| z4mba2zOysU>0PeO^vEO1O`js_MO;J=KDKzP7~I*l=)q5CM)#;I=E{X$0ixescZAn- zE>o4eY&cVcbB482NnBw?;1sz}K8}mJoEB+wUF+wFoqqD709$SW$JZy@E_y%N_2Xa1 z1@55eqU=SviC2CkPwAE_nm*N3ST=X%dKYuP*6v$uYfHT^vUc8gnY;0y0$0?c(3PUs z@A%fNS(+bR(iS^8O~&Yl!1b#+89!N{Z&1sStrsZN%Xp;j7<)7HbXBubh5vEhdhfLl zSVQ<3^0>}rS-s!JYyYA;{@%HUIzs diff --git a/testfiles/cli_tests/testcases/export-dpi_expected.ps b/testfiles/cli_tests/testcases/export-dpi_expected.ps index a92caa7ef3..a431f332fc 100644 --- a/testfiles/cli_tests/testcases/export-dpi_expected.ps +++ b/testfiles/cli_tests/testcases/export-dpi_expected.ps @@ -1,11 +1,11 @@ %!PS-Adobe-3.0 -%%Creator: cairo 1.15.10 (http://cairographics.org) -%%CreationDate: Thu Mar 5 09:46:34 2020 +%%Creator: cairo 1.18.0 (https://cairographics.org) +%%CreationDate: Tue May 7 07:52:53 2024 %%Pages: 1 %%DocumentData: Clean7Bit %%LanguageLevel: 3 %%DocumentMedia: 61x32mm 173 90 0 () () -%%BoundingBox: 0 0 173 90 +%%BoundingBox: 0 1 173 91 %%EndComments %%BeginProlog /languagelevel where @@ -104,12 +104,12 @@ %%Page: 1 1 %%BeginPageSetup %%PageMedia: 61x32mm -%%PageBoundingBox: 0 0 173 90 -173 90 cairo_set_page_size +%%PageBoundingBox: 0 1 173 91 +173 91 cairo_set_page_size %%EndPageSetup -q 0 0 173 90 rectclip -1 0 0 -1 0 90 cm q -0.9 0.950196 0.9 rg +q 0 1 173 90 rectclip +1 0 0 -1 0 91 cm q +0.898039 0.94922 0.898039 rg 90 45 m 90 69.852 69.852 90 45 90 c 20.148 90 0 69.852 0 45 c 0 20.148 20.148 0 45 0 c 69.852 0 90 20.148 90 45 c h 90 45 m f @@ -136,182 +136,183 @@ q /ImageMatrix [ 384 0 0 -375 0 375 ] >> cairo_image - Gb"0WGB=Pn\c6Z%%44*ud^u(.KMYC`,R3VR5&gnFp9BgFm_na4)sko\Vu^)(,`76nLgk[36 - P9Z$5sk2j#t_(uHIo;7Valj8d58Y[Ur..*%#:"!A&,^uC(f:Bbi",b%L`6$jJ)Ci,eA*1$"+N#!#6)C[pk'/9 - n-As.+,m.L5O\pW?pkJ%JNrW/,AQ-Ir..'L;$2=+&,a8")YaDai"&<,^`W=RIk[T$rkKWNK - +U!1#QDL\N2V:ln-Ar#6k0'F5O\n&C>]X#JNjuK"+T5>r5"1-p]H$(%n>'U4DH$&RNFCCUJ - %AZ^8AogM?k[i'UuSi@73U.K$8?#o!iE*$?QHqo!S9>L6B]jb)4m[rHF^qM'-E9hue - JqHlgBaNQmIF;jpcTF$ir5(&.P[(I))PMnUD&CZP%VDpgH?1"I;M.^SQ0ga@]@*DJT+pW - d(H6ur1nmLQrN89Fe,r4i8[:3q#8AWb/"$2S15cn&56Zf["d>o:/7g&-s-KIZ5[8l0N]'#3 - OMl=n],;U-T1?-L#Rj]:QQ)Bu^Y>p9$md*r&FCi6>sTBrTEUs^Mkr7Q;GCl%/d^A%;l4*t# - "?V,ce=rF_gH/ruIA9RHYB!d@t'XihNK$X\e0!01jFEb@V`lU*Soh='WVE_1s[$otmQ9VM; - +8B?("+T5>r..),#6)C[p]H"6%K(cAn-ArK)YaDai",_t2=LkM^`W=rC>]X#JNrWoeA*1$" - +T6iW.@2&#QDL\;$2=+&,^uCU'CV4+8>qf6k0'F5O\pWL`6$jJ)Ck8&N0%_rkKWN,AQ-Ir. - .*%8(>r"1flpd> - )Ic%%Vf3LTX!WA8891c*#qB*OhhfX8cI:pDpYVr!QCbh#e54l!#M5S[q")B4l-](# - Kc(o,Y-G.@[TZn5j]#`i8M*TK;/kPrd4JCQ[g4(j)h%gs!i1#r^ur9>/R:Q$pmfg=aCS#i0 - )>:d`ugr9Flq^[Jj,SA4EkZKX4]eF=%'1CFEs'kS22P5=N;Z.7j9`6__]/tAQ38TW\q?;\X - Wp[;klp=bq#8%6sl1e1%MDMRN&X,>$Zc@PG-BC^[utWH<4*S=O*;/(4$2H*d_65K>4%5?:qb - +n2PuX,.roqF:G>P.SPE_*ej4aGe1Tk?%TGpn.P/l>P*4Bl:$@fO8+(L3jGtT%@pBK)(!uo - jQEUe[t^EmehC@p?:bpPFJRbJ+rpqGePla@kafH*ePg7:7/J:g=4qW3PE#ieb,9[X5[,`/o - Qm-k0'5W'0T:;;[F"1F9Gc>_4UF[>jJ"sP77HG/%la^(39--=\e-8c`K=Eo$8]_9M6L?j*\ - n0+X=WiE2"TL;tlJea[(%<,r..),#6)C[p]H"6%K(cA - n-ArK)YaDai",_t2=LkM^`W=rC>]X#JNrWoeA*1$"+T6iW.@2&#QDL\;$2=+&,^uCU'CV4+ - 8>qf6k0'F5O\pWL`6$jJ)Ck8&N0%_rkKWN,AQ-Ir..*%8(>>=%07fbK7Nsj7GR3GKS]c^[]4T=T4973Kf - Dq4Fa:1mK7"iB9N:jf)fe10)h(<159 - sO^L,+DOsBZE@1>e-X&efO'M[>@t1-t&@4)T@tsCg3m;@V-TZj)"!'=YBKl%dk:fP\ba0XQT,eZ==;3N`]B8LF;$M"^0N)!q^(tm.l0a-r - ]Z';]8s;'dE"OCbG1BW``d!H.VWuCHKr&&I=Y=e-B?B&h];XN#=gu9o\n1.l)LX#FC0/tAm - Lt>53qk-Z]BPAjIU-C7gNaZ7@#qVU - oue/.8tjDi>mk#U&6df!I;709*>mn^4&\?VlmGn>9!Bs+?M.O$\.I+ER@5/X[rnMi",a:5=5;4cmKIb&,^uCn4-f7b)Bm7 - $iGQ?n-AsjnGZ6Aoel4DrkKWN,K(\(4Wao\5O\pW^`Q5Ee"/4(^`W=RJ)HA6r1QXcp]H$(& - ,[%!:RRoXm=-eeJNrW/r`I76Vg^96rkKWN"+Mlb:Plf]NVDGk&,^uCnAfSK'Y!m">2^"iJN - rYEUAhOi?G)6ommLbW7V,)0MUfX+`&PDXi9cqfB6&=unG>Rs%#$q>k0G](r*kmLf:N@u'Bg - ]`NcjLN,IZRZ#2^B6o'F+iBE"_C[dHq6?1jEZ0f0`+NCH[?-:2FS`tNo]U\6(Q3m+;bO#R%4Q%2r5"AG5h96q9M-[p=46b4M7:-HkdGf - oPl"kVr^2:NS42)]OM$=s!3Brq6IrmWgTOg.:cVgMd!E*Db4TNUkRk&X^HG0.,,Bg&osi,= - lUkL)/H1>JOV?W3p%d5m-jiq/c4F*-T>8$GSaP:$-#PT29LhE1YlRFF#?+.VD62#6@cJua! - U1@`b]oYUP]07fSW+6-.n%/ph2jNiG)"e8cpTr/qY55j"0Sp,JZ)*&`LKO7ZVrQ3uYdrRb- - oHRPQO?]8Tq2Th%aSS8;61F(7<>[7E&,]:>nAi]MU$,VlrkKUE#(N`3TC4Fai",`_;Xha'r - V?F*#l_U],mAd7_Op;R#_PA27nLN=S+)2JBPN&^n4-p'+'\95]!`OV^`S76Ir*)rU7.gRjs - Kj*J!:ZFYn_be)H.+CJNla6rn*;s`APX,#R5e:O(*]*2WPB[CbW^JpnO57O)=\kAgi9rn4, - 4L+5?@aW2<*eVh75'7iB,bS+EOG?NP`8O"u;O2WmkRFmd/)L[YQJ-m8:pF8k6"jWp*:_&rH - IJDcJ?qK'@.3rFOn$4-X`U-FK[M7^_cW'=ATY&\t!4o@Us - &H&e1J#JU;'UCst+gZf50+;(+Bkd@jl$K;Ni/\iTZ,^DHa)BtuJB;&G%\*DWQ$S>:Z/fDPf - mi2Tr`&&+BG.K/?+F*[1SZ]m=rOMt?L!$3ro`(p\XXFi/t[>/OY5,pf:qR5h,"+TYg=o - @OHdVZYcGZk/Cn=(Q20g9)Z*hGq2Qt%Sc3$#S[kXti>]#Hd7^i6;.eMK*?aQnmVVod?KE2" - R&QR*_F3c[(A.!706h"f1c&rOqnb9ZA/6=Qg[^JBbD:V"Ap?, - +[hEFU3!%fa))m^20,553)2lg5kfhO!mke?)>leKU=6Xt)fB%(J-PT_c=h-aE;bfsV-RLcabN1=N]LUAdPnX']_P>S@M%A-8eW]p)"jCW&&r'>KCr36HU3qP$er'?2Hr`Fe;a-)M"+(U/C - +)CA$MTCU(5KM]55OJ(j[63!6rj;\q5k#"--MZ%bJ+haTCJfRV5<'+%OM(gIg?HquK)T=YI - uYluLHil:Jj8Ypk9jJ`@SqX/s!])cK+o!sBr=2A3'J"H)f8H%_U??[&$i7o:](*1N-/\-P? - aE$39fprr+Y=1_.%HC%oPr=q@g^nqelESqGVDt>^HnR)ZG$#k=a,RjYT24I_a3Y&q*?C2NO%Jp>;R - qWh6k0C3ci?d`^)Qo+O('D-"WV*h-AcS)6rTV\XM=E,!8=6(W^`lkmV(V&_5^;"Yr4Z#;'( - 'qIJ54$VWIV&QiGMDS)-'rb2UF2"p - 4nqdo^qdO]WZ\Zh!f$i?+4u/5.c'4kJ3^-s!aihgP9lpm1+W-^fp\F7fN%$lhA\UGMYOQ2Q - ,1$^:kb:"$NYQg5CYcf2iIO=p$,TQKBsu\dij31G`T)-59>Y&n-@WBn>AYQFEZqs^$rVpr,GAKK0RG - +Jj1:HLY?rrTB6!EG$a&4Iup3(Ilcl@g.Q35lAsOS0DL1d,l#hcJ,X]jNu%D$qpk*'#MD]< - Vka6Mo,hUBIlnB-i=E5Xi(llAl/pDGV(BT+r]Kj*0_g87'E%*n5?)0*UuhMO-`Gu7ZBO4-" - Fmjeh/56&<:MI5KBslY,Jg"KiCbcccVr_QLHe5>B8>9rj;V9+S^R''E"!N%i(ZJDE3 - 4#rWj)56&H!J8FpKGm9Q%LVsTNF_&oJ?i/]^E2F_VlS3Llj((U+l'GLD,k0MhE\X)ibm>iC - ,+/G:^O#2@cE;U64o>``F(&f>&#l[BdkKWsD>0`bK56=RY/.#YbIuj_`5LTIln.EV-p>=OC - \,!"K?1T7l*h.HTST&lJhgn!_CbkkYc@L2rqYDd=ea=R+=P`]5M9\h - l)e_RELRc$-@:R+GD*$qT"'_Tb?o//Efklr'>Kfsu$iEa<#ptdejE]'``aA%q5`mV"q@g`_O_QitUYZIulerJ#)m-%_s1]K:'IsC&Y%EeaNhC+ - D1+5RQmC#T(!!a+D1+5RQpTC&,\`ki/]^Er,;C8d%=G8KBsfW,L)joGPC+qGPHfX_&mCj*5 - $dJErP"gccAk55LTHirnod6JNnr.r/p+P^Ae[m&3oq7c(FLLE6J'0huA>q,krV/TD6'Dp]H - !W1\`32T?b`Qn!2b9TKi5MccA;%58*nYWTr.[HogO'p:ik9P$J"=ec$@P[L\MY&,-#ikWb5 - MrM/HP^]*s\L'Yl?f_6-Nm3a0,,cs6/**QLi-ShXsJ8[/.-( - X:4D:pk+5)J#)lr-Y/8!r)(G6i"*,Ki/]^EejlpQS3LltDX[na6h!U75CPQi/E`'T5n/=*4 - Tp81q%K2$3#T@%'uHgDXhRlp^mEI,mAcZl;(LEY">H]QLi-Ad0'@j3skn.e=Y, - PUF"jIIXg)Nn.5F_8dbQ>e=Ve^GPIZr*V-,q&-'EeUnlpE8,ZR*[E>6CG[9om5KHTO599DE - n4oWFrFt:NO22p6Yi&<4r1TH-YoqG7rj;Wn2rG3E=R`4UrX@U;<#RsfoV_(p^;D%gYi&<4r - ;"fu5?ZL.X]N!K+(RUPT+:\'-%:b@rRlXB48_D<'E%*V5NafJ1&]c>[n6-L:KoXD^E\Lu6a - dNimb/]\+G%YucT;Bg@JXKaqsQL8r3\chj%oK[l^t]3Wd*'(F%^KD]BT(>!cJ;rNEA6_EtA - Ah$0rZY]N0Nn\THk"rq':dT5eNK/V,mpZ2Y8g(l)@5DL,kTVppT'9<3TUgjuS&Pqdlc6YenPkV-H^>h4JoURtgI:-^.cMS,"4#qg6,UGMMHE2AdEU,uA - 9!3qbNtL4nQChC\=i=sC(5=ErI=$1;Tep,CcRDYVVm"^qma"t^?SVLN/k\BuFSJDTOiJ6%) - FVnq:G!nP`q?Y'B^a=^Hm`m%j>Ue7q]PZd"@f:2iPVapWoqHo,JQA8jg)s=Sm5`3l-E^4rmdbB*)AcT*dllV;H!"^Sc8EAoXCb`V]VF`5KM]55OJ(jW?fT[re.)&^`R* - *_4PoIPH2s9W'B5O_r2-3\)<<)piA:VBKPa85=eA^+)CB/e:U]h#MEPT`u=kirssV;%oQ

@2a:;W.re.,'_&m3[_&or]*)/I4RF/1@I - DI#$]\niQIld5I48_AO$iYtTVaKpc_]P\Yi/^ui3qU]Zr':YtrWr/Z7.Q1piWF[lP(Gn"r< - Z4,L-NbNK0KG9OmqiDIuohF5GJ(mD$Qiq-s'VgIn#-%?9pKq.^VnumNM - FbS%sC5R&3EtU;9=7WNL.(k[>R3=/A`(&GP"c*iC#%Y=6Ha43 - +*h.qhitVjRK,t-S[9:8hNi`WlgJ?ko-!33[]0tU9fb;OnR8u9^h6"PBloekLT*IQfjul5B`>Drqp - HYO!a]tS/a5.P5cfLNUn1b_3#6pdR2+_jAO+NuJ#MA$Ypj5EURoX$?%pI\/5"=or\)SoXa4_6pk/3[R%t"L;WhmM+Q@ - N">hP8;1I"DdbkZ6Z1[%9/c'N?ru)+apk,Xnpg]6]gb62KJ)H@hJj6^-:O7:Ir..'># - l`JH5K,M!R?a30#Q?+opg\CEBPY$F#Q?+rpg[P-PT!0YY6KL7O"u;O[cJ%uLVY)-UR9u#%J - `qu.r'@Z!e9-h@Y-g&jH$H=pZU!Lp]H!['E#*YIi=dpi=Gi`;Xh_-r2H]uC2%C55O_07_&l - nOVnTDWrkKV0"b1@*T*f'sbI7S'i/a%Q5;qI2rH7$/r)$eia85``1R,ip_=XXDli&u+nAi8+_&m=]r]:ek-BKAar?b-SXRFg9O1ZR)((>\a[.nA.L]/FtP$OX'W - M$+2ls\QEd.p>[0&a/gEnTc*?LP#]h-]!1dfB+CF;Bu[SXhet'7NZLp=Iik/)>DO\TIf?VB - 26T,Z?;>VQVsLGMEaq,3':RVG - rN/_BG^l<\+8Jji=Nde[WEu@MSBF$=+o;>=3),o1[):W'hO]";-7=-3*kGip&(_l9<5"[8T - -(ijHLTnj6iCK701L5J)X0C_El8bWHo]2-]#;-=a#Qe1A+W7*Rsmd`7JXNB:7>olV6Q3IBO - QreJ$^QiP7Adt5L&l3g>'c.aJc0g:9h@7oKVcGYM]3TU#0p;n*k[G.tg.\k0MEA6V$NSq*R - dto#DonJIHOUFctP"R#Sg=Hja]sBaM_7DX3Rq[d?kITqo5q)?.Dnp]H"pG^oAYqDJfbJ)Ck - 8JNjq@Vp&9:kaT$[^$l:+^`W>]+b/bqle4N.r..*%#Q@iB+8RX?l9X>=r..*%`tJA.[QOL% - 8+6c_^`W=r]0G#\-beaRJ)Ck8JNoIlVpnO8YeV@U"+T7T&+@hCJ*[Sni",a:5F1f:4[4+E5 - O\pW^`V8.0n7pQd1\b3%"I1Gr..(uo>fKkm*J&V&,^uCn4+7D.V^IpH4]iRJ)Ck8Oi8CK^L - @KJ^`W=RJ,'#MrB4*S"+T5>r3;>J&tZbo&,^uCn;$I.M8#dbV$[.:+8>qfIX$?5!6GIs5O\ - pW^`Nm3MGJA#2uVh>!!!*\l5ilkl?LOrM)UoOS!DF:rK*]QmH2-HQj,!Oi.*qs - P(^U.F!c.$:(W3*#"X5p'6Bl=dD4IL4F-oHfZBNjU9O^J%VfH:-6i\/O>S>j()gWgb62[ - Q6$h30S35&B%6+EeUKaXf8i^W/A5!EkgH+]3Sdd`2fe*&iSMKLHh?dihN&Nd6ruOdSuSC[T - 8ZVLg^!aflVCjZI$)kc*\U1sR/Hg;f3d;0RcL@7R8,c\3k/O:-U8rT8l0B>[QV;J4gQUoUG - >2W%kAaal11:3Rtr2!D/>f?N02;UlFL^SLm,P=J$7oSX^7Cl37F)LWq39)p%"/Bs569PRJl - Yfq.&7n=Gd%(&kF'n4C=K:5O#R=s3D6O)ko9iYGfPSagib.DS*>/XWf?lb%umpdOWd(=q1C - DcbLXt"+T6iW.@2&#QDL\;$2=+&,^uCU'CV4+8>qf6k0'F5O\pWL`6$jJ)Ck8&N0%_rkKWN - ,AQ-Ir..*%8(>r - "1gU#QDL\p`jP3&,^uCn42!D+8>qfi/agf5O\pW^`QDUJ)Ck8JNok5rkKWN"+N^u/H4-Xe] - [&M86X*foWt(5AXltMIc[2_s7u+2s3?H,)Ws%sR+F"n7!`1Wkm4D;0XIeXG6kJQlM`Gc]7C - -`_&e4VqG/A"VX9ekBklSU%5AgW'&6aP>pZs+I`pKIrs54GI;,M2c^bMI_=?a!6T9d2h - H1EnhbaL\VDi]>NJP;AhMRM@YpEkpb`5,2KYH!,/tI)jSG#'g[skV8/4*FXK=JDP1Wq=San - e(:]T1HGfhn^=k4Qjq_,jHmK-q<%Z8hq7s`jF4H(2j@7BP@pVfq\==sdIPEF\ek(tZ+2$3> - 1f<$dR!.:la1ent - k;b)ZdO4g&]3OHl_*4OTIeqK;dN7628UQLt,i>LUuke_8o1S%IY/tWpNF?a - >+FGRXZm;(cc7l<.@l4(`;,H'7Mg0AK48m+c$Gq--6.49qEKD,jrg372eRYtr^?!+Ka8p[N - kXT%eGQZGOVc4/nN13Z%JJgFhuQn8YN[/SL"cRYUT$1=>=in]48K&\\-0tB$/U3!&cN`&5Y - M7uB(L6=<%=]7slo:opoP7uM!5OcbR&)DR(-9VsZa]NK*Y0'Kl9/c75bNi9YZ2Jn@'1%dCE - ::U!4*q9qPu0%l&RVo$RN45S(*:]^LWj*F[[>abd"Ph4r6"&WLH7`G8JSEiPBqr`\Wh/l)6 - I5dOCEVm-PDB(/kgXr"VZ;Qg=O+^p]H$(O/RUon-As.+&N,ii",a:5GAA]^`W=RImabDJNr - W/reMKf"+T5>r"1gU#QDL\p`jP3&,^uCn42!D+8>qfi/agf5O\pW^`QDUJ)Ck8JNok5rkKW - N"+N]Jr..*%#QBMtp]H$(&,["sn-As.+8@'qi",a:5O_1m^`W=RJ)HBdJNrW/rkKUQ"+T5> - r..),#6)C[p]H"6NOASG<15(b5KUt>h:]:;c@3kI6$mM<+[&joX]TRLH:6-NaQ9a[Ae@>1J - nNC;RC=Sd&[MWa$eATgkC1C_JH[FQbH>L+\GfaW^.=[j0sLR)W[WNa2EGmS_o.>1D5b^Yc6 - 5=lPWdrh>+U$qjlD!)#<84A*4g?M'O#]ph3n^e33Bm(Q3a4]bC\(EbSHgVr)rf-ba+i+\hS - Nk[]A.FYBY).:N2Y3ruERbRqLT-a&!tjf(BOq+8B?X_fq&oDIKj;,$.cG?LF.sn8^&fuH - 5BI[KhLh3C=^=s5;e':51[0c+`C9VI:3;.UI3Ca_jQZ6FC%LPgB9oaG0@?+celFPG-Jj^k7 - _2aP2ficj6C0p2:L9")&)G<;=[hZBoqVgK]"\n!r!I0GEj`IUb_U!)psIOt/a\pj)9N51d_ - jRF@R`9spk,i+JNrW/ra8,'r..*%#:"!A&,^uC(f:Bbi",b%L`6$jJ)Ci,eA*1$"+N#!#6) - C[pk'/9n-As.+,m.L5O\pW?pkJ%JNrW/,AQ-Ir..'L;$2=+&,a8")YaDai"&<,^`W=RIk[T - $rkKWNK+U!1#QDL\N2V:ln-Ar#6k0'F5O\n&C>]X#JNjuK"+T5>r5"1-Abr12,(DH")B&M4 - iracbehbn;:JJNJruL\P3X0IiO@H/,Q`&$MfB"e5JkGY]k0;:u&OFE+,M#(;2ua=Q<)sJ~> + Gb"0WGB=Pn\c6Z%%44*ud^u(.KMYC`,R3VR5&gnKp2Q7Z`&D[F0HFt^*I:WdZt^9],XaScL + oN'NJJs^89a14HlgALhd]2?KYb?*knl:OVYAefG'qBe?I+8CICT2DI + M[:\J1-)3\,s'V'Jn-As.+,m.L5O\pW?pkJ%JNrW/,AQ-Ir..'L;$2=+&,a8")YaDai"&<, + ^`W=RIk[T$rkKWNK+U!1#QDL\N2V:ln-Ar#6k0'F5O\n&C>]X#JNjuK"+T5>r5"1-p]H$(% + n>'a+8>qf0VSdN^`W?(&N0%_rkKV7W.@2&#QA+!%K(cAn-:1Qi",a:58[8Hp8EuCBRSCQN% + U+D`nFSu.jCok?Y\AJLCkPX#Vsi%/`9!l-$(GWiPEc=>a%qpYT\!,_DE*Je. + danp)M$1q,@^:MF-0X\CY^2;IMuY,\e11.Rn!UZ&&pa]<&Pb2>c_.9RF/Zp#a + eLjE[X/^;:XDef_.3eHQG!$c(MB[B'/Wn02pRFO7HkGiTebEGlI;`HqAG^)..lo4[RnO`A8 + h?0CFpPs.$Z=.%W](MEAF'9TuH==V+7=gAa/Ic%_H-#0mQ3T)#'")<6NLd6(>WFfOF7rE(O + 3N`FcAQ;#YKefMQ-iO(j@X/[kQl;%i^_11d9!,dNElJH$p(7MFcCOse_,djM*qo]dn-Hugc + IiQ8FG./@u'In,!,]@rm-(#OrbWPA_>.$rB?e82ij7(]EJMqTh9X0i`2Ll`9k + 'rH@b^Eb`.kBQ(0@(SiX(2&BPZ&MohTgQ6uB:EFW&KjOE69Ppa]5cdDVZ6R;ENK5V7W41ln + f%0==[9uNK#@B0Zqq>TuRd3<=>cr+J6!uGm7gure'`@8W[P@M8c`O(OsJ(i)tjHnH'r+TO< + ^`Q2hp]H$(&,["sn-As.+8@'qi",a:5O_1m^`W=RJ)HBdJNrW/rkKUQ"+T5>r..),#6)C[p + ]H"6%K(cAn-ArK)YaDai",_t2=LkM^`W=rC>]X#JNrWoeA*1$"+T6iW.@2&#QDL\;$2=+&, + ^uCU'CV4+8>qf6k0'F5O\pWL`6$jJ)Ck8&N0%_rkKWN,AQ-Ir..*%8(>_"Ji#WPB$Vn)WUfY%kA\V4-P&H$p"=F32\"qNi]k*NW:5V,e#[@m8oXgqCLR2chr%lI'tG + BdB]EVba&AafbD!=ot%2FJhncS]LnLgGG9,Q$8_q+rHE!LV5Ggqf9`u4_=a]@r]Ik;4k^sb + 0r'@X1cMB!eX?[OVclH)C.7EiU0Dh6m]\1T6hko^o('NT/\ToqqekhsV9>4!?ZK$PilLRdW1bFi>p@(]lI" + .8a_p$]Sh2nqP:IJQ"ujeh<)fOaO[DD)EB*R8Adl`_h0;e@&?2=oa":9P^`F`ZVUA&efD8!SOMROojUVF@!eN4[3F0e=ZmL1FT?mM)Q9`Y@Gto + PN`d!I$6q8*T4@kba#LUAKkier0a,C:up"50a?R14_d3<>*f@8IYB+mSTa<+U$36!sNDF`K + BS_&(K*ZA&EJ)Ck8JNok5rkKWN"+N]Jr..*%#QBMtp]H$(&,["sn-As. + +8@'qi",a:5O_1m^`W=RJ)HBdJNrW/rkKUQ"+T5>r..),#6)C[p]H"6%K(cAn-ArK)YaDai + ",_t2=LkM^`W=rC>]X#JNrWoeA*1$"+T6iW.@2&#QDL\;$2=+&,^uCU'CV4+8>qf6k0'F5O + \pWL`6$jJ)Ck8&N+V)UQK9jS\Xm5OGj`GiE>,T,M)kUggC&(iG[.+Deu`2jO*BHJ&^ms91M + gM5L^72+gUl!D?$up%_Xdtctph3qm//+X)kq[.S_9M,&#U$?.aYiBhfUoL`f'CTrOfdg/-% + N"M7r[qeXF.Ml@LQd0aVgfW%12rdgA#bU2\G0QPL\;=:2=d1M.YkeH<#SlG0S_rR1S0lRoA^Lioq[Z'1\pN%HJH2aQKlgD/.0/5+3ec6>FHq + \Ydh50j!O4oZXAO4rlgl^?/$Pcr?0Vk2'lQT<%Ia3m!;4.kDYGj7Xk+.`CeY'RZF%s/l6XJ + ^KmBC2:=es9N!gMVnM]3TW0oHO$Mkf&V6R<62gH1/q>)o$UpFRSI9&oNW`jq-Pq3SS6%rO; + M;l\Z'b$TR6^nuD#Vl2EES(hp1b + [_eJOq\NejI1Kl9tn++fX4kIrU>Beiibau(b7ZkfE(qn!Y&>pRm4XaKHNqF5N`dNg,82,AL + h@dAQ!3qBdOkf*?c22DG/(,0[I@4$:-F^"hauoVOd?R\\;Fg,-$EH&#MSYNSH[;gtm$=OQ=E+N1'#mR8-*/ZW + OT:jQJKOMa)L@4EL4OdPbi##D#,/#QDL\$.J;Qe^3(oq&o/3rkKWN,@S=XVbCGEq+WpF"+T + 5>r8IE<qfi/\G!-u!V?_]SXUJ)CjpT2L*:KT@V!+8>qfi"+]ej&9&IQ_2.Q&,^u + Cn4+7D.+/+p^`W=RIt[UGeb?r=&,^uCn4,*\.()>MJ)Ck8JNlL!TaWLWp]H$(&,[%!:O-5+ + _#f,E&,^uC:Z!8SpVI?0qHb$:JNrW/rlFLs,<'[IIsM!a + )ggeGS>4BgPZ.;Z'BNW8S9b;-<>.;Z%uH/T,9>JN + rWo8\lKAnV>ONB]0L";,Zo(K)@'s/YPZgp]H!['E#*XJ#KrU3qrf,'GLDL/:Yh8jmWNS-mn + _!=oK,e(2Fkrn-Aq@,PYbgrfl!"_B8QJN8Tc9q3.l)c)Ad85O_/D_&nU,-`8:=WB:Ld^`N^ + _Ik9!>Rmf9p&,]9inAc1?8LR,#X%J"/nAit058NP1^!_8s#Q@hOnAdlo\Q3[/o3:G;%l/pu + O!XXDXmd6o>b911-S8`?hu"cd9/s$Z=Y]gCC`3.jJj7hRD#SZRSaJY3ld&.t5NbtkF7N'!< + QtG(O7u/8':0dW?P%^fpf;!LVR5nOq=ISbrPIG.pb1R4,]LlL6SnU!p)0MA^hI3Rr]BVib* + '`9?4OXqHVUFackNZ4P+/[o_VC>rB`[7`ilgRSie`?*KSJUn>/5*W2:OOl1s,&ZSi/5lPJ` + 6;OAq?a1@aG)`)ERt:1)/T'V6$lTpWJ0VY[:gNS?P_l`dQoKrs@A4F$[q]8+Q%j[HRGS-h(V\jd^6nVB?cq3.l$jnk6DT'5d] + LBsUUdK>:J#te;P0e5UmI + NJDI-[HsrE9@5s;lD;/6r,5<">Y%JcDI+sk"g)*oT#t6`d*NGBq84:bc+tKQr^_A=K0Mm]_&n7-8:(Jl5=d + NF+0kf9GS^ef+N4J5`;4QSiHb3NX>]huet:F5Vo(:%p':kPOCEf+@fa$0)Z%]rZK/_Ilci?_hs3rod;F<&H3ef$iY7rT-O)kR+.`9W>FJ'ZF8[e- + iNjrh`jg"c^C$CIL6A[+N4J53pFsdG5fFUrcJ-`EPNZlR,CafrHZEVRE3$3PPPrW&HmTF<< + [k5muL2M%F^rFMPLoB-[s./:4gM=%qRGN_iEol9=Sg3rU[TjOr.?GU>Q^`r7%hIX: + (!_i\C;s_7dYLOcY5n>If8hRe,rE@UVm8pcti`2&a.Gi24[au0SFSF.>ckXIRL5jhAK%!A> + ?L"L8I,Bf.3mFWBW+G$rt_i + 4L2j>?(kj6sX/r,#34+*i9-e342Q8o&YO@-/aEiG&?1MWe`u($:j-&=%Ius=+J%F]:GJ!9 + %95^3D+'"g%QA0o'aY50s%CSFE!EP(?;IuoouJ%Fo@YALt9/!65=dNF*sqpC_3r-:o;CW0!5TbQR,F#^J%Y5pS'E,="D@L^X>@"IS?l1cKZn-:Bhp`n%U5FhP%l2775&$grJO#2@C4SGXDrTW5bd,`ODOQGs:.3SC1 + bo[6662YX#SaBLf5;DmOmR5E31[`!k,1$6Mn-=eE7Xs8Trm"a*]Z&!opYW7Q2h,/frDHN^; + O=-XWn;pU%?VSf7rNicNq&.Tf:3r;XrG"8#L":N3b\<6iLF6,q8.VArYPJu@cMCAgYK;b?c + Tme%aCGfBI5*1<31pYPHE,*WD)8=(OK/1PtCr=kL<:Z5;Dsgl`[DI'Xd1^D3*H>Pi22dG$a + &d045k!6-;_NIqBalqF;`&2X)+bo`Q'+4L$q + +);C:O>R/jV"o)m_%@@WEu"ZsWsJ\qo4S*jrMK2,0%LO3+DV8^8Fsm^Q3[>=5DU3TOlh_ZF + Z9'tIh4F2,L%=)^i]S.`n8-MA.??sXFK.=&f(>,jpuq?"+LkHp`n$*YM3CQ#Q>iXn4/p3q" + t[#62]Z%1k1N5*]KcWq"uqRNo5sTIsb1B:$3&$s+RO"p`n$*I/b5d+F`fMRQnVk_=.^/+D1 + +5RQkeGq#-!;#B>A\')_!3hu/$"IJIulMjJ#)nH.<:X:r,IU\+LIf_D-:$7R1Jd_KBee%bAk0KUH4/VVim@#!l6.QVFOl4PNkCHpXVb@$U]" + SRO+DV8^8FU8Tf-HRVZhsT:pk,Xkp`mVI3@EsVe/sa@*q0D;6h!Tt5DoU[S(+&?55Yh=OHP + 2[')Za]k%6Z(*9/##^^'i],mAcDp`nbBpWiZbnV7Y6%oQ5=eA^T.]q,0,GL83e(+K*W>+Bpk'7rr99A/,PR.]L/02qH/b<#n-:Bkp + sUO=rcIj0-M?PYDgoMEUnlpEQi&!N9^5Eoh`QTW87B?,0)60H'uomd+iZh=2c&0am_:!%59 + k?4kj@"ArZHd6kg)?2hdQ@kf)+pg8Gok[s50HKoP6nI[U7jnMH;sT30i*4Y:"MWm\(Ah.u( + SD,="4srI]dTmS>Kk?C]*/_9R\rSk/B(lpDP0!4sooF"DeJsIChHa$a^?DoI:<1khi2KN";Xd2@5LLUeW2SK"nn4Ki\B4UpF!?*g3b/ + 11,dQn4hr3)!s3=]4^A9VW927s+m`FCJ.JDXB4o#"%(O?&-l8d)3R&1\cTu7Ao$.kCE`+=S5Vc)n1%HlL)Sh2)J + =cHtTp3^PKT?H0*$pJ7@>2+;O!p.^p$@d\s"$YLQQj`E!:e@!//%F#>CK8 + rs,Y,S8^>?OH;Iqj#f01[CU-YM%A98dm$9Fq'Fs0JQ<90&..ZObCMle%PLpDN$Kk]qsHT7K + 4-Q7W%5B\STFH;0su4al&`Bp+tH3U+F[*sSMG>YMcNl"nMqS&I;qbm&TkSlp,MZ[)D2:lms + Acn^@9&&$KarT>g?>/FuBa9m4jEJ?^/Qu)cgT?>!Jh4G3QIR"\Q/V,mP8NCjekXIp%?r`WuJc1C=%_W'@8HWUL/l=F0JW')f@IE1MUlr'=p-r38Y>'$CpH&,]9inAc1?8 + ZtgC5O_/D_&nU,VkgaHX#^Xf^`N^_Ik8keFhJ^m^`R+jIk8d810kdre_gu9;+C&qZM`*-lJ + N3N;,$K"K)@(K(:U`[_B8QJR,F#/q4hZN4F6W*rkKVp"FhuATAnSak*BWS"+QOLr36rcC?f + Tgg=kHqr)$A]a&;hLCi4njn4-p'+,fWd;IA,NYCSq-7nLN=Mt!X<\H2]F+6.bl+Nf=1!Boi",`_;Xh`NrLpt3r..()%K;a$5F$AVD>ug" + j%kYNr[\Oab_c7cF_FgJ*IX=S$uFgN`tnYV=#0P\DqohTMUrOtA?+6\a\DDtpq`p?r6tT&c + Z^7q%KGZ'rBgW38"IPsW4"*&,A9s4U?0573NW]oq>0(%qlBQ(FrnkALs$>jMb&osG28Pm^S + Xijlflc[mQ7KiHN&0DO'hF+AVTr<)$cs-/#\M(Imb*,?^`(7o;Unk(#LdmrHCkU-(V6N^A> + )>Bm\@,Dpm5pf5WRQ(eC@9oY?QI&5JRu05la'o]FO[ht;8]di=u?5MQ.ChN#;6;2uOM;S1\ + ,k9p2]1e%?(AUF%nF5=(hfu'u5cZfD:N5q=5m\;'eb'hdXI1nPGfsi>C$`=G82RqX`0@1Eo + WcL2^eE"td$`t(Gg%50E9:rp_c#&NnO8PtqY%Q>7Is^V*\ + X_m2HfUc\I2r.V;tXW)J"D6Vo\dX]L\eu]?]=Rn2qDK?jG5I8pp,maFbk1EO0t>0eu5]W\% + PCK9+&(\SFY5+eTKD17iQdXqP.q)s3O'mnY`cL:QlT\)&A>T_'p]aLAo6,p!$_0STp4Eq^: + :!>9WpL2Le3a*]\*kb9'mhOZtbO2IqVLN1K%<7W7t4j]Phm9M6L'FBbXj(Q8!b<+"AUW"o< + I+8>qfT"[G#-=$i]JNrW/rkKThq,:U$roi*srk7e+rkKWN@j6C@=rKFc#lK68#X6 + f+;i",a:5O_/YIjFrL+nu.hi",bEqpY*09]6N[JNrYE7/jRnOu2q9J)Ck8H-6,Nl:P:7>,3 + /ArkKWN"+N<0O8tFS!e9,=r..'FVB):tL)S?'i",a:5O_1[[/YHW:4COTLBdan+8>qf,RjL + tf[@i'Ln+HTEW(a>N@'.(HgJ4_I-,C:HXa.:qsSX5kluW28e?LPSK;r;W0)ErOs+>ja%26g + RHuf!:-p_S2VM2P>GARIi_Q.,cYWQZD62h@7)o1L_R%+_jBUYc2?0\)N;`3mIeVg#%lsUFp + \1H<8($U5T^tELs4U!9=hA==[H/ae;Sh.O@<#u4SR1A`%__XsnJF(0F5hk$'-oMcn+c/EVp + e5IrTa)nZ.gNoI!OW>k3p&P"I\ZV4*$<3Il0TOTrPi,\tPRV`h9kJI/@0k3PSNX:4[,1?\N + q$8)HQ1jm6?Ad5adB*Pn\B$SU[o".TVghU.f74dkkBL3/Nnc@]JCHXXGME)+;UL=)/ + ok+S3H*dX+#Y7CPHrK['1Y&<:dgLV1:k7mBL<0+8%nD53D\^.Fr!Q5Gi-'UfWV.3^9O?,X=8c[G`5KhnLghABN:ZmM[iX5q:g*BVtbo + O-:eK3SU8,D,-ZkLWecQiE7(C5C&pTBuKtMf(1gHGT9.l/'lr5P"Yd7p_D7m)-.YRi!*2"; + _,-EiOB7mMideX4"=hZ8/I:-'qK";4oLSBl,#FR'jAFiToQj3-+hiDGV!4S9.K:#A"EZoto + M)R@XQ5L9IH(bMUVFN>!aOoZ)UGO)[Zt-2Ku%+F3m)HXYY-Bs/QV+%&#)M;$sZi(Skrc8"0 + qf8X'MF;`i",_t2=LkM^`W=rC>]X#JNrWoeA*1$"+T6iW.@2& + #QDL\;$2=+&,^uCU'CV4+8>qf6k0'F5O\pWL`6$jJ)Ck8&N0%_rkKWN,AQ-Ir..*%8(>r"1gU#QDL\p`jP3&, + ^uCn42!D+8>qfi/ah!`Lk/97_/F'8.,\u&*@s_o?s)s21Ig]kJ*Vc.jlNSP1QL,m\:%!l+) + kP,IHdQbcLNq+iFRH%t=%U)d%%tmQ,a\ri)^6XM_A(brdh(9Cs!tGULNk8'1.SgTgAH:@a0 + m&,b\?aL2#pYkR&&2F];"t^/87h<6(uPWBO+AQVrs/>+*P"Cs#m + )aQJ*bVka!HY.q8kmH(`k,G_g:o")"XID$b2_#f*m!=KYJ/m]U+SCu?+V4/\g]-Xtt`4Ku/ + MKA>ao8h.WNR!IE'U9V+FHl+7bB.=De!F(aIY1['W;:3DP=3 + Aih=(!4I$FF![/q&[.tN!PU%nH[*qgcIiQXGL&sM>3f6e9o%,[.%J3-t(H.TOl?5">8ALAW + pn]X)kp0-WQ]%@d6A`,Vh1^P3p]H$(O/RUon-As.+&N + ,ii",a:5GAA]^`W=RImabDJNrW/reMKf"+T5>r"1gU#QDL\p`jP3&,^uCn42!D+8>qfi/ag + f5O\pW^`QDUJ)Ck8JNok5rkKWN"+N]Jr..*%#QBMtp]H$(&,["sn-As.+8@'qi",a:5O_1m + ^`W=RJ)HBdJNrW/rkKUQ"+T5>r..),#6)C[p]H"6NOekK<1Y@f5KUW0mi=Q7n!N0lTYksaP + 9-bL>7nMWCXgp@f6=bO\8cPGGEHJ:*NDA'@,-6%?RE>Woa`,dpg^kPGDq8 + o@C#5D=49uEb2AVac7?%LAlb]ZdMOf`c#rk0 + 4gUI*QF9;#X_k21kQmod!0"l-SOMr?+D,^rf/D42)H^\_g/]X#JNjuK"+T5 + >r5"1-p]H$(%n>'a+8>qf0VSdN^`W?(&N0%_rkKV7W.@2&#QA+!%K(cAn-:1Qi",a:58[6" + J)Ck8_2'"@qP,hkJ+D.c1A\o/#QA,TB?\Dgg.(-%42sF*o\>7qHu2'^!c:5s[ZD@=<2M0%D + N;_p#c_[C_tqF5k5PG^TeWJ~> Q Q Q showpage diff --git a/testfiles/cli_tests/testcases/export-id_expected.svg b/testfiles/cli_tests/testcases/export-id_expected.svg index 6dfe9e703c..a6dc45ffbc 100644 --- a/testfiles/cli_tests/testcases/export-id_expected.svg +++ b/testfiles/cli_tests/testcases/export-id_expected.svg @@ -1,110 +1,93 @@ - - - - image/svg+xml - - - - + version="1.1" + id="svg1" + sodipodi:docname="theta.svg" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + id="defs1" /> + id="namedview1" + pagecolor="#ffffff" + bordercolor="#000000" + borderopacity="0.25" + inkscape:showpageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#d1d1d1" /> - - + fill="red" + id="red" /> + height="100" + fill="green" + id="green" /> + + + width="100" + height="100" + fill="purple" + id="purple" /> - - + fill="white" + id="red-cover" /> + height="50" + fill="white" + id="green-cover" /> + + + width="50" + height="50" + fill="white" + id="purple-cover" /> diff --git a/testfiles/cli_tests/testcases/export-id_export-id-only_expected.svg b/testfiles/cli_tests/testcases/export-id_export-id-only_expected.svg index 138e29cf00..677f008cf9 100644 --- a/testfiles/cli_tests/testcases/export-id_export-id-only_expected.svg +++ b/testfiles/cli_tests/testcases/export-id_export-id-only_expected.svg @@ -1,47 +1,30 @@ - - - - image/svg+xml - - - - + version="1.1" + id="svg1" + sodipodi:docname="theta.svg" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + id="defs1" /> + id="namedview1" + pagecolor="#ffffff" + bordercolor="#000000" + borderopacity="0.25" + inkscape:showpageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#d1d1d1" /> + width="100" + height="100" + fill="blue" + id="blue" /> diff --git a/testfiles/cli_tests/testcases/export-id_export-id-only_export-area-drawing_expected.svg b/testfiles/cli_tests/testcases/export-id_export-id-only_export-area-drawing_expected.svg index d0cd1906ff..8d6c72f87a 100644 --- a/testfiles/cli_tests/testcases/export-id_export-id-only_export-area-drawing_expected.svg +++ b/testfiles/cli_tests/testcases/export-id_export-id-only_export-area-drawing_expected.svg @@ -1,47 +1,30 @@ - - - - image/svg+xml - - - - + version="1.1" + id="svg1" + sodipodi:docname="theta.svg" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + id="defs1" /> + id="namedview1" + pagecolor="#ffffff" + bordercolor="#000000" + borderopacity="0.25" + inkscape:showpageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#d1d1d1" /> + width="100" + height="100" + fill="yellow" + id="yellow" /> diff --git a/testfiles/cli_tests/testcases/export-png-color-mode-gray-8_expected.png b/testfiles/cli_tests/testcases/export-png-color-mode-gray-8_expected.png index c53d9932803b33a8b7f72cf0ff8b724daa7756e6..5fe0ad73058e9adde5a2f41de22d8a992cc01dfc 100644 GIT binary patch literal 6388 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 W{p14Ba# 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 Cx6LL1 literal 11485 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 Yy@b7zopr0NLuEJOBUy diff --git a/testfiles/cli_tests/testcases/export-png-color-mode-rgba-8_expected.png b/testfiles/cli_tests/testcases/export-png-color-mode-rgba-8_expected.png index 479838206ad85a080adce1fd3655f9aaf0ed1303..ed761204b78654a9414698760e69552ad3508671 100644 GIT binary patch delta 15309 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"one")); ASSERT_TRUE(one != nullptr); - // TODO: Fix when Inkscape preserves colour names (i.e. 'red') - EXPECT_EQ(one->style->fill.get_value(), Glib::ustring("#ff0000")); - EXPECT_EQ(one->style->stroke.get_value(), Glib::ustring("#008000")); + EXPECT_EQ(one->style->fill.get_value(), Glib::ustring("red")); + EXPECT_EQ(one->style->stroke.get_value(), Glib::ustring("green")); EXPECT_EQ(one->style->opacity.get_value(), Glib::ustring("0.5")); EXPECT_EQ(one->style->stroke_width.get_value(), Glib::ustring("2px")); @@ -77,7 +76,7 @@ TEST_F(ObjectTest, Styles) { ASSERT_TRUE(two != nullptr); EXPECT_EQ(two->style->fill.get_value(), Glib::ustring("#808080")); - EXPECT_EQ(two->style->stroke.get_value(), Glib::ustring("#008000")); + EXPECT_EQ(two->style->stroke.get_value(), Glib::ustring("green")); EXPECT_EQ(two->style->opacity.get_value(), Glib::ustring("0.5")); EXPECT_EQ(two->style->stroke_width.get_value(), Glib::ustring("4px")); @@ -93,7 +92,7 @@ TEST_F(ObjectTest, Styles) { ASSERT_TRUE(four != nullptr); EXPECT_EQ(four->style->fill.get_value(), Glib::ustring("#d0d0d0")); - EXPECT_EQ(four->style->stroke.get_value(), Glib::ustring("#ff0000")); + EXPECT_EQ(four->style->stroke.get_value(), Glib::ustring("red")); EXPECT_EQ(four->style->opacity.get_value(), Glib::ustring("0.5")); EXPECT_EQ(four->style->stroke_width.get_value(), Glib::ustring("2px")); } diff --git a/testfiles/src/oklab-color-test.cpp b/testfiles/src/oklab-color-test.cpp deleted file mode 100644 index f4cc949b40..0000000000 --- a/testfiles/src/oklab-color-test.cpp +++ /dev/null @@ -1,171 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/** @file Tests for the OKLab/OKLch color space backend. - */ -/* - * Authors: - * RafaƂ Siejakowski - * - * Copyright (C) 2022 Authors - * Released under GNU GPL v2+, read the file 'COPYING' for more information. - */ - -#include -#include - -#include "color.h" -#include "oklab.h" - -unsigned constexpr L=0, A=1, B=2; -double constexpr EPS = 1e-7; - -inline Oklab::Triplet random_triplet() -{ - return { g_random_double_range(0.0, 1.0), - g_random_double_range(0.0, 1.0), - g_random_double_range(0.0, 1.0) }; -} - -/** Test converting black and white to OKLab. */ -TEST(OklabColorTest, BlackWhite) -{ - using namespace Oklab; - - auto const black = linear_rgb_to_oklab({0, 0, 0}); - EXPECT_NEAR(black[L], 0.0, EPS); - EXPECT_NEAR(black[A], 0.0, EPS); - EXPECT_NEAR(black[B], 0.0, EPS); - - auto const white = linear_rgb_to_oklab({1.0, 1.0, 1.0}); - EXPECT_NEAR(white[L], 1.0, EPS); - EXPECT_NEAR(white[A], 0.0, EPS); - EXPECT_NEAR(white[B], 0.0, EPS); -} - -/** Test linear RGB -> OKLab -> linear RGB roundtrip. */ -TEST(OKlabColorTest, RGBRoundrtip) -{ - using namespace Oklab; - g_random_set_seed(13375336); // We always seed for tests' repeatability - - for (unsigned i = 0; i < 10'000; i++) { - Triplet rgb = random_triplet(); - auto const roundtrip = oklab_to_linear_rgb(linear_rgb_to_oklab(rgb)); - for (size_t i : {0, 1, 2}) { - EXPECT_NEAR(roundtrip[i], rgb[i], EPS); - } - } -} - -/** Test OKLab -> linear RGB -> OKLab roundtrip. */ -TEST(OKlabColorTest, OklabRoundrtip) -{ - using namespace Oklab; - g_random_set_seed(0xCAFECAFE); - - for (unsigned i = 0; i < 10'000; i++) { - Triplet lab = linear_rgb_to_oklab(random_triplet()); - auto const roundtrip = linear_rgb_to_oklab(oklab_to_linear_rgb(lab)); - for (size_t i : {0, 1, 2}) { - EXPECT_NEAR(roundtrip[i], lab[i], EPS); - } - } -} - -/** Test OKLab -> OKLch -> OKLab roundtrip. */ -TEST(OKlabColorTest, PolarRectRoundrtip) -{ - using namespace Oklab; - g_random_set_seed(0xB747A380); - - for (unsigned i = 0; i < 10'000; i++) { - Triplet lab = linear_rgb_to_oklab(random_triplet()); - auto const roundtrip = oklch_to_oklab(oklab_to_oklch(lab)); - for (size_t i : {1, 2}) { // No point testing [0] since L == L - EXPECT_NEAR(roundtrip[i], lab[i], EPS); - } - } -} - -/** Test OKLch -> OKLab -> OKLch roundtrip. */ -TEST(OKlabColorTest, RectPolarRoundrtip) -{ - using namespace Oklab; - g_random_set_seed(0xFA18B52); - - for (unsigned i = 0; i < 10'000; i++) { - Triplet lch = oklab_to_oklch(linear_rgb_to_oklab(random_triplet())); - auto const roundtrip = oklab_to_oklch(oklch_to_oklab(lch)); - for (size_t i : {1, 2}) { // No point testing [0] - EXPECT_NEAR(roundtrip[i], lch[i], EPS); - } - } -} - -/** Test maximum chroma calculations. */ -TEST(OKlabColorTest, Saturate) -{ - using namespace Oklab; - g_random_set_seed(0x987654); - - /** Test whether a number lies near to the endpoint of the unit interval. */ - auto const near_end = [](double x) -> bool { - return x > 0.999 || x < 0.0001; - }; - - for (unsigned i = 0; i < 10'000; i++) { - // Get a random l, h pair and compute the maximum chroma. - auto [l, _, h] = oklab_to_oklch(linear_rgb_to_oklab(random_triplet())); - auto const chromax = max_chroma(l, h); - - // Try maximally saturating the color and verifying that after converting - // the result to RGB we end up hitting the boundary of the sRGB gamut. - auto [r, g, b] = oklab_to_linear_rgb(oklch_to_oklab({l, chromax, h})); - EXPECT_TRUE(near_end(r) || near_end(g) || near_end(b)); - } -} - -/** Test OKHSL -> OKLab -> OKHSL conversion roundtrip. */ -TEST(OKlabColorTest, HSLabRoundtrip) -{ - using namespace Oklab; - g_random_set_seed(908070); - - for (unsigned i = 0; i < 10'000; i++) { - auto const hsl = random_triplet(); - if (hsl[1] < 0.001) { - // Grayscale colors don't have unique hues, - // so we skip them (mapping is not bijective). - continue; - } - auto const roundtrip = oklab_to_okhsl(okhsl_to_oklab(hsl)); - for (size_t i : {0, 1, 2}) { - EXPECT_NEAR(roundtrip[i], hsl[i], EPS); - } - } -} - -/** Test OKLab -> OKHSL -> OKLab conversion roundtrip. */ -TEST(OKlabColorTest, LabHSLRoundtrip) -{ - using namespace Oklab; - g_random_set_seed(5043071); - - for (unsigned i = 0; i < 10'000; i++) { - auto const lab = linear_rgb_to_oklab(random_triplet()); - auto const roundtrip = okhsl_to_oklab(oklab_to_okhsl(lab)); - for (size_t i : {0, 1, 2}) { - EXPECT_NEAR(roundtrip[i], lab[i], EPS); - } - } -} - -/* - Local Variables: - mode:c++ - c-file-style:"stroustrup" - c-file-offsets:((innamespace . 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 : \ No newline at end of file diff --git a/testfiles/src/style-elem-test.cpp b/testfiles/src/style-elem-test.cpp index 83dfde6b7d..5da4f7c629 100644 --- a/testfiles/src/style-elem-test.cpp +++ b/testfiles/src/style-elem-test.cpp @@ -58,13 +58,13 @@ TEST_F(ObjectTest, StyleElems) { ASSERT_TRUE(one); for (auto &style : one->get_styles()) { - EXPECT_EQ(style->fill.get_value(), Glib::ustring("#ff0000")); + EXPECT_EQ(style->fill.get_value(), Glib::ustring("red")); } auto two = cast(doc->getObjectById("style02")); ASSERT_TRUE(one); for (auto &style : two->get_styles()) { - EXPECT_EQ(style->fill.get_value(), Glib::ustring("#008000")); + EXPECT_EQ(style->fill.get_value(), Glib::ustring("green")); } } diff --git a/testfiles/src/style-internal-test.cpp b/testfiles/src/style-internal-test.cpp index 6312bb4509..6d539f193b 100644 --- a/testfiles/src/style-internal-test.cpp +++ b/testfiles/src/style-internal-test.cpp @@ -70,6 +70,16 @@ TEST(StyleInternalTest, testSPIDashArrayValidity) // EXPECT_FALSE(array23.is_valid()); // negative total: invalid and removed by 'read' } +TEST(StyleInternalTest, testSPIPaint) +{ + SPIPaint paint; + EXPECT_EQ(paint.get_value(), ""); + paint.read("red"); + EXPECT_EQ(paint.get_value(), "red"); + paint.clear(); + EXPECT_EQ(paint.get_value(), ""); +} + /* Local Variables: mode:c++ diff --git a/testfiles/src/style-test.cpp b/testfiles/src/style-test.cpp index 886d837cb3..1e1683651d 100644 --- a/testfiles/src/style-test.cpp +++ b/testfiles/src/style-test.cpp @@ -85,7 +85,7 @@ std::vector getStyleData() StyleRead("overflow:visible"), // SPIEnum StyleRead("overflow:auto"), // SPIEnum - StyleRead("color:#ff0000"), StyleRead("color:blue", "color:#0000ff"), + StyleRead("color:#ff0000"), StyleRead("color:blue", "color:blue"), // StyleRead("color:currentColor"), SVG 1.1 does not allow color value 'currentColor' // Font shorthand @@ -343,7 +343,7 @@ std::vector getStyleMatchData() // SPIColor StyleMatch("color:blue", "color:blue", true ), StyleMatch("color:blue", "color:red", false), - StyleMatch("color:red", "color:#ff0000", true ), + StyleMatch("color:red", "color:#ff0000", false), // SPIPaint StyleMatch("fill:blue", "fill:blue", true ), diff --git a/testfiles/src/svg-color-test.cpp b/testfiles/src/svg-color-test.cpp deleted file mode 100644 index c4e937928d..0000000000 --- a/testfiles/src/svg-color-test.cpp +++ /dev/null @@ -1,112 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/** @file - * Test for SVG colors - *//* - * Authors: see git history - * - * Copyright (C) 2010 Authors - * Released under GNU GPL v2+, read the file 'COPYING' for more information. - */ -#include "svg/svg-color.h" - -#include -#include - -#include "preferences.h" -#include "svg/svg-icc-color.h" - -static void check_rgb24(unsigned const rgb24) -{ - Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - char css[8]; - - prefs->setBool("/options/svgoutput/usenamedcolors", false); - sp_svg_write_color(css, sizeof(css), rgb24 << 8); - ASSERT_EQ(sp_svg_read_color(css, 0xff), rgb24 << 8); - - prefs->setBool("/options/svgoutput/usenamedcolors", true); - sp_svg_write_color(css, sizeof(css), rgb24 << 8); - ASSERT_EQ(sp_svg_read_color(css, 0xff), rgb24 << 8); -} - -TEST(SvgColorTest, testWrite) -{ - unsigned const components[] = {0, 0x80, 0xff, 0xc0, 0x77}; - unsigned const nc = G_N_ELEMENTS(components); - for (unsigned i = nc * nc * nc; i--;) { - unsigned tmp = i; - unsigned rgb24 = 0; - for (unsigned c = 0; c < 3; ++c) { - unsigned const component = components[tmp % nc]; - rgb24 = (rgb24 << 8) | component; - tmp /= nc; - } - ASSERT_TRUE(tmp == 0); - check_rgb24(rgb24); - } - - /* And a few completely random ones. */ - for (unsigned i = 500; i--;) { /* Arbitrary number of iterations. */ - unsigned const rgb24 = (std::rand() >> 4) & 0xffffff; - check_rgb24(rgb24); - } -} - -TEST(SvgColorTest, testReadColor) -{ - gchar const *val[] = {"#f0f", "#ff00ff", "rgb(255,0,255)", "fuchsia"}; - size_t const n = sizeof(val) / sizeof(*val); - for (size_t i = 0; i < n; i++) { - gchar const *end = 0; - guint32 result = sp_svg_read_color(val[i], &end, 0x3); - ASSERT_EQ(result, 0xff00ff00); - ASSERT_LT(val[i], end); - } -} - -TEST(SvgColorTest, testIccColor) -{ - struct - { - unsigned numEntries; - bool shouldPass; - char const *name; - char const *str; - } cases[] = { - {1, true, "named", "icc-color(named, 3)"}, - {0, false, "", "foodle"}, - {1, true, "a", "icc-color(a, 3)"}, - {4, true, "named", "icc-color(named, 3, 0, 0.1, 2.5)"}, - {0, false, "", "icc-color(named, 3"}, - {0, false, "", "icc-color(space named, 3)"}, - {0, false, "", "icc-color(tab\tnamed, 3)"}, - {0, false, "", "icc-color(0name, 3)"}, - {0, false, "", "icc-color(-name, 3)"}, - {1, true, "positive", "icc-color(positive, +3)"}, - {1, true, "negative", "icc-color(negative, -3)"}, - {1, true, "positive", "icc-color(positive, +0.1)"}, - {1, true, "negative", "icc-color(negative, -0.1)"}, - {0, false, "", "icc-color(named, value)"}, - {1, true, "hyphen-name", "icc-color(hyphen-name, 1)"}, - {1, true, "under_name", "icc-color(under_name, 1)"}, - }; - - for (size_t i = 0; i < G_N_ELEMENTS(cases); i++) { - SVGICCColor tmp; - char const *str = cases[i].str; - char const *result = nullptr; - - bool parseRet = sp_svg_read_icc_color(str, &result, &tmp); - ASSERT_EQ(parseRet, cases[i].shouldPass) << str; - ASSERT_EQ(tmp.colors.size(), cases[i].numEntries) << str; - if (cases[i].shouldPass) { - ASSERT_STRNE(str, result); - ASSERT_EQ(tmp.colorProfile, cases[i].name) << str; - } else { - ASSERT_STREQ(str, result); - ASSERT_TRUE(tmp.colorProfile.empty()); - } - } -} - -// vim: filetype=cpp:expandtab:shiftwidth=4:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/testfiles/src/ui-util-test.cpp b/testfiles/src/ui-util-test.cpp new file mode 100644 index 0000000000..1cd95a2baf --- /dev/null +++ b/testfiles/src/ui-util-test.cpp @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Test utilities from src/ui/ + */ +/* + * Authors: + * Martin Owens + * + * Copyright (C) 2024 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include "test-utils.h" + +#include "ui/widget/canvas/util.h" + +using namespace Inkscape::UI::Widget; + +::testing::AssertionResult Array3IsNear(std::array const &A, std::vector const &B, double epsilon) +{ + std::vector av; + for (auto v : A) { + av.emplace_back((double)v); + } + return VectorIsNear(av, B, epsilon); +} + +TEST(UtilTest, CheckerboardDarken) +{ + EXPECT_TRUE(Array3IsNear(checkerboard_darken(0x00000000), {0.08, 0.08, 0.08}, 0.01)); + EXPECT_TRUE(Array3IsNear(checkerboard_darken(0x00000080), {0.0398, 0.0398, 0.0398}, 0.01)); + EXPECT_TRUE(Array3IsNear(checkerboard_darken(0x000000ff), {0, 0, 0}, 0.01)); + EXPECT_TRUE(Array3IsNear(checkerboard_darken(0x00000080), {0.0398, 0.0398, 0.0398}, 0.01)); + EXPECT_TRUE(Array3IsNear(checkerboard_darken(0xffffff00), {0.92, 0.92, 0.92}, 0.01)); + EXPECT_TRUE(Array3IsNear(checkerboard_darken(0xffffffff), {1, 1, 1}, 0.01)); + EXPECT_TRUE(Array3IsNear(checkerboard_darken(0x80808000), {0.422, 0.422, 0.422}, 0.01)); + EXPECT_TRUE(Array3IsNear(checkerboard_darken(0x80808080), {0.462, 0.462, 0.462}, 0.01)); + EXPECT_TRUE(Array3IsNear(checkerboard_darken(0x808080ff), {0.502, 0.502, 0.502}, 0.01)); +} + +// vim: filetype=cpp:expandtab:shiftwidth=4:softtabstop=4:fileencoding=utf-8:textwidth=99 : -- GitLab