534 lines
14 KiB
Go
534 lines
14 KiB
Go
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)
|
||
}
|