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 }