"""
Sony .spi1d LUT Format Input / Output Utilities
===============================================
Define the *Sony* *.spi1d* *LUT* format related input / output utilities
objects:
- :func:`colour.io.read_LUT_SonySPI1D`
- :func:`colour.io.write_LUT_SonySPI1D`
"""
from __future__ import annotations
from pathlib import Path
import numpy as np
from colour.io.luts import LUT1D, LUT3x1D, LUTSequence
from colour.io.luts.common import path_to_title
from colour.utilities import (
as_float_array,
as_int_scalar,
attest,
format_array_as_row,
usage_warning,
)
__author__ = "Colour Developers"
__copyright__ = "Copyright 2013 Colour Developers"
__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
__maintainer__ = "Colour Developers"
__email__ = "colour-developers@colour-science.org"
__status__ = "Production"
__all__ = [
"read_LUT_SonySPI1D",
"write_LUT_SonySPI1D",
]
[docs]
def read_LUT_SonySPI1D(path: str | Path) -> LUT1D | LUT3x1D:
"""
Read given *Sony* *.spi1d* *LUT* file.
Parameters
----------
path
*LUT* path.
Returns
-------
:class:`colour.LUT1D` or :class:`colour.LUT3x1D`
:class:`LUT1D` or :class:`LUT3x1D` class instance.
Examples
--------
Reading a 1D *Sony* *.spi1d* *LUT*:
>>> import os
>>> path = os.path.join(
... os.path.dirname(__file__),
... "tests",
... "resources",
... "sony_spi1d",
... "eotf_sRGB_1D.spi1d",
... )
>>> print(read_LUT_SonySPI1D(path))
LUT1D - eotf sRGB 1D
--------------------
<BLANKLINE>
Dimensions : 1
Domain : [-0.1 1.5]
Size : (16,)
Comment 01 : Generated by "Colour 0.3.11".
Comment 02 : "colour.models.eotf_sRGB".
Reading a 3x1D *Sony* *.spi1d* *LUT*:
>>> path = os.path.join(
... os.path.dirname(__file__),
... "tests",
... "resources",
... "sony_spi1d",
... "eotf_sRGB_3x1D.spi1d",
... )
>>> print(read_LUT_SonySPI1D(path))
LUT3x1D - eotf sRGB 3x1D
------------------------
<BLANKLINE>
Dimensions : 2
Domain : [[-0.1 -0.1 -0.1]
[ 1.5 1.5 1.5]]
Size : (16, 3)
Comment 01 : Generated by "Colour 0.3.11".
Comment 02 : "colour.models.eotf_sRGB".
"""
title = path_to_title(path)
domain_min, domain_max = np.array([0, 1])
dimensions = 1
data = []
comments = []
with open(path) as spi1d_file:
lines = filter(None, (line.strip() for line in spi1d_file.readlines()))
for line in lines:
if line.startswith("#"):
comments.append(line[1:].strip())
continue
tokens = line.split()
if tokens[0] == "Version":
continue
if tokens[0] == "From":
domain_min, domain_max = as_float_array(tokens[1:])
elif tokens[0] == "Length":
continue
elif tokens[0] == "Components":
component = as_int_scalar(tokens[1])
attest(
component in (1, 3),
"Only 1 or 3 components are supported!",
)
dimensions = 1 if component == 1 else 2
elif tokens[0] in ("{", "}"):
continue
else:
data.append(tokens)
table = as_float_array(data)
LUT: LUT1D | LUT3x1D
if dimensions == 1:
LUT = LUT1D(
np.squeeze(table),
title,
np.array([domain_min, domain_max]),
comments=comments,
)
elif dimensions == 2:
LUT = LUT3x1D(
table,
title,
np.array(
[
[domain_min, domain_min, domain_min],
[domain_max, domain_max, domain_max],
]
),
comments=comments,
)
return LUT
[docs]
def write_LUT_SonySPI1D(
LUT: LUT1D | LUT3x1D | LUTSequence, path: str | Path, decimals: int = 7
) -> bool:
"""
Write given *LUT* to given *Sony* *.spi1d* *LUT* file.
Parameters
----------
LUT
:class:`LUT1D`, :class:`LUT3x1D` or :class:`LUTSequence` class instance
to write at given path.
path
*LUT* path.
decimals
Formatting decimals.
Returns
-------
:class:`bool`
Definition success.
Warnings
--------
- If a :class:`LUTSequence` class instance is passed as ``LUT``, the
first *LUT* in the *LUT* sequence will be used.
Examples
--------
Writing a 1D *Sony* *.spi1d* *LUT*:
>>> from colour.algebra import spow
>>> domain = np.array([-0.1, 1.5])
>>> LUT = LUT1D(
... spow(LUT1D.linear_table(16), 1 / 2.2),
... "My LUT",
... domain,
... comments=["A first comment.", "A second comment."],
... )
>>> write_LUT_SonySPI1D(LUT, "My_LUT.cube") # doctest: +SKIP
Writing a 3x1D *Sony* *.spi1d* *LUT*:
>>> domain = np.array([[-0.1, -0.1, -0.1], [1.5, 1.5, 1.5]])
>>> LUT = LUT3x1D(
... spow(LUT3x1D.linear_table(16), 1 / 2.2),
... "My LUT",
... domain,
... comments=["A first comment.", "A second comment."],
... )
>>> write_LUT_SonySPI1D(LUT, "My_LUT.cube") # doctest: +SKIP
"""
path = str(path)
if isinstance(LUT, LUTSequence):
usage_warning(
f'"LUT" is a "LUTSequence" instance was passed, using first '
f'sequence "LUT":\n{LUT}'
)
LUTxD = LUT[0]
else:
LUTxD = LUT
attest(not LUTxD.is_domain_explicit(), '"LUT" domain must be implicit!')
attest(
isinstance(LUTxD, (LUT1D, LUT3x1D)),
'"LUT" must be either a 1D or 3x1D "LUT"!',
)
is_1D = isinstance(LUTxD, LUT1D)
if is_1D:
domain = LUTxD.domain
else:
domain = np.unique(LUTxD.domain)
attest(len(domain) == 2, 'Non-uniform "LUT" domain is unsupported!')
with open(path, "w") as spi1d_file:
spi1d_file.write("Version 1\n")
spi1d_file.write(f"From {format_array_as_row(domain, decimals)}\n")
spi1d_file.write(
f"Length {LUTxD.table.size if is_1D else LUTxD.table.shape[0]}\n"
)
spi1d_file.write(f"Components {1 if is_1D else 3}\n")
spi1d_file.write("{\n")
for array in LUTxD.table:
spi1d_file.write(f" {format_array_as_row(array, decimals)}\n")
spi1d_file.write("}\n")
if LUTxD.comments:
for comment in LUTxD.comments:
spi1d_file.write(f"# {comment}\n")
return True