Coverage for io/luts/sony_spi1d.py: 78%

1"""

2 Sony .spi1d LUT Format Input / Output Utilities

3===============================================

4

5 Define the *Sony* *.spi1d* *LUT* format related input / output utilities

6 objects:

7

8- :func:`colour.io.read_LUT_SonySPI1D`

9- :func:`colour.io.write_LUT_SonySPI1D`

10"""

11

12 from __future__ import annotations

13

14 import typing

15

16 import numpy as np

17

18 if typing.TYPE_CHECKING:

19 from colour.hints import PathLike

20

21 from colour.io.luts import LUT1D, LUT3x1D, LUTSequence

22 from colour.io.luts.common import path_to_title

23 from colour.utilities import (

24 as_float_array,

25 as_int_scalar,

26 attest,

27 format_array_as_row,

28 usage_warning,

29)

30

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"

37

38 __all__ = [

39 "read_LUT_SonySPI1D",

40 "write_LUT_SonySPI1D",

41]

42

43

44 def read_LUT_SonySPI1D(path: str | PathLike) -> LUT1D | LUT3x1D:

45 """

46 Read the specified *Sony* *.spi1d* *LUT* file.

47

48 Parameters

49 ----------

50 path

51 *LUT* file path.

52

53 Returns

54 -------

55 :class:`colour.LUT1D` or :class:`colour.LUT3x1D`

56 :class:`LUT1D` or :class:`LUT3x1D` class instance.

57

58 Examples

59 --------

60 Reading a 1D *Sony* *.spi1d* *LUT*:

61

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".

79

80 Reading a 3x1D *Sony* *.spi1d* *LUT*:

81

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 """

100

101 title = path_to_title(path)

102 domain_min, domain_max = np.array([0, 1])

103 dimensions = 1

104 data = []

105

106 comments = []

107

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

114

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 )

128

129 dimensions = 1 if component == 1 else 2

130 elif tokens[0] in ("{", "}"):

131 continue

132 else:

133 data.append(tokens)

134

135 table = as_float_array(data)

136

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 )

157

158 return LUT

159

160

161 def 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.

166

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.

176

177 Returns

178 -------

179 :class:`bool`

180 Whether the write operation was successful.

181

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.

186

187 Examples

188 --------

189 Writing a 1D *Sony* *.spi1d* *LUT*:

190

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

200

201 Writing a 3x1D *Sony* *.spi1d* *LUT*:

202

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 """

212

213 path = str(path)

214

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

223

224 attest(not LUTxD.is_domain_explicit(), '"LUT" domain must be implicit!')

225

226 attest(

227 isinstance(LUTxD, (LUT1D, LUT3x1D)),

228 '"LUT" must be either a 1D or 3x1D "LUT"!',

229 )

230

231 is_1D = isinstance(LUTxD, LUT1D)

232

233 if is_1D:

234 domain = LUTxD.domain

235 else:

236 domain = np.unique(LUTxD.domain)

237

238 attest(len(domain) == 2, 'Non-uniform "LUT" domain is unsupported!')

239

240 with open(path, "w") as spi1d_file:

241 spi1d_file.write("Version 1\n")

242

243 spi1d_file.write(f"From {format_array_as_row(domain, decimals)}\n")

244

245 spi1d_file.write(

246 f"Length {LUTxD.table.size if is_1D else LUTxD.table.shape[0]}\n"

247 )

248

249 spi1d_file.write(f"Components {1 if is_1D else 3}\n")

250

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")

256

257 if LUTxD.comments:

258 spi1d_file.writelines(f"# {comment}\n" for comment in LUTxD.comments)

259

260 return True