From 135ee64867795370164c0848abe1e29a8303409f Mon Sep 17 00:00:00 2001 From: Martin Owens Date: Tue, 12 Sep 2023 08:41:51 -0400 Subject: [PATCH 1/3] Redesign CMS User Experence and Fix bugs This redesigns how the CMS for soft proofing and monitor profile works by removing and simplifying the preferences, adding new toggle actions for switching each on without effecting the others and allowing the CMS preferences to be loaded from the new display UI. --- .../{color-management.svg => color-cms.svg} | 0 .../scalable/actions/color-colorproof.svg | 2 + .../scalable/actions/color-gamutwarn.svg | 2 + .../{color-management.svg => color-cms.svg} | 0 .../scalable/actions/color-colorproof.svg | 2 + .../scalable/actions/color-gamutwarn.svg | 2 + .../actions/cms-color-ink-symbolic.svg | 23 + .../actions/cms-color-rgb-symbolic.svg | 23 + .../symbolic/actions/color-cms-symbolic.svg} | 0 .../actions/color-colorproof-symbolic.svg | 2 + .../actions/color-displayprofile-symbolic.svg | 2 + .../actions/color-gamutwarn-symbolic.svg | 2 + .../actions/cms-color-ink-symbolic.svg | 13 + .../actions/cms-color-rgb-symbolic.svg | 23 + .../symbolic/actions/color-cms-symbolic.svg} | 15 +- .../actions/color-colorproof-symbolic.svg | 2 + .../actions/color-displayprofile-symbolic.svg | 2 + .../actions/color-gamutwarn-symbolic.svg | 2 + share/ui/cms-popover.glade | 436 ++++++++ share/ui/display-popup.glade | 70 +- share/ui/document-properties.glade | 306 ++++++ share/ui/menus.ui | 33 +- src/CMakeLists.txt | 3 - src/actions/actions-canvas-mode.cpp | 173 ++-- src/actions/actions-canvas-mode.h | 3 + src/actions/actions-helper.cpp | 47 + src/actions/actions-helper.h | 10 + src/actions/actions-tools.cpp | 17 +- src/attributes.cpp | 1 + src/attributes.h | 1 + src/color.cpp | 2 +- src/color/CMakeLists.txt | 8 +- src/color/cms-system.cpp | 239 ++--- src/color/cms-system.h | 18 +- src/color/cms-util.cpp | 3 + src/color/cms-util.h | 2 + src/color/components.cpp | 76 ++ src/color/components.h | 38 + src/color/manager.cpp | 207 ++++ src/color/manager.h | 75 ++ src/colorspace.h | 51 - src/desktop.cpp | 25 + src/desktop.h | 6 + src/display/drawing-item.cpp | 28 +- src/display/drawing.cpp | 12 + src/display/drawing.h | 9 + src/display/rendermode.h | 3 +- src/document.cpp | 7 +- src/document.h | 8 +- .../internal/pdfinput/svg-builder.cpp | 3 +- src/inkscape-window.cpp | 19 + src/inkscape-window.h | 4 + src/object/color-profile.cpp | 221 +++-- src/object/color-profile.h | 70 +- src/object/sp-image.cpp | 45 +- src/object/sp-object.cpp | 8 + src/object/sp-object.h | 1 + src/preferences-skeleton.h | 5 +- src/profile-manager.cpp | 102 -- src/profile-manager.h | 55 - src/svg/svg-color.cpp | 8 +- src/ui/CMakeLists.txt | 4 +- src/ui/dialog/dialog-base.h | 5 + src/ui/dialog/dialog-container.cpp | 14 + src/ui/dialog/dialog-container.h | 1 + src/ui/dialog/document-properties.cpp | 382 +++---- src/ui/dialog/document-properties.h | 51 +- src/ui/dialog/inkscape-preferences.cpp | 89 +- src/ui/dialog/inkscape-preferences.h | 16 +- src/ui/icon-loader.cpp | 7 + src/ui/widget/canvas-grid.cpp | 31 +- src/ui/widget/canvas-grid.h | 5 +- src/ui/widget/canvas.cpp | 56 +- src/ui/widget/canvas.h | 10 +- src/ui/widget/canvas/prefs.h | 2 - src/ui/widget/cms-popover.cpp | 132 +++ src/ui/widget/cms-popover.h | 68 ++ src/ui/widget/color-icc-selector.cpp | 938 ------------------ src/ui/widget/color-icc-selector.h | 72 -- src/ui/widget/color-notebook.cpp | 6 +- src/ui/widget/color-scales.cpp | 6 +- src/ui/widget/color-scales.h | 3 +- src/ui/widget/color-slider.cpp | 4 + src/ui/widget/desktop-widget.cpp | 32 +- 84 files changed, 2310 insertions(+), 2199 deletions(-) rename share/icons/Tango/scalable/actions/{color-management.svg => color-cms.svg} (100%) create mode 100644 share/icons/Tango/scalable/actions/color-colorproof.svg create mode 100644 share/icons/Tango/scalable/actions/color-gamutwarn.svg rename share/icons/hicolor/scalable/actions/{color-management.svg => color-cms.svg} (100%) create mode 100644 share/icons/hicolor/scalable/actions/color-colorproof.svg create mode 100644 share/icons/hicolor/scalable/actions/color-gamutwarn.svg create mode 100644 share/icons/hicolor/symbolic/actions/cms-color-ink-symbolic.svg create mode 100644 share/icons/hicolor/symbolic/actions/cms-color-rgb-symbolic.svg rename share/icons/{multicolor/symbolic/actions/color-management-symbolic.svg => hicolor/symbolic/actions/color-cms-symbolic.svg} (100%) create mode 100644 share/icons/hicolor/symbolic/actions/color-colorproof-symbolic.svg create mode 100644 share/icons/hicolor/symbolic/actions/color-displayprofile-symbolic.svg create mode 100644 share/icons/hicolor/symbolic/actions/color-gamutwarn-symbolic.svg create mode 100644 share/icons/multicolor/symbolic/actions/cms-color-ink-symbolic.svg create mode 100644 share/icons/multicolor/symbolic/actions/cms-color-rgb-symbolic.svg rename share/icons/{hicolor/symbolic/actions/color-management-symbolic.svg => multicolor/symbolic/actions/color-cms-symbolic.svg} (93%) create mode 100644 share/icons/multicolor/symbolic/actions/color-colorproof-symbolic.svg create mode 100644 share/icons/multicolor/symbolic/actions/color-displayprofile-symbolic.svg create mode 100644 share/icons/multicolor/symbolic/actions/color-gamutwarn-symbolic.svg create mode 100644 share/ui/cms-popover.glade create mode 100644 share/ui/document-properties.glade create mode 100644 src/color/components.cpp create mode 100644 src/color/components.h create mode 100644 src/color/manager.cpp create mode 100644 src/color/manager.h delete mode 100644 src/colorspace.h delete mode 100644 src/profile-manager.cpp delete mode 100644 src/profile-manager.h create mode 100644 src/ui/widget/cms-popover.cpp create mode 100644 src/ui/widget/cms-popover.h delete mode 100644 src/ui/widget/color-icc-selector.cpp delete mode 100644 src/ui/widget/color-icc-selector.h diff --git a/share/icons/Tango/scalable/actions/color-management.svg b/share/icons/Tango/scalable/actions/color-cms.svg similarity index 100% rename from share/icons/Tango/scalable/actions/color-management.svg rename to share/icons/Tango/scalable/actions/color-cms.svg diff --git a/share/icons/Tango/scalable/actions/color-colorproof.svg b/share/icons/Tango/scalable/actions/color-colorproof.svg new file mode 100644 index 0000000000..68f016f58d --- /dev/null +++ b/share/icons/Tango/scalable/actions/color-colorproof.svg @@ -0,0 +1,2 @@ + +image/svg+xml diff --git a/share/icons/Tango/scalable/actions/color-gamutwarn.svg b/share/icons/Tango/scalable/actions/color-gamutwarn.svg new file mode 100644 index 0000000000..de5ccabdef --- /dev/null +++ b/share/icons/Tango/scalable/actions/color-gamutwarn.svg @@ -0,0 +1,2 @@ + +image/svg+xml! diff --git a/share/icons/hicolor/scalable/actions/color-management.svg b/share/icons/hicolor/scalable/actions/color-cms.svg similarity index 100% rename from share/icons/hicolor/scalable/actions/color-management.svg rename to share/icons/hicolor/scalable/actions/color-cms.svg diff --git a/share/icons/hicolor/scalable/actions/color-colorproof.svg b/share/icons/hicolor/scalable/actions/color-colorproof.svg new file mode 100644 index 0000000000..68f016f58d --- /dev/null +++ b/share/icons/hicolor/scalable/actions/color-colorproof.svg @@ -0,0 +1,2 @@ + +image/svg+xml diff --git a/share/icons/hicolor/scalable/actions/color-gamutwarn.svg b/share/icons/hicolor/scalable/actions/color-gamutwarn.svg new file mode 100644 index 0000000000..de5ccabdef --- /dev/null +++ b/share/icons/hicolor/scalable/actions/color-gamutwarn.svg @@ -0,0 +1,2 @@ + +image/svg+xml! diff --git a/share/icons/hicolor/symbolic/actions/cms-color-ink-symbolic.svg b/share/icons/hicolor/symbolic/actions/cms-color-ink-symbolic.svg new file mode 100644 index 0000000000..6caecaa4a4 --- /dev/null +++ b/share/icons/hicolor/symbolic/actions/cms-color-ink-symbolic.svg @@ -0,0 +1,23 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/share/icons/hicolor/symbolic/actions/cms-color-rgb-symbolic.svg b/share/icons/hicolor/symbolic/actions/cms-color-rgb-symbolic.svg new file mode 100644 index 0000000000..db4631b768 --- /dev/null +++ b/share/icons/hicolor/symbolic/actions/cms-color-rgb-symbolic.svg @@ -0,0 +1,23 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/share/icons/multicolor/symbolic/actions/color-management-symbolic.svg b/share/icons/hicolor/symbolic/actions/color-cms-symbolic.svg similarity index 100% rename from share/icons/multicolor/symbolic/actions/color-management-symbolic.svg rename to share/icons/hicolor/symbolic/actions/color-cms-symbolic.svg diff --git a/share/icons/hicolor/symbolic/actions/color-colorproof-symbolic.svg b/share/icons/hicolor/symbolic/actions/color-colorproof-symbolic.svg new file mode 100644 index 0000000000..68f016f58d --- /dev/null +++ b/share/icons/hicolor/symbolic/actions/color-colorproof-symbolic.svg @@ -0,0 +1,2 @@ + +image/svg+xml diff --git a/share/icons/hicolor/symbolic/actions/color-displayprofile-symbolic.svg b/share/icons/hicolor/symbolic/actions/color-displayprofile-symbolic.svg new file mode 100644 index 0000000000..1b2eccaf29 --- /dev/null +++ b/share/icons/hicolor/symbolic/actions/color-displayprofile-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/share/icons/hicolor/symbolic/actions/color-gamutwarn-symbolic.svg b/share/icons/hicolor/symbolic/actions/color-gamutwarn-symbolic.svg new file mode 100644 index 0000000000..de5ccabdef --- /dev/null +++ b/share/icons/hicolor/symbolic/actions/color-gamutwarn-symbolic.svg @@ -0,0 +1,2 @@ + +image/svg+xml! diff --git a/share/icons/multicolor/symbolic/actions/cms-color-ink-symbolic.svg b/share/icons/multicolor/symbolic/actions/cms-color-ink-symbolic.svg new file mode 100644 index 0000000000..da39fe4847 --- /dev/null +++ b/share/icons/multicolor/symbolic/actions/cms-color-ink-symbolic.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/share/icons/multicolor/symbolic/actions/cms-color-rgb-symbolic.svg b/share/icons/multicolor/symbolic/actions/cms-color-rgb-symbolic.svg new file mode 100644 index 0000000000..db4631b768 --- /dev/null +++ b/share/icons/multicolor/symbolic/actions/cms-color-rgb-symbolic.svg @@ -0,0 +1,23 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/share/icons/hicolor/symbolic/actions/color-management-symbolic.svg b/share/icons/multicolor/symbolic/actions/color-cms-symbolic.svg similarity index 93% rename from share/icons/hicolor/symbolic/actions/color-management-symbolic.svg rename to share/icons/multicolor/symbolic/actions/color-cms-symbolic.svg index 74ef23e2f5..37027abbcf 100644 --- a/share/icons/hicolor/symbolic/actions/color-management-symbolic.svg +++ b/share/icons/multicolor/symbolic/actions/color-cms-symbolic.svg @@ -14,7 +14,15 @@ width="16" id="svg1" version="1.1"> - + + + @@ -30,6 +38,7 @@ + diff --git a/share/icons/multicolor/symbolic/actions/color-displayprofile-symbolic.svg b/share/icons/multicolor/symbolic/actions/color-displayprofile-symbolic.svg new file mode 100644 index 0000000000..bc44a21462 --- /dev/null +++ b/share/icons/multicolor/symbolic/actions/color-displayprofile-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/share/icons/multicolor/symbolic/actions/color-gamutwarn-symbolic.svg b/share/icons/multicolor/symbolic/actions/color-gamutwarn-symbolic.svg new file mode 100644 index 0000000000..ac7ff65c7e --- /dev/null +++ b/share/icons/multicolor/symbolic/actions/color-gamutwarn-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/share/ui/cms-popover.glade b/share/ui/cms-popover.glade new file mode 100644 index 0000000000..4ad7a3ff6e --- /dev/null +++ b/share/ui/cms-popover.glade @@ -0,0 +1,436 @@ + + + + + + True + False + cms-color-ink + + + True + False + cms-color-rgb + + + True + False + cms-color-ink + + + True + False + cms-color-rgb + + + True + False + cms-color-ink + + + True + False + cms-color-rgb + + + True + False + cms-color-ink + + + True + False + cms-color-ink + + + True + False + color-colorproof + + + True + False + color-displayprofile + + + True + False + document-properties + + + True + False + color-gamutwarn + + + True + False + gear + + + False + True + left + none + + + + True + False + start + start + + + True + False + center + 3 + 3 + + + 0 + 5 + 2 + + + + + True + True + False + True + center + center + 10 + 10 + 10 + 5 + win.canvas-color-prefs + img-prefs + none + + + 1 + 0 + + + + + True + False + center + 5 + 5 + 5 + + + True + True + True + center + win.canvas-color-displayprofile + img-displayprofile + + + False + True + 0 + + + + + True + True + True + win.canvas-color-gamutwarn + img-gamutwarn + + + False + True + 1 + + + + + True + True + True + win.canvas-color-colorproof + img-colorproof + + + False + True + 2 + + + + + 0 + 9 + 2 + + + + + True + True + False + True + center + center + 10 + 10 + 10 + 5 + win.canvas-color-props + img-doc-prefs + none + + + 0 + 0 + + + + + True + False + center + 3 + 3 + + + 0 + 1 + 2 + + + + + True + False + Standard Red/Green/Blue screen based color space + 12 + 12 + 5 + 5 + sRGB + middle + 0.5 + 0.5 + + + + + + + 0 + 2 + 2 + + + + + True + False + center + 5 + 5 + + + True + True + True + center + img-cms-color-red + none + + + False + True + 0 + + + + + True + True + True + center + img-cms-color-green + none + + + False + True + 1 + + + + + True + True + True + center + img-cms-color-blue + none + + + False + True + 2 + + + + + 0 + 3 + 2 + + + + + False + center + 5 + 5 + True + + + True + True + True + center + img-cms-color-cyan + none + + + False + True + 0 + + + + + True + True + True + center + img-cms-color-magenta + none + + + False + True + 1 + + + + + True + True + True + center + img-cms-color-yellow + none + + + False + True + 2 + + + + + True + True + True + center + img-cms-color-black + none + + + False + True + 3 + + + + + 0 + 4 + 2 + + + + + colors-spots-sep + False + center + 3 + 3 + + + 0 + 8 + 2 + + + + + False + center + 5 + 5 + True + + + True + True + True + center + img-cms-color-spot + none + + + False + True + 0 + + + + + 0 + 7 + 2 + + + + + False + Standard Red/Green/Blue screen based color space + 12 + 12 + 5 + 5 + Spot Colors + middle + 0.5 + 0.5 + + + + + + + 0 + 6 + 2 + + + + + + diff --git a/share/ui/display-popup.glade b/share/ui/display-popup.glade index 2cc7f1c6c7..523c4e73fd 100644 --- a/share/ui/display-popup.glade +++ b/share/ui/display-popup.glade @@ -16,19 +16,6 @@ 8 8 8 - - - True - start - 5 - Display mode: - - - 0 - 0 - 3 - - @@ -123,7 +110,7 @@ 0 - 8 + 7 @@ -134,7 +121,7 @@ 0 - 9 + 8 @@ -145,7 +132,7 @@ 0 - 7 + 6 3 @@ -157,7 +144,7 @@ 0 - 11 + 9 3 @@ -171,7 +158,7 @@ 0 - 12 + 10 3 @@ -192,7 +179,7 @@ 1 - 8 + 7 @@ -212,52 +199,21 @@ 1 - 9 + 8 - + True - start + False 5 - 5 - - - True - True - True - Toggle between normal and color managed modes - win.canvas-color-manage - - - True - color-management-symbolic - - - - - - - True - True - True - Toggle between normal and grayscale modes - win.canvas-color-mode - - - True - grayscale-mode-symbolic - - - - - 1 - - + 5 + Display Mode: + 0 0 - 6 + 0 3 diff --git a/share/ui/document-properties.glade b/share/ui/document-properties.glade new file mode 100644 index 0000000000..d0f9a5e0bd --- /dev/null +++ b/share/ui/document-properties.glade @@ -0,0 +1,306 @@ + + + + + + + + + + + + Standard SVG (sRGB) + + + + + + + + + + + + + + + + + + + + + + + Automatic + auto + + + Perceptual + perceptual + + + Saturation + saturation + + + Relative Colorimetric + relative-colorimetric + + + Relative Colorimetric with no BPC + relative-colorimetric-nobpc + + + Absolute Colorimetric + absolute-colorimetric + + + + + 500 + True + False + 10 + 10 + 5 + vertical + + + True + False + Color Managed Profile + 0 + + + + + + False + True + 0 + + + + + True + False + 10 + 10 + Color management in Inkscape is handled by the use of Color Profiles (icc) which define how colors are transformed from the SVG standard color working space, which is always sRGB, into alternate color spaces used for printing and other output. + +Please see the Inkscape website cms learn page for more information: + True + 0 + + + False + True + 1 + + + + + https://inkscape.org/learn/cms + True + True + True + none + https://inkscape.org/learn/cms + + + False + True + 2 + + + + + True + False + Target Color Profile + 0 + + + + + + False + True + 3 + + + + + True + False + 10 + 10 + This target profile will change how Inkscape's color pickers work, settings colors with both the RGB value and the color profile's colors. Usually CMYK. It will also be used by color managed display mode for color proofing for printing. + True + 0 + + + False + True + 4 + + + + + True + False + + + True + False + color-profiles-store + 0 + 0 + + + + 0 + + + + + True + True + 0 + + + + + True + False + rendering-intent-store + 1 + + + + 0 + + + + + False + True + 1 + + + + + True + True + True + 5 + + + True + False + document-open + + + + + False + True + 2 + + + + + False + True + 5 + + + + + Convert all Existing Colors ... + True + True + True + end + 20 + 20 + 10 + 10 + + + False + True + 6 + + + + + True + False + Other Color Profiles + 0 + + + + + + False + True + 7 + + + + + True + True + 5 + 5 + 5 + 10 + in + + + True + True + other-profiles-store + False + 0 + False + 2 + + + + + + Name + + + + 0 + + + + + + + Rendering Intent + + + + 1 + + + + + + + + + True + True + 8 + + + + diff --git a/share/ui/menus.ui b/share/ui/menus.ui index 5822c5ef52..80755cba66 100644 --- a/share/ui/menus.ui +++ b/share/ui/menus.ui @@ -527,14 +527,31 @@
- - Gray Scale - win.canvas-color-mode - - - Color Management - win.canvas-color-manage - + + _Color Mode +
+ + _Gray Scale + win.canvas-color-mode + + + _Proof Colors + win.canvas-color-softproof + +
+
+ + _Display Profile + win.canvas-color-displayprofile + +
+
+ + _Setup + win.canvas-color-prefs + +
+
Page _Grid diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1a735b3643..bb1a37270b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -57,7 +57,6 @@ set(inkscape_SRC perspective-line.cpp preferences.cpp print.cpp - profile-manager.cpp proj_pt.cpp pure-transform.cpp rdf.cpp @@ -97,7 +96,6 @@ set(inkscape_SRC color-rgba.h hsluv.h color.h - colorspace.h composite-undo-stack-observer.h conditions.h conn-avoid-ref.h @@ -151,7 +149,6 @@ set(inkscape_SRC preferences-skeleton.h preferences.h print.h - profile-manager.h proj_pt.h pure-transform.h rdf.h diff --git a/src/actions/actions-canvas-mode.cpp b/src/actions/actions-canvas-mode.cpp index 5a37abad3b..8941d04892 100644 --- a/src/actions/actions-canvas-mode.cpp +++ b/src/actions/actions-canvas-mode.cpp @@ -19,7 +19,9 @@ #include "actions-helper.h" +#include "color/manager.h" #include "desktop.h" +#include "document.h" #include "inkscape-application.h" #include "inkscape-window.h" @@ -27,7 +29,10 @@ #include "display/drawing.h" // Setting gray scale parameters. #include "display/control/canvas-item-drawing.h" +#include "ui/dialog/dialog-container.h" +#include "ui/dialog/inkscape-preferences.h" #include "ui/widget/canvas.h" +#include "document-undo.h" // TODO: Use action state rather than set variable in Canvas (via Desktop). // TODO: Move functions from Desktop to Canvas. @@ -199,67 +204,91 @@ canvas_color_mode_gray(InkscapeWindow *win) * Toggle Gray scale on/off. */ void -canvas_color_mode_toggle(InkscapeWindow *win) +canvas_color_mode_toggle(bool state, InkscapeWindow *win) { - auto action = win->lookup_action("canvas-color-mode"); - if (!action) { - show_output("canvas_color_mode_toggle: action missing!"); - return; - } - - auto saction = Glib::RefPtr::cast_dynamic(action); - if (!saction) { - show_output("canvas_color_mode_toggle: action not SimpleAction!"); - return; - } - - bool state = false; - saction->get_state(state); - state = !state; - saction->change_state(state); - if (state) { // Set gray scale parameters. canvas_color_mode_gray(win); } + auto canvas = win->get_desktop()->getCanvas(); + canvas->set_color_mode(state ? Inkscape::ColorMode::GRAYSCALE : Inkscape::ColorMode::NORMAL); +} +void +canvas_color_prefs(InkscapeWindow *win) +{ SPDesktop* dt = win->get_desktop(); - auto canvas = dt->getCanvas(); - canvas->set_color_mode(state ? Inkscape::ColorMode::GRAYSCALE : Inkscape::ColorMode::NORMAL); + dt->getContainer()->get_dialog_page("Preferences", PREFS_PAGE_IO_CMS); } +void +canvas_color_props(InkscapeWindow *win) +{ + SPDesktop* dt = win->get_desktop(); + dt->getContainer()->get_dialog_page("DocumentProperties", 3); +} + +void +canvas_color_gamutwarn(bool state, InkscapeWindow *win) +{ + auto canvas = win->get_desktop()->getCanvas(); + canvas->set_color_mode(state ? Inkscape::ColorMode::GAMUTWARN : Inkscape::ColorMode::COLORPROOF); +} /** - * Toggle Color management on/off. + * Keep the gamut warning up to date, based on the color proof setting */ -void -canvas_color_manage_toggle(InkscapeWindow *win) +void _update_actions_canvas_mode(InkscapeWindow *win, bool is_colorproof) { - auto action = win->lookup_action("canvas-color-manage"); - if (!action) { - show_output("canvas_color_manage_toggle: action missing!"); - return; + if (auto action = Glib::RefPtr::cast_dynamic(win->lookup_action("canvas-color-colorproof"))) { + action->change_state(is_colorproof); + auto desktop = win->get_desktop(); + desktop->getCanvas()->set_color_mode(is_colorproof ? Inkscape::ColorMode::COLORPROOF : Inkscape::ColorMode::NORMAL); } - - auto saction = Glib::RefPtr::cast_dynamic(action); - if (!saction) { - show_output("canvas_color_manage_toggle: action not SimpleAction!"); - return; + if (auto action = Glib::RefPtr::cast_dynamic(win->lookup_action("canvas-color-gamutwarn"))) { + bool active = false; + action->get_state(active); + if (!is_colorproof && active) { + canvas_color_gamutwarn(false, win); + action->change_state(false); + } + action->set_enabled(is_colorproof); } +} - bool state = false; - saction->get_state(state); - state = !state; - saction->change_state(state); - - // Save value as a preference - Inkscape::Preferences *pref = Inkscape::Preferences::get(); - pref->setBool("/options/displayprofile/enable", state); +void +canvas_color_colorproof(bool state, InkscapeWindow *win) +{ + auto prefs = Inkscape::Preferences::get(); + auto desktop = win->get_desktop(); + auto &cm = desktop->getDocument()->getColorManager(); + + // Ensure we have a cmyk profile if enabling for the first time + if (state && !cm.getDefault()) { + auto default_uri = prefs->getString("/options/cms/uri"); + auto default_intent = prefs->getInt("/options/cms/intent"); + if (!default_uri.empty()) { + // Preferences contain a default cms profile, use that + if (auto profile = cm.addProfile(default_uri, (Inkscape::RenderingIntent)(default_intent+2))) { + cm.setDefault(profile); + Inkscape::DocumentUndo::done(profile->document, _("Auto add default color profile"), ""); + // TODO: Convert all colors to the new profile, but ASK first! + // cm.convertToCMYK(profile); + } else { + g_warning("Couldnt set the default cms profile: '%s'", default_uri.c_str()); + } + } else { + // No default, so we're opening the document properties instead + if (auto action = Glib::RefPtr::cast_dynamic(win->lookup_action("canvas-color-colorproof"))) { + action->change_state(false); + } + canvas_color_props(win); + return; + } + } - SPDesktop* dt = win->get_desktop(); - auto canvas = dt->getCanvas(); - canvas->set_cms_active(state); - canvas->redraw_all(); + // Cascade softproofing option to the gamut warning + _update_actions_canvas_mode(win, state); } std::vector> raw_data_canvas_mode = @@ -270,45 +299,37 @@ std::vector> raw_data_canvas_mode = {"win.canvas-display-mode(2)", N_("Display Mode: No Filters"), "Canvas Display", N_("Do not render filters (for speed)") }, {"win.canvas-display-mode(3)", N_("Display Mode: Enhance Thin Lines"), "Canvas Display", N_("Ensure all strokes are displayed on screen as at least 1 pixel wide")}, {"win.canvas-display-mode(4)", N_("Display Mode: Outline Overlay"), "Canvas Display", N_("Show objects as outlines, and the actual drawing below them with reduced opacity")}, + {"win.canvas-display-mode(5)", N_("Display Mode: Grayscale"), "Canvas Display", N_("Show everything in a grayscale mode") }, {"win.canvas-display-mode-cycle", N_("Display Mode: Cycle"), "Canvas Display", N_("Cycle through display modes") }, {"win.canvas-display-mode-toggle", N_("Display Mode: Toggle"), "Canvas Display", N_("Toggle between normal and last non-normal mode")}, {"win.canvas-display-mode-toggle-preview", N_("Display Mode: Toggle Preview"), "Canvas Display", N_("Toggle between preview and previous mode") }, + {"win.canvas-zoom-resize", N_("Zoom on window resize"), "Canvas Display", N_("Toggle zoom on resize mode") }, - {"win.canvas-split-mode(0)", N_("Split Mode: Normal"), "Canvas Display", N_("Do not split canvas") }, - {"win.canvas-split-mode(1)", N_("Split Mode: Split"), "Canvas Display", N_("Render part of the canvas in outline mode") }, - {"win.canvas-split-mode(2)", N_("Split Mode: X-Ray"), "Canvas Display", N_("Render a circular area in outline mode") }, + {"win.canvas-split-mode(0)", N_("Split Mode: Normal"), "Canvas Display", N_("Do not split canvas") }, + {"win.canvas-split-mode(1)", N_("Split Mode: Split"), "Canvas Display", N_("Render part of the canvas in outline mode") }, + {"win.canvas-split-mode(2)", N_("Split Mode: X-Ray"), "Canvas Display", N_("Render a circular area in outline mode") }, - {"win.canvas-color-mode", N_("Color Mode"), "Canvas Display", N_("Toggle between normal and grayscale modes") }, - {"win.canvas-color-manage", N_("Color Managed Mode"), "Canvas Display", N_("Toggle between normal and color managed modes") } + {"win.canvas-color-displayprofile", N_("Display Profile"), "Canvas Display", N_("Toggle the display profile for this monitor") }, + {"win.canvas-color-colorproof", N_("Proof Colors"), "Canvas Display", N_("Toggle the soft proof for CMYK output") }, + {"win.canvas-color-gamutwarn", N_("Gamut Warning"), "Canvas Display", N_("Toogle the soft proof gamut warning") }, // clang-format on }; void add_actions_canvas_mode(InkscapeWindow* win) { - // Sync action with desktop variables. TODO: Remove! - auto prefs = Inkscape::Preferences::get(); - - // Initial States of Actions - int display_mode = prefs->getIntLimited("/options/displaymode", 0, 0, static_cast(Inkscape::RenderMode::size) - 1); // Default, minimum, maximum - bool color_manage = prefs->getBool("/options/displayprofile/enable"); - - SPDesktop* dt = win->get_desktop(); - if (dt) { - auto canvas = dt->getCanvas(); - canvas->set_render_mode(Inkscape::RenderMode(display_mode)); - canvas->set_cms_active(color_manage); - } else { - show_output("add_actions_canvas_mode: no desktop!"); - } - // clang-format off - win->add_action_radio_integer ("canvas-display-mode", sigc::bind(sigc::ptr_fun(&canvas_display_mode), win), display_mode); + win->add_action_radio_integer ("canvas-display-mode", sigc::bind(sigc::ptr_fun(&canvas_display_mode), win), 0); win->add_action( "canvas-display-mode-cycle", sigc::bind(sigc::ptr_fun(&canvas_display_mode_cycle), win)); win->add_action( "canvas-display-mode-toggle", sigc::bind(sigc::ptr_fun(&canvas_display_mode_toggle), win)); win->add_action_radio_integer ("canvas-split-mode", sigc::bind(sigc::ptr_fun(&canvas_split_mode), win), (int)Inkscape::SplitMode::NORMAL); - win->add_action_bool( "canvas-color-mode", sigc::bind(sigc::ptr_fun(&canvas_color_mode_toggle), win)); - win->add_action_bool( "canvas-color-manage", sigc::bind(sigc::ptr_fun(&canvas_color_manage_toggle), win), color_manage); + win->add_action( "canvas-color-prefs", sigc::bind(sigc::ptr_fun(&canvas_color_prefs), win)); + win->add_action( "canvas-color-props", sigc::bind(sigc::ptr_fun(&canvas_color_props), win)); + + add_action_bool( win, "canvas-zoom-resize", false, "/options/stickyzoom/value"); + add_action_bool( win, "canvas-color-displayprofile", false, "/options/displayprofile/enable"); + add_action_toggle(win, "canvas-color-colorproof", false, sigc::bind(sigc::ptr_fun(&canvas_color_colorproof), win)); + add_action_toggle(win, "canvas-color-gamutwarn", false, sigc::bind(sigc::ptr_fun(&canvas_color_gamutwarn), win)); // clang-format on auto app = InkscapeApplication::instance(); @@ -319,6 +340,22 @@ add_actions_canvas_mode(InkscapeWindow* win) app->get_action_extra_data().add_data(raw_data_canvas_mode); } +// Some of our actions are enabled/disabled by the document or prefs state +void update_actions_canvas_mode(InkscapeWindow *win, SPDocument *document) +{ + // This enables softproofing when loading a new document if it's CMYK + _update_actions_canvas_mode(win, document ? document->getColorManager().isPrintColorSpace() : false); +} + +void update_actions_displayprofile(InkscapeWindow *win) +{ + if (auto action = Glib::RefPtr::cast_dynamic(win->lookup_action("canvas-color-displayprofile"))) { + auto prefs = Inkscape::Preferences::get(); + action->set_enabled(!prefs->getString("/options/displayprofile/uri").empty()); + } + +} + /* Local Variables: diff --git a/src/actions/actions-canvas-mode.h b/src/actions/actions-canvas-mode.h index 2d171096d9..85a99b2018 100644 --- a/src/actions/actions-canvas-mode.h +++ b/src/actions/actions-canvas-mode.h @@ -12,8 +12,11 @@ #define INK_ACTIONS_CANVAS_MODE_H class InkscapeWindow; +class SPDocument; void add_actions_canvas_mode(InkscapeWindow* win); +void update_actions_canvas_mode(InkscapeWindow *win, SPDocument *document); +void update_actions_displayprofile(InkscapeWindow *win); #endif // INK_ACTIONS_CANVAS_MODE_H diff --git a/src/actions/actions-helper.cpp b/src/actions/actions-helper.cpp index 942f147f0e..e0c6419159 100644 --- a/src/actions/actions-helper.cpp +++ b/src/actions/actions-helper.cpp @@ -12,11 +12,14 @@ #include "actions-helper.h" #include +#include #include #include #include #include "inkscape-application.h" +#include "inkscape-window.h" +#include "preferences.h" #include "xml/document.h" // for Document #include "xml/node.h" // for Node #include "xml/repr.h" // for sp_repr_document_new, sp_repr_save... @@ -94,6 +97,50 @@ get_document_and_selection(InkscapeApplication* app, SPDocument** document, Inks return true; } +/** + * Add a generic toggle action with callback + * + * group - The document, window or app action group/map + * action_name - The name of the action to add + * callback - Optional callback with the boolean state + */ +void +add_action_toggle(Gio::ActionMap *group, std::string const &action_name, bool inital, std::function callback) +{ + group->add_action_bool(action_name, [=]() { + auto action = group->lookup_action(action_name); + if (!action) { + show_output(action_name + ": action missing!"); + return; + } + + auto saction = Glib::RefPtr::cast_dynamic(action); + if (!saction) { + show_output(action_name + ": action not SimpleAction!"); + return; + } + + bool state = false; + saction->get_state(state); + state = !state; + saction->change_state(state); + + if (callback) + callback(state); + }, inital); +} + +void +add_action_bool(Gio::ActionMap *group, std::string const &action_name, bool inital, std::string const &pref) +{ + auto prefs = Inkscape::Preferences::get(); + inital = prefs->getBool(pref, inital); + // FUTURE: We may want to observe the preference and update the action state here. + add_action_toggle(group, action_name, inital, [prefs, pref](bool state) { + prefs->setBool(pref, state); + }); +} + /* Local Variables: mode:c++ diff --git a/src/actions/actions-helper.h b/src/actions/actions-helper.h index 61ed6f1fca..e436211bbe 100644 --- a/src/actions/actions-helper.h +++ b/src/actions/actions-helper.h @@ -11,11 +11,19 @@ #ifndef INK_ACTIONS_HELPER_H #define INK_ACTIONS_HELPER_H +#include +#include +#include + namespace Glib { class ustring; } // namespace Glib +namespace Gio { +class ActionMap; +} class InkscapeApplication; +class InkscapeWindow; class SPDocument; namespace Inkscape { @@ -26,6 +34,8 @@ void active_window_start_helper(); void active_window_end_helper(); void show_output(Glib::ustring const &data, bool is_cerr = true); bool get_document_and_selection(InkscapeApplication* app, SPDocument** document, Inkscape::Selection** selection); +void add_action_toggle(Gio::ActionMap *group, std::string const &action_name, bool inital, std::function callback = nullptr); +void add_action_bool(Gio::ActionMap *group, std::string const &action_name, bool inital, std::string const &pref); #endif // INK_ACTIONS_HELPER_H diff --git a/src/actions/actions-tools.cpp b/src/actions/actions-tools.cpp index adafe6be0c..cff020ae00 100644 --- a/src/actions/actions-tools.cpp +++ b/src/actions/actions-tools.cpp @@ -275,22 +275,7 @@ tool_preferences(Glib::ustring const &tool, InkscapeWindow *win) show_output("tool-preferences: no desktop!"); return; } - - auto prefs = Inkscape::Preferences::get(); - prefs->setInt("/dialogs/preferences/page", tool_it->second.pref); - Inkscape::UI::Dialog::DialogContainer* container = dt->getContainer(); - - // Create dialog if it doesn't exist (also sets page if dialog not already in opened tab). - container->new_floating_dialog("Preferences"); - - // Find dialog and explicitly set page (in case not set in previous line). - auto dialog = Inkscape::UI::Dialog::DialogManager::singleton().find_floating_dialog("Preferences"); - if (dialog) { - auto pref_dialog = dynamic_cast(dialog); - if (pref_dialog) { - pref_dialog->showPage(); // Switch to page indicated in preferences file (set above). - } - } + dt->getContainer()->get_dialog_page("Preferences", tool_it->second.pref); } /** diff --git a/src/attributes.cpp b/src/attributes.cpp index 55d0746213..8ceb5c4578 100644 --- a/src/attributes.cpp +++ b/src/attributes.cpp @@ -134,6 +134,7 @@ static SPStyleProp const props[] = { {SPAttr::LOCAL, "local"}, {SPAttr::NAME, "name"}, {SPAttr::RENDERING_INTENT, "rendering-intent"}, + {SPAttr::DEFAULT, "inkscape:default"}, /* SPGuide */ {SPAttr::ORIENTATION, "orientation"}, {SPAttr::POSITION, "position"}, diff --git a/src/attributes.h b/src/attributes.h index 4c204b98fe..660fafacec 100644 --- a/src/attributes.h +++ b/src/attributes.h @@ -133,6 +133,7 @@ enum class SPAttr { LOCAL, NAME, RENDERING_INTENT, + DEFAULT, /* SPGuide */ ORIENTATION, POSITION, diff --git a/src/color.cpp b/src/color.cpp index f516161a8a..63c049875e 100644 --- a/src/color.cpp +++ b/src/color.cpp @@ -162,7 +162,7 @@ void SPColor::setColorProfile(Inkscape::ColorProfile *profile) { unsetColorProfile(); if (profile) { - _icc.colorProfile = profile->name; + _icc.colorProfile = profile->getName(); for (int i = 0; i < profile->getChannelCount(); i++) { _icc.colors.emplace_back(-1.0); } diff --git a/src/color/CMakeLists.txt b/src/color/CMakeLists.txt index 3cdda4a9d8..79a378c610 100644 --- a/src/color/CMakeLists.txt +++ b/src/color/CMakeLists.txt @@ -3,8 +3,10 @@ set(color_SRC cms-system.cpp cms-util.cpp - cmyk-conv.cpp + cmyk-conv.cpp color-conv.cpp + components.cpp + manager.cpp # ------- # Headers @@ -12,8 +14,10 @@ set(color_SRC cms-color-types.h cms-system.h cms-util.h - cmyk-conv.h + cmyk-conv.h color-conv.h + components.h + manager.h ) add_inkscape_source("${color_SRC}") diff --git a/src/color/cms-system.cpp b/src/color/cms-system.cpp index 55372feb20..41837d160a 100644 --- a/src/color/cms-system.cpp +++ b/src/color/cms-system.cpp @@ -33,34 +33,6 @@ 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()?). @@ -193,27 +165,10 @@ std::vector> CMSSystem::get_directory_paths() 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. @@ -259,75 +214,6 @@ cmsHPROFILE CMSSystem::get_monitor_profile() 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 { @@ -335,9 +221,11 @@ std::vector CMSSystem::get_monitor_profile_names() const for (auto const &profile_info : system_profile_infos) { if (profile_info.get_profileclass() == cmsSigDisplayClass && - profile_info.get_colorspace() == cmsSigRgbData) + profile_info.get_colorspace() == cmsSigRgbData && + profile_info.is_output()) { - result.emplace_back(profile_info.get_name()); + std::string name = profile_info.get_name(); + result.emplace_back(name); } } std::sort(result.begin(), result.end()); @@ -346,7 +234,7 @@ std::vector CMSSystem::get_monitor_profile_names() const } // Returns vector of names to list in Preferences dialog: proofing profiles. -std::vector CMSSystem::get_softproof_profile_names() const +std::vector CMSSystem::get_cms_profile_names() const { std::vector result; @@ -375,6 +263,16 @@ std::string CMSSystem::get_path_for_profile(Glib::ustring const &name) const return result; } +const ICCProfileInfo *CMSSystem::get_info_for_profile(Glib::ustring const &name) const +{ + for (auto const &profile_info : system_profile_infos) { + if (name == profile_info.get_name() || name == profile_info.get_path()) { + return &profile_info; + } + } + return nullptr; +} + // 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) @@ -382,71 +280,70 @@ void CMSSystem::do_transform(cmsHTRANSFORM transform, unsigned char *inBuf, unsi 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() +/** + * Apply the CMS transform to the cairo surface and paint it into the output surface. + * + * Based on ink_cairo_surface_filter in cauri-templates.h + */ +void CMSSystem::do_transform(cmsHTRANSFORM transform, cairo_surface_t *in, cairo_surface_t *out) { - bool preferences_changed = false; + cairo_surface_flush(in); - 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; + 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); - gamutWarn = warn; - lastIntent = intent; - lastProofIntent = proofIntent; - lastBPC = bpc; - lastGamutColor = gamutColor; + if (stride != cairo_image_surface_get_stride(out) + || width != cairo_image_surface_get_width(out) + || height != cairo_image_surface_get_height(out)) { + g_warning("Different image formats while applying CMS!"); + return; } - auto monitor_profile = get_monitor_profile(); - auto proof_profile = get_proof_profile(); + for (int i = 0; i < height; i++) { + auto row_in = px_in + i * stride; + auto row_out = px_out + i * stride; + do_transform(transform, row_in, row_out, width); + } - bool need_to_update = preferences_changed || current_monitor_profile_changed || current_proof_profile_changed; + cairo_surface_mark_dirty(out); +} - 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; - } +/* + * Get the color managed trasform for the screen. + * + * There is one transform for all monitors, anything more complex and the user should + * use their operating system CMS configurations instead of the Inkscape monitor cms. + * + * Transform immutably shared between CMSSystem and Canvas. + */ +std::shared_ptr const &CMSSystem::get_screen_transform() +{ + bool preferences_changed = false; - current_transform = CMSTransform::create( - cmsCreateProofingTransform(sRGB_profile, TYPE_BGRA_8, monitor_profile, TYPE_BGRA_8, - proof_profile, intent, proofIntent, dwFlags)); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool display = prefs->getIntLimited("/options/displayprofile/enabled", false); + int displayIntent = prefs->getIntLimited("/options/displayprofile/intent", 0, 0, 3); - } else if (monitor_profile) { + if (lastDisplay != display || lastDisplayIntent != displayIntent) { + preferences_changed = true; + lastDisplay = display; + lastDisplayIntent = displayIntent; + } + + auto monitor_profile = display ? get_monitor_profile() : nullptr; + bool need_to_update = preferences_changed || current_monitor_profile_changed; + + if (need_to_update) { + if (monitor_profile) { current_transform = CMSTransform::create( - cmsCreateTransform(sRGB_profile, TYPE_BGRA_8, monitor_profile, TYPE_BGRA_8, intent, 0)); + cmsCreateTransform(sRGB_profile, TYPE_BGRA_8, monitor_profile, TYPE_BGRA_8, displayIntent, 0)); + } else { + current_transform = nullptr; } } diff --git a/src/color/cms-system.h b/src/color/cms-system.h index a040146eca..49631f6c27 100644 --- a/src/color/cms-system.h +++ b/src/color/cms-system.h @@ -77,12 +77,13 @@ public: 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::vector get_cms_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); + std::shared_ptr const &get_screen_transform(); + const ICCProfileInfo *get_info_for_profile(Glib::ustring const &name) const; static void do_transform(cmsHTRANSFORM transform, unsigned char *inBuf, unsigned char *outBuf, unsigned size); + static void do_transform(cmsHTRANSFORM transform, cairo_surface_t *in, cairo_surface_t *out); private: CMSSystem(); @@ -99,11 +100,14 @@ private: 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; + bool lastDisplay = false; + int lastDisplayIntent = INTENT_PERCEPTUAL; + bool lastProof = false; int lastProofIntent = INTENT_PERCEPTUAL; + bool lastBPC = false; + bool lastGamutWarn = false; + Gdk::RGBA lastGamutColor = Gdk::RGBA("#808080"); + bool current_monitor_profile_changed = true; // Force at least one update. bool current_proof_profile_changed = true; diff --git a/src/color/cms-util.cpp b/src/color/cms-util.cpp index 36546820b3..84e3136d67 100644 --- a/src/color/cms-util.cpp +++ b/src/color/cms-util.cpp @@ -30,6 +30,9 @@ ICCProfileInfo::ICCProfileInfo(cmsHPROFILE profile, std::string &&path, bool in_ _name = get_color_profile_name(profile); _colorspace = cmsGetColorSpace(profile); _profileclass = cmsGetDeviceClass(profile); + + // Output RGB ICC profiles are usually monitors + _output = cmsIsCLUT(profile, INTENT_PERCEPTUAL, LCMS_USED_AS_OUTPUT); } bool is_icc_file(std::string const &filepath) diff --git a/src/color/cms-util.h b/src/color/cms-util.h index e9262d9fa6..b95e37bd60 100644 --- a/src/color/cms-util.h +++ b/src/color/cms-util.h @@ -28,11 +28,13 @@ public: cmsColorSpaceSignature get_colorspace() const { return _colorspace; } cmsProfileClassSignature get_profileclass() const { return _profileclass; } bool in_home() const { return _in_home; } + bool is_output() const { return _output; } private: std::string _path; std::string _name; bool _in_home; + bool _output; cmsColorSpaceSignature _colorspace; cmsProfileClassSignature _profileclass; }; diff --git a/src/color/components.cpp b/src/color/components.cpp new file mode 100644 index 0000000000..eeae0bf35c --- /dev/null +++ b/src/color/components.cpp @@ -0,0 +1,76 @@ + + +#include +#include +#include + +#include + +#include "components.h" + +namespace Color { + +Component::Component(std::string name, std::string tip, guint scale) + : name(std::move(name)) + , tip(std::move(tip)) + , scale(scale) +{ +} + + +std::vector getComponents(unsigned int space) +{ + static std::map > sets; + + if (sets.empty()) { + sets[cmsSigXYZData].push_back(Component("_X", "X", 2)); // TYPE_XYZ_16 + sets[cmsSigXYZData].push_back(Component("_Y", "Y", 1)); + sets[cmsSigXYZData].push_back(Component("_Z", "Z", 2)); + + sets[cmsSigLabData].push_back(Component("_L", "L", 100)); // TYPE_Lab_16 + sets[cmsSigLabData].push_back(Component("_a", "a", 256)); + sets[cmsSigLabData].push_back(Component("_b", "b", 256)); + + // cmsSigLuvData + + sets[cmsSigYCbCrData].push_back(Component("_Y", "Y", 1)); // TYPE_YCbCr_16 + sets[cmsSigYCbCrData].push_back(Component("C_b", "Cb", 1)); + sets[cmsSigYCbCrData].push_back(Component("C_r", "Cr", 1)); + + sets[cmsSigYxyData].push_back(Component("_Y", "Y", 1)); // TYPE_Yxy_16 + sets[cmsSigYxyData].push_back(Component("_x", "x", 1)); + sets[cmsSigYxyData].push_back(Component("y", "y", 1)); + + sets[cmsSigRgbData].push_back(Component(_("_R:"), _("Red"), 1)); // TYPE_RGB_16 + sets[cmsSigRgbData].push_back(Component(_("_G:"), _("Green"), 1)); + sets[cmsSigRgbData].push_back(Component(_("_B:"), _("Blue"), 1)); + + sets[cmsSigGrayData].push_back(Component(_("G:"), _("Gray"), 1)); // TYPE_GRAY_16 + + sets[cmsSigHsvData].push_back(Component(_("_H:"), _("Hue"), 360)); // TYPE_HSV_16 + sets[cmsSigHsvData].push_back(Component(_("_S:"), _("Saturation"), 1)); + sets[cmsSigHsvData].push_back(Component("_V:", "Value", 1)); + + sets[cmsSigHlsData].push_back(Component(_("_H:"), _("Hue"), 360)); // TYPE_HLS_16 + sets[cmsSigHlsData].push_back(Component(_("_L:"), _("Lightness"), 1)); + sets[cmsSigHlsData].push_back(Component(_("_S:"), _("Saturation"), 1)); + + sets[cmsSigCmykData].push_back(Component(_("_C:"), _("Cyan"), 1)); // TYPE_CMYK_16 + sets[cmsSigCmykData].push_back(Component(_("_M:"), _("Magenta"), 1)); + sets[cmsSigCmykData].push_back(Component(_("_Y:"), _("Yellow"), 1)); + sets[cmsSigCmykData].push_back(Component(_("_K:"), _("Black"), 1)); + + sets[cmsSigCmyData].push_back(Component(_("_C:"), _("Cyan"), 1)); // TYPE_CMY_16 + sets[cmsSigCmyData].push_back(Component(_("_M:"), _("Magenta"), 1)); + sets[cmsSigCmyData].push_back(Component(_("_Y:"), _("Yellow"), 1)); + } + + std::vector target; + if (sets.find(space) != sets.end()) { + target = sets[space]; + } + return target; +} + +}; // namespace Color + diff --git a/src/color/components.h b/src/color/components.h new file mode 100644 index 0000000000..9645e57999 --- /dev/null +++ b/src/color/components.h @@ -0,0 +1,38 @@ +// 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_COLOR_COMPONENTS_H +#define SEEN_COLOR_COMPONENTS_H + +#include +#include + +namespace Color +{ + +class Component +{ +public: + Component() = delete; + ~Component() = default; + + Component(std::string name, std::string tip, unsigned int scale); + + std::string name; + std::string tip; + unsigned int scale; +}; + +std::vector getComponents(unsigned int space); + +} // namespace Color + +#endif // SEEN_COLOR_COMPONENTS_H diff --git a/src/color/manager.cpp b/src/color/manager.cpp new file mode 100644 index 0000000000..434f1bbcc1 --- /dev/null +++ b/src/color/manager.cpp @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * ColorManager - 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 "color/manager.h" + +#include "object/color-profile.h" +#include "object/sp-root.h" +#include "object/sp-defs.h" +#include "io/sys.h" + +#include "document.h" +#include "color/cms-system.h" + +namespace Inkscape { + +ColorManager::ColorManager(SPDocument *document) + : _document(document) +{ + _resource_connection = _document->connectResourcesChanged("iccprofile", [=]() { + // Very few icc profiles are ever expected to exist at the same time, and their + // update frequency is also very low. So we're just going to destroy and recollect + _profiles.clear(); + _modified_connections.clear(); + for (auto obj : _document->getResourceList("iccprofile")) { + if (auto cp = cast(obj)) { + _profiles.push_back(cp); + _modified_connections.push_back(cp->connectModified([this](SPObject* obj, guint flags) { + if (auto cp = cast(obj)) { + _modified_signal.emit(cp); + } + })); + } + } + _changed_signal.emit(); + }); +} + +ColorManager::~ColorManager() +{ + _document = nullptr; +} + +/** + * Finds the named color profile. This name is used in the icc color derective + * to match included profiles. + * + * @arg name - The name of the color profile to match. + * @returns the matching ColorProfile or nullptr if none found. + */ +ColorProfile *ColorManager::find(std::string const &name) const +{ + for (auto cp : _profiles) { + if (cp->getName() == name) + return cp; + } + return nullptr; +} + +ColorProfile *ColorManager::addProfile(std::string const &uri, RenderingIntent intent) +{ + + auto system = Inkscape::CMSSystem::get(); + auto info = system->get_info_for_profile(uri); + if (!info) + return nullptr; + auto name = info->get_name(); + + //XXX old code has sanitizeName(name); + if (auto existing = find(name)) { + return existing; + } + + if (!Inkscape::IO::file_test(uri.c_str(), G_FILE_TEST_EXISTS)) { + g_warning("CMS File not found: %s", uri.c_str()); + return nullptr; + } + + 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 + cprofRepr->setAttribute("name", nameStr); + cprofRepr->setAttribute("xlink:href", "file://" + uri); + + if (auto defs = _document->getDefs()) { + defs->getRepr()->addChild(cprofRepr, nullptr); + } + if (auto cm = cast(_document->getObjectByRepr(cprofRepr))) { + if (!cm->getHandle()) { + g_warning("Invalid cms profile: %s", uri.c_str()); + cm->deleteObject(); + return nullptr; + } + cm->setRenderingIntent(intent); + return cm; + } + return nullptr; +} + +/** + * Removes the given profile, if it can, reverts colors to sRGB automatically + */ +void ColorManager::removeProfile(ColorProfile *profile) +{ + // TODO: revert linked colors here. + profile->deleteObject(false); +} + +/** + * Return the default color profile, or nullptr if no default is set. + * + * @returns the default profile, or nullptr if none found. + */ +ColorProfile *ColorManager::getDefault() const +{ + for (auto cp : _profiles) { + if (cp->isDefault()) + return cp; + } + return nullptr; +} + +/** + * Set the given profile as the default document profile used in + * color picking and soft proof visualisation. + * + * @arg profile - The profile to set as default, if undef sets no default + * which reverts the document to sRGB mode. + */ +void ColorManager::setDefault(ColorProfile *profile) +{ + if (auto existing = getDefault()) { + existing->setDefault(false); + } + if (profile) { + profile->setDefault(true); + } +} + +/** + * Returns true if this document is in CMYK mode. + * + * @returns true if the document is CMYK, false if it's RGB + */ +bool ColorManager::isPrintColorSpace() const +{ + if (auto profile = getDefault()) { + return profile->isPrintColorSpace(); + } + return false; +} + +/* + * Convert all colors to sRGB, removing any icc profile use + * + * The rendering intent is taken from the icc profile being used. + * + * @arg profile - The profile to remove, if nullptr all profiles are removed. + * @returns the number of colors changed + */ +int ColorManager::convertToRGB(ColorProfile *profile) +{ + int count = 0; + // We recurse SPObjects instead of SPItems to get into defs without exceptions + _document->getRoot()->recursivelyCallback([=](SPObject *obj) { + // Find all colors in this object + }); + return count; +} + +/* + * Convert all colors to an icc CMYK profile, replacing sRGB values but does not + * change any color already using an icc profile. + * + * The rendering intent is taken from the icc profile being used. + * + * @arg profile - The color profile to convert sRGB into. + * @returns + */ +int ColorManager::convertToCMYK(ColorProfile *profile) +{ + int count = 0; + // We recurse SPObjects instead of SPItems to get into defs without exceptions + _document->getRoot()->recursivelyCallback([=](SPObject *obj) { + // Find all colors in this object + }); + return count; +} + +} // 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/manager.h b/src/color/manager.h new file mode 100644 index 0000000000..1d77d7bbd4 --- /dev/null +++ b/src/color/manager.h @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * ColorManager - 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_INKSCAPE_COLOR_MANAGER_H +#define SEEN_INKSCAPE_COLOR_MANAGER_H + +#include + +#include "helper/auto-connection.h" +#include "object/color-profile.h" + +class SPDocument; + +namespace Inkscape { + +class ColorProfile; + +class ColorManager +{ +public: + ColorManager(SPDocument *document); + ~ColorManager(); + + std::vector::iterator begin() { return std::begin(_profiles); } + std::vector::iterator end() { return std::end(_profiles); } + + ColorProfile *find(std::string const &name) const; + ColorProfile *getDefault() const; + ColorProfile *addProfile(std::string const &uri, RenderingIntent intent = RenderingIntent::AUTO); + void removeProfile(ColorProfile *profile); + void setDefault(ColorProfile *profile); + bool isPrintColorSpace() const; + + int convertToRGB(ColorProfile *profile = nullptr); + int convertToCMYK(ColorProfile *profile = nullptr); + + sigc::connection connectChanged(const sigc::slot &slot) { return _changed_signal.connect(slot); } + sigc::connection connectModified(const sigc::slot &slot) { return _modified_signal.connect(slot); } + +private: + ColorManager(ColorManager const &) = delete; + void operator=(ColorManager const &) = delete; + + SPDocument* _document = nullptr; + std::vector _profiles; + + // Signals In + Inkscape::auto_connection _resource_connection; + std::vector _modified_connections; + + // Signals Out + sigc::signal _changed_signal; + sigc::signal _modified_signal; +}; + +} + +#endif // SEEN_INKSCAPE_COLOR_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/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.cpp b/src/desktop.cpp index 47c4eab6bb..4a479cc1e9 100644 --- a/src/desktop.cpp +++ b/src/desktop.cpp @@ -31,6 +31,7 @@ #include "desktop.h" #include "color.h" +#include "color/manager.h" #include "desktop-events.h" #include "desktop-style.h" #include "document-undo.h" @@ -1379,6 +1380,14 @@ SPDesktop::setDocument (SPDocument *doc) }); // End Fomerly in View::View ^^^^^^^^^^^^^^^ + _profiles_changed_connection = document->getColorManager().connectChanged([this]() { + update_canvas_cms(); + }); + _profiles_modified_connection = document->getColorManager().connectModified([this](Inkscape::ColorProfile const *) { + update_canvas_cms(); + }); + update_canvas_cms(); + sp_namedview_update_layers_from_document(this); _document_replaced_signal.emit (this, doc); @@ -1548,6 +1557,22 @@ void SPDesktop::on_zoom_end(GtkGesture const * /*zoom*/, GdkEventSequence const _begin_zoom.reset(); } +void SPDesktop::update_canvas_cms() +{ + auto &cm = document->getColorManager(); + auto cp = cm.getDefault(); + if (_default_cms_profile != cp) { + _default_cms_profile = cp; + std::shared_ptr colorproof; + std::shared_ptr gamutwarn; + if (cp) { + colorproof = cp->getColorProofTransform(); + gamutwarn = cp->getGamutWarnTransform(); + } + canvas->set_cms_transforms(colorproof, gamutwarn); + } +} + /* Local Variables: mode:c++ diff --git a/src/desktop.h b/src/desktop.h index a690ad6208..dcf5f941e2 100644 --- a/src/desktop.h +++ b/src/desktop.h @@ -103,6 +103,7 @@ class PageManager; class MessageContext; class MessageStack; class Selection; +class ColorProfile; class CanvasItem; class CanvasItemCatchall; @@ -169,6 +170,8 @@ private: Inkscape::auto_connection _message_changed_connection; Inkscape::auto_connection _document_uri_set_connection; + Inkscape::auto_connection _profiles_changed_connection; + Inkscape::auto_connection _profiles_modified_connection; // End Formerly in View::View ^^^^^^^^^^^^^^^^^^ std::unique_ptr _tool ; @@ -339,6 +342,8 @@ public: void zoom_selection(); void schedule_zoom_from_document(); + void update_canvas_cms(); + double current_zoom() const { return _current_affine.getZoom(); } Geom::Point current_center() const; @@ -554,6 +559,7 @@ private: bool _overlays_visible = true; ///< Whether the overlays are temporarily hidden bool _saved_guides_visible = false; ///< Remembers guides' visibility when hiding overlays + Inkscape::ColorProfile *_default_cms_profile = nullptr; std::unique_ptr _layer_manager; diff --git a/src/display/drawing-item.cpp b/src/display/drawing-item.cpp index ef66affaed..63000ba69c 100644 --- a/src/display/drawing-item.cpp +++ b/src/display/drawing-item.cpp @@ -12,6 +12,8 @@ #include +#include "color/cms-system.h" + #include "display/drawing-context.h" #include "display/drawing-group.h" #include "display/drawing-item.h" @@ -786,8 +788,8 @@ unsigned DrawingItem::render(DrawingContext &dc, RenderContext &rc, Geom::IntRec } // determine whether this shape needs intermediate rendering. - bool const greyscale = _drawing.colorMode() == ColorMode::GRAYSCALE && !(flags & RENDER_OUTLINE); - bool const isolate_root = _contains_unisolated_blend || greyscale; + bool const filtered = _drawing.colorMode() != ColorMode::NORMAL && !(flags & RENDER_OUTLINE); + bool const isolate_root = _contains_unisolated_blend || filtered; bool const needs_intermediate_rendering = _clip // 1. it has a clipping path || _mask // 2. it has a mask @@ -898,9 +900,25 @@ unsigned DrawingItem::render(DrawingContext &dc, RenderContext &rc, Geom::IntRec // instead of cairo_get_target(). } - // 4b. Apply greyscale rendering mode, if root node. - if (greyscale && _child_type == ChildType::ROOT) { - ink_cairo_surface_filter(ict.rawTarget(), ict.rawTarget(), _drawing.grayscaleMatrix()); + // 4b. Apply root color mode filters + if (_child_type == ChildType::ROOT) { + switch (_drawing.colorMode()) { + case ColorMode::NORMAL: + break; + case ColorMode::GRAYSCALE: + ink_cairo_surface_filter(ict.rawTarget(), ict.rawTarget(), _drawing.grayscaleMatrix()); + break; + case ColorMode::COLORPROOF: + if (auto cms_profile = _drawing.getColorProofTransform()) + Inkscape::CMSSystem::do_transform(cms_profile->getHandle(), ict.rawTarget(), ict.rawTarget()); + break; + case ColorMode::GAMUTWARN: + if (auto cms_profile = _drawing.getGamutWarnTransform()) + Inkscape::CMSSystem::do_transform(cms_profile->getHandle(), ict.rawTarget(), ict.rawTarget()); + break; + default: + break; + } } // 5. Render object inside the composited mask + clip diff --git a/src/display/drawing.cpp b/src/display/drawing.cpp index 0377c99a8c..345654dc89 100644 --- a/src/display/drawing.cpp +++ b/src/display/drawing.cpp @@ -215,6 +215,18 @@ void Drawing::setClip(std::optional &&clip) }); } +void Drawing::setColorProofTransform(std::shared_ptr tr, std::shared_ptr gw) +{ + defer([=] { + // No check for setting-same + if (tr == colorproof_transform && gw == gamutwarn_transform) return; + colorproof_transform = tr; + gamutwarn_transform = gw; + _root->_markForUpdate(DrawingItem::STATE_ALL, true); + _clearCache(); + }); +} + void Drawing::setAntialiasingOverride(std::optional antialiasing_override) { defer([=, this] { diff --git a/src/display/drawing.h b/src/display/drawing.h index 895cac4bc8..bbcb9a050c 100644 --- a/src/display/drawing.h +++ b/src/display/drawing.h @@ -27,6 +27,7 @@ #include "nr-filter-colormatrix.h" #include "preferences.h" #include "util/funclog.h" +#include "color/cms-system.h" namespace Inkscape { @@ -93,6 +94,10 @@ public: void setExact(); void setOpacity(double opacity = 1.0); + void setColorProofTransform(std::shared_ptr tr, std::shared_ptr gw); + std::shared_ptr getColorProofTransform() { return colorproof_transform; } + std::shared_ptr getGamutWarnTransform() { return gamutwarn_transform; } + private: void _pickItemsForCaching(); void _clearCache(); @@ -120,6 +125,10 @@ private: bool _select_zero_opacity; std::optional _antialiasing_override; + // Only set if the document has an icc profile set for it's color proofing + std::shared_ptr colorproof_transform; + std::shared_ptr gamutwarn_transform; + std::set _cached_items; // modified by DrawingItem::_setCached() CacheList _candidate_items; // keep this list always sorted with std::greater diff --git a/src/display/rendermode.h b/src/display/rendermode.h index dec852c435..b82dcc6079 100644 --- a/src/display/rendermode.h +++ b/src/display/rendermode.h @@ -47,7 +47,8 @@ enum class SplitDirection { enum class ColorMode { NORMAL, GRAYSCALE, - PRINT_COLORS_PREVIEW + COLORPROOF, + GAMUTWARN }; } // Namespace Inkscape diff --git a/src/document.cpp b/src/document.cpp index 4980186ba1..c79328a049 100644 --- a/src/document.cpp +++ b/src/document.cpp @@ -57,7 +57,7 @@ #include "inkscape.h" #include "layer-manager.h" #include "page-manager.h" -#include "profile-manager.h" +#include "color/manager.h" #include "rdf.h" #include "selection.h" @@ -152,9 +152,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); @@ -169,13 +166,13 @@ SPDocument::SPDocument() : add_actions_undo_document(this); _page_manager = 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 f6f4ee06c0..8b94ffd302 100644 --- a/src/document.h +++ b/src/document.h @@ -85,7 +85,7 @@ namespace Inkscape { class Event; class EventLog; class PageManager; - class ProfileManager; + class ColorManager; class Selection; class UndoStackObserver; namespace XML { @@ -176,12 +176,15 @@ public: Inkscape::PageManager& getPageManager() { return *_page_manager; } const Inkscape::PageManager& getPageManager() const { return *_page_manager; } + Inkscape::ColorManager &getColorManager() { return *_cms_manager; } + const Inkscape::ColorManager &getColorManager() 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 _cms_manager; std::queue pending_resource_changes; @@ -193,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; } @@ -372,7 +373,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/extension/internal/pdfinput/svg-builder.cpp b/src/extension/internal/pdfinput/svg-builder.cpp index b1e7be6bf4..2c7129fbc8 100644 --- a/src/extension/internal/pdfinput/svg-builder.cpp +++ b/src/extension/internal/pdfinput/svg-builder.cpp @@ -30,6 +30,7 @@ #include "Page.h" #include "Stream.h" #include "color.h" +#include "color/manager.h" #include "document.h" #include "extract-uri.h" #include "pdf-parser.h" @@ -900,7 +901,7 @@ std::string SvgBuilder::_getColorProfile(cmsHPROFILE hp) std::string name = get_color_profile_name(hp); // Find the named profile in the document (if already added) - if (_doc->getProfileManager().find(name.c_str())) + if (_doc->getColorManager().find(name)) return name; // Add the profile, we've never seen it before. diff --git a/src/inkscape-window.cpp b/src/inkscape-window.cpp index 10c4bd95ec..34ee32380b 100644 --- a/src/inkscape-window.cpp +++ b/src/inkscape-window.cpp @@ -41,6 +41,7 @@ #include "actions/actions-tools.h" #include "actions/actions-view-mode.h" #include "actions/actions-view-window.h" +#include "color/manager.h" #include "object/sp-namedview.h" // TODO Remove need for this! #include "ui/desktop/menubar.h" #include "ui/desktop/menu-set-tooltips-shift-icons.h" @@ -154,6 +155,14 @@ InkscapeWindow::InkscapeWindow(SPDocument* document) } } + // Apply updates + update_actions_canvas_mode(this, _document); + update_actions_displayprofile(this); + // This is needed because actions can not watch preferences yet. + displayprofile_observer = prefs->createObserver("/options/displayprofile/uri", [this]() { + update_actions_displayprofile(this); + }); + // ========= Update text for Accellerators ======= Inkscape::Shortcuts::getInstance().update_gui_text_recursive(this); } @@ -176,6 +185,16 @@ InkscapeWindow::change_document(SPDocument* document) _app->set_active_document(_document); add_document_actions(); + // Monitor icc profile changes and update the available actions + if (_document) { + icc_changed_connection = _document->getColorManager().connectChanged([this]() { + update_actions_canvas_mode(this, _document); + }); + } else { + icc_changed_connection.disconnect(); + } + update_actions_canvas_mode(this, _document); + setup_view(); update_dialogs(); } diff --git a/src/inkscape-window.h b/src/inkscape-window.h index c53974b854..ba9bb93e81 100644 --- a/src/inkscape-window.h +++ b/src/inkscape-window.h @@ -18,6 +18,8 @@ #define INKSCAPE_WINDOW_H #include +#include "helper/auto-connection.h" +#include "preferences.h" // observer namespace Gtk { class Box; } @@ -46,6 +48,8 @@ private: void setup_view(); void add_document_actions(); + Inkscape::auto_connection icc_changed_connection; + Inkscape::PrefObserver displayprofile_observer; public: // TODO: Can we avoid it being public? Probably yes in GTK4. bool on_key_press_event(GdkEventKey* event) final; diff --git a/src/object/color-profile.cpp b/src/object/color-profile.cpp index 4a8e2b2892..4fbbea9f52 100644 --- a/src/object/color-profile.cpp +++ b/src/object/color-profile.cpp @@ -57,9 +57,11 @@ public: cmsColorSpaceSignature _profileSpace; cmsHTRANSFORM _transf; cmsHTRANSFORM _revTransf; - cmsHTRANSFORM _gamutTransf; + std::shared_ptr _colorproof; + std::shared_ptr _gamutwarn; }; + cmsColorSpaceSignature asICColorSpaceSig(ColorSpaceSig const & sig) { return ColorSpaceSigWrapper(sig); @@ -78,8 +80,7 @@ ColorProfileImpl::ColorProfileImpl() _profileClass(cmsSigInputClass), _profileSpace(cmsSigRgbData), _transf(nullptr), - _revTransf(nullptr), - _gamutTransf(nullptr) + _revTransf(nullptr) { } @@ -107,25 +108,14 @@ ColorProfile::ColorProfile() : SPObject() { 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; + return getName() < other.getName(); } -/** - * Callback: free object - */ void ColorProfile::release() { // Unregister ourselves if ( this->document ) { @@ -142,16 +132,6 @@ void ColorProfile::release() { 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; @@ -172,10 +152,6 @@ void ColorProfileImpl::_clearProfile() cmsDeleteTransform( _revTransf ); _revTransf = nullptr; } - if ( _gamutTransf ) { - cmsDeleteTransform( _gamutTransf ); - _gamutTransf = nullptr; - } if ( _profHandle ) { cmsCloseProfile( _profHandle ); _profHandle = nullptr; @@ -188,8 +164,6 @@ void ColorProfileImpl::_clearProfile() 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); SPObject::build(document, repr); @@ -198,6 +172,7 @@ void ColorProfile::build(SPDocument *document, Inkscape::XML::Node *repr) { this->readAttr(SPAttr::LOCAL); this->readAttr(SPAttr::NAME); this->readAttr(SPAttr::RENDERING_INTENT); + this->readAttr(SPAttr::DEFAULT); // Register if ( document ) { @@ -257,6 +232,10 @@ void ColorProfile::set(SPAttr key, gchar const *value) { this->requestModified(SP_OBJECT_MODIFIED_FLAG); break; + case SPAttr::DEFAULT: + _is_default = value && std::string(value) == "1"; + break; + case SPAttr::LOCAL: if ( this->local ) { g_free( this->local ); @@ -267,39 +246,12 @@ void ColorProfile::set(SPAttr key, gchar const *value) { break; case SPAttr::NAME: - if ( this->name ) { - g_free( this->name ); - this->name = nullptr; - } - this->name = g_strdup( value ); + this->name = value; this->requestModified(SP_OBJECT_MODIFIED_FLAG); 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; - } - } else { - this->rendering_intent = RENDERING_INTENT_UNKNOWN; - } - + rendering_intent = renderingIntentEnum(value ? value : ""); this->requestModified(SP_OBJECT_MODIFIED_FLAG); break; @@ -317,20 +269,24 @@ Inkscape::XML::Node* ColorProfile::write(Inkscape::XML::Document *xml_doc, Inksc repr = xml_doc->createElement("svg:color-profile"); } - if ( (flags & SP_OBJECT_WRITE_ALL) || this->href ) { + 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->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) || !name.empty()) { + repr->setAttribute("name", name); } - if ( (flags & SP_OBJECT_WRITE_ALL) || this->intentStr ) { - repr->setAttribute( "rendering-intent", this->intentStr ); + if ((flags & SP_OBJECT_WRITE_ALL) || _is_default) { + repr->setAttribute("inkscape:default", _is_default ? "1" : ""); + } + + if ((flags & SP_OBJECT_WRITE_ALL) || rendering_intent != RenderingIntent::UNKNOWN) { + repr->setAttribute("rendering-intent", renderingIntentId(rendering_intent)); } SPObject::write(xml_doc, repr, flags); @@ -371,27 +327,87 @@ cmsUInt32Number ColorProfileImpl::_getInputFormat( cmsColorSpaceSignature space return possible[index].inForm; } -static int getLcmsIntent( guint svgIntent ) +static int getLcmsIntent(Inkscape::RenderingIntent svgIntent) { int intent = INTENT_PERCEPTUAL; switch ( svgIntent ) { - case Inkscape::RENDERING_INTENT_RELATIVE_COLORIMETRIC: + case Inkscape::RenderingIntent::RELATIVE_COLORIMETRIC: + case Inkscape::RenderingIntent::RELATIVE_COLORIMETRIC_NOBPC: intent = INTENT_RELATIVE_COLORIMETRIC; break; - case Inkscape::RENDERING_INTENT_SATURATION: + case Inkscape::RenderingIntent::SATURATION: intent = INTENT_SATURATION; break; - case Inkscape::RENDERING_INTENT_ABSOLUTE_COLORIMETRIC: + case Inkscape::RenderingIntent::ABSOLUTE_COLORIMETRIC: intent = INTENT_ABSOLUTE_COLORIMETRIC; break; - case Inkscape::RENDERING_INTENT_PERCEPTUAL: - case Inkscape::RENDERING_INTENT_UNKNOWN: - case Inkscape::RENDERING_INTENT_AUTO: + case Inkscape::RenderingIntent::PERCEPTUAL: + case Inkscape::RenderingIntent::UNKNOWN: + case Inkscape::RenderingIntent::AUTO: default: intent = INTENT_PERCEPTUAL; } return intent; } +int ColorProfile::getCmsIntent(RenderingIntent intent) const +{ + return getLcmsIntent(getRenderingIntent(intent)); +} + +static int getLcmsFlags(Inkscape::RenderingIntent svgIntent) +{ + int flags = 0; + if (svgIntent == Inkscape::RenderingIntent::RELATIVE_COLORIMETRIC) { + flags |= cmsFLAGS_BLACKPOINTCOMPENSATION; + } + return flags; +} + +int ColorProfile::getCmsFlags(RenderingIntent intent) const +{ + return getLcmsFlags(getRenderingIntent(intent)); +} + +std::vector ColorProfile::getComponents() const +{ + return Color::getComponents(asICColorSpaceSig(getColorSpace())); +} + +std::string ColorProfile::renderingIntentId(RenderingIntent intent) +{ + return intentIds[(int)intent]; +} + +Inkscape::RenderingIntent ColorProfile::renderingIntentEnum(std::string const &intent_id) +{ + auto it = std::find(intentIds.begin(), intentIds.end(), intent_id); + if (it != intentIds.end()) + return (RenderingIntent)std::distance(intentIds.begin(), it); + return RenderingIntent::UNKNOWN; +} + +void ColorProfile::setRenderingIntent(std::string const &intent) +{ + getRepr()->setAttribute("rendering-intent", intent); +} + +void ColorProfile::setRenderingIntent(RenderingIntent intent) +{ + setRenderingIntent(intentIds[(int)intent]); +} + +void ColorProfile::setDefault(bool value) +{ + getRepr()->setAttributeOrRemoveIfEmpty("inkscape:default", value ? "1" : ""); +} + +/** + * Gets the number of objects / colors specified in this profile + */ +unsigned int ColorProfile::getShapeCount() const +{ + return 0; +} Inkscape::ColorSpaceSig ColorProfile::getColorSpace() const { return ColorSpaceSigWrapper(impl->_profileSpace); @@ -401,37 +417,48 @@ Inkscape::ColorProfileClassSig ColorProfile::getProfileClass() const { return ColorProfileClassSigWrapper(impl->_profileClass); } -cmsHTRANSFORM ColorProfile::getTransfToSRGB8() +cmsHTRANSFORM ColorProfile::getTransfToSRGB8(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 ); + if (!impl->_transf && impl->_profHandle) { + int flags = getCmsFlags(intent); + int cms_intent = getCmsIntent(intent); + impl->_transf = cmsCreateTransform(impl->_profHandle, ColorProfileImpl::_getInputFormat(impl->_profileSpace), ColorProfileImpl::getSRGBProfile(), TYPE_RGBA_8, cms_intent, flags); } return impl->_transf; } -cmsHTRANSFORM ColorProfile::getTransfFromSRGB8() +cmsHTRANSFORM ColorProfile::getTransfFromSRGB8(RenderingIntent intent) { - 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 (!impl->_revTransf && impl->_profHandle) { + int flags = getCmsFlags(intent); + int cms_intent = getCmsIntent(intent); + impl->_revTransf = cmsCreateTransform(ColorProfileImpl::getSRGBProfile(), TYPE_RGBA_8, impl->_profHandle, ColorProfileImpl::_getInputFormat(impl->_profileSpace), cms_intent, flags); } return impl->_revTransf; } -cmsHTRANSFORM ColorProfile::getTransfGamutCheck() +std::shared_ptr ColorProfile::getColorProofTransform() { - 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 (!impl->_colorproof) { + auto sRGB = ColorProfileImpl::getSRGBProfile(); + impl->_colorproof = CMSTransform::create( + cmsCreateProofingTransform(sRGB, TYPE_BGRA_8, sRGB, TYPE_BGRA_8, + getHandle(), getCmsIntent(), getCmsIntent(), + cmsFLAGS_SOFTPROOFING)); } - return impl->_gamutTransf; + return impl->_colorproof; +} + +std::shared_ptr ColorProfile::getGamutWarnTransform() +{ + if ( !impl->_gamutwarn ) { + auto sRGB = ColorProfileImpl::getSRGBProfile(); + impl->_gamutwarn = CMSTransform::create( + cmsCreateProofingTransform(sRGB, TYPE_BGRA_8, sRGB, TYPE_GRAY_8, + getHandle(), INTENT_RELATIVE_COLORIMETRIC, INTENT_RELATIVE_COLORIMETRIC, + (cmsFLAGS_GAMUTCHECK | cmsFLAGS_SOFTPROOFING))); + } + return impl->_gamutwarn; } // Check if a particular color is out of gamut. @@ -452,9 +479,8 @@ bool ColorProfile::GamutCheck(SPColor color) static_cast(SP_RGBA32_B_U(val)), 255}; - cmsHTRANSFORM gamutCheck = ColorProfile::getTransfGamutCheck(); - if (gamutCheck) { - cmsDoTransform(gamutCheck, &check_color, &outofgamut, 1); + if (auto transform = getGamutWarnTransform()) { + cmsDoTransform(transform->getHandle(), &check_color, &outofgamut, 1); } cmsSetAlarmCodes(oldAlarmCodes); @@ -462,7 +488,7 @@ bool ColorProfile::GamutCheck(SPColor color) return (outofgamut != 0); } -gint ColorProfile::getChannelCount() const +unsigned int ColorProfile::getChannelCount() const { return cmsChannelsOf(asICColorSpaceSig(getColorSpace())); } @@ -485,7 +511,6 @@ void errorHandlerCB(cmsContext /*contextID*/, cmsUInt32Number errorCode, char co //g_message("lcms: Error %d; %s", errorCode, errorText); } - /* Local Variables: mode:c++ diff --git a/src/object/color-profile.h b/src/object/color-profile.h index 0e93e04ae3..53da094fb6 100644 --- a/src/object/color-profile.h +++ b/src/object/color-profile.h @@ -20,19 +20,37 @@ #include #include "sp-object.h" +#include "color/components.h" #include "color/cms-color-types.h" +#include "color/cms-system.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 ColorManager; + +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 +}; +static std::vector intentIds = { + "", + "auto", + "perceptual", + "relative-colorimetric", + "saturation", + "absolute-colorimetric", + "relative-colorimetric-nobpc", }; class ColorProfileImpl; @@ -51,31 +69,53 @@ public: ColorSpaceSig getColorSpace() const; ColorProfileClassSig getProfileClass() const; - cmsHTRANSFORM getTransfToSRGB8(); - cmsHTRANSFORM getTransfFromSRGB8(); - cmsHTRANSFORM getTransfGamutCheck(); + cmsHTRANSFORM getTransfToSRGB8(RenderingIntent intent = RenderingIntent::UNKNOWN); + cmsHTRANSFORM getTransfFromSRGB8(RenderingIntent intent = RenderingIntent::UNKNOWN); + std::shared_ptr getColorProofTransform(); + std::shared_ptr getGamutWarnTransform(); bool GamutCheck(SPColor color); - int getChannelCount() const; + unsigned int getChannelCount() const; + unsigned int getShapeCount() const; bool isPrintColorSpace(); cmsHPROFILE getHandle(); - // TODO: Make private char* href; char* local; - char* name; - char* intentStr; - unsigned int rendering_intent; // FIXME: type the enum and hold that instead + std::string getName() const { return name; } + static std::string renderingIntentId(RenderingIntent intent); + static RenderingIntent renderingIntentEnum(std::string const &intent_id); + + RenderingIntent getRenderingIntent(RenderingIntent intent = RenderingIntent::UNKNOWN) const { + return intent == RenderingIntent::UNKNOWN ? rendering_intent : intent; + } + void setRenderingIntent(std::string const &intent); + void setRenderingIntent(RenderingIntent intent); + int getCmsIntent(RenderingIntent intent = RenderingIntent::UNKNOWN) const; + int getCmsFlags(RenderingIntent intent = RenderingIntent::UNKNOWN) const; + + std::vector getComponents() const; + + // Default color manager + bool isDefault() const { return _is_default; } protected: + friend class Inkscape::ColorManager; + ColorProfileImpl *impl; void build(SPDocument* doc, Inkscape::XML::Node* repr) override; void release() override; void set(SPAttr key, char const* value) override; + void setDefault(bool value = true); Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; + +private: + RenderingIntent rendering_intent = RenderingIntent::UNKNOWN; + std::string name; + bool _is_default = false; }; } // namespace Inkscape diff --git a/src/object/sp-image.cpp b/src/object/sp-image.cpp index c4f6d0a8f8..6957daa794 100644 --- a/src/object/sp-image.cpp +++ b/src/object/sp-image.cpp @@ -24,7 +24,7 @@ #include #include -#include +#include "color/manager.h" #include <2geom/rect.h> #include <2geom/transforms.h> @@ -248,55 +248,22 @@ void SPImage::apply_profile(Inkscape::Pixbuf *pixbuf) { 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 ) { + if (px) { + if (auto cp = document->getColorManager().find(color_profile)) { + auto prof = cp->getHandle(); 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 ) { + if (profileClass != cmsSigNamedColorClass) { + if (cmsHTRANSFORM transf = cp->getTransfToSRGB8()) { 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." ); } diff --git a/src/object/sp-object.cpp b/src/object/sp-object.cpp index 3c7db06371..b37e3da0ea 100644 --- a/src/object/sp-object.cpp +++ b/src/object/sp-object.cpp @@ -1836,6 +1836,14 @@ void SPObject::recursivePrintTree( unsigned level ) } } +void SPObject::recursivelyCallback(std::function callback, unsigned level) +{ + callback(this); + for (auto& child: children) { + child.recursivelyCallback(callback, level + 1); + } +} + // Function to allow tracing of program flow through SPObject and derived classes. // To trace function, add at entrance ('in' = true) and exit of function ('in' = false). void SPObject::objectTrace( std::string const &text, bool in, unsigned flags ) { diff --git a/src/object/sp-object.h b/src/object/sp-object.h index 1e2d07a7dc..362e0df24a 100644 --- a/src/object/sp-object.h +++ b/src/object/sp-object.h @@ -872,6 +872,7 @@ public: virtual void read_content(); void recursivePrintTree(unsigned level = 0); // For debugging + void recursivelyCallback(std::function callback, unsigned level = 0); void objectTrace(std::string const &, bool in = true, unsigned flags = 0); /** diff --git a/src/preferences-skeleton.h b/src/preferences-skeleton.h index 1f525419e2..f53325047c 100644 --- a/src/preferences-skeleton.h +++ b/src/preferences-skeleton.h @@ -282,11 +282,8 @@ static char const preferences_skeleton[] = uri="" /> 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/svg/svg-color.cpp b/src/svg/svg-color.cpp index 66ae587c12..f82b1b0e7a 100644 --- a/src/svg/svg-color.cpp +++ b/src/svg/svg-color.cpp @@ -30,11 +30,11 @@ #include // g_assert #include "color.h" -#include "colorspace.h" +#include "color/manager.h" +#include "color/components.h" #include "document.h" #include "inkscape.h" #include "preferences.h" -#include "profile-manager.h" #include "strneq.h" #include "svg-icc-color.h" @@ -509,11 +509,11 @@ 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())) { + if (auto prof = SP_ACTIVE_DOCUMENT->getColorManager().find(icc->colorProfile)) { guchar color_out[4] = {0,0,0,0}; cmsHTRANSFORM trans = prof->getTransfToSRGB8(); if ( trans ) { - std::vector comps = colorspace::getColorSpaceInfo( prof ); + std::vector comps = prof->getComponents(); size_t count = prof->getChannelCount(); size_t cap = std::min(count, comps.size()); diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt index b53a042f1f..a1d1abb274 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -191,8 +191,8 @@ set(ui_SRC widget/canvas/graphics.cpp widget/canvas-grid.cpp widget/canvas-notice.cpp + widget/cms-popover.cpp widget/color-entry.cpp - widget/color-icc-selector.cpp widget/color-notebook.cpp widget/color-palette.cpp widget/color-palette-preview.cpp @@ -475,9 +475,9 @@ set(ui_SRC widget/canvas/cairographics.h widget/canvas-grid.h widget/canvas-notice.h + widget/cms-popover.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/dialog/dialog-base.h b/src/ui/dialog/dialog-base.h index 19dfb38f5d..96de32e8f2 100644 --- a/src/ui/dialog/dialog-base.h +++ b/src/ui/dialog/dialog-base.h @@ -51,6 +51,11 @@ public: */ virtual void update() {} + /** + * Show a specific tab, if the dialog supports tabs + */ + virtual void showPage(int page) {} + // Public for future use, say if the desktop is smartly set when docking dialogs. void setDesktop(SPDesktop *new_desktop); diff --git a/src/ui/dialog/dialog-container.cpp b/src/ui/dialog/dialog-container.cpp index 007c3d8407..6dea2ee451 100644 --- a/src/ui/dialog/dialog-container.cpp +++ b/src/ui/dialog/dialog-container.cpp @@ -478,6 +478,20 @@ bool DialogContainer::recreate_dialogs_from_state(InkscapeWindow* inkscape_windo return restored; } +/** + * Open any dialog at a specific page (if the dialog supports it) + */ +void DialogContainer::get_dialog_page(std::string const &name, int page) +{ + // Create dialog if it doesn't exist + new_floating_dialog(name); + + // Find dialog and explicitly set page (in case not set in previous line). + if (DialogBase* existing_dialog = find_existing_dialog(name)) { + existing_dialog->showPage(page); + } +} + /** * Add a new floating dialog (or reuse existing one if it's already up) */ diff --git a/src/ui/dialog/dialog-container.h b/src/ui/dialog/dialog-container.h index e824bf985d..7ca99a1acd 100644 --- a/src/ui/dialog/dialog-container.h +++ b/src/ui/dialog/dialog-container.h @@ -57,6 +57,7 @@ public: // Dialog-related functions void new_dialog(const Glib::ustring& dialog_type); + void get_dialog_page(std::string const &name, int page); DialogWindow* new_floating_dialog(const Glib::ustring& dialog_type); bool has_dialog_of_type(DialogBase *dialog); DialogBase *get_dialog(const Glib::ustring& dialog_type); diff --git a/src/ui/dialog/document-properties.cpp b/src/ui/dialog/document-properties.cpp index 0cd9b50955..7824ff9869 100644 --- a/src/ui/dialog/document-properties.cpp +++ b/src/ui/dialog/document-properties.cpp @@ -42,12 +42,14 @@ #include "selection.h" #include "color/cms-system.h" +#include "color/manager.h" #include "helper/auto-connection.h" #include "io/sys.h" #include "object/color-profile.h" #include "object/sp-grid.h" #include "object/sp-root.h" #include "object/sp-script.h" +#include "ui/builder-utils.h" #include "ui/dialog/filedialog.h" #include "ui/icon-loader.h" #include "ui/icon-names.h" @@ -163,9 +165,10 @@ static void connect_remove_popup_menu(Gtk::TreeView &tree_view, sigc::slot(1, 1, false, true)) , _page_guides(Gtk::make_managed(1, 1)) - , _page_cms(Gtk::make_managed(1, 1)) + , _page_cms(Gtk::make_managed(1, 1, true, true)) , _page_scripting(Gtk::make_managed(1, 1)) , _page_external_scripts(Gtk::make_managed(1, 1)) , _page_embedded_scripts(Gtk::make_managed(1, 1)) @@ -199,14 +202,6 @@ DocumentProperties::DocumentProperties() _notebook.append_page(*_page_scripting, _("Scripting")); _notebook.append_page(*_page_metadata1, _("Metadata")); _notebook.append_page(*_page_metadata2, _("License")); - _notebook.signal_switch_page().connect([this](Gtk::Widget const *, unsigned const page){ - // we cannot use widget argument, as this notification fires during destruction with all pages passed one by one - // page no 3 - cms - if (page == 3) { - // lazy-load color profiles; it can get prohibitively expensive when hundreds are installed - populate_available_profiles(); - } - }); _wr.setUpdating (true); build_page(); @@ -222,6 +217,11 @@ DocumentProperties::DocumentProperties() show_all_children(); } +void DocumentProperties::showPage(int page) +{ + _notebook.set_current_page(page); +} + //======================================================================== /** @@ -601,290 +601,147 @@ void DocumentProperties::build_guides() gtk_actionable_set_action_name(GTK_ACTIONABLE(_delete_guides_btn.gobj()), "doc.delete-all-guides"); } -/// Populates the available color profiles combo box -void DocumentProperties::populate_available_profiles(){ - // scanning can be expensive; avoid if possible - if (!_AvailableProfilesListStore->children().empty()) return; - - _AvailableProfilesListStore->clear(); // Clear any existing items in the combo box - - // 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()) { - Gtk::TreeModel::Row row; - - // add a separator between profiles from the user's home directory and system profiles - if (!first && info.in_home() != home) +void DocumentProperties::build_cms() { - row = *(_AvailableProfilesListStore->append()); - row[_AvailableProfilesListColumns.fileColumn] = ""; - row[_AvailableProfilesListColumns.nameColumn] = ""; - row[_AvailableProfilesListColumns.separatorColumn] = true; - } - home = info.in_home(); - first = false; + auto &box_cms = get_widget(_builder, "color-tab"); + _page_cms->set_visible(true); + _page_cms->table().attach(box_cms, 0, 0); + box_cms.set_vexpand(true); - row = *(_AvailableProfilesListStore->append()); - row[_AvailableProfilesListColumns.fileColumn] = info.get_path(); - row[_AvailableProfilesListColumns.nameColumn] = info.get_name(); - row[_AvailableProfilesListColumns.separatorColumn] = false; - } -} + auto &profile = get_widget(_builder, "color-profile"); + auto &intent = get_widget(_builder, "rendering-intent"); -/** - * Cleans up name to remove disallowed characters. - * Some discussion at http://markmail.org/message/bhfvdfptt25kgtmj - * Allowed ASCII first characters: ':', 'A'-'Z', '_', 'a'-'z' - * Allowed ASCII remaining chars add: '-', '.', '0'-'9', - * - * @param str the string to clean up. - * - * Note: for use with ICC profiles only. - * This function has been restored to make ICC profiles work, as their names need to be sanitized. - * BUT, it is not clear to me whether we really need to strip all non-ASCII characters. - * We do it currently, because sp_svg_read_icc_color cannot parse Unicode. - */ -void sanitizeName(std::string& str) { - if (str.empty()) return; + _profile_changed_connection = profile.signal_changed().connect([this, &profile, &intent]() { + _cms_profiles_changed_connection.block(); + _cms_profiles_modified_connection.block(); + _intent_changed_connection.block(); - auto val = str.at(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.at(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.replace(i, 1, "-"); - } - } - } - if (str.at(str.size() - 1) == '-') { - str.pop_back(); - } -} - -/// Links the selected color profile in the combo box to the document -void DocumentProperties::linkSelectedProfile() -{ - //store this profile in the SVG document (create element in the XML) if (auto document = getDocument()){ - // Find the index of the currently-selected row in the color profiles combobox - Gtk::TreeModel::iterator iter = _AvailableProfilesList.get_active(); - if (!iter) - return; - - // Read the filename and description from the list of available profiles - 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; + auto &cm = document->getColorManager(); + // This also removes uses of the color profile from shapes + if (auto prev = cm.getDefault()) { + cm.removeProfile(prev); } - 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); + // Default intent if needed, update combo because we're signal blocked + auto intent_id = intent.get_active_id(); + if (intent_id.empty()) { + intent_id = "relative-colorimetric"; } - g_assert(document->getDefs()); - defsRepr->addChild(cprofRepr, nullptr); + auto profile_id = profile.get_active_id(); + std::string uri = !profile_id.empty() ? Inkscape::CMSSystem::get()->get_path_for_profile(profile_id) : ""; + auto cp = cm.addProfile(uri, ColorProfile::renderingIntentEnum(intent_id)); - // TODO check if this next line was sometimes needed. It being there caused an assertion. - //Inkscape::GC::release(defsRepr); + // This also sets sRGB when the profile add fails + cm.setDefault(cp); - // inform the document, so we can undo - DocumentUndo::done(document, _("Link Color Profile"), ""); + // The intent_id might have changed if the active_id was empty + intent.set_active_id(intent_id); + intent.set_sensitive((bool)cp); - populate_linked_profiles_box(); + DocumentUndo::done(document, _("Set default color profile"), ""); } -} - -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); } }; + _intent_changed_connection.unblock(); + _cms_profiles_modified_connection.unblock(); + _cms_profiles_changed_connection.unblock(); + }); -void DocumentProperties::populate_linked_profiles_box() -{ - _LinkedProfilesListStore->clear(); + _intent_changed_connection = intent.signal_changed().connect([this, &intent]() { + _cms_profiles_modified_connection.block(); if (auto document = getDocument()) { - std::vector current = document->getResourceList( "iccprofile" ); - if (! current.empty()) { - _emb_profiles_observer.set((*(current.begin()))->parent); + if (auto profile = document->getColorManager().getDefault()) { + profile->setRenderingIntent(intent.get_active_id()); } - - std::set _current; - std::transform(current.begin(), - current.end(), - std::inserter(_current, _current.begin()), - static_caster()); - - for (auto const &profile: _current) { - Gtk::TreeModel::Row row = *(_LinkedProfilesListStore->append()); - row[_LinkedProfilesListColumns.nameColumn] = profile->name; + DocumentUndo::done(document, _("Set default color rendering intent"), ""); } - } + _cms_profiles_modified_connection.unblock(); + }); + } -void DocumentProperties::onColorProfileSelectRow() +void DocumentProperties::init_cms(SPDocument *document) { - Glib::RefPtr sel = _LinkedProfilesList.get_selection(); - if (sel) { - _unlink_btn.set_sensitive(sel->count_selected_rows () > 0); - } -} + auto &manager = document->getColorManager(); -void DocumentProperties::removeSelectedProfile(){ - Glib::ustring name; - if(_LinkedProfilesList.get_selection()) { - Gtk::TreeModel::iterator i = _LinkedProfilesList.get_selection()->get_selected(); + _cms_profiles_changed_connection = manager.connectChanged([this, document]() { + update_cms(document); + }); + _cms_profiles_modified_connection = manager.connectModified([this](ColorProfile *cm) { + update_cms(cm->document); + }); - if(i){ - name = (*i)[_LinkedProfilesListColumns.nameColumn]; - } else { - return; - } - } - 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. - } - } + update_cms(document); } - populate_linked_profiles_box(); - onColorProfileSelectRow(); -} - -bool DocumentProperties::_AvailableProfilesList_separator(Glib::RefPtr const &model, - Gtk::TreeModel::const_iterator const &iter) +void DocumentProperties::deinit_cms() { - bool separator = (*iter)[_AvailableProfilesListColumns.separatorColumn]; - return separator; + _cms_profiles_changed_connection.disconnect(); + _cms_profiles_modified_connection.disconnect(); } -void DocumentProperties::build_cms() +void DocumentProperties::update_cms(SPDocument *document) { - _page_cms->set_visible(true); - Gtk::Label *label_link= Gtk::make_managed("", Gtk::ALIGN_START); - label_link->set_markup (_("Linked Color Profiles:")); - auto const label_avail = Gtk::make_managed("", Gtk::ALIGN_START); - label_avail->set_markup (_("Available Color Profiles:")); - - _unlink_btn.set_tooltip_text(_("Unlink Profile")); - docprops_style_button(_unlink_btn, INKSCAPE_ICON("list-remove")); - - int row = 0; - - label_link->set_hexpand(); - label_link->set_halign(Gtk::ALIGN_START); - label_link->set_valign(Gtk::ALIGN_CENTER); - _page_cms->table().attach(*label_link, 0, row, 3, 1); - - row++; - - _LinkedProfilesListScroller.set_hexpand(); - _LinkedProfilesListScroller.set_valign(Gtk::ALIGN_CENTER); - _page_cms->table().attach(_LinkedProfilesListScroller, 0, row, 3, 1); - - row++; - - auto const spacer = Gtk::make_managed(Gtk::ORIENTATION_HORIZONTAL); - spacer->set_size_request(SPACE_SIZE_X, SPACE_SIZE_Y); - - spacer->set_hexpand(); - spacer->set_valign(Gtk::ALIGN_CENTER); - _page_cms->table().attach(*spacer, 0, row, 3, 1); - - row++; - - label_avail->set_hexpand(); - label_avail->set_halign(Gtk::ALIGN_START); - label_avail->set_valign(Gtk::ALIGN_CENTER); - _page_cms->table().attach(*label_avail, 0, row, 3, 1); - - row++; - - _AvailableProfilesList.set_hexpand(); - _AvailableProfilesList.set_valign(Gtk::ALIGN_CENTER); - _page_cms->table().attach(_AvailableProfilesList, 0, row, 1, 1); - - _unlink_btn.set_halign(Gtk::ALIGN_CENTER); - _unlink_btn.set_valign(Gtk::ALIGN_CENTER); - _page_cms->table().attach(_unlink_btn, 2, row, 1, 1); - - // Set up the Available Profiles combo box - _AvailableProfilesListStore = Gtk::ListStore::create(_AvailableProfilesListColumns); - _AvailableProfilesList.set_model(_AvailableProfilesListStore); - _AvailableProfilesList.pack_start(_AvailableProfilesListColumns.nameColumn); - _AvailableProfilesList.set_row_separator_func(sigc::mem_fun(*this, &DocumentProperties::_AvailableProfilesList_separator)); - _AvailableProfilesList.signal_changed().connect( sigc::mem_fun(*this, &DocumentProperties::linkSelectedProfile) ); - - //# Set up the Linked Profiles combo box - _LinkedProfilesListStore = Gtk::ListStore::create(_LinkedProfilesListColumns); - _LinkedProfilesList.set_model(_LinkedProfilesListStore); - _LinkedProfilesList.append_column(_("Profile Name"), _LinkedProfilesListColumns.nameColumn); -// _LinkedProfilesList.append_column(_("Color Preview"), _LinkedProfilesListColumns.previewColumn); - _LinkedProfilesList.set_headers_visible(false); -// TODO restore? _LinkedProfilesList.set_fixed_height_mode(true); - - populate_linked_profiles_box(); + auto colors = get_object(_builder, "color-profiles-store"); + auto others = get_object(_builder, "other-profiles-store"); + auto &profile = get_widget(_builder, "color-profile"); + auto &intent = get_widget(_builder, "rendering-intent"); + + _profile_changed_connection.block(); + _intent_changed_connection.block(); + + class ColorColumns : public Gtk::TreeModel::ColumnRecord { + public: + Gtk::TreeModelColumn name; + ColorColumns() { add(name); } + }; + class OtherColumns : public Gtk::TreeModel::ColumnRecord { + public: + Gtk::TreeModelColumn name; + Gtk::TreeModelColumn intent; + Gtk::TreeModelColumn uri; + OtherColumns() { add(name); add(intent); add(uri); } + }; + auto cols = OtherColumns(); - _LinkedProfilesListScroller.add(_LinkedProfilesList); - _LinkedProfilesListScroller.set_shadow_type(Gtk::SHADOW_IN); - _LinkedProfilesListScroller.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS); - _LinkedProfilesListScroller.set_size_request(-1, 90); + colors->clear(); + others->clear(); - _unlink_btn.signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::removeSelectedProfile)); + auto row = *(colors->append()); + row[ColorColumns().name] = _("Standard SVG (sRGB)"); + profile.set_active(0); + intent.set_sensitive(false); - _LinkedProfilesList.get_selection()->signal_changed().connect( sigc::mem_fun(*this, &DocumentProperties::onColorProfileSelectRow) ); + std::vector profiles; + for (auto const &name: Inkscape::CMSSystem::get()->get_cms_profile_names()) { + auto row = *(colors->append()); + row[ColorColumns().name] = name; + profiles.push_back(name); + } - connect_remove_popup_menu(_LinkedProfilesList, sigc::mem_fun(*this, &DocumentProperties::removeSelectedProfile)); + for (auto cm : document->getColorManager()) { + auto name = cm->getName(); + if (std::find(profiles.begin(), profiles.end(), name) == profiles.end()) { + // Custom ICC, probably using the file manager, add to known profiles + auto row = *(colors->append()); + row[ColorColumns().name] = name; + } - if (auto document = getDocument()) { - std::vector current = document->getResourceList( "defs" ); - if (!current.empty()) { - _emb_profiles_observer.set((*(current.begin()))->parent); + auto intent_id = ColorProfile::renderingIntentId(cm->getRenderingIntent()); + if (cm->isDefault()) { + if (intent_id.empty()) + intent_id = "auto"; + profile.set_active_id(cm->getName()); + intent.set_sensitive(true); + intent.set_active_id(intent_id); + } else { + auto row = *(others->append()); + row[cols.name] = name; + row[cols.intent] = intent_id; + row[cols.uri] = _("[embeded]"); // XXX Can't get uri yet. } - _emb_profiles_observer.signal_changed().connect(sigc::mem_fun(*this, &DocumentProperties::populate_linked_profiles_box)); - onColorProfileSelectRow(); } + _intent_changed_connection.unblock(); + _profile_changed_connection.unblock(); } void DocumentProperties::build_scripting() @@ -1561,10 +1418,6 @@ void DocumentProperties::update_widgets() _rcp_gui.setRgba32 (nv->guidecolor); _rcp_hgui.setRgba32 (nv->guidehicolor); - //------------------------------------------------Color Management page - - populate_linked_profiles_box(); - //-----------------------------------------------------------meta pages // update the RDF entities; note that this may modify document, maybe doc-undo should be called? if (auto document = getDocument()) { @@ -1646,14 +1499,15 @@ void DocumentProperties::documentReplaced() { _root_connection.disconnect(); _namedview_connection.disconnect(); + deinit_cms(); if (auto desktop = getDesktop()) { _wr.setDesktop(desktop); _namedview_connection.connect(desktop->getNamedView()->getRepr()); if (auto document = desktop->getDocument()) { _root_connection.connect(document->getRoot()->getRepr()); + init_cms(document); } - populate_linked_profiles_box(); update_widgets(); rebuild_gridspage(); } diff --git a/src/ui/dialog/document-properties.h b/src/ui/dialog/document-properties.h index cccecb382a..d0ba8c662d 100644 --- a/src/ui/dialog/document-properties.h +++ b/src/ui/dialog/document-properties.h @@ -84,6 +84,10 @@ public: void update() override; void rebuild_gridspage(); + void showPage(int page) override; + +private: + Glib::RefPtr _builder; protected: void build_page(); @@ -92,7 +96,11 @@ protected: void build_snap(); void build_gridspage(); - void build_cms(); + void build_cms(); + void init_cms(SPDocument *document); + void deinit_cms(); + void update_cms(SPDocument *document); + void build_scripting(); void build_metadata(); @@ -101,13 +109,6 @@ protected: virtual void on_response (int); - void populate_available_profiles(); - void populate_linked_profiles_box(); - void linkSelectedProfile(); - void removeSelectedProfile(); - - void onColorProfileSelectRow(); - void populate_script_lists(); void addExternalScript(); void browseExternalScript(); @@ -128,7 +129,7 @@ protected: 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; Gtk::Notebook _notebook; UI::Widget::NotebookPage *_page_page; @@ -155,34 +156,6 @@ protected: Gtk::Button _delete_guides_btn; //--------------------------------------------------------------- UI::Widget::PageProperties* _page; - //--------------------------------------------------------------- - Gtk::Button _unlink_btn; - class AvailableProfilesColumns : public Gtk::TreeModel::ColumnRecord - { - public: - AvailableProfilesColumns() - { add(fileColumn); add(nameColumn); add(separatorColumn); } - Gtk::TreeModelColumn fileColumn; - Gtk::TreeModelColumn nameColumn; - Gtk::TreeModelColumn separatorColumn; - }; - AvailableProfilesColumns _AvailableProfilesListColumns; - Glib::RefPtr _AvailableProfilesListStore; - Gtk::ComboBox _AvailableProfilesList; - bool _AvailableProfilesList_separator(Glib::RefPtr const &model, - Gtk::TreeModel::const_iterator const &iter); - class LinkedProfilesColumns : public Gtk::TreeModel::ColumnRecord - { - public: - LinkedProfilesColumns() - { add(nameColumn); add(previewColumn); } - Gtk::TreeModelColumn nameColumn; - Gtk::TreeModelColumn previewColumn; - }; - LinkedProfilesColumns _LinkedProfilesListColumns; - Glib::RefPtr _LinkedProfilesListStore; - Gtk::TreeView _LinkedProfilesList; - Gtk::ScrolledWindow _LinkedProfilesListScroller; //--------------------------------------------------------------- Gtk::Button _external_add_btn; @@ -264,6 +237,10 @@ private: // nodes connected to listeners WatchConnection _namedview_connection; WatchConnection _root_connection; + Inkscape::auto_connection _cms_profiles_changed_connection; + Inkscape::auto_connection _cms_profiles_modified_connection; + Inkscape::auto_connection _profile_changed_connection; + Inkscape::auto_connection _intent_changed_connection; }; } // namespace Dialog diff --git a/src/ui/dialog/inkscape-preferences.cpp b/src/ui/dialog/inkscape-preferences.cpp index 708d438948..f8efaeb748 100644 --- a/src/ui/dialog/inkscape-preferences.cpp +++ b/src/ui/dialog/inkscape-preferences.cpp @@ -394,7 +394,9 @@ InkscapePreferences::InkscapePreferences() initPageRendering(); initPageSpellcheck(); - signal_map().connect(sigc::mem_fun(*this, &InkscapePreferences::showPage)); + signal_map().connect([this]() { + showPage(Inkscape::Preferences::get()->getInt("/dialogs/preferences/page", 0)); + }); //calculate the size request for this dialog _page_list.expand_all(); @@ -2221,7 +2223,7 @@ static void profileComboChanged( Gtk::ComboBoxText* combo ) } } -static void proofComboChanged( Gtk::ComboBoxText* combo ) +static void cmsComboChanged( Gtk::ComboBoxText* combo ) { Glib::ustring active = combo->get_active_text(); auto cms_system = Inkscape::CMSSystem::get(); @@ -2229,7 +2231,7 @@ static void proofComboChanged( Gtk::ComboBoxText* combo ) if ( !path.empty() ) { Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - prefs->setString("/options/softproof/uri", path); + prefs->setString("/options/cms/uri", path); } } @@ -2242,7 +2244,7 @@ static void gamutColorChanged( Gtk::ColorButton* btn ) { gchar* tmp = g_strdup_printf("#%02x%02x%02x", (r >> 8), (g >> 8), (b >> 8) ); Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - prefs->setString("/options/softproof/gamutcolor", tmp); + prefs->setString("/options/cms/gamutcolor", tmp); g_free(tmp); } @@ -2373,9 +2375,8 @@ void InkscapePreferences::initPageIO() // CMS options Inkscape::Preferences *prefs = Inkscape::Preferences::get(); /* TRANSLATORS: see http://www.newsandtech.com/issues/2004/03-04/pt/03-04_rendering.htm */ - Glib::ustring const intentLabels[] = {_("Perceptual"), _("Relative Colorimetric"), - _("Saturation"), _("Absolute Colorimetric")}; - int const intentValues[] = {0, 1, 2, 3}; + Glib::ustring intentLabels[] = {_("Perceptual"), _("Relative Colorimetric"), _("Saturation"), _("Absolute Colorimetric"), _("Relative Colorimetric without Black Point Correction")}; + int intentValues[] = {0, 1, 2, 3, 4}; _page_cms.add_group_header( _("Display adjustment")); @@ -2391,25 +2392,13 @@ void InkscapePreferences::initPageIO() g_free(profileTip); profileTip = nullptr; - _cms_from_user.init( _("Use profile from user"), "/options/displayprofile/use_user_profile", false); - _page_cms.add_line( true, "", _cms_from_user, "", - _("Use a user specified ICC profile for monitor color correction. Warning: System wide color correction should be disabled."), false); - _cms_intent.init("/options/displayprofile/intent", intentLabels, intentValues, 0); _page_cms.add_line( true, _("Display rendering intent:"), _cms_intent, "", _("The rendering intent to use to calibrate display output"), false); - _page_cms.add_group_header( _("Proofing")); - - _cms_softproof.init( _("Simulate output on screen"), "/options/softproof/enable", false); - _page_cms.add_line( true, "", _cms_softproof, "", - _("Simulates output of target device"), false); + _page_cms.add_group_header( _("Device profile")); - _cms_gamutwarn.init( _("Mark out of gamut colors"), "/options/softproof/gamutwarn", false); - _page_cms.add_line( true, "", _cms_gamutwarn, "", - _("Highlights colors that are out of gamut for the target device"), false); - - Glib::ustring colorStr = prefs->getString("/options/softproof/gamutcolor"); + Glib::ustring colorStr = prefs->getString("/options/cms/gamutcolor"); Gdk::RGBA tmpColor( colorStr.empty() ? "#00ff00" : colorStr); _cms_gamutcolor.set_rgba( tmpColor ); @@ -2417,23 +2406,19 @@ void InkscapePreferences::initPageIO() _page_cms.add_line( true, _("Out of gamut warning color:"), _cms_gamutcolor, "", _("Selects the color used for out of gamut warning"), false); - _page_cms.add_line( true, _("Device profile:"), _cms_proof_profile, "", + _page_cms.add_line( true, _("Default profile:"), _cms_default_profile, "", _("The ICC profile to use to simulate device output"), false); - _cms_proof_intent.init("/options/softproof/intent", intentLabels, intentValues, 0); - _page_cms.add_line( true, _("Device rendering intent:"), _cms_proof_intent, "", + _cms_default_intent.init("/options/cms/intent", intentLabels, intentValues, 0); + _page_cms.add_line( true, _("Default rendering intent:"), _cms_default_intent, "", _("The rendering intent to use to calibrate device output"), false); - _cms_proof_blackpoint.init( _("Black point compensation"), "/options/softproof/bpc", false); - _page_cms.add_line( true, "", _cms_proof_blackpoint, "", - _("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" ); gint index = 0; - _cms_display_profile.append(_("")); + _cms_display_profile.append(_("Controlled by the Operating System")); index++; for (auto const &name : names) { _cms_display_profile.append( name ); @@ -2447,14 +2432,14 @@ void InkscapePreferences::initPageIO() _cms_display_profile.set_active(0); } - names = cms_system->get_softproof_profile_names(); - current = prefs->getString("/options/softproof/uri"); + names = cms_system->get_cms_profile_names(); + current = prefs->getString("/options/cms/uri"); index = 0; for (auto const &name : names) { - _cms_proof_profile.append( name ); + _cms_default_profile.append( name ); Glib::ustring path = cms_system->get_path_for_profile(name); if ( !path.empty() && path == current ) { - _cms_proof_profile.set_active(index); + _cms_default_profile.set_active(index); } index++; } @@ -2463,7 +2448,7 @@ void InkscapePreferences::initPageIO() _cms_gamutcolor.signal_color_set().connect(sigc::bind(&gamutColorChanged, &_cms_gamutcolor)); _cms_display_profile.signal_changed().connect(sigc::bind(&profileComboChanged, &_cms_display_profile)); - _cms_proof_profile.signal_changed().connect(sigc::bind(&proofComboChanged, &_cms_proof_profile)); + _cms_default_profile.signal_changed().connect(sigc::bind(&cmsComboChanged, &_cms_default_profile)); this->AddPage(_page_cms, _("Color management"), iter_io, PREFS_PAGE_IO_CMS); @@ -3817,25 +3802,6 @@ bool InkscapePreferences::GetSizeRequest(const Gtk::TreeModel::iterator& iter) return false; } -// Check if iter points to page indicated in preferences. -bool InkscapePreferences::matchPage(Gtk::TreeModel::const_iterator const &iter) -{ - auto const &row = *iter; - Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - int desired_page = prefs->getInt("/dialogs/preferences/page", 0); - _init = false; - if (desired_page == row[_page_list_columns._col_id]) - { - auto const path = _page_list.get_model()->get_path(iter); - _page_list.expand_to_path(path); - _page_list.get_selection()->select(iter); - if (desired_page == PREFS_PAGE_UI_THEME) - symbolicThemeCheck(); - return true; - } - return false; -} - void InkscapePreferences::on_reset_open_recent_clicked() { Glib::RefPtr manager = Gtk::RecentManager::get_default(); @@ -3910,10 +3876,23 @@ void InkscapePreferences::on_pagelist_selection_changed() } // Show page indicated in preferences file. -void InkscapePreferences::showPage() +void InkscapePreferences::showPage(int desired_page) { _search.set_text(""); - _page_list.get_model()->foreach_iter(sigc::mem_fun(*this, &InkscapePreferences::matchPage)); + _page_list.get_model()->foreach_iter([this,desired_page](Gtk::TreeModel::const_iterator const &iter) {; + auto const &row = *iter; + _init = false; + if (desired_page == row[_page_list_columns._col_id]) + { + auto const path = _page_list.get_model()->get_path(iter); + _page_list.expand_to_path(path); + _page_list.get_selection()->select(iter); + if (desired_page == PREFS_PAGE_UI_THEME) + symbolicThemeCheck(); + return true; + } + return false; + }); } } // namespace Inkscape::UI::Dialog diff --git a/src/ui/dialog/inkscape-preferences.h b/src/ui/dialog/inkscape-preferences.h index 41babd679d..956b2ca5d2 100644 --- a/src/ui/dialog/inkscape-preferences.h +++ b/src/ui/dialog/inkscape-preferences.h @@ -119,7 +119,7 @@ public: InkscapePreferences(); ~InkscapePreferences() final; - void showPage(); // Show page indicated by "/dialogs/preferences/page". + void showPage(int page) override; protected: Gtk::Frame _page_frame; @@ -502,16 +502,11 @@ protected: UI::Widget::PrefEntry _save_autosave_path; UI::Widget::PrefSpinButton _save_autosave_max; - Gtk::ComboBoxText _cms_display_profile; - UI::Widget::PrefCheckButton _cms_from_user; + Gtk::ComboBoxText _cms_display_profile; UI::Widget::PrefCombo _cms_intent; - - UI::Widget::PrefCheckButton _cms_softproof; - UI::Widget::PrefCheckButton _cms_gamutwarn; - Gtk::ColorButton _cms_gamutcolor; - Gtk::ComboBoxText _cms_proof_profile; - UI::Widget::PrefCombo _cms_proof_intent; - UI::Widget::PrefCheckButton _cms_proof_blackpoint; + Gtk::ColorButton _cms_gamutcolor; + Gtk::ComboBoxText _cms_default_profile; + UI::Widget::PrefCombo _cms_default_intent; Gtk::Notebook _grids_notebook; UI::Widget::PrefRadioButton _grids_no_emphasize_on_zoom; @@ -639,7 +634,6 @@ protected: Gtk::TreeModel::iterator AddPage(UI::Widget::DialogPage& p, Glib::ustring title, Gtk::TreeModel::iterator parent, int id); Gtk::TreePath get_next_result(Gtk::TreeModel::iterator& iter, bool check_children = true); Gtk::TreePath get_prev_result(Gtk::TreeModel::iterator& iter, bool iterate = true); - bool matchPage(Gtk::TreeModel::const_iterator const &iter); static void AddSelcueCheckbox(UI::Widget::DialogPage& p, Glib::ustring const &prefs_path, bool def_value); static void AddGradientCheckbox(UI::Widget::DialogPage& p, Glib::ustring const &prefs_path, bool def_value); diff --git a/src/ui/icon-loader.cpp b/src/ui/icon-loader.cpp index b45ec55817..0262ae2964 100644 --- a/src/ui/icon-loader.cpp +++ b/src/ui/icon-loader.cpp @@ -25,7 +25,14 @@ #include "desktop.h" #include "inkscape.h" +#include "ui/util.h" +Glib::RefPtr get_icon_theme() +{ + Glib::RefPtr display = Gdk::Display::get_default(); + Glib::RefPtr screen = display->get_default_screen(); + return Gtk::IconTheme::get_for_screen(screen); +} Gtk::Image *sp_get_icon_image(Glib::ustring const &icon_name, int size) { diff --git a/src/ui/widget/canvas-grid.cpp b/src/ui/widget/canvas-grid.cpp index 46360b1602..1c157dfad8 100644 --- a/src/ui/widget/canvas-grid.cpp +++ b/src/ui/widget/canvas-grid.cpp @@ -41,6 +41,7 @@ #include "io/resource.h" #include "object/sp-grid.h" #include "object/sp-root.h" +#include "ui/controller.h" #include "ui/builder-utils.h" #include "ui/controller.h" #include "ui/dialog/command-palette.h" @@ -49,6 +50,7 @@ #include "ui/widget/desktop-widget.h" // Hopefully temp. #include "ui/widget/events/canvas-event.h" #include "ui/widget/canvas-notice.h" +#include "ui/widget/cms-popover.h" #include "ui/widget/ink-ruler.h" #include "util/units.h" @@ -119,7 +121,8 @@ CanvasGrid::CanvasGrid(SPDesktopWidget *dtw) _vscrollbar.set_name("CanvasScrollbar"); _vscrollbar.set_vexpand(true); - // CMS Adjust (To be replaced by Gio::Action) + // CMS Adjust + _cms_popover = Inkscape::UI::create_builder("cms-popover.glade"); _cms_adjust.set_name("CMS_Adjust"); _cms_adjust.add(*Gtk::make_managed("color-management", Gtk::ICON_SIZE_MENU)); // Can't access via C++ API, fixed in Gtk4. @@ -127,14 +130,9 @@ CanvasGrid::CanvasGrid(SPDesktopWidget *dtw) _cms_adjust.set_tooltip_text(_("Toggle color-managed display for this document window")); // popover with some common display mode related options - _builder_display_popup = create_builder("display-popup.glade"); - auto popover = &get_widget (_builder_display_popup, "popover"); - auto sticky_zoom = &get_widget(_builder_display_popup, "zoom-resize"); - - // To be replaced by Gio::Action: - sticky_zoom->signal_toggled().connect([this](){ _dtw->sticky_zoom_toggled(); }); + _display_popup = Inkscape::UI::create_builder("display-popup.glade"); _quick_actions.set_name("QuickActions"); - _quick_actions.set_popover(*popover); + _quick_actions.set_popover(Inkscape::UI::get_widget(_display_popup, "popover")); _quick_actions.set_image_from_icon_name("display-symbolic"); _quick_actions.set_direction(Gtk::ARROW_LEFT); _quick_actions.set_tooltip_text(_("Display options")); @@ -185,18 +183,14 @@ void CanvasGrid::on_realize() { } if (!id.empty()) { - // if CMS is ON show alternative icons - if (_canvas->get_cms_active()) { - id += "-alt"; - } _quick_actions.set_image_from_icon_name(id + "-symbolic"); } }; set_display_icon(); - // when display mode state changes, update icon - auto cms_action = Glib::RefPtr::cast_dynamic(map->lookup_action("canvas-color-manage")); + // when display mode state changes, update icon, TODO: This should watch prefs instead + auto cms_action = Glib::RefPtr::cast_dynamic(map->lookup_action("canvas-color-displayprofile")); auto disp_action = Glib::RefPtr::cast_dynamic(map->lookup_action("canvas-display-mode")); if (cms_action && disp_action) { @@ -204,7 +198,7 @@ void CanvasGrid::on_realize() { cms_action-> signal_activate().connect([=](const Glib::VariantBase& state){ set_display_icon(); }); } else { - g_warning("No canvas-display-mode and/or canvas-color-manage action available to canvas-grid"); + g_warning("No canvas-display-mode and/or canvas-color-displayprofile action available to canvas-grid"); } } else { @@ -214,11 +208,6 @@ void CanvasGrid::on_realize() { parent_type::on_realize(); } -// TODO: remove when sticky zoom gets replaced by Gio::Action: -Gtk::ToggleButton* CanvasGrid::GetStickyZoom() { - return &get_widget(_builder_display_popup, "zoom-resize"); -} - // _dt2r should be a member of _canvas. // get_display_area should be a member of _canvas. void CanvasGrid::updateRulers() @@ -713,8 +702,6 @@ void CanvasGrid::_adjustmentChanged() } // TODO Add actions so we can set shortcuts. -// * Sticky Zoom -// * CMS Adjust // * Guide Lock } // namespace Inkscape::UI::Widget diff --git a/src/ui/widget/canvas-grid.h b/src/ui/widget/canvas-grid.h index ae13f7ffd2..23cbccde10 100644 --- a/src/ui/widget/canvas-grid.h +++ b/src/ui/widget/canvas-grid.h @@ -116,9 +116,10 @@ private: std::unique_ptr _vruler; Gtk::ToggleButton _guide_lock; - Gtk::ToggleButton _cms_adjust; + Gtk::MenuButton _cms_adjust; Gtk::MenuButton _quick_actions; - Glib::RefPtr _builder_display_popup; + Glib::RefPtr _display_popup; + Glib::RefPtr _cms_popover; // To be replaced by stateful Gio::Actions bool _show_scrollbars = true; diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp index 312956f2c4..bd70653e27 100644 --- a/src/ui/widget/canvas.cpp +++ b/src/ui/widget/canvas.cpp @@ -153,7 +153,7 @@ struct RedrawData bool decoupled_mode; Cairo::RefPtr snapshot_drawn; Geom::OptIntRect grabbed; - std::shared_ptr cms_transform; + std::shared_ptr screen_transform; // Saved prefs int coarsener_min_size; @@ -232,6 +232,10 @@ public: std::unique_ptr updater; // Tracks the unclean region and decides how to redraw it. Cairo::RefPtr invalidated; // Buffers invalidations while the updater is in use by the background process. + // CMS + std::shared_ptr colorproof_transform; + std::shared_ptr gamutwarn_transform; + // Graphics state; holds all the graphics resources, including the drawn content. std::unique_ptr graphics; void activate_graphics(); @@ -341,8 +345,7 @@ Canvas::Canvas() d->prefs.debug_sticky_decoupled.action = [=] { d->schedule_redraw(); }; d->prefs.debug_animate.action = [=] { queue_draw(); }; d->prefs.outline_overlay_opacity.action = [=] { queue_draw(); }; - d->prefs.softproof.action = [=] { set_cms_transform(); redraw_all(); }; - d->prefs.displayprofile.action = [=] { set_cms_transform(); redraw_all(); }; + d->prefs.displayprofile.action = [=] { set_screen_transform(); redraw_all(); }; d->prefs.request_opengl.action = [=] { if (get_realized()) { d->deactivate(); @@ -380,9 +383,8 @@ Canvas::Canvas() _split_direction = SplitDirection::EAST; _split_frac = {0.5, 0.5}; - // CMS Set initial CMS transform. - set_cms_transform(); - // If we have monitor dependence: signal_map().connect([this]() { this->set_cms_transform(); }); + // Set initial color managed screen transform. + set_screen_transform(); // Recreate stores on HiDPI change. property_scale_factor().signal_changed().connect([this] { d->schedule_redraw(); }); @@ -603,6 +605,7 @@ void CanvasPrivate::launch_redraw() graphics->set_background_in_stores(background_in_stores_required()); q->_drawing->setClip(calc_page_clip()); + q->_drawing->setColorProofTransform(colorproof_transform, gamutwarn_transform); // Stores. handle_stores_action(stores.update(Fragment{ q->_affine, q->get_area_world() })); @@ -679,7 +682,7 @@ void CanvasPrivate::launch_redraw() rd.snapshot_drawn = stores.snapshot().drawn ? stores.snapshot().drawn->copy() : Cairo::RefPtr(); rd.grabbed = q->_grabbed_canvas_item && prefs.block_updates ? (roundedOutwards(q->_grabbed_canvas_item->get_bounds()) & rd.visible & rd.store.rect).regularized() : Geom::OptIntRect(); - rd.cms_transform = q->_cms_active ? q->_cms_transform : nullptr; + rd.screen_transform = q->_screen_transform; abort_flags.store((int)AbortFlags::None, std::memory_order_relaxed); @@ -1734,6 +1737,15 @@ void Canvas::set_antialiasing_enabled(bool enabled) } } +void Canvas::set_cms_transforms(std::shared_ptr colorproof, std::shared_ptr gamutwarn) +{ + if (colorproof != d->colorproof_transform || gamutwarn != d->gamutwarn_transform) { + d->colorproof_transform = colorproof; + d->gamutwarn_transform = gamutwarn; + d->schedule_redraw(); + } +} + void Canvas::set_clip_to_page_mode(bool clip) { if (clip != d->clip_to_page) { @@ -1791,20 +1803,9 @@ bool CanvasPrivate::is_point_on_page(const Geom::Point &point) const return false; } -// Set the cms transform -void Canvas::set_cms_transform() +void Canvas::set_screen_transform() { - // TO DO: Select per monitor. Note Gtk has a bug where the monitor is not correctly reported on start-up. - // auto display = get_display(); - // auto monitor = display->get_monitor_at_window(get_window()); - // std::cout << " " << monitor->get_manufacturer() << ", " << monitor->get_model() << std::endl; - - // gtk4 - // 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 */ ); + _screen_transform = CMSSystem::get()->get_screen_transform(); } // Change cursor @@ -2404,16 +2405,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. - 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(); + // 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.screen_transform) { + Inkscape::CMSSystem::do_transform(rd.screen_transform->getHandle(), 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 09b47fee57..bf98692e75 100644 --- a/src/ui/widget/canvas.h +++ b/src/ui/widget/canvas.h @@ -93,10 +93,7 @@ public: Inkscape::SplitMode get_split_mode() const { return _split_mode; } void set_clip_to_page_mode(bool clip); void set_antialiasing_enabled(bool enabled); - - // CMS - void set_cms_active(bool active) { _cms_active = active; } - bool get_cms_active() const { return _cms_active; } + void set_cms_transforms(std::shared_ptr colorproof, std::shared_ptr gamutwarn); /* Observers */ @@ -188,9 +185,8 @@ private: bool _antialiasing_enabled = true; // CMS - bool _cms_active = false; - std::shared_ptr _cms_transform; ///< The lcms transform to apply to canvas. - void set_cms_transform(); ///< Set the lcms transform. + std::shared_ptr _screen_transform; ///< The lcms transform to apply for the screen + void set_screen_transform(); ///< Set the screen lcms transform. /* Internal state */ diff --git a/src/ui/widget/canvas/prefs.h b/src/ui/widget/canvas/prefs.h index 9f81c6b221..befa7519d8 100644 --- a/src/ui/widget/canvas/prefs.h +++ b/src/ui/widget/canvas/prefs.h @@ -24,9 +24,7 @@ public: Pref numthreads = { "/options/threading/numthreads", 0, 1, 256 }; // Colour management - Pref use_user_profile = { "/options/displayprofile/use_user_profile" }; Pref displayprofile = { "/options/displayprofile" }; - Pref softproof = { "/options/softproof" }; // Auto-scrolling Pref autoscrolldistance = { "/options/autoscrolldistance/value", 0, -1000, 10000 }; diff --git a/src/ui/widget/cms-popover.cpp b/src/ui/widget/cms-popover.cpp new file mode 100644 index 0000000000..10318cc4a8 --- /dev/null +++ b/src/ui/widget/cms-popover.cpp @@ -0,0 +1,132 @@ +// 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. + */ + +#include "cms-popover.h" + +#include "gtkmm/image.h" + +#include "ui/icon-loader.h" +#include "ui/builder-utils.h" + +#include "desktop.h" +#include "desktop-widget.h" +#include "document.h" +#include "color/manager.h" + +namespace Inkscape::UI::Widget { + +static void _set_icons(Gtk::Box &box, std::string const &type, std::vector &&colors) +{ + int index = 0; + for (Gtk::Widget *child : box.get_children()) { + if (auto btn = dynamic_cast(child)) { + if (auto img = dynamic_cast(btn->get_image())) { + img->set(sp_get_cms_icon(type, colors[index])); + } + } + index++; + } +} + +CmsPopover::CmsPopover(BaseObjectType *cobj, Glib::RefPtr const &builder, SPDesktopWidget* dtw) + : Gtk::Popover(cobj) + , _builder(builder) + , _colors_label(get_widget(_builder, "colors-label")) + , _colors_rgb(get_widget(_builder, "colors-rgb")) + , _colors_cmyk(get_widget(_builder, "colors-cmyk")) + , _dtw(dtw) +{ +} + +void CmsPopover::on_show() +{ + // Update ink icons and show/hide the right things. + auto desktop = _dtw->get_desktop(); + auto document = desktop->getDocument(); + auto &manager = document->getColorManager(); + + // Remove myself if the desktop replaces the document at any time + document_replaced_connection = desktop->connectDocumentReplaced([this](SPDesktop *, SPDocument *) { + this->hide(); + }); + + // If anything changes while the popup is open, refresh our UI + profiles_changed_connection = manager.connectChanged([this, document]() { + this->refresh(document); + }); + profiles_modified_connection = manager.connectModified([this, document](ColorProfile *cp) { + this->refresh(document); + }); + + refresh(document); + Gtk::Popover::on_show(); +} + + +void CmsPopover::refresh(SPDocument *document) +{ + if (!_dtw || !_dtw->get_desktop()) + return; + + auto &manager = document->getColorManager(); + _profile = manager.getDefault(); + + if (_profile && _profile->isPrintColorSpace()) { + _colors_cmyk.show(); + _colors_rgb.hide(); + _colors_label.set_text(_profile->getName()); + + _set_icons(_colors_cmyk, "ink", { + Gdk::RGBA("cyan"), + Gdk::RGBA("magenta"), + Gdk::RGBA("yellow"), + Gdk::RGBA("black"), + }); + // TODO: Set CMYK color seperator button actions + } else { + _colors_cmyk.hide(); + _colors_rgb.show(); + // No translation needed + _colors_label.set_text("sRGB"); + _set_icons(_colors_rgb, "rgb", { + Gdk::RGBA("red"), + Gdk::RGBA("green"), + Gdk::RGBA("blue"), + }); + // TODO: Set RGB color seperator button actions + } + + // FUTURE: deal with spot colors here. + // 1. Look at all swatches which have the icc profile AND are set as spot colors + // 2. Duplicate the spot color button and generate an ink icon for it + // 3. Set the color seperator action + +} + +void CmsPopover::on_hide() +{ + profiles_modified_connection.disconnect(); + profiles_changed_connection.disconnect(); + document_replaced_connection.disconnect(); + Gtk::Popover::on_hide(); +} + + +} // 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/cms-popover.h b/src/ui/widget/cms-popover.h new file mode 100644 index 0000000000..5774d92287 --- /dev/null +++ b/src/ui/widget/cms-popover.h @@ -0,0 +1,68 @@ +// 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 INKSCAPE_UI_WIDGET_CMS_POPOVER_H +#define INKSCAPE_UI_WIDGET_CMS_POPOVER_H + +#include +#include +#include +#include "gtkmm/togglebutton.h" +#include +#include "helper/auto-connection.h" + +class SPDesktopWidget; +class SPDocument; + +namespace Inkscape { + class ColorProfile; +}; + +namespace Inkscape::UI::Widget { + +class CmsPopover final : public Gtk::Popover +{ +public: + CmsPopover() = default; + CmsPopover(BaseObjectType *cobj, Glib::RefPtr const &, SPDesktopWidget *); + ~CmsPopover() override {}; + +private: + void refresh(SPDocument *document); + + Glib::RefPtr _builder; + Gtk::Label &_colors_label; + Gtk::Box &_colors_rgb; + Gtk::Box &_colors_cmyk; + + SPDesktopWidget *_dtw = nullptr; + Inkscape::ColorProfile *_profile = nullptr; + + Inkscape::auto_connection document_replaced_connection; + Inkscape::auto_connection profiles_changed_connection; + Inkscape::auto_connection profiles_modified_connection; +protected: + void on_show() override; + void on_hide() override; +}; + +} // namespace Inkscape::UI::Widget + +#endif // INKSCAPE_UI_WIDGET_CMS_POPOVER_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-icc-selector.cpp b/src/ui/widget/color-icc-selector.cpp deleted file mode 100644 index 431d01d216..0000000000 --- a/src/ui/widget/color-icc-selector.cpp +++ /dev/null @@ -1,938 +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" -#include "ui/widget/scrollprotected.h" - -#define noDEBUG_LCMS - -#include "object/color-profile.h" -#include "color/color-profile-cms-fns.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_show_all(dialog); \ - } \ - } -#endif // DEBUG_LCMS - -static constexpr int XPAD = 4; -static constexpr int YPAD = 1; - -namespace { - -GtkWidget *_scrollprotected_combo_box_new_with_model(GtkTreeModel *model) -{ - auto const 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(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 = _scrollprotected_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); - gtk_widget_set_no_show_all(_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); - _impl->_compUI[i]._slider->set_no_show_all(); - - 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(); - gtk_widget_set_tooltip_text(_impl->_compUI[i]._btn, (i < things.size()) ? things[i].tip.c_str() : ""); - sp_dialog_defocus_on_enter(_impl->_compUI[i]._btn); - gtk_label_set_mnemonic_widget(GTK_LABEL(_impl->_compUI[i]._label), _impl->_compUI[i]._btn); - gtk_widget_set_visible(_impl->_compUI[i]._btn, true); - gtk_widget_set_no_show_all(_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(); - gtk_widget_set_tooltip_text(_impl->_sbtn, _("Alpha (opacity)")); - sp_dialog_defocus_on_enter(_impl->_sbtn); - 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 = cmsChannelsOf(asICColorSpaceSig(newProf->getColorSpace())); - - std::vector things = - colorspace::getColorSpaceInfo(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)) { - _profChannelCount = _prof->getChannelCount(); - - if (profChanged) { - std::vector things = - colorspace::getColorSpaceInfo(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 ac1f6b713e..5823cdcd5d 100644 --- a/src/ui/widget/color-notebook.cpp +++ b/src/ui/widget/color-notebook.cpp @@ -26,8 +26,8 @@ #include "document.h" #include "inkscape.h" #include "preferences.h" -#include "profile-manager.h" #include "color/cms-system.h" +#include "color/manager.h" #include "object/color-profile.h" #include "ui/dialog-events.h" #include "ui/icon-loader.h" @@ -278,12 +278,12 @@ void ColorNotebook::_updateICCButtons() /* update out-of-gamut icon */ Inkscape::ColorProfile *target_profile = - _document->getProfileManager().find(name.c_str()); + _document->getColorManager().find(name); if (target_profile) gtk_widget_set_sensitive(_box_outofgamut, target_profile->GamutCheck(color)); /* update too-much-ink icon */ - Inkscape::ColorProfile *prof = _document->getProfileManager().find(name.c_str()); + Inkscape::ColorProfile *prof = _document->getColorManager().find(name); if (prof && prof->isPrintColorSpace()) { gtk_widget_set_visible(_box_toomuchink, true); double ink_sum = 0; diff --git a/src/ui/widget/color-scales.cpp b/src/ui/widget/color-scales.cpp index 215613ef90..655366c8da 100644 --- a/src/ui/widget/color-scales.cpp +++ b/src/ui/widget/color-scales.cpp @@ -31,7 +31,6 @@ #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" @@ -69,12 +68,11 @@ static const char* color_mode_icons[] = { "color-selector-hsx", "color-selector-hsluv", "color-selector-okhsl", - "color-selector-cms", nullptr }; 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) { @@ -97,7 +95,6 @@ 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(); default: throw std::invalid_argument("There's no factory for the requested color mode"); } @@ -113,7 +110,6 @@ std::vector get_color_pickers() { SPColorScalesMode::CMYK, SPColorScalesMode::OKLAB, SPColorScalesMode::HSLUV, - SPColorScalesMode::CMS }) { auto label = get_color_mode_label(mode); diff --git a/src/ui/widget/color-scales.h b/src/ui/widget/color-scales.h index 76d8a7c0a7..0ef0eb329e 100644 --- a/src/ui/widget/color-scales.h +++ b/src/ui/widget/color-scales.h @@ -34,8 +34,7 @@ enum class SPColorScalesMode { CMYK, HSV, HSLUV, - OKLAB, - CMS + OKLAB }; template diff --git a/src/ui/widget/color-slider.cpp b/src/ui/widget/color-slider.cpp index 2aab2bdcba..703f1e0f43 100644 --- a/src/ui/widget/color-slider.cpp +++ b/src/ui/widget/color-slider.cpp @@ -126,6 +126,10 @@ Gtk::EventSequenceState ColorSlider::on_click_released(Gtk::GestureMultiPress co void ColorSlider::on_motion(GtkEventControllerMotion const * const motion, double const x, double const y) { + auto const state = Controller::get_device_state(GTK_EVENT_CONTROLLER(motion)); + if (!(state & (GDK_BUTTON1_MASK | GDK_BUTTON2_MASK | GDK_BUTTON3_MASK))) { + _dragging = false; + } if (_dragging) { auto const value = get_value_at(*_drawing_area, x, y); auto const state = Controller::get_device_state(GTK_EVENT_CONTROLLER(motion)); diff --git a/src/ui/widget/desktop-widget.cpp b/src/ui/widget/desktop-widget.cpp index e189cf1df8..c4ad84579d 100644 --- a/src/ui/widget/desktop-widget.cpp +++ b/src/ui/widget/desktop-widget.cpp @@ -168,9 +168,6 @@ SPDesktopWidget::SPDesktopWidget(InkscapeWindow *inkscape_window, SPDocument *do /* Canvas */ _canvas = _canvas_grid->GetCanvas(); - _ds_sticky_zoom = prefs->createObserver("/options/stickyzoom/value", [this]() { sticky_zoom_updated(); }); - sticky_zoom_updated(); - /* Dialog Container */ _container = std::make_unique(inkscape_window); _columns = _container->get_columns(); @@ -310,17 +307,12 @@ SPDesktopWidget::updateTitle(gchar const* uri) Name += N_("enhance thin lines"); } else if (render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY) { Name += N_("outline overlay"); - } - - if (color_mode != Inkscape::ColorMode::NORMAL && - render_mode != Inkscape::RenderMode::NORMAL) { - Name += ", "; - } - - if (color_mode == Inkscape::ColorMode::GRAYSCALE) { + } else if (color_mode == Inkscape::ColorMode::GRAYSCALE) { Name += N_("grayscale"); - } else if (color_mode == Inkscape::ColorMode::PRINT_COLORS_PREVIEW) { - Name += N_("print colors preview"); + } else if (color_mode == Inkscape::ColorMode::COLORPROOF) { + Name += N_("color proof"); + } else if (color_mode == Inkscape::ColorMode::GAMUTWARN) { + Name += N_("gamut checking"); } if (*Name.rbegin() == '(') { // Can not use C++11 .back() or .pop_back() with ustring! @@ -831,20 +823,6 @@ void SPDesktopWidget::onFocus(bool const has_focus) // ------------------------ Zoom ------------------------ -void -SPDesktopWidget::sticky_zoom_toggled() -{ - Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - prefs->setBool("/options/stickyzoom/value", _canvas_grid->GetStickyZoom()->get_active()); -} - -void -SPDesktopWidget::sticky_zoom_updated() -{ - Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - _canvas_grid->GetStickyZoom()->set_active(prefs->getBool("/options/stickyzoom/value", false)); -} - void SPDesktopWidget::update_zoom() { -- GitLab From 58740166b163b1cbd1714d79409125971700e3db Mon Sep 17 00:00:00 2001 From: Martin Owens Date: Mon, 9 Oct 2023 16:08:48 -0400 Subject: [PATCH 2/3] Refactor icc use in SPColor and remove duplications --- src/color.cpp | 300 +++--- src/color.h | 46 +- src/color/cms-system.cpp | 1 - src/color/cms-system.h | 16 + src/color/components.cpp | 105 ++- src/color/components.h | 15 +- src/color/spaces.cpp | 17 + src/color/spaces.h | 81 ++ src/display/cairo-utils.cpp | 3 +- src/display/drawing-paintserver.cpp | 6 +- src/display/nr-filter-diffuselighting.cpp | 9 - src/display/nr-filter-diffuselighting.h | 6 - src/display/nr-filter-flood.cpp | 16 +- src/display/nr-filter-flood.h | 4 - src/display/nr-filter-specularlighting.cpp | 9 - src/display/nr-filter-specularlighting.h | 4 - src/display/nr-style.cpp | 2 +- .../internal/pdfinput/svg-builder.cpp | 1 - src/object/color-profile.cpp | 34 +- src/object/filters/diffuselighting.cpp | 36 +- src/object/filters/diffuselighting.h | 9 +- src/object/filters/flood.cpp | 36 +- src/object/filters/flood.h | 6 +- src/object/filters/specularlighting.cpp | 31 +- src/object/filters/specularlighting.h | 9 +- src/object/sp-gradient.cpp | 12 +- src/object/sp-mesh-array.cpp | 20 +- src/object/sp-mesh-gradient.cpp | 4 +- src/object/sp-solid-color.cpp | 3 +- src/style-internal.cpp | 3 +- src/style-internal.h | 2 - src/svg/CMakeLists.txt | 1 - src/svg/svg-color.cpp | 138 --- src/svg/svg-color.h | 13 +- src/svg/svg-icc-color.h | 38 - src/ui/icon-loader.cpp | 48 + src/ui/icon-loader.h | 4 + src/ui/selected-color.cpp | 17 - src/ui/tools/tweak-tool.cpp | 36 +- src/ui/util.cpp | 22 + src/ui/util.h | 3 + src/ui/widget/canvas-grid.cpp | 18 +- src/ui/widget/canvas-grid.h | 4 +- src/ui/widget/cms-popover.cpp | 11 +- src/ui/widget/color-notebook.cpp | 3 +- src/ui/widget/color-scales.cpp | 870 +++++++----------- src/ui/widget/color-scales.h | 42 +- src/ui/widget/color-slider.cpp | 16 +- src/ui/widget/paint-selector.cpp | 6 +- src/util/object-renderer.cpp | 3 +- testfiles/src/attributes-test.cpp | 1 + testfiles/src/color-profile-test.cpp | 30 +- testfiles/src/svg-color-test.cpp | 12 +- 53 files changed, 986 insertions(+), 1196 deletions(-) create mode 100644 src/color/spaces.cpp create mode 100644 src/color/spaces.h delete mode 100644 src/svg/svg-icc-color.h diff --git a/src/color.cpp b/src/color.cpp index 63c049875e..8e8bb9bd99 100644 --- a/src/color.cpp +++ b/src/color.cpp @@ -16,7 +16,6 @@ #include #include #include -#include #include #include "color.h" @@ -25,15 +24,9 @@ #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) @@ -57,7 +50,7 @@ SPColor &SPColor::operator=(SPColor const &other) return *this; } - set(other.v.c[0], other.v.c[1], other.v.c[2]); + //set(other.v.c[0], other.v.c[1], other.v.c[2]); copyColors(other); return *this; } @@ -67,12 +60,13 @@ SPColor &SPColor::operator=(SPColor const &other) */ bool SPColor::operator == (SPColor const& other) const { - bool match = + bool match = false; + /* (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); +*/ + match &= profileMatches(*this, other); return match; } @@ -83,19 +77,19 @@ bool SPColor::operator == (SPColor const& other) const */ bool SPColor::isClose(SPColor const& other, float epsilon) const { - bool match = (fabs((v.c[0]) - (other.v.c[0])) < epsilon) + /*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); + && (fabs((v.c[2]) - (other.v.c[2])) < epsilon);*/ - match &= profileMatches(_icc, other._icc); + //match &= profileMatches(*this, other); - return match; + return false; //match; } /** * Matches two profile colors within PROFILE_EPSILON distance. */ -static bool profileMatches(SVGICCColor const &first, SVGICCColor const &second) +bool SPColor::profileMatches(SPColor const &first, SPColor const &second) { if (first.colorProfile != second.colorProfile || first.colors.size() != second.colors.size()) { return false; @@ -110,136 +104,141 @@ static bool profileMatches(SVGICCColor const &first, SVGICCColor const &second) } /** - * Sets RGB values and colorspace in color. - * \pre 0 <={r,g,b}<=1 + * Sets the color in the specified color space, replacing any + * existing color space and removing any icc profile. + * + * @arg space - The new color space to use + * @arg colors - A vector of doubles between 0 and 1 for each channel */ -void SPColor::set(float r, float g, float b) +/*void SPColor::set(Inkscape::Color::Space space, std::vector const &colors) { - 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(); -} + _profile.clear(); + _space = space; + update(_space, colors); +}*/ /** - * Converts 32bit value to RGB floats and sets color. + * Sets the color in the specified profile color space, replacing any + * existing color and it's associated profile. + * + * @args profile - The new color profile to set. + * @args colors - A vector of doubles between 0 and 1 for each channel */ -void SPColor::set(guint32 value) +/*void SPColor::set(ColorProfile *profile, std::vector const &colors) { - 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(); -} + if (!profile) { + g_error("Color profile must exist when setting a profile color."); + return; + } + set(profile->getColorSpace(), colors); + _profile = profile; +}*/ /** - * Returns true if this color has a color profile set. + * Updates the color, keeping the internal color space and icc profile and converting + * the given color from it's input color space if its different. + * + * If the icc profile color space is the same as the input color space, no conversion is + * done and it is assumed the values are *within* the profile space. + * + * @arg in_space - The color space of the input colors + * @arg in_colors - A vector of soubles between 0 and 1 for each input channel + * @arg in_profile - An optional profile which overrides in_space for conversion */ -bool SPColor::hasColorProfile() const -{ - return !_icc.colorProfile.empty(); -} +/*void SPColor::update(Color::Space in_space, std::vector const &in_colors, ColorProfile *in_profile = nullptr) +{ + std::vector colors; + if (_space == in_space) { + colors = in_colors; + } else if (_profile) { + if (in_profile) + colors = convert(in_profile, in_colors, _profile); + else + colors = convert(in_space, in_colors, _profile); + } else { + if (in_profile) + colors = convert(in_profile, in_colors, _space); + else + colors = convert(in_space, in_colors, _space); + } + _colors.clear(); + for (int color : colors) { + if (color < 0.0 || color > 1.0) { + g_error("Color channel set out of bounds. 0.0<=%f<=1.0", color) + color = 0.0; + } + _colors.push_back(color); + } +}*/ /** - * Returns true if there is a defined color for the set profile. + * Converts 32bit value to RGB floats and sets color, forcing this color to be sRGB. */ -bool SPColor::hasColors() const +void SPColor::set(guint32 value) { - return hasColorProfile() && !_icc.colors.empty() && _icc.colors[0] != -1.0; +/* set(Inkscape::Color::Space::RGB, + {(value >> 24) / 255.0F, + ((value >> 16) & 0xff) / 255.0F, + ((value >> 8) & 0xff) / 255.0F});*/ } - -void SPColor::setColorProfile(Inkscape::ColorProfile *profile) +void SPColor::set(float r, float g, float b) { - unsetColorProfile(); - if (profile) { - _icc.colorProfile = profile->getName(); - 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); + g_warning("NOPE"); } void SPColor::copyColors(const SPColor &other) { - if (!profileMatches(_icc, other._icc)) { - _icc = other._icc; // copy + if (!profileMatches(*this, other)) { + colors = other.colors; + colorProfile = other.colorProfile; } } void SPColor::setColor(unsigned int index, double value) { - if (index < 0 || index > _icc.colors.size()) { + if (index < 0 || index > colors.size()) { g_warning("Can't set profile-based color, index out of range."); } - _icc.colors[index] = value; + 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() +void SPColor::unset() { - _icc.colorProfile = ""; - _icc.colors.clear(); +// _space = Color::Space::NONE; +// _profile.clear(); +// colors.clear(); } /** * Convert SPColor with integer alpha value to 32bit RGBA value. * \pre alpha < 256 */ -guint32 SPColor::toRGBA32( int alpha ) const +guint32 SPColor::toRGBA32(int alpha) const { - return_val_if_fail (alpha <= 0xff, 0x0); +// 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]), +/* 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; + alpha );*/ + return 0; //rgba; } /** * Convert SPColor with float alpha value to 32bit RGBA value. * \pre color != NULL && 0 <= alpha <= 1 */ -guint32 SPColor::toRGBA32( double alpha ) const +guint32 SPColor::toRGBA32(double alpha) const { - return_val_if_fail(alpha >= 0.0, 0x0); - return_val_if_fail(alpha <= 1.0, 0x0); + //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)) ); } @@ -256,8 +255,8 @@ std::string SPColor::toString() const if ( !css.str().empty() ) { css << " "; } - css << "icc-color(" << _icc.colorProfile; - for (double color : _icc.colors) { + css << "icc-color(" << colorProfile; + for (double color : colors) { css << ", " << color; } css << ')'; @@ -276,6 +275,7 @@ bool SPColor::fromString(char const *str) { guint32 const rgb0 = sp_svg_read_color(str, &str, 0xff); if (rgb0 == 0xff) { + unset(); return false; } set(rgb0); @@ -283,9 +283,9 @@ bool SPColor::fromString(char const *str) ++str; } if (strneq(str, "icc-color(", 10)) { - if (!sp_svg_read_icc_color(str, &str, &_icc)) { + if (!read_icc_color(str, &str)) { g_warning("Couldn't parse icc-color format in css."); - unsetColorProfile(); + unset(); } } return true; @@ -298,14 +298,14 @@ bool SPColor::fromString(char const *str) void SPColor::get_rgb_floatv(float *rgb) const { - return_if_fail (rgb != nullptr); + //return_if_fail (rgb != nullptr); if (!is_set()) { return; } - rgb[0] = v.c[0]; - rgb[1] = v.c[1]; - rgb[2] = v.c[2]; + rgb[0] = 0; //v.c[0]; + rgb[1] = 0; //v.c[1]; + rgb[2] = 0; //v.c[2]; } /** @@ -315,15 +315,15 @@ SPColor::get_rgb_floatv(float *rgb) const void SPColor::get_cmyk_floatv(float *cmyk) const { - return_if_fail (cmyk != nullptr); + //return_if_fail (cmyk != nullptr); if (!is_set()) { return; } - SPColor::rgb_to_cmyk_floatv( cmyk, + /*SPColor::rgb_to_cmyk_floatv( cmyk, v.c[0], v.c[1], - v.c[2] ); + v.c[2] );*/ } /* Plain mode helpers */ @@ -556,6 +556,98 @@ SPColor::hsluv_to_rgb_floatv(float *rgb, float h, float s, float l) } } +/* + * Some discussion at http://markmail.org/message/bhfvdfptt25kgtmj + * Allowed ASCII first characters: ':', 'A'-'Z', '_', 'a'-'z' + * Allowed ASCII remaining chars add: '-', '.', '0'-'9', + */ +bool SPColor::read_icc_color(gchar const *str, gchar const **end_ptr) +{ + bool good = true; + + if (end_ptr) { + *end_ptr = str; + } + colorProfile.clear(); + 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 == '.') ) { + 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 ) { + 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 { + colorProfile.clear(); + colors.clear(); + } + + return good; +} + /* Local Variables: mode:c++ diff --git a/src/color.h b/src/color.h index 9a3fa133ff..f90939d810 100644 --- a/src/color.h +++ b/src/color.h @@ -17,7 +17,7 @@ #include -#include "svg/svg-icc-color.h" +#include "color/components.h" typedef unsigned int guint32; // uint is guaranteed to hold up to 2^32 − 1 @@ -38,11 +38,10 @@ typedef unsigned int guint32; // uint is guaranteed to hold up to 2^32 − 1 #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; - +using Inkscape::Color::Space; namespace Inkscape { class ColorProfile; -}; +} /** * An RGB color with optional icc-color part @@ -61,22 +60,35 @@ public: bool operator != ( SPColor const& other ) const { return !(*this == other); }; operator bool() const { return is_set(); } - bool isClose( SPColor const& other, float epsilon ) const; + double operator[](const unsigned int index) const { + if (index < colors.size()) + return colors[index]; + return -1; + } + + void unset(); + + // Convert this color into a different color space + // XXX SPColor &convertTo(Space space) const; + // Compare two colors and decide if they are close matches + bool isClose(SPColor const& other, float epsilon) const; + + // Old API for setting RGB values void set(float r, float g, float b); void set(guint32 value); - bool hasColorProfile() const; - void unsetColorProfile(); + //bool hasColorProfile() const; + //void unsetColorProfile(); void setColorProfile(Inkscape::ColorProfile *profile); - const std::string &getColorProfile() const { return _icc.colorProfile; } + const std::string &getColorProfile() const { return colorProfile; } - bool hasColors() const; + bool hasColors() const { return true; } void unsetColors(); - void setColors(std::vector &&values); + void setColors(std::vector &&values, Space space = Space::RGB); void setColor(unsigned int index, double value); void copyColors(const SPColor &other); - const std::vector &getColors() const { return _icc.colors; } + const std::vector &getColors() const { return colors; } guint32 toRGBA32( int alpha ) const; guint32 toRGBA32( double alpha ) const; @@ -84,14 +96,10 @@ public: 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; } + bool is_set() const { return !colors.empty(); } void get_rgb_floatv (float *rgb) const; void get_cmyk_floatv (float *cmyk) const; @@ -109,8 +117,12 @@ public: 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); + bool read_icc_color(char const *str, char const **end_ptr = nullptr); private: - SVGICCColor _icc; + static bool profileMatches(SPColor const &first, SPColor const &second); + + std::string colorProfile; + std::vector colors; }; #endif // SEEN_SP_COLOR_H diff --git a/src/color/cms-system.cpp b/src/color/cms-system.cpp index 41837d160a..89fbcd5d8b 100644 --- a/src/color/cms-system.cpp +++ b/src/color/cms-system.cpp @@ -274,7 +274,6 @@ const ICCProfileInfo *CMSSystem::get_info_for_profile(Glib::ustring const &name) } // 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); diff --git a/src/color/cms-system.h b/src/color/cms-system.h index 49631f6c27..e5bc4d5372 100644 --- a/src/color/cms-system.h +++ b/src/color/cms-system.h @@ -54,6 +54,22 @@ private: cmsHTRANSFORM _handle; }; +/*class CMSProfile : Inkscape::Color::Profile +{ + explicit CMSProfile(cmsHPROFILE handle) : _handle(handle) { assert(_handle); } + + CMSProfile(CMSProfile const &) = delete; + CMSProfile &operator=(CMSProfile const &) = delete; + ~CMSProfile() { cmsCloseProfile(_handle); } + + std::vector toRGB(std::vector const colors) const; + std::vector fromRGB(std::vector const rgb) const; + + cmsHPROFILE getHandle() const { return _handle; } +private: + cmsHPROFILE _handle; +};*/ + class CMSSystem { public: diff --git a/src/color/components.cpp b/src/color/components.cpp index eeae0bf35c..cb11d2abda 100644 --- a/src/color/components.cpp +++ b/src/color/components.cpp @@ -1,68 +1,84 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Manage color space components + *//* + * 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 - #include -#include "components.h" - -namespace Color { +namespace Inkscape::Color { -Component::Component(std::string name, std::string tip, guint scale) +Component::Component(std::string name, std::string tip, unsigned int cms_scale, unsigned int ink_scale) : name(std::move(name)) , tip(std::move(tip)) - , scale(scale) + , cms_scale(cms_scale) + , ink_scale(ink_scale) { } - -std::vector getComponents(unsigned int space) +std::vector getComponents(Space space) { - static std::map > sets; + static std::map> sets; if (sets.empty()) { - sets[cmsSigXYZData].push_back(Component("_X", "X", 2)); // TYPE_XYZ_16 - sets[cmsSigXYZData].push_back(Component("_Y", "Y", 1)); - sets[cmsSigXYZData].push_back(Component("_Z", "Z", 2)); - sets[cmsSigLabData].push_back(Component("_L", "L", 100)); // TYPE_Lab_16 - sets[cmsSigLabData].push_back(Component("_a", "a", 256)); - sets[cmsSigLabData].push_back(Component("_b", "b", 256)); + // Inkscape internal components + + sets[Space::RGB].emplace_back(_("_R:"), _("Red"), 1, 255); // TYPE_RGB_16 + sets[Space::RGB].emplace_back(_("_G:"), _("Green"), 1, 255); + sets[Space::RGB].emplace_back(_("_B:"), _("Blue"), 1, 255); - // cmsSigLuvData + sets[Space::HSL].emplace_back(_("_H:"), _("Hue"), 360, 255); // TYPE_HLS_16 + sets[Space::HSL].emplace_back(_("_L:"), _("Lightness"), 1, 100); + sets[Space::HSL].emplace_back(_("_S:"), _("Saturation"), 1, 100); - sets[cmsSigYCbCrData].push_back(Component("_Y", "Y", 1)); // TYPE_YCbCr_16 - sets[cmsSigYCbCrData].push_back(Component("C_b", "Cb", 1)); - sets[cmsSigYCbCrData].push_back(Component("C_r", "Cr", 1)); + sets[Space::CMYK].emplace_back(_("_C:"), _("Cyan"), 1, 100); // TYPE_CMYK_16 + sets[Space::CMYK].emplace_back(_("_M:"), _("Magenta"), 1, 100); + sets[Space::CMYK].emplace_back(_("_Y:"), _("Yellow"), 1, 100); + sets[Space::CMYK].emplace_back(_("_K:"), _("Black"), 1, 100); - sets[cmsSigYxyData].push_back(Component("_Y", "Y", 1)); // TYPE_Yxy_16 - sets[cmsSigYxyData].push_back(Component("_x", "x", 1)); - sets[cmsSigYxyData].push_back(Component("y", "y", 1)); + sets[Space::CMY].emplace_back(_("_C:"), _("Cyan"), 1, 100); // TYPE_CMY_16 + sets[Space::CMY].emplace_back(_("_M:"), _("Magenta"), 1, 100); + sets[Space::CMY].emplace_back(_("_Y:"), _("Yellow"), 1, 100); - sets[cmsSigRgbData].push_back(Component(_("_R:"), _("Red"), 1)); // TYPE_RGB_16 - sets[cmsSigRgbData].push_back(Component(_("_G:"), _("Green"), 1)); - sets[cmsSigRgbData].push_back(Component(_("_B:"), _("Blue"), 1)); + sets[Space::HSV].emplace_back(_("_H:"), _("Hue"), 360, 255); // TYPE_HSV_16 + sets[Space::HSV].emplace_back(_("_S:"), _("Saturation"), 1, 255); + sets[Space::HSV].emplace_back(_("_V:"), _("Value"), 1, 255); - sets[cmsSigGrayData].push_back(Component(_("G:"), _("Gray"), 1)); // TYPE_GRAY_16 + sets[Space::HSLUV].emplace_back(_("_H*"), _("Hue"), 360, 255); // TYPE_LUV_16 + sets[Space::HSLUV].emplace_back(_("_S*"), _("Saturation"), 1, 255); + sets[Space::HSLUV].emplace_back(_("_L*"), _("Lightness"), 1, 255); - sets[cmsSigHsvData].push_back(Component(_("_H:"), _("Hue"), 360)); // TYPE_HSV_16 - sets[cmsSigHsvData].push_back(Component(_("_S:"), _("Saturation"), 1)); - sets[cmsSigHsvData].push_back(Component("_V:", "Value", 1)); + sets[Space::OKLAB].emplace_back(_("_Hok"), _("Hue"), 0, 100); + sets[Space::OKLAB].emplace_back(_("_Sok"), _("Saturation"), 0, 360); + sets[Space::OKLAB].emplace_back(_("_Lok"), _("Lightness"), 0, 360); - sets[cmsSigHlsData].push_back(Component(_("_H:"), _("Hue"), 360)); // TYPE_HLS_16 - sets[cmsSigHlsData].push_back(Component(_("_L:"), _("Lightness"), 1)); - sets[cmsSigHlsData].push_back(Component(_("_S:"), _("Saturation"), 1)); + // CMS icc profile only components - sets[cmsSigCmykData].push_back(Component(_("_C:"), _("Cyan"), 1)); // TYPE_CMYK_16 - sets[cmsSigCmykData].push_back(Component(_("_M:"), _("Magenta"), 1)); - sets[cmsSigCmykData].push_back(Component(_("_Y:"), _("Yellow"), 1)); - sets[cmsSigCmykData].push_back(Component(_("_K:"), _("Black"), 1)); + sets[Space::XYZ].emplace_back("_X", "X", 2, 0); // TYPE_XYZ_16 + sets[Space::XYZ].emplace_back("_Y", "Y", 1, 0); + sets[Space::XYZ].emplace_back("_Z", "Z", 2, 0); - sets[cmsSigCmyData].push_back(Component(_("_C:"), _("Cyan"), 1)); // TYPE_CMY_16 - sets[cmsSigCmyData].push_back(Component(_("_M:"), _("Magenta"), 1)); - sets[cmsSigCmyData].push_back(Component(_("_Y:"), _("Yellow"), 1)); + sets[Space::YCbCr].emplace_back("_Y", "Y", 1, 255); // TYPE_YCbCr_16 + sets[Space::YCbCr].emplace_back("C_b", "Cb", 1, 255); + sets[Space::YCbCr].emplace_back("C_r", "Cr", 1, 255); + + sets[Space::LAB].emplace_back("_L", "L", 100, 100); // TYPE_Lab_16 + sets[Space::LAB].emplace_back("_a", "a", 256, 256); + sets[Space::LAB].emplace_back("_b", "b", 256, 256); + + sets[Space::YXY].emplace_back("_Y", "Y", 1, 255); // TYPE_Yxy_16 + sets[Space::YXY].emplace_back("_x", "x", 1, 255); + sets[Space::YXY].emplace_back("y", "y", 1, 255); + + sets[Space::Gray].emplace_back(_("G:"), _("Gray"), 1, 255); // TYPE_GRAY_16 } std::vector target; @@ -72,5 +88,10 @@ std::vector getComponents(unsigned int space) return target; } -}; // namespace Color +std::vector getComponents(cmsUInt32Number cmssig) +{ + return getComponents(_lcmssig_to_space[cmssig]); +} + +}; // namespace Inkscape::Color diff --git a/src/color/components.h b/src/color/components.h index 9645e57999..c62e3594d8 100644 --- a/src/color/components.h +++ b/src/color/components.h @@ -15,7 +15,10 @@ #include #include -namespace Color +#include +#include "spaces.h" + +namespace Inkscape::Color { class Component @@ -24,15 +27,17 @@ public: Component() = delete; ~Component() = default; - Component(std::string name, std::string tip, unsigned int scale); + Component(std::string name, std::string tip, unsigned int cms_scale, unsigned int ink_scale); std::string name; std::string tip; - unsigned int scale; + unsigned int cms_scale; + unsigned int ink_scale; }; -std::vector getComponents(unsigned int space); +std::vector getComponents(Space space); +std::vector getComponents(cmsUInt32Number space); -} // namespace Color +} // namespace Inkscape::Color #endif // SEEN_COLOR_COMPONENTS_H diff --git a/src/color/spaces.cpp b/src/color/spaces.cpp new file mode 100644 index 0000000000..48ec24e32b --- /dev/null +++ b/src/color/spaces.cpp @@ -0,0 +1,17 @@ +// 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 "spaces.h" + +namespace Inkscape::Color { + + +}; // namespace Color + diff --git a/src/color/spaces.h b/src/color/spaces.h new file mode 100644 index 0000000000..4eaf6eeb8c --- /dev/null +++ b/src/color/spaces.h @@ -0,0 +1,81 @@ +// 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_COLOR_SPACES_H +#define SEEN_COLOR_SPACES_H + +#include +#include +#include +#include + +#include "lcms2.h" + +class SPColor; +class CMSProfile; + +namespace Inkscape::Color +{ + +// The spaces we support are a mixture of ICC profile spaces +// and internal spaces converted to and from RGB +enum class Space { + NONE, + RGB, + HSL, + CMYK, + CMY, + HSV, + HSLUV, + OKLAB, + XYZ, + YXY, + LAB, + YCbCr, + Gray, +}; + +// 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::RGB}, + {cmsSigHlsData, Space::HSL}, + {cmsSigCmykData, Space::CMYK}, + {cmsSigCmyData, Space::CMY}, + {cmsSigHsvData, Space::HSV}, + {cmsSigLuvData, Space::HSLUV}, + {customSigOKLabData, Space::OKLAB}, + {cmsSigXYZData, Space::XYZ}, + {cmsSigXYZData, Space::YXY}, + {cmsSigLabData, Space::LAB}, + {cmsSigYCbCrData, Space::YCbCr}, + {cmsSigGrayData, Space::Gray}, +}; + +class Convertor +{ +public: + Convertor() = delete; + ~Convertor() = default; + + virtual Space getType() const { return Space::RGB; } + virtual unsigned int getComponentCount() const { return 3; } + + virtual SPColor const &toRGB(SPColor const &color) const { return color; } + virtual SPColor const &fromRGB(SPColor const &color) const { return color; } + + virtual SPColor toProfile(std::shared_ptr profile, SPColor const &color) const; + virtual SPColor fromProfile(std::shared_ptr profile, SPColor const &color) const; +}; + +} // namespace Inkscape::Color + +#endif // SEEN_COLOR_SPACES_H diff --git a/src/display/cairo-utils.cpp b/src/display/cairo-utils.cpp index e2b5bd620c..dd7d972946 100644 --- a/src/display/cairo-utils.cpp +++ b/src/display/cairo-utils.cpp @@ -1020,7 +1020,8 @@ ink_cairo_set_source_rgba32(cairo_t *ct, guint32 rgba) void ink_cairo_set_source_color(cairo_t *ct, SPColor const &c, double opacity) { - cairo_set_source_rgba(ct, c.v.c[0], c.v.c[1], c.v.c[2], opacity); + auto &vc = c.getColors(); // XXX Force to be sRGB + cairo_set_source_rgba(ct, vc[0], vc[1], vc[2], opacity); } void ink_matrix_to_2geom(Geom::Affine &m, cairo_matrix_t const &cm) diff --git a/src/display/drawing-paintserver.cpp b/src/display/drawing-paintserver.cpp index e018d8b940..66cbc70694 100644 --- a/src/display/drawing-paintserver.cpp +++ b/src/display/drawing-paintserver.cpp @@ -45,7 +45,8 @@ 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); + auto &c = stop.color.getColors(); + cairo_pattern_add_color_stop_rgba(pat, stop.offset, c[0], c[1], c[2], stop.opacity * opacity); } return pat; @@ -103,7 +104,8 @@ 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); + auto &c = stop.color.getColors(); + cairo_pattern_add_color_stop_rgba(pat, stop.offset, c[0], c[1], c[2], stop.opacity * opacity); } return pat; 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..5802c500b4 100644 --- a/src/display/nr-filter-flood.cpp +++ b/src/display/nr-filter-flood.cpp @@ -18,7 +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" @@ -36,15 +35,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 +90,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..0cdd071da1 100644 --- a/src/display/nr-filter-specularlighting.cpp +++ b/src/display/nr-filter-specularlighting.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-icc-color.h" #include "svg/svg-color.h" namespace Inkscape { @@ -154,14 +153,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-style.cpp b/src/display/nr-style.cpp index 8d21061aef..d1f911eefe 100644 --- a/src/display/nr-style.cpp +++ b/src/display/nr-style.cpp @@ -323,7 +323,7 @@ auto NRStyle::preparePaint(Inkscape::DrawingContext &dc, Inkscape::RenderContext } break; case NRStyleData::PaintType::COLOR: { - auto const &c = paint.color.v.c; + auto const &c = paint.color.getColors(); cp.pattern = CairoPatternUniqPtr(cairo_pattern_create_rgba(c[0], c[1], c[2], paint.opacity)); break; } diff --git a/src/extension/internal/pdfinput/svg-builder.cpp b/src/extension/internal/pdfinput/svg-builder.cpp index 2c7129fbc8..240a425f29 100644 --- a/src/extension/internal/pdfinput/svg-builder.cpp +++ b/src/extension/internal/pdfinput/svg-builder.cpp @@ -37,7 +37,6 @@ #include "pdf-utils.h" #include "png.h" #include "poppler-cairo-font-engine.h" -#include "profile-manager.h" #include "color/cms-util.h" #include "display/cairo-utils.h" diff --git a/src/object/color-profile.cpp b/src/object/color-profile.cpp index 4fbbea9f52..461ae62a21 100644 --- a/src/object/color-profile.cpp +++ b/src/object/color-profile.cpp @@ -52,11 +52,15 @@ public: void _clearProfile(); - cmsHPROFILE _profHandle; + //cmsHPROFILE _profHandle; + std::shared_ptr _profile; + cmsProfileClassSignature _profileClass; cmsColorSpaceSignature _profileSpace; + cmsHTRANSFORM _transf; cmsHTRANSFORM _revTransf; + std::shared_ptr _colorproof; std::shared_ptr _gamutwarn; }; @@ -76,7 +80,7 @@ cmsProfileClassSignature asICColorProfileClassSig(ColorProfileClassSig const & s ColorProfileImpl::ColorProfileImpl() : - _profHandle(nullptr), + //_profHandle(nullptr), _profileClass(cmsSigInputClass), _profileSpace(cmsSigRgbData), _transf(nullptr), @@ -152,10 +156,10 @@ void ColorProfileImpl::_clearProfile() cmsDeleteTransform( _revTransf ); _revTransf = nullptr; } - if ( _profHandle ) { + /*if ( _profHandle ) { cmsCloseProfile( _profHandle ); _profHandle = nullptr; - } + }*/ } /** @@ -218,15 +222,15 @@ void ColorProfile::set(SPAttr key, gchar const *value) { try { auto hrefUri = Inkscape::URI(this->href, docUri); auto contents = hrefUri.getContents(); - this->impl->_profHandle = cmsOpenProfileFromMem(contents.data(), contents.size()); + //this->impl->_profHandle = cmsOpenProfileFromMem(contents.data(), contents.size()); } catch (...) { g_warning("Failed to open CMS profile URI '%.100s'", this->href); } - if ( this->impl->_profHandle ) { + /*if ( this->impl->_profHandle ) { this->impl->_profileSpace = cmsGetColorSpace( this->impl->_profHandle ); this->impl->_profileClass = cmsGetDeviceClass( this->impl->_profHandle ); - } + }*/ } } this->requestModified(SP_OBJECT_MODIFIED_FLAG); @@ -246,7 +250,7 @@ void ColorProfile::set(SPAttr key, gchar const *value) { break; case SPAttr::NAME: - this->name = value; + this->name = value ? value : ""; this->requestModified(SP_OBJECT_MODIFIED_FLAG); break; @@ -368,7 +372,7 @@ int ColorProfile::getCmsFlags(RenderingIntent intent) const return getLcmsFlags(getRenderingIntent(intent)); } -std::vector ColorProfile::getComponents() const +std::vector ColorProfile::getComponents() const { return Color::getComponents(asICColorSpaceSig(getColorSpace())); } @@ -419,22 +423,24 @@ Inkscape::ColorProfileClassSig ColorProfile::getProfileClass() const { cmsHTRANSFORM ColorProfile::getTransfToSRGB8(RenderingIntent intent) { - if (!impl->_transf && impl->_profHandle) { +/* if (!impl->_transf && impl->_profHandle) { int flags = getCmsFlags(intent); int cms_intent = getCmsIntent(intent); impl->_transf = cmsCreateTransform(impl->_profHandle, ColorProfileImpl::_getInputFormat(impl->_profileSpace), ColorProfileImpl::getSRGBProfile(), TYPE_RGBA_8, cms_intent, flags); } - return impl->_transf; + return impl->_transf;*/ + return nullptr; } cmsHTRANSFORM ColorProfile::getTransfFromSRGB8(RenderingIntent intent) { - if (!impl->_revTransf && impl->_profHandle) { +/* if (!impl->_revTransf && impl->_profHandle) { int flags = getCmsFlags(intent); int cms_intent = getCmsIntent(intent); impl->_revTransf = cmsCreateTransform(ColorProfileImpl::getSRGBProfile(), TYPE_RGBA_8, impl->_profHandle, ColorProfileImpl::_getInputFormat(impl->_profileSpace), cms_intent, flags); } - return impl->_revTransf; + return impl->_revTransf;*/ + return nullptr; } std::shared_ptr ColorProfile::getColorProofTransform() @@ -501,7 +507,7 @@ bool ColorProfile::isPrintColorSpace() cmsHPROFILE ColorProfile::getHandle() { - return impl->_profHandle; + return nullptr; //impl->_profHandle; } void errorHandlerCB(cmsContext /*contextID*/, cmsUInt32Number errorCode, char const *errorText) diff --git a/src/object/filters/diffuselighting.cpp b/src/object/filters/diffuselighting.cpp index 4d462b2fc6..0b19b8ea39 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,8 @@ 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.fromString(value); requestModified(SP_OBJECT_MODIFIED_FLAG); break; } @@ -168,13 +147,7 @@ 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"); - } + repr->setAttributeOrRemoveIfEmpty("lighting-color", lighting_color.toString()); SPFilterPrimitive::write(doc, repr, flags); @@ -206,10 +179,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.toRGBA32(1.0); // 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..6f897520b6 100644 --- a/src/object/filters/diffuselighting.h +++ b/src/object/filters/diffuselighting.h @@ -16,10 +16,8 @@ #include #include #include "sp-filter-primitive.h" -#include "svg/svg-icc-color.h" #include "number-opt-number.h" - -struct SVGICCColor; +#include "color.h" namespace Inkscape { namespace Filters { @@ -36,14 +34,13 @@ public: private: float surfaceScale = 1.0f; float diffuseConstant = 1.0f; - uint32_t lighting_color = 0xffffffff; + + SPColor 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..267d9e5910 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) { + flood_color.fromString(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.toRGBA32(opacity)); return flood; } diff --git a/src/object/filters/flood.h b/src/object/filters/flood.h index 99cc4cfc76..a2d3917b64 100644 --- a/src/object/filters/flood.h +++ b/src/object/filters/flood.h @@ -16,7 +16,7 @@ #include #include #include "sp-filter-primitive.h" -#include "svg/svg-icc-color.h" +#include "color.h" class SPFeFlood final : public SPFilterPrimitive @@ -25,9 +25,9 @@ public: int tag() const override { return tag_of; } private: - uint32_t color = 0x0; + + SPColor 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/specularlighting.cpp b/src/object/filters/specularlighting.cpp index 10a9d61fdd..965ad6ba3a 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.fromString(value); requestModified(SP_OBJECT_MODIFIED_FLAG); break; } @@ -177,12 +159,8 @@ 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); - } + repr->setAttributeOrRemoveIfEmpty("lighting-color", lighting_color.toString()); SPFilterPrimitive::write(doc, repr, flags); return repr; @@ -214,10 +192,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.toRGBA32(1.0); // 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..87594ff82c 100644 --- a/src/object/filters/specularlighting.h +++ b/src/object/filters/specularlighting.h @@ -17,11 +17,9 @@ #include #include -#include "svg/svg-icc-color.h" #include "sp-filter-primitive.h" #include "number-opt-number.h" - -struct SVGICCColor; +#include "color.h" namespace Inkscape { namespace Filters { @@ -39,15 +37,14 @@ private: float surfaceScale = 1.0f; float specularConstant = 1.0f; float specularExponent = 1.0f; - uint32_t lighting_color = 0xffffffff; + + SPColor 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.cpp b/src/object/sp-gradient.cpp index 1a81abda07..dc690aeb6b 100644 --- a/src/object/sp-gradient.cpp +++ b/src/object/sp-gradient.cpp @@ -1156,8 +1156,8 @@ sp_gradient_pattern_common_setup(cairo_pattern_t *cp, 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); + auto &c = stop.color.getColors(); + cairo_pattern_add_color_stop_rgba(cp, stop.offset, c[0], c[1], c[2], stop.opacity * opacity); } } @@ -1182,8 +1182,8 @@ SPGradient::create_preview_pattern(double width) 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); + auto &c = stop.color.getColors(); + cairo_pattern_add_color_stop_rgba(pat, stop.offset, c[0], c[1], c[2], stop.opacity); } } else { @@ -1196,8 +1196,8 @@ SPGradient::create_preview_pattern(double width) for (unsigned i = 0; i < 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); + auto &c = node->color.getColors(); + cairo_pattern_add_color_stop_rgba(pat, i*offset, c[0], c[1], c[2], node->opacity); } } diff --git a/src/object/sp-mesh-array.cpp b/src/object/sp-mesh-array.cpp index e040e98382..614f6cfede 100644 --- a/src/object/sp-mesh-array.cpp +++ b/src/object/sp-mesh-array.cpp @@ -2158,9 +2158,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.getColors(); + auto &color3 = n[3]->color.getColors(); + auto &color6 = n[6]->color.getColors(); // Distance nodes from selected corner Geom::Point d[7]; @@ -2173,17 +2173,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[c] - color0[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[c] - color3[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 +2203,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[cdm] - color0[cdm]) / slope_ave[ cdm ] ); + length_right = std::abs( (color6[cdm] - color3[cdm]) / slope_ave[ cdm ] ); } // Move closest handle a maximum of mid point... but don't shorten diff --git a/src/object/sp-mesh-gradient.cpp b/src/object/sp-mesh-gradient.cpp index 4bbc455534..a51af32585 100644 --- a/src/object/sp-mesh-gradient.cpp +++ b/src/object/sp-mesh-gradient.cpp @@ -216,9 +216,9 @@ std::unique_ptr SPMeshGradient::create_drawing_pai // << " calculated as " << t << "." << std::endl; } - auto color = patch.getColor(k); + auto &color = patch.getColor(k).getColors(); 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); diff --git a/src/object/sp-solid-color.cpp b/src/object/sp-solid-color.cpp index b7d520a357..d0fabe0716 100644 --- a/src/object/sp-solid-color.cpp +++ b/src/object/sp-solid-color.cpp @@ -63,7 +63,8 @@ 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)); + g_error("This must be re-written so it doesn't use FLOAT POINTORS, I MEAN what the absolute fuck is this even doing."); + return std::make_unique(nullptr, SP_SCALE24_TO_FLOAT(style->solid_opacity.value)); } /* diff --git a/src/style-internal.cpp b/src/style-internal.cpp index fd948d46a1..04687a6aa7 100644 --- a/src/style-internal.cpp +++ b/src/style-internal.cpp @@ -1633,8 +1633,7 @@ SPIPaint::reset( bool init ) { paintOrigin = SP_CSS_PAINT_ORIGIN_NORMAL; colorSet = false; noneSet = false; - value.color.set(0x0); - value.color.unsetColorProfile(); + value.color.unset(); tag = nullptr; value.href.reset(); diff --git a/src/style-internal.h b/src/style-internal.h index 6c26873280..391e7a652a 100644 --- a/src/style-internal.h +++ b/src/style-internal.h @@ -35,8 +35,6 @@ #include "object/uri.h" -#include "svg/svg-icc-color.h" - #include "xml/repr.h" namespace Inkscape { diff --git a/src/svg/CMakeLists.txt b/src/svg/CMakeLists.txt index 6a66a6291a..818a233a04 100644 --- a/src/svg/CMakeLists.txt +++ b/src/svg/CMakeLists.txt @@ -24,7 +24,6 @@ set(svg_SRC 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 index f82b1b0e7a..41031610ab 100644 --- a/src/svg/svg-color.cpp +++ b/src/svg/svg-color.cpp @@ -24,8 +24,6 @@ #include #include #include -#include -#include #include // g_assert @@ -36,7 +34,6 @@ #include "inkscape.h" #include "preferences.h" #include "strneq.h" -#include "svg-icc-color.h" #include "color/cms-system.h" #include "object/color-profile.h" @@ -504,141 +501,6 @@ sp_svg_create_color_hash() 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->getColorManager().find(icc->colorProfile)) { - guchar color_out[4] = {0,0,0,0}; - cmsHTRANSFORM trans = prof->getTransfToSRGB8(); - if ( trans ) { - std::vector comps = prof->getComponents(); - - 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. diff --git a/src/svg/svg-color.h b/src/svg/svg-color.h index b2c2f79737..56bbf662d7 100644 --- a/src/svg/svg-color.h +++ b/src/svg/svg-color.h @@ -10,17 +10,20 @@ #ifndef SVG_SVG_COLOR_H_SEEN #define SVG_SVG_COLOR_H_SEEN +#include +#include +#include + +namespace Inkscape { + class CMSTransform; +}; + 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/svg/svg-icc-color.h b/src/svg/svg-icc-color.h deleted file mode 100644 index abd6918a81..0000000000 --- a/src/svg/svg-icc-color.h +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/** @file - * TODO: insert short description here - *//* - * Authors: see git history - * - * Copyright (C) 2010 Authors - * Released under GNU GPL v2+, read the file 'COPYING' for more information. - */ -#ifndef SVG_ICC_COLOR_H_SEEN -#define SVG_ICC_COLOR_H_SEEN - -#include -#include - -/** - * An icc-color specification. Corresponds to the DOM interface of the same name. - * - * Referenced by SPIPaint. - */ -struct SVGICCColor -{ - std::string colorProfile; - std::vector colors; -}; - -#endif /* !SVG_ICC_COLOR_H_SEEN */ - -/* - Local Variables: - mode:c++ - c-file-style:"stroustrup" - c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) - indent-tabs-mode:nil - fill-column:99 - End: -*/ -// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/icon-loader.cpp b/src/ui/icon-loader.cpp index 0262ae2964..b914e814a4 100644 --- a/src/ui/icon-loader.cpp +++ b/src/ui/icon-loader.cpp @@ -23,6 +23,7 @@ #include #include +#include "color/color-conv.h" #include "desktop.h" #include "inkscape.h" #include "ui/util.h" @@ -129,6 +130,53 @@ Gtk::Image *get_shape_image(Glib::ustring const &shape_type, std::uint32_t const return image; } +/** + * Similar to get_color_class, but sets the icon-pallete instead of the main color. + */ +[[nodiscard]] static Glib::ustring const &get_palette_class(std::uint32_t const rgba_color, + Glib::RefPtr const &screen) +{ + static std::unordered_map palette_classes; + auto &color_class = palette_classes[rgba_color]; + if (!color_class.empty()) return color_class; + + // 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-palette-%1", hex); + // GTK CSS does not support #RRGGBBAA, so we throw out the opacity + hex.resize(6); + // 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; -gtk-icon-palette: success #%2, warning #%2, error #%2; }", + color_class, hex); + 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; + Gtk::StyleContext::add_provider_for_screen(screen, css_provider, priority); + return color_class; +} + + +void apply_cms_icon(Gtk::Image *image, Glib::ustring const &cms_type, std::uint32_t const rgba_color) +{ + Glib::RefPtr display = Gdk::Display::get_default(); + Glib::RefPtr screen = display->get_default_screen(); + Glib::RefPtr icon_theme = Gtk::IconTheme::get_for_screen(screen); + + auto icon_name = Glib::ustring::compose("cms-color-%1-symbolic", cms_type); + if (!icon_theme->has_icon(icon_name)) { + g_warning("Can't find CMS icon '%s'", icon_name.c_str()); + return; + } + + // Set the image icon with it's style directly to the widget + image->get_style_context()->add_class(get_palette_class(rgba_color, screen)); + auto const icon = Gio::ThemedIcon::create(icon_name); + image->set(icon, Gtk::ICON_SIZE_BUTTON); +} + } // namespace Inkscape::UI /* diff --git a/src/ui/icon-loader.h b/src/ui/icon-loader.h index 4554b11778..7d76e334c8 100644 --- a/src/ui/icon-loader.h +++ b/src/ui/icon-loader.h @@ -41,6 +41,10 @@ struct GetShapeIconResult final { Glib::ustring icon_name; Glib::ustring color_c std::uint32_t rgba_color, Gtk::IconSize icon_size = Gtk::ICON_SIZE_BUTTON); +void apply_cms_icon(Gtk::Image *image, + Glib::ustring const &cms_type, + std::uint32_t const rgba_color); + } // namespace Inkscape::UI #endif // SEEN_INK_ICON_LOADER_H diff --git a/src/ui/selected-color.cpp b/src/ui/selected-color.cpp index b43976f0cc..48651f7f14 100644 --- a/src/ui/selected-color.cpp +++ b/src/ui/selected-color.cpp @@ -70,23 +70,12 @@ guint32 SelectedColor::value() const 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 )) { @@ -105,12 +94,6 @@ void SelectedColor::setColorAlpha(SPColor const &color, gfloat alpha, bool 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 } } diff --git a/src/ui/tools/tweak-tool.cpp b/src/ui/tools/tweak-tool.cpp index 604612d354..53dde3acfc 100644 --- a/src/ui/tools/tweak-tool.cpp +++ b/src/ui/tools/tweak-tool.cpp @@ -585,9 +585,10 @@ 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) +/* static void +tweak_colorpaint (SPColor &spc, guint32 goal, double force, bool do_h, bool do_s, bool do_l) { + auto &color = spc->getColors(); float rgb_g[3]; if (!do_h || !do_s || !do_l) { @@ -615,12 +616,14 @@ tweak_colorpaint (float *color, guint32 goal, double force, bool do_h, bool do_s 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 void +tweak_colorjitter (SPColor &spc, double force, bool do_h, bool do_s, bool do_l) { float hsl_c[3]; + // XXX Replace with proper hsl convertor later + auto &color = spc.getColors(); SPColor::rgb_to_hsl_floatv (hsl_c, color[0], color[1], color[2]); if (do_h) { @@ -639,16 +642,17 @@ tweak_colorjitter (float *color, double force, bool do_h, bool do_s, bool do_l) hsl_c[2] += g_random_double_range(-hsl_c[2], 1 - hsl_c[2]) * force; } + g_error("You disbaled this code because it's so appaulingly bad"); SPColor::hsl_to_rgb_floatv (color, hsl_c[0], hsl_c[1], hsl_c[2]); -} +}*/ static void -tweak_color (guint mode, float *color, guint32 goal, double force, bool do_h, bool do_s, bool do_l) +tweak_color (guint mode, SPColor const &color, guint32 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); + //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); + //tweak_colorjitter (color, force, do_h, do_s, do_l); } } @@ -778,10 +782,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_color (mode, stop->getColor(), rgb_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_color(mode, prevStop->getColor(), rgb_goal, force * (offset_h - pos_e) / (offset_h - offset_l), do_h, do_s, do_l); stop->updateRepr(); @@ -791,14 +795,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_color(mode, prevStop->getColor(), rgb_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_color (mode, prevStop->getColor(), rgb_goal, force * tweak_profile (fabs (pos_e - offset_h), r), do_h, do_s, do_l); stop->updateRepr(); @@ -820,7 +824,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_color (mode, stop->getColor(), rgb_goal, force * tweak_profile (distance, radius), do_h, do_s, do_l); stop->updateRepr(); } @@ -937,7 +941,7 @@ sp_tweak_color_recursive (guint mode, SPItem *item, SPItem *item_at_point, 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); + tweak_color (mode, style->fill.value.color, fill_goal, this_force, do_h, do_s, do_l); item->updateRepr(); did = true; } @@ -947,7 +951,7 @@ sp_tweak_color_recursive (guint mode, SPItem *item, SPItem *item_at_point, 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); + tweak_color (mode, style->stroke.value.color, stroke_goal, this_force, do_h, do_s, do_l); item->updateRepr(); did = true; } diff --git a/src/ui/util.cpp b/src/ui/util.cpp index 069bd862cb..9065d8830c 100644 --- a/src/ui/util.cpp +++ b/src/ui/util.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -398,6 +399,27 @@ void set_dark_titlebar(Glib::RefPtr const &win, bool is_dark) #endif } +double get_adj_scaled(Glib::RefPtr const &a) +{ + return a->get_value() / a->get_upper(); +} + +void set_adj_scaled(Glib::RefPtr &a, double v, bool constrained) +{ + auto upper = a->get_upper(); + double val = v * upper; + if (constrained) { + // TODO: do we want preferences for these? + if (upper == 255) { + val = round(val/16) * 16; + } else { + val = round(val/10) * 10; + } + } + a->set_value(val); +} + + /* Local Variables: mode:c++ diff --git a/src/ui/util.h b/src/ui/util.h index 08e689f0f4..319cbf1058 100644 --- a/src/ui/util.h +++ b/src/ui/util.h @@ -208,6 +208,9 @@ auto const_wrap(T const *p, bool take_copy = false) return std::shared_ptr(std::move(unconst_wrapped)); } +double get_adj_scaled(Glib::RefPtr const &a); +void set_adj_scaled(Glib::RefPtr &a, double v, bool constrained = false); + #endif // UI_UTIL_SEEN /* diff --git a/src/ui/widget/canvas-grid.cpp b/src/ui/widget/canvas-grid.cpp index 1c157dfad8..cbf7a633ef 100644 --- a/src/ui/widget/canvas-grid.cpp +++ b/src/ui/widget/canvas-grid.cpp @@ -123,11 +123,11 @@ CanvasGrid::CanvasGrid(SPDesktopWidget *dtw) // CMS Adjust _cms_popover = Inkscape::UI::create_builder("cms-popover.glade"); - _cms_adjust.set_name("CMS_Adjust"); - _cms_adjust.add(*Gtk::make_managed("color-management", Gtk::ICON_SIZE_MENU)); - // Can't access via C++ API, fixed in Gtk4. - gtk_actionable_set_action_name( GTK_ACTIONABLE(_cms_adjust.gobj()), "win.canvas-color-manage"); - _cms_adjust.set_tooltip_text(_("Toggle color-managed display for this document window")); + _cms_actions.set_name("CMSActions"); + _cms_actions.set_popover(Inkscape::UI::get_derived_widget(_cms_popover, "popover", _dtw)); + _cms_actions.set_image_from_icon_name("color-cms-symbolic"); + _cms_actions.set_direction(Gtk::ARROW_UP); + _cms_actions.set_tooltip_text(_("Color management options")); // popover with some common display mode related options _display_popup = Inkscape::UI::create_builder("display-popup.glade"); @@ -140,7 +140,7 @@ CanvasGrid::CanvasGrid(SPDesktopWidget *dtw) // Main grid attach(_subgrid, 0, 0, 1, 2); attach(_hscrollbar, 0, 2, 1, 1); - attach(_cms_adjust, 1, 2, 1, 1); + attach(_cms_actions, 1, 2, 1, 1); attach(_quick_actions, 1, 0, 1, 1); attach(_vscrollbar, 1, 1, 1, 1); @@ -279,14 +279,14 @@ CanvasGrid::ShowScrollbars(bool state) // Show scrollbars _hscrollbar.set_visible(true); _vscrollbar.set_visible(true); - _cms_adjust.set_visible(true); - _cms_adjust.show_all_children(); + _cms_actions.set_visible(true); + _cms_actions.show_all_children(); _quick_actions.set_visible(true); } else { // Hide scrollbars _hscrollbar.set_visible(false); _vscrollbar.set_visible(false); - _cms_adjust.set_visible(false); + _cms_actions.set_visible(false); _quick_actions.set_visible(false); } } diff --git a/src/ui/widget/canvas-grid.h b/src/ui/widget/canvas-grid.h index 23cbccde10..30fc32caa6 100644 --- a/src/ui/widget/canvas-grid.h +++ b/src/ui/widget/canvas-grid.h @@ -85,7 +85,7 @@ public: Gtk::Adjustment *GetHAdj() { return _hadj.get(); }; Gtk::Adjustment *GetVAdj() { return _vadj.get(); }; Gtk::ToggleButton *GetGuideLock() { return &_guide_lock; } - Gtk::ToggleButton *GetCmsAdjust() { return &_cms_adjust; } + Gtk::ToggleButton *GetCmsAdjust() { return &_cms_actions; } Gtk::ToggleButton *GetStickyZoom(); Dialog::CommandPalette *getCommandPalette() { return _command_palette.get(); } @@ -116,7 +116,7 @@ private: std::unique_ptr _vruler; Gtk::ToggleButton _guide_lock; - Gtk::MenuButton _cms_adjust; + Gtk::MenuButton _cms_actions; Gtk::MenuButton _quick_actions; Glib::RefPtr _display_popup; Glib::RefPtr _cms_popover; diff --git a/src/ui/widget/cms-popover.cpp b/src/ui/widget/cms-popover.cpp index 10318cc4a8..29fe38ddc8 100644 --- a/src/ui/widget/cms-popover.cpp +++ b/src/ui/widget/cms-popover.cpp @@ -11,13 +11,14 @@ #include "gtkmm/image.h" -#include "ui/icon-loader.h" -#include "ui/builder-utils.h" - +#include "color/manager.h" #include "desktop.h" #include "desktop-widget.h" #include "document.h" -#include "color/manager.h" + +#include "ui/builder-utils.h" +#include "ui/icon-loader.h" +#include "ui/util.h" namespace Inkscape::UI::Widget { @@ -27,7 +28,7 @@ static void _set_icons(Gtk::Box &box, std::string const &type, std::vector(child)) { if (auto img = dynamic_cast(btn->get_image())) { - img->set(sp_get_cms_icon(type, colors[index])); + apply_cms_icon(img, type, to_guint32(colors[index])); } } index++; diff --git a/src/ui/widget/color-notebook.cpp b/src/ui/widget/color-notebook.cpp index 5823cdcd5d..2458ac9871 100644 --- a/src/ui/widget/color-notebook.cpp +++ b/src/ui/widget/color-notebook.cpp @@ -266,7 +266,8 @@ void ColorNotebook::_updateICCButtons() g_return_if_fail((0.0 <= alpha) && (alpha <= 1.0)); /* update color management icon*/ - gtk_widget_set_sensitive(_box_colormanaged, color.hasColorProfile()); + g_warning("Color management is broken here..."); + //gtk_widget_set_sensitive(_box_colormanaged, color.hasColorProfile()); gtk_widget_set_sensitive(_box_toomuchink, false); gtk_widget_set_sensitive(_box_outofgamut, false); diff --git a/src/ui/widget/color-scales.cpp b/src/ui/widget/color-scales.cpp index 655366c8da..3d9455e384 100644 --- a/src/ui/widget/color-scales.cpp +++ b/src/ui/widget/color-scales.cpp @@ -31,6 +31,7 @@ #include "ui/icon-loader.h" #include "ui/pack.h" #include "ui/selected-color.h" +#include "ui/util.h" #include "ui/widget/color-slider.h" #include "ui/widget/ink-color-wheel.h" #include "ui/widget/oklab-color-wheel.h" @@ -75,26 +76,23 @@ const char* color_mode_name[] = { N_("None"), N_("RGB"), N_("HSL"), N_("CMYK"), N_("HSV"), N_("HSLuv"), N_("OKHSL"), nullptr }; -const char* get_color_mode_icon(SPColorScalesMode mode) { - auto index = static_cast(mode); - assert(index > 0 && index < (sizeof(color_mode_icons) / sizeof(color_mode_icons[0]))); - return color_mode_icons[index]; +const char* get_color_mode_icon(Color::Space mode) { + return color_mode_icons[static_cast(mode)]; } -const char* get_color_mode_label(SPColorScalesMode mode) { - auto index = static_cast(mode); - assert(index > 0 && index < (sizeof(color_mode_name) / sizeof(color_mode_name[0]))); - return color_mode_name[index]; +const char* get_color_mode_label(Color::Space mode) { + return color_mode_name[static_cast(mode)]; } -std::unique_ptr get_factory(SPColorScalesMode mode) { + +std::unique_ptr get_factory(Color::Space mode) { switch (mode) { - case SPColorScalesMode::RGB: return std::make_unique>(); - case SPColorScalesMode::HSL: return std::make_unique>(); - case SPColorScalesMode::HSV: return std::make_unique>(); - case SPColorScalesMode::CMYK: return std::make_unique>(); - case SPColorScalesMode::HSLUV: return std::make_unique>(); - case SPColorScalesMode::OKLAB: return std::make_unique>(); + case Color::Space::RGB: return std::make_unique>(); + case Color::Space::HSL: return std::make_unique>(); + case Color::Space::HSV: return std::make_unique>(); + case Color::Space::CMYK: return std::make_unique>(); + case Color::Space::HSLUV: return std::make_unique>(); + case Color::Space::OKLAB: return std::make_unique>(); default: throw std::invalid_argument("There's no factory for the requested color mode"); } @@ -104,12 +102,12 @@ std::vector get_color_pickers() { std::vector pickers; for (auto mode : { - SPColorScalesMode::HSL, - SPColorScalesMode::HSV, - SPColorScalesMode::RGB, - SPColorScalesMode::CMYK, - SPColorScalesMode::OKLAB, - SPColorScalesMode::HSLUV, + Color::Space::HSL, + Color::Space::HSV, + Color::Space::RGB, + Color::Space::CMYK, + Color::Space::OKLAB, + Color::Space::HSLUV, }) { auto label = get_color_mode_label(mode); @@ -126,28 +124,7 @@ std::vector get_color_pickers() { } -template -gchar const *ColorScales::SUBMODE_NAMES[] = { N_("None"), N_("RGB"), N_("HSL"), - N_("CMYK"), N_("HSV"), N_("HSLuv"), N_("OKHSL") }; - -// Preference name for the saved state of toggle-able color wheel -template <> -gchar const * const ColorScales::_pref_wheel_visibility = - "/wheel_vis_hsl"; - -template <> -gchar const * const ColorScales::_pref_wheel_visibility = - "/wheel_vis_hsv"; - -template <> -gchar const * const ColorScales::_pref_wheel_visibility = - "/wheel_vis_hsluv"; - -template <> -gchar const * const ColorScales::_pref_wheel_visibility = - "/wheel_vis_okhsl"; - -template +template ColorScales::ColorScales(SelectedColor &color, bool no_alpha) : Gtk::Box() , _color(color) @@ -157,9 +134,9 @@ ColorScales::ColorScales(SelectedColor &color, bool no_alpha) , _wheel(nullptr) { for (gint i = 0; i < 5; i++) { - _l[i] = nullptr; - _s[i] = nullptr; - _b[i] = nullptr; + _lbl[i] = nullptr; + _slide[i] = nullptr; + _btn[i] = nullptr; } _initUI(no_alpha); @@ -168,23 +145,24 @@ ColorScales::ColorScales(SelectedColor &color, bool no_alpha) _color_dragged = _color.signal_dragged.connect([this](){ _onColorChanged(); }); } -template +template void ColorScales::_initUI(bool no_alpha) { set_orientation(Gtk::ORIENTATION_VERTICAL); + auto wheel_pref = std::string("/colorselector/") + color_mode_name[static_cast(MODE)] + "/wheel"; Gtk::Expander *wheel_frame = nullptr; if constexpr ( - MODE == SPColorScalesMode::HSL || - MODE == SPColorScalesMode::HSV || - MODE == SPColorScalesMode::HSLUV || - MODE == SPColorScalesMode::OKLAB) + MODE == Color::Space::HSL || + MODE == Color::Space::HSV || + MODE == Color::Space::HSLUV || + MODE == Color::Space::OKLAB) { /* Create wheel */ - if constexpr (MODE == SPColorScalesMode::HSLUV) { + if constexpr (MODE == Color::Space::HSLUV) { _wheel = Gtk::make_managed(); - } else if constexpr (MODE == SPColorScalesMode::OKLAB) { + } else if constexpr (MODE == Color::Space::OKLAB) { _wheel = Gtk::make_managed(); } else { _wheel = Gtk::make_managed(); @@ -237,7 +215,7 @@ void ColorScales::_initUI(bool no_alpha) wheel_frame->set_vexpand(visible); // Save wheel visibility - Inkscape::Preferences::get()->setBool(_prefs + _pref_wheel_visibility, visible); + Inkscape::Preferences::get()->setBool(wheel_pref, visible); }); wheel_frame->add(*_wheel); @@ -251,73 +229,72 @@ void ColorScales::_initUI(bool no_alpha) for (gint i = 0; i < 5; i++) { /* Label */ - _l[i] = Gtk::make_managed("", true); + _lbl[i] = Gtk::make_managed("", true); - _l[i]->set_halign(Gtk::ALIGN_START); - _l[i]->set_visible(true); + _lbl[i]->set_halign(Gtk::ALIGN_START); + _lbl[i]->set_visible(true); - _l[i]->set_margin_start(2 * XPAD); - _l[i]->set_margin_end(XPAD); - _l[i]->set_margin_top(YPAD); - _l[i]->set_margin_bottom(YPAD); - grid->attach(*_l[i], 0, i, 1, 1); + _lbl[i]->set_margin_start(2 * XPAD); + _lbl[i]->set_margin_end(XPAD); + _lbl[i]->set_margin_top(YPAD); + _lbl[i]->set_margin_bottom(YPAD); + grid->attach(*_lbl[i], 0, i, 1, 1); /* Adjustment */ - _a.push_back(Gtk::Adjustment::create(0.0, 0.0, _range_limit, 1.0, 10.0, 10.0)); + _adj.push_back(Gtk::Adjustment::create(0.0, 0.0, _range_limit, 1.0, 10.0, 10.0)); /* Slider */ - _s[i] = Gtk::make_managed(_a[i]); - _s[i]->set_visible(true); + _slide[i] = Gtk::make_managed(_adj[i]); + _slide[i]->set_visible(true); - _s[i]->set_margin_start(XPAD); - _s[i]->set_margin_end(XPAD); - _s[i]->set_margin_top(YPAD); - _s[i]->set_margin_bottom(YPAD); - _s[i]->set_hexpand(true); - grid->attach(*_s[i], 1, i, 1, 1); + _slide[i]->set_margin_start(XPAD); + _slide[i]->set_margin_end(XPAD); + _slide[i]->set_margin_top(YPAD); + _slide[i]->set_margin_bottom(YPAD); + _slide[i]->set_hexpand(true); + grid->attach(*_slide[i], 1, i, 1, 1); /* Spinbutton */ - _b[i] = Gtk::make_managed>(_a[i], 1.0); - sp_dialog_defocus_on_enter(_b[i]->gobj()); - _l[i]->set_mnemonic_widget(*_b[i]); - _b[i]->set_visible(true); - - _b[i]->set_margin_start(XPAD); - _b[i]->set_margin_end(XPAD); - _b[i]->set_margin_top(YPAD); - _b[i]->set_margin_bottom(YPAD); - _b[i]->set_halign(Gtk::ALIGN_END); - _b[i]->set_valign(Gtk::ALIGN_CENTER); - grid->attach(*_b[i], 2, i, 1, 1); + _btn[i] = Gtk::make_managed>(_adj[i], 1.0); + sp_dialog_defocus_on_enter(_btn[i]->gobj()); + _lbl[i]->set_mnemonic_widget(*_btn[i]); + _btn[i]->set_visible(true); + + _btn[i]->set_margin_start(XPAD); + _btn[i]->set_margin_end(XPAD); + _btn[i]->set_margin_top(YPAD); + _btn[i]->set_margin_bottom(YPAD); + _btn[i]->set_halign(Gtk::ALIGN_END); + _btn[i]->set_valign(Gtk::ALIGN_CENTER); + grid->attach(*_btn[i], 2, i, 1, 1); /* Signals */ - _a[i]->signal_value_changed().connect([this, i](){ _adjustmentChanged(i); }); - _s[i]->signal_grabbed.connect([this](){ _sliderAnyGrabbed(); }); - _s[i]->signal_released.connect([this](){ _sliderAnyReleased(); }); - _s[i]->signal_value_changed.connect([this](){ _sliderAnyChanged(); }); + _adj[i]->signal_value_changed().connect([this, i](){ _adjustmentChanged(i); }); + _slide[i]->signal_grabbed.connect([this](){ _sliderAnyGrabbed(); }); + _slide[i]->signal_released.connect([this](){ _sliderAnyReleased(); }); + _slide[i]->signal_value_changed.connect([this](){ _sliderAnyChanged(); }); } // Prevent 5th bar from being shown by PanelDialog::show_all_children - _l[4]->set_no_show_all(true); - _s[4]->set_no_show_all(true); - _b[4]->set_no_show_all(true); + _lbl[4]->set_no_show_all(true); + _slide[4]->set_no_show_all(true); + _btn[4]->set_no_show_all(true); setupMode(no_alpha); if constexpr ( - MODE == SPColorScalesMode::HSL || - MODE == SPColorScalesMode::HSV || - MODE == SPColorScalesMode::HSLUV || - MODE == SPColorScalesMode::OKLAB) + MODE == Color::Space::HSL || + MODE == Color::Space::HSV || + MODE == Color::Space::HSLUV || + MODE == Color::Space::OKLAB) { // Restore the visibility of the wheel - bool visible = Inkscape::Preferences::get()->getBool(_prefs + _pref_wheel_visibility, - false); + bool visible = Inkscape::Preferences::get()->getBool(wheel_pref, false); wheel_frame->set_expanded(visible); wheel_frame->set_vexpand(visible); } } -template +template void ColorScales::_recalcColor() { SPColor color; @@ -325,16 +302,16 @@ void ColorScales::_recalcColor() gfloat c[5]; if constexpr ( - MODE == SPColorScalesMode::RGB || - MODE == SPColorScalesMode::HSL || - MODE == SPColorScalesMode::HSV || - MODE == SPColorScalesMode::HSLUV || - MODE == SPColorScalesMode::OKLAB) + MODE == Color::Space::RGB || + MODE == Color::Space::HSL || + MODE == Color::Space::HSV || + MODE == Color::Space::HSLUV || + MODE == Color::Space::OKLAB) { _getRgbaFloatv(c); color.set(c[0], c[1], c[2]); alpha = c[3]; - } else if constexpr (MODE == SPColorScalesMode::CMYK) { + } else if constexpr (MODE == Color::Space::CMYK) { _getCmykaFloatv(c); float rgb[3]; @@ -348,7 +325,7 @@ void ColorScales::_recalcColor() _color.setColorAlpha(color, alpha); } -template +template void ColorScales::_updateDisplay(bool update_wheel) { #ifdef DUMP_CHANGE_INFO @@ -362,42 +339,42 @@ void ColorScales::_updateDisplay(bool update_wheel) SPColor color = _color.color(); - if constexpr (MODE == SPColorScalesMode::RGB) { + if constexpr (MODE == Color::Space::RGB) { color.get_rgb_floatv(c); c[3] = _color.alpha(); c[4] = 0.0; - } else if constexpr (MODE == SPColorScalesMode::HSL) { + } else if constexpr (MODE == Color::Space::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); } - } else if constexpr (MODE == SPColorScalesMode::HSV) { + } else if constexpr (MODE == Color::Space::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); } - } else if constexpr (MODE == SPColorScalesMode::CMYK) { + } else if constexpr (MODE == Color::Space::CMYK) { color.get_cmyk_floatv(c); c[4] = _color.alpha(); - } else if constexpr (MODE == SPColorScalesMode::HSLUV) { + } else if constexpr (MODE == Color::Space::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); } - } else if constexpr (MODE == SPColorScalesMode::OKLAB) { + } else if constexpr (MODE == Color::Space::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(i, hsl[i]); } - setScaled(_a[3], _color.alpha()); - setScaled(_a[4], 0.0); + setScaled(3, _color.alpha()); + setScaled(4, 0.0); _updateSliders(CSC_CHANNELS_ALL); _updating = false; if (update_wheel) { @@ -409,48 +386,41 @@ void ColorScales::_updateDisplay(bool update_wheel) } _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]); + setScaled(0, c[0]); + setScaled(1, c[1]); + setScaled(2, c[2]); + setScaled(3, c[3]); + setScaled(4, c[4]); _updateSliders(CSC_CHANNELS_ALL); _updating = false; } /* Helpers for setting color value */ -template -double ColorScales::getScaled(Glib::RefPtr const &a) +template +double ColorScales::getScaled(int index) const { - return a->get_value() / a->get_upper(); + return get_adj_scaled(_adj[index]); } -template -void ColorScales::setScaled(Glib::RefPtr &a, double v, bool constrained) +template +void ColorScales::setScaled(int index, double value) { - auto upper = a->get_upper(); - double val = v * upper; - if (constrained) { - // TODO: do we want preferences for these? - if (upper == 255) { - val = round(val/16) * 16; - } else { - val = round(val/10) * 10; - } - } - a->set_value(val); + set_adj_scaled(_adj[index], value); } -template -void ColorScales::_setRangeLimit(gdouble upper) + +template +std::vector ColorScales::getAllScaled() const { - _range_limit = upper; - for (auto & i : _a) { - i->set_upper(upper); + std::vector ret; + for (int i = 0; i < _channel_count - 1; i++) { + ret.push_back(getScaled(i)); } + return ret; } -template + +template void ColorScales::_onColorChanged() { if (!get_visible()) { return; } @@ -458,7 +428,7 @@ void ColorScales::_onColorChanged() _updateDisplay(); } -template +template void ColorScales::on_show() { Gtk::Box::on_show(); @@ -466,84 +436,74 @@ void ColorScales::on_show() _updateDisplay(); } -template +template void ColorScales::_getRgbaFloatv(gfloat *rgba) { g_return_if_fail(rgba != nullptr); - 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]); - } 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]); - } 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]); - } 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]); - } 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]); - } else if constexpr (MODE == SPColorScalesMode::OKLAB) { + if constexpr (MODE == Color::Space::RGB) { + rgba[0] = getScaled(0); + rgba[1] = getScaled(1); + rgba[2] = getScaled(2); + } else if constexpr (MODE == Color::Space::HSL) { + SPColor::hsl_to_rgb_floatv(rgba, getScaled(0), getScaled(1), getScaled(2)); + } else if constexpr (MODE == Color::Space::HSV) { + SPColor::hsv_to_rgb_floatv(rgba, getScaled(0), getScaled(1), getScaled(2)); + } else if constexpr (MODE == Color::Space::CMYK) { + SPColor::cmyk_to_rgb_floatv(rgba, getScaled(0), getScaled(1), + getScaled(2), getScaled(3)); + } else if constexpr (MODE == Color::Space::HSLUV) { + SPColor::hsluv_to_rgb_floatv(rgba, getScaled(0), getScaled(1), + getScaled(2)); + } else if constexpr (MODE == Color::Space::OKLAB) { auto const tmp = Oklab::oklab_to_rgb( - Oklab::okhsl_to_oklab({ getScaled(_a[0]), - getScaled(_a[1]), - getScaled(_a[2]) })); + Oklab::okhsl_to_oklab({ getScaled(0), + getScaled(1), + getScaled(2) })); for (size_t i : {0, 1, 2}) { rgba[i] = static_cast(tmp[i]); } - rgba[3] = getScaled(_a[3]); } else { g_warning("file %s: line %d: Illegal color selector mode", __FILE__, __LINE__); } + rgba[3] = _alpha_index > -1 ? getScaled(_alpha_index) : 1.0; } -template +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])); + if constexpr (MODE == Color::Space::RGB) { + SPColor::rgb_to_cmyk_floatv(cmyka, getScaled(0), getScaled(1), getScaled(2)); + } else if constexpr (MODE == Color::Space::HSL) { + SPColor::hsl_to_rgb_floatv(rgb, getScaled(0), getScaled(1), + getScaled(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])); + } else if constexpr (MODE == Color::Space::HSLUV) { + SPColor::hsluv_to_rgb_floatv(rgb, getScaled(0), getScaled(1), + getScaled(2)); SPColor::rgb_to_cmyk_floatv(cmyka, rgb[0], rgb[1], rgb[2]); - cmyka[4] = getScaled(_a[3]); - } else if constexpr (MODE == SPColorScalesMode::OKLAB) { + } else if constexpr (MODE == Color::Space::OKLAB) { auto const tmp = Oklab::oklab_to_rgb( - Oklab::okhsl_to_oklab({ getScaled(_a[0]), - getScaled(_a[1]), - getScaled(_a[2]) })); + Oklab::okhsl_to_oklab({ getScaled(0), + getScaled(1), + getScaled(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 if constexpr (MODE == Color::Space::CMYK) { + cmyka[0] = getScaled(0); + cmyka[1] = getScaled(1); + cmyka[2] = getScaled(2); + cmyka[3] = getScaled(3); } else { g_warning("file %s: line %d: Illegal color selector mode", __FILE__, __LINE__); } + cmyka[4] = _alpha_index > -1 ? getScaled(_alpha_index) : 1.0; } -template +template guint32 ColorScales::_getRgba32() { gfloat c[4]; @@ -556,248 +516,109 @@ guint32 ColorScales::_getRgba32() return rgba; } -template +template void ColorScales::setupMode(bool no_alpha) { + _updating = true; + gfloat rgba[4]; gfloat c[4]; - int alpha_index = 0; - if constexpr (MODE == SPColorScalesMode::NONE) { + if constexpr (MODE == Color::Space::NONE) { rgba[0] = rgba[1] = rgba[2] = rgba[3] = 1.0; } else { _getRgbaFloatv(rgba); } - if constexpr (MODE == SPColorScalesMode::RGB) { - _setRangeLimit(255.0); - _a[3]->set_upper(100.0); - _l[0]->set_markup_with_mnemonic(_("_R:")); - _s[0]->set_tooltip_text(_("Red")); - _b[0]->set_tooltip_text(_("Red")); - _l[1]->set_markup_with_mnemonic(_("_G:")); - _s[1]->set_tooltip_text(_("Green")); - _b[1]->set_tooltip_text(_("Green")); - _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)")); - _s[0]->setMap(nullptr); - _l[4]->set_visible(false); - _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]); - _updateSliders(CSC_CHANNELS_ALL); - _updating = false; - } else if constexpr (MODE == SPColorScalesMode::HSL) { - _setRangeLimit(100.0); - - _l[0]->set_markup_with_mnemonic(_("_H:")); - _s[0]->set_tooltip_text(_("Hue")); - _b[0]->set_tooltip_text(_("Hue")); - _a[0]->set_upper(360.0); - - _l[1]->set_markup_with_mnemonic(_("_S:")); - _s[1]->set_tooltip_text(_("Saturation")); - _b[1]->set_tooltip_text(_("Saturation")); - - _l[2]->set_markup_with_mnemonic(_("_L:")); - _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)")); - _s[0]->setMap(sp_color_scales_hue_map()); - _l[4]->set_visible(false); - _s[4]->set_visible(false); - _b[4]->set_visible(false); - _updating = true; - c[0] = 0.0; + int index = 0; + for (auto &comp : Color::getComponents(MODE)) { + _adj[index]->set_upper(comp.ink_scale); + _lbl[index]->set_markup_with_mnemonic(comp.name); + _btn[index]->set_tooltip_text(comp.tip); + _slide[index]->set_tooltip_text(comp.tip); + index++; + } - SPColor::rgb_to_hsl_floatv(c, rgba[0], rgba[1], rgba[2]); + if (!no_alpha) { + _adj[index]->set_upper(100); + _lbl[index]->set_markup_with_mnemonic(_("_A:")); + _slide[index]->set_tooltip_text(_("Alpha (opacity)")); + _btn[index]->set_tooltip_text(_("Alpha (opacity)")); + // Not sure why this is set. + _lbl[index]->set_no_show_all(true); + _slide[index]->set_no_show_all(true); + _btn[index]->set_no_show_all(true); + + // Set alpha last, it's always the same + setScaled(index, rgba[3]); + _alpha_index = index; + index++; + } else { + _alpha_index = -1; + } - setScaled(_a[0], c[0]); - setScaled(_a[1], c[1]); - setScaled(_a[2], c[2]); - setScaled(_a[3], rgba[3]); + // Show used widgets, hide unused widgets + for (int i = 0; i < 5; i++) { + _btn[i]->set_visible(i < index); + _lbl[i]->set_visible(i < index); + _slide[i]->set_visible(i < index); + } - _updateSliders(CSC_CHANNELS_ALL); - _updating = false; - } else if constexpr (MODE == SPColorScalesMode::HSV) { - _setRangeLimit(100.0); - - _l[0]->set_markup_with_mnemonic(_("_H:")); - _s[0]->set_tooltip_text(_("Hue")); - _b[0]->set_tooltip_text(_("Hue")); - _a[0]->set_upper(360.0); - - _l[1]->set_markup_with_mnemonic(_("_S:")); - _s[1]->set_tooltip_text(_("Saturation")); - _b[1]->set_tooltip_text(_("Saturation")); - - _l[2]->set_markup_with_mnemonic(_("_V:")); - _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)")); - _s[0]->setMap(sp_color_scales_hue_map()); - _l[4]->set_visible(false); - _s[4]->set_visible(false); - _b[4]->set_visible(false); - _updating = true; + if constexpr (MODE == Color::Space::RGB) { + setScaled(0, rgba[0]); + setScaled(1, rgba[1]); + setScaled(2, rgba[2]); + } else if constexpr (MODE == Color::Space::HSL) { + _slide[0]->setMap(sp_color_scales_hue_map()); 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]); - - _updateSliders(CSC_CHANNELS_ALL); - _updating = false; - } else if constexpr (MODE == SPColorScalesMode::CMYK) { - _setRangeLimit(100.0); - _l[0]->set_markup_with_mnemonic(_("_C:")); - _s[0]->set_tooltip_text(_("Cyan")); - _b[0]->set_tooltip_text(_("Cyan")); - - _l[1]->set_markup_with_mnemonic(_("_M:")); - _s[1]->set_tooltip_text(_("Magenta")); - _b[1]->set_tooltip_text(_("Magenta")); - - _l[2]->set_markup_with_mnemonic(_("_Y:")); - _s[2]->set_tooltip_text(_("Yellow")); - _b[2]->set_tooltip_text(_("Yellow")); - - _l[3]->set_markup_with_mnemonic(_("_K:")); - _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)")); - - _s[0]->setMap(nullptr); - _l[4]->set_visible(true); - _s[4]->set_visible(true); - _b[4]->set_visible(true); - _updating = true; + SPColor::rgb_to_hsl_floatv(c, rgba[0], rgba[1], rgba[2]); + setScaled(0, c[0]); + setScaled(1, c[1]); + setScaled(2, c[2]); + } else if constexpr (MODE == Color::Space::HSV) { + _slide[0]->setMap(sp_color_scales_hue_map()); + c[0] = 0.0; + SPColor::rgb_to_hsv_floatv(c, rgba[0], rgba[1], rgba[2]); + setScaled(0, c[0]); + setScaled(1, c[1]); + setScaled(2, c[2]); + } else if constexpr (MODE == Color::Space::CMYK) { 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], rgba[3]); - _updateSliders(CSC_CHANNELS_ALL); - _updating = false; - } else if constexpr (MODE == SPColorScalesMode::HSLUV) { - _setRangeLimit(100.0); - - _l[0]->set_markup_with_mnemonic(_("_H*:")); - _s[0]->set_tooltip_text(_("Hue")); - _b[0]->set_tooltip_text(_("Hue")); - _a[0]->set_upper(360.0); - - _l[1]->set_markup_with_mnemonic(_("_S*:")); - _s[1]->set_tooltip_text(_("Saturation")); - _b[1]->set_tooltip_text(_("Saturation")); - - _l[2]->set_markup_with_mnemonic(_("_L*:")); - _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)")); - - _s[0]->setMap(hsluvHueMap(0.0f, 0.0f, &_sliders_maps[0])); - _s[1]->setMap(hsluvSaturationMap(0.0f, 0.0f, &_sliders_maps[1])); - _s[2]->setMap(hsluvLightnessMap(0.0f, 0.0f, &_sliders_maps[2])); - - _l[4]->set_visible(false); - _s[4]->set_visible(false); - _b[4]->set_visible(false); - _updating = true; + setScaled(0, c[0]); + setScaled(1, c[1]); + setScaled(2, c[2]); + setScaled(3, c[3]); + } else if constexpr (MODE == Color::Space::HSLUV) { + _slide[0]->setMap(hsluvHueMap(0.0f, 0.0f, &_sliders_maps[0])); + _slide[1]->setMap(hsluvSaturationMap(0.0f, 0.0f, &_sliders_maps[1])); + _slide[2]->setMap(hsluvLightnessMap(0.0f, 0.0f, &_sliders_maps[2])); 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]); - - _updateSliders(CSC_CHANNELS_ALL); - _updating = false; - } else if constexpr (MODE == SPColorScalesMode::OKLAB) { - _setRangeLimit(100.0); - - _l[0]->set_markup_with_mnemonic(_("_HOK:")); - _s[0]->set_tooltip_text(_("Hue")); - _b[0]->set_tooltip_text(_("Hue")); - _a[0]->set_upper(360.0); - - _l[1]->set_markup_with_mnemonic(_("_SOK:")); - _s[1]->set_tooltip_text(_("Saturation")); - _b[1]->set_tooltip_text(_("Saturation")); - - _l[2]->set_markup_with_mnemonic(_("_LOK:")); - _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)")); - - _l[4]->set_visible(false); - _s[4]->set_visible(false); - _b[4]->set_visible(false); - _updating = true; - + setScaled(0, c[0]); + setScaled(1, c[1]); + setScaled(2, c[2]); + setScaled(3, rgba[3]); + } else if constexpr (MODE == Color::Space::OKLAB) { 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(i, tmp[i]); } - setScaled(_a[3], rgba[3]); - - _updateSliders(CSC_CHANNELS_ALL); - _updating = false; } else { g_warning("file %s: line %d: Illegal color selector mode", __FILE__, __LINE__); } - - if (no_alpha && alpha_index > 0) { - _l[alpha_index]->set_visible(false); - _s[alpha_index]->set_visible(false); - _b[alpha_index]->set_visible(false); - _l[alpha_index]->set_no_show_all(true); - _s[alpha_index]->set_no_show_all(true); - _b[alpha_index]->set_no_show_all(true); - } + _channel_count = index; + _updateSliders(CSC_CHANNELS_ALL); + _updating = false; } -template -SPColorScalesMode ColorScales::getMode() const { return MODE; } +template +Color::Space ColorScales::getMode() const { return MODE; } -template +template void ColorScales::_sliderAnyGrabbed() { if (_updating) { return; } @@ -808,7 +629,7 @@ void ColorScales::_sliderAnyGrabbed() } } -template +template void ColorScales::_sliderAnyReleased() { if (_updating) { return; } @@ -819,7 +640,7 @@ void ColorScales::_sliderAnyReleased() } } -template +template void ColorScales::_sliderAnyChanged() { if (_updating) { return; } @@ -827,7 +648,7 @@ void ColorScales::_sliderAnyChanged() _recalcColor(); } -template +template void ColorScales::_adjustmentChanged(int channel) { if (_updating) { return; } @@ -836,13 +657,13 @@ void ColorScales::_adjustmentChanged(int channel) _recalcColor(); } -template +template void ColorScales::_wheelChanged() { if constexpr ( - MODE == SPColorScalesMode::NONE || - MODE == SPColorScalesMode::RGB || - MODE == SPColorScalesMode::CMYK) + MODE == Color::Space::NONE || + MODE == Color::Space::RGB || + MODE == Color::Space::CMYK) { return; } @@ -871,7 +692,12 @@ void ColorScales::_wheelChanged() _updating = false; } -template +/** + * Set the gradient color in each of the channels + * + * @param channels - The channel NOT to update (this one is being moved!) + */ +template void ColorScales::_updateSliders(guint channels) { gfloat rgb0[3], rgbm[3], rgb1[3]; @@ -881,175 +707,167 @@ void ColorScales::_updateSliders(guint channels) #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]) }; + if constexpr (MODE == Color::Space::CMYK) { + return { getScaled(0), getScaled(1), getScaled(2), getScaled(3) }; } else { - return { getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2]), 0.0 }; + return { getScaled(0), getScaled(1), getScaled(2), 0.0 }; } }(); - if constexpr (MODE == SPColorScalesMode::RGB) { - if ((channels != CSC_CHANNEL_R) && (channels != CSC_CHANNEL_A)) { + if (channels == CSC_CHANNEL_A || channels == CSC_CHANNEL_CMYKA) { + // Alpha never updates the visual colors + } else if constexpr (MODE == Color::Space::RGB) { + if (channels != CSC_CHANNEL_R) { /* Update red */ - _s[0]->setColors(SP_RGBA32_F_COMPOSE(0.0, adj[1], adj[2], 1.0), + _slide[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)) { + if (channels != CSC_CHANNEL_G) { /* Update green */ - _s[1]->setColors(SP_RGBA32_F_COMPOSE(adj[0], 0.0, adj[2], 1.0), + _slide[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)) { + if (channels != CSC_CHANNEL_B) { /* Update blue */ - _s[2]->setColors(SP_RGBA32_F_COMPOSE(adj[0], adj[1], 0.0, 1.0), + _slide[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) { + + /* Update alpha */ + _slide[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 == Color::Space::HSL) { /* Hue is never updated */ - if ((channels != CSC_CHANNEL_S) && (channels != CSC_CHANNEL_A)) { + if (channels != CSC_CHANNEL_S) { /* 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), + _slide[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)) { + if (channels != CSC_CHANNEL_V) { /* 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), + _slide[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) { + + /* Update alpha */ + SPColor::hsl_to_rgb_floatv(rgb0, adj[0], adj[1], adj[2]); + _slide[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 == Color::Space::HSV) { /* Hue is never updated */ - if ((channels != CSC_CHANNEL_S) && (channels != CSC_CHANNEL_A)) { + if (channels != CSC_CHANNEL_S) { /* 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), + _slide[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)) { + if (channels != CSC_CHANNEL_V) { /* 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), + _slide[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 alpha */ + SPColor::hsv_to_rgb_floatv(rgb0, adj[0], adj[1], adj[2]); + _slide[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 == Color::Space::CMYK) { + if (channels != CSC_CHANNEL_C) { /* 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), + _slide[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)) { + if (channels != CSC_CHANNEL_M) { /* 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), + _slide[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)) { + if (channels != CSC_CHANNEL_Y) { /* 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), + _slide[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)) { + if (channels != CSC_CHANNEL_K) { /* 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), + _slide[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)) { + + /* Update alpha */ + SPColor::cmyk_to_rgb_floatv(rgb0, adj[0], adj[1], adj[2], adj[3]); + _slide[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 == Color::Space::HSLUV) { + if (channels != CSC_CHANNEL_H) { /* Update hue */ - _s[0]->setMap(hsluvHueMap(adj[1], adj[2], &_sliders_maps[0])); + _slide[0]->setMap(hsluvHueMap(adj[1], adj[2], &_sliders_maps[0])); } - if ((channels != CSC_CHANNEL_S) && (channels != CSC_CHANNEL_A)) { + if (channels != CSC_CHANNEL_S) { /* Update saturation (scaled chroma) */ - _s[1]->setMap(hsluvSaturationMap(adj[0], adj[2], &_sliders_maps[1])); + _slide[1]->setMap(hsluvSaturationMap(adj[0], adj[2], &_sliders_maps[1])); } - if ((channels != CSC_CHANNEL_V) && (channels != CSC_CHANNEL_A)) { + if (channels != CSC_CHANNEL_V) { /* Update lightness */ - _s[2]->setMap(hsluvLightnessMap(adj[0], adj[1], &_sliders_maps[2])); + _slide[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)); + /* Update alpha */ + SPColor::hsluv_to_rgb_floatv(rgb0, adj[0], adj[1], adj[2]); + _slide[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 == Color::Space::OKLAB) { + if (channels != CSC_CHANNEL_H) { + _slide[0]->setMap(Oklab::render_hue_scale(adj[1], adj[2], &_sliders_maps[0])); } - } 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) { + _slide[1]->setMap(Oklab::render_saturation_scale(360.0 * adj[0], adj[2], &_sliders_maps[1])); } - 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 ((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 (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 (channels != CSC_CHANNEL_V) { + _slide[2]->setMap(Oklab::render_lightness_scale(360.0 * adj[0], adj[1], &_sliders_maps[2])); } + // Update the alpha gradient. + auto const rgb = Oklab::oklab_to_rgb(Oklab::okhsl_to_oklab({adj[0], adj[1], adj[2]})); + _slide[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)); } else { g_warning("file %s: line %d: Illegal color selector mode", __FILE__, __LINE__); } @@ -1139,7 +957,7 @@ static guchar const *sp_color_scales_hsluv_map(guchar *map, return map; } -template +template guchar const *ColorScales::hsluvHueMap(gfloat s, gfloat l, std::array *map) { @@ -1148,7 +966,7 @@ guchar const *ColorScales::hsluvHueMap(gfloat s, gfloat l, }); } -template +template guchar const *ColorScales::hsluvSaturationMap(gfloat h, gfloat l, std::array *map) { @@ -1157,7 +975,7 @@ guchar const *ColorScales::hsluvSaturationMap(gfloat h, gfloat l, }); } -template +template guchar const *ColorScales::hsluvLightnessMap(gfloat h, gfloat s, std::array *map) { @@ -1166,53 +984,39 @@ guchar const *ColorScales::hsluvLightnessMap(gfloat h, gfloat s, }); } -template +template ColorScalesFactory::ColorScalesFactory() {} -template +template Gtk::Widget *ColorScalesFactory::createWidget(Inkscape::UI::SelectedColor &color, bool no_alpha) const { Gtk::Widget *w = Gtk::make_managed>(color, no_alpha); return w; } -template +template Glib::ustring ColorScalesFactory::modeName() const { - if constexpr (MODE == SPColorScalesMode::RGB) { - return gettext(ColorScales<>::SUBMODE_NAMES[1]); - } else if constexpr (MODE == SPColorScalesMode::HSL) { - return gettext(ColorScales<>::SUBMODE_NAMES[2]); - } else if constexpr (MODE == SPColorScalesMode::CMYK) { - return gettext(ColorScales<>::SUBMODE_NAMES[3]); - } else if constexpr (MODE == SPColorScalesMode::HSV) { - return gettext(ColorScales<>::SUBMODE_NAMES[4]); - } else if constexpr (MODE == SPColorScalesMode::HSLUV) { - return gettext(ColorScales<>::SUBMODE_NAMES[5]); - } else if constexpr (MODE == SPColorScalesMode::OKLAB) { - return gettext(ColorScales<>::SUBMODE_NAMES[6]); - } else { - return gettext(ColorScales<>::SUBMODE_NAMES[0]); - } + return get_color_mode_label(MODE); } // Explicit instantiations -template class ColorScales; -template class ColorScales; -template class ColorScales; -template class ColorScales; -template class ColorScales; -template class ColorScales; -template class ColorScales; - -template class ColorScalesFactory; -template class ColorScalesFactory; -template class ColorScalesFactory; -template class ColorScalesFactory; -template class ColorScalesFactory; -template class ColorScalesFactory; -template class ColorScalesFactory; +template class ColorScales; +template class ColorScales; +template class ColorScales; +template class ColorScales; +template class ColorScales; +template class ColorScales; +template class ColorScales; + +template class ColorScalesFactory; +template class ColorScalesFactory; +template class ColorScalesFactory; +template class ColorScalesFactory; +template class ColorScalesFactory; +template class ColorScalesFactory; +template class ColorScalesFactory; } // namespace Inkscape::UI::Widget diff --git a/src/ui/widget/color-scales.h b/src/ui/widget/color-scales.h index 0ef0eb329e..5642c4dfd8 100644 --- a/src/ui/widget/color-scales.h +++ b/src/ui/widget/color-scales.h @@ -21,36 +21,27 @@ #include "helper/auto-connection.h" #include "ui/selected-color.h" +#include "color/components.h" namespace Inkscape::UI::Widget { class ColorSlider; class ColorWheel; -enum class SPColorScalesMode { - NONE, - RGB, - HSL, - CMYK, - HSV, - HSLUV, - OKLAB -}; - -template +template class ColorScales : public Gtk::Box { public: - static gchar const *SUBMODE_NAMES[]; - - static double getScaled(Glib::RefPtr const &a); - static void setScaled(Glib::RefPtr &a, double v, bool constrained = false); + std::vector getAllScaled() const; ColorScales(SelectedColor &color, bool no_alpha); + double getScaled(int index) const; + void setScaled(int index, double value); + void setupMode(bool no_alpha); - SPColorScalesMode getMode() const; + Color::Space getMode() const; static guchar const *hsluvHueMap(gfloat s, gfloat l, std::array *map); @@ -78,24 +69,21 @@ protected: void _recalcColor(); void _updateDisplay(bool update_wheel = true); - void _setRangeLimit(gdouble upper); - SelectedColor &_color; gdouble _range_limit; gboolean _updating : 1; gboolean _dragging : 1; - std::vector> _a; /* Channel adjustments */ - Inkscape::UI::Widget::ColorSlider *_s[5]; /* Channel sliders */ - Gtk::Widget *_b[5]; /* Spinbuttons */ - Gtk::Label *_l[5]; /* Labels */ + std::vector> _adj; /* Channel adjustments */ + Inkscape::UI::Widget::ColorSlider *_slide[5]; /* Channel sliders */ + Gtk::Widget *_btn[5]; /* Spinbuttons */ + Gtk::Label *_lbl[5]; /* Labels */ std::array _sliders_maps[4]; Inkscape::UI::Widget::ColorWheel *_wheel; - const Glib::ustring _prefs = "/color_scales"; - static gchar const * const _pref_wheel_visibility; - auto_connection _color_changed; auto_connection _color_dragged; + int _alpha_index = -1; + int _channel_count = 0; public: // By default, disallow copy constructor and assignment operator @@ -103,7 +91,7 @@ public: ColorScales &operator=(ColorScales const &obj) = delete; }; -template +template class ColorScalesFactory : public Inkscape::UI::ColorSelectorFactory { public: @@ -115,7 +103,7 @@ public: struct ColorPickerDescription { - SPColorScalesMode mode; + Color::Space mode; const char* icon; const char* label; Glib::ustring visibility_path; diff --git a/src/ui/widget/color-slider.cpp b/src/ui/widget/color-slider.cpp index 703f1e0f43..04c413c40a 100644 --- a/src/ui/widget/color-slider.cpp +++ b/src/ui/widget/color-slider.cpp @@ -21,7 +21,7 @@ #include "ui/widget/color-slider.h" #include "preferences.h" #include "ui/controller.h" -#include "ui/widget/color-scales.h" +#include "ui/util.h" static const gint ARROW_SIZE = 8; @@ -107,7 +107,7 @@ Gtk::EventSequenceState ColorSlider::on_click_pressed(Gtk::GestureMultiPress con auto const value = get_value_at(*_drawing_area, x, y); auto const state = Controller::get_current_event_state(click); auto const constrained = get_constrained(state); - ColorScales<>::setScaled(_adjustment, value, constrained); + set_adj_scaled(_adjustment, value, constrained); signal_dragged.emit(); return Gtk::EVENT_SEQUENCE_NONE; } @@ -134,7 +134,7 @@ void ColorSlider::on_motion(GtkEventControllerMotion const * const motion, auto const value = get_value_at(*_drawing_area, x, y); auto const state = Controller::get_device_state(GTK_EVENT_CONTROLLER(motion)); auto const constrained = get_constrained(state); - ColorScales<>::setScaled(_adjustment, value, constrained); + set_adj_scaled(_adjustment, value, constrained); signal_dragged.emit(); } } @@ -161,7 +161,7 @@ void ColorSlider::setAdjustment(Glib::RefPtr adjustment) _adjustment_value_changed_connection = _adjustment->signal_value_changed().connect(sigc::mem_fun(*this, &ColorSlider::_onAdjustmentValueChanged)); - _value = ColorScales<>::getScaled(_adjustment); + _value = get_adj_scaled(_adjustment); _onAdjustmentChanged(); } @@ -171,15 +171,15 @@ void ColorSlider::_onAdjustmentChanged() { _drawing_area->queue_draw(); } void ColorSlider::_onAdjustmentValueChanged() { - if (_value != ColorScales<>::getScaled(_adjustment)) { + if (_value != get_adj_scaled(_adjustment)) { constexpr int cx = 0, cy = 0; // formerly held CSS padding, now Box handles that auto const cw = _drawing_area->get_width (); auto const ch = _drawing_area->get_height(); - if ((gint)(ColorScales<>::getScaled(_adjustment) * cw) != (gint)(_value * cw)) { + if ((gint)(get_adj_scaled(_adjustment) * cw) != (gint)(_value * cw)) { gint ax, ay; gfloat value; value = _value; - _value = ColorScales<>::getScaled(_adjustment); + _value = get_adj_scaled(_adjustment); ax = (int)(cx + value * cw - ARROW_SIZE / 2 - 2); ay = cy; _drawing_area->queue_draw_area(ax, ay, ARROW_SIZE + 4, ch); @@ -188,7 +188,7 @@ void ColorSlider::_onAdjustmentValueChanged() _drawing_area->queue_draw_area(ax, ay, ARROW_SIZE + 4, ch); } else { - _value = ColorScales<>::getScaled(_adjustment); + _value = get_adj_scaled(_adjustment); } } } diff --git a/src/ui/widget/paint-selector.cpp b/src/ui/widget/paint-selector.cpp index e70a0f076a..69ba4ec2fe 100644 --- a/src/ui/widget/paint-selector.cpp +++ b/src/ui/widget/paint-selector.cpp @@ -63,10 +63,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 @@ -1172,7 +1168,7 @@ void PaintSelector::setFlatColor(SPDesktop *desktop, gchar const *color_property #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() : "")); + (color ? color.colorProfile.c_str() : "")); #endif // SP_PS_VERBOSE sp_repr_css_set_property(css, color_property, colorStr.c_str()); diff --git a/src/util/object-renderer.cpp b/src/util/object-renderer.cpp index f9829e6f90..a6eb204a2e 100644 --- a/src/util/object-renderer.cpp +++ b/src/util/object-renderer.cpp @@ -225,7 +225,8 @@ 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); + auto &rgb = stop.color.getColors(); // XXX convertTo(Color::Space::RGB); + ctx->set_source_rgba(rgb[0], rgb[1], rgb[2], stop.opacity); ctx->fill_preserve(); ctx->set_source_rgb(0.5, 0.5, 0.5); ctx->stroke(); diff --git a/testfiles/src/attributes-test.cpp b/testfiles/src/attributes-test.cpp index df00ea9dc4..d1500edde9 100644 --- a/testfiles/src/attributes-test.cpp +++ b/testfiles/src/attributes-test.cpp @@ -269,6 +269,7 @@ std::vector getKnownAttrs() AttributeInfo("refX", true), AttributeInfo("refY", true), AttributeInfo("rendering-intent", true), + AttributeInfo("inkscape:default", true), AttributeInfo("repeatCount", true), AttributeInfo("repeatDur", true), AttributeInfo("requiredFeatures", true), diff --git a/testfiles/src/color-profile-test.cpp b/testfiles/src/color-profile-test.cpp index 4089920416..9aa26e4dc0 100644 --- a/testfiles/src/color-profile-test.cpp +++ b/testfiles/src/color-profile-test.cpp @@ -58,21 +58,22 @@ TEST_F(ColorProfileTest, SetRenderingIntent) { struct { gchar const *attr; - guint intVal; + Inkscape::RenderingIntent intent; } 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}, + {"auto", Inkscape::RenderingIntent::AUTO}, + {"perceptual", Inkscape::RenderingIntent::PERCEPTUAL}, + {"relative-colorimetric", Inkscape::RenderingIntent::RELATIVE_COLORIMETRIC}, + {"relative-colorimetric-nobpc", Inkscape::RenderingIntent::RELATIVE_COLORIMETRIC_NOBPC}, + {"absolute-colorimetric", Inkscape::RenderingIntent::ABSOLUTE_COLORIMETRIC}, + {"saturation", Inkscape::RenderingIntent::SATURATION}, + {"something-else", Inkscape::RenderingIntent::UNKNOWN}, + {"auto2", Inkscape::RenderingIntent::UNKNOWN}, }; for (auto i : cases) { - _prof->setKeyValue( SPAttr::RENDERING_INTENT, i.attr); - ASSERT_EQ( (guint)i.intVal, _prof->rendering_intent ) << i.attr; + _prof->setKeyValue(SPAttr::RENDERING_INTENT, i.attr); + ASSERT_EQ(i.intent, _prof->getRenderingIntent()) << i.attr; } } @@ -102,14 +103,11 @@ TEST_F(ColorProfileTest, SetName) }; 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, i); + ASSERT_EQ(std::string(i), _prof->getName()); } _prof->setKeyValue( SPAttr::NAME, NULL ); - ASSERT_EQ( (gchar*)0, _prof->name ); + ASSERT_TRUE(_prof->getName().empty()); } diff --git a/testfiles/src/svg-color-test.cpp b/testfiles/src/svg-color-test.cpp index c4e937928d..cd44de1534 100644 --- a/testfiles/src/svg-color-test.cpp +++ b/testfiles/src/svg-color-test.cpp @@ -13,7 +13,7 @@ #include #include "preferences.h" -#include "svg/svg-icc-color.h" +#include "color.h" static void check_rgb24(unsigned const rgb24) { @@ -92,19 +92,19 @@ TEST(SvgColorTest, testIccColor) }; for (size_t i = 0; i < G_N_ELEMENTS(cases); i++) { - SVGICCColor tmp; + SPColor tmp; char const *str = cases[i].str; char const *result = nullptr; - bool parseRet = sp_svg_read_icc_color(str, &result, &tmp); + bool parseRet = tmp.read_icc_color(str, &result); ASSERT_EQ(parseRet, cases[i].shouldPass) << str; - ASSERT_EQ(tmp.colors.size(), cases[i].numEntries) << str; + ASSERT_EQ(tmp.getColors().size(), cases[i].numEntries) << str; if (cases[i].shouldPass) { ASSERT_STRNE(str, result); - ASSERT_EQ(tmp.colorProfile, cases[i].name) << str; + ASSERT_EQ(tmp.getColorProfile(), cases[i].name) << str; } else { ASSERT_STREQ(str, result); - ASSERT_TRUE(tmp.colorProfile.empty()); + ASSERT_TRUE(tmp.getColorProfile().empty()); } } } -- GitLab From bfaeff01ce2ce642f4f38905aee87ebd419862d9 Mon Sep 17 00:00:00 2001 From: Martin Owens Date: Wed, 1 Nov 2023 14:19:34 -0400 Subject: [PATCH 3/3] Color refactoring... not complete yet. --- src/CMakeLists.txt | 2 +- src/actions/actions-canvas-mode.cpp | 17 +- src/attributes.cpp | 1 - src/attributes.h | 1 - src/color.h | 6 +- src/color/CMakeLists.txt | 23 - src/color/cms-color-types.h | 75 --- src/color/cms-system.cpp | 365 ------------ src/color/cms-system.h | 152 ----- src/color/cms-util.cpp | 126 ---- src/color/cms-util.h | 58 -- 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/color/components.cpp | 97 --- src/color/manager.cpp | 207 ------- src/color/manager.h | 75 --- src/color/spaces.cpp | 17 - src/color/spaces.h | 81 --- src/colors/CMakeLists.txt | 66 +++ src/colors/cms/profile.cpp | 281 +++++++++ src/colors/cms/profile.h | 69 +++ src/colors/cms/system.cpp | 274 +++++++++ src/colors/cms/system.h | 91 +++ src/colors/cms/transform.cpp | 180 ++++++ src/colors/cms/transform.h | 66 +++ src/colors/color.cpp | 552 ++++++++++++++++++ src/colors/color.h | 118 ++++ src/colors/dragndrop.cpp | 97 +++ src/colors/dragndrop.h | 40 ++ src/colors/manager.cpp | 227 +++++++ src/colors/manager.h | 83 +++ src/colors/parser.cpp | 192 ++++++ src/colors/parser.h | 79 +++ src/colors/printer.cpp | 57 ++ src/colors/printer.h | 97 +++ src/colors/spaces/base.cpp | 106 ++++ src/colors/spaces/base.h | 83 +++ src/colors/spaces/cms.cpp | 158 +++++ src/colors/spaces/cms.h | 67 +++ src/colors/spaces/cmyk.cpp | 69 +++ src/colors/spaces/cmyk.h | 43 ++ src/colors/spaces/components.cpp | 106 ++++ src/{color => colors/spaces}/components.h | 27 +- src/colors/spaces/enum.h | 59 ++ src/colors/spaces/hsl.cpp | 127 ++++ src/colors/spaces/hsl.h | 48 ++ src/colors/spaces/hsluv.cpp | 141 +++++ src/colors/spaces/hsluv.h | 53 ++ src/colors/spaces/hsv.cpp | 133 +++++ src/colors/spaces/hsv.h | 46 ++ src/colors/spaces/lab.cpp | 141 +++++ src/colors/spaces/lab.h | 59 ++ src/colors/spaces/lch.cpp | 118 ++++ src/colors/spaces/lch.h | 63 ++ src/colors/spaces/linear-rgb.cpp | 89 +++ src/colors/spaces/linear-rgb.h | 42 ++ src/colors/spaces/luv.cpp | 130 +++++ src/colors/spaces/luv.h | 58 ++ src/colors/spaces/named.cpp | 236 ++++++++ src/colors/spaces/named.h | 47 ++ src/colors/spaces/okhsl.cpp | 70 +++ src/colors/spaces/okhsl.h | 49 ++ src/colors/spaces/oklab.cpp | 148 +++++ src/colors/spaces/oklab.h | 59 ++ src/colors/spaces/oklch.cpp | 375 ++++++++++++ src/colors/spaces/oklch.h | 57 ++ src/colors/spaces/rgb.cpp | 79 +++ src/colors/spaces/rgb.h | 48 ++ src/colors/spaces/xyz.cpp | 78 +++ src/colors/spaces/xyz.h | 65 +++ src/colors/tracker.cpp | 266 +++++++++ src/colors/tracker.h | 97 +++ src/colors/utils.cpp | 203 +++++++ src/colors/utils.h | 56 ++ src/colors/xml-color.cpp | 167 ++++++ src/colors/xml-color.h | 46 ++ src/desktop.cpp | 24 +- src/desktop.h | 2 - src/display/drawing-item.cpp | 6 +- src/display/drawing.cpp | 2 +- src/display/drawing.h | 12 +- src/document.cpp | 4 +- src/document.h | 10 +- .../internal/pdfinput/svg-builder.cpp | 10 +- src/inkscape-window.cpp | 4 +- src/inkscape.cpp | 3 +- src/object/color-profile.cpp | 515 ++++------------ src/object/color-profile.h | 107 +--- src/object/sp-image.cpp | 11 +- src/svg/svg-color.cpp | 5 +- src/ui/dialog/document-properties.cpp | 72 +-- src/ui/dialog/export.cpp | 9 +- src/ui/dialog/global-palettes.cpp | 10 +- src/ui/dialog/inkscape-preferences.cpp | 53 +- src/ui/icon-loader.cpp | 1 - src/ui/tools/tweak-tool.cpp | 36 +- src/ui/widget/canvas.cpp | 14 +- src/ui/widget/canvas.h | 8 +- src/ui/widget/cms-popover.cpp | 15 +- src/ui/widget/color-notebook.cpp | 25 +- src/ui/widget/color-scales.cpp | 218 +++---- src/ui/widget/color-scales.h | 10 +- src/util/numeric/converters.h | 2 + testfiles/CMakeLists.txt | 65 +-- testfiles/data/SwappedRedAndGreen.icc | Bin 0 -> 15720 bytes testfiles/data/color-cms.svg | 29 + testfiles/data/default_cmyk.icc | Bin 0 -> 187484 bytes testfiles/data/display.icc | Bin 0 -> 864 bytes testfiles/src/color-profile-test.cpp | 125 ---- testfiles/src/colors/cms-test.cpp | 252 ++++++++ testfiles/src/colors/color-test.cpp | 421 +++++++++++++ testfiles/src/colors/dragndrop-test.cpp | 82 +++ testfiles/src/colors/manager-test.cpp | 67 +++ testfiles/src/colors/parser-test.cpp | 112 ++++ testfiles/src/colors/spaces-cms-test.cpp | 69 +++ testfiles/src/colors/spaces-cmyk-test.cpp | 88 +++ testfiles/src/colors/spaces-hsl-test.cpp | 102 ++++ testfiles/src/colors/spaces-hsluv-test.cpp | 57 ++ testfiles/src/colors/spaces-hsv-test.cpp | 105 ++++ testfiles/src/colors/spaces-lab-test.cpp | 81 +++ testfiles/src/colors/spaces-lch-test.cpp | 89 +++ .../src/colors/spaces-linear-rgb-test.cpp | 78 +++ testfiles/src/colors/spaces-luv-test.cpp | 71 +++ testfiles/src/colors/spaces-named-test.cpp | 46 ++ testfiles/src/colors/spaces-oklab-test.cpp | 80 +++ testfiles/src/colors/spaces-oklch-test.cpp | 84 +++ testfiles/src/colors/spaces-rgb-test.cpp | 93 +++ testfiles/src/colors/spaces-testbase.h | 371 ++++++++++++ testfiles/src/colors/spaces-xyz-test.cpp | 79 +++ testfiles/src/colors/tracker-test.cpp | 210 +++++++ testfiles/src/colors/utils-test.cpp | 125 ++++ testfiles/src/colors/xml-color-test.cpp | 122 ++++ testfiles/src/oklab-color-test.cpp | 171 ------ testfiles/src/svg-color-test.cpp | 112 ---- 137 files changed, 10021 insertions(+), 2783 deletions(-) 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/color/components.cpp delete mode 100644 src/color/manager.cpp delete mode 100644 src/color/manager.h delete mode 100644 src/color/spaces.cpp delete mode 100644 src/color/spaces.h 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.cpp create mode 100644 src/colors/color.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 rename src/{color => colors/spaces}/components.h (53%) create mode 100644 src/colors/spaces/enum.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/tracker.cpp create mode 100644 src/colors/tracker.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/SwappedRedAndGreen.icc create mode 100644 testfiles/data/color-cms.svg create mode 100644 testfiles/data/default_cmyk.icc create mode 100644 testfiles/data/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-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/spaces-cms-test.cpp create mode 100644 testfiles/src/colors/spaces-cmyk-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-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/tracker-test.cpp create mode 100644 testfiles/src/colors/utils-test.cpp create mode 100644 testfiles/src/colors/xml-color-test.cpp delete mode 100644 testfiles/src/oklab-color-test.cpp delete mode 100644 testfiles/src/svg-color-test.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index bb1a37270b..e09a096e5b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -304,7 +304,7 @@ 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) add_subdirectory(extension) diff --git a/src/actions/actions-canvas-mode.cpp b/src/actions/actions-canvas-mode.cpp index 8941d04892..978efa589f 100644 --- a/src/actions/actions-canvas-mode.cpp +++ b/src/actions/actions-canvas-mode.cpp @@ -19,7 +19,7 @@ #include "actions-helper.h" -#include "color/manager.h" +#include "colors/manager.h" #include "desktop.h" #include "document.h" #include "inkscape-application.h" @@ -264,19 +264,20 @@ canvas_color_colorproof(bool state, InkscapeWindow *win) auto &cm = desktop->getDocument()->getColorManager(); // Ensure we have a cmyk profile if enabling for the first time - if (state && !cm.getDefault()) { + if (state && !cm.defaultSpace()) { auto default_uri = prefs->getString("/options/cms/uri"); auto default_intent = prefs->getInt("/options/cms/intent"); if (!default_uri.empty()) { // Preferences contain a default cms profile, use that - if (auto profile = cm.addProfile(default_uri, (Inkscape::RenderingIntent)(default_intent+2))) { - cm.setDefault(profile); - Inkscape::DocumentUndo::done(profile->document, _("Auto add default color profile"), ""); + //if (auto profile = cm.addProfile(default_uri)) { + // XXX profile->setIntent(Inkscape::Colors::RenderingIntent)(default_intent+2); + //cm.setDefault(profile); + //Inkscape::DocumentUndo::done(desktop->getDocument(), _("Auto add default color profile"), ""); // TODO: Convert all colors to the new profile, but ASK first! // cm.convertToCMYK(profile); - } else { - g_warning("Couldnt set the default cms profile: '%s'", default_uri.c_str()); - } + //} else { + //g_warning("Couldnt set the default cms profile: '%s'", default_uri.c_str()); + //} } else { // No default, so we're opening the document properties instead if (auto action = Glib::RefPtr::cast_dynamic(win->lookup_action("canvas-color-colorproof"))) { diff --git a/src/attributes.cpp b/src/attributes.cpp index 8ceb5c4578..55d0746213 100644 --- a/src/attributes.cpp +++ b/src/attributes.cpp @@ -134,7 +134,6 @@ static SPStyleProp const props[] = { {SPAttr::LOCAL, "local"}, {SPAttr::NAME, "name"}, {SPAttr::RENDERING_INTENT, "rendering-intent"}, - {SPAttr::DEFAULT, "inkscape:default"}, /* SPGuide */ {SPAttr::ORIENTATION, "orientation"}, {SPAttr::POSITION, "position"}, diff --git a/src/attributes.h b/src/attributes.h index 660fafacec..4c204b98fe 100644 --- a/src/attributes.h +++ b/src/attributes.h @@ -133,7 +133,6 @@ enum class SPAttr { LOCAL, NAME, RENDERING_INTENT, - DEFAULT, /* SPGuide */ ORIENTATION, POSITION, diff --git a/src/color.h b/src/color.h index f90939d810..150e3489cb 100644 --- a/src/color.h +++ b/src/color.h @@ -15,10 +15,9 @@ * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ +#include #include -#include "color/components.h" - typedef unsigned int guint32; // uint is guaranteed to hold up to 2^32 − 1 /* Useful composition macros */ @@ -38,7 +37,6 @@ typedef unsigned int guint32; // uint is guaranteed to hold up to 2^32 − 1 #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) -using Inkscape::Color::Space; namespace Inkscape { class ColorProfile; } @@ -85,7 +83,7 @@ public: bool hasColors() const { return true; } void unsetColors(); - void setColors(std::vector &&values, Space space = Space::RGB); + void setColors(std::vector &&values); void setColor(unsigned int index, double value); void copyColors(const SPColor &other); const std::vector &getColors() const { return colors; } diff --git a/src/color/CMakeLists.txt b/src/color/CMakeLists.txt deleted file mode 100644 index 79a378c610..0000000000 --- a/src/color/CMakeLists.txt +++ /dev/null @@ -1,23 +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 - components.cpp - manager.cpp - - # ------- - # Headers - color-profile-cms-fns.h - cms-color-types.h - cms-system.h - cms-util.h - cmyk-conv.h - color-conv.h - components.h - manager.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 c6b90878b2..0000000000 --- a/src/color/cms-color-types.h +++ /dev/null @@ -1,75 +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; - -protected: - FourCCSig( guint32 value ) : value(value) {}; - - guint32 value; -}; - -class ColorSpaceSig : public FourCCSig { -public: - ColorSpaceSig( ColorSpaceSig const &other ) = default; - -protected: - ColorSpaceSig( guint32 value ) : FourCCSig(value) {}; -}; - -class ColorProfileClassSig : public FourCCSig { -public: - ColorProfileClassSig( ColorProfileClassSig const &other ) = default; - -protected: - 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 89fbcd5d8b..0000000000 --- a/src/color/cms-system.cpp +++ /dev/null @@ -1,365 +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 -#include -#include -#endif - -using Inkscape::ColorProfile; - -namespace Inkscape { - -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; - - Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - - 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; -} - -// 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 && - profile_info.is_output()) - { - std::string name = profile_info.get_name(); - result.emplace_back(name); - } - } - std::sort(result.begin(), result.end()); - - return result; -} - -// Returns vector of names to list in Preferences dialog: proofing profiles. -std::vector CMSSystem::get_cms_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; -} - -const ICCProfileInfo *CMSSystem::get_info_for_profile(Glib::ustring const &name) const -{ - for (auto const &profile_info : system_profile_infos) { - if (name == profile_info.get_name() || name == profile_info.get_path()) { - return &profile_info; - } - } - return nullptr; -} - -// Static, doesn't rely on class. Simply calls lcms' cmsDoTransform. -void CMSSystem::do_transform(cmsHTRANSFORM transform, unsigned char *inBuf, unsigned char *outBuf, unsigned size) -{ - cmsDoTransform(transform, inBuf, outBuf, size); -} - -/** - * Apply the CMS transform to the cairo surface and paint it into the output surface. - * - * Based on ink_cairo_surface_filter in cauri-templates.h - */ -void CMSSystem::do_transform(cmsHTRANSFORM transform, cairo_surface_t *in, cairo_surface_t *out) -{ - 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)) { - g_warning("Different image formats while applying CMS!"); - return; - } - - for (int i = 0; i < height; i++) { - auto row_in = px_in + i * stride; - auto row_out = px_out + i * stride; - do_transform(transform, row_in, row_out, width); - } - - cairo_surface_mark_dirty(out); -} - - -/* - * Get the color managed trasform for the screen. - * - * There is one transform for all monitors, anything more complex and the user should - * use their operating system CMS configurations instead of the Inkscape monitor cms. - * - * Transform immutably shared between CMSSystem and Canvas. - */ -std::shared_ptr const &CMSSystem::get_screen_transform() -{ - bool preferences_changed = false; - - Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - bool display = prefs->getIntLimited("/options/displayprofile/enabled", false); - int displayIntent = prefs->getIntLimited("/options/displayprofile/intent", 0, 0, 3); - - if (lastDisplay != display || lastDisplayIntent != displayIntent) { - preferences_changed = true; - lastDisplay = display; - lastDisplayIntent = displayIntent; - } - - auto monitor_profile = display ? get_monitor_profile() : nullptr; - bool need_to_update = preferences_changed || current_monitor_profile_changed; - - if (need_to_update) { - if (monitor_profile) { - current_transform = CMSTransform::create( - cmsCreateTransform(sRGB_profile, TYPE_BGRA_8, monitor_profile, TYPE_BGRA_8, displayIntent, 0)); - } else { - current_transform = nullptr; - } - } - - 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 e5bc4d5372..0000000000 --- a/src/color/cms-system.h +++ /dev/null @@ -1,152 +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 CMSProfile : Inkscape::Color::Profile -{ - explicit CMSProfile(cmsHPROFILE handle) : _handle(handle) { assert(_handle); } - - CMSProfile(CMSProfile const &) = delete; - CMSProfile &operator=(CMSProfile const &) = delete; - ~CMSProfile() { cmsCloseProfile(_handle); } - - std::vector toRGB(std::vector const colors) const; - std::vector fromRGB(std::vector const rgb) const; - - cmsHPROFILE getHandle() const { return _handle; } -private: - cmsHPROFILE _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_cms_profile_names() const; - std::string get_path_for_profile(Glib::ustring const &name) const; - std::shared_ptr const &get_screen_transform(); - const ICCProfileInfo *get_info_for_profile(Glib::ustring const &name) const; - - static void do_transform(cmsHTRANSFORM transform, unsigned char *inBuf, unsigned char *outBuf, unsigned size); - static void do_transform(cmsHTRANSFORM transform, cairo_surface_t *in, cairo_surface_t *out); - -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 lastDisplay = false; - int lastDisplayIntent = INTENT_PERCEPTUAL; - bool lastProof = false; - int lastProofIntent = INTENT_PERCEPTUAL; - bool lastBPC = false; - bool lastGamutWarn = false; - Gdk::RGBA lastGamutColor = Gdk::RGBA("#808080"); - - 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 84e3136d67..0000000000 --- a/src/color/cms-util.cpp +++ /dev/null @@ -1,126 +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); - - // Output RGB ICC profiles are usually monitors - _output = cmsIsCLUT(profile, INTENT_PERCEPTUAL, LCMS_USED_AS_OUTPUT); -} - -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 b95e37bd60..0000000000 --- a/src/color/cms-util.h +++ /dev/null @@ -1,58 +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; } - bool is_output() const { return _output; } - -private: - std::string _path; - std::string _name; - bool _in_home; - bool _output; - 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/color/components.cpp b/src/color/components.cpp deleted file mode 100644 index cb11d2abda..0000000000 --- a/src/color/components.cpp +++ /dev/null @@ -1,97 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/** @file - * Manage color space components - *//* - * 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 - -namespace Inkscape::Color { - -Component::Component(std::string name, std::string tip, unsigned int cms_scale, unsigned int ink_scale) - : name(std::move(name)) - , tip(std::move(tip)) - , cms_scale(cms_scale) - , ink_scale(ink_scale) -{ -} - -std::vector getComponents(Space space) -{ - static std::map> sets; - - if (sets.empty()) { - - // Inkscape internal components - - sets[Space::RGB].emplace_back(_("_R:"), _("Red"), 1, 255); // TYPE_RGB_16 - sets[Space::RGB].emplace_back(_("_G:"), _("Green"), 1, 255); - sets[Space::RGB].emplace_back(_("_B:"), _("Blue"), 1, 255); - - sets[Space::HSL].emplace_back(_("_H:"), _("Hue"), 360, 255); // TYPE_HLS_16 - sets[Space::HSL].emplace_back(_("_L:"), _("Lightness"), 1, 100); - sets[Space::HSL].emplace_back(_("_S:"), _("Saturation"), 1, 100); - - sets[Space::CMYK].emplace_back(_("_C:"), _("Cyan"), 1, 100); // TYPE_CMYK_16 - sets[Space::CMYK].emplace_back(_("_M:"), _("Magenta"), 1, 100); - sets[Space::CMYK].emplace_back(_("_Y:"), _("Yellow"), 1, 100); - sets[Space::CMYK].emplace_back(_("_K:"), _("Black"), 1, 100); - - sets[Space::CMY].emplace_back(_("_C:"), _("Cyan"), 1, 100); // TYPE_CMY_16 - sets[Space::CMY].emplace_back(_("_M:"), _("Magenta"), 1, 100); - sets[Space::CMY].emplace_back(_("_Y:"), _("Yellow"), 1, 100); - - sets[Space::HSV].emplace_back(_("_H:"), _("Hue"), 360, 255); // TYPE_HSV_16 - sets[Space::HSV].emplace_back(_("_S:"), _("Saturation"), 1, 255); - sets[Space::HSV].emplace_back(_("_V:"), _("Value"), 1, 255); - - sets[Space::HSLUV].emplace_back(_("_H*"), _("Hue"), 360, 255); // TYPE_LUV_16 - sets[Space::HSLUV].emplace_back(_("_S*"), _("Saturation"), 1, 255); - sets[Space::HSLUV].emplace_back(_("_L*"), _("Lightness"), 1, 255); - - sets[Space::OKLAB].emplace_back(_("_Hok"), _("Hue"), 0, 100); - sets[Space::OKLAB].emplace_back(_("_Sok"), _("Saturation"), 0, 360); - sets[Space::OKLAB].emplace_back(_("_Lok"), _("Lightness"), 0, 360); - - // CMS icc profile only components - - sets[Space::XYZ].emplace_back("_X", "X", 2, 0); // TYPE_XYZ_16 - sets[Space::XYZ].emplace_back("_Y", "Y", 1, 0); - sets[Space::XYZ].emplace_back("_Z", "Z", 2, 0); - - sets[Space::YCbCr].emplace_back("_Y", "Y", 1, 255); // TYPE_YCbCr_16 - sets[Space::YCbCr].emplace_back("C_b", "Cb", 1, 255); - sets[Space::YCbCr].emplace_back("C_r", "Cr", 1, 255); - - sets[Space::LAB].emplace_back("_L", "L", 100, 100); // TYPE_Lab_16 - sets[Space::LAB].emplace_back("_a", "a", 256, 256); - sets[Space::LAB].emplace_back("_b", "b", 256, 256); - - sets[Space::YXY].emplace_back("_Y", "Y", 1, 255); // TYPE_Yxy_16 - sets[Space::YXY].emplace_back("_x", "x", 1, 255); - sets[Space::YXY].emplace_back("y", "y", 1, 255); - - sets[Space::Gray].emplace_back(_("G:"), _("Gray"), 1, 255); // TYPE_GRAY_16 - } - - std::vector target; - if (sets.find(space) != sets.end()) { - target = sets[space]; - } - return target; -} - -std::vector getComponents(cmsUInt32Number cmssig) -{ - return getComponents(_lcmssig_to_space[cmssig]); -} - -}; // namespace Inkscape::Color - diff --git a/src/color/manager.cpp b/src/color/manager.cpp deleted file mode 100644 index 434f1bbcc1..0000000000 --- a/src/color/manager.cpp +++ /dev/null @@ -1,207 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * ColorManager - 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 "color/manager.h" - -#include "object/color-profile.h" -#include "object/sp-root.h" -#include "object/sp-defs.h" -#include "io/sys.h" - -#include "document.h" -#include "color/cms-system.h" - -namespace Inkscape { - -ColorManager::ColorManager(SPDocument *document) - : _document(document) -{ - _resource_connection = _document->connectResourcesChanged("iccprofile", [=]() { - // Very few icc profiles are ever expected to exist at the same time, and their - // update frequency is also very low. So we're just going to destroy and recollect - _profiles.clear(); - _modified_connections.clear(); - for (auto obj : _document->getResourceList("iccprofile")) { - if (auto cp = cast(obj)) { - _profiles.push_back(cp); - _modified_connections.push_back(cp->connectModified([this](SPObject* obj, guint flags) { - if (auto cp = cast(obj)) { - _modified_signal.emit(cp); - } - })); - } - } - _changed_signal.emit(); - }); -} - -ColorManager::~ColorManager() -{ - _document = nullptr; -} - -/** - * Finds the named color profile. This name is used in the icc color derective - * to match included profiles. - * - * @arg name - The name of the color profile to match. - * @returns the matching ColorProfile or nullptr if none found. - */ -ColorProfile *ColorManager::find(std::string const &name) const -{ - for (auto cp : _profiles) { - if (cp->getName() == name) - return cp; - } - return nullptr; -} - -ColorProfile *ColorManager::addProfile(std::string const &uri, RenderingIntent intent) -{ - - auto system = Inkscape::CMSSystem::get(); - auto info = system->get_info_for_profile(uri); - if (!info) - return nullptr; - auto name = info->get_name(); - - //XXX old code has sanitizeName(name); - if (auto existing = find(name)) { - return existing; - } - - if (!Inkscape::IO::file_test(uri.c_str(), G_FILE_TEST_EXISTS)) { - g_warning("CMS File not found: %s", uri.c_str()); - return nullptr; - } - - 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 - cprofRepr->setAttribute("name", nameStr); - cprofRepr->setAttribute("xlink:href", "file://" + uri); - - if (auto defs = _document->getDefs()) { - defs->getRepr()->addChild(cprofRepr, nullptr); - } - if (auto cm = cast(_document->getObjectByRepr(cprofRepr))) { - if (!cm->getHandle()) { - g_warning("Invalid cms profile: %s", uri.c_str()); - cm->deleteObject(); - return nullptr; - } - cm->setRenderingIntent(intent); - return cm; - } - return nullptr; -} - -/** - * Removes the given profile, if it can, reverts colors to sRGB automatically - */ -void ColorManager::removeProfile(ColorProfile *profile) -{ - // TODO: revert linked colors here. - profile->deleteObject(false); -} - -/** - * Return the default color profile, or nullptr if no default is set. - * - * @returns the default profile, or nullptr if none found. - */ -ColorProfile *ColorManager::getDefault() const -{ - for (auto cp : _profiles) { - if (cp->isDefault()) - return cp; - } - return nullptr; -} - -/** - * Set the given profile as the default document profile used in - * color picking and soft proof visualisation. - * - * @arg profile - The profile to set as default, if undef sets no default - * which reverts the document to sRGB mode. - */ -void ColorManager::setDefault(ColorProfile *profile) -{ - if (auto existing = getDefault()) { - existing->setDefault(false); - } - if (profile) { - profile->setDefault(true); - } -} - -/** - * Returns true if this document is in CMYK mode. - * - * @returns true if the document is CMYK, false if it's RGB - */ -bool ColorManager::isPrintColorSpace() const -{ - if (auto profile = getDefault()) { - return profile->isPrintColorSpace(); - } - return false; -} - -/* - * Convert all colors to sRGB, removing any icc profile use - * - * The rendering intent is taken from the icc profile being used. - * - * @arg profile - The profile to remove, if nullptr all profiles are removed. - * @returns the number of colors changed - */ -int ColorManager::convertToRGB(ColorProfile *profile) -{ - int count = 0; - // We recurse SPObjects instead of SPItems to get into defs without exceptions - _document->getRoot()->recursivelyCallback([=](SPObject *obj) { - // Find all colors in this object - }); - return count; -} - -/* - * Convert all colors to an icc CMYK profile, replacing sRGB values but does not - * change any color already using an icc profile. - * - * The rendering intent is taken from the icc profile being used. - * - * @arg profile - The color profile to convert sRGB into. - * @returns - */ -int ColorManager::convertToCMYK(ColorProfile *profile) -{ - int count = 0; - // We recurse SPObjects instead of SPItems to get into defs without exceptions - _document->getRoot()->recursivelyCallback([=](SPObject *obj) { - // Find all colors in this object - }); - return count; -} - -} // 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/manager.h b/src/color/manager.h deleted file mode 100644 index 1d77d7bbd4..0000000000 --- a/src/color/manager.h +++ /dev/null @@ -1,75 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * ColorManager - 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_INKSCAPE_COLOR_MANAGER_H -#define SEEN_INKSCAPE_COLOR_MANAGER_H - -#include - -#include "helper/auto-connection.h" -#include "object/color-profile.h" - -class SPDocument; - -namespace Inkscape { - -class ColorProfile; - -class ColorManager -{ -public: - ColorManager(SPDocument *document); - ~ColorManager(); - - std::vector::iterator begin() { return std::begin(_profiles); } - std::vector::iterator end() { return std::end(_profiles); } - - ColorProfile *find(std::string const &name) const; - ColorProfile *getDefault() const; - ColorProfile *addProfile(std::string const &uri, RenderingIntent intent = RenderingIntent::AUTO); - void removeProfile(ColorProfile *profile); - void setDefault(ColorProfile *profile); - bool isPrintColorSpace() const; - - int convertToRGB(ColorProfile *profile = nullptr); - int convertToCMYK(ColorProfile *profile = nullptr); - - sigc::connection connectChanged(const sigc::slot &slot) { return _changed_signal.connect(slot); } - sigc::connection connectModified(const sigc::slot &slot) { return _modified_signal.connect(slot); } - -private: - ColorManager(ColorManager const &) = delete; - void operator=(ColorManager const &) = delete; - - SPDocument* _document = nullptr; - std::vector _profiles; - - // Signals In - Inkscape::auto_connection _resource_connection; - std::vector _modified_connections; - - // Signals Out - sigc::signal _changed_signal; - sigc::signal _modified_signal; -}; - -} - -#endif // SEEN_INKSCAPE_COLOR_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/color/spaces.cpp b/src/color/spaces.cpp deleted file mode 100644 index 48ec24e32b..0000000000 --- a/src/color/spaces.cpp +++ /dev/null @@ -1,17 +0,0 @@ -// 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 "spaces.h" - -namespace Inkscape::Color { - - -}; // namespace Color - diff --git a/src/color/spaces.h b/src/color/spaces.h deleted file mode 100644 index 4eaf6eeb8c..0000000000 --- a/src/color/spaces.h +++ /dev/null @@ -1,81 +0,0 @@ -// 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_COLOR_SPACES_H -#define SEEN_COLOR_SPACES_H - -#include -#include -#include -#include - -#include "lcms2.h" - -class SPColor; -class CMSProfile; - -namespace Inkscape::Color -{ - -// The spaces we support are a mixture of ICC profile spaces -// and internal spaces converted to and from RGB -enum class Space { - NONE, - RGB, - HSL, - CMYK, - CMY, - HSV, - HSLUV, - OKLAB, - XYZ, - YXY, - LAB, - YCbCr, - Gray, -}; - -// 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::RGB}, - {cmsSigHlsData, Space::HSL}, - {cmsSigCmykData, Space::CMYK}, - {cmsSigCmyData, Space::CMY}, - {cmsSigHsvData, Space::HSV}, - {cmsSigLuvData, Space::HSLUV}, - {customSigOKLabData, Space::OKLAB}, - {cmsSigXYZData, Space::XYZ}, - {cmsSigXYZData, Space::YXY}, - {cmsSigLabData, Space::LAB}, - {cmsSigYCbCrData, Space::YCbCr}, - {cmsSigGrayData, Space::Gray}, -}; - -class Convertor -{ -public: - Convertor() = delete; - ~Convertor() = default; - - virtual Space getType() const { return Space::RGB; } - virtual unsigned int getComponentCount() const { return 3; } - - virtual SPColor const &toRGB(SPColor const &color) const { return color; } - virtual SPColor const &fromRGB(SPColor const &color) const { return color; } - - virtual SPColor toProfile(std::shared_ptr profile, SPColor const &color) const; - virtual SPColor fromProfile(std::shared_ptr profile, SPColor const &color) const; -}; - -} // namespace Inkscape::Color - -#endif // SEEN_COLOR_SPACES_H diff --git a/src/colors/CMakeLists.txt b/src/colors/CMakeLists.txt new file mode 100644 index 0000000000..3265f5a7f0 --- /dev/null +++ b/src/colors/CMakeLists.txt @@ -0,0 +1,66 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +set(colors_SRC + cms/profile.cpp + cms/system.cpp + cms/transform.cpp + color.cpp + dragndrop.cpp + manager.cpp + parser.cpp + printer.cpp + spaces/base.cpp + spaces/cms.cpp + spaces/cmyk.cpp + spaces/components.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 + tracker.cpp + utils.cpp + xml-color.cpp + + # ------- + # Headers + cms/profile.h + cms/system.h + cms/transform.h + color.h + dragndrop.h + manager.h + parser.h + printer.h + spaces/base.h + spaces/cms.h + spaces/cmyk.h + spaces/components.h + spaces/enum.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 + tracker.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..618cf76789 --- /dev/null +++ b/src/colors/cms/profile.cpp @@ -0,0 +1,281 @@ +// 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 + +#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)) + , _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); +} + +/** + * Return the profile's checksum identifier. + */ +std::string Profile::getId() const +{ + 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); + } + return oo.str(); +} + +/** + * 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 Input format used for color object transformation. + * + * This value uses 16bits per channel during transformations and converts back to doubles afterwards. + */ +cmsUInt32Number Profile::getInputFormat() const +{ + static const std::map map = { + {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}, + }; + auto iter = map.find(getColorSpace()); + if (iter != map.end()) { + return iter->second; + } + return 0; +} + +/** + * 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; +} + +/** + * Dump the entire profile as a base64 encoded string. This is used for color-profile href data. + */ +std::string Profile::dumpBase64() 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 Glib::Base64::encode(std::string(buf.begin(), buf.end())); +} + +// Note; see SvgBuilder::_getColorProfile(cmsHPROFILE hp) for turning profile into bytes for storage. + +} // 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..ee83246f4e --- /dev/null +++ b/src/colors/cms/profile.h @@ -0,0 +1,69 @@ +// 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 + +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->getId() < p2->getId(); } + + 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 getId() == other.getId(); } + + cmsHPROFILE getHandle() const { return _handle; } + std::string const &getPath() const { return _path; } + bool inHome() const { return _in_home; } + + bool isForDisplay() const; + std::string getId() const; + std::string getName(bool sanitize = false) const; + unsigned int getSize() const; + cmsUInt32Number getInputFormat() const; + cmsColorSpaceSignature getColorSpace() const; + cmsProfileClassSignature getProfileClass() const; + + static bool isIccFile(std::string const &filepath); + std::string dumpBase64() const; +private: + cmsHPROFILE _handle; + std::string _path; + bool _in_home = false; +}; + +} // 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..d2c236c468 --- /dev/null +++ b/src/colors/cms/system.cpp @@ -0,0 +1,274 @@ +// 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 +#include + +#include "io/resource.h" +#include "profile.h" +#include "transform.h" + +#ifdef _WIN32 +#include +#include +#endif + +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..0a8c1640ed --- /dev/null +++ b/src/colors/cms/system.h @@ -0,0 +1,91 @@ +// 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 +{ +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(); +private: + System(); + ~System() = default; + + // 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..a66e5c3ef9 --- /dev/null +++ b/src/colors/cms/transform.cpp @@ -0,0 +1,180 @@ +// 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 "profile.h" + +namespace Inkscape::Colors::CMS { + +/** + * Construct a color transform object from the lcms2 object. + */ +std::shared_ptr const Transform::create(cmsHTRANSFORM handle) +{ + return handle ? std::make_shared(handle) : nullptr; +} + +/** + * Construct a transformation suitable for display conversion in a cairo buffer + * + * @arg from - The CMS Profile the cairo data will start in. + * @arg to - The target CMS Profile the cairo data needs to end up in. + */ +std::shared_ptr const Transform::create_for_cairo(std::shared_ptr const &from, std::shared_ptr const &to) +{ + // TODO: Add and expand for gamut warning and print preview, cmsCreateProofingTransform might not be needed + // as it takes in three profiles to do the translation. + if (!to || !from) + return nullptr; + return create( + cmsCreateTransform(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); + return create(cmsCreateTransform(from->getHandle(), from->getInputFormat(), to->getHandle(), + to->getInputFormat(), lt, flags)); +} + +/** + * 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 || cmsGetTransformOutputFormat(_handle) != TYPE_BGRA_8) { + g_error("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)) { + g_warning("Different image formats while applying CMS!"); + return; + } + + 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 a single Color object's data. + * + * @arg io - The input/output color as a vector of numbers between 0.0 and 1.0 + * @arg size_a - The number of channels the input color has. + * @arg size_b - The number of channels the output color is expected to have. + * + * @returns the modified color in io + */ +bool Transform::do_transform(std::vector &io, unsigned int size_a, unsigned int size_b) const +{ + std::vector pre(size_a); + std::vector post(size_b); + + for (unsigned int a = 0; a < size_a; a++) { + // double to uint16 conversion + pre[a] = io[0] * 65535; + io.erase(io.begin()); + } + cmsDoTransform(_handle, &pre.front(), &post.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(post)) { + io.insert(io.begin(), val / 65535.0); + } + return true; +} + +/** + * 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..071c2d9718 --- /dev/null +++ b/src/colors/cms/transform.h @@ -0,0 +1,66 @@ +// 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 // cmsHTRANSFORM +#include +#include + +#include "colors/spaces/enum.h" + +typedef struct _cairo_surface cairo_surface_t; + +namespace Inkscape::Colors::CMS { + +class Profile; +class Transform +{ +public: + static std::shared_ptr const create(cmsHTRANSFORM handle); + static std::shared_ptr const create_for_cairo(std::shared_ptr const &from, + std::shared_ptr const &to); + static std::shared_ptr const create_for_cms(std::shared_ptr const &from, + std::shared_ptr const &to, RenderingIntent intent); + + Transform(cmsHTRANSFORM handle) + : _handle(handle) + { + assert(_handle); + } + ~Transform() { cmsDeleteTransform(_handle); } + 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; + bool do_transform(std::vector &io, unsigned int size_a, unsigned int size_b) const; + +private: + cmsHTRANSFORM _handle; + + static unsigned int lcms_intent(RenderingIntent intent, unsigned int &flags); +}; + +} // 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.cpp b/src/colors/color.cpp new file mode 100644 index 0000000000..51fccc58b2 --- /dev/null +++ b/src/colors/color.cpp @@ -0,0 +1,552 @@ +// 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" + +namespace Inkscape::Colors { + +/** + * Default color construction will use the app color manager's default space. + */ +Color::Color() + : _space(Manager::get().defaultSpace()) +{} + +/** + * Construct a color from a string directly. + * + * @arg parsable - The CSS color string to parse + * @arg document - Optional document to enable icc profile use. + * @arg default - Optional default color if the parsing fails. If not set, + * an empty RGBA color will be set. + */ +Color::Color(std::string const &parsable, SPDocument *document, std::optional def) +{ + if (auto color = Manager::get().parseColor(parsable, document)) { + set(*color, false); + } else if (def) { + set(*def, false); + } else { + _space = Manager::get().find("RGB"); + } +} + +/** + * Create a color, given the name of the space and the values to store. + * + * @arg space_name - The name 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(std::string const &space_name, std::vector values, SPDocument *document) +{ + if (auto space = Manager::get().find(space_name, document)) { + _space = space; + _values = std::move(values); + } else { + throw ColorError("Can not find color space."); + } +} + +/** + * Compatability layer for making blind RGB colors + */ +Color::Color(guint32 color, bool opacity) + : _space(Manager::get().find("RGB")) +{ + set(color, opacity); +} + +/** + * Construct an empty color with the given color space. + * + * @arg space - The color space future setters will use. + */ +Color::Color(std::shared_ptr const &space) + : _space(space) +{} + +/** + * 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 const &space, std::vector colors) + : _values(std::move(colors)) + , _space(space) +{} + +/** + * 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 +{ + bool the_same = (_space == other._space and _values.size() == other._values.size()); + for (auto i = 0; the_same and i < (int)_values.size(); i++) { + the_same = the_same and (_values[i] == other._values[i]); + } + return the_same; +} + +/** + * Get a single channel from this color. Returns -1 if the color is no valid. + */ +double Color::operator[](const unsigned int index) const +{ + if (index < _values.size()) + return _values[index]; + if (_values.size() && index == _space->getComponentCount()) + return 1.0; + return -1; +} + +/** + * 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 +{ + if (_values.size() < _space->getComponentCount()) + return ""; + 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. + */ +guint32 Color::toRGBA(double opacity) const +{ + return _space->toRGBA(_values, opacity); +} + +/** + * Return the RGBA int32 as an ARGB format number. + */ +guint32 Color::toARGB(double opacity) const +{ + auto value = toRGBA(opacity); + return (value >> 8) | ((value & 0xff) << 24); +} + +/** + * Convert to the same format as the other color. + */ +void Color::convertInPlace(Color const &other) +{ + convertInPlace(other._space); + if (other.hasOpacity()) { + addOpacity(); + } else { + removeOpacity(); + } +} + +/** + * Convert this color into a different color space. + */ +void Color::convertInPlace(std::shared_ptr to_space) +{ + if (_values.size() && _space != to_space) + _space->convert(_values, to_space); + _space = to_space; + _name = ""; +} + +/** + * Convert this color into a named color space. + * + * @returns true if the space was found, false if it wasn't found. + */ +bool Color::convertInPlace(std::string const &name) +{ + if (auto space = Manager::get().find(name, _space->getDocument())) { + convertInPlace(space); + return true; + } + return false; +} + +/** + * Convert this color into the first matched color space of the given type. + */ +bool Color::convertInPlace(Space::Type type) +{ + if (auto space = Manager::get().find(type)) { + convertInPlace(space); + return true; + } + return false; +} + +/** + * Return a copy of this color converted to the same format as the other color. + */ +Color Color::convert(Color const &other) const +{ + Color copy = *this; + copy.convertInPlace(other); + return copy; +} + +/** + * Convert a copy of this color into a different color space. + */ +Color Color::convert(std::shared_ptr to_space) const +{ + Color copy = *this; + copy.convertInPlace(to_space); + return copy; +} + +/** + * Convert a copy of this color into the named color space. + * + * if the space is not available in this color manager, an empty color is returned. + */ +std::optional Color::convert(std::string const &name) const +{ + Color copy = *this; + if (copy.convertInPlace(name)) + 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::convert(Space::Type type) const +{ + Color copy = *this; + if (copy.convertInPlace(type)) + return copy; + return {}; +} + +/** + * 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. + */ +void Color::set(Color const &other, bool keep_space) +{ + auto prev_space = _space; + _space = other._space; + _values = other._values; + // This is a convience mechanism to make API usage easier. + if (_space != prev_space && keep_space) { + // Convert back to the previous space if needed. + convertInPlace(prev_space); + } else { + _name = other._name; + } +} + +/** + * 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 = ""; + auto size = _space->getComponentCount(); + if (values.size() == size || values.size() == size + 1) { + _values = std::move(values); + } +} + +/** + * 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 be maintained + */ +void Color::set(std::string const &parsable, bool keep_space) +{ + if (auto color = Manager::get().parseColor(parsable)) + set(*color, keep_space); +} + +/** + * 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. + */ +void Color::set(guint32 rgba, bool opacity) +{ + if (_space->getType() != Space::Type::RGB) { + // Ensure we are in RGB + _space = Manager::get().find(Space::Type::RGB); + } + _name = ""; + _values.clear(); + _values.emplace_back(SP_RGBA32_R_F(rgba)); + _values.emplace_back(SP_RGBA32_G_F(rgba)); + _values.emplace_back(SP_RGBA32_B_F(rgba)); + if (opacity) + _values.emplace_back(SP_RGBA32_A_F(rgba)); +} + +/** + * 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 +{ + auto pos = getOpacityChannel(); + return _values.size() == pos + 1 ? _values[pos] : (_values.size() ? 1.0 : 0.0); +} + +/** + * Get the opacity channel index + */ +unsigned int Color::getOpacityChannel() const +{ + return _space->getComponentCount(); +} + +/** + * Return the power of 2 of the opacity index. + */ +unsigned int Color::getOpacityPin() const +{ + return std::pow(2, getOpacityChannel()); +} + +/** + * Set the opacity of this color object. + */ +void Color::setOpacity(double opacity) +{ + if (hasOpacity()) { + _values[getOpacityChannel()] = opacity; + } else if (_values.size() == _space->getComponentCount()) { + _values.emplace_back(opacity); + } else if (_values.empty()) { + g_warning("Refusing to set opacity on an empty color."); + } else { + g_warning("Bad color is not empty, but is not an expected size."); + } +} + +/* + * Remove any opacity channel from this color. + */ +void Color::removeOpacity() +{ + if (hasOpacity()) + _values.pop_back(); +} + +/** + * Make sure the values for this color are within range. + */ +void Color::normalizeInPlace() +{ + _space->normalize(_values); +} + +/** + * Invert the color for each channel. Always ignores opacity. + * + * @arg pin - Bit field, which channels should not change. + */ +void Color::invertInPlace(unsigned int pin) +{ + for (unsigned int i = 0; i < _values.size(); i++) { + if (pin & (1 << i)) + continue; + + _values[i] = 1.0 - _values[i]; + } +} + +/** + * Invert the color for each channel. Always ignores opacity. + * + @ @arg force - The amount of jitter to add to each channel. + * @arg pin - Bit field, which channels should not change. + */ +void Color::jitterInPlace(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); + } + _space->normalize(_values); +} + +/* + * 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 - Which channels should not change. 1=0,2=1,4=2,etc + */ +void Color::averageInPlace(Color const &other, double pos, unsigned int pin) +{ + _color_mutate_inplace(other, pin, [pos](auto &value, auto otherValue) { + value = value * (1 - pos) + otherValue * pos; + }); +} +/** + * Move towards the other color by the given weight. + * + * @arg other - The other color to move towards with + * @arg weight - The weight to give the other color when mixing. + * @pin pin - Which channels should not change. 1=0,2=1,4=2,etc + */ +void Color::moveTowardsInPlace(Color const &other, double weight, unsigned int pin) +{ + _color_mutate_inplace(other, pin, [weight](auto &value, auto otherValue) { + value += (otherValue - value) * weight; + }); +} + +/** + * Modify this color to be the average between two colors, returning the result. + * + * @arg other - The other color to average with + * @arg pos - The weighting to give each color + */ +Color Color::average(Color const &other, double pos) const +{ + Color copy = *this; + copy.averageInPlace(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; + auto copy = other.convert(*this); + for (unsigned int i = 0; i < _values.size(); i++) { + ret += 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 = 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) + return isSimilar(other.convert(_space), epsilon); + return isClose(other, epsilon); +} + +// Color-color in-place modification template +template +void Color::_color_mutate_inplace(Color const &other, unsigned int pin, Func func) +{ + // 1. If other is bad, return without action + if (!other) + return; + + // 2. If this is bad, modify to be equal to other + if (_values.empty()) { + _values = other._values; + _space = other._space; + return; + } + + // 3. Convert the other's space if it's different + if (_space && other._space != _space) + return _color_mutate_inplace(other.convert(_space), pin, func); + + // 4. Ensure opacity is compatible + if (!hasOpacity() && other.hasOpacity()) + addOpacity(); // Enable opacity if needed + + // 5. 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..aed9586668 --- /dev/null +++ b/src/colors/color.h @@ -0,0 +1,118 @@ +// 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 "utils.h" + +class SPDocument; + +namespace Inkscape::Colors { +namespace Space { +class AnySpace; +enum class Type; +} // namespace Space + +static double _EPSILON = 1e-4; + +class Color +{ +public: + Color(); + ~Color() = default; + + Color(std::shared_ptr const &space, std::vector colors); + Color(std::string const &space_name, std::vector values, SPDocument *document = nullptr); + Color(std::string const &value, SPDocument *document = nullptr, std::optional def = {}); + Color(guint32 color, bool alpha = true); + Color(Color const &other) { _space = other._space; set(other, false); } + Color &operator=(Color const &other) { set(other, false); return *this; } + + bool operator==(Color const &other) const; + bool operator!=(Color const &other) const { return !(*this == other); }; + double operator[](const unsigned int index) const; + operator bool() const { return _space && !_values.empty(); } + + 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(); } + + void unset() + { + _values.clear(); + _name.clear(); + } + void set(Color const &other, bool keep_space = true); + void set(unsigned int index, double value) { _values[index] = value; } + void set(std::string const &parsable, bool keep_space = true); + void set(guint32 rgba, bool opacity = true); + + bool hasOpacity() const; + double getOpacity() const; + unsigned int getOpacityChannel() const; + unsigned int getOpacityPin() const; + void setOpacity(double opacity); + void addOpacity(double opacity = 1.0) { setOpacity(opacity * getOpacity()); } + void removeOpacity(); + + Color average(Color const &other, double pos = 0.5) const; + double difference(Color const &other) const; + bool isClose(Color const &other, double epsilon = _EPSILON) const; + bool isSimilar(Color const &other, double epsilon = _EPSILON) const; + + void convertInPlace(Color const &other); + void convertInPlace(std::shared_ptr space); + bool convertInPlace(std::string const &name); + bool convertInPlace(Space::Type type); + Color convert(Color const &other) const; + Color convert(std::shared_ptr to_space) const; + std::optional convert(std::string const &name) const; + std::optional convert(Space::Type type) const; + + std::string toString(bool opacity = true) const; + guint32 toRGBA(double opacity = 1.0) const; + guint32 toARGB(double opacity = 1.0) const; + + std::string getName() const { return _name; } + void setName(std::string name) { _name = std::move(name); } + + void normalizeInPlace(); + void invertInPlace(unsigned int pin = 0); + void jitterInPlace(double force, unsigned int pin = 0); + void averageInPlace(Color const &other, double pos = 0.5, unsigned int pin = 0); + void moveTowardsInPlace(Color const &other, double weight, unsigned int pin = 0); +protected: + std::string _name; + std::vector _values; + std::shared_ptr _space; + +private: + template + void _color_mutate_inplace(Color const &other, unsigned int pin, Func avgFunc); + + Color(std::shared_ptr const &space); +}; + +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/dragndrop.cpp b/src/colors/dragndrop.cpp new file mode 100644 index 0000000000..b3ad42e104 --- /dev/null +++ b/src/colors/dragndrop.cpp @@ -0,0 +1,97 @@ +// 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" +#include "xml-color.h" + +static char const *mimeOSWB_COLOR = "application/x-oswb-color"; +static char const *mimeX_COLOR = "application/x-color"; +static char const *mimeTEXT = "text/plain"; + +namespace Inkscape::Colors { + +/** + * Get a list of drag-n-drop mime types for colors. + */ +std::vector const &getMIMETypes() +{ + static std::vector mimetypes = {mimeOSWB_COLOR, mimeX_COLOR, mimeTEXT}; + return mimetypes; +} + +/** + * Convert a color into a dragable object + */ +std::pair, int> getMIMEData(std::string const &type, std::optional color) +{ + auto from_data = [] (void const *p, int len) { + std::vector v(len); + std::memcpy(v.data(), p, len); + return v; + }; + + if (!color || !*color) { + return {{}, 0}; + } else if (type == mimeTEXT) { + auto str = color->toString(); + return std::make_pair(from_data(str.c_str(), str.size()), 8); + } else if (type == mimeX_COLOR) { + // X-color is only ever in RGBA + auto rgb = color->toRGBA(); + std::array tmp = { + (uint16_t)((SP_RGBA32_R_U(rgb) << 8) | SP_RGBA32_R_U(rgb)), + (uint16_t)((SP_RGBA32_G_U(rgb) << 8) | SP_RGBA32_G_U(rgb)), + (uint16_t)((SP_RGBA32_B_U(rgb) << 8) | SP_RGBA32_B_U(rgb)), + (uint16_t)((SP_RGBA32_A_U(rgb) << 8) | SP_RGBA32_A_U(rgb)), + }; + return std::make_pair(from_data(tmp.data(), 8), 16); + } else if (type == mimeOSWB_COLOR) { + auto xml = color_to_xml_string(color); + return std::make_pair(from_data(xml.c_str(), xml.size()), 8); + } else { + return {{}, 0}; + } +} + +/** + * Convert a dropped object into a color, if possible. + */ +bool fromMIMEData(std::string const &type_str, char const *data, int len, std::optional &output) +{ + if (type_str == mimeTEXT) { + // unused + } else if (type_str == mimeX_COLOR) { + // unused + } else if (type_str == mimeOSWB_COLOR) { + std::string xml(data, len); + output = xml_string_to_color(xml, nullptr); + 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:fileencoding=utf-8:textwidth=99 : diff --git a/src/colors/dragndrop.h b/src/colors/dragndrop.h new file mode 100644 index 0000000000..7eca534e1e --- /dev/null +++ b/src/colors/dragndrop.h @@ -0,0 +1,40 @@ +// 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 + +namespace Inkscape::Colors { + +class Color; + +std::vector const &getMIMETypes(); +std::pair, int> getMIMEData(std::string const &type, std::optional color); +bool fromMIMEData(std::string const &type_str, char const *data, int len, std::optional &output); + +} // 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..194a7869ea --- /dev/null +++ b/src/colors/manager.cpp @@ -0,0 +1,227 @@ +// 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" + +// Each Internal space should be imported here. +#include "spaces/cms.h" +#include "spaces/cmyk.h" +#include "spaces/hsl.h" +#include "spaces/hsluv.h" +#include "spaces/hsv.h" +#include "spaces/lab.h" +#include "spaces/lch.h" +#include "spaces/luv.h" +#include "spaces/linear-rgb.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::CMYK()); + 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()); + setDefault(_spaces[0]); + + 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", "RGB", 3)); + addParser(new CssParser("device-cmyk", "CMYK", 4)); + addParser(new CssParser("srgb-linear", "linearRGB", 3)); + addParser(new CssParser("xyz", "XYZ", 3)); +} + +/** + * Add the given space and assume ownership over it. + */ +std::shared_ptr Manager::addSpace(Space::AnySpace *space) +{ + if (find(space->getName(), space->getDocument())) { + throw ColorError("Can not add the same color space twice."); + } + _spaces.emplace_back(space); + return _spaces.back(); +} + +void Manager::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); +} + +/** + * Removes the given space from the list of available spaces. + */ +void Manager::removeSpace(std::shared_ptr space) +{ + for (auto iter = _spaces.begin(); iter != _spaces.end(); ++iter) { + if (*iter == space) { + _spaces.erase(iter); + return; + } + } + throw ColorError("Failed to remove space"); +} + +/** + * Finds the named color profile. This name is used in the icc color derective + * to match included profiles. + * + * @arg name - The name of the color profile to match. + * @arg document - The document in which icc profiles should be matched. If not + * specified the match will be in global spaces only. If set + * the match will find global or document spaces. + * + * @returns the matching color space if found. + */ +std::shared_ptr Manager::find(std::string const &name, SPDocument *document) const +{ + for (auto cp : _spaces) { + if (cp->getName() == name && (!cp->getDocument() || cp->getDocument() == document)) { + return cp; + } + } + return {}; +} + +/** + * 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 +{ + for (auto cp : _spaces) { + if (cp->getType() == type && !cp->getDocument()) + return cp; + } + return {}; +} + +/** + * Parse the color with an optional char string found in XML Attributes. + * + * @arg value - An ascii formated text or nullptr. + * @arg doc - Am optional document context for parsing icc colors. + */ +std::optional Manager::parseAttr(char const *value, SPDocument *doc) const +{ + if (value) + return parseColor(value, doc); + return {}; +} + +/** + * Turn a string into a Color object. + * + * Each available color space will be asked to parse the color in turn + * and the successful parser will return a Color object. + * + * @arg value - A std string of the CSS color. + * @arg doc - Am optional document context for parsing icc colors. + * + * @returns a color object or none if no parser matches. + */ +std::optional Manager::parseColor(std::string const &value, SPDocument *doc) const +{ + std::istringstream ss(value); + return _parse_color(ss, doc); +} + +std::optional Manager::_parse_color(std::istringstream &ss, SPDocument *doc) const +{ + auto prefix = Parser::getCssPrefix(ss); + auto iter = _parsers.find(prefix); + if (iter != _parsers.end()) { + for (auto &parser : iter->second) { + auto pos = ss.tellg(); + bool more = false; + std::vector output; + auto space_name = parser->parseColor(ss, output, more); + if (more) { + if (auto next = _parse_color(ss, doc)) { + return next; + } + } + if (auto space = find(space_name, doc)) { + auto count = space->getComponentCount(); + if (output.size() == count || output.size() == count + 1) { + return Color(space, output); + } + } + ss.clear(); + ss.seekg(pos); + } + } + return {}; +} + +/** + * Set the given profile as the default document profile used in + * color picking and soft proof visualisation. + * + * @arg profile - The profile to set as default, if undef sets no default + * which reverts the document to sRGB mode. + */ +void Manager::setDefault(std::shared_ptr space) +{ + _default_space = space; +} + +} // 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..c57d53dcce --- /dev/null +++ b/src/colors/manager.h @@ -0,0 +1,83 @@ +// 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 +#include + +class SPDocument; + +namespace Inkscape::Colors { + +namespace Space { +enum class Type; +class AnySpace; +} // namespace Space +class Color; +class Parser; + +class Manager +{ +private: + Manager(); + + Manager(Manager const &) = delete; + void operator=(Manager const &) = delete; +public: + // Anonymous access comes without document and cms tracking. + static Manager &get() + { + static Manager instance; + return instance; + } + ~Manager() = default; + + std::vector>::iterator begin() { return std::begin(_spaces); } + std::vector>::iterator end() { return std::end(_spaces); } + + std::shared_ptr addSpace(Space::AnySpace *space); + void removeSpace(std::shared_ptr space); + + std::shared_ptr find(std::string const &name, SPDocument *doc = nullptr) const; + std::shared_ptr find(Space::Type type) const; + std::shared_ptr defaultSpace() const { return _default_space; } + + void setDefault(std::shared_ptr profile); + + std::optional parseAttr(char const *value, SPDocument *doc = nullptr) const; + std::optional parseColor(std::string const &value, SPDocument *doc = nullptr) const; +private: + std::vector> _spaces; + std::shared_ptr _default_space; + std::map>> _parsers; + + std::optional _parse_color(std::istringstream &ss, SPDocument *doc) const; + + void addParser(Parser *parser); +}; + +} // 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..9b690c386b --- /dev/null +++ b/src/colors/parser.cpp @@ -0,0 +1,192 @@ +// 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 "utils.h" + +#include +#include + +namespace Inkscape::Colors { + +std::string Parser::parseColor(std::istringstream &ss, std::vector &output, bool &more) const +{ + if (parse(ss, output, more)) { + return _space; + } + 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 + * + * @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) +{ + // TODO: Add optional seperator control so we can specify if a seperator must appear, or if a space is enoug + if (!(ss >> value)) { + 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()) { + g_warning("Unknown unit in css color parsing '%s'", unit.c_str()); + } 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..73f950ddfa --- /dev/null +++ b/src/colors/parser.h @@ -0,0 +1,79 @@ +// 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 + +namespace Inkscape::Colors { + +class Parser +{ +public: + Parser(std::string prefix, std::string default_space = "") + : _prefix(std::move(prefix)) + , _space(std::move(default_space)) + {} + + 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); + + 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; + std::string _space; +}; + +class LegacyParser : public Parser +{ +public: + LegacyParser(std::string const &prefix, std::string space, bool alpha) + : Parser(alpha ? prefix + "a" : prefix, space) + , _alpha(alpha) + {} +protected: + bool _alpha = false; +}; + +class HueParser : public LegacyParser +{ +public: + HueParser(std::string const &prefix, std::string space, bool alpha) + : LegacyParser(prefix, space, alpha) + {} + bool parse(std::istringstream &ss, std::vector &output) const; +}; + +class HexParser : public Parser +{ +public: + HexParser() : Parser("#", "RGB") {} + bool parse(std::istringstream &input, std::vector &output, bool &more) const override; +}; + +class CssParser : public Parser +{ +public: + CssParser(std::string prefix, std::string space, unsigned int channels) + : Parser(prefix, space) + , _channels(channels) + {} +private: + bool parse(std::istringstream &ss, std::vector &output) const override; + + unsigned int _channels; +}; + +} // 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..fa0a88f37b --- /dev/null +++ b/src/colors/printer.cpp @@ -0,0 +1,57 @@ +// 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" + +#include "util/numeric/converters.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) { + *this << (_count ? _sep : "") << Util::format_number(value); + } + _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..9d9511b0d7 --- /dev/null +++ b/src/colors/printer.h @@ -0,0 +1,97 @@ +// 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")); + *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) { + g_warning("Expected %d channels but got %d", _channels, _count); + 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..74cc28de64 --- /dev/null +++ b/src/colors/spaces/base.cpp @@ -0,0 +1,106 @@ +// 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 +#include + +#include "colors/cms/profile.h" +#include "colors/cms/transform.h" +#include "colors/color.h" +#include "components.h" + +#include "util/numeric/converters.h" + +namespace Inkscape::Colors::Space { + +AnySpace::AnySpace() +{ + if (!srgb_profile) { + srgb_profile = Colors::CMS::Profile::create_srgb(); + } +} + +/** + * 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. + */ +void AnySpace::convert(std::vector &output, std::shared_ptr to_space) +{ + // Firstly convert from the formatted values (i.e. hsl) into the profile values (i.e. sRGB) + spaceToProfile(output); + // Secondly convert the color profile itself using lcms2 if the profiles are different + profileToProfile(output, to_space); + // Thirdly convert to the formatted values (i.e. hsl) from the profile values (i.e. sRGB) + to_space->profileToSpace(output); +} + +/** + * Convert from the space's format, to the profile's data format. + */ +void AnySpace::spaceToProfile(std::vector &output) const {} + +/** + * Convert from the profile's format, to the space's data format. + */ +void AnySpace::profileToSpace(std::vector &output) const {} + +/** + * Deal with values which are outside the range of allowed values. + */ +void AnySpace::normalize(std::vector &output) const +{ + for (auto &value : output) { + value = std::clamp(value, 0.0, 1.0); + } +} + +/** + * Step two in coverting a color, convert it's profile to another profile (if needed) + */ +void AnySpace::profileToProfile(std::vector &output, std::shared_ptr to_space) +{ + auto from_profile = getProfile(); + auto to_profile = to_space->getProfile(); + if (*to_profile == *from_profile) + return; + + // 1. Look in the transform cache for the color profile + auto to_profile_id = to_profile->getId(); + if (_transforms.find(to_profile_id) == _transforms.end()) { + // 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; + + // 2. Create a new transform for this one way profile-pair + // Note this should probably be a unique_ptr + _transforms.emplace(to_profile_id, Colors::CMS::Transform::create_for_cms(from_profile, to_profile, intent)); + } + + // 3. Use the transform to convert the output colors. + _transforms[to_profile_id]->do_transform(output, getComponentCount(), to_space->getComponentCount()); +} + +/** + * Return a list of Component objects, in order of the channels in this color space + */ +std::vector AnySpace::getComponents() const +{ + return Space::getComponents(getType()); +} + +}; // namespace Inkscape::Colors::Space diff --git a/src/colors/spaces/base.h b/src/colors/spaces/base.h new file mode 100644 index 0000000000..ecff70b68c --- /dev/null +++ b/src/colors/spaces/base.h @@ -0,0 +1,83 @@ +// 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 "enum.h" + +#include "colors/parser.h" + +typedef unsigned int guint32; + +#define SCALE_UP(v, a, b) (v * (b - a)) + a; +#define SCALE_DOWN(v, a, b) (v - a) / (b - a); + +class SPDocument; + +namespace Inkscape::Colors { +namespace CMS { +class Profile; +class Transform; +} // namespace CMS +class Color; +class Manager; +class Tracker; + +namespace Space { + +class Component; + +class AnySpace +{ +public: + ~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 unsigned int getComponentCount() const = 0; + virtual std::shared_ptr const getProfile() const = 0; + virtual RenderingIntent getIntent() const { return RenderingIntent::UNKNOWN; } + virtual SPDocument *getDocument() const { return nullptr; } + + std::vector getComponents() const; +protected: + friend class Colors::Color; + friend class Colors::Manager; + + AnySpace(); + virtual std::vector getParsers() const { return {}; } + virtual std::string toString(std::vector const &values, bool opacity = true) const = 0; + virtual guint32 toRGBA(std::vector const &values, double opacity = 1.0) const = 0; + + void convert(std::vector &output, std::shared_ptr to_space); + void profileToProfile(std::vector &output, std::shared_ptr to_space); + virtual void spaceToProfile(std::vector &output) const; + virtual void profileToSpace(std::vector &output) const; + virtual void normalize(std::vector &output) const; + + std::shared_ptr srgb_profile; +private: + std::map> _transforms; +}; + +} // 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..61760ab0a2 --- /dev/null +++ b/src/colors/spaces/cms.cpp @@ -0,0 +1,158 @@ +// 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 + +#include "colors/cms/profile.h" +#include "colors/cms/transform.h" +#include "colors/color.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, SPDocument *document) + : AnySpace() + , _document(document) + , _profile_name(profile->getName(true)) + , _profile_size(profile->getSize()) + , _profile(profile) +{} + +/** + * Naked CMS space for testing and possible data retention where the profile is unavailable in the future. + */ +CMS::CMS(std::string profile_name, unsigned profile_size) + : AnySpace() + , _document(nullptr) + , _profile_name(std::move(profile_name)) + , _profile_size(profile_size) + , _profile(nullptr) +{ +} + +/** + * Sets the rendering intent and generates a new rgb transform if needed. + */ +void CMS::setIntent(RenderingIntent in) +{ + _intent = in; + if (in != RenderingIntent::UNKNOWN) { + to_rgb = Colors::CMS::Transform::create_for_cms(srgb_profile, _profile, _intent); + if (!to_rgb) { + g_warning("Failed to make transform!"); + if (!srgb_profile) { + g_warning("No sRGB profile."); + } + if (!_profile) { + g_warning("No internal profile."); + } + } + } else { + to_rgb.reset(); + } +} + +/** + * Get the color space type from this icc profile. + */ +Space::Type CMS::getType() const +{ + return _lcmssig_to_space[_profile->getColorSpace()]; +} + +/** + * 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() == ')') { + g_warning("Found SVG2 ICC named color '%s' for profile '%s', which not supported yet.", named.c_str(), + icc_name.c_str()); + } + } + + 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(getComponentCount(), _profile_name); + 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. + */ +guint32 CMS::toRGBA(std::vector const &values, double opacity) const +{ + std::vector copy = values; + if (!to_rgb) { + if (_profile) + g_warning("Can not convert to sRGB when the rendering intent is not set!"); + return 0x0; + } + to_rgb->do_transform(copy, getComponentCount(), 3); + // CMS color channels never include opacity, it's not in the specification + return SP_RGBA32_F_COMPOSE(copy[0], copy[1], copy[2], opacity); +} + +}; // namespace Inkscape::Colors::Space diff --git a/src/colors/spaces/cms.h b/src/colors/spaces/cms.h new file mode 100644 index 0000000000..42677f342e --- /dev/null +++ b/src/colors/spaces/cms.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_CMS_H +#define SEEN_COLORS_SPACES_CMS_H + +#include "base.h" + +namespace Inkscape::Colors::Space { + +class Component; + +// A class for containing the icc profile and the machinery for converting colors +class CMS : public AnySpace +{ +public: + CMS(std::shared_ptr profile, + SPDocument *document = nullptr); + CMS(std::string profile_name, unsigned profile_size); + ~CMS() = default; + + Space::Type getType() const override; + std::string const getName() const override { return _profile_name; } + unsigned int getComponentCount() const override { return _profile_size; } + + std::shared_ptr const getProfile() const override { return _profile; } + RenderingIntent getIntent() const override { return _intent; } + void setIntent(RenderingIntent intent); + +protected: + friend class Inkscape::Colors::Color; + friend class Inkscape::Colors::Manager; + friend class Inkscape::Colors::Tracker; + + SPDocument *getDocument() const override { return _document; } + std::string toString(std::vector const &values, bool opacity = true) const override; + guint32 toRGBA(std::vector const &values, double opacity = 1.0) const override; + + void setName(std::string name) { _profile_name = std::move(name); } + +private: + SPDocument *_document; + std::string _profile_name; + unsigned _profile_size; + std::shared_ptr _profile; + RenderingIntent _intent = RenderingIntent::UNKNOWN; + + std::shared_ptr to_rgb; +public: + class CmsParser : public Parser { + public: + CmsParser() : Parser("icc-color") {} + 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..72449495ba --- /dev/null +++ b/src/colors/spaces/cmyk.cpp @@ -0,0 +1,69 @@ +// 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 "cmyk.h" + +#include "colors/color.h" +#include "colors/printer.h" + +namespace Inkscape::Colors::Space { + +/** + * Convert the CMYK 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 CMYK::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 CMYK 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 CMYK::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 CMYK color to a CSS Color Module Level 5 string. + * + * @arg values - A vector of doubles for each channel in the CMYK space + * @arg opacity - True if the opacity should be included in the output. + */ +std::string CMYK::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; +} + +}; // namespace Inkscape::Colors::Space diff --git a/src/colors/spaces/cmyk.h b/src/colors/spaces/cmyk.h new file mode 100644 index 0000000000..f88dba04df --- /dev/null +++ b/src/colors/spaces/cmyk.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_CMYK_H +#define SEEN_COLORS_SPACES_CMYK_H + +#include "rgb.h" + +namespace Inkscape::Colors::Space { + +/** + * This sRGB based CMYK space is uncalibrated and fixed to the sRGB icc profile. + */ +class CMYK : public RGB +{ +public: + CMYK() = default; + ~CMYK() = default; + + Space::Type getType() const override { return Space::Type::CMYK; } + unsigned int getComponentCount() const override { return 4; } + std::string const getName() const { return "CMYK"; } + + void spaceToProfile(std::vector &output) const override; + void profileToSpace(std::vector &output) const override; + +protected: + friend class Inkscape::Colors::Color; + friend class Inkscape::Colors::Manager; + + std::string toString(std::vector const &values, bool opacity = true) const override; +}; + +} // namespace Inkscape::Colors::Space + +#endif // SEEN_COLORS_SPACES_CMYK_H diff --git a/src/colors/spaces/components.cpp b/src/colors/spaces/components.cpp new file mode 100644 index 0000000000..900f0910bf --- /dev/null +++ b/src/colors/spaces/components.cpp @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Manage color space components + *//* + * 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 "enum.h" + +namespace Inkscape::Colors::Space { + +Component::Component(std::string id, std::string name, std::string tip, unsigned int cms_scale, unsigned int ink_scale) + : id(id) + , name(std::move(name)) + , tip(std::move(tip)) + , cms_scale(cms_scale) + , ink_scale(ink_scale) +{} + +std::vector getComponents(Type space) +{ + static std::map> sets; + + if (sets.empty()) { + // Inkscape internal components + sets[Space::Type::RGB].emplace_back("r", _("_R:"), _("Red"), 1, 255); // TYPE_RGB_16 + sets[Space::Type::RGB].emplace_back("g", _("_G:"), _("Green"), 1, 255); + sets[Space::Type::RGB].emplace_back("b", _("_B:"), _("Blue"), 1, 255); + + sets[Space::Type::linearRGB].emplace_back("r", _("l_R:"), _("Linear Red"), 1, 255); + sets[Space::Type::linearRGB].emplace_back("g", _("l_G:"), _("Linear Green"), 1, 255); + sets[Space::Type::linearRGB].emplace_back("b", _("l_B:"), _("Linear Blue"), 1, 255); + + sets[Space::Type::HSL].emplace_back("h", _("_H:"), _("Hue"), 360, 255); // TYPE_HLS_16 + sets[Space::Type::HSL].emplace_back("s", _("_S:"), _("Saturation"), 1, 100); + sets[Space::Type::HSL].emplace_back("l", _("_L:"), _("Lightness"), 1, 100); + + sets[Space::Type::CMYK].emplace_back("c", _("_C:"), _("Cyan"), 1, 100); // TYPE_CMYK_16 + sets[Space::Type::CMYK].emplace_back("m", _("_M:"), _("Magenta"), 1, 100); + sets[Space::Type::CMYK].emplace_back("y", _("_Y:"), _("Yellow"), 1, 100); + sets[Space::Type::CMYK].emplace_back("k", _("_K:"), _("Black"), 1, 100); + + sets[Space::Type::CMY].emplace_back("c", _("_C:"), _("Cyan"), 1, 100); // TYPE_CMY_16 + sets[Space::Type::CMY].emplace_back("m", _("_M:"), _("Magenta"), 1, 100); + sets[Space::Type::CMY].emplace_back("y", _("_Y:"), _("Yellow"), 1, 100); + + sets[Space::Type::HSV].emplace_back("h", _("_H:"), _("Hue"), 360, 255); // TYPE_HSV_16 + sets[Space::Type::HSV].emplace_back("s", _("_S:"), _("Saturation"), 1, 255); + sets[Space::Type::HSV].emplace_back("v", _("_V:"), _("Value"), 1, 255); + + sets[Space::Type::HSLUV].emplace_back("h", _("_H*"), _("Hue"), 360, 255); // TYPE_LUV_16 + sets[Space::Type::HSLUV].emplace_back("s", _("_S*"), _("Saturation"), 1, 255); + sets[Space::Type::HSLUV].emplace_back("l", _("_L*"), _("Lightness"), 1, 255); + + sets[Space::Type::LUV].emplace_back("l", _("_L*"), _("Luminance"), 360, 255); // TYPE_LUV_16 + sets[Space::Type::LUV].emplace_back("u", _("_u*"), _("Chroma U"), 1, 255); + sets[Space::Type::LUV].emplace_back("v", _("_v*"), _("Chroma V"), 1, 255); + + sets[Space::Type::LCH].emplace_back("l", _("_L"), _("Luminance"), 360, 255); // TYPE_LUV_16 + sets[Space::Type::LCH].emplace_back("c", _("_C"), _("Chroma"), 1, 255); + sets[Space::Type::LCH].emplace_back("h", _("_H"), _("Hue"), 1, 255); + + sets[Space::Type::OKLAB].emplace_back("h", _("_Hok"), _("Hue"), 0, 100); + sets[Space::Type::OKLAB].emplace_back("s", _("_Sok"), _("Saturation"), 0, 360); + sets[Space::Type::OKLAB].emplace_back("l", _("_Lok"), _("Lightness"), 0, 360); + + sets[Space::Type::OKLCH].emplace_back("l", _("_Lok"), _("Lightness"), 0, 100); + sets[Space::Type::OKLCH].emplace_back("c", _("_Cok"), _("Chroma"), 0, 360); + sets[Space::Type::OKLCH].emplace_back("h", _("_Hok"), _("Hue"), 0, 360); + + // CMS icc profile only components + sets[Space::Type::XYZ].emplace_back("x", "_X", "X", 2, 0); // TYPE_XYZ_16 + sets[Space::Type::XYZ].emplace_back("y", "_Y", "Y", 1, 0); + sets[Space::Type::XYZ].emplace_back("z", "_Z", "Z", 2, 0); + + sets[Space::Type::YCbCr].emplace_back("y", "_Y", "Y", 1, 255); // TYPE_YCbCr_16 + sets[Space::Type::YCbCr].emplace_back("cb", "C_b", "Cb", 1, 255); + sets[Space::Type::YCbCr].emplace_back("cr", "C_r", "Cr", 1, 255); + + sets[Space::Type::LAB].emplace_back("l", "_L", "L", 100, 100); // TYPE_Lab_16 + sets[Space::Type::LAB].emplace_back("a", "_a", "a", 256, 256); + sets[Space::Type::LAB].emplace_back("b", "_b", "b", 256, 256); + + sets[Space::Type::YXY].emplace_back("y1", "_Y", "Y", 1, 255); // TYPE_Yxy_16 + sets[Space::Type::YXY].emplace_back("x", "_x", "x", 1, 255); + sets[Space::Type::YXY].emplace_back("y2", "y", "y", 1, 255); + + sets[Space::Type::Gray].emplace_back("gray", _("G:"), _("Gray"), 1, 255); // TYPE_GRAY_16 + } + + std::vector target; + if (sets.find(space) != sets.end()) { + target = sets[space]; + } + return target; +} + +}; // namespace Inkscape::Colors::Space diff --git a/src/color/components.h b/src/colors/spaces/components.h similarity index 53% rename from src/color/components.h rename to src/colors/spaces/components.h index c62e3594d8..8bde3677c6 100644 --- a/src/color/components.h +++ b/src/colors/spaces/components.h @@ -9,35 +9,34 @@ * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ -#ifndef SEEN_COLOR_COMPONENTS_H -#define SEEN_COLOR_COMPONENTS_H +#ifndef SEEN_COLORS_COMPONENTS_H +#define SEEN_COLORS_COMPONENTS_H -#include +#include +#include #include +#include -#include -#include "spaces.h" +namespace Inkscape::Colors::Space { -namespace Inkscape::Color -{ +enum class Type; -class Component +struct Component { -public: Component() = delete; ~Component() = default; - Component(std::string name, std::string tip, unsigned int cms_scale, unsigned int ink_scale); + Component(std::string id, std::string name, std::string tip, unsigned int cms_scale, unsigned int ink_scale); + std::string id; std::string name; std::string tip; unsigned int cms_scale; unsigned int ink_scale; }; -std::vector getComponents(Space space); -std::vector getComponents(cmsUInt32Number space); +std::vector getComponents(Type space); -} // namespace Inkscape::Color +} // namespace Inkscape::Colors::Space -#endif // SEEN_COLOR_COMPONENTS_H +#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..aa3be4c5e2 --- /dev/null +++ b/src/colors/spaces/enum.h @@ -0,0 +1,59 @@ +// 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 + +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 +}; + +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, // RGB + 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, +}; + +} // namespace Space +} // namespace Inkscape::Colors + +#endif // SEEN_COLORS_SPACES_ENUM_H diff --git a/src/colors/spaces/hsl.cpp b/src/colors/spaces/hsl.cpp new file mode 100644 index 0000000000..55db2f23fd --- /dev/null +++ b/src/colors/spaces/hsl.cpp @@ -0,0 +1,127 @@ +// 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) { // Grey + 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; +} + +/** + * Normalize values, which is slightly different from HSL because Hue + * is rotational (360 degrees) and wraps around instead. + */ +void HSL::normalize(std::vector &output) const +{ + output[0] -= std::floor(output[0]); + AnySpace::normalize(output); +} + +/** + * 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..9c6df24208 --- /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() = default; + + Space::Type getType() const override { return Space::Type::HSL; } + std::string const getName() const { return "HSL"; } + +protected: + friend class Inkscape::Colors::Color; + friend class Inkscape::Colors::Manager; + + 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; + void normalize(std::vector &output) const override; + +public: + class Parser : public HueParser + { + public: + Parser(bool alpha) : HueParser("hsl", "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..6174baf84b --- /dev/null +++ b/src/colors/spaces/hsluv.cpp @@ -0,0 +1,141 @@ +// 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 +#include <2geom/ray.h> +#include <2geom/line.h> + +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 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 : 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]; + 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. + */ +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; + in_out[1] = s; + in_out[2] = l; +} + +}; // namespace Inkscape::Colors::Space diff --git a/src/colors/spaces/hsluv.h b/src/colors/spaces/hsluv.h new file mode 100644 index 0000000000..b3a6f7c0f9 --- /dev/null +++ b/src/colors/spaces/hsluv.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_HSLUV_H +#define SEEN_COLORS_SPACES_HSLUV_H + +#include "lch.h" + +namespace Inkscape::Colors::Space { + +class HSLuv : public RGB +{ +public: + HSLuv() = default; + ~HSLuv() = default; + + Space::Type getType() const override { return Space::Type::HSLUV; } + std::string const getName() const { return "HSLuv"; } + +protected: + friend class Inkscape::Colors::Color; + friend class Inkscape::Colors::Manager; + + 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); +}; + +} // 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..b6a82eee0b --- /dev/null +++ b/src/colors/spaces/hsv.cpp @@ -0,0 +1,133 @@ +// 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; +} + +/** + * Normalize values, which is slightly different from HSL because Hue + * is rotational (360 degrees) and wraps around instead. + */ +void HSV::normalize(std::vector &output) const +{ + output[0] -= std::floor(output[0]); + AnySpace::normalize(output); +} + +/** + * 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) + << (1.0 - values[1]) * values[2] + << 1.0 - values[2] + << values.back(); +} + +}; // namespace Inkscape::Colors::Space diff --git a/src/colors/spaces/hsv.h b/src/colors/spaces/hsv.h new file mode 100644 index 0000000000..d98c8a225c --- /dev/null +++ b/src/colors/spaces/hsv.h @@ -0,0 +1,46 @@ +// 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() = default; + + Space::Type getType() const override { return Space::Type::HSV; } + std::string const getName() const { return "HSV"; } + + void spaceToProfile(std::vector &output) const override; + void profileToSpace(std::vector &output) const override; +protected: + friend class Inkscape::Colors::Color; + friend class Inkscape::Colors::Manager; + + + std::string toString(std::vector const &values, bool opacity) const override; + void normalize(std::vector &output) const override; +public: + class fromHwbParser : public HueParser { + public: + fromHwbParser(bool alpha) : HueParser("hwb", "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..6d3468f1fc --- /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 "colors/color.h" +#include "colors/printer.h" + +#include + +namespace Inkscape::Colors::Space { + +enum {X, Y, Z}; +enum {L, A, B}; + +/** + * Changes the values from 0..1, to typical lab scaling used + * in calculations. L:0..100, A:-128..128 B:-128..128 + */ +void Lab::scaleUp(std::vector &in_out) +{ + in_out[L] = SCALE_UP(in_out[L], 0, 100); + in_out[A] = SCALE_UP(in_out[A], -128, 128); + in_out[B] = SCALE_UP(in_out[B], -128, 128); +} + +/** + * Changes the values from typical lab scaling (see above) to + * values 0..1 used in the color module. + */ +void Lab::scaleDown(std::vector &in_out) +{ + in_out[L] = SCALE_DOWN(in_out[L], 0, 100); + in_out[A] = SCALE_DOWN(in_out[A], -128, 128); + in_out[B] = SCALE_DOWN(in_out[B], -128, 128); +} + +/** + * 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[L] + 16.0) / 116.0; + in_out[X] = in_out[A] / 500.0 + y; + in_out[Y] = y; + in_out[Z] = y - in_out[B] / 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[Y] > 0.008856) { + l = 116 * std::pow(in_out[Y], 0.33333) - 16; + } else { + l = 903.3 * in_out[Y]; + } + + 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[B] = 200 * (in_out[Y] - in_out[Z]); + in_out[A] = 500 * (in_out[X] - in_out[Y]); + in_out[L] = 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] * 100 // Luminance 0..100 + << values[1] * 250 - 125 // A -125..125 + << values[2] * 250 - 125; // B -125..125 + + if (opacity && values.size() == 4) + os << values[3]; + + return os; +} + +bool Lab::Parser::parse(std::istringstream &ss, std::vector &output) const +{ + bool end = false; + if (append_css_value(ss, output, end, ',', 100) + && append_css_value(ss, output, end, ',', 125) + && append_css_value(ss, output, end, '/', 125) + && (append_css_value(ss, output, end) || true) // optional + && end) + { + // The A and B portions are scaled 250, between -125 and 125. The percentage + // is also 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..38bf3b3179 --- /dev/null +++ b/src/colors/spaces/lab.h @@ -0,0 +1,59 @@ +// 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() = default; + + Space::Type getType() const override { return Space::Type::LAB; } + std::string const getName() const { return "Lab"; } + +protected: + friend class Inkscape::Colors::Color; + friend class Inkscape::Colors::Manager; + + 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); + } + + class Parser : public Colors::Parser { + public: + Parser() : Colors::Parser("lab", "Lab") {} + bool parse(std::istringstream &input, std::vector &output) const override; + }; + + std::string toString(std::vector const &values, bool opacity) const override; +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); +}; + +} // 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..6a33cc9455 --- /dev/null +++ b/src/colors/spaces/lch.cpp @@ -0,0 +1,118 @@ +// 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 { + +enum {L, C, H}; + +/** + * 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[L] = SCALE_UP(in_out[L], 0, 100); + in_out[C] = SCALE_UP(in_out[C], 0, 150); + in_out[H] = SCALE_UP(in_out[H], 0, 360); +} + +/** + * 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[L] = SCALE_DOWN(in_out[L], 0, 100); + in_out[C] = SCALE_DOWN(in_out[C], 0, 150); + in_out[H] = SCALE_DOWN(in_out[H], 0, 360); +} + +/** + * 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] * 100 // Luminance 0..100 + << values[1] * 150 // Chroma 0..150 + << values[2] * 360; // Hue 0..360 + + if (opacity && values.size() == 4) + os << values[3]; + return os; +} + +bool Lch::Parser::parse(std::istringstream &ss, std::vector &output) const +{ + bool end = false; + return append_css_value(ss, output, end, ',', 100) // luminance + && append_css_value(ss, output, end, ',', 150) // chroma + && append_css_value(ss, output, end, '/', 360) // 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..abc68cc5e7 --- /dev/null +++ b/src/colors/spaces/lch.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_LCH_H +#define SEEN_COLORS_SPACES_LCH_H + +#include "luv.h" + +namespace Inkscape::Colors::Space { + +class Lch : public RGB +{ +public: + Lch() = default; + ~Lch() = default; + + Space::Type getType() const override { return Space::Type::LCH; } + std::string const getName() const { return "Lch"; } + +protected: + friend class Inkscape::Colors::Color; + friend class Inkscape::Colors::Manager; + + 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); + } + + class Parser : public Colors::Parser { + public: + Parser() : Colors::Parser("lch", "Lch") {} + bool parse(std::istringstream &input, std::vector &output) const override; + }; + + std::string toString(std::vector const &values, bool opacity) const override; +public: + 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..c4dd62e5bd --- /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..56b83d4d6b --- /dev/null +++ b/src/colors/spaces/linear-rgb.h @@ -0,0 +1,42 @@ +// 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() = default; + + Space::Type getType() const override { return Space::Type::linearRGB; } + std::string const getName() const { return "linearRGB"; } + +protected: + friend class Inkscape::Colors::Color; + friend class Inkscape::Colors::Manager; + + 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..4d3916dce1 --- /dev/null +++ b/src/colors/spaces/luv.cpp @@ -0,0 +1,130 @@ +// 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 { + +static const double REF_U = 0.19783000664283680764; +static const double REF_V = 0.46831999493879100370; + +enum {X, Y, Z}; +enum {L, U, V}; + +/** + * Changes the values from 0..1, to typical luv scaling used + * in calculations. L:0..100, U:-100..200 V:-200..120 + */ +void Luv::scaleUp(std::vector &in_out) +{ + in_out[L] = SCALE_UP(in_out[L], 0, 100); + in_out[U] = SCALE_UP(in_out[U], -100, 200); + in_out[V] = SCALE_UP(in_out[V], -200, 120); +} + +/** + * Changes the values from typical luv scaling (see above) to + * values 0..1 used in the color module. + */ +void Luv::scaleDown(std::vector &in_out) +{ + in_out[L] = SCALE_DOWN(in_out[L], 0, 100); + in_out[U] = SCALE_DOWN(in_out[U], -100, 200); + in_out[V] = SCALE_DOWN(in_out[V], -200, 120); +} + +/** + * 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..47fd48ccd7 --- /dev/null +++ b/src/colors/spaces/luv.h @@ -0,0 +1,58 @@ +// 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 +static const double KAPPA = 903.29629629629629629630; +static const double EPSILON = 0.00885645167903563082; + +class Luv : public RGB +{ +public: + Luv() = default; + ~Luv() = default; + + Space::Type getType() const override { return Space::Type::LUV; } + std::string const getName() const { return "Luv"; } + +protected: + friend class Inkscape::Colors::Color; + friend class Inkscape::Colors::Manager; + + 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); +}; + +} // 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..404f184833 --- /dev/null +++ b/src/colors/spaces/named.cpp @@ -0,0 +1,236 @@ +// 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 + +#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..c49b5366e3 --- /dev/null +++ b/src/colors/spaces/named.h @@ -0,0 +1,47 @@ +// 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() = default; + + Space::Type getType() const override { return Space::Type::RGB; } + std::string const getName() const { return "CSSNAME"; } + + static std::string getNameFor(unsigned int rgba); +protected: + friend class Inkscape::Colors::Color; + friend class Inkscape::Colors::Manager; + + class NameParser : public Colors::Parser { + public: + NameParser() : Colors::Parser("", "CSSNAME") {} + bool parse(std::istringstream &input, std::vector &output) const override; + }; + + std::string toString(std::vector const &values, bool opacity = true) 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..e1f5fdadc4 --- /dev/null +++ b/src/colors/spaces/okhsl.cpp @@ -0,0 +1,70 @@ +// 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 +#include <2geom/angle.h> + +#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..d39f7f315c --- /dev/null +++ b/src/colors/spaces/okhsl.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_OKHSL_H +#define SEEN_COLORS_SPACES_OKHSL_H + +#include "oklab.h" + +namespace Inkscape::Colors::Space { + +class OkHsl : public RGB +{ +public: + OkHsl() = default; + ~OkHsl() = default; + + Space::Type getType() const override { return Space::Type::OKHSL; } + std::string const getName() const { return "OkHsl"; } + +protected: + friend class Inkscape::Colors::Color; + friend class Inkscape::Colors::Manager; + + 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..aa6671ee19 --- /dev/null +++ b/src/colors/spaces/oklab.cpp @@ -0,0 +1,148 @@ +// 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 +#include <2geom/math-utils.h> + +#include "colors/printer.h" + +namespace Inkscape::Colors::Space { + +void OkLab::scaleUp(std::vector &in_out) +{ + in_out[0] = SCALE_UP(in_out[0], 0, 1.0); + in_out[1] = SCALE_UP(in_out[1], -0.4, 0.4); + in_out[2] = SCALE_UP(in_out[2], -0.4, 0.4); +} + +void OkLab::scaleDown(std::vector &in_out) +{ + in_out[0] = SCALE_DOWN(in_out[0], 0, 1.0); + in_out[1] = SCALE_DOWN(in_out[1], -0.4, 0.4); + in_out[2] = SCALE_DOWN(in_out[2], -0.4, 0.4); +} + + +/** 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]; +} + +/** + * 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::vector cones(3); + 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, ',', 0.4) // chroma-a + && append_css_value(ss, output, end, '/', 0.4) // chroma-b + && (append_css_value(ss, output, end) || true) // optional opacity + && end) { + // Values are between -0.4 to 0.4, but also between -100% and 100% + // So these values are post processed into the range of 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 0..1 + << values[1] * 0.8 - 0.4 // Chroma A -0.4..0.4 + << values[2] * 0.8 - 0.4; // Chroma B -0.4..0.4 + + if (opacity && values.size() == 4) + os << values[3]; + + 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..36d67df874 --- /dev/null +++ b/src/colors/spaces/oklab.h @@ -0,0 +1,59 @@ +// 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() = default; + + Space::Type getType() const override { return Space::Type::OKLAB; } + std::string const getName() const { return "OkLab"; } + +protected: + friend class Inkscape::Colors::Color; + friend class Inkscape::Colors::Manager; + + 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); + } + + class Parser : public Colors::Parser { + public: + Parser() : Colors::Parser("oklab", "OkLab") {} + bool parse(std::istringstream &input, std::vector &output) const override; + }; + + std::string toString(std::vector const &values, bool opacity) const override; +public: + 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..2c704808d8 --- /dev/null +++ b/src/colors/spaces/oklch.cpp @@ -0,0 +1,375 @@ +// 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 +#include <2geom/angle.h> +#include <2geom/polynomial.h> + +#include "colors/color.h" +#include "colors/printer.h" + +namespace Inkscape::Colors::Space { + +/** + * 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]), 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()); + } 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; +}; + +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 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("OkLch", {l, c, h}).convert("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("OkLch", {l, c, h}).convert("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("OkLch", {l, c, h}).convert("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, ',', 0.4) // Chroma + && append_css_value(ss, output, end, '/', 360) // 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 0..1 + << values[1] * 0.4 // Chroma 0..0.4 + << values[2] * 360; // Hue 0..360 + + if (opacity && values.size() == 4) + os << values[3]; + + 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..8903ddca04 --- /dev/null +++ b/src/colors/spaces/oklch.h @@ -0,0 +1,57 @@ +// 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() = default; + + Space::Type getType() const override { return Space::Type::OKLCH; } + std::string const getName() const { return "OkLch"; } + +protected: + friend class Inkscape::Colors::Color; + friend class Inkscape::Colors::Manager; + + 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); + } + + class Parser : public Colors::Parser { + public: + Parser() : Colors::Parser("oklch", "OkLch") {} + bool parse(std::istringstream &input, std::vector &output) const override; + }; + + std::string toString(std::vector const &values, bool opacity) const override; +public: + static void toOkLab(std::vector &output); + static void fromOkLab(std::vector &output); + static double max_chroma(double l, double h); +}; + +} // 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..11c3e16f73 --- /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 = 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. + */ +guint32 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); +} + +guint32 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) + && append_css_value(ss, output, end, ',', 255) + && append_css_value(ss, output, end, !_alpha ? '/' : ',', 255) + && (append_css_value(ss, output, end) || !_alpha) + && 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..c4ecd9cc4f --- /dev/null +++ b/src/colors/spaces/rgb.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_RGB_H +#define SEEN_COLORS_SPACES_RGB_H + +#include "base.h" + +namespace Inkscape::Colors::Space { + +class RGB : public AnySpace +{ +public: + RGB() = default; + ~RGB() = default; + + Space::Type getType() const override { return Space::Type::RGB; } + std::string const getName() const { return "RGB"; } + unsigned int getComponentCount() const override { return 3; } + std::shared_ptr const getProfile() const override; + +protected: + friend class Inkscape::Colors::Color; + friend class Inkscape::Colors::Manager; + + class Parser : public LegacyParser + { + public: + Parser(bool alpha) : LegacyParser("rgb", "RGB", alpha) {} + bool parse(std::istringstream &input, std::vector &output) const override; + }; + + std::string toString(std::vector const &values, bool opacity = true) const override; + guint32 toRGBA(std::vector const &values, double opacity = 1.0) const override; +private: + guint32 _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..e454585c0a --- /dev/null +++ b/src/colors/spaces/xyz.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_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. +static const std::vector illuminant_d65 = { 0.9504, 1.0000, 1.0888 }; + +/* for sRGB, reference white D65 */ +static const std::vector d65[3] = { + { 3.24096994190452134377, -1.53738317757009345794, -0.49861076029300328366 }, + { -0.96924363628087982613, 1.87596750150772066772, 0.04155505740717561247 }, + { 0.05563007969699360846, -0.20397695888897656435, 1.05697151424287856072 } +}; + +static 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() = default; + + Space::Type getType() const override { return Space::Type::XYZ; } + std::string const getName() const { return "XYZ"; } + +protected: + friend class Inkscape::Colors::Color; + friend class Inkscape::Colors::Manager; + + 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/tracker.cpp b/src/colors/tracker.cpp new file mode 100644 index 0000000000..e723777eea --- /dev/null +++ b/src/colors/tracker.cpp @@ -0,0 +1,266 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Tracker - 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 "tracker.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(Tracker *man, ColorProfile *elem); + ~ColorProfileLink(); + + Tracker *tracker = nullptr; + ColorProfile *cp = nullptr; + std::shared_ptr space; + +private: + // Wanted to use an auto_connection here, but the compiler said "use of deleted function" + sigc::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(Tracker *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) { + Manager::get().removeSpace(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) { + g_warning("Unexpected generation of CMS profile space: '%s'", space->getName().c_str()); + Manager::get().removeSpace(space); + space.reset(); + } + + 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; +} + + +Tracker::Tracker(SPDocument *document) + : _document(document) +{ + assert(document); + _resource_connection = + _document->connectResourcesChanged("iccprofile", sigc::mem_fun(*this, &Tracker::refreshResources)); +} + +Tracker::~Tracker() +{ + _document = nullptr; +} + +/** + * Make sure the icc-profile resource list is linked and up to date + * with the color manager's list of available color spaces. + */ +void Tracker::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 Tracker::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 Tracker::addProfile(std::shared_ptr profile, std::string name, + RenderingIntent intent) +{ + auto space = new Colors::Space::CMS(profile, _document); + if (!name.empty()) { + // The name from the color-profile xml element overrides any internal name + space->setName(std::move(name)); + } + if (intent == RenderingIntent::UNKNOWN) { + space->setIntent(RenderingIntent::PERCEPTUAL); + } else { + space->setIntent(intent); + } + return std::static_pointer_cast(Manager::get().addSpace(space)); +} + +/** + * 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 Tracker::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 document color-profile SPObject for the named space. 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 *Tracker::getColorProfileForSpace(std::string const &name) const +{ + return getColorProfileForSpace(Manager::get().find(name, _document)); +} + +/** + * Get the document color-profile SPObject for the given space. + * Returns nullptr if the space is not a CMS color space. + */ +ColorProfile *Tracker::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 Tracker::setRenderingIntent(std::string const &name, RenderingIntent intent) +{ + if (auto cp = getColorProfileForSpace(name)) { + cp->setRenderingIntent(intent); + _document->ensureUpToDate(); + } +} + +} // 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/tracker.h b/src/colors/tracker.h new file mode 100644 index 0000000000..a297f00c94 --- /dev/null +++ b/src/colors/tracker.h @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Colors::Tracker - 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_TRACKER_H +#define SEEN_COLORS_TRACKER_H + +#include +#include +#include + +#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 Color; +class ColorProfileLink; +enum class RenderingIntent; + +class Tracker +{ +public: + Tracker() = delete; + ~Tracker(); + + Tracker(Tracker const &) = delete; + void operator=(Tracker const &) = delete; + + Tracker(SPDocument *document); + + std::vector>::iterator begin() { return std::begin(_links); } + std::vector>::iterator end() { return std::end(_links); } + + 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); + + 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); + + ColorProfile *getColorProfileForSpace(std::string const &name) const; + ColorProfile *getColorProfileForSpace(std::shared_ptr space) const; +private: + void refreshResources(); + + SPDocument *_document = nullptr; + std::vector> _links; + + Inkscape::auto_connection _resource_connection; + sigc::signal _changed_signal; + +protected: + friend class ColorProfileLink; + + sigc::signal)> _modified_signal; +}; + +} // namespace Colors +} // namespace Inkscape + +#endif // SEEN_COLORS_TRACKER_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/utils.cpp b/src/colors/utils.cpp new file mode 100644 index 0000000000..4e2a921ba3 --- /dev/null +++ b/src/colors/utils.cpp @@ -0,0 +1,203 @@ +// 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. + */ +guint32 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; +} + +/** + * 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(guint32 value, bool alpha) +{ + std::ostringstream oo; + 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 || !*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(); +} + +/** + * Return the average color between a list of other colors. + * + * @arg others - The other colors to average with, if they are not in the same + * color space as this color, they are copied and converted first. + */ +Color average_color_between(std::vector const &colors) +{ + unsigned int count = 0; + std::vector values; + Color initial; + + for (auto const &color : colors) { + if (!color) + continue; + + if (values.empty()) { + // Late setting the first entry skips invalid colors + initial = color; + values = color.getValues(); + } else { + // Convert the color to the same space and opacity format + // so we can be sure it contains the same number of channels + auto copy = color.convert(initial); + for (unsigned int i = 0; i < values.size(); i++) { + values[i] += copy[i]; + } + } + count++; + } + + for (double &value : values) { + value /= count; + } + return Color(initial.getSpace(), values); +} + +/** + * Return the average color between two colors. + */ +Color average_color(Color const &c1, Color const c2, double coord) +{ + auto ret = c1; // copy + ret.averageInPlace(c2, coord); + return ret; +} + +/** + * Make a darker or lighter version of the color, useful for making checkerboards. + */ +Color make_contrasted_color(Color const &orig, double amount) +{ + Color color = *orig.convert(Space::Type::HSL); + color.set(2, (color[2] < 0.08 ? 0.08 : -0.08) * amount); + color.convertInPlace(orig.getSpace()); + return color; +} + +double perceptual_lightness(double l) +{ + return l <= 0.885645168 ? l * 0.09032962963 : std::cbrt(l) * 0.249914424 - 0.16; +} + +double get_perceptual_lightness(Color const &color) +{ + return perceptual_lightness((*color.convert(Space::Type::HSLUV))[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}; + } +} + + +}; // 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..56ad50a349 --- /dev/null +++ b/src/colors/utils.h @@ -0,0 +1,56 @@ +// 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 + +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) + +/** + * 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; + +guint32 hex_to_rgba(std::string const &value); +std::string rgba_to_hex(guint32 value, bool alpha = false); +std::string color_to_id(std::optional const &color); +std::string desc_to_id(std::string const &desc); + +Color average_color_between(std::vector const &colors); +Color average_color(Color const &c1, Color const c2, double coord); +Color make_contrasted_color(Color const &orig, double amount); + +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..fe8fc79e6c --- /dev/null +++ b/src/colors/xml-color.cpp @@ -0,0 +1,167 @@ +// 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 "color.h" +#include "manager.h" +#include "spaces/base.h" +#include "spaces/cms.h" +#include "spaces/components.h" + +#include "xml/node.h" +#include "xml/node-iterators.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 color - The color to convert into xml + */ +std::string color_to_xml_string(std::optional const &color) +{ + auto doc = color_to_xml(color); + 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 + */ +std::optional xml_string_to_color(std::string const &xmls, SPDocument *doc) +{ + auto color_doc = sp_repr_read_buf(xmls, nullptr); + auto ret = xml_to_color(color_doc, doc); + GC::release(color_doc); + return ret; +} + +XML::Document *color_to_xml(std::optional const &color) +{ + auto *document = new XML::SimpleDocument(); + auto root = document->createElement("paint"); + document->appendChild(root); + + if (!color || !*color) { + auto node = document->createElement("nocolor"); + root->appendChild(node); + GC::release(node); + GC::release(root); + return document; + } + + 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 < space->getComponentCount(); i++) { + node->setAttributeCssDouble(components[i].id, (*color)[i]); + } + + GC::release(node); + GC::release(root); + return document; +} + +std::optional xml_to_color(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 {}; + } + if (auto color_xml = get_node(paint, "color")) { + auto space_name = color_xml->attribute("space"); + + if (!space_name) { + g_warning("Invalid color data, no space specified."); + return {}; + } + auto space = Manager::get().find(space_name, doc); + if (!space) { + g_warning("Can't find the color space."); + return {}; + } + + if (auto cms = std::dynamic_pointer_cast(space)) { + auto icc_id = color_xml->attribute("icc"); + if (icc_id && cms->getProfile()->getId() != icc_id) { + g_warning("Mismatched icc profiles in color data: '%s'", space_name); + // Not returning, will still return something + } + } + + 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; + } + } + return {}; +} + +} // 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..2b7cb38c4e --- /dev/null +++ b/src/colors/xml-color.h @@ -0,0 +1,46 @@ +// 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 + +class SPDocument; + +namespace Inkscape::XML { + class Document; +} + +namespace Inkscape::Colors { + +class Color; + +XML::Document *color_to_xml(std::optional const &color); +std::string color_to_xml_string(std::optional const &color); + +std::optional xml_string_to_color(std::string const &xmls, SPDocument *doc); +std::optional xml_to_color(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/desktop.cpp b/src/desktop.cpp index 4a479cc1e9..831032dadf 100644 --- a/src/desktop.cpp +++ b/src/desktop.cpp @@ -31,7 +31,7 @@ #include "desktop.h" #include "color.h" -#include "color/manager.h" +#include "colors/tracker.h" #include "desktop-events.h" #include "desktop-style.h" #include "document-undo.h" @@ -1380,12 +1380,12 @@ SPDesktop::setDocument (SPDocument *doc) }); // End Fomerly in View::View ^^^^^^^^^^^^^^^ - _profiles_changed_connection = document->getColorManager().connectChanged([this]() { + _profiles_changed_connection = document->getColorTracker().connectChanged([this]() { update_canvas_cms(); }); - _profiles_modified_connection = document->getColorManager().connectModified([this](Inkscape::ColorProfile const *) { + /*_profiles_modified_connection = document->getColorTracker().connectModified([this](std::shared_ptr) { update_canvas_cms(); - }); + });*/ update_canvas_cms(); sp_namedview_update_layers_from_document(this); @@ -1559,18 +1559,16 @@ void SPDesktop::on_zoom_end(GtkGesture const * /*zoom*/, GdkEventSequence const void SPDesktop::update_canvas_cms() { - auto &cm = document->getColorManager(); - auto cp = cm.getDefault(); - if (_default_cms_profile != cp) { - _default_cms_profile = cp; - std::shared_ptr colorproof; + //if (_default_color_space != cs) { + //_default_color_space = cs; + /*std::shared_ptr colorproof; std::shared_ptr gamutwarn; - if (cp) { + if (cs) { colorproof = cp->getColorProofTransform(); gamutwarn = cp->getGamutWarnTransform(); - } - canvas->set_cms_transforms(colorproof, gamutwarn); - } + }*/ + //canvas->set_cms_transforms(colorproof, gamutwarn); + //} } /* diff --git a/src/desktop.h b/src/desktop.h index dcf5f941e2..cb879298b9 100644 --- a/src/desktop.h +++ b/src/desktop.h @@ -103,7 +103,6 @@ class PageManager; class MessageContext; class MessageStack; class Selection; -class ColorProfile; class CanvasItem; class CanvasItemCatchall; @@ -559,7 +558,6 @@ private: bool _overlays_visible = true; ///< Whether the overlays are temporarily hidden bool _saved_guides_visible = false; ///< Remembers guides' visibility when hiding overlays - Inkscape::ColorProfile *_default_cms_profile = nullptr; std::unique_ptr _layer_manager; diff --git a/src/display/drawing-item.cpp b/src/display/drawing-item.cpp index 63000ba69c..44c71e8f89 100644 --- a/src/display/drawing-item.cpp +++ b/src/display/drawing-item.cpp @@ -12,7 +12,7 @@ #include -#include "color/cms-system.h" +#include "colors/cms/transform.h" #include "display/drawing-context.h" #include "display/drawing-group.h" @@ -910,11 +910,11 @@ unsigned DrawingItem::render(DrawingContext &dc, RenderContext &rc, Geom::IntRec break; case ColorMode::COLORPROOF: if (auto cms_profile = _drawing.getColorProofTransform()) - Inkscape::CMSSystem::do_transform(cms_profile->getHandle(), ict.rawTarget(), ict.rawTarget()); + cms_profile->do_transform(ict.rawTarget(), ict.rawTarget()); break; case ColorMode::GAMUTWARN: if (auto cms_profile = _drawing.getGamutWarnTransform()) - Inkscape::CMSSystem::do_transform(cms_profile->getHandle(), ict.rawTarget(), ict.rawTarget()); + cms_profile->do_transform(ict.rawTarget(), ict.rawTarget()); break; default: break; diff --git a/src/display/drawing.cpp b/src/display/drawing.cpp index 345654dc89..8cc5ac79b1 100644 --- a/src/display/drawing.cpp +++ b/src/display/drawing.cpp @@ -215,7 +215,7 @@ void Drawing::setClip(std::optional &&clip) }); } -void Drawing::setColorProofTransform(std::shared_ptr tr, std::shared_ptr gw) +void Drawing::setColorProofTransform(std::shared_ptr tr, std::shared_ptr gw) { defer([=] { // No check for setting-same diff --git a/src/display/drawing.h b/src/display/drawing.h index bbcb9a050c..88a2fff9b8 100644 --- a/src/display/drawing.h +++ b/src/display/drawing.h @@ -27,7 +27,7 @@ #include "nr-filter-colormatrix.h" #include "preferences.h" #include "util/funclog.h" -#include "color/cms-system.h" +#include "colors/cms/system.h" namespace Inkscape { @@ -94,9 +94,9 @@ public: void setExact(); void setOpacity(double opacity = 1.0); - void setColorProofTransform(std::shared_ptr tr, std::shared_ptr gw); - std::shared_ptr getColorProofTransform() { return colorproof_transform; } - std::shared_ptr getGamutWarnTransform() { return gamutwarn_transform; } + void setColorProofTransform(std::shared_ptr tr, std::shared_ptr gw); + std::shared_ptr getColorProofTransform() { return colorproof_transform; } + std::shared_ptr getGamutWarnTransform() { return gamutwarn_transform; } private: void _pickItemsForCaching(); @@ -126,8 +126,8 @@ private: std::optional _antialiasing_override; // Only set if the document has an icc profile set for it's color proofing - std::shared_ptr colorproof_transform; - std::shared_ptr gamutwarn_transform; + std::shared_ptr colorproof_transform; + std::shared_ptr gamutwarn_transform; std::set _cached_items; // modified by DrawingItem::_setCached() CacheList _candidate_items; // keep this list always sorted with std::greater diff --git a/src/document.cpp b/src/document.cpp index c79328a049..464b09ca17 100644 --- a/src/document.cpp +++ b/src/document.cpp @@ -57,7 +57,7 @@ #include "inkscape.h" #include "layer-manager.h" #include "page-manager.h" -#include "color/manager.h" +#include "colors/tracker.h" #include "rdf.h" #include "selection.h" @@ -166,7 +166,7 @@ SPDocument::SPDocument() : add_actions_undo_document(this); _page_manager = std::make_unique(this); - _cms_manager = std::make_unique(this); + _color_tracker = std::make_unique(this); } SPDocument::~SPDocument() { diff --git a/src/document.h b/src/document.h index 8b94ffd302..c4188222ef 100644 --- a/src/document.h +++ b/src/document.h @@ -85,7 +85,9 @@ namespace Inkscape { class Event; class EventLog; class PageManager; - class ColorManager; + namespace Colors { + class Tracker; + } class Selection; class UndoStackObserver; namespace XML { @@ -176,15 +178,15 @@ public: Inkscape::PageManager& getPageManager() { return *_page_manager; } const Inkscape::PageManager& getPageManager() const { return *_page_manager; } - Inkscape::ColorManager &getColorManager() { return *_cms_manager; } - const Inkscape::ColorManager &getColorManager() const { return *_cms_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 _cms_manager; + std::unique_ptr _color_tracker; std::queue pending_resource_changes; diff --git a/src/extension/internal/pdfinput/svg-builder.cpp b/src/extension/internal/pdfinput/svg-builder.cpp index 240a425f29..e4bc3fc11f 100644 --- a/src/extension/internal/pdfinput/svg-builder.cpp +++ b/src/extension/internal/pdfinput/svg-builder.cpp @@ -30,7 +30,7 @@ #include "Page.h" #include "Stream.h" #include "color.h" -#include "color/manager.h" +#include "colors/manager.h" #include "document.h" #include "extract-uri.h" #include "pdf-parser.h" @@ -38,7 +38,7 @@ #include "png.h" #include "poppler-cairo-font-engine.h" -#include "color/cms-util.h" +#include "colors/cms/profile.h" #include "display/cairo-utils.h" #include "display/nr-filter-utils.h" #include "object/sp-defs.h" @@ -897,13 +897,15 @@ 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 = Inkscape::Colors::CMS::Profile::create(hp); + std::string name = profile->getName(); // Find the named profile in the document (if already added) - if (_doc->getColorManager().find(name)) + if (Colors::Manager::get().find(name, _doc)) return name; // Add the profile, we've never seen it before. + // TODO, move to using the new createColorProfile static function. cmsUInt32Number len = 0; cmsSaveProfileToMem(hp, nullptr, &len); auto buf = (unsigned char *)malloc(len * sizeof(unsigned char)); diff --git a/src/inkscape-window.cpp b/src/inkscape-window.cpp index 34ee32380b..9368fd5f60 100644 --- a/src/inkscape-window.cpp +++ b/src/inkscape-window.cpp @@ -41,7 +41,7 @@ #include "actions/actions-tools.h" #include "actions/actions-view-mode.h" #include "actions/actions-view-window.h" -#include "color/manager.h" +#include "colors/tracker.h" #include "object/sp-namedview.h" // TODO Remove need for this! #include "ui/desktop/menubar.h" #include "ui/desktop/menu-set-tooltips-shift-icons.h" @@ -187,7 +187,7 @@ InkscapeWindow::change_document(SPDocument* document) // Monitor icc profile changes and update the available actions if (_document) { - icc_changed_connection = _document->getColorManager().connectChanged([this]() { + icc_changed_connection = _document->getColorTracker().connectChanged([this]() { update_actions_canvas_mode(this, _document); }); } else { diff --git a/src/inkscape.cpp b/src/inkscape.cpp index 24075dbc67..a796b6e612 100644 --- a/src/inkscape.cpp +++ b/src/inkscape.cpp @@ -40,7 +40,7 @@ #include "path-prefix.h" #include "selection.h" -#include "color/cms-system.h" +#include "colors/cms/system.h" #include "debug/simple-event.h" #include "debug/event-tracker.h" #include "io/resource.h" @@ -300,7 +300,6 @@ Application::~Application() } Inkscape::Preferences::unload(); - Inkscape::CMSSystem::unload(); _S_inst = nullptr; // this will probably break things diff --git a/src/object/color-profile.cpp b/src/object/color-profile.cpp index 461ae62a21..1ea2bb0383 100644 --- a/src/object/color-profile.cpp +++ b/src/object/color-profile.cpp @@ -11,258 +11,103 @@ #include "color-profile.h" #include - -#include // for g_free, guchar, g_utf8_case... - -#ifdef _WIN32 -#include -#include -#endif - -#include +#include #include "attributes.h" -#include "color.h" #include "document.h" #include "inkscape.h" #include "preferences.h" #include "uri.h" - -#include "color/color-profile-cms-fns.h" +#include "sp-defs.h" #include "xml/document.h" #include "xml/href-attribute-helper.h" -using Inkscape::ColorProfile; -using Inkscape::ColorProfileImpl; +#include "colors/cms/profile.h" 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; - std::shared_ptr _profile; - - cmsProfileClassSignature _profileClass; - cmsColorSpaceSignature _profileSpace; - - cmsHTRANSFORM _transf; - cmsHTRANSFORM _revTransf; - - std::shared_ptr _colorproof; - std::shared_ptr _gamutwarn; +static std::map intentIds = { + {Colors::RenderingIntent::UNKNOWN, ""}, + {Colors::RenderingIntent::AUTO, "auto"}, + {Colors::RenderingIntent::PERCEPTUAL, "perceptual"}, + {Colors::RenderingIntent::SATURATION, "saturation"}, + {Colors::RenderingIntent::ABSOLUTE_COLORIMETRIC, "absolute-colorimetric"}, + {Colors::RenderingIntent::RELATIVE_COLORIMETRIC, "relative-colorimetric"}, + {Colors::RenderingIntent::RELATIVE_COLORIMETRIC_NOBPC, "relative-colorimetric-nobpc"}, }; - -cmsColorSpaceSignature asICColorSpaceSig(ColorSpaceSig const & sig) -{ - 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) -{ -} - - -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; -} - -ColorProfile::~ColorProfile() = default; - -bool ColorProfile::operator<(ColorProfile const &other) const { - return getName() < other.getName(); -} - void ColorProfile::release() { // Unregister ourselves 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; - } - - 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 ( _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); - 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); - this->readAttr(SPAttr::DEFAULT); // Register - if ( document ) { - document->addResource( "iccprofile", this ); + if (document) { + document->addResource("iccprofile", this); } } - /** * Callback: set attribute. */ 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::DEFAULT: - _is_default = value && std::string(value) == "1"; 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: - this->name = value ? 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: - rendering_intent = renderingIntentEnum(value ? value : ""); - this->requestModified(SP_OBJECT_MODIFIED_FLAG); + // 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 : intentIds) { + if (pair.second == value) { + intent = pair.first; + break; + } + } + } break; default: - SPObject::set(key, value); - break; + return SPObject::set(key, value); } + this->requestModified(SP_OBJECT_MODIFIED_FLAG); } /** @@ -273,249 +118,97 @@ Inkscape::XML::Node* ColorProfile::write(Inkscape::XML::Document *xml_doc, Inksc 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) || 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->local) { - repr->setAttribute("local", this->local); - } - - if ((flags & SP_OBJECT_WRITE_ALL) || !name.empty()) { - repr->setAttribute("name", name); - } - - if ((flags & SP_OBJECT_WRITE_ALL) || _is_default) { - repr->setAttribute("inkscape:default", _is_default ? "1" : ""); - } - - if ((flags & SP_OBJECT_WRITE_ALL) || rendering_intent != RenderingIntent::UNKNOWN) { - repr->setAttribute("rendering-intent", renderingIntentId(rendering_intent)); - } + repr->setAttributeOrRemoveIfEmpty("local", local); + repr->setAttributeOrRemoveIfEmpty("name", name); + repr->setAttributeOrRemoveIfEmpty("rendering-intent", intentIds[intent]); SPObject::write(xml_doc, repr, flags); - return repr; } - -struct MapMap { - cmsColorSpaceSignature space; - cmsUInt32Number inForm; -}; - -cmsUInt32Number ColorProfileImpl::_getInputFormat( cmsColorSpaceSignature space ) -{ - 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; +/** + * Return the profile data, if any. Returns empty string if none + * is available. + */ +std::string ColorProfile::getProfileData() const +{ + // 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().c_str()); } } - - return possible[index].inForm; -} - -static int getLcmsIntent(Inkscape::RenderingIntent svgIntent) -{ - int intent = INTENT_PERCEPTUAL; - switch ( svgIntent ) { - case Inkscape::RenderingIntent::RELATIVE_COLORIMETRIC: - case Inkscape::RenderingIntent::RELATIVE_COLORIMETRIC_NOBPC: - intent = INTENT_RELATIVE_COLORIMETRIC; - break; - case Inkscape::RenderingIntent::SATURATION: - intent = INTENT_SATURATION; - break; - case Inkscape::RenderingIntent::ABSOLUTE_COLORIMETRIC: - intent = INTENT_ABSOLUTE_COLORIMETRIC; - break; - case Inkscape::RenderingIntent::PERCEPTUAL: - case Inkscape::RenderingIntent::UNKNOWN: - case Inkscape::RenderingIntent::AUTO: - default: - intent = INTENT_PERCEPTUAL; - } - return intent; -} -int ColorProfile::getCmsIntent(RenderingIntent intent) const -{ - return getLcmsIntent(getRenderingIntent(intent)); -} - -static int getLcmsFlags(Inkscape::RenderingIntent svgIntent) -{ - int flags = 0; - if (svgIntent == Inkscape::RenderingIntent::RELATIVE_COLORIMETRIC) { - flags |= cmsFLAGS_BLACKPOINTCOMPENSATION; - } - return flags; -} - -int ColorProfile::getCmsFlags(RenderingIntent intent) const -{ - return getLcmsFlags(getRenderingIntent(intent)); -} - -std::vector ColorProfile::getComponents() const -{ - return Color::getComponents(asICColorSpaceSig(getColorSpace())); -} - -std::string ColorProfile::renderingIntentId(RenderingIntent intent) -{ - return intentIds[(int)intent]; -} - -Inkscape::RenderingIntent ColorProfile::renderingIntentEnum(std::string const &intent_id) -{ - auto it = std::find(intentIds.begin(), intentIds.end(), intent_id); - if (it != intentIds.end()) - return (RenderingIntent)std::distance(intentIds.begin(), it); - return RenderingIntent::UNKNOWN; -} - -void ColorProfile::setRenderingIntent(std::string const &intent) -{ - getRepr()->setAttribute("rendering-intent", intent); -} - -void ColorProfile::setRenderingIntent(RenderingIntent intent) -{ - setRenderingIntent(intentIds[(int)intent]); -} - -void ColorProfile::setDefault(bool value) -{ - getRepr()->setAttributeOrRemoveIfEmpty("inkscape:default", value ? "1" : ""); + return ""; } /** - * Gets the number of objects / colors specified in this profile + * Set the rendering intent for this color profile. */ -unsigned int ColorProfile::getShapeCount() const -{ - return 0; -} - -Inkscape::ColorSpaceSig ColorProfile::getColorSpace() const { - return ColorSpaceSigWrapper(impl->_profileSpace); -} - -Inkscape::ColorProfileClassSig ColorProfile::getProfileClass() const { - return ColorProfileClassSigWrapper(impl->_profileClass); -} - -cmsHTRANSFORM ColorProfile::getTransfToSRGB8(RenderingIntent intent) -{ -/* if (!impl->_transf && impl->_profHandle) { - int flags = getCmsFlags(intent); - int cms_intent = getCmsIntent(intent); - impl->_transf = cmsCreateTransform(impl->_profHandle, ColorProfileImpl::_getInputFormat(impl->_profileSpace), ColorProfileImpl::getSRGBProfile(), TYPE_RGBA_8, cms_intent, flags); - } - return impl->_transf;*/ - return nullptr; -} - -cmsHTRANSFORM ColorProfile::getTransfFromSRGB8(RenderingIntent intent) +void ColorProfile::setRenderingIntent(Colors::RenderingIntent intent) { -/* if (!impl->_revTransf && impl->_profHandle) { - int flags = getCmsFlags(intent); - int cms_intent = getCmsIntent(intent); - impl->_revTransf = cmsCreateTransform(ColorProfileImpl::getSRGBProfile(), TYPE_RGBA_8, impl->_profHandle, ColorProfileImpl::_getInputFormat(impl->_profileSpace), cms_intent, flags); - } - return impl->_revTransf;*/ - return nullptr; + setAttribute("rendering-intent", intentIds[intent]); } -std::shared_ptr ColorProfile::getColorProofTransform() +/** + * 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->_colorproof) { - auto sRGB = ColorProfileImpl::getSRGBProfile(); - impl->_colorproof = CMSTransform::create( - cmsCreateProofingTransform(sRGB, TYPE_BGRA_8, sRGB, TYPE_BGRA_8, - getHandle(), getCmsIntent(), getCmsIntent(), - cmsFLAGS_SOFTPROOFING)); + if (name.empty()) { + g_error("Refusing to create a color profile with an empty name!"); + return nullptr; } - return impl->_colorproof; -} - -std::shared_ptr ColorProfile::getGamutWarnTransform() -{ - if ( !impl->_gamutwarn ) { - auto sRGB = ColorProfileImpl::getSRGBProfile(); - impl->_gamutwarn = CMSTransform::create( - cmsCreateProofingTransform(sRGB, TYPE_BGRA_8, sRGB, TYPE_GRAY_8, - getHandle(), 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->_gamutwarn; -} -// 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); - if (auto transform = getGamutWarnTransform()) { - cmsDoTransform(transform->getHandle(), &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); -} - -unsigned int ColorProfile::getChannelCount() const -{ - return cmsChannelsOf(asICColorSpaceSig(getColorSpace())); -} - -bool ColorProfile::isPrintColorSpace() -{ - ColorSpaceSigWrapper colorspace = getColorSpace(); - return (colorspace == cmsSigCmykData) || (colorspace == cmsSigCmyData); -} - -cmsHPROFILE ColorProfile::getHandle() -{ - return nullptr; //impl->_profHandle; + // Complete the creation by appending to the defs. This must be done last. + return cast(doc->getDefs()->appendChildRepr(repr)); } -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); -} +} // namespace Inkscape /* Local Variables: diff --git a/src/object/color-profile.h b/src/object/color-profile.h index 53da094fb6..21b3cb74b5 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,112 +10,53 @@ #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 "sp-object.h" -#include "color/components.h" -#include "color/cms-color-types.h" -#include "color/cms-system.h" -struct SPColor; +#include "colors/spaces/enum.h" // RenderingIntent +#include "colors/cms/system.h" namespace Inkscape { -class ColorManager; +class URI; -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 -}; -static std::vector intentIds = { - "", - "auto", - "perceptual", - "relative-colorimetric", - "saturation", - "absolute-colorimetric", - "relative-colorimetric-nobpc", +enum class ColorProfileStorage { + HREF_DATA, + HREF_FILE, + LOCAL_ID, }; -class ColorProfileImpl; - - -/** - * Color Profile. - */ 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(RenderingIntent intent = RenderingIntent::UNKNOWN); - cmsHTRANSFORM getTransfFromSRGB8(RenderingIntent intent = RenderingIntent::UNKNOWN); - std::shared_ptr getColorProofTransform(); - std::shared_ptr getGamutWarnTransform(); - bool GamutCheck(SPColor color); - unsigned int getChannelCount() const; - unsigned int getShapeCount() const; - bool isPrintColorSpace(); - cmsHPROFILE getHandle(); - - // TODO: Make private - char* href; - char* local; + static ColorProfile *createFromProfile(SPDocument *doc, + Colors::CMS::Profile const &profile, + std::string const &name, + ColorProfileStorage storage); std::string getName() const { return name; } - static std::string renderingIntentId(RenderingIntent intent); - static RenderingIntent renderingIntentEnum(std::string const &intent_id); - - RenderingIntent getRenderingIntent(RenderingIntent intent = RenderingIntent::UNKNOWN) const { - return intent == RenderingIntent::UNKNOWN ? rendering_intent : intent; - } - void setRenderingIntent(std::string const &intent); - void setRenderingIntent(RenderingIntent intent); - int getCmsIntent(RenderingIntent intent = RenderingIntent::UNKNOWN) const; - int getCmsFlags(RenderingIntent intent = RenderingIntent::UNKNOWN) const; + std::string getLocalProfileId() const { return local; } + std::string getProfileData() const; + Colors::RenderingIntent getRenderingIntent() const { return intent; } - std::vector getComponents() const; - - // Default color manager - bool isDefault() const { return _is_default; } + // 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: - friend class Inkscape::ColorManager; - - ColorProfileImpl *impl; - void build(SPDocument* doc, Inkscape::XML::Node* repr) override; void release() override; void set(SPAttr key, char const* value) override; - void setDefault(bool value = true); Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; - private: - RenderingIntent rendering_intent = RenderingIntent::UNKNOWN; std::string name; - bool _is_default = false; + std::string local; + Colors::RenderingIntent intent; + + std::unique_ptr uri; }; } // namespace Inkscape diff --git a/src/object/sp-image.cpp b/src/object/sp-image.cpp index 6957daa794..fd40015842 100644 --- a/src/object/sp-image.cpp +++ b/src/object/sp-image.cpp @@ -24,7 +24,6 @@ #include #include -#include "color/manager.h" #include <2geom/rect.h> #include <2geom/transforms.h> @@ -42,8 +41,8 @@ #include "xml/quote.h" #include "xml/href-attribute-helper.h" -#include "color/cms-system.h" -#include "color-profile.h" +#include "colors/manager.h" +#include "colors/cms/system.h" //#define DEBUG_LCMS #ifdef DEBUG_LCMS @@ -249,8 +248,8 @@ void SPImage::apply_profile(Inkscape::Pixbuf *pixbuf) { guchar* px = pixbuf->pixels(); if (px) { - if (auto cp = document->getColorManager().find(color_profile)) { - auto prof = cp->getHandle(); + if (auto cp = Inkscape::Colors::Manager::get().find(color_profile, document)) { + /*auto prof = cp->getHandle(); cmsProfileClassSignature profileClass = cmsGetDeviceClass( prof ); if (profileClass != cmsSigNamedColorClass) { if (cmsHTRANSFORM transf = cp->getTransfToSRGB8()) { @@ -266,7 +265,7 @@ void SPImage::apply_profile(Inkscape::Pixbuf *pixbuf) { } } 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." ); } diff --git a/src/svg/svg-color.cpp b/src/svg/svg-color.cpp index 41031610ab..bf558aafc3 100644 --- a/src/svg/svg-color.cpp +++ b/src/svg/svg-color.cpp @@ -28,14 +28,13 @@ #include // g_assert #include "color.h" -#include "color/manager.h" -#include "color/components.h" +#include "colors/manager.h" #include "document.h" #include "inkscape.h" #include "preferences.h" #include "strneq.h" -#include "color/cms-system.h" +#include "colors/cms/system.h" #include "object/color-profile.h" struct SPSVGColor { diff --git a/src/ui/dialog/document-properties.cpp b/src/ui/dialog/document-properties.cpp index 7824ff9869..1c70080855 100644 --- a/src/ui/dialog/document-properties.cpp +++ b/src/ui/dialog/document-properties.cpp @@ -41,8 +41,10 @@ #include "page-manager.h" #include "selection.h" -#include "color/cms-system.h" -#include "color/manager.h" +#include "colors/cms/system.h" +#include "colors/cms/profile.h" +#include "colors/manager.h" +#include "colors/tracker.h" #include "helper/auto-connection.h" #include "io/sys.h" #include "object/color-profile.h" @@ -608,36 +610,35 @@ void DocumentProperties::build_cms() _page_cms->table().attach(box_cms, 0, 0); box_cms.set_vexpand(true); - auto &profile = get_widget(_builder, "color-profile"); - auto &intent = get_widget(_builder, "rendering-intent"); + auto &profile_box = get_widget(_builder, "color-profile"); + auto &intent_box = get_widget(_builder, "rendering-intent"); - _profile_changed_connection = profile.signal_changed().connect([this, &profile, &intent]() { + _profile_changed_connection = profile_box.signal_changed().connect([this, &profile_box, &intent_box]() { _cms_profiles_changed_connection.block(); _cms_profiles_modified_connection.block(); _intent_changed_connection.block(); - if (auto document = getDocument()){ + if (auto document = getDocument()){ auto &cm = document->getColorManager(); // This also removes uses of the color profile from shapes - if (auto prev = cm.getDefault()) { - cm.removeProfile(prev); - } + if (auto prev = cm.defaultSpace()) { + //cm.removeProfile(prev); + } // Default intent if needed, update combo because we're signal blocked - auto intent_id = intent.get_active_id(); + auto intent_id = intent_box.get_active_id(); if (intent_id.empty()) { intent_id = "relative-colorimetric"; - } + } - auto profile_id = profile.get_active_id(); - std::string uri = !profile_id.empty() ? Inkscape::CMSSystem::get()->get_path_for_profile(profile_id) : ""; - auto cp = cm.addProfile(uri, ColorProfile::renderingIntentEnum(intent_id)); + //auto space = cm.find(profile_box.get_active_id()); + //auto cp = cm.addProfile(uri, ColorProfile::renderingIntentEnum(intent_id)); // This also sets sRGB when the profile add fails - cm.setDefault(cp); + //cm.setDefault(cp); // The intent_id might have changed if the active_id was empty - intent.set_active_id(intent_id); - intent.set_sensitive((bool)cp); + intent_box.set_active_id(intent_id); + //intent.set_sensitive((bool)cp); DocumentUndo::done(document, _("Set default color profile"), ""); } @@ -646,11 +647,11 @@ void DocumentProperties::build_cms() _cms_profiles_changed_connection.unblock(); }); - _intent_changed_connection = intent.signal_changed().connect([this, &intent]() { + _intent_changed_connection = intent_box.signal_changed().connect([this, &intent_box]() { _cms_profiles_modified_connection.block(); if (auto document = getDocument()) { - if (auto profile = document->getColorManager().getDefault()) { - profile->setRenderingIntent(intent.get_active_id()); + if (auto profile = document->getColorManager().defaultSpace()) { + //profile->setRenderingIntent(intent.get_active_id()); } DocumentUndo::done(document, _("Set default color rendering intent"), ""); } @@ -661,13 +662,13 @@ void DocumentProperties::build_cms() void DocumentProperties::init_cms(SPDocument *document) { - auto &manager = document->getColorManager(); + auto &tracker = document->getColorTracker(); - _cms_profiles_changed_connection = manager.connectChanged([this, document]() { + _cms_profiles_changed_connection = tracker.connectChanged([this, document]() { update_cms(document); }); - _cms_profiles_modified_connection = manager.connectModified([this](ColorProfile *cm) { - update_cms(cm->document); + _cms_profiles_modified_connection = tracker.connectModified([this](std::shared_ptr space) { + //update_cms(cm->document); }); update_cms(document); @@ -711,26 +712,25 @@ void DocumentProperties::update_cms(SPDocument *document) profile.set_active(0); intent.set_sensitive(false); - std::vector profiles; - for (auto const &name: Inkscape::CMSSystem::get()->get_cms_profile_names()) { + auto profiles = Inkscape::Colors::CMS::System::get().getOutputProfiles(); + for (auto const &profile : profiles) { auto row = *(colors->append()); - row[ColorColumns().name] = name; - profiles.push_back(name); + row[ColorColumns().name] = profile->getName(); } for (auto cm : document->getColorManager()) { - auto name = cm->getName(); - if (std::find(profiles.begin(), profiles.end(), name) == profiles.end()) { + auto name = "Name of thing"; //cm->getName(); + //if (std::find(profiles.begin(), profiles.end(), name) == profiles.end()) { // Custom ICC, probably using the file manager, add to known profiles - auto row = *(colors->append()); - row[ColorColumns().name] = name; - } + //auto row = *(colors->append()); + //row[ColorColumns().name] = name; + //} - auto intent_id = ColorProfile::renderingIntentId(cm->getRenderingIntent()); - if (cm->isDefault()) { + auto intent_id = std::string("none"); //ColorProfile::renderingIntentId(cm->getRenderingIntent()); + if (false) { //cm->isDefault()) { if (intent_id.empty()) intent_id = "auto"; - profile.set_active_id(cm->getName()); + profile.set_active_id(name); intent.set_sensitive(true); intent.set_active_id(intent_id); } else { diff --git a/src/ui/dialog/export.cpp b/src/ui/dialog/export.cpp index a55897ac59..a6f227cc32 100644 --- a/src/ui/dialog/export.cpp +++ b/src/ui/dialog/export.cpp @@ -34,7 +34,8 @@ #include "message.h" // for MessageType #include "message-stack.h" -#include "color/color-conv.h" +#include "colors/utils.h" + #include "extension/output.h" #include "helper/png-write.h" #include "io/resource.h" @@ -473,14 +474,14 @@ std::string Export::defaultFilename(SPDocument *doc, std::string &filename_entry void set_export_bg_color(SPObject* object, guint32 color) { if (object) { - object->setAttribute("inkscape:export-bgcolor", Inkscape::Util::rgba_color_to_string(color).c_str()); + object->setAttribute("inkscape:export-bgcolor", Inkscape::Colors::rgba_to_hex(color, true)); } } guint32 get_export_bg_color(SPObject* object, guint32 default_color) { if (object) { - if (auto color = Inkscape::Util::string_to_rgba_color(object->getAttribute("inkscape:export-bgcolor"))) { - return *color; + if (auto attr = object->getAttribute("inkscape:export-bgcolor")) { + return Inkscape::Colors::hex_to_rgba(attr); } } return default_color; diff --git a/src/ui/dialog/global-palettes.cpp b/src/ui/dialog/global-palettes.cpp index 8e427b3932..baa8dad701 100644 --- a/src/ui/dialog/global-palettes.cpp +++ b/src/ui/dialog/global-palettes.cpp @@ -33,7 +33,6 @@ #include #include -#include "color/cmyk-conv.h" #include "helper/choose-file.h" #include "hsluv.h" #include "io/resource.h" @@ -182,8 +181,6 @@ 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; for (int index = 0; index < color_count; ++index) { @@ -215,7 +212,8 @@ void load_acb_palette(PaletteFileData& palette, std::string const &fname) { 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); + unsigned int r = 0; unsigned int g = 0; unsigned int b = 0; + //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 << '%'; } @@ -268,7 +266,6 @@ 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; @@ -296,7 +293,8 @@ void load_ase_swatches(PaletteFileData& palette, std::string const &fname) { 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); + //auto [r, g, b] = convert.cmyk_to_rgb(c, m, y, k); + unsigned int r = 0; unsigned int g = 0; unsigned int b = 0; ost << "C: " << c << "% M: " << m << "% Y: " << y << "% K: " << k << '%'; palette.colors.push_back( PaletteFileData::Color {{c, m, y, k}, PaletteFileData::Cmyk100, {r, g, b}, color_name, ost.str(), mode} diff --git a/src/ui/dialog/inkscape-preferences.cpp b/src/ui/dialog/inkscape-preferences.cpp index f8efaeb748..70c1f83a14 100644 --- a/src/ui/dialog/inkscape-preferences.cpp +++ b/src/ui/dialog/inkscape-preferences.cpp @@ -66,7 +66,8 @@ #include "selection.h" #include "style.h" -#include "color/cms-system.h" +#include "colors/cms/system.h" +#include "colors/cms/profile.h" #include "display/control/canvas-item-grid.h" #include "display/nr-filter-gaussian.h" #include "extension/internal/gdkpixbuf-input.h" @@ -107,7 +108,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; @@ -2210,28 +2210,29 @@ static void profileComboChanged( Gtk::ComboBoxText* combo ) { Inkscape::Preferences *prefs = Inkscape::Preferences::get(); int rowNum = combo->get_active_row_number(); - if ( rowNum < 1 ) { + if (rowNum < 1) { prefs->setString("/options/displayprofile/uri", ""); } 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 active = cms_system.getProfile(combo->get_active_text())) { + Glib::ustring path = active->getPath(); + if ( !path.empty() ) { + prefs->setString("/options/displayprofile/uri", path); + } } } } static void cmsComboChanged( 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); + auto &cms_system = Inkscape::Colors::CMS::System::get(); + if (auto active = cms_system.getProfile(combo->get_active_text())) { + Glib::ustring path = active->getPath(); - if ( !path.empty() ) { - Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - prefs->setString("/options/cms/uri", path); + if ( !path.empty() ) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString("/options/cms/uri", path); + } } } @@ -2380,8 +2381,9 @@ void InkscapePreferences::initPageIO() _page_cms.add_group_header( _("Display adjustment")); + auto &cms_system = Inkscape::Colors::CMS::System::get(); Glib::ustring tmpStr; - for (auto const &path : Inkscape::CMSSystem::get_directory_paths()) { + for (auto const &path : cms_system.getDirectoryPaths()) { tmpStr += "\n"; tmpStr += path.first; } @@ -2414,16 +2416,14 @@ void InkscapePreferences::initPageIO() _("The rendering intent to use to calibrate device output"), false); { - auto cms_system = Inkscape::CMSSystem::get(); - std::vector names = cms_system->get_monitor_profile_names(); + auto profiles = cms_system.getDisplayProfiles(); Glib::ustring current = prefs->getString( "/options/displayprofile/uri" ); gint index = 0; _cms_display_profile.append(_("Controlled by the Operating System")); 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 const &profile : profiles) { + _cms_display_profile.append(profile->getName()); + if (!current.empty() && profile->getPath() == current) { _cms_display_profile.set_active(index); } index++; @@ -2432,13 +2432,12 @@ void InkscapePreferences::initPageIO() _cms_display_profile.set_active(0); } - names = cms_system->get_cms_profile_names(); + profiles = cms_system.getOutputProfiles(); current = prefs->getString("/options/cms/uri"); index = 0; - for (auto const &name : names) { - _cms_default_profile.append( name ); - Glib::ustring path = cms_system->get_path_for_profile(name); - if ( !path.empty() && path == current ) { + for (auto const &profile : profiles) { + _cms_default_profile.append(profile->getName()); + if (!current.empty() && profile->getPath() == current) { _cms_default_profile.set_active(index); } index++; diff --git a/src/ui/icon-loader.cpp b/src/ui/icon-loader.cpp index b914e814a4..ccbf7c83a9 100644 --- a/src/ui/icon-loader.cpp +++ b/src/ui/icon-loader.cpp @@ -23,7 +23,6 @@ #include #include -#include "color/color-conv.h" #include "desktop.h" #include "inkscape.h" #include "ui/util.h" diff --git a/src/ui/tools/tweak-tool.cpp b/src/ui/tools/tweak-tool.cpp index 53dde3acfc..604612d354 100644 --- a/src/ui/tools/tweak-tool.cpp +++ b/src/ui/tools/tweak-tool.cpp @@ -585,10 +585,9 @@ sp_tweak_dilate_recursive (Inkscape::Selection *selection, SPItem *item, Geom::P return did; } -/* static void -tweak_colorpaint (SPColor &spc, guint32 goal, double force, bool do_h, bool do_s, bool do_l) + static void +tweak_colorpaint (float *color, guint32 goal, double force, bool do_h, bool do_s, bool do_l) { - auto &color = spc->getColors(); float rgb_g[3]; if (!do_h || !do_s || !do_l) { @@ -616,14 +615,12 @@ tweak_colorpaint (SPColor &spc, guint32 goal, double force, bool do_h, bool do_s double d = rgb_g[i] - color[i]; color[i] += d * force; } -}*/ +} -/* static void -tweak_colorjitter (SPColor &spc, double force, bool do_h, bool do_s, bool do_l) + static void +tweak_colorjitter (float *color, double force, bool do_h, bool do_s, bool do_l) { float hsl_c[3]; - // XXX Replace with proper hsl convertor later - auto &color = spc.getColors(); SPColor::rgb_to_hsl_floatv (hsl_c, color[0], color[1], color[2]); if (do_h) { @@ -642,17 +639,16 @@ tweak_colorjitter (SPColor &spc, double force, bool do_h, bool do_s, bool do_l) hsl_c[2] += g_random_double_range(-hsl_c[2], 1 - hsl_c[2]) * force; } - g_error("You disbaled this code because it's so appaulingly bad"); SPColor::hsl_to_rgb_floatv (color, hsl_c[0], hsl_c[1], hsl_c[2]); -}*/ +} static void -tweak_color (guint mode, SPColor const &color, guint32 goal, double force, bool do_h, bool do_s, bool do_l) +tweak_color (guint mode, float *color, guint32 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); + 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); + tweak_colorjitter (color, force, do_h, do_s, do_l); } } @@ -782,10 +778,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(), rgb_goal, + tweak_color (mode, stop->getColor().v.c, rgb_goal, force * (pos_e - offset_l) / (offset_h - offset_l), do_h, do_s, do_l); - tweak_color(mode, prevStop->getColor(), rgb_goal, + tweak_color(mode, prevStop->getColor().v.c, rgb_goal, force * (offset_h - pos_e) / (offset_h - offset_l), do_h, do_s, do_l); stop->updateRepr(); @@ -795,14 +791,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(), rgb_goal, + tweak_color(mode, prevStop->getColor().v.c, rgb_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, prevStop->getColor(), rgb_goal, + tweak_color (mode, stop->getColor().v.c, rgb_goal, force * tweak_profile (fabs (pos_e - offset_h), r), do_h, do_s, do_l); stop->updateRepr(); @@ -824,7 +820,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(), rgb_goal, + tweak_color (mode, stop->getColor().v.c, rgb_goal, force * tweak_profile (distance, radius), do_h, do_s, do_l); stop->updateRepr(); } @@ -941,7 +937,7 @@ sp_tweak_color_recursive (guint mode, SPItem *item, SPItem *item_at_point, 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, fill_goal, this_force, do_h, do_s, do_l); + tweak_color (mode, style->fill.value.color.v.c, fill_goal, this_force, do_h, do_s, do_l); item->updateRepr(); did = true; } @@ -951,7 +947,7 @@ sp_tweak_color_recursive (guint mode, SPItem *item, SPItem *item_at_point, 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, stroke_goal, this_force, do_h, do_s, do_l); + tweak_color (mode, style->stroke.value.color.v.c, stroke_goal, this_force, do_h, do_s, do_l); item->updateRepr(); did = true; } diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp index bd70653e27..440cfe88fd 100644 --- a/src/ui/widget/canvas.cpp +++ b/src/ui/widget/canvas.cpp @@ -38,7 +38,7 @@ #include "canvas/stores.h" #include "canvas/synchronizer.h" #include "canvas/util.h" -#include "color/cms-system.h" // Color correction +#include "colors/cms/transform.h" // Color correction #include "color.h" // Background color #include "desktop.h" #include "display/control/canvas-item-drawing.h" @@ -153,7 +153,7 @@ struct RedrawData bool decoupled_mode; Cairo::RefPtr snapshot_drawn; Geom::OptIntRect grabbed; - std::shared_ptr screen_transform; + std::shared_ptr screen_transform; // Saved prefs int coarsener_min_size; @@ -233,8 +233,8 @@ public: Cairo::RefPtr invalidated; // Buffers invalidations while the updater is in use by the background process. // CMS - std::shared_ptr colorproof_transform; - std::shared_ptr gamutwarn_transform; + std::shared_ptr colorproof_transform; + std::shared_ptr gamutwarn_transform; // Graphics state; holds all the graphics resources, including the drawn content. std::unique_ptr graphics; @@ -1737,7 +1737,7 @@ void Canvas::set_antialiasing_enabled(bool enabled) } } -void Canvas::set_cms_transforms(std::shared_ptr colorproof, std::shared_ptr gamutwarn) +void Canvas::set_cms_transforms(std::shared_ptr colorproof, std::shared_ptr gamutwarn) { if (colorproof != d->colorproof_transform || gamutwarn != d->gamutwarn_transform) { d->colorproof_transform = colorproof; @@ -1805,7 +1805,7 @@ bool CanvasPrivate::is_point_on_page(const Geom::Point &point) const void Canvas::set_screen_transform() { - _screen_transform = CMSSystem::get()->get_screen_transform(); + // XXX _screen_transform = CMSSystem::get()->get_screen_transform(); } // Change cursor @@ -2409,7 +2409,7 @@ void CanvasPrivate::paint_single_buffer(Cairo::RefPtr const // 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.screen_transform) { - Inkscape::CMSSystem::do_transform(rd.screen_transform->getHandle(), surface->cobj(), surface->cobj()); + rd.screen_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 bf98692e75..278d319541 100644 --- a/src/ui/widget/canvas.h +++ b/src/ui/widget/canvas.h @@ -38,10 +38,12 @@ class GestureMultiPress; class SPDesktop; namespace Inkscape { +namespace Colors::CMS { + class Transform; +} class CanvasItem; class CanvasItemGroup; -class CMSTransform; class Drawing; namespace UI::Widget { @@ -93,7 +95,7 @@ public: Inkscape::SplitMode get_split_mode() const { return _split_mode; } void set_clip_to_page_mode(bool clip); void set_antialiasing_enabled(bool enabled); - void set_cms_transforms(std::shared_ptr colorproof, std::shared_ptr gamutwarn); + void set_cms_transforms(std::shared_ptr colorproof, std::shared_ptr gamutwarn); /* Observers */ @@ -185,7 +187,7 @@ private: bool _antialiasing_enabled = true; // CMS - std::shared_ptr _screen_transform; ///< The lcms transform to apply for the screen + std::shared_ptr _screen_transform; ///< The lcms transform to apply for the screen void set_screen_transform(); ///< Set the screen lcms transform. /* Internal state */ diff --git a/src/ui/widget/cms-popover.cpp b/src/ui/widget/cms-popover.cpp index 29fe38ddc8..7584f72a04 100644 --- a/src/ui/widget/cms-popover.cpp +++ b/src/ui/widget/cms-popover.cpp @@ -11,7 +11,7 @@ #include "gtkmm/image.h" -#include "color/manager.h" +#include "colors/tracker.h" #include "desktop.h" #include "desktop-widget.h" #include "document.h" @@ -50,7 +50,7 @@ void CmsPopover::on_show() // Update ink icons and show/hide the right things. auto desktop = _dtw->get_desktop(); auto document = desktop->getDocument(); - auto &manager = document->getColorManager(); + auto &tracker = document->getColorTracker(); // Remove myself if the desktop replaces the document at any time document_replaced_connection = desktop->connectDocumentReplaced([this](SPDesktop *, SPDocument *) { @@ -58,10 +58,10 @@ void CmsPopover::on_show() }); // If anything changes while the popup is open, refresh our UI - profiles_changed_connection = manager.connectChanged([this, document]() { + profiles_changed_connection = tracker.connectChanged([this, document]() { this->refresh(document); }); - profiles_modified_connection = manager.connectModified([this, document](ColorProfile *cp) { + profiles_modified_connection = tracker.connectModified([this, document](std::shared_ptr space) { this->refresh(document); }); @@ -75,10 +75,9 @@ void CmsPopover::refresh(SPDocument *document) if (!_dtw || !_dtw->get_desktop()) return; - auto &manager = document->getColorManager(); - _profile = manager.getDefault(); + _profile = nullptr; //manager.getDefault(); - if (_profile && _profile->isPrintColorSpace()) { + /*if (_profile && _profile->isPrintColorSpace()) { _colors_cmyk.show(); _colors_rgb.hide(); _colors_label.set_text(_profile->getName()); @@ -101,7 +100,7 @@ void CmsPopover::refresh(SPDocument *document) Gdk::RGBA("blue"), }); // TODO: Set RGB color seperator button actions - } + }*/ // FUTURE: deal with spot colors here. // 1. Look at all swatches which have the icc profile AND are set as spot colors diff --git a/src/ui/widget/color-notebook.cpp b/src/ui/widget/color-notebook.cpp index 2458ac9871..fdef868743 100644 --- a/src/ui/widget/color-notebook.cpp +++ b/src/ui/widget/color-notebook.cpp @@ -26,8 +26,7 @@ #include "document.h" #include "inkscape.h" #include "preferences.h" -#include "color/cms-system.h" -#include "color/manager.h" +#include "colors/manager.h" #include "object/color-profile.h" #include "ui/dialog-events.h" #include "ui/icon-loader.h" @@ -38,8 +37,6 @@ #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; @@ -278,29 +275,29 @@ void ColorNotebook::_updateICCButtons() _setCurrentPage(getPageIndex("CMS"), true); /* update out-of-gamut icon */ - Inkscape::ColorProfile *target_profile = - _document->getColorManager().find(name); - if (target_profile) - gtk_widget_set_sensitive(_box_outofgamut, target_profile->GamutCheck(color)); + Inkscape::ColorProfile *target_profile = nullptr; + //_document->getColorManager().find(name); + //if (target_profile) + // gtk_widget_set_sensitive(_box_outofgamut, target_profile->GamutCheck(color)); /* update too-much-ink icon */ - Inkscape::ColorProfile *prof = _document->getColorManager().find(name); + /*Inkscape::ColorProfile *prof = nullptr; //_document->getColorManager().find(name); if (prof && prof->isPrintColorSpace()) { gtk_widget_set_visible(_box_toomuchink, true); double ink_sum = 0; for (double i : color.getColors()) { 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) + /*if (ink_sum > 3.2) gtk_widget_set_sensitive(_box_toomuchink, true); - } - else { + }*/ + //else { gtk_widget_set_visible(_box_toomuchink, false); - } + //} } else { Inkscape::Preferences *prefs = Inkscape::Preferences::get(); auto page = prefs->getString("/colorselector/page"); diff --git a/src/ui/widget/color-scales.cpp b/src/ui/widget/color-scales.cpp index 3d9455e384..d32d510bf1 100644 --- a/src/ui/widget/color-scales.cpp +++ b/src/ui/widget/color-scales.cpp @@ -76,23 +76,23 @@ const char* color_mode_name[] = { N_("None"), N_("RGB"), N_("HSL"), N_("CMYK"), N_("HSV"), N_("HSLuv"), N_("OKHSL"), nullptr }; -const char* get_color_mode_icon(Color::Space mode) { +const char* get_color_mode_icon(Colors::Space::Type mode) { return color_mode_icons[static_cast(mode)]; } -const char* get_color_mode_label(Color::Space mode) { +const char* get_color_mode_label(Colors::Space::Type mode) { return color_mode_name[static_cast(mode)]; } -std::unique_ptr get_factory(Color::Space mode) { +std::unique_ptr get_factory(Colors::Space::Type mode) { switch (mode) { - case Color::Space::RGB: return std::make_unique>(); - case Color::Space::HSL: return std::make_unique>(); - case Color::Space::HSV: return std::make_unique>(); - case Color::Space::CMYK: return std::make_unique>(); - case Color::Space::HSLUV: return std::make_unique>(); - case Color::Space::OKLAB: return std::make_unique>(); + case Colors::Space::Type::RGB: return std::make_unique>(); + case Colors::Space::Type::HSL: return std::make_unique>(); + case Colors::Space::Type::HSV: return std::make_unique>(); + case Colors::Space::Type::CMYK: return std::make_unique>(); + case Colors::Space::Type::HSLUV: return std::make_unique>(); + case Colors::Space::Type::OKLAB: return std::make_unique>(); default: throw std::invalid_argument("There's no factory for the requested color mode"); } @@ -102,12 +102,12 @@ std::vector get_color_pickers() { std::vector pickers; for (auto mode : { - Color::Space::HSL, - Color::Space::HSV, - Color::Space::RGB, - Color::Space::CMYK, - Color::Space::OKLAB, - Color::Space::HSLUV, + Colors::Space::Type::HSL, + Colors::Space::Type::HSV, + Colors::Space::Type::RGB, + Colors::Space::Type::CMYK, + Colors::Space::Type::OKLAB, + Colors::Space::Type::HSLUV, }) { auto label = get_color_mode_label(mode); @@ -124,7 +124,7 @@ std::vector get_color_pickers() { } -template +template ColorScales::ColorScales(SelectedColor &color, bool no_alpha) : Gtk::Box() , _color(color) @@ -145,7 +145,7 @@ ColorScales::ColorScales(SelectedColor &color, bool no_alpha) _color_dragged = _color.signal_dragged.connect([this](){ _onColorChanged(); }); } -template +template void ColorScales::_initUI(bool no_alpha) { set_orientation(Gtk::ORIENTATION_VERTICAL); @@ -154,15 +154,15 @@ void ColorScales::_initUI(bool no_alpha) Gtk::Expander *wheel_frame = nullptr; if constexpr ( - MODE == Color::Space::HSL || - MODE == Color::Space::HSV || - MODE == Color::Space::HSLUV || - MODE == Color::Space::OKLAB) + MODE == Colors::Space::Type::HSL || + MODE == Colors::Space::Type::HSV || + MODE == Colors::Space::Type::HSLUV || + MODE == Colors::Space::Type::OKLAB) { /* Create wheel */ - if constexpr (MODE == Color::Space::HSLUV) { + if constexpr (MODE == Colors::Space::Type::HSLUV) { _wheel = Gtk::make_managed(); - } else if constexpr (MODE == Color::Space::OKLAB) { + } else if constexpr (MODE == Colors::Space::Type::OKLAB) { _wheel = Gtk::make_managed(); } else { _wheel = Gtk::make_managed(); @@ -282,10 +282,10 @@ void ColorScales::_initUI(bool no_alpha) setupMode(no_alpha); if constexpr ( - MODE == Color::Space::HSL || - MODE == Color::Space::HSV || - MODE == Color::Space::HSLUV || - MODE == Color::Space::OKLAB) + MODE == Colors::Space::Type::HSL || + MODE == Colors::Space::Type::HSV || + MODE == Colors::Space::Type::HSLUV || + MODE == Colors::Space::Type::OKLAB) { // Restore the visibility of the wheel bool visible = Inkscape::Preferences::get()->getBool(wheel_pref, false); @@ -294,7 +294,7 @@ void ColorScales::_initUI(bool no_alpha) } } -template +template void ColorScales::_recalcColor() { SPColor color; @@ -302,16 +302,16 @@ void ColorScales::_recalcColor() gfloat c[5]; if constexpr ( - MODE == Color::Space::RGB || - MODE == Color::Space::HSL || - MODE == Color::Space::HSV || - MODE == Color::Space::HSLUV || - MODE == Color::Space::OKLAB) + MODE == Colors::Space::Type::RGB || + MODE == Colors::Space::Type::HSL || + MODE == Colors::Space::Type::HSV || + MODE == Colors::Space::Type::HSLUV || + MODE == Colors::Space::Type::OKLAB) { _getRgbaFloatv(c); color.set(c[0], c[1], c[2]); alpha = c[3]; - } else if constexpr (MODE == Color::Space::CMYK) { + } else if constexpr (MODE == Colors::Space::Type::CMYK) { _getCmykaFloatv(c); float rgb[3]; @@ -325,7 +325,7 @@ void ColorScales::_recalcColor() _color.setColorAlpha(color, alpha); } -template +template void ColorScales::_updateDisplay(bool update_wheel) { #ifdef DUMP_CHANGE_INFO @@ -339,33 +339,33 @@ void ColorScales::_updateDisplay(bool update_wheel) SPColor color = _color.color(); - if constexpr (MODE == Color::Space::RGB) { + if constexpr (MODE == Colors::Space::Type::RGB) { color.get_rgb_floatv(c); c[3] = _color.alpha(); c[4] = 0.0; - } else if constexpr (MODE == Color::Space::HSL) { + } else if constexpr (MODE == Colors::Space::Type::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); } - } else if constexpr (MODE == Color::Space::HSV) { + } else if constexpr (MODE == Colors::Space::Type::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); } - } else if constexpr (MODE == Color::Space::CMYK) { + } else if constexpr (MODE == Colors::Space::Type::CMYK) { color.get_cmyk_floatv(c); c[4] = _color.alpha(); - } else if constexpr (MODE == Color::Space::HSLUV) { + } else if constexpr (MODE == Colors::Space::Type::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); } - } else if constexpr (MODE == Color::Space::OKLAB) { + } else if constexpr (MODE == Colors::Space::Type::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]})); @@ -396,20 +396,20 @@ void ColorScales::_updateDisplay(bool update_wheel) } /* Helpers for setting color value */ -template +template double ColorScales::getScaled(int index) const { return get_adj_scaled(_adj[index]); } -template +template void ColorScales::setScaled(int index, double value) { set_adj_scaled(_adj[index], value); } -template +template std::vector ColorScales::getAllScaled() const { std::vector ret; @@ -420,7 +420,7 @@ std::vector ColorScales::getAllScaled() const } -template +template void ColorScales::_onColorChanged() { if (!get_visible()) { return; } @@ -428,7 +428,7 @@ void ColorScales::_onColorChanged() _updateDisplay(); } -template +template void ColorScales::on_show() { Gtk::Box::on_show(); @@ -436,26 +436,26 @@ void ColorScales::on_show() _updateDisplay(); } -template +template void ColorScales::_getRgbaFloatv(gfloat *rgba) { g_return_if_fail(rgba != nullptr); - if constexpr (MODE == Color::Space::RGB) { + if constexpr (MODE == Colors::Space::Type::RGB) { rgba[0] = getScaled(0); rgba[1] = getScaled(1); rgba[2] = getScaled(2); - } else if constexpr (MODE == Color::Space::HSL) { + } else if constexpr (MODE == Colors::Space::Type::HSL) { SPColor::hsl_to_rgb_floatv(rgba, getScaled(0), getScaled(1), getScaled(2)); - } else if constexpr (MODE == Color::Space::HSV) { + } else if constexpr (MODE == Colors::Space::Type::HSV) { SPColor::hsv_to_rgb_floatv(rgba, getScaled(0), getScaled(1), getScaled(2)); - } else if constexpr (MODE == Color::Space::CMYK) { + } else if constexpr (MODE == Colors::Space::Type::CMYK) { SPColor::cmyk_to_rgb_floatv(rgba, getScaled(0), getScaled(1), getScaled(2), getScaled(3)); - } else if constexpr (MODE == Color::Space::HSLUV) { + } else if constexpr (MODE == Colors::Space::Type::HSLUV) { SPColor::hsluv_to_rgb_floatv(rgba, getScaled(0), getScaled(1), getScaled(2)); - } else if constexpr (MODE == Color::Space::OKLAB) { + } else if constexpr (MODE == Colors::Space::Type::OKLAB) { auto const tmp = Oklab::oklab_to_rgb( Oklab::okhsl_to_oklab({ getScaled(0), getScaled(1), @@ -469,30 +469,30 @@ void ColorScales::_getRgbaFloatv(gfloat *rgba) rgba[3] = _alpha_index > -1 ? getScaled(_alpha_index) : 1.0; } -template +template void ColorScales::_getCmykaFloatv(gfloat *cmyka) { gfloat rgb[3]; g_return_if_fail(cmyka != nullptr); - if constexpr (MODE == Color::Space::RGB) { + if constexpr (MODE == Colors::Space::Type::RGB) { SPColor::rgb_to_cmyk_floatv(cmyka, getScaled(0), getScaled(1), getScaled(2)); - } else if constexpr (MODE == Color::Space::HSL) { + } else if constexpr (MODE == Colors::Space::Type::HSL) { SPColor::hsl_to_rgb_floatv(rgb, getScaled(0), getScaled(1), getScaled(2)); SPColor::rgb_to_cmyk_floatv(cmyka, rgb[0], rgb[1], rgb[2]); - } else if constexpr (MODE == Color::Space::HSLUV) { + } else if constexpr (MODE == Colors::Space::Type::HSLUV) { SPColor::hsluv_to_rgb_floatv(rgb, getScaled(0), getScaled(1), getScaled(2)); SPColor::rgb_to_cmyk_floatv(cmyka, rgb[0], rgb[1], rgb[2]); - } else if constexpr (MODE == Color::Space::OKLAB) { + } else if constexpr (MODE == Colors::Space::Type::OKLAB) { auto const tmp = Oklab::oklab_to_rgb( Oklab::okhsl_to_oklab({ getScaled(0), getScaled(1), getScaled(2) })); SPColor::rgb_to_cmyk_floatv(cmyka, (float)tmp[0], (float)tmp[1], (float)tmp[2]); - } else if constexpr (MODE == Color::Space::CMYK) { + } else if constexpr (MODE == Colors::Space::Type::CMYK) { cmyka[0] = getScaled(0); cmyka[1] = getScaled(1); cmyka[2] = getScaled(2); @@ -503,7 +503,7 @@ void ColorScales::_getCmykaFloatv(gfloat *cmyka) cmyka[4] = _alpha_index > -1 ? getScaled(_alpha_index) : 1.0; } -template +template guint32 ColorScales::_getRgba32() { gfloat c[4]; @@ -516,7 +516,7 @@ guint32 ColorScales::_getRgba32() return rgba; } -template +template void ColorScales::setupMode(bool no_alpha) { _updating = true; @@ -524,20 +524,20 @@ void ColorScales::setupMode(bool no_alpha) gfloat rgba[4]; gfloat c[4]; - if constexpr (MODE == Color::Space::NONE) { + if constexpr (MODE == Colors::Space::Type::NONE) { rgba[0] = rgba[1] = rgba[2] = rgba[3] = 1.0; } else { _getRgbaFloatv(rgba); } int index = 0; - for (auto &comp : Color::getComponents(MODE)) { + /*for (auto &comp : Colors::getComponents(MODE)) { _adj[index]->set_upper(comp.ink_scale); _lbl[index]->set_markup_with_mnemonic(comp.name); _btn[index]->set_tooltip_text(comp.tip); _slide[index]->set_tooltip_text(comp.tip); index++; - } + }*/ if (!no_alpha) { _adj[index]->set_upper(100); @@ -564,11 +564,11 @@ void ColorScales::setupMode(bool no_alpha) _slide[i]->set_visible(i < index); } - if constexpr (MODE == Color::Space::RGB) { + if constexpr (MODE == Colors::Space::Type::RGB) { setScaled(0, rgba[0]); setScaled(1, rgba[1]); setScaled(2, rgba[2]); - } else if constexpr (MODE == Color::Space::HSL) { + } else if constexpr (MODE == Colors::Space::Type::HSL) { _slide[0]->setMap(sp_color_scales_hue_map()); c[0] = 0.0; @@ -576,7 +576,7 @@ void ColorScales::setupMode(bool no_alpha) setScaled(0, c[0]); setScaled(1, c[1]); setScaled(2, c[2]); - } else if constexpr (MODE == Color::Space::HSV) { + } else if constexpr (MODE == Colors::Space::Type::HSV) { _slide[0]->setMap(sp_color_scales_hue_map()); c[0] = 0.0; @@ -584,13 +584,13 @@ void ColorScales::setupMode(bool no_alpha) setScaled(0, c[0]); setScaled(1, c[1]); setScaled(2, c[2]); - } else if constexpr (MODE == Color::Space::CMYK) { + } else if constexpr (MODE == Colors::Space::Type::CMYK) { SPColor::rgb_to_cmyk_floatv(c, rgba[0], rgba[1], rgba[2]); setScaled(0, c[0]); setScaled(1, c[1]); setScaled(2, c[2]); setScaled(3, c[3]); - } else if constexpr (MODE == Color::Space::HSLUV) { + } else if constexpr (MODE == Colors::Space::Type::HSLUV) { _slide[0]->setMap(hsluvHueMap(0.0f, 0.0f, &_sliders_maps[0])); _slide[1]->setMap(hsluvSaturationMap(0.0f, 0.0f, &_sliders_maps[1])); _slide[2]->setMap(hsluvLightnessMap(0.0f, 0.0f, &_sliders_maps[2])); @@ -602,7 +602,7 @@ void ColorScales::setupMode(bool no_alpha) setScaled(1, c[1]); setScaled(2, c[2]); setScaled(3, rgba[3]); - } else if constexpr (MODE == Color::Space::OKLAB) { + } else if constexpr (MODE == Colors::Space::Type::OKLAB) { 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(i, tmp[i]); @@ -615,10 +615,10 @@ void ColorScales::setupMode(bool no_alpha) _updating = false; } -template -Color::Space ColorScales::getMode() const { return MODE; } +template +Colors::Space::Type ColorScales::getMode() const { return MODE; } -template +template void ColorScales::_sliderAnyGrabbed() { if (_updating) { return; } @@ -629,7 +629,7 @@ void ColorScales::_sliderAnyGrabbed() } } -template +template void ColorScales::_sliderAnyReleased() { if (_updating) { return; } @@ -640,7 +640,7 @@ void ColorScales::_sliderAnyReleased() } } -template +template void ColorScales::_sliderAnyChanged() { if (_updating) { return; } @@ -648,7 +648,7 @@ void ColorScales::_sliderAnyChanged() _recalcColor(); } -template +template void ColorScales::_adjustmentChanged(int channel) { if (_updating) { return; } @@ -657,13 +657,13 @@ void ColorScales::_adjustmentChanged(int channel) _recalcColor(); } -template +template void ColorScales::_wheelChanged() { if constexpr ( - MODE == Color::Space::NONE || - MODE == Color::Space::RGB || - MODE == Color::Space::CMYK) + MODE == Colors::Space::Type::NONE || + MODE == Colors::Space::Type::RGB || + MODE == Colors::Space::Type::CMYK) { return; } @@ -697,7 +697,7 @@ void ColorScales::_wheelChanged() * * @param channels - The channel NOT to update (this one is being moved!) */ -template +template void ColorScales::_updateSliders(guint channels) { gfloat rgb0[3], rgbm[3], rgb1[3]; @@ -707,7 +707,7 @@ void ColorScales::_updateSliders(guint channels) #endif std::array const adj = [this]() -> std::array { - if constexpr (MODE == Color::Space::CMYK) { + if constexpr (MODE == Colors::Space::Type::CMYK) { return { getScaled(0), getScaled(1), getScaled(2), getScaled(3) }; } else { return { getScaled(0), getScaled(1), getScaled(2), 0.0 }; @@ -716,7 +716,7 @@ void ColorScales::_updateSliders(guint channels) if (channels == CSC_CHANNEL_A || channels == CSC_CHANNEL_CMYKA) { // Alpha never updates the visual colors - } else if constexpr (MODE == Color::Space::RGB) { + } else if constexpr (MODE == Colors::Space::Type::RGB) { if (channels != CSC_CHANNEL_R) { /* Update red */ _slide[0]->setColors(SP_RGBA32_F_COMPOSE(0.0, adj[1], adj[2], 1.0), @@ -740,7 +740,7 @@ void ColorScales::_updateSliders(guint channels) _slide[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 == Color::Space::HSL) { + } else if constexpr (MODE == Colors::Space::Type::HSL) { /* Hue is never updated */ if (channels != CSC_CHANNEL_S) { /* Update saturation */ @@ -766,7 +766,7 @@ void ColorScales::_updateSliders(guint channels) _slide[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 == Color::Space::HSV) { + } else if constexpr (MODE == Colors::Space::Type::HSV) { /* Hue is never updated */ if (channels != CSC_CHANNEL_S) { /* Update saturation */ @@ -792,7 +792,7 @@ void ColorScales::_updateSliders(guint channels) _slide[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 == Color::Space::CMYK) { + } else if constexpr (MODE == Colors::Space::Type::CMYK) { if (channels != CSC_CHANNEL_C) { /* Update C */ SPColor::cmyk_to_rgb_floatv(rgb0, 0.0, adj[1], adj[2], adj[3]); @@ -835,7 +835,7 @@ void ColorScales::_updateSliders(guint channels) _slide[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 == Color::Space::HSLUV) { + } else if constexpr (MODE == Colors::Space::Type::HSLUV) { if (channels != CSC_CHANNEL_H) { /* Update hue */ _slide[0]->setMap(hsluvHueMap(adj[1], adj[2], &_sliders_maps[0])); @@ -853,7 +853,7 @@ void ColorScales::_updateSliders(guint channels) _slide[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 == Color::Space::OKLAB) { + } else if constexpr (MODE == Colors::Space::Type::OKLAB) { if (channels != CSC_CHANNEL_H) { _slide[0]->setMap(Oklab::render_hue_scale(adj[1], adj[2], &_sliders_maps[0])); } @@ -957,7 +957,7 @@ static guchar const *sp_color_scales_hsluv_map(guchar *map, return map; } -template +template guchar const *ColorScales::hsluvHueMap(gfloat s, gfloat l, std::array *map) { @@ -966,7 +966,7 @@ guchar const *ColorScales::hsluvHueMap(gfloat s, gfloat l, }); } -template +template guchar const *ColorScales::hsluvSaturationMap(gfloat h, gfloat l, std::array *map) { @@ -975,7 +975,7 @@ guchar const *ColorScales::hsluvSaturationMap(gfloat h, gfloat l, }); } -template +template guchar const *ColorScales::hsluvLightnessMap(gfloat h, gfloat s, std::array *map) { @@ -984,39 +984,39 @@ guchar const *ColorScales::hsluvLightnessMap(gfloat h, gfloat s, }); } -template +template ColorScalesFactory::ColorScalesFactory() {} -template +template Gtk::Widget *ColorScalesFactory::createWidget(Inkscape::UI::SelectedColor &color, bool no_alpha) const { Gtk::Widget *w = Gtk::make_managed>(color, no_alpha); return w; } -template +template Glib::ustring ColorScalesFactory::modeName() const { return get_color_mode_label(MODE); } // Explicit instantiations -template class ColorScales; -template class ColorScales; -template class ColorScales; -template class ColorScales; -template class ColorScales; -template class ColorScales; -template class ColorScales; - -template class ColorScalesFactory; -template class ColorScalesFactory; -template class ColorScalesFactory; -template class ColorScalesFactory; -template class ColorScalesFactory; -template class ColorScalesFactory; -template class ColorScalesFactory; +template class ColorScales; +template class ColorScales; +template class ColorScales; +template class ColorScales; +template class ColorScales; +template class ColorScales; +template class ColorScales; + +template class ColorScalesFactory; +template class ColorScalesFactory; +template class ColorScalesFactory; +template class ColorScalesFactory; +template class ColorScalesFactory; +template class ColorScalesFactory; +template class ColorScalesFactory; } // namespace Inkscape::UI::Widget diff --git a/src/ui/widget/color-scales.h b/src/ui/widget/color-scales.h index 5642c4dfd8..47462b5c8d 100644 --- a/src/ui/widget/color-scales.h +++ b/src/ui/widget/color-scales.h @@ -21,14 +21,14 @@ #include "helper/auto-connection.h" #include "ui/selected-color.h" -#include "color/components.h" +#include "colors/spaces/enum.h" namespace Inkscape::UI::Widget { class ColorSlider; class ColorWheel; -template +template class ColorScales : public Gtk::Box { @@ -41,7 +41,7 @@ public: void setScaled(int index, double value); void setupMode(bool no_alpha); - Color::Space getMode() const; + Colors::Space::Type getMode() const; static guchar const *hsluvHueMap(gfloat s, gfloat l, std::array *map); @@ -91,7 +91,7 @@ public: ColorScales &operator=(ColorScales const &obj) = delete; }; -template +template class ColorScalesFactory : public Inkscape::UI::ColorSelectorFactory { public: @@ -103,7 +103,7 @@ public: struct ColorPickerDescription { - Color::Space mode; + Colors::Space::Type mode; const char* icon; const char* label; Glib::ustring visibility_path; diff --git a/src/util/numeric/converters.h b/src/util/numeric/converters.h index 6feb14a244..200c225c81 100644 --- a/src/util/numeric/converters.h +++ b/src/util/numeric/converters.h @@ -15,6 +15,8 @@ * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ +#include + #include #include #include diff --git a/testfiles/CMakeLists.txt b/testfiles/CMakeLists.txt index af83b6cd14..5a3e6f9a54 100644 --- a/testfiles/CMakeLists.txt +++ b/testfiles/CMakeLists.txt @@ -58,48 +58,28 @@ if(${CMAKE_SIZEOF_VOID_P} EQUAL 8) endif() set(TEST_SOURCES - async_channel-test - async_funclog-test - async_progress-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-object-test - sp-object-tags-test - object-set-test - object-style-test - path-boolop-test - path-reverse-lpe-test - rebase-hrefs-test - stream-test - style-elem-test - style-internal-test - style-test - svg-affine-test - svg-box-test - svg-color-test - svg-length-test - svg-stringstream-test - sp-gradient-test - svg-path-geom-test - visual-bounds-test - geom-pathstroke-test - object-test - sp-glyph-kerning-test - cairo-utils-test - svg-extension-test - curve-test - 2geom-characterization-test - xml-test - sp-item-group-test - lpe-test - ${LPE_TESTS_64bit} + colors/cms-test + colors/color-test + colors/dragndrop-test + colors/manager-test + colors/parser-test + colors/spaces-cmyk-test + colors/spaces-cms-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-oklab-test + colors/spaces-oklch-test + colors/spaces-rgb-test + colors/spaces-xyz-test + colors/tracker-test + colors/utils-test + colors/xml-color-test ) add_library(cpp_test_static_library SHARED unittest.cpp doc-per-case-test.cpp lpespaths-test.h test-with-svg-object-pairs.cpp) @@ -108,6 +88,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/SwappedRedAndGreen.icc b/testfiles/data/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/color-cms.svg b/testfiles/data/color-cms.svg new file mode 100644 index 0000000000..dcbdfb89ef --- /dev/null +++ b/testfiles/data/color-cms.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/testfiles/data/default_cmyk.icc b/testfiles/data/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/display.icc b/testfiles/data/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 9aa26e4dc0..0000000000 --- a/testfiles/src/color-profile-test.cpp +++ /dev/null @@ -1,125 +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; - Inkscape::RenderingIntent intent; - } - const cases[] = { - {"auto", Inkscape::RenderingIntent::AUTO}, - {"perceptual", Inkscape::RenderingIntent::PERCEPTUAL}, - {"relative-colorimetric", Inkscape::RenderingIntent::RELATIVE_COLORIMETRIC}, - {"relative-colorimetric-nobpc", Inkscape::RenderingIntent::RELATIVE_COLORIMETRIC_NOBPC}, - {"absolute-colorimetric", Inkscape::RenderingIntent::ABSOLUTE_COLORIMETRIC}, - {"saturation", Inkscape::RenderingIntent::SATURATION}, - {"something-else", Inkscape::RenderingIntent::UNKNOWN}, - {"auto2", Inkscape::RenderingIntent::UNKNOWN}, - }; - - for (auto i : cases) { - _prof->setKeyValue(SPAttr::RENDERING_INTENT, i.attr); - ASSERT_EQ(i.intent, _prof->getRenderingIntent()) << 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_EQ(std::string(i), _prof->getName()); - } - _prof->setKeyValue( SPAttr::NAME, NULL ); - ASSERT_TRUE(_prof->getName().empty()); -} - - -} // 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..995d3666ad --- /dev/null +++ b/testfiles/src/colors/cms-test.cpp @@ -0,0 +1,252 @@ +// 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/cms/system.h" +#include "colors/cms/transform.h" +#include "colors/color.h" +#include "colors/manager.h" +#include "preferences.h" + +static std::string icc_dir = INKSCAPE_TESTS_DIR "/data"; +static std::string grb_profile = INKSCAPE_TESTS_DIR "/data/SwappedRedAndGreen.icc"; +static std::string cmyk_profile = INKSCAPE_TESTS_DIR "/data/default_cmyk.icc"; +static std::string display_profile = INKSCAPE_TESTS_DIR "/data/display.icc"; +static std::string not_a_profile = INKSCAPE_TESTS_DIR "/data/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, "path1", false); + ASSERT_TRUE(profile); + ASSERT_EQ(profile->getId(), "00000000000000000000000000000000"); + ASSERT_EQ(profile->getPath(), "path1"); + 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->getInputFormat(), TYPE_RGB_16); + 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); + ASSERT_EQ(profile->getInputFormat(), TYPE_RGB_16); +} + +TEST(ColorCmsProfile, create_srgb) +{ + auto profile = CMS::Profile::create_srgb(); + ASSERT_TRUE(profile); + ASSERT_EQ(profile->getInputFormat(), TYPE_RGB_16); +} + +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, applyTransform) +{ + 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, 3, 3); + 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); +} + +} // 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..cfe0ae6eb5 --- /dev/null +++ b/testfiles/src/colors/color-test.cpp @@ -0,0 +1,421 @@ +// 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" +#include "colors/manager.h" +#include "colors/spaces/base.h" + +using namespace Inkscape::Colors; + +namespace { + +TEST(ColorsColor, construct_space_obj) +{ + auto space = Manager::get().find("HSL"); + ASSERT_TRUE(space); + + ASSERT_EQ(Color(space, {0, 1, 0.5}).toString(), "hsl(0, 1, 0.5)"); +} + +TEST(ColorsColor, construct_space_name) +{ + ASSERT_EQ(Color("HSL", {0, 1, 0.5}).toString(), "hsl(0, 1, 0.5)"); +} + +TEST(ColorsColor, construct_css_string) +{ + ASSERT_EQ(Color("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("red"); + auto other = Color(color); + ASSERT_EQ(other.toString(), "red"); +} + +TEST(ColorsColor, setter) +{ + auto color = Color("red"); + color = Color("green"); + ASSERT_EQ(color.toString(), "green"); + + color.set(Color("#0000ff"), true); + ASSERT_EQ(color.toString(), "blue"); + color.set(Color("#0000ff"), false); + ASSERT_EQ(color.toString(), "#0000ff"); + + color.set(1, 1.0); + ASSERT_EQ(color.toString(), "#00ffff"); + + color.set("red", true); + ASSERT_EQ(color.toString(), "#ff0000"); + color.set("red", false); + ASSERT_EQ(color.toString(), "red"); + + color.set(0x00ff00ff, true); + ASSERT_EQ(color.toString(), "#00ff00ff"); + color.set(0x00ff00, false); + ASSERT_EQ(color.toString(), "blue"); + + color.setValues({0.2, 1.0, 0.5}); + ASSERT_EQ(color.toString(), "#33ff80"); +} + +TEST(ColorsColor, conditionals) +{ + // == + ASSERT_EQ(Color("red"), Color("red")); + // != + ASSERT_NE(Color("green"), Color("#ff0000")); + // bool + ASSERT_TRUE(Color("blue")); + ASSERT_FALSE(Color("")); +} + +TEST(ColorsColor, getSpace) +{ + auto color = Color("red"); + ASSERT_TRUE(color.getSpace()); + ASSERT_EQ(color.getSpace()->getName(), "CSSNAME"); +} + +TEST(ColorsColor, values) +{ + auto color = Color("red"); + auto &values = color.getValues(); + ASSERT_EQ(values.size(), 3); + ASSERT_EQ(values[0], 1.0); + ASSERT_EQ(values[1], 0.0); + ASSERT_EQ(values[2], 0.0); +} + +TEST(ColorsColor, unset) +{ + auto color = Color("red"); + ASSERT_TRUE(color); + color.unset(); + ASSERT_FALSE(color); + ASSERT_TRUE(color.getSpace()); +} + +TEST(ColorsColor, Opacity) +{ + auto color = Color("red"); + ASSERT_FALSE(color.hasOpacity()); + ASSERT_FALSE(color.convert("HSL")->hasOpacity()); + color.setOpacity(1.0); + ASSERT_TRUE(color.hasOpacity()); + ASSERT_EQ(color.getOpacity(), 1.0); + ASSERT_EQ(color.toString(), "#ff0000ff"); + color.setOpacity(0.5); + ASSERT_TRUE(color.hasOpacity()); + ASSERT_EQ(color.getOpacity(), 0.5); + ASSERT_EQ(color.toString(), "#ff000080"); + color.addOpacity(0.5); + ASSERT_EQ(color.getOpacity(), 0.25); + ASSERT_EQ(color.toString(), "#ff000040"); + color.removeOpacity(); + ASSERT_FALSE(color.hasOpacity()); + ASSERT_EQ(color.toString(), "red"); + color.addOpacity(0.5); + ASSERT_TRUE(color.hasOpacity()); + ASSERT_EQ(color.getOpacity(), 0.5); +} + +TEST(ColorsColor, colorOpacityPin) +{ + auto color = Color("red"); + + ASSERT_EQ(color.getOpacityChannel(), 3); + ASSERT_EQ(color.getOpacityPin(), 8); + color.convertInPlace("CMYK"); + ASSERT_EQ(color.getOpacityChannel(), 4); + ASSERT_EQ(color.getOpacityPin(), 16); +} + +TEST(ColorsColor, average) +{ + auto c1 = Color("#ff0000"); + auto c2 = Color("#0000ff"); + ASSERT_EQ(c1.average(c2).toString(), "#800080"); + ASSERT_EQ(c2.average(c1).toString(), "#800080"); + c1.setOpacity(0.5); + ASSERT_EQ(c1.average(c2, 0.25).toString(), "#bf00409f"); + c1.removeOpacity(); + c2.setOpacity(0.5); + ASSERT_EQ(c1.average(c2, 0.75).toString(), "#4000bf9f"); + + c1 = Color("#00000000"); + c1.averageInPlace(Color("white"), 0.25, 1); + ASSERT_EQ(c1.toString(), "#00404040"); + + c1 = Color("#00000000"); + c1.averageInPlace(Color("white"), 0.25, 2); + ASSERT_EQ(c1.toString(), "#40004040"); + + c1 = Color("#00000000"); + c1.averageInPlace(Color("white"), 0.25, 4+2); + ASSERT_EQ(c1.toString(), "#40000040"); + + c1 = Color("#00000000"); + c1.averageInPlace(Color("white"), 0.25, c1.getOpacityPin()); + ASSERT_EQ(c1.toString(), "#40404000"); +} + +TEST(ColorsColor, difference) +{ + auto color = Color("green"); + ASSERT_NEAR(color.difference(Color("red")), 1.251, 0.001); + ASSERT_NEAR(color.difference(Color("blue")), 1.251, 0.001); + ASSERT_NEAR(color.difference(Color("black")), 0.251, 0.001); +} + +TEST(ColorsColor, similarAndClose) +{ + double one_hex_away = 0.004; + auto c1 = Color("#ff0000"); + auto c2 = Color("#0000ff"); + ASSERT_FALSE(c1.isClose(c2)); + ASSERT_FALSE(c1.isSimilar(c2)); + + ASSERT_TRUE(c1.isClose(c1)); + ASSERT_TRUE(c1.isSimilar(c1)); + + c2 = Color("red"); + ASSERT_FALSE(c1.isClose(c2)); + ASSERT_TRUE(c1.isSimilar(c2)); + + c2 = Color("#fe0101"); + ASSERT_TRUE(c1.isClose(c2, one_hex_away)); + ASSERT_TRUE(c1.isSimilar(c2, one_hex_away)); + + c2 = Color("#fe0102"); + ASSERT_FALSE(c1.isClose(c2, one_hex_away)); + ASSERT_FALSE(c1.isSimilar(c2, one_hex_away)); +} + +TEST(ColorsColor, convertInPlace_other) +{ + auto other = Color("red"); + auto color = Color("hsl(120, 1, 0.251)"); + color.convertInPlace(other); + ASSERT_EQ(color.toString(), "green"); + other.addOpacity(); + color.convertInPlace(other); + ASSERT_EQ(color.toString(), "#008000ff"); +} + +TEST(ColorsColor, convertInPlace_space_obj) +{ + auto space = Manager::get().find("HSL"); + ASSERT_TRUE(space); + + auto color = Color("red"); + color.convertInPlace(space); + ASSERT_EQ(color.toString(), "hsl(0, 1, 0.5)"); +} + +TEST(ColorsColor, convertInPlace_space_name) +{ + auto color = Color("red"); + ASSERT_TRUE(color.convertInPlace("HSL")); + ASSERT_EQ(color.toString(), "hsl(0, 1, 0.5)"); + ASSERT_FALSE(color.convertInPlace("bloop")); + ASSERT_EQ(color.toString(), "hsl(0, 1, 0.5)"); +} + +TEST(ColorsColor, convertInPlace_space_type) +{ + auto color = Color("red"); + ASSERT_TRUE(color.convertInPlace(Space::Type::HSL)); + ASSERT_EQ(color.toString(), "hsl(0, 1, 0.5)"); + ASSERT_FALSE(color.convertInPlace(Space::Type::NONE)); + ASSERT_EQ(color.toString(), "hsl(0, 1, 0.5)"); +} + +TEST(ColorsColor, convert_other) +{ + auto other = Color("red"); + ASSERT_EQ(Color("hsl(120, 1, 0.251)").convert(other).toString(), "green"); + other.addOpacity(); + ASSERT_EQ(Color("hsl(120, 1, 0.251)").convert(other).toString(), "#008000ff"); +} + +TEST(ColorsColor, convert_space_obj) +{ + auto space = Manager::get().find("HSL"); + ASSERT_TRUE(space); + ASSERT_EQ(Color("red").convert(space).toString(), "hsl(0, 1, 0.5)"); +} + +TEST(ColorsColor, convert_space_name) +{ + auto color = Color("red"); + ASSERT_EQ(color.convert("HSL")->toString(), "hsl(0, 1, 0.5)"); + + auto none = color.convert("bloop"); + ASSERT_FALSE(none); +} + +TEST(ColorsColor, convert_space_type) +{ + auto color = Color("red"); + ASSERT_EQ(color.convert(Space::Type::HSL)->toString(), "hsl(0, 1, 0.5)"); + + auto none = color.convert(Space::Type::NONE); + ASSERT_FALSE(none); +} + +TEST(ColorsColor, toString) +{ + ASSERT_EQ(Color("red").toString(), "red"); + ASSERT_EQ(Color("#ff0").toString(), "#ffff00"); + ASSERT_EQ(Color("rgb(80,90,255 / 128)").toString(true), "#505aff80"); + ASSERT_EQ(Color("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, name) +{ + auto color = Color("red"); + ASSERT_FALSE(color.getName().size()); + color.setName("Rouge"); + ASSERT_EQ(color.getName(), "Rouge"); + + color.unset(); + ASSERT_FALSE(color.getName().size()); + + color.setName("Rouge"); + color.convertInPlace("HSL"); + ASSERT_FALSE(color.getName().size()); +} + +TEST(ColorsColor, normalizeColor) +{ + auto color = Color("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.normalizeInPlace(); + ASSERT_EQ(color[0], 1.0); + ASSERT_EQ(color[1], 1.0); + ASSERT_EQ(color[2], 0.0); + + color.convertInPlace("HSL"); + color.set(0, 4.1); + color.normalizeInPlace(); + ASSERT_NEAR(color[0], 0.1, 0.001); + + color.set(0, -0.2); + color.normalizeInPlace(); + ASSERT_NEAR(color[0], 0.8, 0.001); + + color.set(0, -2.2); + color.normalizeInPlace(); + ASSERT_NEAR(color[0], 0.8, 0.001); + + color.setOpacity(4.2); + color.normalizeInPlace(); + ASSERT_NEAR(color[3], 1.0, 0.001); +} + +TEST(ColorsColor, invertColor) +{ + auto color = Color("red"); + color.invertInPlace(); + ASSERT_EQ(color.toString(), "aqua"); + color.invertInPlace(); + ASSERT_EQ(color.toString(), "red"); + + color = Color("hsl(90,0.5,0.1)"); + color.invertInPlace(); + ASSERT_EQ(color.toString(), "hsl(270, 0.5, 0.9)"); + + color.invertInPlace(2); + ASSERT_EQ(color.toString(), "hsl(90, 0.5, 0.1)"); +} + +TEST(ColorsColor, jitterColor) +{ + auto color = Color("gray"); + + std::srand(1); // fixed random seed + + color.jitterInPlace(0.1, 0xff); + ASSERT_EQ(color.toString(), "gray"); + + color.jitterInPlace(0.1); + ASSERT_EQ(color.toString(), "#897d87"); + + color.jitterInPlace(0.2); + ASSERT_EQ(color.toString(), "#989278"); + + color.jitterInPlace(0.2, 0x02); + ASSERT_EQ(color.toString(), "#8f9285"); + + color.setOpacity(0.5); + color.jitterInPlace(0.5, color.getOpacityPin()); + ASSERT_EQ(color[color.getOpacityChannel()], 0.5); +} + +TEST(ColorsColor, averageInPlace) +{ + auto c1 = Color(0x1a1a1a1a); + c1.averageInPlace(Color("white"), 0.2, 2); + ASSERT_EQ(c1.toString(), "#481a4848"); + c1.averageInPlace(Color("white"), 0.3, 4+2); + ASSERT_EQ(c1.toString(), "#7f1a487f"); + c1.averageInPlace(Color("white"), 0.5, c1.getOpacityPin()); + ASSERT_EQ(c1.toString(), "#bf8da37f"); +} + +TEST(ColorsColor, moveTowardsColor) +{ + auto c1 = Color("#00000000"); + c1.moveTowardsInPlace(Color("white"), 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/dragndrop-test.cpp b/testfiles/src/colors/dragndrop-test.cpp new file mode 100644 index 0000000000..c30c5c411d --- /dev/null +++ b/testfiles/src/colors/dragndrop-test.cpp @@ -0,0 +1,82 @@ +// 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 + +#include "colors/color.h" +#include "colors/dragndrop.h" + +using namespace Inkscape; +using namespace Inkscape::Colors; + +namespace { + +TEST(ColorDragAndDrop, test_getMimeTypes) +{ + ASSERT_EQ(getMIMETypes().size(), 3); +} + +TEST(ColorDragAndDrop, test_getMimeData_none) +{ + auto data = getMIMEData("text/bad-format", Color("red")); + ASSERT_EQ(data.second, 0); + data = getMIMEData("text/text", Color("none")); + ASSERT_EQ(data.second, 0); + data = getMIMEData("text/text", {}); + ASSERT_EQ(data.second, 0); +} + +TEST(ColorDragAndDrop, test_getMimeData_oswb) +{ + auto data = getMIMEData("application/x-oswb-color", Color("red")); + ASSERT_EQ(data.second, 8); + ASSERT_EQ(data.first[0], '<'); + ASSERT_EQ(data.first[10], 'i'); + ASSERT_EQ(data.first[20], 'e'); + ASSERT_EQ(data.first[30], 'U'); +} + +TEST(ColorDragAndDrop, test_getMimeData_x_color) +{ + auto data = getMIMEData("application/x-color", Color("red")); + ASSERT_EQ(data.second, 16); + ASSERT_EQ(data.first[0], '\xFF'); + ASSERT_EQ(data.first[2], '\x0'); + ASSERT_EQ(data.first[4], '\x0'); +} + +TEST(ColorDragAndDrop, test_getMimeData_text) +{ + auto data = getMIMEData("text/plain", Color("red")); + ASSERT_EQ(data.second, 8); + ASSERT_EQ(data.first[0], 'r'); + ASSERT_EQ(data.first[2], 'd'); +} + +TEST(ColorDragAndDrop, test_fromMimeData) +{ + std::optional color; + auto data = getMIMEData("application/x-oswb-color", Color("red")); + ASSERT_TRUE(fromMIMEData("application/x-oswb-color", reinterpret_cast(data.first.data()), data.first.size(), color)); + ASSERT_EQ(color->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..87ff4f597f --- /dev/null +++ b/testfiles/src/colors/manager-test.cpp @@ -0,0 +1,67 @@ +// 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/system.h" +#include "colors/color.h" +#include "colors/manager.h" +#include "colors/tracker.h" +#include "colors/spaces/base.h" +#include "colors/spaces/cms.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 { + +TEST(ColorManagerDocTest, spaceComponents) +{ + auto &cm = Manager::get(); + + ASSERT_TRUE(cm.find("RGB")); + auto comp = cm.find("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("HSL")); + comp = cm.find("HSL")->getComponents(); + ASSERT_EQ(comp.size(), 3); + ASSERT_EQ(comp[0].name, "_H:"); + ASSERT_EQ(comp[1].name, "_S:"); + ASSERT_EQ(comp[2].name, "_L:"); + + ASSERT_TRUE(cm.find("CMYK")); + comp = cm.find("CMYK")->getComponents(); + 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:"); +} + +} // 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..48175ee13e --- /dev/null +++ b/testfiles/src/colors/parser-test.cpp @@ -0,0 +1,112 @@ +// 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/parser.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) +{ + 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); +} + +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) + && Parser::append_css_value(tests, output, end, ',', 3) + && Parser::append_css_value(tests, output, end, ',', 0.1) + && Parser::append_css_value(tests, output, end, '/', 5) + && Parser::append_css_value(tests, output, end)); + 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), "RGB"); + ASSERT_TRUE(more); + +} + +} // 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..6a9c5e5ce4 --- /dev/null +++ b/testfiles/src/colors/spaces-cms-test.cpp @@ -0,0 +1,69 @@ +// 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/spaces/cms.h" +#include "colors/parser.h" + +using namespace Inkscape::Colors; + +namespace { + +// Get access to protected members for testing +class CMS : public Space::CMS +{ +public: + CMS(unsigned size) : Space::CMS("test-profile", size) {}; + std::string toString(std::vector const &values) const { return Space::CMS::toString(values); } +}; + +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, printColor) +{ + auto space = CMS(4); + ASSERT_EQ(space.toString({}), ""); + ASSERT_EQ(space.toString({1}), ""); + ASSERT_EQ(space.toString({1, 2, 3, 4}), "#000000 icc-color(test-profile, 1, 2, 3, 4)"); + + space = CMS(2); + ASSERT_EQ(space.toString({1}), ""); + ASSERT_EQ(space.toString({1, 2}), "#000000 icc-color(test-profile, 1, 2)"); + ASSERT_EQ(space.toString({1, 2, 3}), "#000000 icc-color(test-profile, 1, 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-cmyk-test.cpp b/testfiles/src/colors/spaces-cmyk-test.cpp new file mode 100644 index 0000000000..067a3a21a9 --- /dev/null +++ b/testfiles/src/colors/spaces-cmyk-test.cpp @@ -0,0 +1,88 @@ +// 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 "spaces-testbase.h" + +namespace { + +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", { }, "", true), + _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 }) +)); + +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"); +} + +} // 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..31023f2010 --- /dev/null +++ b/testfiles/src/colors/spaces-hsl-test.cpp @@ -0,0 +1,102 @@ +// 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 { + +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", { }, "", true), + _P(out, "HSL", { }, "", false), + _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", { 0.0, 1.0, 1.0, 1.0 }) +)); + +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..e97018a9a1 --- /dev/null +++ b/testfiles/src/colors/spaces-hsluv-test.cpp @@ -0,0 +1,57 @@ +// 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 { + +// 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}) +)); + +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", { 1.0, 1.0, 1.0, 1.0 }), + _P(inb, "HSLuv", {-0.2, -0.2, -0.2, -0.2 }, "HSLuv", { 0.0, 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 }) +)); + +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..0094217f0d --- /dev/null +++ b/testfiles/src/colors/spaces-hsv-test.cpp @@ -0,0 +1,105 @@ +// 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 { + +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", { }, "", true), + _P(out, "HSV", { }, "", false), + _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", { 0.0, 1.0, 1.0, 1.0 }) +)); + +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"); +} + +} // 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..f34c266555 --- /dev/null +++ b/testfiles/src/colors/spaces-lab-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 "spaces-testbase.h" + +#include "colors/spaces/lab.h" + +namespace { + +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", { }, "", true), + _P(out, "Lab", { }, "", false), + _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 }) +)); + +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..4b63e076be --- /dev/null +++ b/testfiles/src/colors/spaces-lch-test.cpp @@ -0,0 +1,89 @@ +// 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 "spaces-testbase.h" + +#include "colors/spaces/lch.h" + +namespace { + +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", { }, "", true), + _P(out, "Lch", { }, "", false), + _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, 1.0, 1.0 }), + _P(inb, "Lch", {-0.2, -0.2, -0.2, -0.2 }, "Lch", { 0.0, 0.0, 0.0, 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 }) +)); + +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..4fb57a5f61 --- /dev/null +++ b/testfiles/src/colors/spaces-linear-rgb-test.cpp @@ -0,0 +1,78 @@ +// 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 "spaces-testbase.h" + +#include "colors/spaces/linear-rgb.h" + +namespace { + +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", { }, "", true), + _P(out, "linearRGB", { }, "", false), + _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 }) +)); + +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..18aa2fd3b1 --- /dev/null +++ b/testfiles/src/colors/spaces-luv-test.cpp @@ -0,0 +1,71 @@ +// 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 "spaces-testbase.h" + +#include "colors/spaces/luv.h" + +namespace { + +// 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 }) +)); + +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..2f169e8c17 --- /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(" red ").toRGBA(), 0xff0000ff); + ASSERT_EQ(Color("BLUE ").toRGBA(0.5), 0x0000ff80); +} + +TEST(ColorsSpaceRgb, fromStringFailures) +{ + ASSERT_FALSE(Color("réd")); +} + +TEST(ColorsSpaceRgb, toString) +{ + ASSERT_EQ(Color(0xff000000, false).convert("CSSNAME")->toString(), "red"); + ASSERT_EQ(Color("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-oklab-test.cpp b/testfiles/src/colors/spaces-oklab-test.cpp new file mode 100644 index 0000000000..c7f18ac4ae --- /dev/null +++ b/testfiles/src/colors/spaces-oklab-test.cpp @@ -0,0 +1,80 @@ +// 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 "spaces-testbase.h" + +#include "colors/spaces/oklab.h" + +namespace { + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesLAB, 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(ColorsSpacesLAB, badColorString, testing::Values( + "oklab", "oklab(", "oklab(100" +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesLAB, toString, testing::Values( + _P(out, "OkLab", { }, "", true), + _P(out, "OkLab", { }, "", false), + _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(ColorsSpacesLAB, 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(ColorsSpacesLAB, 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 }) +)); + +TEST(ColorsSpacesLAB, randomConversion) +{ + // Isolate conversion functions + EXPECT_TRUE(RandomPassFunc(Space::OkLab::fromLinearRGB, Space::OkLab::toLinearRGB, 1000)); + + // Full stack conversion + //EXPECT_TRUE(RandomPassthrough("OkLab", "RGB", 1)); +} + +TEST(ColorsSpacesLAB, 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..8c5e567004 --- /dev/null +++ b/testfiles/src/colors/spaces-oklch-test.cpp @@ -0,0 +1,84 @@ +// 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 "spaces-testbase.h" + +#include "colors/spaces/oklch.h" + +namespace { + +// 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(ColorsSpacesLCH, 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(ColorsSpacesLCH, badColorString, testing::Values( + "oklch", "oklch(", "oklch(100" +)); + +INSTANTIATE_TEST_SUITE_P(ColorsSpacesLCH, 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(ColorsSpacesLCH, 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(ColorsSpacesLCH, 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, 1.0, 1.0 }), + _P(inb, "OkLch", {-0.2, -0.2, -0.2, -0.2 }, "OkLch", { 0.0, 0.0, 0.0, 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 }) +)); + +TEST(ColorsSpacesLCH, randomConversion) +{ + // Isolate conversion functions + //EXPECT_TRUE(RandomPassFunc(Space::OkLch::toOkLab, Space::OkLch::toOkLab, 1000)); + + // Full stack conversion + //EXPECT_TRUE(RandomPassthrough("OkLch", "RGB", 1000)); +} + +TEST(ColorsSpacesLCH, 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..6ddcb0f86a --- /dev/null +++ b/testfiles/src/colors/spaces-rgb-test.cpp @@ -0,0 +1,93 @@ +// 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 "spaces-testbase.h" + +namespace { + +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 }) +)); + +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"); +} + +/*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..c280e4f98c --- /dev/null +++ b/testfiles/src/colors/spaces-testbase.h @@ -0,0 +1,371 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Shared test header for testing color spaces + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include + +#include "colors/color.h" +#include "colors/manager.h" +#include "colors/spaces/base.h" +#include "colors/spaces/components.h" + +using namespace Inkscape::Colors; + +namespace { + +/** + * Allow the correct tracing of the file and line where data came from. + */ +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(); +} + +#include + +/** + * 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() << " Diff " << 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(); +} + +/** + * Test that a color space actually exists, to catch test writing mistakes instead of crashing. + */ +void testSpaceName(std::string const &name) +{ + auto &manager = Manager::get(); + ASSERT_TRUE(manager.find(name)) << "Unknown Color Space '" << name << "'"; +} + +// 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 << "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(test.val); + 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(); + EXPECT_EQ(Hex(Color(test.val).toRGBA(true)), Hex(test.rgba)); +} + +class badColorString : public testing::TestWithParam {}; + +TEST_P(badColorString, returnsNone) { + EXPECT_FALSE(Color(GetParam())); +} + +/* ===== Out test ===== */ + +struct out : traced_data +{ + const std::string 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 inb : traced_data +{ + const std::string space_in; + const std::vector in; + const std::string space_out; + const std::vector out; + bool both_directions = true; + + Color do_conversion(bool inplace) const + { + auto result = Color(space_in, in); + if (inplace) { + result.convertInPlace(space_out); + return result; + } + return *result.convert(space_out); + } + + ::testing::AssertionResult forward_test(bool inplace) const + { + auto result = do_conversion(inplace); + return VectorIsNear(result.getValues(), out, 0.005); + } + + ::testing::AssertionResult backward_test(bool inplace) const + { + return inb{_file, _line, space_out, out, space_in, in}.forward_test(inplace); + } + + // Send the results back to be tested for a pass-through test + ::testing::AssertionResult through_test(bool inplace) const + { + auto result = do_conversion(inplace); + return inb{_file, _line, space_out, result.getValues(), space_in, in}.forward_test(inplace); + } +}; +void PrintTo(const inb& obj, std::ostream* oo) { + *oo << obj.space_in << print_values(obj.in); + *oo << "<->"; + *oo << 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)) << " " << test.space_in << " copy to " << test.space_out; + if (test.both_directions) { + EXPECT_TRUE(test.backward_test(false)) << " " << test.space_in << " copy from " << 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 " << test.space_in << " to " << test.space_out; + if (test.both_directions) { + EXPECT_TRUE(test.backward_test(true)) << " in place " << test.space_in << " from " << test.space_out; + } +} + +/** + * Generate a count of random doubles between 0 and 1. + * + * Randomly appends an extra value for optional opacity. + */ +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; +} + +/** + * 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(std::string const &from, std::string const &to, unsigned count = 1000) +{ + (void)&RandomPassthrough; // Avoid compile warning + std::srand(13375336); // We always seed for tests' repeatability + + testSpaceName(from); + testSpaceName(to); + + auto space = Manager::get().find(from); + if (!space) + return ::testing::AssertionFailure() << "can't find space " << from; + + auto ccount = space->getComponentCount(); + for (unsigned i = 0; i < count; i++) { + auto ret = inb{"", 0, from, random_values(ccount), to, {}}.through_test(true); + if (!ret) { + return ret << " | " << from << "->" << to; + } + } + 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.normalizeInPlace(); + 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..49cea27681 --- /dev/null +++ b/testfiles/src/colors/spaces-xyz-test.cpp @@ -0,0 +1,79 @@ +// 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 "spaces-testbase.h" + +#include "colors/spaces/xyz.h" + +namespace { + +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", { }, "", true), + _P(out, "XYZ", { }, "", false), + _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 }) +)); + +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/tracker-test.cpp b/testfiles/src/colors/tracker-test.cpp new file mode 100644 index 0000000000..8de76ea278 --- /dev/null +++ b/testfiles/src/colors/tracker-test.cpp @@ -0,0 +1,210 @@ +// 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/system.h" +#include "colors/color.h" +#include "colors/manager.h" +#include "colors/tracker.h" +#include "colors/spaces/base.h" +#include "colors/spaces/cms.h" +#include "colors/spaces/components.h" +#include "colors/spaces/enum.h" +#include "document.h" +#include "inkscape.h" +#include "object/color-profile.h" +#include "object/sp-root.h" + +static std::string icc_dir = INKSCAPE_TESTS_DIR "/data/"; +static std::string svg_file = INKSCAPE_TESTS_DIR "/data/color-cms.svg"; +static std::string grb_profile = INKSCAPE_TESTS_DIR "/data/SwappedRedAndGreen.icc"; +static std::string cmyk_profile = INKSCAPE_TESTS_DIR "/data/default_cmyk.icc"; +static std::string display_profile = INKSCAPE_TESTS_DIR "/data/display.icc"; + +using namespace Inkscape; +using namespace Inkscape::Colors; + +namespace { + +class ColorManagerDocTest : 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(svg_file.c_str(), false); + } + void TearDown() override + { + delete doc; + doc = nullptr; + } + + SPDocument *doc = nullptr; +}; + + +TEST_F(ColorManagerDocTest, loadDocument) +{ + auto &cm = Manager::get(); + + ASSERT_FALSE(cm.find("nonsense")); + + // Internal spaces + ASSERT_TRUE(cm.find("RGB")); + ASSERT_TRUE(cm.find("CSSNAME")); + ASSERT_TRUE(cm.find("HSL")); + + ASSERT_TRUE(cm.find(Space::Type::RGB)); + ASSERT_TRUE(cm.find(Space::Type::HSL)); + + // Inverse icc lookup + ASSERT_FALSE(cm.find("grb")); + ASSERT_TRUE(cm.find("RGB", doc)); + + // Document icc profiles + ASSERT_TRUE(cm.find("grb", doc)); + ASSERT_TRUE(cm.find("cmyk-rcm", doc)); + ASSERT_TRUE(cm.find("cmyk-acm", doc)); + + ASSERT_TRUE(cm.find(Space::Type::CMYK)); + ASSERT_EQ(cm.find(Space::Type::CMYK)->getName(), "CMYK"); + ASSERT_EQ(cm.find(Space::Type::RGB)->getName(), "RGB"); +} + +TEST_F(ColorManagerDocTest, updateIntent) +{ + auto &cm = Manager::get(); + auto space = cm.find("grb", doc); + ASSERT_TRUE(space); + + auto &tracker = doc->getColorTracker(); + auto cp = tracker.getColorProfileForSpace(space); + ASSERT_TRUE(cp); + + ASSERT_EQ(space->getIntent(), RenderingIntent::PERCEPTUAL); + ASSERT_EQ(cp->getRenderingIntent(), RenderingIntent::UNKNOWN); + ASSERT_EQ(cp->getAttribute("rendering-intent"), nullptr); + tracker.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 = cm.find("cmyk-acm", doc); + ASSERT_EQ(space->getIntent(), RenderingIntent::ABSOLUTE_COLORIMETRIC); + + space = cm.find("cmyk-rcm", doc); + ASSERT_EQ(space->getIntent(), RenderingIntent::RELATIVE_COLORIMETRIC); +} + +TEST_F(ColorManagerDocTest, createColorProfile) +{ + auto &cm = Manager::get(); + ASSERT_FALSE(cm.find("C.icc", doc)); + + auto &tracker = doc->getColorTracker(); + tracker.attachProfileToDoc("C.icc", ColorProfileStorage::LOCAL_ID, RenderingIntent::AUTO); + ASSERT_TRUE(cm.find("C.icc", doc)); + auto space = std::static_pointer_cast(cm.find("C.icc", doc)); + + ASSERT_TRUE(space); + ASSERT_EQ(space->getIntent(), RenderingIntent::AUTO); +} + +TEST_F(ColorManagerDocTest, deleteColorProfile) +{ + auto cp0 = doc->getObjectById("cp2"); + ASSERT_TRUE(cp0); + + auto &cm = Manager::get(); + ASSERT_TRUE(cm.find("cmyk-rcm", doc)); + auto &tracker = doc->getColorTracker(); + auto cp = tracker.getColorProfileForSpace("cmyk-rcm"); + ASSERT_TRUE(cp); + cp->deleteObject(); + ASSERT_FALSE(cm.find("cmyk-rcm", doc)); +} + +TEST_F(ColorManagerDocTest, cmsAddMultiple) +{ + auto &tracker = doc->getColorTracker(); + auto &cm = Manager::get(); + ASSERT_EQ(cm.find("grb", doc)->getType(), Space::Type::RGB); + EXPECT_THROW(tracker.addProfileURI(grb_profile, "grb", RenderingIntent::RELATIVE_COLORIMETRIC), ColorError); +} + +TEST_F(ColorManagerDocTest, cmsParsing) +{ + auto &cm = Manager::get(); + ASSERT_EQ(cm.find("grb", doc)->getType(), Space::Type::RGB); + ASSERT_EQ(cm.find("cmyk-rcm", doc)->getType(), Space::Type::CMYK); + ASSERT_EQ(cm.find("cmyk-acm", doc)->getType(), Space::Type::CMYK); + + + ASSERT_EQ(cm.parseColor("#000001 icc-color(grb, 1, 0.8, 0.6)", doc)->toString(), "#ccff99 icc-color(grb, 1, 0.8, 0.6)"); + ASSERT_EQ(cm.parseColor("icc-color(grb, 1.0, 0.8, 0.6)", doc)->toString(), "#ccff99 icc-color(grb, 1, 0.8, 0.6)"); + ASSERT_EQ(cm.parseColor("#000002 icc-color(cmyk-rcm, 0.5, 0, 0, 0)", doc)->toString(), + "#49ffff icc-color(cmyk-rcm, 0.5, 0, 0, 0)"); + ASSERT_EQ(cm.parseColor("#000003 icc-color(cmyk-acm, 0.5, 0, 0, 0)", doc)->toString(), + "#44ffff icc-color(cmyk-acm, 0.5, 0, 0, 0)"); + + ASSERT_EQ(cm.parseColor("icc-color(cmyk-acm, 1.0, 0.8, 0.6, 0.0)", doc)->toRGBA(), 0x002246ff); +} + +TEST_F(ColorManagerDocTest, applyConversion) +{ + auto &cm = Manager::get(); + + auto color = *cm.parseColor("red"); + ASSERT_EQ(color.toString(), "red"); + + // Converting an anonymous color fails + color.convertInPlace("grb"); + ASSERT_EQ(color.toString(), "red"); + ASSERT_FALSE(color.convert("grb")); + + // Specifying the space properly works + auto grb = cm.find("grb", doc); + ASSERT_EQ(color.convert(grb).toString(), "#ff0000 icc-color(grb, 0, 1, 0)"); + + // Double conversion does nothing + color.convertInPlace(grb); + color.convertInPlace("grb"); + ASSERT_EQ(color.toString(), "#ff0000 icc-color(grb, 0, 1, 0)"); + + // Once not anonymous, converting to other icc profiles is possible + ASSERT_EQ(color.convert("cmyk-rcm")->toString(), "#840021 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 + ASSERT_EQ(color.convert("cmyk-acm")->toString(), "#740006 icc-color(cmyk-acm, 0, 1, 1, 0)"); +} + +} // 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..5ff561deb3 --- /dev/null +++ b/testfiles/src/colors/utils-test.cpp @@ -0,0 +1,125 @@ +// 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" +#include "colors/manager.h" +#include "colors/utils.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("not-a-color")), "none"); + EXPECT_EQ(color_to_id(Color("red")), "css-red"); + EXPECT_EQ(color_to_id(Color("#0000ff")), "rgb-0000ff"); + + auto color = Color("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.convertInPlace("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_average_color_between) +{ + std::vector colors; + + colors = {Color("black"), Color("white")}; + EXPECT_EQ(average_color_between(colors).toString(), "gray"); + + colors = {Color("hsl(180,1,1)"), Color("hsla(60,0,0, 0.5)")}; + EXPECT_EQ(average_color_between(colors).toString(), "hsl(120, 0.5, 0.5)"); + + colors = {Color("hsl(180,1,1)"), Color("red"), Color("blue")}; + EXPECT_EQ(average_color_between(colors).toString(), "hsl(139, 1, 0.667)"); + + colors = {Color("device-cmyk(0.5 0.5 0.0 0.2 / 0.5)"), Color("red")}; + EXPECT_EQ(average_color_between(colors).toString(), "device-cmyk(0.25 0.75 0.5 0.1 / 75%)"); +} + +TEST(ColorUtils, test_average_color) +{ + EXPECT_EQ(average_color(Color("red"), Color("blue"), 0.0).toString(), "red"); + EXPECT_EQ(average_color(Color("red"), Color("blue"), 1.0).toString(), "blue"); + EXPECT_EQ(average_color(Color("red"), Color("blue"), 0.5).toString(), "purple"); + EXPECT_EQ(average_color(Color("red"), Color("blue"), 0.25).toString(), "#bf0040"); +} + +TEST(ColorUtils, test_make_contrasted_color) +{ + EXPECT_EQ(make_contrasted_color(Color("#000000"), 0.2).toString(), "#040404"); + EXPECT_EQ(make_contrasted_color(Color("#000000"), 0.4).toString(), "#080808"); + EXPECT_EQ(make_contrasted_color(Color("#000000"), 0.6).toString(), "#0c0c0c"); + EXPECT_EQ(make_contrasted_color(Color("#ffffff"), 0.2).toString(), "#fdfdfd"); + EXPECT_EQ(make_contrasted_color(Color("#ffffff"), 0.4).toString(), "#f9f9f9"); + EXPECT_EQ(make_contrasted_color(Color("#ffffff"), 0.6).toString(), "#f5f5f5"); + EXPECT_EQ(make_contrasted_color(Color("#a1a1a1"), 0.2).toString(), "#fdfdfd"); + EXPECT_EQ(make_contrasted_color(Color("#1a1a1a"), 0.4).toString(), "#f9f9f9"); + EXPECT_EQ(make_contrasted_color(Color("#808080"), 0.6).toString(), "#f5f5f5"); +} + +TEST(ColorUtils, test_get_perceptual_lightness) +{ + EXPECT_NEAR(get_perceptual_lightness(Color("red")), 0.780, 0.001); + EXPECT_NEAR(get_perceptual_lightness(Color("black")), 0.0, 0.001); + EXPECT_NEAR(get_perceptual_lightness(Color("white")), 1.0, 0.001); + EXPECT_NEAR(get_perceptual_lightness(Color("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); +} + +} // 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..06996a874a --- /dev/null +++ b/testfiles/src/colors/xml-color-test.cpp @@ -0,0 +1,122 @@ +// 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 + +#include "colors/color.h" +#include "colors/cms/profile.h" +#include "colors/manager.h" +#include "colors/spaces/cms.h" +#include "colors/xml-color.h" + +using namespace Inkscape; +using namespace Inkscape::Colors; + +static std::string cmyk_profile = INKSCAPE_TESTS_DIR "/data/default_cmyk.icc"; + +namespace { + +TEST(ColorXmlColor, test_color_to_xml_string) +{ + ASSERT_EQ(color_to_xml_string({}), R"( + + + +)"); + ASSERT_EQ(color_to_xml_string(Color("bad-color")), R"( + + + +)"); + ASSERT_EQ(color_to_xml_string(Color("#cf321244")), R"( + + + +)"); + ASSERT_EQ(color_to_xml_string(Color("hsl(180,1,1)")), R"( + + + +)"); +} + +TEST(ColorXmlColor, test_icc_color_xml) +{ + auto profile = CMS::Profile::create_from_uri(cmyk_profile); + auto space = std::static_pointer_cast(Manager::get().addSpace(new Colors::Space::CMS(profile))); + space->setIntent(RenderingIntent::AUTO); + std::vector vals = {0.5, 0.2, 0.1, 0.23}; + auto color = Color(space, vals); + auto str = color_to_xml_string(color); + + ASSERT_EQ(str, R"( + + + +)"); + + auto reverse = xml_string_to_color(str, nullptr); + ASSERT_EQ(reverse->toString(), color.toString()); +} + +TEST(ColorXmlColor, test_xml_string_to_color) +{ + ASSERT_FALSE(xml_string_to_color(R"( + + + +)", nullptr)); + ASSERT_EQ(xml_string_to_color(R"( + + + +)", nullptr)->toString(), "#cf321244"); + ASSERT_EQ(xml_string_to_color(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/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/svg-color-test.cpp b/testfiles/src/svg-color-test.cpp deleted file mode 100644 index cd44de1534..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 "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++) { - SPColor tmp; - char const *str = cases[i].str; - char const *result = nullptr; - - bool parseRet = tmp.read_icc_color(str, &result); - ASSERT_EQ(parseRet, cases[i].shouldPass) << str; - ASSERT_EQ(tmp.getColors().size(), cases[i].numEntries) << str; - if (cases[i].shouldPass) { - ASSERT_STRNE(str, result); - ASSERT_EQ(tmp.getColorProfile(), cases[i].name) << str; - } else { - ASSERT_STREQ(str, result); - ASSERT_TRUE(tmp.getColorProfile().empty()); - } - } -} - -// vim: filetype=cpp:expandtab:shiftwidth=4:softtabstop=4:fileencoding=utf-8:textwidth=99 : -- GitLab