mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 04:44:37 +03:00
Caption support (#2462)
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
5
vendor/github.com/asticode/go-astisub/.gitignore
generated
vendored
Normal file
5
vendor/github.com/asticode/go-astisub/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.idea/
|
||||
cover*
|
||||
test
|
||||
14
vendor/github.com/asticode/go-astisub/.travis.yml
generated
vendored
Normal file
14
vendor/github.com/asticode/go-astisub/.travis.yml
generated
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
language: go
|
||||
go:
|
||||
- 1.x
|
||||
- tip
|
||||
install:
|
||||
- go get -t ./...
|
||||
- go get golang.org/x/tools/cmd/cover
|
||||
- go get github.com/mattn/goveralls
|
||||
matrix:
|
||||
allow_failures:
|
||||
- go: tip
|
||||
script:
|
||||
- go test -race -v -coverprofile=coverage.out
|
||||
- $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci
|
||||
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.
|
||||
95
vendor/github.com/asticode/go-astisub/README.md
generated
vendored
Normal file
95
vendor/github.com/asticode/go-astisub/README.md
generated
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
[](http://goreportcard.com/report/github.com/asticode/go-astisub)
|
||||
[](https://godoc.org/github.com/asticode/go-astisub)
|
||||
[](https://travis-ci.com/asticode/go-astisub#)
|
||||
[](https://coveralls.io/github/asticode/go-astisub)
|
||||
|
||||
This is a Golang library to manipulate subtitles.
|
||||
|
||||
It allows you to manipulate `srt`, `stl`, `ttml`, `ssa/ass`, `webvtt` and `teletext` files for now.
|
||||
|
||||
Available operations are `parsing`, `writing`, `syncing`, `fragmenting`, `unfragmenting`, `merging` and `optimizing`.
|
||||
|
||||
# Installation
|
||||
|
||||
To install the library:
|
||||
|
||||
go get github.com/asticode/go-astisub
|
||||
|
||||
To install the CLI:
|
||||
|
||||
go install github.com/asticode/go-astisub/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
|
||||
- [x] .teletext
|
||||
- [ ] .smi
|
||||
10
vendor/github.com/asticode/go-astisub/language.go
generated
vendored
Normal file
10
vendor/github.com/asticode/go-astisub/language.go
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
package astisub
|
||||
|
||||
// Languages
|
||||
const (
|
||||
LanguageChinese = "chinese"
|
||||
LanguageEnglish = "english"
|
||||
LanguageFrench = "french"
|
||||
LanguageJapanese = "japanese"
|
||||
LanguageNorwegian = "norwegian"
|
||||
)
|
||||
159
vendor/github.com/asticode/go-astisub/srt.go
generated
vendored
Normal file
159
vendor/github.com/asticode/go-astisub/srt.go
generated
vendored
Normal file
@@ -0,0 +1,159 @@
|
||||
package astisub
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 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 lineNum int
|
||||
var s = &Item{}
|
||||
for scanner.Scan() {
|
||||
// Fetch line
|
||||
line = strings.TrimSpace(scanner.Text())
|
||||
lineNum++
|
||||
|
||||
// Remove BOM header
|
||||
if lineNum == 1 {
|
||||
line = strings.TrimPrefix(line, string(BytesBOM))
|
||||
}
|
||||
|
||||
// Line contains time boundaries
|
||||
if strings.Contains(line, srtTimeBoundariesSeparator) {
|
||||
// Return the wrong number of rows
|
||||
if len(s.Lines) == 0 {
|
||||
err = fmt.Errorf("astisub: line %d: no lines", lineNum)
|
||||
return
|
||||
}
|
||||
|
||||
// Remove last item of previous subtitle since it's the index
|
||||
index := s.Lines[len(s.Lines)-1]
|
||||
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 Index
|
||||
s.Index, _ = strconv.Atoi(index.String())
|
||||
|
||||
// Extract time boundaries
|
||||
s1 := strings.Split(line, srtTimeBoundariesSeparator)
|
||||
if l := len(s1); l < 2 {
|
||||
err = fmt.Errorf("astisub: line %d: time boundaries has only %d element(s)", lineNum, l)
|
||||
return
|
||||
}
|
||||
// We do this to eliminate extra stuff like positions which are not documented anywhere
|
||||
s2 := strings.Split(s1[1], " ")
|
||||
|
||||
// Parse time boundaries
|
||||
if s.StartAt, err = parseDurationSRT(s1[0]); err != nil {
|
||||
err = fmt.Errorf("astisub: line %d: parsing srt duration %s failed: %w", lineNum, s1[0], err)
|
||||
return
|
||||
}
|
||||
if s.EndAt, err = parseDurationSRT(s2[0]); err != nil {
|
||||
err = fmt.Errorf("astisub: line %d: parsing srt duration %s failed: %w", lineNum, s2[0], err)
|
||||
return
|
||||
}
|
||||
|
||||
// Append subtitle
|
||||
o.Items = append(o.Items, s)
|
||||
} else {
|
||||
// Add text
|
||||
s.Lines = append(s.Lines, Line{Items: []LineItem{{Text: strings.TrimSpace(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 = fmt.Errorf("astisub: writing failed: %w", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
1297
vendor/github.com/asticode/go-astisub/ssa.go
generated
vendored
Normal file
1297
vendor/github.com/asticode/go-astisub/ssa.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1085
vendor/github.com/asticode/go-astisub/stl.go
generated
vendored
Normal file
1085
vendor/github.com/asticode/go-astisub/stl.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
779
vendor/github.com/asticode/go-astisub/subtitles.go
generated
vendored
Normal file
779
vendor/github.com/asticode/go-astisub/subtitles.go
generated
vendored
Normal file
@@ -0,0 +1,779 @@
|
||||
package astisub
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/asticode/go-astikit"
|
||||
)
|
||||
|
||||
// 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
|
||||
STL STLOptions
|
||||
}
|
||||
|
||||
// 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 = fmt.Errorf("astisub: opening %s failed: %w", o.Filename, err)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// Parse the content
|
||||
switch filepath.Ext(strings.ToLower(o.Filename)) {
|
||||
case ".srt":
|
||||
s, err = ReadFromSRT(f)
|
||||
case ".ssa", ".ass":
|
||||
s, err = ReadFromSSA(f)
|
||||
case ".stl":
|
||||
s, err = ReadFromSTL(f, o.STL)
|
||||
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
|
||||
Index int
|
||||
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 = fmt.Errorf("parsing int %s with base %d failed: %w", s, base, err)
|
||||
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))
|
||||
}
|
||||
|
||||
type Justification int
|
||||
|
||||
var (
|
||||
JustificationUnchanged = Justification(1)
|
||||
JustificationLeft = Justification(2)
|
||||
JustificationCentered = Justification(3)
|
||||
JustificationRight = Justification(4)
|
||||
)
|
||||
|
||||
// StyleAttributes represents style attributes
|
||||
type StyleAttributes struct {
|
||||
SSAAlignment *int
|
||||
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 *float64 // pixels
|
||||
SSAOutlineColour *Color
|
||||
SSAPrimaryColour *Color
|
||||
SSAScaleX *float64 // %
|
||||
SSAScaleY *float64 // %
|
||||
SSASecondaryColour *Color
|
||||
SSAShadow *float64 // pixels
|
||||
SSASpacing *float64 // pixels
|
||||
SSAStrikeout *bool
|
||||
SSAUnderline *bool
|
||||
STLBoxing *bool
|
||||
STLItalics *bool
|
||||
STLJustification *Justification
|
||||
STLPosition *STLPosition
|
||||
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
|
||||
WebVTTItalics bool
|
||||
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() {
|
||||
if sa.STLJustification != nil {
|
||||
switch *sa.STLJustification {
|
||||
case JustificationCentered:
|
||||
// default to middle anyway?
|
||||
case JustificationRight:
|
||||
sa.WebVTTAlign = "right"
|
||||
case JustificationLeft:
|
||||
sa.WebVTTAlign = "left"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (sa *StyleAttributes) propagateTeletextAttributes() {
|
||||
if sa.TeletextColor != nil {
|
||||
sa.TTMLColor = astikit.StrPtr("#" + sa.TeletextColor.TTMLString())
|
||||
}
|
||||
}
|
||||
|
||||
//reference for migration: https://w3c.github.io/ttml-webvtt-mapping/
|
||||
func (sa *StyleAttributes) propagateTTMLAttributes() {
|
||||
if sa.TTMLTextAlign != nil {
|
||||
sa.WebVTTAlign = *sa.TTMLTextAlign
|
||||
}
|
||||
if sa.TTMLExtent != nil {
|
||||
//region settings
|
||||
lineHeight := 5 //assuming height of line as 5.33vh
|
||||
dimensions := strings.Split(*sa.TTMLExtent, " ")
|
||||
if len(dimensions) > 1 {
|
||||
sa.WebVTTWidth = dimensions[0]
|
||||
if height, err := strconv.Atoi(strings.ReplaceAll(dimensions[1], "%", "")); err == nil {
|
||||
sa.WebVTTLines = height / lineHeight
|
||||
}
|
||||
//cue settings
|
||||
//default TTML WritingMode is lrtb i.e. left to right, top to bottom
|
||||
sa.WebVTTSize = dimensions[1]
|
||||
if sa.TTMLWritingMode != nil && strings.HasPrefix(*sa.TTMLWritingMode, "tb") {
|
||||
sa.WebVTTSize = dimensions[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
if sa.TTMLOrigin != nil {
|
||||
//region settings
|
||||
sa.WebVTTRegionAnchor = "0%,0%"
|
||||
sa.WebVTTViewportAnchor = strings.ReplaceAll(strings.TrimSpace(*sa.TTMLOrigin), " ", ",")
|
||||
sa.WebVTTScroll = "up"
|
||||
//cue settings
|
||||
coordinates := strings.Split(*sa.TTMLOrigin, " ")
|
||||
if len(coordinates) > 1 {
|
||||
sa.WebVTTLine = coordinates[0]
|
||||
sa.WebVTTPosition = coordinates[1]
|
||||
if sa.TTMLWritingMode != nil && strings.HasPrefix(*sa.TTMLWritingMode, "tb") {
|
||||
sa.WebVTTLine = coordinates[1]
|
||||
sa.WebVTTPosition = coordinates[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
STLCountryOfOrigin string
|
||||
STLCreationDate *time.Time
|
||||
STLDisplayStandardCode string
|
||||
STLMaximumNumberOfDisplayableCharactersInAnyTextRow *int
|
||||
STLMaximumNumberOfDisplayableRows *int
|
||||
STLPublisher string
|
||||
STLRevisionDate *time.Time
|
||||
STLSubtitleListReferenceCode string
|
||||
STLTimecodeStartOfProgramme time.Duration
|
||||
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
|
||||
}
|
||||
|
||||
// Order
|
||||
s.Order()
|
||||
|
||||
// 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 {
|
||||
// Only override end time if longer
|
||||
if s.Items[i].EndAt < s.Items[j].EndAt {
|
||||
s.Items[i].EndAt = s.Items[j].EndAt
|
||||
}
|
||||
s.Items = append(s.Items[:j], s.Items[j+1:]...)
|
||||
j--
|
||||
} else if s.Items[i].EndAt < s.Items[j].StartAt {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 = fmt.Errorf("astisub: creating %s failed: %w", dst, err)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// Write the content
|
||||
switch filepath.Ext(strings.ToLower(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 = fmt.Errorf("astisub: atoi of %s failed: %w", s, err)
|
||||
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 = fmt.Errorf("astisub: atoi of %s failed: %w", s, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse minutes
|
||||
var minutes int
|
||||
s = strings.TrimSpace(partMinutes)
|
||||
if minutes, err = strconv.Atoi(s); err != nil {
|
||||
err = fmt.Errorf("astisub: atoi of %s failed: %w", s, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse hours
|
||||
var hours int
|
||||
if len(partHours) > 0 {
|
||||
s = strings.TrimSpace(partHours)
|
||||
if hours, err = strconv.Atoi(s); err != nil {
|
||||
err = fmt.Errorf("astisub: atoi of %s failed: %w", s, err)
|
||||
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
|
||||
}
|
||||
997
vendor/github.com/asticode/go-astisub/teletext.go
generated
vendored
Normal file
997
vendor/github.com/asticode/go-astisub/teletext.go
generated
vendored
Normal file
@@ -0,0 +1,997 @@
|
||||
package astisub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math/bits"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/asticode/go-astikit"
|
||||
"github.com/asticode/go-astits"
|
||||
)
|
||||
|
||||
// Errors
|
||||
var (
|
||||
ErrNoValidTeletextPID = errors.New("astisub: no valid teletext PID")
|
||||
)
|
||||
|
||||
type teletextCharset [96][]byte
|
||||
|
||||
type teletextNationalSubset [13][]byte
|
||||
|
||||
// Chapter: 15.2 | Page: 109 | Link: http://www.etsi.org/deliver/etsi_i_ets/300700_300799/300706/01_60/ets_300706e01p.pdf
|
||||
// It is indexed by triplet1 then by national option subset code
|
||||
var teletextCharsets = map[uint8]map[uint8]struct {
|
||||
g0 *teletextCharset
|
||||
g2 *teletextCharset
|
||||
national *teletextNationalSubset
|
||||
}{
|
||||
0: {
|
||||
0: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetEnglish},
|
||||
1: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetFrench},
|
||||
2: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetSwedishFinnishHungarian},
|
||||
3: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetCzechSlovak},
|
||||
4: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetGerman},
|
||||
5: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetPortugueseSpanish},
|
||||
6: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetItalian},
|
||||
7: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin},
|
||||
},
|
||||
1: {
|
||||
0: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetPolish},
|
||||
1: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetFrench},
|
||||
2: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetSwedishFinnishHungarian},
|
||||
3: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetCzechSlovak},
|
||||
4: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetGerman},
|
||||
5: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin},
|
||||
6: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetItalian},
|
||||
7: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin},
|
||||
},
|
||||
2: {
|
||||
0: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetEnglish},
|
||||
1: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetFrench},
|
||||
2: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetSwedishFinnishHungarian},
|
||||
3: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetCzechSlovak},
|
||||
4: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetGerman},
|
||||
5: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetPortugueseSpanish},
|
||||
6: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetItalian},
|
||||
7: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin},
|
||||
},
|
||||
3: {
|
||||
0: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin},
|
||||
1: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin},
|
||||
2: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin},
|
||||
3: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin},
|
||||
4: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin},
|
||||
5: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetSerbianCroatianSlovenian},
|
||||
6: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin},
|
||||
7: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetRomanian},
|
||||
},
|
||||
4: {
|
||||
0: {g0: teletextCharsetG0CyrillicOption1, g2: teletextCharsetG2Cyrillic},
|
||||
1: {g0: teletextCharsetG0CyrillicOption2, g2: teletextCharsetG2Cyrillic},
|
||||
2: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetEstonian},
|
||||
3: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetCzechSlovak},
|
||||
4: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetGerman},
|
||||
5: {g0: teletextCharsetG0CyrillicOption3, g2: teletextCharsetG2Cyrillic},
|
||||
6: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetLettishLithuanian},
|
||||
},
|
||||
6: {
|
||||
3: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetTurkish},
|
||||
7: {g0: teletextCharsetG0Greek, g2: teletextCharsetG2Greek},
|
||||
},
|
||||
8: {
|
||||
0: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Arabic, national: teletextNationalSubsetEnglish},
|
||||
1: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Arabic, national: teletextNationalSubsetFrench},
|
||||
7: {g0: teletextCharsetG0Arabic, g2: teletextCharsetG2Arabic},
|
||||
},
|
||||
10: {
|
||||
5: {g0: teletextCharsetG0Hebrew, g2: teletextCharsetG2Arabic},
|
||||
7: {g0: teletextCharsetG0Arabic, g2: teletextCharsetG2Arabic},
|
||||
},
|
||||
}
|
||||
|
||||
// Teletext G0 charsets
|
||||
var (
|
||||
teletextCharsetG0CyrillicOption1 = &teletextCharset{
|
||||
[]byte{0x20}, []byte{0x21}, []byte{0x22}, []byte{0x23}, []byte{0x24}, []byte{0x25}, []byte{0xd1, 0x8b},
|
||||
[]byte{0x27}, []byte{0x28}, []byte{0x29}, []byte{0x2a}, []byte{0x2b}, []byte{0x2c}, []byte{0x2d},
|
||||
[]byte{0x2e}, []byte{0x2f}, []byte{0x30}, []byte{0x31}, []byte{0xe3, 0x88, 0x80}, []byte{0x33}, []byte{0x34},
|
||||
[]byte{0x35}, []byte{0x36}, []byte{0x37}, []byte{0x38}, []byte{0x39}, []byte{0x3a}, []byte{0x3b},
|
||||
[]byte{0x3c}, []byte{0x3d}, []byte{0x3e}, []byte{0x3f}, []byte{0xd0, 0xa7}, []byte{0xd0, 0x90},
|
||||
[]byte{0xd0, 0x91}, []byte{0xd0, 0xa6}, []byte{0xd0, 0x94}, []byte{0xd0, 0x95}, []byte{0xd0, 0xa4},
|
||||
[]byte{0xd0, 0x93}, []byte{0xd0, 0xa5}, []byte{0xd0, 0x98}, []byte{0xd0, 0x88}, []byte{0xd0, 0x9a},
|
||||
[]byte{0xd0, 0x9b}, []byte{0xd0, 0x9c}, []byte{0xd0, 0x9d}, []byte{0xd0, 0x9e}, []byte{0xd0, 0x9f},
|
||||
[]byte{0xd0, 0x8c}, []byte{0xd0, 0xa0}, []byte{0xd0, 0xa1}, []byte{0xd0, 0xa2}, []byte{0xd0, 0xa3},
|
||||
[]byte{0xd0, 0x92}, []byte{0xd0, 0x83}, []byte{0xd0, 0x89}, []byte{0xd0, 0x8a}, []byte{0xd0, 0x97},
|
||||
[]byte{0xd0, 0x8b}, []byte{0xd0, 0x96}, []byte{0xd0, 0x82}, []byte{0xd0, 0xa8}, []byte{0xd0, 0x8f},
|
||||
[]byte{0xd1, 0x87}, []byte{0xd0, 0xb0}, []byte{0xd0, 0xb1}, []byte{0xd1, 0x86}, []byte{0xd0, 0xb4},
|
||||
[]byte{0xd0, 0xb5}, []byte{0xd1, 0x84}, []byte{0xd0, 0xb3}, []byte{0xd1, 0x85}, []byte{0xd0, 0xb8},
|
||||
[]byte{0xd0, 0xa8}, []byte{0xd0, 0xba}, []byte{0xd0, 0xbb}, []byte{0xd0, 0xbc}, []byte{0xd0, 0xbd},
|
||||
[]byte{0xd0, 0xbe}, []byte{0xd0, 0xbf}, []byte{0xd0, 0xac}, []byte{0xd1, 0x80}, []byte{0xd1, 0x81},
|
||||
[]byte{0xd1, 0x82}, []byte{0xd1, 0x83}, []byte{0xd0, 0xb2}, []byte{0xd0, 0xa3}, []byte{0xd0, 0xa9},
|
||||
[]byte{0xd0, 0xaa}, []byte{0xd0, 0xb7}, []byte{0xd0, 0xab}, []byte{0xd0, 0xb6}, []byte{0xd0, 0xa2},
|
||||
[]byte{0xd1, 0x88}, []byte{0xd0, 0xaf},
|
||||
}
|
||||
teletextCharsetG0CyrillicOption2 = &teletextCharset{
|
||||
[]byte{0x20}, []byte{0x21}, []byte{0x22}, []byte{0x23}, []byte{0x24}, []byte{0x25}, []byte{0xd1, 0x8b},
|
||||
[]byte{0x27}, []byte{0x28}, []byte{0x29}, []byte{0x2a}, []byte{0x2b}, []byte{0x2c}, []byte{0x2d},
|
||||
[]byte{0x2e}, []byte{0x2f}, []byte{0x30}, []byte{0x31}, []byte{0x32}, []byte{0x33}, []byte{0x34},
|
||||
[]byte{0x35}, []byte{0x36}, []byte{0x37}, []byte{0x38}, []byte{0x39}, []byte{0x3a}, []byte{0x3b},
|
||||
[]byte{0x3c}, []byte{0x3d}, []byte{0x3e}, []byte{0x3f}, []byte{0xd0, 0xae}, []byte{0xd0, 0x90},
|
||||
[]byte{0xd0, 0x91}, []byte{0xd0, 0xa6}, []byte{0xd0, 0x94}, []byte{0xd0, 0x95}, []byte{0xd0, 0xa4},
|
||||
[]byte{0xd0, 0x93}, []byte{0xd0, 0xa5}, []byte{0xd0, 0x98}, []byte{0xd0, 0x99}, []byte{0xd0, 0x9a},
|
||||
[]byte{0xd0, 0x9b}, []byte{0xd0, 0x9c}, []byte{0xd0, 0x9d}, []byte{0xd0, 0x9e}, []byte{0xd0, 0x9f},
|
||||
[]byte{0xd0, 0xaf}, []byte{0xd0, 0xa0}, []byte{0xd0, 0xa1}, []byte{0xd0, 0xa2}, []byte{0xd0, 0xa3},
|
||||
[]byte{0xd0, 0x96}, []byte{0xd0, 0x92}, []byte{0xd0, 0xac}, []byte{0xd0, 0xaa}, []byte{0xd0, 0x97},
|
||||
[]byte{0xd0, 0xa8}, []byte{0xd0, 0xad}, []byte{0xd0, 0xa9}, []byte{0xd0, 0xa7}, []byte{0xd0, 0xab},
|
||||
[]byte{0xd1, 0x8e}, []byte{0xd0, 0xb0}, []byte{0xd0, 0xb1}, []byte{0xd1, 0x86}, []byte{0xd0, 0xb4},
|
||||
[]byte{0xd0, 0xb5}, []byte{0xd1, 0x84}, []byte{0xd0, 0xb3}, []byte{0xd1, 0x85}, []byte{0xd0, 0xb8},
|
||||
[]byte{0xd0, 0xb9}, []byte{0xd0, 0xba}, []byte{0xd0, 0xbb}, []byte{0xd0, 0xbc}, []byte{0xd0, 0xbd},
|
||||
[]byte{0xd0, 0xbe}, []byte{0xd0, 0xbf}, []byte{0xd1, 0x8f}, []byte{0xd1, 0x80}, []byte{0xd1, 0x81},
|
||||
[]byte{0xd1, 0x82}, []byte{0xd1, 0x83}, []byte{0xd0, 0xb6}, []byte{0xd0, 0xb2}, []byte{0xd1, 0x8c},
|
||||
[]byte{0xd1, 0x8a}, []byte{0xd0, 0xb7}, []byte{0xd1, 0x88}, []byte{0xd1, 0x8d}, []byte{0xd1, 0x89},
|
||||
[]byte{0xd1, 0x87}, []byte{0xd1, 0x8b},
|
||||
}
|
||||
teletextCharsetG0CyrillicOption3 = &teletextCharset{
|
||||
[]byte{0x20}, []byte{0x21}, []byte{0x22}, []byte{0x23}, []byte{0x24}, []byte{0x25}, []byte{0xc3, 0xaf},
|
||||
[]byte{0x27}, []byte{0x28}, []byte{0x29}, []byte{0x2a}, []byte{0x2b}, []byte{0x2c}, []byte{0x2d},
|
||||
[]byte{0x2e}, []byte{0x2f}, []byte{0x30}, []byte{0x31}, []byte{0x32}, []byte{0x33}, []byte{0x34},
|
||||
[]byte{0x35}, []byte{0x36}, []byte{0x37}, []byte{0x38}, []byte{0x39}, []byte{0x3a}, []byte{0x3b},
|
||||
[]byte{0x3c}, []byte{0x3d}, []byte{0x3e}, []byte{0x3f}, []byte{0xd0, 0xae}, []byte{0xd0, 0x90},
|
||||
[]byte{0xd0, 0x91}, []byte{0xd0, 0xa6}, []byte{0xd0, 0x94}, []byte{0xd0, 0x95}, []byte{0xd0, 0xa4},
|
||||
[]byte{0xd0, 0x93}, []byte{0xd0, 0xa5}, []byte{0xd0, 0x98}, []byte{0xd0, 0x99}, []byte{0xd0, 0x9a},
|
||||
[]byte{0xd0, 0x9b}, []byte{0xd0, 0x9c}, []byte{0xd0, 0x9d}, []byte{0xd0, 0x9e}, []byte{0xd0, 0x9f},
|
||||
[]byte{0xd0, 0xaf}, []byte{0xd0, 0xa0}, []byte{0xd0, 0xa1}, []byte{0xd0, 0xa2}, []byte{0xd0, 0xa3},
|
||||
[]byte{0xd0, 0x96}, []byte{0xd0, 0x92}, []byte{0xd0, 0xac}, []byte{0x49}, []byte{0xd0, 0x97},
|
||||
[]byte{0xd0, 0xa8}, []byte{0xd0, 0xad}, []byte{0xd0, 0xa9}, []byte{0xd0, 0xa7}, []byte{0xc3, 0x8f},
|
||||
[]byte{0xd1, 0x8e}, []byte{0xd0, 0xb0}, []byte{0xd0, 0xb1}, []byte{0xd1, 0x86}, []byte{0xd0, 0xb4},
|
||||
[]byte{0xd0, 0xb5}, []byte{0xd1, 0x84}, []byte{0xd0, 0xb3}, []byte{0xd1, 0x85}, []byte{0xd0, 0xb8},
|
||||
[]byte{0xd0, 0xb9}, []byte{0xd0, 0xba}, []byte{0xd0, 0xbb}, []byte{0xd0, 0xbc}, []byte{0xd0, 0xbd},
|
||||
[]byte{0xd0, 0xbe}, []byte{0xd0, 0xbf}, []byte{0xd1, 0x8f}, []byte{0xd1, 0x80}, []byte{0xd1, 0x81},
|
||||
[]byte{0xd1, 0x82}, []byte{0xd1, 0x83}, []byte{0xd0, 0xb6}, []byte{0xd0, 0xb2}, []byte{0xd1, 0x8c},
|
||||
[]byte{0x69}, []byte{0xd0, 0xb7}, []byte{0xd1, 0x88}, []byte{0xd1, 0x8d}, []byte{0xd1, 0x89},
|
||||
[]byte{0xd1, 0x87}, []byte{0xc3, 0xbf},
|
||||
}
|
||||
teletextCharsetG0Greek = &teletextCharset{
|
||||
[]byte{0x20}, []byte{0x21}, []byte{0x22}, []byte{0x23}, []byte{0x24}, []byte{0x25}, []byte{0x26},
|
||||
[]byte{0x27}, []byte{0x28}, []byte{0x29}, []byte{0x2a}, []byte{0x2b}, []byte{0x2c}, []byte{0x2d},
|
||||
[]byte{0x2e}, []byte{0x2f}, []byte{0x30}, []byte{0x31}, []byte{0x32}, []byte{0x33}, []byte{0x34},
|
||||
[]byte{0x35}, []byte{0x36}, []byte{0x37}, []byte{0x38}, []byte{0x39}, []byte{0x3a}, []byte{0x3b},
|
||||
[]byte{0x3c}, []byte{0x3d}, []byte{0x3e}, []byte{0x3f}, []byte{0xce, 0x90}, []byte{0xce, 0x91},
|
||||
[]byte{0xce, 0x92}, []byte{0xce, 0x93}, []byte{0xce, 0x94}, []byte{0xce, 0x95}, []byte{0xce, 0x96},
|
||||
[]byte{0xce, 0x97}, []byte{0xce, 0x98}, []byte{0xce, 0x99}, []byte{0xce, 0x9a}, []byte{0xce, 0x9b},
|
||||
[]byte{0xce, 0x9c}, []byte{0xce, 0x9d}, []byte{0xce, 0x9e}, []byte{0xce, 0x9f}, []byte{0xce, 0xa0},
|
||||
[]byte{0xce, 0xa1}, []byte{0xce, 0xa2}, []byte{0xce, 0xa3}, []byte{0xce, 0xa4}, []byte{0xce, 0xa5},
|
||||
[]byte{0xce, 0xa6}, []byte{0xce, 0xa7}, []byte{0xce, 0xa8}, []byte{0xce, 0xa9}, []byte{0xce, 0xaa},
|
||||
[]byte{0xce, 0xab}, []byte{0xce, 0xac}, []byte{0xce, 0xad}, []byte{0xce, 0xae}, []byte{0xce, 0xaf},
|
||||
[]byte{0xce, 0xb0}, []byte{0xce, 0xb1}, []byte{0xce, 0xb2}, []byte{0xce, 0xb3}, []byte{0xce, 0xb4},
|
||||
[]byte{0xce, 0xb5}, []byte{0xce, 0xb6}, []byte{0xce, 0xb7}, []byte{0xce, 0xb8}, []byte{0xce, 0xb9},
|
||||
[]byte{0xce, 0xba}, []byte{0xce, 0xbb}, []byte{0xce, 0xbc}, []byte{0xce, 0xbd}, []byte{0xce, 0xbe},
|
||||
[]byte{0xce, 0xbf}, []byte{0xcf, 0x80}, []byte{0xcf, 0x81}, []byte{0xcf, 0x82}, []byte{0xcf, 0x83},
|
||||
[]byte{0xcf, 0x84}, []byte{0xcf, 0x85}, []byte{0xcf, 0x86}, []byte{0xcf, 0x87}, []byte{0xcf, 0x88},
|
||||
[]byte{0xcf, 0x89}, []byte{0xcf, 0x8a}, []byte{0xcf, 0x8b}, []byte{0xcf, 0x8c}, []byte{0xcf, 0x8d},
|
||||
[]byte{0xcf, 0x8e}, []byte{0xcf, 0x8f},
|
||||
}
|
||||
teletextCharsetG0Latin = &teletextCharset{
|
||||
[]byte{0x20}, []byte{0x21}, []byte{0x22}, []byte{0xc2, 0xa3}, []byte{0x24}, []byte{0x25}, []byte{0x26},
|
||||
[]byte{0x27}, []byte{0x28}, []byte{0x29}, []byte{0x2a}, []byte{0x2b}, []byte{0x2c}, []byte{0x2d},
|
||||
[]byte{0x2e}, []byte{0x2f}, []byte{0x30}, []byte{0x31}, []byte{0x32}, []byte{0x33}, []byte{0x34},
|
||||
[]byte{0x35}, []byte{0x36}, []byte{0x37}, []byte{0x38}, []byte{0x39}, []byte{0x3a}, []byte{0x3b},
|
||||
[]byte{0x3c}, []byte{0x3d}, []byte{0x3e}, []byte{0x3f}, []byte{0x40}, []byte{0x41}, []byte{0x42},
|
||||
[]byte{0x43}, []byte{0x44}, []byte{0x45}, []byte{0x46}, []byte{0x47}, []byte{0x48}, []byte{0x49},
|
||||
[]byte{0x4a}, []byte{0x4b}, []byte{0x4c}, []byte{0x4d}, []byte{0x4e}, []byte{0x4f}, []byte{0x50},
|
||||
[]byte{0x51}, []byte{0x52}, []byte{0x53}, []byte{0x54}, []byte{0x55}, []byte{0x56}, []byte{0x57},
|
||||
[]byte{0x58}, []byte{0x59}, []byte{0x5a}, []byte{0xc2, 0xab}, []byte{0xc2, 0xbd}, []byte{0xc2, 0xbb},
|
||||
[]byte{0x5e}, []byte{0x23}, []byte{0x2d}, []byte{0x61}, []byte{0x62}, []byte{0x63}, []byte{0x64},
|
||||
[]byte{0x65}, []byte{0x66}, []byte{0x67}, []byte{0x68}, []byte{0x69}, []byte{0x6a}, []byte{0x6b},
|
||||
[]byte{0x6c}, []byte{0x6d}, []byte{0x6e}, []byte{0x6f}, []byte{0x70}, []byte{0x71}, []byte{0x72},
|
||||
[]byte{0x73}, []byte{0x74}, []byte{0x75}, []byte{0x76}, []byte{0x77}, []byte{0x78}, []byte{0x79},
|
||||
[]byte{0x7a}, []byte{0xc2, 0xbc}, []byte{0xc2, 0xa6}, []byte{0xc2, 0xbe}, []byte{0xc3, 0xb7}, []byte{0x7f},
|
||||
}
|
||||
// TODO Add
|
||||
teletextCharsetG0Arabic = teletextCharsetG0Latin
|
||||
teletextCharsetG0Hebrew = teletextCharsetG0Latin
|
||||
)
|
||||
|
||||
// Teletext G2 charsets
|
||||
var (
|
||||
teletextCharsetG2Latin = &teletextCharset{
|
||||
[]byte{0x20}, []byte{0xc2, 0xa1}, []byte{0xc2, 0xa2}, []byte{0xc2, 0xa3}, []byte{0x24},
|
||||
[]byte{0xc2, 0xa5}, []byte{0x23}, []byte{0xc2, 0xa7}, []byte{0xc2, 0xa4}, []byte{0xe2, 0x80, 0x98},
|
||||
[]byte{0xe2, 0x80, 0x9c}, []byte{0xc2, 0xab}, []byte{0xe2, 0x86, 0x90}, []byte{0xe2, 0x86, 0x91},
|
||||
[]byte{0xe2, 0x86, 0x92}, []byte{0xe2, 0x86, 0x93}, []byte{0xc2, 0xb0}, []byte{0xc2, 0xb1},
|
||||
[]byte{0xc2, 0xb2}, []byte{0xc2, 0xb3}, []byte{0xc3, 0x97}, []byte{0xc2, 0xb5}, []byte{0xc2, 0xb6},
|
||||
[]byte{0xc2, 0xb7}, []byte{0xc3, 0xb7}, []byte{0xe2, 0x80, 0x99}, []byte{0xe2, 0x80, 0x9d},
|
||||
[]byte{0xc2, 0xbb}, []byte{0xc2, 0xbc}, []byte{0xc2, 0xbd}, []byte{0xc2, 0xbe}, []byte{0xc2, 0xbf},
|
||||
[]byte{0x20}, []byte{0xcc, 0x80}, []byte{0xcc, 0x81}, []byte{0xcc, 0x82}, []byte{0xcc, 0x83},
|
||||
[]byte{0xcc, 0x84}, []byte{0xcc, 0x86}, []byte{0xcc, 0x87}, []byte{0xcc, 0x88}, []byte{0x00},
|
||||
[]byte{0xcc, 0x8a}, []byte{0xcc, 0xa7}, []byte{0x5f}, []byte{0xcc, 0x8b}, []byte{0xcc, 0xa8},
|
||||
[]byte{0xcc, 0x8c}, []byte{0xe2, 0x80, 0x95}, []byte{0xc2, 0xb9}, []byte{0xc2, 0xae}, []byte{0xc2, 0xa9},
|
||||
[]byte{0xe2, 0x84, 0xa2}, []byte{0xe2, 0x99, 0xaa}, []byte{0xe2, 0x82, 0xac}, []byte{0xe2, 0x80, 0xb0},
|
||||
[]byte{0xce, 0xb1}, []byte{0x00}, []byte{0x00}, []byte{0x00}, []byte{0xe2, 0x85, 0x9b},
|
||||
[]byte{0xe2, 0x85, 0x9c}, []byte{0xe2, 0x85, 0x9d}, []byte{0xe2, 0x85, 0x9e}, []byte{0xce, 0xa9},
|
||||
[]byte{0xc3, 0x86}, []byte{0xc4, 0x90}, []byte{0xc2, 0xaa}, []byte{0xc4, 0xa6}, []byte{0x00},
|
||||
[]byte{0xc4, 0xb2}, []byte{0xc4, 0xbf}, []byte{0xc5, 0x81}, []byte{0xc3, 0x98}, []byte{0xc5, 0x92},
|
||||
[]byte{0xc2, 0xba}, []byte{0xc3, 0x9e}, []byte{0xc5, 0xa6}, []byte{0xc5, 0x8a}, []byte{0xc5, 0x89},
|
||||
[]byte{0xc4, 0xb8}, []byte{0xc3, 0xa6}, []byte{0xc4, 0x91}, []byte{0xc3, 0xb0}, []byte{0xc4, 0xa7},
|
||||
[]byte{0xc4, 0xb1}, []byte{0xc4, 0xb3}, []byte{0xc5, 0x80}, []byte{0xc5, 0x82}, []byte{0xc3, 0xb8},
|
||||
[]byte{0xc5, 0x93}, []byte{0xc3, 0x9f}, []byte{0xc3, 0xbe}, []byte{0xc5, 0xa7}, []byte{0xc5, 0x8b},
|
||||
[]byte{0x20},
|
||||
}
|
||||
// TODO Add
|
||||
teletextCharsetG2Arabic = teletextCharsetG2Latin
|
||||
teletextCharsetG2Cyrillic = teletextCharsetG2Latin
|
||||
teletextCharsetG2Greek = teletextCharsetG2Latin
|
||||
)
|
||||
|
||||
var teletextNationalSubsetCharactersPositionInG0 = [13]uint8{0x03, 0x04, 0x20, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40, 0x5b, 0x5c, 0x5d, 0x5e}
|
||||
|
||||
// Teletext national subsets
|
||||
var (
|
||||
teletextNationalSubsetCzechSlovak = &teletextNationalSubset{
|
||||
[]byte{0x23}, []byte{0xc5, 0xaf}, []byte{0xc4, 0x8d}, []byte{0xc5, 0xa5}, []byte{0xc5, 0xbe},
|
||||
[]byte{0xc3, 0xbd}, []byte{0xc3, 0xad}, []byte{0xc5, 0x99}, []byte{0xc3, 0xa9}, []byte{0xc3, 0xa1},
|
||||
[]byte{0xc4, 0x9b}, []byte{0xc3, 0xba}, []byte{0xc5, 0xa1},
|
||||
}
|
||||
teletextNationalSubsetEnglish = &teletextNationalSubset{
|
||||
[]byte{0xc2, 0xa3}, []byte{0x24}, []byte{0x40}, []byte{0xc2, 0xab}, []byte{0xc2, 0xbd}, []byte{0xc2, 0xbb},
|
||||
[]byte{0x5e}, []byte{0x23}, []byte{0x2d}, []byte{0xc2, 0xbc}, []byte{0xc2, 0xa6}, []byte{0xc2, 0xbe},
|
||||
[]byte{0xc3, 0xb7},
|
||||
}
|
||||
teletextNationalSubsetEstonian = &teletextNationalSubset{
|
||||
[]byte{0x23}, []byte{0xc3, 0xb5}, []byte{0xc5, 0xa0}, []byte{0xc3, 0x84}, []byte{0xc3, 0x96},
|
||||
[]byte{0xc5, 0xbe}, []byte{0xc3, 0x9c}, []byte{0xc3, 0x95}, []byte{0xc5, 0xa1}, []byte{0xc3, 0xa4},
|
||||
[]byte{0xc3, 0xb6}, []byte{0xc5, 0xbe}, []byte{0xc3, 0xbc},
|
||||
}
|
||||
teletextNationalSubsetFrench = &teletextNationalSubset{
|
||||
[]byte{0xc3, 0xa9}, []byte{0xc3, 0xaf}, []byte{0xc3, 0xa0}, []byte{0xc3, 0xab}, []byte{0xc3, 0xaa},
|
||||
[]byte{0xc3, 0xb9}, []byte{0xc3, 0xae}, []byte{0x23}, []byte{0xc3, 0xa8}, []byte{0xc3, 0xa2},
|
||||
[]byte{0xc3, 0xb4}, []byte{0xc3, 0xbb}, []byte{0xc3, 0xa7},
|
||||
}
|
||||
teletextNationalSubsetGerman = &teletextNationalSubset{
|
||||
[]byte{0x23}, []byte{0x24}, []byte{0xc2, 0xa7}, []byte{0xc3, 0x84}, []byte{0xc3, 0x96}, []byte{0xc3, 0x9c},
|
||||
[]byte{0x5e}, []byte{0x5f}, []byte{0xc2, 0xb0}, []byte{0xc3, 0xa4}, []byte{0xc3, 0xb6}, []byte{0xc3, 0xbc},
|
||||
[]byte{0xc3, 0x9f},
|
||||
}
|
||||
teletextNationalSubsetItalian = &teletextNationalSubset{
|
||||
[]byte{0xc2, 0xa3}, []byte{0x24}, []byte{0xc3, 0xa9}, []byte{0xc2, 0xb0}, []byte{0xc3, 0xa7},
|
||||
[]byte{0xc2, 0xbb}, []byte{0x5e}, []byte{0x23}, []byte{0xc3, 0xb9}, []byte{0xc3, 0xa0}, []byte{0xc3, 0xb2},
|
||||
[]byte{0xc3, 0xa8}, []byte{0xc3, 0xac},
|
||||
}
|
||||
teletextNationalSubsetLettishLithuanian = &teletextNationalSubset{
|
||||
[]byte{0x23}, []byte{0x24}, []byte{0xc5, 0xa0}, []byte{0xc4, 0x97}, []byte{0xc4, 0x99}, []byte{0xc5, 0xbd},
|
||||
[]byte{0xc4, 0x8d}, []byte{0xc5, 0xab}, []byte{0xc5, 0xa1}, []byte{0xc4, 0x85}, []byte{0xc5, 0xb3},
|
||||
[]byte{0xc5, 0xbe}, []byte{0xc4, 0xaf},
|
||||
}
|
||||
teletextNationalSubsetPolish = &teletextNationalSubset{
|
||||
[]byte{0x23}, []byte{0xc5, 0x84}, []byte{0xc4, 0x85}, []byte{0xc5, 0xbb}, []byte{0xc5, 0x9a},
|
||||
[]byte{0xc5, 0x81}, []byte{0xc4, 0x87}, []byte{0xc3, 0xb3}, []byte{0xc4, 0x99}, []byte{0xc5, 0xbc},
|
||||
[]byte{0xc5, 0x9b}, []byte{0xc5, 0x82}, []byte{0xc5, 0xba},
|
||||
}
|
||||
teletextNationalSubsetPortugueseSpanish = &teletextNationalSubset{
|
||||
[]byte{0xc3, 0xa7}, []byte{0x24}, []byte{0xc2, 0xa1}, []byte{0xc3, 0xa1}, []byte{0xc3, 0xa9},
|
||||
[]byte{0xc3, 0xad}, []byte{0xc3, 0xb3}, []byte{0xc3, 0xba}, []byte{0xc2, 0xbf}, []byte{0xc3, 0xbc},
|
||||
[]byte{0xc3, 0xb1}, []byte{0xc3, 0xa8}, []byte{0xc3, 0xa0},
|
||||
}
|
||||
teletextNationalSubsetRomanian = &teletextNationalSubset{
|
||||
[]byte{0x23}, []byte{0xc2, 0xa4}, []byte{0xc5, 0xa2}, []byte{0xc3, 0x82}, []byte{0xc5, 0x9e},
|
||||
[]byte{0xc4, 0x82}, []byte{0xc3, 0x8e}, []byte{0xc4, 0xb1}, []byte{0xc5, 0xa3}, []byte{0xc3, 0xa2},
|
||||
[]byte{0xc5, 0x9f}, []byte{0xc4, 0x83}, []byte{0xc3, 0xae},
|
||||
}
|
||||
teletextNationalSubsetSerbianCroatianSlovenian = &teletextNationalSubset{
|
||||
[]byte{0x23}, []byte{0xc3, 0x8b}, []byte{0xc4, 0x8c}, []byte{0xc4, 0x86}, []byte{0xc5, 0xbd},
|
||||
[]byte{0xc4, 0x90}, []byte{0xc5, 0xa0}, []byte{0xc3, 0xab}, []byte{0xc4, 0x8d}, []byte{0xc4, 0x87},
|
||||
[]byte{0xc5, 0xbe}, []byte{0xc4, 0x91}, []byte{0xc5, 0xa1},
|
||||
}
|
||||
teletextNationalSubsetSwedishFinnishHungarian = &teletextNationalSubset{
|
||||
[]byte{0x23}, []byte{0xc2, 0xa4}, []byte{0xc3, 0x89}, []byte{0xc3, 0x84}, []byte{0xc3, 0x96},
|
||||
[]byte{0xc3, 0x85}, []byte{0xc3, 0x9c}, []byte{0x5f}, []byte{0xc3, 0xa9}, []byte{0xc3, 0xa4},
|
||||
[]byte{0xc3, 0xb6}, []byte{0xc3, 0xa5}, []byte{0xc3, 0xbc},
|
||||
}
|
||||
teletextNationalSubsetTurkish = &teletextNationalSubset{
|
||||
[]byte{0x54}, []byte{0xc4, 0x9f}, []byte{0xc4, 0xb0}, []byte{0xc5, 0x9e}, []byte{0xc3, 0x96},
|
||||
[]byte{0xc3, 0x87}, []byte{0xc3, 0x9c}, []byte{0xc4, 0x9e}, []byte{0xc4, 0xb1}, []byte{0xc5, 0x9f},
|
||||
[]byte{0xc3, 0xb6}, []byte{0xc3, 0xa7}, []byte{0xc3, 0xbc},
|
||||
}
|
||||
)
|
||||
|
||||
// Teletext PES data types
|
||||
const (
|
||||
teletextPESDataTypeEBU = "EBU"
|
||||
teletextPESDataTypeUnknown = "unknown"
|
||||
)
|
||||
|
||||
func teletextPESDataType(dataIdentifier uint8) string {
|
||||
switch {
|
||||
case dataIdentifier >= 0x10 && dataIdentifier <= 0x1f:
|
||||
return teletextPESDataTypeEBU
|
||||
}
|
||||
return teletextPESDataTypeUnknown
|
||||
}
|
||||
|
||||
// Teletext PES data unit ids
|
||||
const (
|
||||
teletextPESDataUnitIDEBUNonSubtitleData = 0x2
|
||||
teletextPESDataUnitIDEBUSubtitleData = 0x3
|
||||
teletextPESDataUnitIDStuffing = 0xff
|
||||
)
|
||||
|
||||
// TeletextOptions represents teletext options
|
||||
type TeletextOptions struct {
|
||||
Page int
|
||||
PID int
|
||||
}
|
||||
|
||||
// ReadFromTeletext parses a teletext content
|
||||
// http://www.etsi.org/deliver/etsi_en/300400_300499/300472/01.03.01_60/en_300472v010301p.pdf
|
||||
// http://www.etsi.org/deliver/etsi_i_ets/300700_300799/300706/01_60/ets_300706e01p.pdf
|
||||
// TODO Update README
|
||||
// TODO Add tests
|
||||
func ReadFromTeletext(r io.Reader, o TeletextOptions) (s *Subtitles, err error) {
|
||||
// Init
|
||||
s = &Subtitles{}
|
||||
var dmx = astits.NewDemuxer(context.Background(), r)
|
||||
|
||||
// Get the teletext PID
|
||||
var pid uint16
|
||||
if pid, err = teletextPID(dmx, o); err != nil {
|
||||
if err != ErrNoValidTeletextPID {
|
||||
err = fmt.Errorf("astisub: getting teletext PID failed: %w", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Create character decoder
|
||||
cd := newTeletextCharacterDecoder()
|
||||
|
||||
// Create page buffer
|
||||
b := newTeletextPageBuffer(o.Page, cd)
|
||||
|
||||
// Loop in data
|
||||
var firstTime, lastTime time.Time
|
||||
var d *astits.DemuxerData
|
||||
var ps []*teletextPage
|
||||
for {
|
||||
// Fetch next data
|
||||
if d, err = dmx.NextData(); err != nil {
|
||||
if err == astits.ErrNoMorePackets {
|
||||
err = nil
|
||||
break
|
||||
}
|
||||
err = fmt.Errorf("astisub: fetching next data failed: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
// We only parse PES data
|
||||
if d.PES == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// This data is not of interest to us
|
||||
if d.PID != pid || d.PES.Header.StreamID != astits.StreamIDPrivateStream1 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get time
|
||||
t := teletextDataTime(d)
|
||||
if t.IsZero() {
|
||||
continue
|
||||
}
|
||||
|
||||
// First and last time
|
||||
if firstTime.IsZero() || firstTime.After(t) {
|
||||
firstTime = t
|
||||
}
|
||||
if lastTime.IsZero() || lastTime.Before(t) {
|
||||
lastTime = t
|
||||
}
|
||||
|
||||
// Append pages
|
||||
ps = append(ps, b.process(d.PES, t)...)
|
||||
}
|
||||
|
||||
// Dump buffer
|
||||
ps = append(ps, b.dump(lastTime)...)
|
||||
|
||||
// Parse pages
|
||||
for _, p := range ps {
|
||||
p.parse(s, cd, firstTime)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// TODO Add tests
|
||||
func teletextDataTime(d *astits.DemuxerData) time.Time {
|
||||
if d.PES.Header != nil && d.PES.Header.OptionalHeader != nil && d.PES.Header.OptionalHeader.PTS != nil {
|
||||
return d.PES.Header.OptionalHeader.PTS.Time()
|
||||
} else if d.FirstPacket != nil && d.FirstPacket.AdaptationField != nil && d.FirstPacket.AdaptationField.PCR != nil {
|
||||
return d.FirstPacket.AdaptationField.PCR.Time()
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
// If the PID teletext option is not indicated, it will walk through the ts data until it reaches a PMT packet to
|
||||
// detect the first valid teletext PID
|
||||
// TODO Add tests
|
||||
func teletextPID(dmx *astits.Demuxer, o TeletextOptions) (pid uint16, err error) {
|
||||
// PID is in the options
|
||||
if o.PID > 0 {
|
||||
pid = uint16(o.PID)
|
||||
return
|
||||
}
|
||||
|
||||
// Loop in data
|
||||
var d *astits.DemuxerData
|
||||
for {
|
||||
// Fetch next data
|
||||
if d, err = dmx.NextData(); err != nil {
|
||||
if err == astits.ErrNoMorePackets {
|
||||
err = ErrNoValidTeletextPID
|
||||
return
|
||||
}
|
||||
err = fmt.Errorf("astisub: fetching next data failed: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
// PMT data
|
||||
if d.PMT != nil {
|
||||
// Retrieve valid teletext PIDs
|
||||
var pids []uint16
|
||||
for _, s := range d.PMT.ElementaryStreams {
|
||||
for _, dsc := range s.ElementaryStreamDescriptors {
|
||||
if dsc.Tag == astits.DescriptorTagTeletext || dsc.Tag == astits.DescriptorTagVBITeletext {
|
||||
pids = append(pids, s.ElementaryPID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No valid teletext PIDs
|
||||
if len(pids) == 0 {
|
||||
err = ErrNoValidTeletextPID
|
||||
return
|
||||
}
|
||||
|
||||
// Set pid
|
||||
pid = pids[0]
|
||||
log.Printf("astisub: no teletext pid specified, using pid %d", pid)
|
||||
|
||||
// Rewind
|
||||
if _, err = dmx.Rewind(); err != nil {
|
||||
err = fmt.Errorf("astisub: rewinding failed: %w", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type teletextPageBuffer struct {
|
||||
cd *teletextCharacterDecoder
|
||||
currentPage *teletextPage
|
||||
donePages []*teletextPage
|
||||
magazineNumber uint8
|
||||
pageNumber int
|
||||
receiving bool
|
||||
}
|
||||
|
||||
func newTeletextPageBuffer(page int, cd *teletextCharacterDecoder) *teletextPageBuffer {
|
||||
return &teletextPageBuffer{
|
||||
cd: cd,
|
||||
magazineNumber: uint8(page / 100),
|
||||
pageNumber: page % 100,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Add tests
|
||||
func (b *teletextPageBuffer) dump(lastTime time.Time) (ps []*teletextPage) {
|
||||
if b.currentPage != nil {
|
||||
b.currentPage.end = lastTime
|
||||
ps = []*teletextPage{b.currentPage}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// TODO Add tests
|
||||
func (b *teletextPageBuffer) process(d *astits.PESData, t time.Time) (ps []*teletextPage) {
|
||||
// Data identifier
|
||||
var offset int
|
||||
dataIdentifier := uint8(d.Data[offset])
|
||||
offset += 1
|
||||
|
||||
// Check data type
|
||||
if teletextPESDataType(dataIdentifier) != teletextPESDataTypeEBU {
|
||||
return
|
||||
}
|
||||
|
||||
// Loop through data units
|
||||
for offset < len(d.Data) {
|
||||
// ID
|
||||
id := uint8(d.Data[offset])
|
||||
offset += 1
|
||||
|
||||
// Length
|
||||
length := uint8(d.Data[offset])
|
||||
offset += 1
|
||||
|
||||
// Offset end
|
||||
offsetEnd := offset + int(length)
|
||||
if offsetEnd > len(d.Data) {
|
||||
break
|
||||
}
|
||||
|
||||
// Parse data unit
|
||||
b.parseDataUnit(d.Data[offset:offsetEnd], id, t)
|
||||
|
||||
// Seek to end of data unit
|
||||
offset = offsetEnd
|
||||
}
|
||||
|
||||
// Dump buffer
|
||||
ps = b.donePages
|
||||
b.donePages = []*teletextPage(nil)
|
||||
return ps
|
||||
}
|
||||
|
||||
// TODO Add tests
|
||||
func (b *teletextPageBuffer) parseDataUnit(i []byte, id uint8, t time.Time) {
|
||||
// Check id
|
||||
if id != teletextPESDataUnitIDEBUSubtitleData {
|
||||
return
|
||||
}
|
||||
|
||||
// Field parity: i[0]&0x20 > 0
|
||||
// Line offset: uint8(i[0] & 0x1f)
|
||||
// Framing code
|
||||
framingCode := uint8(i[1])
|
||||
|
||||
// Check framing code
|
||||
if framingCode != 0xe4 {
|
||||
return
|
||||
}
|
||||
|
||||
// Magazine number and packet number
|
||||
h1, ok := astikit.ByteHamming84Decode(i[2])
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
h2, ok := astikit.ByteHamming84Decode(i[3])
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
h := h2<<4 | h1
|
||||
magazineNumber := h & 0x7
|
||||
if magazineNumber == 0 {
|
||||
magazineNumber = 8
|
||||
}
|
||||
packetNumber := h >> 3
|
||||
|
||||
// Parse packet
|
||||
b.parsePacket(i[4:], magazineNumber, packetNumber, t)
|
||||
}
|
||||
|
||||
// TODO Add tests
|
||||
func (b *teletextPageBuffer) parsePacket(i []byte, magazineNumber, packetNumber uint8, t time.Time) {
|
||||
if packetNumber == 0 {
|
||||
b.parsePacketHeader(i, magazineNumber, t)
|
||||
} else if b.receiving && magazineNumber == b.magazineNumber && (packetNumber >= 1 && packetNumber <= 25) {
|
||||
b.parsePacketData(i, packetNumber)
|
||||
} else {
|
||||
// Designation code
|
||||
designationCode, ok := astikit.ByteHamming84Decode(i[0])
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Parse packet
|
||||
if b.receiving && magazineNumber == b.magazineNumber && packetNumber == 26 {
|
||||
// TODO Implement
|
||||
} else if b.receiving && magazineNumber == b.magazineNumber && packetNumber == 28 {
|
||||
b.parsePacket28And29(i[1:], packetNumber, designationCode)
|
||||
} else if magazineNumber == b.magazineNumber && packetNumber == 29 {
|
||||
b.parsePacket28And29(i[1:], packetNumber, designationCode)
|
||||
} else if magazineNumber == 8 && packetNumber == 30 {
|
||||
b.parsePacket30(i, designationCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Add tests
|
||||
func (b *teletextPageBuffer) parsePacketHeader(i []byte, magazineNumber uint8, t time.Time) (transmissionDone bool) {
|
||||
// Page number units
|
||||
pageNumberUnits, ok := astikit.ByteHamming84Decode(i[0])
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Page number tens
|
||||
pageNumberTens, ok := astikit.ByteHamming84Decode(i[1])
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
pageNumber := int(pageNumberTens)*10 + int(pageNumberUnits)
|
||||
|
||||
// 0xff is a reserved page number value
|
||||
if pageNumberTens == 0xf && pageNumberUnits == 0xf {
|
||||
return
|
||||
}
|
||||
|
||||
// Update magazine and page number
|
||||
if b.magazineNumber == 0 && b.pageNumber == 0 {
|
||||
// C6
|
||||
controlBits, ok := astikit.ByteHamming84Decode(i[5])
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
subtitleFlag := controlBits&0x8 > 0
|
||||
|
||||
// This is a subtitle page
|
||||
if subtitleFlag {
|
||||
b.magazineNumber = magazineNumber
|
||||
b.pageNumber = pageNumber
|
||||
log.Printf("astisub: no teletext page specified, using page %d%.2d", b.magazineNumber, b.pageNumber)
|
||||
}
|
||||
}
|
||||
|
||||
// C11 --> C14
|
||||
controlBits, ok := astikit.ByteHamming84Decode(i[7])
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
magazineSerial := controlBits&0x1 > 0
|
||||
charsetCode := controlBits >> 1
|
||||
|
||||
// Page transmission is done
|
||||
if b.receiving && ((magazineSerial && pageNumber != b.pageNumber) ||
|
||||
(!magazineSerial && pageNumber != b.pageNumber && magazineNumber == b.magazineNumber)) {
|
||||
b.receiving = false
|
||||
return
|
||||
}
|
||||
|
||||
// Invalid magazine or page number
|
||||
if pageNumber != b.pageNumber || magazineNumber != b.magazineNumber {
|
||||
return
|
||||
}
|
||||
|
||||
// Now that we know when the previous page ends we can add it to the done slice
|
||||
if b.currentPage != nil {
|
||||
b.currentPage.end = t
|
||||
b.donePages = append(b.donePages, b.currentPage)
|
||||
}
|
||||
|
||||
// Reset
|
||||
b.receiving = true
|
||||
b.currentPage = newTeletextPage(charsetCode, t)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO Add tests
|
||||
func (b *teletextPageBuffer) parsePacketData(i []byte, packetNumber uint8) {
|
||||
// Make sure the map is initialized
|
||||
if _, ok := b.currentPage.data[packetNumber]; !ok {
|
||||
b.currentPage.data[packetNumber] = make([]byte, 40)
|
||||
}
|
||||
|
||||
// Loop through input
|
||||
b.currentPage.rows = append(b.currentPage.rows, int(packetNumber))
|
||||
for idx := uint8(0); idx < 40; idx++ {
|
||||
v, ok := astikit.ByteParity(bits.Reverse8(i[idx]))
|
||||
if !ok {
|
||||
v = 0
|
||||
}
|
||||
b.currentPage.data[packetNumber][idx] = v
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Add tests
|
||||
func (b *teletextPageBuffer) parsePacket28And29(i []byte, packetNumber, designationCode uint8) {
|
||||
// Invalid designation code
|
||||
if designationCode != 0 && designationCode != 4 {
|
||||
return
|
||||
}
|
||||
|
||||
// Triplet 1
|
||||
// TODO triplet1 should be the results of hamming 24/18 decoding
|
||||
triplet1 := uint32(i[2])<<16 | uint32(i[1])<<8 | uint32(i[0])
|
||||
|
||||
// We only process x/28 format 1
|
||||
if packetNumber == 28 && triplet1&0xf > 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Update character decoder
|
||||
if packetNumber == 28 {
|
||||
b.cd.setTripletX28(triplet1)
|
||||
} else {
|
||||
b.cd.setTripletM29(triplet1)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Add tests
|
||||
func (b *teletextPageBuffer) parsePacket30(i []byte, designationCode uint8) {
|
||||
// Switch on designation code to determine format
|
||||
switch designationCode {
|
||||
case 0, 1:
|
||||
b.parsePacket30Format1(i)
|
||||
case 2, 3:
|
||||
b.parsePacket30Format2(i)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *teletextPageBuffer) parsePacket30Format1(i []byte) {
|
||||
// TODO Implement
|
||||
|
||||
}
|
||||
|
||||
func (b *teletextPageBuffer) parsePacket30Format2(i []byte) {
|
||||
// TODO Implement
|
||||
}
|
||||
|
||||
type teletextCharacterDecoder struct {
|
||||
c teletextCharset
|
||||
lastPageCharsetCode *uint8
|
||||
tripletM29 *uint32
|
||||
tripletX28 *uint32
|
||||
}
|
||||
|
||||
func newTeletextCharacterDecoder() *teletextCharacterDecoder {
|
||||
return &teletextCharacterDecoder{}
|
||||
}
|
||||
|
||||
// TODO Add tests
|
||||
func (d *teletextCharacterDecoder) setTripletM29(i uint32) {
|
||||
if *d.tripletM29 != i {
|
||||
d.tripletM29 = astikit.UInt32Ptr(i)
|
||||
d.updateCharset(d.lastPageCharsetCode, true)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Add tests
|
||||
func (d *teletextCharacterDecoder) setTripletX28(i uint32) {
|
||||
if *d.tripletX28 != i {
|
||||
d.tripletX28 = astikit.UInt32Ptr(i)
|
||||
d.updateCharset(d.lastPageCharsetCode, true)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Add tests
|
||||
func (d *teletextCharacterDecoder) decode(i byte) []byte {
|
||||
if i < 0x20 {
|
||||
return []byte{}
|
||||
}
|
||||
return d.c[i-0x20]
|
||||
}
|
||||
|
||||
// TODO Add tests
|
||||
func (d *teletextCharacterDecoder) updateCharset(pageCharsetCode *uint8, force bool) {
|
||||
// Charset is up to date
|
||||
if d.lastPageCharsetCode != nil && *pageCharsetCode == *d.lastPageCharsetCode && !force {
|
||||
return
|
||||
}
|
||||
d.lastPageCharsetCode = pageCharsetCode
|
||||
|
||||
// Get triplet
|
||||
var triplet uint32
|
||||
if d.tripletX28 != nil {
|
||||
triplet = *d.tripletX28
|
||||
} else if d.tripletM29 != nil {
|
||||
triplet = *d.tripletM29
|
||||
}
|
||||
|
||||
// Get charsets
|
||||
d.c = *teletextCharsetG0Latin
|
||||
var nationalOptionSubset *teletextNationalSubset
|
||||
if v1, ok := teletextCharsets[uint8((triplet&0x3f80)>>10)]; ok {
|
||||
if v2, ok := v1[*pageCharsetCode]; ok {
|
||||
d.c = *v2.g0
|
||||
nationalOptionSubset = v2.national
|
||||
}
|
||||
}
|
||||
|
||||
// Update g0 with national option subset
|
||||
if nationalOptionSubset != nil {
|
||||
for k, v := range nationalOptionSubset {
|
||||
d.c[teletextNationalSubsetCharactersPositionInG0[k]] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type teletextPage struct {
|
||||
charsetCode uint8
|
||||
data map[uint8][]byte
|
||||
end time.Time
|
||||
rows []int
|
||||
start time.Time
|
||||
}
|
||||
|
||||
func newTeletextPage(charsetCode uint8, start time.Time) *teletextPage {
|
||||
return &teletextPage{
|
||||
charsetCode: charsetCode,
|
||||
data: make(map[uint8][]byte),
|
||||
start: start,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *teletextPage) parse(s *Subtitles, d *teletextCharacterDecoder, firstTime time.Time) {
|
||||
// Update charset
|
||||
d.updateCharset(astikit.UInt8Ptr(p.charsetCode), false)
|
||||
|
||||
// No data
|
||||
if len(p.data) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Order rows
|
||||
sort.Ints(p.rows)
|
||||
|
||||
// Create item
|
||||
i := &Item{
|
||||
EndAt: p.end.Sub(firstTime),
|
||||
StartAt: p.start.Sub(firstTime),
|
||||
}
|
||||
|
||||
// Loop through rows
|
||||
for _, idxRow := range p.rows {
|
||||
parseTeletextRow(i, d, nil, p.data[uint8(idxRow)])
|
||||
}
|
||||
|
||||
// Append item
|
||||
s.Items = append(s.Items, i)
|
||||
}
|
||||
|
||||
type decoder interface {
|
||||
decode(i byte) []byte
|
||||
}
|
||||
|
||||
type styler interface {
|
||||
hasBeenSet() bool
|
||||
hasChanged(s *StyleAttributes) bool
|
||||
parseSpacingAttribute(i byte)
|
||||
propagateStyleAttributes(s *StyleAttributes)
|
||||
update(sa *StyleAttributes)
|
||||
}
|
||||
|
||||
func parseTeletextRow(i *Item, d decoder, fs func() styler, row []byte) {
|
||||
// Loop through columns
|
||||
var l = Line{}
|
||||
var li = LineItem{InlineStyle: &StyleAttributes{}}
|
||||
var started bool
|
||||
var s styler
|
||||
for _, v := range row {
|
||||
// Create specific styler
|
||||
if fs != nil {
|
||||
s = fs()
|
||||
}
|
||||
|
||||
// Get spacing attributes
|
||||
var color *Color
|
||||
var doubleHeight, doubleSize, doubleWidth *bool
|
||||
switch v {
|
||||
case 0x0:
|
||||
color = ColorBlack
|
||||
case 0x1:
|
||||
color = ColorRed
|
||||
case 0x2:
|
||||
color = ColorGreen
|
||||
case 0x3:
|
||||
color = ColorYellow
|
||||
case 0x4:
|
||||
color = ColorBlue
|
||||
case 0x5:
|
||||
color = ColorMagenta
|
||||
case 0x6:
|
||||
color = ColorCyan
|
||||
case 0x7:
|
||||
color = ColorWhite
|
||||
case 0xa:
|
||||
started = false
|
||||
case 0xb:
|
||||
started = true
|
||||
case 0xc:
|
||||
doubleHeight = astikit.BoolPtr(false)
|
||||
doubleSize = astikit.BoolPtr(false)
|
||||
doubleWidth = astikit.BoolPtr(false)
|
||||
case 0xd:
|
||||
doubleHeight = astikit.BoolPtr(true)
|
||||
case 0xe:
|
||||
doubleWidth = astikit.BoolPtr(true)
|
||||
case 0xf:
|
||||
doubleSize = astikit.BoolPtr(true)
|
||||
default:
|
||||
if s != nil {
|
||||
s.parseSpacingAttribute(v)
|
||||
}
|
||||
}
|
||||
|
||||
// Style has been set
|
||||
if color != nil || doubleHeight != nil || doubleSize != nil || doubleWidth != nil || (s != nil && s.hasBeenSet()) {
|
||||
// Style has changed
|
||||
if color != li.InlineStyle.TeletextColor || doubleHeight != li.InlineStyle.TeletextDoubleHeight ||
|
||||
doubleSize != li.InlineStyle.TeletextDoubleSize || doubleWidth != li.InlineStyle.TeletextDoubleWidth ||
|
||||
(s != nil && s.hasChanged(li.InlineStyle)) {
|
||||
// Line has started
|
||||
if started {
|
||||
// Append line item
|
||||
appendTeletextLineItem(&l, li, s)
|
||||
|
||||
// Create new line item
|
||||
sa := &StyleAttributes{}
|
||||
*sa = *li.InlineStyle
|
||||
li = LineItem{InlineStyle: sa}
|
||||
}
|
||||
|
||||
// Update style attributes
|
||||
if color != nil && color != li.InlineStyle.TeletextColor {
|
||||
li.InlineStyle.TeletextColor = color
|
||||
}
|
||||
if doubleHeight != nil && doubleHeight != li.InlineStyle.TeletextDoubleHeight {
|
||||
li.InlineStyle.TeletextDoubleHeight = doubleHeight
|
||||
}
|
||||
if doubleSize != nil && doubleSize != li.InlineStyle.TeletextDoubleSize {
|
||||
li.InlineStyle.TeletextDoubleSize = doubleSize
|
||||
}
|
||||
if doubleWidth != nil && doubleWidth != li.InlineStyle.TeletextDoubleWidth {
|
||||
li.InlineStyle.TeletextDoubleWidth = doubleWidth
|
||||
}
|
||||
if s != nil {
|
||||
s.update(li.InlineStyle)
|
||||
}
|
||||
}
|
||||
} else if started {
|
||||
// Append text
|
||||
li.Text += string(d.decode(v))
|
||||
}
|
||||
}
|
||||
|
||||
// Append line item
|
||||
appendTeletextLineItem(&l, li, s)
|
||||
|
||||
// Append line
|
||||
if len(l.Items) > 0 {
|
||||
i.Lines = append(i.Lines, l)
|
||||
}
|
||||
}
|
||||
|
||||
func appendTeletextLineItem(l *Line, li LineItem, s styler) {
|
||||
// There's some text
|
||||
if len(strings.TrimSpace(li.Text)) > 0 {
|
||||
// Make sure inline style exists
|
||||
if li.InlineStyle == nil {
|
||||
li.InlineStyle = &StyleAttributes{}
|
||||
}
|
||||
|
||||
// Get number of spaces before
|
||||
li.InlineStyle.TeletextSpacesBefore = astikit.IntPtr(0)
|
||||
for _, c := range li.Text {
|
||||
if c == ' ' {
|
||||
*li.InlineStyle.TeletextSpacesBefore++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Get number of spaces after
|
||||
li.InlineStyle.TeletextSpacesAfter = astikit.IntPtr(0)
|
||||
for idx := len(li.Text) - 1; idx >= 0; idx-- {
|
||||
if li.Text[idx] == ' ' {
|
||||
*li.InlineStyle.TeletextSpacesAfter++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Propagate style attributes
|
||||
li.InlineStyle.propagateTeletextAttributes()
|
||||
if s != nil {
|
||||
s.propagateStyleAttributes(li.InlineStyle)
|
||||
}
|
||||
|
||||
// Append line item
|
||||
li.Text = strings.TrimSpace(li.Text)
|
||||
l.Items = append(l.Items, li)
|
||||
}
|
||||
}
|
||||
686
vendor/github.com/asticode/go-astisub/ttml.go
generated
vendored
Normal file
686
vendor/github.com/asticode/go-astisub/ttml.go
generated
vendored
Normal file
@@ -0,0 +1,686 @@
|
||||
package astisub
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/asticode/go-astikit"
|
||||
)
|
||||
|
||||
// https://www.w3.org/TR/ttaf1-dfxp/
|
||||
// http://www.skynav.com:8080/ttv/check
|
||||
// https://www.speechpad.com/captions/ttml
|
||||
|
||||
// TTML languages
|
||||
const (
|
||||
ttmlLanguageChinese = "zh"
|
||||
ttmlLanguageEnglish = "en"
|
||||
ttmlLanguageJapanese = "ja"
|
||||
ttmlLanguageFrench = "fr"
|
||||
ttmlLanguageNorwegian = "no"
|
||||
)
|
||||
|
||||
// TTML language mapping
|
||||
var ttmlLanguageMapping = astikit.NewBiMap().
|
||||
Set(ttmlLanguageChinese, LanguageChinese).
|
||||
Set(ttmlLanguageEnglish, LanguageEnglish).
|
||||
Set(ttmlLanguageFrench, LanguageFrench).
|
||||
Set(ttmlLanguageJapanese, LanguageJapanese).
|
||||
Set(ttmlLanguageNorwegian, LanguageNorwegian)
|
||||
|
||||
// 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"`
|
||||
Tickrate int `xml:"tickRate,attr"`
|
||||
XMLName xml.Name `xml:"tt"`
|
||||
}
|
||||
|
||||
// metadata returns the Metadata of the TTML
|
||||
func (t TTMLIn) metadata() (m *Metadata) {
|
||||
m = &Metadata{
|
||||
Framerate: t.Framerate,
|
||||
Title: t.Metadata.Title,
|
||||
TTMLCopyright: t.Metadata.Copyright,
|
||||
}
|
||||
if v, ok := ttmlLanguageMapping.Get(astikit.StrPad(t.Lang, ' ', 2, astikit.PadCut)); ok {
|
||||
m.Language = v.(string)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 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 = fmt.Errorf("astisub: getting next token failed: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Start element
|
||||
if se, ok := t.(xml.StartElement); ok {
|
||||
var e = TTMLInItem{}
|
||||
if err = d.DecodeElement(&e, &se); err != nil {
|
||||
err = fmt.Errorf("astisub: decoding xml.StartElement failed: %w", err)
|
||||
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
|
||||
ticks, tickrate int // Tickrate is in ticks/s
|
||||
}
|
||||
|
||||
// UnmarshalText implements the TextUnmarshaler interface
|
||||
// Possible formats are:
|
||||
// - hh:mm:ss.mmm
|
||||
// - hh:mm:ss:fff (fff being frames)
|
||||
// - [ticks]t ([ticks] being the tick amount)
|
||||
func (d *TTMLInDuration) UnmarshalText(i []byte) (err error) {
|
||||
// Reset duration
|
||||
d.d = time.Duration(0)
|
||||
d.frames = 0
|
||||
d.ticks = 0
|
||||
|
||||
// Check offset time
|
||||
text := string(i)
|
||||
if matches := ttmlRegexpOffsetTime.FindStringSubmatch(text); matches != nil {
|
||||
// Parse value
|
||||
var value float64
|
||||
if value, err = strconv.ParseFloat(matches[1], 64); err != nil {
|
||||
err = fmt.Errorf("astisub: failed to parse value %s", matches[1])
|
||||
return
|
||||
}
|
||||
|
||||
// Parse metric
|
||||
metric := matches[3]
|
||||
|
||||
// Update duration
|
||||
if metric == "t" {
|
||||
d.ticks = int(value)
|
||||
} else if metric == "f" {
|
||||
d.frames = int(value)
|
||||
} else {
|
||||
// Get timebase
|
||||
var timebase time.Duration
|
||||
switch metric {
|
||||
case "h":
|
||||
timebase = time.Hour
|
||||
case "m":
|
||||
timebase = time.Minute
|
||||
case "s":
|
||||
timebase = time.Second
|
||||
case "ms":
|
||||
timebase = time.Millisecond
|
||||
default:
|
||||
err = fmt.Errorf("astisub: invalid metric %s", metric)
|
||||
return
|
||||
}
|
||||
|
||||
// Update duration
|
||||
d.d = time.Duration(value * float64(timebase.Nanoseconds()))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Extract clock time frames
|
||||
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 = fmt.Errorf("astisub: atoi %s failed: %w", s, err)
|
||||
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() (o time.Duration) {
|
||||
if d.ticks > 0 && d.tickrate > 0 {
|
||||
return time.Duration(float64(d.ticks) * 1e9 / float64(d.tickrate))
|
||||
}
|
||||
o = d.d
|
||||
if d.frames > 0 && d.framerate > 0 {
|
||||
o += time.Duration(float64(d.frames) / float64(d.framerate) * float64(time.Second.Nanoseconds()))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 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 = fmt.Errorf("astisub: xml decoding failed: %w", err)
|
||||
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.Begin.tickrate = ttml.Tickrate
|
||||
ts.End.framerate = ttml.Framerate
|
||||
ts.End.tickrate = ttml.Tickrate
|
||||
|
||||
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 = fmt.Errorf("astisub: unmarshaling items failed: %w", err)
|
||||
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 {
|
||||
if v, ok := ttmlLanguageMapping.GetInverse(s.Metadata.Language); ok {
|
||||
ttml.Lang = v.(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 idx, lineItem := range line.Items {
|
||||
// Init ttml item
|
||||
var ttmlItem = TTMLOutItem{
|
||||
Text: lineItem.Text,
|
||||
TTMLOutStyleAttributes: ttmlOutStyleAttributesFromStyleAttributes(lineItem.InlineStyle),
|
||||
XMLName: xml.Name{Local: "span"},
|
||||
}
|
||||
// condition to avoid adding space as the last character.
|
||||
if idx < len(line.Items)-1 {
|
||||
ttmlItem.Text = ttmlItem.Text + " "
|
||||
}
|
||||
|
||||
// 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 = fmt.Errorf("astisub: xml encoding failed: %w", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
537
vendor/github.com/asticode/go-astisub/webvtt.go
generated
vendored
Normal file
537
vendor/github.com/asticode/go-astisub/webvtt.go
generated
vendored
Normal file
@@ -0,0 +1,537 @@
|
||||
package astisub
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
// https://www.w3.org/TR/webvtt1/
|
||||
|
||||
// Constants
|
||||
const (
|
||||
webvttBlockNameComment = "comment"
|
||||
webvttBlockNameRegion = "region"
|
||||
webvttBlockNameStyle = "style"
|
||||
webvttBlockNameText = "text"
|
||||
webvttTimeBoundariesSeparator = " --> "
|
||||
webvttTimestampMap = "X-TIMESTAMP-MAP"
|
||||
)
|
||||
|
||||
// Vars
|
||||
var (
|
||||
bytesWebVTTItalicEndTag = []byte("</i>")
|
||||
bytesWebVTTItalicStartTag = []byte("<i>")
|
||||
bytesWebVTTTimeBoundariesSeparator = []byte(webvttTimeBoundariesSeparator)
|
||||
webVTTRegexpStartTag = regexp.MustCompile(`(<v([\.\w]*)([\s\w]+)+>)`)
|
||||
)
|
||||
|
||||
// parseDurationWebVTT parses a .vtt duration
|
||||
func parseDurationWebVTT(i string) (time.Duration, error) {
|
||||
return parseDuration(i, ".", 3)
|
||||
}
|
||||
|
||||
// https://tools.ietf.org/html/rfc8216#section-3.5
|
||||
// Eg., `X-TIMESTAMP-MAP=LOCAL:00:00:00.000,MPEGTS:900000` => 10s
|
||||
// `X-TIMESTAMP-MAP=LOCAL:00:00:00.000,MPEGTS:180000` => 2s
|
||||
func parseTimestampMapWebVTT(line string) (timeOffset time.Duration, err error) {
|
||||
splits := strings.Split(line, "=")
|
||||
if len(splits) <= 1 {
|
||||
err = fmt.Errorf("astisub: invalid X-TIMESTAMP-MAP, no '=' found")
|
||||
return
|
||||
}
|
||||
right := splits[1]
|
||||
|
||||
var local time.Duration
|
||||
var mpegts int64
|
||||
for _, split := range strings.Split(right, ",") {
|
||||
splits := strings.SplitN(split, ":", 2)
|
||||
if len(splits) <= 1 {
|
||||
err = fmt.Errorf("astisub: invalid X-TIMESTAMP-MAP, part %q didn't contain ':'", right)
|
||||
return
|
||||
}
|
||||
|
||||
switch strings.ToLower(strings.TrimSpace(splits[0])) {
|
||||
case "local":
|
||||
local, err = parseDurationWebVTT(splits[1])
|
||||
if err != nil {
|
||||
err = fmt.Errorf("astisub: parsing webvtt duration failed: %w", err)
|
||||
return
|
||||
}
|
||||
case "mpegts":
|
||||
mpegts, err = strconv.ParseInt(splits[1], 10, 0)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("astisub: parsing int %s failed: %w", splits[1], err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
timeOffset = time.Duration(mpegts)*time.Second/90000 - local
|
||||
return
|
||||
}
|
||||
|
||||
// ReadFromWebVTT parses a .vtt content
|
||||
// TODO Tags (u, i, b)
|
||||
// TODO Class
|
||||
func ReadFromWebVTT(i io.Reader) (o *Subtitles, err error) {
|
||||
// Init
|
||||
o = NewSubtitles()
|
||||
var scanner = bufio.NewScanner(i)
|
||||
var line string
|
||||
var lineNum int
|
||||
|
||||
// Skip the header
|
||||
for scanner.Scan() {
|
||||
lineNum++
|
||||
line = scanner.Text()
|
||||
line = strings.TrimPrefix(line, string(BytesBOM))
|
||||
if fs := strings.Fields(line); len(fs) > 0 && fs[0] == "WEBVTT" {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Scan
|
||||
var item = &Item{}
|
||||
var blockName string
|
||||
var comments []string
|
||||
var index int
|
||||
var timeOffset time.Duration
|
||||
|
||||
for scanner.Scan() {
|
||||
// Fetch line
|
||||
line = strings.TrimSpace(scanner.Text())
|
||||
lineNum++
|
||||
|
||||
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: line %d: Invalid region style %s", lineNum, 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 = fmt.Errorf("atoi of %s failed: %w", split[1], err)
|
||||
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,
|
||||
Index: index,
|
||||
InlineStyle: &StyleAttributes{},
|
||||
}
|
||||
|
||||
// Reset index
|
||||
index = 0
|
||||
|
||||
// Split line on time boundaries
|
||||
var left = strings.Split(line, webvttTimeBoundariesSeparator)
|
||||
|
||||
// Split line on space to get remaining of time data
|
||||
var right = strings.Split(left[1], " ")
|
||||
|
||||
// Parse time boundaries
|
||||
if item.StartAt, err = parseDurationWebVTT(left[0]); err != nil {
|
||||
err = fmt.Errorf("astisub: line %d: parsing webvtt duration %s failed: %w", lineNum, left[0], err)
|
||||
return
|
||||
}
|
||||
if item.EndAt, err = parseDurationWebVTT(right[0]); err != nil {
|
||||
err = fmt.Errorf("astisub: line %d: parsing webvtt duration %s failed: %w", lineNum, right[0], err)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse style
|
||||
if len(right) > 1 {
|
||||
// Add styles
|
||||
for index := 1; index < len(right); index++ {
|
||||
// Empty
|
||||
if right[index] == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Split line on ":"
|
||||
var split = strings.Split(right[index], ":")
|
||||
if len(split) <= 1 {
|
||||
err = fmt.Errorf("astisub: line %d: Invalid inline style '%s'", lineNum, right[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: line %d: Unknown region %s", lineNum, 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)
|
||||
|
||||
case strings.HasPrefix(line, webvttTimestampMap):
|
||||
if len(item.Lines) > 0 {
|
||||
err = errors.New("astisub: found timestamp map after processing subtitle items")
|
||||
return
|
||||
}
|
||||
|
||||
timeOffset, err = parseTimestampMapWebVTT(line)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("astisub: parsing webvtt timestamp map failed: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Text
|
||||
default:
|
||||
// Switch on block name
|
||||
switch blockName {
|
||||
case webvttBlockNameComment:
|
||||
comments = append(comments, line)
|
||||
case webvttBlockNameStyle:
|
||||
// TODO Do something with the style
|
||||
case webvttBlockNameText:
|
||||
// Parse line
|
||||
if l := parseTextWebVTT(line); len(l.Items) > 0 {
|
||||
item.Lines = append(item.Lines, l)
|
||||
}
|
||||
default:
|
||||
// This is the ID
|
||||
index, _ = strconv.Atoi(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if timeOffset > 0 {
|
||||
o.Add(timeOffset)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// parseTextWebVTT parses the input line to fill the Line
|
||||
func parseTextWebVTT(i string) (o Line) {
|
||||
// Create tokenizer
|
||||
tr := html.NewTokenizer(strings.NewReader(i))
|
||||
|
||||
// Loop
|
||||
italic := false
|
||||
for {
|
||||
// Get next tag
|
||||
t := tr.Next()
|
||||
|
||||
// Process error
|
||||
if err := tr.Err(); err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
switch t {
|
||||
case html.EndTagToken:
|
||||
// Parse italic
|
||||
if bytes.Equal(tr.Raw(), bytesWebVTTItalicEndTag) {
|
||||
italic = false
|
||||
continue
|
||||
}
|
||||
case html.StartTagToken:
|
||||
// Parse voice name
|
||||
if matches := webVTTRegexpStartTag.FindStringSubmatch(string(tr.Raw())); len(matches) > 3 {
|
||||
if s := strings.TrimSpace(matches[3]); s != "" {
|
||||
o.VoiceName = s
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse italic
|
||||
if bytes.Equal(tr.Raw(), bytesWebVTTItalicStartTag) {
|
||||
italic = true
|
||||
continue
|
||||
}
|
||||
case html.TextToken:
|
||||
if s := strings.TrimSpace(string(tr.Raw())); s != "" {
|
||||
// Get style attribute
|
||||
var sa *StyleAttributes
|
||||
if italic {
|
||||
sa = &StyleAttributes{
|
||||
WebVTTItalics: italic,
|
||||
}
|
||||
sa.propagateWebVTTAttributes()
|
||||
}
|
||||
|
||||
// Append item
|
||||
o.Items = append(o.Items, LineItem{
|
||||
InlineStyle: sa,
|
||||
Text: s,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
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))...)
|
||||
} else if s.Regions[id].Style != nil && s.Regions[id].Style.InlineStyle != nil && s.Regions[id].Style.InlineStyle.WebVTTLines != 0 {
|
||||
c = append(c, bytesSpace...)
|
||||
c = append(c, []byte("lines="+strconv.Itoa(s.Regions[id].Style.InlineStyle.WebVTTLines))...)
|
||||
}
|
||||
if s.Regions[id].InlineStyle.WebVTTRegionAnchor != "" {
|
||||
c = append(c, bytesSpace...)
|
||||
c = append(c, []byte("regionanchor="+s.Regions[id].InlineStyle.WebVTTRegionAnchor)...)
|
||||
} else if s.Regions[id].Style != nil && s.Regions[id].Style.InlineStyle != nil && s.Regions[id].Style.InlineStyle.WebVTTRegionAnchor != "" {
|
||||
c = append(c, bytesSpace...)
|
||||
c = append(c, []byte("regionanchor="+s.Regions[id].Style.InlineStyle.WebVTTRegionAnchor)...)
|
||||
}
|
||||
if s.Regions[id].InlineStyle.WebVTTScroll != "" {
|
||||
c = append(c, bytesSpace...)
|
||||
c = append(c, []byte("scroll="+s.Regions[id].InlineStyle.WebVTTScroll)...)
|
||||
} else if s.Regions[id].Style != nil && s.Regions[id].Style.InlineStyle != nil && s.Regions[id].Style.InlineStyle.WebVTTScroll != "" {
|
||||
c = append(c, bytesSpace...)
|
||||
c = append(c, []byte("scroll="+s.Regions[id].Style.InlineStyle.WebVTTScroll)...)
|
||||
}
|
||||
if s.Regions[id].InlineStyle.WebVTTViewportAnchor != "" {
|
||||
c = append(c, bytesSpace...)
|
||||
c = append(c, []byte("viewportanchor="+s.Regions[id].InlineStyle.WebVTTViewportAnchor)...)
|
||||
} else if s.Regions[id].Style != nil && s.Regions[id].Style.InlineStyle != nil && s.Regions[id].Style.InlineStyle.WebVTTViewportAnchor != "" {
|
||||
c = append(c, bytesSpace...)
|
||||
c = append(c, []byte("viewportanchor="+s.Regions[id].Style.InlineStyle.WebVTTViewportAnchor)...)
|
||||
}
|
||||
if s.Regions[id].InlineStyle.WebVTTWidth != "" {
|
||||
c = append(c, bytesSpace...)
|
||||
c = append(c, []byte("width="+s.Regions[id].InlineStyle.WebVTTWidth)...)
|
||||
} else if s.Regions[id].Style != nil && s.Regions[id].Style.InlineStyle != nil && s.Regions[id].Style.InlineStyle.WebVTTWidth != "" {
|
||||
c = append(c, bytesSpace...)
|
||||
c = append(c, []byte("width="+s.Regions[id].Style.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)...)
|
||||
} else if item.Style != nil && item.Style.InlineStyle != nil && item.Style.InlineStyle.WebVTTAlign != "" {
|
||||
c = append(c, bytesSpace...)
|
||||
c = append(c, []byte("align:"+item.Style.InlineStyle.WebVTTAlign)...)
|
||||
}
|
||||
if item.InlineStyle.WebVTTLine != "" {
|
||||
c = append(c, bytesSpace...)
|
||||
c = append(c, []byte("line:"+item.InlineStyle.WebVTTLine)...)
|
||||
} else if item.Style != nil && item.Style.InlineStyle != nil && item.Style.InlineStyle.WebVTTLine != "" {
|
||||
c = append(c, bytesSpace...)
|
||||
c = append(c, []byte("line:"+item.Style.InlineStyle.WebVTTLine)...)
|
||||
}
|
||||
if item.InlineStyle.WebVTTPosition != "" {
|
||||
c = append(c, bytesSpace...)
|
||||
c = append(c, []byte("position:"+item.InlineStyle.WebVTTPosition)...)
|
||||
} else if item.Style != nil && item.Style.InlineStyle != nil && item.Style.InlineStyle.WebVTTPosition != "" {
|
||||
c = append(c, bytesSpace...)
|
||||
c = append(c, []byte("position:"+item.Style.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)...)
|
||||
} else if item.Style != nil && item.Style.InlineStyle != nil && item.Style.InlineStyle.WebVTTSize != "" {
|
||||
c = append(c, bytesSpace...)
|
||||
c = append(c, []byte("size:"+item.Style.InlineStyle.WebVTTSize)...)
|
||||
}
|
||||
if item.InlineStyle.WebVTTVertical != "" {
|
||||
c = append(c, bytesSpace...)
|
||||
c = append(c, []byte("vertical:"+item.InlineStyle.WebVTTVertical)...)
|
||||
} else if item.Style != nil && item.Style.InlineStyle != nil && item.Style.InlineStyle.WebVTTVertical != "" {
|
||||
c = append(c, bytesSpace...)
|
||||
c = append(c, []byte("vertical:"+item.Style.InlineStyle.WebVTTVertical)...)
|
||||
}
|
||||
}
|
||||
|
||||
// Add new line
|
||||
c = append(c, bytesLineSeparator...)
|
||||
|
||||
// Loop through lines
|
||||
for _, l := range item.Lines {
|
||||
c = append(c, l.webVTTBytes()...)
|
||||
}
|
||||
|
||||
// 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 = fmt.Errorf("astisub: writing failed: %w", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (l Line) webVTTBytes() (c []byte) {
|
||||
if l.VoiceName != "" {
|
||||
c = append(c, []byte("<v "+l.VoiceName+">")...)
|
||||
}
|
||||
for idx, li := range l.Items {
|
||||
c = append(c, li.webVTTBytes()...)
|
||||
// condition to avoid adding space as the last character.
|
||||
if idx < len(l.Items)-1 {
|
||||
c = append(c, []byte(" ")...)
|
||||
}
|
||||
}
|
||||
c = append(c, bytesLineSeparator...)
|
||||
return
|
||||
}
|
||||
|
||||
func (li LineItem) webVTTBytes() (c []byte) {
|
||||
// Get color
|
||||
var color string
|
||||
if li.InlineStyle != nil && li.InlineStyle.TTMLColor != nil {
|
||||
color = cssColor(*li.InlineStyle.TTMLColor)
|
||||
}
|
||||
|
||||
// Get italics
|
||||
i := li.InlineStyle != nil && li.InlineStyle.WebVTTItalics
|
||||
|
||||
// Append
|
||||
if color != "" {
|
||||
c = append(c, []byte("<c."+color+">")...)
|
||||
}
|
||||
if i {
|
||||
c = append(c, []byte("<i>")...)
|
||||
}
|
||||
c = append(c, []byte(li.Text)...)
|
||||
if i {
|
||||
c = append(c, []byte("</i>")...)
|
||||
}
|
||||
if color != "" {
|
||||
c = append(c, []byte("</c>")...)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func cssColor(rgb string) string {
|
||||
colors := map[string]string{
|
||||
"#00ffff": "cyan", // narrator, thought
|
||||
"#ffff00": "yellow", // out of vision
|
||||
"#ff0000": "red", // noises
|
||||
"#ff00ff": "magenta", // song
|
||||
"#00ff00": "lime", // foreign speak
|
||||
}
|
||||
return colors[strings.ToLower(rgb)] // returning the empty string is ok
|
||||
}
|
||||
Reference in New Issue
Block a user