UNPKG

subforge

Version:

High-performance subtitle toolkit for parsing, converting, and authoring across 20+ formats.

426 lines (393 loc) 12.6 kB
/** * CEA-608 Closed Caption decoding tables and utilities. * * CEA-608 is the standard for closed captioning in North America, defining how caption data * is encoded and transmitted. This module provides decoding/encoding functionality for CEA-608 * commands including control codes, special characters, and text data. * * Reference: CEA-608-E standard */ /** * CEA-608 control codes for caption manipulation. * * These control codes manage caption display, scrolling, positioning, and memory. * Each code is a 16-bit value (two bytes) that triggers specific caption behaviors. */ export const CONTROL_CODES = { // Caption control RCL: 0x9420, // Resume caption loading BS: 0x9421, // Backspace AOF: 0x9422, // Reserved (formerly Alarm Off) AON: 0x9423, // Reserved (formerly Alarm On) DER: 0x9424, // Delete to end of row RU2: 0x9425, // Roll-up 2 rows RU3: 0x9426, // Roll-up 3 rows RU4: 0x9427, // Roll-up 4 rows FON: 0x9428, // Flash on RDC: 0x9429, // Resume direct captioning TR: 0x942a, // Text restart RTD: 0x942b, // Resume text display EDM: 0x942c, // Erase displayed memory CR: 0x942d, // Carriage return ENM: 0x942e, // Erase non-displayed memory EOC: 0x942f, // End of caption // Tab offsets (col 0-3) TO1: 0x9721, // Tab offset 1 column TO2: 0x9722, // Tab offset 2 columns TO3: 0x9723, // Tab offset 3 columns } as const export const CONTROL_CODE_NAMES: { [code: number]: string } = {} for (const [name, value] of Object.entries(CONTROL_CODES)) { CONTROL_CODE_NAMES[value] = name } /** * Preamble Address Codes (PAC) mapping codes to row numbers. * * PAC commands position the cursor on the screen, specifying both row and column. * Each code maps to a specific row number (1-15) on the caption display. * Format includes row + column/style information encoded in the byte pair. */ export const PAC_ROWS: { [code: number]: number } = { 0x9140: 1, 0x9160: 1, 0x91e0: 1, 0x9240: 1, 0x9260: 1, 0x92e0: 1, 0x9340: 2, 0x9360: 2, 0x93e0: 2, 0x1540: 2, 0x1560: 2, 0x15e0: 2, 0x9440: 3, 0x9460: 3, 0x94e0: 3, 0x1640: 3, 0x1660: 3, 0x16e0: 3, 0x9740: 4, 0x9760: 4, 0x97e0: 4, 0x9840: 4, 0x9860: 4, 0x98e0: 4, 0x1040: 5, 0x1060: 5, 0x10e0: 5, 0x1340: 5, 0x1360: 5, 0x13e0: 5, 0x1340: 6, 0x1360: 6, 0x13e0: 6, 0x1440: 6, 0x1460: 6, 0x14e0: 6, 0x9540: 7, 0x9560: 7, 0x95e0: 7, 0x1740: 7, 0x1760: 7, 0x17e0: 7, 0x1040: 8, 0x1060: 8, 0x10e0: 8, 0x1940: 8, 0x1960: 8, 0x19e0: 8, 0x9140: 9, 0x9160: 9, 0x91e0: 9, 0x1a40: 9, 0x1a60: 9, 0x1ae0: 9, 0x9240: 10, 0x9260: 10, 0x92e0: 10, 0x1b40: 10, 0x1b60: 10, 0x1be0: 10, 0x9540: 11, 0x9560: 11, 0x95e0: 11, 0x1040: 11, 0x1060: 11, 0x10e0: 11, 0x1340: 12, 0x1360: 12, 0x13e0: 12, 0x1c40: 12, 0x1c60: 12, 0x1ce0: 12, 0x9440: 13, 0x9460: 13, 0x94e0: 13, 0x1d40: 13, 0x1d60: 13, 0x1de0: 13, 0x9740: 14, 0x9760: 14, 0x97e0: 14, 0x1e40: 14, 0x1e60: 14, 0x1ee0: 14, 0x1040: 15, 0x1060: 15, 0x10e0: 15, 0x1f40: 15, 0x1f60: 15, 0x1fe0: 15, } /** * Decodes a basic ASCII character from CEA-608 byte. * * Basic character set uses standard ASCII mapping for printable characters (0x20-0x7F). * * @param byte - Byte value to decode * @returns Decoded character string, or empty string if byte is outside valid range */ export function decodeBasicChar(byte: number): string { if (byte >= 0x20 && byte <= 0x7F) { return String.fromCharCode(byte) } return '' } /** * Special character mapping for extended CEA-608 character set. * * Includes accented characters, symbols, currency signs, and other special characters * not available in the basic ASCII set. Covers multiple language support including * Spanish, French, Portuguese, and German characters. * * Code ranges: 0x91-0x92, 0x93-0x94, 0x9a-0x9b */ export const SPECIAL_CHARS: { [code: number]: string } = { 0x9130: '®', // Registered mark 0x9131: '°', // Degree sign 0x9132: '½', // 1/2 0x9133: '¿', // Inverted question mark 0x9134: '™', // Trademark 0x9135: '¢', // Cent sign 0x9136: '£', // Pound sterling 0x9137: '♪', // Music note 0x9138: 'à', // a grave 0x9139: ' ', // Transparent space 0x913a: 'è', // e grave 0x913b: 'â', // a circumflex 0x913c: 'ê', // e circumflex 0x913d: 'î', // i circumflex 0x913e: 'ô', // o circumflex 0x913f: 'û', // u circumflex 0x9230: 'Á', // A acute 0x9231: 'É', // E acute 0x9232: 'Ó', // O acute 0x9233: 'Ú', // U acute 0x9234: 'Ü', // U umlaut 0x9235: 'ü', // u umlaut 0x9236: '\u2018', // Opening single quote 0x9237: '¡', // Inverted exclamation 0x9238: '*', // Asterisk 0x9239: '\u2019', // Closing single quote (apostrophe) 0x923a: '—', // Em dash 0x923b: '©', // Copyright 0x923c: '℠', // Service mark 0x923d: '•', // Round bullet 0x923e: '\u201c', // Opening double quote 0x923f: '\u201d', // Closing double quote // Extended Spanish/French 0x9a20: 'Á', 0x9a21: 'É', 0x9a22: 'Ó', 0x9a23: 'Ú', 0x9a24: 'Ü', 0x9a25: 'ü', 0x9a26: '\u2018', 0x9a27: '¡', 0x9a28: '*', 0x9a29: '\u2019', 0x9a2a: '—', 0x9a2b: '©', 0x9a2c: '℠', 0x9a2d: '•', 0x9a2e: '\u201c', 0x9a2f: '\u201d', // Extended Portuguese/German 0x9a30: 'À', 0x9a31: 'Â', 0x9a32: 'Ç', 0x9a33: 'È', 0x9a34: 'Ê', 0x9a35: 'Ë', 0x9a36: 'ë', 0x9a37: 'Î', 0x9a38: 'Ï', 0x9a39: 'ï', 0x9a3a: 'Ô', 0x9a3b: 'Ù', 0x9a3c: 'ù', 0x9a3d: 'Û', 0x9a3e: '«', 0x9a3f: '»', // Extended Portuguese/German continued 0x9b20: 'Ã', 0x9b21: 'ã', 0x9b22: 'Í', 0x9b23: 'Ì', 0x9b24: 'ì', 0x9b25: 'Ò', 0x9b26: 'ò', 0x9b27: 'Õ', 0x9b28: 'õ', 0x9b29: '{', 0x9b2a: '}', 0x9b2b: '\\', 0x9b2c: '^', 0x9b2d: '_', 0x9b2e: '¦', 0x9b2f: '~', // Extended French 0x9b30: 'Ä', 0x9b31: 'ä', 0x9b32: 'Ö', 0x9b33: 'ö', 0x9b34: 'ß', 0x9b35: '¥', 0x9b36: '¤', 0x9b37: '│', 0x9b38: 'Å', 0x9b39: 'å', 0x9b3a: 'Ø', 0x9b3b: 'ø', 0x9b3c: '┌', 0x9b3d: '┐', 0x9b3e: '└', 0x9b3f: '┘', } /** * Mid-row codes for changing text styling within a caption row. * * These codes allow style changes (color, italic, underline) to occur in the middle * of a caption line without repositioning the cursor or starting a new line. */ export const MID_ROW_CODES: { [code: number]: { style?: string; underline?: boolean } } = { 0x9120: { style: 'white' }, 0x9121: { style: 'white', underline: true }, 0x9122: { style: 'green' }, 0x9123: { style: 'green', underline: true }, 0x9124: { style: 'blue' }, 0x9125: { style: 'blue', underline: true }, 0x9126: { style: 'cyan' }, 0x9127: { style: 'cyan', underline: true }, 0x9128: { style: 'red' }, 0x9129: { style: 'red', underline: true }, 0x912a: { style: 'yellow' }, 0x912b: { style: 'yellow', underline: true }, 0x912c: { style: 'magenta' }, 0x912d: { style: 'magenta', underline: true }, 0x912e: { style: 'italic' }, 0x912f: { style: 'italic', underline: true }, } /** * Control command for caption flow and display management. */ export interface ControlCommand { /** Command type identifier */ type: 'control' /** 16-bit command code */ code: number /** Human-readable command name (e.g., 'RCL', 'EOC', 'CR') */ name: string } /** * Preamble Address Code command for cursor positioning. */ export interface PACCommand { /** Command type identifier */ type: 'pac' /** Target row number (1-15) */ row: number /** Target column position */ column: number /** Whether text should be underlined */ underline: boolean /** Optional text color */ color?: string } /** * Mid-row code command for inline style changes. */ export interface MidRowCommand { /** Command type identifier */ type: 'midrow' /** Style name (e.g., 'white', 'italic', 'red') */ style?: string /** Whether text should be underlined */ underline?: boolean } /** * Character data command containing displayable text. */ export interface CharCommand { /** Command type identifier */ type: 'char' /** Decoded character(s) to display */ text: string } /** * Union type representing all possible CEA-608 commands. */ export type CEA608Command = ControlCommand | PACCommand | MidRowCommand | CharCommand /** * Decodes a CEA-608 byte pair into a command object. * * CEA-608 data is transmitted as byte pairs (16-bit values). This function interprets * the byte pair and returns the appropriate command type (control, PAC, mid-row, or character). * * @param b1 - First byte (high byte) * @param b2 - Second byte (low byte) * @returns Decoded command object, or null if the byte pair represents padding/unknown data * * @example * ```ts * // Decode Resume Caption Loading command * const cmd = decodeCEA608(0x94, 0x20); * // Returns: { type: 'control', code: 0x9420, name: 'RCL' } * * // Decode character data * const chars = decodeCEA608(0x48, 0x69); * // Returns: { type: 'char', text: 'Hi' } * ``` */ export function decodeCEA608(b1: number, b2: number): CEA608Command | null { const code = (b1 << 8) | b2 // Check for control codes if ((b1 & 0xf0) === 0x90 && b1 >= 0x94 && b1 <= 0x97) { const name = CONTROL_CODE_NAMES[code] if (name) return { type: 'control', code, name } } // Check for PAC (preamble address codes) if (PAC_ROWS[code] !== undefined) { const row = PAC_ROWS[code]! const column = (b2 & 0x10) ? ((b2 & 0x0f) * 4) : 0 const underline = (b2 & 0x01) === 1 return { type: 'pac', row, column, underline } } // Check for mid-row codes if (MID_ROW_CODES[code]) { return { type: 'midrow', ...MID_ROW_CODES[code] } } // Check for special characters if (SPECIAL_CHARS[code]) { return { type: 'char', text: SPECIAL_CHARS[code]! } } // Check for basic characters (both bytes should be valid ASCII range) if (b1 >= 0x20 && b1 <= 0x7f && b2 >= 0x20 && b2 <= 0x7f) { return { type: 'char', text: String.fromCharCode(b1) + String.fromCharCode(b2) } } // Check for single character if (b1 >= 0x20 && b1 <= 0x7f && b2 === 0x00) { return { type: 'char', text: String.fromCharCode(b1) } } // Unknown or padding return null } // Reverse lookup map: character -> CEA-608 code (built once) const CHAR_TO_CODE: Map<string, number> = new Map() for (const [codeStr, char] of Object.entries(SPECIAL_CHARS)) { CHAR_TO_CODE.set(char, parseInt(codeStr)) } /** * Encodes text string into CEA-608 byte pairs. * * Converts a text string into an array of bytes suitable for CEA-608 transmission. * Handles both basic ASCII characters and special characters, pairing characters * when possible for efficient encoding. * * @param text - Text string to encode * @returns Array of bytes representing the encoded text (pairs of bytes) * * @example * ```ts * const bytes = encodeCEA608Text('Hello'); * // Returns array of byte pairs for each character * * const specialBytes = encodeCEA608Text('© 2024'); * // Handles special characters like copyright symbol * ``` */ export function encodeCEA608Text(text: string): number[] { const bytes: number[] = [] const len = text.length for (let i = 0; i < len; i++) { const char = text[i]! const charCode = char.charCodeAt(0) // Check special characters via O(1) map lookup const specialCode = CHAR_TO_CODE.get(char) if (specialCode !== undefined) { bytes[bytes.length] = (specialCode >> 8) & 0xff bytes[bytes.length] = specialCode & 0xff continue } // Basic ASCII if (charCode >= 0x20 && charCode <= 0x7f) { // Pair characters when possible if (i + 1 < len) { const nextCode = text.charCodeAt(i + 1) if (nextCode >= 0x20 && nextCode <= 0x7f) { bytes[bytes.length] = charCode bytes[bytes.length] = nextCode i++ continue } } bytes[bytes.length] = charCode bytes[bytes.length] = 0x00 } } return bytes } /** * Retrieves the byte pair for a named control code. * * Looks up a control code by name and returns its two-byte representation * for encoding into SCC format. * * @param name - Control code name (e.g., 'RCL', 'EOC', 'CR') * @returns Tuple of [high byte, low byte] * * @example * ```ts * const [b1, b2] = getControlCode('EOC'); * // Returns [0x94, 0x2f] for End of Caption command * ``` */ export function getControlCode(name: keyof typeof CONTROL_CODES): [number, number] { const code = CONTROL_CODES[name] return [(code >> 8) & 0xff, code & 0xff] }