Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 83 additions & 12 deletions subtitles.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ type StyleAttributes struct {
WebVTTItalics bool
WebVTTLine string
WebVTTLines int
WebVTTPosition string
WebVTTPosition *WebVTTPosition
WebVTTRegionAnchor string
WebVTTScroll string
WebVTTSize string
Expand Down Expand Up @@ -307,28 +307,28 @@ func (sa *StyleAttributes) propagateSRTAttributes() {
switch sa.SRTPosition {
case 7: // top-left
sa.WebVTTAlign = "left"
sa.WebVTTPosition = "10%"
sa.WebVTTPosition = newWebVTTPosition("10%")
case 8: // top-center
sa.WebVTTPosition = "10%"
sa.WebVTTPosition = newWebVTTPosition("10%")
case 9: // top-right
sa.WebVTTAlign = "right"
sa.WebVTTPosition = "10%"
sa.WebVTTPosition = newWebVTTPosition("10%")
case 4: // middle-left
sa.WebVTTAlign = "left"
sa.WebVTTPosition = "50%"
sa.WebVTTPosition = newWebVTTPosition("50%")
case 5: // middle-center
sa.WebVTTPosition = "50%"
sa.WebVTTPosition = newWebVTTPosition("50%")
case 6: // middle-right
sa.WebVTTAlign = "right"
sa.WebVTTPosition = "50%"
sa.WebVTTPosition = newWebVTTPosition("50%")
case 1: // bottom-left
sa.WebVTTAlign = "left"
sa.WebVTTPosition = "90%"
sa.WebVTTPosition = newWebVTTPosition("90%")
case 2: // bottom-center
sa.WebVTTPosition = "90%"
sa.WebVTTPosition = newWebVTTPosition("90%")
case 3: // bottom-right
sa.WebVTTAlign = "right"
sa.WebVTTPosition = "90%"
sa.WebVTTPosition = newWebVTTPosition("90%")
}

sa.WebVTTBold = sa.SRTBold
Expand Down Expand Up @@ -411,10 +411,10 @@ func (sa *StyleAttributes) propagateTTMLAttributes() {
coordinates := strings.Split(*sa.TTMLOrigin, " ")
if len(coordinates) > 1 {
sa.WebVTTLine = coordinates[0]
sa.WebVTTPosition = coordinates[1]
sa.WebVTTPosition = newWebVTTPosition(coordinates[1])
if sa.TTMLWritingMode != nil && strings.HasPrefix(*sa.TTMLWritingMode, "tb") {
sa.WebVTTLine = coordinates[1]
sa.WebVTTPosition = coordinates[0]
sa.WebVTTPosition = newWebVTTPosition(coordinates[0])
}
}
}
Expand All @@ -428,6 +428,77 @@ func (sa *StyleAttributes) propagateWebVTTAttributes() {
sa.SRTBold = sa.WebVTTBold
sa.SRTItalics = sa.WebVTTItalics
sa.SRTUnderline = sa.WebVTTUnderline

// may be overridden by position parsing later
switch sa.WebVTTAlign {
case "left", "right", "center", "start", "end":
sa.TTMLTextAlign = astikit.StrPtr(sa.WebVTTAlign)
}

for _, tag := range sa.WebVTTTags {
switch tag.Name {
case "c":
if len(tag.Classes) > 0 {
for _, color := range tag.Classes {
if strings.HasPrefix(color, "bg_") && len(color) > 3 {
if bgColor, err := newColorFromWebVTTString(color[3:]); err == nil {
sa.TTMLBackgroundColor = astikit.StrPtr("#" + bgColor.TTMLString())
}
} else {
if fgColor, err := newColorFromWebVTTString(color); err == nil {
sa.TTMLColor = astikit.StrPtr("#" + fgColor.TTMLString())
}
}
}
}
}
}

// Handle WebVTT position and alignment conversion
sa.propagateWebVTTPosition()

if sa.WebVTTSize != "" {
sa.TTMLExtent = astikit.StrPtr(fmt.Sprintf("%s 10%%", sa.WebVTTSize))
}
}

// propagateWebVTTPosition handles WebVTT position and line attributes conversion
func (sa *StyleAttributes) propagateWebVTTPosition() {
var yPos string
var hasPosition, hasLine bool

// Parse position if available
if sa.WebVTTPosition != nil {
hasPosition = true

// Handle position alignment (takes precedence over WebVTTAlign)
switch sa.WebVTTPosition.Alignment {
case "line-left":
sa.TTMLTextAlign = astikit.StrPtr("left")
case "center":
sa.TTMLTextAlign = astikit.StrPtr("center")
case "line-right":
sa.TTMLTextAlign = astikit.StrPtr("right")
}
}

// Handle line if available
if sa.WebVTTLine != "" {
hasLine = true
yPos = sa.WebVTTLine
}

// Set TTMLOrigin based on available position and line data
if hasPosition && hasLine {
// Both position and line are available
sa.TTMLOrigin = astikit.StrPtr(fmt.Sprintf("%s %s", sa.WebVTTPosition.XPosition, yPos))
} else if hasPosition {
// Only position is available, use default Y position (80% for bottom)
sa.TTMLOrigin = astikit.StrPtr(fmt.Sprintf("%s 80%%", sa.WebVTTPosition.XPosition))
} else if hasLine {
// Only line is available, use default X position (10% for left)
sa.TTMLOrigin = astikit.StrPtr(fmt.Sprintf("10%% %s", yPos))
}
}

// merge - base on parent, override style attributes if defined in child
Expand Down
2 changes: 1 addition & 1 deletion testdata/example-in.vtt
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ NOTE This a comment inside the VTT
and this is the second line

2
00:02:04.08 --> 00:02:07.12 region:fred position:10%,start align:left size:35%
00:02:04.08 --> 00:02:07.12 region:fred position:10%,line-left align:left size:35%
MAN:
How did we end up here?

Expand Down
2 changes: 1 addition & 1 deletion testdata/example-out.vtt
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ NOTE This a comment inside the VTT
and this is the second line

2
00:02:04.080 --> 00:02:07.120 align:left position:10%,start region:fred size:35%
00:02:04.080 --> 00:02:07.120 align:left position:10%,line-left region:fred size:35%
MAN:
How did we end up here?

Expand Down
6 changes: 3 additions & 3 deletions ttml_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ func TestTTML(t *testing.T) {
assert.Equal(t, &astisub.Metadata{Framerate: 25, Language: astisub.LanguageFrench, Title: "Title test", TTMLCopyright: "Copyright test"}, s.Metadata)
// Styles
assert.Equal(t, 3, len(s.Styles))
assert.Equal(t, astisub.Style{ID: "style_0", InlineStyle: &astisub.StyleAttributes{TTMLColor: astikit.StrPtr("white"), TTMLExtent: astikit.StrPtr("100% 10%"), TTMLFontFamily: astikit.StrPtr("sansSerif"), TTMLFontStyle: astikit.StrPtr("normal"), TTMLOrigin: astikit.StrPtr("0% 90%"), TTMLTextAlign: astikit.StrPtr("center"), WebVTTAlign: "center", WebVTTLine: "0%", WebVTTLines: 2, WebVTTPosition: "90%", WebVTTRegionAnchor: "0%,0%", WebVTTScroll: "up", WebVTTSize: "10%", WebVTTViewportAnchor: "0%,90%", WebVTTWidth: "100%"}, Style: s.Styles["style_2"]}, *s.Styles["style_0"])
assert.Equal(t, astisub.Style{ID: "style_1", InlineStyle: &astisub.StyleAttributes{TTMLColor: astikit.StrPtr("white"), TTMLExtent: astikit.StrPtr("100% 13%"), TTMLFontFamily: astikit.StrPtr("sansSerif"), TTMLFontStyle: astikit.StrPtr("normal"), TTMLOrigin: astikit.StrPtr("0% 87%"), TTMLTextAlign: astikit.StrPtr("center"), WebVTTAlign: "center", WebVTTLine: "0%", WebVTTLines: 2, WebVTTPosition: "87%", WebVTTRegionAnchor: "0%,0%", WebVTTScroll: "up", WebVTTSize: "13%", WebVTTViewportAnchor: "0%,87%", WebVTTWidth: "100%"}}, *s.Styles["style_1"])
assert.Equal(t, astisub.Style{ID: "style_2", InlineStyle: &astisub.StyleAttributes{TTMLColor: astikit.StrPtr("white"), TTMLExtent: astikit.StrPtr("100% 20%"), TTMLFontFamily: astikit.StrPtr("sansSerif"), TTMLFontStyle: astikit.StrPtr("normal"), TTMLOrigin: astikit.StrPtr("0% 80%"), TTMLTextAlign: astikit.StrPtr("center"), WebVTTAlign: "center", WebVTTLine: "0%", WebVTTLines: 4, WebVTTPosition: "80%", WebVTTRegionAnchor: "0%,0%", WebVTTScroll: "up", WebVTTSize: "20%", WebVTTViewportAnchor: "0%,80%", WebVTTWidth: "100%"}}, *s.Styles["style_2"])
assert.Equal(t, astisub.Style{ID: "style_0", InlineStyle: &astisub.StyleAttributes{TTMLColor: astikit.StrPtr("white"), TTMLExtent: astikit.StrPtr("100% 10%"), TTMLFontFamily: astikit.StrPtr("sansSerif"), TTMLFontStyle: astikit.StrPtr("normal"), TTMLOrigin: astikit.StrPtr("0% 90%"), TTMLTextAlign: astikit.StrPtr("center"), WebVTTAlign: "center", WebVTTLine: "0%", WebVTTLines: 2, WebVTTPosition: &astisub.WebVTTPosition{XPosition: "90%"}, WebVTTRegionAnchor: "0%,0%", WebVTTScroll: "up", WebVTTSize: "10%", WebVTTViewportAnchor: "0%,90%", WebVTTWidth: "100%"}, Style: s.Styles["style_2"]}, *s.Styles["style_0"])
assert.Equal(t, astisub.Style{ID: "style_1", InlineStyle: &astisub.StyleAttributes{TTMLColor: astikit.StrPtr("white"), TTMLExtent: astikit.StrPtr("100% 13%"), TTMLFontFamily: astikit.StrPtr("sansSerif"), TTMLFontStyle: astikit.StrPtr("normal"), TTMLOrigin: astikit.StrPtr("0% 87%"), TTMLTextAlign: astikit.StrPtr("center"), WebVTTAlign: "center", WebVTTLine: "0%", WebVTTLines: 2, WebVTTPosition: &astisub.WebVTTPosition{XPosition: "87%"}, WebVTTRegionAnchor: "0%,0%", WebVTTScroll: "up", WebVTTSize: "13%", WebVTTViewportAnchor: "0%,87%", WebVTTWidth: "100%"}}, *s.Styles["style_1"])
assert.Equal(t, astisub.Style{ID: "style_2", InlineStyle: &astisub.StyleAttributes{TTMLColor: astikit.StrPtr("white"), TTMLExtent: astikit.StrPtr("100% 20%"), TTMLFontFamily: astikit.StrPtr("sansSerif"), TTMLFontStyle: astikit.StrPtr("normal"), TTMLOrigin: astikit.StrPtr("0% 80%"), TTMLTextAlign: astikit.StrPtr("center"), WebVTTAlign: "center", WebVTTLine: "0%", WebVTTLines: 4, WebVTTPosition: &astisub.WebVTTPosition{XPosition: "80%"}, WebVTTRegionAnchor: "0%,0%", WebVTTScroll: "up", WebVTTSize: "20%", WebVTTViewportAnchor: "0%,80%", WebVTTWidth: "100%"}}, *s.Styles["style_2"])
// Regions
assert.Equal(t, 3, len(s.Regions))
assert.Equal(t, astisub.Region{ID: "region_0", Style: s.Styles["style_0"], InlineStyle: &astisub.StyleAttributes{TTMLColor: astikit.StrPtr("blue")}}, *s.Regions["region_0"])
Expand Down
119 changes: 111 additions & 8 deletions webvtt.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,39 @@ var (
webVTTRegexpTag = regexp.MustCompile(`(</*\s*([^\.\s]+)(\.[^\s/]*)*\s*([^/]*)\s*/*>)`)
)

type WebVTTPosition struct {
XPosition string
Alignment string
}

// newWebVTTPosition creates a new WebVTTPosition from a string.
// The string can be in the format "XPosition,Alignment" or just "XPosition".
func newWebVTTPosition(s string) *WebVTTPosition {
if s == "" {
return nil
}

parts := strings.Split(s, ",")
if len(parts) != 2 {
return &WebVTTPosition{XPosition: strings.TrimSpace(s)}
}

return &WebVTTPosition{
XPosition: strings.TrimSpace(parts[0]),
Alignment: strings.TrimSpace(parts[1]),
}
}

func (p *WebVTTPosition) String() string {
if p == nil {
return ""
}
if p.Alignment != "" {
return fmt.Sprintf("%s,%s", p.XPosition, p.Alignment)
}
return p.XPosition
}

// parseDurationWebVTT parses a .vtt duration
func parseDurationWebVTT(i string) (time.Duration, error) {
return parseDuration(i, ".", 3)
Expand Down Expand Up @@ -272,7 +305,7 @@ func ReadFromWebVTT(i io.Reader) (o *Subtitles, err error) {
case "line":
item.InlineStyle.WebVTTLine = split[1]
case "position":
item.InlineStyle.WebVTTPosition = split[1]
item.InlineStyle.WebVTTPosition = newWebVTTPosition(split[1])
case "region":
if _, ok := o.Regions[split[1]]; !ok {
err = fmt.Errorf("astisub: line %d: Unknown region %s", lineNum, split[1])
Expand Down Expand Up @@ -574,12 +607,12 @@ func (s Subtitles) WriteToWebVTT(o io.Writer) (err error) {
c = append(c, bytesSpace...)
c = append(c, []byte("line:"+item.Style.InlineStyle.WebVTTLine)...)
}
if item.InlineStyle.WebVTTPosition != "" {
if item.InlineStyle.WebVTTPosition != nil {
c = append(c, bytesSpace...)
c = append(c, []byte("position:"+item.InlineStyle.WebVTTPosition)...)
} else if item.Style != nil && item.Style.InlineStyle != nil && item.Style.InlineStyle.WebVTTPosition != "" {
c = append(c, []byte("position:"+item.InlineStyle.WebVTTPosition.String())...)
} else if item.Style != nil && item.Style.InlineStyle != nil && item.Style.InlineStyle.WebVTTPosition != nil {
c = append(c, bytesSpace...)
c = append(c, []byte("position:"+item.Style.InlineStyle.WebVTTPosition)...)
c = append(c, []byte("position:"+item.Style.InlineStyle.WebVTTPosition.String())...)
}
if item.Region != nil {
c = append(c, bytesSpace...)
Expand Down Expand Up @@ -648,10 +681,21 @@ func (li LineItem) webVTTBytes(previous, next *LineItem) (c []byte) {
c = append(c, []byte("<"+formatDurationWebVTT(li.StartAt)+">")...)
}

// Get color
// Get color - only add TTMLColor-based tag if there are no WebVTT color tags
var color string
if li.InlineStyle != nil && li.InlineStyle.TTMLColor != nil {
color = cssColor(*li.InlineStyle.TTMLColor)
var hasColorTags bool
if li.InlineStyle != nil {
// Check if we have WebVTT color tags
for _, tag := range li.InlineStyle.WebVTTTags {
if tag.Name == "c" {
hasColorTags = true
break
}
}
// Only use TTMLColor if we don't have WebVTT color tags
if !hasColorTags && li.InlineStyle.TTMLColor != nil {
color = cssColor(*li.InlineStyle.TTMLColor)
}
}

// Append
Expand Down Expand Up @@ -692,3 +736,62 @@ func cssColor(rgb string) string {
}
return colors[strings.ToLower(rgb)] // returning the empty string is ok
}

// webVTTColorMap maps WebVTT color class names to Color instances
var webVTTColorMap = map[string]*Color{
"black": ColorBlack,
"red": ColorRed,
"green": ColorGreen,
"yellow": ColorYellow,
"blue": ColorBlue,
"magenta": ColorMagenta,
"cyan": ColorCyan,
"white": ColorWhite,
"silver": ColorSilver,
"gray": ColorGray,
"maroon": ColorMaroon,
"olive": ColorOlive,
"lime": ColorLime,
"teal": ColorTeal,
"navy": ColorNavy,
"purple": ColorPurple,
}

func newColorFromWebVTTString(color string) (*Color, error) {
switch color {
case "black":
return ColorBlack, nil
case "red":
return ColorRed, nil
case "green":
return ColorGreen, nil
case "yellow":
return ColorYellow, nil
case "blue":
return ColorBlue, nil
case "magenta":
return ColorMagenta, nil
case "cyan":
return ColorCyan, nil
case "white":
return ColorWhite, nil
case "silver":
return ColorSilver, nil
case "gray":
return ColorGray, nil
case "maroon":
return ColorMaroon, nil
case "olive":
return ColorOlive, nil
case "lime":
return ColorLime, nil
case "teal":
return ColorTeal, nil
case "navy":
return ColorNavy, nil
case "purple":
return ColorPurple, nil
default:
return nil, fmt.Errorf("unknown color class %s", color)
}
}
Loading