.
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/ 0000775 0000000 0000000 00000000000 14740661475 0013612 5 ustar 00root root 0000000 0000000 ttconv-1.1.1/doc/data_model.md 0000664 0000000 0000000 00000007152 14740661475 0016232 0 ustar 00root root 0000000 0000000 # 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.md 0000664 0000000 0000000 00000003761 14740661475 0016420 0 ustar 00root root 0000000 0000000 # 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.md 0000664 0000000 0000000 00000002553 14740661475 0014720 0 ustar 00root root 0000000 0000000 # 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
...
```
is:
```xml
```
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.md 0000664 0000000 0000000 00000006317 14740661475 0016264 0 ustar 00root root 0000000 0000000 # 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.md 0000664 0000000 0000000 00000005032 14740661475 0016226 0 ustar 00root root 0000000 0000000 # 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.md 0000664 0000000 0000000 00000002206 14740661475 0016340 0 ustar 00root root 0000000 0000000 # 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/ 0000775 0000000 0000000 00000000000 14740661475 0014534 5 ustar 00root root 0000000 0000000 ttconv-1.1.1/scripts/ci.sh 0000775 0000000 0000000 00000000645 14740661475 0015473 0 ustar 00root root 0000000 0000000 #!/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.sh 0000775 0000000 0000000 00000000155 14740661475 0016667 0 ustar 00root root 0000000 0000000 #!/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.sh 0000664 0000000 0000000 00000000134 14740661475 0016363 0 ustar 00root root 0000000 0000000 #!/bin/sh
pipenv run python -m pylint --exit-zero src/main/python/ttconv/ src/test/python/
ttconv-1.1.1/scripts/unit_test.sh 0000664 0000000 0000000 00000000116 14740661475 0017104 0 ustar 00root root 0000000 0000000 #!/bin/sh
pipenv run python -m unittest discover -v -s src/test/python/ -t .
ttconv-1.1.1/setup.py 0000664 0000000 0000000 00000002737 14740661475 0014570 0 ustar 00root root 0000000 0000000 """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/ 0000775 0000000 0000000 00000000000 14740661475 0013634 5 ustar 00root root 0000000 0000000 ttconv-1.1.1/src/main/ 0000775 0000000 0000000 00000000000 14740661475 0014560 5 ustar 00root root 0000000 0000000 ttconv-1.1.1/src/main/python/ 0000775 0000000 0000000 00000000000 14740661475 0016101 5 ustar 00root root 0000000 0000000 ttconv-1.1.1/src/main/python/ttconv/ 0000775 0000000 0000000 00000000000 14740661475 0017416 5 ustar 00root root 0000000 0000000 ttconv-1.1.1/src/main/python/ttconv/__init__.py 0000664 0000000 0000000 00000000000 14740661475 0021515 0 ustar 00root root 0000000 0000000 ttconv-1.1.1/src/main/python/ttconv/config.py 0000664 0000000 0000000 00000006320 14740661475 0021236 0 ustar 00root root 0000000 0000000 #!/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/ 0000775 0000000 0000000 00000000000 14740661475 0021066 5 ustar 00root root 0000000 0000000 ttconv-1.1.1/src/main/python/ttconv/filters/__init__.py 0000664 0000000 0000000 00000000000 14740661475 0023165 0 ustar 00root root 0000000 0000000 ttconv-1.1.1/src/main/python/ttconv/filters/doc/ 0000775 0000000 0000000 00000000000 14740661475 0021633 5 ustar 00root root 0000000 0000000 ttconv-1.1.1/src/main/python/ttconv/filters/doc/__init__.py 0000664 0000000 0000000 00000003223 14740661475 0023744 0 ustar 00root root 0000000 0000000 #!/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.py 0000664 0000000 0000000 00000023725 14740661475 0022760 0 ustar 00root root 0000000 0000000 #!/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.py 0000664 0000000 0000000 00000004441 14740661475 0024626 0 ustar 00root root 0000000 0000000 #!/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/ 0000775 0000000 0000000 00000000000 14740661475 0021645 5 ustar 00root root 0000000 0000000 ttconv-1.1.1/src/main/python/ttconv/filters/isd/__init__.py 0000664 0000000 0000000 00000002575 14740661475 0023767 0 ustar 00root root 0000000 0000000 #!/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.py 0000664 0000000 0000000 00000006164 14740661475 0027346 0 ustar 00root root 0000000 0000000 #!/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.py 0000664 0000000 0000000 00000006060 14740661475 0025530 0 ustar 00root root 0000000 0000000 #!/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.py 0000664 0000000 0000000 00000005061 14740661475 0025046 0 ustar 00root root 0000000 0000000 #!/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.py 0000664 0000000 0000000 00000004341 14740661475 0027742 0 ustar 00root root 0000000 0000000 #!/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.py 0000664 0000000 0000000 00000003077 14740661475 0023573 0 ustar 00root root 0000000 0000000 #!/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.py 0000664 0000000 0000000 00000004127 14740661475 0025163 0 ustar 00root root 0000000 0000000 #!/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.py 0000664 0000000 0000000 00000005442 14740661475 0027166 0 ustar 00root root 0000000 0000000 #!/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/ 0000775 0000000 0000000 00000000000 14740661475 0020351 5 ustar 00root root 0000000 0000000 ttconv-1.1.1/src/main/python/ttconv/imsc/__init__.py 0000664 0000000 0000000 00000000000 14740661475 0022450 0 ustar 00root root 0000000 0000000 ttconv-1.1.1/src/main/python/ttconv/imsc/attributes.py 0000664 0000000 0000000 00000031701 14740661475 0023113 0 ustar 00root root 0000000 0000000 #!/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.py 0000664 0000000 0000000 00000005253 14740661475 0022175 0 ustar 00root root 0000000 0000000 #!/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.py 0000664 0000000 0000000 00000142253 14740661475 0022546 0 ustar 00root root 0000000 0000000 #!/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