diff --git a/nd2reader/common.py b/nd2reader/common.py index df5ddcf..65acea4 100644 --- a/nd2reader/common.py +++ b/nd2reader/common.py @@ -351,3 +351,14 @@ def check_or_make_dir(directory): """ if not os.path.exists(directory): os.makedirs(directory) + +def decode_str(value): + try: + decoded = value.decode('utf8') + except UnicodeDecodeError: + try: + decoded = value.decode('utf16') + except UnicodeDecodeError: + decoded = '' + + return decoded diff --git a/nd2reader/reader.py b/nd2reader/reader.py index 8246125..add799c 100644 --- a/nd2reader/reader.py +++ b/nd2reader/reader.py @@ -2,7 +2,7 @@ from pims.base_frames import FramesSequenceND from nd2reader.exceptions import EmptyFileError, InvalidFileType -from nd2reader.parser import Parser +from nd2reader.sync_parser import SyncParser import numpy as np @@ -23,13 +23,13 @@ def __init__(self, filename): # first use the parser to parse the file self._fh = open(filename, "rb") - self._parser = Parser(self._fh) + self.parser = SyncParser(self._fh) # Setup metadata - self.metadata = self._parser.metadata + self.metadata = self.parser.metadata # Set data type - self._dtype = self._parser.get_dtype_from_metadata() + self._dtype = self.parser.get_dtype_from_metadata() # Setup the axes self._setup_axes() @@ -37,6 +37,12 @@ def __init__(self, filename): # Other properties self._timesteps = None + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + @classmethod def class_exts(cls): """Let PIMS open function use this reader for opening .nd2 files @@ -73,16 +79,7 @@ def get_frame_2D(self, c=0, t=0, z=0, x=0, y=0, v=0): x = self.metadata["width"] y = self.metadata["height"] - return self._parser.get_image_by_attributes(t, v, c, z, y, x) - - @property - def parser(self): - """ - Returns the parser object. - Returns: - Parser: the parser object - """ - return self._parser + return self.parser.get_image_by_attributes(t, v, c, z, y, x) @property def pixel_type(self): @@ -202,6 +199,6 @@ def get_timesteps(self): if self._timesteps is not None and len(self._timesteps) > 0: return self._timesteps - self._timesteps = np.array(list(self._parser._raw_metadata.acquisition_times), dtype=np.float) * 1000.0 + self._timesteps = np.array(list(self.parser._raw_metadata.acquisition_times), dtype=np.float) * 1000.0 return self._timesteps diff --git a/nd2reader/sync_parser.py b/nd2reader/sync_parser.py new file mode 100644 index 0000000..fbd07b1 --- /dev/null +++ b/nd2reader/sync_parser.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +import struct + +import array +import warnings +from pims.base_frames import Frame +import numpy as np + +from nd2reader.exceptions import InvalidVersionError +from nd2reader.common import decode_str + +class SyncParser(object): + metadata = None + + """Parses ND2 files synchronously instead of relying on the stored + metadata (which is wrong or incomplete in a lot of cases). + + """ + def __init__(self, fh): + self._fh = fh + + self.parse() + + """Parse the whole file, store a representation of it but not the image + data, to manage memory usage and keep accessing images efficient. """ + def parse(self): + # file version information + print(self._collect_block_containing(b'ND2')) + + # ImageCalibration + print(self._collect_block_containing(b'Image')) + + def _collect_block_containing(self, value_of_interest): + value = decode_str(self._skip_to(value_of_interest)) + found = value_of_interest in value + for block in self._read_block(): + if value_of_interest in block: + found = True + + if found: + value, search_on = self._collect_str(value, block) + if not search_on: + break + + return value + + def _skip_to(self, to_value): + for block in self._read_block(): + if to_value not in block: + continue + + return b' '.join(block.split(b'\x00')).strip() + + def _collect_str(self, value, block): + # Get the string from the current block of bytes + converted = b' '.join(block.split(b'\x00')).strip() + if len(converted) == 0: + return value, False + + value += decode_str(converted) + return value, True + + def _read_block(self): + """Read through the file, 16 bytes at a time + """ + block = True # placeholder for the `while` + while block: + block = self._fh.read(16) + if block: + yield block diff --git a/tests/test_sync_parser.py b/tests/test_sync_parser.py new file mode 100644 index 0000000..da218b2 --- /dev/null +++ b/tests/test_sync_parser.py @@ -0,0 +1,25 @@ +import unittest +from os import path +from nd2reader.artificial import ArtificialND2 +from nd2reader.common import check_or_make_dir +from nd2reader.sync_parser import SyncParser + + +class TestSyncParser(unittest.TestCase): + def create_test_nd2(self): + with ArtificialND2(self.test_file) as artificial: + artificial.close() + + def setUp(self): + dir_path = path.dirname(path.realpath(__file__)) + check_or_make_dir(path.join(dir_path, 'test_data/')) + self.test_file = path.join(dir_path, 'test_data/test.nd2') + self.create_test_nd2() + + def test_can_open_test_file(self): + self.create_test_nd2() + + with open(self.test_file, 'rb') as fh: + parser = SyncParser(fh) + +