From 8e62e9983222f05a691df1dfc06d900751a50e70 Mon Sep 17 00:00:00 2001 From: Chris Markiewicz Date: Tue, 3 Jan 2023 13:21:50 -0500 Subject: [PATCH 1/3] ENH: Support multiline header fields in TCK Treat repeated keys as adding lines to the field Closes gh-957 --- nibabel/streamlines/tck.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/nibabel/streamlines/tck.py b/nibabel/streamlines/tck.py index ec8e7dbce7..144c7bef26 100644 --- a/nibabel/streamlines/tck.py +++ b/nibabel/streamlines/tck.py @@ -6,6 +6,7 @@ import os import warnings +from contextlib import suppress import numpy as np @@ -331,6 +332,8 @@ def _read_header(cls, fileobj): f.seek(1, os.SEEK_CUR) # Skip \n found_end = False + key = None + tmp_hdr = {} # Read all key-value pairs contained in the header, stop at EOF for n_line, line in enumerate(f, 1): @@ -343,15 +346,22 @@ def _read_header(cls, fileobj): found_end = True break - if ':' not in line: # Invalid header line + # Set new key if available, otherwise append to last known key + with suppress(ValueError): + key, line = line.split(':', 1) + key = key.strip() + + # Apparent continuation line before any keys are found + if key is None: raise HeaderError(f'Invalid header (line {n_line}): {line}') - key, value = line.split(':', 1) - hdr[key.strip()] = value.strip() + tmp_hdr.setdefault(key, []).append(line.strip()) if not found_end: raise HeaderError('Missing END in the header.') + hdr.update({key: '\n'.join(val) for key, val in tmp_hdr.items()}) + offset_data = f.tell() # Set the file position where it was, in case it was previously open From be2cc874496d3a2af1dba14651c2702592efc676 Mon Sep 17 00:00:00 2001 From: Chris Markiewicz Date: Tue, 3 Jan 2023 13:40:19 -0500 Subject: [PATCH 2/3] ENH: Remove restriction on writing newlines in header --- nibabel/streamlines/tck.py | 5 ----- nibabel/streamlines/tests/test_tck.py | 4 ---- 2 files changed, 9 deletions(-) diff --git a/nibabel/streamlines/tck.py b/nibabel/streamlines/tck.py index 144c7bef26..43df2f87e0 100644 --- a/nibabel/streamlines/tck.py +++ b/nibabel/streamlines/tck.py @@ -267,11 +267,6 @@ def _write_header(fileobj, header): ) out = '\n'.join(lines) - # Check the header is well formatted. - if out.count('\n') > len(lines) - 1: # \n only allowed between lines. - msg = f"Key-value pairs cannot contain '\\n':\n{out}" - raise HeaderError(msg) - if out.count(':') > len(lines): # : only one per line (except the last one which contains END). msg = f"Key-value pairs cannot contain ':':\n{out}" diff --git a/nibabel/streamlines/tests/test_tck.py b/nibabel/streamlines/tests/test_tck.py index f514d3f3df..eb464042df 100644 --- a/nibabel/streamlines/tests/test_tck.py +++ b/nibabel/streamlines/tests/test_tck.py @@ -192,10 +192,6 @@ def test_write_simple_file(self): # TCK file containing not well formatted entries in its header. tck_file = BytesIO() tck = TckFile(tractogram) - tck.header['new_entry'] = 'value\n' # \n not allowed - with pytest.raises(HeaderError): - tck.save(tck_file) - tck.header['new_entry'] = 'val:ue' # : not allowed with pytest.raises(HeaderError): tck.save(tck_file) From 95df42eab6ae1806981213a210e2e79bd568ed81 Mon Sep 17 00:00:00 2001 From: Chris Markiewicz Date: Tue, 3 Jan 2023 13:40:57 -0500 Subject: [PATCH 3/3] TEST: Test that multiline header items can be parsed --- nibabel/streamlines/tests/test_tck.py | 9 +++++++++ nibabel/tests/data/multiline_header_field.tck | Bin 0 -> 4411 bytes 2 files changed, 9 insertions(+) create mode 100644 nibabel/tests/data/multiline_header_field.tck diff --git a/nibabel/streamlines/tests/test_tck.py b/nibabel/streamlines/tests/test_tck.py index eb464042df..3df7dd4f2d 100644 --- a/nibabel/streamlines/tests/test_tck.py +++ b/nibabel/streamlines/tests/test_tck.py @@ -31,6 +31,7 @@ def setup_module(): # standard.tck contains only streamlines DATA['standard_tck_fname'] = pjoin(data_path, 'standard.tck') DATA['matlab_nan_tck_fname'] = pjoin(data_path, 'matlab_nan.tck') + DATA['multiline_header_fname'] = pjoin(data_path, 'multiline_header_field.tck') DATA['streamlines'] = [ np.arange(1 * 3, dtype='f4').reshape((1, 3)), @@ -87,6 +88,14 @@ def test_load_matlab_nan_file(self): assert len(streamlines) == 1 assert streamlines[0].shape == (108, 3) + def test_load_multiline_header_file(self): + for lazy_load in [False, True]: + tck = TckFile.load(DATA['multiline_header_fname'], lazy_load=lazy_load) + streamlines = list(tck.tractogram.streamlines) + assert len(tck.header['command_history'].splitlines()) == 3 + assert len(streamlines) == 1 + assert streamlines[0].shape == (253, 3) + def test_writeable_data(self): data = DATA['simple_tractogram'] for key in ('simple_tck_fname', 'simple_tck_big_endian_fname'): diff --git a/nibabel/tests/data/multiline_header_field.tck b/nibabel/tests/data/multiline_header_field.tck new file mode 100644 index 0000000000000000000000000000000000000000..42ebedc43a94cb15657a7e52fb6e89196b258f3b GIT binary patch literal 4411 zcmeH{OKaRP5XbkdPcg^#V6VNeB!(UWN$4SML)lX=Lbc_!x_TICoMd}xK3YFo$M$+5 zEumTZfPe+U!^j$q{_`6dk6zsadXaAf%UxqJgJc*fD+CJM)iQ%C?F7ze*O&B{F~SzS zQURUcWq|p1BP~S2ZnT8iEM7un#JwipY?+X)Y-^`v*6vPvNaF;i#^Ur#b3e^(5vHHc zZ$2jJ)O6|a5a!79P)x5rT<{xLH(H=_b}da!mfZi%+ttAr8&yISq%62HVk7@{eEv>^ zZGVU%>mP;>Qp+4+))xVyyg^r%ki?|lvqW1{G0|P4hpG^iJ~01SH~EAMLI zg$XfL7U0#b^nv7mGheF^8p&^$@hm=l&1h8?3ZXCFE?thdPzH)UbX9U#>D%tG)cHGv1tPJIw5v_G^HZM+eQ-#u99>6I5oc`QHTyD= z29N5*T!VFkJfN(3P+zH+lX$f_Ysblo`R#%ntd}wHTKewK3>GX@y!MjXowiFlEoH$q zKBoZB;_$fUWl!WsZ%oadkn z&2~KO=V*_bj0}tnyhH{bDCuM8OT6cp^VpyNlYwJ