Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
88 changes: 88 additions & 0 deletions subtitles.go
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,94 @@ 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 := parseWebVTTColorClass(color[3:]); err == nil {
sa.TTMLBackgroundColor = astikit.StrPtr("#" + bgColor.TTMLString())
}
} else {
if fgColor, err := parseWebVTTColorClass(color); err == nil {
sa.TTMLColor = astikit.StrPtr("#" + fgColor.TTMLString())
}
}
}
}
}
}

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

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

// parseWebVTTPosition parses WebVTT position string like "10%,line-left".
// Returns (xPosition, alignment)
func (sa *StyleAttributes) parseWebVTTPosition() (string, string) {
if sa.WebVTTPosition == "" {
return "", ""
}

parts := strings.Split(sa.WebVTTPosition, ",")
if len(parts) != 2 {
return sa.WebVTTPosition, ""
}

return strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])
}

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

// Parse position if available
if sa.WebVTTPosition != "" {
hasPosition = true
var alignment string
xPos, alignment = sa.parseWebVTTPosition()

// Handle position alignment (takes precedence over WebVTTAlign)
switch 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", xPos, yPos))
} else if hasPosition {
// Only position is available, use default Y position (80% for bottom)
sa.TTMLOrigin = astikit.StrPtr(fmt.Sprintf("%s 80%%", xPos))
} 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
44 changes: 41 additions & 3 deletions webvtt.go
Original file line number Diff line number Diff line change
Expand Up @@ -648,10 +648,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 +703,30 @@ 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 parseWebVTTColorClass(color string) (*Color, error) {
if c, ok := webVTTColorMap[color]; ok {
return c, nil
}
return nil, fmt.Errorf("unknown color class %s", color)
}
94 changes: 93 additions & 1 deletion webvtt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"testing"
"time"

"github.com/asticode/go-astikit"
"github.com/asticode/go-astisub"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand All @@ -27,7 +28,16 @@ func TestWebVTT(t *testing.T) {
assert.Equal(t, s.Regions["bill"], s.Items[0].Region)
assert.Equal(t, s.Regions["fred"], s.Items[1].Region)
// Styles
assert.Equal(t, astisub.StyleAttributes{WebVTTAlign: "left", WebVTTPosition: "10%,start", WebVTTSize: "35%"}, *s.Items[1].InlineStyle)
expected := astisub.StyleAttributes{
WebVTTAlign: "left",
WebVTTPosition: "10%,line-left",
WebVTTSize: "35%",
}
// propagateWebVTTAttributes() sets these based on WebVTT attributes
expected.TTMLTextAlign = astikit.StrPtr("left") // From WebVTTAlign since "start" is not a valid position alignment
expected.TTMLOrigin = astikit.StrPtr("10% 80%") // From position: 10% X, 80% Y (default when no line specified)
expected.TTMLExtent = astikit.StrPtr("35% 10%") // From size: 35% width, 10% height (default when no line specified)
assert.Equal(t, expected, *s.Items[1].InlineStyle)

// No subtitles to write
w := &bytes.Buffer{}
Expand Down Expand Up @@ -255,3 +265,85 @@ func TestWebVTTParseDuration(t *testing.T) {
assert.NotNil(t, s.Items[1].InlineStyle)
assert.Equal(t, s.Items[1].InlineStyle.WebVTTAlign, "middle")
}

func TestWebVTTColorToTTML(t *testing.T) {
testData := `WEBVTT

1
00:00:01.000 --> 00:00:03.000
<c.red>Red text</c> and <c.blue.bg_yellow>blue text on yellow background</c>

2
00:00:04.000 --> 00:00:06.000
<c.green>Green text</c> with <c.bg_cyan>text on cyan background</c>

3
00:00:07.000 --> 00:00:09.000
Normal text with <c.magenta>magenta</c> and <c.orange>unknown color</c>`

s, err := astisub.ReadFromWebVTT(strings.NewReader(testData))
require.NoError(t, err)
require.Len(t, s.Items, 3)

// Test item 1: Red text and blue text on yellow background
item1 := s.Items[0]
require.Len(t, item1.Lines, 1)
require.Len(t, item1.Lines[0].Items, 3) // "Red text", " and ", "blue text on yellow background"

// Check red text
redItem := item1.Lines[0].Items[0]
assert.Equal(t, "Red text", redItem.Text)
require.NotNil(t, redItem.InlineStyle)
require.NotNil(t, redItem.InlineStyle.TTMLColor)
assert.Equal(t, "#ff0000", *redItem.InlineStyle.TTMLColor) // Red color
assert.Nil(t, redItem.InlineStyle.TTMLBackgroundColor)

// Check blue text on yellow background
blueYellowItem := item1.Lines[0].Items[2]
assert.Equal(t, "blue text on yellow background", blueYellowItem.Text)
require.NotNil(t, blueYellowItem.InlineStyle)
require.NotNil(t, blueYellowItem.InlineStyle.TTMLColor)
require.NotNil(t, blueYellowItem.InlineStyle.TTMLBackgroundColor)
assert.Equal(t, "#0000ff", *blueYellowItem.InlineStyle.TTMLColor) // Blue color
assert.Equal(t, "#ffff00", *blueYellowItem.InlineStyle.TTMLBackgroundColor) // Yellow background

// Test item 2: Green text and background color only
item2 := s.Items[1]
require.Len(t, item2.Lines, 1)
require.Len(t, item2.Lines[0].Items, 3) // "Green text", " with ", "text on cyan background"

// Check green text
greenItem := item2.Lines[0].Items[0]
assert.Equal(t, "Green text", greenItem.Text)
require.NotNil(t, greenItem.InlineStyle)
require.NotNil(t, greenItem.InlineStyle.TTMLColor)
assert.Equal(t, "#008000", *greenItem.InlineStyle.TTMLColor) // Green color
assert.Nil(t, greenItem.InlineStyle.TTMLBackgroundColor)

// Check text with cyan background only
cyanBgItem := item2.Lines[0].Items[2]
assert.Equal(t, "text on cyan background", cyanBgItem.Text)
require.NotNil(t, cyanBgItem.InlineStyle)
assert.Nil(t, cyanBgItem.InlineStyle.TTMLColor) // No foreground color specified
require.NotNil(t, cyanBgItem.InlineStyle.TTMLBackgroundColor)
assert.Equal(t, "#00ffff", *cyanBgItem.InlineStyle.TTMLBackgroundColor) // Cyan background

// Test item 3: Known and unknown colors
item3 := s.Items[2]
require.Len(t, item3.Lines, 1)
require.Len(t, item3.Lines[0].Items, 4) // "Normal text with ", "magenta", " and ", "unknown color"

// Check magenta text (known color)
magentaItem := item3.Lines[0].Items[1]
assert.Equal(t, "magenta", magentaItem.Text)
require.NotNil(t, magentaItem.InlineStyle)
require.NotNil(t, magentaItem.InlineStyle.TTMLColor)
assert.Equal(t, "#ff00ff", *magentaItem.InlineStyle.TTMLColor) // Magenta color

// Check unknown color (should not set TTMLColor because "orange" is not in webVTTColorMap)
unknownColorItem := item3.Lines[0].Items[3]
assert.Equal(t, "unknown color", unknownColorItem.Text)
require.NotNil(t, unknownColorItem.InlineStyle)
assert.Nil(t, unknownColorItem.InlineStyle.TTMLColor) // Unknown color should not be converted
assert.Nil(t, unknownColorItem.InlineStyle.TTMLBackgroundColor)
}