mirror of
https://github.com/zoriya/go-astisub.git
synced 2026-08-16 02:33:21 +00:00
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
This commit is contained in:
+103
@@ -430,6 +430,109 @@ func (sa *StyleAttributes) propagateWebVTTAttributes() {
|
||||
sa.SRTUnderline = sa.WebVTTUnderline
|
||||
}
|
||||
|
||||
// merge - base on parent, override style attributes if defined in child
|
||||
// TODO: handle more formats than just TTML
|
||||
func (sa *StyleAttributes) merge(parent *StyleAttributes) {
|
||||
if parent == nil || sa == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if sa.TTMLBackgroundColor == nil {
|
||||
sa.TTMLBackgroundColor = parent.TTMLBackgroundColor
|
||||
}
|
||||
|
||||
if sa.TTMLColor == nil {
|
||||
sa.TTMLColor = parent.TTMLColor
|
||||
}
|
||||
|
||||
if sa.TTMLDirection == nil {
|
||||
sa.TTMLDirection = parent.TTMLDirection
|
||||
}
|
||||
|
||||
if sa.TTMLDisplay == nil {
|
||||
sa.TTMLDisplay = parent.TTMLDisplay
|
||||
}
|
||||
|
||||
if sa.TTMLDisplayAlign == nil {
|
||||
sa.TTMLDisplayAlign = parent.TTMLDisplayAlign
|
||||
}
|
||||
|
||||
if sa.TTMLExtent == nil {
|
||||
sa.TTMLExtent = parent.TTMLExtent
|
||||
}
|
||||
|
||||
if sa.TTMLFontFamily == nil {
|
||||
sa.TTMLFontFamily = parent.TTMLFontFamily
|
||||
}
|
||||
|
||||
if sa.TTMLFontSize == nil {
|
||||
sa.TTMLFontSize = parent.TTMLFontSize
|
||||
}
|
||||
|
||||
if sa.TTMLFontStyle == nil {
|
||||
sa.TTMLFontStyle = parent.TTMLFontStyle
|
||||
}
|
||||
if sa.TTMLFontWeight == nil {
|
||||
sa.TTMLFontWeight = parent.TTMLFontWeight
|
||||
}
|
||||
|
||||
if sa.TTMLLineHeight == nil {
|
||||
sa.TTMLLineHeight = parent.TTMLLineHeight
|
||||
}
|
||||
|
||||
if sa.TTMLOpacity == nil {
|
||||
sa.TTMLOpacity = parent.TTMLOpacity
|
||||
}
|
||||
|
||||
if sa.TTMLOrigin == nil {
|
||||
sa.TTMLOrigin = parent.TTMLOrigin
|
||||
}
|
||||
|
||||
if sa.TTMLOverflow == nil {
|
||||
sa.TTMLOverflow = parent.TTMLOverflow
|
||||
}
|
||||
|
||||
if sa.TTMLPadding == nil {
|
||||
sa.TTMLPadding = parent.TTMLPadding
|
||||
}
|
||||
|
||||
if sa.TTMLShowBackground == nil {
|
||||
sa.TTMLShowBackground = parent.TTMLShowBackground
|
||||
}
|
||||
|
||||
if sa.TTMLTextAlign == nil {
|
||||
sa.TTMLTextAlign = parent.TTMLTextAlign
|
||||
}
|
||||
|
||||
if sa.TTMLTextDecoration == nil {
|
||||
sa.TTMLTextDecoration = parent.TTMLTextDecoration
|
||||
}
|
||||
|
||||
if sa.TTMLTextOutline == nil {
|
||||
sa.TTMLTextOutline = parent.TTMLTextOutline
|
||||
}
|
||||
|
||||
if sa.TTMLUnicodeBidi == nil {
|
||||
sa.TTMLUnicodeBidi = parent.TTMLUnicodeBidi
|
||||
}
|
||||
|
||||
if sa.TTMLVisibility == nil {
|
||||
sa.TTMLVisibility = parent.TTMLVisibility
|
||||
}
|
||||
|
||||
if sa.TTMLWrapOption == nil {
|
||||
sa.TTMLWrapOption = parent.TTMLWrapOption
|
||||
}
|
||||
|
||||
if sa.TTMLWritingMode == nil {
|
||||
sa.TTMLWritingMode = parent.TTMLWritingMode
|
||||
}
|
||||
|
||||
if sa.TTMLZIndex == nil {
|
||||
sa.TTMLZIndex = parent.TTMLZIndex
|
||||
}
|
||||
}
|
||||
|
||||
// Metadata represents metadata
|
||||
// TODO Merge attributes
|
||||
type Metadata struct {
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
<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">
|
||||
<head>
|
||||
<metadata>
|
||||
<ttm:title>Title test</ttm:title>
|
||||
<ttm:copyright>Copyright test</ttm:copyright>
|
||||
</metadata>
|
||||
<styling>
|
||||
<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%"/>
|
||||
<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%"/>
|
||||
<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%"/>
|
||||
</styling>
|
||||
<layout>
|
||||
<region xml:id="region_0" tt:style="style_0" tt:color="blue"/>
|
||||
<region xml:id="region_1" tt:style="style_1"/>
|
||||
<region xml:id="region_2" tt:style="style_2"/>
|
||||
</layout>
|
||||
</head>
|
||||
<body region="region_0" style="style_0">
|
||||
<div style="style_1" tts:color="yellow">
|
||||
<p xml:id="sub_1" begin="00:00:00.000" end="00:01:00.000" color="red">
|
||||
text1.0
|
||||
<span style="style_1" color="black">text1.1</span>
|
||||
</p>
|
||||
</div>
|
||||
<div region="region_1">
|
||||
<p xml:id="sub_2" begin="00:01:00.000" end="00:02:00.000">
|
||||
<span style="style_1">text2</span>
|
||||
</p>
|
||||
<p xml:id="sub_3" begin="00:01:00.000" end="00:02:00.000" region="region_2">
|
||||
text2.1
|
||||
</p>
|
||||
</div>
|
||||
<div tts:color="blue">
|
||||
<p xml:id="sub_2" begin="00:02:00.000" end="00:03:00.000">
|
||||
<span style="style_1">text3</span>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</tt>
|
||||
@@ -41,17 +41,34 @@ var (
|
||||
ttmlRegexpOffsetTime = regexp.MustCompile(`^(\d+(\.\d+)?)(h|m|s|ms|f|t)$`)
|
||||
)
|
||||
|
||||
type TTMLInBodyDiv struct {
|
||||
XMLName xml.Name `xml:"div"`
|
||||
Subtitles []TTMLInSubtitle `xml:"p"`
|
||||
|
||||
Region string `xml:"region,attr,omitempty"`
|
||||
Style string `xml:"style,attr,omitempty"`
|
||||
TTMLInStyleAttributes
|
||||
}
|
||||
type TTMLInBody struct {
|
||||
XMLName xml.Name `xml:"body"`
|
||||
Divs []TTMLInBodyDiv `xml:"div"`
|
||||
|
||||
Region string `xml:"region,attr,omitempty"`
|
||||
Style string `xml:"style,attr,omitempty"`
|
||||
TTMLInStyleAttributes
|
||||
}
|
||||
|
||||
// TTMLIn represents an input TTML that must be unmarshaled
|
||||
// We split it from the output TTML as we can't add strict namespace without breaking retrocompatibility
|
||||
type TTMLIn struct {
|
||||
Framerate int `xml:"frameRate,attr"`
|
||||
Lang string `xml:"lang,attr"`
|
||||
Metadata TTMLInMetadata `xml:"head>metadata"`
|
||||
Regions []TTMLInRegion `xml:"head>layout>region"`
|
||||
Styles []TTMLInStyle `xml:"head>styling>style"`
|
||||
Subtitles []TTMLInSubtitle `xml:"body>div>p"`
|
||||
Tickrate int `xml:"tickRate,attr"`
|
||||
XMLName xml.Name `xml:"tt"`
|
||||
Framerate int `xml:"frameRate,attr"`
|
||||
Lang string `xml:"lang,attr"`
|
||||
Metadata TTMLInMetadata `xml:"head>metadata"`
|
||||
Regions []TTMLInRegion `xml:"head>layout>region"`
|
||||
Styles []TTMLInStyle `xml:"head>styling>style"`
|
||||
Body TTMLInBody `xml:"body"`
|
||||
Tickrate int `xml:"tickRate,attr"`
|
||||
XMLName xml.Name `xml:"tt"`
|
||||
}
|
||||
|
||||
// metadata returns the Metadata of the TTML
|
||||
@@ -386,94 +403,121 @@ func ReadFromTTML(i io.Reader) (o *Subtitles, err error) {
|
||||
}
|
||||
|
||||
// Loop through subtitles
|
||||
for _, ts := range ttml.Subtitles {
|
||||
// Init item
|
||||
ts.Begin.framerate = ttml.Framerate
|
||||
ts.Begin.tickrate = ttml.Tickrate
|
||||
ts.End.framerate = ttml.Framerate
|
||||
ts.End.tickrate = ttml.Tickrate
|
||||
bodyInlineStyle := ttml.Body.TTMLInStyleAttributes.styleAttributes()
|
||||
for _, div := range ttml.Body.Divs {
|
||||
divInlineStyle := div.TTMLInStyleAttributes.styleAttributes()
|
||||
|
||||
var s = &Item{
|
||||
EndAt: ts.End.duration(),
|
||||
InlineStyle: ts.TTMLInStyleAttributes.styleAttributes(),
|
||||
StartAt: ts.Begin.duration(),
|
||||
// Propagate styles from Body -> Div
|
||||
divInlineStyle.merge(bodyInlineStyle)
|
||||
if div.Region == "" {
|
||||
div.Region = ttml.Body.Region
|
||||
}
|
||||
if div.Style == "" {
|
||||
div.Style = ttml.Body.Style
|
||||
}
|
||||
for _, ts := range div.Subtitles {
|
||||
// Init item
|
||||
ts.Begin.framerate = ttml.Framerate
|
||||
ts.Begin.tickrate = ttml.Tickrate
|
||||
ts.End.framerate = ttml.Framerate
|
||||
ts.End.tickrate = ttml.Tickrate
|
||||
|
||||
// Add region
|
||||
if len(ts.Region) > 0 {
|
||||
if _, ok := o.Regions[ts.Region]; !ok {
|
||||
err = fmt.Errorf("astisub: Region %s requested by subtitle between %s and %s doesn't exist", ts.Region, s.StartAt, s.EndAt)
|
||||
itemInlineStyle := ts.TTMLInStyleAttributes.styleAttributes()
|
||||
|
||||
// Propagate styles from Body -> Div -> Item.
|
||||
// If the item has its own Region, Style, or InlineStyle, it overrides the Div's.
|
||||
// This ensures all relevant styles are preserved at the item level,
|
||||
// maintaining compatibility with existing logic that relies on the Subtitles structure.
|
||||
itemInlineStyle.merge(divInlineStyle)
|
||||
if ts.Region == "" {
|
||||
ts.Region = div.Region
|
||||
}
|
||||
if ts.Style == "" {
|
||||
ts.Style = div.Style
|
||||
}
|
||||
|
||||
var s = &Item{
|
||||
EndAt: ts.End.duration(),
|
||||
InlineStyle: itemInlineStyle,
|
||||
StartAt: ts.Begin.duration(),
|
||||
}
|
||||
|
||||
// Add region
|
||||
if len(ts.Region) > 0 {
|
||||
if _, ok := o.Regions[ts.Region]; !ok {
|
||||
err = fmt.Errorf("astisub: Region %s requested by subtitle between %s and %s doesn't exist", ts.Region, s.StartAt, s.EndAt)
|
||||
return
|
||||
}
|
||||
s.Region = o.Regions[ts.Region]
|
||||
}
|
||||
|
||||
// Add style
|
||||
if len(ts.Style) > 0 {
|
||||
if _, ok := o.Styles[ts.Style]; !ok {
|
||||
err = fmt.Errorf("astisub: Style %s requested by subtitle between %s and %s doesn't exist", ts.Style, s.StartAt, s.EndAt)
|
||||
return
|
||||
}
|
||||
s.Style = o.Styles[ts.Style]
|
||||
}
|
||||
|
||||
// Remove items identation
|
||||
lines := strings.Split(ts.Items, "\n")
|
||||
for i := 0; i < len(lines); i++ {
|
||||
lines[i] = strings.TrimLeftFunc(lines[i], unicode.IsSpace)
|
||||
}
|
||||
|
||||
// Unmarshal items
|
||||
var items = TTMLInItems{}
|
||||
if err = newTTMLXmlDecoder(strings.Join(lines, "")).Decode(&items); err != nil {
|
||||
err = fmt.Errorf("astisub: unmarshaling items failed: %w", err)
|
||||
return
|
||||
}
|
||||
s.Region = o.Regions[ts.Region]
|
||||
}
|
||||
|
||||
// Add style
|
||||
if len(ts.Style) > 0 {
|
||||
if _, ok := o.Styles[ts.Style]; !ok {
|
||||
err = fmt.Errorf("astisub: Style %s requested by subtitle between %s and %s doesn't exist", ts.Style, s.StartAt, s.EndAt)
|
||||
return
|
||||
}
|
||||
s.Style = o.Styles[ts.Style]
|
||||
}
|
||||
|
||||
// Remove items identation
|
||||
lines := strings.Split(ts.Items, "\n")
|
||||
for i := 0; i < len(lines); i++ {
|
||||
lines[i] = strings.TrimLeftFunc(lines[i], unicode.IsSpace)
|
||||
}
|
||||
|
||||
// Unmarshal items
|
||||
var items = TTMLInItems{}
|
||||
if err = newTTMLXmlDecoder(strings.Join(lines, "")).Decode(&items); err != nil {
|
||||
err = fmt.Errorf("astisub: unmarshaling items failed: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Loop through texts
|
||||
var l = &Line{}
|
||||
for _, tt := range items {
|
||||
// New line specified with the "br" tag
|
||||
if strings.ToLower(tt.XMLName.Local) == "br" {
|
||||
s.Lines = append(s.Lines, *l)
|
||||
l = &Line{}
|
||||
continue
|
||||
}
|
||||
|
||||
// New line decoded as a line break. This can happen if there's a "br" tag within the text since
|
||||
// since the go xml unmarshaler will unmarshal a "br" tag as a line break if the field has the
|
||||
// chardata xml tag.
|
||||
for idx, li := range strings.Split(tt.Text, "\n") {
|
||||
// New line
|
||||
if idx > 0 {
|
||||
// Loop through texts
|
||||
var l = &Line{}
|
||||
for _, tt := range items {
|
||||
// New line specified with the "br" tag
|
||||
if strings.ToLower(tt.XMLName.Local) == "br" {
|
||||
s.Lines = append(s.Lines, *l)
|
||||
l = &Line{}
|
||||
continue
|
||||
}
|
||||
|
||||
// Init line item
|
||||
var t = LineItem{
|
||||
InlineStyle: tt.TTMLInStyleAttributes.styleAttributes(),
|
||||
Text: li,
|
||||
}
|
||||
|
||||
// Add style
|
||||
if len(tt.Style) > 0 {
|
||||
if _, ok := o.Styles[tt.Style]; !ok {
|
||||
err = fmt.Errorf("astisub: Style %s requested by item with text %s doesn't exist", tt.Style, tt.Text)
|
||||
return
|
||||
// New line decoded as a line break. This can happen if there's a "br" tag within the text since
|
||||
// since the go xml unmarshaler will unmarshal a "br" tag as a line break if the field has the
|
||||
// chardata xml tag.
|
||||
for idx, li := range strings.Split(tt.Text, "\n") {
|
||||
// New line
|
||||
if idx > 0 {
|
||||
s.Lines = append(s.Lines, *l)
|
||||
l = &Line{}
|
||||
}
|
||||
t.Style = o.Styles[tt.Style]
|
||||
|
||||
// Init line item
|
||||
var t = LineItem{
|
||||
InlineStyle: tt.TTMLInStyleAttributes.styleAttributes(),
|
||||
Text: li,
|
||||
}
|
||||
|
||||
// Add style
|
||||
if len(tt.Style) > 0 {
|
||||
if _, ok := o.Styles[tt.Style]; !ok {
|
||||
err = fmt.Errorf("astisub: Style %s requested by item with text %s doesn't exist", tt.Style, tt.Text)
|
||||
return
|
||||
}
|
||||
t.Style = o.Styles[tt.Style]
|
||||
}
|
||||
|
||||
// Append items
|
||||
l.Items = append(l.Items, t)
|
||||
}
|
||||
|
||||
// Append items
|
||||
l.Items = append(l.Items, t)
|
||||
}
|
||||
s.Lines = append(s.Lines, *l)
|
||||
|
||||
// Append subtitle
|
||||
o.Items = append(o.Items, s)
|
||||
}
|
||||
s.Lines = append(s.Lines, *l)
|
||||
|
||||
// Append subtitle
|
||||
o.Items = append(o.Items, s)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -85,3 +85,20 @@ func TestWriteToTTMLWithIndentOption(t *testing.T) {
|
||||
|
||||
assert.Equal(t, strings.TrimSpace(string(c)), strings.TrimSpace(w.String()))
|
||||
}
|
||||
|
||||
func TestTTMLMergeStyleAttributes(t *testing.T) {
|
||||
// Open
|
||||
s, err := astisub.OpenFile("./testdata/example-in-merging-style.ttml")
|
||||
assert.NoError(t, err)
|
||||
// Styles
|
||||
assert.Equal(t, 4, len(s.Items))
|
||||
assert.Equal(t, s.Items[0].Region.ID, "region_0")
|
||||
assert.Equal(t, s.Items[0].Style.ID, "style_1")
|
||||
assert.Equal(t, *s.Items[0].InlineStyle.TTMLColor, "red")
|
||||
assert.Equal(t, s.Items[1].Region.ID, "region_1")
|
||||
assert.Equal(t, s.Items[1].Style.ID, "style_0")
|
||||
assert.Equal(t, s.Items[1].Lines[0].Items[0].Style.ID, "style_1")
|
||||
assert.Equal(t, s.Items[2].Region.ID, "region_2")
|
||||
assert.Equal(t, s.Items[2].Style.ID, "style_0")
|
||||
assert.Equal(t, *s.Items[3].InlineStyle.TTMLColor, "blue")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user