Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 := newColorFromHTMLString(*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.HTMLString()
}

// 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