You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

132 lines
4.1 KiB

  1. import struct
  2. import array
  3. import six
  4. def read_chunk(fh, chunk_location):
  5. """
  6. Reads a piece of data given the location of its pointer.
  7. :param fh: an open file handle to the ND2
  8. :param chunk_location: a pointer
  9. :type chunk_location: int
  10. :rtype: bytes
  11. """
  12. if chunk_location is None:
  13. return None
  14. fh.seek(chunk_location)
  15. # The chunk metadata is always 16 bytes long
  16. chunk_metadata = fh.read(16)
  17. header, relative_offset, data_length = struct.unpack("IIQ", chunk_metadata)
  18. if header != 0xabeceda:
  19. raise ValueError("The ND2 file seems to be corrupted.")
  20. # We start at the location of the chunk metadata, skip over the metadata, and then proceed to the
  21. # start of the actual data field, which is at some arbitrary place after the metadata.
  22. fh.seek(chunk_location + 16 + relative_offset)
  23. return fh.read(data_length)
  24. def read_array(fh, kind, chunk_location):
  25. kinds = {'double': 'd',
  26. 'int': 'i',
  27. 'float': 'f'}
  28. if kind not in kinds:
  29. raise ValueError('You attempted to read an array of an unknown type.')
  30. raw_data = read_chunk(fh, chunk_location)
  31. if raw_data is None:
  32. return None
  33. return array.array(kinds[kind], raw_data)
  34. def _parse_unsigned_char(data):
  35. return struct.unpack("B", data.read(1))[0]
  36. def _parse_unsigned_int(data):
  37. return struct.unpack("I", data.read(4))[0]
  38. def _parse_unsigned_long(data):
  39. return struct.unpack("Q", data.read(8))[0]
  40. def _parse_double(data):
  41. return struct.unpack("d", data.read(8))[0]
  42. def _parse_string(data):
  43. value = data.read(2)
  44. while not value.endswith(six.b("\x00\x00")):
  45. # the string ends at the first instance of \x00\x00
  46. value += data.read(2)
  47. return value.decode("utf16")[:-1].encode("utf8")
  48. def _parse_char_array(data):
  49. array_length = struct.unpack("Q", data.read(8))[0]
  50. return array.array("B", data.read(array_length))
  51. def _parse_metadata_item(data, cursor_position):
  52. """
  53. Reads hierarchical data, analogous to a Python dict.
  54. """
  55. new_count, length = struct.unpack("<IQ", data.read(12))
  56. length -= data.tell() - cursor_position
  57. next_data_length = data.read(length)
  58. value = read_metadata(next_data_length, new_count)
  59. # Skip some offsets
  60. data.read(new_count * 8)
  61. return value
  62. def _get_value(data, data_type, cursor_position):
  63. """
  64. ND2s use various codes to indicate different data types, which we translate here.
  65. """
  66. parser = {1: _parse_unsigned_char,
  67. 2: _parse_unsigned_int,
  68. 3: _parse_unsigned_int,
  69. 5: _parse_unsigned_long,
  70. 6: _parse_double,
  71. 8: _parse_string,
  72. 9: _parse_char_array,
  73. 11: _parse_metadata_item}
  74. return parser[data_type](data) if data_type < 11 else parser[data_type](data, cursor_position)
  75. def read_metadata(data, count):
  76. """
  77. Iterates over each element some section of the metadata and parses it.
  78. """
  79. if data is None:
  80. return None
  81. data = six.BytesIO(data)
  82. metadata = {}
  83. for _ in range(count):
  84. cursor_position = data.tell()
  85. header = data.read(2)
  86. if not header:
  87. # We've reached the end of some hierarchy of data
  88. break
  89. if six.PY3:
  90. header = header.decode("utf8")
  91. data_type, name_length = map(ord, header)
  92. name = data.read(name_length * 2).decode("utf16")[:-1].encode("utf8")
  93. value = _get_value(data, data_type, cursor_position)
  94. if name not in metadata.keys():
  95. metadata[name] = value
  96. else:
  97. if not isinstance(metadata[name], list):
  98. # We have encountered this key exactly once before. Since we're seeing it again, we know we
  99. # need to convert it to a list before proceeding.
  100. metadata[name] = [metadata[name]]
  101. # We've encountered this key before so we're guaranteed to be dealing with a list. Thus we append
  102. # the value to the already-existing list.
  103. metadata[name].append(value)
  104. return metadata