@nxtedition/lib 22.0.12 → 22.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/ass.js +129 -31
  2. package/package.json +2 -1
package/ass.js CHANGED
@@ -1,36 +1,126 @@
1
+ // @ts-check
1
2
  import fp from 'lodash/fp.js'
3
+ import { wordwrap } from './wordwrap.js'
2
4
 
3
5
  const BASE_WIDTH = 1920
4
6
  const BASE_HEIGHT = 1080
5
7
 
6
- export function encodeASS(events, { styles = {}, width = BASE_WIDTH, height = BASE_HEIGHT } = {}) {
8
+ /**
9
+ * @typedef {object} SubtitleEvent
10
+ * @property {number} start
11
+ * @property {number | null} [duration]
12
+ * @property {number | null} [end]
13
+ * @property {string | null} [style]
14
+ * @property {string | null} [text]
15
+ *
16
+ * @typedef {object} Styles
17
+ * @property {string} [name]
18
+ * @property {string} [fontname]
19
+ * @property {string} [fontsize]
20
+ * @property {string} [primaryColour]
21
+ * @property {string} [secondaryColour]
22
+ * @property {string} [outlineColour]
23
+ * @property {string} [backColour]
24
+ * @property {string} [bold]
25
+ * @property {string} [italic]
26
+ * @property {string} [underline]
27
+ * @property {string} [strikeOut]
28
+ * @property {string} [scaleX]
29
+ * @property {string} [scaleY]
30
+ * @property {string} [spacing]
31
+ * @property {string} [angle]
32
+ * @property {string} [borderStyle]
33
+ * @property {string} [outline]
34
+ * @property {string} [shadow]
35
+ * @property {string} [alignment]
36
+ * @property {string} [marginL]
37
+ * @property {string} [marginR]
38
+ * @property {string} [marginV]
39
+ * @property {string} [encoding]
40
+ *
41
+ * @typedef {object} Options
42
+ * @property {{ [styleName: string]: Styles }} [styles]
43
+ * @property {number} [width]
44
+ * @property {number} [height]
45
+ * @property {boolean} [scaledBorderAndShadow] TODO: default to true when all client styles depend on it?
46
+ * @property {boolean} [futureWordWrapping]
47
+ */
48
+
49
+ /**
50
+ * @param {SubtitleEvent[]} events
51
+ * @param {Options} [options]
52
+ * @returns {string}
53
+ */
54
+ export function encodeASS(
55
+ events,
56
+ {
57
+ styles = {},
58
+ width = BASE_WIDTH,
59
+ height = BASE_HEIGHT,
60
+ scaledBorderAndShadow = false,
61
+ futureWordWrapping = false,
62
+ } = {},
63
+ ) {
64
+ const scale = typeof width === 'number' ? BASE_WIDTH / width : 1
65
+ const playResX = scale * (width || BASE_WIDTH)
66
+ const playResY = scale * (height || BASE_HEIGHT)
7
67
  return [
8
- encASSHeader({ width, height }),
9
- encASSStyles({ styles, width, height }),
10
- encASSEvents(events),
68
+ encASSHeader({ playResX, playResY, scaledBorderAndShadow, futureWordWrapping }),
69
+ encASSStyles(styles),
70
+ encASSEvents(events, styles, playResX, futureWordWrapping),
11
71
  ].join('\n')
12
72
  }
13
73
 
14
- function formatDialogues(events) {
74
+ /**
75
+ * @param {SubtitleEvent[]} events
76
+ * @param {{ [styleName: string]: Styles }} styles
77
+ * @param {number} scriptResX
78
+ * @param {boolean} futureWordWrapping
79
+ */
80
+ function formatDialogues(events, styles, scriptResX, futureWordWrapping) {
15
81
  let s = ''
16
82
  for (const { start, duration, end = duration != null ? start + duration : null, text, style } of [
17
83
  ...events,
18
84
  ].sort((a, b) => a.start - b.start)) {
85
+ const styleName = style || 'nxt-default'
86
+ const eventStyle = styles[styleName]
87
+ const fontSize = parseFloat(eventStyle.fontsize || '32')
88
+ const fontFamily = eventStyle.fontname ?? 'Arial'
89
+ const maxWidth = Math.max(
90
+ 0,
91
+ scriptResX - parseFloat(eventStyle.marginL || '0') - parseFloat(eventStyle.marginR || '0'),
92
+ )
19
93
  if (typeof text === 'string' && text.length > 0 && Number.isFinite(start)) {
20
94
  s += `Dialogue: 0,${formatASSTime(start) || '0:00:00.00'},${
21
95
  formatASSTime(end) || '9:59:59.00'
22
- },${style || 'nxt-default'},,0000,0000,0000,,${(text || '').replace(/\\n|\n/, '\\N')}\n`
96
+ },${styleName},,0000,0000,0000,,${text
97
+ .split(/\\n|\n|\\N/)
98
+ .map((line) => line.trim())
99
+ .flatMap((line) =>
100
+ futureWordWrapping ? wordwrap(line, fontSize, fontFamily, maxWidth) : [line],
101
+ )
102
+ .map((line) => line.trim())
103
+ .join('\\N')}`
23
104
  }
24
105
  }
25
106
  return s
26
107
  }
27
108
 
28
- function encASSEvents(events) {
109
+ /**
110
+ * @param {SubtitleEvent[]} events
111
+ * @param {{ [styleName: string]: Styles }} styles
112
+ * @param {number} scriptResX
113
+ * @param {boolean} futureWordWrapping
114
+ */
115
+ function encASSEvents(events, styles, scriptResX, futureWordWrapping) {
29
116
  return `[Events]
30
117
  Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
31
- ${formatDialogues(events)}`
118
+ ${formatDialogues(events, styles, scriptResX, futureWordWrapping)}`
32
119
  }
33
120
 
121
+ /**
122
+ * @param {{ [styleName: string]: Styles }} styles
123
+ */
34
124
  const formatStyles = fp.pipe(
35
125
  fp.entries,
36
126
  fp.reduce((s, [id, style]) => {
@@ -87,40 +177,48 @@ const formatStyles = fp.pipe(
87
177
  }, ''),
88
178
  )
89
179
 
90
- function encASSStyles({ styles, width, height }) {
91
- const scaledStyles = {}
92
-
93
- for (const [id, style] of Object.entries(styles)) {
94
- scaledStyles[id] = { ...style }
95
- for (const key of ['fontsize', 'marginV', 'marginL', 'marginR']) {
96
- const scale = key === 'fontsize' ? height / BASE_HEIGHT : width / BASE_WIDTH
97
-
98
- if (typeof style[key] === 'string') {
99
- const scaled = Number(style[key]) * scale
100
- scaledStyles[id][key] = String(Math.round(scaled * 10) / 10)
101
- }
102
- }
103
- }
104
-
180
+ /**
181
+ * @param {Styles} styles
182
+ * @returns {string}
183
+ */
184
+ function encASSStyles(styles) {
105
185
  return `[V4+ Styles]
106
186
  Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
107
- ${formatStyles(scaledStyles)}`
187
+ ${formatStyles(styles)}`
108
188
  }
109
189
 
110
- function encASSHeader({ width, height }) {
190
+ /**
191
+ * @param {object} params
192
+ * @param {number} params.playResX
193
+ * @param {number} params.playResY
194
+ * @param {boolean} [params.scaledBorderAndShadow]
195
+ * @param {boolean} params.futureWordWrapping
196
+ */
197
+ function encASSHeader({ playResX, playResY, scaledBorderAndShadow, futureWordWrapping }) {
111
198
  // WrapStyle
112
199
  // 0: smart wrapping, lines are evenly broken
113
200
  // 1: end-of-line word wrapping, only \N breaks
114
201
  // 2: no word wrapping, \n \N both breaks
115
202
  // 3: same as 0, but lower line gets wider.
116
- return `[Script Info]
117
- ScriptType: v4.00+
118
- PlayResX: ${width || BASE_WIDTH}
119
- PlayResY: ${height || BASE_HEIGHT}
120
- WrapStyle: 1
121
- `
203
+ const header = [
204
+ '[Script Info]',
205
+ 'ScriptType: v4.00+',
206
+ `PlayResX: ${playResX}`,
207
+ `PlayResY: ${playResY}`,
208
+ // new way is to not let ass wrap (2) -- we handle it ourselves
209
+ // to make sure hub and render gets the same result
210
+ `WrapStyle: ${futureWordWrapping ? 2 : 1}`,
211
+ ]
212
+ if (scaledBorderAndShadow) {
213
+ header.push('ScaledBorderAndShadow: Yes')
214
+ }
215
+ return `${header.join('\n')}\n`
122
216
  }
123
217
 
218
+ /**
219
+ * @param {number | null | undefined} seconds
220
+ * @returns {string}
221
+ */
124
222
  function formatASSTime(seconds) {
125
223
  if (seconds == null) {
126
224
  return ''
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/lib",
3
- "version": "22.0.12",
3
+ "version": "22.0.14",
4
4
  "license": "MIT",
5
5
  "author": "Robert Nagy <robert.nagy@boffins.se>",
6
6
  "type": "module",
@@ -59,6 +59,7 @@
59
59
  "@elastic/transport": "^8.9.1",
60
60
  "@nxtedition/nxt-undici": "^4.2.26",
61
61
  "@swc/wasm-web": "^1.10.1",
62
+ "canvas": "^3.1.0",
62
63
  "content-type": "^1.0.5",
63
64
  "date-fns": "^3.6.0",
64
65
  "fast-querystring": "^1.1.1",