Coverage for colour/io/tests/test_ctl.py: 100%
39 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 23:01 +1300
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 23:01 +1300
1"""Define the unit tests for the :mod:`colour.io.ctl` module."""
3from __future__ import annotations
5import os
6import shutil
7import tempfile
8import textwrap
10import numpy as np
12from colour.constants import TOLERANCE_ABSOLUTE_TESTS
13from colour.io import (
14 ctl_render,
15 process_image_ctl,
16 read_image,
17 template_ctl_transform_float,
18 template_ctl_transform_float3,
19)
20from colour.utilities import full, is_ctlrender_installed
22__author__ = "Colour Developers"
23__copyright__ = "Copyright 2013 Colour Developers"
24__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
25__maintainer__ = "Colour Developers"
26__email__ = "colour-developers@colour-science.org"
27__status__ = "Production"
29__all__ = [
30 "ROOT_RESOURCES",
31 "TestCtlRender",
32 "TestProcessImageCtl",
33 "TestTemplateCtlTransformFloat",
34 "TestTemplateCtlTransformFloat3",
35]
37ROOT_RESOURCES: str = os.path.join(os.path.dirname(__file__), "resources")
39# TODO: Reinstate coverage when "ctlrender" is tivially available
40# cross-platform.
43class TestCtlRender:
44 """Define :func:`colour.io.ctl.ctl_render` definition unit tests methods."""
46 def setup_method(self) -> None:
47 """Initialise the common tests attributes."""
49 self._temporary_directory = tempfile.mkdtemp()
51 def teardown_method(self) -> None:
52 """After tests actions."""
54 shutil.rmtree(self._temporary_directory)
56 def test_ctl_render(self) -> None: # pragma: no cover
57 """Test :func:`colour.io.ctl.ctl_render` definition."""
59 if not is_ctlrender_installed():
60 return
62 ctl_adjust_gain_float = template_ctl_transform_float(
63 "rIn * gain[0]",
64 "gIn * gain[1]",
65 "bIn * gain[2]",
66 description="Adjust Gain",
67 parameters=["input float gain[3] = {1.0, 1.0, 1.0}"],
68 )
70 ctl_adjust_exposure_float = template_ctl_transform_float(
71 "rIn * pow(2, exposure)",
72 "gIn * pow(2, exposure)",
73 "bIn * pow(2, exposure)",
74 description="Adjust Exposure",
75 parameters=["input float exposure = 0.0"],
76 )
78 path_input = os.path.join(ROOT_RESOURCES, "CMS_Test_Pattern.exr")
79 path_output = os.path.join(
80 self._temporary_directory, "CMS_Test_Pattern_Float.exr"
81 )
83 ctl_render(
84 path_input,
85 path_output,
86 {
87 ctl_adjust_gain_float: ["-param3 gain 0.5 1.0 2.0"],
88 ctl_adjust_exposure_float: ["-param1 exposure 1.0"],
89 },
90 "-verbose",
91 "-force",
92 )
94 np.testing.assert_allclose(
95 read_image(path_output)[..., 0:3],
96 read_image(path_input) * [1, 2, 4],
97 atol=TOLERANCE_ABSOLUTE_TESTS,
98 )
100 ctl_render(
101 path_input,
102 path_output,
103 {
104 os.path.join(ROOT_RESOURCES, "Adjust_Exposure_Float3.ctl"): [
105 "-param1 exposure 1.0"
106 ],
107 },
108 "-verbose",
109 "-force",
110 env=dict(os.environ, CTL_MODULE_PATH=ROOT_RESOURCES),
111 )
113 np.testing.assert_allclose(
114 read_image(path_output)[..., 0:3],
115 read_image(path_input) * 2,
116 atol=TOLERANCE_ABSOLUTE_TESTS,
117 )
120class TestProcessImageCtl:
121 """
122 Define :func:`colour.io.ctl.process_image_ctl` definition unit tests
123 methods.
124 """
126 def test_process_image_ctl(self) -> None: # pragma: no cover
127 """Test :func:`colour.io.ctl.process_image_ctl` definition."""
129 if not is_ctlrender_installed():
130 return
132 ctl_adjust_gain_float = template_ctl_transform_float(
133 "rIn * gain[0]",
134 "gIn * gain[1]",
135 "bIn * gain[2]",
136 description="Adjust Gain",
137 parameters=["input float gain[3] = {1.0, 1.0, 1.0}"],
138 )
140 np.testing.assert_allclose(
141 process_image_ctl(
142 0.18,
143 {
144 ctl_adjust_gain_float: ["-param3 gain 0.5 1.0 2.0"],
145 },
146 "-verbose",
147 "-force",
148 ),
149 0.18 / 2,
150 atol=0.0001,
151 )
153 np.testing.assert_allclose(
154 process_image_ctl(
155 np.array([0.18, 0.18, 0.18]),
156 {
157 ctl_adjust_gain_float: ["-param3 gain 0.5 1.0 2.0"],
158 },
159 ),
160 np.array([0.18 / 2, 0.18, 0.18 * 2]),
161 atol=0.0001,
162 )
164 np.testing.assert_allclose(
165 process_image_ctl(
166 np.array([[0.18, 0.18, 0.18]]),
167 {
168 ctl_adjust_gain_float: ["-param3 gain 0.5 1.0 2.0"],
169 },
170 ),
171 np.array([[0.18 / 2, 0.18, 0.18 * 2]]),
172 atol=0.0001,
173 )
175 np.testing.assert_allclose(
176 process_image_ctl(
177 np.array([[[0.18, 0.18, 0.18]]]),
178 {
179 ctl_adjust_gain_float: ["-param3 gain 0.5 1.0 2.0"],
180 },
181 ),
182 np.array([[[0.18 / 2, 0.18, 0.18 * 2]]]),
183 atol=0.0001,
184 )
186 np.testing.assert_allclose(
187 process_image_ctl(
188 full([4, 2, 3], 0.18),
189 {
190 ctl_adjust_gain_float: ["-param3 gain 0.5 1.0 2.0"],
191 },
192 ),
193 full([4, 2, 3], 0.18) * [0.5, 1.0, 2.0],
194 atol=0.0001,
195 )
198class TestTemplateCtlTransformFloat:
199 """
200 Define :func:`colour.io.ctl.template_ctl_transform_float` definition unit
201 tests methods.
202 """
204 def test_template_ctl_transform_float(self) -> None:
205 """Test :func:`colour.io.ctl.template_ctl_transform_float` definition."""
207 ctl_foo_bar_float = template_ctl_transform_float(
208 "rIn + foo[0]",
209 "gIn + foo[1]",
210 "bIn + foo[2]",
211 description="Foo & Bar",
212 imports=['import "Foo.ctl";', 'import "Bar.ctl";'],
213 parameters=[
214 "input float foo[3] = {1.0, 1.0, 1.0}",
215 "input float bar = 1.0",
216 ],
217 header="// Custom Header",
218 )
220 target = textwrap.dedent(
221 """
222 // Foo & Bar
224 import "Foo.ctl";
225 import "Bar.ctl";
227 // Custom Header
228 void main
229 (
230 output varying float rOut,
231 output varying float gOut,
232 output varying float bOut,
233 output varying float aOut,
234 input varying float rIn,
235 input varying float gIn,
236 input varying float bIn,
237 input varying float aIn = 1.0,
238 input float foo[3] = {1.0, 1.0, 1.0},
239 input float bar = 1.0
240 )
241 {
242 rOut = rIn + foo[0];
243 gOut = gIn + foo[1];
244 bOut = bIn + foo[2];
245 aOut = aIn;
246 }"""[1:]
247 )
248 assert ctl_foo_bar_float == target
250 ctl_no_description_float = template_ctl_transform_float("rIn * 2.0")
252 target_no_description = (
253 '// "float" Processing Function\n\n'
254 "void main\n"
255 "(\n"
256 " output varying float rOut,\n"
257 " output varying float gOut,\n"
258 " output varying float bOut,\n"
259 " output varying float aOut,\n"
260 " input varying float rIn,\n"
261 " input varying float gIn,\n"
262 " input varying float bIn,\n"
263 " input varying float aIn = 1.0)\n"
264 "{\n"
265 " rOut = rIn * 2.0;\n"
266 " gOut = rIn * 2.0;\n"
267 " bOut = rIn * 2.0;\n"
268 " aOut = aIn;\n"
269 "}"
270 )
271 assert ctl_no_description_float == target_no_description
274class TestTemplateCtlTransformFloat3:
275 """
276 Define :func:`colour.io.ctl.template_ctl_transform_float3` definition unit
277 tests methods.
278 """
280 def test_template_ctl_transform_float3(self) -> None:
281 """Test :func:`colour.io.ctl.template_ctl_transform_float3` definition."""
283 ctl_foo_bar_float3 = template_ctl_transform_float3(
284 "baz(rgbIn, foo, bar)",
285 description="Foo, Bar & Baz",
286 imports=[
287 '// import "Foo.ctl";',
288 '// import "Bar.ctl";',
289 '// import "Baz.ctl";',
290 ],
291 parameters=[
292 "input float foo[3] = {1.0, 1.0, 1.0}",
293 "input float bar = 1.0",
294 ],
295 header=textwrap.dedent(
296 """
297 float[3] baz(float rgbIn[3], float foo[3], float qux)
298 {
299 float rgbOut[3];
301 rgbOut[0] = rgbIn[0] * foo[0]* qux;
302 rgbOut[1] = rgbIn[1] * foo[1]* qux;
303 rgbOut[2] = rgbIn[2] * foo[2]* qux;
305 return rgbOut;
306 }
307"""[1:]
308 ),
309 )
310 # fmt: off
311 target = textwrap.dedent(
312 """
313 // Foo, Bar & Baz
315 // import "Foo.ctl";
316 // import "Bar.ctl";
317 // import "Baz.ctl";
319 float[3] baz(float rgbIn[3], float foo[3], float qux)
320 {
321 float rgbOut[3];
323 rgbOut[0] = rgbIn[0] * foo[0]* qux;
324 rgbOut[1] = rgbIn[1] * foo[1]* qux;
325 rgbOut[2] = rgbIn[2] * foo[2]* qux;
327 return rgbOut;
328 }
330 void main
331 (
332 output varying float rOut,
333 output varying float gOut,
334 output varying float bOut,
335 output varying float aOut,
336 input varying float rIn,
337 input varying float gIn,
338 input varying float bIn,
339 input varying float aIn = 1.0,
340 input float foo[3] = {1.0, 1.0, 1.0},
341 input float bar = 1.0
342 )
343 {
344 float rgbIn[3] = {rIn, gIn, bIn};
346 float rgbOut[3] = baz(rgbIn, foo, bar);
348 rOut = rgbOut[0];
349 gOut = rgbOut[1];
350 bOut = rgbOut[2];
351 aOut = aIn;
352 }"""[
353 1:
354 ]
355 )
356 assert ctl_foo_bar_float3 == target
358 ctl_no_description_float3 = template_ctl_transform_float3("rgbIn * 2.0")
360 target_no_description = (
361 '// "float3" Processing Function\n\n'
362 "void main\n"
363 "(\n"
364 " output varying float rOut,\n"
365 " output varying float gOut,\n"
366 " output varying float bOut,\n"
367 " output varying float aOut,\n"
368 " input varying float rIn,\n"
369 " input varying float gIn,\n"
370 " input varying float bIn,\n"
371 " input varying float aIn = 1.0)\n"
372 "{\n"
373 " float rgbIn[3] = {rIn, gIn, bIn};\n\n"
374 " float rgbOut[3] = rgbIn * 2.0;\n\n"
375 " rOut = rgbOut[0];\n"
376 " gOut = rgbOut[1];\n"
377 " bOut = rgbOut[2];\n"
378 " aOut = aIn;\n"
379 "}"
380 )
381 assert ctl_no_description_float3 == target_no_description