diff --git a/nrrd/reader.py b/nrrd/reader.py index 37a2aa7..d5823f3 100644 --- a/nrrd/reader.py +++ b/nrrd/reader.py @@ -339,22 +339,30 @@ def read_data(header, fh=None, filename=None): # Get the total number of data points by multiplying the size of each dimension together total_data_points = header['sizes'].prod() - # If encoding is raw and byte skip is -1, then seek backwards to the data - # Otherwise skip the number of lines requested - if header['encoding'] == 'raw' and byte_skip == -1: - fh.seek(-dtype.itemsize * total_data_points, 2) - else: + # Skip the number of lines requested when line_skip >= 0 + # Irrespective of the NRRD file having attached/detached header + # Lines are skipped before getting to the beginning of the data + if line_skip >= 0: for _ in range(line_skip): fh.readline() - - # If a compression encoding is used, then byte skip AFTER decompressing - if header['encoding'] == 'raw': - # Skip the requested number of bytes and then parse the data using NumPy + else: + raise NRRDError('Invalid lineskip, allowed values are greater than or equal to 0') + + # Skip the requested number of bytes or seek backward, and then parse the data using NumPy + if byte_skip < -1: + raise NRRDError('Invalid byteskip, allowed values are greater than or equal to -1') + elif byte_skip >= 0: fh.seek(byte_skip, os.SEEK_CUR) + elif byte_skip == -1 and header['encoding'] not in ['gzip', 'gz', 'bzip2', 'bz2']: + fh.seek(-dtype.itemsize * total_data_points, os.SEEK_END) + else: + # The only case left should be: byte_skip == -1 and header['encoding'] == 'gzip' + byte_skip = -dtype.itemsize * total_data_points + + # If a compression encoding is used, then byte skip AFTER decompressing + if header['encoding'] == 'raw': data = np.fromfile(fh, dtype) elif header['encoding'] in ['ASCII', 'ascii', 'text', 'txt']: - # Skip the requested number of bytes and then parse the data using NumPy - fh.seek(byte_skip, os.SEEK_CUR) data = np.fromfile(fh, dtype, sep=' ') else: # Handle compressed data now @@ -423,7 +431,6 @@ def read(filename, custom_field_map=None): """ """Read a NRRD file and return a tuple (data, header).""" - with open(filename, 'rb') as fh: header = read_header(fh, custom_field_map) data = read_data(header, fh, filename) diff --git a/nrrd/tests/data/BallBinary30x30x30.nii.gz b/nrrd/tests/data/BallBinary30x30x30.nii.gz new file mode 100644 index 0000000..e7cabb6 Binary files /dev/null and b/nrrd/tests/data/BallBinary30x30x30.nii.gz differ diff --git a/nrrd/tests/data/BallBinary30x30x30_byteskip_minus_five.nhdr b/nrrd/tests/data/BallBinary30x30x30_byteskip_minus_five.nhdr new file mode 100644 index 0000000..39e3030 --- /dev/null +++ b/nrrd/tests/data/BallBinary30x30x30_byteskip_minus_five.nhdr @@ -0,0 +1,14 @@ +NRRD0004 +# Complete NRRD file format specification at: +# http://teem.sourceforge.net/nrrd/format.html +type: short +dimension: 3 +space: left-posterior-superior +sizes: 30 30 30 +byte skip: -5 +space directions: (1,0,0) (0,1,0) (0,0,1) +kinds: domain domain domain +endian: little +encoding: raw +space origin: (0,0,0) +data file: BallBinary30x30x30.raw diff --git a/nrrd/tests/data/BallBinary30x30x30_byteskip_minus_one.nhdr b/nrrd/tests/data/BallBinary30x30x30_byteskip_minus_one.nhdr new file mode 100644 index 0000000..3e4bd41 --- /dev/null +++ b/nrrd/tests/data/BallBinary30x30x30_byteskip_minus_one.nhdr @@ -0,0 +1,14 @@ +NRRD0004 +# Complete NRRD file format specification at: +# http://teem.sourceforge.net/nrrd/format.html +type: short +dimension: 3 +space: left-posterior-superior +sizes: 30 30 30 +byte skip: -1 +space directions: (1,0,0) (0,1,0) (0,0,1) +kinds: domain domain domain +endian: little +encoding: raw +space origin: (0,0,0) +data file: BallBinary30x30x30.raw diff --git a/nrrd/tests/data/BallBinary30x30x30_byteskip_minus_one_nifti.nhdr b/nrrd/tests/data/BallBinary30x30x30_byteskip_minus_one_nifti.nhdr new file mode 100644 index 0000000..e30cd98 --- /dev/null +++ b/nrrd/tests/data/BallBinary30x30x30_byteskip_minus_one_nifti.nhdr @@ -0,0 +1,14 @@ +NRRD0004 +# Complete NRRD file format specification at: +# http://teem.sourceforge.net/nrrd/format.html +type: short +dimension: 3 +space: left-posterior-superior +sizes: 30 30 30 +byte skip: -1 +space directions: (1,0,0) (0,1,0) (0,0,1) +kinds: domain domain domain +endian: little +encoding: gzip +space origin: (0,0,0) +data file: BallBinary30x30x30.nii.gz diff --git a/nrrd/tests/data/BallBinary30x30x30_gz_byteskip_minus_one.nrrd b/nrrd/tests/data/BallBinary30x30x30_gz_byteskip_minus_one.nrrd new file mode 100644 index 0000000..b6f90fe Binary files /dev/null and b/nrrd/tests/data/BallBinary30x30x30_gz_byteskip_minus_one.nrrd differ diff --git a/nrrd/tests/data/BallBinary30x30x30_nifti.nhdr b/nrrd/tests/data/BallBinary30x30x30_nifti.nhdr new file mode 100644 index 0000000..2627295 --- /dev/null +++ b/nrrd/tests/data/BallBinary30x30x30_nifti.nhdr @@ -0,0 +1,14 @@ +NRRD0004 +# Complete NRRD file format specification at: +# http://teem.sourceforge.net/nrrd/format.html +type: short +dimension: 3 +space: left-posterior-superior +sizes: 30 30 30 +# byte skip: -1 +space directions: (1,0,0) (0,1,0) (0,0,1) +kinds: domain domain domain +endian: little +encoding: gzip +space origin: (0,0,0) +data file: BallBinary30x30x30.nii.gz diff --git a/nrrd/tests/test_reading.py b/nrrd/tests/test_reading.py index 0a102cb..e30b9f0 100644 --- a/nrrd/tests/test_reading.py +++ b/nrrd/tests/test_reading.py @@ -68,6 +68,46 @@ def test_read_detached_header_and_data(self): # Test that the data read is able to be edited self.assertTrue(data.flags['WRITEABLE']) + + def test_read_detached_header_and_data_with_byteskip_minus1(self): + expected_header = self.expected_header + expected_header[u'data file'] = os.path.basename(RAW_DATA_FILE_PATH) + expected_header[u'byte skip'] = -1 + + data, header = nrrd.read(RAW_BYTESKIP_NHDR_FILE_PATH) + + np.testing.assert_equal(self.expected_header, header) + np.testing.assert_equal(data, self.expected_data) + + # Test that the data read is able to be edited + self.assertTrue(data.flags['WRITEABLE']) + + def test_read_detached_header_and_nifti_data_with_byteskip_minus1(self): + expected_header = self.expected_header + expected_header[u'data file'] = os.path.basename(RAW_DATA_FILE_PATH) + expected_header[u'byte skip'] = -1 + expected_header[u'encoding'] = 'gzip' + expected_header[u'data file'] = 'BallBinary30x30x30.nii.gz' + + data, header = nrrd.read(GZ_BYTESKIP_NIFTI_NHDR_FILE_PATH) + + np.testing.assert_equal(self.expected_header, header) + np.testing.assert_equal(data, self.expected_data) + + # Test that the data read is able to be edited + self.assertTrue(data.flags['WRITEABLE']) + + def test_read_detached_header_and_nifti_data(self): + + with self.assertRaisesRegex(nrrd.NRRDError, 'Size of the data does not equal ' + + 'the product of all the dimensions: 27000-27176=-176'): + nrrd.read(GZ_NIFTI_NHDR_FILE_PATH) + + def test_read_detached_header_and_data_with_byteskip_minus5(self): + + with self.assertRaisesRegex(nrrd.NRRDError, 'Invalid byteskip, allowed values ' + +'are greater than or equal to -1'): + nrrd.read(RAW_INVALID_BYTESKIP_NHDR_FILE_PATH) def test_read_header_and_gz_compressed_data(self): expected_header = self.expected_header @@ -80,6 +120,20 @@ def test_read_header_and_gz_compressed_data(self): # Test that the data read is able to be edited self.assertTrue(data.flags['WRITEABLE']) + + def test_read_header_and_gz_compressed_data_with_byteskip_minus1(self): + expected_header = self.expected_header + expected_header[u'encoding'] = 'gzip' + expected_header[u'type'] = 'int16' + expected_header[u'byte skip'] = -1 + + data, header = nrrd.read(GZ_BYTESKIP_NRRD_FILE_PATH) + + np.testing.assert_equal(self.expected_header, header) + np.testing.assert_equal(data, self.expected_data) + + # Test that the data read is able to be edited + self.assertTrue(data.flags['WRITEABLE']) def test_read_header_and_bz2_compressed_data(self): expected_header = self.expected_header @@ -130,7 +184,7 @@ def test_read_dup_field_error_and_warn(self): self.assertTrue("Duplicate header field: 'type'" in str(w[0].message)) self.assertEqual(expected_header, header) - nrrd.reader._NRRD_ALLOW_DUPLICATE_FIELD = False + nrrd.reader.ALLOW_DUPLICATE_FIELD = False def test_read_header_and_ascii_1d_data(self): expected_header = {u'dimension': 1, diff --git a/nrrd/tests/util.py b/nrrd/tests/util.py index 7435ac0..746f618 100644 --- a/nrrd/tests/util.py +++ b/nrrd/tests/util.py @@ -4,10 +4,15 @@ DATA_DIR_PATH = os.path.join(os.path.dirname(__file__), 'data') RAW_NRRD_FILE_PATH = os.path.join(DATA_DIR_PATH, 'BallBinary30x30x30.nrrd') RAW_NHDR_FILE_PATH = os.path.join(DATA_DIR_PATH, 'BallBinary30x30x30.nhdr') +RAW_BYTESKIP_NHDR_FILE_PATH = os.path.join(DATA_DIR_PATH, 'BallBinary30x30x30_byteskip_minus_one.nhdr') +GZ_BYTESKIP_NIFTI_NHDR_FILE_PATH = os.path.join(DATA_DIR_PATH, 'BallBinary30x30x30_byteskip_minus_one_nifti.nhdr') +GZ_NIFTI_NHDR_FILE_PATH = os.path.join(DATA_DIR_PATH, 'BallBinary30x30x30_nifti.nhdr') +RAW_INVALID_BYTESKIP_NHDR_FILE_PATH = os.path.join(DATA_DIR_PATH, 'BallBinary30x30x30_byteskip_minus_five.nhdr') RAW_DATA_FILE_PATH = os.path.join(DATA_DIR_PATH, 'BallBinary30x30x30.raw') GZ_NRRD_FILE_PATH = os.path.join(DATA_DIR_PATH, 'BallBinary30x30x30_gz.nrrd') BZ2_NRRD_FILE_PATH = os.path.join(DATA_DIR_PATH, 'BallBinary30x30x30_bz2.nrrd') GZ_LINESKIP_NRRD_FILE_PATH = os.path.join(DATA_DIR_PATH, 'BallBinary30x30x30_gz_lineskip.nrrd') +GZ_BYTESKIP_NRRD_FILE_PATH = os.path.join(DATA_DIR_PATH, 'BallBinary30x30x30_gz_byteskip_minus_one.nrrd') RAW_4D_NRRD_FILE_PATH = os.path.join(DATA_DIR_PATH, 'test_simple4d_raw.nrrd') ASCII_1D_NRRD_FILE_PATH = os.path.join(DATA_DIR_PATH, 'test1d_ascii.nrrd')