sub-cli/internal/format/ass/ass.go

534 lines
14 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package ass
import (
"bufio"
"fmt"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"sub-cli/internal/model"
)
// 常量定义
const (
ASSHeader = "[Script Info]"
ASSStylesHeader = "[V4+ Styles]"
ASSEventsHeader = "[Events]"
DefaultFormat = "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text"
)
// Parse 解析ASS文件为ASSFile结构
func Parse(filePath string) (model.ASSFile, error) {
file, err := os.Open(filePath)
if err != nil {
return model.ASSFile{}, fmt.Errorf("打开ASS文件失败: %w", err)
}
defer file.Close()
result := model.NewASSFile()
scanner := bufio.NewScanner(file)
// 当前解析的区块
currentSection := ""
var styleFormat, eventFormat []string
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, ";") {
// 跳过空行和注释行
continue
}
// 检查章节标题
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
currentSection = line
continue
}
switch currentSection {
case ASSHeader:
// 解析脚本信息
if strings.Contains(line, ":") {
parts := strings.SplitN(line, ":", 2)
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
result.ScriptInfo[key] = value
}
case ASSStylesHeader:
// 解析样式格式行和样式定义
if strings.HasPrefix(line, "Format:") {
formatStr := strings.TrimPrefix(line, "Format:")
styleFormat = parseFormatLine(formatStr)
} else if strings.HasPrefix(line, "Style:") {
styleValues := parseStyleLine(line)
if len(styleFormat) > 0 && len(styleValues) > 0 {
style := model.ASSStyle{
Name: styleValues[0], // 第一个值通常是样式名称
Properties: make(map[string]string),
}
// 将原始格式行保存下来
style.Properties["Format"] = "Name, " + strings.Join(styleFormat[1:], ", ")
style.Properties["Style"] = strings.Join(styleValues, ", ")
// 解析各个样式属性
for i := 0; i < len(styleFormat) && i < len(styleValues); i++ {
style.Properties[styleFormat[i]] = styleValues[i]
}
result.Styles = append(result.Styles, style)
}
}
case ASSEventsHeader:
// 解析事件格式行和对话行
if strings.HasPrefix(line, "Format:") {
formatStr := strings.TrimPrefix(line, "Format:")
eventFormat = parseFormatLine(formatStr)
} else if len(eventFormat) > 0 &&
(strings.HasPrefix(line, "Dialogue:") ||
strings.HasPrefix(line, "Comment:")) {
eventType := "Dialogue"
if strings.HasPrefix(line, "Comment:") {
eventType = "Comment"
line = strings.TrimPrefix(line, "Comment:")
} else {
line = strings.TrimPrefix(line, "Dialogue:")
}
values := parseEventLine(line)
if len(values) >= len(eventFormat) {
event := model.NewASSEvent()
event.Type = eventType
// 填充事件属性
for i, format := range eventFormat {
value := values[i]
switch strings.TrimSpace(format) {
case "Layer":
layer, _ := strconv.Atoi(value)
event.Layer = layer
case "Start":
event.StartTime = parseASSTimestamp(value)
case "End":
event.EndTime = parseASSTimestamp(value)
case "Style":
event.Style = value
case "Name":
event.Name = value
case "MarginL":
marginL, _ := strconv.Atoi(value)
event.MarginL = marginL
case "MarginR":
marginR, _ := strconv.Atoi(value)
event.MarginR = marginR
case "MarginV":
marginV, _ := strconv.Atoi(value)
event.MarginV = marginV
case "Effect":
event.Effect = value
case "Text":
// 文本可能包含逗号,所以需要特殊处理
textStartIndex := strings.Index(line, value)
if textStartIndex >= 0 {
event.Text = line[textStartIndex:]
} else {
event.Text = value
}
}
}
result.Events = append(result.Events, event)
}
}
}
}
if err := scanner.Err(); err != nil {
return model.ASSFile{}, fmt.Errorf("读取ASS文件失败: %w", err)
}
return result, nil
}
// Generate 生成ASS文件
func Generate(assFile model.ASSFile, filePath string) error {
// 确保目录存在
dir := filepath.Dir(filePath)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("创建目录失败: %w", err)
}
file, err := os.Create(filePath)
if err != nil {
return fmt.Errorf("创建ASS文件失败: %w", err)
}
defer file.Close()
writer := bufio.NewWriter(file)
// 写入脚本信息
writer.WriteString(ASSHeader + "\n")
for key, value := range assFile.ScriptInfo {
writer.WriteString(fmt.Sprintf("%s: %s\n", key, value))
}
writer.WriteString("\n")
// 写入样式信息
writer.WriteString(ASSStylesHeader + "\n")
if len(assFile.Styles) > 0 {
// 获取样式格式
format := "Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding"
if style := assFile.Styles[0]; style.Properties["Format"] != "" {
format = "Format: " + style.Properties["Format"]
}
writer.WriteString(format + "\n")
// 写入各个样式
for _, style := range assFile.Styles {
if style.Properties["Style"] != "" {
writer.WriteString("Style: " + style.Properties["Style"] + "\n")
} else {
// 手动构造样式行
writer.WriteString(fmt.Sprintf("Style: %s,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,2,10,10,10,1\n", style.Name))
}
}
} else {
// 写入默认样式
writer.WriteString("Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n")
writer.WriteString("Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,2,10,10,10,1\n")
}
writer.WriteString("\n")
// 写入事件信息
writer.WriteString(ASSEventsHeader + "\n")
writer.WriteString(DefaultFormat + "\n")
// 写入各个对话行
for _, event := range assFile.Events {
startTime := formatASSTimestamp(event.StartTime)
endTime := formatASSTimestamp(event.EndTime)
line := fmt.Sprintf("%s: %d,%s,%s,%s,%s,%d,%d,%d,%s,%s\n",
event.Type,
event.Layer,
startTime,
endTime,
event.Style,
event.Name,
event.MarginL,
event.MarginR,
event.MarginV,
event.Effect,
event.Text)
writer.WriteString(line)
}
return writer.Flush()
}
// Format 格式化ASS文件
func Format(filePath string) error {
// 解析文件
assFile, err := Parse(filePath)
if err != nil {
return err
}
// 重新生成文件
return Generate(assFile, filePath)
}
// ConvertToSubtitle 将ASS文件转换为通用字幕格式
func ConvertToSubtitle(filePath string) (model.Subtitle, error) {
assFile, err := Parse(filePath)
if err != nil {
return model.Subtitle{}, err
}
subtitle := model.NewSubtitle()
subtitle.Format = "ass"
// 复制脚本信息到元数据
for key, value := range assFile.ScriptInfo {
subtitle.Metadata[key] = value
}
// 复制样式信息到FormatData
styleMap := make(map[string]model.ASSStyle)
for _, style := range assFile.Styles {
styleMap[style.Name] = style
}
subtitle.FormatData["styles"] = styleMap
// 转换事件到字幕条目
for i, event := range assFile.Events {
entry := model.NewSubtitleEntry()
entry.Index = i + 1
entry.StartTime = event.StartTime
entry.EndTime = event.EndTime
entry.Text = event.Text
// 保存ASS特定属性到FormatData
eventData := make(map[string]interface{})
eventData["type"] = event.Type
eventData["layer"] = event.Layer
eventData["style"] = event.Style
eventData["name"] = event.Name
eventData["marginL"] = event.MarginL
eventData["marginR"] = event.MarginR
eventData["marginV"] = event.MarginV
eventData["effect"] = event.Effect
entry.FormatData["ass"] = eventData
// 设置基本样式属性
if style, ok := styleMap[event.Style]; ok {
if bold, exists := style.Properties["Bold"]; exists && bold == "1" {
entry.Styles["bold"] = "true"
}
if italic, exists := style.Properties["Italic"]; exists && italic == "1" {
entry.Styles["italic"] = "true"
}
if underline, exists := style.Properties["Underline"]; exists && underline == "1" {
entry.Styles["underline"] = "true"
}
}
subtitle.Entries = append(subtitle.Entries, entry)
}
return subtitle, nil
}
// ConvertFromSubtitle 将通用字幕格式转换为ASS文件
func ConvertFromSubtitle(subtitle model.Subtitle, filePath string) error {
assFile := model.NewASSFile()
// 复制元数据到脚本信息
for key, value := range subtitle.Metadata {
assFile.ScriptInfo[key] = value
}
// 添加标题(如果有)
if subtitle.Title != "" {
assFile.ScriptInfo["Title"] = subtitle.Title
}
// 从FormatData恢复样式如果有
if styles, ok := subtitle.FormatData["styles"].(map[string]model.ASSStyle); ok {
for _, style := range styles {
assFile.Styles = append(assFile.Styles, style)
}
}
// 转换字幕条目到ASS事件
for _, entry := range subtitle.Entries {
event := model.NewASSEvent()
event.StartTime = entry.StartTime
event.EndTime = entry.EndTime
event.Text = entry.Text
// 从FormatData恢复ASS特定属性如果有
if assData, ok := entry.FormatData["ass"].(map[string]interface{}); ok {
if eventType, ok := assData["type"].(string); ok {
event.Type = eventType
}
if layer, ok := assData["layer"].(int); ok {
event.Layer = layer
}
if style, ok := assData["style"].(string); ok {
event.Style = style
}
if name, ok := assData["name"].(string); ok {
event.Name = name
}
if marginL, ok := assData["marginL"].(int); ok {
event.MarginL = marginL
}
if marginR, ok := assData["marginR"].(int); ok {
event.MarginR = marginR
}
if marginV, ok := assData["marginV"].(int); ok {
event.MarginV = marginV
}
if effect, ok := assData["effect"].(string); ok {
event.Effect = effect
}
} else {
// 根据基本样式设置ASS样式
if _, ok := entry.Styles["bold"]; ok {
// 创建一个加粗样式(如果尚未存在)
styleName := "Bold"
found := false
for _, style := range assFile.Styles {
if style.Name == styleName {
found = true
break
}
}
if !found {
boldStyle := model.ASSStyle{
Name: styleName,
Properties: map[string]string{
"Bold": "1",
},
}
assFile.Styles = append(assFile.Styles, boldStyle)
}
event.Style = styleName
}
if _, ok := entry.Styles["italic"]; ok {
// 创建一个斜体样式(如果尚未存在)
styleName := "Italic"
found := false
for _, style := range assFile.Styles {
if style.Name == styleName {
found = true
break
}
}
if !found {
italicStyle := model.ASSStyle{
Name: styleName,
Properties: map[string]string{
"Italic": "1",
},
}
assFile.Styles = append(assFile.Styles, italicStyle)
}
event.Style = styleName
}
if _, ok := entry.Styles["underline"]; ok {
// 创建一个下划线样式(如果尚未存在)
styleName := "Underline"
found := false
for _, style := range assFile.Styles {
if style.Name == styleName {
found = true
break
}
}
if !found {
underlineStyle := model.ASSStyle{
Name: styleName,
Properties: map[string]string{
"Underline": "1",
},
}
assFile.Styles = append(assFile.Styles, underlineStyle)
}
event.Style = styleName
}
}
assFile.Events = append(assFile.Events, event)
}
// 生成ASS文件
return Generate(assFile, filePath)
}
// 辅助函数
// parseFormatLine 解析格式行中的各个字段
func parseFormatLine(formatStr string) []string {
fields := strings.Split(formatStr, ",")
result := make([]string, 0, len(fields))
for _, field := range fields {
result = append(result, strings.TrimSpace(field))
}
return result
}
// parseStyleLine 解析样式行
func parseStyleLine(line string) []string {
// 去掉"Style:"前缀
styleStr := strings.TrimPrefix(line, "Style:")
return splitCSV(styleStr)
}
// parseEventLine 解析事件行
func parseEventLine(line string) []string {
return splitCSV(line)
}
// splitCSV 拆分CSV格式的字符串但保留Text字段中的逗号
func splitCSV(line string) []string {
var result []string
inText := false
current := ""
for _, char := range line {
if char == ',' && !inText {
result = append(result, strings.TrimSpace(current))
current = ""
} else {
current += string(char)
// 这是个简化处理实际ASS格式更复杂
// 当处理到足够数量的字段后剩余部分都当作Text字段
if len(result) >= 9 {
inText = true
}
}
}
if current != "" {
result = append(result, strings.TrimSpace(current))
}
return result
}
// parseASSTimestamp 解析ASS格式的时间戳 (h:mm:ss.cc)
func parseASSTimestamp(timeStr string) model.Timestamp {
// 匹配 h:mm:ss.cc 格式
re := regexp.MustCompile(`(\d+):(\d+):(\d+)\.(\d+)`)
matches := re.FindStringSubmatch(timeStr)
if len(matches) == 5 {
hours, _ := strconv.Atoi(matches[1])
minutes, _ := strconv.Atoi(matches[2])
seconds, _ := strconv.Atoi(matches[3])
// ASS使用厘秒1/100秒需要转换为毫秒
centiseconds, _ := strconv.Atoi(matches[4])
milliseconds := centiseconds * 10
return model.Timestamp{
Hours: hours,
Minutes: minutes,
Seconds: seconds,
Milliseconds: milliseconds,
}
}
// 返回零时间戳,如果解析失败
return model.Timestamp{}
}
// formatASSTimestamp 格式化为ASS格式的时间戳 (h:mm:ss.cc)
func formatASSTimestamp(timestamp model.Timestamp) string {
// ASS使用厘秒1/100秒
centiseconds := timestamp.Milliseconds / 10
return fmt.Sprintf("%d:%02d:%02d.%02d",
timestamp.Hours,
timestamp.Minutes,
timestamp.Seconds,
centiseconds)
}