summaryrefslogtreecommitdiff
path: root/mkvlib/parser/ssa.go
diff options
context:
space:
mode:
authorb5f0d6c3 <[email protected]>2022-04-29 08:31:56 +0800
committerb5f0d6c3 <[email protected]>2022-04-29 08:31:56 +0800
commit9fd8a4735bb7d9127daeb21fa98c78c7ad9e5a7f (patch)
tree01839ecc8526e23aaf8a55654684fc41216d6ef8 /mkvlib/parser/ssa.go
parent31c46c2f40b69911be11fe5adad39dfc295a4f2b (diff)
update mkvlib:new parser
Diffstat (limited to 'mkvlib/parser/ssa.go')
-rw-r--r--mkvlib/parser/ssa.go1298
1 files changed, 1298 insertions, 0 deletions
diff --git a/mkvlib/parser/ssa.go b/mkvlib/parser/ssa.go
new file mode 100644
index 0000000..9c37ec4
--- /dev/null
+++ b/mkvlib/parser/ssa.go
@@ -0,0 +1,1298 @@
+package parser
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "log"
+ "regexp"
+ "sort"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/asticode/go-astikit"
+)
+
+// 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) {
+ o, err = ReadFromSSAWithOptions(i, defaultSSAOptions())
+ return o, err
+}
+
+// ReadFromSSAWithOptions parses an .ssa content
+func ReadFromSSAWithOptions(i io.Reader, opts SSAOptions) (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
+ isFirstLine := true
+ scanner.Buffer(make([]byte, 0), 1024*1024*1024)
+ for scanner.Scan() {
+ // Fetch line
+ line = strings.TrimSpace(scanner.Text())
+
+ // Remove BOM header
+ if isFirstLine {
+ line = strings.TrimPrefix(line, string(BytesBOM))
+ isFirstLine = false
+ }
+
+ // 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:
+ if opts.OnUnknownSectionName != nil {
+ opts.OnUnknownSectionName(line)
+ }
+ 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 || split[0] == "" {
+ if opts.OnInvalidLine != nil {
+ opts.OnInvalidLine(line)
+ }
+ continue
+ }
+ 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 = fmt.Errorf("astisub: parsing script info block failed: %w", err)
+ 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 = fmt.Errorf("astisub: building new ssa event failed: %w", err)
+ return
+ }
+ es = append(es, e)
+ case ssaSectionNameStyles:
+ var s *ssaStyle
+ if s, err = newSSAStyleFromString(content, format); err != nil {
+ err = fmt.Errorf("astisub: building new ssa style failed: %w", err)
+ 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 = fmt.Errorf("astisub: atoi of %s failed: %w", content, err)
+ }
+ switch header {
+ case ssaScriptInfoNamePlayDepth:
+ b.playDepth = astikit.IntPtr(v)
+ case ssaScriptInfoNamePlayResX:
+ b.playResX = astikit.IntPtr(v)
+ case ssaScriptInfoNamePlayResY:
+ b.playResY = astikit.IntPtr(v)
+ }
+ // Float
+ case ssaScriptInfoNameTimer:
+ var v float64
+ if v, err = strconv.ParseFloat(strings.Replace(content, ",", ".", -1), 64); err != nil {
+ err = fmt.Errorf("astisub: parseFloat of %s failed: %w", content, err)
+ }
+ b.timer = astikit.Float64Ptr(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 *float64 // pixels
+ outlineColour *Color
+ marginLeft *int // pixels
+ marginRight *int // pixels
+ marginVertical *int // pixels
+ name string
+ primaryColour *Color
+ scaleX *float64 // %
+ scaleY *float64 // %
+ secondaryColour *Color
+ shadow *float64 // pixels
+ spacing *float64 // 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 = astikit.BoolPtr(b)
+ case ssaStyleFormatNameItalic:
+ s.italic = astikit.BoolPtr(b)
+ case ssaStyleFormatNameStrikeout:
+ s.strikeout = astikit.BoolPtr(b)
+ case ssaStyleFormatNameUnderline:
+ s.underline = astikit.BoolPtr(b)
+ }
+ // Color
+ case ssaStyleFormatNamePrimaryColour, ssaStyleFormatNameSecondaryColour,
+ ssaStyleFormatNameTertiaryColour, ssaStyleFormatNameOutlineColour, ssaStyleFormatNameBackColour:
+ // Build color
+ var c *Color
+ if c, err = newColorFromSSAColor(item); err != nil {
+ err = fmt.Errorf("astisub: building new %s from ssa color %s failed: %w", attr, item, err)
+ 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,
+ ssaStyleFormatNameOutline, ssaStyleFormatNameShadow, ssaStyleFormatNameSpacing:
+ // Parse float
+ var f float64
+ if f, err = strconv.ParseFloat(item, 64); err != nil {
+ err = fmt.Errorf("astisub: parsing float %s failed: %w", item, err)
+ return
+ }
+
+ // Set float
+ switch attr {
+ case ssaStyleFormatNameAlphaLevel:
+ s.alphaLevel = astikit.Float64Ptr(f)
+ case ssaStyleFormatNameAngle:
+ s.angle = astikit.Float64Ptr(f)
+ case ssaStyleFormatNameFontSize:
+ s.fontSize = astikit.Float64Ptr(f)
+ case ssaStyleFormatNameScaleX:
+ s.scaleX = astikit.Float64Ptr(f)
+ case ssaStyleFormatNameScaleY:
+ s.scaleY = astikit.Float64Ptr(f)
+ case ssaStyleFormatNameOutline:
+ s.outline = astikit.Float64Ptr(f)
+ case ssaStyleFormatNameShadow:
+ s.shadow = astikit.Float64Ptr(f)
+ case ssaStyleFormatNameSpacing:
+ s.spacing = astikit.Float64Ptr(f)
+ }
+ // Int
+ case ssaStyleFormatNameAlignment, ssaStyleFormatNameBorderStyle, ssaStyleFormatNameEncoding,
+ ssaStyleFormatNameMarginL, ssaStyleFormatNameMarginR, ssaStyleFormatNameMarginV:
+ // Parse int
+ var i int
+ if i, err = strconv.Atoi(item); err != nil {
+ err = fmt.Errorf("astisub: atoi of %s failed: %w", item, err)
+ return
+ }
+
+ // Set int
+ switch attr {
+ case ssaStyleFormatNameAlignment:
+ s.alignment = astikit.IntPtr(i)
+ case ssaStyleFormatNameBorderStyle:
+ s.borderStyle = astikit.IntPtr(i)
+ case ssaStyleFormatNameEncoding:
+ s.encoding = astikit.IntPtr(i)
+ case ssaStyleFormatNameMarginL:
+ s.marginLeft = astikit.IntPtr(i)
+ case ssaStyleFormatNameMarginR:
+ s.marginRight = astikit.IntPtr(i)
+ case ssaStyleFormatNameMarginV:
+ s.marginVertical = astikit.IntPtr(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,
+ ssaStyleFormatNameOutline, ssaStyleFormatNameShadow, ssaStyleFormatNameSpacing:
+ 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
+ case ssaStyleFormatNameOutline:
+ f = s.outline
+ case ssaStyleFormatNameShadow:
+ f = s.shadow
+ case ssaStyleFormatNameSpacing:
+ f = s.spacing
+ }
+ if f != nil {
+ v = strconv.FormatFloat(*f, 'f', 3, 64)
+ }
+ // Int
+ case ssaStyleFormatNameAlignment, ssaStyleFormatNameBorderStyle, ssaStyleFormatNameEncoding,
+ ssaStyleFormatNameMarginL, ssaStyleFormatNameMarginR, ssaStyleFormatNameMarginV:
+ 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
+ }
+ 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 = fmt.Errorf("astisub: parsing ssa duration %s failed: %w", item, err)
+ 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 = fmt.Errorf("astisub: atoi of %s failed: %w", item, err)
+ return
+ }
+
+ // Set int
+ switch attr {
+ case ssaEventFormatNameLayer:
+ e.layer = astikit.IntPtr(i)
+ case ssaEventFormatNameMarginL:
+ e.marginLeft = astikit.IntPtr(i)
+ case ssaEventFormatNameMarginR:
+ e.marginRight = astikit.IntPtr(i)
+ case ssaEventFormatNameMarginV:
+ e.marginVertical = astikit.IntPtr(i)
+ }
+ // String
+ case ssaEventFormatNameEffect, ssaEventFormatNameName, ssaEventFormatNameStyle, ssaEventFormatNameText:
+ switch attr {
+ case ssaEventFormatNameEffect:
+ e.effect = item
+ case ssaEventFormatNameName:
+ e.name = item
+ case ssaEventFormatNameStyle:
+ // *Default is reserved
+ // http://www.tcax.org/docs/ass-specs.htm
+ if item == "*Default" {
+ e.style = "Default"
+ } else {
+ e.style = item
+ }
+ case ssaEventFormatNameText:
+ e.text = strings.TrimSpace(item)
+ }
+ // Marked
+ case ssaEventFormatNameMarked:
+ if item == "Marked=1" {
+ e.marked = astikit.BoolPtr(true)
+ } else {
+ e.marked = astikit.BoolPtr(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)
+ } else if idxs[0] > 0 {
+ l.Items = append(l.Items, LineItem{Text: s[previousEffectEndOffset:idxs[0]]})
+ }
+ 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 = fmt.Errorf("astisub: writing script info block failed: %w", err)
+ 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 = fmt.Errorf("astisub: writing styles block failed: %w", err)
+ 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 = fmt.Errorf("astisub: writing events block failed: %w", err)
+ return
+ }
+ }
+ return
+}
+
+// SSAOptions
+type SSAOptions struct {
+ OnUnknownSectionName func(name string)
+ OnInvalidLine func(line string)
+}
+
+func defaultSSAOptions() SSAOptions {
+ return SSAOptions{
+ OnUnknownSectionName: func(name string) {
+ log.Printf("astisub: unknown section: %s", name)
+ },
+ OnInvalidLine: func(line string) {
+ log.Printf("astisub: not understood: '%s', ignoring", line)
+ },
+ }
+}