diff --git a/colour_clf_io/__init__.py b/colour_clf_io/__init__.py
index 84f6c38..73d9757 100644
--- a/colour_clf_io/__init__.py
+++ b/colour_clf_io/__init__.py
@@ -127,7 +127,11 @@ def read_clf(path: str | Path) -> ProcessList:
xml = lxml.etree.parse(str(path)) # noqa: S320
xml_process_list = xml.getroot()
- return ProcessList.from_xml(xml_process_list)
+ process_list = ProcessList.from_xml(xml_process_list)
+ if process_list is None:
+ err = "Process list could not be parsed."
+ raise ValueError(err)
+ return process_list
def parse_clf(text: str | bytes) -> ProcessList | None:
@@ -153,3 +157,29 @@ def parse_clf(text: str | bytes) -> ProcessList | None:
xml = lxml.etree.fromstring(text) # noqa: S320
return ProcessList.from_xml(xml)
+
+
+def write_clf(process_list: ProcessList, path: str | Path | None = None) -> None | str:
+ """
+ Write the given *ProcessList* as a CLF file to the target
+ location. If no *path* is given the CLF document will be returned as a string.
+
+ Parameters
+ ----------
+ process_list
+ *ProcessList* that should be written.
+ path
+ Location of the file, or *None* to return a string representation of the
+ CLF document.
+
+ Returns
+ -------
+ :class:`colour_clf_io.ProcessList`
+ """
+ xml = process_list.to_xml()
+ serialised = lxml.etree.tostring(xml)
+ if path is None:
+ return serialised.decode("utf-8")
+ with open(path, "wb") as f:
+ f.write(serialised)
+ return None
diff --git a/colour_clf_io/elements.py b/colour_clf_io/elements.py
index 503e807..b05f2ca 100644
--- a/colour_clf_io/elements.py
+++ b/colour_clf_io/elements.py
@@ -8,25 +8,28 @@
from __future__ import annotations
+import itertools
import typing
from dataclasses import dataclass
if typing.TYPE_CHECKING:
import numpy.typing as npt
-if typing.TYPE_CHECKING:
- import lxml.etree
+import lxml.etree
from colour_clf_io.errors import ParsingError
from colour_clf_io.parsing import (
ParserConfig,
XMLParsable,
+ XMLWritable,
check_none,
child_element,
child_element_or_exception,
map_optional,
retrieve_attributes,
retrieve_attributes_as_float,
+ set_attr_if_not_none,
+ set_element_if_not_none,
three_floats,
)
from colour_clf_io.values import Channel
@@ -50,7 +53,7 @@
@dataclass
-class Array(XMLParsable):
+class Array(XMLParsable, XMLWritable):
"""
Represent an *Array* element.
@@ -124,6 +127,27 @@ def from_xml(
return Array(values=values, dim=dimensions)
+ def to_xml(self) -> lxml.etree._Element:
+ """
+ Serialise this object as an XML object.
+
+ Returns
+ -------
+ :class:`lxml.etree._Element`
+ """
+ xml = lxml.etree.Element("Array")
+ xml.set("dim", " ".join(map(str, self.dim)))
+ if len(self.dim) <= 1:
+ xml.text = "\n".join(map(str, self.values))
+ else:
+ row_length = self.dim[-1]
+ text = "\n".join(
+ " ".join(map(str, row))
+ for row in itertools.batched(self.values, row_length)
+ )
+ xml.text = text
+ return xml
+
def as_array(self) -> npt.NDArray:
"""
Convert the *CLF* element into a numpy array.
@@ -144,7 +168,7 @@ def as_array(self) -> npt.NDArray:
@dataclass
-class CalibrationInfo(XMLParsable):
+class CalibrationInfo(XMLParsable, XMLWritable):
"""
Represent a *CalibrationInfo* container element for a
:class:`colour_clf_io.ProcessList` class instance.
@@ -227,9 +251,32 @@ def from_xml(
return CalibrationInfo(**attributes)
+ def to_xml(self) -> lxml.etree._Element:
+ """
+ Serialise this object as an XML object.
+
+ Returns
+ -------
+ :class:`lxml.etree._Element`
+ """
+ xml = lxml.etree.Element("CalibrationInfo")
+ set_attr_if_not_none(
+ xml, "DisplayDeviceSerialNum", self.display_device_serial_num
+ )
+ set_attr_if_not_none(
+ xml, "DisplayDeviceHostName", self.display_device_host_name
+ )
+ set_attr_if_not_none(xml, "OperatorName", self.operator_name)
+ set_attr_if_not_none(xml, "CalibrationDateTime", self.calibration_date_time)
+ set_attr_if_not_none(xml, "MeasurementProbe", self.measurement_probe)
+ set_attr_if_not_none(
+ xml, "CalibrationSoftwareName", self.calibration_software_name
+ )
+ return xml
+
@dataclass
-class SOPNode(XMLParsable):
+class SOPNode(XMLParsable, XMLWritable):
"""
Represent a *SOPNode* element for a :class:`colour_clf_io.ASC_CDL`
*Process Node*.
@@ -312,6 +359,20 @@ def from_xml(
return SOPNode(slope=slope, offset=offset, power=power)
+ def to_xml(self) -> lxml.etree._Element:
+ """
+ Serialise this object as an XML object.
+
+ Returns
+ -------
+ :class:`lxml.etree._Element`
+ """
+ xml = lxml.etree.Element("SOPNode")
+ set_element_if_not_none(xml, "Slope", " ".join(map(str, self.slope)))
+ set_element_if_not_none(xml, "Offset", " ".join(map(str, self.offset)))
+ set_element_if_not_none(xml, "Power", " ".join(map(str, self.power)))
+ return xml
+
@classmethod
def default(cls) -> SOPNode:
"""
@@ -331,7 +392,7 @@ def default(cls) -> SOPNode:
@dataclass
-class SatNode(XMLParsable):
+class SatNode(XMLParsable, XMLWritable):
"""
Represent a *SatNode* element for a :class:`colour_clf_io.ASC_CDL`
*Process Node*.
@@ -399,6 +460,18 @@ def from_xml(
return SatNode(saturation=saturation)
+ def to_xml(self) -> lxml.etree._Element:
+ """
+ Serialise this object as an XML object.
+
+ Returns
+ -------
+ :class:`lxml.etree._Element`
+ """
+ xml = lxml.etree.Element("SatNode")
+ set_element_if_not_none(xml, "Saturation", self.saturation)
+ return xml
+
@classmethod
def default(cls) -> SatNode:
"""
@@ -414,7 +487,7 @@ def default(cls) -> SatNode:
@dataclass
-class Info(XMLParsable):
+class Info(XMLParsable, XMLWritable):
"""
Represent an *Info* element.
@@ -520,9 +593,27 @@ def from_xml(xml: lxml.etree._Element | None, config: ParserConfig) -> Info | No
return Info(calibration_info=calibration_info, **attributes)
+ def to_xml(self) -> lxml.etree._Element:
+ """
+ Serialise this object as an XML object.
+
+ Returns
+ -------
+ :class:`lxml.etree._Element`
+ """
+ xml = lxml.etree.Element("Info")
+ set_attr_if_not_none(xml, "AppRelease", self.app_release)
+ set_attr_if_not_none(xml, "Copyright", self.copyright)
+ set_attr_if_not_none(xml, "Revision", self.revision)
+ set_attr_if_not_none(xml, "AcesTransformID", self.aces_transform_id)
+ set_attr_if_not_none(xml, "AcesUserName", self.aces_user_name)
+ if self.calibration_info is not None:
+ xml.append(self.calibration_info.to_xml())
+ return xml
+
@dataclass
-class LogParams(XMLParsable):
+class LogParams(XMLParsable, XMLWritable):
"""
Represent a *LogParams* element for a :class:`colour_clf_io.Log`
*Process Node*.
@@ -649,6 +740,26 @@ def from_xml(
return LogParams(channel=channel, **attributes)
+ def to_xml(self) -> lxml.etree._Element:
+ """
+ Serialise this object as an XML object.
+
+ Returns
+ -------
+ :class:`lxml.etree._Element`
+ """
+ xml = lxml.etree.Element("LogParams")
+ set_attr_if_not_none(xml, "base", self.base)
+ set_attr_if_not_none(xml, "logSideSlope", self.log_side_slope)
+ set_attr_if_not_none(xml, "logSideOffset", self.log_side_offset)
+ set_attr_if_not_none(xml, "linSideSlope", self.lin_side_slope)
+ set_attr_if_not_none(xml, "linSideOffset", self.lin_side_offset)
+ set_attr_if_not_none(xml, "linSideBreak", self.lin_side_break)
+ set_attr_if_not_none(xml, "linearSlope", self.linear_slope)
+ if self.channel is not None:
+ xml.set("channel", self.channel.value)
+ return xml
+
@classmethod
def default(cls) -> LogParams:
"""
@@ -673,7 +784,7 @@ def default(cls) -> LogParams:
@dataclass
-class ExponentParams(XMLParsable):
+class ExponentParams(XMLParsable, XMLWritable):
"""
Represent a *ExponentParams* element for a :class:`colour_clf_io.Exponent`
*Process Node*.
@@ -772,6 +883,21 @@ def from_xml(
return ExponentParams(channel=channel, exponent=exponent, **attributes)
+ def to_xml(self) -> lxml.etree._Element:
+ """
+ Serialise this object as an XML object.
+
+ Returns
+ -------
+ :class:`lxml.etree._Element`
+ """
+ xml = lxml.etree.Element("ExponentParams")
+ set_attr_if_not_none(xml, "exponent", self.exponent)
+ set_attr_if_not_none(xml, "offset", self.offset)
+ if self.channel is not None:
+ xml.set("channel", self.channel.value)
+ return xml
+
@classmethod
def default(cls) -> ExponentParams:
"""
diff --git a/colour_clf_io/parsing.py b/colour_clf_io/parsing.py
index 33675c4..07f0d03 100644
--- a/colour_clf_io/parsing.py
+++ b/colour_clf_io/parsing.py
@@ -18,8 +18,7 @@
from collections.abc import Callable, Iterable
from typing import Any
-if typing.TYPE_CHECKING:
- import lxml.etree
+import lxml.etree
from colour_clf_io.errors import ParsingError
@@ -34,6 +33,7 @@
"NAMESPACE_NAME",
"ParserConfig",
"XMLParsable",
+ "XMLWritable",
"map_optional",
"retrieve_attributes",
"retrieve_attributes_as_float",
@@ -120,6 +120,29 @@ def from_xml(
"""
+class XMLWritable(ABC):
+ """
+ Define the base class for objects that can be serialised to XML.
+
+ This is an :class:`ABCMeta` abstract class that must be inherited by
+ sub-classes.
+
+ Methods
+ -------
+ - :meth:`~colour_lf_io.parsing.XMLParsable.to_xml`
+ """
+
+ @abstractmethod
+ def to_xml(self) -> lxml.etree._Element:
+ """
+ Serialise this object as an XML object.
+
+ Returns
+ -------
+ :class:`lxml.etree._Element`
+ """
+
+
def map_optional(function: Callable, value: Any | None) -> Any:
"""
Apply the given function to given ``value`` if ``value`` is not ``None``.
@@ -490,3 +513,14 @@ def three_floats(text: str | None) -> tuple[float, float, float]:
values = tuple(map(float, parts))
# Note: Repacking here to satisfy type check.
return values[0], values[1], values[2]
+
+
+def set_attr_if_not_none(node: lxml.etree._Element, attr: str, value: Any) -> None:
+ if value is not None:
+ node.set(attr, str(value))
+
+
+def set_element_if_not_none(node: lxml.etree._Element, name: str, value: Any) -> None:
+ if value is not None and value != "":
+ child = lxml.etree.SubElement(node, name)
+ child.text = str(value)
diff --git a/colour_clf_io/process_list.py b/colour_clf_io/process_list.py
index 8e05170..964a93c 100644
--- a/colour_clf_io/process_list.py
+++ b/colour_clf_io/process_list.py
@@ -19,6 +19,8 @@
check_none,
element_as_text,
elements_as_text_list,
+ set_attr_if_not_none,
+ set_element_if_not_none,
)
from colour_clf_io.process_nodes import (
ProcessNode,
@@ -200,3 +202,29 @@ def from_xml(xml: lxml.etree._Element | None) -> ProcessList | None:
info=info,
description=description,
)
+
+ def to_xml(self) -> lxml.etree._Element:
+ """
+ Serialise this object as an XML object.
+
+ Returns
+ -------
+ :class:`lxml.etree._Element`
+ """
+ xml = lxml.etree.Element("ProcessList")
+ set_attr_if_not_none(xml, "id", self.id)
+ set_attr_if_not_none(xml, "compCLFversion", self.compatible_CLF_version)
+ set_attr_if_not_none(xml, "name", self.name)
+ set_attr_if_not_none(xml, "inverseOf", self.inverse_of)
+ set_element_if_not_none(xml, "InputDescriptor", self.input_descriptor)
+ set_element_if_not_none(xml, "OutputDescriptor", self.output_descriptor)
+ if self.info:
+ xml.append(self.info.to_xml())
+ for description_text in self.description:
+ description_element = lxml.etree.SubElement(xml, "Description")
+ description_element.text = description_text
+ # TODO: we might have to store a single list of children in order to preserve
+ # ordering of description and process nodes
+ for process_node in self.process_nodes:
+ xml.append(process_node.to_xml())
+ return xml
diff --git a/colour_clf_io/process_nodes.py b/colour_clf_io/process_nodes.py
index 6a0353f..2553213 100644
--- a/colour_clf_io/process_nodes.py
+++ b/colour_clf_io/process_nodes.py
@@ -27,12 +27,15 @@
from colour_clf_io.parsing import (
ParserConfig,
XMLParsable,
+ XMLWritable,
child_element,
child_elements,
element_as_float,
elements_as_text_list,
map_optional,
retrieve_attributes,
+ set_attr_if_not_none,
+ set_element_if_not_none,
sliding_window,
)
from colour_clf_io.values import (
@@ -95,7 +98,7 @@ def register(constructor: Callable) -> Callable:
@dataclass
-class ProcessNode(XMLParsable, ABC):
+class ProcessNode(XMLParsable, XMLWritable, ABC):
"""
Represent a *ProcessNode*, an operation to be applied to the image data.
@@ -173,6 +176,25 @@ def parse_attributes(xml: lxml.etree._Element, config: ParserConfig) -> dict:
**attributes,
}
+ def write_process_node_attributes(self, node: lxml.etree._Element) -> None:
+ """
+ Add the data of the *ProcessNode* as attributes to the given XML node.
+
+ Parameters
+ ----------
+ node
+ Target node that will receive the new attributes.
+ """
+ set_attr_if_not_none(node, "id", self.id)
+ set_attr_if_not_none(node, "name", self.name)
+ set_attr_if_not_none(node, "inBitDepth", self.in_bit_depth.value)
+ set_attr_if_not_none(node, "outBitDepth", self.out_bit_depth.value)
+ if self.description is None:
+ return
+ for description_text in self.description:
+ description_element = lxml.etree.SubElement(node, "Description")
+ description_element.text = description_text
+
def assert_bit_depth_compatibility(process_nodes: list[ProcessNode]) -> bool:
"""
@@ -330,6 +352,25 @@ def from_xml(xml: lxml.etree._Element | None, config: ParserConfig) -> LUT1D | N
**super_args,
)
+ def to_xml(self) -> lxml.etree._Element:
+ """
+ Serialise this object as an XML object.
+
+ Returns
+ -------
+ :class:`lxml.etree._Element`
+ """
+ xml = lxml.etree.Element("LUT1D")
+ self.write_process_node_attributes(xml)
+ if self.half_domain:
+ xml.set("halfDomain", "true")
+ if self.raw_halfs:
+ xml.set("rawHalfs", "true")
+ if self.interpolation is not None:
+ xml.set("interpolation", self.interpolation.value)
+ xml.append(self.array.to_xml())
+ return xml
+
@dataclass
class LUT3D(ProcessNode):
@@ -399,6 +440,25 @@ def from_xml(xml: lxml.etree._Element | None, config: ParserConfig) -> LUT3D | N
**super_args,
)
+ def to_xml(self) -> lxml.etree._Element:
+ """
+ Serialise this object as an XML object.
+
+ Returns
+ -------
+ :class:`lxml.etree._Element`
+ """
+ xml = lxml.etree.Element("LUT3D")
+ self.write_process_node_attributes(xml)
+ if self.half_domain:
+ xml.set("halfDomain", "true")
+ if self.raw_halfs:
+ xml.set("rawHalfs", "true")
+ if self.interpolation is not None:
+ xml.set("interpolation", self.interpolation.value)
+ xml.append(self.array.to_xml())
+ return xml
+
@dataclass
class Matrix(ProcessNode):
@@ -452,11 +512,23 @@ def from_xml(
if array is None:
exception = "Matrix processing node does not have an Array element."
-
raise ParsingError(exception)
return Matrix(array=array, **super_args)
+ def to_xml(self) -> lxml.etree._Element:
+ """
+ Serialise this object as an XML object.
+
+ Returns
+ -------
+ :class:`lxml.etree._Element`
+ """
+ xml = lxml.etree.Element("Matrix")
+ self.write_process_node_attributes(xml)
+ xml.append(self.array.to_xml())
+ return xml
+
@dataclass
class Range(ProcessNode):
@@ -531,6 +603,24 @@ def optional_float(name: str) -> float | None:
**super_args,
)
+ def to_xml(self) -> lxml.etree._Element:
+ """
+ Serialise this object as an XML object.
+
+ Returns
+ -------
+ :class:`lxml.etree._Element`
+ """
+ xml = lxml.etree.Element("Range")
+ self.write_process_node_attributes(xml)
+ set_element_if_not_none(xml, "minInValue", self.min_in_value)
+ set_element_if_not_none(xml, "maxInValue", self.max_in_value)
+ set_element_if_not_none(xml, "minOutValue", self.min_out_value)
+ set_element_if_not_none(xml, "maxOutValue", self.max_out_value)
+ if self.style is not None:
+ xml.set("style", self.style.value)
+ return xml
+
@dataclass
class Log(ProcessNode):
@@ -592,6 +682,21 @@ def from_xml(xml: lxml.etree._Element | None, config: ParserConfig) -> Log | Non
return Log(style=style, log_params=params, **super_args)
+ def to_xml(self) -> lxml.etree._Element:
+ """
+ Serialise this object as an XML object.
+
+ Returns
+ -------
+ :class:`lxml.etree._Element`
+ """
+ xml = lxml.etree.Element("Log")
+ self.write_process_node_attributes(xml)
+ xml.set("style", self.style.value)
+ for log_params in self.log_params:
+ xml.append(log_params.to_xml())
+ return xml
+
@dataclass
class Exponent(ProcessNode):
@@ -666,6 +771,21 @@ def from_xml(
return Exponent(style=style, exponent_params=params, **super_args)
+ def to_xml(self) -> lxml.etree._Element:
+ """
+ Serialise this object as an XML object.
+
+ Returns
+ -------
+ :class:`lxml.etree._Element`
+ """
+ xml = lxml.etree.Element("Exponent")
+ self.write_process_node_attributes(xml)
+ xml.set("style", self.style.value)
+ for exponent_params in self.exponent_params:
+ xml.append(exponent_params.to_xml())
+ return xml
+
@dataclass
class ASC_CDL(ProcessNode):
@@ -722,3 +842,20 @@ def from_xml(
sat_node = SatNode.from_xml(child_element(xml, "SatNode", config), config)
return ASC_CDL(style=style, sopnode=sop_node, sat_node=sat_node, **super_args)
+
+ def to_xml(self) -> lxml.etree._Element:
+ """
+ Serialise this object as an XML object.
+
+ Returns
+ -------
+ :class:`lxml.etree._Element`
+ """
+ xml = lxml.etree.Element("ASC_CDL")
+ self.write_process_node_attributes(xml)
+ xml.set("style", self.style.value)
+ if self.sat_node is not None:
+ xml.append(self.sat_node.to_xml())
+ if self.sopnode is not None:
+ xml.append(self.sopnode.to_xml())
+ return xml
diff --git a/colour_clf_io/tests/test_clf_common.py b/colour_clf_io/tests/test_clf_common.py
index 0de0100..3e69fe9 100644
--- a/colour_clf_io/tests/test_clf_common.py
+++ b/colour_clf_io/tests/test_clf_common.py
@@ -37,7 +37,7 @@
""".strip()
-def wrap_snippet(snippet: str) -> str:
+def wrap_snippet(snippet: str | bytes) -> str:
"""
Take a string that should contain the text representation of a *CLF* node, and
returns valid *CLF* file. Essentially the given string is pasted into the
diff --git a/colour_clf_io/tests/test_clf_writing.py b/colour_clf_io/tests/test_clf_writing.py
new file mode 100644
index 0000000..49f1e29
--- /dev/null
+++ b/colour_clf_io/tests/test_clf_writing.py
@@ -0,0 +1,415 @@
+# !/usr/bin/env python
+"""Define the unit tests for the :mod:`colour.io.clf` module."""
+
+from __future__ import annotations
+
+import os
+
+import colour_clf_io.elements
+import colour_clf_io.process_nodes
+import colour_clf_io.values
+from colour_clf_io import parse_clf, write_clf
+
+from .test_clf_common import wrap_snippet
+
+__author__ = "Colour Developers"
+__copyright__ = "Copyright 2024 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__ = [
+ "ROOT_CLF",
+ "EXAMPLE_WRAPPER",
+ "TestWriteCLF",
+]
+
+ROOT_CLF: str = os.path.join(os.path.dirname(__file__), "resources")
+
+EXAMPLE_WRAPPER: str = """
+
+{0}
+
+"""
+
+
+def assert_valid_roundtrip_for_snippet(example: str) -> None:
+ doc_original = wrap_snippet(example)
+ assert_valid_roundtrip_for_doc(doc_original)
+
+
+def assert_valid_roundtrip_for_file(path: str) -> None:
+ with open(path) as f:
+ file_content = f.read()
+ assert_valid_roundtrip_for_doc(file_content)
+
+
+def assert_valid_roundtrip_for_doc(doc: str) -> None:
+ doc_original = parse_clf(doc)
+ assert doc_original is not None
+ xml = write_clf(doc_original)
+ assert xml is not None
+ doc_after_roundtrip = parse_clf(xml)
+ assert doc_original == doc_after_roundtrip
+
+
+class TestWriteCLF:
+ """
+ Define tests methods for parsing *CLF* files using the functionality
+ provided in the :mod: `colour.io.clf`module.
+ """
+
+ def test_sample_document_1(self) -> None:
+ """
+ Test parsing of the sample file `ACES2065_1_to_ACEScct.xml`.
+ """
+
+ path = os.path.join(ROOT_CLF, "ACES2065_1_to_ACEScct.xml")
+ assert_valid_roundtrip_for_file(path)
+
+ def test_read_sample_document_2(self) -> None:
+ """
+ Test parsing of the sample file `LMT Kodak 2383 Print Emulation.xml`.
+ """
+ path = os.path.join(ROOT_CLF, "LMT Kodak 2383 Print Emulation.xml")
+ assert_valid_roundtrip_for_file(path)
+
+ def test_read_sample_document_3(self) -> None:
+ """
+ Test parsing of the sample file `LMT_ARRI_K1S1_709_EI800_v3.xml`.
+ """
+
+ path = os.path.join(ROOT_CLF, "LMT_ARRI_K1S1_709_EI800_v3.xml")
+ assert_valid_roundtrip_for_file(path)
+
+ def test_LUT1D_example(self) -> None:
+ """
+ Test parsing of the example process node from the official *CLF* specification
+ Example 1.
+ """
+
+ example = """
+
+ 1D LUT - Turn 4 grey levels into 4 inverted codes
+
+ 3
+ 2
+ 1
+ 0
+
+
+ """
+ assert_valid_roundtrip_for_snippet(example)
+
+ def test_LUT3D_example(self) -> None:
+ """
+ Test parsing of the example process node from the official *CLF* specification
+ Example 2.
+ """
+
+ example = """
+
+ 3D LUT
+
+ 0.0 0.0 0.0
+ 0.0 0.0 1.0
+ 0.0 1.0 0.0
+ 0.0 1.0 1.0
+ 1.0 0.0 0.0
+ 1.0 0.0 1.0
+ 1.0 1.0 0.0
+ 1.0 1.0 1.0
+
+
+ """ # noqa: E501
+
+ assert_valid_roundtrip_for_snippet(example)
+
+ def test_matrix_example_1(self) -> None:
+ """
+ Test parsing of the example process node from the official *CLF* specification
+ Example 3.
+ """
+
+ example = """
+
+ 3x3 color space conversion from AP0 to AP1
+
+ 1.45143931614567 -0.236510746893740 -0.214928569251925
+ -0.0765537733960204 1.17622969983357 -0.0996759264375522
+ 0.00831614842569772 -0.00603244979102103 0.997716301365324
+
+
+ """
+
+ assert_valid_roundtrip_for_snippet(example)
+
+ def test_matrix_example_2(self) -> None:
+ """
+ Test parsing of the example process node from the official *CLF* specification
+ Example 4.
+ """
+
+ example = """
+
+ 3x4 Matrix , 4th column is offset
+
+ 1.2 0.0 0.0 0.002
+ 0.0 1.03 0.001 -0.005
+ 0.004 -0.007 1.004 0.0
+
+
+ """ # noqa: E501
+
+ assert_valid_roundtrip_for_snippet(example)
+
+ def test_range_example(self) -> None:
+ """
+ Test parsing of the example process node from the official *CLF* specification
+ Example 5.
+ """
+
+ example = """
+
+ 10-bit full range to SMPTE range
+ 0
+ 1023
+ 64
+ 940
+
+ """
+
+ assert_valid_roundtrip_for_snippet(example)
+
+ def test_log_example_1(self) -> None:
+ """
+ Test parsing of the example process node from the official *CLF* specification
+ Example 6.
+ """
+
+ example = """
+
+ Base 10 Logarithm
+
+ """
+
+ assert_valid_roundtrip_for_snippet(example)
+
+ def test_log_example_2(self) -> None:
+ """
+ Test parsing of the example process node from the official *CLF* specification
+ Example 7.
+ """
+
+ example = """
+
+ Linear to DJI D-Log
+
+
+ """
+
+ assert_valid_roundtrip_for_snippet(example)
+
+ def test_exponent_example_1(self) -> None:
+ """
+ Test parsing of the example process node from the official *CLF* specification
+ Example 8.
+ """
+
+ example = """
+
+ Basic 2.2 Gamma
+
+
+ """
+
+ assert_valid_roundtrip_for_snippet(example)
+
+ def test_exponent_example_2(self) -> None:
+ """
+ Test parsing of the example process node from the official *CLF* specification
+ Example 9.
+ """
+
+ example = """
+
+ EOTF (sRGB)
+
+
+ """
+
+ assert_valid_roundtrip_for_snippet(example)
+
+ def test_exponent_example_3(self) -> None:
+ """
+ Test parsing of the example process node from the official *CLF* specification
+ Example 10.
+ """
+
+ example = """
+
+ CIE L*
+
+
+ """
+
+ assert_valid_roundtrip_for_snippet(example)
+
+ def test_exponent_example_4(self) -> None:
+ """
+ Test parsing of the example process node from the official *CLF* specification
+ Example 11.
+ """
+
+ example = """
+
+ Rec. 709 OETF
+
+
+ """
+
+ assert_valid_roundtrip_for_snippet(example)
+
+ def test_ASC_CDL_example(self) -> None:
+ """
+ Test parsing of the example process node from the official *CLF* specification
+ Example 12.
+ """
+
+ example = """
+
+ scene 1 exterior look
+
+ 1.000000 1.000000 0.900000
+ -0.030000 -0.020000 0.000000
+ 1.2500000 1.000000 1.000000
+
+
+ 1.700000
+
+
+ """
+
+ assert_valid_roundtrip_for_snippet(example)
+
+ def test_ACES2065_1_to_ACEScg_example(self) -> None:
+ """
+ Test parsing of the example process node from the official *CLF* specification
+ Example 13.
+ """
+
+ # Note that this string uses binary encoding, as the XML document specifies its
+ # own encoding.
+ example = b"""
+
+
+ ACEScsc.ACES_to_ACEScg.a1.0.3
+ ACES2065-1 to ACEScg
+
+ ACES2065-1 to ACEScg
+ ACES2065-1
+ ACEScg
+
+
+ 1.451439316146 -0.236510746894 -0.214928569252
+ -0.076553773396 1.176229699834 -0.099675926438
+ 0.008316148426 -0.006032449791 0.997716301365
+
+
+
+ """
+
+ doc = parse_clf(example)
+
+ assert doc is not None
+
+ assert len(doc.process_nodes) == 1
+ assert isinstance(doc.process_nodes[0], colour_clf_io.process_nodes.Matrix)
+
+ def test_ACES2065_1_to_ACEScct_example(self) -> None:
+ """
+ Test parsing of the example process node from the official *CLF* specification
+ Example 14.
+ """
+
+ # Note that this string uses binary encoding, as the XML document specifies its
+ # own encoding.
+ example = b"""
+
+ ACES2065-1 to ACEScct Log working space
+ Academy Color Encoding Specification (ACES2065-1)
+ ACEScct Log working space
+
+ ACEScsc.ACES_to_ACEScct.a1.0.3
+ ACES2065-1 to ACEScct
+
+
+
+ 1.451439316146 -0.236510746894 -0.214928569252
+ -0.076553773396 1.176229699834 -0.099675926438
+ 0.008316148426 -0.006032449791 0.997716301365
+
+
+
+
+
+
+ """ # noqa: E501
+
+ doc = parse_clf(example)
+
+ assert doc is not None
+
+ assert len(doc.process_nodes) == 2
+ assert isinstance(doc.process_nodes[0], colour_clf_io.process_nodes.Matrix)
+ assert isinstance(doc.process_nodes[1], colour_clf_io.process_nodes.Log)
+
+ def test_CIE_XYZ_to_CIELAB_example(self) -> None:
+ """
+ Test parsing of the example process node from the official *CLF* specification
+ Example 14.
+ """
+
+ # Note that this string uses binary encoding, as the XML document specifies its
+ # own encoding.
+ example = b"""
+
+ CIE-XYZ D65 to CIELAB L*, a*, b* (scaled by 1/100, neutrals at
+ 0.0 chroma)
+ CIE-XYZ, D65 white (scaled [0,1])
+ CIELAB L*, a*, b* (scaled by 1/100, neutrals at 0.0
+ chroma)
+
+
+ 1.052126639 0.000000000 0.000000000
+ 0.000000000 1.000000000 0.000000000
+ 0.000000000 0.000000000 0.918224951
+
+
+
+
+
+
+
+ 0.00000000 1.00000000 0.00000000
+ 4.31034483 -4.31034483 0.00000000
+ 0.00000000 1.72413793 -1.72413793
+
+
+
+ """
+
+ doc = parse_clf(example)
+
+ assert doc is not None
+
+ assert len(doc.process_nodes) == 3
+ assert isinstance(doc.process_nodes[0], colour_clf_io.process_nodes.Matrix)
+ assert isinstance(doc.process_nodes[1], colour_clf_io.process_nodes.Exponent)
+ assert isinstance(doc.process_nodes[2], colour_clf_io.process_nodes.Matrix)