Skip to content

Commit c417e85

Browse files
committed
support for returning combined channel list of multi-part files
1 parent 3bb9396 commit c417e85

File tree

2 files changed

+136
-7
lines changed

2 files changed

+136
-7
lines changed

src/parse_metadata.py

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -101,25 +101,44 @@ def read_exr_header(exrpath, maxreadsize=2000):
101101
ord(openxr_version_number[0]),
102102
ord(version_field_attrs[0]), ord(version_field_attrs[1]),
103103
ord(version_field_attrs[2]))
104+
is_multipart = bool(ord(version_field_attrs[0]) & 0b10000)
105+
if is_multipart:
106+
log.info('multi-part bit is set')
104107
log.info("METADATA:")
105108
i = 0
109+
part_count = 0
110+
next_header = False
106111

107112
while i < maxreadsize:
108113

109114
# We'll always have attribute name, attribute type separated by a
110115
# null byte. Then attribute size and attribute value follow
111116
attribute_name, attribute_name_length = read_until_null(exr_file)
112-
attribute_type, _ = read_until_null(exr_file)
113-
attribute_size = int(struct.unpack('i', exr_file.read(4))[0])
114117

115118
# If we're reading only byte it means it's the null byte
116-
# and we've reached the end of the header
119+
# and we've reached the end of the header. In multi-part files
120+
# the headers are done after two null bytes in a row.
117121
if attribute_name_length == 1:
118-
log.debug('reached the end of the header!')
119-
break
122+
if is_multipart is False or next_header is True:
123+
log.debug('reached the end of the header!')
124+
break
125+
else:
126+
part_count += 1
127+
next_header = True
128+
log.debug('end of part {}'.format(part_count))
129+
continue
130+
else:
131+
next_header = False
120132

121-
if not attribute_name in metadata:
133+
attribute_type, _ = read_until_null(exr_file)
134+
attribute_size = int(struct.unpack('i', exr_file.read(4))[0])
135+
if attribute_name not in metadata:
122136
metadata[attribute_name] = {}
137+
elif is_multipart and attribute_name != "channels":
138+
# in multi-part files, skip over all attributes that already exist
139+
# except for the channel list
140+
exr_file.read(attribute_size)
141+
continue
123142

124143
# How many bytes of the attribute value we've read
125144
byte_count = 0
@@ -182,7 +201,7 @@ def read_exr_header(exrpath, maxreadsize=2000):
182201
'ySampling': y_sampling
183202
}
184203

185-
metadata[attribute_name] = channel_data
204+
metadata[attribute_name].update(channel_data)
186205

187206
elif attribute_type == b'chromaticities':
188207
chromaticities = struct.unpack('f' * 8, exr_file.read(4 * 8))

tests/test_exr.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,116 @@ def test_exr_meta_owner():
104104
'owner': 'Copyright 2006 Industrial Light & Magic',
105105
'screenWindowWidth': 1.0,
106106
'lineOrder': 'INCREASING_Y'
107+
}),
108+
# Beachball (multipart)
109+
pytest.param(os.path.join(EXR_IMAGES_DIR_PATH, 'Beachball', 'multipart.0001.exr'), {
110+
'compression': 'ZIPS_COMPRESSION',
111+
'pixelAspectRatio': 1.0,
112+
'displayWindow': {
113+
'xMax': 2047,
114+
'xMin': 0,
115+
'yMax': 1555,
116+
'yMin': 0
117+
},
118+
'dataWindow': {
119+
'xMax': 1530,
120+
'xMin': 654,
121+
'yMax': 1120,
122+
'yMin': 245
123+
},
124+
'channels': {
125+
'A': {
126+
'pLinear': 0,
127+
'pixel_type': 1,
128+
'reserved': [0, 0, 0],
129+
'xSampling': 1,
130+
'ySampling': 1
131+
},
132+
'B': {
133+
'pLinear': 0,
134+
'pixel_type': 1,
135+
'reserved': [0, 0, 0],
136+
'xSampling': 1,
137+
'ySampling': 1
138+
},
139+
'G': {
140+
'pLinear': 0,
141+
'pixel_type': 1,
142+
'reserved': [0, 0, 0],
143+
'xSampling': 1,
144+
'ySampling': 1
145+
},
146+
'R': {
147+
'pLinear': 0,
148+
'pixel_type': 1,
149+
'reserved': [0, 0, 0],
150+
'xSampling': 1,
151+
'ySampling': 1
152+
},
153+
'Z': {
154+
'pLinear': 0,
155+
'pixel_type': 1,
156+
'reserved': [0, 0, 0],
157+
'xSampling': 1,
158+
'ySampling': 1
159+
},
160+
'disparityL.x': {
161+
'pLinear': 0,
162+
'pixel_type': 1,
163+
'reserved': [0, 0, 0],
164+
'xSampling': 1,
165+
'ySampling': 1
166+
},
167+
'disparityL.y': {
168+
'pLinear': 0,
169+
'pixel_type': 1,
170+
'reserved': [0, 0, 0],
171+
'xSampling': 1,
172+
'ySampling': 1
173+
},
174+
'disparityR.x': {
175+
'pLinear': 0,
176+
'pixel_type': 1,
177+
'reserved': [0, 0, 0],
178+
'xSampling': 1,
179+
'ySampling': 1
180+
},
181+
'disparityR.y': {
182+
'pLinear': 0,
183+
'pixel_type': 1,
184+
'reserved': [0, 0, 0],
185+
'xSampling': 1,
186+
'ySampling': 1
187+
},
188+
'forward.u': {
189+
'pLinear': 0,
190+
'pixel_type': 1,
191+
'reserved': [0, 0, 0],
192+
'xSampling': 1,
193+
'ySampling': 1
194+
},
195+
'forward.v': {
196+
'pLinear': 0,
197+
'pixel_type': 1,
198+
'reserved': [0, 0, 0],
199+
'xSampling': 1,
200+
'ySampling': 1
201+
},
202+
'whitebarmask.mask': {
203+
'pLinear': 0,
204+
'pixel_type': 1,
205+
'reserved': [0, 0, 0],
206+
'xSampling': 1,
207+
'ySampling': 1
208+
}
209+
},
210+
'screenWindowCenter': [0.0, 0.0],
211+
'screenWindowWidth': 1.0,
212+
'lineOrder': 'INCREASING_Y',
213+
'chunkCount': 876,
214+
'name': 'rgba_right',
215+
'type': 'scanlineimage',
216+
'view': 'right',
107217
})
108218
])
109219
def test_exr_meta_all(input_path, expected_metadata):

0 commit comments

Comments
 (0)