← Guides

Subtitling

SMPTE ST 428-7 rendering quirks that break subtitles in the cinema

SMPTE ST 428-7 rendering quirks: IssueDate time zones, EditRate vs TimeCodeRate, inverted Vposition, LoadFont UUIDs, markers, ContentKind.

A subtitle XML can pass schema validation and still fail at the worst possible moment: on a cinema server, in front of an audience, or at a lab QC station a week before the premiere. ST 428-7:2014 is a tight spec, but the failures that actually happen in production cluster around a handful of details the schema cannot protect you from.

This is a field guide to those details. Every quirk below is one we have either been bitten by directly or verified against production DCPs and lab rejections.

1. IssueDate: the time zone string that gets you rejected

<IssueDate> is an xs:dateTime, so all of these are schema-valid:

2026-08-17T10:20:11Z
2026-08-17T10:20:11+02:00
2026-08-17T12:16:24.389278

Schema-valid and interoperable are different targets. Two real rejections bracket the safe zone:

The form that survives both is 2026-08-17T10:20:11+00:00 — the value is UTC, and the zone is numeric. It is also what asdcplib, OpenDCP, and DCP-o-matic emit. Two corollaries:

2. EditRate vs TimeCodeRate: which clock are your frames on?

An ST 428-7 reel carries two rate fields that are easy to treat as redundant:

<EditRate>25 1</EditRate>
<TimeCodeRate>25</TimeCodeRate>

They are not redundant. EditRate is a rational (numerator/denominator) that defines the reel's edit units — and subtitle TimeIn/TimeOut are frame numbers at the EditRate. TimeCodeRate is an integer (24, 25, 30) describing the timecode display rate.

The failure mode: a tool converts SRT timecodes to frame numbers using the TimeCodeRate (or, worse, a hardcoded 25) while the reel's EditRate says something else. On a 25 fps show the two coincide and the bug hides. On an HFR reel — EditRate 48 1 with TimeCodeRate 24 — every subtitle is suddenly off by a factor of two. The conversion must always be seconds × (EditRate numerator / denominator).

And whatever the rate: <StartTime> is always 00:00:00:00 for a DCP subtitle reel. Hour offsets belong to broadcast, not cinema.

3. Vposition counts up from the bottom

Interop's VPosition is a 0.0–1.0 fraction measured down from the top. ST 428-7's Vposition is a 1.0–100.0 percentage measured up from the bottom. Yes, inverted — and yes, this is the single most common reason ported subtitles render at the top of the screen.

The convention on real productions: single-line cues at Vposition="8.0", two-line cues with the top line at 14.0 and the bottom at 8.0 — 6.0 units of line spacing:

<Text Vposition="8.0" Halign="center" Direction="ltr" Valign="bottom" Hposition="0.0">

This is a deep enough trap that it forced a document-format migration in AlphaSUB itself: document version 2 redefined the internal VerticalPosition.percentage from "percent down from the top" to "percent up from the bottom" — the DCP/SMPTE convention — because keeping the inverted convention internally meant every DCP export was one forgotten complement away from mirrored subtitles. Version-1 projects are migrated on decode by complementing every stored percentage. If your own tooling stores vertical position internally, pick the SMPTE direction and convert at the InterOp boundary, not the other way around.

4. LoadFont UUIDs: the font is an asset, not a file

In SMPTE, fonts are referenced by UUID and shipped as separate package assets named by that UUID:

<LoadFont ID="FontID">urn:uuid:25586ca2-816b-4272-ae29-518fa616ff2e</LoadFont>

If the UUID in the XML does not match the font asset's UUID in the package, the server has no font — and what happens next is implementation-defined, none of it good. Facilities standardize on a house subtitle font, and you will see the same UUID recur across an entire catalog of projects. Treat that UUID as part of the facility's delivery spec, not as a random identifier you can regenerate.

Which brings up the reproducibility trap: if your export pipeline generates a fresh random font UUID on every export when none is pinned, two exports of the same project produce different XML. Byte-comparison QC fails on nondeterminism rather than regression — and the test gets disabled by whoever hits it first, which is worse than not having the test. Pin the URN.

For InterOp the constraint is different but just as hard: one font per presentation, max 640 KB, referenced by filename. A full CJK font blows straight past that.

5. Omitted attributes are not default attributes

This one is not in the standard at all — it is in how real exporters behave, and it silently corrupts packages. When a subtitle style key is absent from your project data, the exporter applies its own fallback, and that fallback routinely disagrees with your UI's default. Concrete examples from a production codebase:

Key Absent → emitted UI default On-screen consequence
font size 40 42 Different glyph size
effect size 1.5 1.50 (%.2f) Differs no matter which you pick
font effect derived from border width (none/border) verbatim Outline appears or vanishes
effect color derived from border color verbatim Wrong outline color

So "only write non-default values" — the tidy-looking design — is a corruption vector. Always write every value, in the exact canonical spelling the writer produces: %.2f for floats, plain integers for sizes, uppercase 8-digit hex for colors.

Colors: three orderings, one hex string

DCP XML colors are AARRGGBB — alpha first. Application internals are often RRGGBB, or RRGGBBAA with alpha last. Readers in the wild are lenient (optional #, 6 or 8 digits, any case); writers emit bare uppercase 8-digit. A 6-digit lowercase value round-tripped through a lenient reader and a canonical writer comes out changed — correct for an edit, wrong for a passthrough. Go through a single conversion helper and never "tidy up" a value that came from a third-party package.

InterOp effect attributes vanish at border width zero

An InterOp-specific behaviour worth knowing: Effect and EffectColor only reach the XML when the border width is greater than zero; otherwise the attributes are dropped entirely. If your outline disappeared between exports, check the border width before blaming the font.

6. Markers: FFEC/FFMC, and the schema-order trap

Markers are not subtitles, but they ride in the same CPL and break the same way, so they belong in any subtitle author's mental model. RDD 52 (Bv2.1) §8.5 mandates:

Two things the mandate does not say. First, the semantics: "End Credits" means card-style credits, "Moving Credits" means rollers; with no rolling credits FFMC may equal FFEC, and with neither, FFEC may equal FFMC. Second, the enforcement: popular open-source QC (ClairMeta) has no marker-specific checks at all — it validates schema shape and parses the marker list, but never looks at label values or offsets. A package can pass the usual QC path with garbage markers.

Marker assets are pure CPL XML: MainMarkers extends the generic asset type, so it has no KeyId, no Hash, no track file, no PKL or ASSETMAP entry. But element order is normative — the CPL schemas are sequence-ordered and servers reject reordered elements, with MainMarkers preceding MainPicture in the AssetList. And per RDD 52 §8.2, all Duration elements within a reel must be equal — the marker asset's duration must match picture, sound, and subtitle on that reel.

7. ContentKind: right place, right scope, right value

ContentKind is a required child of CompositionPlaylist, positioned after ContentTitleText and before ContentVersion — again, sequence-ordered. It carries an optional scope attribute whose default differs by standard:

Both CPL schemas type ContentKind as a bare xs:string — no enumeration — so schema validation will happily accept Featuer. Values attested in production include feature, trailer, short, advertisement, test; the longer lists circulating in reference tooling should be treated as UI conveniences, not a validated vocabulary.

One conflation to avoid: the ISDCF content-type codes embedded in ContentTitleText (FTR, TLR, SHR, TST, …) are title codes, not ContentKind values. Setting one does not set the other, and ContentKind = feature is what triggers the FFEC/FFMC marker requirement above.

8. The audio reel your subtitles share

Subtitle reels live in the same composition as the sound track, and RDD 52's audio rules reach across: audio is always 48 kHz; in the 16-channel container, channels 9, 10, and 16 are recorded as silence, any unused channel in a soundfield group is silent, and the channel layout must remain constant across the entire composition — you cannot change soundfield group between reels. If your reel split is fine for picture and subtitles but the mix changes layout at the reel boundary, the package is invalid and the subtitle reel's duration equality rule (§8.2) still has to hold regardless.


AlphaSUB's DCP export and AlphaDCP's authoring pipeline handle these natively: +00:00 UTC issue dates from a single shared helper, EditRate-based frame conversion, the up-from-the-bottom vertical convention (with automatic migration of older projects), pinned font UUIDs, verbatim metadata passthrough, and a Bv2.1 marker/ContentKind validator that checks what generic QC tools skip. Details at alpha-sub.com.