TTML & IMSC1 Format Compliance

The failure this page catches is a syntactically valid TTML1 document that a distributor rejects because it uses a TTML feature the IMSC1 Text profile prohibits — an animate element, a smpte:backgroundImage, a ttp:timeBase of smpte instead of media — or omits the profile designator that tells a player which constraint set applies. TTML1 is a broad vocabulary; IMSC1 (the W3C TTML Profiles for Internet Media Subtitles and Captions) is a deliberately narrow subset of it, and the Text profile narrows it further by excluding every image feature. A document can validate against the TTML1 schema and still be non-conformant IMSC1, so a distinct compliance check is required — separate from any WebVTT-to-IMSC1 conversion, which is a production step. This page is the validation step: an lxml pass that loads a TTML document and returns the exact clauses it breaks — the missing http://www.w3.org/ns/ttml/profile/imsc1/text designator, a non-media timeBase, a cellResolution other than 32 15, and any prohibited element or style attribute.

import io
from lxml import etree

# Namespaces from W3C TTML1 and IMSC1 (TTML Profiles for Internet Media
# Subtitles and Captions 1.0.1). smpte: is the SMPTE-TT extension namespace.
NS = {
    "tt": "http://www.w3.org/ns/ttml",
    "ttp": "http://www.w3.org/ns/ttml#parameter",
    "tts": "http://www.w3.org/ns/ttml#styling",
    "smpte": "http://www.smpte-ra.org/schemas/2052-1/2010/smpte-tt",
}
IMSC1_TEXT_PROFILE = "http://www.w3.org/ns/ttml/profile/imsc1/text"
IMSC1_IMAGE_PROFILE = "http://www.w3.org/ns/ttml/profile/imsc1/image"


def q(prefix: str, local: str) -> str:
    return f"{{{NS[prefix]}}}{local}"


# Elements illegal under the IMSC1 Text profile (IMSC1 §6.7 prohibited features).
PROHIBITED_ELEMENTS = {
    q("tt", "animate"): "TTML animation (animate) is prohibited by IMSC1",
    q("tt", "audio"): "embedded audio is outside the IMSC1 Text profile",
    q("tt", "image"): "image element is Image-profile only, illegal in Text profile",
    q("smpte", "image"): "smpte:image is Image-profile only",
}
PROHIBITED_ATTRS = {
    q("smpte", "backgroundImage"): "smpte:backgroundImage is Image-profile only",
    q("tts", "zIndex"): "tts:zIndex is not supported by IMSC1",
}


def validate_imsc1_text(source) -> list:
    """Return a list of (code, message) IMSC1 Text-profile violations for a TTML
    document. Raises lxml.etree.XMLSyntaxError on malformed XML."""
    violations = []
    tree = etree.parse(source)
    root = tree.getroot()
    if root.tag != q("tt", "tt"):
        return [("STRUCTURE", "root element is not tt:tt")]

    # IMSC1 §6.1 — a conforming document MUST declare a profile designator.
    profile = root.get(q("ttp", "profile"))
    if profile not in (IMSC1_TEXT_PROFILE, IMSC1_IMAGE_PROFILE):
        violations.append(("PROFILE", f"missing/invalid ttp:profile: {profile!r}"))

    # IMSC1 §6.4 — ttp:timeBase MUST be 'media'; smpte/clock are prohibited.
    time_base = root.get(q("ttp", "timeBase"), "media")
    if time_base != "media":
        violations.append(("TIMEBASE", f"ttp:timeBase='{time_base}', IMSC1 requires 'media'"))

    # TTML1 default cellResolution is '32 15'; absence is legal but flagged for QC.
    cell = root.get(q("ttp", "cellResolution"))
    if cell is None:
        violations.append(("CELLRES", "ttp:cellResolution absent — default '32 15' assumed"))
    elif cell.split() != ["32", "15"]:
        violations.append(("CELLRES", f"non-standard cellResolution '{cell}'"))

    # Single walk: flag prohibited elements and disallowed style attributes.
    for el in root.iter():
        if el.tag in PROHIBITED_ELEMENTS:
            violations.append(("ELEMENT", PROHIBITED_ELEMENTS[el.tag]))
        for attr in el.attrib:
            if attr in PROHIBITED_ATTRS:
                violations.append(("ATTRIBUTE", PROHIBITED_ATTRS[attr]))
    return violations


if __name__ == "__main__":
    sample = """<?xml version="1.0" encoding="UTF-8"?>
<tt xmlns="http://www.w3.org/ns/ttml"
    xmlns:ttp="http://www.w3.org/ns/ttml#parameter"
    xmlns:tts="http://www.w3.org/ns/ttml#styling"
    xmlns:smpte="http://www.smpte-ra.org/schemas/2052-1/2010/smpte-tt"
    ttp:profile="http://www.w3.org/ns/ttml/profile/imsc1/text"
    ttp:timeBase="smpte" ttp:cellResolution="40 19">
  <body><div>
    <p begin="00:00:01.000" end="00:00:04.000"
       smpte:backgroundImage="logo.png">Hello world</p>
  </div></body>
</tt>"""
    for code, msg in validate_imsc1_text(io.BytesIO(sample.encode("utf-8"))):
        print(f"[{code}] {msg}")

Code walkthrough

The namespace map is load-bearing, not boilerplate. lxml reports every element and attribute as a Clark-notation name — {namespace}local — so a checker that matches on the bare local name animate would miss the fact that IMSC1 constrains the TTML-namespace element specifically and would false-positive on an unrelated animate from some other vocabulary. The q() helper builds those fully qualified names once, and PROHIBITED_ELEMENTS/PROHIBITED_ATTRS are keyed by them, so the tree walk compares namespace-qualified identities rather than fragile strings.

validate_imsc1_text parses with etree.parse, which raises XMLSyntaxError on malformed XML — that is deliberate, because an unparseable document is a different failure class from a well-formed but non-conformant one and should surface before any profile logic runs. The root-tag guard rejects anything whose root is not tt:tt immediately, since the rest of the checks assume a TTML document.

The three parameter checks map one-to-one onto IMSC1’s document-level constraints. The profile check enforces §6.1: a conforming document must carry an IMSC1 designator, and accepting either the Text or Image profile URI lets the same validator flag a Text-only pipeline that was handed an Image-profile file. The timeBase check enforces §6.4 — IMSC1 requires media time, so a smpte or clock timeBase (common in files exported from SMPTE-TT authoring tools) is a violation. The cellResolution check treats the TTML1 default of 32 15 as canonical: absence is technically legal because the default applies, but it is flagged for QC because an unstated cell grid is a frequent source of downstream layout drift, and any explicit value other than 32 15 is reported outright.

The final single root.iter() walk is where the Text-profile narrowing happens. Every prohibited element — animate, audio, image, smpte:image — and every prohibited attribute — smpte:backgroundImage, tts:zIndex — is caught in one pass over the tree, so validation cost is linear in document size. Keeping the prohibited sets as module-level dictionaries rather than inline conditionals means the constraint list is data, not control flow: extending the checker to a new prohibited feature is a one-line addition to PROHIBITED_ELEMENTS or PROHIBITED_ATTRS, and the same tables can be swapped when the declared profile is Image rather than Text. The function returns a list of (code, message) tuples rather than raising, so a batch validator can aggregate violations across a library the way the parent SCC vs SRT vs WebVTT architecture reference aggregates per-format checks, and each code maps cleanly onto a column in a compliance report.

Threshold reference table

Constraint Required value Source / clause
Text profile designator http://www.w3.org/ns/ttml/profile/imsc1/text IMSC1 §6.1
Image profile designator http://www.w3.org/ns/ttml/profile/imsc1/image IMSC1 §6.1
ttp:timeBase media (smpte/clock prohibited) IMSC1 §6.4
ttp:cellResolution default 32 15 TTML1 / IMSC1
Prohibited elements (Text) animate, audio, image, smpte:image IMSC1 §6.7
Prohibited attributes smpte:backgroundImage, tts:zIndex IMSC1 Text profile
Image features Image profile only IMSC1 §6.7
Root element tt:tt TTML1

Edge cases & known gotchas

  • EBU-TT-D extensions: files targeting European delivery often carry ebutts: (urn:ebu:tt:style) style attributes. These are legal EBU-TT-D but are not IMSC1-defined, so extend the validator with a warn-level check rather than treating them as hard violations — they interoperate, but a strict IMSC1 profile does not require support for them.
  • smpte:backgroundImage is Image-profile only: the same smpte:backgroundImage that is a violation under the Text profile is a legitimate feature under the IMSC1 Image profile. Branch the prohibited-attribute set on the declared profile so an Image-profile document is not failed for using its defining feature.
  • Region overflow: IMSC1 defaults tts:overflow to hidden, so text that exceeds its region is clipped, not wrapped. A document can pass every check here and still lose glyphs at render — pair structural validation with a layout/extent check against the region’s tts:extent.
  • Profile via metadata: some authoring tools declare conformance through ttp:contentProfiles or a <ttp:profile> child element rather than the ttp:profile attribute. Treat a missing attribute as a warning and inspect those alternatives before hard-failing, or a conformant file gets a false PROFILE violation.
  • Default timeBase is media: because media is the TTML1 default, a file that omits ttp:timeBase entirely is compliant — the validator’s .get(..., "media") fallback encodes that, so do not “fix” an absent attribute by rejecting it.
  • Prohibited styling beyond zIndex: tts:zIndex is the common offender, but IMSC1 also constrains vertical tts:writingMode, tts:textCombine and several animation-adjacent style values; add them to PROHIBITED_ATTRS when your source authoring tool is known to emit them, so a stray style attribute is reported rather than silently passed to a player that ignores it.

Integration hook

This validator is the gate that a TTML document must clear before it is treated as deliverable IMSC1. It is the compliance counterpart to the production step in IMSC1 & TTML packaging for OTT: where converting WebVTT to IMSC1 emits a TTML document, this pass proves that the emitted document stays inside the IMSC1 Text profile before packaging. Run it immediately after conversion and again at ingest of any third-party TTML, so a non-conformant feature is caught as a violation list rather than a distributor rejection.

Frequently asked questions

Is a valid TTML1 file automatically valid IMSC1? No. TTML1 is a superset; IMSC1 is a constrained profile of it. A document can pass TTML1 schema validation and still use features IMSC1 prohibits — animation, audio, images in the Text profile, or a non-media timeBase — so a separate IMSC1 profile check is required.

What makes the Text profile different from the Image profile? The Text profile carries styled text and forbids all image features, while the Image profile carries rendered subtitle images (smpte:backgroundImage, smpte:image) and forbids the text-rendering features. A smpte:backgroundImage that is a violation under Text is legal under Image, so the profile designator determines which constraint set applies.

Why flag a missing cellResolution if the default is legal? Because an unstated 32 15 grid is a common source of layout drift when a downstream tool assumes a different cell resolution. The default is conformant, so it is reported as a QC flag rather than a hard failure — but making the grid explicit removes the ambiguity.

Part of: Broadcast Media Closed Captioning & QC Automation — the architecture and compliance reference for broadcast caption pipelines.