Skip to content

Commit 63f9ef2

Browse files
committed
Merge pull request #354 from bcipolli/gifti-fix3
RF: Gifti images are XML serializable Eliminate (as much as possible) hand-written XML nibabel.xmlutils.XmlSerializable interface provides to_xml functions Deprecate helper functions that should not have been public
2 parents 6fb4ea0 + e2bd1e5 commit 63f9ef2

File tree

5 files changed

+149
-86
lines changed

5 files changed

+149
-86
lines changed

nibabel/gifti/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,4 @@
1919

2020
from .giftiio import read, write
2121
from .gifti import (GiftiMetaData, GiftiNVPairs, GiftiLabelTable, GiftiLabel,
22-
GiftiCoordSystem, data_tag, GiftiDataArray, GiftiImage)
22+
GiftiCoordSystem, GiftiDataArray, GiftiImage)

nibabel/gifti/gifti.py

Lines changed: 95 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import numpy as np
1414

15+
from .. import xmlutils as xml
1516
from ..nifti1 import data_type_codes, xform_codes, intent_codes
1617
from .util import (array_index_order_codes, gifti_encoding_codes,
1718
gifti_endian_codes, KIND2FMT)
@@ -22,7 +23,7 @@
2223
import base64
2324

2425

25-
class GiftiMetaData(object):
26+
class GiftiMetaData(xml.XmlSerializable):
2627
""" A list of GiftiNVPairs in stored in
2728
the list self.data """
2829
def __init__(self, nvpair=None):
@@ -50,18 +51,15 @@ def metadata(self):
5051
self.data_as_dict[ele.name] = ele.value
5152
return self.data_as_dict
5253

53-
def to_xml(self):
54-
if len(self.data) == 0:
55-
return "<MetaData/>\n"
56-
res = "<MetaData>\n"
54+
def _to_xml_element(self):
55+
metadata = xml.Element('MetaData')
5756
for ele in self.data:
58-
nvpair = """<MD>
59-
\t<Name><![CDATA[%s]]></Name>
60-
\t<Value><![CDATA[%s]]></Value>
61-
</MD>\n""" % (ele.name, ele.value)
62-
res = res + nvpair
63-
res = res + "</MetaData>\n"
64-
return res
57+
md = xml.SubElement(metadata, 'MD')
58+
name = xml.SubElement(md, 'Name')
59+
value = xml.SubElement(md, 'Value')
60+
name.text = ele.name
61+
value.text = ele.value
62+
return metadata
6563

6664
def print_summary(self):
6765
print(self.metadata)
@@ -77,7 +75,7 @@ def __init__(self, name='', value=''):
7775
self.value = value
7876

7977

80-
class GiftiLabelTable(object):
78+
class GiftiLabelTable(xml.XmlSerializable):
8179

8280
def __init__(self):
8381
self.labels = []
@@ -88,31 +86,22 @@ def get_labels_as_dict(self):
8886
self.labels_as_dict[ele.key] = ele.label
8987
return self.labels_as_dict
9088

91-
def to_xml(self):
92-
if len(self.labels) == 0:
93-
return "<LabelTable/>\n"
94-
res = "<LabelTable>\n"
89+
def _to_xml_element(self):
90+
labeltable = xml.Element('LabelTable')
9591
for ele in self.labels:
96-
col = ''
97-
if not ele.red is None:
98-
col += ' Red="%s"' % str(ele.red)
99-
if not ele.green is None:
100-
col += ' Green="%s"' % str(ele.green)
101-
if not ele.blue is None:
102-
col += ' Blue="%s"' % str(ele.blue)
103-
if not ele.alpha is None:
104-
col += ' Alpha="%s"' % str(ele.alpha)
105-
lab = """\t<Label Key="%s"%s><![CDATA[%s]]></Label>\n""" % \
106-
(str(ele.key), col, ele.label)
107-
res = res + lab
108-
res = res + "</LabelTable>\n"
109-
return res
92+
label = xml.SubElement(labeltable, 'Label')
93+
label.attrib['Key'] = str(ele.key)
94+
label.text = ele.label
95+
for attr in ['Red', 'Green', 'Blue', 'Alpha']:
96+
if getattr(ele, attr.lower(), None) is not None:
97+
label.attrib[attr] = str(getattr(ele, attr.lower()))
98+
return labeltable
11099

111100
def print_summary(self):
112101
print(self.get_labels_as_dict())
113102

114103

115-
class GiftiLabel(object):
104+
class GiftiLabel(xml.XmlSerializable):
116105
key = int
117106
label = str
118107
# rgba
@@ -165,7 +154,7 @@ def _arr2txt(arr, elem_fmt):
165154
return '\n'.join(fmt % tuple(row) for row in arr)
166155

167156

168-
class GiftiCoordSystem(object):
157+
class GiftiCoordSystem(xml.XmlSerializable):
169158
dataspace = int
170159
xformspace = int
171160
xform = np.ndarray # 4x4 numpy array
@@ -179,28 +168,37 @@ def __init__(self, dataspace=0, xformspace=0, xform=None):
179168
else:
180169
self.xform = xform
181170

182-
def to_xml(self):
183-
if self.xform is None:
184-
return "<CoordinateSystemTransformMatrix/>\n"
185-
res = ("""<CoordinateSystemTransformMatrix>
186-
\t<DataSpace><![CDATA[%s]]></DataSpace>
187-
\t<TransformedSpace><![CDATA[%s]]></TransformedSpace>\n"""
188-
% (xform_codes.niistring[self.dataspace],
189-
xform_codes.niistring[self.xformspace]))
190-
res = res + "<MatrixData>\n"
191-
res += _arr2txt(self.xform, '%10.6f')
192-
res = res + "</MatrixData>\n"
193-
res = res + "</CoordinateSystemTransformMatrix>\n"
194-
return res
171+
def _to_xml_element(self):
172+
coord_xform = xml.Element('CoordinateSystemTransformMatrix')
173+
if self.xform is not None:
174+
dataspace = xml.SubElement(coord_xform, 'DataSpace')
175+
dataspace.text = xform_codes.niistring[self.dataspace]
176+
xformed_space = xml.SubElement(coord_xform, 'TransformedSpace')
177+
xformed_space.text = xform_codes.niistring[self.xformspace]
178+
matrix_data = xml.SubElement(coord_xform, 'MatrixData')
179+
matrix_data.text = _arr2txt(self.xform, '%10.6f')
180+
return coord_xform
195181

196182
def print_summary(self):
197183
print('Dataspace: ', xform_codes.niistring[self.dataspace])
198184
print('XFormSpace: ', xform_codes.niistring[self.xformspace])
199185
print('Affine Transformation Matrix: \n', self.xform)
200186

201187

188+
@np.deprecate_with_doc("This is an internal API that will be discontinued.")
202189
def data_tag(dataarray, encoding, datatype, ordering):
203-
""" Creates the data tag depending on the required encoding """
190+
class DataTag(xml.XmlSerializable):
191+
def __init__(self, *args):
192+
self.args = args
193+
def _to_xml_element(self):
194+
return _data_tag_element(*self.args)
195+
196+
return DataTag(dataarray, encoding, datatype, ordering).to_xml()
197+
198+
199+
def _data_tag_element(dataarray, encoding, datatype, ordering):
200+
""" Creates the data tag depending on the required encoding,
201+
returns as XML element"""
204202
import zlib
205203
ord = array_index_order_codes.npcode[ordering]
206204
enclabel = gifti_encoding_codes.label[encoding]
@@ -215,10 +213,13 @@ def data_tag(dataarray, encoding, datatype, ordering):
215213
raise NotImplementedError("In what format are the external files?")
216214
else:
217215
da = ''
218-
return "<Data>" + da + "</Data>\n"
216+
217+
data = xml.Element('Data')
218+
data.text = da
219+
return data
219220

220221

221-
class GiftiDataArray(object):
222+
class GiftiDataArray(xml.XmlSerializable):
222223

223224
# These are for documentation only; we don't use these class variables
224225
intent = int
@@ -299,26 +300,37 @@ def from_array(klass,
299300
cda.meta = GiftiMetaData.from_dict(meta)
300301
return cda
301302

302-
def to_xml(self):
303+
def _to_xml_element(self):
303304
# fix endianness to machine endianness
304305
self.endian = gifti_endian_codes.code[sys.byteorder]
305-
result = ""
306-
result += self.to_xml_open()
307-
# write metadata
308-
if not self.meta is None:
309-
result += self.meta.to_xml()
310-
# write coord sys
311-
if not self.coordsys is None:
312-
result += self.coordsys.to_xml()
306+
307+
data_array = xml.Element('DataArray', attrib={
308+
'Intent': intent_codes.niistring[self.intent],
309+
'DataType': data_type_codes.niistring[self.datatype],
310+
'ArrayIndexingOrder': array_index_order_codes.label[self.ind_ord],
311+
'Dimensionality': str(self.num_dim),
312+
'Encoding': gifti_encoding_codes.specs[self.encoding],
313+
'Endian': gifti_endian_codes.specs[self.endian],
314+
'ExternalFileName': self.ext_fname,
315+
'ExternalFileOffset': self.ext_offset})
316+
for di, dn in enumerate(self.dims):
317+
data_array.attrib['Dim%d' % di] = str(dn)
318+
319+
if self.meta is not None:
320+
data_array.append(self.meta._to_xml_element())
321+
if self.coordsys is not None:
322+
data_array.append(self.coordsys._to_xml_element())
313323
# write data array depending on the encoding
314324
dt_kind = data_type_codes.dtype[self.datatype].kind
315-
result += data_tag(self.data,
316-
gifti_encoding_codes.specs[self.encoding],
317-
KIND2FMT[dt_kind],
318-
self.ind_ord)
319-
result = result + self.to_xml_close()
320-
return result
325+
data_array.append(
326+
_data_tag_element(self.data,
327+
gifti_encoding_codes.specs[self.encoding],
328+
KIND2FMT[dt_kind],
329+
self.ind_ord))
321330

331+
return data_array
332+
333+
@np.deprecate_with_doc("Use the to_xml() function instead.")
322334
def to_xml_open(self):
323335
out = """<DataArray Intent="%s"
324336
\tDataType="%s"
@@ -342,6 +354,7 @@ def to_xml_open(self):
342354
self.ext_offset,
343355
)
344356

357+
@np.deprecate_with_doc("Use the to_xml() function instead.")
345358
def to_xml_close(self):
346359
return "</DataArray>\n"
347360

@@ -371,7 +384,8 @@ def metadata(self):
371384
return self.meta.metadata
372385

373386

374-
class GiftiImage(object):
387+
class GiftiImage(xml.XmlSerializable):
388+
375389
def __init__(self, meta=None, labeltable=None, darrays=None,
376390
version="1.0"):
377391
if darrays is None:
@@ -497,17 +511,21 @@ def print_summary(self):
497511
print(da.print_summary())
498512
print('----end----')
499513

500-
def to_xml(self):
514+
515+
def _to_xml_element(self):
516+
GIFTI = xml.Element('GIFTI', attrib={
517+
'Version': self.version,
518+
'NumberOfDataArrays': str(self.numDA)})
519+
if self.meta is not None:
520+
GIFTI.append(self.meta._to_xml_element())
521+
if self.labeltable is not None:
522+
GIFTI.append(self.labeltable._to_xml_element())
523+
for dar in self.darrays:
524+
GIFTI.append(dar._to_xml_element())
525+
return GIFTI
526+
527+
def to_xml(self, enc='utf-8'):
501528
""" Return XML corresponding to image content """
502-
res = """<?xml version="1.0" encoding="UTF-8"?>
529+
return b"""<?xml version="1.0" encoding="UTF-8"?>
503530
<!DOCTYPE GIFTI SYSTEM "http://www.nitrc.org/frs/download.php/115/gifti.dtd">
504-
<GIFTI Version="%s" NumberOfDataArrays="%s">\n""" % (self.version,
505-
str(self.numDA))
506-
if not self.meta is None:
507-
res += self.meta.to_xml()
508-
if not self.labeltable is None:
509-
res += self.labeltable.to_xml()
510-
for dar in self.darrays:
511-
res += dar.to_xml()
512-
res += "</GIFTI>"
513-
return res
531+
""" + xml.XmlSerializable.to_xml(self, enc)

nibabel/gifti/giftiio.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,5 +80,5 @@ def write(image, filename):
8080
The Gifti file is stored in endian convention of the current machine.
8181
"""
8282
# Our giftis are always utf-8 encoded - see GiftiImage.to_xml
83-
with codecs.open(filename, 'wb', encoding='utf-8') as f:
83+
with open(filename, 'wb') as f:
8484
f.write(image.to_xml())

nibabel/gifti/tests/test_gifti.py

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,18 @@
44

55
import numpy as np
66

7-
from nibabel.gifti import giftiio
7+
from nibabel.externals.six import string_types
8+
from nibabel.gifti import (GiftiImage, GiftiDataArray, GiftiLabel,
9+
GiftiLabelTable, GiftiMetaData, giftiio)
10+
from nibabel.gifti.gifti import data_tag
11+
from nibabel.nifti1 import data_type_codes, intent_codes
812

9-
from .test_giftiio import (DATA_FILE1, DATA_FILE2, DATA_FILE3, DATA_FILE4,
10-
DATA_FILE5, DATA_FILE6)
11-
from ..gifti import (GiftiImage, GiftiDataArray, GiftiLabel, GiftiLabelTable,
12-
GiftiMetaData)
13-
from ...nifti1 import data_type_codes, intent_codes
14-
from ...testing import clear_and_catch_warnings
1513
from numpy.testing import (assert_array_almost_equal,
1614
assert_array_equal)
1715
from nose.tools import (assert_true, assert_false, assert_equal, assert_raises)
16+
from nibabel.testing import clear_and_catch_warnings
17+
from .test_giftiio import (DATA_FILE1, DATA_FILE2, DATA_FILE3, DATA_FILE4,
18+
DATA_FILE5, DATA_FILE6)
1819

1920

2021
def test_gifti_image():
@@ -70,6 +71,17 @@ def test_dataarray():
7071
da = GiftiDataArray.from_array(bs_arr, 'triangle')
7172
assert_equal(da.datatype, data_type_codes[arr.dtype])
7273

74+
# Smoke test on deprecated functions
75+
da = GiftiDataArray.from_array(np.ones((1,)), 'triangle')
76+
with clear_and_catch_warnings() as w:
77+
warnings.filterwarnings('always', category=DeprecationWarning)
78+
assert_true(isinstance(da.to_xml_open(), string_types))
79+
assert_equal(len(w), 1)
80+
with clear_and_catch_warnings() as w:
81+
warnings.filterwarnings('once', category=DeprecationWarning)
82+
assert_true(isinstance(da.to_xml_close(), string_types))
83+
assert_equal(len(w), 1)
84+
7385

7486
def test_labeltable():
7587
img = GiftiImage()
@@ -163,3 +175,11 @@ def assign_labeltable(val):
163175
def assign_metadata(val):
164176
img.meta = val
165177
assert_raises(TypeError, assign_metadata, 'not-a-meta')
178+
179+
180+
def test_data_tag_deprecated():
181+
img = GiftiImage()
182+
with clear_and_catch_warnings() as w:
183+
warnings.filterwarnings('once', category=DeprecationWarning)
184+
data_tag(np.array([]), 'ASCII', '%i', 1)
185+
assert_equal(len(w), 1)

nibabel/xmlutils.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*-
2+
# vi: set ft=python sts=4 ts=4 sw=4 et:
3+
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
4+
#
5+
# See COPYING file distributed along with the NiBabel package for the
6+
# copyright and license terms.
7+
#
8+
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
9+
"""
10+
Thin layer around xml.etree.ElementTree, to abstract nibabel xml support.
11+
"""
12+
from xml.etree.ElementTree import Element, SubElement, tostring
13+
14+
15+
class XmlSerializable(object):
16+
""" Basic interface for serializing an object to xml"""
17+
18+
def _to_xml_element(self):
19+
""" Output should be a xml.etree.ElementTree.Element"""
20+
raise NotImplementedError()
21+
22+
def to_xml(self, enc='utf-8'):
23+
""" Output should be an xml string with the given encoding.
24+
(default: utf-8)"""
25+
return tostring(self._to_xml_element(), enc)

0 commit comments

Comments
 (0)