File size: 4,201 Bytes
f8a99c0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
057da11
f8a99c0
 
 
 
 
 
 
d913a07
f8a99c0
d913a07
 
057da11
f8a99c0
d913a07
f8a99c0
 
d913a07
 
f8a99c0
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
import fs from 'fs';
import path from 'path';
import { tempPath, escapeXml } from './helpers.js';

function secToAssTime(s) {
  const sign = s < 0 ? -1 : 1;
  s = Math.abs(s);
  const h = Math.floor(s / 3600);
  const m = Math.floor((s % 3600) / 60);
  const sec = Math.floor(s % 60);
  const cs = Math.floor((s - Math.floor(s)) * 100);
  return `${h}:${String(m).padStart(2, '0')}:${String(sec).padStart(2, '0')}.${String(cs).padStart(2, '0')}`;
}

function assColorFromHex(hex) {
  if (!hex) return '&H00FFFFFF';
  // support simple named colors and rgb/rgba strings
  const named = {
    red: '#FF0000',
    white: '#FFFFFF',
    black: '#000000',
    transparent: '#00000000',
    blue: '#0000FF',
    green: '#00FF00',
    yellow: '#FFFF00',
    gray: '#808080',
    grey: '#808080'
  };
  if (typeof hex === 'string' && !hex.startsWith('#')) {
    const key = hex.toLowerCase();
    if (named[key]) hex = named[key];
    else {
      // try to parse rgb()/rgba()
      const m = hex.match(/rgba?\s*\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([0-9.]+))?\)/i);
      if (m) {
        const r = parseInt(m[1], 10);
        const g = parseInt(m[2], 10);
        const b = parseInt(m[3], 10);
        const a = m[4] !== undefined ? Math.round(parseFloat(m[4]) * 255) : 255;
        const aa = a.toString(16).padStart(2, '0').toUpperCase();
        hex = `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}${aa}`;
      }
    }
  }

  let h = String(hex).replace(/^#/, '');
  // support RRGGBB or RRGGBBAA or short 3-char
  if (h.length === 3) h = h.split('').map(c => c + c).join('');
  if (h.length === 6) h = 'FF' + h; // add alpha FF (opaque) at front
  if (h.length === 8) {
    const a = h.slice(0, 2);
    const r = h.slice(2, 4);
    const g = h.slice(4, 6);
    const b = h.slice(6, 8);
    // ASS uses &HAABBGGRR where AA is alpha (00 opaque)
    // convert alpha from hex (FF opaque) to ASS alpha where 00 opaque -> ASSalpha = (255 - alpha)
    const parsed = parseInt(a, 16);
    const alpha = isNaN(parsed) ? 255 : 255 - parsed;
    const aa = alpha.toString(16).padStart(2, '0').toUpperCase();
    return `&H${aa}${b}${g}${r}`;
  }
  return '&H00FFFFFF';
}

export async function createAssSubtitle(text, opts = {}) {
  const { vw = 1080, vh = 1920, fontName = 'Arial', fontSize = 40, fontColor = '#FFFFFF', fontWeight, boxColor = null, x = vw / 2, y = vh / 2, from = 0, to = 3, alignment = 2 } = opts;
  const safeText = (text == null) ? '' : String(text).replace(/\r/g, '').replace(/\n/g, '\\N').replace(/([{}])/g, '\\$1');
  const assPath = tempPath('bubble', 'ass');
  const styleName = 'BubbleStyle';
  const primary = assColorFromHex(fontColor);
  const back = assColorFromHex(boxColor);
  const bold = ('' + fontWeight).indexOf('7') === 0 || (String(fontWeight) === '700' || String(fontWeight).toLowerCase().includes('bold')) ? '1' : '0';
  const fontSz = Math.round(fontSize);
  const header = `[Script Info]\nTitle: Bubble\nScriptType: v4.00+\nPlayResX: ${vw}\nPlayResY: ${vh}\nScaledBorderAndShadow: yes\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n`;

  // Match public/test.ass: use BorderStyle=3 and reasonable Outline value so BackColour is drawn reliably
  const outlineVal = 6;
  const styleLine = `Style: ${styleName},${fontName},${fontSz},${primary},&H00FFFFFF,${back || '&H00000000'},&H00000000,${bold},0,0,0,100,100,0,0,3,${outlineVal},0,${alignment},10,10,10,1\n\n`;

  const eventsHeader = `[Events]\nFormat: Layer, Start, End, Style, Text\n`;
  const start = secToAssTime(from);
  const end = secToAssTime(to);
  const dialog = `Dialogue: 0,${start},${end},${styleName},{\\pos(${Math.round(x)},${Math.round(y)})}${safeText}\n`;
  const content = header + styleLine + eventsHeader + dialog;
  fs.writeFileSync(assPath, content, 'utf8');
  const meta = { path: assPath };
  return meta;
}

export default createAssSubtitle;