YTSFlix_Go/vendor/github.com/asticode/go-astisub/subtitles.go

703 lines
18 KiB
Go
Raw Normal View History

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