From c417e85a92ca22d30ed76c84ff5635574850b320 Mon Sep 17 00:00:00 2001 From: Stefan Ihringer Date: Thu, 18 Nov 2021 22:33:24 +0100 Subject: [PATCH] support for returning combined channel list of multi-part files --- src/parse_metadata.py | 33 ++++++++++--- tests/test_exr.py | 110 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 7 deletions(-) diff --git a/src/parse_metadata.py b/src/parse_metadata.py index e466f9e..bcd0708 100644 --- a/src/parse_metadata.py +++ b/src/parse_metadata.py @@ -101,25 +101,44 @@ def read_exr_header(exrpath, maxreadsize=2000): ord(openxr_version_number[0]), ord(version_field_attrs[0]), ord(version_field_attrs[1]), ord(version_field_attrs[2])) + is_multipart = bool(ord(version_field_attrs[0]) & 0b10000) + if is_multipart: + log.info('multi-part bit is set') log.info("METADATA:") i = 0 + part_count = 0 + next_header = False while i < maxreadsize: # We'll always have attribute name, attribute type separated by a # null byte. Then attribute size and attribute value follow attribute_name, attribute_name_length = read_until_null(exr_file) - attribute_type, _ = read_until_null(exr_file) - attribute_size = int(struct.unpack('i', exr_file.read(4))[0]) # If we're reading only byte it means it's the null byte - # and we've reached the end of the header + # and we've reached the end of the header. In multi-part files + # the headers are done after two null bytes in a row. if attribute_name_length == 1: - log.debug('reached the end of the header!') - break + if is_multipart is False or next_header is True: + log.debug('reached the end of the header!') + break + else: + part_count += 1 + next_header = True + log.debug('end of part {}'.format(part_count)) + continue + else: + next_header = False - if not attribute_name in metadata: + attribute_type, _ = read_until_null(exr_file) + attribute_size = int(struct.unpack('i', exr_file.read(4))[0]) + if attribute_name not in metadata: metadata[attribute_name] = {} + elif is_multipart and attribute_name != "channels": + # in multi-part files, skip over all attributes that already exist + # except for the channel list + exr_file.read(attribute_size) + continue # How many bytes of the attribute value we've read byte_count = 0 @@ -182,7 +201,7 @@ def read_exr_header(exrpath, maxreadsize=2000): 'ySampling': y_sampling } - metadata[attribute_name] = channel_data + metadata[attribute_name].update(channel_data) elif attribute_type == b'chromaticities': chromaticities = struct.unpack('f' * 8, exr_file.read(4 * 8)) diff --git a/tests/test_exr.py b/tests/test_exr.py index 681c326..ab450d1 100644 --- a/tests/test_exr.py +++ b/tests/test_exr.py @@ -104,6 +104,116 @@ def test_exr_meta_owner(): 'owner': 'Copyright 2006 Industrial Light & Magic', 'screenWindowWidth': 1.0, 'lineOrder': 'INCREASING_Y' + }), + # Beachball (multipart) + pytest.param(os.path.join(EXR_IMAGES_DIR_PATH, 'Beachball', 'multipart.0001.exr'), { + 'compression': 'ZIPS_COMPRESSION', + 'pixelAspectRatio': 1.0, + 'displayWindow': { + 'xMax': 2047, + 'xMin': 0, + 'yMax': 1555, + 'yMin': 0 + }, + 'dataWindow': { + 'xMax': 1530, + 'xMin': 654, + 'yMax': 1120, + 'yMin': 245 + }, + 'channels': { + 'A': { + 'pLinear': 0, + 'pixel_type': 1, + 'reserved': [0, 0, 0], + 'xSampling': 1, + 'ySampling': 1 + }, + 'B': { + 'pLinear': 0, + 'pixel_type': 1, + 'reserved': [0, 0, 0], + 'xSampling': 1, + 'ySampling': 1 + }, + 'G': { + 'pLinear': 0, + 'pixel_type': 1, + 'reserved': [0, 0, 0], + 'xSampling': 1, + 'ySampling': 1 + }, + 'R': { + 'pLinear': 0, + 'pixel_type': 1, + 'reserved': [0, 0, 0], + 'xSampling': 1, + 'ySampling': 1 + }, + 'Z': { + 'pLinear': 0, + 'pixel_type': 1, + 'reserved': [0, 0, 0], + 'xSampling': 1, + 'ySampling': 1 + }, + 'disparityL.x': { + 'pLinear': 0, + 'pixel_type': 1, + 'reserved': [0, 0, 0], + 'xSampling': 1, + 'ySampling': 1 + }, + 'disparityL.y': { + 'pLinear': 0, + 'pixel_type': 1, + 'reserved': [0, 0, 0], + 'xSampling': 1, + 'ySampling': 1 + }, + 'disparityR.x': { + 'pLinear': 0, + 'pixel_type': 1, + 'reserved': [0, 0, 0], + 'xSampling': 1, + 'ySampling': 1 + }, + 'disparityR.y': { + 'pLinear': 0, + 'pixel_type': 1, + 'reserved': [0, 0, 0], + 'xSampling': 1, + 'ySampling': 1 + }, + 'forward.u': { + 'pLinear': 0, + 'pixel_type': 1, + 'reserved': [0, 0, 0], + 'xSampling': 1, + 'ySampling': 1 + }, + 'forward.v': { + 'pLinear': 0, + 'pixel_type': 1, + 'reserved': [0, 0, 0], + 'xSampling': 1, + 'ySampling': 1 + }, + 'whitebarmask.mask': { + 'pLinear': 0, + 'pixel_type': 1, + 'reserved': [0, 0, 0], + 'xSampling': 1, + 'ySampling': 1 + } + }, + 'screenWindowCenter': [0.0, 0.0], + 'screenWindowWidth': 1.0, + 'lineOrder': 'INCREASING_Y', + 'chunkCount': 876, + 'name': 'rgba_right', + 'type': 'scanlineimage', + 'view': 'right', }) ]) def test_exr_meta_all(input_path, expected_metadata):