88
99from __future__ import annotations
1010
11+ import itertools
1112import typing
1213from dataclasses import dataclass
1314
1415if typing .TYPE_CHECKING :
1516 import numpy .typing as npt
1617
17- if typing .TYPE_CHECKING :
18- import lxml .etree
18+ import lxml .etree
1919
2020from colour_clf_io .errors import ParsingError
2121from colour_clf_io .parsing import (
2222 ParserConfig ,
2323 XMLParsable ,
24+ XMLWritable ,
2425 check_none ,
2526 child_element ,
2627 child_element_or_exception ,
2728 map_optional ,
2829 retrieve_attributes ,
2930 retrieve_attributes_as_float ,
31+ set_attr_if_not_none ,
32+ set_element_if_not_none ,
3033 three_floats ,
3134)
3235from colour_clf_io .values import Channel
5053
5154
5255@dataclass
53- class Array (XMLParsable ):
56+ class Array (XMLParsable , XMLWritable ):
5457 """
5558 Represent an *Array* element.
5659
@@ -124,6 +127,20 @@ def from_xml(
124127
125128 return Array (values = values , dim = dimensions )
126129
130+ def to_xml (self ) -> lxml .etree ._Element :
131+ xml = lxml .etree .Element ("Array" )
132+ xml .set ("dim" , " " .join (map (str , self .dim )))
133+ if len (self .dim ) <= 1 :
134+ xml .text = "\n " .join (self .values )
135+ else :
136+ row_length = self .dim [- 1 ]
137+ text = "\n " .join (
138+ " " .join (map (str , row ))
139+ for row in itertools .batched (self .values , row_length )
140+ )
141+ xml .text = text
142+ return xml
143+
127144 def as_array (self ) -> npt .NDArray :
128145 """
129146 Convert the *CLF* element into a numpy array.
@@ -144,7 +161,7 @@ def as_array(self) -> npt.NDArray:
144161
145162
146163@dataclass
147- class CalibrationInfo (XMLParsable ):
164+ class CalibrationInfo (XMLParsable , XMLWritable ):
148165 """
149166 Represent a *CalibrationInfo* container element for a
150167 :class:`colour_clf_io.ProcessList` class instance.
@@ -227,9 +244,25 @@ def from_xml(
227244
228245 return CalibrationInfo (** attributes )
229246
247+ def to_xml (self ) -> lxml .etree ._Element :
248+ xml = lxml .etree .Element ("CalibrationInfo" )
249+ set_attr_if_not_none (
250+ xml , "DisplayDeviceSerialNum" , self .display_device_serial_num
251+ )
252+ set_attr_if_not_none (
253+ xml , "DisplayDeviceHostName" , self .display_device_host_name
254+ )
255+ set_attr_if_not_none (xml , "OperatorName" , self .operator_name )
256+ set_attr_if_not_none (xml , "CalibrationDateTime" , self .calibration_date_time )
257+ set_attr_if_not_none (xml , "MeasurementProbe" , self .measurement_probe )
258+ set_attr_if_not_none (
259+ xml , "CalibrationSoftwareName" , self .calibration_software_name
260+ )
261+ return xml
262+
230263
231264@dataclass
232- class SOPNode (XMLParsable ):
265+ class SOPNode (XMLParsable , XMLWritable ):
233266 """
234267 Represent a *SOPNode* element for a :class:`colour_clf_io.ASC_CDL`
235268 *Process Node*.
@@ -312,6 +345,13 @@ def from_xml(
312345
313346 return SOPNode (slope = slope , offset = offset , power = power )
314347
348+ def to_xml (self ) -> lxml .etree ._Element :
349+ xml = lxml .etree .Element ("SOPNode" )
350+ set_element_if_not_none (xml , "Slope" , " " .join (map (str , self .slope )))
351+ set_element_if_not_none (xml , "Offset" , " " .join (map (str , self .offset )))
352+ set_element_if_not_none (xml , "Power" , " " .join (map (str , self .power )))
353+ return xml
354+
315355 @classmethod
316356 def default (cls ) -> SOPNode :
317357 """
@@ -331,7 +371,7 @@ def default(cls) -> SOPNode:
331371
332372
333373@dataclass
334- class SatNode (XMLParsable ):
374+ class SatNode (XMLParsable , XMLWritable ):
335375 """
336376 Represent a *SatNode* element for a :class:`colour_clf_io.ASC_CDL`
337377 *Process Node*.
@@ -399,6 +439,11 @@ def from_xml(
399439
400440 return SatNode (saturation = saturation )
401441
442+ def to_xml (self ) -> lxml .etree ._Element :
443+ xml = lxml .etree .Element ("SatNode" )
444+ set_element_if_not_none (xml , "Saturation" , self .saturation )
445+ return xml
446+
402447 @classmethod
403448 def default (cls ) -> SatNode :
404449 """
@@ -414,7 +459,7 @@ def default(cls) -> SatNode:
414459
415460
416461@dataclass
417- class Info (XMLParsable ):
462+ class Info (XMLParsable , XMLWritable ):
418463 """
419464 Represent an *Info* element.
420465
@@ -520,9 +565,20 @@ def from_xml(xml: lxml.etree._Element | None, config: ParserConfig) -> Info | No
520565
521566 return Info (calibration_info = calibration_info , ** attributes )
522567
568+ def to_xml (self ) -> lxml .etree ._Element :
569+ xml = lxml .etree .Element ("Info" )
570+ set_attr_if_not_none (xml , "AppRelease" , self .app_release )
571+ set_attr_if_not_none (xml , "Copyright" , self .copyright )
572+ set_attr_if_not_none (xml , "Revision" , self .revision )
573+ set_attr_if_not_none (xml , "AcesTransformID" , self .aces_transform_id )
574+ set_attr_if_not_none (xml , "AcesUserName" , self .aces_user_name )
575+ if self .calibration_info is not None :
576+ xml .append (self .calibration_info .to_xml ())
577+ return xml
578+
523579
524580@dataclass
525- class LogParams (XMLParsable ):
581+ class LogParams (XMLParsable , XMLWritable ):
526582 """
527583 Represent a *LogParams* element for a :class:`colour_clf_io.Log`
528584 *Process Node*.
@@ -641,14 +697,27 @@ def from_xml(
641697 "lin_side_slope" : "linSideSlope" ,
642698 "lin_side_offset" : "linSideOffset" ,
643699 "lin_side_break" : "linSideBreak" ,
644- "linear_slope" : "linearSlope " ,
700+ "linear_slope" : "" ,
645701 },
646702 )
647703
648704 channel = map_optional (Channel , xml .get ("channel" ))
649705
650706 return LogParams (channel = channel , ** attributes )
651707
708+ def to_xml (self ) -> lxml .etree ._Element :
709+ xml = lxml .etree .Element ("LogParams" )
710+ set_attr_if_not_none (xml , "base" , self .base )
711+ set_attr_if_not_none (xml , "logSideSlope" , self .log_side_slope )
712+ set_attr_if_not_none (xml , "logSideOffset" , self .log_side_offset )
713+ set_attr_if_not_none (xml , "linSideSlope" , self .lin_side_slope )
714+ set_attr_if_not_none (xml , "linSideOffset" , self .lin_side_offset )
715+ set_attr_if_not_none (xml , "linSideBreak" , self .lin_side_break )
716+ set_attr_if_not_none (xml , "linearSlope" , self .linear_slope )
717+ if self .channel is not None :
718+ xml .set ("channel" , self .channel .value )
719+ return xml
720+
652721 @classmethod
653722 def default (cls ) -> LogParams :
654723 """
@@ -673,7 +742,7 @@ def default(cls) -> LogParams:
673742
674743
675744@dataclass
676- class ExponentParams (XMLParsable ):
745+ class ExponentParams (XMLParsable , XMLWritable ):
677746 """
678747 Represent a *ExponentParams* element for a :class:`colour_clf_io.Exponent`
679748 *Process Node*.
@@ -772,6 +841,14 @@ def from_xml(
772841
773842 return ExponentParams (channel = channel , exponent = exponent , ** attributes )
774843
844+ def to_xml (self ) -> lxml .etree ._Element :
845+ xml = lxml .etree .Element ("ExponentParams" )
846+ set_attr_if_not_none (xml , "exponent" , self .exponent )
847+ set_attr_if_not_none (xml , "offset" , self .offset )
848+ if self .channel is not None :
849+ xml .set ("channel" , self .channel .value )
850+ return xml
851+
775852 @classmethod
776853 def default (cls ) -> ExponentParams :
777854 """
0 commit comments