diff --git a/nibabel/gifti/__init__.py b/nibabel/gifti/__init__.py index 55b23e8cd0..c9bf85b3a0 100644 --- a/nibabel/gifti/__init__.py +++ b/nibabel/gifti/__init__.py @@ -19,4 +19,4 @@ from .giftiio import read, write from .gifti import (GiftiMetaData, GiftiNVPairs, GiftiLabelTable, GiftiLabel, - GiftiCoordSystem, data_tag, GiftiDataArray, GiftiImage) + GiftiCoordSystem, GiftiDataArray, GiftiImage) diff --git a/nibabel/gifti/gifti.py b/nibabel/gifti/gifti.py index c472d6eff7..071f2c3968 100644 --- a/nibabel/gifti/gifti.py +++ b/nibabel/gifti/gifti.py @@ -12,6 +12,7 @@ import numpy as np +from .. import xmlutils as xml from ..nifti1 import data_type_codes, xform_codes, intent_codes from .util import (array_index_order_codes, gifti_encoding_codes, gifti_endian_codes, KIND2FMT) @@ -22,7 +23,7 @@ import base64 -class GiftiMetaData(object): +class GiftiMetaData(xml.XmlSerializable): """ A list of GiftiNVPairs in stored in the list self.data """ def __init__(self, nvpair=None): @@ -50,18 +51,15 @@ def metadata(self): self.data_as_dict[ele.name] = ele.value return self.data_as_dict - def to_xml(self): - if len(self.data) == 0: - return "\n" - res = "\n" + def _to_xml_element(self): + metadata = xml.Element('MetaData') for ele in self.data: - nvpair = """ -\t -\t -\n""" % (ele.name, ele.value) - res = res + nvpair - res = res + "\n" - return res + md = xml.SubElement(metadata, 'MD') + name = xml.SubElement(md, 'Name') + value = xml.SubElement(md, 'Value') + name.text = ele.name + value.text = ele.value + return metadata def print_summary(self): print(self.metadata) @@ -77,7 +75,7 @@ def __init__(self, name='', value=''): self.value = value -class GiftiLabelTable(object): +class GiftiLabelTable(xml.XmlSerializable): def __init__(self): self.labels = [] @@ -88,31 +86,22 @@ def get_labels_as_dict(self): self.labels_as_dict[ele.key] = ele.label return self.labels_as_dict - def to_xml(self): - if len(self.labels) == 0: - return "\n" - res = "\n" + def _to_xml_element(self): + labeltable = xml.Element('LabelTable') for ele in self.labels: - col = '' - if not ele.red is None: - col += ' Red="%s"' % str(ele.red) - if not ele.green is None: - col += ' Green="%s"' % str(ele.green) - if not ele.blue is None: - col += ' Blue="%s"' % str(ele.blue) - if not ele.alpha is None: - col += ' Alpha="%s"' % str(ele.alpha) - lab = """\t\n""" % \ - (str(ele.key), col, ele.label) - res = res + lab - res = res + "\n" - return res + label = xml.SubElement(labeltable, 'Label') + label.attrib['Key'] = str(ele.key) + label.text = ele.label + for attr in ['Red', 'Green', 'Blue', 'Alpha']: + if getattr(ele, attr.lower(), None) is not None: + label.attrib[attr] = str(getattr(ele, attr.lower())) + return labeltable def print_summary(self): print(self.get_labels_as_dict()) -class GiftiLabel(object): +class GiftiLabel(xml.XmlSerializable): key = int label = str # rgba @@ -165,7 +154,7 @@ def _arr2txt(arr, elem_fmt): return '\n'.join(fmt % tuple(row) for row in arr) -class GiftiCoordSystem(object): +class GiftiCoordSystem(xml.XmlSerializable): dataspace = int xformspace = int xform = np.ndarray # 4x4 numpy array @@ -179,19 +168,16 @@ def __init__(self, dataspace=0, xformspace=0, xform=None): else: self.xform = xform - def to_xml(self): - if self.xform is None: - return "\n" - res = (""" -\t -\t\n""" - % (xform_codes.niistring[self.dataspace], - xform_codes.niistring[self.xformspace])) - res = res + "\n" - res += _arr2txt(self.xform, '%10.6f') - res = res + "\n" - res = res + "\n" - return res + def _to_xml_element(self): + coord_xform = xml.Element('CoordinateSystemTransformMatrix') + if self.xform is not None: + dataspace = xml.SubElement(coord_xform, 'DataSpace') + dataspace.text = xform_codes.niistring[self.dataspace] + xformed_space = xml.SubElement(coord_xform, 'TransformedSpace') + xformed_space.text = xform_codes.niistring[self.xformspace] + matrix_data = xml.SubElement(coord_xform, 'MatrixData') + matrix_data.text = _arr2txt(self.xform, '%10.6f') + return coord_xform def print_summary(self): print('Dataspace: ', xform_codes.niistring[self.dataspace]) @@ -199,8 +185,20 @@ def print_summary(self): print('Affine Transformation Matrix: \n', self.xform) +@np.deprecate_with_doc("This is an internal API that will be discontinued.") def data_tag(dataarray, encoding, datatype, ordering): - """ Creates the data tag depending on the required encoding """ + class DataTag(xml.XmlSerializable): + def __init__(self, *args): + self.args = args + def _to_xml_element(self): + return _data_tag_element(*self.args) + + return DataTag(dataarray, encoding, datatype, ordering).to_xml() + + +def _data_tag_element(dataarray, encoding, datatype, ordering): + """ Creates the data tag depending on the required encoding, + returns as XML element""" import zlib ord = array_index_order_codes.npcode[ordering] enclabel = gifti_encoding_codes.label[encoding] @@ -215,10 +213,13 @@ def data_tag(dataarray, encoding, datatype, ordering): raise NotImplementedError("In what format are the external files?") else: da = '' - return "" + da + "\n" + + data = xml.Element('Data') + data.text = da + return data -class GiftiDataArray(object): +class GiftiDataArray(xml.XmlSerializable): # These are for documentation only; we don't use these class variables intent = int @@ -299,26 +300,37 @@ def from_array(klass, cda.meta = GiftiMetaData.from_dict(meta) return cda - def to_xml(self): + def _to_xml_element(self): # fix endianness to machine endianness self.endian = gifti_endian_codes.code[sys.byteorder] - result = "" - result += self.to_xml_open() - # write metadata - if not self.meta is None: - result += self.meta.to_xml() - # write coord sys - if not self.coordsys is None: - result += self.coordsys.to_xml() + + data_array = xml.Element('DataArray', attrib={ + 'Intent': intent_codes.niistring[self.intent], + 'DataType': data_type_codes.niistring[self.datatype], + 'ArrayIndexingOrder': array_index_order_codes.label[self.ind_ord], + 'Dimensionality': str(self.num_dim), + 'Encoding': gifti_encoding_codes.specs[self.encoding], + 'Endian': gifti_endian_codes.specs[self.endian], + 'ExternalFileName': self.ext_fname, + 'ExternalFileOffset': self.ext_offset}) + for di, dn in enumerate(self.dims): + data_array.attrib['Dim%d' % di] = str(dn) + + if self.meta is not None: + data_array.append(self.meta._to_xml_element()) + if self.coordsys is not None: + data_array.append(self.coordsys._to_xml_element()) # write data array depending on the encoding dt_kind = data_type_codes.dtype[self.datatype].kind - result += data_tag(self.data, - gifti_encoding_codes.specs[self.encoding], - KIND2FMT[dt_kind], - self.ind_ord) - result = result + self.to_xml_close() - return result + data_array.append( + _data_tag_element(self.data, + gifti_encoding_codes.specs[self.encoding], + KIND2FMT[dt_kind], + self.ind_ord)) + return data_array + + @np.deprecate_with_doc("Use the to_xml() function instead.") def to_xml_open(self): out = """\n" @@ -371,7 +384,8 @@ def metadata(self): return self.meta.metadata -class GiftiImage(object): +class GiftiImage(xml.XmlSerializable): + def __init__(self, meta=None, labeltable=None, darrays=None, version="1.0"): if darrays is None: @@ -497,17 +511,21 @@ def print_summary(self): print(da.print_summary()) print('----end----') - def to_xml(self): + + def _to_xml_element(self): + GIFTI = xml.Element('GIFTI', attrib={ + 'Version': self.version, + 'NumberOfDataArrays': str(self.numDA)}) + if self.meta is not None: + GIFTI.append(self.meta._to_xml_element()) + if self.labeltable is not None: + GIFTI.append(self.labeltable._to_xml_element()) + for dar in self.darrays: + GIFTI.append(dar._to_xml_element()) + return GIFTI + + def to_xml(self, enc='utf-8'): """ Return XML corresponding to image content """ - res = """ + return b""" -\n""" % (self.version, - str(self.numDA)) - if not self.meta is None: - res += self.meta.to_xml() - if not self.labeltable is None: - res += self.labeltable.to_xml() - for dar in self.darrays: - res += dar.to_xml() - res += "" - return res +""" + xml.XmlSerializable.to_xml(self, enc) diff --git a/nibabel/gifti/giftiio.py b/nibabel/gifti/giftiio.py index ee68bf2629..2660267021 100644 --- a/nibabel/gifti/giftiio.py +++ b/nibabel/gifti/giftiio.py @@ -80,5 +80,5 @@ def write(image, filename): The Gifti file is stored in endian convention of the current machine. """ # Our giftis are always utf-8 encoded - see GiftiImage.to_xml - with codecs.open(filename, 'wb', encoding='utf-8') as f: + with open(filename, 'wb') as f: f.write(image.to_xml()) diff --git a/nibabel/gifti/tests/test_gifti.py b/nibabel/gifti/tests/test_gifti.py index cc9a09da12..9433c90219 100644 --- a/nibabel/gifti/tests/test_gifti.py +++ b/nibabel/gifti/tests/test_gifti.py @@ -4,17 +4,18 @@ import numpy as np -from nibabel.gifti import giftiio +from nibabel.externals.six import string_types +from nibabel.gifti import (GiftiImage, GiftiDataArray, GiftiLabel, + GiftiLabelTable, GiftiMetaData, giftiio) +from nibabel.gifti.gifti import data_tag +from nibabel.nifti1 import data_type_codes, intent_codes -from .test_giftiio import (DATA_FILE1, DATA_FILE2, DATA_FILE3, DATA_FILE4, - DATA_FILE5, DATA_FILE6) -from ..gifti import (GiftiImage, GiftiDataArray, GiftiLabel, GiftiLabelTable, - GiftiMetaData) -from ...nifti1 import data_type_codes, intent_codes -from ...testing import clear_and_catch_warnings from numpy.testing import (assert_array_almost_equal, assert_array_equal) from nose.tools import (assert_true, assert_false, assert_equal, assert_raises) +from nibabel.testing import clear_and_catch_warnings +from .test_giftiio import (DATA_FILE1, DATA_FILE2, DATA_FILE3, DATA_FILE4, + DATA_FILE5, DATA_FILE6) def test_gifti_image(): @@ -70,6 +71,17 @@ def test_dataarray(): da = GiftiDataArray.from_array(bs_arr, 'triangle') assert_equal(da.datatype, data_type_codes[arr.dtype]) + # Smoke test on deprecated functions + da = GiftiDataArray.from_array(np.ones((1,)), 'triangle') + with clear_and_catch_warnings() as w: + warnings.filterwarnings('always', category=DeprecationWarning) + assert_true(isinstance(da.to_xml_open(), string_types)) + assert_equal(len(w), 1) + with clear_and_catch_warnings() as w: + warnings.filterwarnings('once', category=DeprecationWarning) + assert_true(isinstance(da.to_xml_close(), string_types)) + assert_equal(len(w), 1) + def test_labeltable(): img = GiftiImage() @@ -163,3 +175,11 @@ def assign_labeltable(val): def assign_metadata(val): img.meta = val assert_raises(TypeError, assign_metadata, 'not-a-meta') + + +def test_data_tag_deprecated(): + img = GiftiImage() + with clear_and_catch_warnings() as w: + warnings.filterwarnings('once', category=DeprecationWarning) + data_tag(np.array([]), 'ASCII', '%i', 1) + assert_equal(len(w), 1) diff --git a/nibabel/xmlutils.py b/nibabel/xmlutils.py new file mode 100644 index 0000000000..fa23466006 --- /dev/null +++ b/nibabel/xmlutils.py @@ -0,0 +1,25 @@ +# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## +# +# See COPYING file distributed along with the NiBabel package for the +# copyright and license terms. +# +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## +""" +Thin layer around xml.etree.ElementTree, to abstract nibabel xml support. +""" +from xml.etree.ElementTree import Element, SubElement, tostring + + +class XmlSerializable(object): + """ Basic interface for serializing an object to xml""" + + def _to_xml_element(self): + """ Output should be a xml.etree.ElementTree.Element""" + raise NotImplementedError() + + def to_xml(self, enc='utf-8'): + """ Output should be an xml string with the given encoding. + (default: utf-8)""" + return tostring(self._to_xml_element(), enc)