Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
7 changes: 5 additions & 2 deletions srt.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,10 @@ func parseTextSrt(i string, sa *StyleAttributes) (o Line) {
sa.SRTUnderline = true
case "font":
if c := htmlTokenAttribute(&token, "color"); c != nil {
sa.SRTColor = c
// Parse the color string into a Color struct
if color, err := newColorFromTTMLString(*c); err == nil {
sa.SRTColor = color
}
}
}
case html.TextToken:
Expand Down Expand Up @@ -266,7 +269,7 @@ func (li LineItem) srtBytes() (c []byte) {
// Get color
var color string
if li.InlineStyle != nil && li.InlineStyle.SRTColor != nil {
color = *li.InlineStyle.SRTColor
color = "#" + li.InlineStyle.SRTColor.TTMLString()
}

// Get bold/italics/underline
Expand Down
6 changes: 3 additions & 3 deletions srt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,15 +92,15 @@ func TestSRTStyled(t *testing.T) {
assert.Equal(t, " = 100", s.Items[9].Lines[0].Items[3].Text)

// assert the styles of the items
assert.Equal(t, "#00ff00", *s.Items[0].Lines[0].Items[0].InlineStyle.SRTColor)
assert.Equal(t, astisub.ColorLime, s.Items[0].Lines[0].Items[0].InlineStyle.SRTColor)
assert.True(t, s.Items[0].Lines[0].Items[0].InlineStyle.SRTBold)
assert.False(t, s.Items[0].Lines[0].Items[0].InlineStyle.SRTItalics)
assert.False(t, s.Items[0].Lines[0].Items[0].InlineStyle.SRTUnderline)
assert.Equal(t, "#ff00ff", *s.Items[1].Lines[0].Items[0].InlineStyle.SRTColor)
assert.Equal(t, astisub.ColorMagenta, s.Items[1].Lines[0].Items[0].InlineStyle.SRTColor)
assert.False(t, s.Items[1].Lines[0].Items[0].InlineStyle.SRTBold)
assert.False(t, s.Items[1].Lines[0].Items[0].InlineStyle.SRTItalics)
assert.False(t, s.Items[1].Lines[0].Items[0].InlineStyle.SRTUnderline)
assert.Equal(t, "#00ff00", *s.Items[2].Lines[0].Items[0].InlineStyle.SRTColor)
assert.Equal(t, astisub.ColorLime, s.Items[2].Lines[0].Items[0].InlineStyle.SRTColor)
assert.False(t, s.Items[2].Lines[0].Items[0].InlineStyle.SRTBold)
assert.False(t, s.Items[2].Lines[0].Items[0].InlineStyle.SRTItalics)
assert.False(t, s.Items[2].Lines[0].Items[0].InlineStyle.SRTUnderline)
Expand Down
73 changes: 62 additions & 11 deletions stl.go
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,31 @@ func stlVerticalPositionFromStyle(sa *StyleAttributes) int {
func (li LineItem) STLString() string {
rs := li.Text
if li.InlineStyle != nil {
// Add color code prefix
if li.InlineStyle.STLColor != nil {
var colorCode byte
switch li.InlineStyle.STLColor {
case ColorBlack:
colorCode = 0x00
case ColorRed:
colorCode = 0x01
case ColorGreen:
colorCode = 0x02
case ColorYellow:
colorCode = 0x03
case ColorBlue:
colorCode = 0x04
case ColorMagenta:
colorCode = 0x05
case ColorCyan:
colorCode = 0x06
case ColorWhite:
colorCode = 0x07
default:
colorCode = 0x07 // Default to white
}
rs = string(rune(colorCode)) + rs
}
if li.InlineStyle.STLItalics != nil && *li.InlineStyle.STLItalics {
rs = string(rune(0x80)) + rs + string(rune(0x81))
}
Expand Down Expand Up @@ -867,6 +892,7 @@ func (h *stlCharacterHandler) decode(i byte) (o []byte) {

type stlStyler struct {
boxing *bool
color *Color
italics *bool
underline *bool
}
Expand All @@ -877,6 +903,22 @@ func newSTLStyler() *stlStyler {

func (s *stlStyler) parseSpacingAttribute(i byte) {
switch i {
case 0x00:
s.color = ColorBlack
case 0x01:
s.color = ColorRed
case 0x02:
s.color = ColorGreen
case 0x03:
s.color = ColorYellow
case 0x04:
s.color = ColorBlue
case 0x05:
s.color = ColorMagenta
case 0x06:
s.color = ColorCyan
case 0x07:
s.color = ColorWhite
case 0x80:
s.italics = astikit.BoolPtr(true)
case 0x81:
Expand All @@ -893,11 +935,11 @@ func (s *stlStyler) parseSpacingAttribute(i byte) {
}

func (s *stlStyler) hasBeenSet() bool {
return s.italics != nil || s.boxing != nil || s.underline != nil
return s.italics != nil || s.boxing != nil || s.underline != nil || s.color != nil
}

func (s *stlStyler) hasChanged(sa *StyleAttributes) bool {
return s.boxing != sa.STLBoxing || s.italics != sa.STLItalics || s.underline != sa.STLUnderline
return s.boxing != sa.STLBoxing || s.italics != sa.STLItalics || s.underline != sa.STLUnderline || s.color != sa.STLColor
}

func (s *stlStyler) propagateStyleAttributes(sa *StyleAttributes) {
Expand All @@ -908,6 +950,9 @@ func (s *stlStyler) update(sa *StyleAttributes) {
if s.boxing != nil && s.boxing != sa.STLBoxing {
sa.STLBoxing = s.boxing
}
if s.color != nil && s.color != sa.STLColor {
sa.STLColor = s.color
}
if s.italics != nil && s.italics != sa.STLItalics {
sa.STLItalics = s.italics
}
Expand Down Expand Up @@ -1053,10 +1098,6 @@ func parseSTLJustificationCode(i byte) Justification {
}
}

func isTeletextControlCode(i byte) (b bool) {
return i <= 0x1f
}

func parseOpenSubtitleRow(i *Item, d decoder, fs func() styler, row []byte) error {
// Loop through columns
var l = Line{}
Expand All @@ -1068,14 +1109,22 @@ func parseOpenSubtitleRow(i *Item, d decoder, fs func() styler, row []byte) erro
s = fs()
}

if isTeletextControlCode(v) {
// Check if this is a valid control code (color or style codes)
isTeletextControlCode := v <= 0x1f
isColorCode := v >= 0x00 && v <= 0x07
isStyleCode := v >= 0x80 && v <= 0x85

// Error on teletext control codes that aren't color or style codes
if isTeletextControlCode && !isColorCode && !isStyleCode {
return errors.New("teletext control code in open text")
}

// Parse spacing attributes (color and style codes)
if s != nil {
s.parseSpacingAttribute(v)
}

// Style has been set
// Style has been set by a control code
if s != nil && s.hasBeenSet() {
// Style has changed
if s.hasChanged(li.InlineStyle) {
Expand All @@ -1090,10 +1139,12 @@ func parseOpenSubtitleRow(i *Item, d decoder, fs func() styler, row []byte) erro
}
s.update(li.InlineStyle)
}
} else {
// Append text
li.Text += string(d.decode(v))
// Control codes don't get appended as text, continue to next byte
continue
}

// Not a control code, append as text
li.Text += string(d.decode(v))
}

appendOpenSubtitleLineItem(&l, li, s)
Expand Down
175 changes: 175 additions & 0 deletions stl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,178 @@ func TestTTMLToSTLGSIBlock(t *testing.T) {
assert.True(t, displayStandardCode == "0" || displayStandardCode == "1" || displayStandardCode == "2",
"Display standard code should be '0', '1', or '2', got '%s'", displayStandardCode)
}

func TestSTLColors(t *testing.T) {
// Create subtitles with different colored text
s := astisub.NewSubtitles()
s.Metadata = &astisub.Metadata{
Framerate: 25,
STLDisplayStandardCode: "0", // Open subtitling
}

// Add items with different colors
s.Items = []*astisub.Item{
{
StartAt: time.Second,
EndAt: 2 * time.Second,
Lines: []astisub.Line{
{
Items: []astisub.LineItem{
{
Text: "Red text",
InlineStyle: &astisub.StyleAttributes{
STLColor: astisub.ColorRed,
},
},
},
},
},
},
{
StartAt: 3 * time.Second,
EndAt: 4 * time.Second,
Lines: []astisub.Line{
{
Items: []astisub.LineItem{
{
Text: "Green text",
InlineStyle: &astisub.StyleAttributes{
STLColor: astisub.ColorGreen,
},
},
},
},
},
},
{
StartAt: 5 * time.Second,
EndAt: 6 * time.Second,
Lines: []astisub.Line{
{
Items: []astisub.LineItem{
{
Text: "Blue text",
InlineStyle: &astisub.StyleAttributes{
STLColor: astisub.ColorBlue,
},
},
},
},
},
},
{
StartAt: 7 * time.Second,
EndAt: 8 * time.Second,
Lines: []astisub.Line{
{
Items: []astisub.LineItem{
{
Text: "Yellow text",
InlineStyle: &astisub.StyleAttributes{
STLColor: astisub.ColorYellow,
},
},
},
},
},
},
}

// Write to STL
w := &bytes.Buffer{}
err := s.WriteToSTL(w)
assert.NoError(t, err)

// Read back from STL
s2, err := astisub.ReadFromSTL(bytes.NewReader(w.Bytes()), astisub.STLOptions{})
assert.NoError(t, err)

// Verify colors are preserved
assert.Equal(t, 4, len(s2.Items))
assert.Equal(t, astisub.ColorRed, s2.Items[0].Lines[0].Items[0].InlineStyle.STLColor)
assert.Equal(t, astisub.ColorGreen, s2.Items[1].Lines[0].Items[0].InlineStyle.STLColor)
assert.Equal(t, astisub.ColorBlue, s2.Items[2].Lines[0].Items[0].InlineStyle.STLColor)
assert.Equal(t, astisub.ColorYellow, s2.Items[3].Lines[0].Items[0].InlineStyle.STLColor)
}

func TestSTLColorsFromWebVTT(t *testing.T) {
// Open WebVTT file with colors
s, err := astisub.OpenFile("./testdata/example-out-styled.vtt")
assert.NoError(t, err)
assert.NotNil(t, s)

// Set metadata for STL
s.Metadata = &astisub.Metadata{
Framerate: 25,
STLDisplayStandardCode: "0", // Open subtitling
}

// Write to STL
w := &bytes.Buffer{}
err = s.WriteToSTL(w)
assert.NoError(t, err)

// Read back from STL
s2, err := astisub.ReadFromSTL(bytes.NewReader(w.Bytes()), astisub.STLOptions{})
assert.NoError(t, err)

// Verify colors are preserved through WebVTT -> STL -> STL round trip
// Item 1: lime color (maps to STLColor in WebVTT processing)
assert.Equal(t, 1, len(s2.Items[0].Lines))
if len(s2.Items[0].Lines[0].Items) > 0 {
assert.NotNil(t, s2.Items[0].Lines[0].Items[0].InlineStyle)
assert.NotNil(t, s2.Items[0].Lines[0].Items[0].InlineStyle.STLColor, "Item 1 should have STL color")
}

// Item 2: magenta color
assert.Equal(t, 1, len(s2.Items[1].Lines))
if len(s2.Items[1].Lines[0].Items) > 0 {
assert.NotNil(t, s2.Items[1].Lines[0].Items[0].InlineStyle)
assert.Equal(t, astisub.ColorMagenta, s2.Items[1].Lines[0].Items[0].InlineStyle.STLColor, "Item 2 should have magenta color")
}
}

func TestSTLColorsFromTTML(t *testing.T) {
// Open TTML file with colors
s, err := astisub.OpenFile("./testdata/example-in.ttml")
assert.NoError(t, err)
assert.NotNil(t, s)

// Set metadata for STL
s.Metadata = &astisub.Metadata{
Framerate: 25,
STLDisplayStandardCode: "0", // Open subtitling
}

// Write to STL
w := &bytes.Buffer{}
err = s.WriteToSTL(w)
assert.NoError(t, err)

// Read back from STL
s2, err := astisub.ReadFromSTL(bytes.NewReader(w.Bytes()), astisub.STLOptions{})
assert.NoError(t, err)

// Verify colors are preserved through TTML -> STL -> STL round trip
// Item 1: has black color in span
assert.Equal(t, 1, len(s2.Items[0].Lines))
if len(s2.Items[0].Lines[0].Items) > 0 {
assert.NotNil(t, s2.Items[0].Lines[0].Items[0].InlineStyle)
assert.Equal(t, astisub.ColorBlack, s2.Items[0].Lines[0].Items[0].InlineStyle.STLColor, "Item 1 should have black color")
}

// Item 2: has green color in one of the spans (across multiple lines due to <br/>)
foundGreen := false
for _, line := range s2.Items[1].Lines {
for _, item := range line.Items {
if item.InlineStyle != nil && item.InlineStyle.STLColor == astisub.ColorGreen {
foundGreen = true
break
}
}
if foundGreen {
break
}
}
assert.True(t, foundGreen, "Item 2 should have a span with green color")
}
Loading