27
27
Depending on if/where PyAAF is installed, you may need to set this env var:
28
28
OTIO_AAF_PYTHON_LIB - should point at the PyAAF module.
29
29
"""
30
-
30
+ import colorsys
31
+ import copy
32
+ import numbers
31
33
import os
32
34
import sys
33
- import numbers
34
- import copy
35
+
36
+ try :
37
+ # Python 2
38
+ text_type = unicode
39
+ except NameError :
40
+ # Python 3
41
+ text_type = str
42
+
35
43
try :
36
44
# Python 3.3+
37
45
import collections .abc as collections_abc
@@ -103,9 +111,10 @@ def _get_class_name(item):
103
111
def _transcribe_property (prop ):
104
112
# XXX: The unicode type doesn't exist in Python 3 (all strings are unicode)
105
113
# so we have to use type(u"") which works in both Python 2 and 3.
106
- if isinstance (prop , (str , type (u"" ), numbers .Integral , float )):
114
+ if isinstance (prop , (str , type (u"" ), numbers .Integral , float , dict )):
107
115
return prop
108
-
116
+ elif isinstance (prop , set ):
117
+ return list (prop )
109
118
elif isinstance (prop , list ):
110
119
result = {}
111
120
for child in prop :
@@ -115,8 +124,7 @@ def _transcribe_property(prop):
115
124
# @TODO: There may be more properties that we might want also.
116
125
# If you want to see what is being skipped, turn on debug.
117
126
if debug :
118
- debug_message = \
119
- "Skipping unrecognized property: {} of parent {}"
127
+ debug_message = "Skipping unrecognized property: {} of parent {}"
120
128
print (debug_message .format (child , prop ))
121
129
return result
122
130
elif hasattr (prop , "properties" ):
@@ -128,6 +136,110 @@ def _transcribe_property(prop):
128
136
return str (prop )
129
137
130
138
139
+ def _otio_color_from_hue (hue ):
140
+ """Return an OTIO marker color, based on hue in range of [0.0, 1.0].
141
+
142
+ Args:
143
+ hue (float): marker color hue value
144
+
145
+ Returns:
146
+ otio.schema.MarkerColor: converted / estimated marker color
147
+
148
+ """
149
+ if hue <= 0.04 or hue > 0.93 :
150
+ return otio .schema .MarkerColor .RED
151
+ if hue <= 0.13 :
152
+ return otio .schema .MarkerColor .ORANGE
153
+ if hue <= 0.2 :
154
+ return otio .schema .MarkerColor .YELLOW
155
+ if hue <= 0.43 :
156
+ return otio .schema .MarkerColor .GREEN
157
+ if hue <= 0.52 :
158
+ return otio .schema .MarkerColor .CYAN
159
+ if hue <= 0.74 :
160
+ return otio .schema .MarkerColor .BLUE
161
+ if hue <= 0.82 :
162
+ return otio .schema .MarkerColor .PURPLE
163
+ return otio .schema .MarkerColor .MAGENTA
164
+
165
+
166
+ def _marker_color_from_string (color ):
167
+ """Tries to derive a valid marker color from a string.
168
+
169
+ Args:
170
+ color (str): color name (e.g. "Yellow")
171
+
172
+ Returns:
173
+ otio.schema.MarkerColor: matching color or `None`
174
+ """
175
+ if not color :
176
+ return
177
+
178
+ return getattr (otio .schema .MarkerColor , color .upper (), None )
179
+
180
+
181
+ def _convert_rgb_to_marker_color (rgb_dict ):
182
+ """Returns a matching OTIO marker color for a given AAF color string.
183
+
184
+ Adapted from `get_nearest_otio_color()` in the `xges.py` adapter.
185
+
186
+ Args:
187
+ rgb_dict (dict): marker color as dict,
188
+ e.g. `"{'red': 41471, 'green': 12134, 'blue': 6564}"`
189
+
190
+ Returns:
191
+ otio.schema.MarkerColor: converted / estimated marker color
192
+
193
+ """
194
+
195
+ float_colors = {
196
+ (1.0 , 0.0 , 0.0 ): otio .schema .MarkerColor .RED ,
197
+ (0.0 , 1.0 , 0.0 ): otio .schema .MarkerColor .GREEN ,
198
+ (0.0 , 0.0 , 1.0 ): otio .schema .MarkerColor .BLUE ,
199
+ (0.0 , 0.0 , 0.0 ): otio .schema .MarkerColor .BLACK ,
200
+ (1.0 , 1.0 , 1.0 ): otio .schema .MarkerColor .WHITE ,
201
+ }
202
+
203
+ # convert from UInt to float
204
+ red = float (rgb_dict ["red" ]) / 65535.0
205
+ green = float (rgb_dict ["green" ]) / 65535.0
206
+ blue = float (rgb_dict ["blue" ]) / 65535.0
207
+ rgb_float = (red , green , blue )
208
+
209
+ # check for exact match
210
+ marker_color = float_colors .get (rgb_float )
211
+ if marker_color :
212
+ return marker_color
213
+
214
+ # try to get an approxiate match based on hue
215
+ hue , lightness , saturation = colorsys .rgb_to_hls (red , green , blue )
216
+ nearest = None
217
+ if saturation < 0.2 :
218
+ if lightness > 0.65 :
219
+ nearest = otio .schema .MarkerColor .WHITE
220
+ else :
221
+ nearest = otio .schema .MarkerColor .BLACK
222
+ if nearest is None :
223
+ if lightness < 0.13 :
224
+ nearest = otio .schema .MarkerColor .BLACK
225
+ if lightness > 0.9 :
226
+ nearest = otio .schema .MarkerColor .WHITE
227
+ if nearest is None :
228
+ nearest = _otio_color_from_hue (hue )
229
+ if nearest == otio .schema .MarkerColor .RED and lightness > 0.53 :
230
+ nearest = otio .schema .MarkerColor .PINK
231
+ if (
232
+ nearest == otio .schema .MarkerColor .MAGENTA
233
+ and hue < 0.89
234
+ and lightness < 0.42
235
+ ):
236
+ # some darker magentas look more like purple
237
+ nearest = otio .schema .MarkerColor .PURPLE
238
+
239
+ # default to red color
240
+ return nearest or otio .schema .MarkerColor .RED
241
+
242
+
131
243
def _find_timecode_mobs (item ):
132
244
mobs = [item .mob ]
133
245
@@ -474,6 +586,17 @@ def _transcribe(item, parents, edit_rate, indent=0):
474
586
_transcribe_log (msg , indent )
475
587
result = otio .schema .Track ()
476
588
589
+ # if parent is a sequence add SlotID / PhysicalTrackNumber to attach markers
590
+ parent = parents [- 1 ]
591
+ if isinstance (parent , (aaf2 .components .Sequence , aaf2 .components .NestedScope )):
592
+ timeline_slots = [
593
+ p for p in parents if isinstance (p , aaf2 .mobslots .TimelineMobSlot )
594
+ ]
595
+ timeline_slot = timeline_slots [- 1 ]
596
+ if timeline_slot :
597
+ metadata ["PhysicalTrackNumber" ] = list (parent .slots ).index (item ) + 1
598
+ metadata ["SlotID" ] = int (timeline_slot ["SlotID" ].value )
599
+
477
600
for component in item .components :
478
601
child = _transcribe (component , parents + [item ], edit_rate , indent + 2 )
479
602
_add_child (result , child , component )
@@ -524,11 +647,51 @@ def _transcribe(item, parents, edit_rate, indent=0):
524
647
)
525
648
526
649
elif isinstance (item , aaf2 .components .DescriptiveMarker ):
650
+ event_mobs = [p for p in parents if isinstance (p , aaf2 .mobslots .EventMobSlot )]
651
+ if event_mobs :
652
+ _transcribe_log (
653
+ "Create marker for '{}'" .format (_encoded_name (item )), indent
654
+ )
527
655
528
- # Markers come in on their own separate Track.
529
- # TODO: We should consolidate them onto the same track(s) as the clips
530
- # result = otio.schema.Marker()
531
- pass
656
+ result = otio .schema .Marker ()
657
+ result .name = metadata ["Comment" ]
658
+
659
+ event_mob = event_mobs [- 1 ]
660
+
661
+ metadata ["AttachedSlotID" ] = int (metadata ["DescribedSlots" ][0 ])
662
+ metadata ["AttachedPhysicalTrackNumber" ] = int (
663
+ event_mob ["PhysicalTrackNumber" ].value
664
+ )
665
+
666
+ # determine marker color
667
+ color = _marker_color_from_string (
668
+ metadata .get ("CommentMarkerAttributeList" , {}).get ("_ATN_CRM_COLOR" )
669
+ )
670
+ if color is None :
671
+ color = _convert_rgb_to_marker_color (
672
+ metadata ["CommentMarkerColor" ]
673
+ )
674
+ result .color = color
675
+
676
+ position = metadata ["Position" ]
677
+
678
+ # Length can be None, but the property will always exist
679
+ # so get('Length', 1) wouldn't help.
680
+ length = metadata ["Length" ]
681
+ if length is None :
682
+ length = 1
683
+
684
+ result .marked_range = otio .opentime .TimeRange (
685
+ start_time = otio .opentime .from_frames (position , edit_rate ),
686
+ duration = otio .opentime .from_frames (length , edit_rate ),
687
+ )
688
+ else :
689
+ _transcribe_log (
690
+ "Cannot attach marker item '{}'. "
691
+ "Missing event mob in hierarchy." .format (
692
+ _encoded_name (item )
693
+ )
694
+ )
532
695
533
696
elif isinstance (item , aaf2 .components .Selector ):
534
697
msg = "Transcribe selector for {}" .format (_encoded_name (item ))
@@ -986,6 +1149,72 @@ def _fix_transitions(thing):
986
1149
_fix_transitions (child )
987
1150
988
1151
1152
+ def _attach_markers (collection ):
1153
+ """Search for markers on tracks and attach them to their corresponding item.
1154
+
1155
+ Marked ranges will also be transformed into the new parent space.
1156
+
1157
+ """
1158
+ # iterate all timeline objects
1159
+ for timeline in collection .each_child (descended_from_type = otio .schema .Timeline ):
1160
+ tracks_map = {}
1161
+
1162
+ # build track mapping
1163
+ for track in timeline .each_child (descended_from_type = otio .schema .Track ):
1164
+ metadata = track .metadata .get ("AAF" , {})
1165
+ slot_id = metadata .get ("SlotID" )
1166
+ track_number = metadata .get ("PhysicalTrackNumber" )
1167
+ if slot_id is None or track_number is None :
1168
+ continue
1169
+
1170
+ tracks_map [(int (slot_id ), int (track_number ))] = track
1171
+
1172
+ # iterate all tracks for their markers and attach them to the matching item
1173
+ for current_track in timeline .each_child (descended_from_type = otio .schema .Track ):
1174
+ for marker in list (current_track .markers ):
1175
+ metadata = marker .metadata .get ("AAF" , {})
1176
+ slot_id = metadata .get ("AttachedSlotID" )
1177
+ track_number = metadata .get ("AttachedPhysicalTrackNumber" )
1178
+ target_track = tracks_map .get ((slot_id , track_number ))
1179
+ if target_track is None :
1180
+ raise AAFAdapterError (
1181
+ "Marker '{}' cannot be attached to an item. SlotID: '{}', "
1182
+ "PhysicalTrackNumber: '{}'" .format (
1183
+ marker .name , slot_id , track_number
1184
+ )
1185
+ )
1186
+
1187
+ # remove marker from current parent track
1188
+ current_track .markers .remove (marker )
1189
+
1190
+ # determine new item to attach the marker to
1191
+ target_item = target_track .child_at_time (marker .marked_range .start_time )
1192
+ if target_item is None :
1193
+ target_item = target_track
1194
+
1195
+ # attach marker to target item
1196
+ target_item .markers .append (marker )
1197
+
1198
+ # transform marked range into new item range
1199
+ marked_start_local = current_track .transformed_time (
1200
+ marker .marked_range .start_time , target_item
1201
+ )
1202
+
1203
+ marker .marked_range = otio .opentime .TimeRange (
1204
+ start_time = marked_start_local , duration = marker .marked_range .duration
1205
+ )
1206
+
1207
+ _transcribe_log (
1208
+ "Marker: '{}' (time: {}), attached to item: '{}'" .format (
1209
+ marker .name ,
1210
+ marker .marked_range .start_time .value ,
1211
+ target_item .name ,
1212
+ )
1213
+ )
1214
+
1215
+ return collection
1216
+
1217
+
989
1218
def _simplify (thing ):
990
1219
# If the passed in is an empty dictionary or None, nothing to do.
991
1220
# Without this check it would still return thing, but this way we avoid
@@ -1167,8 +1396,20 @@ def _contains_something_valuable(thing):
1167
1396
return True
1168
1397
1169
1398
1170
- def read_from_file (filepath , simplify = True , transcribe_log = False ):
1399
+ def read_from_file (filepath , simplify = True , transcribe_log = False , attach_markers = True ):
1400
+ """Reads AAF content from `filepath` and outputs an OTIO timeline object.
1401
+
1402
+ Args:
1403
+ filepath (str): AAF filepath
1404
+ simplify (bool, optional): simplify timeline structure by stripping empty items
1405
+ transcribe_log (bool, optional): log activity as items are getting transcribed
1406
+ attach_markers (bool, optional): attaches markers to their appropriate items
1407
+ like clip, gap. etc on the track
1171
1408
1409
+ Returns:
1410
+ otio.schema.Timeline
1411
+
1412
+ """
1172
1413
# 'activate' transcribe logging if adapter argument is provided.
1173
1414
# Note that a global 'switch' is used in order to avoid
1174
1415
# passing another argument around in the _transcribe() method.
@@ -1190,8 +1431,11 @@ def read_from_file(filepath, simplify=True, transcribe_log=False):
1190
1431
# Transcribe just the top-level mobs
1191
1432
result = _transcribe (top , parents = list (), edit_rate = None )
1192
1433
1193
- # AAF is typically more deeply nested than OTIO.
1434
+ # Attach marker to the appropriate clip, gap etc.
1435
+ if attach_markers :
1436
+ result = _attach_markers (result )
1194
1437
1438
+ # AAF is typically more deeply nested than OTIO.
1195
1439
# Lets try to simplify the structure by collapsing or removing
1196
1440
# unnecessary stuff.
1197
1441
if simplify :
0 commit comments