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")], +)