diff --git a/stl.go b/stl.go index 248b5f9..91e00fc 100644 --- a/stl.go +++ b/stl.go @@ -3,6 +3,7 @@ package astisub import ( "bytes" "encoding/binary" + "errors" "fmt" "io" "math" @@ -161,6 +162,14 @@ const ( // TTI Special Extension Block Number const extensionBlockNumberReservedUserData = 0xfe +const stlLineSeparator = 0x8a + +type STLPosition struct { + VerticalPosition int + MaxRows int + Rows int +} + // ReadFromSTL parses an .stl content func ReadFromSTL(i io.Reader) (o *Subtitles, err error) { // Init @@ -189,10 +198,15 @@ func ReadFromSTL(i io.Reader) (o *Subtitles, err error) { // Update metadata // TODO Add more STL fields to metadata o.Metadata = &Metadata{ - Framerate: g.framerate, + Framerate: g.framerate, + STLCountryOfOrigin: g.countryOfOrigin, + STLCreationDate: &g.creationDate, + STLDisplayStandardCode: g.displayStandardCode, STLMaximumNumberOfDisplayableCharactersInAnyTextRow: astikit.IntPtr(g.maximumNumberOfDisplayableCharactersInAnyTextRow), STLMaximumNumberOfDisplayableRows: astikit.IntPtr(g.maximumNumberOfDisplayableRows), STLPublisher: g.publisher, + STLRevisionDate: &g.revisionDate, + STLSubtitleListReferenceCode: g.subtitleListReferenceCode, Title: g.originalProgramTitle, } if v, ok := stlLanguageMapping.Get(g.languageCode); ok { @@ -218,15 +232,38 @@ func ReadFromSTL(i io.Reader) (o *Subtitles, err error) { continue } + justification := parseSTLJustificationCode(t.justificationCode) + rows := bytes.Split(t.text, []byte{stlLineSeparator}) + + position := STLPosition{ + MaxRows: g.maximumNumberOfDisplayableRows, + Rows: len(rows), + VerticalPosition: t.verticalPosition, + } + + styleAttributes := StyleAttributes{ + STLJustification: &justification, + STLPosition: &position, + } + styleAttributes.propagateSTLAttributes() + // Create item var i = &Item{ - EndAt: t.timecodeOut - g.timecodeStartOfProgramme, - StartAt: t.timecodeIn - g.timecodeStartOfProgramme, + EndAt: t.timecodeOut - g.timecodeStartOfProgramme, + InlineStyle: &styleAttributes, + StartAt: t.timecodeIn - g.timecodeStartOfProgramme, } // Loop through rows - for _, text := range bytes.Split(t.text, []byte{0x8a}) { - parseTeletextRow(i, ch, func() styler { return newSTLStyler() }, text) + for _, text := range bytes.Split(t.text, []byte{stlLineSeparator}) { + if g.displayStandardCode == stlDisplayStandardCodeOpenSubtitling { + err = parseOpenSubtitleRow(i, ch, func() styler { return newSTLStyler() }, text) + if err != nil { + return nil, err + } + } else { + parseTeletextRow(i, ch, func() styler { return newSTLStyler() }, text) + } } // Append item @@ -302,7 +339,8 @@ func newGSIBlock(s Subtitles) (g *gsiBlock) { languageCode: stlLanguageCodeFrench, maximumNumberOfDisplayableCharactersInAnyTextRow: 40, maximumNumberOfDisplayableRows: 23, - subtitleListReferenceCode: "12345678", + revisionDate: Now(), + subtitleListReferenceCode: "", timecodeStatus: stlTimecodeStatusIntendedForUse, totalNumberOfDisks: 1, totalNumberOfSubtitleGroups: 1, @@ -312,6 +350,11 @@ func newGSIBlock(s Subtitles) (g *gsiBlock) { // Add metadata if s.Metadata != nil { + if s.Metadata.STLCreationDate != nil { + g.creationDate = *s.Metadata.STLCreationDate + } + g.countryOfOrigin = s.Metadata.STLCountryOfOrigin + g.displayStandardCode = s.Metadata.STLDisplayStandardCode g.framerate = s.Metadata.Framerate if v, ok := stlLanguageMapping.GetInverse(s.Metadata.Language); ok { g.languageCode = v.(string) @@ -324,6 +367,10 @@ func newGSIBlock(s Subtitles) (g *gsiBlock) { g.maximumNumberOfDisplayableRows = *s.Metadata.STLMaximumNumberOfDisplayableRows } g.publisher = s.Metadata.STLPublisher + if s.Metadata.STLRevisionDate != nil { + g.revisionDate = *s.Metadata.STLRevisionDate + } + g.subtitleListReferenceCode = s.Metadata.STLSubtitleListReferenceCode } // Timecode first in cue @@ -598,18 +645,46 @@ func newTTIBlock(i *Item, idx int) (t *ttiBlock) { subtitleNumber: idx, timecodeIn: i.StartAt, timecodeOut: i.EndAt, - verticalPosition: 20, + verticalPosition: stlVerticalPositionFromStyle(i.InlineStyle), } // Add text var lines []string for _, l := range i.Lines { - lines = append(lines, l.String()) + var lineItems []string + for _, li := range l.Items { + lineItems = append(lineItems, li.STLString()) + } + lines = append(lines, strings.Join(lineItems, " ")) } - t.text = []byte(strings.Join(lines, "\n")) + t.text = []byte(strings.Join(lines, string(rune(stlLineSeparator)))) return } +func stlVerticalPositionFromStyle(sa *StyleAttributes) int { + if sa != nil && sa.STLPosition != nil { + return sa.STLPosition.VerticalPosition + } else { + return 20 + } +} + +func (li LineItem) STLString() string { + rs := li.Text + if li.InlineStyle != nil { + if li.InlineStyle.STLItalics != nil && *li.InlineStyle.STLItalics { + rs = string(rune(0x80)) + rs + string(rune(0x81)) + } + if li.InlineStyle.STLUnderline != nil && *li.InlineStyle.STLUnderline { + rs = string(rune(0x82)) + rs + string(rune(0x83)) + } + if li.InlineStyle.STLBoxing != nil && *li.InlineStyle.STLBoxing { + rs = string(rune(0x84)) + rs + string(rune(0x85)) + } + } + return rs +} + // parseTTIBlock parses a TTI block func parseTTIBlock(p []byte, framerate int) *ttiBlock { return &ttiBlock{ @@ -883,3 +958,89 @@ func encodeTextSTL(i string) (o []byte) { } return } + +func parseSTLJustificationCode(i byte) Justification { + switch i { + case 0x00: + return JustificationUnchanged + case 0x01: + return JustificationLeft + case 0x02: + return JustificationCentered + case 0x03: + return JustificationRight + default: + return JustificationUnchanged + } +} + +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{} + var li = LineItem{InlineStyle: &StyleAttributes{}} + var s styler + for _, v := range row { + // Create specific styler + if fs != nil { + s = fs() + } + + if isTeletextControlCode(v) { + return errors.New("teletext control code in open text") + } + if s != nil { + s.parseSpacingAttribute(v) + } + + // Style has been set + if s != nil && s.hasBeenSet() { + // Style has changed + if s.hasChanged(li.InlineStyle) { + if len(li.Text) > 0 { + // Append line item + appendOpenSubtitleLineItem(&l, li, s) + + // Create new line item + sa := &StyleAttributes{} + *sa = *li.InlineStyle + li = LineItem{InlineStyle: sa} + } + s.update(li.InlineStyle) + } + } else { + // Append text + li.Text += string(d.decode(v)) + } + } + + appendOpenSubtitleLineItem(&l, li, s) + + // Append line + if len(l.Items) > 0 { + i.Lines = append(i.Lines, l) + } + return nil +} + +func appendOpenSubtitleLineItem(l *Line, li LineItem, s styler) { + // There's some text + if len(strings.TrimSpace(li.Text)) > 0 { + // Make sure inline style exists + if li.InlineStyle == nil { + li.InlineStyle = &StyleAttributes{} + } + + // Propagate style attributes + if s != nil { + s.propagateStyleAttributes(li.InlineStyle) + } + + // Append line item + li.Text = strings.TrimSpace(li.Text) + l.Items = append(l.Items, li) + } +} diff --git a/stl_test.go b/stl_test.go index d4c3e71..f445d45 100644 --- a/stl_test.go +++ b/stl_test.go @@ -13,17 +13,27 @@ import ( func TestSTL(t *testing.T) { // Init - astisub.Now = func() (t time.Time) { - t, _ = time.Parse("060102", "170702") - return - } + creationDate, _ := time.Parse("060102", "170702") + revisionDate, _ := time.Parse("060102", "010101") // Open s, err := astisub.OpenFile("./testdata/example-in.stl") assert.NoError(t, err) assertSubtitleItems(t, s) // Metadata - assert.Equal(t, &astisub.Metadata{Framerate: 25, Language: astisub.LanguageFrench, STLMaximumNumberOfDisplayableCharactersInAnyTextRow: astikit.IntPtr(40), STLMaximumNumberOfDisplayableRows: astikit.IntPtr(23), STLPublisher: "Copyright test", Title: "Title test"}, s.Metadata) + assert.Equal(t, &astisub.Metadata{ + Framerate: 25, + Language: astisub.LanguageFrench, + STLCreationDate: &creationDate, + STLMaximumNumberOfDisplayableCharactersInAnyTextRow: astikit.IntPtr(40), + STLMaximumNumberOfDisplayableRows: astikit.IntPtr(23), + STLPublisher: "Copyright test", + STLDisplayStandardCode: "1", + STLRevisionDate: &revisionDate, + STLSubtitleListReferenceCode: "12345678", + STLCountryOfOrigin: "FRA", + Title: "Title test"}, + s.Metadata) // No subtitles to write w := &bytes.Buffer{} @@ -37,3 +47,38 @@ func TestSTL(t *testing.T) { assert.NoError(t, err) assert.Equal(t, string(c), w.String()) } + +func TestOPNSTL(t *testing.T) { + // Init + creationDate, _ := time.Parse("060102", "200110") + revisionDate, _ := time.Parse("060102", "200110") + + // Open + s, err := astisub.OpenFile("./testdata/example-opn-in.stl") + assert.NoError(t, err) + // Metadata + assert.Equal(t, &astisub.Metadata{ + Framerate: 25, + Language: astisub.LanguageEnglish, + STLCountryOfOrigin: "NOR", + STLCreationDate: &creationDate, + STLDisplayStandardCode: "0", + STLMaximumNumberOfDisplayableCharactersInAnyTextRow: astikit.IntPtr(38), + STLMaximumNumberOfDisplayableRows: astikit.IntPtr(11), + STLPublisher: "", + STLRevisionDate: &revisionDate, + Title: ""}, + s.Metadata) + + // No subtitles to write + w := &bytes.Buffer{} + err = astisub.Subtitles{}.WriteToSTL(w) + assert.EqualError(t, err, astisub.ErrNoSubtitlesToWrite.Error()) + + // Write + c, err := ioutil.ReadFile("./testdata/example-opn-out.stl") + assert.NoError(t, err) + err = s.WriteToSTL(w) + assert.NoError(t, err) + assert.Equal(t, string(c), w.String()) +} diff --git a/subtitles.go b/subtitles.go index e8e7f07..cfdc11b 100644 --- a/subtitles.go +++ b/subtitles.go @@ -158,6 +158,15 @@ func (c *Color) TTMLString() string { return fmt.Sprintf("%.6x", uint32(c.Red)<<16|uint32(c.Green)<<8|uint32(c.Blue)) } +type Justification int + +var ( + JustificationUnchanged = Justification(1) + JustificationLeft = Justification(2) + JustificationCentered = Justification(3) + JustificationRight = Justification(4) +) + // StyleAttributes represents style attributes type StyleAttributes struct { SSAAlignment *int @@ -188,6 +197,8 @@ type StyleAttributes struct { SSAUnderline *bool STLBoxing *bool STLItalics *bool + STLJustification *Justification + STLPosition *STLPosition STLUnderline *bool TeletextColor *Color TeletextDoubleHeight *bool @@ -221,6 +232,7 @@ type StyleAttributes struct { TTMLWritingMode string TTMLZIndex int WebVTTAlign string + WebVTTItalics bool WebVTTLine string WebVTTLines int WebVTTPosition string @@ -265,9 +277,14 @@ type Metadata struct { SSATimer *float64 SSAUpdateDetails string SSAWrapStyle string + STLCountryOfOrigin string + STLCreationDate *time.Time + STLDisplayStandardCode string STLMaximumNumberOfDisplayableCharactersInAnyTextRow *int STLMaximumNumberOfDisplayableRows *int STLPublisher string + STLRevisionDate *time.Time + STLSubtitleListReferenceCode string Title string TTMLCopyright string } diff --git a/testdata/example-opn-in.stl b/testdata/example-opn-in.stl new file mode 100644 index 0000000..72792a7 Binary files /dev/null and b/testdata/example-opn-in.stl differ diff --git a/testdata/example-opn-out.stl b/testdata/example-opn-out.stl new file mode 100644 index 0000000..68e27a6 Binary files /dev/null and b/testdata/example-opn-out.stl differ diff --git a/webvtt.go b/webvtt.go index 526b057..3315e4e 100644 --- a/webvtt.go +++ b/webvtt.go @@ -344,7 +344,9 @@ func (s Subtitles) WriteToWebVTT(o io.Writer) (err error) { // Loop through lines for _, l := range item.Lines { - c = append(c, []byte(l.String())...) + for _, li := range l.Items { + c = append(c, li.webVTTBytes()...) + } c = append(c, bytesLineSeparator...) } @@ -362,3 +364,15 @@ func (s Subtitles) WriteToWebVTT(o io.Writer) (err error) { } return } + +func (li LineItem) webVTTBytes() []byte { + var c []byte + if li.InlineStyle != nil && li.InlineStyle.WebVTTItalics { + c = append(c, []byte("")...) + c = append(c, []byte(li.Text)...) + c = append(c, []byte("")...) + } else { + c = append(c, []byte(li.Text)...) + } + return c +}