|
| 1 | +""" |
| 2 | +Defines helper functionality for CLF tests. |
| 3 | +""" |
| 4 | + |
| 5 | +import os |
| 6 | +import tempfile |
| 7 | +from collections.abc import Generator |
| 8 | +from typing import Any |
| 9 | + |
| 10 | +import numpy as np |
| 11 | + |
| 12 | +import colour_clf_io as clf |
| 13 | +from colour_clf_io.processing import apply |
| 14 | + |
| 15 | +__all__ = [ |
| 16 | + "assert_ocio_consistency", |
| 17 | + "assert_ocio_consistency_for_file", |
| 18 | + "snippet_to_process_list", |
| 19 | +] |
| 20 | + |
| 21 | +from colour.hints import NDArrayFloat |
| 22 | + |
| 23 | +EXAMPLE_WRAPPER = """<?xml version="1.0" ?> |
| 24 | +<ProcessList id="Example Wrapper" compCLFversion="3.0"> |
| 25 | +{0} |
| 26 | +</ProcessList> |
| 27 | +""" |
| 28 | + |
| 29 | +RESOURCES_ROOT: str = os.path.join(os.path.dirname(__file__), "resources") |
| 30 | + |
| 31 | + |
| 32 | +def wrap_snippet(snippet: str) -> str: |
| 33 | + """# noqa: D401 |
| 34 | + Takes a string that should contain the text representation of a CLF node, and |
| 35 | + returns valid CLF document. Essentially the given string is pasted into the |
| 36 | + `ProcessList` if a CLF document. |
| 37 | +
|
| 38 | + This is useful to quickly convert example snippets of Process Nodes into valid CLF |
| 39 | + documents for parsing. |
| 40 | + """ |
| 41 | + return EXAMPLE_WRAPPER.format(snippet) |
| 42 | + |
| 43 | + |
| 44 | +def snippet_to_process_list(snippet: str) -> clf.ProcessList | None: |
| 45 | + """# noqa: D401 |
| 46 | + Takes a string that should contain a valid body for a XML Process List and |
| 47 | + returns the parsed `ProcessList`. |
| 48 | + """ |
| 49 | + doc = wrap_snippet(snippet) |
| 50 | + return clf.read_clf(doc) |
| 51 | + |
| 52 | + |
| 53 | +def snippet_as_tmp_file(snippet: str) -> str: |
| 54 | + doc = wrap_snippet(snippet) |
| 55 | + tmp_folder = tempfile.gettempdir() |
| 56 | + tmp_file_name = tempfile.mktemp(suffix=".clf") # noqa: S306 |
| 57 | + file_name = os.path.join(tmp_folder, tmp_file_name) |
| 58 | + with open(file_name, "w") as f: |
| 59 | + f.write(doc) |
| 60 | + return file_name |
| 61 | + |
| 62 | + |
| 63 | +def ocio_output_for_file( |
| 64 | + path: str, rgb: tuple[float, float, float] | NDArrayFloat | None = None |
| 65 | +) -> tuple[float, float, float]: |
| 66 | + """Apply a color transform file to a flattened, one-dimensional list of |
| 67 | + R,G,B values. |
| 68 | + """ |
| 69 | + from PyOpenColorIO import FileTransform, GetCurrentConfig |
| 70 | + |
| 71 | + xform = FileTransform(src=path) |
| 72 | + cpu = GetCurrentConfig().getProcessor(xform).getDefaultCPUProcessor() |
| 73 | + result = cpu.applyRGB(rgb) |
| 74 | + # Note: depending on the input, `applyRGB` will either return the result data, or |
| 75 | + # modify the data in place. If the return value was `None` the data was modified |
| 76 | + # in place and we use that instead. |
| 77 | + if result is None: |
| 78 | + result = rgb |
| 79 | + assert result is not None |
| 80 | + return (result[0], result[1], result[2]) |
| 81 | + |
| 82 | + |
| 83 | +def ocio_output_for_snippet( |
| 84 | + snippet: str, rgb: tuple[float, float, float] |
| 85 | +) -> NDArrayFloat: |
| 86 | + f = snippet_as_tmp_file(snippet) |
| 87 | + try: |
| 88 | + r, g, b = ocio_output_for_file(f, rgb) |
| 89 | + return np.array([r, g, b]) |
| 90 | + finally: |
| 91 | + os.remove(f) |
| 92 | + |
| 93 | + |
| 94 | +def result_as_array(result_text: str) -> NDArrayFloat: |
| 95 | + result_parts = result_text.strip().split() |
| 96 | + if len(result_parts) != 3: |
| 97 | + message = f"Invalid OCIO result: {result_text}" |
| 98 | + raise RuntimeError(message) |
| 99 | + result_values = list(map(float, result_parts)) |
| 100 | + return np.array(result_values) |
| 101 | + |
| 102 | + |
| 103 | +def assert_ocio_consistency( |
| 104 | + value: NDArrayFloat, snippet: str, err_msg: str = "", decimals: int = 5 |
| 105 | +) -> None: |
| 106 | + """Assert that the colour library calculates the same output as the OCIO reference |
| 107 | + implementation for the given CLF snippet. |
| 108 | + """ |
| 109 | + process_list = snippet_to_process_list(snippet) |
| 110 | + if process_list is None: |
| 111 | + err = "Invalid CLF snippet." |
| 112 | + raise AssertionError(err) |
| 113 | + process_list_output = apply(process_list, value, normalised_values=True) |
| 114 | + value_tuple = value[0], value[1], value[2] |
| 115 | + ocio_output = ocio_output_for_snippet(snippet, value_tuple) |
| 116 | + # Note: OCIO only accepts 16-bit floats so the precision agreement is limited. |
| 117 | + np.testing.assert_array_almost_equal( |
| 118 | + process_list_output, ocio_output, err_msg=err_msg, decimal=decimals |
| 119 | + ) |
| 120 | + |
| 121 | + |
| 122 | +def assert_ocio_consistency_for_file(value_rgb: NDArrayFloat, clf_path: str) -> None: |
| 123 | + """Assert that the colour library calculates the same output as the OCIO reference |
| 124 | + implementation for the given file. |
| 125 | + """ |
| 126 | + |
| 127 | + clf_data = clf.read_clf_from_file(clf_path) |
| 128 | + if clf_data is None: |
| 129 | + err = "Invalid CLF snippet." |
| 130 | + raise AssertionError(err) |
| 131 | + process_list_output = apply(clf_data, value_rgb, normalised_values=True) |
| 132 | + ocio_output = ocio_output_for_file(clf_path, value_rgb) |
| 133 | + np.testing.assert_array_almost_equal(process_list_output, ocio_output) |
| 134 | + |
| 135 | + |
| 136 | +def rgb_sample_iter( |
| 137 | + step: float = 0.2, |
| 138 | +) -> Generator[tuple[np.floating[Any], np.floating[Any], np.floating[Any]]]: |
| 139 | + for r in np.arange(0.0, 1.0, step): |
| 140 | + for g in np.arange(0.0, 1.0, step): |
| 141 | + for b in np.arange(0.0, 1.0, step): |
| 142 | + yield r, g, b |
0 commit comments