This commit is contained in:
2018-11-04 15:58:15 +01:00
commit f956bcee28
1178 changed files with 584552 additions and 0 deletions

21
vendor/github.com/asticode/go-astisub/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016 Quentin Renard
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

91
vendor/github.com/asticode/go-astisub/README.md generated vendored Normal file
View File

@@ -0,0 +1,91 @@
[![GoReportCard](http://goreportcard.com/badge/github.com/asticode/go-astisub)](http://goreportcard.com/report/github.com/asticode/go-astisub)
[![GoDoc](https://godoc.org/github.com/asticode/go-astisub?status.svg)](https://godoc.org/github.com/asticode/go-astisub)
[![Travis](https://travis-ci.org/asticode/go-astisub.svg?branch=master)](https://travis-ci.org/asticode/go-astisub#)
[![Coveralls](https://coveralls.io/repos/github/asticode/go-astisub/badge.svg?branch=master)](https://coveralls.io/repos/github/asticode/go-astisub)
This is a Golang library to manipulate subtitles.
It allows you to manipulate `srt`, `stl`, `ttml`, `ssa/ass` and `webvtt` files for now.
Available operations are `parsing`, `writing`, `syncing`, `fragmenting`, `unfragmenting`, `merging` and `optimizing`.
# Installation
To install the library and command line program, use the following:
go get -u github.com/asticode/go-astisub/...
# Using the library in your code
WARNING: the code below doesn't handle errors for readibility purposes. However you SHOULD!
```go
// Open subtitles
s1, _ := astisub.OpenFile("/path/to/example.ttml")
s2, _ := astisub.ReadFromSRT(bytes.NewReader([]byte("00:01:00.000 --> 00:02:00.000\nCredits")))
// Add a duration to every subtitles (syncing)
s1.Add(-2*time.Second)
// Fragment the subtitles
s1.Fragment(2*time.Second)
// Merge subtitles
s1.Merge(s2)
// Optimize subtitles
s1.Optimize()
// Unfragment the subtitles
s1.Unfragment()
// Write subtitles
s1.Write("/path/to/example.srt")
var buf = &bytes.Buffer{}
s2.WriteToTTML(buf)
```
# Using the CLI
If **astisub** has been installed properly you can:
- convert any type of subtitle to any other type of subtitle:
astisub convert -i example.srt -o example.ttml
- fragment any type of subtitle:
astisub fragment -i example.srt -f 2s -o example.out.srt
- merge any type of subtitle into any other type of subtitle:
astisub merge -i example.srt -i example.ttml -o example.out.srt
- optimize any type of subtitle:
astisub optimize -i example.srt -o example.out.srt
- unfragment any type of subtitle:
astisub unfragment -i example.srt -o example.out.srt
- sync any type of subtitle:
astisub sync -i example.srt -s "-2s" -o example.out.srt
# Features and roadmap
- [x] parsing
- [x] writing
- [x] syncing
- [x] fragmenting/unfragmenting
- [x] merging
- [x] ordering
- [x] optimizing
- [x] .srt
- [x] .ttml
- [x] .vtt
- [x] .stl
- [x] .ssa/.ass
- [ ] .teletext
- [ ] .smi

7
vendor/github.com/asticode/go-astisub/language.go generated vendored Normal file
View File

@@ -0,0 +1,7 @@
package astisub
// Languages
const (
LanguageEnglish = "english"
LanguageFrench = "french"
)

135
vendor/github.com/asticode/go-astisub/srt.go generated vendored Normal file
View File

@@ -0,0 +1,135 @@
package astisub
import (
"bufio"
"io"
"strconv"
"strings"
"time"
"github.com/pkg/errors"
)
// Constants
const (
srtTimeBoundariesSeparator = " --> "
)
// Vars
var (
bytesSRTTimeBoundariesSeparator = []byte(srtTimeBoundariesSeparator)
)
// parseDurationSRT parses an .srt duration
func parseDurationSRT(i string) (time.Duration, error) {
return parseDuration(i, ",", 3)
}
// ReadFromSRT parses an .srt content
func ReadFromSRT(i io.Reader) (o *Subtitles, err error) {
// Init
o = NewSubtitles()
var scanner = bufio.NewScanner(i)
// Scan
var line string
var s = &Item{}
for scanner.Scan() {
// Fetch line
line = scanner.Text()
// Line contains time boundaries
if strings.Contains(line, srtTimeBoundariesSeparator) {
// Remove last item of previous subtitle since it's the index
s.Lines = s.Lines[:len(s.Lines)-1]
// Remove trailing empty lines
if len(s.Lines) > 0 {
for i := len(s.Lines) - 1; i >= 0; i-- {
if len(s.Lines[i].Items) > 0 {
for j := len(s.Lines[i].Items) - 1; j >= 0; j-- {
if len(s.Lines[i].Items[j].Text) == 0 {
s.Lines[i].Items = s.Lines[i].Items[:j]
} else {
break
}
}
if len(s.Lines[i].Items) == 0 {
s.Lines = s.Lines[:i]
}
}
}
}
// Init subtitle
s = &Item{}
// Fetch time boundaries
boundaries := strings.Split(line, srtTimeBoundariesSeparator)
if s.StartAt, err = parseDurationSRT(boundaries[0]); err != nil {
err = errors.Wrapf(err, "astisub: parsing srt duration %s failed", boundaries[0])
return
}
if s.EndAt, err = parseDurationSRT(boundaries[1]); err != nil {
err = errors.Wrapf(err, "astisub: parsing srt duration %s failed", boundaries[1])
return
}
// Append subtitle
o.Items = append(o.Items, s)
} else {
// Add text
s.Lines = append(s.Lines, Line{Items: []LineItem{{Text: line}}})
}
}
return
}
// formatDurationSRT formats an .srt duration
func formatDurationSRT(i time.Duration) string {
return formatDuration(i, ",", 3)
}
// WriteToSRT writes subtitles in .srt format
func (s Subtitles) WriteToSRT(o io.Writer) (err error) {
// Do not write anything if no subtitles
if len(s.Items) == 0 {
err = ErrNoSubtitlesToWrite
return
}
// Add BOM header
var c []byte
c = append(c, BytesBOM...)
// Loop through subtitles
for k, v := range s.Items {
// Add time boundaries
c = append(c, []byte(strconv.Itoa(k+1))...)
c = append(c, bytesLineSeparator...)
c = append(c, []byte(formatDurationSRT(v.StartAt))...)
c = append(c, bytesSRTTimeBoundariesSeparator...)
c = append(c, []byte(formatDurationSRT(v.EndAt))...)
c = append(c, bytesLineSeparator...)
// Loop through lines
for _, l := range v.Lines {
c = append(c, []byte(l.String())...)
c = append(c, bytesLineSeparator...)
}
// Add new line
c = append(c, bytesLineSeparator...)
}
// Remove last new line
c = c[:len(c)-1]
// Write
if _, err = o.Write(c); err != nil {
err = errors.Wrap(err, "astisub: writing failed")
return
}
return
}

1254
vendor/github.com/asticode/go-astisub/ssa.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

860
vendor/github.com/asticode/go-astisub/stl.go generated vendored Normal file
View File

@@ -0,0 +1,860 @@
package astisub
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"math"
"strconv"
"strings"
"time"
"github.com/asticode/go-astitools/byte"
"github.com/asticode/go-astitools/map"
"github.com/asticode/go-astitools/ptr"
"github.com/pkg/errors"
"golang.org/x/text/unicode/norm"
)
// https://tech.ebu.ch/docs/tech/tech3264.pdf
// https://github.com/yanncoupin/stl2srt/blob/master/to_srt.py
// STL block sizes
const (
stlBlockSizeGSI = 1024
stlBlockSizeTTI = 128
)
// STL character code table number
const (
stlCharacterCodeTableNumberLatin uint16 = 12336
stlCharacterCodeTableNumberLatinCyrillic = 12337
stlCharacterCodeTableNumberLatinArabic = 12338
stlCharacterCodeTableNumberLatinGreek = 12339
stlCharacterCodeTableNumberLatinHebrew = 12340
)
// STL character code tables
// TODO Add missing tables
var (
stlCharacterCodeTables = map[uint16]*astimap.Map{
stlCharacterCodeTableNumberLatin: astimap.NewMap(0x0, "").
Set(0x20, " ").Set(0x21, "!").Set(0x22, "\"").Set(0x23, "#").
Set(0x24, "¤").Set(0x25, "%").Set(0x26, "&").Set(0x27, "'").
Set(0x28, "(").Set(0x29, ")").Set(0x2a, "*").Set(0x2b, "+").
Set(0x2c, ",").Set(0x2d, "-").Set(0x2e, ".").Set(0x2f, "/").
Set(0x30, "0").Set(0x31, "1").Set(0x32, "2").Set(0x33, "3").
Set(0x34, "4").Set(0x35, "5").Set(0x36, "6").Set(0x37, "7").
Set(0x38, "8").Set(0x39, "9").Set(0x3a, ":").Set(0x3b, ";").
Set(0x3c, "<").Set(0x3d, "=").Set(0x3e, ">").Set(0x3f, "?").
Set(0x40, "@").Set(0x41, "A").Set(0x42, "B").Set(0x43, "C").
Set(0x44, "D").Set(0x45, "E").Set(0x46, "F").Set(0x47, "G").
Set(0x48, "H").Set(0x49, "I").Set(0x4a, "J").Set(0x4b, "K").
Set(0x4c, "L").Set(0x4d, "M").Set(0x4e, "N").Set(0x4f, "O").
Set(0x50, "P").Set(0x51, "Q").Set(0x52, "R").Set(0x53, "S").
Set(0x54, "T").Set(0x55, "U").Set(0x56, "V").Set(0x57, "W").
Set(0x58, "X").Set(0x59, "Y").Set(0x5a, "Z").Set(0x5b, "[").
Set(0x5c, "\\").Set(0x5d, "]").Set(0x5e, "^").Set(0x5f, "_").
Set(0x60, "`").Set(0x61, "a").Set(0x62, "b").Set(0x63, "c").
Set(0x64, "d").Set(0x65, "e").Set(0x66, "f").Set(0x67, "g").
Set(0x68, "h").Set(0x69, "i").Set(0x6a, "j").Set(0x6b, "k").
Set(0x6c, "l").Set(0x6d, "m").Set(0x6e, "n").Set(0x6f, "o").
Set(0x70, "p").Set(0x71, "q").Set(0x72, "r").Set(0x73, "s").
Set(0x74, "t").Set(0x75, "u").Set(0x76, "v").Set(0x77, "w").
Set(0x78, "x").Set(0x79, "y").Set(0x7a, "z").Set(0x7b, "{").
Set(0x7c, "|").Set(0x7d, "}").Set(0x7e, "~").
Set(0xa0, string([]byte{0xC2, 0xA0})).Set(0xa1, "¡").Set(0xa2, "¢").
Set(0xa3, "£").Set(0xa4, "$").Set(0xa5, "¥").Set(0xa7, "§").
Set(0xa9, "").Set(0xaa, "“").Set(0xab, "«").Set(0xac, "←").
Set(0xad, "↑").Set(0xae, "→").Set(0xaf, "↓").
Set(0xb0, "°").Set(0xb1, "±").Set(0xb2, "²").Set(0xb3, "³").
Set(0xb4, "×").Set(0xb5, "µ").Set(0xb6, "¶").Set(0xb7, "·").
Set(0xb8, "÷").Set(0xb9, "").Set(0xba, "”").Set(0xbb, "»").
Set(0xbc, "¼").Set(0xbd, "½").Set(0xbe, "¾").Set(0xbf, "¿").
Set(0xc1, string([]byte{0xCC, 0x80})).Set(0xc2, string([]byte{0xCC, 0x81})).
Set(0xc3, string([]byte{0xCC, 0x82})).Set(0xc4, string([]byte{0xCC, 0x83})).
Set(0xc5, string([]byte{0xCC, 0x84})).Set(0xc6, string([]byte{0xCC, 0x86})).
Set(0xc7, string([]byte{0xCC, 0x87})).Set(0xc8, string([]byte{0xCC, 0x88})).
Set(0xca, string([]byte{0xCC, 0x8A})).Set(0xcb, string([]byte{0xCC, 0xA7})).
Set(0xcd, string([]byte{0xCC, 0x8B})).Set(0xce, string([]byte{0xCC, 0xA8})).
Set(0xcf, string([]byte{0xCC, 0x8C})).
Set(0xd0, "―").Set(0xd1, "¹").Set(0xd2, "®").Set(0xd3, "©").
Set(0xd4, "™").Set(0xd5, "♪").Set(0xd6, "¬").Set(0xd7, "¦").
Set(0xdc, "⅛").Set(0xdd, "⅜").Set(0xde, "⅝").Set(0xdf, "⅞").
Set(0xe0, "Ω").Set(0xe1, "Æ").Set(0xe2, "Đ").Set(0xe3, "ª").
Set(0xe4, "Ħ").Set(0xe6, "IJ").Set(0xe7, "Ŀ").Set(0xe8, "Ł").
Set(0xe9, "Ø").Set(0xea, "Œ").Set(0xeb, "º").Set(0xec, "Þ").
Set(0xed, "Ŧ").Set(0xee, "Ŋ").Set(0xef, "ʼn").
Set(0xf0, "ĸ").Set(0xf1, "æ").Set(0xf2, "đ").Set(0xf3, "ð").
Set(0xf4, "ħ").Set(0xf5, "ı").Set(0xf6, "ij").Set(0xf7, "ŀ").
Set(0xf8, "ł").Set(0xf9, "ø").Set(0xfa, "œ").Set(0xfb, "ß").
Set(0xfc, "þ").Set(0xfd, "ŧ").Set(0xfe, "ŋ").Set(0xff, string([]byte{0xC2, 0xAD})),
}
)
// STL code page numbers
const (
stlCodePageNumberCanadaFrench uint32 = 3683891
stlCodePageNumberMultilingual = 3683632
stlCodePageNumberNordic = 3683893
stlCodePageNumberPortugal = 3683888
stlCodePageNumberUnitedStates = 3420983
)
// STL comment flag
const (
stlCommentFlagTextContainsSubtitleData = '\x00'
stlCommentFlagTextContainsCommentsNotIntendedForTransmission = '\x01'
)
// STL country codes
const (
stlCountryCodeFrance = "FRA"
)
// STL cumulative status
const (
stlCumulativeStatusFirstSubtitleOfACumulativeSet = '\x01'
stlCumulativeStatusIntermediateSubtitleOfACumulativeSet = '\x02'
stlCumulativeStatusLastSubtitleOfACumulativeSet = '\x03'
stlCumulativeStatusSubtitleNotPartOfACumulativeSet = '\x00'
)
// STL display standard code
const (
stlDisplayStandardCodeOpenSubtitling = "0"
stlDisplayStandardCodeLevel1Teletext = "1"
stlDisplayStandardCodeLevel2Teletext = "2"
)
// STL framerate mapping
var stlFramerateMapping = astimap.NewMap("STL25.01", 25).
Set("STL25.01", 25).
Set("STL30.01", 30)
// STL justification code
const (
stlJustificationCodeCentredText = '\x02'
stlJustificationCodeLeftJustifiedText = '\x01'
stlJustificationCodeRightJustifiedText = '\x03'
stlJustificationCodeUnchangedPresentation = '\x00'
)
// STL language codes
const (
stlLanguageCodeEnglish = "09"
stlLanguageCodeFrench = "0F"
)
// STL language mapping
var stlLanguageMapping = astimap.NewMap(stlLanguageCodeEnglish, LanguageEnglish).
Set(stlLanguageCodeFrench, LanguageFrench)
// STL timecode status
const (
stlTimecodeStatusNotIntendedForUse = "0"
stlTimecodeStatusIntendedForUse = "1"
)
// TTI Special Extension Block Number
const extensionBlockNumberReservedUserData = 0xfe
// ReadFromSTL parses an .stl content
func ReadFromSTL(i io.Reader) (o *Subtitles, err error) {
// Init
o = NewSubtitles()
// Read GSI block
var b []byte
if b, err = readNBytes(i, stlBlockSizeGSI); err != nil {
return
}
// Parse GSI block
var g *gsiBlock
if g, err = parseGSIBlock(b); err != nil {
err = errors.Wrap(err, "astisub: building gsi block failed")
return
}
// Create character handler
var ch *stlCharacterHandler
if ch, err = newSTLCharacterHandler(g.characterCodeTableNumber); err != nil {
err = errors.Wrap(err, "astisub: creating stl character handler failed")
return
}
// Update metadata
// TODO Add more STL fields to metadata
o.Metadata = &Metadata{
Framerate: g.framerate,
Language: stlLanguageMapping.B(g.languageCode).(string),
STLPublisher: g.publisher,
Title: g.originalProgramTitle,
}
// Parse Text and Timing Information (TTI) blocks.
for {
// Read TTI block
if b, err = readNBytes(i, stlBlockSizeTTI); err != nil {
if err == io.EOF {
err = nil
break
}
return
}
// Parse TTI block
var t = parseTTIBlock(b, g.framerate)
if t.extensionBlockNumber != extensionBlockNumberReservedUserData {
// Create item
var i = &Item{
EndAt: t.timecodeOut - g.timecodeStartOfProgramme,
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)
}
// Append item
o.Items = append(o.Items, i)
}
}
return
}
// readNBytes reads n bytes
func readNBytes(i io.Reader, c int) (o []byte, err error) {
o = make([]byte, c)
var n int
if n, err = i.Read(o); err != nil || n != len(o) {
if err != nil {
if err == io.EOF {
return
}
err = errors.Wrapf(err, "astisub: reading %d bytes failed", c)
return
}
err = fmt.Errorf("astisub: Read %d bytes, should have read %d", n, c)
return
}
return
}
// gsiBlock represents a GSI block
type gsiBlock struct {
characterCodeTableNumber uint16
codePageNumber uint32
countryOfOrigin string
creationDate time.Time
diskSequenceNumber int
displayStandardCode string
editorContactDetails string
editorName string
framerate int
languageCode string
maximumNumberOfDisplayableCharactersInAnyTextRow int
maximumNumberOfDisplayableRows int
originalEpisodeTitle string
originalProgramTitle string
publisher string
revisionDate time.Time
revisionNumber int
subtitleListReferenceCode string
timecodeFirstInCue time.Duration
timecodeStartOfProgramme time.Duration
timecodeStatus string
totalNumberOfDisks int
totalNumberOfSubtitleGroups int
totalNumberOfSubtitles int
totalNumberOfTTIBlocks int
translatedEpisodeTitle string
translatedProgramTitle string
translatorContactDetails string
translatorName string
userDefinedArea string
}
// newGSIBlock builds the subtitles GSI block
func newGSIBlock(s Subtitles) (g *gsiBlock) {
// Init
g = &gsiBlock{
characterCodeTableNumber: stlCharacterCodeTableNumberLatin,
codePageNumber: stlCodePageNumberMultilingual,
countryOfOrigin: stlCountryCodeFrance,
creationDate: Now(),
diskSequenceNumber: 1,
displayStandardCode: stlDisplayStandardCodeLevel1Teletext,
framerate: 25,
languageCode: stlLanguageCodeFrench,
maximumNumberOfDisplayableCharactersInAnyTextRow: 40,
maximumNumberOfDisplayableRows: 23,
subtitleListReferenceCode: "12345678",
timecodeStatus: stlTimecodeStatusIntendedForUse,
totalNumberOfDisks: 1,
totalNumberOfSubtitleGroups: 1,
totalNumberOfSubtitles: len(s.Items),
totalNumberOfTTIBlocks: len(s.Items),
}
// Add metadata
if s.Metadata != nil {
g.framerate = s.Metadata.Framerate
g.languageCode = stlLanguageMapping.A(s.Metadata.Language).(string)
g.originalProgramTitle = s.Metadata.Title
g.publisher = s.Metadata.STLPublisher
}
// Timecode first in cue
if len(s.Items) > 0 {
g.timecodeFirstInCue = s.Items[0].StartAt
}
return
}
// parseGSIBlock parses a GSI block
func parseGSIBlock(b []byte) (g *gsiBlock, err error) {
// Init
g = &gsiBlock{
characterCodeTableNumber: binary.BigEndian.Uint16(b[12:14]),
countryOfOrigin: string(bytes.TrimSpace(b[274:277])),
codePageNumber: binary.BigEndian.Uint32(append([]byte{0x0}, b[0:3]...)),
displayStandardCode: string(bytes.TrimSpace([]byte{b[11]})),
editorName: string(bytes.TrimSpace(b[309:341])),
editorContactDetails: string(bytes.TrimSpace(b[341:373])),
framerate: stlFramerateMapping.B(string(b[3:11])).(int),
languageCode: string(bytes.TrimSpace(b[14:16])),
originalEpisodeTitle: string(bytes.TrimSpace(b[48:80])),
originalProgramTitle: string(bytes.TrimSpace(b[16:48])),
publisher: string(bytes.TrimSpace(b[277:309])),
subtitleListReferenceCode: string(bytes.TrimSpace(b[208:224])),
timecodeStatus: string(bytes.TrimSpace([]byte{b[255]})),
translatedEpisodeTitle: string(bytes.TrimSpace(b[80:112])),
translatedProgramTitle: string(bytes.TrimSpace(b[112:144])),
translatorContactDetails: string(bytes.TrimSpace(b[176:208])),
translatorName: string(bytes.TrimSpace(b[144:176])),
userDefinedArea: string(bytes.TrimSpace(b[448:])),
}
// Creation date
if v := strings.TrimSpace(string(b[224:230])); len(v) > 0 {
if g.creationDate, err = time.Parse("060102", v); err != nil {
err = errors.Wrapf(err, "astisub: parsing date %s failed", v)
return
}
}
// Revision date
if v := strings.TrimSpace(string(b[230:236])); len(v) > 0 {
if g.revisionDate, err = time.Parse("060102", v); err != nil {
err = errors.Wrapf(err, "astisub: parsing date %s failed", v)
return
}
}
// Revision number
if v := strings.TrimSpace(string(b[236:238])); len(v) > 0 {
if g.revisionNumber, err = strconv.Atoi(v); err != nil {
err = errors.Wrapf(err, "astisub: atoi of %s failed", v)
return
}
}
// Total number of TTI blocks
if v := strings.TrimSpace(string(b[238:243])); len(v) > 0 {
if g.totalNumberOfTTIBlocks, err = strconv.Atoi(v); err != nil {
err = errors.Wrapf(err, "astisub: atoi of %s failed", v)
return
}
}
// Total number of subtitles
if v := strings.TrimSpace(string(b[243:248])); len(v) > 0 {
if g.totalNumberOfSubtitles, err = strconv.Atoi(v); err != nil {
err = errors.Wrapf(err, "astisub: atoi of %s failed", v)
return
}
}
// Total number of subtitle groups
if v := strings.TrimSpace(string(b[248:251])); len(v) > 0 {
if g.totalNumberOfSubtitleGroups, err = strconv.Atoi(v); err != nil {
err = errors.Wrapf(err, "astisub: atoi of %s failed", v)
return
}
}
// Maximum number of displayable characters in any text row
if v := strings.TrimSpace(string(b[251:253])); len(v) > 0 {
if g.maximumNumberOfDisplayableCharactersInAnyTextRow, err = strconv.Atoi(v); err != nil {
err = errors.Wrapf(err, "astisub: atoi of %s failed", v)
return
}
}
// Maximum number of displayable rows
if v := strings.TrimSpace(string(b[253:255])); len(v) > 0 {
if g.maximumNumberOfDisplayableRows, err = strconv.Atoi(v); err != nil {
err = errors.Wrapf(err, "astisub: atoi of %s failed", v)
return
}
}
// Timecode start of programme
if v := strings.TrimSpace(string(b[256:264])); len(v) > 0 {
if g.timecodeStartOfProgramme, err = parseDurationSTL(v, g.framerate); err != nil {
err = errors.Wrapf(err, "astisub: parsing of stl duration %s failed", v)
return
}
}
// Timecode first in cue
if v := strings.TrimSpace(string(b[264:272])); len(v) > 0 {
if g.timecodeFirstInCue, err = parseDurationSTL(v, g.framerate); err != nil {
err = errors.Wrapf(err, "astisub: parsing of stl duration %s failed", v)
return
}
}
// Total number of disks
if v := strings.TrimSpace(string(b[272])); len(v) > 0 {
if g.totalNumberOfDisks, err = strconv.Atoi(v); err != nil {
err = errors.Wrapf(err, "astisub: atoi of %s failed", v)
return
}
}
// Disk sequence number
if v := strings.TrimSpace(string(b[273])); len(v) > 0 {
if g.diskSequenceNumber, err = strconv.Atoi(v); err != nil {
err = errors.Wrapf(err, "astisub: atoi of %s failed", v)
return
}
}
return
}
// bytes transforms the GSI block into []byte
func (b gsiBlock) bytes() (o []byte) {
bs := make([]byte, 4)
binary.BigEndian.PutUint32(bs, b.codePageNumber)
o = append(o, astibyte.ToLength(bs[1:], ' ', 3)...) // Code page number
o = append(o, astibyte.ToLength([]byte(stlFramerateMapping.A(b.framerate).(string)), ' ', 8)...) // Disk format code
o = append(o, astibyte.ToLength([]byte(b.displayStandardCode), ' ', 1)...) // Display standard code
binary.BigEndian.PutUint16(bs, b.characterCodeTableNumber)
o = append(o, astibyte.ToLength(bs[:2], ' ', 2)...) // Character code table number
o = append(o, astibyte.ToLength([]byte(b.languageCode), ' ', 2)...) // Language code
o = append(o, astibyte.ToLength([]byte(b.originalProgramTitle), ' ', 32)...) // Original program title
o = append(o, astibyte.ToLength([]byte(b.originalEpisodeTitle), ' ', 32)...) // Original episode title
o = append(o, astibyte.ToLength([]byte(b.translatedProgramTitle), ' ', 32)...) // Translated program title
o = append(o, astibyte.ToLength([]byte(b.translatedEpisodeTitle), ' ', 32)...) // Translated episode title
o = append(o, astibyte.ToLength([]byte(b.translatorName), ' ', 32)...) // Translator's name
o = append(o, astibyte.ToLength([]byte(b.translatorContactDetails), ' ', 32)...) // Translator's contact details
o = append(o, astibyte.ToLength([]byte(b.subtitleListReferenceCode), ' ', 16)...) // Subtitle list reference code
o = append(o, astibyte.ToLength([]byte(b.creationDate.Format("060102")), ' ', 6)...) // Creation date
o = append(o, astibyte.ToLength([]byte(b.revisionDate.Format("060102")), ' ', 6)...) // Revision date
o = append(o, astibyte.ToLength(astibyte.PadLeft([]byte(strconv.Itoa(b.revisionNumber)), '0', 2), '0', 2)...) // Revision number
o = append(o, astibyte.ToLength(astibyte.PadLeft([]byte(strconv.Itoa(b.totalNumberOfTTIBlocks)), '0', 5), '0', 5)...) // Total number of TTI blocks
o = append(o, astibyte.ToLength(astibyte.PadLeft([]byte(strconv.Itoa(b.totalNumberOfSubtitles)), '0', 5), '0', 5)...) // Total number of subtitles
o = append(o, astibyte.ToLength(astibyte.PadLeft([]byte(strconv.Itoa(b.totalNumberOfSubtitleGroups)), '0', 3), '0', 3)...) // Total number of subtitle groups
o = append(o, astibyte.ToLength(astibyte.PadLeft([]byte(strconv.Itoa(b.maximumNumberOfDisplayableCharactersInAnyTextRow)), '0', 2), '0', 2)...) // Maximum number of displayable characters in any text row
o = append(o, astibyte.ToLength(astibyte.PadLeft([]byte(strconv.Itoa(b.maximumNumberOfDisplayableRows)), '0', 2), '0', 2)...) // Maximum number of displayable rows
o = append(o, astibyte.ToLength([]byte(b.timecodeStatus), ' ', 1)...) // Timecode status
o = append(o, astibyte.ToLength([]byte(formatDurationSTL(b.timecodeStartOfProgramme, b.framerate)), ' ', 8)...) // Timecode start of a programme
o = append(o, astibyte.ToLength([]byte(formatDurationSTL(b.timecodeFirstInCue, b.framerate)), ' ', 8)...) // Timecode first in cue
o = append(o, astibyte.ToLength([]byte(strconv.Itoa(b.totalNumberOfDisks)), ' ', 1)...) // Total number of disks
o = append(o, astibyte.ToLength([]byte(strconv.Itoa(b.diskSequenceNumber)), ' ', 1)...) // Disk sequence number
o = append(o, astibyte.ToLength([]byte(b.countryOfOrigin), ' ', 3)...) // Country of origin
o = append(o, astibyte.ToLength([]byte(b.publisher), ' ', 32)...) // Publisher
o = append(o, astibyte.ToLength([]byte(b.editorName), ' ', 32)...) // Editor's name
o = append(o, astibyte.ToLength([]byte(b.editorContactDetails), ' ', 32)...) // Editor's contact details
o = append(o, astibyte.ToLength([]byte{}, ' ', 75+576)...) // Spare bytes + user defined area // // Editor's contact details
return
}
// parseDurationSTL parses a STL duration
func parseDurationSTL(i string, framerate int) (d time.Duration, err error) {
// Parse hours
var hours, hoursString = 0, i[0:2]
if hours, err = strconv.Atoi(hoursString); err != nil {
err = errors.Wrapf(err, "astisub: atoi of %s failed", hoursString)
return
}
// Parse minutes
var minutes, minutesString = 0, i[2:4]
if minutes, err = strconv.Atoi(minutesString); err != nil {
err = errors.Wrapf(err, "astisub: atoi of %s failed", minutesString)
return
}
// Parse seconds
var seconds, secondsString = 0, i[4:6]
if seconds, err = strconv.Atoi(secondsString); err != nil {
err = errors.Wrapf(err, "astisub: atoi of %s failed", secondsString)
return
}
// Parse frames
var frames, framesString = 0, i[6:8]
if frames, err = strconv.Atoi(framesString); err != nil {
err = errors.Wrapf(err, "astisub: atoi of %s failed", framesString)
return
}
// Set duration
d = time.Duration(hours)*time.Hour + time.Duration(minutes)*time.Minute + time.Duration(seconds)*time.Second + time.Duration(1e9*frames/framerate)*time.Nanosecond
return
}
// formatDurationSTL formats a STL duration
func formatDurationSTL(d time.Duration, framerate int) (o string) {
// Add hours
if d.Hours() < 10 {
o += "0"
}
var delta = int(math.Floor(d.Hours()))
o += strconv.Itoa(delta)
d -= time.Duration(delta) * time.Hour
// Add minutes
if d.Minutes() < 10 {
o += "0"
}
delta = int(math.Floor(d.Minutes()))
o += strconv.Itoa(delta)
d -= time.Duration(delta) * time.Minute
// Add seconds
if d.Seconds() < 10 {
o += "0"
}
delta = int(math.Floor(d.Seconds()))
o += strconv.Itoa(delta)
d -= time.Duration(delta) * time.Second
// Add frames
var frames = int(int(d.Nanoseconds()) * framerate / 1e9)
if frames < 10 {
o += "0"
}
o += strconv.Itoa(frames)
return
}
// ttiBlock represents a TTI block
type ttiBlock struct {
commentFlag byte
cumulativeStatus byte
extensionBlockNumber int
justificationCode byte
subtitleGroupNumber int
subtitleNumber int
text []byte
timecodeIn time.Duration
timecodeOut time.Duration
verticalPosition int
}
// newTTIBlock builds an item TTI block
func newTTIBlock(i *Item, idx int) (t *ttiBlock) {
// Init
t = &ttiBlock{
commentFlag: stlCommentFlagTextContainsSubtitleData,
cumulativeStatus: stlCumulativeStatusSubtitleNotPartOfACumulativeSet,
extensionBlockNumber: 255,
justificationCode: stlJustificationCodeLeftJustifiedText,
subtitleGroupNumber: 0,
subtitleNumber: idx,
timecodeIn: i.StartAt,
timecodeOut: i.EndAt,
verticalPosition: 20,
}
// Add text
var lines []string
for _, l := range i.Lines {
lines = append(lines, l.String())
}
t.text = []byte(strings.Join(lines, "\n"))
return
}
// parseTTIBlock parses a TTI block
func parseTTIBlock(p []byte, framerate int) *ttiBlock {
return &ttiBlock{
commentFlag: p[15],
cumulativeStatus: p[4],
extensionBlockNumber: int(uint8(p[3])),
justificationCode: p[14],
subtitleGroupNumber: int(uint8(p[0])),
subtitleNumber: int(binary.LittleEndian.Uint16(p[1:3])),
text: p[16:128],
timecodeIn: parseDurationSTLBytes(p[5:9], framerate),
timecodeOut: parseDurationSTLBytes(p[9:13], framerate),
verticalPosition: int(uint8(p[13])),
}
}
// bytes transforms the TTI block into []byte
func (t *ttiBlock) bytes(g *gsiBlock) (o []byte) {
o = append(o, byte(uint8(t.subtitleGroupNumber))) // Subtitle group number
var b = make([]byte, 2)
binary.LittleEndian.PutUint16(b, uint16(t.subtitleNumber))
o = append(o, b...) // Subtitle number
o = append(o, byte(uint8(t.extensionBlockNumber))) // Extension block number
o = append(o, t.cumulativeStatus) // Cumulative status
o = append(o, formatDurationSTLBytes(t.timecodeIn, g.framerate)...) // Timecode in
o = append(o, formatDurationSTLBytes(t.timecodeOut, g.framerate)...) // Timecode out
o = append(o, byte(uint8(t.verticalPosition))) // Vertical position
o = append(o, t.justificationCode) // Justification code
o = append(o, t.commentFlag) // Comment flag
o = append(o, astibyte.ToLength(encodeTextSTL(string(t.text)), '\x8f', 112)...) // Text field
return
}
// formatDurationSTLBytes formats a STL duration in bytes
func formatDurationSTLBytes(d time.Duration, framerate int) (o []byte) {
// Add hours
var hours = int(math.Floor(d.Hours()))
o = append(o, byte(uint8(hours)))
d -= time.Duration(hours) * time.Hour
// Add minutes
var minutes = int(math.Floor(d.Minutes()))
o = append(o, byte(uint8(minutes)))
d -= time.Duration(minutes) * time.Minute
// Add seconds
var seconds = int(math.Floor(d.Seconds()))
o = append(o, byte(uint8(seconds)))
d -= time.Duration(seconds) * time.Second
// Add frames
var frames = int(int(d.Nanoseconds()) * framerate / 1e9)
o = append(o, byte(uint8(frames)))
return
}
// parseDurationSTLBytes parses a STL duration in bytes
func parseDurationSTLBytes(b []byte, framerate int) time.Duration {
return time.Duration(uint8(b[0]))*time.Hour + time.Duration(uint8(b[1]))*time.Minute + time.Duration(uint8(b[2]))*time.Second + time.Duration(1e9*int(uint8(b[3]))/framerate)*time.Nanosecond
}
type stlCharacterHandler struct {
accent string
c uint16
m *astimap.Map
}
func newSTLCharacterHandler(characterCodeTable uint16) (*stlCharacterHandler, error) {
if v, ok := stlCharacterCodeTables[characterCodeTable]; ok {
return &stlCharacterHandler{
c: characterCodeTable,
m: v,
}, nil
}
return nil, fmt.Errorf("astisub: table doesn't exist for character code table %d", characterCodeTable)
}
// TODO Use this instead of encodeTextSTL => use in teletext process like for decode
// TODO Test
func (h *stlCharacterHandler) encode(i []byte) byte {
return ' '
}
func (h *stlCharacterHandler) decode(i byte) (o []byte) {
k := int(i)
if !h.m.InA(k) {
return
}
v := h.m.B(k).(string)
if len(h.accent) > 0 {
o = norm.NFC.Bytes([]byte(v + h.accent))
h.accent = ""
return
} else if h.c == stlCharacterCodeTableNumberLatin && k >= 0xc0 && k <= 0xcf {
h.accent = v
return
}
return []byte(v)
}
type stlStyler struct {
boxing *bool
italics *bool
underline *bool
}
func newSTLStyler() *stlStyler {
return &stlStyler{}
}
func (s *stlStyler) parseSpacingAttribute(i byte) {
switch i {
case 0x80:
s.italics = astiptr.Bool(true)
case 0x81:
s.italics = astiptr.Bool(false)
case 0x82:
s.underline = astiptr.Bool(true)
case 0x83:
s.underline = astiptr.Bool(false)
case 0x84:
s.boxing = astiptr.Bool(true)
case 0x85:
s.boxing = astiptr.Bool(false)
}
}
func (s *stlStyler) hasBeenSet() bool {
return s.italics != nil || s.boxing != nil || s.underline != nil
}
func (s *stlStyler) hasChanged(sa *StyleAttributes) bool {
return s.boxing != sa.STLBoxing || s.italics != sa.STLItalics || s.underline != sa.STLUnderline
}
func (s *stlStyler) propagateStyleAttributes(sa *StyleAttributes) {
sa.propagateSTLAttributes()
}
func (s *stlStyler) update(sa *StyleAttributes) {
if s.boxing != nil && s.boxing != sa.STLBoxing {
sa.STLBoxing = s.boxing
}
if s.italics != nil && s.italics != sa.STLItalics {
sa.STLItalics = s.italics
}
if s.underline != nil && s.underline != sa.STLUnderline {
sa.STLUnderline = s.underline
}
}
// WriteToSTL writes subtitles in .stl format
func (s Subtitles) WriteToSTL(o io.Writer) (err error) {
// Do not write anything if no subtitles
if len(s.Items) == 0 {
err = ErrNoSubtitlesToWrite
return
}
// Write GSI block
var g = newGSIBlock(s)
if _, err = o.Write(g.bytes()); err != nil {
err = errors.Wrap(err, "astisub: writing gsi block failed")
return
}
// Loop through items
for idx, item := range s.Items {
// Write tti block
if _, err = o.Write(newTTIBlock(item, idx+1).bytes(g)); err != nil {
err = errors.Wrapf(err, "astisub: writing tti block #%d failed", idx+1)
return
}
}
return
}
// TODO Remove below
// STL unicode diacritic
var stlUnicodeDiacritic = astimap.NewMap(byte('\x00'), "\x00").
Set(byte('\xc1'), "\u0300"). // Grave accent
Set(byte('\xc2'), "\u0301"). // Acute accent
Set(byte('\xc3'), "\u0302"). // Circumflex
Set(byte('\xc4'), "\u0303"). // Tilde
Set(byte('\xc5'), "\u0304"). // Macron
Set(byte('\xc6'), "\u0306"). // Breve
Set(byte('\xc7'), "\u0307"). // Dot
Set(byte('\xc8'), "\u0308"). // Umlaut
Set(byte('\xca'), "\u030a"). // Ring
Set(byte('\xcb'), "\u0327"). // Cedilla
Set(byte('\xcd'), "\u030B"). // Double acute accent
Set(byte('\xce'), "\u0328"). // Ogonek
Set(byte('\xcf'), "\u030c") // Caron
// STL unicode mapping
var stlUnicodeMapping = astimap.NewMap(byte('\x00'), "\x00").
Set(byte('\x8a'), "\u000a"). // Line break
Set(byte('\xa8'), "\u00a4"). // ¤
Set(byte('\xa9'), "\u2018"). //
Set(byte('\xaa'), "\u201C"). // “
Set(byte('\xab'), "\u00AB"). // «
Set(byte('\xac'), "\u2190"). // ←
Set(byte('\xad'), "\u2191"). // ↑
Set(byte('\xae'), "\u2192"). // →
Set(byte('\xaf'), "\u2193"). // ↓
Set(byte('\xb4'), "\u00D7"). // ×
Set(byte('\xb8'), "\u00F7"). // ÷
Set(byte('\xb9'), "\u2019"). //
Set(byte('\xba'), "\u201D"). // ”
Set(byte('\xbc'), "\u00BC"). // ¼
Set(byte('\xbd'), "\u00BD"). // ½
Set(byte('\xbe'), "\u00BE"). // ¾
Set(byte('\xbf'), "\u00BF"). // ¿
Set(byte('\xd0'), "\u2015"). // ―
Set(byte('\xd1'), "\u00B9"). // ¹
Set(byte('\xd2'), "\u00AE"). // ®
Set(byte('\xd3'), "\u00A9"). // ©
Set(byte('\xd4'), "\u2122"). // ™
Set(byte('\xd5'), "\u266A"). // ♪
Set(byte('\xd6'), "\u00AC"). // ¬
Set(byte('\xd7'), "\u00A6"). // ¦
Set(byte('\xdc'), "\u215B"). // ⅛
Set(byte('\xdd'), "\u215C"). // ⅜
Set(byte('\xde'), "\u215D"). // ⅝
Set(byte('\xdf'), "\u215E"). // ⅞
Set(byte('\xe0'), "\u2126"). // Ohm Ω
Set(byte('\xe1'), "\u00C6"). // Æ
Set(byte('\xe2'), "\u0110"). // Đ
Set(byte('\xe3'), "\u00AA"). // ª
Set(byte('\xe4'), "\u0126"). // Ħ
Set(byte('\xe6'), "\u0132"). // IJ
Set(byte('\xe7'), "\u013F"). // Ŀ
Set(byte('\xe8'), "\u0141"). // Ł
Set(byte('\xe9'), "\u00D8"). // Ø
Set(byte('\xea'), "\u0152"). // Œ
Set(byte('\xeb'), "\u00BA"). // º
Set(byte('\xec'), "\u00DE"). // Þ
Set(byte('\xed'), "\u0166"). // Ŧ
Set(byte('\xee'), "\u014A"). // Ŋ
Set(byte('\xef'), "\u0149"). // ʼn
Set(byte('\xf0'), "\u0138"). // ĸ
Set(byte('\xf1'), "\u00E6"). // æ
Set(byte('\xf2'), "\u0111"). // đ
Set(byte('\xf3'), "\u00F0"). // ð
Set(byte('\xf4'), "\u0127"). // ħ
Set(byte('\xf5'), "\u0131"). // ı
Set(byte('\xf6'), "\u0133"). // ij
Set(byte('\xf7'), "\u0140"). // ŀ
Set(byte('\xf8'), "\u0142"). // ł
Set(byte('\xf9'), "\u00F8"). // ø
Set(byte('\xfa'), "\u0153"). // œ
Set(byte('\xfb'), "\u00DF"). // ß
Set(byte('\xfc'), "\u00FE"). // þ
Set(byte('\xfd'), "\u0167"). // ŧ
Set(byte('\xfe'), "\u014B"). // ŋ
Set(byte('\xff'), "\u00AD") // Soft hyphen
// encodeTextSTL encodes the STL text
func encodeTextSTL(i string) (o []byte) {
i = string(norm.NFD.Bytes([]byte(i)))
for _, c := range i {
if stlUnicodeMapping.InB(string(c)) {
o = append(o, stlUnicodeMapping.A(string(c)).(byte))
} else if stlUnicodeDiacritic.InB(string(c)) {
o = append(o[:len(o)-1], stlUnicodeDiacritic.A(string(c)).(byte), o[len(o)-1])
} else {
o = append(o, byte(c))
}
}
return
}

702
vendor/github.com/asticode/go-astisub/subtitles.go generated vendored Normal file
View File

@@ -0,0 +1,702 @@
package astisub
import (
"fmt"
"math"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/pkg/errors"
)
// Bytes
var (
BytesBOM = []byte{239, 187, 191}
bytesLineSeparator = []byte("\n")
bytesSpace = []byte(" ")
)
// Colors
var (
ColorBlack = &Color{}
ColorBlue = &Color{Blue: 255}
ColorCyan = &Color{Blue: 255, Green: 255}
ColorGray = &Color{Blue: 128, Green: 128, Red: 128}
ColorGreen = &Color{Green: 128}
ColorLime = &Color{Green: 255}
ColorMagenta = &Color{Blue: 255, Red: 255}
ColorMaroon = &Color{Red: 128}
ColorNavy = &Color{Blue: 128}
ColorOlive = &Color{Green: 128, Red: 128}
ColorPurple = &Color{Blue: 128, Red: 128}
ColorRed = &Color{Red: 255}
ColorSilver = &Color{Blue: 192, Green: 192, Red: 192}
ColorTeal = &Color{Blue: 128, Green: 128}
ColorYellow = &Color{Green: 255, Red: 255}
ColorWhite = &Color{Blue: 255, Green: 255, Red: 255}
)
// Errors
var (
ErrInvalidExtension = errors.New("astisub: invalid extension")
ErrNoSubtitlesToWrite = errors.New("astisub: no subtitles to write")
)
// Now allows testing functions using it
var Now = func() time.Time {
return time.Now()
}
// Options represents open or write options
type Options struct {
Filename string
Teletext TeletextOptions
}
// Open opens a subtitle reader based on options
func Open(o Options) (s *Subtitles, err error) {
// Open the file
var f *os.File
if f, err = os.Open(o.Filename); err != nil {
err = errors.Wrapf(err, "astisub: opening %s failed", o.Filename)
return
}
defer f.Close()
// Parse the content
switch filepath.Ext(o.Filename) {
case ".srt":
s, err = ReadFromSRT(f)
case ".ssa", ".ass":
s, err = ReadFromSSA(f)
case ".stl":
s, err = ReadFromSTL(f)
case ".ts":
s, err = ReadFromTeletext(f, o.Teletext)
case ".ttml":
s, err = ReadFromTTML(f)
case ".vtt":
s, err = ReadFromWebVTT(f)
default:
err = ErrInvalidExtension
}
return
}
// OpenFile opens a file regardless of other options
func OpenFile(filename string) (*Subtitles, error) {
return Open(Options{Filename: filename})
}
// Subtitles represents an ordered list of items with formatting
type Subtitles struct {
Items []*Item
Metadata *Metadata
Regions map[string]*Region
Styles map[string]*Style
}
// NewSubtitles creates new subtitles
func NewSubtitles() *Subtitles {
return &Subtitles{
Regions: make(map[string]*Region),
Styles: make(map[string]*Style),
}
}
// Item represents a text to show between 2 time boundaries with formatting
type Item struct {
Comments []string
EndAt time.Duration
InlineStyle *StyleAttributes
Lines []Line
Region *Region
StartAt time.Duration
Style *Style
}
// String implements the Stringer interface
func (i Item) String() string {
var os []string
for _, l := range i.Lines {
os = append(os, l.String())
}
return strings.Join(os, " - ")
}
// Color represents a color
type Color struct {
Alpha, Blue, Green, Red uint8
}
// newColorFromSSAString builds a new color based on an SSA string
func newColorFromSSAString(s string, base int) (c *Color, err error) {
var i int64
if i, err = strconv.ParseInt(s, base, 64); err != nil {
err = errors.Wrapf(err, "parsing int %s with base %d failed", s, base)
return
}
c = &Color{
Alpha: uint8(i>>24) & 0xff,
Blue: uint8(i>>16) & 0xff,
Green: uint8(i>>8) & 0xff,
Red: uint8(i) & 0xff,
}
return
}
// SSAString expresses the color as an SSA string
func (c *Color) SSAString() string {
return fmt.Sprintf("%.8x", uint32(c.Alpha)<<24|uint32(c.Blue)<<16|uint32(c.Green)<<8|uint32(c.Red))
}
// TTMLString expresses the color as a TTML string
func (c *Color) TTMLString() string {
return fmt.Sprintf("%.6x", uint32(c.Red)<<16|uint32(c.Green)<<8|uint32(c.Blue))
}
// StyleAttributes represents style attributes
type StyleAttributes struct {
SSAAlignment *int
SSAAlphaLevel *float64
SSAAngle *float64 // degrees
SSABackColour *Color
SSABold *bool
SSABorderStyle *int
SSAEffect string
SSAEncoding *int
SSAFontName string
SSAFontSize *float64
SSAItalic *bool
SSALayer *int
SSAMarginLeft *int // pixels
SSAMarginRight *int // pixels
SSAMarginVertical *int // pixels
SSAMarked *bool
SSAOutline *int // pixels
SSAOutlineColour *Color
SSAPrimaryColour *Color
SSAScaleX *float64 // %
SSAScaleY *float64 // %
SSASecondaryColour *Color
SSAShadow *int // pixels
SSASpacing *int // pixels
SSAStrikeout *bool
SSAUnderline *bool
STLBoxing *bool
STLItalics *bool
STLUnderline *bool
TeletextColor *Color
TeletextDoubleHeight *bool
TeletextDoubleSize *bool
TeletextDoubleWidth *bool
TeletextSpacesAfter *int
TeletextSpacesBefore *int
// TODO Use pointers with real types below
TTMLBackgroundColor string // https://htmlcolorcodes.com/fr/
TTMLColor string
TTMLDirection string
TTMLDisplay string
TTMLDisplayAlign string
TTMLExtent string
TTMLFontFamily string
TTMLFontSize string
TTMLFontStyle string
TTMLFontWeight string
TTMLLineHeight string
TTMLOpacity string
TTMLOrigin string
TTMLOverflow string
TTMLPadding string
TTMLShowBackground string
TTMLTextAlign string
TTMLTextDecoration string
TTMLTextOutline string
TTMLUnicodeBidi string
TTMLVisibility string
TTMLWrapOption string
TTMLWritingMode string
TTMLZIndex int
WebVTTAlign string
WebVTTLine string
WebVTTLines int
WebVTTPosition string
WebVTTRegionAnchor string
WebVTTScroll string
WebVTTSize string
WebVTTVertical string
WebVTTViewportAnchor string
WebVTTWidth string
}
func (sa *StyleAttributes) propagateSSAAttributes() {}
func (sa *StyleAttributes) propagateSTLAttributes() {}
func (sa *StyleAttributes) propagateTeletextAttributes() {
if sa.TeletextColor != nil {
sa.TTMLColor = "#" + sa.TeletextColor.TTMLString()
}
}
func (sa *StyleAttributes) propagateTTMLAttributes() {}
func (sa *StyleAttributes) propagateWebVTTAttributes() {}
// Metadata represents metadata
// TODO Merge attributes
type Metadata struct {
Comments []string
Framerate int
Language string
SSACollisions string
SSAOriginalEditing string
SSAOriginalScript string
SSAOriginalTiming string
SSAOriginalTranslation string
SSAPlayDepth *int
SSAPlayResX, SSAPlayResY *int
SSAScriptType string
SSAScriptUpdatedBy string
SSASynchPoint string
SSATimer *float64
SSAUpdateDetails string
SSAWrapStyle string
STLPublisher string
Title string
TTMLCopyright string
}
// Region represents a subtitle's region
type Region struct {
ID string
InlineStyle *StyleAttributes
Style *Style
}
// Style represents a subtitle's style
type Style struct {
ID string
InlineStyle *StyleAttributes
Style *Style
}
// Line represents a set of formatted line items
type Line struct {
Items []LineItem
VoiceName string
}
// String implement the Stringer interface
func (l Line) String() string {
var texts []string
for _, i := range l.Items {
texts = append(texts, i.Text)
}
return strings.Join(texts, " ")
}
// LineItem represents a formatted line item
type LineItem struct {
InlineStyle *StyleAttributes
Style *Style
Text string
}
// Add adds a duration to each time boundaries. As in the time package, duration can be negative.
func (s *Subtitles) Add(d time.Duration) {
for idx := 0; idx < len(s.Items); idx++ {
s.Items[idx].EndAt += d
s.Items[idx].StartAt += d
if s.Items[idx].EndAt <= 0 && s.Items[idx].StartAt <= 0 {
s.Items = append(s.Items[:idx], s.Items[idx+1:]...)
idx--
} else if s.Items[idx].StartAt <= 0 {
s.Items[idx].StartAt = time.Duration(0)
}
}
}
// Duration returns the subtitles duration
func (s Subtitles) Duration() time.Duration {
if len(s.Items) == 0 {
return time.Duration(0)
}
return s.Items[len(s.Items)-1].EndAt
}
// ForceDuration updates the subtitles duration.
// If requested duration is bigger, then we create a dummy item.
// If requested duration is smaller, then we remove useless items and we cut the last item or add a dummy item.
func (s *Subtitles) ForceDuration(d time.Duration, addDummyItem bool) {
// Requested duration is the same as the subtitles'one
if s.Duration() == d {
return
}
// Requested duration is bigger than subtitles'one
if s.Duration() > d {
// Find last item before input duration and update end at
var lastIndex = -1
for index, i := range s.Items {
// Start at is bigger than input duration, we've found the last item
if i.StartAt >= d {
lastIndex = index
break
} else if i.EndAt > d {
s.Items[index].EndAt = d
}
}
// Last index has been found
if lastIndex != -1 {
s.Items = s.Items[:lastIndex]
}
}
// Add dummy item with the minimum duration possible
if addDummyItem && s.Duration() < d {
s.Items = append(s.Items, &Item{EndAt: d, Lines: []Line{{Items: []LineItem{{Text: "..."}}}}, StartAt: d - time.Millisecond})
}
}
// Fragment fragments subtitles with a specific fragment duration
func (s *Subtitles) Fragment(f time.Duration) {
// Nothing to fragment
if len(s.Items) == 0 {
return
}
// Here we want to simulate fragments of duration f until there are no subtitles left in that period of time
var fragmentStartAt, fragmentEndAt = time.Duration(0), f
for fragmentStartAt < s.Items[len(s.Items)-1].EndAt {
// We loop through subtitles and process the ones that either contain the fragment start at,
// or contain the fragment end at
//
// It's useless processing subtitles contained between fragment start at and end at
// |____________________| <- subtitle
// | |
// fragment start at fragment end at
for i, sub := range s.Items {
// Init
var newSub = &Item{}
*newSub = *sub
// A switch is more readable here
switch {
// Subtitle contains fragment start at
// |____________________| <- subtitle
// | |
// fragment start at fragment end at
case sub.StartAt < fragmentStartAt && sub.EndAt > fragmentStartAt:
sub.StartAt = fragmentStartAt
newSub.EndAt = fragmentStartAt
// Subtitle contains fragment end at
// |____________________| <- subtitle
// | |
// fragment start at fragment end at
case sub.StartAt < fragmentEndAt && sub.EndAt > fragmentEndAt:
sub.StartAt = fragmentEndAt
newSub.EndAt = fragmentEndAt
default:
continue
}
// Insert new sub
s.Items = append(s.Items[:i], append([]*Item{newSub}, s.Items[i:]...)...)
}
// Update fragments boundaries
fragmentStartAt += f
fragmentEndAt += f
}
// Order
s.Order()
}
// IsEmpty returns whether the subtitles are empty
func (s Subtitles) IsEmpty() bool {
return len(s.Items) == 0
}
// Merge merges subtitles i into subtitles
func (s *Subtitles) Merge(i *Subtitles) {
// Append items
s.Items = append(s.Items, i.Items...)
s.Order()
// Add regions
for _, region := range i.Regions {
if _, ok := s.Regions[region.ID]; !ok {
s.Regions[region.ID] = region
}
}
// Add styles
for _, style := range i.Styles {
if _, ok := s.Styles[style.ID]; !ok {
s.Styles[style.ID] = style
}
}
}
// Optimize optimizes subtitles
func (s *Subtitles) Optimize() {
// Nothing to optimize
if len(s.Items) == 0 {
return
}
// Remove unused regions and style
s.removeUnusedRegionsAndStyles()
}
// removeUnusedRegionsAndStyles removes unused regions and styles
func (s *Subtitles) removeUnusedRegionsAndStyles() {
// Loop through items
var usedRegions, usedStyles = make(map[string]bool), make(map[string]bool)
for _, item := range s.Items {
// Add region
if item.Region != nil {
usedRegions[item.Region.ID] = true
}
// Add style
if item.Style != nil {
usedStyles[item.Style.ID] = true
}
// Loop through lines
for _, line := range item.Lines {
// Loop through line items
for _, lineItem := range line.Items {
// Add style
if lineItem.Style != nil {
usedStyles[lineItem.Style.ID] = true
}
}
}
}
// Loop through regions
for id, region := range s.Regions {
if _, ok := usedRegions[region.ID]; ok {
if region.Style != nil {
usedStyles[region.Style.ID] = true
}
} else {
delete(s.Regions, id)
}
}
// Loop through style
for id, style := range s.Styles {
if _, ok := usedStyles[style.ID]; !ok {
delete(s.Styles, id)
}
}
}
// Order orders items
func (s *Subtitles) Order() {
// Nothing to do if less than 1 element
if len(s.Items) <= 1 {
return
}
// Order
var swapped = true
for swapped {
swapped = false
for index := 1; index < len(s.Items); index++ {
if s.Items[index-1].StartAt > s.Items[index].StartAt {
var tmp = s.Items[index-1]
s.Items[index-1] = s.Items[index]
s.Items[index] = tmp
swapped = true
}
}
}
}
// RemoveStyling removes the styling from the subtitles
func (s *Subtitles) RemoveStyling() {
s.Regions = map[string]*Region{}
s.Styles = map[string]*Style{}
for _, i := range s.Items {
i.Region = nil
i.Style = nil
i.InlineStyle = nil
for idxLine, l := range i.Lines {
for idxLineItem := range l.Items {
i.Lines[idxLine].Items[idxLineItem].InlineStyle = nil
i.Lines[idxLine].Items[idxLineItem].Style = nil
}
}
}
}
// Unfragment unfragments subtitles
func (s *Subtitles) Unfragment() {
// Nothing to do if less than 1 element
if len(s.Items) <= 1 {
return
}
// Loop through items
for i := 0; i < len(s.Items)-1; i++ {
for j := i + 1; j < len(s.Items); j++ {
// Items are the same
if s.Items[i].String() == s.Items[j].String() && s.Items[i].EndAt == s.Items[j].StartAt {
s.Items[i].EndAt = s.Items[j].EndAt
s.Items = append(s.Items[:j], s.Items[j+1:]...)
j--
}
}
}
// Order
s.Order()
}
// Write writes subtitles to a file
func (s Subtitles) Write(dst string) (err error) {
// Create the file
var f *os.File
if f, err = os.Create(dst); err != nil {
err = errors.Wrapf(err, "astisub: creating %s failed", dst)
return
}
defer f.Close()
// Write the content
switch filepath.Ext(dst) {
case ".srt":
err = s.WriteToSRT(f)
case ".ssa", ".ass":
err = s.WriteToSSA(f)
case ".stl":
err = s.WriteToSTL(f)
case ".ttml":
err = s.WriteToTTML(f)
case ".vtt":
err = s.WriteToWebVTT(f)
default:
err = ErrInvalidExtension
}
return
}
// parseDuration parses a duration in "00:00:00.000", "00:00:00,000" or "0:00:00:00" format
func parseDuration(i, millisecondSep string, numberOfMillisecondDigits int) (o time.Duration, err error) {
// Split milliseconds
var parts = strings.Split(i, millisecondSep)
var milliseconds int
var s string
if len(parts) >= 2 {
// Invalid number of millisecond digits
s = strings.TrimSpace(parts[len(parts)-1])
if len(s) > 3 {
err = fmt.Errorf("astisub: Invalid number of millisecond digits detected in %s", i)
return
}
// Parse milliseconds
if milliseconds, err = strconv.Atoi(s); err != nil {
err = errors.Wrapf(err, "astisub: atoi of %s failed", s)
return
}
milliseconds *= int(math.Pow10(numberOfMillisecondDigits - len(s)))
s = strings.Join(parts[:len(parts)-1], millisecondSep)
} else {
s = i
}
// Split hours, minutes and seconds
parts = strings.Split(strings.TrimSpace(s), ":")
var partSeconds, partMinutes, partHours string
if len(parts) == 2 {
partSeconds = parts[1]
partMinutes = parts[0]
} else if len(parts) == 3 {
partSeconds = parts[2]
partMinutes = parts[1]
partHours = parts[0]
} else {
err = fmt.Errorf("astisub: No hours, minutes or seconds detected in %s", i)
return
}
// Parse seconds
var seconds int
s = strings.TrimSpace(partSeconds)
if seconds, err = strconv.Atoi(s); err != nil {
err = errors.Wrapf(err, "astisub: atoi of %s failed", s)
return
}
// Parse minutes
var minutes int
s = strings.TrimSpace(partMinutes)
if minutes, err = strconv.Atoi(s); err != nil {
err = errors.Wrapf(err, "astisub: atoi of %s failed", s)
return
}
// Parse hours
var hours int
if len(partHours) > 0 {
s = strings.TrimSpace(partHours)
if hours, err = strconv.Atoi(s); err != nil {
err = errors.Wrapf(err, "astisub: atoi of %s failed", s)
return
}
}
// Generate output
o = time.Duration(milliseconds)*time.Millisecond + time.Duration(seconds)*time.Second + time.Duration(minutes)*time.Minute + time.Duration(hours)*time.Hour
return
}
// formatDuration formats a duration
func formatDuration(i time.Duration, millisecondSep string, numberOfMillisecondDigits int) (s string) {
// Parse hours
var hours = int(i / time.Hour)
var n = i % time.Hour
if hours < 10 {
s += "0"
}
s += strconv.Itoa(hours) + ":"
// Parse minutes
var minutes = int(n / time.Minute)
n = i % time.Minute
if minutes < 10 {
s += "0"
}
s += strconv.Itoa(minutes) + ":"
// Parse seconds
var seconds = int(n / time.Second)
n = i % time.Second
if seconds < 10 {
s += "0"
}
s += strconv.Itoa(seconds) + millisecondSep
// Parse milliseconds
var milliseconds = float64(n/time.Millisecond) / float64(1000)
s += fmt.Sprintf("%."+strconv.Itoa(numberOfMillisecondDigits)+"f", milliseconds)[2:]
return
}
// appendStringToBytesWithNewLine adds a string to bytes then adds a new line
func appendStringToBytesWithNewLine(i []byte, s string) (o []byte) {
o = append(i, []byte(s)...)
o = append(o, bytesLineSeparator...)
return
}

1004
vendor/github.com/asticode/go-astisub/teletext.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

673
vendor/github.com/asticode/go-astisub/ttml.go generated vendored Normal file
View File

@@ -0,0 +1,673 @@
package astisub
import (
"encoding/xml"
"fmt"
"io"
"math"
"regexp"
"strconv"
"strings"
"time"
"sort"
"github.com/asticode/go-astitools/map"
"github.com/asticode/go-astitools/string"
"github.com/pkg/errors"
)
// https://www.w3.org/TR/ttaf1-dfxp/
// http://www.skynav.com:8080/ttv/check
// https://www.speechpad.com/captions/ttml
// TTML languages
const (
ttmlLanguageEnglish = "en"
ttmlLanguageFrench = "fr"
)
// TTML language mapping
var ttmlLanguageMapping = astimap.NewMap(ttmlLanguageEnglish, LanguageEnglish).
Set(ttmlLanguageFrench, LanguageFrench)
// TTML Clock Time Frames and Offset Time
var (
ttmlRegexpClockTimeFrames = regexp.MustCompile("\\:[\\d]+$")
ttmlRegexpOffsetTime = regexp.MustCompile("^(\\d+)(\\.(\\d+))?(h|m|s|ms|f|t)$")
)
// TTMLIn represents an input TTML that must be unmarshaled
// We split it from the output TTML as we can't add strict namespace without breaking retrocompatibility
type TTMLIn struct {
Framerate int `xml:"frameRate,attr"`
Lang string `xml:"lang,attr"`
Metadata TTMLInMetadata `xml:"head>metadata"`
Regions []TTMLInRegion `xml:"head>layout>region"`
Styles []TTMLInStyle `xml:"head>styling>style"`
Subtitles []TTMLInSubtitle `xml:"body>div>p"`
XMLName xml.Name `xml:"tt"`
}
// metadata returns the Metadata of the TTML
func (t TTMLIn) metadata() *Metadata {
return &Metadata{
Framerate: t.Framerate,
Language: ttmlLanguageMapping.B(astistring.ToLength(t.Lang, " ", 2)).(string),
Title: t.Metadata.Title,
TTMLCopyright: t.Metadata.Copyright,
}
}
// TTMLInMetadata represents an input TTML Metadata
type TTMLInMetadata struct {
Copyright string `xml:"copyright"`
Title string `xml:"title"`
}
// TTMLInStyleAttributes represents input TTML style attributes
type TTMLInStyleAttributes struct {
BackgroundColor string `xml:"backgroundColor,attr,omitempty"`
Color string `xml:"color,attr,omitempty"`
Direction string `xml:"direction,attr,omitempty"`
Display string `xml:"display,attr,omitempty"`
DisplayAlign string `xml:"displayAlign,attr,omitempty"`
Extent string `xml:"extent,attr,omitempty"`
FontFamily string `xml:"fontFamily,attr,omitempty"`
FontSize string `xml:"fontSize,attr,omitempty"`
FontStyle string `xml:"fontStyle,attr,omitempty"`
FontWeight string `xml:"fontWeight,attr,omitempty"`
LineHeight string `xml:"lineHeight,attr,omitempty"`
Opacity string `xml:"opacity,attr,omitempty"`
Origin string `xml:"origin,attr,omitempty"`
Overflow string `xml:"overflow,attr,omitempty"`
Padding string `xml:"padding,attr,omitempty"`
ShowBackground string `xml:"showBackground,attr,omitempty"`
TextAlign string `xml:"textAlign,attr,omitempty"`
TextDecoration string `xml:"textDecoration,attr,omitempty"`
TextOutline string `xml:"textOutline,attr,omitempty"`
UnicodeBidi string `xml:"unicodeBidi,attr,omitempty"`
Visibility string `xml:"visibility,attr,omitempty"`
WrapOption string `xml:"wrapOption,attr,omitempty"`
WritingMode string `xml:"writingMode,attr,omitempty"`
ZIndex int `xml:"zIndex,attr,omitempty"`
}
// StyleAttributes converts TTMLInStyleAttributes into a StyleAttributes
func (s TTMLInStyleAttributes) styleAttributes() (o *StyleAttributes) {
o = &StyleAttributes{
TTMLBackgroundColor: s.BackgroundColor,
TTMLColor: s.Color,
TTMLDirection: s.Direction,
TTMLDisplay: s.Display,
TTMLDisplayAlign: s.DisplayAlign,
TTMLExtent: s.Extent,
TTMLFontFamily: s.FontFamily,
TTMLFontSize: s.FontSize,
TTMLFontStyle: s.FontStyle,
TTMLFontWeight: s.FontWeight,
TTMLLineHeight: s.LineHeight,
TTMLOpacity: s.Opacity,
TTMLOrigin: s.Origin,
TTMLOverflow: s.Overflow,
TTMLPadding: s.Padding,
TTMLShowBackground: s.ShowBackground,
TTMLTextAlign: s.TextAlign,
TTMLTextDecoration: s.TextDecoration,
TTMLTextOutline: s.TextOutline,
TTMLUnicodeBidi: s.UnicodeBidi,
TTMLVisibility: s.Visibility,
TTMLWrapOption: s.WrapOption,
TTMLWritingMode: s.WritingMode,
TTMLZIndex: s.ZIndex,
}
o.propagateTTMLAttributes()
return
}
// TTMLInHeader represents an input TTML header
type TTMLInHeader struct {
ID string `xml:"id,attr,omitempty"`
Style string `xml:"style,attr,omitempty"`
TTMLInStyleAttributes
}
// TTMLInRegion represents an input TTML region
type TTMLInRegion struct {
TTMLInHeader
XMLName xml.Name `xml:"region"`
}
// TTMLInStyle represents an input TTML style
type TTMLInStyle struct {
TTMLInHeader
XMLName xml.Name `xml:"style"`
}
// TTMLInSubtitle represents an input TTML subtitle
type TTMLInSubtitle struct {
Begin *TTMLInDuration `xml:"begin,attr,omitempty"`
End *TTMLInDuration `xml:"end,attr,omitempty"`
ID string `xml:"id,attr,omitempty"`
Items string `xml:",innerxml"` // We must store inner XML here since there's no tag to describe both any tag and chardata
Region string `xml:"region,attr,omitempty"`
Style string `xml:"style,attr,omitempty"`
TTMLInStyleAttributes
}
// TTMLInItems represents input TTML items
type TTMLInItems []TTMLInItem
// UnmarshalXML implements the XML unmarshaler interface
func (i *TTMLInItems) UnmarshalXML(d *xml.Decoder, start xml.StartElement) (err error) {
// Get next tokens
var t xml.Token
for {
// Get next token
if t, err = d.Token(); err != nil {
if err == io.EOF {
break
}
err = errors.Wrap(err, "astisub: getting next token failed")
return
}
// Start element
if se, ok := t.(xml.StartElement); ok {
var e = TTMLInItem{}
if err = d.DecodeElement(&e, &se); err != nil {
err = errors.Wrap(err, "astisub: decoding xml.StartElement failed")
return
}
*i = append(*i, e)
} else if b, ok := t.(xml.CharData); ok {
var str = strings.TrimSpace(string(b))
if len(str) > 0 {
*i = append(*i, TTMLInItem{Text: str})
}
}
}
return nil
}
// TTMLInItem represents an input TTML item
type TTMLInItem struct {
Style string `xml:"style,attr,omitempty"`
Text string `xml:",chardata"`
TTMLInStyleAttributes
XMLName xml.Name
}
// TTMLInDuration represents an input TTML duration
type TTMLInDuration struct {
d time.Duration
frames, framerate int // Framerate is in frame/s
}
// UnmarshalText implements the TextUnmarshaler interface
// Possible formats are:
// - hh:mm:ss.mmm
// - hh:mm:ss:fff (fff being frames)
func (d *TTMLInDuration) UnmarshalText(i []byte) (err error) {
var text = string(i)
if matches := ttmlRegexpOffsetTime.FindStringSubmatch(text); matches != nil {
metric := matches[4]
value, err := strconv.Atoi(matches[1])
if err != nil {
err = errors.Wrapf(err, "astisub: failed to parse value %s", matches[1])
return err
}
d.d = time.Duration(0)
var (
nsBase int64
fraction int
fractionBase float64
)
if len(matches[3]) > 0 {
fraction, err = strconv.Atoi(matches[3])
fractionBase = math.Pow10(len(matches[3]))
if err != nil {
err = errors.Wrapf(err, "astisub: failed to parse fraction %s", matches[3])
return err
}
}
switch metric {
case "h":
nsBase = time.Hour.Nanoseconds()
case "m":
nsBase = time.Minute.Nanoseconds()
case "s":
nsBase = time.Second.Nanoseconds()
case "ms":
nsBase = time.Millisecond.Nanoseconds()
case "f":
nsBase = time.Second.Nanoseconds()
d.frames = value % d.framerate
value = value / d.framerate
// TODO: fraction of frames
case "t":
// TODO: implement ticks
return errors.New("astisub: offset time in ticks not implemented")
}
d.d += time.Duration(nsBase * int64(value))
if fractionBase > 0 {
d.d += time.Duration(nsBase * int64(fraction) / int64(fractionBase))
}
return nil
}
if indexes := ttmlRegexpClockTimeFrames.FindStringIndex(text); indexes != nil {
// Parse frames
var s = text[indexes[0]+1 : indexes[1]]
if d.frames, err = strconv.Atoi(s); err != nil {
err = errors.Wrapf(err, "astisub: atoi %s failed", s)
return
}
// Update text
text = text[:indexes[0]] + ".000"
}
d.d, err = parseDuration(text, ".", 3)
return
}
// duration returns the input TTML Duration's time.Duration
func (d TTMLInDuration) duration() time.Duration {
if d.framerate > 0 {
return d.d + time.Duration(float64(d.frames)/float64(d.framerate)*1e9)*time.Nanosecond
}
return d.d
}
// ReadFromTTML parses a .ttml content
func ReadFromTTML(i io.Reader) (o *Subtitles, err error) {
// Init
o = NewSubtitles()
// Unmarshal XML
var ttml TTMLIn
if err = xml.NewDecoder(i).Decode(&ttml); err != nil {
err = errors.Wrap(err, "astisub: xml decoding failed")
return
}
// Add metadata
o.Metadata = ttml.metadata()
// Loop through styles
var parentStyles = make(map[string]*Style)
for _, ts := range ttml.Styles {
var s = &Style{
ID: ts.ID,
InlineStyle: ts.TTMLInStyleAttributes.styleAttributes(),
}
o.Styles[s.ID] = s
if len(ts.Style) > 0 {
parentStyles[ts.Style] = s
}
}
// Take care of parent styles
for id, s := range parentStyles {
if _, ok := o.Styles[id]; !ok {
err = fmt.Errorf("astisub: Style %s requested by style %s doesn't exist", id, s.ID)
return
}
s.Style = o.Styles[id]
}
// Loop through regions
for _, tr := range ttml.Regions {
var r = &Region{
ID: tr.ID,
InlineStyle: tr.TTMLInStyleAttributes.styleAttributes(),
}
if len(tr.Style) > 0 {
if _, ok := o.Styles[tr.Style]; !ok {
err = fmt.Errorf("astisub: Style %s requested by region %s doesn't exist", tr.Style, r.ID)
return
}
r.Style = o.Styles[tr.Style]
}
o.Regions[r.ID] = r
}
// Loop through subtitles
for _, ts := range ttml.Subtitles {
// Init item
ts.Begin.framerate = ttml.Framerate
ts.End.framerate = ttml.Framerate
var s = &Item{
EndAt: ts.End.duration(),
InlineStyle: ts.TTMLInStyleAttributes.styleAttributes(),
StartAt: ts.Begin.duration(),
}
// Add region
if len(ts.Region) > 0 {
if _, ok := o.Regions[ts.Region]; !ok {
err = fmt.Errorf("astisub: Region %s requested by subtitle between %s and %s doesn't exist", ts.Region, s.StartAt, s.EndAt)
return
}
s.Region = o.Regions[ts.Region]
}
// Add style
if len(ts.Style) > 0 {
if _, ok := o.Styles[ts.Style]; !ok {
err = fmt.Errorf("astisub: Style %s requested by subtitle between %s and %s doesn't exist", ts.Style, s.StartAt, s.EndAt)
return
}
s.Style = o.Styles[ts.Style]
}
// Unmarshal items
var items = TTMLInItems{}
if err = xml.Unmarshal([]byte("<span>"+ts.Items+"</span>"), &items); err != nil {
err = errors.Wrap(err, "astisub: unmarshaling items failed")
return
}
// Loop through texts
var l = &Line{}
for _, tt := range items {
// New line specified with the "br" tag
if strings.ToLower(tt.XMLName.Local) == "br" {
s.Lines = append(s.Lines, *l)
l = &Line{}
continue
}
// New line decoded as a line break. This can happen if there's a "br" tag within the text since
// since the go xml unmarshaler will unmarshal a "br" tag as a line break if the field has the
// chardata xml tag.
for idx, li := range strings.Split(tt.Text, "\n") {
// New line
if idx > 0 {
s.Lines = append(s.Lines, *l)
l = &Line{}
}
// Init line item
var t = LineItem{
InlineStyle: tt.TTMLInStyleAttributes.styleAttributes(),
Text: strings.TrimSpace(li),
}
// Add style
if len(tt.Style) > 0 {
if _, ok := o.Styles[tt.Style]; !ok {
err = fmt.Errorf("astisub: Style %s requested by item with text %s doesn't exist", tt.Style, tt.Text)
return
}
t.Style = o.Styles[tt.Style]
}
// Append items
l.Items = append(l.Items, t)
}
}
s.Lines = append(s.Lines, *l)
// Append subtitle
o.Items = append(o.Items, s)
}
return
}
// TTMLOut represents an output TTML that must be marshaled
// We split it from the input TTML as this time we'll add strict namespaces
type TTMLOut struct {
Lang string `xml:"xml:lang,attr,omitempty"`
Metadata *TTMLOutMetadata `xml:"head>metadata,omitempty"`
Styles []TTMLOutStyle `xml:"head>styling>style,omitempty"` //!\\ Order is important! Keep Styling above Layout
Regions []TTMLOutRegion `xml:"head>layout>region,omitempty"`
Subtitles []TTMLOutSubtitle `xml:"body>div>p,omitempty"`
XMLName xml.Name `xml:"http://www.w3.org/ns/ttml tt"`
XMLNamespaceTTM string `xml:"xmlns:ttm,attr"`
XMLNamespaceTTS string `xml:"xmlns:tts,attr"`
}
// TTMLOutMetadata represents an output TTML Metadata
type TTMLOutMetadata struct {
Copyright string `xml:"ttm:copyright,omitempty"`
Title string `xml:"ttm:title,omitempty"`
}
// TTMLOutStyleAttributes represents output TTML style attributes
type TTMLOutStyleAttributes struct {
BackgroundColor string `xml:"tts:backgroundColor,attr,omitempty"`
Color string `xml:"tts:color,attr,omitempty"`
Direction string `xml:"tts:direction,attr,omitempty"`
Display string `xml:"tts:display,attr,omitempty"`
DisplayAlign string `xml:"tts:displayAlign,attr,omitempty"`
Extent string `xml:"tts:extent,attr,omitempty"`
FontFamily string `xml:"tts:fontFamily,attr,omitempty"`
FontSize string `xml:"tts:fontSize,attr,omitempty"`
FontStyle string `xml:"tts:fontStyle,attr,omitempty"`
FontWeight string `xml:"tts:fontWeight,attr,omitempty"`
LineHeight string `xml:"tts:lineHeight,attr,omitempty"`
Opacity string `xml:"tts:opacity,attr,omitempty"`
Origin string `xml:"tts:origin,attr,omitempty"`
Overflow string `xml:"tts:overflow,attr,omitempty"`
Padding string `xml:"tts:padding,attr,omitempty"`
ShowBackground string `xml:"tts:showBackground,attr,omitempty"`
TextAlign string `xml:"tts:textAlign,attr,omitempty"`
TextDecoration string `xml:"tts:textDecoration,attr,omitempty"`
TextOutline string `xml:"tts:textOutline,attr,omitempty"`
UnicodeBidi string `xml:"tts:unicodeBidi,attr,omitempty"`
Visibility string `xml:"tts:visibility,attr,omitempty"`
WrapOption string `xml:"tts:wrapOption,attr,omitempty"`
WritingMode string `xml:"tts:writingMode,attr,omitempty"`
ZIndex int `xml:"tts:zIndex,attr,omitempty"`
}
// ttmlOutStyleAttributesFromStyleAttributes converts StyleAttributes into a TTMLOutStyleAttributes
func ttmlOutStyleAttributesFromStyleAttributes(s *StyleAttributes) TTMLOutStyleAttributes {
if s == nil {
return TTMLOutStyleAttributes{}
}
return TTMLOutStyleAttributes{
BackgroundColor: s.TTMLBackgroundColor,
Color: s.TTMLColor,
Direction: s.TTMLDirection,
Display: s.TTMLDisplay,
DisplayAlign: s.TTMLDisplayAlign,
Extent: s.TTMLExtent,
FontFamily: s.TTMLFontFamily,
FontSize: s.TTMLFontSize,
FontStyle: s.TTMLFontStyle,
FontWeight: s.TTMLFontWeight,
LineHeight: s.TTMLLineHeight,
Opacity: s.TTMLOpacity,
Origin: s.TTMLOrigin,
Overflow: s.TTMLOverflow,
Padding: s.TTMLPadding,
ShowBackground: s.TTMLShowBackground,
TextAlign: s.TTMLTextAlign,
TextDecoration: s.TTMLTextDecoration,
TextOutline: s.TTMLTextOutline,
UnicodeBidi: s.TTMLUnicodeBidi,
Visibility: s.TTMLVisibility,
WrapOption: s.TTMLWrapOption,
WritingMode: s.TTMLWritingMode,
ZIndex: s.TTMLZIndex,
}
}
// TTMLOutHeader represents an output TTML header
type TTMLOutHeader struct {
ID string `xml:"xml:id,attr,omitempty"`
Style string `xml:"style,attr,omitempty"`
TTMLOutStyleAttributes
}
// TTMLOutRegion represents an output TTML region
type TTMLOutRegion struct {
TTMLOutHeader
XMLName xml.Name `xml:"region"`
}
// TTMLOutStyle represents an output TTML style
type TTMLOutStyle struct {
TTMLOutHeader
XMLName xml.Name `xml:"style"`
}
// TTMLOutSubtitle represents an output TTML subtitle
type TTMLOutSubtitle struct {
Begin TTMLOutDuration `xml:"begin,attr"`
End TTMLOutDuration `xml:"end,attr"`
ID string `xml:"id,attr,omitempty"`
Items []TTMLOutItem
Region string `xml:"region,attr,omitempty"`
Style string `xml:"style,attr,omitempty"`
TTMLOutStyleAttributes
}
// TTMLOutItem represents an output TTML Item
type TTMLOutItem struct {
Style string `xml:"style,attr,omitempty"`
Text string `xml:",chardata"`
TTMLOutStyleAttributes
XMLName xml.Name
}
// TTMLOutDuration represents an output TTML duration
type TTMLOutDuration time.Duration
// MarshalText implements the TextMarshaler interface
func (t TTMLOutDuration) MarshalText() ([]byte, error) {
return []byte(formatDuration(time.Duration(t), ".", 3)), nil
}
// WriteToTTML writes subtitles in .ttml format
func (s Subtitles) WriteToTTML(o io.Writer) (err error) {
// Do not write anything if no subtitles
if len(s.Items) == 0 {
return ErrNoSubtitlesToWrite
}
// Init TTML
var ttml = TTMLOut{
XMLNamespaceTTM: "http://www.w3.org/ns/ttml#metadata",
XMLNamespaceTTS: "http://www.w3.org/ns/ttml#styling",
}
// Add metadata
if s.Metadata != nil {
ttml.Lang = ttmlLanguageMapping.A(s.Metadata.Language).(string)
if len(s.Metadata.TTMLCopyright) > 0 || len(s.Metadata.Title) > 0 {
ttml.Metadata = &TTMLOutMetadata{
Copyright: s.Metadata.TTMLCopyright,
Title: s.Metadata.Title,
}
}
}
// Add regions
var k []string
for _, region := range s.Regions {
k = append(k, region.ID)
}
sort.Strings(k)
for _, id := range k {
var ttmlRegion = TTMLOutRegion{TTMLOutHeader: TTMLOutHeader{
ID: s.Regions[id].ID,
TTMLOutStyleAttributes: ttmlOutStyleAttributesFromStyleAttributes(s.Regions[id].InlineStyle),
}}
if s.Regions[id].Style != nil {
ttmlRegion.Style = s.Regions[id].Style.ID
}
ttml.Regions = append(ttml.Regions, ttmlRegion)
}
// Add styles
k = []string{}
for _, style := range s.Styles {
k = append(k, style.ID)
}
sort.Strings(k)
for _, id := range k {
var ttmlStyle = TTMLOutStyle{TTMLOutHeader: TTMLOutHeader{
ID: s.Styles[id].ID,
TTMLOutStyleAttributes: ttmlOutStyleAttributesFromStyleAttributes(s.Styles[id].InlineStyle),
}}
if s.Styles[id].Style != nil {
ttmlStyle.Style = s.Styles[id].Style.ID
}
ttml.Styles = append(ttml.Styles, ttmlStyle)
}
// Add items
for _, item := range s.Items {
// Init subtitle
var ttmlSubtitle = TTMLOutSubtitle{
Begin: TTMLOutDuration(item.StartAt),
End: TTMLOutDuration(item.EndAt),
TTMLOutStyleAttributes: ttmlOutStyleAttributesFromStyleAttributes(item.InlineStyle),
}
// Add region
if item.Region != nil {
ttmlSubtitle.Region = item.Region.ID
}
// Add style
if item.Style != nil {
ttmlSubtitle.Style = item.Style.ID
}
// Add lines
for _, line := range item.Lines {
// Loop through line items
for _, lineItem := range line.Items {
// Init ttml item
var ttmlItem = TTMLOutItem{
Text: lineItem.Text,
TTMLOutStyleAttributes: ttmlOutStyleAttributesFromStyleAttributes(lineItem.InlineStyle),
XMLName: xml.Name{Local: "span"},
}
// Add style
if lineItem.Style != nil {
ttmlItem.Style = lineItem.Style.ID
}
// Add ttml item
ttmlSubtitle.Items = append(ttmlSubtitle.Items, ttmlItem)
}
// Add line break
ttmlSubtitle.Items = append(ttmlSubtitle.Items, TTMLOutItem{XMLName: xml.Name{Local: "br"}})
}
// Remove last line break
if len(ttmlSubtitle.Items) > 0 {
ttmlSubtitle.Items = ttmlSubtitle.Items[:len(ttmlSubtitle.Items)-1]
}
// Append subtitle
ttml.Subtitles = append(ttml.Subtitles, ttmlSubtitle)
}
// Marshal XML
var e = xml.NewEncoder(o)
e.Indent("", " ")
if err = e.Encode(ttml); err != nil {
err = errors.Wrap(err, "astisub: xml encoding failed")
return
}
return
}

315
vendor/github.com/asticode/go-astisub/webvtt.go generated vendored Normal file
View File

@@ -0,0 +1,315 @@
package astisub
import (
"bufio"
"fmt"
"io"
"strconv"
"strings"
"time"
"sort"
"github.com/pkg/errors"
)
// https://www.w3.org/TR/webvtt1/
// Constants
const (
webvttBlockNameComment = "comment"
webvttBlockNameRegion = "region"
webvttBlockNameStyle = "style"
webvttBlockNameText = "text"
webvttTimeBoundariesSeparator = " --> "
)
// Vars
var (
bytesWebVTTTimeBoundariesSeparator = []byte(webvttTimeBoundariesSeparator)
)
// parseDurationWebVTT parses a .vtt duration
func parseDurationWebVTT(i string) (time.Duration, error) {
return parseDuration(i, ".", 3)
}
// ReadFromWebVTT parses a .vtt content
// TODO Tags (u, i, b)
// TODO Class
// TODO Speaker name
func ReadFromWebVTT(i io.Reader) (o *Subtitles, err error) {
// Init
o = NewSubtitles()
var scanner = bufio.NewScanner(i)
var line string
// Skip the header
for scanner.Scan() {
line = scanner.Text()
line = strings.TrimPrefix(line, string(BytesBOM))
if len(line) > 0 && line == "WEBVTT" {
break
}
}
// Scan
var item = &Item{}
var blockName string
var comments []string
for scanner.Scan() {
// Fetch line
line = scanner.Text()
// Check prefixes
switch {
// Comment
case strings.HasPrefix(line, "NOTE "):
blockName = webvttBlockNameComment
comments = append(comments, strings.TrimPrefix(line, "NOTE "))
// Empty line
case len(line) == 0:
// Reset block name
blockName = ""
// Region
case strings.HasPrefix(line, "Region: "):
// Add region styles
var r = &Region{InlineStyle: &StyleAttributes{}}
for _, part := range strings.Split(strings.TrimPrefix(line, "Region: "), " ") {
// Split on "="
var split = strings.Split(part, "=")
if len(split) <= 1 {
err = fmt.Errorf("astisub: Invalid region style %s", part)
return
}
// Switch on key
switch split[0] {
case "id":
r.ID = split[1]
case "lines":
if r.InlineStyle.WebVTTLines, err = strconv.Atoi(split[1]); err != nil {
err = errors.Wrapf(err, "atoi of %s failed", split[1])
return
}
case "regionanchor":
r.InlineStyle.WebVTTRegionAnchor = split[1]
case "scroll":
r.InlineStyle.WebVTTScroll = split[1]
case "viewportanchor":
r.InlineStyle.WebVTTViewportAnchor = split[1]
case "width":
r.InlineStyle.WebVTTWidth = split[1]
}
}
r.InlineStyle.propagateWebVTTAttributes()
// Add region
o.Regions[r.ID] = r
// Style
case strings.HasPrefix(line, "STYLE "):
blockName = webvttBlockNameStyle
// Time boundaries
case strings.Contains(line, webvttTimeBoundariesSeparator):
// Set block name
blockName = webvttBlockNameText
// Init new item
item = &Item{
Comments: comments,
InlineStyle: &StyleAttributes{},
}
// Split line on time boundaries
var parts = strings.Split(line, webvttTimeBoundariesSeparator)
// Split line on space to catch inline styles as well
var partsRight = strings.Split(parts[1], " ")
// Parse time boundaries
if item.StartAt, err = parseDurationWebVTT(parts[0]); err != nil {
err = errors.Wrapf(err, "astisub: parsing webvtt duration %s failed", parts[0])
return
}
if item.EndAt, err = parseDurationWebVTT(partsRight[0]); err != nil {
err = errors.Wrapf(err, "astisub: parsing webvtt duration %s failed", partsRight[0])
return
}
// Parse style
if len(partsRight) > 1 {
// Add styles
for index := 1; index < len(partsRight); index++ {
// Split line on ":"
var split = strings.Split(partsRight[index], ":")
if len(split) <= 1 {
err = fmt.Errorf("astisub: Invalid inline style %s", partsRight[index])
return
}
// Switch on key
switch split[0] {
case "align":
item.InlineStyle.WebVTTAlign = split[1]
case "line":
item.InlineStyle.WebVTTLine = split[1]
case "position":
item.InlineStyle.WebVTTPosition = split[1]
case "region":
if _, ok := o.Regions[split[1]]; !ok {
err = fmt.Errorf("astisub: Unknown region %s", split[1])
return
}
item.Region = o.Regions[split[1]]
case "size":
item.InlineStyle.WebVTTSize = split[1]
case "vertical":
item.InlineStyle.WebVTTVertical = split[1]
}
}
}
item.InlineStyle.propagateWebVTTAttributes()
// Reset comments
comments = []string{}
// Append item
o.Items = append(o.Items, item)
// Text
default:
// Switch on block name
switch blockName {
case webvttBlockNameComment:
comments = append(comments, line)
case webvttBlockNameStyle:
// TODO Do something with the style
case webvttBlockNameText:
item.Lines = append(item.Lines, Line{Items: []LineItem{{Text: line}}})
default:
// This is the ID
// TODO Do something with the id
}
}
}
return
}
// formatDurationWebVTT formats a .vtt duration
func formatDurationWebVTT(i time.Duration) string {
return formatDuration(i, ".", 3)
}
// WriteToWebVTT writes subtitles in .vtt format
func (s Subtitles) WriteToWebVTT(o io.Writer) (err error) {
// Do not write anything if no subtitles
if len(s.Items) == 0 {
err = ErrNoSubtitlesToWrite
return
}
// Add header
var c []byte
c = append(c, []byte("WEBVTT\n\n")...)
// Add regions
var k []string
for _, region := range s.Regions {
k = append(k, region.ID)
}
sort.Strings(k)
for _, id := range k {
c = append(c, []byte("Region: id="+s.Regions[id].ID)...)
if s.Regions[id].InlineStyle.WebVTTLines != 0 {
c = append(c, bytesSpace...)
c = append(c, []byte("lines="+strconv.Itoa(s.Regions[id].InlineStyle.WebVTTLines))...)
}
if s.Regions[id].InlineStyle.WebVTTRegionAnchor != "" {
c = append(c, bytesSpace...)
c = append(c, []byte("regionanchor="+s.Regions[id].InlineStyle.WebVTTRegionAnchor)...)
}
if s.Regions[id].InlineStyle.WebVTTScroll != "" {
c = append(c, bytesSpace...)
c = append(c, []byte("scroll="+s.Regions[id].InlineStyle.WebVTTScroll)...)
}
if s.Regions[id].InlineStyle.WebVTTViewportAnchor != "" {
c = append(c, bytesSpace...)
c = append(c, []byte("viewportanchor="+s.Regions[id].InlineStyle.WebVTTViewportAnchor)...)
}
if s.Regions[id].InlineStyle.WebVTTWidth != "" {
c = append(c, bytesSpace...)
c = append(c, []byte("width="+s.Regions[id].InlineStyle.WebVTTWidth)...)
}
c = append(c, bytesLineSeparator...)
}
if len(s.Regions) > 0 {
c = append(c, bytesLineSeparator...)
}
// Loop through subtitles
for index, item := range s.Items {
// Add comments
if len(item.Comments) > 0 {
c = append(c, []byte("NOTE ")...)
for _, comment := range item.Comments {
c = append(c, []byte(comment)...)
c = append(c, bytesLineSeparator...)
}
c = append(c, bytesLineSeparator...)
}
// Add time boundaries
c = append(c, []byte(strconv.Itoa(index+1))...)
c = append(c, bytesLineSeparator...)
c = append(c, []byte(formatDurationWebVTT(item.StartAt))...)
c = append(c, bytesWebVTTTimeBoundariesSeparator...)
c = append(c, []byte(formatDurationWebVTT(item.EndAt))...)
// Add styles
if item.InlineStyle != nil {
if item.InlineStyle.WebVTTAlign != "" {
c = append(c, bytesSpace...)
c = append(c, []byte("align:"+item.InlineStyle.WebVTTAlign)...)
}
if item.InlineStyle.WebVTTLine != "" {
c = append(c, bytesSpace...)
c = append(c, []byte("line:"+item.InlineStyle.WebVTTLine)...)
}
if item.InlineStyle.WebVTTPosition != "" {
c = append(c, bytesSpace...)
c = append(c, []byte("position:"+item.InlineStyle.WebVTTPosition)...)
}
if item.Region != nil {
c = append(c, bytesSpace...)
c = append(c, []byte("region:"+item.Region.ID)...)
}
if item.InlineStyle.WebVTTSize != "" {
c = append(c, bytesSpace...)
c = append(c, []byte("size:"+item.InlineStyle.WebVTTSize)...)
}
if item.InlineStyle.WebVTTVertical != "" {
c = append(c, bytesSpace...)
c = append(c, []byte("vertical:"+item.InlineStyle.WebVTTVertical)...)
}
}
// Add new line
c = append(c, bytesLineSeparator...)
// Loop through lines
for _, l := range item.Lines {
c = append(c, []byte(l.String())...)
c = append(c, bytesLineSeparator...)
}
// Add new line
c = append(c, bytesLineSeparator...)
}
// Remove last new line
c = c[:len(c)-1]
// Write
if _, err = o.Write(c); err != nil {
err = errors.Wrap(err, "astisub: writing failed")
return
}
return
}