diff --git a/contrib/opentimelineio_contrib/adapters/contrib_adapters.plugin_manifest.json b/contrib/opentimelineio_contrib/adapters/contrib_adapters.plugin_manifest.json index cee2946293..7a9478d475 100644 --- a/contrib/opentimelineio_contrib/adapters/contrib_adapters.plugin_manifest.json +++ b/contrib/opentimelineio_contrib/adapters/contrib_adapters.plugin_manifest.json @@ -63,6 +63,13 @@ "execution_scope": "in process", "filepath": "kdenlive.py", "suffixes": ["kdenlive"] + }, + { + "OTIO_SCHEMA": "Adapter.1", + "name": "srt", + "execution_scope": "in process", + "filepath": "srt.py", + "suffixes": ["srt"] } ], "schemadefs" : [ diff --git a/contrib/opentimelineio_contrib/adapters/srt.py b/contrib/opentimelineio_contrib/adapters/srt.py new file mode 100644 index 0000000000..c77a92e56c --- /dev/null +++ b/contrib/opentimelineio_contrib/adapters/srt.py @@ -0,0 +1,232 @@ +# +# Copyright Contributors to the OpenTimelineIO project +# +# Licensed under the Apache License, Version 2.0 (the "Apache License") +# with the following modification; you may not use this file except in +# compliance with the Apache License and the following modification to it: +# Section 6. Trademarks. is deleted and replaced with: +# +# 6. Trademarks. This License does not grant permission to use the trade +# names, trademarks, service marks, or product names of the Licensor +# and its affiliates, except as required to comply with Section 4(c) of +# the License and to reproduce the content of the NOTICE file. +# +# You may obtain a copy of the Apache License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the Apache License with the above modification is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the Apache License for the specific +# language governing permissions and limitations under the Apache License. +# + +"""SRT Adapter harness""" + +from itertools import groupby +from html.parser import HTMLParser +from queue import LifoQueue +import opentimelineio as otio +import io + + +class SRTStyleParser(HTMLParser): + class StyleState: + + def __init__(self): + self.italics = False + self.bold = False + self.underline = False + self.strikethrough = False + self.font_size = '10' + self.font_color = 'BLACK' + self.font_face = '' + + def clear_state(self): + self.italics = False + self.bold = False + self.underline = False + self.strikethrough = False + self.font_size = '' + self.font_color = 'BLACK' + self.font_face = '' + + def set_font_size(self, value): + self.font_size = value + + def set_font_face(self, value): + self.font_face = value + + def set_font_color(self, value): + self.font_color = value + + def __repr__(self): + return 'StyleState(' \ + 'italics=' + str(self.italics) + \ + ',bold=' + str(self.bold) + \ + ',underline=' + str(self.underline) + \ + ',strikethrough=' + str(self.strikethrough) + \ + ',fontSize=' + str(self.font_size) + \ + ',fontColor=' + str(self.font_color) + \ + ',fontFace=' + str(self.font_face) + \ + ')' + + def __init__(self): + super().__init__() + self.tagStack = LifoQueue() + self.styleState = self.StyleState() + self.dataState = "" + self.dataList = [] + self.TAG_FUNCTION_MAP = { + 'b': self._process_bold_tag, + 'u': self._process_underline_tag, + 'i': self._process_italics_tag, + 's': self._process_strikethrough_tag, + 'font': self._process_font_tag + } + + def _process_tag(self, tag_attrs_tuple): + if tag_attrs_tuple[0] in self.TAG_FUNCTION_MAP: + self.TAG_FUNCTION_MAP[tag_attrs_tuple[0]](tag_attrs_tuple[1]) + + def _process_bold_tag(self, tag_attrs): + self.styleState.bold = True + + def _process_underline_tag(self, tag_attrs): + self.styleState.underline = True + + def _process_italics_tag(self, tag_attrs): + self.styleState.italics = True + + def _process_strikethrough_tag(self, tag_attrs): + self.styleState.strikethrough = True + + def _process_font_tag(self, tag_attrs): + FONT_ATTR_MAP = { + 'color': self.styleState.set_font_color, + 'face': self.styleState.set_font_face, + 'size': self.styleState.set_font_size, + } + for attribute, value in tag_attrs: + if attribute in FONT_ATTR_MAP: + FONT_ATTR_MAP[attribute](value) + + def parse_srt(self, srt_string): + self.styleState.clear_state() + self.feed(srt_string) + return self.dataList + + def handle_starttag(self, tag, attrs): + attr_list = [] + for attr in attrs: + attr_list.append(attr) + self.tagStack.put((tag, attr_list)) + + def handle_endtag(self, tag): + topTag = self.tagStack.get() + if topTag[0] != tag: + raise Exception('Invalid HTML') + self._process_tag(topTag) + if self.tagStack.empty(): + self.dataList.append((self.dataState, self.styleState)) + self.styleState = self.StyleState() + self.dataState = '' + + def handle_data(self, data): + if self.tagStack.empty(): + self.dataList.append((data, None)) + else: + self.dataState = data + + def error(self, message): + pass + + +def timed_text_to_srt_block(timed_text): + in_time_str = otio.opentime.to_time_string(timed_text.in_time) + out_time_str = otio.opentime.to_time_string(timed_text.out_time) + in_time_str_decimal_index = in_time_str.rfind('.') + out_time_str_decimal_index = out_time_str.rfind('.') + in_time_ms = in_time_str[in_time_str_decimal_index + 1:] + out_time_ms = out_time_str[out_time_str_decimal_index + 1:] + in_time_str = in_time_str[0:in_time_str_decimal_index] + ',{:0<3}'.format( + in_time_ms[:3]) + out_time_str = out_time_str[0:out_time_str_decimal_index] + ',{:0<3}'.format( + out_time_ms[:3]) + text = '' + for caption in timed_text.texts: + text = text + caption + timed_text_str = in_time_str + ' --> ' + out_time_str + '\n' + text + return timed_text_str + + +def write_to_string(input_otio): + if not isinstance(input_otio, otio.schema.Subtitles): + raise ValueError('Object not of type Subtitles!') + + str_string = '' + block_count = 0 + + for timed_text in input_otio.timed_texts: + block_count += 1 + str_string = str_string + str(block_count) + '\n' + timed_text_to_srt_block( + timed_text) + '\n' + + return str_string.strip() + + +def read_from_file(filepath): + with io.open(filepath) as f: + subs = [list(g) for b, g in groupby(f, lambda x: bool(x.strip())) if b] + + timed_texts = [] + + style_id_count = 0 + styles_map = {} + style_parser = SRTStyleParser() + for sub in subs: + timestamps = sub[1].strip() + timestamps_data = timestamps.split() + start_time_str = timestamps_data[0].strip().replace(',', '.') + end_time_str = timestamps_data[2].strip().replace(',', '.') + start_time = otio.opentime.from_time_string(start_time_str, 24) + end_time = otio.opentime.from_time_string(end_time_str, 24) + xpos1 = None + ypos1 = None + xpos2 = None + ypos2 = None + if (len(timestamps_data) == 7): + xpos1 = float(timestamps_data[3][3:]) + xpos2 = float(timestamps_data[4][3:]) + ypos1 = float(timestamps_data[5][3:]) + ypos2 = float(timestamps_data[6][3:]) + subtitle_text = '' + for text in sub[2:]: + subtitle_text = subtitle_text + text + + if len(subtitle_text.strip()) == 0: + subtitle_text = '\n' + subtitle_data = style_parser.parse_srt(subtitle_text) + tt = otio.schema.TimedText(in_time=start_time, out_time=end_time) + for data in subtitle_data: + style_id = '' + if data[1] is not None: + style_id_count += 1 + style = otio.schema.TimedTextStyle(style_id='style' + str(style_id_count), + text_color=data[1].font_color, + text_size=float(data[1].font_size), + text_bold=data[1].bold, + text_italics=data[1].italics, + text_underline=data[1].underline, + font_family=data[1].font_face) + style_id = str(style_id_count) + styles_map[style_id] = style + tt.add_text(text=data[0], styleID=style_id) + timed_texts.append(tt) + subtitles = otio.schema.Subtitles(timed_texts=timed_texts) + track = otio.schema.Track(name="Subtitles Track") + timeline = otio.schema.Timeline("SRT Timeline", tracks=[track]) + timeline.metadata['styles'] = styles_map + timeline.tracks[0].append(subtitles) + return timeline diff --git a/contrib/opentimelineio_contrib/adapters/tests/sample_data/srt_example.otio b/contrib/opentimelineio_contrib/adapters/tests/sample_data/srt_example.otio new file mode 100644 index 0000000000..cdc3a54d96 --- /dev/null +++ b/contrib/opentimelineio_contrib/adapters/tests/sample_data/srt_example.otio @@ -0,0 +1,142 @@ +{ + "OTIO_SCHEMA": "Subtitles.1", + "metadata": {}, + "name": "", + "source_range": null, + "effects": [], + "markers": [], + "extent_x": 0.0, + "extent_y": 0.0, + "padding_x": 0.0, + "padding_y": 0.0, + "background_color": "", + "background_opacity": 0.0, + "timed_texts": [ + { + "OTIO_SCHEMA": "TimedText.1", + "metadata": {}, + "name": "", + "color": "GREEN", + "marked_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 76.72799999999984 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 805.5840000000001 + } + }, + "texts": [ + "Subtitle 1 Line 1\n" + ], + "style_ids": [ + "" + ] + }, + { + "OTIO_SCHEMA": "TimedText.1", + "metadata": {}, + "name": "", + "color": "GREEN", + "marked_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 74.25600000000009 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 883.2719999999999 + } + }, + "texts": [ + "Subtitle 2 Line 1\nSubtitle 2 Line 2\n" + ], + "style_ids": [ + "" + ] + }, + { + "OTIO_SCHEMA": "TimedText.1", + "metadata": {}, + "name": "", + "color": "GREEN", + "marked_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 51.048 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 958.5360000000001 + } + }, + "texts": [ + "Subtitle 3 Line 1\nSubtitle 3 Line 2\nSubtitle 3 Line 3\n" + ], + "style_ids": [ + "" + ] + }, + { + "OTIO_SCHEMA": "TimedText.1", + "metadata": {}, + "name": "", + "color": "GREEN", + "marked_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 50.2320000000002 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 1010.5919999999999 + } + }, + "texts": [ + "\n" + ], + "style_ids": [ + "" + ] + }, + { + "OTIO_SCHEMA": "TimedText.1", + "metadata": {}, + "name": "", + "color": "GREEN", + "marked_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 60.59999999999991 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 1061.856 + } + }, + "texts": [ + "Subtitle 5 Line 1" + ], + "style_ids": [ + "" + ] + } + ], + "display_alignment": "after" +} \ No newline at end of file diff --git a/contrib/opentimelineio_contrib/adapters/tests/sample_data/srt_example.srt b/contrib/opentimelineio_contrib/adapters/tests/sample_data/srt_example.srt new file mode 100644 index 0000000000..0113b7d86b --- /dev/null +++ b/contrib/opentimelineio_contrib/adapters/tests/sample_data/srt_example.srt @@ -0,0 +1,22 @@ +1 +00:00:33,566 --> 00:00:36,763 +Subtitle 1 Line 1 + +2 +00:00:36,803 --> 00:00:39,897 +Subtitle 2 Line 1 +Subtitle 2 Line 2 + +3 +00:00:39,939 --> 00:00:42,066 +Subtitle 3 Line 1 +Subtitle 3 Line 2 +Subtitle 3 Line 3 + +4 +00:00:42,108 --> 00:00:44,201 + + +5 +00:00:44,244 --> 00:00:46,769 +Subtitle 5 Line 1 \ No newline at end of file diff --git a/contrib/opentimelineio_contrib/adapters/tests/test_srt_adapter.py b/contrib/opentimelineio_contrib/adapters/tests/test_srt_adapter.py new file mode 100644 index 0000000000..9b3f6bbcde --- /dev/null +++ b/contrib/opentimelineio_contrib/adapters/tests/test_srt_adapter.py @@ -0,0 +1,84 @@ +# +# Copyright Contributors to the OpenTimelineIO project +# +# Licensed under the Apache License, Version 2.0 (the "Apache License") +# with the following modification; you may not use this file except in +# compliance with the Apache License and the following modification to it: +# Section 6. Trademarks. is deleted and replaced with: +# +# 6. Trademarks. This License does not grant permission to use the trade +# names, trademarks, service marks, or product names of the Licensor +# and its affiliates, except as required to comply with Section 4(c) of +# the License and to reproduce the content of the NOTICE file. +# +# You may obtain a copy of the Apache License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the Apache License with the above modification is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the Apache License for the specific +# language governing permissions and limitations under the Apache License. +# + +"""Unit tests for the srt file adapter""" + +import unittest +import os +import tempfile +import io + +import opentimelineio as otio + +OTIO_SAMPLE_DATA_DIR = os.path.join( + os.path.dirname(__file__), + "sample_data" +) +SRT_EXAMPLE_PATH = os.path.join(OTIO_SAMPLE_DATA_DIR, "srt_example.srt") +SRT_OTIO_EXAMPLE_PATH = os.path.join(OTIO_SAMPLE_DATA_DIR, + "srt_example.otio") + + +class SRTTest(unittest.TestCase): + maxDiff = None + + def setUp(self): + fd, self.tmp_path = tempfile.mkstemp(suffix=".srt", text=True) + os.close(fd) + + fd, self.tmp_path_otio = tempfile.mkstemp(suffix=".otio", text=True) + os.close(fd) + + def tearDown(self): + os.unlink(self.tmp_path) + + def test_srt_read(self): + st = otio.adapters.read_from_file(SRT_EXAMPLE_PATH) + otio.adapters.write_to_file(st, self.tmp_path_otio) + + with io.open(self.tmp_path_otio) as f: + test_data = f.read().strip() + + with io.open(SRT_OTIO_EXAMPLE_PATH) as f: + baseline_data = f.read().strip() + + self.maxDiff = None + self.assertMultiLineEqual(baseline_data, test_data) + + def test_otio(self): + st = otio.adapters.read_from_file(SRT_OTIO_EXAMPLE_PATH) + otio.adapters.write_to_file(st, self.tmp_path) + + with io.open(self.tmp_path) as f: + test_data = f.read() + + with io.open(SRT_EXAMPLE_PATH) as f: + baseline_data = f.read() + + self.maxDiff = None + self.assertMultiLineEqual(baseline_data, test_data) + + +if __name__ == '__main__': + unittest.main() diff --git a/docs/tutorials/otio-plugins.md b/docs/tutorials/otio-plugins.md index eb2dc917c1..ff86e20196 100644 --- a/docs/tutorials/otio-plugins.md +++ b/docs/tutorials/otio-plugins.md @@ -639,6 +639,26 @@ RvSession Adapter harness +### srt + +``` +SRT Adapter harness +``` + +*source*: `opentimelineio_contrib/adapters/srt.py` + + +*Supported Features (with arguments)*: + +- read_from_file: + - filepath +- write_to_string: + - input_otio + + + + + ### xges ``` diff --git a/docs/tutorials/otio-serialized-schema-only-fields.md b/docs/tutorials/otio-serialized-schema-only-fields.md index 6f36ae7e41..bb2ce3ced9 100644 --- a/docs/tutorials/otio-serialized-schema-only-fields.md +++ b/docs/tutorials/otio-serialized-schema-only-fields.md @@ -237,6 +237,23 @@ parameters: - *name* - *source_range* +### Subtitles.1 + +parameters: +- *background_color* +- *background_opacity* +- *display_alignment* +- *effects* +- *extent_x* +- *extent_y* +- *markers* +- *metadata* +- *name* +- *padding_x* +- *padding_y* +- *source_range* +- *timed_texts* + ### TimeEffect.1 parameters: @@ -244,6 +261,27 @@ parameters: - *metadata* - *name* +### TimedText.1 + +parameters: +- *color* +- *marked_range* +- *metadata* +- *name* +- *text* + +### TimedTextStyle.1 + +parameters: +- *font_family* +- *style_id* +- *text_alignment* +- *text_bold* +- *text_color* +- *text_italics* +- *text_size* +- *text_underline* + ### Timeline.1 parameters: diff --git a/docs/tutorials/otio-serialized-schema.md b/docs/tutorials/otio-serialized-schema.md index e7447ff089..4e4e129dc3 100644 --- a/docs/tutorials/otio-serialized-schema.md +++ b/docs/tutorials/otio-serialized-schema.md @@ -522,6 +522,31 @@ parameters: - *name*: - *source_range*: +### Subtitles.1 + +*full module path*: `opentimelineio.schema.Subtitles` + +*documentation*: + +``` +None +``` + +parameters: +- *background_color*: +- *background_opacity*: +- *display_alignment*: +- *effects*: +- *extent_x*: +- *extent_y*: +- *markers*: +- *metadata*: +- *name*: +- *padding_x*: +- *padding_y*: +- *source_range*: +- *timed_texts*: + ### TimeEffect.1 *full module path*: `opentimelineio.schema.TimeEffect` @@ -537,6 +562,43 @@ parameters: - *metadata*: - *name*: +### TimedText.1 + +*full module path*: `opentimelineio.schema.TimedText` + +*documentation*: + +``` +None +``` + +parameters: +- *color*: +- *marked_range*: +- *metadata*: +- *name*: +- *text*: + +### TimedTextStyle.1 + +*full module path*: `opentimelineio.schema.TimedTextStyle` + +*documentation*: + +``` +None +``` + +parameters: +- *font_family*: +- *style_id*: +- *text_alignment*: +- *text_bold*: +- *text_color*: +- *text_italics*: +- *text_size*: +- *text_underline*: + ### Timeline.1 *full module path*: `opentimelineio.schema.Timeline` diff --git a/src/opentimelineio/CMakeLists.txt b/src/opentimelineio/CMakeLists.txt index ed7b208341..1223fde675 100644 --- a/src/opentimelineio/CMakeLists.txt +++ b/src/opentimelineio/CMakeLists.txt @@ -29,6 +29,10 @@ set(OPENTIMELINEIO_HEADER_FILES serialization.h stack.h stackAlgorithm.h + stringUtils.h + subtitles.h + timedText.h + timedTextStyle.h timeEffect.h timeline.h track.h @@ -64,6 +68,9 @@ add_library(opentimelineio ${OTIO_SHARED_OR_STATIC_LIB} stack.cpp stackAlgorithm.cpp stringUtils.cpp + subtitles.cpp + timedText.cpp + timedTextStyle.cpp stringUtils.h # stringUtils.h is a private header timeEffect.cpp timeline.cpp diff --git a/src/opentimelineio/subtitles.cpp b/src/opentimelineio/subtitles.cpp new file mode 100644 index 0000000000..0d8dc59470 --- /dev/null +++ b/src/opentimelineio/subtitles.cpp @@ -0,0 +1,97 @@ +#include "opentimelineio/subtitles.h" + +namespace opentimelineio { + namespace OPENTIMELINEIO_VERSION { + + Subtitles::Subtitles(double extent_x, + double extent_y, + double padding_x, + double padding_y, + std::string background_color, + double background_opacity, + DisplayAlignment display_alignment, + std::vector timed_texts) + : Parent(), + _extent_x(extent_x), + _extent_y(extent_y), + _padding_x(padding_x), + _padding_y(padding_y), + _background_color(background_color), + _background_opacity(background_opacity), + _display_alignment(display_alignment), + _timed_texts(timed_texts.begin(), timed_texts.end()) { + } + + Subtitles::~Subtitles() noexcept {} + + TimeRange Subtitles::available_range(ErrorStatus *) const { + TimeRange timeRange; + for (auto timed_text : _timed_texts) { + timeRange = timeRange.extended_by(timed_text.value->marked_range()); + } + return timeRange; + } + + bool Subtitles::read_from(Reader &reader) { + auto result = reader.read("extent_x", &_extent_x) && + reader.read("extent_y", &_extent_y) && + reader.read("padding_x", &_padding_x) && + reader.read("padding_y", &_padding_y) && + reader.read("background_color", &_background_color) && + reader.read("background_opacity", &_background_opacity) && + reader.read("timed_texts", &_timed_texts); + std::string display_alignment_value; + result = result && reader.read("display_alignment", &display_alignment_value); + if (!result) { + return result; + } + + if (display_alignment_value == "before") { + _display_alignment = DisplayAlignment::before; + } else if (display_alignment_value == "after") { + _display_alignment = DisplayAlignment::after; + } else if (display_alignment_value == "center") { + _display_alignment = DisplayAlignment::justify; + } else if (display_alignment_value == "justify") { + _display_alignment = DisplayAlignment::justify; + } else { + // Unrecognized value + ErrorStatus error_status = ErrorStatus(ErrorStatus::JSON_PARSE_ERROR, + "Unknown display_alignment: " + display_alignment_value); + reader.error(error_status); + return false; + } + + return Parent::read_from(reader); + } + + void Subtitles::write_to(Writer &writer) const { + Parent::write_to(writer); + writer.write("extent_x", _extent_x); + writer.write("extent_y", _extent_y); + writer.write("padding_x", _padding_x); + writer.write("padding_y", _padding_y); + writer.write("background_color", _background_color); + writer.write("background_opacity", _background_opacity); + writer.write("timed_texts", _timed_texts); + + std::string display_alignment_value; + switch (_display_alignment) { + case DisplayAlignment::before: + display_alignment_value = "before"; + break; + case DisplayAlignment::after: + display_alignment_value = "after"; + break; + case DisplayAlignment::center: + display_alignment_value = "center"; + break; + case DisplayAlignment::justify: + display_alignment_value = "justify"; + break; + } + writer.write("display_alignment", display_alignment_value); + } + + } +} diff --git a/src/opentimelineio/subtitles.h b/src/opentimelineio/subtitles.h new file mode 100644 index 0000000000..45a7925b98 --- /dev/null +++ b/src/opentimelineio/subtitles.h @@ -0,0 +1,119 @@ +#pragma once + +#include "opentimelineio/version.h" +#include "opentimelineio/item.h" +#include "opentimelineio/timedText.h" + +namespace opentimelineio { + namespace OPENTIMELINEIO_VERSION { + class Subtitles : public Item { + public: + enum DisplayAlignment { + before = 0, + after = 1, + center = 2, + justify = 3 + }; + + struct Schema { + static auto constexpr name = "Subtitles"; + static int constexpr version = 1; + }; + + using Parent = Item; + + Subtitles(double extent_x = 0.f, + double extent_y = 0.f, + double padding_x = 0.f, + double padding_y = 0.f, + std::string background_color = std::string(), + double background_opacity = 0.f, + DisplayAlignment display_alignment = DisplayAlignment::after, + std::vector timed_texts = std::vector()); + + virtual TimeRange available_range(ErrorStatus *error_status) const; + + double const extent_x() const { + return _extent_x; + } + + void set_extent_x(double const extent_x) { + _extent_x = extent_x; + } + + double const extent_y() const { + return _extent_y; + } + + void set_extent_y(double const extent_y) { + _extent_y = extent_y; + } + + double const padding_x() const { + return _padding_x; + } + + void set_padding_x(double const padding_x) { + _padding_x = padding_x; + } + + double const padding_y() const { + return _padding_y; + } + + void set_padding_y(double const padding_y) { + _padding_y = padding_y; + } + + std::string const &background_color() const { + return _background_color; + } + + void set_background_color(std::string const &background_color) { + _background_color = background_color; + } + + double const background_opacity() const { + return _background_opacity; + } + + void set_background_opacity(double const background_opacity) { + _background_opacity = background_opacity; + } + + DisplayAlignment const display_alignment() const { + return _display_alignment; + } + + void set_display_alignment(DisplayAlignment const display_alignment) { + _display_alignment = display_alignment; + } + + std::vector>& timed_texts() { + return _timed_texts; + } + + std::vector> const& timed_texts() const { + return _timed_texts; + } + + protected: + ~Subtitles(); + + virtual bool read_from(Reader &); + + virtual void write_to(Writer &) const; + + private: + double _extent_x; + double _extent_y; + double _padding_x; + double _padding_y; + std::string _background_color; + double _background_opacity; + DisplayAlignment _display_alignment; + std::vector> _timed_texts; + }; + + } +} diff --git a/src/opentimelineio/timedText.cpp b/src/opentimelineio/timedText.cpp new file mode 100644 index 0000000000..ace3b8fb0d --- /dev/null +++ b/src/opentimelineio/timedText.cpp @@ -0,0 +1,27 @@ +#include "opentimelineio/timedText.h" + +namespace opentimelineio { + namespace OPENTIMELINEIO_VERSION { + + TimedText::TimedText(RationalTime const &in_time, + RationalTime const &out_time) + : Parent() { + set_marked_range(TimeRange::range_from_start_end_time(in_time, out_time)); + } + + TimedText::~TimedText() {} + + bool TimedText::read_from(Reader &reader) { + return reader.read("texts", &_texts) && + reader.read("style_ids", &_styleIDs) && + Parent::read_from(reader); + } + + void TimedText::write_to(Writer &writer) const { + Parent::write_to(writer); + writer.write("texts", _texts); + writer.write("style_ids", _styleIDs); + } + + } +} diff --git a/src/opentimelineio/timedText.h b/src/opentimelineio/timedText.h new file mode 100644 index 0000000000..9030a75351 --- /dev/null +++ b/src/opentimelineio/timedText.h @@ -0,0 +1,56 @@ +#pragma once + +#include "opentimelineio/version.h" +#include "opentimelineio/marker.h" +#include "opentimelineio/timedTextStyle.h" + +namespace opentimelineio { + namespace OPENTIMELINEIO_VERSION { + + class TimedText : public Marker { + public: + struct Schema { + static auto constexpr name = "TimedText"; + static int constexpr version = 1; + }; + + using Parent = Marker; + + explicit TimedText(RationalTime const &in_time = RationalTime(), + RationalTime const &out_time = RationalTime()); + + std::vector const &texts() const { + return _texts; + } + + std::vector const &styleIDs() const { + return _texts; + } + + void add_text(std::string const &text, std::string const &styleID = "") { + _texts.emplace_back(text); + _styleIDs.emplace_back(styleID); + } + + RationalTime in_time() const { + return marked_range().start_time(); + } + + RationalTime out_time() const { + return marked_range().end_time_exclusive(); + } + + protected: + ~TimedText(); + + virtual bool read_from(Reader &); + + virtual void write_to(Writer &) const; + + private: + std::vector _texts; + std::vector _styleIDs; + }; + + } +} diff --git a/src/opentimelineio/timedTextStyle.cpp b/src/opentimelineio/timedTextStyle.cpp new file mode 100644 index 0000000000..129c80f240 --- /dev/null +++ b/src/opentimelineio/timedTextStyle.cpp @@ -0,0 +1,52 @@ +#include "opentimelineio/timedTextStyle.h" + +namespace opentimelineio { + namespace OPENTIMELINEIO_VERSION { + + TimedTextStyle::TimedTextStyle(const std::string &style_id, + std::string text_alignment, + std::string text_color, + double text_size, + bool text_bold, + bool text_italics, + bool text_underline, + std::string font_family) + : Parent(), + _style_id(style_id), + _text_alignment(text_alignment), + _text_color(text_color), + _text_size(text_size), + _text_bold(text_bold), + _text_italics(text_italics), + _text_underline(text_underline), + _font_family(font_family) { + } + + TimedTextStyle::~TimedTextStyle() {} + + bool TimedTextStyle::read_from(Reader &reader) { + return reader.read("style_id", &_style_id) && + reader.read("text_alignment", &_text_alignment) && + reader.read("text_color", &_text_color) && + reader.read("text_size", &_text_size) && + reader.read("text_bold", &_text_bold) && + reader.read("text_italics", &_text_italics) && + reader.read("text_underline", &_text_underline) && + reader.read("font_family", &_font_family) && + Parent::read_from(reader); + } + + void TimedTextStyle::write_to(Writer &writer) const { + Parent::write_to(writer); + writer.write("style_id", _style_id); + writer.write("text_alignment", _text_alignment); + writer.write("text_color", _text_color); + writer.write("text_size", _text_size); + writer.write("text_bold", _text_bold); + writer.write("text_italics", _text_italics); + writer.write("text_underline", _text_underline); + writer.write("font_family", _font_family); + } + + } +} diff --git a/src/opentimelineio/timedTextStyle.h b/src/opentimelineio/timedTextStyle.h new file mode 100644 index 0000000000..3b98cabc0a --- /dev/null +++ b/src/opentimelineio/timedTextStyle.h @@ -0,0 +1,119 @@ +#pragma once + +#include "opentimelineio/version.h" +#include "opentimelineio/serializableObjectWithMetadata.h" +#include "opentimelineio/marker.h" + +namespace opentimelineio { + namespace OPENTIMELINEIO_VERSION { + + class TimedTextStyle : public SerializableObject { + public: + struct TextAlignment { + static auto constexpr left = "LEFT"; + static auto constexpr top = "TOP"; + static auto constexpr right = "RIGHT"; + static auto constexpr bottom = "BOTTOM"; + static auto constexpr center = "CENTER"; + }; + + struct Schema { + static auto constexpr name = "TimedTextStyle"; + static int constexpr version = 1; + }; + + using Parent = SerializableObject; + + TimedTextStyle(std::string const &style_id = std::string(), + std::string text_alignment = TextAlignment::bottom, + std::string text_color = Marker::Color::black, + double text_size = 10, + bool text_bold = false, + bool text_italics = false, + bool text_underline = false, + std::string font_family = std::string()); + + std::string const &style_id() const { + return _style_id; + } + + void set_style_id(std::string const &style_id) { + _style_id = style_id; + } + + std::string const &text_alignment() const { + return _text_alignment; + } + + void set_text_alignment(std::string const &text_alignment) { + _text_alignment = text_alignment; + } + + std::string const &text_color() const { + return _text_color; + } + + void set_text_color(std::string const &text_color) { + _text_color = text_color; + } + + double const text_size() const { + return _text_size; + } + + void set_text_size(double const text_size) { + _text_size = text_size; + } + + bool const is_text_bold() const { + return _text_bold; + } + + void set_text_bold(bool const text_bold) { + _text_bold = text_bold; + } + + bool const is_text_italicized() const { + return _text_italics; + } + + void set_text_italicized(bool const text_italics) { + _text_italics = text_italics; + } + + bool const is_text_underlined() const { + return _text_underline; + } + + void set_text_underlined(bool const text_underline) { + _text_underline = text_underline; + } + + std::string const &font_family() const { + return _font_family; + } + + void set_font_family(std::string const &font_family) { + _font_family = font_family; + } + + protected: + ~TimedTextStyle(); + + virtual bool read_from(Reader &); + + virtual void write_to(Writer &) const; + + private: + std::string _text_color; + std::string _style_id; + std::string _text_alignment; + double _text_size; + bool _text_bold; + bool _text_italics; + bool _text_underline; + std::string _font_family; + }; + + } +} diff --git a/src/opentimelineio/typeRegistry.cpp b/src/opentimelineio/typeRegistry.cpp index b15d341b22..1296901406 100644 --- a/src/opentimelineio/typeRegistry.cpp +++ b/src/opentimelineio/typeRegistry.cpp @@ -18,6 +18,9 @@ #include "opentimelineio/serializableObject.h" #include "opentimelineio/serializableObjectWithMetadata.h" #include "opentimelineio/stack.h" +#include "opentimelineio/subtitles.h" +#include "opentimelineio/timedText.h" +#include "opentimelineio/timedTextStyle.h" #include "opentimelineio/timeEffect.h" #include "opentimelineio/timeline.h" #include "opentimelineio/track.h" @@ -58,6 +61,9 @@ TypeRegistry::TypeRegistry() register_type(); register_type(); register_type(); + register_type(); + register_type(); + register_type(); register_type(); register_type_from_existing_type("Filler", 1, "Gap", nullptr); diff --git a/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp b/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp index 549ebc4038..438cafe5f6 100644 --- a/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp +++ b/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp @@ -22,8 +22,11 @@ #include "opentimelineio/timeline.h" #include "opentimelineio/track.h" #include "opentimelineio/transition.h" +#include "opentimelineio/timedText.h" +#include "opentimelineio/timedTextStyle.h" #include "opentimelineio/serializableCollection.h" #include "opentimelineio/stack.h" +#include "opentimelineio/subtitles.h" #include "opentimelineio/unknownSchema.h" #include "otio_utils.h" @@ -41,6 +44,9 @@ using EffectVectorProxy = using TrackVectorProxy = MutableSequencePyAPI>, Track*>; +using TimedTextVectorProxy = + MutableSequencePyAPI>, TimedText*>; + using SOWithMetadata = SerializableObjectWithMetadata; namespace { @@ -192,6 +198,67 @@ static void define_bases1(py::module m) { .def_property("name", [](SOWithMetadata* so) { return plain_string(so->name()); }, &SOWithMetadata::set_name); + + auto timed_text_style_class = py::class_>(m, "TimedTextStyle", py::dynamic_attr()) + .def(py::init([](std::string style_id, + std::string text_alignment, + std::string text_color, + float text_size, + bool text_bold, + bool text_italics, + bool text_underline, + std::string font_family) { + return new TimedTextStyle(style_id, + text_alignment, + text_color, + text_size, + text_bold, + text_italics, + text_underline, + font_family); + }), + "style_id"_a = std::string(), + "text_alignment"_a = TimedTextStyle::TextAlignment::bottom, + "text_color"_a = Marker::Color::black, + "text_size"_a = 10.0f, + "text_bold"_a = false, + "text_italics"_a = false, + "text_underline"_a = false, + "font_family"_a = std::string()); + + py::class_(timed_text_style_class, "TextAlignment") + .def_property_readonly_static("LEFT", [](py::object /* self */) { return TimedTextStyle::TextAlignment::left; }) + .def_property_readonly_static("TOP", [](py::object /* self */) { return TimedTextStyle::TextAlignment::top; }) + .def_property_readonly_static("RIGHT", [](py::object /* self */) { return TimedTextStyle::TextAlignment::right; }) + .def_property_readonly_static("BOTTOM", [](py::object /* self */) { return TimedTextStyle::TextAlignment::bottom; }) + .def_property_readonly_static("CENTER", [](py::object /* self */) { return TimedTextStyle::TextAlignment::center; }); + + timed_text_style_class + .def_property("style_id", [](TimedTextStyle* tts) { + return plain_string(tts->style_id()); + }, &TimedTextStyle::set_style_id) + .def_property("text_alignment", [](TimedTextStyle* tts) { + return plain_string(tts->text_alignment()); + }, &TimedTextStyle::set_text_alignment) + .def_property("text_color", [](TimedTextStyle* tts) { + return plain_string(tts->text_color()); + }, &TimedTextStyle::set_text_color) + .def_property("text_size", [](TimedTextStyle* tts) { + return tts->text_size(); + }, &TimedTextStyle::set_text_size) + .def_property("text_bold", [](TimedTextStyle* tts) { + return tts->is_text_bold(); + }, &TimedTextStyle::set_text_bold) + .def_property("text_italics", [](TimedTextStyle* tts) { + return tts->is_text_italicized(); + }, &TimedTextStyle::set_text_italicized) + .def_property("text_underline", [](TimedTextStyle* tts) { + return tts->is_text_underlined(); + }, &TimedTextStyle::set_text_underlined) + .def_property("font_family", [](TimedTextStyle* tts) { + return plain_string(tts->font_family()); + }, &TimedTextStyle::set_font_family); } static void define_bases2(py::module m) { @@ -290,9 +357,25 @@ static void define_bases2(py::module m) { .def("children_if", [](SerializableCollection* t, py::object descended_from_type, optional const& search_range) { return children_if(t, descended_from_type, search_range); }, "descended_from_type"_a = py::none(), "search_range"_a = nullopt); + + py::class_>(m, "TimedText", py::dynamic_attr()) + .def(py::init([](RationalTime in_time, RationalTime out_time) { + return new TimedText(in_time, out_time); + }), + "in_time"_a = RationalTime(), + "out_time"_a = RationalTime()) + .def_property_readonly("in_time", &TimedText::in_time) + .def_property_readonly("out_time", &TimedText::out_time) + .def_property_readonly("texts", &TimedText::texts) + .def_property_readonly("style_ids", &TimedText::styleIDs) + .def("add_text", [](TimedText* timedText, const std::string& text, const std::string& styleID){ + timedText->add_text(text, styleID); + }, "text"_a, "styleID"_a = ""); } static void define_items_and_compositions(py::module m) { + TimedTextVectorProxy::define_py_class(m, "TimedTextVector"); + py::class_>(m, "Item", py::dynamic_attr()) .def(py::init([](std::string name, optional source_range, py::object effects, py::object markers, py::bool_ enabled, py::object metadata) { @@ -598,6 +681,41 @@ static void define_items_and_compositions(py::module m) { .def("children_if", [](Timeline* t, py::object descended_from_type, optional const& search_range) { return children_if(t, descended_from_type, search_range); }, "descended_from_type"_a = py::none(), "search_range"_a = nullopt); + + auto subtitles_class = py::class_>(m, "Subtitles", py::dynamic_attr()); + + py::enum_(subtitles_class, "DisplayAlignment", "Attribute used to specify the alignment of block areas in the block progression direction.") + .value("before", Subtitles::DisplayAlignment::before, "") + .value("after", Subtitles::DisplayAlignment::after, "") + .value("center", Subtitles::DisplayAlignment::center, "") + .value("justify", Subtitles::DisplayAlignment::justify, ""); + + subtitles_class + .def(py::init([](float extent_x, float extent_y, float padding_x, float padding_y, + std::string background_color, float background_opacity, Subtitles::DisplayAlignment display_alignment, + py::object timed_texts) { + return new Subtitles(extent_x, extent_y, padding_x, padding_y, + background_color, background_opacity, display_alignment, + py_to_vector(timed_texts)); + }), + "extent_x"_a = 0.f, + "extent_y"_a = 0.f, + "padding_x"_a = 0.f, + "padding_y"_a = 0.f, + "background_color"_a = std::string(), + "background_opacity"_a = 0.f, + "display_alignment"_a = Subtitles::DisplayAlignment::after, + "timed_texts"_a = py::none()) + .def_property("extent_x", &Subtitles::extent_x, &Subtitles::set_extent_x) + .def_property("extent_y", &Subtitles::extent_y, &Subtitles::set_extent_y) + .def_property("padding_x", &Subtitles::padding_x, &Subtitles::set_padding_x) + .def_property("padding_y", &Subtitles::padding_y, &Subtitles::set_padding_y) + .def_property("background_color", &Subtitles::background_color, &Subtitles::set_background_color) + .def_property("background_opacity", &Subtitles::background_opacity, &Subtitles::set_background_opacity) + .def_property("display_alignment", &Subtitles::display_alignment, &Subtitles::set_display_alignment) + .def_property_readonly("timed_texts", [](Subtitles* subtitles) { + return ((TimedTextVectorProxy*) &subtitles->timed_texts()); + }); } static void define_effects(py::module m) { diff --git a/src/py-opentimelineio/opentimelineio/core/_core_utils.py b/src/py-opentimelineio/opentimelineio/core/_core_utils.py index 3e35426edd..043b406250 100644 --- a/src/py-opentimelineio/opentimelineio/core/_core_utils.py +++ b/src/py-opentimelineio/opentimelineio/core/_core_utils.py @@ -393,6 +393,7 @@ def __deepcopy__(self, memo=None): _add_mutable_sequence_methods(AnyVector, conversion_func=_value_to_any) _add_mutable_sequence_methods(_otio.MarkerVector) _add_mutable_sequence_methods(_otio.EffectVector) +_add_mutable_sequence_methods(_otio.TimedTextVector) _add_mutable_sequence_methods(_otio.Composition, side_effecting_insertions=True) _add_mutable_sequence_methods(_otio.SerializableCollection) diff --git a/src/py-opentimelineio/opentimelineio/schema/__init__.py b/src/py-opentimelineio/opentimelineio/schema/__init__.py index f390bbbd4d..6236c453f0 100644 --- a/src/py-opentimelineio/opentimelineio/schema/__init__.py +++ b/src/py-opentimelineio/opentimelineio/schema/__init__.py @@ -40,6 +40,9 @@ MissingReference, SerializableCollection, Stack, + Subtitles, + TimedText, + TimedTextStyle, Timeline, Track, Transition @@ -63,6 +66,9 @@ marker, serializable_collection, stack, + subtitles, + timed_text, + timed_text_style, timeline, track, transition, diff --git a/src/py-opentimelineio/opentimelineio/schema/subtitles.py b/src/py-opentimelineio/opentimelineio/schema/subtitles.py new file mode 100644 index 0000000000..3d39fda878 --- /dev/null +++ b/src/py-opentimelineio/opentimelineio/schema/subtitles.py @@ -0,0 +1,44 @@ +from .. core._core_utils import add_method +from .. import _otio + + +@add_method(_otio.Subtitles) +def __str__(self): + return ( + 'Subtitles(' + '{}, {}, {}, {}, "{}", {}, {}, {})' .format( + self.extent_x, + self.extent_y, + self.padding_x, + self.padding_y, + self.background_color, + self.background_opacity, + self.display_alignment, + str(self.timed_texts) + ) + ) + + +@add_method(_otio.Subtitles) +def __repr__(self): + return ( + 'Subtitles(' + 'extent_x={}, ' + 'extent_y={}, ' + 'padding_x={}, ' + 'padding_y={}, ' + 'background_color={}, ' + 'background_opacity={}, ' + 'display_alignment={}, ' + 'timed_texts={} ' + ')' .format( + repr(self.extent_x), + repr(self.extent_y), + repr(self.padding_x), + repr(self.padding_y), + repr(self.background_color), + repr(self.background_opacity), + repr(self.display_alignment), + repr(self.timed_texts) + ) + ) diff --git a/src/py-opentimelineio/opentimelineio/schema/timed_text.py b/src/py-opentimelineio/opentimelineio/schema/timed_text.py new file mode 100644 index 0000000000..b262ea6d27 --- /dev/null +++ b/src/py-opentimelineio/opentimelineio/schema/timed_text.py @@ -0,0 +1,32 @@ +from .. core._core_utils import add_method +from .. import _otio + + +@add_method(_otio.TimedText) +def __str__(self): + return ( + 'TimedText(' + '"{}", {}, {}, {})' .format( + self.in_time, + self.out_time, + self.texts, + self.style_ids + ) + ) + + +@add_method(_otio.TimedText) +def __repr__(self): + return ( + 'TimedText(' + 'in_time={}, ' + 'out_time={}, ' + 'texts={}, ' + 'style_ids={}' + ')' .format( + repr(self.in_time), + repr(self.out_time), + repr(self.texts), + repr(self.style_ids) + ) + ) diff --git a/src/py-opentimelineio/opentimelineio/schema/timed_text_style.py b/src/py-opentimelineio/opentimelineio/schema/timed_text_style.py new file mode 100644 index 0000000000..8d84deb18c --- /dev/null +++ b/src/py-opentimelineio/opentimelineio/schema/timed_text_style.py @@ -0,0 +1,44 @@ +from .. core._core_utils import add_method +from .. import _otio + + +@add_method(_otio.TimedTextStyle) +def __str__(self): + return ( + 'TimedTextStyle(' + '"{}", "{}", "{}", {}, {}, {}, {}, "{}")' .format( + self.style_id, + self.text_alignment, + self.text_color, + self.text_size, + self.text_bold, + self.text_italics, + self.text_underline, + self.font_family + ) + ) + + +@add_method(_otio.TimedTextStyle) +def __repr__(self): + return ( + 'TimedTextStyle(' + 'style_id={}, ' + 'text_alignment={}, ' + 'text_color={}, ' + 'text_size={}, ' + 'text_bold={}, ' + 'text_italics={}, ' + 'text_underline={}, ' + 'font_family={}' + ')' .format( + repr(self.style_id), + repr(self.text_alignment), + repr(self.text_color), + repr(self.text_size), + repr(self.text_bold), + repr(self.text_italics), + repr(self.text_underline), + repr(self.font_family), + ) + )