init
This commit is contained in:
21
vendor/github.com/asticode/go-astisub/LICENSE
generated
vendored
Normal file
21
vendor/github.com/asticode/go-astisub/LICENSE
generated
vendored
Normal 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
91
vendor/github.com/asticode/go-astisub/README.md
generated
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
[](http://goreportcard.com/report/github.com/asticode/go-astisub)
|
||||
[](https://godoc.org/github.com/asticode/go-astisub)
|
||||
[](https://travis-ci.org/asticode/go-astisub#)
|
||||
[](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
7
vendor/github.com/asticode/go-astisub/language.go
generated
vendored
Normal 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
135
vendor/github.com/asticode/go-astisub/srt.go
generated
vendored
Normal 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
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
860
vendor/github.com/asticode/go-astisub/stl.go
generated
vendored
Normal 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
702
vendor/github.com/asticode/go-astisub/subtitles.go
generated
vendored
Normal 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
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
673
vendor/github.com/asticode/go-astisub/ttml.go
generated
vendored
Normal 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
315
vendor/github.com/asticode/go-astisub/webvtt.go
generated
vendored
Normal 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
|
||||
}
|
Reference in New Issue
Block a user