def check_subtitle_cpl_max_elements(self, playlist, asset, folder): """ Maximum number of subtitle Text or Image elements. A subtitle instance shall contain no more than six (6) Text elements or three (3) Image elements. Reference : SMPTE 429-2-2013 8.4.4 """ st_dict = self.st_util.get_subtitle_xml(asset, folder) if not st_dict: return subtitles = keys_by_name_dict(st_dict, 'Subtitle') if not subtitles: return for idx, st in enumerate(subtitles[0]): text_count = len(keys_by_name_dict(st, 'Text')) if text_count > 6: self.error( "Too many Text elements ({}) for subtitle {}".format( text_count, st['Subtitle@SpotNumber'])) img_count = len(keys_by_name_dict(st, 'Image')) if img_count > 6: self.error( "Too many Image elements ({}) for subtitle {}".format( img_count, st['Subtitle@SpotNumber']))
def check_subtitle_cpl_loadfont(self, playlist, asset, folder): """ Text subtitle must contains one and only one LoadFont element. As specified in SMPTE 429-2 8.4.1, only exception is PNG based subtitles. SMPTE 428-7-2014 5.11.1 also specify that the LoadFont ID attribute shall be a string of one or more character. This is not enforced at the XSD schema level so we explicitly check it here. On the other hand, Interop spec doesn't have strict requirement on the presence of LoadFont tag. References: TI Subtitle Specification for DLP Cinema (v1.1) 2.7 https://web.archive.org/web/20140924175755/http://dlp.com/downloads/pdf_dlp_cinema_CineCanvas_Rev_C.pdf SMPTE ST 428-7:2014 5.11.1 SMPTE ST 429-2:2013 8.4.1 """ st_dict = self.st_util.get_subtitle_xml(asset, folder) if not st_dict: return if self.dcp.schema == 'Interop': return loadfont_attribute = "LoadFont@ID" text_elems = keys_by_name_dict(st_dict, 'Text') loadfont_elems = keys_by_name_dict(st_dict, loadfont_attribute) if text_elems and len(loadfont_elems) != 1: self.error("Text based subtitle shall contain one and only one " "LoadFont element, found {}".format( len(loadfont_elems))) if text_elems and not loadfont_elems[0]: self.error("LoadFont element with an empty ID attribute")
def check_subtitle_cpl_loadfont(self, playlist, asset, folder): """ Text subtitle must contains one and only one LoadFont element. As specified in SMPTE 429-2 8.4.1, only exception is PNG based subtitles. SMPTE 428-7-2014 5.11.1 also specify that the LoadFont ID attribute shall be a string of one or more character. This is not enforced at the XSD schema level so we explicitly check it here. Reference : SMPTE ST 428-7-2014 5.11.1 SMPTE ST 429-2-2013 8.4.1 """ st_dict = self.st_util.get_subtitle_xml(asset, folder) if not st_dict: return if self.dcp.schema == 'SMPTE': loadfont_attribute = "LoadFont@ID" else: loadfont_attribute = "LoadFont@Id" # Interop text_elems = keys_by_name_dict(st_dict, 'Text') loadfont_elems = keys_by_name_dict(st_dict, loadfont_attribute) if text_elems and len(loadfont_elems) != 1: raise CheckException( "Text based subtitle shall contain one and only one " "LoadFont element, found {}".format(len(loadfont_elems))) if text_elems and not loadfont_elems[0]: raise CheckException("LoadFont element with an empty ID attribute")
def check_subtitle_cpl_content(self, playlist, asset, folder): """ Subtitle individual structure check. """ st_dict = self.get_subtitle_xml(asset, folder) if not st_dict: return subtitles = keys_by_name_dict(st_dict, 'Subtitle') for st in subtitles[0]: has_image = keys_by_name_dict(st, 'Image') has_text = keys_by_name_dict(st, 'Text') if not has_image and not has_text: raise CheckException( "Subtitle {} element must define one Text or Image" "".format(st['Subtitle@SpotNumber']))
def check_subtitle_cpl_position(self, playlist, asset, folder): """ Subtitles vertical position (out of screen) check. VAlign="top", VPosition="0" : out of the top of the screen VAlign="bottom", VPosition="0" : some char like 'g' will be cut References: TI Subtitle Specification for DLP Cinema (v1.1) 2.10 https://web.archive.org/web/20140924175755/http://dlp.com/downloads/pdf_dlp_cinema_CineCanvas_Rev_C.pdf SMPTE ST 428-7:2014 6.2.4 """ st_dict = self.st_util.get_subtitle_xml(asset, folder) if not st_dict: return subs = keys_by_name_dict(st_dict, 'Subtitle') flat_subs = [item for sublist in subs for item in sublist] for st in flat_subs: st_idx = st['Subtitle@SpotNumber'] valign = keys_by_pattern_dict(st, ['@VAlign']) vpos = keys_by_pattern_dict(st, ['@VPosition']) for a, p in zip(valign, vpos): if a == 'top' and p == 0: self.error( "Subtitle {} is out of screen (top)".format(st_idx)) if a == 'bottom' and p == 0: self.error( "Subtitle {} is nearly out of screen (bottom), some " "characters will be cut".format(st_idx))
def check_subtitle_cpl_st_timing(self, playlist, asset, folder): """ Subtitle individual duration / fade time check. """ st_dict = self.get_subtitle_xml(asset, folder) if not st_dict: return subtitles = keys_by_name_dict(st_dict, 'Subtitle') editrate = self.get_subtitle_editrate(asset, st_dict) for st in subtitles[0]: st_idx = st['Subtitle@SpotNumber'] st_in, st_out = st['Subtitle@TimeIn'], st['Subtitle@TimeOut'] dur = (self.st_tc_frames(st_out, editrate) - self.st_tc_frames(st_in, editrate)) if dur <= 0: raise CheckException( "Subtitle {} null or negative duration".format(st_idx)) f_s, f_d = self.get_subtitle_fade_io(st, editrate) if f_s and f_s > dur: raise CheckException( "Subtitle {} FadeUpTime longer than duration".format( st_idx)) if f_d and f_d > dur: raise CheckException( "Subtitle {} FadeDownTime longer than duration".format( st_idx))
def check_subtitle_cpl_duration(self, playlist, asset, folder): """ Subtitle duration coherence with CPL. """ st_dict = self.get_subtitle_xml(asset, folder) if not st_dict: return st_rate = self.get_subtitle_editrate(asset, st_dict) subtitles = keys_by_name_dict(st_dict, 'Subtitle') _, asset = asset last_tc = 0 for st in subtitles[0]: st_out = self.st_tc_frames(st['Subtitle@TimeOut'], st_rate) if st_out > last_tc: last_tc = st_out cpl_rate = asset['EditRate'] cpl_dur = asset['Duration'] ratio_editrate = st_rate / cpl_rate last_tc_st = last_tc / ratio_editrate if last_tc_st > cpl_dur: reel_cpl = get_reel_for_asset(playlist, asset['Id'])['Position'] raise CheckException( "Subtitle exceed track duration. Subtitle {} - Track {} " "- Reel {}".format(frame_to_tc(last_tc_st, cpl_rate), frame_to_tc(cpl_dur, cpl_rate), reel_cpl))
def check_subtitle_cpl_position(self, playlist, asset, folder): """ Subtitles vertical position (out of screen) check. VAlign="top", VPosition="0" : out of the top of the screen VAlign="bottom", VPosition="0" : some char like 'g' will be cut """ st_dict = self.get_subtitle_xml(asset, folder) if not st_dict: return subs = keys_by_name_dict(st_dict, 'Subtitle') flat_subs = [item for sublist in subs for item in sublist] for st in flat_subs: st_idx = st['Subtitle@SpotNumber'] valign = keys_by_pattern_dict(st, ['@VAlign']) vpos = keys_by_pattern_dict(st, ['@VPosition']) for a, p in zip(valign, vpos): if a == 'top' and p == 0: raise CheckException( "Subtitle {} is out of screen (top)".format(st_idx)) if a == 'bottom' and p == 0: raise CheckException( "Subtitle {} is nearly out of screen (bottom), some " "characters will be cut".format(st_idx))
def check_subtitle_cpl_first_tt_event(self, playlist, asset, folder): """ First Timed Text Event of Composition TimeIn greater than 4s. The composition's first Timed Text event's TimeIn attribute should be greater than or equal to 4 seconds. Reference: SMPTE RDD 52:2020 7.2.4 """ reel_cpl = get_reel_for_asset(playlist, asset[1]['Id'])['Position'] first_reel_of_st = get_first_reel_for_asset_type(playlist, 'Subtitle') # We are probably checking a Caption track if not first_reel_of_st: return None first_reel_of_st = first_reel_of_st['Position'] if not first_reel_of_st or reel_cpl != first_reel_of_st: return st_dict = self.st_util.get_subtitle_xml(asset, folder) if not st_dict: return subtitles = keys_by_name_dict(st_dict, 'Subtitle') if not subtitles: return st_editrate = self.st_util.get_subtitle_editrate(asset, st_dict) first_tc = subtitles[0][0]['Subtitle@TimeIn'] first_tc_frames = self.st_util.st_tc_frames(first_tc, st_editrate) if (first_tc_frames < 4 * st_editrate): self.error("First Timed Text event of CPL happens " "earlier than 4 seconds: {}".format(first_tc))
def check_subtitle_cpl_loadfont(self, playlist, asset, folder): """ Text subtitle must contains one and only one LoadFont element. As specified in SMPTE 429-2 8.4.1, only exception is PNG based subtitles. """ st_dict = self.st_util.get_subtitle_xml(asset, folder) if not st_dict: return if self.dcp.schema == 'SMPTE': text_elems = keys_by_name_dict(st_dict, 'Text') loadfont_elems = keys_by_name_dict(st_dict, 'LoadFont@ID') if text_elems and len(loadfont_elems) != 1: raise CheckException( "Text based subtitle shall contain one and only one " "LoadFont element, found {}".format(len(loadfont_elems)))
def check_subtitle_cpl_font_ref(self, playlist, asset, folder): """ Subtitle font references check. """ st_dict = self.get_subtitle_xml(asset, folder) if not st_dict: return if self.dcp.schema == 'SMPTE': font_id = self.get_subtitle_elem(st_dict, 'LoadFont@ID') font_ref = keys_by_name_dict(st_dict, 'Font@ID') else: font_id = self.get_subtitle_elem(st_dict, 'LoadFont@Id') font_ref = keys_by_name_dict(st_dict, 'Font@Id') for ref in font_ref: if ref != font_id: raise CheckException( "Subtitle reference unknown font {} (loaded {})".format( ref, font_id))
def check_subtitle_cpl_empty(self, playlist, asset, folder): """ Empty Subtitle file check. """ st_dict = self.st_util.get_subtitle_xml(asset, folder) if not st_dict: return subtitles = keys_by_name_dict(st_dict, 'Subtitle') if not subtitles: raise CheckException("Subtitle file is empty")
def check_subtitle_cpl_empty(self, playlist, asset, folder): """ Empty Subtitle file check. References: N/A """ st_dict = self.st_util.get_subtitle_xml(asset, folder) if not st_dict: return subtitles = keys_by_name_dict(st_dict, 'Subtitle') if not subtitles: self.error("Subtitle file is empty")
def check_subtitle_cpl_content(self, playlist, asset, folder): """ Subtitle individual structure check. Reference : Interop TI Subtitle Spec 1.1 2.9 SMPTE 428-7-2014 6 """ st_dict = self.st_util.get_subtitle_xml(asset, folder) if not st_dict: return subtitles = keys_by_name_dict(st_dict, 'Subtitle') if not subtitles: return for st in subtitles[0]: has_image = keys_by_name_dict(st, 'Image') has_text = keys_by_name_dict(st, 'Text') if not has_image and not has_text: raise CheckException( "Subtitle {} element must define one Text or Image" "".format(st['Subtitle@SpotNumber']))
def check_subtitle_cpl_content(self, playlist, asset, folder): """ Subtitle individual structure check. References: TI Subtitle Specification for DLP Cinema (v1.1) 2.9 https://web.archive.org/web/20140924175755/http://dlp.com/downloads/pdf_dlp_cinema_CineCanvas_Rev_C.pdf SMPTE ST 428-7:2014 6 """ st_dict = self.st_util.get_subtitle_xml(asset, folder) if not st_dict: return subtitles = keys_by_name_dict(st_dict, 'Subtitle') if not subtitles: return for st in subtitles[0]: has_image = keys_by_name_dict(st, 'Image') has_text = keys_by_name_dict(st, 'Text') if not has_image and not has_text: self.error( "Subtitle {} element must define one Text or Image".format( st['Subtitle@SpotNumber']))
def check_subtitle_cpl_image(self, playlist, asset, folder): """ Subtitle image element must reference a valid PNG file. """ st_dict = self.get_subtitle_xml(asset, folder) if not st_dict: return # TODO : Implement the test for SMPTE if self.dcp.schema != 'Interop': return imgs = keys_by_name_dict(st_dict, 'Image') for img in imgs: if not os.path.exists(os.path.join(folder, img)): raise CheckException( "Subtitle image reference {} not found in folder {}" "".format(img, os.path.relpath(folder, self.dcp.path)))
def check_subtitle_cpl_font_ref(self, playlist, asset, folder): """ Subtitle font references check. References: TI Subtitle Specification for DLP Cinema (v1.1) 2.7 https://web.archive.org/web/20140924175755/http://dlp.com/downloads/pdf_dlp_cinema_CineCanvas_Rev_C.pdf SMPTE ST 428-7:2014 5.11.1 """ st_dict = self.st_util.get_subtitle_xml(asset, folder) if not st_dict: return if self.dcp.schema == 'SMPTE': font_id = self.st_util.get_subtitle_elem(st_dict, 'LoadFont@ID') font_ref = keys_by_name_dict(st_dict, 'Font@ID') else: font_id = self.st_util.get_subtitle_elem(st_dict, 'LoadFont@Id') font_ref = keys_by_name_dict(st_dict, 'Font@Id') for ref in font_ref: if ref != font_id: self.error( "Subtitle reference unknown font {} (loaded {})".format( ref, font_id))
def check_subtitle_cpl_concurrent_visibility(self, playlist, asset, folder): """ Maximum number of subtitle visible on screen at once. Up to two (2) subtitle instances may be visible on screen at any time. The visibility period of an instance shall include fade-in and fade-out times. Reference : SMPTE 429-2-2013 8.4.4 """ st_dict = self.st_util.get_subtitle_xml(asset, folder) if not st_dict: return subtitles = keys_by_name_dict(st_dict, 'Subtitle') editrate = self.st_util.get_subtitle_editrate(asset, st_dict) if not subtitles: return st_list = [] for idx, st in enumerate(subtitles[0]): st_in, st_out = st['Subtitle@TimeIn'], st['Subtitle@TimeOut'] st_list.append((idx, self.st_util.st_tc_frames(st_in, editrate), 0)) st_list.append((idx, self.st_util.st_tc_frames(st_out, editrate), 1)) st_list = sorted(st_list, key=lambda x: (x[1], x[0], x[2])) vis_list = [] current_vis = 0 for idx, st in enumerate(st_list): current_vis += 1 if st[2] == 0 else -1 vis_list.append(current_vis) for idx, v in enumerate(vis_list): if v > 2: st = subtitles[0][st_list[idx][0]] st_in, st_out = st['Subtitle@TimeIn'], st['Subtitle@TimeOut'] self.error( "Too many subtitles ({}) visible at once between {} and {}" .format(v, st_in, st_out))
def check_subtitle_cpl_image(self, playlist, asset, folder): """ Subtitle image element must reference a valid PNG file. References: TI Subtitle Specification for DLP Cinema (v1.1) 2.17 https://web.archive.org/web/20140924175755/http://dlp.com/downloads/pdf_dlp_cinema_CineCanvas_Rev_C.pdf """ st_dict = self.st_util.get_subtitle_xml(asset, folder) if not st_dict: return # TODO : Implement the test for SMPTE if self.dcp.schema != 'Interop': return imgs = keys_by_name_dict(st_dict, 'Image') for img in imgs: if not os.path.exists(os.path.join(folder, img)): self.error("Subtitle image reference {} not found in folder {}" "".format(img, os.path.relpath(folder, self.dcp.path)))
def check_subtitle_cpl_font_glyph(self, playlist, asset, folder): """ Check for missing font glyphs. Reference : N/A """ st_dict = self.st_util.get_subtitle_xml(asset, folder) if not st_dict: return subtitles = keys_by_name_dict(st_dict, 'Subtitle') if not subtitles: return # See SMPTE ST 428-7-2014 sections 6.3 and 6.4 for possible # Subtitle Text and Font hierarchy. Note that here we just # recursively iterate to extract all relevant childs whitout # checking if the specific hierarchy is valid or not. all_text = self.st_util.extract_subtitle_text(subtitles[0]) unique_chars = set() for text in all_text: for char in text: unique_chars.add(char) path, uri = self.st_util.get_font_path(st_dict, folder) if not path: return if not os.path.exists(path): return face = freetype.Face(path) font_chars = [six.unichr(c) for c, n in face.get_chars()] missing_glyphs = [] for char in unique_chars: if char not in font_chars: missing_glyphs.append(char) if missing_glyphs: raise CheckException( "Font ({}) is missing required glyphs : {}".format( os.path.basename(path), ", ".join(missing_glyphs)))