From 2947a7543da4088b4d5180531801bc7eb65f8ef8 Mon Sep 17 00:00:00 2001
From: Thomas Mansencal <thomas.mansencal@gmail.com>
Date: Fri, 3 Feb 2023 23:01:45 +1300
Subject: [PATCH 1/2] Implement support for "spimtx" formatter.

---
 apps/common.py                                | 27 +++++++++++++++++++
 apps/rgb_colourspace_transformation_matrix.py |  6 ++++-
 2 files changed, 32 insertions(+), 1 deletion(-)

diff --git a/apps/common.py b/apps/common.py
index 9ef2dfd..c3197ad 100644
--- a/apps/common.py
+++ b/apps/common.py
@@ -3,8 +3,10 @@
 ======
 """
 
+from io import StringIO
 from colour.adaptation import CHROMATIC_ADAPTATION_TRANSFORMS
 from colour.colorimetry import CCS_ILLUMINANTS
+from colour.io import LUTOperatorMatrix, write_LUT_SonySPImtx
 from colour.models import RGB_COLOURSPACES
 from colour.utilities import as_float_array
 
@@ -23,6 +25,7 @@
     "ILLUMINANTS_OPTIONS",
     "NUKE_COLORMATRIX_NODE_TEMPLATE",
     "nuke_format_matrix",
+    "spimtx_format_matrix",
 ]
 
 RGB_COLOURSPACE_OPTIONS: List[Dict] = [
@@ -102,3 +105,27 @@ def pretty(x: Iterable) -> str:
     tcl += f"     {{{pretty(M[2])}}}"
 
     return tcl
+
+
+def spimtx_format_matrix(M: ArrayLike, decimals: int = 10) -> str:
+    """
+    Format given matrix as a *Sony* *.spimtx* *LUT* formatted matrix.
+
+    Parameters
+    ----------
+    M
+        Matrix to format.
+    decimals
+        Decimals to use when formatting the matrix.
+
+    Returns
+    -------
+    :class:`str`
+        *Sony* *.spimtx* *LUT* formatted matrix.
+    """
+
+    string = StringIO()
+
+    write_LUT_SonySPImtx(LUTOperatorMatrix(M), string, decimals)
+
+    return string.getvalue()
diff --git a/apps/rgb_colourspace_transformation_matrix.py b/apps/rgb_colourspace_transformation_matrix.py
index 4954fb9..3f3236f 100644
--- a/apps/rgb_colourspace_transformation_matrix.py
+++ b/apps/rgb_colourspace_transformation_matrix.py
@@ -21,6 +21,7 @@
     NUKE_COLORMATRIX_NODE_TEMPLATE,
     RGB_COLOURSPACE_OPTIONS,
     nuke_format_matrix,
+    spimtx_format_matrix,
 )
 
 __author__ = "Colour Developers"
@@ -118,6 +119,7 @@
                         {"label": "str", "value": "str"},
                         {"label": "repr", "value": "repr"},
                         {"label": "Nuke", "value": "Nuke"},
+                        {"label": "Spimtx", "value": "Spimtx"},
                     ],
                     value=DEFAULT_STATE["formatter"],
                     clearable=False,
@@ -250,7 +252,7 @@ def set_RGB_to_RGB_matrix_output(
             M_f = str(M)
         elif formatter == "repr":
             M_f = repr(M)
-        else:
+        elif formatter == "Nuke":
 
             def slugify(string: str) -> str:
                 """Slugify given string for *Nuke*."""
@@ -270,6 +272,8 @@ def slugify(string: str) -> str:
                     f"{slugify(output_colourspace)}"
                 ),
             )
+        elif formatter == "Spimtx":
+            M_f = spimtx_format_matrix(M, decimals)
 
         return M_f
 

From e988210fe80bd4f3bb8d9d8712e9d0d62013a224 Mon Sep 17 00:00:00 2001
From: Thomas Mansencal <thomas.mansencal@gmail.com>
Date: Sat, 4 Feb 2023 10:42:05 +1300
Subject: [PATCH 2/2] Implement support for "OCIO" formatter and copy to
 clipboard button.

---
 apps/common.py                                |  69 ++++++--
 ...urspace_chromatically_adapted_primaries.py | 125 +++++++++-----
 apps/rgb_colourspace_transformation_matrix.py | 154 ++++++++++++------
 3 files changed, 245 insertions(+), 103 deletions(-)

diff --git a/apps/common.py b/apps/common.py
index c3197ad..2b04826 100644
--- a/apps/common.py
+++ b/apps/common.py
@@ -20,15 +20,17 @@
 __status__ = "Production"
 
 __all__ = [
-    "RGB_COLOURSPACE_OPTIONS",
-    "CHROMATIC_ADAPTATION_TRANSFORM_OPTIONS",
-    "ILLUMINANTS_OPTIONS",
-    "NUKE_COLORMATRIX_NODE_TEMPLATE",
+    "OPTIONS_RGB_COLOURSPACE",
+    "OPTIONS_CHROMATIC_ADAPTATION_TRANSFORM",
+    "OPTIONS_ILLUMINANTS",
+    "TEMPLATE_NUKE_NODE_COLORMATRIX",
     "nuke_format_matrix",
     "spimtx_format_matrix",
+    "TEMPLATE_OCIO_COLORSPACE",
+    "matrix_3x3_to_4x4",
 ]
 
-RGB_COLOURSPACE_OPTIONS: List[Dict] = [
+OPTIONS_RGB_COLOURSPACE: List[Dict] = [
     {"label": key, "value": key}
     for key in sorted(RGB_COLOURSPACES.keys())
     if key not in ("aces", "adobe1998", "prophoto")
@@ -37,7 +39,7 @@
 *RGB* colourspace options for a :class:`Dropdown` class instance.
 """
 
-CHROMATIC_ADAPTATION_TRANSFORM_OPTIONS: List[Dict] = [
+OPTIONS_CHROMATIC_ADAPTATION_TRANSFORM: List[Dict] = [
     {"label": key, "value": key}
     for key in sorted(CHROMATIC_ADAPTATION_TRANSFORMS.keys())
 ]
@@ -46,7 +48,7 @@
 instance.
 """
 
-ILLUMINANTS_OPTIONS: List[Dict] = [
+OPTIONS_ILLUMINANTS: List[Dict] = [
     {"label": key, "value": key}
     for key in sorted(
         CCS_ILLUMINANTS["CIE 1931 2 Degree Standard Observer"].keys()
@@ -57,13 +59,13 @@
 :class:`Dropdown`class instance.
 """
 
-NUKE_COLORMATRIX_NODE_TEMPLATE: str = """
+TEMPLATE_NUKE_NODE_COLORMATRIX: str = """
 ColorMatrix {{
  inputs 0
  matrix {{
-     {0}
+     {matrix}
    }}
- name "{1}"
+ name "{name}"
  selected true
  xpos 0
  ypos 0
@@ -129,3 +131,50 @@ def spimtx_format_matrix(M: ArrayLike, decimals: int = 10) -> str:
     write_LUT_SonySPImtx(LUTOperatorMatrix(M), string, decimals)
 
     return string.getvalue()
+
+
+TEMPLATE_OCIO_COLORSPACE = """
+  - !<ColorSpace>
+    name: Linear {name}
+    aliases: []
+    family: Utility
+    equalitygroup: ""
+    bitdepth: 32f
+    description: |
+      Convert from {input_colourspace} to Linear {output_colourspace}
+    isdata: false
+    encoding: scene-linear
+    allocation: uniform
+    from_scene_reference: !<GroupTransform>
+      name: {input_colourspace} to Linear {output_colourspace}
+      children:
+        - !<MatrixTransform> {{matrix: {matrix}}}
+"""[
+    1:
+]
+"""
+*OpenColorIO* *ColorSpace* template.
+"""
+
+
+def matrix_3x3_to_4x4(M):
+    """
+    Convert given 3x3 matrix :math:`M` to a raveled 4x4 matrix.
+
+    Parameters
+    ----------
+    M : array_like
+        3x3 matrix :math:`M` to convert.
+
+    Returns
+    -------
+    list
+        Raveled 4x4 matrix.
+    """
+
+    import numpy as np
+
+    M_I = np.identity(4)
+    M_I[:3, :3] = M
+
+    return np.ravel(M_I)
diff --git a/apps/rgb_colourspace_chromatically_adapted_primaries.py b/apps/rgb_colourspace_chromatically_adapted_primaries.py
index 097756b..efe9488 100644
--- a/apps/rgb_colourspace_chromatically_adapted_primaries.py
+++ b/apps/rgb_colourspace_chromatically_adapted_primaries.py
@@ -8,7 +8,7 @@
 from contextlib import suppress
 from dash.dcc import Dropdown, Link, Location, Markdown, Slider
 from dash.dependencies import Input, Output
-from dash.html import A, Code, Div, H3, H5, Li, Pre, Ul
+from dash.html import A, Button, Code, Div, H3, H5, Li, Pre, Ul
 from urllib.parse import parse_qs, urlencode, urlparse
 
 from colour.colorimetry import CCS_ILLUMINANTS
@@ -17,9 +17,9 @@
 
 from app import APP, SERVER_URL
 from apps.common import (
-    CHROMATIC_ADAPTATION_TRANSFORM_OPTIONS,
-    ILLUMINANTS_OPTIONS,
-    RGB_COLOURSPACE_OPTIONS,
+    OPTIONS_CHROMATIC_ADAPTATION_TRANSFORM,
+    OPTIONS_ILLUMINANTS,
+    OPTIONS_RGB_COLOURSPACE,
 )
 
 __author__ = "Colour Developers"
@@ -34,7 +34,7 @@
     "APP_PATH",
     "APP_DESCRIPTION",
     "APP_UID",
-    "DEFAULT_STATE",
+    "STATE_DEFAULT",
     "LAYOUT",
     "set_primaries_output",
     "update_state_on_url_query_change",
@@ -66,10 +66,19 @@
 App unique id.
 """
 
-DEFAULT_STATE = {
-    "colourspace": RGB_COLOURSPACE_OPTIONS[0]["value"],
-    "illuminant": ILLUMINANTS_OPTIONS[0]["value"],
-    "chromatic_adaptation_transform": CHROMATIC_ADAPTATION_TRANSFORM_OPTIONS[
+
+def _uid(id_):
+    """
+    Generate a unique id for given id by appending the application *UID*.
+    """
+
+    return f"{id_}-{APP_UID}"
+
+
+STATE_DEFAULT = {
+    "colourspace": OPTIONS_RGB_COLOURSPACE[0]["value"],
+    "illuminant": OPTIONS_ILLUMINANTS[0]["value"],
+    "chromatic_adaptation_transform": OPTIONS_CHROMATIC_ADAPTATION_TRANSFORM[
         0
     ]["value"],
     "formatter": "str",
@@ -81,58 +90,68 @@
 
 LAYOUT: Div = Div(
     [
-        Location(id=f"url-{APP_UID}", refresh=False),
+        Location(id=_uid("url"), refresh=False),
         H3([Link(APP_NAME, href=APP_PATH)], className="text-center"),
         Div(
             [
                 Markdown(APP_DESCRIPTION),
                 H5(children="Colourspace"),
                 Dropdown(
-                    id=f"colourspace-{APP_UID}",
-                    options=RGB_COLOURSPACE_OPTIONS,
-                    value=DEFAULT_STATE["colourspace"],
+                    id=_uid("colourspace"),
+                    options=OPTIONS_RGB_COLOURSPACE,
+                    value=STATE_DEFAULT["colourspace"],
                     clearable=False,
                     className="app-widget",
                 ),
                 H5(children="Illuminant"),
                 Dropdown(
-                    id=f"illuminant-{APP_UID}",
-                    options=ILLUMINANTS_OPTIONS,
-                    value=DEFAULT_STATE["illuminant"],
+                    id=_uid("illuminant"),
+                    options=OPTIONS_ILLUMINANTS,
+                    value=STATE_DEFAULT["illuminant"],
                     clearable=False,
                     className="app-widget",
                 ),
                 H5(children="Chromatic Adaptation Transform"),
                 Dropdown(
-                    id=f"chromatic-adaptation-transform-{APP_UID}",
-                    options=CHROMATIC_ADAPTATION_TRANSFORM_OPTIONS,
-                    value=DEFAULT_STATE["chromatic_adaptation_transform"],
+                    id=_uid("chromatic-adaptation-transform"),
+                    options=OPTIONS_CHROMATIC_ADAPTATION_TRANSFORM,
+                    value=STATE_DEFAULT["chromatic_adaptation_transform"],
                     clearable=False,
                     className="app-widget",
                 ),
                 H5(children="Formatter"),
                 Dropdown(
-                    id=f"formatter-{APP_UID}",
+                    id=_uid("formatter"),
                     options=[
                         {"label": "str", "value": "str"},
                         {"label": "repr", "value": "repr"},
                     ],
-                    value=DEFAULT_STATE["formatter"],
+                    value=STATE_DEFAULT["formatter"],
                     clearable=False,
                     className="app-widget",
                 ),
                 H5(children="Decimals"),
                 Slider(
-                    id=f"decimals-{APP_UID}",
+                    id=_uid("decimals"),
                     min=1,
                     max=15,
                     step=1,
-                    value=DEFAULT_STATE["decimals"],
+                    value=STATE_DEFAULT["decimals"],
                     marks={i + 1: str(i + 1) for i in range(15)},
                     className="app-widget",
                 ),
+                Button(
+                    "Copy to Clipboard",
+                    id=_uid("copy-to-clipboard-button"),
+                    n_clicks=0,
+                    style={"width": "100%"},
+                ),
                 Pre(
-                    [Code(id=f"primaries-{APP_UID}", className="code shell")],
+                    [
+                        Code(
+                            id=_uid("primaries-output"), className="code shell"
+                        )
+                    ],
                     className="app-widget app-output",
                 ),
                 Ul(
@@ -172,6 +191,7 @@
                     ],
                     className="list-inline text-center",
                 ),
+                Div(id=_uid("dev-null"), style={"display": "none"}),
             ],
             className="col-6 mx-auto",
         ),
@@ -185,13 +205,15 @@
 
 
 @APP.callback(
-    Output(component_id=f"primaries-{APP_UID}", component_property="children"),
+    Output(
+        component_id=_uid("primaries-output"), component_property="children"
+    ),
     [
-        Input(f"colourspace-{APP_UID}", "value"),
-        Input(f"illuminant-{APP_UID}", "value"),
-        Input(f"chromatic-adaptation-transform-{APP_UID}", "value"),
-        Input(f"formatter-{APP_UID}", "value"),
-        Input(f"decimals-{APP_UID}", "value"),
+        Input(_uid("colourspace"), "value"),
+        Input(_uid("illuminant"), "value"),
+        Input(_uid("chromatic-adaptation-transform"), "value"),
+        Input(_uid("formatter"), "value"),
+        Input(_uid("decimals"), "value"),
     ],
 )
 def set_primaries_output(
@@ -248,14 +270,14 @@ def set_primaries_output(
 
 @APP.callback(
     [
-        Output(f"colourspace-{APP_UID}", "value"),
-        Output(f"illuminant-{APP_UID}", "value"),
-        Output(f"chromatic-adaptation-transform-{APP_UID}", "value"),
-        Output(f"formatter-{APP_UID}", "value"),
-        Output(f"decimals-{APP_UID}", "value"),
+        Output(_uid("colourspace"), "value"),
+        Output(_uid("illuminant"), "value"),
+        Output(_uid("chromatic-adaptation-transform"), "value"),
+        Output(_uid("formatter"), "value"),
+        Output(_uid("decimals"), "value"),
     ],
     [
-        Input("url", "href"),
+        Input(_uid("url"), "href"),
     ],
 )
 def update_state_on_url_query_change(href: str) -> tuple:
@@ -283,7 +305,7 @@ def value_from_query(value: str) -> str:
         with suppress(KeyError):
             return query[value][0]
 
-        return DEFAULT_STATE[value.replace("-", "_")]
+        return STATE_DEFAULT[value.replace("-", "_")]
 
     state = (
         value_from_query("colourspace"),
@@ -297,13 +319,13 @@ def value_from_query(value: str) -> str:
 
 
 @APP.callback(
-    Output(f"url-{APP_UID}", "search"),
+    Output(_uid("url"), "search"),
     [
-        Input(f"colourspace-{APP_UID}", "value"),
-        Input(f"illuminant-{APP_UID}", "value"),
-        Input(f"chromatic-adaptation-transform-{APP_UID}", "value"),
-        Input(f"formatter-{APP_UID}", "value"),
-        Input(f"decimals-{APP_UID}", "value"),
+        Input(_uid("colourspace"), "value"),
+        Input(_uid("illuminant"), "value"),
+        Input(_uid("chromatic-adaptation-transform"), "value"),
+        Input(_uid("formatter"), "value"),
+        Input(_uid("decimals"), "value"),
     ],
 )
 def update_url_query_on_state_change(
@@ -347,3 +369,20 @@ def update_url_query_on_state_change(
     )
 
     return f"?{query}"
+
+
+APP.clientside_callback(
+    f"""
+    function(n_clicks) {{
+        var primariesOutput = document.getElementById(\
+"{_uid('primaries-output')}");
+        var content = primariesOutput.textContent;
+        navigator.clipboard.writeText(content).then(function() {{
+        }}, function() {{
+        }});
+        return content;
+    }}
+    """,
+    [Output(component_id=_uid("dev-null"), component_property="children")],
+    [Input(_uid("copy-to-clipboard-button"), "n_clicks")],
+)
diff --git a/apps/rgb_colourspace_transformation_matrix.py b/apps/rgb_colourspace_transformation_matrix.py
index 3f3236f..794d131 100644
--- a/apps/rgb_colourspace_transformation_matrix.py
+++ b/apps/rgb_colourspace_transformation_matrix.py
@@ -9,7 +9,7 @@
 from contextlib import suppress
 from dash.dcc import Dropdown, Location, Link, Markdown, Slider
 from dash.dependencies import Input, Output
-from dash.html import A, Code, Div, H3, H5, Li, Pre, Ul
+from dash.html import A, Button, Code, Div, H3, H5, Li, Pre, Ul
 from urllib.parse import parse_qs, urlencode, urlparse
 
 from colour.models import RGB_COLOURSPACES, matrix_RGB_to_RGB
@@ -17,9 +17,11 @@
 
 from app import APP, SERVER_URL
 from apps.common import (
-    CHROMATIC_ADAPTATION_TRANSFORM_OPTIONS,
-    NUKE_COLORMATRIX_NODE_TEMPLATE,
-    RGB_COLOURSPACE_OPTIONS,
+    OPTIONS_CHROMATIC_ADAPTATION_TRANSFORM,
+    OPTIONS_RGB_COLOURSPACE,
+    TEMPLATE_NUKE_NODE_COLORMATRIX,
+    TEMPLATE_OCIO_COLORSPACE,
+    matrix_3x3_to_4x4,
     nuke_format_matrix,
     spimtx_format_matrix,
 )
@@ -36,7 +38,7 @@
     "APP_NAME",
     "APP_DESCRIPTION",
     "APP_UID",
-    "DEFAULT_STATE",
+    "STATE_DEFAULT",
     "LAYOUT",
     "set_RGB_to_RGB_matrix_output",
     "update_state_on_url_query_change",
@@ -68,10 +70,19 @@
 App unique id.
 """
 
-DEFAULT_STATE = {
-    "input_colourspace": RGB_COLOURSPACE_OPTIONS[0]["value"],
-    "output_colourspace": RGB_COLOURSPACE_OPTIONS[0]["value"],
-    "chromatic_adaptation_transform": CHROMATIC_ADAPTATION_TRANSFORM_OPTIONS[
+
+def _uid(id_):
+    """
+    Generate a unique id for given id by appending the application *UID*.
+    """
+
+    return f"{id_}-{APP_UID}"
+
+
+STATE_DEFAULT = {
+    "input_colourspace": OPTIONS_RGB_COLOURSPACE[0]["value"],
+    "output_colourspace": OPTIONS_RGB_COLOURSPACE[0]["value"],
+    "chromatic_adaptation_transform": OPTIONS_CHROMATIC_ADAPTATION_TRANSFORM[
         0
     ]["value"],
     "formatter": "str",
@@ -83,62 +94,71 @@
 
 LAYOUT: Div = Div(
     [
-        Location(id=f"url-{APP_UID}", refresh=False),
+        Location(id=_uid("url"), refresh=False),
         H3([Link(APP_NAME, href=APP_PATH)], className="text-center"),
         Div(
             [
                 Markdown(APP_DESCRIPTION),
                 H5(children="Input Colourspace"),
                 Dropdown(
-                    id=f"input-colourspace-{APP_UID}",
-                    options=RGB_COLOURSPACE_OPTIONS,
-                    value=DEFAULT_STATE["input_colourspace"],
+                    id=_uid("input-colourspace"),
+                    options=OPTIONS_RGB_COLOURSPACE,
+                    value=STATE_DEFAULT["input_colourspace"],
                     clearable=False,
                     className="app-widget",
                 ),
                 H5(children="Output Colourspace"),
                 Dropdown(
-                    id=f"output-colourspace-{APP_UID}",
-                    options=RGB_COLOURSPACE_OPTIONS,
-                    value=DEFAULT_STATE["output_colourspace"],
+                    id=_uid("output-colourspace"),
+                    options=OPTIONS_RGB_COLOURSPACE,
+                    value=STATE_DEFAULT["output_colourspace"],
                     clearable=False,
                     className="app-widget",
                 ),
                 H5(children="Chromatic Adaptation Transform"),
                 Dropdown(
-                    id=f"chromatic-adaptation-transform-{APP_UID}",
-                    options=CHROMATIC_ADAPTATION_TRANSFORM_OPTIONS,
-                    value=DEFAULT_STATE["chromatic_adaptation_transform"],
+                    id=_uid("chromatic-adaptation-transform"),
+                    options=OPTIONS_CHROMATIC_ADAPTATION_TRANSFORM,
+                    value=STATE_DEFAULT["chromatic_adaptation_transform"],
                     clearable=False,
                     className="app-widget",
                 ),
                 H5(children="Formatter"),
                 Dropdown(
-                    id=f"formatter-{APP_UID}",
+                    id=_uid("formatter"),
                     options=[
                         {"label": "str", "value": "str"},
                         {"label": "repr", "value": "repr"},
-                        {"label": "Nuke", "value": "Nuke"},
-                        {"label": "Spimtx", "value": "Spimtx"},
+                        {"label": "Nuke", "value": "nuke"},
+                        {"label": "OpenColorIO", "value": "opencolorio"},
+                        {"label": "Spimtx", "value": "spimtx"},
                     ],
-                    value=DEFAULT_STATE["formatter"],
+                    value=STATE_DEFAULT["formatter"],
                     clearable=False,
                     className="app-widget",
                 ),
                 H5(children="Decimals"),
                 Slider(
-                    id=f"decimals-{APP_UID}",
+                    id=_uid("decimals"),
                     min=1,
                     max=15,
                     step=1,
-                    value=DEFAULT_STATE["decimals"],
+                    value=STATE_DEFAULT["decimals"],
                     marks={i + 1: str(i + 1) for i in range(15)},
                     className="app-widget",
                 ),
+                Button(
+                    "Copy to Clipboard",
+                    id=_uid("copy-to-clipboard-button"),
+                    n_clicks=0,
+                    style={"width": "100%"},
+                ),
                 Pre(
                     [
                         Code(
-                            id=f"RGB-transformation-matrix-{APP_UID}",
+                            id=_uid(
+                                "rgb-colourspace-transformation-matrix-output"
+                            ),
                             className="code shell",
                         )
                     ],
@@ -181,6 +201,7 @@
                     ],
                     className="list-inline text-center",
                 ),
+                Div(id=_uid("dev-null"), style={"display": "none"}),
             ],
             className="col-6 mx-auto",
         ),
@@ -195,15 +216,15 @@
 
 @APP.callback(
     Output(
-        component_id=f"RGB-transformation-matrix-{APP_UID}",
+        component_id=_uid("rgb-colourspace-transformation-matrix-output"),
         component_property="children",
     ),
     [
-        Input(f"input-colourspace-{APP_UID}", "value"),
-        Input(f"output-colourspace-{APP_UID}", "value"),
-        Input(f"chromatic-adaptation-transform-{APP_UID}", "value"),
-        Input(f"formatter-{APP_UID}", "value"),
-        Input(f"decimals-{APP_UID}", "value"),
+        Input(_uid("input-colourspace"), "value"),
+        Input(_uid("output-colourspace"), "value"),
+        Input(_uid("chromatic-adaptation-transform"), "value"),
+        Input(_uid("formatter"), "value"),
+        Input(_uid("decimals"), "value"),
     ],
 )
 def set_RGB_to_RGB_matrix_output(
@@ -252,7 +273,7 @@ def set_RGB_to_RGB_matrix_output(
             M_f = str(M)
         elif formatter == "repr":
             M_f = repr(M)
-        elif formatter == "Nuke":
+        elif formatter == "nuke":
 
             def slugify(string: str) -> str:
                 """Slugify given string for *Nuke*."""
@@ -264,15 +285,31 @@ def slugify(string: str) -> str:
                 string = re.sub(pattern, "_", string)
                 return string
 
-            M_f = NUKE_COLORMATRIX_NODE_TEMPLATE.format(
-                nuke_format_matrix(M, decimals),
-                (
+            M_f = TEMPLATE_NUKE_NODE_COLORMATRIX.format(
+                name=(
                     f"{slugify(input_colourspace)}"
                     f"__to__"
                     f"{slugify(output_colourspace)}"
                 ),
+                matrix=nuke_format_matrix(M, decimals),
             )
-        elif formatter == "Spimtx":
+        elif formatter == "opencolorio":
+            M_f = TEMPLATE_OCIO_COLORSPACE.format(
+                name=output_colourspace,
+                input_colourspace=input_colourspace,
+                output_colourspace=output_colourspace,
+                matrix=re.sub(
+                    r"\s+",
+                    " ",
+                    repr(matrix_3x3_to_4x4(M))
+                    .replace("array(", "")
+                    .replace("[ ", "[")
+                    .replace(")", "")
+                    .replace("\n", ""),
+                ),
+            )
+
+        elif formatter == "spimtx":
             M_f = spimtx_format_matrix(M, decimals)
 
         return M_f
@@ -280,14 +317,14 @@ def slugify(string: str) -> str:
 
 @APP.callback(
     [
-        Output(f"input-colourspace-{APP_UID}", "value"),
-        Output(f"output-colourspace-{APP_UID}", "value"),
-        Output(f"chromatic-adaptation-transform-{APP_UID}", "value"),
-        Output(f"formatter-{APP_UID}", "value"),
-        Output(f"decimals-{APP_UID}", "value"),
+        Output(_uid("input-colourspace"), "value"),
+        Output(_uid("output-colourspace"), "value"),
+        Output(_uid("chromatic-adaptation-transform"), "value"),
+        Output(_uid("formatter"), "value"),
+        Output(_uid("decimals"), "value"),
     ],
     [
-        Input("url", "href"),
+        Input(_uid("url"), "href"),
     ],
 )
 def update_state_on_url_query_change(href: str) -> tuple:
@@ -315,7 +352,7 @@ def value_from_query(value: str) -> str:
         with suppress(KeyError):
             return query[value][0]
 
-        return DEFAULT_STATE[value.replace("-", "_")]
+        return STATE_DEFAULT[value.replace("-", "_")]
 
     state = (
         value_from_query("input-colourspace"),
@@ -329,13 +366,13 @@ def value_from_query(value: str) -> str:
 
 
 @APP.callback(
-    Output(f"url-{APP_UID}", "search"),
+    Output(_uid("url"), "search"),
     [
-        Input(f"input-colourspace-{APP_UID}", "value"),
-        Input(f"output-colourspace-{APP_UID}", "value"),
-        Input(f"chromatic-adaptation-transform-{APP_UID}", "value"),
-        Input(f"formatter-{APP_UID}", "value"),
-        Input(f"decimals-{APP_UID}", "value"),
+        Input(_uid("input-colourspace"), "value"),
+        Input(_uid("output-colourspace"), "value"),
+        Input(_uid("chromatic-adaptation-transform"), "value"),
+        Input(_uid("formatter"), "value"),
+        Input(_uid("decimals"), "value"),
     ],
 )
 def update_url_query_on_state_change(
@@ -378,3 +415,20 @@ def update_url_query_on_state_change(
     )
 
     return f"?{query}"
+
+
+APP.clientside_callback(
+    f"""
+    function(n_clicks) {{
+        var rgbColourspaceTransformationMatrixOutput = document.getElementById(\
+"{_uid('rgb-colourspace-transformation-matrix-output')}");
+        var content = rgbColourspaceTransformationMatrixOutput.textContent;
+        navigator.clipboard.writeText(content).then(function() {{
+        }}, function() {{
+        }});
+        return content;
+    }}
+    """,
+    [Output(component_id=_uid("dev-null"), component_property="children")],
+    [Input(_uid("copy-to-clipboard-button"), "n_clicks")],
+)