1255 lines
35 KiB
Go
1255 lines
35 KiB
Go
|
package astisub
|
||
|
|
||
|
import (
|
||
|
"bufio"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"regexp"
|
||
|
"sort"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
"time"
|
||
|
|
||
|
"github.com/asticode/go-astitools/ptr"
|
||
|
"github.com/pkg/errors"
|
||
|
)
|
||
|
|
||
|
// https://www.matroska.org/technical/specs/subtitles/ssa.html
|
||
|
// http://moodub.free.fr/video/ass-specs.doc
|
||
|
// https://en.wikipedia.org/wiki/SubStation_Alpha
|
||
|
|
||
|
// SSA alignment
|
||
|
const (
|
||
|
ssaAlignmentCentered = 2
|
||
|
ssaAlignmentLeft = 1
|
||
|
ssaAlignmentLeftJustifiedTopTitle = 5
|
||
|
ssaAlignmentMidTitle = 8
|
||
|
ssaAlignmentRight = 3
|
||
|
ssaAlignmentTopTitle = 4
|
||
|
)
|
||
|
|
||
|
// SSA border styles
|
||
|
const (
|
||
|
ssaBorderStyleOpaqueBox = 3
|
||
|
ssaBorderStyleOutlineAndDropShadow = 1
|
||
|
)
|
||
|
|
||
|
// SSA collisions
|
||
|
const (
|
||
|
ssaCollisionsNormal = "Normal"
|
||
|
ssaCollisionsReverse = "Reverse"
|
||
|
)
|
||
|
|
||
|
// SSA event categories
|
||
|
const (
|
||
|
ssaEventCategoryCommand = "Command"
|
||
|
ssaEventCategoryComment = "Comment"
|
||
|
ssaEventCategoryDialogue = "Dialogue"
|
||
|
ssaEventCategoryMovie = "Movie"
|
||
|
ssaEventCategoryPicture = "Picture"
|
||
|
ssaEventCategorySound = "Sound"
|
||
|
)
|
||
|
|
||
|
// SSA event format names
|
||
|
const (
|
||
|
ssaEventFormatNameEffect = "Effect"
|
||
|
ssaEventFormatNameEnd = "End"
|
||
|
ssaEventFormatNameLayer = "Layer"
|
||
|
ssaEventFormatNameMarginL = "MarginL"
|
||
|
ssaEventFormatNameMarginR = "MarginR"
|
||
|
ssaEventFormatNameMarginV = "MarginV"
|
||
|
ssaEventFormatNameMarked = "Marked"
|
||
|
ssaEventFormatNameName = "Name"
|
||
|
ssaEventFormatNameStart = "Start"
|
||
|
ssaEventFormatNameStyle = "Style"
|
||
|
ssaEventFormatNameText = "Text"
|
||
|
)
|
||
|
|
||
|
// SSA script info names
|
||
|
const (
|
||
|
ssaScriptInfoNameCollisions = "Collisions"
|
||
|
ssaScriptInfoNameOriginalEditing = "Original Editing"
|
||
|
ssaScriptInfoNameOriginalScript = "Original Script"
|
||
|
ssaScriptInfoNameOriginalTiming = "Original Timing"
|
||
|
ssaScriptInfoNameOriginalTranslation = "Original Translation"
|
||
|
ssaScriptInfoNamePlayDepth = "PlayDepth"
|
||
|
ssaScriptInfoNamePlayResX = "PlayResX"
|
||
|
ssaScriptInfoNamePlayResY = "PlayResY"
|
||
|
ssaScriptInfoNameScriptType = "ScriptType"
|
||
|
ssaScriptInfoNameScriptUpdatedBy = "Script Updated By"
|
||
|
ssaScriptInfoNameSynchPoint = "Synch Point"
|
||
|
ssaScriptInfoNameTimer = "Timer"
|
||
|
ssaScriptInfoNameTitle = "Title"
|
||
|
ssaScriptInfoNameUpdateDetails = "Update Details"
|
||
|
ssaScriptInfoNameWrapStyle = "WrapStyle"
|
||
|
)
|
||
|
|
||
|
// SSA section names
|
||
|
const (
|
||
|
ssaSectionNameEvents = "events"
|
||
|
ssaSectionNameScriptInfo = "script.info"
|
||
|
ssaSectionNameStyles = "styles"
|
||
|
ssaSectionNameUnknown = "unknown"
|
||
|
)
|
||
|
|
||
|
// SSA style format names
|
||
|
const (
|
||
|
ssaStyleFormatNameAlignment = "Alignment"
|
||
|
ssaStyleFormatNameAlphaLevel = "AlphaLevel"
|
||
|
ssaStyleFormatNameAngle = "Angle"
|
||
|
ssaStyleFormatNameBackColour = "BackColour"
|
||
|
ssaStyleFormatNameBold = "Bold"
|
||
|
ssaStyleFormatNameBorderStyle = "BorderStyle"
|
||
|
ssaStyleFormatNameEncoding = "Encoding"
|
||
|
ssaStyleFormatNameFontName = "Fontname"
|
||
|
ssaStyleFormatNameFontSize = "Fontsize"
|
||
|
ssaStyleFormatNameItalic = "Italic"
|
||
|
ssaStyleFormatNameMarginL = "MarginL"
|
||
|
ssaStyleFormatNameMarginR = "MarginR"
|
||
|
ssaStyleFormatNameMarginV = "MarginV"
|
||
|
ssaStyleFormatNameName = "Name"
|
||
|
ssaStyleFormatNameOutline = "Outline"
|
||
|
ssaStyleFormatNameOutlineColour = "OutlineColour"
|
||
|
ssaStyleFormatNamePrimaryColour = "PrimaryColour"
|
||
|
ssaStyleFormatNameScaleX = "ScaleX"
|
||
|
ssaStyleFormatNameScaleY = "ScaleY"
|
||
|
ssaStyleFormatNameSecondaryColour = "SecondaryColour"
|
||
|
ssaStyleFormatNameShadow = "Shadow"
|
||
|
ssaStyleFormatNameSpacing = "Spacing"
|
||
|
ssaStyleFormatNameStrikeout = "Strikeout"
|
||
|
ssaStyleFormatNameTertiaryColour = "TertiaryColour"
|
||
|
ssaStyleFormatNameUnderline = "Underline"
|
||
|
)
|
||
|
|
||
|
// SSA wrap style
|
||
|
const (
|
||
|
ssaWrapStyleEndOfLineWordWrapping = "1"
|
||
|
ssaWrapStyleNoWordWrapping = "2"
|
||
|
ssaWrapStyleSmartWrapping = "0"
|
||
|
ssaWrapStyleSmartWrappingWithLowerLinesGettingWider = "3"
|
||
|
)
|
||
|
|
||
|
// SSA regexp
|
||
|
var ssaRegexpEffect = regexp.MustCompile("\\{[^\\{]+\\}")
|
||
|
|
||
|
// ReadFromSSA parses an .ssa content
|
||
|
func ReadFromSSA(i io.Reader) (o *Subtitles, err error) {
|
||
|
// Init
|
||
|
o = NewSubtitles()
|
||
|
var scanner = bufio.NewScanner(i)
|
||
|
var si = &ssaScriptInfo{}
|
||
|
var ss = []*ssaStyle{}
|
||
|
var es = []*ssaEvent{}
|
||
|
|
||
|
// Scan
|
||
|
var line, sectionName string
|
||
|
var format map[int]string
|
||
|
for scanner.Scan() {
|
||
|
// Fetch line
|
||
|
line = strings.TrimSpace(scanner.Text())
|
||
|
|
||
|
// Empty line
|
||
|
if len(line) == 0 {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
// Section name
|
||
|
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
||
|
switch strings.ToLower(line[1 : len(line)-1]) {
|
||
|
case "events":
|
||
|
sectionName = ssaSectionNameEvents
|
||
|
format = make(map[int]string)
|
||
|
continue
|
||
|
case "script info":
|
||
|
sectionName = ssaSectionNameScriptInfo
|
||
|
continue
|
||
|
case "v4 styles", "v4+ styles", "v4 styles+":
|
||
|
sectionName = ssaSectionNameStyles
|
||
|
format = make(map[int]string)
|
||
|
continue
|
||
|
default:
|
||
|
sectionName = ssaSectionNameUnknown
|
||
|
continue
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Unknown section
|
||
|
if sectionName == ssaSectionNameUnknown {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
// Comment
|
||
|
if len(line) > 0 && line[0] == ';' {
|
||
|
si.comments = append(si.comments, strings.TrimSpace(line[1:]))
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
// Split on ":"
|
||
|
var split = strings.Split(line, ":")
|
||
|
if len(split) < 2 {
|
||
|
err = fmt.Errorf("astisub: line '%s' should contain at least one ':'", line)
|
||
|
return
|
||
|
}
|
||
|
var header = strings.TrimSpace(split[0])
|
||
|
var content = strings.TrimSpace(strings.Join(split[1:], ":"))
|
||
|
|
||
|
// Switch on section name
|
||
|
switch sectionName {
|
||
|
case ssaSectionNameScriptInfo:
|
||
|
if err = si.parse(header, content); err != nil {
|
||
|
err = errors.Wrap(err, "astisub: parsing script info block failed")
|
||
|
return
|
||
|
}
|
||
|
case ssaSectionNameEvents, ssaSectionNameStyles:
|
||
|
// Parse format
|
||
|
if header == "Format" {
|
||
|
for idx, item := range strings.Split(content, ",") {
|
||
|
format[idx] = strings.TrimSpace(item)
|
||
|
}
|
||
|
} else {
|
||
|
// No format provided
|
||
|
if len(format) == 0 {
|
||
|
err = fmt.Errorf("astisub: no %s format provided", sectionName)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Switch on section name
|
||
|
switch sectionName {
|
||
|
case ssaSectionNameEvents:
|
||
|
var e *ssaEvent
|
||
|
if e, err = newSSAEventFromString(header, content, format); err != nil {
|
||
|
err = errors.Wrap(err, "astisub: building new ssa event failed")
|
||
|
return
|
||
|
}
|
||
|
es = append(es, e)
|
||
|
case ssaSectionNameStyles:
|
||
|
var s *ssaStyle
|
||
|
if s, err = newSSAStyleFromString(content, format); err != nil {
|
||
|
err = errors.Wrap(err, "astisub: building new ssa style failed")
|
||
|
return
|
||
|
}
|
||
|
ss = append(ss, s)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Set metadata
|
||
|
o.Metadata = si.metadata()
|
||
|
|
||
|
// Loop through styles
|
||
|
for _, s := range ss {
|
||
|
var st = s.style()
|
||
|
o.Styles[st.ID] = st
|
||
|
}
|
||
|
|
||
|
// Loop through events
|
||
|
for _, e := range es {
|
||
|
// Only process dialogues
|
||
|
if e.category == ssaEventCategoryDialogue {
|
||
|
// Build item
|
||
|
var item *Item
|
||
|
if item, err = e.item(o.Styles); err != nil {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Append item
|
||
|
o.Items = append(o.Items, item)
|
||
|
}
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// newColorFromSSAColor builds a new color based on an SSA color
|
||
|
func newColorFromSSAColor(i string) (_ *Color, _ error) {
|
||
|
// Empty
|
||
|
if len(i) == 0 {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Check whether input is decimal or hexadecimal
|
||
|
var s = i
|
||
|
var base = 10
|
||
|
if strings.HasPrefix(i, "&H") {
|
||
|
s = i[2:]
|
||
|
base = 16
|
||
|
}
|
||
|
return newColorFromSSAString(s, base)
|
||
|
}
|
||
|
|
||
|
// newSSAColorFromColor builds a new SSA color based on a color
|
||
|
func newSSAColorFromColor(i *Color) string {
|
||
|
return "&H" + i.SSAString()
|
||
|
}
|
||
|
|
||
|
// ssaScriptInfo represents an SSA script info block
|
||
|
type ssaScriptInfo struct {
|
||
|
collisions string
|
||
|
comments []string
|
||
|
originalEditing string
|
||
|
originalScript string
|
||
|
originalTiming string
|
||
|
originalTranslation string
|
||
|
playDepth *int
|
||
|
playResX, playResY *int
|
||
|
scriptType string
|
||
|
scriptUpdatedBy string
|
||
|
synchPoint string
|
||
|
timer *float64
|
||
|
title string
|
||
|
updateDetails string
|
||
|
wrapStyle string
|
||
|
}
|
||
|
|
||
|
// newSSAScriptInfo builds an SSA script info block based on metadata
|
||
|
func newSSAScriptInfo(m *Metadata) (o *ssaScriptInfo) {
|
||
|
// Init
|
||
|
o = &ssaScriptInfo{}
|
||
|
|
||
|
// Add metadata
|
||
|
if m != nil {
|
||
|
o.collisions = m.SSACollisions
|
||
|
o.comments = m.Comments
|
||
|
o.originalEditing = m.SSAOriginalEditing
|
||
|
o.originalScript = m.SSAOriginalScript
|
||
|
o.originalTiming = m.SSAOriginalTiming
|
||
|
o.originalTranslation = m.SSAOriginalTranslation
|
||
|
o.playDepth = m.SSAPlayDepth
|
||
|
o.playResX = m.SSAPlayResX
|
||
|
o.playResY = m.SSAPlayResY
|
||
|
o.scriptType = m.SSAScriptType
|
||
|
o.scriptUpdatedBy = m.SSAScriptUpdatedBy
|
||
|
o.synchPoint = m.SSASynchPoint
|
||
|
o.timer = m.SSATimer
|
||
|
o.title = m.Title
|
||
|
o.updateDetails = m.SSAUpdateDetails
|
||
|
o.wrapStyle = m.SSAWrapStyle
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// parse parses a script info header/content
|
||
|
func (b *ssaScriptInfo) parse(header, content string) (err error) {
|
||
|
switch header {
|
||
|
case ssaScriptInfoNameCollisions:
|
||
|
b.collisions = content
|
||
|
case ssaScriptInfoNameOriginalEditing:
|
||
|
b.originalEditing = content
|
||
|
case ssaScriptInfoNameOriginalScript:
|
||
|
b.originalScript = content
|
||
|
case ssaScriptInfoNameOriginalTiming:
|
||
|
b.originalTiming = content
|
||
|
case ssaScriptInfoNameOriginalTranslation:
|
||
|
b.originalTranslation = content
|
||
|
case ssaScriptInfoNameScriptType:
|
||
|
b.scriptType = content
|
||
|
case ssaScriptInfoNameScriptUpdatedBy:
|
||
|
b.scriptUpdatedBy = content
|
||
|
case ssaScriptInfoNameSynchPoint:
|
||
|
b.synchPoint = content
|
||
|
case ssaScriptInfoNameTitle:
|
||
|
b.title = content
|
||
|
case ssaScriptInfoNameUpdateDetails:
|
||
|
b.updateDetails = content
|
||
|
case ssaScriptInfoNameWrapStyle:
|
||
|
b.wrapStyle = content
|
||
|
// Int
|
||
|
case ssaScriptInfoNamePlayResX, ssaScriptInfoNamePlayResY, ssaScriptInfoNamePlayDepth:
|
||
|
var v int
|
||
|
if v, err = strconv.Atoi(content); err != nil {
|
||
|
err = errors.Wrapf(err, "astisub: atoi of %s failed", content)
|
||
|
}
|
||
|
switch header {
|
||
|
case ssaScriptInfoNamePlayDepth:
|
||
|
b.playDepth = astiptr.Int(v)
|
||
|
case ssaScriptInfoNamePlayResX:
|
||
|
b.playResX = astiptr.Int(v)
|
||
|
case ssaScriptInfoNamePlayResY:
|
||
|
b.playResY = astiptr.Int(v)
|
||
|
}
|
||
|
// Float
|
||
|
case ssaScriptInfoNameTimer:
|
||
|
var v float64
|
||
|
if v, err = strconv.ParseFloat(strings.Replace(content, ",", ".", -1), 64); err != nil {
|
||
|
err = errors.Wrapf(err, "astisub: parseFloat of %s failed", content)
|
||
|
}
|
||
|
b.timer = astiptr.Float(v)
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// metadata returns the block as Metadata
|
||
|
func (b *ssaScriptInfo) metadata() *Metadata {
|
||
|
return &Metadata{
|
||
|
Comments: b.comments,
|
||
|
SSACollisions: b.collisions,
|
||
|
SSAOriginalEditing: b.originalEditing,
|
||
|
SSAOriginalScript: b.originalScript,
|
||
|
SSAOriginalTiming: b.originalTiming,
|
||
|
SSAOriginalTranslation: b.originalTranslation,
|
||
|
SSAPlayDepth: b.playDepth,
|
||
|
SSAPlayResX: b.playResX,
|
||
|
SSAPlayResY: b.playResY,
|
||
|
SSAScriptType: b.scriptType,
|
||
|
SSAScriptUpdatedBy: b.scriptUpdatedBy,
|
||
|
SSASynchPoint: b.synchPoint,
|
||
|
SSATimer: b.timer,
|
||
|
SSAUpdateDetails: b.updateDetails,
|
||
|
SSAWrapStyle: b.wrapStyle,
|
||
|
Title: b.title,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// bytes returns the block as bytes
|
||
|
func (b *ssaScriptInfo) bytes() (o []byte) {
|
||
|
o = []byte("[Script Info]")
|
||
|
o = append(o, bytesLineSeparator...)
|
||
|
for _, c := range b.comments {
|
||
|
o = appendStringToBytesWithNewLine(o, "; "+c)
|
||
|
}
|
||
|
if len(b.collisions) > 0 {
|
||
|
o = appendStringToBytesWithNewLine(o, ssaScriptInfoNameCollisions+": "+b.collisions)
|
||
|
}
|
||
|
if len(b.originalEditing) > 0 {
|
||
|
o = appendStringToBytesWithNewLine(o, ssaScriptInfoNameOriginalEditing+": "+b.originalEditing)
|
||
|
}
|
||
|
if len(b.originalScript) > 0 {
|
||
|
o = appendStringToBytesWithNewLine(o, ssaScriptInfoNameOriginalScript+": "+b.originalScript)
|
||
|
}
|
||
|
if len(b.originalTiming) > 0 {
|
||
|
o = appendStringToBytesWithNewLine(o, ssaScriptInfoNameOriginalTiming+": "+b.originalTiming)
|
||
|
}
|
||
|
if len(b.originalTranslation) > 0 {
|
||
|
o = appendStringToBytesWithNewLine(o, ssaScriptInfoNameOriginalTranslation+": "+b.originalTranslation)
|
||
|
}
|
||
|
if b.playDepth != nil {
|
||
|
o = appendStringToBytesWithNewLine(o, ssaScriptInfoNamePlayDepth+": "+strconv.Itoa(*b.playDepth))
|
||
|
}
|
||
|
if b.playResX != nil {
|
||
|
o = appendStringToBytesWithNewLine(o, ssaScriptInfoNamePlayResX+": "+strconv.Itoa(*b.playResX))
|
||
|
}
|
||
|
if b.playResY != nil {
|
||
|
o = appendStringToBytesWithNewLine(o, ssaScriptInfoNamePlayResY+": "+strconv.Itoa(*b.playResY))
|
||
|
}
|
||
|
if len(b.scriptType) > 0 {
|
||
|
o = appendStringToBytesWithNewLine(o, ssaScriptInfoNameScriptType+": "+b.scriptType)
|
||
|
}
|
||
|
if len(b.scriptUpdatedBy) > 0 {
|
||
|
o = appendStringToBytesWithNewLine(o, ssaScriptInfoNameScriptUpdatedBy+": "+b.scriptUpdatedBy)
|
||
|
}
|
||
|
if len(b.synchPoint) > 0 {
|
||
|
o = appendStringToBytesWithNewLine(o, ssaScriptInfoNameSynchPoint+": "+b.synchPoint)
|
||
|
}
|
||
|
if b.timer != nil {
|
||
|
o = appendStringToBytesWithNewLine(o, ssaScriptInfoNameTimer+": "+strings.Replace(strconv.FormatFloat(*b.timer, 'f', -1, 64), ".", ",", -1))
|
||
|
}
|
||
|
if len(b.title) > 0 {
|
||
|
o = appendStringToBytesWithNewLine(o, ssaScriptInfoNameTitle+": "+b.title)
|
||
|
}
|
||
|
if len(b.updateDetails) > 0 {
|
||
|
o = appendStringToBytesWithNewLine(o, ssaScriptInfoNameUpdateDetails+": "+b.updateDetails)
|
||
|
}
|
||
|
if len(b.wrapStyle) > 0 {
|
||
|
o = appendStringToBytesWithNewLine(o, ssaScriptInfoNameWrapStyle+": "+b.wrapStyle)
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// ssaStyle represents an SSA style
|
||
|
type ssaStyle struct {
|
||
|
alignment *int
|
||
|
alphaLevel *float64
|
||
|
angle *float64 // degrees
|
||
|
backColour *Color
|
||
|
bold *bool
|
||
|
borderStyle *int
|
||
|
encoding *int
|
||
|
fontName string
|
||
|
fontSize *float64
|
||
|
italic *bool
|
||
|
outline *int // pixels
|
||
|
outlineColour *Color
|
||
|
marginLeft *int // pixels
|
||
|
marginRight *int // pixels
|
||
|
marginVertical *int // pixels
|
||
|
name string
|
||
|
primaryColour *Color
|
||
|
scaleX *float64 // %
|
||
|
scaleY *float64 // %
|
||
|
secondaryColour *Color
|
||
|
shadow *int // pixels
|
||
|
spacing *int // pixels
|
||
|
strikeout *bool
|
||
|
underline *bool
|
||
|
}
|
||
|
|
||
|
// newSSAStyleFromStyle returns an SSA style based on a Style
|
||
|
func newSSAStyleFromStyle(i Style) *ssaStyle {
|
||
|
return &ssaStyle{
|
||
|
alignment: i.InlineStyle.SSAAlignment,
|
||
|
alphaLevel: i.InlineStyle.SSAAlphaLevel,
|
||
|
angle: i.InlineStyle.SSAAngle,
|
||
|
backColour: i.InlineStyle.SSABackColour,
|
||
|
bold: i.InlineStyle.SSABold,
|
||
|
borderStyle: i.InlineStyle.SSABorderStyle,
|
||
|
encoding: i.InlineStyle.SSAEncoding,
|
||
|
fontName: i.InlineStyle.SSAFontName,
|
||
|
fontSize: i.InlineStyle.SSAFontSize,
|
||
|
italic: i.InlineStyle.SSAItalic,
|
||
|
outline: i.InlineStyle.SSAOutline,
|
||
|
outlineColour: i.InlineStyle.SSAOutlineColour,
|
||
|
marginLeft: i.InlineStyle.SSAMarginLeft,
|
||
|
marginRight: i.InlineStyle.SSAMarginRight,
|
||
|
marginVertical: i.InlineStyle.SSAMarginVertical,
|
||
|
name: i.ID,
|
||
|
primaryColour: i.InlineStyle.SSAPrimaryColour,
|
||
|
scaleX: i.InlineStyle.SSAScaleX,
|
||
|
scaleY: i.InlineStyle.SSAScaleY,
|
||
|
secondaryColour: i.InlineStyle.SSASecondaryColour,
|
||
|
shadow: i.InlineStyle.SSAShadow,
|
||
|
spacing: i.InlineStyle.SSASpacing,
|
||
|
strikeout: i.InlineStyle.SSAStrikeout,
|
||
|
underline: i.InlineStyle.SSAUnderline,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// newSSAStyleFromString returns an SSA style based on an input string and a format
|
||
|
func newSSAStyleFromString(content string, format map[int]string) (s *ssaStyle, err error) {
|
||
|
// Split content
|
||
|
var items = strings.Split(content, ",")
|
||
|
|
||
|
// Not enough items
|
||
|
if len(items) < len(format) {
|
||
|
err = fmt.Errorf("astisub: content has %d items whereas style format has %d items", len(items), len(format))
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Loop through items
|
||
|
s = &ssaStyle{}
|
||
|
for idx, item := range items {
|
||
|
// Index not found in format
|
||
|
var attr string
|
||
|
var ok bool
|
||
|
if attr, ok = format[idx]; !ok {
|
||
|
err = fmt.Errorf("astisub: index %d not found in style format %+v", idx, format)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Switch on attribute name
|
||
|
switch attr {
|
||
|
// Bool
|
||
|
case ssaStyleFormatNameBold, ssaStyleFormatNameItalic, ssaStyleFormatNameStrikeout,
|
||
|
ssaStyleFormatNameUnderline:
|
||
|
var b = item == "-1"
|
||
|
switch attr {
|
||
|
case ssaStyleFormatNameBold:
|
||
|
s.bold = astiptr.Bool(b)
|
||
|
case ssaStyleFormatNameItalic:
|
||
|
s.italic = astiptr.Bool(b)
|
||
|
case ssaStyleFormatNameStrikeout:
|
||
|
s.strikeout = astiptr.Bool(b)
|
||
|
case ssaStyleFormatNameUnderline:
|
||
|
s.underline = astiptr.Bool(b)
|
||
|
}
|
||
|
// Color
|
||
|
case ssaStyleFormatNamePrimaryColour, ssaStyleFormatNameSecondaryColour,
|
||
|
ssaStyleFormatNameTertiaryColour, ssaStyleFormatNameOutlineColour, ssaStyleFormatNameBackColour:
|
||
|
// Build color
|
||
|
var c *Color
|
||
|
if c, err = newColorFromSSAColor(item); err != nil {
|
||
|
err = errors.Wrapf(err, "astisub: building new %s from ssa color %s failed", attr, item)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Set color
|
||
|
switch attr {
|
||
|
case ssaStyleFormatNameBackColour:
|
||
|
s.backColour = c
|
||
|
case ssaStyleFormatNamePrimaryColour:
|
||
|
s.primaryColour = c
|
||
|
case ssaStyleFormatNameSecondaryColour:
|
||
|
s.secondaryColour = c
|
||
|
case ssaStyleFormatNameTertiaryColour, ssaStyleFormatNameOutlineColour:
|
||
|
s.outlineColour = c
|
||
|
}
|
||
|
// Float
|
||
|
case ssaStyleFormatNameAlphaLevel, ssaStyleFormatNameAngle, ssaStyleFormatNameFontSize,
|
||
|
ssaStyleFormatNameScaleX, ssaStyleFormatNameScaleY:
|
||
|
// Parse float
|
||
|
var f float64
|
||
|
if f, err = strconv.ParseFloat(item, 64); err != nil {
|
||
|
err = errors.Wrapf(err, "astisub: parsing float %s failed", item)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Set float
|
||
|
switch attr {
|
||
|
case ssaStyleFormatNameAlphaLevel:
|
||
|
s.alphaLevel = astiptr.Float(f)
|
||
|
case ssaStyleFormatNameAngle:
|
||
|
s.angle = astiptr.Float(f)
|
||
|
case ssaStyleFormatNameFontSize:
|
||
|
s.fontSize = astiptr.Float(f)
|
||
|
case ssaStyleFormatNameScaleX:
|
||
|
s.scaleX = astiptr.Float(f)
|
||
|
case ssaStyleFormatNameScaleY:
|
||
|
s.scaleY = astiptr.Float(f)
|
||
|
}
|
||
|
// Int
|
||
|
case ssaStyleFormatNameAlignment, ssaStyleFormatNameBorderStyle, ssaStyleFormatNameEncoding,
|
||
|
ssaStyleFormatNameMarginL, ssaStyleFormatNameMarginR, ssaStyleFormatNameMarginV,
|
||
|
ssaStyleFormatNameOutline, ssaStyleFormatNameShadow, ssaStyleFormatNameSpacing:
|
||
|
// Parse int
|
||
|
var i int
|
||
|
if i, err = strconv.Atoi(item); err != nil {
|
||
|
err = errors.Wrapf(err, "astisub: atoi of %s failed", item)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Set int
|
||
|
switch attr {
|
||
|
case ssaStyleFormatNameAlignment:
|
||
|
s.alignment = astiptr.Int(i)
|
||
|
case ssaStyleFormatNameBorderStyle:
|
||
|
s.borderStyle = astiptr.Int(i)
|
||
|
case ssaStyleFormatNameEncoding:
|
||
|
s.encoding = astiptr.Int(i)
|
||
|
case ssaStyleFormatNameMarginL:
|
||
|
s.marginLeft = astiptr.Int(i)
|
||
|
case ssaStyleFormatNameMarginR:
|
||
|
s.marginRight = astiptr.Int(i)
|
||
|
case ssaStyleFormatNameMarginV:
|
||
|
s.marginVertical = astiptr.Int(i)
|
||
|
case ssaStyleFormatNameOutline:
|
||
|
s.outline = astiptr.Int(i)
|
||
|
case ssaStyleFormatNameShadow:
|
||
|
s.shadow = astiptr.Int(i)
|
||
|
case ssaStyleFormatNameSpacing:
|
||
|
s.spacing = astiptr.Int(i)
|
||
|
}
|
||
|
// String
|
||
|
case ssaStyleFormatNameFontName, ssaStyleFormatNameName:
|
||
|
switch attr {
|
||
|
case ssaStyleFormatNameFontName:
|
||
|
s.fontName = item
|
||
|
case ssaStyleFormatNameName:
|
||
|
s.name = item
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// ssaUpdateFormat updates an SSA format
|
||
|
func ssaUpdateFormat(n string, formatMap map[string]bool, format []string) []string {
|
||
|
if _, ok := formatMap[n]; !ok {
|
||
|
formatMap[n] = true
|
||
|
format = append(format, n)
|
||
|
}
|
||
|
return format
|
||
|
}
|
||
|
|
||
|
// updateFormat updates the format based on the non empty fields
|
||
|
func (s ssaStyle) updateFormat(formatMap map[string]bool, format []string) []string {
|
||
|
if s.alignment != nil {
|
||
|
format = ssaUpdateFormat(ssaStyleFormatNameAlignment, formatMap, format)
|
||
|
}
|
||
|
if s.alphaLevel != nil {
|
||
|
format = ssaUpdateFormat(ssaStyleFormatNameAlphaLevel, formatMap, format)
|
||
|
}
|
||
|
if s.angle != nil {
|
||
|
format = ssaUpdateFormat(ssaStyleFormatNameAngle, formatMap, format)
|
||
|
}
|
||
|
if s.backColour != nil {
|
||
|
format = ssaUpdateFormat(ssaStyleFormatNameBackColour, formatMap, format)
|
||
|
}
|
||
|
if s.bold != nil {
|
||
|
format = ssaUpdateFormat(ssaStyleFormatNameBold, formatMap, format)
|
||
|
}
|
||
|
if s.borderStyle != nil {
|
||
|
format = ssaUpdateFormat(ssaStyleFormatNameBorderStyle, formatMap, format)
|
||
|
}
|
||
|
if s.encoding != nil {
|
||
|
format = ssaUpdateFormat(ssaStyleFormatNameEncoding, formatMap, format)
|
||
|
}
|
||
|
if len(s.fontName) > 0 {
|
||
|
format = ssaUpdateFormat(ssaStyleFormatNameFontName, formatMap, format)
|
||
|
}
|
||
|
if s.fontSize != nil {
|
||
|
format = ssaUpdateFormat(ssaStyleFormatNameFontSize, formatMap, format)
|
||
|
}
|
||
|
if s.italic != nil {
|
||
|
format = ssaUpdateFormat(ssaStyleFormatNameItalic, formatMap, format)
|
||
|
}
|
||
|
if s.marginLeft != nil {
|
||
|
format = ssaUpdateFormat(ssaStyleFormatNameMarginL, formatMap, format)
|
||
|
}
|
||
|
if s.marginRight != nil {
|
||
|
format = ssaUpdateFormat(ssaStyleFormatNameMarginR, formatMap, format)
|
||
|
}
|
||
|
if s.marginVertical != nil {
|
||
|
format = ssaUpdateFormat(ssaStyleFormatNameMarginV, formatMap, format)
|
||
|
}
|
||
|
if s.outline != nil {
|
||
|
format = ssaUpdateFormat(ssaStyleFormatNameOutline, formatMap, format)
|
||
|
}
|
||
|
if s.outlineColour != nil {
|
||
|
format = ssaUpdateFormat(ssaStyleFormatNameOutlineColour, formatMap, format)
|
||
|
}
|
||
|
if s.primaryColour != nil {
|
||
|
format = ssaUpdateFormat(ssaStyleFormatNamePrimaryColour, formatMap, format)
|
||
|
}
|
||
|
if s.scaleX != nil {
|
||
|
format = ssaUpdateFormat(ssaStyleFormatNameScaleX, formatMap, format)
|
||
|
}
|
||
|
if s.scaleY != nil {
|
||
|
format = ssaUpdateFormat(ssaStyleFormatNameScaleY, formatMap, format)
|
||
|
}
|
||
|
if s.secondaryColour != nil {
|
||
|
format = ssaUpdateFormat(ssaStyleFormatNameSecondaryColour, formatMap, format)
|
||
|
}
|
||
|
if s.shadow != nil {
|
||
|
format = ssaUpdateFormat(ssaStyleFormatNameShadow, formatMap, format)
|
||
|
}
|
||
|
if s.spacing != nil {
|
||
|
format = ssaUpdateFormat(ssaStyleFormatNameSpacing, formatMap, format)
|
||
|
}
|
||
|
if s.strikeout != nil {
|
||
|
format = ssaUpdateFormat(ssaStyleFormatNameStrikeout, formatMap, format)
|
||
|
}
|
||
|
if s.underline != nil {
|
||
|
format = ssaUpdateFormat(ssaStyleFormatNameUnderline, formatMap, format)
|
||
|
}
|
||
|
return format
|
||
|
}
|
||
|
|
||
|
// string returns the block as a string
|
||
|
func (s ssaStyle) string(format []string) string {
|
||
|
var ss = []string{s.name}
|
||
|
for _, attr := range format {
|
||
|
var v string
|
||
|
var found = true
|
||
|
switch attr {
|
||
|
// Bool
|
||
|
case ssaStyleFormatNameBold, ssaStyleFormatNameItalic, ssaStyleFormatNameStrikeout,
|
||
|
ssaStyleFormatNameUnderline:
|
||
|
var b *bool
|
||
|
switch attr {
|
||
|
case ssaStyleFormatNameBold:
|
||
|
b = s.bold
|
||
|
case ssaStyleFormatNameItalic:
|
||
|
b = s.italic
|
||
|
case ssaStyleFormatNameStrikeout:
|
||
|
b = s.strikeout
|
||
|
case ssaStyleFormatNameUnderline:
|
||
|
b = s.underline
|
||
|
}
|
||
|
if b != nil {
|
||
|
v = "0"
|
||
|
if *b {
|
||
|
v = "1"
|
||
|
}
|
||
|
}
|
||
|
// Color
|
||
|
case ssaStyleFormatNamePrimaryColour, ssaStyleFormatNameSecondaryColour,
|
||
|
ssaStyleFormatNameOutlineColour, ssaStyleFormatNameBackColour:
|
||
|
var c *Color
|
||
|
switch attr {
|
||
|
case ssaStyleFormatNameBackColour:
|
||
|
c = s.backColour
|
||
|
case ssaStyleFormatNamePrimaryColour:
|
||
|
c = s.primaryColour
|
||
|
case ssaStyleFormatNameSecondaryColour:
|
||
|
c = s.secondaryColour
|
||
|
case ssaStyleFormatNameOutlineColour:
|
||
|
c = s.outlineColour
|
||
|
}
|
||
|
if c != nil {
|
||
|
v = newSSAColorFromColor(c)
|
||
|
}
|
||
|
// Float
|
||
|
case ssaStyleFormatNameAlphaLevel, ssaStyleFormatNameAngle, ssaStyleFormatNameFontSize,
|
||
|
ssaStyleFormatNameScaleX, ssaStyleFormatNameScaleY:
|
||
|
var f *float64
|
||
|
switch attr {
|
||
|
case ssaStyleFormatNameAlphaLevel:
|
||
|
f = s.alphaLevel
|
||
|
case ssaStyleFormatNameAngle:
|
||
|
f = s.angle
|
||
|
case ssaStyleFormatNameFontSize:
|
||
|
f = s.fontSize
|
||
|
case ssaStyleFormatNameScaleX:
|
||
|
f = s.scaleX
|
||
|
case ssaStyleFormatNameScaleY:
|
||
|
f = s.scaleY
|
||
|
}
|
||
|
if f != nil {
|
||
|
v = strconv.FormatFloat(*f, 'f', 3, 64)
|
||
|
}
|
||
|
// Int
|
||
|
case ssaStyleFormatNameAlignment, ssaStyleFormatNameBorderStyle, ssaStyleFormatNameEncoding,
|
||
|
ssaStyleFormatNameMarginL, ssaStyleFormatNameMarginR, ssaStyleFormatNameMarginV,
|
||
|
ssaStyleFormatNameOutline, ssaStyleFormatNameShadow, ssaStyleFormatNameSpacing:
|
||
|
var i *int
|
||
|
switch attr {
|
||
|
case ssaStyleFormatNameAlignment:
|
||
|
i = s.alignment
|
||
|
case ssaStyleFormatNameBorderStyle:
|
||
|
i = s.borderStyle
|
||
|
case ssaStyleFormatNameEncoding:
|
||
|
i = s.encoding
|
||
|
case ssaStyleFormatNameMarginL:
|
||
|
i = s.marginLeft
|
||
|
case ssaStyleFormatNameMarginR:
|
||
|
i = s.marginRight
|
||
|
case ssaStyleFormatNameMarginV:
|
||
|
i = s.marginVertical
|
||
|
case ssaStyleFormatNameOutline:
|
||
|
i = s.outline
|
||
|
case ssaStyleFormatNameShadow:
|
||
|
i = s.shadow
|
||
|
case ssaStyleFormatNameSpacing:
|
||
|
i = s.spacing
|
||
|
}
|
||
|
if i != nil {
|
||
|
v = strconv.Itoa(*i)
|
||
|
}
|
||
|
// String
|
||
|
case ssaStyleFormatNameFontName:
|
||
|
switch attr {
|
||
|
case ssaStyleFormatNameFontName:
|
||
|
v = s.fontName
|
||
|
}
|
||
|
default:
|
||
|
found = false
|
||
|
}
|
||
|
if found {
|
||
|
ss = append(ss, v)
|
||
|
}
|
||
|
}
|
||
|
return strings.Join(ss, ",")
|
||
|
}
|
||
|
|
||
|
// style converts ssaStyle to Style
|
||
|
func (s ssaStyle) style() (o *Style) {
|
||
|
o = &Style{
|
||
|
ID: s.name,
|
||
|
InlineStyle: &StyleAttributes{
|
||
|
SSAAlignment: s.alignment,
|
||
|
SSAAlphaLevel: s.alphaLevel,
|
||
|
SSAAngle: s.angle,
|
||
|
SSABackColour: s.backColour,
|
||
|
SSABold: s.bold,
|
||
|
SSABorderStyle: s.borderStyle,
|
||
|
SSAEncoding: s.encoding,
|
||
|
SSAFontName: s.fontName,
|
||
|
SSAFontSize: s.fontSize,
|
||
|
SSAItalic: s.italic,
|
||
|
SSAOutline: s.outline,
|
||
|
SSAOutlineColour: s.outlineColour,
|
||
|
SSAMarginLeft: s.marginLeft,
|
||
|
SSAMarginRight: s.marginRight,
|
||
|
SSAMarginVertical: s.marginVertical,
|
||
|
SSAPrimaryColour: s.primaryColour,
|
||
|
SSAScaleX: s.scaleX,
|
||
|
SSAScaleY: s.scaleY,
|
||
|
SSASecondaryColour: s.secondaryColour,
|
||
|
SSAShadow: s.shadow,
|
||
|
SSASpacing: s.spacing,
|
||
|
SSAStrikeout: s.strikeout,
|
||
|
SSAUnderline: s.underline,
|
||
|
},
|
||
|
}
|
||
|
o.InlineStyle.propagateSSAAttributes()
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// ssaEvent represents an SSA event
|
||
|
type ssaEvent struct {
|
||
|
category string
|
||
|
effect string
|
||
|
end time.Duration
|
||
|
layer *int
|
||
|
marked *bool
|
||
|
marginLeft *int // pixels
|
||
|
marginRight *int // pixels
|
||
|
marginVertical *int // pixels
|
||
|
name string
|
||
|
start time.Duration
|
||
|
style string
|
||
|
text string
|
||
|
}
|
||
|
|
||
|
// newSSAEventFromItem returns an SSA Event based on an input item
|
||
|
func newSSAEventFromItem(i Item) (e *ssaEvent) {
|
||
|
// Init
|
||
|
e = &ssaEvent{
|
||
|
category: ssaEventCategoryDialogue,
|
||
|
end: i.EndAt,
|
||
|
start: i.StartAt,
|
||
|
}
|
||
|
|
||
|
// Style
|
||
|
if i.Style != nil {
|
||
|
e.style = i.Style.ID
|
||
|
}
|
||
|
|
||
|
// Inline style
|
||
|
if i.InlineStyle != nil {
|
||
|
e.effect = i.InlineStyle.SSAEffect
|
||
|
e.layer = i.InlineStyle.SSALayer
|
||
|
e.marginLeft = i.InlineStyle.SSAMarginLeft
|
||
|
e.marginRight = i.InlineStyle.SSAMarginRight
|
||
|
e.marginVertical = i.InlineStyle.SSAMarginVertical
|
||
|
e.marked = i.InlineStyle.SSAMarked
|
||
|
}
|
||
|
|
||
|
// Text
|
||
|
var lines []string
|
||
|
for _, l := range i.Lines {
|
||
|
var items []string
|
||
|
for _, item := range l.Items {
|
||
|
var s string
|
||
|
if item.InlineStyle != nil && len(item.InlineStyle.SSAEffect) > 0 {
|
||
|
s += item.InlineStyle.SSAEffect
|
||
|
}
|
||
|
s += item.Text
|
||
|
items = append(items, s)
|
||
|
}
|
||
|
if len(l.VoiceName) > 0 {
|
||
|
e.name = l.VoiceName
|
||
|
}
|
||
|
lines = append(lines, strings.Join(items, ""))
|
||
|
}
|
||
|
e.text = strings.Join(lines, "\\n")
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// newSSAEventFromString returns an SSA event based on an input string and a format
|
||
|
func newSSAEventFromString(header, content string, format map[int]string) (e *ssaEvent, err error) {
|
||
|
// Split content
|
||
|
var items = strings.Split(content, ",")
|
||
|
|
||
|
// Not enough items
|
||
|
if len(items) < len(format) {
|
||
|
err = fmt.Errorf("astisub: content has %d items whereas style format has %d items", len(items), len(format))
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Last item may contain commas, therefore we need to fix it
|
||
|
items[len(format)-1] = strings.Join(items[len(format)-1:], ",")
|
||
|
items = items[:len(format)]
|
||
|
|
||
|
// Loop through items
|
||
|
e = &ssaEvent{category: header}
|
||
|
for idx, item := range items {
|
||
|
// Index not found in format
|
||
|
var attr string
|
||
|
var ok bool
|
||
|
if attr, ok = format[idx]; !ok {
|
||
|
err = fmt.Errorf("astisub: index %d not found in event format %+v", idx, format)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Switch on attribute name
|
||
|
switch attr {
|
||
|
// Duration
|
||
|
case ssaEventFormatNameStart, ssaEventFormatNameEnd:
|
||
|
// Parse duration
|
||
|
var d time.Duration
|
||
|
if d, err = parseDurationSSA(item); err != nil {
|
||
|
err = errors.Wrapf(err, "astisub: parsing ssa duration %s failed", item)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Set duration
|
||
|
switch attr {
|
||
|
case ssaEventFormatNameEnd:
|
||
|
e.end = d
|
||
|
case ssaEventFormatNameStart:
|
||
|
e.start = d
|
||
|
}
|
||
|
// Int
|
||
|
case ssaEventFormatNameLayer, ssaEventFormatNameMarginL, ssaEventFormatNameMarginR,
|
||
|
ssaEventFormatNameMarginV:
|
||
|
// Parse int
|
||
|
var i int
|
||
|
if i, err = strconv.Atoi(item); err != nil {
|
||
|
err = errors.Wrapf(err, "astisub: atoi of %s failed", item)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Set int
|
||
|
switch attr {
|
||
|
case ssaEventFormatNameLayer:
|
||
|
e.layer = astiptr.Int(i)
|
||
|
case ssaEventFormatNameMarginL:
|
||
|
e.marginLeft = astiptr.Int(i)
|
||
|
case ssaEventFormatNameMarginR:
|
||
|
e.marginRight = astiptr.Int(i)
|
||
|
case ssaEventFormatNameMarginV:
|
||
|
e.marginVertical = astiptr.Int(i)
|
||
|
}
|
||
|
// String
|
||
|
case ssaEventFormatNameEffect, ssaEventFormatNameName, ssaEventFormatNameStyle, ssaEventFormatNameText:
|
||
|
switch attr {
|
||
|
case ssaEventFormatNameEffect:
|
||
|
e.effect = item
|
||
|
case ssaEventFormatNameName:
|
||
|
e.name = item
|
||
|
case ssaEventFormatNameStyle:
|
||
|
e.style = item
|
||
|
case ssaEventFormatNameText:
|
||
|
e.text = item
|
||
|
}
|
||
|
// Marked
|
||
|
case ssaEventFormatNameMarked:
|
||
|
if item == "Marked=1" {
|
||
|
e.marked = astiptr.Bool(true)
|
||
|
} else {
|
||
|
e.marked = astiptr.Bool(false)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// item converts an SSA event to an Item
|
||
|
func (e *ssaEvent) item(styles map[string]*Style) (i *Item, err error) {
|
||
|
// Init item
|
||
|
i = &Item{
|
||
|
EndAt: e.end,
|
||
|
InlineStyle: &StyleAttributes{
|
||
|
SSAEffect: e.effect,
|
||
|
SSALayer: e.layer,
|
||
|
SSAMarginLeft: e.marginLeft,
|
||
|
SSAMarginRight: e.marginRight,
|
||
|
SSAMarginVertical: e.marginVertical,
|
||
|
SSAMarked: e.marked,
|
||
|
},
|
||
|
StartAt: e.start,
|
||
|
}
|
||
|
|
||
|
// Set style
|
||
|
if len(e.style) > 0 {
|
||
|
var ok bool
|
||
|
if i.Style, ok = styles[e.style]; !ok {
|
||
|
err = fmt.Errorf("astisub: style %s not found", e.style)
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Loop through lines
|
||
|
for _, s := range strings.Split(e.text, "\\n") {
|
||
|
// Init
|
||
|
s = strings.TrimSpace(s)
|
||
|
var l = Line{VoiceName: e.name}
|
||
|
|
||
|
// Extract effects
|
||
|
var matches = ssaRegexpEffect.FindAllStringIndex(s, -1)
|
||
|
if len(matches) > 0 {
|
||
|
// Loop through matches
|
||
|
var lineItem *LineItem
|
||
|
var previousEffectEndOffset int
|
||
|
for _, idxs := range matches {
|
||
|
if lineItem != nil {
|
||
|
lineItem.Text = s[previousEffectEndOffset:idxs[0]]
|
||
|
l.Items = append(l.Items, *lineItem)
|
||
|
}
|
||
|
previousEffectEndOffset = idxs[1]
|
||
|
lineItem = &LineItem{InlineStyle: &StyleAttributes{SSAEffect: s[idxs[0]:idxs[1]]}}
|
||
|
}
|
||
|
lineItem.Text = s[previousEffectEndOffset:]
|
||
|
l.Items = append(l.Items, *lineItem)
|
||
|
} else {
|
||
|
l.Items = append(l.Items, LineItem{Text: s})
|
||
|
}
|
||
|
|
||
|
// Add line
|
||
|
i.Lines = append(i.Lines, l)
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// updateFormat updates the format based on the non empty fields
|
||
|
func (e ssaEvent) updateFormat(formatMap map[string]bool, format []string) []string {
|
||
|
if len(e.effect) > 0 {
|
||
|
format = ssaUpdateFormat(ssaEventFormatNameEffect, formatMap, format)
|
||
|
}
|
||
|
if e.layer != nil {
|
||
|
format = ssaUpdateFormat(ssaEventFormatNameLayer, formatMap, format)
|
||
|
}
|
||
|
if e.marginLeft != nil {
|
||
|
format = ssaUpdateFormat(ssaEventFormatNameMarginL, formatMap, format)
|
||
|
}
|
||
|
if e.marginRight != nil {
|
||
|
format = ssaUpdateFormat(ssaEventFormatNameMarginR, formatMap, format)
|
||
|
}
|
||
|
if e.marginVertical != nil {
|
||
|
format = ssaUpdateFormat(ssaEventFormatNameMarginV, formatMap, format)
|
||
|
}
|
||
|
if e.marked != nil {
|
||
|
format = ssaUpdateFormat(ssaEventFormatNameMarked, formatMap, format)
|
||
|
}
|
||
|
if len(e.name) > 0 {
|
||
|
format = ssaUpdateFormat(ssaEventFormatNameName, formatMap, format)
|
||
|
}
|
||
|
if len(e.style) > 0 {
|
||
|
format = ssaUpdateFormat(ssaEventFormatNameStyle, formatMap, format)
|
||
|
}
|
||
|
return format
|
||
|
}
|
||
|
|
||
|
// formatDurationSSA formats an .ssa duration
|
||
|
func formatDurationSSA(i time.Duration) string {
|
||
|
return formatDuration(i, ".", 2)
|
||
|
}
|
||
|
|
||
|
// string returns the block as a string
|
||
|
func (e *ssaEvent) string(format []string) string {
|
||
|
var ss []string
|
||
|
for _, attr := range format {
|
||
|
var v string
|
||
|
var found = true
|
||
|
switch attr {
|
||
|
// Duration
|
||
|
case ssaEventFormatNameEnd, ssaEventFormatNameStart:
|
||
|
switch attr {
|
||
|
case ssaEventFormatNameEnd:
|
||
|
v = formatDurationSSA(e.end)
|
||
|
case ssaEventFormatNameStart:
|
||
|
v = formatDurationSSA(e.start)
|
||
|
}
|
||
|
// Marked
|
||
|
case ssaEventFormatNameMarked:
|
||
|
if e.marked != nil {
|
||
|
if *e.marked {
|
||
|
v = "Marked=1"
|
||
|
} else {
|
||
|
v = "Marked=0"
|
||
|
}
|
||
|
}
|
||
|
// Int
|
||
|
case ssaEventFormatNameLayer, ssaEventFormatNameMarginL, ssaEventFormatNameMarginR,
|
||
|
ssaEventFormatNameMarginV:
|
||
|
var i *int
|
||
|
switch attr {
|
||
|
case ssaEventFormatNameLayer:
|
||
|
i = e.layer
|
||
|
case ssaEventFormatNameMarginL:
|
||
|
i = e.marginLeft
|
||
|
case ssaEventFormatNameMarginR:
|
||
|
i = e.marginRight
|
||
|
case ssaEventFormatNameMarginV:
|
||
|
i = e.marginVertical
|
||
|
}
|
||
|
if i != nil {
|
||
|
v = strconv.Itoa(*i)
|
||
|
}
|
||
|
// String
|
||
|
case ssaEventFormatNameEffect, ssaEventFormatNameName, ssaEventFormatNameStyle, ssaEventFormatNameText:
|
||
|
switch attr {
|
||
|
case ssaEventFormatNameEffect:
|
||
|
v = e.effect
|
||
|
case ssaEventFormatNameName:
|
||
|
v = e.name
|
||
|
case ssaEventFormatNameStyle:
|
||
|
v = e.style
|
||
|
case ssaEventFormatNameText:
|
||
|
v = e.text
|
||
|
}
|
||
|
default:
|
||
|
found = false
|
||
|
}
|
||
|
if found {
|
||
|
ss = append(ss, v)
|
||
|
}
|
||
|
}
|
||
|
return strings.Join(ss, ",")
|
||
|
}
|
||
|
|
||
|
// parseDurationSSA parses an .ssa duration
|
||
|
func parseDurationSSA(i string) (time.Duration, error) {
|
||
|
return parseDuration(i, ".", 3)
|
||
|
}
|
||
|
|
||
|
// WriteToSSA writes subtitles in .ssa format
|
||
|
func (s Subtitles) WriteToSSA(o io.Writer) (err error) {
|
||
|
// Do not write anything if no subtitles
|
||
|
if len(s.Items) == 0 {
|
||
|
err = ErrNoSubtitlesToWrite
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Write Script Info block
|
||
|
var si = newSSAScriptInfo(s.Metadata)
|
||
|
if _, err = o.Write(si.bytes()); err != nil {
|
||
|
err = errors.Wrap(err, "astisub: writing script info block failed")
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Write Styles block
|
||
|
if len(s.Styles) > 0 {
|
||
|
// Header
|
||
|
var b = []byte("\n[V4 Styles]\n")
|
||
|
|
||
|
// Format
|
||
|
var formatMap = make(map[string]bool)
|
||
|
var format = []string{ssaStyleFormatNameName}
|
||
|
var styles = make(map[string]*ssaStyle)
|
||
|
var styleNames []string
|
||
|
for _, s := range s.Styles {
|
||
|
var ss = newSSAStyleFromStyle(*s)
|
||
|
format = ss.updateFormat(formatMap, format)
|
||
|
styles[ss.name] = ss
|
||
|
styleNames = append(styleNames, ss.name)
|
||
|
}
|
||
|
b = append(b, []byte("Format: "+strings.Join(format, ", ")+"\n")...)
|
||
|
|
||
|
// Styles
|
||
|
sort.Strings(styleNames)
|
||
|
for _, n := range styleNames {
|
||
|
b = append(b, []byte("Style: "+styles[n].string(format)+"\n")...)
|
||
|
}
|
||
|
|
||
|
// Write
|
||
|
if _, err = o.Write(b); err != nil {
|
||
|
err = errors.Wrap(err, "astisub: writing styles block failed")
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Write Events block
|
||
|
if len(s.Items) > 0 {
|
||
|
// Header
|
||
|
var b = []byte("\n[Events]\n")
|
||
|
|
||
|
// Format
|
||
|
var formatMap = make(map[string]bool)
|
||
|
var format = []string{
|
||
|
ssaEventFormatNameStart,
|
||
|
ssaEventFormatNameEnd,
|
||
|
}
|
||
|
var events []*ssaEvent
|
||
|
for _, i := range s.Items {
|
||
|
var e = newSSAEventFromItem(*i)
|
||
|
format = e.updateFormat(formatMap, format)
|
||
|
events = append(events, e)
|
||
|
}
|
||
|
format = append(format, ssaEventFormatNameText)
|
||
|
b = append(b, []byte("Format: "+strings.Join(format, ", ")+"\n")...)
|
||
|
|
||
|
// Styles
|
||
|
for _, e := range events {
|
||
|
b = append(b, []byte(ssaEventCategoryDialogue+": "+e.string(format)+"\n")...)
|
||
|
}
|
||
|
|
||
|
// Write
|
||
|
if _, err = o.Write(b); err != nil {
|
||
|
err = errors.Wrap(err, "astisub: writing events block failed")
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
return
|
||
|
}
|