Skip to content

Commit 09d3f95

Browse files
authored
Merge pull request #417 from moloney/enh-csa-write
ENH: Add writer for Siemens CSA header
2 parents 977d044 + 122a923 commit 09d3f95

File tree

2 files changed

+121
-0
lines changed

2 files changed

+121
-0
lines changed

nibabel/nicom/csareader.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""CSA header reader from SPM spec"""
22

33
import numpy as np
4+
import struct
45

56
from .structreader import Unpacker
67
from .utils import find_private_section
@@ -28,6 +29,10 @@ class CSAReadError(CSAError):
2829
pass
2930

3031

32+
class CSAWriteError(CSAError):
33+
pass
34+
35+
3136
def get_csa_header(dcm_data, csa_type='image'):
3237
"""Get CSA header information from DICOM header
3338
@@ -161,6 +166,96 @@ def read(csa_str):
161166
return csa_dict
162167

163168

169+
def write(csa_header):
170+
''' Write string from CSA header `csa_header`
171+
172+
Parameters
173+
----------
174+
csa_header : dict
175+
header information as dict, where `header` has fields (at least)
176+
``type, n_tags, tags``. ``header['tags']`` is also a dictionary
177+
with one key, value pair for each tag in the header.
178+
179+
Returns
180+
-------
181+
csa_str : str
182+
byte string containing CSA header information
183+
'''
184+
result = []
185+
if csa_header['type'] == 2:
186+
result.append(b'SV10')
187+
result.append(csa_header['unused0'])
188+
if not 0 < csa_header['n_tags'] <= 128:
189+
raise CSAWriteError('Number of tags `t` should be '
190+
'0 < t <= 128')
191+
result.append(struct.pack('2I',
192+
csa_header['n_tags'],
193+
csa_header['check'])
194+
)
195+
196+
# Build list of tags in correct order
197+
tags = list(csa_header['tags'].items())
198+
tags.sort(key=lambda x: x[1]['tag_no'])
199+
tag0_n_items = tags[0][1]['n_items']
200+
201+
# Add the information for each tag
202+
for tag_name, tag_dict in tags:
203+
vm = tag_dict['vm']
204+
vr = tag_dict['vr']
205+
n_items = tag_dict['n_items']
206+
assert n_items < 100
207+
result.append(struct.pack('64si4s3i',
208+
make_nt_str(tag_name),
209+
vm,
210+
make_nt_str(vr),
211+
tag_dict['syngodt'],
212+
n_items,
213+
tag_dict['last3'])
214+
)
215+
216+
# Figure out the number of values for this tag
217+
if vm == 0:
218+
n_values = n_items
219+
else:
220+
n_values = vm
221+
222+
# Add each item for this tag
223+
for item_no in range(n_items):
224+
# Figure out the item length
225+
if item_no >= n_values or tag_dict['items'][item_no] == '':
226+
item_len = 0
227+
else:
228+
item = tag_dict['items'][item_no]
229+
if not isinstance(item, str):
230+
item = str(item)
231+
item_nt_str = make_nt_str(item)
232+
item_len = len(item_nt_str)
233+
234+
# These values aren't actually preserved in the dict
235+
# representation of the header. Best we can do is set the ones
236+
# that determine the item length appropriately.
237+
x0, x1, x2, x3 = 0, 0, 0, 0
238+
if csa_header['type'] == 1: # CSA1 - odd length calculation
239+
x0 = tag0_n_items + item_len
240+
if item_len < 0 or (ptr + item_len) > csa_len:
241+
if item_no < vm:
242+
items.append('')
243+
break
244+
else: # CSA2
245+
x1 = item_len
246+
result.append(struct.pack('4i', x0, x1, x2, x3))
247+
248+
if item_len == 0:
249+
continue
250+
251+
result.append(item_nt_str)
252+
# go to 4 byte boundary
253+
plus4 = item_len % 4
254+
if plus4 != 0:
255+
result.append(b'\x00' * (4 - plus4))
256+
return b''.join(result)
257+
258+
164259
def get_scalar(csa_dict, tag_name):
165260
try:
166261
items = csa_dict['tags'][tag_name]['items']
@@ -258,3 +353,18 @@ def nt_str(s):
258353
if zero_pos == -1:
259354
return s
260355
return s[:zero_pos].decode('latin-1')
356+
357+
358+
def make_nt_str(s):
359+
''' Create a null terminated byte string from a unicode object.
360+
361+
Parameters
362+
----------
363+
s : unicode
364+
365+
Returns
366+
-------
367+
result : bytes
368+
s encoded as latin-1 with a null char appended
369+
'''
370+
return s.encode('latin-1') + b'\x00'

nibabel/nicom/tests/test_csareader.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,14 @@ def test_missing_csa_elem():
130130
del dcm[csa_tag]
131131
hdr = csa.get_csa_header(dcm, 'image')
132132
assert hdr is None
133+
134+
135+
def test_read_write_rt():
136+
# Try doing a read-write-read round trip and make sure the dictionary
137+
# representation of the header is the same. We can't exactly reproduce the
138+
# original string representation currently.
139+
for csa_str in (CSA2_B0, CSA2_B1000):
140+
csa_info = csa.read(csa_str)
141+
new_csa_str = csa.write(csa_info)
142+
new_csa_info = csa.read(new_csa_str)
143+
assert csa_info == new_csa_info

0 commit comments

Comments
 (0)