Coverage for colour/io/luts/sony_spi1d.py: 100%
68 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-15 19:01 +1300
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-15 19:01 +1300
1"""
2Sony .spi1d LUT Format Input / Output Utilities
3===============================================
5Define the *Sony* *.spi1d* *LUT* format related input / output utilities
6objects:
8- :func:`colour.io.read_LUT_SonySPI1D`
9- :func:`colour.io.write_LUT_SonySPI1D`
10"""
12from __future__ import annotations
14import typing
16import numpy as np
18if typing.TYPE_CHECKING:
19 from colour.hints import PathLike
21from colour.io.luts import LUT1D, LUT3x1D, LUTSequence
22from colour.io.luts.common import path_to_title
23from colour.utilities import (
24 as_float_array,
25 as_int_scalar,
26 attest,
27 format_array_as_row,
28 usage_warning,
29)
31__author__ = "Colour Developers"
32__copyright__ = "Copyright 2013 Colour Developers"
33__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
34__maintainer__ = "Colour Developers"
35__email__ = "colour-developers@colour-science.org"
36__status__ = "Production"
38__all__ = [
39 "read_LUT_SonySPI1D",
40 "write_LUT_SonySPI1D",
41]
44def read_LUT_SonySPI1D(path: str | PathLike) -> LUT1D | LUT3x1D:
45 """
46 Read the specified *Sony* *.spi1d* *LUT* file.
48 Parameters
49 ----------
50 path
51 *LUT* file path.
53 Returns
54 -------
55 :class:`colour.LUT1D` or :class:`colour.LUT3x1D`
56 :class:`LUT1D` or :class:`LUT3x1D` class instance.
58 Examples
59 --------
60 Reading a 1D *Sony* *.spi1d* *LUT*:
62 >>> import os
63 >>> path = os.path.join(
64 ... os.path.dirname(__file__),
65 ... "tests",
66 ... "resources",
67 ... "sony_spi1d",
68 ... "eotf_sRGB_1D.spi1d",
69 ... )
70 >>> print(read_LUT_SonySPI1D(path))
71 LUT1D - eotf sRGB 1D
72 --------------------
73 <BLANKLINE>
74 Dimensions : 1
75 Domain : [-0.1 1.5]
76 Size : (16,)
77 Comment 01 : Generated by "Colour 0.3.11".
78 Comment 02 : "colour.models.eotf_sRGB".
80 Reading a 3x1D *Sony* *.spi1d* *LUT*:
82 >>> path = os.path.join(
83 ... os.path.dirname(__file__),
84 ... "tests",
85 ... "resources",
86 ... "sony_spi1d",
87 ... "eotf_sRGB_3x1D.spi1d",
88 ... )
89 >>> print(read_LUT_SonySPI1D(path))
90 LUT3x1D - eotf sRGB 3x1D
91 ------------------------
92 <BLANKLINE>
93 Dimensions : 2
94 Domain : [[-0.1 -0.1 -0.1]
95 [ 1.5 1.5 1.5]]
96 Size : (16, 3)
97 Comment 01 : Generated by "Colour 0.3.11".
98 Comment 02 : "colour.models.eotf_sRGB".
99 """
101 title = path_to_title(path)
102 domain_min, domain_max = np.array([0, 1])
103 dimensions = 1
104 data = []
106 comments = []
108 with open(path) as spi1d_file:
109 lines = filter(None, (line.strip() for line in spi1d_file))
110 for line in lines:
111 if line.startswith("#"):
112 comments.append(line[1:].strip())
113 continue
115 tokens = line.split()
116 if tokens[0] == "Version":
117 continue
118 if tokens[0] == "From":
119 domain_min, domain_max = as_float_array(tokens[1:])
120 elif tokens[0] == "Length":
121 continue
122 elif tokens[0] == "Components":
123 component = as_int_scalar(tokens[1])
124 attest(
125 component in (1, 3),
126 "Only 1 or 3 components are supported!",
127 )
129 dimensions = 1 if component == 1 else 2
130 elif tokens[0] in ("{", "}"):
131 continue
132 else:
133 data.append(tokens)
135 table = as_float_array(data)
137 LUT: LUT1D | LUT3x1D
138 if dimensions == 1:
139 LUT = LUT1D(
140 np.squeeze(table),
141 title,
142 np.array([domain_min, domain_max]),
143 comments=comments,
144 )
145 elif dimensions == 2:
146 LUT = LUT3x1D(
147 table,
148 title,
149 np.array(
150 [
151 [domain_min, domain_min, domain_min],
152 [domain_max, domain_max, domain_max],
153 ]
154 ),
155 comments=comments,
156 )
158 return LUT
161def write_LUT_SonySPI1D(
162 LUT: LUT1D | LUT3x1D | LUTSequence, path: str | PathLike, decimals: int = 7
163) -> bool:
164 """
165 Write the specified *LUT* to the specified *Sony* *.spi1d* *LUT* file.
167 Parameters
168 ----------
169 LUT
170 :class:`LUT1D`, :class:`LUT3x1D` or :class:`LUTSequence` class
171 instance to write at the specified path.
172 path
173 *LUT* file path.
174 decimals
175 Number of decimal places for formatting numeric values.
177 Returns
178 -------
179 :class:`bool`
180 Whether the write operation was successful.
182 Warnings
183 --------
184 - If a :class:`LUTSequence` class instance is passed as ``LUT``,
185 the first *LUT* in the *LUT* sequence will be used.
187 Examples
188 --------
189 Writing a 1D *Sony* *.spi1d* *LUT*:
191 >>> from colour.algebra import spow
192 >>> domain = np.array([-0.1, 1.5])
193 >>> LUT = LUT1D(
194 ... spow(LUT1D.linear_table(16), 1 / 2.2),
195 ... "My LUT",
196 ... domain,
197 ... comments=["A first comment.", "A second comment."],
198 ... )
199 >>> write_LUT_SonySPI1D(LUT, "My_LUT.spi1d") # doctest: +SKIP
201 Writing a 3x1D *Sony* *.spi1d* *LUT*:
203 >>> domain = np.array([[-0.1, -0.1, -0.1], [1.5, 1.5, 1.5]])
204 >>> LUT = LUT3x1D(
205 ... spow(LUT3x1D.linear_table(16), 1 / 2.2),
206 ... "My LUT",
207 ... domain,
208 ... comments=["A first comment.", "A second comment."],
209 ... )
210 >>> write_LUT_SonySPI1D(LUT, "My_LUT.spi1d") # doctest: +SKIP
211 """
213 path = str(path)
215 if isinstance(LUT, LUTSequence):
216 usage_warning(
217 f'"LUT" is a "LUTSequence" instance was passed, using first '
218 f'sequence "LUT":\n{LUT}'
219 )
220 LUTxD = LUT[0]
221 else:
222 LUTxD = LUT
224 attest(not LUTxD.is_domain_explicit(), '"LUT" domain must be implicit!')
226 attest(
227 isinstance(LUTxD, (LUT1D, LUT3x1D)),
228 '"LUT" must be either a 1D or 3x1D "LUT"!',
229 )
231 is_1D = isinstance(LUTxD, LUT1D)
233 if is_1D:
234 domain = LUTxD.domain
235 else:
236 domain = np.unique(LUTxD.domain)
238 attest(len(domain) == 2, 'Non-uniform "LUT" domain is unsupported!')
240 with open(path, "w") as spi1d_file:
241 spi1d_file.write("Version 1\n")
243 spi1d_file.write(f"From {format_array_as_row(domain, decimals)}\n")
245 spi1d_file.write(
246 f"Length {LUTxD.table.size if is_1D else LUTxD.table.shape[0]}\n"
247 )
249 spi1d_file.write(f"Components {1 if is_1D else 3}\n")
251 spi1d_file.write("{\n")
252 spi1d_file.writelines(
253 f" {format_array_as_row(array, decimals)}\n" for array in LUTxD.table
254 )
255 spi1d_file.write("}\n")
257 if LUTxD.comments:
258 spi1d_file.writelines(f"# {comment}\n" for comment in LUTxD.comments)
260 return True