88
99from __future__ import annotations
1010
11+ import sys
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
4851 "ExponentParams" ,
4952]
5053
54+ if sys .version_info >= (3 , 12 ):
55+ from itertools import batched
56+ else :
57+ from itertools import islice
58+
59+ T = typing .TypeVar ("T" )
60+
61+ def batched (iterable : typing .Iterable [T ], n : int ) -> typing .Iterator [tuple [T , ...]]:
62+ if n < 1 :
63+ err = "n must be at least one"
64+ raise ValueError (err )
65+ it = iter (iterable )
66+ while batch := tuple (islice (it , n )):
67+ yield batch
68+
5169
5270@dataclass
53- class Array (XMLParsable ):
71+ class Array (XMLParsable , XMLWritable ):
5472 """
5573 Represent an *Array* element.
5674
@@ -124,6 +142,30 @@ def from_xml(
124142
125143 return Array (values = values , dim = dimensions )
126144
145+ def to_xml (self ) -> lxml .etree ._Element :
146+ """
147+ Serialise this object as an XML object.
148+
149+ Returns
150+ -------
151+ :class:`lxml.etree._Element`
152+ """
153+ xml = lxml .etree .Element ("Array" )
154+ xml .set ("dim" , " " .join (map (str , self .dim )))
155+
156+ def wrap_with_newlines (s : str ) -> str :
157+ return f"\n { s } \n "
158+
159+ if len (self .dim ) <= 1 :
160+ text = "\n " .join (map (str , self .values ))
161+ else :
162+ row_length = self .dim [- 1 ]
163+ text = "\n " .join (
164+ " " .join (map (str , row )) for row in batched (self .values , row_length )
165+ )
166+ xml .text = wrap_with_newlines (text )
167+ return xml
168+
127169 def as_array (self ) -> npt .NDArray :
128170 """
129171 Convert the *CLF* element into a numpy array.
@@ -144,7 +186,7 @@ def as_array(self) -> npt.NDArray:
144186
145187
146188@dataclass
147- class CalibrationInfo (XMLParsable ):
189+ class CalibrationInfo (XMLParsable , XMLWritable ):
148190 """
149191 Represent a *CalibrationInfo* container element for a
150192 :class:`colour_clf_io.ProcessList` class instance.
@@ -227,9 +269,32 @@ def from_xml(
227269
228270 return CalibrationInfo (** attributes )
229271
272+ def to_xml (self ) -> lxml .etree ._Element :
273+ """
274+ Serialise this object as an XML object.
275+
276+ Returns
277+ -------
278+ :class:`lxml.etree._Element`
279+ """
280+ xml = lxml .etree .Element ("CalibrationInfo" )
281+ set_attr_if_not_none (
282+ xml , "DisplayDeviceSerialNum" , self .display_device_serial_num
283+ )
284+ set_attr_if_not_none (
285+ xml , "DisplayDeviceHostName" , self .display_device_host_name
286+ )
287+ set_attr_if_not_none (xml , "OperatorName" , self .operator_name )
288+ set_attr_if_not_none (xml , "CalibrationDateTime" , self .calibration_date_time )
289+ set_attr_if_not_none (xml , "MeasurementProbe" , self .measurement_probe )
290+ set_attr_if_not_none (
291+ xml , "CalibrationSoftwareName" , self .calibration_software_name
292+ )
293+ return xml
294+
230295
231296@dataclass
232- class SOPNode (XMLParsable ):
297+ class SOPNode (XMLParsable , XMLWritable ):
233298 """
234299 Represent a *SOPNode* element for a :class:`colour_clf_io.ASC_CDL`
235300 *Process Node*.
@@ -312,6 +377,20 @@ def from_xml(
312377
313378 return SOPNode (slope = slope , offset = offset , power = power )
314379
380+ def to_xml (self ) -> lxml .etree ._Element :
381+ """
382+ Serialise this object as an XML object.
383+
384+ Returns
385+ -------
386+ :class:`lxml.etree._Element`
387+ """
388+ xml = lxml .etree .Element ("SOPNode" )
389+ set_element_if_not_none (xml , "Slope" , " " .join (map (str , self .slope )))
390+ set_element_if_not_none (xml , "Offset" , " " .join (map (str , self .offset )))
391+ set_element_if_not_none (xml , "Power" , " " .join (map (str , self .power )))
392+ return xml
393+
315394 @classmethod
316395 def default (cls ) -> SOPNode :
317396 """
@@ -331,7 +410,7 @@ def default(cls) -> SOPNode:
331410
332411
333412@dataclass
334- class SatNode (XMLParsable ):
413+ class SatNode (XMLParsable , XMLWritable ):
335414 """
336415 Represent a *SatNode* element for a :class:`colour_clf_io.ASC_CDL`
337416 *Process Node*.
@@ -399,6 +478,18 @@ def from_xml(
399478
400479 return SatNode (saturation = saturation )
401480
481+ def to_xml (self ) -> lxml .etree ._Element :
482+ """
483+ Serialise this object as an XML object.
484+
485+ Returns
486+ -------
487+ :class:`lxml.etree._Element`
488+ """
489+ xml = lxml .etree .Element ("SatNode" )
490+ set_element_if_not_none (xml , "Saturation" , self .saturation )
491+ return xml
492+
402493 @classmethod
403494 def default (cls ) -> SatNode :
404495 """
@@ -414,7 +505,7 @@ def default(cls) -> SatNode:
414505
415506
416507@dataclass
417- class Info (XMLParsable ):
508+ class Info (XMLParsable , XMLWritable ):
418509 """
419510 Represent an *Info* element.
420511
@@ -520,9 +611,27 @@ def from_xml(xml: lxml.etree._Element | None, config: ParserConfig) -> Info | No
520611
521612 return Info (calibration_info = calibration_info , ** attributes )
522613
614+ def to_xml (self ) -> lxml .etree ._Element :
615+ """
616+ Serialise this object as an XML object.
617+
618+ Returns
619+ -------
620+ :class:`lxml.etree._Element`
621+ """
622+ xml = lxml .etree .Element ("Info" )
623+ set_attr_if_not_none (xml , "AppRelease" , self .app_release )
624+ set_attr_if_not_none (xml , "Copyright" , self .copyright )
625+ set_attr_if_not_none (xml , "Revision" , self .revision )
626+ set_attr_if_not_none (xml , "AcesTransformID" , self .aces_transform_id )
627+ set_attr_if_not_none (xml , "AcesUserName" , self .aces_user_name )
628+ if self .calibration_info is not None :
629+ xml .append (self .calibration_info .to_xml ())
630+ return xml
631+
523632
524633@dataclass
525- class LogParams (XMLParsable ):
634+ class LogParams (XMLParsable , XMLWritable ):
526635 """
527636 Represent a *LogParams* element for a :class:`colour_clf_io.Log`
528637 *Process Node*.
@@ -649,6 +758,26 @@ def from_xml(
649758
650759 return LogParams (channel = channel , ** attributes )
651760
761+ def to_xml (self ) -> lxml .etree ._Element :
762+ """
763+ Serialise this object as an XML object.
764+
765+ Returns
766+ -------
767+ :class:`lxml.etree._Element`
768+ """
769+ xml = lxml .etree .Element ("LogParams" )
770+ set_attr_if_not_none (xml , "base" , self .base )
771+ set_attr_if_not_none (xml , "logSideSlope" , self .log_side_slope )
772+ set_attr_if_not_none (xml , "logSideOffset" , self .log_side_offset )
773+ set_attr_if_not_none (xml , "linSideSlope" , self .lin_side_slope )
774+ set_attr_if_not_none (xml , "linSideOffset" , self .lin_side_offset )
775+ set_attr_if_not_none (xml , "linSideBreak" , self .lin_side_break )
776+ set_attr_if_not_none (xml , "linearSlope" , self .linear_slope )
777+ if self .channel is not None :
778+ xml .set ("channel" , self .channel .value )
779+ return xml
780+
652781 @classmethod
653782 def default (cls ) -> LogParams :
654783 """
@@ -673,7 +802,7 @@ def default(cls) -> LogParams:
673802
674803
675804@dataclass
676- class ExponentParams (XMLParsable ):
805+ class ExponentParams (XMLParsable , XMLWritable ):
677806 """
678807 Represent a *ExponentParams* element for a :class:`colour_clf_io.Exponent`
679808 *Process Node*.
@@ -772,6 +901,21 @@ def from_xml(
772901
773902 return ExponentParams (channel = channel , exponent = exponent , ** attributes )
774903
904+ def to_xml (self ) -> lxml .etree ._Element :
905+ """
906+ Serialise this object as an XML object.
907+
908+ Returns
909+ -------
910+ :class:`lxml.etree._Element`
911+ """
912+ xml = lxml .etree .Element ("ExponentParams" )
913+ set_attr_if_not_none (xml , "exponent" , self .exponent )
914+ set_attr_if_not_none (xml , "offset" , self .offset )
915+ if self .channel is not None :
916+ xml .set ("channel" , self .channel .value )
917+ return xml
918+
775919 @classmethod
776920 def default (cls ) -> ExponentParams :
777921 """
0 commit comments