Skip to content

Commit 90801f8

Browse files
Support body div level style and region (#131)
* [ttml] Support style and region inheritance body>div>p * [ttml/test] Test case for style and region inheritance * Add more case * Rename function and correct code
1 parent 2d1d91b commit 90801f8

File tree

4 files changed

+278
-75
lines changed

4 files changed

+278
-75
lines changed

subtitles.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,109 @@ func (sa *StyleAttributes) propagateWebVTTAttributes() {
430430
sa.SRTUnderline = sa.WebVTTUnderline
431431
}
432432

433+
// merge - base on parent, override style attributes if defined in child
434+
// TODO: handle more formats than just TTML
435+
func (sa *StyleAttributes) merge(parent *StyleAttributes) {
436+
if parent == nil || sa == nil {
437+
return
438+
}
439+
440+
if sa.TTMLBackgroundColor == nil {
441+
sa.TTMLBackgroundColor = parent.TTMLBackgroundColor
442+
}
443+
444+
if sa.TTMLColor == nil {
445+
sa.TTMLColor = parent.TTMLColor
446+
}
447+
448+
if sa.TTMLDirection == nil {
449+
sa.TTMLDirection = parent.TTMLDirection
450+
}
451+
452+
if sa.TTMLDisplay == nil {
453+
sa.TTMLDisplay = parent.TTMLDisplay
454+
}
455+
456+
if sa.TTMLDisplayAlign == nil {
457+
sa.TTMLDisplayAlign = parent.TTMLDisplayAlign
458+
}
459+
460+
if sa.TTMLExtent == nil {
461+
sa.TTMLExtent = parent.TTMLExtent
462+
}
463+
464+
if sa.TTMLFontFamily == nil {
465+
sa.TTMLFontFamily = parent.TTMLFontFamily
466+
}
467+
468+
if sa.TTMLFontSize == nil {
469+
sa.TTMLFontSize = parent.TTMLFontSize
470+
}
471+
472+
if sa.TTMLFontStyle == nil {
473+
sa.TTMLFontStyle = parent.TTMLFontStyle
474+
}
475+
if sa.TTMLFontWeight == nil {
476+
sa.TTMLFontWeight = parent.TTMLFontWeight
477+
}
478+
479+
if sa.TTMLLineHeight == nil {
480+
sa.TTMLLineHeight = parent.TTMLLineHeight
481+
}
482+
483+
if sa.TTMLOpacity == nil {
484+
sa.TTMLOpacity = parent.TTMLOpacity
485+
}
486+
487+
if sa.TTMLOrigin == nil {
488+
sa.TTMLOrigin = parent.TTMLOrigin
489+
}
490+
491+
if sa.TTMLOverflow == nil {
492+
sa.TTMLOverflow = parent.TTMLOverflow
493+
}
494+
495+
if sa.TTMLPadding == nil {
496+
sa.TTMLPadding = parent.TTMLPadding
497+
}
498+
499+
if sa.TTMLShowBackground == nil {
500+
sa.TTMLShowBackground = parent.TTMLShowBackground
501+
}
502+
503+
if sa.TTMLTextAlign == nil {
504+
sa.TTMLTextAlign = parent.TTMLTextAlign
505+
}
506+
507+
if sa.TTMLTextDecoration == nil {
508+
sa.TTMLTextDecoration = parent.TTMLTextDecoration
509+
}
510+
511+
if sa.TTMLTextOutline == nil {
512+
sa.TTMLTextOutline = parent.TTMLTextOutline
513+
}
514+
515+
if sa.TTMLUnicodeBidi == nil {
516+
sa.TTMLUnicodeBidi = parent.TTMLUnicodeBidi
517+
}
518+
519+
if sa.TTMLVisibility == nil {
520+
sa.TTMLVisibility = parent.TTMLVisibility
521+
}
522+
523+
if sa.TTMLWrapOption == nil {
524+
sa.TTMLWrapOption = parent.TTMLWrapOption
525+
}
526+
527+
if sa.TTMLWritingMode == nil {
528+
sa.TTMLWritingMode = parent.TTMLWritingMode
529+
}
530+
531+
if sa.TTMLZIndex == nil {
532+
sa.TTMLZIndex = parent.TTMLZIndex
533+
}
534+
}
535+
433536
// Metadata represents metadata
434537
// TODO Merge attributes
435538
type Metadata struct {
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<tt xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://www.w3.org/2006/10/ttaf1" xmlns:tt="http://www.w3.org/2006/10/ttaf1" xmlns:tts="http://www.w3.org/2006/10/ttaf1#styling" xmlns:ttp="http://www.w3.org/2006/10/ttaf1#parameter" xmlns:ttm="http://www.w3.org/2006/10/ttaf1#metadata" xml:lang="fr-FR" ttp:timeBase="smpte" ttp:frameRate="25" ttp:frameRateMultiplier="1:1" ttp:markerMode="discontinuous">
2+
<head>
3+
<metadata>
4+
<ttm:title>Title test</ttm:title>
5+
<ttm:copyright>Copyright test</ttm:copyright>
6+
</metadata>
7+
<styling>
8+
<style xml:id="style_0" tts:style="style_2" tts:fontFamily="sansSerif" tts:color="white" tts:fontStyle="normal" tts:textAlign="center" tts:origin="0% 90%" tts:extent="100% 10%"/>
9+
<style xml:id="style_1" tts:fontFamily="sansSerif" tts:color="white" tts:fontStyle="normal" tts:textAlign="center" tts:origin="0% 87%" tts:extent="100% 13%"/>
10+
<style xml:id="style_2" tts:fontFamily="sansSerif" tts:color="white" tts:fontStyle="normal" tts:textAlign="center" tts:origin="0% 80%" tts:extent="100% 20%"/>
11+
</styling>
12+
<layout>
13+
<region xml:id="region_0" tt:style="style_0" tt:color="blue"/>
14+
<region xml:id="region_1" tt:style="style_1"/>
15+
<region xml:id="region_2" tt:style="style_2"/>
16+
</layout>
17+
</head>
18+
<body region="region_0" style="style_0">
19+
<div style="style_1" tts:color="yellow">
20+
<p xml:id="sub_1" begin="00:00:00.000" end="00:01:00.000" color="red">
21+
text1.0
22+
<span style="style_1" color="black">text1.1</span>
23+
</p>
24+
</div>
25+
<div region="region_1">
26+
<p xml:id="sub_2" begin="00:01:00.000" end="00:02:00.000">
27+
<span style="style_1">text2</span>
28+
</p>
29+
<p xml:id="sub_3" begin="00:01:00.000" end="00:02:00.000" region="region_2">
30+
text2.1
31+
</p>
32+
</div>
33+
<div tts:color="blue">
34+
<p xml:id="sub_2" begin="00:02:00.000" end="00:03:00.000">
35+
<span style="style_1">text3</span>
36+
</p>
37+
</div>
38+
</body>
39+
</tt>

ttml.go

Lines changed: 119 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,34 @@ var (
4141
ttmlRegexpOffsetTime = regexp.MustCompile(`^(\d+(\.\d+)?)(h|m|s|ms|f|t)$`)
4242
)
4343

44+
type TTMLInBodyDiv struct {
45+
XMLName xml.Name `xml:"div"`
46+
Subtitles []TTMLInSubtitle `xml:"p"`
47+
48+
Region string `xml:"region,attr,omitempty"`
49+
Style string `xml:"style,attr,omitempty"`
50+
TTMLInStyleAttributes
51+
}
52+
type TTMLInBody struct {
53+
XMLName xml.Name `xml:"body"`
54+
Divs []TTMLInBodyDiv `xml:"div"`
55+
56+
Region string `xml:"region,attr,omitempty"`
57+
Style string `xml:"style,attr,omitempty"`
58+
TTMLInStyleAttributes
59+
}
60+
4461
// TTMLIn represents an input TTML that must be unmarshaled
4562
// We split it from the output TTML as we can't add strict namespace without breaking retrocompatibility
4663
type TTMLIn struct {
47-
Framerate int `xml:"frameRate,attr"`
48-
Lang string `xml:"lang,attr"`
49-
Metadata TTMLInMetadata `xml:"head>metadata"`
50-
Regions []TTMLInRegion `xml:"head>layout>region"`
51-
Styles []TTMLInStyle `xml:"head>styling>style"`
52-
Subtitles []TTMLInSubtitle `xml:"body>div>p"`
53-
Tickrate int `xml:"tickRate,attr"`
54-
XMLName xml.Name `xml:"tt"`
64+
Framerate int `xml:"frameRate,attr"`
65+
Lang string `xml:"lang,attr"`
66+
Metadata TTMLInMetadata `xml:"head>metadata"`
67+
Regions []TTMLInRegion `xml:"head>layout>region"`
68+
Styles []TTMLInStyle `xml:"head>styling>style"`
69+
Body TTMLInBody `xml:"body"`
70+
Tickrate int `xml:"tickRate,attr"`
71+
XMLName xml.Name `xml:"tt"`
5572
}
5673

5774
// metadata returns the Metadata of the TTML
@@ -386,94 +403,121 @@ func ReadFromTTML(i io.Reader) (o *Subtitles, err error) {
386403
}
387404

388405
// Loop through subtitles
389-
for _, ts := range ttml.Subtitles {
390-
// Init item
391-
ts.Begin.framerate = ttml.Framerate
392-
ts.Begin.tickrate = ttml.Tickrate
393-
ts.End.framerate = ttml.Framerate
394-
ts.End.tickrate = ttml.Tickrate
395-
396-
var s = &Item{
397-
EndAt: ts.End.duration(),
398-
InlineStyle: ts.TTMLInStyleAttributes.styleAttributes(),
399-
StartAt: ts.Begin.duration(),
406+
bodyInlineStyle := ttml.Body.TTMLInStyleAttributes.styleAttributes()
407+
for _, div := range ttml.Body.Divs {
408+
divInlineStyle := div.TTMLInStyleAttributes.styleAttributes()
409+
410+
// Propagate styles from Body -> Div
411+
divInlineStyle.merge(bodyInlineStyle)
412+
if div.Region == "" {
413+
div.Region = ttml.Body.Region
414+
}
415+
if div.Style == "" {
416+
div.Style = ttml.Body.Style
400417
}
418+
for _, ts := range div.Subtitles {
419+
// Init item
420+
ts.Begin.framerate = ttml.Framerate
421+
ts.Begin.tickrate = ttml.Tickrate
422+
ts.End.framerate = ttml.Framerate
423+
ts.End.tickrate = ttml.Tickrate
424+
425+
itemInlineStyle := ts.TTMLInStyleAttributes.styleAttributes()
426+
427+
// Propagate styles from Body -> Div -> Item.
428+
// If the item has its own Region, Style, or InlineStyle, it overrides the Div's.
429+
// This ensures all relevant styles are preserved at the item level,
430+
// maintaining compatibility with existing logic that relies on the Subtitles structure.
431+
itemInlineStyle.merge(divInlineStyle)
432+
if ts.Region == "" {
433+
ts.Region = div.Region
434+
}
435+
if ts.Style == "" {
436+
ts.Style = div.Style
437+
}
401438

402-
// Add region
403-
if len(ts.Region) > 0 {
404-
if _, ok := o.Regions[ts.Region]; !ok {
405-
err = fmt.Errorf("astisub: Region %s requested by subtitle between %s and %s doesn't exist", ts.Region, s.StartAt, s.EndAt)
406-
return
439+
var s = &Item{
440+
EndAt: ts.End.duration(),
441+
InlineStyle: itemInlineStyle,
442+
StartAt: ts.Begin.duration(),
407443
}
408-
s.Region = o.Regions[ts.Region]
409-
}
410444

411-
// Add style
412-
if len(ts.Style) > 0 {
413-
if _, ok := o.Styles[ts.Style]; !ok {
414-
err = fmt.Errorf("astisub: Style %s requested by subtitle between %s and %s doesn't exist", ts.Style, s.StartAt, s.EndAt)
415-
return
445+
// Add region
446+
if len(ts.Region) > 0 {
447+
if _, ok := o.Regions[ts.Region]; !ok {
448+
err = fmt.Errorf("astisub: Region %s requested by subtitle between %s and %s doesn't exist", ts.Region, s.StartAt, s.EndAt)
449+
return
450+
}
451+
s.Region = o.Regions[ts.Region]
416452
}
417-
s.Style = o.Styles[ts.Style]
418-
}
419453

420-
// Remove items identation
421-
lines := strings.Split(ts.Items, "\n")
422-
for i := 0; i < len(lines); i++ {
423-
lines[i] = strings.TrimLeftFunc(lines[i], unicode.IsSpace)
424-
}
454+
// Add style
455+
if len(ts.Style) > 0 {
456+
if _, ok := o.Styles[ts.Style]; !ok {
457+
err = fmt.Errorf("astisub: Style %s requested by subtitle between %s and %s doesn't exist", ts.Style, s.StartAt, s.EndAt)
458+
return
459+
}
460+
s.Style = o.Styles[ts.Style]
461+
}
425462

426-
// Unmarshal items
427-
var items = TTMLInItems{}
428-
if err = newTTMLXmlDecoder(strings.Join(lines, "")).Decode(&items); err != nil {
429-
err = fmt.Errorf("astisub: unmarshaling items failed: %w", err)
430-
return
431-
}
463+
// Remove items identation
464+
lines := strings.Split(ts.Items, "\n")
465+
for i := 0; i < len(lines); i++ {
466+
lines[i] = strings.TrimLeftFunc(lines[i], unicode.IsSpace)
467+
}
432468

433-
// Loop through texts
434-
var l = &Line{}
435-
for _, tt := range items {
436-
// New line specified with the "br" tag
437-
if strings.ToLower(tt.XMLName.Local) == "br" {
438-
s.Lines = append(s.Lines, *l)
439-
l = &Line{}
440-
continue
469+
// Unmarshal items
470+
var items = TTMLInItems{}
471+
if err = newTTMLXmlDecoder(strings.Join(lines, "")).Decode(&items); err != nil {
472+
err = fmt.Errorf("astisub: unmarshaling items failed: %w", err)
473+
return
441474
}
442475

443-
// New line decoded as a line break. This can happen if there's a "br" tag within the text since
444-
// since the go xml unmarshaler will unmarshal a "br" tag as a line break if the field has the
445-
// chardata xml tag.
446-
for idx, li := range strings.Split(tt.Text, "\n") {
447-
// New line
448-
if idx > 0 {
476+
// Loop through texts
477+
var l = &Line{}
478+
for _, tt := range items {
479+
// New line specified with the "br" tag
480+
if strings.ToLower(tt.XMLName.Local) == "br" {
449481
s.Lines = append(s.Lines, *l)
450482
l = &Line{}
483+
continue
451484
}
452485

453-
// Init line item
454-
var t = LineItem{
455-
InlineStyle: tt.TTMLInStyleAttributes.styleAttributes(),
456-
Text: li,
457-
}
486+
// New line decoded as a line break. This can happen if there's a "br" tag within the text since
487+
// since the go xml unmarshaler will unmarshal a "br" tag as a line break if the field has the
488+
// chardata xml tag.
489+
for idx, li := range strings.Split(tt.Text, "\n") {
490+
// New line
491+
if idx > 0 {
492+
s.Lines = append(s.Lines, *l)
493+
l = &Line{}
494+
}
458495

459-
// Add style
460-
if len(tt.Style) > 0 {
461-
if _, ok := o.Styles[tt.Style]; !ok {
462-
err = fmt.Errorf("astisub: Style %s requested by item with text %s doesn't exist", tt.Style, tt.Text)
463-
return
496+
// Init line item
497+
var t = LineItem{
498+
InlineStyle: tt.TTMLInStyleAttributes.styleAttributes(),
499+
Text: li,
464500
}
465-
t.Style = o.Styles[tt.Style]
501+
502+
// Add style
503+
if len(tt.Style) > 0 {
504+
if _, ok := o.Styles[tt.Style]; !ok {
505+
err = fmt.Errorf("astisub: Style %s requested by item with text %s doesn't exist", tt.Style, tt.Text)
506+
return
507+
}
508+
t.Style = o.Styles[tt.Style]
509+
}
510+
511+
// Append items
512+
l.Items = append(l.Items, t)
466513
}
467514

468-
// Append items
469-
l.Items = append(l.Items, t)
470515
}
516+
s.Lines = append(s.Lines, *l)
471517

518+
// Append subtitle
519+
o.Items = append(o.Items, s)
472520
}
473-
s.Lines = append(s.Lines, *l)
474-
475-
// Append subtitle
476-
o.Items = append(o.Items, s)
477521
}
478522
return
479523
}

0 commit comments

Comments
 (0)