From 2c26fa57b948eebb44bcc9a44ba64f1eaf477202 Mon Sep 17 00:00:00 2001 From: Chris Markiewicz Date: Sun, 9 Jun 2024 11:10:12 -0400 Subject: [PATCH 1/3] MNT: Fix ruff pre-commit arguments --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 354bd3da1..b348393a4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,7 +16,7 @@ repos: rev: v0.3.4 hooks: - id: ruff - args: [--fix, --show-fix, --exit-non-zero-on-fix] + args: [--fix, --show-fixes, --exit-non-zero-on-fix] exclude: = ["doc", "tools"] - id: ruff-format exclude: = ["doc", "tools"] From 63f0647da9bbd0b584b09a8edc7cbac01deff608 Mon Sep 17 00:00:00 2001 From: Chris Markiewicz Date: Sun, 9 Jun 2024 11:10:28 -0400 Subject: [PATCH 2/3] ENH: Update Nifti extension codes, implement JSON type --- nibabel/nifti1.py | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/nibabel/nifti1.py b/nibabel/nifti1.py index d07e54de1..cc89113df 100644 --- a/nibabel/nifti1.py +++ b/nibabel/nifti1.py @@ -13,6 +13,7 @@ from __future__ import annotations +import json import warnings from io import BytesIO @@ -377,7 +378,7 @@ def __repr__(self): # deal with unknown codes code = self._code - s = f"Nifti1Extension('{code}', '{self._content}')" + s = f"{self.__class__.__name__}('{code}', '{self._content}')" return s def __eq__(self, other): @@ -505,6 +506,20 @@ def _mangle(self, dataset): return dio.read(ds_len) +class NiftiJSONExtension(Nifti1Extension): + """Generic JSON-based NIfTI header extension + + This class handles serialization and deserialization of JSON contents + without any further validation or processing. + """ + + def _unmangle(self, value: bytes) -> dict: + return json.loads(value.decode('utf-8')) + + def _mangle(self, value: dict) -> bytes: + return json.dumps(value).encode('utf-8') + + # NIfTI header extension type codes (ECODE) # see nifti1_io.h for a complete list of all known extensions and # references to their description or contacts of the respective @@ -520,6 +535,21 @@ def _mangle(self, dataset): (12, 'workflow_fwds', Nifti1Extension), (14, 'freesurfer', Nifti1Extension), (16, 'pypickle', Nifti1Extension), + (18, 'mind_ident', Nifti1Extension), + (20, 'b_value', Nifti1Extension), + (22, 'spherical_direction', Nifti1Extension), + (24, 'dt_component', Nifti1Extension), + (26, 'shc_degreeorder', Nifti1Extension), + (28, 'voxbo', Nifti1Extension), + (30, 'caret', Nifti1Extension), + ## Defined in nibabel.cifti2.parse_cifti2 + # (32, 'cifti', Cifti2Extension), + (34, 'variable_frame_timing', Nifti1Extension), + (36, 'unassigned', Nifti1Extension), + (38, 'eval', Nifti1Extension), + (40, 'matlab', Nifti1Extension), + (42, 'quantiphyse', Nifti1Extension), + (44, 'mrs', NiftiJSONExtension), ), fields=('code', 'label', 'handler'), ) From 9afa0be1f33ab7fb276e4cc3d13d87d19e1004bf Mon Sep 17 00:00:00 2001 From: Chris Markiewicz Date: Tue, 11 Jun 2024 22:32:37 -0400 Subject: [PATCH 3/3] TEST: Test NiftiJSONExtension --- nibabel/tests/test_nifti1.py | 51 ++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/nibabel/tests/test_nifti1.py b/nibabel/tests/test_nifti1.py index 5ee4fb3c1..e9f79319e 100644 --- a/nibabel/tests/test_nifti1.py +++ b/nibabel/tests/test_nifti1.py @@ -30,6 +30,7 @@ Nifti1Image, Nifti1Pair, Nifti1PairHeader, + NiftiJSONExtension, data_type_codes, extension_codes, load, @@ -1388,6 +1389,56 @@ def test_nifti_dicom_extension(): Nifti1DicomExtension(2, 0) +def test_json_extension(tmp_path): + nim = load(image_file) + hdr = nim.header + exts_container = hdr.extensions + + # Test basic functionality + json_ext = NiftiJSONExtension('ignore', b'{"key": "value"}') + assert json_ext.get_content() == {'key': 'value'} + byte_content = json_ext._mangle(json_ext.get_content()) + assert byte_content == b'{"key": "value"}' + json_obj = json_ext._unmangle(byte_content) + assert json_obj == {'key': 'value'} + size = 16 * ((len(byte_content) + 7) // 16 + 1) + assert json_ext.get_sizeondisk() == size + + def ext_to_bytes(ext, byteswap=False): + bio = BytesIO() + ext.write_to(bio, byteswap) + return bio.getvalue() + + # Check serialization + bytestring = ext_to_bytes(json_ext) + assert bytestring[:8] == struct.pack('<2I', size, extension_codes['ignore']) + assert bytestring[8:].startswith(byte_content) + assert len(bytestring) == size + + # Save to file and read back + exts_container.append(json_ext) + nim.to_filename(tmp_path / 'test.nii') + + # We used ignore, so it comes back as a Nifti1Extension + rt_img = Nifti1Image.from_filename(tmp_path / 'test.nii') + assert len(rt_img.header.extensions) == 3 + rt_ext = rt_img.header.extensions[-1] + assert rt_ext.get_code() == extension_codes['ignore'] + assert rt_ext.get_content() == byte_content + + # MRS is currently the only JSON extension + json_ext._code = extension_codes['mrs'] + nim.to_filename(tmp_path / 'test.nii') + + # Check that the extension is read back as a NiftiJSONExtension + rt_img = Nifti1Image.from_filename(tmp_path / 'test.nii') + assert len(rt_img.header.extensions) == 3 + rt_ext = rt_img.header.extensions[-1] + assert rt_ext.get_code() == extension_codes['mrs'] + assert isinstance(rt_ext, NiftiJSONExtension) + assert rt_ext.get_content() == json_obj + + class TestNifti1General: """Test class to test nifti1 in general