subforge
Version:
High-performance subtitle toolkit for parsing, converting, and authoring across 20+ formats.
118 lines (104 loc) • 4.63 kB
text/typescript
import type { SubtitleDocument, SubtitleEvent, Style } from '../../../core/types.ts'
import { formatTime } from './time.ts'
import { formatColor } from './color.ts'
import { serializeTags } from './tags.ts'
/**
* Serializes a SubtitleDocument into ASS (Advanced SubStation Alpha) format.
*
* This function converts a subtitle document back into the ASS text format,
* including all sections: Script Info, V4+ Styles, Events, Fonts, and Graphics.
* The output is compatible with subtitle renderers and editors that support ASS.
*
* @param doc - The subtitle document to serialize
* @returns The complete ASS file content as a string
*
* @example
* ```ts
* const doc = parseASS(assContent)
* // Modify the document...
* doc.events[0].text = "Modified text"
* // Serialize back to ASS
* const newAssContent = toASS(doc)
* await Bun.write('output.ass', newAssContent)
* ```
*/
export function toASS(doc: SubtitleDocument): string {
let result = '[Script Info]\n'
if (doc.info.title) result += 'Title: ' + doc.info.title + '\n'
if (doc.info.author) result += 'Original Author: ' + doc.info.author + '\n'
result += 'PlayResX: ' + doc.info.playResX + '\n'
result += 'PlayResY: ' + doc.info.playResY + '\n'
result += 'ScaledBorderAndShadow: ' + (doc.info.scaleBorderAndShadow ? 'yes' : 'no') + '\n'
result += 'WrapStyle: ' + doc.info.wrapStyle + '\n\n'
result += '[V4+ Styles]\n'
result += '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'
const styleValues = Array.from(doc.styles.values())
const styleLen = styleValues.length
for (let i = 0; i < styleLen; i++) {
result += serializeStyle(styleValues[i]!) + '\n'
}
result += '\n'
result += '[Events]\n'
result += 'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n'
const comments = doc.comments
const commentLen = comments.length
const events = doc.events
const eventLen = events.length
let eventIndex = 0
for (let j = 0; j < commentLen; j++) {
if (comments[j]!.beforeEventIndex === eventIndex) {
result += 'Comment: 0,0:00:00.00,0:00:00.00,Default,,0,0,0,,' + comments[j]!.text + '\n'
}
}
for (let i = 0; i < eventLen; i++) {
for (let j = 0; j < commentLen; j++) {
if (comments[j]!.beforeEventIndex === eventIndex) {
result += 'Comment: 0,0:00:00.00,0:00:00.00,Default,,0,0,0,,' + comments[j]!.text + '\n'
}
}
result += serializeEvent(events[i]!) + '\n'
eventIndex++
}
result += '\n'
if (doc.fonts && doc.fonts.length > 0) {
result += '[Fonts]\n'
const fonts = doc.fonts
const fontLen = fonts.length
for (let i = 0; i < fontLen; i++) {
const font = fonts[i]!
result += 'fontname: ' + font.name + '\n'
const chunkSize = 80
const dataLen = font.data.length
for (let j = 0; j < dataLen; j += chunkSize) {
result += font.data.slice(j, j + chunkSize) + '\n'
}
}
result += '\n'
}
if (doc.graphics && doc.graphics.length > 0) {
result += '[Graphics]\n'
const graphics = doc.graphics
const graphicLen = graphics.length
for (let i = 0; i < graphicLen; i++) {
const graphic = graphics[i]!
result += 'filename: ' + graphic.name + '\n'
const chunkSize = 80
const dataLen = graphic.data.length
for (let j = 0; j < dataLen; j += chunkSize) {
result += graphic.data.slice(j, j + chunkSize) + '\n'
}
}
result += '\n'
}
return result
}
function serializeStyle(style: Style): string {
return `Style: ${style.name},${style.fontName},${style.fontSize},${formatColor(style.primaryColor)},${formatColor(style.secondaryColor)},${formatColor(style.outlineColor)},${formatColor(style.backColor)},${style.bold ? -1 : 0},${style.italic ? -1 : 0},${style.underline ? -1 : 0},${style.strikeout ? -1 : 0},${style.scaleX},${style.scaleY},${style.spacing},${style.angle},${style.borderStyle},${style.outline},${style.shadow},${style.alignment},${style.marginL},${style.marginR},${style.marginV},${style.encoding}`
}
function serializeEvent(event: SubtitleEvent): string {
const text = event.dirty && event.segments.length > 0
? serializeTags(event.segments)
: event.text
const effect = event.effect ?? ''
return `Dialogue: ${event.layer},${formatTime(event.start)},${formatTime(event.end)},${event.style},${event.actor},${event.marginL},${event.marginR},${event.marginV},${effect},${text}`
}