Skip to content

Commit aada787

Browse files
apetrynetapetrynetdaniel
authored
Add ImageSequenceReference support to EDL adapter (#804)
* Implemented ImageSequenceReference support in the EDL adapter, including tests. * Updated feature matrix Co-authored-by: apetrynet <[email protected]> Co-authored-by: daniel <[email protected]>
1 parent 44147cb commit aada787

File tree

3 files changed

+189
-10
lines changed

3 files changed

+189
-10
lines changed

docs/tutorials/feature-matrix.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Adapters may or may not support all of the features of OpenTimelineIO or the for
2828
+-------------------------+------+-------+--------+--------+-------+--------+-------+----------+
2929
|Color Decision List ||||||| N/A ||
3030
+-------------------------+------+-------+--------+--------+-------+--------+-------+----------+
31-
|Image Sequence Reference || |||| W-O |||
31+
|Image Sequence Reference || |||| W-O |||
3232
+-------------------------+------+-------+--------+--------+-------+--------+-------+----------+
3333

3434
N/A: Not Applicable

src/py-opentimelineio/opentimelineio/adapters/cmx_3600.py

Lines changed: 80 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,10 @@ def parse_edl(self, edl_string, rate=24):
360360

361361

362362
class ClipHandler(object):
363+
# /path/filename.[1001-1020].ext
364+
image_sequence_pattern = re.compile(
365+
r'.*\.(?P<range>\[(?P<start>[0-9]+)-(?P<end>[0-9]+)\])\.\w+$'
366+
)
363367

364368
def __init__(self, line, comment_data, rate=24):
365369
self.clip_num = None
@@ -376,6 +380,33 @@ def __init__(self, line, comment_data, rate=24):
376380
self.parse(line)
377381
self.clip = self.make_clip(comment_data)
378382

383+
def is_image_sequence(self, comment_data):
384+
return self.image_sequence_pattern.search(
385+
comment_data['media_reference']
386+
) is not None
387+
388+
def create_imagesequence_reference(self, comment_data):
389+
regex_obj = self.image_sequence_pattern.search(
390+
comment_data['media_reference']
391+
)
392+
393+
path, basename = os.path.split(comment_data['media_reference'])
394+
prefix, suffix = basename.split(regex_obj.group('range'))
395+
ref = schema.ImageSequenceReference(
396+
target_url_base=path,
397+
name_prefix=prefix,
398+
name_suffix=suffix,
399+
rate=self.edl_rate,
400+
start_frame=int(regex_obj.group('start')),
401+
frame_zero_padding=len(regex_obj.group('start')),
402+
available_range=opentime.range_from_start_end_time(
403+
opentime.from_timecode(self.source_tc_in, self.edl_rate),
404+
opentime.from_timecode(self.source_tc_out, self.edl_rate)
405+
)
406+
)
407+
408+
return ref
409+
379410
def make_clip(self, comment_data):
380411
clip = schema.Clip()
381412
clip.name = str(self.clip_num)
@@ -392,10 +423,15 @@ def make_clip(self, comment_data):
392423
# TODO: Replace with enum, once one exists
393424
clip.media_reference.generator_kind = 'SMPTEBars'
394425
elif 'media_reference' in comment_data:
395-
clip.media_reference = schema.ExternalReference()
396-
clip.media_reference.target_url = comment_data[
397-
'media_reference'
398-
]
426+
if self.is_image_sequence(comment_data):
427+
clip.media_reference = self.create_imagesequence_reference(
428+
comment_data
429+
)
430+
else:
431+
clip.media_reference = schema.ExternalReference()
432+
clip.media_reference.target_url = comment_data[
433+
'media_reference'
434+
]
399435
else:
400436
clip.media_reference = schema.MissingReference()
401437

@@ -413,6 +449,15 @@ def make_clip(self, comment_data):
413449
os.path.basename(clip.media_reference.target_url)
414450
)[0]
415451

452+
elif (
453+
clip.media_reference and
454+
hasattr(clip.media_reference, 'target_url_base') and
455+
clip.media_reference.target_url_base is not None
456+
):
457+
clip.name = os.path.splitext(
458+
os.path.basename(_get_image_sequence_url(clip))
459+
)[0]
460+
416461
asc_sop = comment_data.get('asc_sop', None)
417462
asc_sat = comment_data.get('asc_sat', None)
418463
if asc_sop or asc_sat:
@@ -1295,6 +1340,9 @@ def _generate_comment_lines(
12951340
if hasattr(clip.media_reference, 'target_url'):
12961341
url = clip.media_reference.target_url
12971342

1343+
elif hasattr(clip.media_reference, 'abstract_target_url'):
1344+
url = _get_image_sequence_url(clip)
1345+
12981346
else:
12991347
url = clip.name
13001348

@@ -1390,6 +1438,22 @@ def _generate_comment_lines(
13901438
return lines
13911439

13921440

1441+
def _get_image_sequence_url(clip):
1442+
ref = clip.media_reference
1443+
start_frame, end_frame = ref.frame_range_for_time_range(
1444+
clip.trimmed_range()
1445+
)
1446+
1447+
frame_range_str = '[{start}-{end}]'.format(
1448+
start=start_frame,
1449+
end=end_frame
1450+
)
1451+
1452+
url = clip.media_reference.abstract_target_url(frame_range_str)
1453+
1454+
return url
1455+
1456+
13931457
def _flip_windows_slashes(path):
13941458
return re.sub(r'\\', '/', path)
13951459

@@ -1403,10 +1467,18 @@ def _reel_from_clip(clip, reelname_len):
14031467

14041468
_reel = clip.name or 'AX'
14051469

1406-
if isinstance(clip.media_reference, schema.ExternalReference):
1407-
_reel = clip.media_reference.name or os.path.basename(
1408-
clip.media_reference.target_url
1409-
)
1470+
valid_refs = (schema.ExternalReference, schema.ImageSequenceReference)
1471+
if isinstance(clip.media_reference, valid_refs):
1472+
if clip.media_reference.name:
1473+
_reel = clip.media_reference.name
1474+
1475+
elif hasattr(clip.media_reference, 'target_url'):
1476+
_reel = os.path.basename(
1477+
clip.media_reference.target_url
1478+
)
1479+
1480+
else:
1481+
_reel = _get_image_sequence_url(clip)
14101482

14111483
# Flip Windows slashes
14121484
_reel = os.path.basename(_flip_windows_slashes(_reel))

tests/test_cmx_3600_adapter.py

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ def test_reelname_length(self):
212212

213213
self.assertMultiLineEqual(result, expected)
214214

215-
# Keep full filename (minus extension) as reelname
215+
# Make sure reel name is only 12 characters long
216216
result = otio.adapters.write_to_string(
217217
tl,
218218
adapter_name="cmx_3600",
@@ -400,6 +400,113 @@ def test_clip_with_tab_and_space_delimiters(self):
400400
31
401401
)
402402

403+
def test_imagesequence_read(self):
404+
trunced_edl1 = '''TITLE: Image Sequence Write
405+
406+
001 myimages V C 01:00:01:00 01:00:02:12 00:00:00:00 00:00:01:12
407+
* FROM CLIP NAME: my_image_sequence
408+
* FROM CLIP: /media/path/my_image_sequence.[1025-1060].ext
409+
* OTIO TRUNCATED REEL NAME FROM: my_image_sequence.[1025-1060].ext
410+
'''
411+
rate = 24
412+
tl1 = otio.adapters.read_from_string(trunced_edl1, 'cmx_3600', rate=rate)
413+
self.assertIsInstance(tl1, otio.schema.Timeline)
414+
415+
clip1 = tl1.tracks[0][0]
416+
media_ref1 = clip1.media_reference
417+
self.assertIsInstance(media_ref1, otio.schema.ImageSequenceReference)
418+
self.assertEqual(media_ref1.start_frame, 1025)
419+
self.assertEqual(media_ref1.end_frame(), 1060)
420+
self.assertEqual(
421+
clip1.available_range(),
422+
otio.opentime.range_from_start_end_time(
423+
otio.opentime.from_timecode('01:00:01:00', rate),
424+
otio.opentime.from_timecode('01:00:02:12', rate)
425+
)
426+
)
427+
428+
# Make sure regex works and uses ExternalReference for non sequences
429+
trunced_edl2 = '''TITLE: Image Sequence Write
430+
431+
001 myimages V C 01:00:01:00 01:00:02:12 00:00:00:00 00:00:01:12
432+
* FROM CLIP NAME: my_image_sequence
433+
* FROM CLIP: /media/path/my_image_file.1025.ext
434+
* OTIO TRUNCATED REEL NAME FROM: my_image_file.1025.ext
435+
'''
436+
437+
tl2 = otio.adapters.read_from_string(trunced_edl2, 'cmx_3600', rate=rate)
438+
clip2 = tl2.tracks[0][0]
439+
media_ref2 = clip2.media_reference
440+
self.assertIsInstance(media_ref2, otio.schema.ExternalReference)
441+
442+
trunced_edl3 = '''TITLE: Image Sequence Write
443+
444+
001 myimages V C 01:00:01:00 01:00:02:12 00:00:00:00 00:00:01:12
445+
* FROM CLIP NAME: my_image_sequence
446+
* FROM CLIP: /media/path/my_image_file.[1025].ext
447+
* OTIO TRUNCATED REEL NAME FROM: my_image_file.[1025].ext
448+
'''
449+
tl3 = otio.adapters.read_from_string(trunced_edl3, 'cmx_3600', rate=rate)
450+
clip3 = tl3.tracks[0][0]
451+
media_ref3 = clip3.media_reference
452+
self.assertIsInstance(media_ref3, otio.schema.ExternalReference)
453+
454+
def test_imagesequence_write(self):
455+
rate = 24
456+
tl = otio.schema.Timeline('Image Sequence Write')
457+
track = otio.schema.Track('V1')
458+
tl.tracks.append(track)
459+
460+
clip = otio.schema.Clip(
461+
name='my_image_sequence',
462+
source_range=otio.opentime.range_from_start_end_time(
463+
otio.opentime.from_timecode('01:00:01:00', rate),
464+
otio.opentime.from_timecode('01:00:02:12', rate)
465+
),
466+
media_reference=otio.schema.ImageSequenceReference(
467+
target_url_base='/media/path/',
468+
name_prefix='my_image_sequence.',
469+
name_suffix='.ext',
470+
rate=rate,
471+
start_frame=1001,
472+
frame_zero_padding=4,
473+
available_range=otio.opentime.range_from_start_end_time(
474+
otio.opentime.from_timecode('01:00:00:00', rate),
475+
otio.opentime.from_timecode('01:00:03:00', rate)
476+
)
477+
)
478+
)
479+
track.append(clip)
480+
481+
# Default behavior
482+
result1 = otio.adapters.write_to_string(tl, 'cmx_3600', rate=rate)
483+
484+
expected_result1 = '''TITLE: Image Sequence Write
485+
486+
001 myimages V C 01:00:01:00 01:00:02:12 00:00:00:00 00:00:01:12
487+
* FROM CLIP NAME: my_image_sequence
488+
* FROM CLIP: /media/path/my_image_sequence.[1025-1060].ext
489+
* OTIO TRUNCATED REEL NAME FROM: my_image_sequence.[1025-1060].ext
490+
'''
491+
self.assertMultiLineEqual(result1, expected_result1)
492+
493+
# Only trunc extension in reel name
494+
result2 = otio.adapters.write_to_string(
495+
tl,
496+
'cmx_3600',
497+
rate=24,
498+
reelname_len=None
499+
)
500+
501+
expected_result2 = '''TITLE: Image Sequence Write
502+
503+
001 my_image_sequence.[1025-1060] V C \
504+
01:00:01:00 01:00:02:12 00:00:00:00 00:00:01:12
505+
* FROM CLIP NAME: my_image_sequence
506+
* FROM CLIP: /media/path/my_image_sequence.[1025-1060].ext
507+
'''
508+
self.assertMultiLineEqual(result2, expected_result2)
509+
403510
def test_dissolve_parse(self):
404511
tl = otio.adapters.read_from_file(DISSOLVE_TEST)
405512
self.assertEqual(len(tl.tracks[0]), 3)

0 commit comments

Comments
 (0)