pax_global_header00006660000000000000000000000064147406614750014530gustar00rootroot0000000000000052 comment=115f0ab77b236b469aceb15e810a4852b42bb97f ttconv-1.1.1/000077500000000000000000000000001474066147500130455ustar00rootroot00000000000000ttconv-1.1.1/.coveragerc000066400000000000000000000002411474066147500151630ustar00rootroot00000000000000[run] data_file = build/.coverage source_pkgs = ttconv [html] directory = build/htmlcov [xml] output = build/coverage.xml [json] output = build/coverage.json ttconv-1.1.1/.gitattributes000066400000000000000000000013001474066147500157320ustar00rootroot00000000000000# Basic .gitattributes for a python repo. # Adapted from https://github.com/alexkaratarakis/gitattributes/blob/master/Python.gitattributes * text=auto # Markdown *.md text diff=markdown # shell files *.sh text eol=lf # Source files # ============ *.pxd text eol=lf diff=python *.py text eol=lf diff=python *.py3 text eol=lf diff=python *.pyw text eol=lf diff=python *.pyx text eol=lf diff=python *.pyz text eol=lf diff=python *.pyi text eol=lf diff=python # Binary files # ============ *.db binary *.p binary *.pkl binary *.pickle binary *.pyc binary export-ignore *.pyo binary export-ignore *.pyd binary # Jupyter notebook *.ipynb text ttconv-1.1.1/.github/000077500000000000000000000000001474066147500144055ustar00rootroot00000000000000ttconv-1.1.1/.github/workflows/000077500000000000000000000000001474066147500164425ustar00rootroot00000000000000ttconv-1.1.1/.github/workflows/main.yml000066400000000000000000000012261474066147500201120ustar00rootroot00000000000000name: CI on: [push, pull_request] jobs: run: runs-on: ubuntu-20.04 if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) steps: - uses: actions/setup-python@v4 with: python-version: '3.7' - name: Initialize pip environment run: python -m pip install pipenv - name: Checkout repo uses: actions/checkout@v2 with: submodules : true - name: Install python environment run: pipenv install --dev - name: Run CI env: PYTHONPATH: src/main/python run: /bin/sh scripts/ci.sh ttconv-1.1.1/.gitignore000066400000000000000000000040401474066147500150330ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # Visual Studio Code .vscode # Idea .idea/ ttconv-1.1.1/.gitmodules000066400000000000000000000003211474066147500152160ustar00rootroot00000000000000[submodule "src/test/resources/ttml/imsc-tests"] path = src/test/resources/ttml/imsc-tests url = https://github.com/w3c/imsc-tests [submodule "imsc-tests/"] url = https://github.com/sandflow/imsc-tests.git ttconv-1.1.1/.pylintrc000066400000000000000000000441651474066147500147240ustar00rootroot00000000000000[MASTER] # A comma-separated list of package or module names from where C extensions may # be loaded. Extensions are loading into the active Python interpreter and may # run arbitrary code. extension-pkg-whitelist= # Specify a score threshold to be exceeded before program exits with error. fail-under=10 # Add files or directories to the blacklist. They should be base names, not # paths. ignore=CVS # Add files or directories matching the regex patterns to the blacklist. The # regex matches against base names, not paths. ignore-patterns= # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). #init-hook= # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the # number of processors available to use. jobs=1 # Control the amount of potential inferred values when inferring a single # object. This can help the performance when dealing with large functions or # complex, nested conditions. limit-inference-results=100 # List of plugins (as comma separated values of python module names) to load, # usually to register additional checkers. load-plugins= # Pickle collected data for later comparisons. persistent=yes # When enabled, pylint would attempt to guess common misconfiguration and emit # user-friendly hints instead of false-positive error messages. suggestion-mode=yes # Allow loading of arbitrary C extensions. Extensions are imported into the # active Python interpreter and may run arbitrary code. unsafe-load-any-extension=no [MESSAGES CONTROL] # Only show warnings with the listed confidence levels. Leave empty to show # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. confidence= # Disable the message, report, category or checker with the given id(s). You # can either give multiple identifiers separated by comma (,) or put this # option multiple times (only on the command line, not in the configuration # file where it should appear only once). You can also use "--disable=all" to # disable everything first and then reenable specific checks. For example, if # you want to run only the similarities checker, you can use "--disable=all # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use "--disable=all --enable=classes # --disable=W". disable=print-statement, parameter-unpacking, unpacking-in-except, old-raise-syntax, backtick, long-suffix, old-ne-operator, old-octal-literal, import-star-module-level, non-ascii-bytes-literal, raw-checker-failed, bad-inline-option, locally-disabled, file-ignored, suppressed-message, useless-suppression, deprecated-pragma, use-symbolic-message-instead, apply-builtin, basestring-builtin, buffer-builtin, cmp-builtin, coerce-builtin, execfile-builtin, file-builtin, long-builtin, raw_input-builtin, reduce-builtin, standarderror-builtin, unicode-builtin, xrange-builtin, coerce-method, delslice-method, getslice-method, setslice-method, no-absolute-import, old-division, dict-iter-method, dict-view-method, next-method-called, metaclass-assignment, indexing-exception, raising-string, reload-builtin, oct-method, hex-method, nonzero-method, cmp-method, input-builtin, round-builtin, intern-builtin, unichr-builtin, map-builtin-not-iterating, zip-builtin-not-iterating, range-builtin-not-iterating, filter-builtin-not-iterating, using-cmp-argument, eq-without-hash, div-method, idiv-method, rdiv-method, exception-message-attribute, invalid-str-codec, sys-max-int, bad-python3-import, deprecated-string-function, deprecated-str-translate-call, deprecated-itertools-function, deprecated-types-field, next-method-defined, dict-items-not-iterating, dict-keys-not-iterating, dict-values-not-iterating, deprecated-operator-function, deprecated-urllib-function, xreadlines-attribute, deprecated-sys-function, exception-escape, comprehension-escape, trailing-whitespace, similarities # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option # multiple time (only on the command line, not in the configuration file where # it should appear only once). See also the "--disable" option for examples. enable=c-extension-no-member [REPORTS] # Python expression which should return a score less than or equal to 10. You # have access to the variables 'error', 'warning', 'refactor', and 'convention' # which contain the number of messages in each category, as well as 'statement' # which is the total number of statements analyzed. This score is used by the # global evaluation report (RP0004). evaluation=10.0 - ((float(5 * error + warning) / statement) * 10) # Template used to display messages. This is a python new-style format string # used to format the message information. See doc for all details. #msg-template= # Set the output format. Available formats are text, parseable, colorized, json # and msvs (visual studio). You can also give a reporter class, e.g. # mypackage.mymodule.MyReporterClass. output-format=text # Tells whether to display a full report or only the messages. reports=no # Activate the evaluation score. score=yes [REFACTORING] # Maximum number of nested blocks for function / method body max-nested-blocks=5 # Complete name of functions that never returns. When checking for # inconsistent-return-statements if a never returning function is called then # it will be considered as an explicit return statement and no message will be # printed. never-returning-functions=sys.exit [BASIC] # Naming style matching correct argument names. argument-naming-style=snake_case # Regular expression matching correct argument names. Overrides argument- # naming-style. #argument-rgx= # Naming style matching correct attribute names. attr-naming-style=snake_case # Regular expression matching correct attribute names. Overrides attr-naming- # style. #attr-rgx= # Bad variable names which should always be refused, separated by a comma. bad-names=foo, bar, baz, toto, tutu, tata # Bad variable names regexes, separated by a comma. If names match any regex, # they will always be refused bad-names-rgxs= # Naming style matching correct class attribute names. class-attribute-naming-style=any # Regular expression matching correct class attribute names. Overrides class- # attribute-naming-style. #class-attribute-rgx= # Naming style matching correct class names. class-naming-style=PascalCase # Regular expression matching correct class names. Overrides class-naming- # style. class-rgx=[A-Z_][a-zA-Z0-9]*$ # Naming style matching correct constant names. const-naming-style=UPPER_CASE # Regular expression matching correct constant names. Overrides const-naming- # style. #const-rgx= # Minimum line length for functions/classes that require docstrings, shorter # ones are exempt. docstring-min-length=-1 # Naming style matching correct function names. function-naming-style=snake_case # Regular expression matching correct function names. Overrides function- # naming-style. #function-rgx= # Good variable names which should always be accepted, separated by a comma. good-names=i, j, k, x, y, r, ex, id, Run, _ # Good variable names regexes, separated by a comma. If names match any regex, # they will always be accepted good-names-rgxs= # Include a hint for the correct naming format with invalid-name. include-naming-hint=no # Naming style matching correct inline iteration names. inlinevar-naming-style=any # Regular expression matching correct inline iteration names. Overrides # inlinevar-naming-style. #inlinevar-rgx= # Naming style matching correct method names. method-naming-style=snake_case # Regular expression matching correct method names. Overrides method-naming- # style. #method-rgx= # Naming style matching correct module names. module-naming-style=snake_case # Regular expression matching correct module names. Overrides module-naming- # style. #module-rgx= # Colon-delimited sets of names that determine each other's naming style when # the name regexes allow several styles. name-group= # Regular expression which should only match function or class names that do # not require a docstring. no-docstring-rgx=^_ # List of decorators that produce properties, such as abc.abstractproperty. Add # to this list to register other decorators that produce valid properties. # These decorators are taken in consideration only for invalid-name. property-classes=abc.abstractproperty # Naming style matching correct variable names. variable-naming-style=snake_case # Regular expression matching correct variable names. Overrides variable- # naming-style. variable-rgx=[a-z_][a-z0-9_]{0,30}$ [FORMAT] # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. expected-line-ending-format= # Regexp for a line that is allowed to be longer than the limit. ignore-long-lines=^\s*(# )??$ # Number of spaces of indent required inside a hanging or continued line. indent-after-paren=2 # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 # tab). indent-string=' ' # Maximum number of characters on a single line. max-line-length=132 # Maximum number of lines in a module. max-module-lines=3000 # List of optional constructs for which whitespace checking is disabled. `dict- # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. # `trailing-comma` allows a space between comma and closing bracket: (a, ). # `empty-line` allows space-only lines. no-space-check=trailing-comma, dict-separator # Allow the body of a class to be on the same line as the declaration if body # contains single statement. single-line-class-stmt=no # Allow the body of an if to be on the same line as the test if there is no # else. single-line-if-stmt=no [LOGGING] # The type of string formatting that logging methods do. `old` means using % # formatting, `new` is for `{}` formatting. logging-format-style=old # Logging modules to check that the string format arguments are in logging # function parameter format. logging-modules=logging [MISCELLANEOUS] # List of note tags to take in consideration, separated by a comma. notes=FIXME, XXX, TODO # Regular expression of note tags to take in consideration. #notes-rgx= [SIMILARITIES] # Ignore comments when computing similarities. ignore-comments=yes # Ignore docstrings when computing similarities. ignore-docstrings=yes # Ignore imports when computing similarities. ignore-imports=no # Minimum lines number of a similarity. min-similarity-lines=4 [SPELLING] # Limits count of emitted suggestions for spelling mistakes. max-spelling-suggestions=4 # Spelling dictionary name. Available dictionaries: none. To make it work, # install the python-enchant package. spelling-dict= # List of comma separated words that should not be checked. spelling-ignore-words= # A path to a file that contains the private dictionary; one word per line. spelling-private-dict-file= # Tells whether to store unknown words to the private dictionary (see the # --spelling-private-dict-file option) instead of raising a message. spelling-store-unknown-words=no [STRING] # This flag controls whether inconsistent-quotes generates a warning when the # character used as a quote delimiter is used inconsistently within a module. check-quote-consistency=no # This flag controls whether the implicit-str-concat should generate a warning # on implicit string concatenation in sequences defined over several lines. check-str-concat-over-line-jumps=no [TYPECHECK] # List of decorators that produce context managers, such as # contextlib.contextmanager. Add to this list to register other decorators that # produce valid context managers. contextmanager-decorators=contextlib.contextmanager # List of members which are set dynamically and missed by pylint inference # system, and so shouldn't trigger E1101 when accessed. Python regular # expressions are accepted. generated-members=SccAttributeCode.BMS.value # Tells whether missing members accessed in mixin class should be ignored. A # mixin class is detected if its name ends with "mixin" (case insensitive). ignore-mixin-members=yes # Tells whether to warn about missing members when the owner of the attribute # is inferred to be None. ignore-none=yes # This flag controls whether pylint should warn about no-member and similar # checks whenever an opaque object is returned when inferring. The inference # can return multiple potential results while evaluating a Python object, but # some branches might not be evaluated, which results in partial inference. In # that case, it might be useful to still emit no-member and other checks for # the rest of the inferred objects. ignore-on-opaque-inference=yes # List of class names for which member attributes should not be checked (useful # for classes with dynamically set attributes). This supports the use of # qualified names. ignored-classes=optparse.Values,thread._local,_thread._local # List of module names for which member attributes should not be checked # (useful for modules/projects where namespaces are manipulated during runtime # and thus existing member attributes cannot be deduced by static analysis). It # supports qualified module names, as well as Unix pattern matching. ignored-modules= # Show a hint with possible names when a member name was not found. The aspect # of finding the hint is based on edit distance. missing-member-hint=yes # The minimum edit distance a name should have in order to be considered a # similar match for a missing member name. missing-member-hint-distance=1 # The total number of similar names that should be taken in consideration when # showing a hint for a missing member. missing-member-max-choices=1 # List of decorators that change the signature of a decorated function. signature-mutators= [VARIABLES] # List of additional names supposed to be defined in builtins. Remember that # you should avoid defining new builtins when possible. additional-builtins= # Tells whether unused global variables should be treated as a violation. allow-global-unused-variables=yes # List of strings which can identify a callback function by name. A callback # name must start or end with one of those strings. callbacks=cb_, _cb # A regular expression matching the name of dummy variables (i.e. expected to # not be used). dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ # Argument names that match this expression will be ignored. Default to name # with leading underscore. ignored-argument-names=_.*|^ignored_|^unused_ # Tells whether we should check for unused import in __init__ files. init-import=no # List of qualified module names which can have objects that can redefine # builtins. redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io [CLASSES] # List of method names used to declare (i.e. assign) instance attributes. defining-attr-methods=__init__, __new__, setUp, __post_init__ # List of member names, which should be excluded from the protected access # warning. exclude-protected=_asdict, _fields, _replace, _source, _make # List of valid names for the first argument in a class method. valid-classmethod-first-arg=cls # List of valid names for the first argument in a metaclass class method. valid-metaclass-classmethod-first-arg=cls [DESIGN] # Maximum number of arguments for function / method. max-args=8 # Maximum number of attributes for a class (see R0902). max-attributes=20 # Maximum number of boolean expressions in an if statement (see R0916). max-bool-expr=5 # Maximum number of branch for function / method body. max-branches=30 # Maximum number of locals for function / method body. max-locals=30 # Maximum number of parents for a class (see R0901). max-parents=7 # Maximum number of public methods for a class (see R0904). max-public-methods=100 # Maximum number of return / yield for function / method body. max-returns=12 # Maximum number of statements in function / method body. max-statements=200 # Minimum number of public methods for a class (see R0903). min-public-methods=0 [IMPORTS] # List of modules that can be imported at any level, not just the top level # one. allow-any-import-level= # Allow wildcard imports from modules that define __all__. allow-wildcard-with-all=no # Analyse import fallback blocks. This can be used to support both Python 2 and # 3 compatible code, which means that the block might have code that exists # only in one or another interpreter, leading to false positives when analysed. analyse-fallback-blocks=no # Deprecated modules which should not be used, separated by a comma. deprecated-modules=optparse,tkinter.tix # Create a graph of external dependencies in the given file (report RP0402 must # not be disabled). ext-import-graph= # Create a graph of every (i.e. internal and external) dependencies in the # given file (report RP0402 must not be disabled). import-graph= # Create a graph of internal dependencies in the given file (report RP0402 must # not be disabled). int-import-graph= # Force import order to recognize a module as part of the standard # compatibility libraries. known-standard-library= # Force import order to recognize a module as part of a third party library. known-third-party=enchant # Couples of modules and preferred modules, separated by a comma. preferred-modules= [EXCEPTIONS] # Exceptions that will emit a warning when being caught. Defaults to # "BaseException, Exception". overgeneral-exceptions=BaseException, Exception ttconv-1.1.1/Dockerfile000066400000000000000000000002501474066147500150340ustar00rootroot00000000000000FROM python:3.7-buster WORKDIR /usr/src/app ADD . . RUN apt-get update && \ apt-get -y install pipenv RUN pipenv install --dev ENV PYTHONPATH src/main/python ttconv-1.1.1/LICENSE.txt000066400000000000000000000024071474066147500146730ustar00rootroot00000000000000Copyright (c) 2020, Sandflow Consulting LLC Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.ttconv-1.1.1/Pipfile000066400000000000000000000002461474066147500143620ustar00rootroot00000000000000[[source]] name = "pypi" url = "https://pypi.org/simple" verify_ssl = true [dev-packages] pylint = "*" coverage = "*" [packages] [requires] python_version = "3.7" ttconv-1.1.1/Pipfile.lock000066400000000000000000000242241474066147500153130ustar00rootroot00000000000000{ "_meta": { "hash": { "sha256": "4e999b45bfac344560201700cc886814aba5b791e524ab6d2afdf420d4d68f06" }, "pipfile-spec": 6, "requires": { "python_version": "3.7" }, "sources": [ { "name": "pypi", "url": "https://pypi.org/simple", "verify_ssl": true } ] }, "default": {}, "develop": { "astroid": { "hashes": [ "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703", "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386" ], "markers": "python_version >= '3.5'", "version": "==2.4.2" }, "colorama": { "hashes": [ "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" ], "markers": "sys_platform == 'win32'", "version": "==0.4.4" }, "coverage": { "hashes": [ "sha256:0203acd33d2298e19b57451ebb0bed0ab0c602e5cf5a818591b4918b1f97d516", "sha256:0f313707cdecd5cd3e217fc68c78a960b616604b559e9ea60cc16795c4304259", "sha256:1c6703094c81fa55b816f5ae542c6ffc625fec769f22b053adb42ad712d086c9", "sha256:1d44bb3a652fed01f1f2c10d5477956116e9b391320c94d36c6bf13b088a1097", "sha256:280baa8ec489c4f542f8940f9c4c2181f0306a8ee1a54eceba071a449fb870a0", "sha256:29a6272fec10623fcbe158fdf9abc7a5fa032048ac1d8631f14b50fbfc10d17f", "sha256:2b31f46bf7b31e6aa690d4c7a3d51bb262438c6dcb0d528adde446531d0d3bb7", "sha256:2d43af2be93ffbad25dd959899b5b809618a496926146ce98ee0b23683f8c51c", "sha256:381ead10b9b9af5f64646cd27107fb27b614ee7040bb1226f9c07ba96625cbb5", "sha256:47a11bdbd8ada9b7ee628596f9d97fbd3851bd9999d398e9436bd67376dbece7", "sha256:4d6a42744139a7fa5b46a264874a781e8694bb32f1d76d8137b68138686f1729", "sha256:50691e744714856f03a86df3e2bff847c2acede4c191f9a1da38f088df342978", "sha256:530cc8aaf11cc2ac7430f3614b04645662ef20c348dce4167c22d99bec3480e9", "sha256:582ddfbe712025448206a5bc45855d16c2e491c2dd102ee9a2841418ac1c629f", "sha256:63808c30b41f3bbf65e29f7280bf793c79f54fb807057de7e5238ffc7cc4d7b9", "sha256:71b69bd716698fa62cd97137d6f2fdf49f534decb23a2c6fc80813e8b7be6822", "sha256:7858847f2d84bf6e64c7f66498e851c54de8ea06a6f96a32a1d192d846734418", "sha256:78e93cc3571fd928a39c0b26767c986188a4118edc67bc0695bc7a284da22e82", "sha256:7f43286f13d91a34fadf61ae252a51a130223c52bfefb50310d5b2deb062cf0f", "sha256:86e9f8cd4b0cdd57b4ae71a9c186717daa4c5a99f3238a8723f416256e0b064d", "sha256:8f264ba2701b8c9f815b272ad568d555ef98dfe1576802ab3149c3629a9f2221", "sha256:9342dd70a1e151684727c9c91ea003b2fb33523bf19385d4554f7897ca0141d4", "sha256:9361de40701666b034c59ad9e317bae95c973b9ff92513dd0eced11c6adf2e21", "sha256:9669179786254a2e7e57f0ecf224e978471491d660aaca833f845b72a2df3709", "sha256:aac1ba0a253e17889550ddb1b60a2063f7474155465577caa2a3b131224cfd54", "sha256:aef72eae10b5e3116bac6957de1df4d75909fc76d1499a53fb6387434b6bcd8d", "sha256:bd3166bb3b111e76a4f8e2980fa1addf2920a4ca9b2b8ca36a3bc3dedc618270", "sha256:c1b78fb9700fc961f53386ad2fd86d87091e06ede5d118b8a50dea285a071c24", "sha256:c3888a051226e676e383de03bf49eb633cd39fc829516e5334e69b8d81aae751", "sha256:c5f17ad25d2c1286436761b462e22b5020d83316f8e8fcb5deb2b3151f8f1d3a", "sha256:c851b35fc078389bc16b915a0a7c1d5923e12e2c5aeec58c52f4aa8085ac8237", "sha256:cb7df71de0af56000115eafd000b867d1261f786b5eebd88a0ca6360cccfaca7", "sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636", "sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8" ], "index": "pypi", "version": "==5.3" }, "isort": { "hashes": [ "sha256:dcab1d98b469a12a1a624ead220584391648790275560e1a43e54c5dceae65e7", "sha256:dcaeec1b5f0eca77faea2a35ab790b4f3680ff75590bfcb7145986905aab2f58" ], "markers": "python_version >= '3.6' and python_version < '4.0'", "version": "==5.6.4" }, "lazy-object-proxy": { "hashes": [ "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d", "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449", "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08", "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a", "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50", "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd", "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239", "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb", "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea", "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e", "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156", "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142", "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442", "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62", "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db", "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531", "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383", "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a", "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357", "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.4.3" }, "mccabe": { "hashes": [ "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" ], "version": "==0.6.1" }, "pylint": { "hashes": [ "sha256:bb4a908c9dadbc3aac18860550e870f58e1a02c9f2c204fdf5693d73be061210", "sha256:bfe68f020f8a0fece830a22dd4d5dddb4ecc6137db04face4c3420a46a52239f" ], "index": "pypi", "version": "==2.6.0" }, "six": { "hashes": [ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.0" }, "toml": { "hashes": [ "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" ], "version": "==0.10.1" }, "typed-ast": { "hashes": [ "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" ], "markers": "python_version < '3.8' and implementation_name == 'cpython'", "version": "==1.4.1" }, "wrapt": { "hashes": [ "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" ], "version": "==1.12.1" } } } ttconv-1.1.1/README.md000066400000000000000000000225041474066147500143270ustar00rootroot00000000000000# ttconv (Timed Text Conversion) $$\ $$\ $$ | $$ | $$$$$$\ $$$$$$\ $$$$$$$\ $$$$$$\ $$$$$$$\ $$\ $$\ \_$$ _|\_$$ _| $$ _____|$$ __$$\ $$ __$$\\$$\ $$ | $$ | $$ | $$ / $$ / $$ |$$ | $$ |\$$\$$ / $$ |$$\ $$ |$$\ $$ | $$ | $$ |$$ | $$ | \$$$ / \$$$$ |\$$$$ |\$$$$$$$\ \$$$$$$ |$$ | $$ | \$ / \____/ \____/ \_______| \______/ \__| \__| \_/ ## Introduction _ttconv_ is a library and command line application written in pure Python for converting between timed text formats used in the presentations of captions, subtitles, karaoke, etc. TTML / IMSC --- --- IMSC / TTML \ / SCC / CEA 608 ------- 0 or more document filters --------- WebVTT / [ Canonical Model ] \ EBU STL ------- --- SRT / SRT --------- / WebVTT ---- _ttconv_ works by mapping the input document, whatever its format, into an internal canonical model, which is then optionally transformed by document filters, and finally mapped to the format of the output document is derived. The canonical model closely follows the [TTML 2](https://www.w3.org/TR/ttml2) data model, as constrained by the [IMSC 1.1 Text Profile](https://www.w3.org/TR/ttml-imsc1.1/#text-profile) specification. ## Online demo [https://ttconv.sandflow.com/](https://ttconv.sandflow.com/) ## Format support _ttconv_ currently supports the following input and output formats. Additional input and output formats are planned, and suggestions/contributions are welcome. ### Input Formats * [CEA 608/.scc](https://en.wikipedia.org/wiki/EIA-608) * [IMSC 1.1 Text Profile/.ttml](https://www.w3.org/TR/ttml-imsc1.1/#text-profile) * [EBU STL](https://tech.ebu.ch/docs/tech/tech3264.pdf) * [SubRip/.srt](https://en.wikipedia.org/wiki/SubRip) * [WebVTT](https://www.w3.org/TR/webvtt1/) ### Output Formats * [SubRip/.srt](https://en.wikipedia.org/wiki/SubRip) * [IMSC 1.1 Text Profile/.ttml](https://www.w3.org/TR/ttml-imsc1.1/#text-profile) * [WebVTT](https://www.w3.org/TR/webvtt1/) ## Quick start To install the latest version of `ttconv`, including pre-releases: ```sh pip install --pre ttconv tt convert -i -o ``` ## Documentation ### Command line `tt convert [-h] -i INPUT -o OUTPUT [--itype ITYPE] [--otype OTYPE] [--config CONFIG] [--config_file CONFIG_FILE]` * `--itype`: `TTML` | `SCC` | `STL` | `SRT` (extrapolated from the filename, if omitted) * `--otype`: `TTML` | `SRT` | `VTT` (extrapolated from the filename, if omitted) * `--filter`: specifies by name a filter to be applied to the content * `--config` and `--config_file`: JSON dictionary where each property specifies (optional) configuration parameters for readers, writers and filters. Example: `tt convert -i <.scc file> -o <.ttml file> --itype SCC --otype TTML --filter lcd --config '{"general": {"progress_bar":false, "log_level":"WARN"}, "lcd": {"bg_color": "transparent", "color": "#FF0000"}}'` ### General configuration (`"general"`) #### progress_bar `"progress_bar": true | false` A progress bar is displayed if `progress_bar` is `true` and `log_level` is `"INFO"`. Default: `true` ### log_level `"log_level": "INFO" | "WARN" | "ERROR"` Logging verbosity Default: `"INFO"` ### document_lang `"document_lang": ` Overrides the top-level language of the input document. Example: `"document_lang": "es-419"` Default: `None` ### IMSC Writer configuration (`"imsc_writer"`) ### time_format `"time_format": "frames" | "clock_time" | "clock_time_with_frames"` Specifies whether the TTML time expressions are in frames (`f`), `HH:MM:SS.mmm` or `HH:MM:SS:FF` Default: `"frames"` if `"fps"` is specified, `"clock_time"` otherwise ### fps `"fps": "/"` Specifies the `ttp:frameRate` and `ttp:frameRateMultiplier` of the output document. Required when `time_format` is `frames` or `clock_time_with_frames`. No effect otherwise. Example: `--config '{"general": {"progress_bar":false, "log_level":"WARN"}, "imsc_writer": {"time_format":"clock_time_with_frames", "fps": "25/1"}}'` ### STL Reader configuration (`"stl_reader"`) #### disable_fill_line_gap `"disable_fill_line_gap" : true | false` `true` means that the STL reader does not fill gaps between lines Default: `false` #### disable_line_padding `"disable_line_padding" : true | false` `true` means that the STL reader does not add padding at the begining/end of lines Default: `false` #### program_start_tc `"program_start_tc" : "TCP" | "HH:MM:SS:FF"` Specifies a starting offset, either the TCP field of the GSI block or a user-specified timecode Default: `"00:00:00:00"` #### font_stack `"font_stack" : [](https://www.w3.org/TR/ttml2/#style-value-font-families)` Overrides the font stack Default: `"Verdana, Arial, Tiresias, sansSerif"` #### max_row_count `"max_row_count" : "MNR" | integer` Specifies a maximum number of rows for open subtitles, either the MNR field of the GSI block or a user-specified value Default: `23` ### SRT Writer configuration (`"srt_writer"`) #### text_formatting `"text_formatting" : true | false` `false` means that the SRT writer does not output any text formatting tags Default: `true` ### VTT Writer configuration (`"vtt_writer"`) #### line_position `"line_position" : true | false` `true` means that the VTT writer outputs line and line alignment cue settings Default: `false` #### text_align `"text_align" : true | false` `true` means that the VTT writer outputs text alignment cue settings Default: `false` #### cue_id `"cue_id" : true | false` `true` means that the VTT writer outputs cue identifiers Default: `true` ### SCC Reader configuration #### text_align `"text_align" : "auto" | "left" | "center" | "right"` Specifies the text alignment. `"auto"` means the reader will use heuristics to determine text alignment. Default: `"auto"` ### LCD filter configuration (`"lcd"`) #### Description The LCD filter merges regions and removes all text formatting with the exception of color and text alignment. #### safe_area `"safe_area" : ` Specifies the safe area (as a percentage of the height and width of the root container) Default: `10` #### color `"color" : | null` If not `null`, overrides text color. The syntax of `TTML color` is specified at . Default: `null` Examples: `"#FFFFFF"` (white), `"white"` #### bg_color `"bg_color" : ` If not `null`, overrides the background color. The syntax of `TTML color` is specified at . Default: `null` Examples: `"#FF0000"` (red), `"transparent"`, `"black"` #### preserve_text_align `"preserve_text_align" : true | false` If `true`, text alignment is preserved, otherwise text is centered. Default: `false` ## Library The overall architecture of the library is as follows: * Reader modules validate and convert input files into instances of the canonical model (see `ttconv.imsc.reader.to_model()` for example); * Filter modules transform instances of the canonical data model, e.g. all text styling and positioning might be removed from an instance of the canonical model to match the limited capabilities of downstream devices; and * Writer modules convert instances of the canonical data model into output files. Processing shared across multiple reader and writer modules is factored out in common modules whenever possible. For example, several output formats require an instance of the canonical data model to be transformed into a sequence of discrete temporal snapshots – a process called ISD generation. The library uses the Python `logging` module to report non-fatal events. Unit tests illustrate the use of the library, e.g. `ReaderWriterTest.test_imsc_1_test_suite` at `src/test/python/test_imsc_writer.py`. Detailed documentation including reference documents is under [`doc`](./doc). ## Dependencies ### Runtime * [python >= 3.7](https://python.org) ### Development The project uses [pipenv](https://pypi.org/project/pipenv/) to manage dependencies. * [pylint](https://pypi.org/project/pylint/) * [coverage](https://pypi.org/project/coverage/) ## Development ### Setup #### Local * run `pipenv install --dev` * set the `PYTHONPATH` environment variable to `src/main/python`, e.g. `export PYTHONPATH=src/main/python` * `pipenv run` can then be used #### Docker ```sh docker build --rm -f Dockerfile -t ttconv:latest . docker run -it --rm ttconv:latest bash ``` ### Example From the root directory of the project: ```sh mkdir build pipenv install --dev export PYTHONPATH=src/main/python python src/main/python/ttconv/tt.py convert -i src/test/resources/scc/mix-rows-roll-up.scc -o build/mix-rows-roll-up.ttml ``` ### Code coverage Unit test code coverage is provided by the script at `scripts/coverage.sh` ### Continuous integration #### Overview Automated testing is provided by the script at `scripts/ci.sh` #### Local Run `PYTHONPATH=src/main/python ./scripts/ci.sh` #### GitHub actions See `.github/workflows/main.yml` #### Docker Run `docker run -it --rm ttconv:latest /bin/sh scripts/ci.sh` ttconv-1.1.1/doc/000077500000000000000000000000001474066147500136125ustar00rootroot00000000000000ttconv-1.1.1/doc/data_model.md000066400000000000000000000071521474066147500162320ustar00rootroot00000000000000# Data model ## Overall The canonical model closely follows the [TTML 2](https://www.w3.org/TR/ttml2) data model, as constrained by the [IMSC 1.1 Text Profile](https://www.w3.org/TR/ttml-imsc1.1/#text-profile) specification. This includes both the static structure of the model as well as temporal, layout and style processing. The objective is for a valid IMSC 1.1 Text Profile document to be mapped into a canonical model instance such that presenting instance results in the same outout as the input IMSC document. The canonical model is specified in `ttconv.model`. The class hierarchy of the canonical model is summarized the following figure: ```txt ContentDocument : Region* Body? Body : Div* Div : (P | Div)* P : (Span | Ruby | Br)* Span : (Span | Br | Text)* Ruby : Rb? Rt? | Rb? Rp Rt? Rp | Rbc Rtc Rtc? Rbc : Rb* Rtc : Rt* | Rp Rt* Rp Rb, Rt, Rp : Span* ``` where: * the `ContentDocument` class corresponds to the `tt` element * the `Body`, `Div`, `P`, `Span`, `Br` and `Region` classes corresponds to the TTML content element of the same name, respectively * the `Text` class corresponds to a TTML text node * the `Ruby`, `Rt`, `Rb`, `Rtc`, `Rbc`, `Rp` classes correspond to the TTML `span` element with the computed value of `tts:ruby` attribute specified in the following table | Canonical model class | Computed value of `tts:ruby` attribute | |-----------------------|----------------------------------------| | `Ruby` | `container` | | `Rt` | `text` | | `Rb` | `base` | | `Rtc` | `textContainer` | | `Rbc` | `baseContainer` | | `Rp` | `delimiter` | ## Basic operation The canonical model allows content elements (instances of `ttconv.model.ContentElement`) to be arranged in a hierarchical structures (using the `ttconv.model.ContentElement.push_child()` and `ttconv.model.ContentElement.remove_child()`) that are associated with a single document (using the `ttconv.model.ContentElement.set_doc()` method with an instance of `ttconv.model.ContentDocument`). ## Divergences with the TTML data model ### Initial values The TTML `initial` elements are accessed using the `ContentDocument.set_initial_value()` and `ContentDocument.get_initial_value()` method. ### Styling Style properties are access using the `ContentElement.get_style()` and `ContentElement.set_style()` methods. The style properties themselves are defined in `ttconv.style_properties`, where the lower camel case names used in TTML are replaced by their equivalent in upper camel case. Deprecated style properties, e.g. `tts:zIndex` are not supported. Only _inline styling_ of content elements is supported, and neither _referential sytling_ nor _chained referential styling_ nor _nested styling_ are supported. ### Metadata TTML `metadata` elements are not supported. ### Timing Only _parallel time container_ semantics are supported and temporal offsets are expressed as `fractions.Fraction` instances in seconds. As a result, the following parameters are not supported: `ttp:frameRate`, `ttp:frameRateMultiplier`, `ttp:subFrameRate`, `ttp:tickRate`. Writer module can express temporal offsets in units of ticks, frames, etc. as demanded by the output format or configured by the user. The `dur` timing attribute is not supported. ### Lengths Extent, origin and position lengths can be expressed in `c`, `%`, `rh`, `rw` and `px` units. ttconv-1.1.1/doc/imsc_reader.md000066400000000000000000000037611474066147500164200ustar00rootroot00000000000000# IMSC Reader ## Overview The IMSC reader (`ttconv/imsc/reader.py`) converts [IMSC 1.1 Text Profile](https://www.w3.org/TR/ttml-imsc1.1/#text-profile) documents into the [data model](./data-model.md). The objective is to preserve rendering fidelity but not necessarily structure, e.g. referential styling is flattened. ## Usage The IMSC reader accepts as input an XML document that conforms to the [ElementTree XML API](https://docs.python.org/3.7/library/xml.etree.elementtree.html) and returns a `model.ContentDocument` object. ```python import xml.etree.ElementTree as et import ttconv.imsc.reader as imsc_reader xml_doc = et.parse('src/test/resources/ttml/imsc-tests/imsc1/ttml/timing/BasicTiming007.ttml') doc = imsc_reader.to_model(xml_doc) # doc can then manipulated and written out using any of the writer modules ``` ## Architecture The input XML document is traversed using depth-first search (DFS). Each XML element encountered is processed using the `from_xml()` method of the corresponding class in `ttconv/imsc/elements.py`. For example, `ttconv.imsc.elements.PElement.from_xml()` is applied to each `

` element. Since the data model is a subset of the IMSC 1.1 model, additional parsing state is preserved across calls to `from_xml()` by associating each parsed XML element in an instance of the `ttconv.imsc.elements.TTMLElement.ParsingContext` structure and its subclasses. To improve code manageability, processing of TTML style and other attributes is conducted in `ttconv/imsc/styles_properties.py` and `ttconv/imsc/attributes.py`, respectively. Each style property in `ttconv/imsc/styles_properties.py` is mapped, as specified by the `model_prop` member, to a style property of the data model in `ttconv/styles_properties.py`. `ttconv/imsc/namespaces.py` and `ttconv/imsc/utils.py` contain common namespace declarations and utility functions, respectively. ## Tests Unit tests include parsing into the data model all of the [IMSC test documents published by W3C](https://github.com/w3c/imsc-tests). ttconv-1.1.1/doc/isd.md000066400000000000000000000025531474066147500147200ustar00rootroot00000000000000# ISD An Intermediate Synchronic Document (ISD) represents a snapshot of a `ContentDocument` at specified moment in time. The ISD model is specified in `ttconv.isd`. The class hierarchy of the canonical model is summarized the following figure: ```txt ISD : ISD.Region* ISD.Region: : Body ``` where `Body` is an instance of the `Body` class of the data model. In other words, each region of an ISD contains a copy of all the elements of the source ContentDocument that are active within the region. For example, the ISD at t=2s of the document: ```xml ...

hello

``` is: ```xml

hello

``` An ISD contains no timing information, i.e. no `begin` or `end` properties, or animation steps. Both the `Origin` and `Position` style properties are always equal. All lengths are expressed in root-relative units `rh` and `rw`. ttconv-1.1.1/doc/references.md000066400000000000000000000063171474066147500162640ustar00rootroot00000000000000# References ## Documents | Document | Notes | |-------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------| | [SCC](https://docs.inqscribe.com/2.2/format_scc.html) | Specification of the SCC file format | | [RP 2052-10:2013 - SMPTE Recommended Practice - Conversion from CEA-608 Data to SMPTE-TT](https://ieeexplore.ieee.org/document/7289645) | Mapping of SCC into the data model | | [RP 2052-11:2013 - SMPTE Recommended Practice - Conversion from CEA-708 Caption Data to SMPTE-TT](https://ieeexplore.ieee.org/document/7290363) | Mapping of SCC into the data model | | [Line 21 Data Services (ANSI/CTA-608-E S-2019)](https://shop.cta.tech/products/line-21-data-services) | Mapping of SCC into the data model | | [ST 2052-1:2013 - SMPTE Standard - Timed Text Format (SMPTE-TT)](https://ieeexplore.ieee.org/document/7291854) | Mapping of SCC into the data model | | [ST 2052-1:2013 - SMPTE Standard - Timed Text Format (SMPTE-TT)](https://ieeexplore.ieee.org/document/7291854) | Mapping of SCC into the data model | | [IMSC 1.1](https://www.w3.org/TR/ttml-imsc1.1/) | Basis for the data model and specification for the IMSC format | | [TTML 2](https://www.w3.org/TR/ttml2/) | Parent of IMSC 1.1 | | [47 CFR 15.119](https://www.govinfo.gov/app/details/CFR-2010-title47-vol1/CFR-2010-title47-vol1-sec15-119) | Closed caption decoder requirements for analog television receivers | | [SCC](https://docs.inqscribe.com/2.2/format_scc.html) | Specification of the SCC file format | | [EBU Tech 3264](https://tech.ebu.ch/docs/tech/tech3264.pdf) | Specification of the EBU Subtitling data exchange format | | [EBU Tech 3360](https://tech.ebu.ch/docs/tech/tech3360.pdf) | EBU-TT, Part 2, Mapping EBU STL (TECH 3264) to EBU-TT subtitle files | ## Links * [W3C IMSC test content](https://github.com/w3c/imsc-tests) * [IMSC renderer](http://sandflow.com/imsc1proc/index.html) * [IMSC validator](https://apps.sandflow.com/imscV/) ttconv-1.1.1/doc/scc_reader.md000066400000000000000000000050321474066147500162260ustar00rootroot00000000000000# SCC Reader ## Overview The SCC reader (`ttconv/scc/reader.py`) converts [SCC](https://docs.inqscribe.com/2.2/format_scc.html) documents into the [data model](./data-model.md). ## Usage The SCC reader accepts as input a [Scenarist Closed Caption](https://www.govinfo.gov/content/pkg/CFR-2007-title47-vol1/pdf/CFR-2007-title47-vol1-sec15-119.pdf) document that conforms to the [CEA-608](https://shop.cta.tech/products/line-21-data-services) encoding specification and returns a `model.ContentDocument` object. ```python import ttconv.scc.reader as scc_reader doc = scc_reader.to_model("src/test/resources/scc/pop-on.scc") # doc can then manipulated and written out using any of the writer modules ``` ## Architecture The input SCC document is read line-by-line. For each line, the time code prefix and following CEA-608 codes (see the `ttconv/scc/codes` package) are processed to generate `SccCaptionParagraph` instances. Each paragraph associates a time and region with the text (including line-breaks) it contains (see definition in `ttconv/scc/content.py`). The paragraphs are then converted to a `model.P`, part of the output `model.ContentDocument` (see the `SccCaptionParagraph::to_paragraph()` method in `ttconv/scc/paragraph.py`), following the recommendations specified in [SMPTE RP 2052-10:2013](https://ieeexplore.ieee.org/document/7289645). The paragraph generation is based on the buffer-based mechanism defined in the CEA-608 format: a buffer of caption content is filled while some other content is displayed. These buffering and displaying processes can be synchronous or asynchronous, based on the caption style (see `ttconv/scc/style.py`). `ttconv/scc/utils.py` contains utility functions to convert geometrical dimensions of different units, and `ttconv/scc/disassembly.py` handles CEA-608 codes conversion to the _disassembly_ format. ## Disassembly The SCC reader can dump SCC content in the [Disassemby](http://www.theneitherworld.com/mcpoodle/SCC_TOOLS/DOCS/SCC_TOOLS.HTML#ccd) format, which is an ad-hoc a human-readable description of the SCC content. ```python import ttconv.scc.reader as scc_reader print(scc_reader.to_disassembly("src/test/resources/scc/pop-on.scc")) ``` For instance, the following SCC line: ``` 00:00:00:22 9425 9425 94ad 94ad 9470 9470 4c6f 7265 6d20 6970 7375 6d20 646f 6c6f 7220 7369 7420 616d 6574 2c80 ``` is converted to: ``` 00:00:00:22 {RU2}{RU2}{CR}{CR}{1500}{1500}Lorem ipsum dolor sit amet, ``` This is useful for debugging. ## Tests Sample SCC files can be found in the `src/test/resources/scc` directory. ttconv-1.1.1/doc/srt_writer.md000066400000000000000000000022061474066147500163400ustar00rootroot00000000000000# SRT Writer ## Overview The SRT writer (`ttconv/srt/writer.py`) converts a [data model](./data-model.md) document into the [SRT](https://en.wikipedia.org/wiki/SubRip#File_format) format. ## Usage The SRT writer takes a `model.ContentDocument` object as input, and returns an SRT document as string. ```python import ttconv.srt.writer as srt_writer # With 'doc' an instance of 'model.ContentDocument' print(srt_writer.from_model(doc)) ``` ## Architecture The input document is processed to extract a list of ISDs ([Intermediate Synchronic Document](./isd.md)), which are passed through filters (in `ttconv/filters`) to: * remove unsupported features * merge document elements * set default property values Once filtered, ISD elements are passed to the `SrtContext` to be converted into `SrtParagraph` instances defined in `ttconv/srt/paragraph.py`, including SRT supported styling (see `ttconv/srt/style.py`). The output document generation is completed after the call of the `SrtContext::finish()` method, which sets the last element assets. The resulting SRT document is gettable calling the overridden built-in `SrtContext::__str__()` function. ttconv-1.1.1/scripts/000077500000000000000000000000001474066147500145345ustar00rootroot00000000000000ttconv-1.1.1/scripts/ci.sh000077500000000000000000000006451474066147500154730ustar00rootroot00000000000000#!/bin/sh # Exit immediately if unit tests exit with a non-zero status. set -e ## Linter pipenv run python -m pylint --exit-zero src/main/python/ttconv/ src/test/python/ ## unit test and coverage pipenv run coverage run -m unittest discover -v -s src/test/python/ -t . pipenv run coverage report | awk '!/-|(Name)/ {if (int($NF) < 80) {print $1 " has less than 80% coverage"; flag=2;}}; END { if (flag) exit(flag)}' ttconv-1.1.1/scripts/coverage.sh000077500000000000000000000001551474066147500166670ustar00rootroot00000000000000#!/bin/sh pipenv run coverage run -m unittest discover -v -s src/test/python/ -t . pipenv run coverage html ttconv-1.1.1/scripts/linter.sh000066400000000000000000000001341474066147500163630ustar00rootroot00000000000000#!/bin/sh pipenv run python -m pylint --exit-zero src/main/python/ttconv/ src/test/python/ ttconv-1.1.1/scripts/unit_test.sh000066400000000000000000000001161474066147500171040ustar00rootroot00000000000000#!/bin/sh pipenv run python -m unittest discover -v -s src/test/python/ -t . ttconv-1.1.1/setup.py000066400000000000000000000027371474066147500145700ustar00rootroot00000000000000"""A setuptools based setup module. """ import pathlib from setuptools import setup, find_packages here = pathlib.Path(__file__).parent.resolve() long_description = (here / 'README.md').read_text(encoding='utf-8') setup( name='ttconv', version='1.1.1', description='Library for conversion of common timed text formats', long_description=long_description, long_description_content_type='text/markdown', url='https://github.com/sandflow/ttconv', author='Sandflow Consulting LLC', author_email='info@sandflow.com', classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'Topic :: Software Development :: Build Tools', 'Environment :: Console', 'License :: OSI Approved :: BSD License', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3 :: Only', 'Topic :: Multimedia' ], keywords='ttml, timed text, caption, subtitle, imsc, scc, srt, stl, smpte-tt, conversion, vtt, webvtt, 608', package_dir={'ttconv': 'src/main/python/ttconv'}, packages=find_packages(where='src/main/python'), python_requires='>=3.7, <4', project_urls={ 'Bug Reports': 'https://github.com/sandflow/ttconv/issues', 'Source': 'https://github.com/sandflow/ttconv', }, entry_points={ "console_scripts": [ "tt = ttconv.tt:main" ] }, ) ttconv-1.1.1/src/000077500000000000000000000000001474066147500136345ustar00rootroot00000000000000ttconv-1.1.1/src/main/000077500000000000000000000000001474066147500145605ustar00rootroot00000000000000ttconv-1.1.1/src/main/python/000077500000000000000000000000001474066147500161015ustar00rootroot00000000000000ttconv-1.1.1/src/main/python/ttconv/000077500000000000000000000000001474066147500174165ustar00rootroot00000000000000ttconv-1.1.1/src/main/python/ttconv/__init__.py000066400000000000000000000000001474066147500215150ustar00rootroot00000000000000ttconv-1.1.1/src/main/python/ttconv/config.py000066400000000000000000000063201474066147500212360ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: UTF-8 -*- # Copyright (c) 2020, Sandflow Consulting LLC # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """TT configuration""" from __future__ import annotations import dataclasses from dataclasses import dataclass from typing import Optional, Dict, List, Any class ModuleConfiguration: """Base class for module configurations""" @classmethod def get_fields(cls) -> List[dataclasses.Field]: """Returns data class fields""" return list(dataclasses.fields(cls)) @classmethod def validate(cls, config_dict: Dict): """Validates configuration dictionary""" for field in cls.get_fields(): optional_field = "Optional" in field.type or cls.get_field_default(field) is not None config_value = config_dict.get(field.name) if not optional_field and config_value is None: raise ValueError("Compulsory configuration field missing:", field.name) @classmethod def parse(cls, config_dict: Dict) -> ModuleConfiguration: """Parses configuration dictionary""" cls.validate(config_dict) kwargs = {} for field in cls.get_fields(): field_value = config_dict.get(field.name, cls.get_field_default(field)) decoder = field.metadata.get("decoder") if decoder is not None: field_value = decoder.__call__(field_value) kwargs[field.name] = field_value instance = cls(**kwargs) return instance @staticmethod def get_field_default(field: dataclasses.Field) -> Optional[Any]: """Returns the default field value if any, None otherwise""" if isinstance(field.default, dataclasses._MISSING_TYPE): return None return field.default @classmethod def name(cls): """Returns the configuration name""" raise NotImplementedError @dataclass class GeneralConfiguration(ModuleConfiguration): """TT general configuration""" log_level: Optional[str] = "INFO" progress_bar: Optional[bool] = True document_lang: Optional[str] = None @classmethod def name(cls): return "general" ttconv-1.1.1/src/main/python/ttconv/filters/000077500000000000000000000000001474066147500210665ustar00rootroot00000000000000ttconv-1.1.1/src/main/python/ttconv/filters/__init__.py000066400000000000000000000000001474066147500231650ustar00rootroot00000000000000ttconv-1.1.1/src/main/python/ttconv/filters/doc/000077500000000000000000000000001474066147500216335ustar00rootroot00000000000000ttconv-1.1.1/src/main/python/ttconv/filters/doc/__init__.py000066400000000000000000000032231474066147500237440ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: UTF-8 -*- # Copyright (c) 2023, Sandflow Consulting LLC # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Collects document instance filters""" import importlib import pkgutil import os.path import sys # registers all document instance filters for importer, package_name, _ in pkgutil.iter_modules([os.path.dirname(__file__)]): full_name = f"{__name__}.{package_name}" importlib.import_module(full_name) ttconv-1.1.1/src/main/python/ttconv/filters/doc/lcd.py000066400000000000000000000237251474066147500227600ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: UTF-8 -*- # Copyright (c) 2023, Sandflow Consulting LLC # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Defines the Least common denominator (LCD) filter.""" from __future__ import annotations import logging import typing from dataclasses import dataclass, field from numbers import Number from ttconv.config import ModuleConfiguration from ttconv.filters.document_filter import DocumentFilter from ttconv.filters.remove_animations import RemoveAnimationFilter from ttconv.filters.supported_style_properties import SupportedStylePropertiesFilter from ttconv.isd import StyleProcessors from ttconv.model import ContentDocument, ContentElement, Region, P from ttconv.style_properties import TextAlignType, ColorType, CoordinateType, DisplayAlignType, ExtentType, LengthType, StyleProperties, WritingModeType, NamedColors import ttconv.utils LOGGER = logging.getLogger(__name__) def _replace_regions(element: ContentElement, region_aliases: typing.Mapping[Region, Region]): merged_region = region_aliases.get(element.get_region()) if merged_region is not None: element.set_region(merged_region) for child in element: _replace_regions(child, region_aliases) def _apply_bg_color(element: ContentElement, bg_color: ColorType): if isinstance(element, P): element.set_style(StyleProperties.BackgroundColor, bg_color) else: for child in element: _apply_bg_color(child, bg_color) def _safe_area_decoder(s: Number) -> int: safe_area = int(s) if 30 < safe_area < 0: raise ValueError("Safe area must be an integer between 0 and 30") return safe_area def _color_decoder(s: typing.Optional[ColorType]) -> typing.Optional[ColorType]: if s is None: return None if not isinstance(s, str): raise ValueError("Color specification must be a string") return ttconv.utils.parse_color(s) @dataclass class LCDDocFilterConfig(ModuleConfiguration): """Configuration class for the Least common denominator (LCD) filter""" @classmethod def name(cls): return "lcd" # specifies the safe area as an integer percentage safe_area: typing.Optional[int] = field(default=10, metadata={"decoder": _safe_area_decoder}) # preserve text alignment preserve_text_align: typing.Optional[bool] = field(default=False, metadata={"decoder": bool}) # overrides the text color color: typing.Optional[ColorType] = field(default=None, metadata={"decoder": _color_decoder}) # overrides the background color bg_color: typing.Optional[ColorType] = field(default=None, metadata={"decoder": _color_decoder}) class LCDDocFilter(DocumentFilter): """Merges regions and removes all text formatting with the exception of color and text alignment.""" @classmethod def get_config_class(cls) -> ModuleConfiguration: return LCDDocFilterConfig def __init__(self, config: LCDDocFilterConfig): super().__init__(config) def process(self, doc: ContentDocument) -> ContentDocument: # clean-up styles supported_styles = { StyleProperties.DisplayAlign: [], StyleProperties.Extent: [], StyleProperties.Origin: [], StyleProperties.Position: [] } if self.config.preserve_text_align: supported_styles.update({StyleProperties.TextAlign: []}) if self.config.color is None: supported_styles.update({StyleProperties.Color: []}) if self.config.bg_color is None: supported_styles.update({StyleProperties.BackgroundColor: []}) style_filter = SupportedStylePropertiesFilter(supported_styles) style_filter.process_initial_values(doc) if doc.get_body() is not None: style_filter.process_element(doc.get_body()) # clean-up animations animation_filter = RemoveAnimationFilter() if doc.get_body() is not None: animation_filter.process_element(doc.get_body()) # clean-up regions initial_extent = doc.get_initial_value(StyleProperties.Extent) initial_origin = doc.get_initial_value(StyleProperties.Origin) initial_writing_mode = doc.get_initial_value(StyleProperties.WritingMode) initial_display_align = doc.get_initial_value(StyleProperties.DisplayAlign) retained_regions = dict() replaced_regions = dict() for region in doc.iter_regions(): # cleanup animations animation_filter.process_element(region) # cleanup styles style_filter.process_element(region) # compute origin if (region.get_style(StyleProperties.Origin)) is not None: StyleProcessors.Origin.compute(None, region) if (region.get_style(StyleProperties.Position)) is not None: StyleProcessors.Position.compute(None, region) region.set_style(StyleProperties.Position, None) if region.get_style(StyleProperties.Origin) is None: region.set_style(StyleProperties.Origin, initial_origin if initial_origin is not None \ else StyleProperties.Origin.make_initial_value()) # compute extent if (region.get_style(StyleProperties.Extent)) is not None: StyleProcessors.Extent.compute(None, region) if region.get_style(StyleProperties.Extent) is None: region.set_style(StyleProperties.Extent, initial_extent if initial_extent is not None \ else StyleProperties.Extent.make_initial_value() ) # computer writing_mode and display_align writing_mode = region.get_style(StyleProperties.WritingMode) if writing_mode is None: writing_mode = initial_writing_mode if initial_writing_mode is not None \ else StyleProperties.WritingMode.make_initial_value() display_align = region.get_style(StyleProperties.DisplayAlign) if display_align is None: display_align = initial_display_align if initial_display_align is not None \ else StyleProperties.DisplayAlign.make_initial_value() # determine new displayAlign value new_display_align = DisplayAlignType.after if writing_mode in (WritingModeType.lrtb, WritingModeType.rltb): if display_align == DisplayAlignType.before and region.get_style(StyleProperties.Origin).y.value < 50: new_display_align = DisplayAlignType.before elif region.get_style(StyleProperties.Origin).y.value + region.get_style(StyleProperties.Extent).height.value < 50: new_display_align = DisplayAlignType.before elif writing_mode == WritingModeType.tblr: if display_align == DisplayAlignType.before and region.get_style(StyleProperties.Origin).x.value < 50: new_display_align = DisplayAlignType.before elif region.get_style(StyleProperties.Origin).x.value + region.get_style(StyleProperties.Extent).width.value < 50: new_display_align = DisplayAlignType.before else: # writing_mode == WritingModeType.tbrl if display_align == DisplayAlignType.before and region.get_style(StyleProperties.Origin).x.value >= 50: new_display_align = DisplayAlignType.before elif region.get_style(StyleProperties.Origin).x.value + region.get_style(StyleProperties.Extent).width.value >= 50: new_display_align = DisplayAlignType.before region.set_style(StyleProperties.DisplayAlign, new_display_align) # reposition region region.set_style( StyleProperties.Origin, CoordinateType( x=LengthType(self.config.safe_area, LengthType.Units.pct), y=LengthType(self.config.safe_area, LengthType.Units.pct) ) ) region.set_style( StyleProperties.Extent, ExtentType( height=LengthType(value=100 - 2 * self.config.safe_area, units=LengthType.Units.pct), width=LengthType(value=100 - 2 * self.config.safe_area, units=LengthType.Units.pct) ) ) # check if a similar region has already been processed fingerprint = ( region.get_begin() or 0, region.get_end() or None, writing_mode, new_display_align ) retained_region = retained_regions.get(fingerprint) if retained_region is None: retained_regions[fingerprint] = region else: replaced_regions[region] = retained_region # prune aliased regions if doc.get_body() is not None: _replace_regions(doc.get_body(), replaced_regions) for region in list(doc.iter_regions()): if region in replaced_regions: doc.remove_region(region.get_id()) # apply background color if self.config.bg_color is not None: _apply_bg_color(doc.get_body(), self.config.bg_color) # apply text color if doc.get_body() is not None and self.config.color is not None: doc.get_body().set_style(StyleProperties.Color, self.config.color) # apply text align if doc.get_body() is not None and not self.config.preserve_text_align: doc.get_body().set_style(StyleProperties.TextAlign, TextAlignType.center) ttconv-1.1.1/src/main/python/ttconv/filters/document_filter.py000066400000000000000000000044411474066147500246260ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: UTF-8 -*- # Copyright (c) 2023, Sandflow Consulting LLC # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Data model filter""" from __future__ import annotations from typing import Optional from ttconv.model import ContentDocument from ttconv.config import ModuleConfiguration class DocumentFilter: """Abstract base class for content document filters""" _all_filters = dict() def __init__(self, config: ModuleConfiguration) -> None: self.config = config def process(self, doc: ContentDocument): """Processes the specified document in place.""" raise NotImplementedError @classmethod def get_config_class(cls) -> ModuleConfiguration: """Returns the configuration class for the filter.""" raise NotImplementedError @classmethod def get_filter_by_name(cls, name) -> Optional[DocumentFilter]: """Returns a list of all document filters""" return DocumentFilter._all_filters.get(name) def __init_subclass__(cls): DocumentFilter._all_filters[cls.get_config_class().name()] = cls from ttconv.filters.doc import * ttconv-1.1.1/src/main/python/ttconv/filters/isd/000077500000000000000000000000001474066147500216455ustar00rootroot00000000000000ttconv-1.1.1/src/main/python/ttconv/filters/isd/__init__.py000066400000000000000000000025751474066147500237670ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: UTF-8 -*- # Copyright (c) 2020, Sandflow Consulting LLC # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Collects ISD filters""" ttconv-1.1.1/src/main/python/ttconv/filters/isd/default_style_properties.py000066400000000000000000000061641474066147500273460ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: UTF-8 -*- # Copyright (c) 2020, Sandflow Consulting LLC # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Style properties default values filter""" import logging from typing import Dict, Type, Any from ttconv.filters.isd_filter import ISDFilter from ttconv.isd import ISD from ttconv.model import ContentElement from ttconv.style_properties import StyleProperty LOGGER = logging.getLogger(__name__) class DefaultStylePropertyValuesISDFilter(ISDFilter): """Filter that remove default style properties""" def __init__(self, style_property_default_values: Dict[Type[StyleProperty], Any]): self.style_property_default_values = style_property_default_values def _process_element(self, element: ContentElement): """Filter ISD element style properties""" element_styles = list(element.iter_styles()) for style_prop in element_styles: value = element.get_style(style_prop) default_value = self.style_property_default_values.get(style_prop) parent = element.parent() if parent is not None and style_prop.is_inherited: # If the parent style property value has not been removed, it means # the value is not set to default, and so that the child style property # value may have been "forced" to the default value, so let's skip it. parent_value = parent.get_style(style_prop) if parent_value is not None and parent_value is not value: continue # Remove the style property if its value is default (and if it is not inherited) if default_value is not None and value == default_value: element.set_style(style_prop, None) for child in element: self._process_element(child) def process(self, isd: ISD): """Filter ISD document style properties""" LOGGER.debug("Apply default style properties filter to ISD.") for region in isd.iter_regions(): self._process_element(region) ttconv-1.1.1/src/main/python/ttconv/filters/isd/merge_paragraphs.py000066400000000000000000000060601474066147500255300ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: UTF-8 -*- # Copyright (c) 2020, Sandflow Consulting LLC # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Paragraphs merging filter""" import logging from ttconv.filters.isd_filter import ISDFilter from ttconv.isd import ISD from ttconv.model import Div, P, Br, ContentElement LOGGER = logging.getLogger(__name__) class ParagraphsMergingISDFilter(ISDFilter): """Filter for merging ISD document paragraphs per region into a single paragraph""" def _get_paragraphs(self, element: ContentElement): """Retrieves child paragraphs""" paragraphs = [] for child in element: if isinstance(child, Div): paragraphs = paragraphs + self._get_paragraphs(child) elif isinstance(child, P): paragraphs.append(child) return paragraphs def process(self, isd: ISD): """Merges the ISD document paragraphs for each regions""" LOGGER.debug("Apply paragraphs merging filter to ISD.") for region in isd.iter_regions(): for body in region: target_div = Div(isd) target_paragraph = P(isd) target_div.push_child(target_paragraph) original_divs = list(body) paragraphs = [] for div in original_divs: paragraphs += self._get_paragraphs(div) if len(paragraphs) <= 1: continue LOGGER.warning("Merging ISD paragraphs.") for div in original_divs: div.remove() for (index, p) in enumerate(paragraphs): for span in list(p): # Remove child from its parent body span.remove() # Add it to the target paragraph target_paragraph.push_child(span) # Separate each merged paragraph by a Br element if index < len(paragraphs) - 1: target_paragraph.push_child(Br(isd)) body.push_child(target_div) ttconv-1.1.1/src/main/python/ttconv/filters/isd/merge_regions.py000066400000000000000000000050611474066147500250460ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: UTF-8 -*- # Copyright (c) 2020, Sandflow Consulting LLC # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Regions merging filter""" import logging from ttconv.filters.isd_filter import ISDFilter from ttconv.isd import ISD from ttconv.model import Body LOGGER = logging.getLogger(__name__) class RegionsMergingISDFilter(ISDFilter): """Filter for merging ISD document regions into a single region""" def process(self, isd: ISD): """Merges the ISD document regions""" LOGGER.debug("Apply regions merging filter to ISD.") original_regions = list(isd.iter_regions()) not_empty_regions = 0 for region in original_regions: not_empty_regions += len(region) if len(original_regions) <= 1 or not_empty_regions <= 1: return LOGGER.warning("Merging ISD regions.") target_body = Body(isd) region_ids = [] for region in original_regions: region_id = region.get_id() for body in region: for child in body: # Remove child from its parent body child.remove() # Add it to the target body target_body.push_child(child) region_ids.append(region_id) isd.remove_region(region_id) target_region = ISD.Region("_".join(region_ids), isd) target_region.push_child(target_body) isd.put_region(target_region) ttconv-1.1.1/src/main/python/ttconv/filters/isd/supported_style_properties.py000066400000000000000000000043411474066147500277420ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: UTF-8 -*- # Copyright (c) 2020, Sandflow Consulting LLC # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Filter for style properties supported by the output""" import logging from typing import Dict, List, Type from ttconv.filters.isd_filter import ISDFilter from ttconv.isd import ISD from ttconv.model import ContentElement from ttconv.style_properties import StyleProperty import ttconv.filters.supported_style_properties LOGGER = logging.getLogger(__name__) class SupportedStylePropertiesISDFilter(ISDFilter): """Filter that remove unsupported style properties""" def __init__(self, supported_style_properties: Dict[Type[StyleProperty], List]): self.filter = ttconv.filters.supported_style_properties.SupportedStylePropertiesFilter(supported_style_properties) def process(self, isd: ISD): """Filter ISD document style properties""" LOGGER.debug("Filter default style properties from ISD.") for region in isd.iter_regions(): self.filter.process_element(region) ttconv-1.1.1/src/main/python/ttconv/filters/isd_filter.py000066400000000000000000000030771474066147500235730ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: UTF-8 -*- # Copyright (c) 2020, Sandflow Consulting LLC # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Data model filter""" from ttconv.isd import ISD class ISDFilter: """Abstract base class for filters""" def process(self, isd: ISD): """Process the specified ISD and returns it.""" raise NotImplementedError ttconv-1.1.1/src/main/python/ttconv/filters/remove_animations.py000066400000000000000000000041271474066147500251630ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: UTF-8 -*- # Copyright (c) 2020, Sandflow Consulting LLC # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Filter that remove animations""" import logging from typing import Dict, List, Type from ttconv.model import ContentDocument, ContentElement from ttconv.style_properties import StyleProperty class RemoveAnimationFilter: """Filter that remove animations""" def __init__(self) -> None: self._has_removed_animations = False def has_removed_animations(self) -> bool: return self._has_removed_animations def process_element(self, element: ContentElement, recursive = True): """Removes animations from content elements""" for step in element.iter_animation_steps(): element.remove_animation_step(step) self._has_removed_animations = True if recursive: for child in element: self.process_element(child)ttconv-1.1.1/src/main/python/ttconv/filters/supported_style_properties.py000066400000000000000000000054421474066147500271660ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: UTF-8 -*- # Copyright (c) 2020, Sandflow Consulting LLC # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Filters style properties""" import logging from typing import Dict, List, Type from ttconv.model import ContentDocument, ContentElement from ttconv.style_properties import StyleProperty class SupportedStylePropertiesFilter: """Filter that removes unsupported style properties""" def __init__(self, supported_style_properties: Dict[Type[StyleProperty], List]): self.supported_style_properties = supported_style_properties def process_initial_values(self, doc: ContentDocument): """Removes initial values that target unsupported style properties""" for style_prop, value in list(doc.iter_initial_values()): if style_prop in self.supported_style_properties: supported_values = self.supported_style_properties[style_prop] if len(supported_values) == 0 or value in supported_values: continue doc.put_initial_value(style_prop, None) def process_element(self, element: ContentElement, recursive = True): """Removes unsupported style properties from content elements""" for style_prop in list(element.iter_styles()): if style_prop in self.supported_style_properties: value = element.get_style(style_prop) supported_values = self.supported_style_properties[style_prop] if len(supported_values) == 0 or value in supported_values: continue element.set_style(style_prop, None) if recursive: for child in element: self.process_element(child)ttconv-1.1.1/src/main/python/ttconv/imsc/000077500000000000000000000000001474066147500203515ustar00rootroot00000000000000ttconv-1.1.1/src/main/python/ttconv/imsc/__init__.py000066400000000000000000000000001474066147500224500ustar00rootroot00000000000000ttconv-1.1.1/src/main/python/ttconv/imsc/attributes.py000066400000000000000000000317011474066147500231130ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: UTF-8 -*- # Copyright (c) 2020, Sandflow Consulting LLC # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. '''Process IMSC non-style attributes''' import re import logging import math from fractions import Fraction import typing from dataclasses import dataclass from enum import Enum from ttconv.time_code import SmpteTimeCode import ttconv.model as model import ttconv.imsc.utils as utils import ttconv.imsc.namespaces as ns from ttconv.time_code import ClockTime LOGGER = logging.getLogger(__name__) class XMLIDAttribute: '''xml:id attribute ''' qn = f'{{{ns.XML}}}id' @staticmethod def extract(ttml_element): return ttml_element.attrib.get(XMLIDAttribute.qn) @staticmethod def set(xml_element, model_value): xml_element.set(XMLIDAttribute.qn, model_value) class XMLLangAttribute: '''xml:lang attribute ''' qn = f'{{{ns.XML}}}lang' @staticmethod def extract(ttml_element): return ttml_element.attrib.get(XMLLangAttribute.qn) @staticmethod def set(ttml_element, lang): ttml_element.set(XMLLangAttribute.qn, lang) class XMLSpaceAttribute: '''xml:space attribute ''' qn = f'{{{ns.XML}}}space' @staticmethod def extract(ttml_element): value = ttml_element.attrib.get(XMLSpaceAttribute.qn) r = None if value is not None: try: r = model.WhiteSpaceHandling(value) except ValueError: LOGGER.error("Bad xml:space value (%s)", value) return r @staticmethod def set(ttml_element, xml_space: model.WhiteSpaceHandling): ttml_element.set(XMLSpaceAttribute.qn, xml_space.value) class RegionAttribute: '''TTML region attribute''' qn = "region" @staticmethod def extract(ttml_element) -> typing.Optional[str]: return ttml_element.attrib.get(RegionAttribute.qn) @staticmethod def set(ttml_element, region_id: str): ttml_element.set(RegionAttribute.qn, region_id) class CellResolutionAttribute: '''ttp:cellResolution attribute ''' qn = f"{{{ns.TTP}}}cellResolution" _CELL_RESOLUTION_RE = re.compile(r"(\d+) (\d+)") @staticmethod def extract(ttml_element) -> model.CellResolutionType: cr = ttml_element.attrib.get(CellResolutionAttribute.qn) if cr is not None: m = CellResolutionAttribute._CELL_RESOLUTION_RE.match(cr) if m is not None: return model.CellResolutionType(columns=int(m.group(1)), rows=int(m.group(2))) LOGGER.error("ttp:cellResolution invalid syntax") # default value in TTML return model.CellResolutionType(rows=15, columns=32) @staticmethod def set(ttml_element, res): ttml_element.set(CellResolutionAttribute.qn, f"{res.columns} {res.rows}") class ExtentAttribute: '''ttp:extent attribute on \\ ''' qn = f"{{{ns.TTS}}}extent" @staticmethod def extract(ttml_element) -> typing.Optional[model.PixelResolutionType]: extent = ttml_element.attrib.get(ExtentAttribute.qn) if extent is not None: s = extent.split(" ") (w, w_units) = utils.parse_length(s[0]) (h, h_units) = utils.parse_length(s[1]) if w_units != "px" or h_units != "px": LOGGER.error("ttp:extent on does not use px units") return None if not w.is_integer() or not h.is_integer(): LOGGER.error("Pixel resolution dimensions must be integer values") return model.PixelResolutionType(int(w), int(h)) return None @staticmethod def set(ttml_element, res): ttml_element.set(ExtentAttribute.qn, f"{res.width:g}px {res.height:g}px") class ActiveAreaAttribute: '''ittp:activeArea attribute on \\ ''' qn = f"{{{ns.ITTP}}}activeArea" @staticmethod def extract(ttml_element) -> typing.Optional[model.ActiveAreaType]: aa = ttml_element.attrib.get(ActiveAreaAttribute.qn) if aa is not None: s = aa.split(" ") if len(s) != 4: LOGGER.error("Syntax error in ittp:activeArea on ") return None (left_offset, left_offset_units) = utils.parse_length(s[0]) (top_offset, top_offset_units) = utils.parse_length(s[1]) (w, w_units) = utils.parse_length(s[2]) (h, h_units) = utils.parse_length(s[3]) if w_units != "%" or h_units != "%" or left_offset_units != "%" or top_offset_units != "%": LOGGER.error("ittp:activeArea on must use % units") return None return model.ActiveAreaType( left_offset / 100, top_offset / 100, w / 100, h / 100 ) return None @staticmethod def set(ttml_element, active_area): ttml_element.set( ActiveAreaAttribute.qn, f"{active_area.left_offset * 100:g}% " f"{active_area.top_offset * 100:g}% " f"{active_area.width * 100:g}% " f"{active_area.height * 100:g}%" ) class TickRateAttribute: '''ttp:tickRate attribute ''' qn = f"{{{ns.TTP}}}tickRate" _TICK_RATE_RE = re.compile(r"(\d+)") @staticmethod def extract(ttml_element) -> int: tr = ttml_element.attrib.get(TickRateAttribute.qn) if tr is not None: m = TickRateAttribute._TICK_RATE_RE.match(tr) if m is not None: return int(m.group(1)) LOGGER.error("ttp:tickRate invalid syntax") # default value return 1 class AspectRatioAttribute: '''ittp:aspectRatio attribute ''' qn = f"{{{ns.ITTP}}}aspectRatio" _re = re.compile(r"(\d+) (\d+)") @staticmethod def extract(ttml_element) -> typing.Optional[Fraction]: ar_raw = ttml_element.attrib.get(AspectRatioAttribute.qn) if ar_raw is None: return None m = AspectRatioAttribute._re.match(ar_raw) if m is None: LOGGER.error("ittp:aspectRatio invalid syntax") return None try: return Fraction(int(m.group(1)), int(m.group(2))) except ZeroDivisionError: LOGGER.error("ittp:aspectRatio denominator is 0") return None class DisplayAspectRatioAttribute: '''ttp:displayAspectRatio attribute ''' qn = f"{{{ns.TTP}}}displayAspectRatio" _re = re.compile(r"(\d+) (\d+)") @staticmethod def extract(ttml_element) -> typing.Optional[Fraction]: ar_raw = ttml_element.attrib.get(DisplayAspectRatioAttribute.qn) if ar_raw is None: return None m = DisplayAspectRatioAttribute._re.match(ar_raw) if m is None: LOGGER.error("ttp:displayAspectRatio invalid syntax") return None try: return Fraction(int(m.group(1)), int(m.group(2))) except ZeroDivisionError: LOGGER.error("ttp:displayAspectRatio denominator is 0") return None @staticmethod def set(ttml_element, display_aspect_ratio: Fraction): ttml_element.set( DisplayAspectRatioAttribute.qn, f"{display_aspect_ratio.numerator:g} {display_aspect_ratio.denominator:g}" ) class FrameRateAttribute: '''ttp:frameRate and ttp:frameRateMultiplier attribute ''' frame_rate_qn = f"{{{ns.TTP}}}frameRate" frame_rate_multiplier_qn = f"{{{ns.TTP}}}frameRateMultiplier" _FRAME_RATE_RE = re.compile(r"(\d+)") _FRAME_RATE_MULT_RE = re.compile(r"(\d+) (\d+)") @staticmethod def extract(ttml_element) -> Fraction: # process ttp:frameRate fr = Fraction(30, 1) fr_raw = ttml_element.attrib.get(FrameRateAttribute.frame_rate_qn) if fr_raw is not None: m = FrameRateAttribute._FRAME_RATE_RE.match(fr_raw) if m is not None: fr = Fraction(m.group(1)) else: LOGGER.error("ttp:frameRate invalid syntax") # process ttp:frameRateMultiplier frm = Fraction(1, 1) frm_raw = ttml_element.attrib.get(FrameRateAttribute.frame_rate_multiplier_qn) if frm_raw is not None: m = FrameRateAttribute._FRAME_RATE_MULT_RE.match(frm_raw) if m is not None: frm = Fraction(int(m.group(1)), int(m.group(2))) else: LOGGER.error("ttp:frameRateMultiplier invalid syntax") return fr * frm @staticmethod def set(ttml_element, frame_rate: Fraction): rounded_fps = round(frame_rate) ttml_element.set( FrameRateAttribute.frame_rate_qn, str(rounded_fps) ) fps_multiplier = frame_rate / rounded_fps if fps_multiplier != 1: ttml_element.set( FrameRateAttribute.frame_rate_multiplier_qn, f"{fps_multiplier.numerator:g} {fps_multiplier.denominator:g}" ) @dataclass class TemporalAttributeParsingContext: frame_rate: Fraction = Fraction(30, 1) tick_rate: int = 1 class TimeExpressionSyntaxEnum(Enum): """IMSC time expression configuration values""" frames = "frames" clock_time = "clock_time" clock_time_with_frames = "clock_time_with_frames" @dataclass class TemporalAttributeWritingContext: frame_rate: typing.Optional[Fraction] = None time_expression_syntax: TimeExpressionSyntaxEnum = TimeExpressionSyntaxEnum.clock_time def to_time_format(context: TemporalAttributeWritingContext, time: Fraction) -> str: if context.time_expression_syntax is TimeExpressionSyntaxEnum.clock_time or context.frame_rate is None: return str(ClockTime.from_seconds(time)) if context.time_expression_syntax is TimeExpressionSyntaxEnum.frames: return f"{math.ceil(time * context.frame_rate)}f" return f"{SmpteTimeCode.from_seconds(time, context.frame_rate)}" class BeginAttribute: '''begin attribute ''' qn = "begin" @staticmethod def extract(context: TemporalAttributeParsingContext, xml_element) -> typing.Optional[Fraction]: # read begin attribute begin_raw = xml_element.attrib.get(BeginAttribute.qn) try: return utils.parse_time_expression(context.tick_rate, context.frame_rate, begin_raw) if begin_raw is not None else None except ValueError: LOGGER.error("bad begin value") return None @staticmethod def set(context: TemporalAttributeWritingContext, ttml_element, begin:Fraction): value = to_time_format(context, begin) ttml_element.set(BeginAttribute.qn, value) class EndAttribute: '''end attributes ''' qn = "end" @staticmethod def extract(context: TemporalAttributeParsingContext, xml_element) -> typing.Optional[Fraction]: # read end attribute end_raw = xml_element.attrib.get(EndAttribute.qn) try: return utils.parse_time_expression(context.tick_rate, context.frame_rate, end_raw) if end_raw is not None else None except ValueError: LOGGER.error("bad end value") return None @staticmethod def set(context: TemporalAttributeWritingContext, ttml_element, end:Fraction): value = to_time_format(context, end) ttml_element.set(EndAttribute.qn, value) class DurAttribute: '''dur attributes ''' qn = "dur" @staticmethod def extract(context: TemporalAttributeParsingContext, xml_element) -> typing.Optional[Fraction]: dur_raw = xml_element.attrib.get(DurAttribute.qn) try: return utils.parse_time_expression(context.tick_rate, context.frame_rate, dur_raw) if dur_raw is not None else None except ValueError: LOGGER.error("bad dur value") return None @staticmethod def set(ttml_element, dur): raise NotImplementedError class TimeContainer(Enum): par = "par" seq = "seq" def is_seq(self) -> bool: return self == TimeContainer.seq def is_par(self) -> bool: return self == TimeContainer.par class TimeContainerAttribute: '''timeContainer attributes ''' qn = "timeContainer" @staticmethod def extract(xml_elem) -> TimeContainer: time_container_raw = xml_elem.attrib.get(TimeContainerAttribute.qn) try: return TimeContainer(time_container_raw) if time_container_raw is not None else TimeContainer.par except ValueError: LOGGER.error("bad timeContainer value") return TimeContainer.par class StyleAttribute: '''style attribute ''' qn = "style" @staticmethod def extract(xml_element) -> typing.List[str]: raw_value = xml_element.attrib.get(StyleAttribute.qn) return raw_value.split(" ") if raw_value is not None else [] ttconv-1.1.1/src/main/python/ttconv/imsc/config.py000066400000000000000000000052531474066147500221750ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: UTF-8 -*- # Copyright (c) 2020, Sandflow Consulting LLC # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """IMSC configuration""" from __future__ import annotations from dataclasses import dataclass, field from fractions import Fraction from typing import Optional from ttconv.config import ModuleConfiguration from ttconv.imsc.attributes import TimeExpressionSyntaxEnum def parse_time_expression_syntax(config_value: str) -> Optional[TimeExpressionSyntaxEnum]: """Parse time expression from string value""" if config_value is None: return config_value str_values = map(lambda e: e.value, list(TimeExpressionSyntaxEnum)) if config_value not in str_values: raise ValueError("Invalid time expression format", config_value) return TimeExpressionSyntaxEnum[config_value] @dataclass class IMSCWriterConfiguration(ModuleConfiguration): """IMSC writer configuration""" class FractionDecoder: """Utility callable for converting string to Fraction""" def __call__(self, value: str) -> Optional[Fraction]: if value is None: return None [num, den] = value.split('/') return Fraction(int(num), int(den)) @classmethod def name(cls): return "imsc_writer" time_format: Optional[TimeExpressionSyntaxEnum] = field( default=None, metadata={"decoder": parse_time_expression_syntax} ) fps: Optional[Fraction] = field( default=None, metadata={"decoder": FractionDecoder()} ) ttconv-1.1.1/src/main/python/ttconv/imsc/elements.py000066400000000000000000001422531474066147500225460ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: UTF-8 -*- # Copyright (c) 2020, Sandflow Consulting LLC # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. '''Process TTML elements''' from __future__ import annotations import logging from fractions import Fraction import typing import numbers import xml.etree.ElementTree as et import ttconv.model as model import ttconv.style_properties as model_styles import ttconv.imsc.namespaces as xml_ns import ttconv.imsc.attributes as imsc_attr from ttconv.imsc.style_properties import StyleProperties import ttconv.imsc.style_properties as imsc_styles LOGGER = logging.getLogger(__name__) class TTMLElement: '''Static information about a TTML element ''' class ParsingContext(imsc_styles.StyleParsingContext): '''State information when parsing a TTML element''' def __init__(self, ttml_class: typing.Type[TTMLElement], parent_ctx: typing.Optional[TTMLElement.ParsingContext] = None): self.doc = parent_ctx.doc if parent_ctx is not None else model.ContentDocument() self.style_elements: typing.Dict[str, StyleElement] = parent_ctx.style_elements if parent_ctx is not None else {} self.temporal_context = parent_ctx.temporal_context if parent_ctx is not None else imsc_attr.TemporalAttributeParsingContext() self.ttml_class: typing.Type[TTMLElement] = ttml_class self.lang: typing.Optional[str] = None self.space: typing.Optional[model.WhiteSpaceHandling] = None self.time_container: imsc_attr.TimeContainer = imsc_attr.TimeContainer.par self.explicit_begin: typing.Optional[Fraction] = None self.implicit_begin: typing.Optional[Fraction] = None self.desired_begin: typing.Optional[Fraction] = None self.explicit_end: typing.Optional[Fraction] = None self.implicit_end: typing.Optional[Fraction] = None self.desired_end: typing.Optional[Fraction] = None self.explicit_dur: typing.Optional[Fraction] = None def process_lang_attribute(self, parent_ctx: TTMLElement.ParsingContext, xml_elem): '''Processes the xml:lang attribute, including inheritance from the parent ''' lang_attr_value = imsc_attr.XMLLangAttribute.extract(xml_elem) self.lang = lang_attr_value if lang_attr_value is not None else parent_ctx.lang def process_space_attribute(self, parent_ctx: TTMLElement.ParsingContext, xml_elem): '''Processes the xml:space attribute, including inheritance from the parent ''' space_attr_value = imsc_attr.XMLSpaceAttribute.extract(xml_elem) self.space = space_attr_value if space_attr_value is not None else parent_ctx.space class WritingContext: '''State information when writing a TTML element''' def __init__(self, frame_rate: Fraction, time_expression_syntax: imsc_attr.TimeExpressionSyntaxEnum): self.temporal_context = imsc_attr.TemporalAttributeWritingContext( frame_rate=frame_rate, time_expression_syntax=time_expression_syntax ) @staticmethod def is_instance(xml_elem) -> bool: '''Returns true if the XML element `xml_elem` is an instance of the class ''' raise NotImplementedError class TTElement(TTMLElement): '''Processes the TTML element ''' class ParsingContext(TTMLElement.ParsingContext): '''State information when parsing a element''' qn = f"{{{xml_ns.TTML}}}tt" @staticmethod def is_instance(xml_elem) -> bool: return xml_elem.tag == TTElement.qn @staticmethod def from_xml( _parent_ctx: typing.Optional[TTMLElement.ParsingContext], xml_elem: et.Element, progress_callback: typing.Callable[[numbers.Real], typing.NoReturn] = None ) -> TTElement.ParsingContext: '''`_parent_ctx` is ignored and can be set to `None` ''' tt_ctx = TTElement.ParsingContext(TTElement) # process attributes space_attr = imsc_attr.XMLSpaceAttribute.extract(xml_elem) tt_ctx.space = space_attr if space_attr is not None else model.WhiteSpaceHandling.DEFAULT lang_attr = imsc_attr.XMLLangAttribute.extract(xml_elem) if lang_attr is None: LOGGER.warning("xml:lang not specified on tt") lang_attr = "" tt_ctx.lang = lang_attr tt_ctx.doc.set_lang(tt_ctx.lang) tt_ctx.doc.set_cell_resolution( imsc_attr.CellResolutionAttribute.extract(xml_elem) ) px_resolution = imsc_attr.ExtentAttribute.extract(xml_elem) if px_resolution is not None: tt_ctx.doc.set_px_resolution(px_resolution) active_area = imsc_attr.ActiveAreaAttribute.extract(xml_elem) if active_area is not None: tt_ctx.doc.set_active_area(active_area) ittp_aspect_ratio = imsc_attr.AspectRatioAttribute.extract(xml_elem) ttp_dar = imsc_attr.DisplayAspectRatioAttribute.extract(xml_elem) if ttp_dar is not None: tt_ctx.doc.set_display_aspect_ratio(ttp_dar) elif ittp_aspect_ratio is not None: tt_ctx.doc.set_display_aspect_ratio(ittp_aspect_ratio) if ittp_aspect_ratio is not None and ttp_dar is not None: LOGGER.warning("Both ittp:aspectRatio and ttp:displayAspectRatio specified on tt") tt_ctx.temporal_context.frame_rate = imsc_attr.FrameRateAttribute.extract(xml_elem) tt_ctx.temporal_context.tick_rate = imsc_attr.TickRateAttribute.extract(xml_elem) # process head and body children elements has_body = False has_head = False for child_element in xml_elem: if BodyElement.is_instance(child_element): if not has_body: has_body = True body_element = ContentElement.from_xml(tt_ctx, child_element) tt_ctx.doc.set_body(body_element.model_element if body_element is not None else None) progress_callback(1) else: LOGGER.error("More than one body element present") elif HeadElement.is_instance(child_element): if not has_head: has_head = True HeadElement.from_xml(tt_ctx, child_element) progress_callback(0.5) else: LOGGER.error("More than one head element present") return tt_ctx @staticmethod def from_model( model_doc: model.ContentDocument, frame_rate: typing.Optional[Fraction], time_expression_syntax: imsc_attr.TimeExpressionSyntaxEnum, progress_callback: typing.Callable[[numbers.Real], typing.NoReturn] ) -> et.Element: '''Converts the data model to an IMSC document contained in an ElementTree Element''' ctx = TTMLElement.WritingContext(frame_rate, time_expression_syntax) tt_element = et.Element(TTElement.qn) imsc_attr.XMLLangAttribute.set(tt_element, model_doc.get_lang()) if model_doc.get_cell_resolution() != model.CellResolutionType(rows=15, columns=32): imsc_attr.CellResolutionAttribute.set(tt_element, model_doc.get_cell_resolution()) has_px = False all_elements = list(model_doc.iter_regions()) if model_doc.get_body() is not None: all_elements.extend(model_doc.get_body().dfs_iterator()) for element in all_elements: for model_style_prop in element.iter_styles(): if StyleProperties.BY_MODEL_PROP[model_style_prop].has_px(element.get_style(model_style_prop)): has_px = True break for animation_step in element.iter_animation_steps(): if StyleProperties.BY_MODEL_PROP[animation_step.style_property].has_px(animation_step.value): has_px = True break if has_px: break if model_doc.get_px_resolution() is not None and has_px: imsc_attr.ExtentAttribute.set(tt_element, model_doc.get_px_resolution()) if model_doc.get_active_area() is not None: imsc_attr.ActiveAreaAttribute.set(tt_element, model_doc.get_active_area()) if model_doc.get_display_aspect_ratio() is not None: imsc_attr.DisplayAspectRatioAttribute.set(tt_element, model_doc.get_display_aspect_ratio()) if frame_rate is not None: imsc_attr.FrameRateAttribute.set(tt_element, frame_rate) # Write the section first head_element = HeadElement.from_model(ctx, model_doc) progress_callback(0.5) if head_element is not None: tt_element.append(head_element) model_body = model_doc.get_body() if model_body is not None: body_element = BodyElement.from_model(ctx, model_body) if body_element is not None: tt_element.append(body_element) progress_callback(1.0) return tt_element class HeadElement(TTMLElement): '''Processes the TTML element ''' class ParsingContext(TTMLElement.ParsingContext): '''Maintains state when parsing a element ''' qn = f"{{{xml_ns.TTML}}}head" @staticmethod def is_instance(xml_elem) -> bool: return xml_elem.tag == HeadElement.qn @staticmethod def from_xml( parent_ctx: typing.Optional[TTMLElement.ParsingContext], xml_elem: et.Element ) -> HeadElement.ParsingContext: '''Converts the XML element `xml_elem` into its representation in the data model. `parent_ctx` contains state information passed from parent to child in the TTML hierarchy. ''' head_ctx = HeadElement.ParsingContext(HeadElement, parent_ctx) # process attributes head_ctx.process_lang_attribute(parent_ctx, xml_elem) head_ctx.process_space_attribute(parent_ctx, xml_elem) # process layout and styling children elements has_layout = False has_styling = False for child_element in xml_elem: if LayoutElement.is_instance(child_element): if not has_layout: has_layout = True LayoutElement.from_xml( head_ctx, child_element ) else: LOGGER.error("Multiple layout elements") elif StylingElement.is_instance(child_element): if not has_styling: has_styling = True StylingElement.from_xml( head_ctx, child_element ) else: LOGGER.error("Multiple styling elements") return head_ctx @staticmethod def from_model( ctx: TTMLElement.WritingContext, model_doc: model.ContentDocument, )-> typing.Optional[et.Element]: '''Converts the ContentDocument `model_doc` into its TTML representation, i.e. an XML element. `ctx` contains state information used in the process. ''' head_element = None styling_element = StylingElement.from_model(ctx, model_doc) if styling_element is not None: if head_element is None: head_element = et.Element(HeadElement.qn) head_element.append(styling_element) layout_element = LayoutElement.from_model(ctx, model_doc) if layout_element is not None: if head_element is None: head_element = et.Element(HeadElement.qn) head_element.append(layout_element) return head_element class LayoutElement(TTMLElement): '''Process the TTML element ''' class ParsingContext(TTMLElement.ParsingContext): '''Maintains state when parsing a element ''' qn = f"{{{xml_ns.TTML}}}layout" @staticmethod def is_instance(xml_elem) -> bool: return xml_elem.tag == LayoutElement.qn @staticmethod def from_xml( parent_ctx: typing.Optional[TTMLElement.ParsingContext], xml_elem: et.Element ) -> typing.Optional[LayoutElement.ParsingContext]: '''Converts the XML element `xml_elem` into its representation in the data model. `parent_ctx` contains state information passed from parent to child in the TTML hierarchy. ''' layout_ctx = LayoutElement.ParsingContext(LayoutElement, parent_ctx) # process attributes layout_ctx.process_lang_attribute(parent_ctx, xml_elem) layout_ctx.process_space_attribute(parent_ctx, xml_elem) # process region elements for child_element in xml_elem: if RegionElement.is_instance(child_element): r = RegionElement.from_xml(layout_ctx, child_element) if r is not None: layout_ctx.doc.put_region(r.model_element) else: LOGGER.warning("Unexpected child of layout element") return layout_ctx @staticmethod def from_model( ctx: TTMLElement.WritingContext, model_doc: model.ContentDocument, ) -> typing.Optional[et.Element]: '''Returns a TTML `layout` element (an XML element) using the information in the ContentDocument `model_doc`. `ctx` contains state information used in the process. ''' layout_element = None for r in model_doc.iter_regions(): region_element = RegionElement.from_model(ctx, r) if region_element is not None: if layout_element is None: layout_element = et.Element(LayoutElement.qn) layout_element.append(region_element) return layout_element class StylingElement(TTMLElement): '''Process the TTML element ''' class ParsingContext(TTMLElement.ParsingContext): '''Maintains state when parsing a element ''' def merge_chained_styles(self, style_element: StyleElement): '''Flattens Chained Referential Styling of the target `style_element` by specifying the style properties of the referenced style elements directly in the target element ''' while len(style_element.style_refs) > 0: style_ref = style_element.style_refs.pop() if style_ref not in self.style_elements: LOGGER.error("Style id not present") continue self.merge_chained_styles(self.style_elements[style_ref]) for style_prop, value in self.style_elements[style_ref].styles.items(): style_element.styles.setdefault(style_prop, value) qn = f"{{{xml_ns.TTML}}}styling" @staticmethod def is_instance(xml_elem) -> bool: return xml_elem.tag == StylingElement.qn @staticmethod def from_xml( parent_ctx: typing.Optional[TTMLElement.ParsingContext], xml_elem: et.Element ) -> typing.Optional[StylingElement.ParsingContext]: '''Converts the XML element `xml_elem` into its representation in the data model. `parent_ctx` contains state information passed from parent to child in the TTML hierarchy. ''' styling_ctx = StylingElement.ParsingContext(StylingElement, parent_ctx) # process style and initial children elements for child_xml_elem in xml_elem: if InitialElement.is_instance(child_xml_elem): InitialElement.from_xml(styling_ctx, child_xml_elem) elif StyleElement.is_instance(child_xml_elem): style_element = StyleElement.from_xml(styling_ctx, child_xml_elem) if style_element is None: continue if style_element.id in styling_ctx.style_elements: LOGGER.error("Duplicate style id") continue style_element.style_elements[style_element.id] = style_element # merge style elements (the data model does not support referential # styling) for style_element in parent_ctx.style_elements.values(): styling_ctx.merge_chained_styles(style_element) return styling_ctx @staticmethod def from_model( _ctx: TTMLElement.WritingContext, model_doc: model.ContentDocument ) -> typing.Optional[et.Element]: '''Returns a TTML `styling` element using the information in the ContentDocument `model_doc`. `ctx` contains state information used in the process. ''' styling_element = None for style_prop, style_value in model_doc.iter_initial_values(): imsc_style_prop = imsc_styles.StyleProperties.BY_MODEL_PROP.get(style_prop) if imsc_style_prop is None: LOGGER.error("Unknown property") continue initial_element = InitialElement.from_model(imsc_style_prop, style_value) if initial_element is not None: if styling_element is None: styling_element = et.Element(StylingElement.qn) styling_element.append(initial_element) return styling_element class StyleElement(TTMLElement): '''Process the TTML