From 1396d6047ccdebd5aed274cdb19946e0e3c3b781 Mon Sep 17 00:00:00 2001 From: Kloon ImKloon Date: Thu, 19 Oct 2023 14:06:43 +0200 Subject: [PATCH 1/7] Support SRT styling and bold/underline in VTT --- srt.go | 150 +++++++++++++++++++++++++++++++++++++++++++++++++-- subtitles.go | 7 +++ webvtt.go | 63 ++++++++++++++++------ 3 files changed, 202 insertions(+), 18 deletions(-) diff --git a/srt.go b/srt.go index 97970b9..8df1e3b 100644 --- a/srt.go +++ b/srt.go @@ -7,6 +7,8 @@ import ( "strconv" "strings" "time" + + "golang.org/x/net/html" ) // Constants @@ -111,7 +113,86 @@ func ReadFromSRT(i io.Reader) (o *Subtitles, err error) { o.Items = append(o.Items, s) } else { // Add text - s.Lines = append(s.Lines, Line{Items: []LineItem{{Text: strings.TrimSpace(line)}}}) + if l := parseTextSrt(line); len(l.Items) > 0 { + s.Lines = append(s.Lines, l) + } + // s.Lines = append(s.Lines, Line{Items: []LineItem{{Text: strings.TrimSpace(line)}}}) + } + } + return +} + +func parseTextSrt(i string) (o Line) { + // Create tokenizer + tr := html.NewTokenizer(strings.NewReader(i)) + + // Loop + type Styles struct { + bold bool + italic bool + underline bool + color *string + } + styles := Styles{} + for { + // Get next tag + t := tr.Next() + + // Process error + if err := tr.Err(); err != nil { + break + } + + // Get current token + token := tr.Token() + + switch t { + case html.EndTagToken: + // Parse italic/bold/underline + switch token.Data { + case "b": + styles.bold = false + case "i": + styles.italic = false + case "u": + styles.underline = false + } + case html.StartTagToken: + // Parse italic/bold/underline + switch token.Data { + case "b": + styles.bold = true + case "i": + styles.italic = true + case "u": + styles.underline = true + case "font": + color, _ := getAttribute(&token, "color") + if color != "" { + styles.color = &color + } + } + case html.TextToken: + if s := strings.TrimSpace(string(tr.Raw())); s != "" { + // Get style attribute + var sa *StyleAttributes + if styles.bold || styles.italic || + styles.underline || styles.color != nil { + sa = &StyleAttributes{ + TTMLColor: styles.color, + WebVTTBold: styles.bold, + WebVTTItalics: styles.italic, + WebVTTUnderline: styles.underline, + } + sa.propagateSRTAttributes() + } + + // Append item + o.Items = append(o.Items, LineItem{ + InlineStyle: sa, + Text: s, + }) + } } } return @@ -146,8 +227,7 @@ func (s Subtitles) WriteToSRT(o io.Writer) (err error) { // Loop through lines for _, l := range v.Lines { - c = append(c, []byte(l.String())...) - c = append(c, bytesLineSeparator...) + c = append(c, l.srtBytes()...) } // Add new line @@ -164,3 +244,67 @@ func (s Subtitles) WriteToSRT(o io.Writer) (err error) { } return } + +func (l Line) srtBytes() (c []byte) { + for idx, li := range l.Items { + c = append(c, li.srtBytes()...) + // condition to avoid adding space as the last character. + if idx < len(l.Items)-1 { + c = append(c, []byte(" ")...) + } + } + c = append(c, bytesLineSeparator...) + return +} + +func (li LineItem) srtBytes() (c []byte) { + // Get color + var color string + if li.InlineStyle != nil && li.InlineStyle.TTMLColor != nil { + color = *li.InlineStyle.TTMLColor + } + + // Get bold/italics/underline + b := li.InlineStyle != nil && li.InlineStyle.WebVTTBold + i := li.InlineStyle != nil && li.InlineStyle.WebVTTItalics + u := li.InlineStyle != nil && li.InlineStyle.WebVTTUnderline + + // Append + if color != "" { + c = append(c, []byte("")...) + } + if b { + c = append(c, []byte("")...) + } + if i { + c = append(c, []byte("")...) + } + if u { + c = append(c, []byte("")...) + } + c = append(c, []byte(li.Text)...) + if u { + c = append(c, []byte("")...) + } + if i { + c = append(c, []byte("")...) + } + if b { + c = append(c, []byte("")...) + } + if color != "" { + c = append(c, []byte("")...) + } + return +} + +func getAttribute(n *html.Token, key string) (string, bool) { + + for _, attr := range n.Attr { + if attr.Key == key { + return attr.Val, true + } + } + + return "", false +} diff --git a/subtitles.go b/subtitles.go index 3225979..69fa91a 100644 --- a/subtitles.go +++ b/subtitles.go @@ -173,6 +173,9 @@ var ( // StyleAttributes represents style attributes type StyleAttributes struct { + SRTBold bool + SRTItalics bool + SRTUnderline bool SSAAlignment *int SSAAlphaLevel *float64 SSAAngle *float64 // degrees @@ -236,7 +239,9 @@ type StyleAttributes struct { TTMLWritingMode *string TTMLZIndex *int WebVTTAlign string + WebVTTBold bool WebVTTItalics bool + WebVTTUnderline bool WebVTTLine string WebVTTLines int WebVTTPosition string @@ -248,6 +253,8 @@ type StyleAttributes struct { WebVTTWidth string } +func (sa *StyleAttributes) propagateSRTAttributes() {} + func (sa *StyleAttributes) propagateSSAAttributes() {} func (sa *StyleAttributes) propagateSTLAttributes() { diff --git a/webvtt.go b/webvtt.go index b153469..8b05e8f 100644 --- a/webvtt.go +++ b/webvtt.go @@ -2,7 +2,6 @@ package astisub import ( "bufio" - "bytes" "errors" "fmt" "io" @@ -29,8 +28,6 @@ const ( // Vars var ( - bytesWebVTTItalicEndTag = []byte("") - bytesWebVTTItalicStartTag = []byte("") bytesWebVTTTimeBoundariesSeparator = []byte(webvttTimeBoundariesSeparator) webVTTRegexpStartTag = regexp.MustCompile(`()`) webVTTEscaper = strings.NewReplacer("&", "&", "<", "<") @@ -290,7 +287,12 @@ func parseTextWebVTT(i string) (o Line) { tr := html.NewTokenizer(strings.NewReader(i)) // Loop - italic := false + type Styles struct { + bold bool + italic bool + underline bool + } + styles := Styles{} for { // Get next tag t := tr.Next() @@ -300,12 +302,19 @@ func parseTextWebVTT(i string) (o Line) { break } + // Get current token + token := tr.Token() + switch t { case html.EndTagToken: - // Parse italic - if bytes.Equal(tr.Raw(), bytesWebVTTItalicEndTag) { - italic = false - continue + // Parse italic/bold/underline + switch token.Data { + case "b": + styles.bold = false + case "i": + styles.italic = false + case "u": + styles.underline = false } case html.StartTagToken: // Parse voice name @@ -316,18 +325,24 @@ func parseTextWebVTT(i string) (o Line) { continue } - // Parse italic - if bytes.Equal(tr.Raw(), bytesWebVTTItalicStartTag) { - italic = true - continue + // Parse italic/bold/underline + switch token.Data { + case "b": + styles.bold = true + case "i": + styles.italic = true + case "u": + styles.underline = true } case html.TextToken: if s := strings.TrimSpace(string(tr.Raw())); s != "" { // Get style attribute var sa *StyleAttributes - if italic { + if styles.bold || styles.italic || styles.underline { sa = &StyleAttributes{ - WebVTTItalics: italic, + WebVTTBold: styles.bold, + WebVTTItalics: styles.italic, + WebVTTUnderline: styles.underline, } sa.propagateWebVTTAttributes() } @@ -516,20 +531,34 @@ func (li LineItem) webVTTBytes() (c []byte) { color = cssColor(*li.InlineStyle.TTMLColor) } - // Get italics + // Get bold/italics/underline + b := li.InlineStyle != nil && li.InlineStyle.WebVTTBold i := li.InlineStyle != nil && li.InlineStyle.WebVTTItalics + u := li.InlineStyle != nil && li.InlineStyle.WebVTTUnderline // Append if color != "" { c = append(c, []byte("")...) } + if b { + c = append(c, []byte("")...) + } if i { c = append(c, []byte("")...) } + if u { + c = append(c, []byte("")...) + } c = append(c, []byte(escapeWebVTT(li.Text))...) + if u { + c = append(c, []byte("")...) + } if i { c = append(c, []byte("")...) } + if b { + c = append(c, []byte("")...) + } if color != "" { c = append(c, []byte("")...) } @@ -537,12 +566,16 @@ func (li LineItem) webVTTBytes() (c []byte) { } func cssColor(rgb string) string { + // https://www.w3.org/TR/webvtt1/#default-text-color colors := map[string]string{ "#00ffff": "cyan", // narrator, thought "#ffff00": "yellow", // out of vision "#ff0000": "red", // noises "#ff00ff": "magenta", // song "#00ff00": "lime", // foreign speak + "#ffffff": "white", + "#0000ff": "blue", + "#000000": "black", } return colors[strings.ToLower(rgb)] // returning the empty string is ok } From 71273b43b15209383c4a5203adb149cf3b2abde3 Mon Sep 17 00:00:00 2001 From: Kloon ImKloon Date: Thu, 19 Oct 2023 14:21:16 +0200 Subject: [PATCH 2/7] Cleanup --- srt.go | 2 ++ subtitles.go | 3 --- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/srt.go b/srt.go index 8df1e3b..f704840 100644 --- a/srt.go +++ b/srt.go @@ -156,6 +156,8 @@ func parseTextSrt(i string) (o Line) { styles.italic = false case "u": styles.underline = false + case "font": + styles.color = nil } case html.StartTagToken: // Parse italic/bold/underline diff --git a/subtitles.go b/subtitles.go index 69fa91a..499e562 100644 --- a/subtitles.go +++ b/subtitles.go @@ -173,9 +173,6 @@ var ( // StyleAttributes represents style attributes type StyleAttributes struct { - SRTBold bool - SRTItalics bool - SRTUnderline bool SSAAlignment *int SSAAlphaLevel *float64 SSAAngle *float64 // degrees From 0a35c992a979b3a1b864dff9ab83957bace31ccc Mon Sep 17 00:00:00 2001 From: Kloon ImKloon Date: Fri, 20 Oct 2023 12:58:11 +0200 Subject: [PATCH 3/7] Implement changes --- srt.go | 57 +++++++++++++++++++--------------------------------- subtitles.go | 37 ++++++++++++++++++++++++++++++++-- webvtt.go | 25 +++++++++++------------ 3 files changed, 68 insertions(+), 51 deletions(-) diff --git a/srt.go b/srt.go index f704840..8843eaa 100644 --- a/srt.go +++ b/srt.go @@ -116,7 +116,6 @@ func ReadFromSRT(i io.Reader) (o *Subtitles, err error) { if l := parseTextSrt(line); len(l.Items) > 0 { s.Lines = append(s.Lines, l) } - // s.Lines = append(s.Lines, Line{Items: []LineItem{{Text: strings.TrimSpace(line)}}}) } } return @@ -127,13 +126,12 @@ func parseTextSrt(i string) (o Line) { tr := html.NewTokenizer(strings.NewReader(i)) // Loop - type Styles struct { + var ( bold bool italic bool underline bool color *string - } - styles := Styles{} + ) for { // Get next tag t := tr.Next() @@ -151,40 +149,38 @@ func parseTextSrt(i string) (o Line) { // Parse italic/bold/underline switch token.Data { case "b": - styles.bold = false + bold = false case "i": - styles.italic = false + italic = false case "u": - styles.underline = false + underline = false case "font": - styles.color = nil + color = nil } case html.StartTagToken: // Parse italic/bold/underline switch token.Data { case "b": - styles.bold = true + bold = true case "i": - styles.italic = true + italic = true case "u": - styles.underline = true + underline = true case "font": - color, _ := getAttribute(&token, "color") - if color != "" { - styles.color = &color + if c := htmlTokenAttribute(&token, "color"); c != nil { + color = c } } case html.TextToken: if s := strings.TrimSpace(string(tr.Raw())); s != "" { // Get style attribute var sa *StyleAttributes - if styles.bold || styles.italic || - styles.underline || styles.color != nil { + if bold || italic || underline || color != nil { sa = &StyleAttributes{ - TTMLColor: styles.color, - WebVTTBold: styles.bold, - WebVTTItalics: styles.italic, - WebVTTUnderline: styles.underline, + SRTColor: color, + SRTBold: bold, + SRTItalics: italic, + SRTUnderline: underline, } sa.propagateSRTAttributes() } @@ -262,14 +258,14 @@ func (l Line) srtBytes() (c []byte) { func (li LineItem) srtBytes() (c []byte) { // Get color var color string - if li.InlineStyle != nil && li.InlineStyle.TTMLColor != nil { - color = *li.InlineStyle.TTMLColor + if li.InlineStyle != nil && li.InlineStyle.SRTColor != nil { + color = *li.InlineStyle.SRTColor } // Get bold/italics/underline - b := li.InlineStyle != nil && li.InlineStyle.WebVTTBold - i := li.InlineStyle != nil && li.InlineStyle.WebVTTItalics - u := li.InlineStyle != nil && li.InlineStyle.WebVTTUnderline + b := li.InlineStyle != nil && li.InlineStyle.SRTBold + i := li.InlineStyle != nil && li.InlineStyle.SRTItalics + u := li.InlineStyle != nil && li.InlineStyle.SRTUnderline // Append if color != "" { @@ -299,14 +295,3 @@ func (li LineItem) srtBytes() (c []byte) { } return } - -func getAttribute(n *html.Token, key string) (string, bool) { - - for _, attr := range n.Attr { - if attr.Key == key { - return attr.Val, true - } - } - - return "", false -} diff --git a/subtitles.go b/subtitles.go index 499e562..b1d1806 100644 --- a/subtitles.go +++ b/subtitles.go @@ -12,6 +12,7 @@ import ( "time" "github.com/asticode/go-astikit" + "golang.org/x/net/html" ) // Bytes @@ -173,6 +174,10 @@ var ( // StyleAttributes represents style attributes type StyleAttributes struct { + SRTColor *string + SRTBold bool + SRTItalics bool + SRTUnderline bool SSAAlignment *int SSAAlphaLevel *float64 SSAAngle *float64 // degrees @@ -250,7 +255,16 @@ type StyleAttributes struct { WebVTTWidth string } -func (sa *StyleAttributes) propagateSRTAttributes() {} +func (sa *StyleAttributes) propagateSRTAttributes() { + // copy relevant attrs to WebVTT ones + if sa.SRTColor != nil { + // TODO: handle non-default colors that need custom styles + sa.TTMLColor = sa.SRTColor + } + sa.WebVTTBold = sa.SRTBold + sa.WebVTTItalics = sa.SRTItalics + sa.WebVTTUnderline = sa.SRTUnderline +} func (sa *StyleAttributes) propagateSSAAttributes() {} @@ -325,7 +339,15 @@ func (sa *StyleAttributes) propagateTTMLAttributes() { } } -func (sa *StyleAttributes) propagateWebVTTAttributes() {} +func (sa *StyleAttributes) propagateWebVTTAttributes() { + // copy relevant attrs to SRT ones + if sa.TTMLColor != nil { + sa.SRTColor = sa.TTMLColor + } + sa.SRTBold = sa.WebVTTBold + sa.SRTItalics = sa.WebVTTItalics + sa.SRTUnderline = sa.WebVTTUnderline +} // Metadata represents metadata // TODO Merge attributes @@ -806,3 +828,14 @@ func appendStringToBytesWithNewLine(i []byte, s string) (o []byte) { o = append(o, bytesLineSeparator...) return } + +func htmlTokenAttribute(t *html.Token, key string) *string { + + for _, attr := range t.Attr { + if attr.Key == key { + return &attr.Val + } + } + + return nil +} diff --git a/webvtt.go b/webvtt.go index 8b05e8f..8c9f3b7 100644 --- a/webvtt.go +++ b/webvtt.go @@ -287,12 +287,11 @@ func parseTextWebVTT(i string) (o Line) { tr := html.NewTokenizer(strings.NewReader(i)) // Loop - type Styles struct { + var ( bold bool italic bool underline bool - } - styles := Styles{} + ) for { // Get next tag t := tr.Next() @@ -310,11 +309,11 @@ func parseTextWebVTT(i string) (o Line) { // Parse italic/bold/underline switch token.Data { case "b": - styles.bold = false + bold = false case "i": - styles.italic = false + italic = false case "u": - styles.underline = false + underline = false } case html.StartTagToken: // Parse voice name @@ -328,21 +327,21 @@ func parseTextWebVTT(i string) (o Line) { // Parse italic/bold/underline switch token.Data { case "b": - styles.bold = true + bold = true case "i": - styles.italic = true + italic = true case "u": - styles.underline = true + underline = true } case html.TextToken: if s := strings.TrimSpace(string(tr.Raw())); s != "" { // Get style attribute var sa *StyleAttributes - if styles.bold || styles.italic || styles.underline { + if bold || italic || underline { sa = &StyleAttributes{ - WebVTTBold: styles.bold, - WebVTTItalics: styles.italic, - WebVTTUnderline: styles.underline, + WebVTTBold: bold, + WebVTTItalics: italic, + WebVTTUnderline: underline, } sa.propagateWebVTTAttributes() } From 718ebed11df55dda86db9472bf7ae68e7c4f660d Mon Sep 17 00:00:00 2001 From: Kloon ImKloon Date: Fri, 20 Oct 2023 14:46:37 +0200 Subject: [PATCH 4/7] Fix tests --- srt.go | 11 ++++++++++- webvtt.go | 6 ++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/srt.go b/srt.go index 8843eaa..e0f0002 100644 --- a/srt.go +++ b/srt.go @@ -121,7 +121,14 @@ func ReadFromSRT(i io.Reader) (o *Subtitles, err error) { return } +// parseTextSrt parses the input line to fill the Line func parseTextSrt(i string) (o Line) { + // special handling needed for empty line + if strings.TrimSpace(i) == "" { + o.Items = []LineItem{{Text: ""}} + return + } + // Create tokenizer tr := html.NewTokenizer(strings.NewReader(i)) @@ -141,6 +148,8 @@ func parseTextSrt(i string) (o Line) { break } + // Get unmodified text + raw := string(tr.Raw()) // Get current token token := tr.Token() @@ -172,7 +181,7 @@ func parseTextSrt(i string) (o Line) { } } case html.TextToken: - if s := strings.TrimSpace(string(tr.Raw())); s != "" { + if s := strings.TrimSpace(raw); s != "" { // Get style attribute var sa *StyleAttributes if bold || italic || underline || color != nil { diff --git a/webvtt.go b/webvtt.go index 8c9f3b7..ebe7150 100644 --- a/webvtt.go +++ b/webvtt.go @@ -301,6 +301,8 @@ func parseTextWebVTT(i string) (o Line) { break } + // Get unmodified text + raw := string(tr.Raw()) // Get current token token := tr.Token() @@ -317,7 +319,7 @@ func parseTextWebVTT(i string) (o Line) { } case html.StartTagToken: // Parse voice name - if matches := webVTTRegexpStartTag.FindStringSubmatch(string(tr.Raw())); len(matches) > 3 { + if matches := webVTTRegexpStartTag.FindStringSubmatch(raw); len(matches) > 3 { if s := strings.TrimSpace(matches[3]); s != "" { o.VoiceName = s } @@ -334,7 +336,7 @@ func parseTextWebVTT(i string) (o Line) { underline = true } case html.TextToken: - if s := strings.TrimSpace(string(tr.Raw())); s != "" { + if s := strings.TrimSpace(raw); s != "" { // Get style attribute var sa *StyleAttributes if bold || italic || underline { From 12d5624a325a28da9ec3cf941c5cb0f1edffc7cf Mon Sep 17 00:00:00 2001 From: Kloon ImKloon Date: Fri, 20 Oct 2023 19:30:09 +0200 Subject: [PATCH 5/7] Add test for styled SRT --- srt_test.go | 19 ++++++++++++++ subtitles_test.go | 45 +++++++++++++++++++++++++++++++++ testdata/example-styled-in.srt | 24 ++++++++++++++++++ testdata/example-styled-out.srt | 24 ++++++++++++++++++ 4 files changed, 112 insertions(+) create mode 100644 testdata/example-styled-in.srt create mode 100644 testdata/example-styled-out.srt diff --git a/srt_test.go b/srt_test.go index f44fbbd..be630ee 100644 --- a/srt_test.go +++ b/srt_test.go @@ -46,3 +46,22 @@ func TestSRTMissingSequence(t *testing.T) { assert.NoError(t, err) assert.Equal(t, string(c), w.String()) } + +func TestSRTStyled(t *testing.T) { + // Open + s, err := astisub.OpenFile("./testdata/example-styled-in.srt") + assert.NoError(t, err) + assertStyledSubtitleItems(t, s) + + // No subtitles to write + w := &bytes.Buffer{} + err = astisub.Subtitles{}.WriteToSRT(w) + assert.EqualError(t, err, astisub.ErrNoSubtitlesToWrite.Error()) + + // Write + c, err := ioutil.ReadFile("./testdata/example-styled-out.srt") + assert.NoError(t, err) + err = s.WriteToSRT(w) + assert.NoError(t, err) + assert.Equal(t, string(c), w.String()) +} diff --git a/subtitles_test.go b/subtitles_test.go index a8012ba..6f06706 100644 --- a/subtitles_test.go +++ b/subtitles_test.go @@ -40,6 +40,51 @@ func assertSubtitleItems(t *testing.T, i *astisub.Subtitles) { assert.Equal(t, "electronic melody)", i.Items[5].Lines[1].String()) } +func assertStyledSubtitleItems(t *testing.T, i *astisub.Subtitles) { + assert.Len(t, i.Items, 6) + assert.Equal(t, 17*time.Second+985*time.Millisecond, i.Items[0].StartAt) + assert.Equal(t, 20*time.Second+521*time.Millisecond, i.Items[0].EndAt) + assert.Equal(t, "[instrumental music]", i.Items[0].Lines[0].String()) + assert.Equal(t, "#00ff00", *i.Items[0].Lines[0].Items[0].InlineStyle.SRTColor) + assert.True(t, i.Items[0].Lines[0].Items[0].InlineStyle.SRTBold) + assert.False(t, i.Items[0].Lines[0].Items[0].InlineStyle.SRTItalics) + assert.False(t, i.Items[0].Lines[0].Items[0].InlineStyle.SRTUnderline) + assert.Equal(t, 47*time.Second+115*time.Millisecond, i.Items[1].StartAt) + assert.Equal(t, 48*time.Second+282*time.Millisecond, i.Items[1].EndAt) + assert.Equal(t, "[ticks]", i.Items[1].Lines[0].String()) + assert.Equal(t, "#ffffff", *i.Items[1].Lines[0].Items[0].InlineStyle.SRTColor) + assert.False(t, i.Items[1].Lines[0].Items[0].InlineStyle.SRTBold) + assert.False(t, i.Items[1].Lines[0].Items[0].InlineStyle.SRTItalics) + assert.False(t, i.Items[1].Lines[0].Items[0].InlineStyle.SRTUnderline) + assert.Equal(t, 58*time.Second+192*time.Millisecond, i.Items[2].StartAt) + assert.Equal(t, 59*time.Second+727*time.Millisecond, i.Items[2].EndAt) + assert.Equal(t, "[instrumental music]", i.Items[2].Lines[0].String()) + assert.Equal(t, "#00ff00", *i.Items[2].Lines[0].Items[0].InlineStyle.SRTColor) + assert.False(t, i.Items[2].Lines[0].Items[0].InlineStyle.SRTBold) + assert.False(t, i.Items[2].Lines[0].Items[0].InlineStyle.SRTItalics) + assert.False(t, i.Items[2].Lines[0].Items[0].InlineStyle.SRTUnderline) + assert.Equal(t, 1*time.Minute+1*time.Second+662*time.Millisecond, i.Items[3].StartAt) + assert.Equal(t, 1*time.Minute+3*time.Second+63*time.Millisecond, i.Items[3].EndAt) + assert.Equal(t, "[dog barking]", i.Items[3].Lines[0].String()) + assert.Nil(t, i.Items[3].Lines[0].Items[0].InlineStyle.SRTColor) + assert.True(t, i.Items[3].Lines[0].Items[0].InlineStyle.SRTBold) + assert.False(t, i.Items[3].Lines[0].Items[0].InlineStyle.SRTItalics) + assert.True(t, i.Items[3].Lines[0].Items[0].InlineStyle.SRTUnderline) + assert.Equal(t, 1*time.Minute+26*time.Second+787*time.Millisecond, i.Items[4].StartAt) + assert.Equal(t, 1*time.Minute+29*time.Second+523*time.Millisecond, i.Items[4].EndAt) + assert.Equal(t, "[beeping]", i.Items[4].Lines[0].String()) + assert.Nil(t, i.Items[4].Lines[0].Items[0].InlineStyle) + assert.Equal(t, 1*time.Minute+29*time.Second+590*time.Millisecond, i.Items[5].StartAt) + assert.Equal(t, 1*time.Minute+31*time.Second+992*time.Millisecond, i.Items[5].EndAt) + assert.Equal(t, "[automated]", i.Items[5].Lines[0].String()) + assert.Nil(t, i.Items[5].Lines[0].Items[0].InlineStyle) + assert.Equal(t, "'The time is 7:35.'", i.Items[5].Lines[1].String()) + assert.Nil(t, i.Items[5].Lines[1].Items[0].InlineStyle.SRTColor) + assert.False(t, i.Items[5].Lines[1].Items[0].InlineStyle.SRTBold) + assert.True(t, i.Items[5].Lines[1].Items[0].InlineStyle.SRTItalics) + assert.False(t, i.Items[5].Lines[1].Items[0].InlineStyle.SRTUnderline) +} + func mockSubtitles() *astisub.Subtitles { return &astisub.Subtitles{Items: []*astisub.Item{{EndAt: 3 * time.Second, StartAt: time.Second, Lines: []astisub.Line{{Items: []astisub.LineItem{{Text: "subtitle-1"}}}}}, {EndAt: 7 * time.Second, StartAt: 3 * time.Second, Lines: []astisub.Line{{Items: []astisub.LineItem{{Text: "subtitle-2"}}}}}}} } diff --git a/testdata/example-styled-in.srt b/testdata/example-styled-in.srt new file mode 100644 index 0000000..e938314 --- /dev/null +++ b/testdata/example-styled-in.srt @@ -0,0 +1,24 @@ +1 +00:00:17,985 --> 00:00:20,521 +[instrumental music] + +2 +00:00:47,115 --> 00:00:48,282 +[ticks] + +3 +00:00:58,192 --> 00:00:59,727 +[instrumental music] + +4 +00:01:01,662 --> 00:01:03,063 +[dog barking] + +5 +00:01:26,787 --> 00:01:29,523 +[beeping] + +6 +00:01:29,590 --> 00:01:31,992 +[automated] +'The time is 7:35.' diff --git a/testdata/example-styled-out.srt b/testdata/example-styled-out.srt new file mode 100644 index 0000000..8439545 --- /dev/null +++ b/testdata/example-styled-out.srt @@ -0,0 +1,24 @@ +1 +00:00:17,985 --> 00:00:20,521 +[instrumental music] + +2 +00:00:47,115 --> 00:00:48,282 +[ticks] + +3 +00:00:58,192 --> 00:00:59,727 +[instrumental music] + +4 +00:01:01,662 --> 00:01:03,063 +[dog barking] + +5 +00:01:26,787 --> 00:01:29,523 +[beeping] + +6 +00:01:29,590 --> 00:01:31,992 +[automated] +'The time is 7:35.' From e2f0067d1050bb971bf35ea8487627b957703a5e Mon Sep 17 00:00:00 2001 From: Kloon ImKloon Date: Sun, 22 Oct 2023 14:27:35 +0200 Subject: [PATCH 6/7] Remove SSA/ASS tags from SRT --- srt.go | 5 +++++ testdata/example-styled-in.srt | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/srt.go b/srt.go index e0f0002..eb65828 100644 --- a/srt.go +++ b/srt.go @@ -4,6 +4,7 @@ import ( "bufio" "fmt" "io" + "regexp" "strconv" "strings" "time" @@ -19,6 +20,7 @@ const ( // Vars var ( bytesSRTTimeBoundariesSeparator = []byte(srtTimeBoundariesSeparator) + regexpSRTSSATags = regexp.MustCompile(`{\\.*?}`) ) // parseDurationSRT parses an .srt duration @@ -182,6 +184,9 @@ func parseTextSrt(i string) (o Line) { } case html.TextToken: if s := strings.TrimSpace(raw); s != "" { + // remove all ssa/ass tags from text + // TODO: maybe add support for {\an8} + s := regexpSRTSSATags.ReplaceAllLiteralString(s, "") // Get style attribute var sa *StyleAttributes if bold || italic || underline || color != nil { diff --git a/testdata/example-styled-in.srt b/testdata/example-styled-in.srt index e938314..b874a40 100644 --- a/testdata/example-styled-in.srt +++ b/testdata/example-styled-in.srt @@ -16,7 +16,7 @@ 5 00:01:26,787 --> 00:01:29,523 -[beeping] +{\an8}[beeping] 6 00:01:29,590 --> 00:01:31,992 From d8d439cb90ceb13dac63416432787ace3823f1b2 Mon Sep 17 00:00:00 2001 From: Kloon ImKloon Date: Mon, 23 Oct 2023 15:15:25 +0200 Subject: [PATCH 7/7] Support ASS position tags in SRT --- srt.go | 52 +++++++++++++++++++++++++++++---- srt_test.go | 1 + subtitles.go | 31 +++++++++++++++++++- subtitles_test.go | 47 ++++++++++++++++++----------- testdata/example-styled-out.srt | 2 +- 5 files changed, 109 insertions(+), 24 deletions(-) diff --git a/srt.go b/srt.go index eb65828..8abca3b 100644 --- a/srt.go +++ b/srt.go @@ -140,6 +140,7 @@ func parseTextSrt(i string) (o Line) { italic bool underline bool color *string + pos byte ) for { // Get next tag @@ -184,16 +185,16 @@ func parseTextSrt(i string) (o Line) { } case html.TextToken: if s := strings.TrimSpace(raw); s != "" { - // remove all ssa/ass tags from text - // TODO: maybe add support for {\an8} - s := regexpSRTSSATags.ReplaceAllLiteralString(s, "") + // Remove all SSA/ASS tags from text + s := regexpSRTSSATags.ReplaceAllStringFunc(s, removeSSATagsWithPos(&pos)) // Get style attribute var sa *StyleAttributes - if bold || italic || underline || color != nil { + if bold || italic || underline || color != nil || pos != 0 { sa = &StyleAttributes{ - SRTColor: color, SRTBold: bold, + SRTColor: color, SRTItalics: italic, + SRTPosition: pos, SRTUnderline: underline, } sa.propagateSRTAttributes() @@ -210,6 +211,38 @@ func parseTextSrt(i string) (o Line) { return } +// Removes SSA/ASS tags from subtitle text +// and extracts position if detected +func removeSSATagsWithPos(pos *byte) func(string) string { + return func(i string) string { + // Based on in the following information: + // https://superuser.com/a/1228528 + switch i { + case `{\an7}`: // top-left + *pos = 7 + case `{\an8}`: // top-center + *pos = 8 + case `{\an9}`: // top-right + *pos = 9 + case `{\an4}`: // middle-left + *pos = 4 + case `{\an5}`: // middle-center + *pos = 5 + case `{\an6}`: // middle-right + *pos = 6 + case `{\an1}`: // bottom-left + *pos = 1 + case `{\an2}`: // bottom-center + *pos = 2 + case `{\an3}`: // bottom-right + *pos = 3 + } + + // Remove tag from subtitle text + return "" + } +} + // formatDurationSRT formats an .srt duration func formatDurationSRT(i time.Duration) string { return formatDuration(i, ",", 3) @@ -281,6 +314,12 @@ func (li LineItem) srtBytes() (c []byte) { i := li.InlineStyle != nil && li.InlineStyle.SRTItalics u := li.InlineStyle != nil && li.InlineStyle.SRTUnderline + // Get position + var pos byte + if li.InlineStyle != nil { + pos = li.InlineStyle.SRTPosition + } + // Append if color != "" { c = append(c, []byte("")...) @@ -294,6 +333,9 @@ func (li LineItem) srtBytes() (c []byte) { if u { c = append(c, []byte("")...) } + if pos != 0 { + c = append(c, []byte(fmt.Sprintf(`{\an%d}`, pos))...) + } c = append(c, []byte(li.Text)...) if u { c = append(c, []byte("")...) diff --git a/srt_test.go b/srt_test.go index be630ee..aa81697 100644 --- a/srt_test.go +++ b/srt_test.go @@ -52,6 +52,7 @@ func TestSRTStyled(t *testing.T) { s, err := astisub.OpenFile("./testdata/example-styled-in.srt") assert.NoError(t, err) assertStyledSubtitleItems(t, s) + assertSRTSubtitleStyles(t, s) // No subtitles to write w := &bytes.Buffer{} diff --git a/subtitles.go b/subtitles.go index b1d1806..727ba07 100644 --- a/subtitles.go +++ b/subtitles.go @@ -174,9 +174,10 @@ var ( // StyleAttributes represents style attributes type StyleAttributes struct { - SRTColor *string SRTBold bool + SRTColor *string SRTItalics bool + SRTPosition byte // 1-9 numpad layout SRTUnderline bool SSAAlignment *int SSAAlphaLevel *float64 @@ -261,6 +262,34 @@ func (sa *StyleAttributes) propagateSRTAttributes() { // TODO: handle non-default colors that need custom styles sa.TTMLColor = sa.SRTColor } + + switch sa.SRTPosition { + case 7: // top-left + sa.WebVTTAlign = "left" + sa.WebVTTPosition = "10%" + case 8: // top-center + sa.WebVTTPosition = "10%" + case 9: // top-right + sa.WebVTTAlign = "right" + sa.WebVTTPosition = "10%" + case 4: // middle-left + sa.WebVTTAlign = "left" + sa.WebVTTPosition = "50%" + case 5: // middle-center + sa.WebVTTPosition = "50%" + case 6: // middle-right + sa.WebVTTAlign = "right" + sa.WebVTTPosition = "50%" + case 1: // bottom-left + sa.WebVTTAlign = "left" + sa.WebVTTPosition = "90%" + case 2: // bottom-center + sa.WebVTTPosition = "90%" + case 3: // bottom-right + sa.WebVTTAlign = "right" + sa.WebVTTPosition = "90%" + } + sa.WebVTTBold = sa.SRTBold sa.WebVTTItalics = sa.SRTItalics sa.WebVTTUnderline = sa.SRTUnderline diff --git a/subtitles_test.go b/subtitles_test.go index 6f06706..84597b1 100644 --- a/subtitles_test.go +++ b/subtitles_test.go @@ -45,41 +45,54 @@ func assertStyledSubtitleItems(t *testing.T, i *astisub.Subtitles) { assert.Equal(t, 17*time.Second+985*time.Millisecond, i.Items[0].StartAt) assert.Equal(t, 20*time.Second+521*time.Millisecond, i.Items[0].EndAt) assert.Equal(t, "[instrumental music]", i.Items[0].Lines[0].String()) + assert.Equal(t, 47*time.Second+115*time.Millisecond, i.Items[1].StartAt) + assert.Equal(t, 48*time.Second+282*time.Millisecond, i.Items[1].EndAt) + assert.Equal(t, "[ticks]", i.Items[1].Lines[0].String()) + assert.Equal(t, 58*time.Second+192*time.Millisecond, i.Items[2].StartAt) + assert.Equal(t, 59*time.Second+727*time.Millisecond, i.Items[2].EndAt) + assert.Equal(t, "[instrumental music]", i.Items[2].Lines[0].String()) + assert.Equal(t, 1*time.Minute+1*time.Second+662*time.Millisecond, i.Items[3].StartAt) + assert.Equal(t, 1*time.Minute+3*time.Second+63*time.Millisecond, i.Items[3].EndAt) + assert.Equal(t, "[dog barking]", i.Items[3].Lines[0].String()) + assert.Equal(t, 1*time.Minute+26*time.Second+787*time.Millisecond, i.Items[4].StartAt) + assert.Equal(t, 1*time.Minute+29*time.Second+523*time.Millisecond, i.Items[4].EndAt) + assert.Equal(t, "[beeping]", i.Items[4].Lines[0].String()) + assert.Equal(t, 1*time.Minute+29*time.Second+590*time.Millisecond, i.Items[5].StartAt) + assert.Equal(t, 1*time.Minute+31*time.Second+992*time.Millisecond, i.Items[5].EndAt) + assert.Equal(t, "[automated]", i.Items[5].Lines[0].String()) + assert.Equal(t, "'The time is 7:35.'", i.Items[5].Lines[1].String()) +} + +func assertSRTSubtitleStyles(t *testing.T, i *astisub.Subtitles) { + assert.Len(t, i.Items, 6) assert.Equal(t, "#00ff00", *i.Items[0].Lines[0].Items[0].InlineStyle.SRTColor) + assert.Zero(t, i.Items[0].Lines[0].Items[0].InlineStyle.SRTPosition) assert.True(t, i.Items[0].Lines[0].Items[0].InlineStyle.SRTBold) assert.False(t, i.Items[0].Lines[0].Items[0].InlineStyle.SRTItalics) assert.False(t, i.Items[0].Lines[0].Items[0].InlineStyle.SRTUnderline) - assert.Equal(t, 47*time.Second+115*time.Millisecond, i.Items[1].StartAt) - assert.Equal(t, 48*time.Second+282*time.Millisecond, i.Items[1].EndAt) - assert.Equal(t, "[ticks]", i.Items[1].Lines[0].String()) assert.Equal(t, "#ffffff", *i.Items[1].Lines[0].Items[0].InlineStyle.SRTColor) + assert.Zero(t, i.Items[1].Lines[0].Items[0].InlineStyle.SRTPosition) assert.False(t, i.Items[1].Lines[0].Items[0].InlineStyle.SRTBold) assert.False(t, i.Items[1].Lines[0].Items[0].InlineStyle.SRTItalics) assert.False(t, i.Items[1].Lines[0].Items[0].InlineStyle.SRTUnderline) - assert.Equal(t, 58*time.Second+192*time.Millisecond, i.Items[2].StartAt) - assert.Equal(t, 59*time.Second+727*time.Millisecond, i.Items[2].EndAt) - assert.Equal(t, "[instrumental music]", i.Items[2].Lines[0].String()) assert.Equal(t, "#00ff00", *i.Items[2].Lines[0].Items[0].InlineStyle.SRTColor) + assert.Zero(t, i.Items[2].Lines[0].Items[0].InlineStyle.SRTPosition) assert.False(t, i.Items[2].Lines[0].Items[0].InlineStyle.SRTBold) assert.False(t, i.Items[2].Lines[0].Items[0].InlineStyle.SRTItalics) assert.False(t, i.Items[2].Lines[0].Items[0].InlineStyle.SRTUnderline) - assert.Equal(t, 1*time.Minute+1*time.Second+662*time.Millisecond, i.Items[3].StartAt) - assert.Equal(t, 1*time.Minute+3*time.Second+63*time.Millisecond, i.Items[3].EndAt) - assert.Equal(t, "[dog barking]", i.Items[3].Lines[0].String()) assert.Nil(t, i.Items[3].Lines[0].Items[0].InlineStyle.SRTColor) + assert.Zero(t, i.Items[3].Lines[0].Items[0].InlineStyle.SRTPosition) assert.True(t, i.Items[3].Lines[0].Items[0].InlineStyle.SRTBold) assert.False(t, i.Items[3].Lines[0].Items[0].InlineStyle.SRTItalics) assert.True(t, i.Items[3].Lines[0].Items[0].InlineStyle.SRTUnderline) - assert.Equal(t, 1*time.Minute+26*time.Second+787*time.Millisecond, i.Items[4].StartAt) - assert.Equal(t, 1*time.Minute+29*time.Second+523*time.Millisecond, i.Items[4].EndAt) - assert.Equal(t, "[beeping]", i.Items[4].Lines[0].String()) - assert.Nil(t, i.Items[4].Lines[0].Items[0].InlineStyle) - assert.Equal(t, 1*time.Minute+29*time.Second+590*time.Millisecond, i.Items[5].StartAt) - assert.Equal(t, 1*time.Minute+31*time.Second+992*time.Millisecond, i.Items[5].EndAt) - assert.Equal(t, "[automated]", i.Items[5].Lines[0].String()) + assert.Nil(t, i.Items[4].Lines[0].Items[0].InlineStyle.SRTColor) + assert.Equal(t, byte(8), i.Items[4].Lines[0].Items[0].InlineStyle.SRTPosition) + assert.False(t, i.Items[4].Lines[0].Items[0].InlineStyle.SRTBold) + assert.False(t, i.Items[4].Lines[0].Items[0].InlineStyle.SRTItalics) + assert.False(t, i.Items[4].Lines[0].Items[0].InlineStyle.SRTUnderline) assert.Nil(t, i.Items[5].Lines[0].Items[0].InlineStyle) - assert.Equal(t, "'The time is 7:35.'", i.Items[5].Lines[1].String()) assert.Nil(t, i.Items[5].Lines[1].Items[0].InlineStyle.SRTColor) + assert.Zero(t, i.Items[5].Lines[1].Items[0].InlineStyle.SRTPosition) assert.False(t, i.Items[5].Lines[1].Items[0].InlineStyle.SRTBold) assert.True(t, i.Items[5].Lines[1].Items[0].InlineStyle.SRTItalics) assert.False(t, i.Items[5].Lines[1].Items[0].InlineStyle.SRTUnderline) diff --git a/testdata/example-styled-out.srt b/testdata/example-styled-out.srt index 8439545..70ffc70 100644 --- a/testdata/example-styled-out.srt +++ b/testdata/example-styled-out.srt @@ -16,7 +16,7 @@ 5 00:01:26,787 --> 00:01:29,523 -[beeping] +{\an8}[beeping] 6 00:01:29,590 --> 00:01:31,992