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 0000000000000000000000000000000000000000..68f016f58dce13974ca4e097b6c20df5fc9f471e --- /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 0000000000000000000000000000000000000000..de5ccabdef01a3d198a529678a550e831264ae4b --- /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 0000000000000000000000000000000000000000..68f016f58dce13974ca4e097b6c20df5fc9f471e --- /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 0000000000000000000000000000000000000000..de5ccabdef01a3d198a529678a550e831264ae4b --- /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 0000000000000000000000000000000000000000..6caecaa4a462cbaf6a63efab3705029e767f9a69 --- /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 0000000000000000000000000000000000000000..db4631b7689354e3e7f650db50950ad0650acd6e --- /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 0000000000000000000000000000000000000000..68f016f58dce13974ca4e097b6c20df5fc9f471e --- /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 0000000000000000000000000000000000000000..1b2eccaf29dbc134f98b49347fb7c219e9424578 --- /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 0000000000000000000000000000000000000000..de5ccabdef01a3d198a529678a550e831264ae4b --- /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 0000000000000000000000000000000000000000..da39fe48477c9cef595bafc4b0c9a5f2ca133ad3 --- /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 0000000000000000000000000000000000000000..db4631b7689354e3e7f650db50950ad0650acd6e --- /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 74ef23e2f57299372e10ffd61f88293ab5dd009d..37027abbcf2e6a00fb6b742807da404c49452a05 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 0000000000000000000000000000000000000000..bc44a21462e1386f607cc0e1fdae405ad0862ae2 --- /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 0000000000000000000000000000000000000000..ac7ff65c7ee2605bead9aa70f9f5e91692e2f41f --- /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 0000000000000000000000000000000000000000..4ad7a3ff6e2097cdae3c14fa4726430d8f68f3dc --- /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 2cc7f1c6c7b81c45e00a82dc499f2b101b7ee680..523c4e73fddff1a2be97c5fa8e3b875e500de594 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 0000000000000000000000000000000000000000..d0f9a5e0bd0ccbac5ab758fe97bc0970da430534 --- /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 5822c5ef525e6531f924e905353e9cbf9a38aead..80755cba665c18fa462b2fc21e6cadadcb9be1a0 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 1a735b364344dbfc1119b617f400c3f2c48049fe..e09a096e5b9e75ecdf6ec00fd77bcf5ee2933f1f 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 @@ -307,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 5a37abad3b4d98ad850a3c700cb21838e893b288..978efa589f2b9a0384efc9436429dac4dbd80285 100644 --- a/src/actions/actions-canvas-mode.cpp +++ b/src/actions/actions-canvas-mode.cpp @@ -19,7 +19,9 @@ #include "actions-helper.h" +#include "colors/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,92 @@ 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.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)) { + // 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 { + // 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 +300,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 +341,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 2d171096d923b5053b7aa13f35af88f1826dccf1..85a99b20188ffe30bc470c25a7320342619c295f 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 942f147f0ee16199df33d76f0c063aaf68f0d4d7..e0c6419159e92d05e447786432b0505bf4ced65c 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 61ed6f1fca5e229b76e0078b0dc5da29a4fcb8ae..e436211bbe6a7af919dfaec62d2f9ca0a98ec04c 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 adafe6be0c041e9eb4f7ca24d9ba16c4f19ef48b..cff020ae001196a9b4f12e567225d6a116262e6a 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/color.cpp b/src/color.cpp index f516161a8a66fd023c156738f0e90c7f4912719d..8e8bb9bd992973f1157fec6037b8edcf4db04c75 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->name; - for (int i = 0; i < profile->getChannelCount(); i++) { - _icc.colors.emplace_back(-1.0); - } - } -} - -void SPColor::setColors(std::vector &&values) -{ - if (values.size() != _icc.colors.size()) { - g_error("Can't set profile-based color, wrong number of colors."); - unsetColors(); - return; - } - _icc.colors = std::move(values); + 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 9a3fa133ff10c5e52357bc9c9fa250cf491a4cc1..150e3489cb21042fd2604852af23ff311b0bca9a 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 "svg/svg-icc-color.h" - typedef unsigned int guint32; // uint is guaranteed to hold up to 2^32 − 1 /* Useful composition macros */ @@ -38,11 +37,9 @@ 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; - namespace Inkscape { class ColorProfile; -}; +} /** * An RGB color with optional icc-color part @@ -61,22 +58,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 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 +94,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 +115,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/CMakeLists.txt b/src/color/CMakeLists.txt deleted file mode 100644 index 3cdda4a9d8334543ad8344b55c158e7cc62b2e80..0000000000000000000000000000000000000000 --- a/src/color/CMakeLists.txt +++ /dev/null @@ -1,19 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0-or-later - -set(color_SRC - cms-system.cpp - cms-util.cpp - cmyk-conv.cpp - color-conv.cpp - - # ------- - # Headers - color-profile-cms-fns.h - cms-color-types.h - cms-system.h - cms-util.h - cmyk-conv.h - color-conv.h -) - -add_inkscape_source("${color_SRC}") diff --git a/src/color/cms-color-types.h b/src/color/cms-color-types.h deleted file mode 100644 index c6b90878b2f3f22c662bea5c098e0c746c2b5752..0000000000000000000000000000000000000000 --- 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 55372feb20bd9dffba8db8ce843cd864d0f025be..0000000000000000000000000000000000000000 --- a/src/color/cms-system.cpp +++ /dev/null @@ -1,469 +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 { - -/** - * Holds information about one ICC profile. - */ -class ProfileInfo -{ -public: - ProfileInfo(cmsHPROFILE prof, Glib::ustring &&path); - - Glib::ustring const &getName() const { return _name; } - Glib::ustring const &getPath() const { return _path; } - cmsColorSpaceSignature getSpace() const { return _profileSpace; } - cmsProfileClassSignature getClass() const { return _profileClass; } - -private: - Glib::ustring _path; - Glib::ustring _name; - cmsColorSpaceSignature _profileSpace; - cmsProfileClassSignature _profileClass; -}; - -ProfileInfo::ProfileInfo(cmsHPROFILE prof, Glib::ustring &&path) - : _path(std::move(path)) - , _name(get_color_profile_name(prof)) - , _profileSpace(cmsGetColorSpace(prof)) - , _profileClass(cmsGetDeviceClass(prof)) -{ -} - -CMSSystem::CMSSystem() -{ - // Read in profiles (move to refresh()?). - load_profiles(); - - // Create generic sRGB profile. - sRGB_profile = cmsCreate_sRGBProfile(); -} - -CMSSystem::~CMSSystem() -{ - if (current_monitor_profile) { - cmsCloseProfile(current_monitor_profile); - } - - if (current_proof_profile) { - cmsCloseProfile(current_proof_profile); - } - - if (sRGB_profile) { - cmsCloseProfile(sRGB_profile); - } -} - -/* - * We track last transform created so we can delete it later. - * - * This is OK since we only have one transform for all montiors/canvases. If we choose to allow the - * user to assign different profiles to different monitors or have CMS preferences that are not - * global, we'll need to have either one transform per monitor or one transform per canvas. - */ - -// Search for system ICC profile files and add them to list. -void CMSSystem::load_profiles() -{ - system_profile_infos.clear(); // Allows us to refresh list if necessary. - - // Get list of all possible file directories, with flag if they are "home" directories or not. - auto directory_paths = get_directory_paths(); - - // Look for icc files in specified directories. - for (auto const &directory_path : directory_paths) { - using Inkscape::IO::Resource::get_filenames; - for (auto &&filename : get_filenames(directory_path.first, {".icc", ".icm"})) { - // Check if files are ICC files and extract out basic information, add to list. - if (!is_icc_file(filename)) { - std::cerr << "CMSSystem::load_profiles: " << filename << " is not an ICC file!" << std::endl; - continue; - } - - cmsHPROFILE profile = cmsOpenProfileFromFile(filename.c_str(), "r"); - if (!profile) { - std::cerr << "CMSSystem::load_profiles: failed to load " << filename << std::endl; - continue; - } - - ICCProfileInfo info{profile, std::move(filename), directory_path.second}; - cmsCloseProfile(profile); - profile = nullptr; - - bool same_name = false; - for (auto const &profile_info : system_profile_infos) { - if (profile_info.get_name() == info.get_name() ) { - same_name = true; - std::cerr << "CMSSystem::load_profiles: ICC profile with duplicate name: " << profile_info.get_name() << ":" << std::endl; - std::cerr << " " << profile_info.get_path() << std::endl; - std::cerr << " " << info.get_path() << std::endl; - break; - } - } - - if (!same_name) { - system_profile_infos.emplace_back(std::move(info)); - } - } - } -} - -// Create list of all directories where ICC profiles are expected to be found. -std::vector> CMSSystem::get_directory_paths() -{ - std::vector> paths; - - // First try user's local directory. - paths.emplace_back(Glib::build_filename(Glib::get_user_data_dir(), "color", "icc"), true); - - // See https://github.com/hughsie/colord/blob/fe10f76536bb27614ced04e0ff944dc6fb4625c0/lib/colord/cd-icc-store.c#L590 - - // User store - paths.emplace_back(Glib::build_filename(Glib::get_user_data_dir(), "icc"), true); - - paths.emplace_back(Glib::build_filename(Glib::get_home_dir(), ".color", "icc"), true); - - // System store - paths.emplace_back("/var/lib/color/icc", false); - paths.emplace_back("/var/lib/colord/icc", false); - - auto data_directories = Glib::get_system_data_dirs(); - for (auto const &data_directory : data_directories) { - paths.emplace_back(Glib::build_filename(data_directory, "color", "icc"), false); - } - -#ifdef __APPLE__ - paths.emplace_back("/System/Library/ColorSync/Profiles", false); - paths.emplace_back("/Library/ColorSync/Profiles", false); - - paths.emplace_back(Glib::build_filename(Glib::get_home_dir(), "Library", "ColorSync", "Profiles"), true); -#endif // __APPLE__ - -#ifdef _WIN32 - wchar_t pathBuf[MAX_PATH + 1]; - pathBuf[0] = 0; - DWORD pathSize = sizeof(pathBuf); - g_assert(sizeof(wchar_t) == sizeof(gunichar2)); - if (GetColorDirectoryW(NULL, pathBuf, &pathSize)) { - auto utf8Path = g_utf16_to_utf8((gunichar2*)(&pathBuf[0]), -1, NULL, NULL, NULL); - if (!g_utf8_validate(utf8Path, -1, NULL)) { - g_warning( "GetColorDirectoryW() resulted in invalid UTF-8" ); - } else { - paths.emplace_back(utf8Path, false); - } - g_free(utf8Path); - } -#endif // _WIN32 - - return paths; -} - -// Get the user set monitor profile. -cmsHPROFILE CMSSystem::get_monitor_profile() -{ - static Glib::ustring current_monitor_uri; - static bool use_user_monitor_profile_old = false; - - Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - bool use_user_monitor_profile = prefs->getBool("/options/displayprofile/use_user_profile", false); - - if (use_user_monitor_profile_old != use_user_monitor_profile) { - use_user_monitor_profile_old = use_user_monitor_profile; - current_monitor_profile_changed = true; - } - - if (!use_user_monitor_profile) { - if (current_monitor_profile) { - cmsCloseProfile(current_monitor_profile); - current_monitor_profile = nullptr; - current_monitor_uri.clear(); - } - return current_monitor_profile; - } - - Glib::ustring new_uri = prefs->getString("/options/displayprofile/uri"); - - if (!new_uri.empty()) { - - // User defined monitor profile. - if (new_uri != current_monitor_uri) { - - // Monitor profile changed - current_monitor_profile_changed = true; - current_monitor_uri.clear(); - - // Delete old profile - if (current_monitor_profile) { - cmsCloseProfile(current_monitor_profile); - } - - // Open new profile - current_monitor_profile = cmsOpenProfileFromFile(new_uri.data(), "r"); - if (current_monitor_profile) { - - // A display profile must be of the right type. - cmsColorSpaceSignature space = cmsGetColorSpace(current_monitor_profile); - cmsProfileClassSignature profClass = cmsGetDeviceClass(current_monitor_profile); - - if (profClass != cmsSigDisplayClass) { - std::cerr << "CMSSystem::get_monitor_profile: Not a display (monitor) profile: " << new_uri << std::endl; - cmsCloseProfile(current_monitor_profile); - current_monitor_profile = nullptr; - } else if (space != cmsSigRgbData) { - std::cerr << "CMSSystem::get_monitor_profile: Not an RGB profile: " << new_uri << std::endl; - cmsCloseProfile(current_monitor_profile); - current_monitor_profile = nullptr; - } else { - current_monitor_uri = new_uri; - } - } - } - } else if (current_monitor_profile) { - cmsCloseProfile(current_monitor_profile); - current_monitor_profile = nullptr; - current_monitor_uri.clear(); - current_monitor_profile_changed = true; - } - - return current_monitor_profile; -} - -// Get the user set proof profile. -cmsHPROFILE CMSSystem::get_proof_profile() -{ - static Glib::ustring current_proof_uri; - - Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - Glib::ustring new_uri = prefs->getString("/options/softproof/uri"); - - if (!new_uri.empty()) { - - // User defined proof profile. - if (new_uri != current_proof_uri) { - - // Proof profile changed - current_proof_profile_changed = true; - current_proof_uri.clear(); - - // Delete old profile - if (current_proof_profile) { - cmsCloseProfile(current_proof_profile); - } - - // Open new profile - current_proof_profile = cmsOpenProfileFromFile(new_uri.data(), "r"); - if (current_proof_profile) { - - // We don't check validity of proof profile! - current_proof_uri = new_uri; - } - } - } else if (current_proof_profile) { - cmsCloseProfile(current_proof_profile); - current_proof_profile = nullptr; - current_proof_uri.clear(); - current_proof_profile_changed = true; - } - - return current_proof_profile; -} - -// Get a color profile handle corresponding to "name" from the document. Also, optionally, get intent. -cmsHPROFILE CMSSystem::get_document_profile(SPDocument *document, unsigned *intent, char const *name) -{ - cmsHPROFILE profile_handle = nullptr; - - // Search through elements for one with matching name. - ColorProfile *color_profile = nullptr; - auto color_profiles = document->getResourceList("iccprofile"); - for (auto *object : color_profiles) { - if (auto color_profile_test = cast(object)) { - if (color_profile_test->name && (strcmp(color_profile_test->name, name) == 0)) { - color_profile = color_profile_test; - } - } - } - - // If found, set profile_handle pointer. - if (color_profile) { - profile_handle = color_profile->getHandle(); - } - - // If requested, fill "RENDERING_INTENT" value. - if (intent) { - *intent = color_profile ? color_profile->rendering_intent : (guint)RENDERING_INTENT_UNKNOWN; - } - - return profile_handle; -} - -// Returns vector of names to list in Preferences dialog: display (monitor) profiles. -std::vector CMSSystem::get_monitor_profile_names() const -{ - std::vector result; - - for (auto const &profile_info : system_profile_infos) { - if (profile_info.get_profileclass() == cmsSigDisplayClass && - profile_info.get_colorspace() == cmsSigRgbData) - { - result.emplace_back(profile_info.get_name()); - } - } - std::sort(result.begin(), result.end()); - - return result; -} - -// Returns vector of names to list in Preferences dialog: proofing profiles. -std::vector CMSSystem::get_softproof_profile_names() const -{ - std::vector result; - - for (auto const &profile_info : system_profile_infos) { - if (profile_info.get_profileclass() == cmsSigOutputClass) { - result.emplace_back(profile_info.get_name()); - } - } - std::sort(result.begin(), result.end()); - - return result; -} - -// Returns location of a profile. -std::string CMSSystem::get_path_for_profile(Glib::ustring const &name) const -{ - std::string result; - - for (auto const &profile_info : system_profile_infos) { - if (name.raw() == profile_info.get_name()) { - result = profile_info.get_path(); - break; - } - } - - return result; -} - -// Static, doesn't rely on class. Simply calls lcms' cmsDoTransform. -// Called from Canvas and icc_color_to_sRGB in sgv-color.cpp. -void CMSSystem::do_transform(cmsHTRANSFORM transform, unsigned char *inBuf, unsigned char *outBuf, unsigned size) -{ - cmsDoTransform(transform, inBuf, outBuf, size); -} - -// Called by Canvas to obtain transform. -// Currently there is one transform for all monitors. -// Transform immutably shared between CMSSystem and Canvas. -std::shared_ptr const &CMSSystem::get_cms_transform() -{ - bool preferences_changed = false; - - Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - bool warn = prefs->getBool( "/options/softproof/gamutwarn"); - int intent = prefs->getIntLimited("/options/displayprofile/intent", 0, 0, 3); - int proofIntent = prefs->getIntLimited("/options/softproof/intent", 0, 0, 3); - bool bpc = prefs->getBool( "/options/softproof/bpc"); - Glib::ustring colorStr = prefs->getString( "/options/softproof/gamutcolor"); - Gdk::RGBA gamutColor(colorStr.empty() ? "#808080" : colorStr); - - if (gamutWarn != warn || - lastIntent != intent || - lastProofIntent != proofIntent || - lastBPC != bpc || - lastGamutColor != gamutColor ) - { - preferences_changed = true; - - gamutWarn = warn; - lastIntent = intent; - lastProofIntent = proofIntent; - lastBPC = bpc; - lastGamutColor = gamutColor; - } - - auto monitor_profile = get_monitor_profile(); - auto proof_profile = get_proof_profile(); - - bool need_to_update = preferences_changed || current_monitor_profile_changed || current_proof_profile_changed; - - if (need_to_update) { - if (proof_profile) { - cmsUInt32Number dwFlags = cmsFLAGS_SOFTPROOFING; - - if (gamutWarn) { - dwFlags |= cmsFLAGS_GAMUTCHECK; - - auto gamutColor_r = gamutColor.get_red_u(); - auto gamutColor_g = gamutColor.get_green_u(); - auto gamutColor_b = gamutColor.get_blue_u(); - - cmsUInt16Number newAlarmCodes[cmsMAXCHANNELS] = {0}; - newAlarmCodes[0] = gamutColor_r; - newAlarmCodes[1] = gamutColor_g; - newAlarmCodes[2] = gamutColor_b; - newAlarmCodes[3] = ~0; - cmsSetAlarmCodes(newAlarmCodes); - } - - if (bpc) { - dwFlags |= cmsFLAGS_BLACKPOINTCOMPENSATION; - } - - current_transform = CMSTransform::create( - cmsCreateProofingTransform(sRGB_profile, TYPE_BGRA_8, monitor_profile, TYPE_BGRA_8, - proof_profile, intent, proofIntent, dwFlags)); - - } else if (monitor_profile) { - current_transform = CMSTransform::create( - cmsCreateTransform(sRGB_profile, TYPE_BGRA_8, monitor_profile, TYPE_BGRA_8, intent, 0)); - } - } - - return current_transform; -} - -CMSSystem *CMSSystem::_instance = nullptr; - -} // namespace Inkscape - -/* - Local Variables: - mode:c++ - c-file-style:"stroustrup" - c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) - indent-tabs-mode:nil - fill-column:99 - End: -*/ -// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/color/cms-system.h b/src/color/cms-system.h deleted file mode 100644 index a040146eca851417dbb7455d10cfff8fe75e9027..0000000000000000000000000000000000000000 --- a/src/color/cms-system.h +++ /dev/null @@ -1,132 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/** @file - * TODO: insert short description here - *//* - * Authors: see git history - * - * Copyright (C) 2017 Authors - * Released under GNU GPL v2+, read the file 'COPYING' for more information. - */ -#ifndef INKSCAPE_COLOR_CMS_SYSTEM_H -#define INKSCAPE_COLOR_CMS_SYSTEM_H - -/** \file - * Access to ICC profiles provided by system. - * Track which profile to use for proofing.(?) - * Track which profile to use on which monitor. - */ - -#include -#include -#include - -#include -#include - -#include // cmsHTRANSFORM - -#include "cms-color-types.h" // cmsColorSpaceSignature, cmsProfileClassSignature -#include "cms-util.h" - -class SPDocument; -class ProfileInfo; - -namespace Inkscape { - -class ColorProfile; - -class CMSTransform -{ -public: - explicit CMSTransform(cmsHTRANSFORM handle) : _handle(handle) { assert(_handle); } - CMSTransform(CMSTransform const &) = delete; - CMSTransform &operator=(CMSTransform const &) = delete; - ~CMSTransform() { cmsDeleteTransform(_handle); } - - cmsHTRANSFORM getHandle() const { return _handle; } - - static std::shared_ptr create(cmsHTRANSFORM handle) - { - return handle ? std::make_shared(handle) : nullptr; - } - -private: - cmsHTRANSFORM _handle; -}; - -class CMSSystem -{ -public: - /** - * Access the singleton CMSSystem object. - */ - static CMSSystem *get() { - if (!_instance) { - _instance = new CMSSystem(); - } - return _instance; - } - - static void unload() { - if (_instance) { - delete _instance; - _instance = nullptr; - } - } - - static std::vector> get_directory_paths(); - std::vector const &get_system_profile_infos() const { return system_profile_infos; } - std::vector get_monitor_profile_names() const; - std::vector get_softproof_profile_names() const; - std::string get_path_for_profile(Glib::ustring const &name) const; - std::shared_ptr const &get_cms_transform(); - static cmsHPROFILE get_document_profile(SPDocument *document, unsigned *intent, char const *name); - - static void do_transform(cmsHTRANSFORM transform, unsigned char *inBuf, unsigned char *outBuf, unsigned size); - -private: - CMSSystem(); - ~CMSSystem(); - - void load_profiles(); // Should this be public (e.g., if a new ColorProfile is created). - cmsHPROFILE get_monitor_profile(); // Get the user set monitor profile. - cmsHPROFILE get_proof_profile(); // Get the user set proof profile. - void clear_transform(); // Clears current_transform. - - static CMSSystem* _instance; - - // List of ICC profiles on system - std::vector system_profile_infos; - - // We track last transform settings. If there is a change, we delete create new transform. - bool gamutWarn = false; - Gdk::RGBA lastGamutColor = Gdk::RGBA("#808080"); - bool lastBPC = false; - int lastIntent = INTENT_PERCEPTUAL; - int lastProofIntent = INTENT_PERCEPTUAL; - bool current_monitor_profile_changed = true; // Force at least one update. - bool current_proof_profile_changed = true; - - // Shared immutably with all canvases. - std::shared_ptr current_transform; - - // So we can delete them later. - cmsHPROFILE current_monitor_profile = nullptr; - cmsHPROFILE current_proof_profile = nullptr; - cmsHPROFILE sRGB_profile = nullptr; // Genric sRGB profile, find it once on inititialization. -}; - -} // namespace Inkscape - -#endif // INKSCAPE_COLOR_CMS_SYSTEM_H - -/* - Local Variables: - mode:c++ - c-file-style:"stroustrup" - c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) - indent-tabs-mode:nil - fill-column:99 - End: -*/ -// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/color/cms-util.cpp b/src/color/cms-util.cpp deleted file mode 100644 index 36546820b3aa366094deb53a1c86bb11552bec43..0000000000000000000000000000000000000000 --- a/src/color/cms-util.cpp +++ /dev/null @@ -1,123 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/** @file - * Utilities to for working with ICC profiles. Used by CMSSystem and ColorProfile classes. - *//* - * Authors: see git history - * - * Copyright (C) 2018 Authors - * Released under GNU GPL v2+, read the file 'COPYING' for more information. - */ - -#include -#include // Debug output -#include -#include // File open flags -#include // Posix read, close. - -#include -#include - -#include "cms-util.h" - -namespace Inkscape { - -ICCProfileInfo::ICCProfileInfo(cmsHPROFILE profile, std::string &&path, bool in_home) - : _path(std::move(path)) - , _in_home(in_home) -{ - assert(profile); - - _name = get_color_profile_name(profile); - _colorspace = cmsGetColorSpace(profile); - _profileclass = cmsGetDeviceClass(profile); -} - -bool is_icc_file(std::string const &filepath) -{ - bool is_icc_file = false; - GStatBuf st; - if (g_stat(filepath.c_str(), &st) == 0 && st.st_size > 128) { - // 0-3 == size - // 36-39 == 'acsp' 0x61637370 - int fd = g_open(filepath.c_str(), O_RDONLY, S_IRWXU); - if (fd != -1) { - guchar scratch[40] = {0}; - size_t len = sizeof(scratch); - - ssize_t got = read(fd, scratch, len); - if (got != -1) { - size_t calcSize = - (scratch[0] << 24) | - (scratch[1] << 16) | - (scratch[2] << 8) | - (scratch[3]); - if ( calcSize > 128 && calcSize <= static_cast(st.st_size) ) { - is_icc_file = - (scratch[36] == 'a') && - (scratch[37] == 'c') && - (scratch[38] == 's') && - (scratch[39] == 'p'); - } - } - close(fd); - - if (is_icc_file) { - cmsHPROFILE profile = cmsOpenProfileFromFile(filepath.c_str(), "r"); - if (profile) { - cmsProfileClassSignature profClass = cmsGetDeviceClass(profile); - if (profClass == cmsSigNamedColorClass) { - is_icc_file = false; // Ignore named color profiles for now. - } - cmsCloseProfile(profile); - } - } - } - } - - return is_icc_file; -} - -std::string get_color_profile_name(cmsHPROFILE profile) -{ - std::string name; - - if (profile) { - cmsUInt32Number byteLen = cmsGetProfileInfoASCII(profile, cmsInfoDescription, "en", "US", nullptr, 0); - if (byteLen > 0) { - std::vector data(byteLen); - cmsUInt32Number readLen = cmsGetProfileInfoASCII(profile, cmsInfoDescription, - "en", "US", - data.data(), data.size()); - if (readLen < data.size()) { - std::cerr << "get_color_profile_name(): read less than expected!" << std::endl; - data.resize(readLen); - } - - // Remove nulls at end which will cause an invalid utf8 string. - while (!data.empty() && data.back() == 0) { - data.pop_back(); - } - - name = std::string(data.begin(), data.end()); - } - - if (name.empty()) { - name = _("(Unnamed)"); - } - } - - return name; -} - -} // namespace Inkscape - -/* - Local Variables: - mode:c++ - c-file-style:"stroustrup" - c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) - indent-tabs-mode:nil - fill-column:99 - End: -*/ -// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/color/cms-util.h b/src/color/cms-util.h deleted file mode 100644 index e9262d9fa6842deda3b9a201cd830e06ca2fdd1f..0000000000000000000000000000000000000000 --- a/src/color/cms-util.h +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/** @file - * Utilities to for working with ICC profiles. Used by CMSSystem and ColorProfile classes. - *//* - * Authors: see git history - * - * Copyright (C) 2018 Authors - * Released under GNU GPL v2+, read the file 'COPYING' for more information. - */ - -#ifndef INKSCAPE_COLOR_CMS_UTIL_H -#define INKSCAPE_COLOR_CMS_UTIL_H - -#include - -#include - -namespace Inkscape { - -// Helper class to store info -class ICCProfileInfo -{ -public: - ICCProfileInfo(cmsHPROFILE profile, std::string &&path, bool in_home); - bool operator<(ICCProfileInfo const &other) const; - std::string const &get_path() const { return _path; } - std::string const &get_name() const { return _name; } - cmsColorSpaceSignature get_colorspace() const { return _colorspace; } - cmsProfileClassSignature get_profileclass() const { return _profileclass; } - bool in_home() const { return _in_home; } - -private: - std::string _path; - std::string _name; - bool _in_home; - cmsColorSpaceSignature _colorspace; - cmsProfileClassSignature _profileclass; -}; - -bool is_icc_file(std::string const &filepath); -std::string get_color_profile_name(cmsHPROFILE profile); // Read as ASCII from profile. - -} // namespace Inkscape - -#endif // INKSCAPE_COLOR_CMS_UTIL_H - -/* - Local Variables: - mode:c++ - c-file-style:"stroustrup" - c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) - indent-tabs-mode:nil - fill-column:99 - End: -*/ -// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/color/cmyk-conv.cpp b/src/color/cmyk-conv.cpp deleted file mode 100644 index e565bd37f9ed19d8329ab2055f024d9a8103fb12..0000000000000000000000000000000000000000 --- 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 524c9bddf238a12979ff7de004d65cd694f021ff..0000000000000000000000000000000000000000 --- 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 8ce3bcbac4b2cf93fefe7cc34e21377783868348..0000000000000000000000000000000000000000 --- 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 0b87704c6d2d1f1864c56caae70e9b02e9cdc584..0000000000000000000000000000000000000000 --- 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 06258c73e13f312c172ad3ca867dca07e9e9e746..0000000000000000000000000000000000000000 --- 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/colors/CMakeLists.txt b/src/colors/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..3265f5a7f038b3cbf19eebf86533683315d24775 --- /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 0000000000000000000000000000000000000000..618cf76789d12add7e3c3f878f52459a185e955e --- /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 0000000000000000000000000000000000000000..ee83246f4ef5390507398a23b596302a6aa54f8a --- /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 0000000000000000000000000000000000000000..d2c236c46871bfe7e77a724dc240833c2dc4ac27 --- /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 0000000000000000000000000000000000000000..0a8c1640edb64a841ba5151148aeb218e0fc34f2 --- /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 0000000000000000000000000000000000000000..a66e5c3ef96f17af9a8b5246561caf1044db1475 --- /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 0000000000000000000000000000000000000000..071c2d9718fdc884fe3be8bebef38b4407dc3d4f --- /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 0000000000000000000000000000000000000000..51fccc58b21f14cbd3f1e5aee06d4833f70d66b2 --- /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 0000000000000000000000000000000000000000..aed9586668b8bace4fc9a8bd4c1fd037ff1b2802 --- /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 0000000000000000000000000000000000000000..b3ad42e1043731e269fd6e2af3d33c1208235898 --- /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 0000000000000000000000000000000000000000..7eca534e1e2da2a18d9c3f50341a8696cf11ac77 --- /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 0000000000000000000000000000000000000000..194a7869ea6e685db8662a53c7e4d6793f6e2503 --- /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 0000000000000000000000000000000000000000..c57d53dcce01a7442b919e9eb77e5c78b92eca2c --- /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 0000000000000000000000000000000000000000..9b690c386b3accd30ba8f0cebfb97f8514d5082c --- /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 0000000000000000000000000000000000000000..73f950ddfaf4e193cf1b15da8b07a8a7f77f7323 --- /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 0000000000000000000000000000000000000000..fa0a88f37bf78dc8b26ad4f98ebf2250289b20a0 --- /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 0000000000000000000000000000000000000000..9d9511b0d7831ff80b6e43dd2a1009a7bd291666 --- /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 0000000000000000000000000000000000000000..74cc28de6494e4a5a24cffecd6dd216287f86e26 --- /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 0000000000000000000000000000000000000000..ecff70b68c328c0ae3271a4a8ae54acaf7ef7ee2 --- /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 0000000000000000000000000000000000000000..61760ab0a248d29229bdfe75bd88cabbbf752466 --- /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 0000000000000000000000000000000000000000..42677f342e2662e65497c03151c7fe619fcfb2a9 --- /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 0000000000000000000000000000000000000000..72449495baa787863f9d7192a66ee7d656572e11 --- /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 0000000000000000000000000000000000000000..f88dba04dfa109bc77bc50ac9758caa866313f07 --- /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 0000000000000000000000000000000000000000..900f0910bf413cd8351a1b3d3974a990b611b824 --- /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/colors/spaces/components.h b/src/colors/spaces/components.h new file mode 100644 index 0000000000000000000000000000000000000000..8bde3677c632b1fd33e0fa668d94ba0ee9b60a93 --- /dev/null +++ b/src/colors/spaces/components.h @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Jon A. Cruz + * Martin Owens + * + * Copyright (C) 2013-2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_COLORS_COMPONENTS_H +#define SEEN_COLORS_COMPONENTS_H + +#include +#include +#include +#include + +namespace Inkscape::Colors::Space { + +enum class Type; + +struct Component +{ + Component() = delete; + ~Component() = default; + + 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(Type space); + +} // namespace Inkscape::Colors::Space + +#endif // SEEN_COLORS_COMPONENTS_H diff --git a/src/colors/spaces/enum.h b/src/colors/spaces/enum.h new file mode 100644 index 0000000000000000000000000000000000000000..aa3be4c5e22fcb478fa495aa98ec20a1fe95e943 --- /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 0000000000000000000000000000000000000000..55db2f23fd9ba601374b26cec6a3593d86883174 --- /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 0000000000000000000000000000000000000000..9c6df24208088a1f2428d780507bd11bd82b42c7 --- /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 0000000000000000000000000000000000000000..6174baf84ba10dd012cc4bbd8eaeac4009b5d831 --- /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 0000000000000000000000000000000000000000..b3a6f7c0f9f3bc735d227a0cc54236a197e42815 --- /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 0000000000000000000000000000000000000000..b6a82eee0b1d1297db463b62d0512574212f6976 --- /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 0000000000000000000000000000000000000000..d98c8a225cad7946e4040d35e74467bba29a4928 --- /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 0000000000000000000000000000000000000000..6d3468f1fc365ecfff0087d5e898bc6d1cd040a6 --- /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 0000000000000000000000000000000000000000..38bf3b31799e6f366af0947a244c583ebf654456 --- /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 0000000000000000000000000000000000000000..6a33cc9455ebc8b368ff53337f9bc0c4ce9c827d --- /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 0000000000000000000000000000000000000000..abc68cc5e75955fc694b9b2bd257f7992bf70316 --- /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 0000000000000000000000000000000000000000..c4dd62e5bdb268bd2a608053883109db5a5749d0 --- /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 0000000000000000000000000000000000000000..56b83d4d6b30ee90bb5eb9279ac4696099f51ede --- /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 0000000000000000000000000000000000000000..4d3916dce1a2646111a08ee8a2337255afaba248 --- /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 0000000000000000000000000000000000000000..47fd48ccd70d8b3c94b2c234bf65a6bad7d83cea --- /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 0000000000000000000000000000000000000000..404f184833eb05729027cceb66d4883bcf9220da --- /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 0000000000000000000000000000000000000000..c49b5366e3e67e4d30e2222727e9c0ee75a1e6ac --- /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 0000000000000000000000000000000000000000..e1f5fdadc49fdd84d580396df4fb381a1dcb3854 --- /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 0000000000000000000000000000000000000000..d39f7f315c18a518555748d2b6ad3351a2a4f80e --- /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 0000000000000000000000000000000000000000..aa6671ee192422a4e62e0ec92eb85629fdc52a08 --- /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 0000000000000000000000000000000000000000..36d67df874c37bc256fe760c1b73a9dfa38d600a --- /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 0000000000000000000000000000000000000000..2c704808d8914e6c93316bbcb10600306adb9be6 --- /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 0000000000000000000000000000000000000000..8903ddca04284eccc00a0152b6418e02c07561b5 --- /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 0000000000000000000000000000000000000000..11c3e16f7366c4ec0434aee3d313c05641af8d01 --- /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 0000000000000000000000000000000000000000..c4ecd9cc4f1267bb239da35f721471afc7c25349 --- /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 0000000000000000000000000000000000000000..19150c8e8ceb88cf760d5a0743be24659a558775 --- /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 0000000000000000000000000000000000000000..e454585c0a93b005d0ccad5794830f64ac1c463e --- /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 0000000000000000000000000000000000000000..e723777eea1d65d9bdd15411167bfa47682a9416 --- /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 0000000000000000000000000000000000000000..a297f00c9416ad0d15e48e8abb831210e7e7e72e --- /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 0000000000000000000000000000000000000000..4e2a921ba34bb0aa085dc51732aeb0853e91fca7 --- /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 0000000000000000000000000000000000000000..56ad50a349524fc0036d10cf40e0ed287cc7142e --- /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 0000000000000000000000000000000000000000..fe8fc79e6c822e90ef7c4be1e276c575c61759ff --- /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 0000000000000000000000000000000000000000..2b7cb38c4ef8b2ccaa8d499f5fd24ff8ab889c25 --- /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/colorspace.h b/src/colorspace.h deleted file mode 100644 index 1903fe026920db5c9d4b9929106e81c72f9965c6..0000000000000000000000000000000000000000 --- 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 47c4eab6bb151f584c3f6217ef91acbd0b18daec..831032dadf7cda43af4c3be28f07475ca64e403d 100644 --- a/src/desktop.cpp +++ b/src/desktop.cpp @@ -31,6 +31,7 @@ #include "desktop.h" #include "color.h" +#include "colors/tracker.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->getColorTracker().connectChanged([this]() { + update_canvas_cms(); + }); + /*_profiles_modified_connection = document->getColorTracker().connectModified([this](std::shared_ptr) { + update_canvas_cms(); + });*/ + update_canvas_cms(); + sp_namedview_update_layers_from_document(this); _document_replaced_signal.emit (this, doc); @@ -1548,6 +1557,20 @@ void SPDesktop::on_zoom_end(GtkGesture const * /*zoom*/, GdkEventSequence const _begin_zoom.reset(); } +void SPDesktop::update_canvas_cms() +{ + //if (_default_color_space != cs) { + //_default_color_space = cs; + /*std::shared_ptr colorproof; + std::shared_ptr gamutwarn; + if (cs) { + 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 a690ad62088d73d20fb5ace53a67fd916e276d42..cb879298b972e624fe2fbb15a4f9dd3c483d894b 100644 --- a/src/desktop.h +++ b/src/desktop.h @@ -169,6 +169,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 +341,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; diff --git a/src/display/cairo-utils.cpp b/src/display/cairo-utils.cpp index e2b5bd620cd47da9a073502f337617603d4e592b..dd7d97294649b818bd1ca456297ca97e9090381e 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-item.cpp b/src/display/drawing-item.cpp index ef66affaedaa77a00cbf60769934e71aab2d0267..44c71e8f8955e1cb474276aa89df39c5e8789884 100644 --- a/src/display/drawing-item.cpp +++ b/src/display/drawing-item.cpp @@ -12,6 +12,8 @@ #include +#include "colors/cms/transform.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()) + cms_profile->do_transform(ict.rawTarget(), ict.rawTarget()); + break; + case ColorMode::GAMUTWARN: + if (auto cms_profile = _drawing.getGamutWarnTransform()) + cms_profile->do_transform(ict.rawTarget(), ict.rawTarget()); + break; + default: + break; + } } // 5. Render object inside the composited mask + clip diff --git a/src/display/drawing-paintserver.cpp b/src/display/drawing-paintserver.cpp index e018d8b9402f557268b9ef253a8cb6353cd4c0ce..66cbc7069409fd0ddb7c62c998b11b90a1cc066f 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/drawing.cpp b/src/display/drawing.cpp index 0377c99a8cde1b98fe4719bed6cb82a3b5caa6d2..8cc5ac79b1f95d47440fc5fd0af7df4bc6130aa6 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 895cac4bc830ba6d7d381d84a7efd30ec1dd5682..88a2fff9b8bc72830c3efb5138001920d620a665 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 "colors/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/nr-filter-diffuselighting.cpp b/src/display/nr-filter-diffuselighting.cpp index 8f80998ba96085aada46fd1ea6ecc523da3461e1..6905023ed9a57b7244f55b19f10e1267229fca58 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 69577b5d14deb918187fa93767d6b5691c10dc80..a50d90939ae67578f1c88aac98699eced40138b5 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 cc7fcb2ce6fe12aa8e57b7a6163931a872ae966e..5802c500b4eb6f89e38d7a52dd91f65a3c2048a7 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 2228e007873da2d8e5a48afe4a79f5e654482a97..31504bbf91f12640dce45802f5a9d0089c4ce803 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 64402bb15176ffa6c4b9d008c759bd54dc4460ce..0cdd071da19f3cd848e1d2efd8251ddb21c8fc4e 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 fdc7eb9a976dda07183b8d8e62e945879eb63678..43d7e0167b31a9330187e2980688653af7a24e35 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 8d21061aef33f6fe8bc499a6679b001d6ed287b4..d1f911eefe2f4a617760d4be58527d822a60c490 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/display/rendermode.h b/src/display/rendermode.h index dec852c435a4a9435b196be6cc98ba662db85b77..b82dcc60799b758623bf7d2fa3cd37e24ed8700e 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 4980186ba1e04d72171b6d9f1924027ded7b784a..464b09ca1796cad92f8d7a5485d2acff0defa5b1 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 "colors/tracker.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); + _color_tracker = 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 f6f4ee06c0d25e5dc554ef865624a9e22f745ef8..c4188222efe8e2e54ca5ad4eb495d4defdda1fa9 100644 --- a/src/document.h +++ b/src/document.h @@ -85,7 +85,9 @@ namespace Inkscape { class Event; class EventLog; class PageManager; - class ProfileManager; + namespace Colors { + class Tracker; + } class Selection; class UndoStackObserver; namespace XML { @@ -176,12 +178,15 @@ public: Inkscape::PageManager& getPageManager() { return *_page_manager; } const Inkscape::PageManager& getPageManager() const { return *_page_manager; } + Inkscape::Colors::Tracker &getColorTracker() { return *_color_tracker; } + const Inkscape::Colors::Tracker &getColorTracker() const { return *_color_tracker; } private: void _importDefsNode(SPDocument *source, Inkscape::XML::Node *defs, Inkscape::XML::Node *target_defs); SPObject *_activexmltree; std::unique_ptr _page_manager; + std::unique_ptr _color_tracker; std::queue pending_resource_changes; @@ -193,9 +198,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 +375,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 b1e7be6bf4ba18c084a83681235aa384f4d42d52..e4bc3fc11f9543e805f871c2bf0f94ca572f5aa4 100644 --- a/src/extension/internal/pdfinput/svg-builder.cpp +++ b/src/extension/internal/pdfinput/svg-builder.cpp @@ -30,15 +30,15 @@ #include "Page.h" #include "Stream.h" #include "color.h" +#include "colors/manager.h" #include "document.h" #include "extract-uri.h" #include "pdf-parser.h" #include "pdf-utils.h" #include "png.h" #include "poppler-cairo-font-engine.h" -#include "profile-manager.h" -#include "color/cms-util.h" +#include "colors/cms/profile.h" #include "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->getProfileManager().find(name.c_str())) + 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 10c4bd95ec8a16a578f9ed186507896c43e81b6c..9368fd5f600f9b9ed5f21d78ca2c958be358e297 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 "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" @@ -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->getColorTracker().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 c53974b854e1aea8b5739bc0679ee1536a5f9180..ba9bb93e81188b7becac162ea418afb1b5412019 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/inkscape.cpp b/src/inkscape.cpp index 24075dbc67aac77a3a898173075512cbbfd31522..a796b6e61203561aa4edd1b420d553f5e73bc580 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 4a8e2b2892038b71c8f1be919815e9c137b02329..1ea2bb0383e9832ce5e6722423a9f7d063a01b32 100644 --- a/src/object/color-profile.cpp +++ b/src/object/color-profile.cpp @@ -11,302 +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; - cmsProfileClassSignature _profileClass; - cmsColorSpaceSignature _profileSpace; - cmsHTRANSFORM _transf; - cmsHTRANSFORM _revTransf; - cmsHTRANSFORM _gamutTransf; +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), - _gamutTransf(nullptr) -{ -} - - -cmsHPROFILE ColorProfileImpl::_sRGBProf = nullptr; - -cmsHPROFILE ColorProfileImpl::getSRGBProfile() { - if ( !_sRGBProf ) { - _sRGBProf = cmsCreate_sRGBProfile(); - } - return ColorProfileImpl::_sRGBProf; -} - -cmsHPROFILE ColorProfileImpl::_NullProf = nullptr; - -cmsHPROFILE ColorProfileImpl::getNULLProfile() { - if ( !_NullProf ) { - _NullProf = cmsCreateNULLProfile(); - } - return _NullProf; -} - -ColorProfile::ColorProfile() : SPObject() { - this->impl = new ColorProfileImpl(); - - this->href = nullptr; - this->local = nullptr; - this->name = nullptr; - this->intentStr = nullptr; - this->rendering_intent = Inkscape::RENDERING_INTENT_UNKNOWN; -} - -ColorProfile::~ColorProfile() = default; - -bool ColorProfile::operator<(ColorProfile const &other) const { - gchar *a_name_casefold = g_utf8_casefold(this->name, -1 ); - gchar *b_name_casefold = g_utf8_casefold(other.name, -1 ); - int result = g_strcmp0(a_name_casefold, b_name_casefold); - g_free(a_name_casefold); - g_free(b_name_casefold); - return result < 0; -} - -/** - * Callback: free object - */ void ColorProfile::release() { // Unregister ourselves if ( this->document ) { this->document->removeResource("iccprofile", this); } - if ( this->href ) { - g_free( this->href ); - this->href = nullptr; - } - - if ( this->local ) { - g_free( this->local ); - this->local = nullptr; - } - - if ( this->name ) { - g_free( this->name ); - this->name = nullptr; - } - - if ( this->intentStr ) { - g_free( this->intentStr ); - this->intentStr = nullptr; - } - - this->impl->_clearProfile(); - - delete this->impl; - this->impl = nullptr; - SPObject::release(); } -void ColorProfileImpl::_clearProfile() -{ - _profileSpace = cmsSigRgbData; - - if ( _transf ) { - cmsDeleteTransform( _transf ); - _transf = nullptr; - } - if ( _revTransf ) { - cmsDeleteTransform( _revTransf ); - _revTransf = nullptr; - } - if ( _gamutTransf ) { - cmsDeleteTransform( _gamutTransf ); - _gamutTransf = nullptr; - } - if ( _profHandle ) { - cmsCloseProfile( _profHandle ); - _profHandle = nullptr; - } -} - /** * Callback: set attributes from associated repr. */ void ColorProfile::build(SPDocument *document, Inkscape::XML::Node *repr) { - g_assert(this->href == nullptr); - g_assert(this->local == nullptr); - g_assert(this->name == nullptr); - g_assert(this->intentStr == nullptr); - SPObject::build(document, repr); this->readAttr(SPAttr::XLINK_HREF); - this->readAttr(SPAttr::ID); this->readAttr(SPAttr::LOCAL); this->readAttr(SPAttr::NAME); this->readAttr(SPAttr::RENDERING_INTENT); // Register - if ( document ) { - document->addResource( "iccprofile", this ); + if (document) { + document->addResource("iccprofile", this); } } - /** * Callback: set attribute. */ void ColorProfile::set(SPAttr key, gchar const *value) { switch (key) { case SPAttr::XLINK_HREF: - if ( this->href ) { - g_free( this->href ); - this->href = nullptr; - } - if ( value ) { - this->href = g_strdup( value ); - if ( *this->href ) { - - // TODO open filename and URIs properly - //FILE* fp = fopen_utf8name( filename, "r" ); - //LCMSAPI cmsHPROFILE LCMSEXPORT cmsOpenProfileFromMem(LPVOID MemPtr, cmsUInt32Number dwSize); - - // Try to open relative - SPDocument *doc = this->document; - if (!doc) { - doc = SP_ACTIVE_DOCUMENT; - g_warning("this has no document. using active"); - } - //# 1. Get complete filename of document - gchar const *docbase = doc->getDocumentFilename(); - - Inkscape::URI docUri(""); - if (docbase) { // The file has already been saved - docUri = Inkscape::URI::from_native_filename(docbase); - } - - this->impl->_clearProfile(); - - try { - auto hrefUri = Inkscape::URI(this->href, docUri); - auto contents = hrefUri.getContents(); - this->impl->_profHandle = cmsOpenProfileFromMem(contents.data(), contents.size()); - } catch (...) { - g_warning("Failed to open CMS profile URI '%.100s'", this->href); - } - - if ( this->impl->_profHandle ) { - this->impl->_profileSpace = cmsGetColorSpace( this->impl->_profHandle ); - this->impl->_profileClass = cmsGetDeviceClass( this->impl->_profHandle ); - } - } + // Href is the filename or the data of the icc profile itself and is used before local + if (value) { + auto fn = document->getDocumentFilename(); + uri = std::make_unique(value, fn ? ("file://" + std::string(fn)).c_str() : nullptr); + } else { + uri.reset(); } - this->requestModified(SP_OBJECT_MODIFIED_FLAG); break; case SPAttr::LOCAL: - if ( this->local ) { - g_free( this->local ); - this->local = nullptr; - } - this->local = g_strdup( value ); - this->requestModified(SP_OBJECT_MODIFIED_FLAG); + // Local is the ID of the profile as a hex string. Provided by Colors::CMS::Profile::getId() + // it's only used if the href isn't set or isn't found on this system in the specified place + local = value ? value : ""; break; case SPAttr::NAME: - if ( this->name ) { - g_free( this->name ); - this->name = nullptr; - } - this->name = g_strdup( value ); - this->requestModified(SP_OBJECT_MODIFIED_FLAG); + // Name is used by the icc-color format to match this profile to a color. It over-rides the + // name given in the icc profile if it's provided. + name = value ? value : ""; break; case SPAttr::RENDERING_INTENT: - if ( this->intentStr ) { - g_free( this->intentStr ); - this->intentStr = nullptr; - } - this->intentStr = g_strdup( value ); - - if ( value ) { - if ( strcmp( value, "auto" ) == 0 ) { - this->rendering_intent = RENDERING_INTENT_AUTO; - } else if ( strcmp( value, "perceptual" ) == 0 ) { - this->rendering_intent = RENDERING_INTENT_PERCEPTUAL; - } else if ( strcmp( value, "relative-colorimetric" ) == 0 ) { - this->rendering_intent = RENDERING_INTENT_RELATIVE_COLORIMETRIC; - } else if ( strcmp( value, "saturation" ) == 0 ) { - this->rendering_intent = RENDERING_INTENT_SATURATION; - } else if ( strcmp( value, "absolute-colorimetric" ) == 0 ) { - this->rendering_intent = RENDERING_INTENT_ABSOLUTE_COLORIMETRIC; - } else { - this->rendering_intent = RENDERING_INTENT_UNKNOWN; + // There is a standard set of rendering intents, the default fallback intent is decided in the + // color CMS system and not here. + intent = Colors::RenderingIntent::UNKNOWN; + if (value) { + for (auto &pair : intentIds) { + if (pair.second == value) { + intent = pair.first; + break; + } } - } else { - this->rendering_intent = RENDERING_INTENT_UNKNOWN; } - - this->requestModified(SP_OBJECT_MODIFIED_FLAG); break; default: - SPObject::set(key, value); - break; + return SPObject::set(key, value); } + this->requestModified(SP_OBJECT_MODIFIED_FLAG); } /** @@ -317,174 +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) || this->local ) { - repr->setAttribute( "local", this->local ); - } - - if ( (flags & SP_OBJECT_WRITE_ALL) || this->name ) { - repr->setAttribute( "name", this->name ); + if ((flags & SP_OBJECT_WRITE_ALL) || uri) { + auto fn = document->getDocumentFilename(); + Inkscape::setHrefAttribute(*repr, fn ? uri->str(("file://" + std::string(fn)).c_str()) : nullptr); } - if ( (flags & SP_OBJECT_WRITE_ALL) || this->intentStr ) { - repr->setAttribute( "rendering-intent", this->intentStr ); - } + repr->setAttributeOrRemoveIfEmpty("local", local); + repr->setAttributeOrRemoveIfEmpty("name", name); + repr->setAttributeOrRemoveIfEmpty("rendering-intent", intentIds[intent]); SPObject::write(xml_doc, repr, flags); - return repr; } - -struct MapMap { - cmsColorSpaceSignature space; - cmsUInt32Number inForm; -}; - -cmsUInt32Number ColorProfileImpl::_getInputFormat( cmsColorSpaceSignature space ) +/** + * Return the profile data, if any. Returns empty string if none + * is available. + */ +std::string ColorProfile::getProfileData() const { - MapMap possible[] = { - {cmsSigXYZData, TYPE_XYZ_16}, - {cmsSigLabData, TYPE_Lab_16}, - //cmsSigLuvData - {cmsSigYCbCrData, TYPE_YCbCr_16}, - {cmsSigYxyData, TYPE_Yxy_16}, - {cmsSigRgbData, TYPE_RGB_16}, - {cmsSigGrayData, TYPE_GRAY_16}, - {cmsSigHsvData, TYPE_HSV_16}, - {cmsSigHlsData, TYPE_HLS_16}, - {cmsSigCmykData, TYPE_CMYK_16}, - {cmsSigCmyData, TYPE_CMY_16}, - }; - - int index = 0; - for ( guint i = 0; i < G_N_ELEMENTS(possible); i++ ) { - if ( possible[i].space == space ) { - index = i; - break; + // Note: The returned data could be Megabytes in length, but we're + // copying the data. We should find a way to pass the const string back + if (uri) { + try { + return uri->getContents(); + } catch (const Gio::Error &e) { + g_warning("Couldn't get color profile: %s", e.what().c_str()); } } - - return possible[index].inForm; -} - -static int getLcmsIntent( guint svgIntent ) -{ - int intent = INTENT_PERCEPTUAL; - switch ( svgIntent ) { - case Inkscape::RENDERING_INTENT_RELATIVE_COLORIMETRIC: - intent = INTENT_RELATIVE_COLORIMETRIC; - break; - case Inkscape::RENDERING_INTENT_SATURATION: - intent = INTENT_SATURATION; - break; - case Inkscape::RENDERING_INTENT_ABSOLUTE_COLORIMETRIC: - intent = INTENT_ABSOLUTE_COLORIMETRIC; - break; - case Inkscape::RENDERING_INTENT_PERCEPTUAL: - case Inkscape::RENDERING_INTENT_UNKNOWN: - case Inkscape::RENDERING_INTENT_AUTO: - default: - intent = INTENT_PERCEPTUAL; - } - return intent; -} - -Inkscape::ColorSpaceSig ColorProfile::getColorSpace() const { - return ColorSpaceSigWrapper(impl->_profileSpace); + return ""; } -Inkscape::ColorProfileClassSig ColorProfile::getProfileClass() const { - return ColorProfileClassSigWrapper(impl->_profileClass); -} - -cmsHTRANSFORM ColorProfile::getTransfToSRGB8() +/** + * Set the rendering intent for this color profile. + */ +void ColorProfile::setRenderingIntent(Colors::RenderingIntent intent) { - if ( !impl->_transf && impl->_profHandle ) { - int intent = getLcmsIntent(rendering_intent); - impl->_transf = cmsCreateTransform( impl->_profHandle, ColorProfileImpl::_getInputFormat(impl->_profileSpace), ColorProfileImpl::getSRGBProfile(), TYPE_RGBA_8, intent, 0 ); - } - return impl->_transf; + setAttribute("rendering-intent", intentIds[intent]); } -cmsHTRANSFORM ColorProfile::getTransfFromSRGB8() +/** + * Create a profile for the given profile in the given document. + * + * @args doc - The SPDocument to add this profile into, creating a new color profile element in it's defs. + * @args profile - The color profile object to use as the data source + * @args name - The name to use, this over-rides the name in the profile + * @args storage - This sets the prefered data source. + * - HREF_DATA - The profile is embeded as a base64 encoded stream. + * - HREF_FILE - The href is a relative or absolute link to the icc profile file. + * the profile MUST be a file. If the document has a file and the path is close + * to the icc profile, it will be relative. + * - LOCAL_ID - The profile's unique id will be stored, no href will be added. + */ +ColorProfile *ColorProfile::createFromProfile(SPDocument *doc, + Colors::CMS::Profile const &profile, + std::string const &name, + ColorProfileStorage storage) { - if ( !impl->_revTransf && impl->_profHandle ) { - int intent = getLcmsIntent(rendering_intent); - impl->_revTransf = cmsCreateTransform( ColorProfileImpl::getSRGBProfile(), TYPE_RGBA_8, impl->_profHandle, ColorProfileImpl::_getInputFormat(impl->_profileSpace), intent, 0 ); + if (name.empty()) { + g_error("Refusing to create a color profile with an empty name!"); + return nullptr; } - return impl->_revTransf; -} - -cmsHTRANSFORM ColorProfile::getTransfGamutCheck() -{ - if ( !impl->_gamutTransf ) { - impl->_gamutTransf = cmsCreateProofingTransform(ColorProfileImpl::getSRGBProfile(), - TYPE_BGRA_8, - ColorProfileImpl::getNULLProfile(), - TYPE_GRAY_8, - impl->_profHandle, - INTENT_RELATIVE_COLORIMETRIC, - INTENT_RELATIVE_COLORIMETRIC, - (cmsFLAGS_GAMUTCHECK | cmsFLAGS_SOFTPROOFING)); + if (storage == ColorProfileStorage::HREF_FILE && profile.getPath().empty()) { + storage = ColorProfileStorage::HREF_DATA; // fallback to data } - return impl->_gamutTransf; -} - -// Check if a particular color is out of gamut. -bool ColorProfile::GamutCheck(SPColor color) -{ - guint32 val = color.toRGBA32(0); - cmsUInt16Number oldAlarmCodes[cmsMAXCHANNELS] = {0}; - cmsGetAlarmCodes(oldAlarmCodes); - cmsUInt16Number newAlarmCodes[cmsMAXCHANNELS] = {0}; - newAlarmCodes[0] = ~0; - cmsSetAlarmCodes(newAlarmCodes); + // Create new object and attach it to the document + auto repr = doc->getReprDoc()->createElement("svg:color-profile"); - cmsUInt8Number outofgamut = 0; - guchar check_color[4] = { - static_cast(SP_RGBA32_R_U(val)), - static_cast(SP_RGBA32_G_U(val)), - static_cast(SP_RGBA32_B_U(val)), - 255}; + // It's expected that the color manager will hace checked for collisions before this call. + repr->setAttributeOrRemoveIfEmpty("name", name); - cmsHTRANSFORM gamutCheck = ColorProfile::getTransfGamutCheck(); - if (gamutCheck) { - cmsDoTransform(gamutCheck, &check_color, &outofgamut, 1); + switch (storage) { + case ColorProfileStorage::LOCAL_ID: + repr->setAttributeOrRemoveIfEmpty("local", profile.getId()); + break; + case ColorProfileStorage::HREF_DATA: + Inkscape::setHrefAttribute(*repr, "data:application/vnd.iccprofile;base64," + profile.dumpBase64()); + break; + case ColorProfileStorage::HREF_FILE: + { + auto uri = Inkscape::URI::from_native_filename(profile.getPath().c_str()); + auto fn = doc->getDocumentFilename(); + Inkscape::setHrefAttribute(*repr, fn ? (uri.str((std::string("file://") + fn).c_str())) : nullptr); + } + break; } - - cmsSetAlarmCodes(oldAlarmCodes); - - return (outofgamut != 0); -} - -gint ColorProfile::getChannelCount() const -{ - return cmsChannelsOf(asICColorSpaceSig(getColorSpace())); -} - -bool ColorProfile::isPrintColorSpace() -{ - ColorSpaceSigWrapper colorspace = getColorSpace(); - return (colorspace == cmsSigCmykData) || (colorspace == cmsSigCmyData); -} - -cmsHPROFILE ColorProfile::getHandle() -{ - return impl->_profHandle; -} - -void errorHandlerCB(cmsContext /*contextID*/, cmsUInt32Number errorCode, char const *errorText) -{ - g_message("lcms: Error %d", errorCode); - g_message(" %p", errorText); - //g_message("lcms: Error %d; %s", errorCode, errorText); + // Complete the creation by appending to the defs. This must be done last. + return cast(doc->getDefs()->appendChildRepr(repr)); } +} // namespace Inkscape /* Local Variables: diff --git a/src/object/color-profile.h b/src/object/color-profile.h index 0e93e04ae33e2f13e1241ca7012e8b8ce4ff0295..21b3cb74b501cd497c6a1511dcbd922035530f3d 100644 --- a/src/object/color-profile.h +++ b/src/object/color-profile.h @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /** @file - * TODO: insert short description here + * SPObject of the color-profile object found a direct child of defs. *//* * Authors: see git history * @@ -10,72 +10,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/cms-color-types.h" -struct SPColor; +#include "colors/spaces/enum.h" // RenderingIntent +#include "colors/cms/system.h" namespace Inkscape { -enum { - RENDERING_INTENT_UNKNOWN = 0, - RENDERING_INTENT_AUTO = 1, - RENDERING_INTENT_PERCEPTUAL = 2, - RENDERING_INTENT_RELATIVE_COLORIMETRIC = 3, - RENDERING_INTENT_SATURATION = 4, - RENDERING_INTENT_ABSOLUTE_COLORIMETRIC = 5 -}; - -class ColorProfileImpl; +class URI; +enum class ColorProfileStorage { + HREF_DATA, + HREF_FILE, + LOCAL_ID, +}; -/** - * Color Profile. - */ class ColorProfile final : public SPObject { public: - ColorProfile(); - ~ColorProfile() override; + ColorProfile() = default; + ~ColorProfile() override = default; int tag() const override { return tag_of; } - bool operator<(ColorProfile const &other) const; - - ColorSpaceSig getColorSpace() const; - ColorProfileClassSig getProfileClass() const; - cmsHTRANSFORM getTransfToSRGB8(); - cmsHTRANSFORM getTransfFromSRGB8(); - cmsHTRANSFORM getTransfGamutCheck(); - bool GamutCheck(SPColor color); - int getChannelCount() const; - bool isPrintColorSpace(); - cmsHPROFILE getHandle(); - + static ColorProfile *createFromProfile(SPDocument *doc, + Colors::CMS::Profile const &profile, + std::string const &name, + ColorProfileStorage storage); - // 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; } + std::string getLocalProfileId() const { return local; } + std::string getProfileData() const; + Colors::RenderingIntent getRenderingIntent() const { return intent; } + // This is the only variable we expect inkscape to modify. Changing the icc + // profile data or ID should instead involve creating a new ColorProfile element. + void setRenderingIntent(Colors::RenderingIntent intent); protected: - ColorProfileImpl *impl; - void build(SPDocument* doc, Inkscape::XML::Node* repr) override; void release() override; void set(SPAttr key, char const* value) override; Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; +private: + std::string name; + std::string local; + Colors::RenderingIntent intent; + + std::unique_ptr uri; }; } // namespace Inkscape diff --git a/src/object/filters/diffuselighting.cpp b/src/object/filters/diffuselighting.cpp index 4d462b2fc65ac2ddeaf8c24e6c7fda507004b887..0b19b8ea3911ec79d3816806a3077b1f258bc582 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 221b38553bda5a80539e7e5d62bf51838bf4eaeb..6f897520b67757a1774d99b9ed9347599e49c827 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 2bae7ca9ab425e639e404e38dce111d2287f0ab0..267d9e59102bcdf9a2f5fcbaf6837950f00fb149 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 99cc4cfc76e2636a1d18150db264a53c46756a84..a2d3917b6487f7403d2ef907cb6c97e2144346d6 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 10a9d61fdd456571fa63985184b4102cab7c7333..965ad6ba3a40de65dd1fd0e21aeb4a115528bb63 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 34d6c36c510d30995681d807af02d87c9a828a47..87594ff82ce8cab4de996867a502031c906e2f59 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 1a81abda07c0647255bb4ce4a43a92d07b44819f..dc690aeb6b23a089b6be0f8c974fd8307675c2e0 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-image.cpp b/src/object/sp-image.cpp index c4f6d0a8f8717930c1f6be7418f8ed65e26833fc..fd4001584298c850ef44c3932643a8ed02831c65 100644 --- a/src/object/sp-image.cpp +++ b/src/object/sp-image.cpp @@ -24,7 +24,6 @@ #include #include -#include #include <2geom/rect.h> #include <2geom/transforms.h> @@ -42,8 +41,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 @@ -248,58 +247,25 @@ 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 = Inkscape::Colors::Manager::get().find(color_profile, document)) { + /*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." ); - } + }*/ } else { DEBUG_MESSAGE( lcmsEight, "in 's sp_image_update. No profile found." ); } diff --git a/src/object/sp-mesh-array.cpp b/src/object/sp-mesh-array.cpp index e040e98382ac021bf75c657eefe0f3148d3dd3d9..614f6cfedeffdde9bcf76baefe05b7150456823f 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 4bbc455534f9d3b31b12031223916363d9512dc1..a51af32585d0d1e35bc60ca75cf7c805ebe63c3f 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-object.cpp b/src/object/sp-object.cpp index 3c7db06371d7964013b0a91cd5abe83db7b20eb0..b37e3da0ea29979d77feb6e2c0c496f90e3fa8dd 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 1e2d07a7dca85038459c1829b820c1415867d1aa..362e0df24a13389be8919116d51a850406914036 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/object/sp-solid-color.cpp b/src/object/sp-solid-color.cpp index b7d520a3573618a9dd6a27b096dcdf79b68bb78f..d0fabe0716ccddaaf6fbc2265a0f955fd22d9b6b 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/preferences-skeleton.h b/src/preferences-skeleton.h index 1f525419e2279e13ca800107d3b0915eb95cdd44..f53325047cbf8e13e050f57895b738721f0a6e19 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 b092980ec8d780707c860658b998bc4de27af415..0000000000000000000000000000000000000000 --- 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 d9ad037e0988c73f1a5a1728111c0850f0ee0ccc..0000000000000000000000000000000000000000 --- 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/style-internal.cpp b/src/style-internal.cpp index fd948d46a1087eb4378ce442f53229afab2e3eba..04687a6aa73aad2e5f7fdec748d80edbc974b8e7 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 6c2687328017478103a19f775ce220d553aaf4ed..391e7a652a1f5d1fd5914775eef12f3b861ff1ba 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 6a66a6291a7532ac7149081c50a12f6b876301b8..818a233a04a9dc063c9291d9e08b3d8af3296ca5 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 66ae587c12d0000e2c4a45cf0bf983dbfb7eb4ca..bf558aafc37c43af0b688be80b1e9ff9b7e7eeec 100644 --- a/src/svg/svg-color.cpp +++ b/src/svg/svg-color.cpp @@ -24,21 +24,17 @@ #include #include #include -#include -#include #include // g_assert #include "color.h" -#include "colorspace.h" +#include "colors/manager.h" #include "document.h" #include "inkscape.h" #include "preferences.h" -#include "profile-manager.h" #include "strneq.h" -#include "svg-icc-color.h" -#include "color/cms-system.h" +#include "colors/cms/system.h" #include "object/color-profile.h" struct SPSVGColor { @@ -504,141 +500,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->getProfileManager().find(icc->colorProfile.c_str())) { - guchar color_out[4] = {0,0,0,0}; - cmsHTRANSFORM trans = prof->getTransfToSRGB8(); - if ( trans ) { - std::vector comps = colorspace::getColorSpaceInfo( prof ); - - size_t count = prof->getChannelCount(); - size_t cap = std::min(count, comps.size()); - guchar color_in[4]; - for (size_t i = 0; i < cap; i++) { - color_in[i] = static_cast((((gdouble)icc->colors[i]) * 256.0) * (gdouble)comps[i].scale); - g_message("input[%d]: %d", (int)i, (int)color_in[i]); - } - - Inkscape::CMSSystem::do_transform( trans, color_in, color_out, 1 ); - g_message("transform to sRGB done"); - } - *r = color_out[0]; - *g = color_out[1]; - *b = color_out[2]; - } - } -} - -/* - * Some discussion at http://markmail.org/message/bhfvdfptt25kgtmj - * Allowed ASCII first characters: ':', 'A'-'Z', '_', 'a'-'z' - * Allowed ASCII remaining chars add: '-', '.', '0'-'9', - */ -bool sp_svg_read_icc_color( gchar const *str, gchar const **end_ptr, SVGICCColor* dest ) -{ - bool good = true; - - if ( end_ptr ) { - *end_ptr = str; - } - if ( dest ) { - dest->colorProfile.clear(); - dest->colors.clear(); - } - - if ( !str ) { - // invalid input - good = false; - } else { - while ( g_ascii_isspace(*str) ) { - str++; - } - - good = strneq( str, "icc-color(", 10 ); - - if ( good ) { - str += 10; - while ( g_ascii_isspace(*str) ) { - str++; - } - - if ( !g_ascii_isalpha(*str) - && ( !(0x080 & *str) ) - && (*str != '_') - && (*str != ':') ) { - // Name must start with a certain type of character - good = false; - } else { - while ( g_ascii_isdigit(*str) || g_ascii_isalpha(*str) - || (*str == '-') || (*str == ':') || (*str == '_') || (*str == '.') ) { - if ( dest ) { - dest->colorProfile += *str; - } - str++; - } - while ( g_ascii_isspace(*str) || *str == ',' ) { - str++; - } - } - } - - if ( good ) { - while ( *str && *str != ')' ) { - if ( g_ascii_isdigit(*str) || *str == '.' || *str == '-' || *str == '+') { - gchar* endPtr = nullptr; - gdouble dbl = g_ascii_strtod( str, &endPtr ); - if ( !errno ) { - if ( dest ) { - dest->colors.push_back( dbl ); - } - str = endPtr; - } else { - good = false; - break; - } - - while ( g_ascii_isspace(*str) || *str == ',' ) { - str++; - } - } else { - break; - } - } - } - - // We need to have ended on a closing parenthesis - if ( good ) { - while ( g_ascii_isspace(*str) ) { - str++; - } - good &= (*str == ')'); - } - } - - if ( good ) { - if ( end_ptr ) { - *end_ptr = str; - } - } else { - if ( dest ) { - dest->colorProfile.clear(); - dest->colors.clear(); - } - } - - return good; -} - - -bool sp_svg_read_icc_color( gchar const *str, SVGICCColor* dest ) -{ - return sp_svg_read_icc_color(str, nullptr, dest); -} - /** * Reading inkscape colors, for things like namedviews, guides, etc. * Non-CSS / SVG specification formatted. Usually just a number. diff --git a/src/svg/svg-color.h b/src/svg/svg-color.h index b2c2f797375d59dd77f42968cbcb2287ab517b5e..56bbf662d7fd470063252356e801455905d0fabd 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 abd6918a8196883f2dc946d30de417d3380510ac..0000000000000000000000000000000000000000 --- 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/CMakeLists.txt b/src/ui/CMakeLists.txt index b53a042f1fa7d5d7881b6e42eccbbc9e50e36dac..a1d1abb2748e724b49868d1c6168d1f9a1b1535d 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 19dfb38f5d1d3fc9234e8740c754503d5cc7527d..96de32e8f2dcd2d6a87cece44663c49831e6903c 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 007c3d8407529904a5fa38ed904190aceb5fb630..6dea2ee45147ace37c6929a94ffd82029c86ea1e 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 e824bf985d7d4a3be2cf6ef01bc3f0d501233f24..7ca99a1acda8e0dbd23a9104eb3a4dda00152e05 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 0cd9b5095542fa344f6b42ee08e4931b3fc3f9e6..1c700808553d11b646f2c8a23e3c83ef4d205fb7 100644 --- a/src/ui/dialog/document-properties.cpp +++ b/src/ui/dialog/document-properties.cpp @@ -41,13 +41,17 @@ #include "page-manager.h" #include "selection.h" -#include "color/cms-system.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" #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 +167,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 +204,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 +219,11 @@ DocumentProperties::DocumentProperties() show_all_children(); } +void DocumentProperties::showPage(int page) +{ + _notebook.set_current_page(page); +} + //======================================================================== /** @@ -601,290 +603,145 @@ 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; - - row = *(_AvailableProfilesListStore->append()); - row[_AvailableProfilesListColumns.fileColumn] = info.get_path(); - row[_AvailableProfilesListColumns.nameColumn] = info.get_name(); - row[_AvailableProfilesListColumns.separatorColumn] = false; - } -} - -/** - * 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; - - 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, "-"); + 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); + + auto &profile_box = get_widget(_builder, "color-profile"); + auto &intent_box = get_widget(_builder, "rendering-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()){ + auto &cm = document->getColorManager(); + // This also removes uses of the color profile from shapes + if (auto prev = cm.defaultSpace()) { + //cm.removeProfile(prev); + } + // Default intent if needed, update combo because we're signal blocked + auto intent_id = intent_box.get_active_id(); + if (intent_id.empty()) { + intent_id = "relative-colorimetric"; } - } - } - 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; - } - Inkscape::XML::Document *xml_doc = document->getReprDoc(); - Inkscape::XML::Node *cprofRepr = xml_doc->createElement("svg:color-profile"); - std::string nameStr = name.empty() ? "profile" : name; // TODO add some auto-numbering to avoid collisions - sanitizeName(nameStr); - cprofRepr->setAttribute("name", nameStr); - cprofRepr->setAttribute("xlink:href", Glib::filename_to_uri(Glib::filename_from_utf8(file))); - cprofRepr->setAttribute("id", file); - - // Checks whether there is a defs element. Creates it when needed - Inkscape::XML::Node *defsRepr = sp_repr_lookup_name(xml_doc, "svg:defs"); - if (!defsRepr) { - defsRepr = xml_doc->createElement("svg:defs"); - xml_doc->root()->addChild(defsRepr, nullptr); - } - g_assert(document->getDefs()); - defsRepr->addChild(cprofRepr, nullptr); + //auto space = cm.find(profile_box.get_active_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_box.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_box.signal_changed().connect([this, &intent_box]() { + _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().defaultSpace()) { + //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 &tracker = document->getColorTracker(); -void DocumentProperties::removeSelectedProfile(){ - Glib::ustring name; - if(_LinkedProfilesList.get_selection()) { - Gtk::TreeModel::iterator i = _LinkedProfilesList.get_selection()->get_selected(); + _cms_profiles_changed_connection = tracker.connectChanged([this, document]() { + update_cms(document); + }); + _cms_profiles_modified_connection = tracker.connectModified([this](std::shared_ptr space) { + //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(); - - _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); + 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(); - _unlink_btn.signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::removeSelectedProfile)); + colors->clear(); + others->clear(); - _LinkedProfilesList.get_selection()->signal_changed().connect( sigc::mem_fun(*this, &DocumentProperties::onColorProfileSelectRow) ); + auto row = *(colors->append()); + row[ColorColumns().name] = _("Standard SVG (sRGB)"); + profile.set_active(0); + intent.set_sensitive(false); - connect_remove_popup_menu(_LinkedProfilesList, sigc::mem_fun(*this, &DocumentProperties::removeSelectedProfile)); + auto profiles = Inkscape::Colors::CMS::System::get().getOutputProfiles(); + for (auto const &profile : profiles) { + auto row = *(colors->append()); + row[ColorColumns().name] = profile->getName(); + } - if (auto document = getDocument()) { - std::vector current = document->getResourceList( "defs" ); - if (!current.empty()) { - _emb_profiles_observer.set((*(current.begin()))->parent); + for (auto cm : document->getColorManager()) { + 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 intent_id = std::string("none"); //ColorProfile::renderingIntentId(cm->getRenderingIntent()); + if (false) { //cm->isDefault()) { + if (intent_id.empty()) + intent_id = "auto"; + profile.set_active_id(name); + 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 cccecb382a3fd6027ccf214b43217f012a26077a..d0ba8c662d0fabe334056c67ebb99f9949a5000d 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/export.cpp b/src/ui/dialog/export.cpp index a55897ac59a93c8e38d073a93394aa2926d695e9..a6f227cc32a5fac8392fb79ec2dee5b526397f56 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 8e427b3932ae2005198cdab5dd260e1d3b6ccb56..baa8dad701df2cf9d10d48825d5711b77da97dd5 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 708d438948a0edbcbda12a5527eb0926f3e07f1a..70c1f83a14e3b998a480fb5612383dbcced42378 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; @@ -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(); @@ -2208,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 proofComboChanged( Gtk::ComboBoxText* combo ) +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/softproof/uri", path); + if ( !path.empty() ) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString("/options/cms/uri", path); + } } } @@ -2242,7 +2245,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,14 +2376,14 @@ 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")); + 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; } @@ -2391,25 +2394,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); - - _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); + _page_cms.add_group_header( _("Device profile")); - 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,28 +2408,22 @@ 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(); + auto profiles = cms_system.getDisplayProfiles(); 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 ); - 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++; @@ -2447,14 +2432,13 @@ void InkscapePreferences::initPageIO() _cms_display_profile.set_active(0); } - names = cms_system->get_softproof_profile_names(); - current = prefs->getString("/options/softproof/uri"); + profiles = cms_system.getOutputProfiles(); + current = prefs->getString("/options/cms/uri"); index = 0; - for (auto const &name : names) { - _cms_proof_profile.append( name ); - Glib::ustring path = cms_system->get_path_for_profile(name); - if ( !path.empty() && path == current ) { - _cms_proof_profile.set_active(index); + for (auto const &profile : profiles) { + _cms_default_profile.append(profile->getName()); + if (!current.empty() && profile->getPath() == current) { + _cms_default_profile.set_active(index); } index++; } @@ -2463,7 +2447,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 +3801,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 +3875,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 41babd679dacb95616b1952a3a0757e8cce4af07..956b2ca5d202dd8f7b9719902976d1071a97be1c 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 b45ec5581704d18bc3919482652767fe7def5c36..ccbf7c83a99475bf64af9fe9d5a445f361cf9951 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) { @@ -122,6 +129,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 4554b11778225086f64c4b8cc8d9040613131109..7d76e334c813ae863d4b0293d47a112fd7ff96c3 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 b43976f0cccf68f132f60b604442cd8f0c31e661..48651f7f1467a5f277834c5668745eefb55b0e14 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/util.cpp b/src/ui/util.cpp index 069bd862cbdf944ab3031b7566561150a860a1a5..9065d8830cfac5215018192e6e1debad2b0ab0e6 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 08e689f0f4c5b3477c69470572ed8d7a633b3e93..319cbf105887c3ca8d36ad53c3397780641175d5 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 46360b1602a3f978be1ed0b7a1d381f320ae2e2b..cbf7a633ef39719f18b9773e19fd6c5d955b2e5c 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,22 +121,18 @@ CanvasGrid::CanvasGrid(SPDesktopWidget *dtw) _vscrollbar.set_name("CanvasScrollbar"); _vscrollbar.set_vexpand(true); - // CMS Adjust (To be replaced by Gio::Action) - _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 Adjust + _cms_popover = Inkscape::UI::create_builder("cms-popover.glade"); + _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 - _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")); @@ -142,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); @@ -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() @@ -290,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); } } @@ -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 ae13f7ffd26f5444f7e116913cb445fbac89f06a..30fc32caa6b1cc85ef228071cad2764f13711e05 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,9 +116,10 @@ private: std::unique_ptr _vruler; Gtk::ToggleButton _guide_lock; - Gtk::ToggleButton _cms_adjust; + Gtk::MenuButton _cms_actions; 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 312956f2c4afb0104bd55d30b6d45efd7b0f7477..440cfe88fd364dc4674203e7e6263018834fbe42 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 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 */ ); + // XXX _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) { + 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 09b47fee57d3693e0bdba51d7f0d5193b11d4731..278d3195414e780355ef7a08bb8c7aeee61ffd9e 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,10 +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); - - // 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 +187,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 9f81c6b221bbb000130338022eec1cc98f5ea5de..befa7519d88bd585ae2c80e442d0bbc9a09be258 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 0000000000000000000000000000000000000000..7584f72a04cb31e322fcf58e742ec96a3be61f06 --- /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 "colors/tracker.h" +#include "desktop.h" +#include "desktop-widget.h" +#include "document.h" + +#include "ui/builder-utils.h" +#include "ui/icon-loader.h" +#include "ui/util.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())) { + apply_cms_icon(img, type, to_guint32(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 &tracker = document->getColorTracker(); + + // 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 = tracker.connectChanged([this, document]() { + this->refresh(document); + }); + profiles_modified_connection = tracker.connectModified([this, document](std::shared_ptr space) { + this->refresh(document); + }); + + refresh(document); + Gtk::Popover::on_show(); +} + + +void CmsPopover::refresh(SPDocument *document) +{ + if (!_dtw || !_dtw->get_desktop()) + return; + + _profile = nullptr; //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 0000000000000000000000000000000000000000..5774d92287ed0fc44f5fa3b5d7181d4b229fe1f0 --- /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 431d01d216a1a786c708ee38b7376e4288def36a..0000000000000000000000000000000000000000 --- 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 9f8afee259afff66398ccbd81fccfeaa0bdf37f2..0000000000000000000000000000000000000000 --- 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 ac1f6b713e3d763e4238080bc4314f902e613784..fdef868743e453d6ad68cde3d7ee20987f9656e7 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 "profile-manager.h" -#include "color/cms-system.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; @@ -266,7 +263,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); @@ -277,29 +275,29 @@ void ColorNotebook::_updateICCButtons() _setCurrentPage(getPageIndex("CMS"), true); /* update out-of-gamut icon */ - Inkscape::ColorProfile *target_profile = - _document->getProfileManager().find(name.c_str()); - if (target_profile) - gtk_widget_set_sensitive(_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->getProfileManager().find(name.c_str()); + /*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 215613ef90d8609ddf584e500835bd6e1d40ced5..d32d510bf1ff802ff9b3d4c43a42d2d1dec157f0 100644 --- a/src/ui/widget/color-scales.cpp +++ b/src/ui/widget/color-scales.cpp @@ -31,7 +31,7 @@ #include "ui/icon-loader.h" #include "ui/pack.h" #include "ui/selected-color.h" -#include "ui/widget/color-icc-selector.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" @@ -69,35 +69,30 @@ 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) { - 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(Colors::Space::Type 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(Colors::Space::Type mode) { + return color_mode_name[static_cast(mode)]; } -std::unique_ptr get_factory(SPColorScalesMode mode) { + +std::unique_ptr get_factory(Colors::Space::Type 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 SPColorScalesMode::CMS: 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"); } @@ -107,13 +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, - SPColorScalesMode::CMS + 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); @@ -130,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) @@ -161,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); @@ -172,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 == Colors::Space::Type::HSL || + MODE == Colors::Space::Type::HSV || + MODE == Colors::Space::Type::HSLUV || + MODE == Colors::Space::Type::OKLAB) { /* Create wheel */ - if constexpr (MODE == SPColorScalesMode::HSLUV) { + if constexpr (MODE == Colors::Space::Type::HSLUV) { _wheel = Gtk::make_managed(); - } else if constexpr (MODE == SPColorScalesMode::OKLAB) { + } else if constexpr (MODE == Colors::Space::Type::OKLAB) { _wheel = Gtk::make_managed(); } else { _wheel = Gtk::make_managed(); @@ -241,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); @@ -255,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 == 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(_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; @@ -329,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 == 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 == SPColorScalesMode::CMYK) { + } else if constexpr (MODE == Colors::Space::Type::CMYK) { _getCmykaFloatv(c); float rgb[3]; @@ -352,7 +325,7 @@ void ColorScales::_recalcColor() _color.setColorAlpha(color, alpha); } -template +template void ColorScales::_updateDisplay(bool update_wheel) { #ifdef DUMP_CHANGE_INFO @@ -366,42 +339,42 @@ void ColorScales::_updateDisplay(bool update_wheel) SPColor color = _color.color(); - if constexpr (MODE == SPColorScalesMode::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 == SPColorScalesMode::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 == SPColorScalesMode::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 == SPColorScalesMode::CMYK) { + } else if constexpr (MODE == Colors::Space::Type::CMYK) { color.get_cmyk_floatv(c); c[4] = _color.alpha(); - } else if constexpr (MODE == SPColorScalesMode::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 == SPColorScalesMode::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]})); _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) { @@ -413,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; } @@ -462,7 +428,7 @@ void ColorScales::_onColorChanged() _updateDisplay(); } -template +template void ColorScales::on_show() { Gtk::Box::on_show(); @@ -470,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 == Colors::Space::Type::RGB) { + rgba[0] = getScaled(0); + rgba[1] = getScaled(1); + rgba[2] = getScaled(2); + } else if constexpr (MODE == Colors::Space::Type::HSL) { + SPColor::hsl_to_rgb_floatv(rgba, getScaled(0), getScaled(1), getScaled(2)); + } else if constexpr (MODE == Colors::Space::Type::HSV) { + SPColor::hsv_to_rgb_floatv(rgba, getScaled(0), getScaled(1), getScaled(2)); + } 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 == Colors::Space::Type::HSLUV) { + SPColor::hsluv_to_rgb_floatv(rgba, getScaled(0), getScaled(1), + getScaled(2)); + } else if constexpr (MODE == Colors::Space::Type::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 == Colors::Space::Type::RGB) { + SPColor::rgb_to_cmyk_floatv(cmyka, getScaled(0), getScaled(1), getScaled(2)); + } 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]); - 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 == 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]); - cmyka[4] = getScaled(_a[3]); - } else if constexpr (MODE == SPColorScalesMode::OKLAB) { + } else if constexpr (MODE == Colors::Space::Type::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 == Colors::Space::Type::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]; @@ -560,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 == Colors::Space::Type::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 : 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); + _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; + } - SPColor::rgb_to_hsl_floatv(c, rgba[0], rgba[1], rgba[2]); + // 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); + } - setScaled(_a[0], c[0]); - setScaled(_a[1], c[1]); - setScaled(_a[2], c[2]); - setScaled(_a[3], rgba[3]); + if constexpr (MODE == Colors::Space::Type::RGB) { + setScaled(0, rgba[0]); + setScaled(1, rgba[1]); + setScaled(2, rgba[2]); + } else if constexpr (MODE == Colors::Space::Type::HSL) { + _slide[0]->setMap(sp_color_scales_hue_map()); + c[0] = 0.0; - _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; + 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 == Colors::Space::Type::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(_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; - + setScaled(0, c[0]); + setScaled(1, c[1]); + setScaled(2, c[2]); + } else if constexpr (MODE == Colors::Space::Type::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 == 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])); 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 == 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(_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 +Colors::Space::Type ColorScales::getMode() const { return MODE; } -template +template void ColorScales::_sliderAnyGrabbed() { if (_updating) { return; } @@ -812,7 +629,7 @@ void ColorScales::_sliderAnyGrabbed() } } -template +template void ColorScales::_sliderAnyReleased() { if (_updating) { return; } @@ -823,7 +640,7 @@ void ColorScales::_sliderAnyReleased() } } -template +template void ColorScales::_sliderAnyChanged() { if (_updating) { return; } @@ -831,7 +648,7 @@ void ColorScales::_sliderAnyChanged() _recalcColor(); } -template +template void ColorScales::_adjustmentChanged(int channel) { if (_updating) { return; } @@ -840,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 == Colors::Space::Type::NONE || + MODE == Colors::Space::Type::RGB || + MODE == Colors::Space::Type::CMYK) { return; } @@ -875,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]; @@ -885,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 == Colors::Space::Type::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 == Colors::Space::Type::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 == Colors::Space::Type::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 == Colors::Space::Type::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 == 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]); 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 == Colors::Space::Type::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])); - } - 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)); + _slide[2]->setMap(hsluvLightnessMap(adj[0], adj[1], &_sliders_maps[2])); } - } else if constexpr (MODE == SPColorScalesMode::OKLAB) { - if (channels != CSC_CHANNEL_H && channels != CSC_CHANNEL_A) { - _s[0]->setMap(Oklab::render_hue_scale(adj[1], adj[2], &_sliders_maps[0])); + /* 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 == Colors::Space::Type::OKLAB) { + if (channels != CSC_CHANNEL_H) { + _slide[0]->setMap(Oklab::render_hue_scale(adj[1], adj[2], &_sliders_maps[0])); } - if ((channels != CSC_CHANNEL_S) && (channels != CSC_CHANNEL_A)) { - _s[1]->setMap(Oklab::render_saturation_scale(360.0 * adj[0], adj[2], &_sliders_maps[1])); + if (channels != CSC_CHANNEL_S) { + _slide[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__); } @@ -1143,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) { @@ -1152,7 +966,7 @@ guchar const *ColorScales::hsluvHueMap(gfloat s, gfloat l, }); } -template +template guchar const *ColorScales::hsluvSaturationMap(gfloat h, gfloat l, std::array *map) { @@ -1161,7 +975,7 @@ guchar const *ColorScales::hsluvSaturationMap(gfloat h, gfloat l, }); } -template +template guchar const *ColorScales::hsluvLightnessMap(gfloat h, gfloat s, std::array *map) { @@ -1170,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 76d8a7c0a7ccbc5950c25ae140e7937d377459c9..47462b5c8d1cc69337959941e880b0f8fa563b59 100644 --- a/src/ui/widget/color-scales.h +++ b/src/ui/widget/color-scales.h @@ -21,37 +21,27 @@ #include "helper/auto-connection.h" #include "ui/selected-color.h" +#include "colors/spaces/enum.h" namespace Inkscape::UI::Widget { class ColorSlider; class ColorWheel; -enum class SPColorScalesMode { - NONE, - RGB, - HSL, - CMYK, - HSV, - HSLUV, - OKLAB, - CMS -}; - -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; + Colors::Space::Type getMode() const; static guchar const *hsluvHueMap(gfloat s, gfloat l, std::array *map); @@ -79,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 @@ -104,7 +91,7 @@ public: ColorScales &operator=(ColorScales const &obj) = delete; }; -template +template class ColorScalesFactory : public Inkscape::UI::ColorSelectorFactory { public: @@ -116,7 +103,7 @@ public: struct ColorPickerDescription { - SPColorScalesMode mode; + Colors::Space::Type 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 2aab2bdcbab8adbc9ff7dfb05b911c242f3981ba..04c413c40a2d8ad87b2d93d0fddd04d29d9ba3db 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; } @@ -126,11 +126,15 @@ 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)); auto const constrained = get_constrained(state); - ColorScales<>::setScaled(_adjustment, value, constrained); + set_adj_scaled(_adjustment, value, constrained); signal_dragged.emit(); } } @@ -157,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(); } @@ -167,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); @@ -184,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/desktop-widget.cpp b/src/ui/widget/desktop-widget.cpp index e189cf1df82815ea6c75740522fa14eb42cccae0..c4ad84579dc2276f4b4065a65987b5169f9f09c9 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() { diff --git a/src/ui/widget/paint-selector.cpp b/src/ui/widget/paint-selector.cpp index e70a0f076ac1669d916751c9ce6ead38e7c9f6e8..69ba4ec2fe7f588958420b78fe3b4632d17c2c4f 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/numeric/converters.h b/src/util/numeric/converters.h index 6feb14a244cb094f6c5fb06c48def7145c8439ae..200c225c81a88e33f996c555ecc37b663f60f51c 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/src/util/object-renderer.cpp b/src/util/object-renderer.cpp index f9829e6f903921c340215ef752f3b21c04097797..a6eb204a2ed6a83b5225876a02139bc376382f30 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/CMakeLists.txt b/testfiles/CMakeLists.txt index af83b6cd14b8487b669a4198ddf5aab46d67cd25..5a3e6f9a546edb81d79b349a2debe1d78d5a093a 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 Binary files /dev/null and b/testfiles/data/SwappedRedAndGreen.icc differ diff --git a/testfiles/data/color-cms.svg b/testfiles/data/color-cms.svg new file mode 100644 index 0000000000000000000000000000000000000000..dcbdfb89efb4dde64a5b23175c25aa44be2c620c --- /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 Binary files /dev/null and b/testfiles/data/default_cmyk.icc differ diff --git a/testfiles/data/display.icc b/testfiles/data/display.icc new file mode 100644 index 0000000000000000000000000000000000000000..12cb9c8b167b92335474155b4ac07c44b0cdabf6 Binary files /dev/null and b/testfiles/data/display.icc differ diff --git a/testfiles/src/attributes-test.cpp b/testfiles/src/attributes-test.cpp index df00ea9dc467f79a5d80f16326cf9df082f488f7..d1500edde98f976f1e77c6b8aba25f959374954c 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 deleted file mode 100644 index 40899204163b5cb3a3e3931d0d30efa6e24b4b6c..0000000000000000000000000000000000000000 --- a/testfiles/src/color-profile-test.cpp +++ /dev/null @@ -1,127 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * Unit tests for color profile. - * - * Author: - * Jon A. Cruz - * - * Copyright (C) 2015 Authors - * - * Released under GNU GPL v2+, read the file 'COPYING' for more information. - */ - -#include "gtest/gtest.h" - -#include "attributes.h" -#include "color/cms-system.h" -#include "object/color-profile.h" -#include "doc-per-case-test.h" - -namespace { - -/** - * Test fixture to inherit a shared doc and create a color profile instance per test. - */ -class ProfTest : public DocPerCaseTest -{ -public: - ProfTest() : - DocPerCaseTest(), - _prof(0) - { - } - -protected: - void SetUp() override - { - DocPerCaseTest::SetUp(); - _prof = new Inkscape::ColorProfile(); - ASSERT_TRUE( _prof != NULL ); - _prof->document = _doc.get(); - } - - void TearDown() override - { - if (_prof) { - delete _prof; - _prof = NULL; - } - DocPerCaseTest::TearDown(); - } - - Inkscape::ColorProfile *_prof; -}; - -typedef ProfTest ColorProfileTest; - -TEST_F(ColorProfileTest, SetRenderingIntent) -{ - struct { - gchar const *attr; - guint intVal; - } - const cases[] = { - {"auto", (guint)Inkscape::RENDERING_INTENT_AUTO}, - {"perceptual", (guint)Inkscape::RENDERING_INTENT_PERCEPTUAL}, - {"relative-colorimetric", (guint)Inkscape::RENDERING_INTENT_RELATIVE_COLORIMETRIC}, - {"saturation", (guint)Inkscape::RENDERING_INTENT_SATURATION}, - {"absolute-colorimetric", (guint)Inkscape::RENDERING_INTENT_ABSOLUTE_COLORIMETRIC}, - {"something-else", (guint)Inkscape::RENDERING_INTENT_UNKNOWN}, - {"auto2", (guint)Inkscape::RENDERING_INTENT_UNKNOWN}, - }; - - for (auto i : cases) { - _prof->setKeyValue( SPAttr::RENDERING_INTENT, i.attr); - ASSERT_EQ( (guint)i.intVal, _prof->rendering_intent ) << i.attr; - } -} - -TEST_F(ColorProfileTest, SetLocal) -{ - gchar const* cases[] = { - "local", - "something", - }; - - for (auto & i : cases) { - _prof->setKeyValue( SPAttr::LOCAL, i); - ASSERT_TRUE( _prof->local != NULL ); - if ( _prof->local ) { - ASSERT_EQ( std::string(i), _prof->local ); - } - } - _prof->setKeyValue( SPAttr::LOCAL, NULL); - ASSERT_EQ( (gchar*)0, _prof->local ); -} - -TEST_F(ColorProfileTest, SetName) -{ - gchar const* cases[] = { - "name", - "something", - }; - - for (auto & i : cases) { - _prof->setKeyValue( SPAttr::NAME, i); - ASSERT_TRUE( _prof->name != NULL ); - if ( _prof->name ) { - ASSERT_EQ( std::string(i), _prof->name ); - } - } - _prof->setKeyValue( SPAttr::NAME, NULL ); - ASSERT_EQ( (gchar*)0, _prof->name ); -} - - -} // namespace - -/* - Local Variables: - mode:c++ - c-file-style:"stroustrup" - c-file-offsets:((innamespace . 0)(inline-open . 0)) - indent-tabs-mode:nil - fill-column:99 - End: -*/ -// vim: expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/testfiles/src/colors/cms-test.cpp b/testfiles/src/colors/cms-test.cpp new file mode 100644 index 0000000000000000000000000000000000000000..995d3666ad2760ed55694afb4e0cec3b075ae2e5 --- /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 0000000000000000000000000000000000000000..cfe0ae6eb5ab3f3960beca91f0340c77f94202ec --- /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 0000000000000000000000000000000000000000..c30c5c411df105418ca67bd4dbad72d1306ba3ac --- /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 0000000000000000000000000000000000000000..87ff4f597fbd458c9f655cf1014f912137b190ca --- /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 0000000000000000000000000000000000000000..48175ee13edd531241028f358cd4b1898aebe65e --- /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 0000000000000000000000000000000000000000..6a9c5e5ce48d9d67ff7c2c31ad1072afe3fe44b7 --- /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 0000000000000000000000000000000000000000..067a3a21a91aaf856dbd231338aabe99b55aa935 --- /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 0000000000000000000000000000000000000000..31023f201016b77554a709c63d02b66302038bd2 --- /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 0000000000000000000000000000000000000000..e97018a9a14262a8140f1ebefa6d5ca007349213 --- /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 0000000000000000000000000000000000000000..0094217f0d6973cae51ab2de109ab4977775621f --- /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 0000000000000000000000000000000000000000..f34c266555df6b7e67bd8898fe8cc6bdd5f8deb5 --- /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 0000000000000000000000000000000000000000..4b63e076be60e3ba116157c38720dc4bc3254d1d --- /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 0000000000000000000000000000000000000000..4fb57a5f61712b393e6f2fa06370e98f8ffd7009 --- /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 0000000000000000000000000000000000000000..18aa2fd3b1da0dde52718c77e386017315c241b1 --- /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 0000000000000000000000000000000000000000..2f169e8c1724ce619b9e57b0ecf39d48b7648899 --- /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 0000000000000000000000000000000000000000..c7f18ac4ae923165a2a7a8e17fca433e6b5f59ae --- /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 0000000000000000000000000000000000000000..8c5e567004becad1b08b4ac395a6c382f3ca114d --- /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 0000000000000000000000000000000000000000..6ddcb0f86afad4ab9190e20be64326663e7e8154 --- /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 0000000000000000000000000000000000000000..c280e4f98c6cc68b993054ac06376afad228119a --- /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 0000000000000000000000000000000000000000..49cea2768159b3e679a8eb4c8f7812b66a7f67f2 --- /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 0000000000000000000000000000000000000000..8de76ea2788800e588eb7e773d2c108cf1a4d022 --- /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 0000000000000000000000000000000000000000..5ff561deb32ffd9d0975fa7c108ba595ba3e26f3 --- /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 0000000000000000000000000000000000000000..06996a874a375ceff922e8dcb29e2f5be88b9e75 --- /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 f4cc949b40ec0480f200a7c4855aa26cc027369e..0000000000000000000000000000000000000000 --- 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 c4e937928d652785b51d4853851bc0598e429812..0000000000000000000000000000000000000000 --- a/testfiles/src/svg-color-test.cpp +++ /dev/null @@ -1,112 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/** @file - * Test for SVG colors - *//* - * Authors: see git history - * - * Copyright (C) 2010 Authors - * Released under GNU GPL v2+, read the file 'COPYING' for more information. - */ -#include "svg/svg-color.h" - -#include -#include - -#include "preferences.h" -#include "svg/svg-icc-color.h" - -static void check_rgb24(unsigned const rgb24) -{ - Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - char css[8]; - - prefs->setBool("/options/svgoutput/usenamedcolors", false); - sp_svg_write_color(css, sizeof(css), rgb24 << 8); - ASSERT_EQ(sp_svg_read_color(css, 0xff), rgb24 << 8); - - prefs->setBool("/options/svgoutput/usenamedcolors", true); - sp_svg_write_color(css, sizeof(css), rgb24 << 8); - ASSERT_EQ(sp_svg_read_color(css, 0xff), rgb24 << 8); -} - -TEST(SvgColorTest, testWrite) -{ - unsigned const components[] = {0, 0x80, 0xff, 0xc0, 0x77}; - unsigned const nc = G_N_ELEMENTS(components); - for (unsigned i = nc * nc * nc; i--;) { - unsigned tmp = i; - unsigned rgb24 = 0; - for (unsigned c = 0; c < 3; ++c) { - unsigned const component = components[tmp % nc]; - rgb24 = (rgb24 << 8) | component; - tmp /= nc; - } - ASSERT_TRUE(tmp == 0); - check_rgb24(rgb24); - } - - /* And a few completely random ones. */ - for (unsigned i = 500; i--;) { /* Arbitrary number of iterations. */ - unsigned const rgb24 = (std::rand() >> 4) & 0xffffff; - check_rgb24(rgb24); - } -} - -TEST(SvgColorTest, testReadColor) -{ - gchar const *val[] = {"#f0f", "#ff00ff", "rgb(255,0,255)", "fuchsia"}; - size_t const n = sizeof(val) / sizeof(*val); - for (size_t i = 0; i < n; i++) { - gchar const *end = 0; - guint32 result = sp_svg_read_color(val[i], &end, 0x3); - ASSERT_EQ(result, 0xff00ff00); - ASSERT_LT(val[i], end); - } -} - -TEST(SvgColorTest, testIccColor) -{ - struct - { - unsigned numEntries; - bool shouldPass; - char const *name; - char const *str; - } cases[] = { - {1, true, "named", "icc-color(named, 3)"}, - {0, false, "", "foodle"}, - {1, true, "a", "icc-color(a, 3)"}, - {4, true, "named", "icc-color(named, 3, 0, 0.1, 2.5)"}, - {0, false, "", "icc-color(named, 3"}, - {0, false, "", "icc-color(space named, 3)"}, - {0, false, "", "icc-color(tab\tnamed, 3)"}, - {0, false, "", "icc-color(0name, 3)"}, - {0, false, "", "icc-color(-name, 3)"}, - {1, true, "positive", "icc-color(positive, +3)"}, - {1, true, "negative", "icc-color(negative, -3)"}, - {1, true, "positive", "icc-color(positive, +0.1)"}, - {1, true, "negative", "icc-color(negative, -0.1)"}, - {0, false, "", "icc-color(named, value)"}, - {1, true, "hyphen-name", "icc-color(hyphen-name, 1)"}, - {1, true, "under_name", "icc-color(under_name, 1)"}, - }; - - for (size_t i = 0; i < G_N_ELEMENTS(cases); i++) { - SVGICCColor tmp; - char const *str = cases[i].str; - char const *result = nullptr; - - bool parseRet = sp_svg_read_icc_color(str, &result, &tmp); - ASSERT_EQ(parseRet, cases[i].shouldPass) << str; - ASSERT_EQ(tmp.colors.size(), cases[i].numEntries) << str; - if (cases[i].shouldPass) { - ASSERT_STRNE(str, result); - ASSERT_EQ(tmp.colorProfile, cases[i].name) << str; - } else { - ASSERT_STREQ(str, result); - ASSERT_TRUE(tmp.colorProfile.empty()); - } - } -} - -// vim: filetype=cpp:expandtab:shiftwidth=4:softtabstop=4:fileencoding=utf-8:textwidth=99 :