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)