@dan-uni/dan-any 0.7.7 → 0.8.1

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.
package/dist/index.js CHANGED
@@ -8,7 +8,6 @@ import { Expose, plainToInstance } from "class-transformer";
8
8
  import { IsDate, IsEmail, IsEnum, IsInt, IsNotEmpty, IsNumber, IsOptional, IsString, Max, Min, validateOrReject } from "class-validator";
9
9
  import hh_mm_ss from "hh-mm-ss";
10
10
  import jssha from "jssha";
11
- import { createCanvas } from "canvas";
12
11
  import { brotliCompressSync, brotliDecompressSync, gunzipSync, gzipSync } from "node:zlib";
13
12
  import { decode, encode as external_base16384_encode } from "base16384";
14
13
  var __webpack_require__ = {};
@@ -1133,14 +1132,14 @@ class UniPool {
1133
1132
  return this.dans;
1134
1133
  case 'danuni.bin':
1135
1134
  return this.toPb();
1135
+ case 'bili.xml':
1136
+ return this.toBiliXML();
1136
1137
  case 'dplayer.json':
1137
1138
  return this.toDplayer();
1138
1139
  case 'artplayer.json':
1139
1140
  return this.toArtplayer();
1140
1141
  case 'ddplay.json':
1141
1142
  return this.toDDplay();
1142
- case 'common.ass':
1143
- return this.toASS();
1144
1143
  default:
1145
1144
  {
1146
1145
  const message = '(err) Unknown format or unsupported now!';
@@ -1314,7 +1313,7 @@ class UniPool {
1314
1313
  static fromASS(ass) {
1315
1314
  return parseAssRawField(ass);
1316
1315
  }
1317
- toASS(options = {
1316
+ toASS(canvasCtx, options = {
1318
1317
  substyle: {}
1319
1318
  }) {
1320
1319
  const fn = this.shared.SOID;
@@ -1322,7 +1321,7 @@ class UniPool {
1322
1321
  filename: fn,
1323
1322
  title: fn,
1324
1323
  ...options
1325
- });
1324
+ }, canvasCtx);
1326
1325
  }
1327
1326
  }
1328
1327
  const color_pad = (s)=>s.length < 2 ? `0${s}` : s;
@@ -1432,8 +1431,7 @@ const splitGrids = ({ fontSize, padding, playResY, bottomSpace })=>{
1432
1431
  2: arrayOfLength(linesCount, 0)
1433
1432
  };
1434
1433
  };
1435
- const measureTextWidth = (()=>{
1436
- const canvasContext = createCanvas(50, 50).getContext('2d');
1434
+ const measureTextWidthConstructor = (canvasContext)=>{
1437
1435
  const supportTextMeasure = !!canvasContext.measureText("\u4E2D");
1438
1436
  if (supportTextMeasure) return (fontName, fontSize, bold, text)=>{
1439
1437
  canvasContext.font = `${bold ? 'bold' : 'normal'} ${fontSize}px ${fontName}`;
@@ -1442,7 +1440,7 @@ const measureTextWidth = (()=>{
1442
1440
  };
1443
1441
  console.warn('[Warn] node-canvas is installed without text measure support, layout may not be correct');
1444
1442
  return (_fontName, fontSize, _bold, text)=>text.length * fontSize;
1445
- })();
1443
+ };
1446
1444
  const resolveAvailableFixGrid = (grids, time)=>{
1447
1445
  for (const [i, grid] of grids.entries())if (grid <= time) return i;
1448
1446
  return -1;
@@ -1455,7 +1453,7 @@ const resolveAvailableScrollGrid = (grids, rectWidth, screenWidth, time, duratio
1455
1453
  }
1456
1454
  return -1;
1457
1455
  };
1458
- const initializeLayout = (config)=>{
1456
+ const initializeLayout = (config, canvasCtx)=>{
1459
1457
  const { playResX, playResY, fontName, fontSize, bold, padding, scrollTime, fixTime, bottomSpace } = config;
1460
1458
  const [paddingTop, paddingRight, paddingBottom, paddingLeft] = padding;
1461
1459
  const defaultFontSize = fontSize[FontSize.NORMAL];
@@ -1464,7 +1462,7 @@ const initializeLayout = (config)=>{
1464
1462
  return (danmaku)=>{
1465
1463
  const targetGrids = grids[danmaku.type];
1466
1464
  const danmakuFontSize = fontSize[danmaku.fontSizeType];
1467
- const rectWidth = measureTextWidth(fontName, danmakuFontSize, bold, danmaku.content) + paddingLeft + paddingRight;
1465
+ const rectWidth = measureTextWidthConstructor(canvasCtx)(fontName, danmakuFontSize, bold, danmaku.content) + paddingLeft + paddingRight;
1468
1466
  const verticalOffset = paddingTop + Math.round((defaultFontSize - danmakuFontSize) / 2);
1469
1467
  if (danmaku.type === DanmakuType.SCROLL) {
1470
1468
  const scrollGrids = targetGrids;
@@ -1509,11 +1507,11 @@ const initializeLayout = (config)=>{
1509
1507
  }
1510
1508
  };
1511
1509
  };
1512
- const layoutDanmaku = (inputList, config)=>{
1510
+ const layoutDanmaku = (inputList, config, canvasCtx)=>{
1513
1511
  const list = [
1514
1512
  ...UniPool2DanmakuLists(inputList)
1515
1513
  ].sort((x, y)=>x.time - y.time);
1516
- const layout = initializeLayout(config);
1514
+ const layout = initializeLayout(config, canvasCtx);
1517
1515
  return DanmakuList2UniPool(list.map(layout).filter((danmaku)=>!!danmaku));
1518
1516
  };
1519
1517
  const formatTime = (seconds)=>{
@@ -1748,10 +1746,10 @@ const ass_gen_config = (overrides = {})=>{
1748
1746
  config.backColor = config.backColor && formatColor(hexColorToRGB(config.backColor));
1749
1747
  return config;
1750
1748
  };
1751
- function generateASS(danmaku, options) {
1749
+ function generateASS(danmaku, options, canvasCtx) {
1752
1750
  const config = ass_gen_config(options.substyle);
1753
1751
  const mergedList = danmaku.merge(config.mergeIn);
1754
- const layoutList = layoutDanmaku(mergedList, config);
1752
+ const layoutList = layoutDanmaku(mergedList, config, canvasCtx);
1755
1753
  const content = ass_create(layoutList, danmaku, config, {
1756
1754
  filename: options?.filename || 'unknown',
1757
1755
  title: options?.title || 'unknown'
@@ -2061,14 +2059,14 @@ class src_UniPool {
2061
2059
  return this.dans;
2062
2060
  case 'danuni.bin':
2063
2061
  return this.toPb();
2062
+ case 'bili.xml':
2063
+ return this.toBiliXML();
2064
2064
  case 'dplayer.json':
2065
2065
  return this.toDplayer();
2066
2066
  case 'artplayer.json':
2067
2067
  return this.toArtplayer();
2068
2068
  case 'ddplay.json':
2069
2069
  return this.toDDplay();
2070
- case 'common.ass':
2071
- return this.toASS();
2072
2070
  default:
2073
2071
  {
2074
2072
  const message = '(err) Unknown format or unsupported now!';
@@ -2242,7 +2240,7 @@ class src_UniPool {
2242
2240
  static fromASS(ass) {
2243
2241
  return parseAssRawField(ass);
2244
2242
  }
2245
- toASS(options = {
2243
+ toASS(canvasCtx, options = {
2246
2244
  substyle: {}
2247
2245
  }) {
2248
2246
  const fn = this.shared.SOID;
@@ -2250,7 +2248,7 @@ class src_UniPool {
2250
2248
  filename: fn,
2251
2249
  title: fn,
2252
2250
  ...options
2253
- });
2251
+ }, canvasCtx);
2254
2252
  }
2255
2253
  }
2256
2254
  export { UniDM, dm_gen_namespaceObject as UniDMTools, id_gen_namespaceObject as UniIDTools, src_UniPool as UniPool, platform_namespaceObject as platform };
@@ -4744,14 +4744,6 @@
4744
4744
  return intrinsic;
4745
4745
  };
4746
4746
  },
4747
- "../../node_modules/.pnpm/canvas@3.1.2/node_modules/canvas/browser.js": function(__unused_webpack_module, exports1) {
4748
- exports1.createCanvas = function(width, height) {
4749
- return Object.assign(document.createElement('canvas'), {
4750
- width: width,
4751
- height: height
4752
- });
4753
- };
4754
- },
4755
4747
  "../../node_modules/.pnpm/define-data-property@1.1.4/node_modules/define-data-property/index.js": function(module1, __unused_webpack_exports, __webpack_require__) {
4756
4748
  "use strict";
4757
4749
  var $defineProperty = __webpack_require__("../../node_modules/.pnpm/es-define-property@1.0.1/node_modules/es-define-property/index.js");
@@ -17037,7 +17029,8 @@ and limitations under the License.
17037
17029
  const field = map.field();
17038
17030
  let key;
17039
17031
  let val;
17040
- const end = reader.pos + reader.uint32();
17032
+ const len = reader.uint32();
17033
+ const end = reader.pos + len;
17041
17034
  while(reader.pos < end){
17042
17035
  const [fieldNo] = reader.tag();
17043
17036
  switch(fieldNo){
@@ -24104,14 +24097,14 @@ and limitations under the License.
24104
24097
  return this.dans;
24105
24098
  case 'danuni.bin':
24106
24099
  return this.toPb();
24100
+ case 'bili.xml':
24101
+ return this.toBiliXML();
24107
24102
  case 'dplayer.json':
24108
24103
  return this.toDplayer();
24109
24104
  case 'artplayer.json':
24110
24105
  return this.toArtplayer();
24111
24106
  case 'ddplay.json':
24112
24107
  return this.toDDplay();
24113
- case 'common.ass':
24114
- return this.toASS();
24115
24108
  default:
24116
24109
  {
24117
24110
  const message = '(err) Unknown format or unsupported now!';
@@ -24285,7 +24278,7 @@ and limitations under the License.
24285
24278
  static fromASS(ass) {
24286
24279
  return parseAssRawField(ass);
24287
24280
  }
24288
- toASS(options = {
24281
+ toASS(canvasCtx, options = {
24289
24282
  substyle: {}
24290
24283
  }) {
24291
24284
  const fn = this.shared.SOID;
@@ -24293,7 +24286,7 @@ and limitations under the License.
24293
24286
  filename: fn,
24294
24287
  title: fn,
24295
24288
  ...options
24296
- });
24289
+ }, canvasCtx);
24297
24290
  }
24298
24291
  }
24299
24292
  const color_pad = (s)=>s.length < 2 ? `0${s}` : s;
@@ -24332,7 +24325,6 @@ and limitations under the License.
24332
24325
  };
24333
24326
  const getDecoratingColor = (color)=>isDarkColor(color) ? WHITE : BLACK;
24334
24327
  const isWhite = (color)=>255 === color.r && 255 === color.g && 255 === color.b;
24335
- var browser = __webpack_require__("../../node_modules/.pnpm/canvas@3.1.2/node_modules/canvas/browser.js");
24336
24328
  const DanmakuType = {
24337
24329
  SCROLL: 1,
24338
24330
  BOTTOM: 2,
@@ -24404,8 +24396,7 @@ and limitations under the License.
24404
24396
  2: arrayOfLength(linesCount, 0)
24405
24397
  };
24406
24398
  };
24407
- const measureTextWidth = (()=>{
24408
- const canvasContext = (0, browser.createCanvas)(50, 50).getContext('2d');
24399
+ const measureTextWidthConstructor = (canvasContext)=>{
24409
24400
  const supportTextMeasure = !!canvasContext.measureText("\u4E2D");
24410
24401
  if (supportTextMeasure) return (fontName, fontSize, bold, text)=>{
24411
24402
  canvasContext.font = `${bold ? 'bold' : 'normal'} ${fontSize}px ${fontName}`;
@@ -24414,7 +24405,7 @@ and limitations under the License.
24414
24405
  };
24415
24406
  console.warn('[Warn] node-canvas is installed without text measure support, layout may not be correct');
24416
24407
  return (_fontName, fontSize, _bold, text)=>text.length * fontSize;
24417
- })();
24408
+ };
24418
24409
  const resolveAvailableFixGrid = (grids, time)=>{
24419
24410
  for (const [i, grid] of grids.entries())if (grid <= time) return i;
24420
24411
  return -1;
@@ -24427,7 +24418,7 @@ and limitations under the License.
24427
24418
  }
24428
24419
  return -1;
24429
24420
  };
24430
- const initializeLayout = (config)=>{
24421
+ const initializeLayout = (config, canvasCtx)=>{
24431
24422
  const { playResX, playResY, fontName, fontSize, bold, padding, scrollTime, fixTime, bottomSpace } = config;
24432
24423
  const [paddingTop, paddingRight, paddingBottom, paddingLeft] = padding;
24433
24424
  const defaultFontSize = fontSize[FontSize.NORMAL];
@@ -24436,7 +24427,7 @@ and limitations under the License.
24436
24427
  return (danmaku)=>{
24437
24428
  const targetGrids = grids[danmaku.type];
24438
24429
  const danmakuFontSize = fontSize[danmaku.fontSizeType];
24439
- const rectWidth = measureTextWidth(fontName, danmakuFontSize, bold, danmaku.content) + paddingLeft + paddingRight;
24430
+ const rectWidth = measureTextWidthConstructor(canvasCtx)(fontName, danmakuFontSize, bold, danmaku.content) + paddingLeft + paddingRight;
24440
24431
  const verticalOffset = paddingTop + Math.round((defaultFontSize - danmakuFontSize) / 2);
24441
24432
  if (danmaku.type === DanmakuType.SCROLL) {
24442
24433
  const scrollGrids = targetGrids;
@@ -24481,11 +24472,11 @@ and limitations under the License.
24481
24472
  }
24482
24473
  };
24483
24474
  };
24484
- const layoutDanmaku = (inputList, config)=>{
24475
+ const layoutDanmaku = (inputList, config, canvasCtx)=>{
24485
24476
  const list = [
24486
24477
  ...UniPool2DanmakuLists(inputList)
24487
24478
  ].sort((x, y)=>x.time - y.time);
24488
- const layout = initializeLayout(config);
24479
+ const layout = initializeLayout(config, canvasCtx);
24489
24480
  return DanmakuList2UniPool(list.map(layout).filter((danmaku)=>!!danmaku));
24490
24481
  };
24491
24482
  const formatTime = (seconds)=>{
@@ -24723,10 +24714,10 @@ and limitations under the License.
24723
24714
  config.backColor = config.backColor && formatColor(hexColorToRGB(config.backColor));
24724
24715
  return config;
24725
24716
  };
24726
- function generateASS(danmaku, options) {
24717
+ function generateASS(danmaku, options, canvasCtx) {
24727
24718
  const config = ass_gen_config(options.substyle);
24728
24719
  const mergedList = danmaku.merge(config.mergeIn);
24729
- const layoutList = layoutDanmaku(mergedList, config);
24720
+ const layoutList = layoutDanmaku(mergedList, config, canvasCtx);
24730
24721
  const content = ass_create(layoutList, danmaku, config, {
24731
24722
  filename: options?.filename || 'unknown',
24732
24723
  title: options?.title || 'unknown'
@@ -25037,14 +25028,14 @@ and limitations under the License.
25037
25028
  return this.dans;
25038
25029
  case 'danuni.bin':
25039
25030
  return this.toPb();
25031
+ case 'bili.xml':
25032
+ return this.toBiliXML();
25040
25033
  case 'dplayer.json':
25041
25034
  return this.toDplayer();
25042
25035
  case 'artplayer.json':
25043
25036
  return this.toArtplayer();
25044
25037
  case 'ddplay.json':
25045
25038
  return this.toDDplay();
25046
- case 'common.ass':
25047
- return this.toASS();
25048
25039
  default:
25049
25040
  {
25050
25041
  const message = '(err) Unknown format or unsupported now!';
@@ -25218,7 +25209,7 @@ and limitations under the License.
25218
25209
  static fromASS(ass) {
25219
25210
  return parseAssRawField(ass);
25220
25211
  }
25221
- toASS(options = {
25212
+ toASS(canvasCtx, options = {
25222
25213
  substyle: {}
25223
25214
  }) {
25224
25215
  const fn = this.shared.SOID;
@@ -25226,7 +25217,7 @@ and limitations under the License.
25226
25217
  filename: fn,
25227
25218
  title: fn,
25228
25219
  ...options
25229
- });
25220
+ }, canvasCtx);
25230
25221
  }
25231
25222
  }
25232
25223
  })();
@@ -1,6 +1,7 @@
1
1
  import type { RawConfig } from './ass/raw';
2
- import type { SubtitleStyle } from './types';
2
+ import type { CanvasCtx, SubtitleStyle } from './types';
3
3
  import { UniPool } from '..';
4
+ export { CanvasCtx };
4
5
  export type Options = {
5
6
  filename?: string;
6
7
  title?: string;
@@ -24,5 +25,5 @@ const assText = generateASS(xmlText, { filename, title: 'Quick Example' })
24
25
  fs.writeFileSync(`${filename}.ass`, assText, 'utf-8')
25
26
  ```
26
27
  */
27
- export declare function generateASS(danmaku: UniPool, options: Options): string;
28
+ export declare function generateASS(danmaku: UniPool, options: Options, canvasCtx: CanvasCtx): string;
28
29
  export declare function parseAssRawField(ass: string): UniPool;
@@ -1,4 +1,28 @@
1
+ import type { CanvasRenderingContext2D as NodeCRCtx2D } from 'canvas';
1
2
  import type { UniDM } from '../utils/dm-gen';
3
+ /**
4
+ * 请根据您的使用环境提供一个 50x50 的 2D Canvas 上下文
5
+ * @example
6
+ * // Node.js + canvas
7
+ * import { createCanvas } from 'canvas'
8
+ * const canvas = createCanvas(50, 50)
9
+ * const ctx = canvas.getContext('2d')
10
+ * @example
11
+ * // Node.js + Fabric.js
12
+ * import { StaticCanvas } from 'fabric/node'
13
+ * const ctx = new StaticCanvas(null, { width: 50, height: 50 }).getContext()
14
+ * @example
15
+ * // Browser + Native Canvas
16
+ * const canvas = document.createElement('canvas')
17
+ * canvas.width = 50
18
+ * canvas.height = 50
19
+ * const ctx = canvas.getContext('2d')
20
+ * @example
21
+ * // Browser + Fabric.js
22
+ * import { Canvas } from 'fabric'
23
+ * const ctx = new Canvas('canvas', { width: 50, height: 50 }).getContext()
24
+ */
25
+ export type CanvasCtx = NodeCRCtx2D | CanvasRenderingContext2D;
2
26
  export interface Context {
3
27
  filename: string;
4
28
  title: string;
@@ -1,4 +1,4 @@
1
1
  import type { UniPool } from '../..';
2
- import type { SubtitleStyle } from '../types';
3
- export declare const measureTextWidth: (fontName: string, fontSize: number, bold: boolean, text: string) => number;
4
- export declare const layoutDanmaku: (inputList: UniPool, config: SubtitleStyle) => UniPool;
2
+ import type { CanvasCtx, SubtitleStyle } from '../types';
3
+ export declare const measureTextWidthConstructor: (canvasContext: CanvasCtx) => (fontName: string, fontSize: number, bold: boolean, text: string) => number;
4
+ export declare const layoutDanmaku: (inputList: UniPool, config: SubtitleStyle, canvasCtx: CanvasCtx) => UniPool;
@@ -1,5 +1,5 @@
1
1
  import 'reflect-metadata/lite';
2
- import type { Options as AssGenOptions } from './ass-gen';
2
+ import type { Options as AssGenOptions, CanvasCtx } from './ass-gen';
3
3
  import type { CommandDm as DM_JSON_BiliCommandGrpc } from './proto/gen/bili/dm_pb';
4
4
  import { UniDM } from './utils/dm-gen';
5
5
  import * as UniDMTools from './utils/dm-gen';
@@ -145,6 +145,9 @@ export declare class UniPool {
145
145
  static fromDDPlay(json: DM_JSON_DDPlay, episodeId: string, options?: Options): UniPool;
146
146
  toDDplay(): DM_JSON_DDPlay;
147
147
  static fromASS(ass: string): UniPool;
148
- toASS(options?: AssGenOptions): string;
148
+ /**
149
+ * 转换为ASS字幕格式的弹幕,需播放器支持多行ASS渲染
150
+ */
151
+ toASS(canvasCtx: CanvasCtx, options?: AssGenOptions): string;
149
152
  }
150
153
  export { platform, UniDM, UniDMTools, UniIDTools, type DM_JSON_BiliCommandGrpc, };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dan-uni/dan-any",
3
- "version": "0.7.7",
3
+ "version": "0.8.1",
4
4
  "description": "A danmaku transformer lib, supporting danmaku from different platforms.",
5
5
  "keywords": [
6
6
  "bangumi",
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "author": "rinne",
19
19
  "main": "dist/index.js",
20
- "module": "src/index.ts",
20
+ "module": "dist/index.js",
21
21
  "types": "dist/src/index.d.ts",
22
22
  "browser": "dist/index.umd.min.js",
23
23
  "scripts": {
@@ -31,9 +31,8 @@
31
31
  "buf": "buf generate"
32
32
  },
33
33
  "dependencies": {
34
- "@bufbuild/protobuf": "^2.6.2",
34
+ "@bufbuild/protobuf": "^2.6.3",
35
35
  "base16384": "^1.0.0",
36
- "canvas": "^3.1.2",
37
36
  "class-transformer": "^0.5.1",
38
37
  "class-validator": "^0.14.2",
39
38
  "fast-xml-parser": "^5.2.5",
@@ -45,10 +44,11 @@
45
44
  },
46
45
  "devDependencies": {
47
46
  "@bufbuild/buf": "^1.56.0",
48
- "@bufbuild/protoc-gen-es": "^2.6.2",
47
+ "@bufbuild/protoc-gen-es": "^2.6.3",
49
48
  "@types/fs-extra": "^11.0.4",
50
49
  "@types/hh-mm-ss": "^1.2.3",
51
50
  "@types/json-bigint": "^1.0.4",
51
+ "canvas": "^3.1.2",
52
52
  "protobufjs": "^7.5.3"
53
53
  }
54
54
  }
@@ -1,10 +1,14 @@
1
+ import { createCanvas } from 'canvas'
1
2
  import { assertType, it } from 'vitest'
2
3
 
3
- import { measureTextWidth } from '../util/layout'
4
+ import { measureTextWidthConstructor } from '../util/layout'
4
5
 
5
6
  it('canvas measureTextWidth', () => {
6
7
  const text = '一段测试文字'
7
- const width = measureTextWidth('SimHei', 25, false, text)
8
+ const canvas = createCanvas(50, 50)
9
+ const width = measureTextWidthConstructor(
10
+ canvas.getContext('2d') as unknown as CanvasRenderingContext2D,
11
+ )('SimHei', 25, false, text)
8
12
  assertType<number>(width)
9
13
  console.info(width, text.length)
10
14
  // assert(width >= 25 * text.length)
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs'
2
2
  import path, { dirname } from 'node:path'
3
3
  import { fileURLToPath } from 'node:url'
4
+ import { createCanvas } from 'canvas'
4
5
  import { it } from 'vitest'
5
6
 
6
7
  import { generateASS } from '../'
@@ -12,9 +13,14 @@ it('generate ass from xml', () => {
12
13
  const filename = '898651903.xml'
13
14
  const xmlPath = path.join(__dirname, filename)
14
15
  const xmlText = fs.readFileSync(xmlPath, 'utf-8')
15
- const assText = generateASS(UniPool.fromBiliXML(xmlText), {
16
- // filename,
17
- // title: '我的忏悔',
18
- })
16
+ const canvas = createCanvas(50, 50)
17
+ const assText = generateASS(
18
+ UniPool.fromBiliXML(xmlText),
19
+ {
20
+ // filename,
21
+ // title: '我的忏悔',
22
+ },
23
+ canvas.getContext('2d') as unknown as CanvasRenderingContext2D,
24
+ )
19
25
  fs.writeFileSync(path.join(__dirname, `${filename}.ass`), assText, 'utf-8')
20
26
  })
@@ -1,6 +1,6 @@
1
1
  // import parse from './parse/bilibili'
2
2
  import type { RawConfig } from './ass/raw'
3
- import type { SubtitleStyle } from './types'
3
+ import type { CanvasCtx, SubtitleStyle } from './types'
4
4
 
5
5
  import { UniPool } from '..'
6
6
  import ass from './ass/create'
@@ -8,6 +8,8 @@ import { deRaw } from './ass/raw'
8
8
  import getConfig from './config'
9
9
  import { DanmakuList2UniPool, layoutDanmaku } from './util'
10
10
 
11
+ export { CanvasCtx }
12
+
11
13
  export type Options = {
12
14
  filename?: string
13
15
  title?: string
@@ -32,13 +34,17 @@ const assText = generateASS(xmlText, { filename, title: 'Quick Example' })
32
34
  fs.writeFileSync(`${filename}.ass`, assText, 'utf-8')
33
35
  ```
34
36
  */
35
- export function generateASS(danmaku: UniPool, options: Options): string {
37
+ export function generateASS(
38
+ danmaku: UniPool,
39
+ options: Options,
40
+ canvasCtx: CanvasCtx,
41
+ ): string {
36
42
  // const result = parse(text)
37
43
  const config = getConfig(options.substyle)
38
44
  // const filteredList = filterDanmaku(result.list, config.block)
39
45
  // const mergedList = mergeDanmaku(result.list, config.mergeIn)
40
46
  const mergedList = danmaku.merge(config.mergeIn)
41
- const layoutList = layoutDanmaku(mergedList, config)
47
+ const layoutList = layoutDanmaku(mergedList, config, canvasCtx)
42
48
  const content = ass(
43
49
  layoutList,
44
50
  danmaku,
@@ -1,7 +1,31 @@
1
1
  // export type BlockRule = string | RegExp
2
-
2
+ import type { CanvasRenderingContext2D as NodeCRCtx2D } from 'canvas'
3
3
  import type { UniDM } from '../utils/dm-gen'
4
4
 
5
+ /**
6
+ * 请根据您的使用环境提供一个 50x50 的 2D Canvas 上下文
7
+ * @example
8
+ * // Node.js + canvas
9
+ * import { createCanvas } from 'canvas'
10
+ * const canvas = createCanvas(50, 50)
11
+ * const ctx = canvas.getContext('2d')
12
+ * @example
13
+ * // Node.js + Fabric.js
14
+ * import { StaticCanvas } from 'fabric/node'
15
+ * const ctx = new StaticCanvas(null, { width: 50, height: 50 }).getContext()
16
+ * @example
17
+ * // Browser + Native Canvas
18
+ * const canvas = document.createElement('canvas')
19
+ * canvas.width = 50
20
+ * canvas.height = 50
21
+ * const ctx = canvas.getContext('2d')
22
+ * @example
23
+ * // Browser + Fabric.js
24
+ * import { Canvas } from 'fabric'
25
+ * const ctx = new Canvas('canvas', { width: 50, height: 50 }).getContext()
26
+ */
27
+ export type CanvasCtx = NodeCRCtx2D | CanvasRenderingContext2D
28
+
5
29
  export interface Context {
6
30
  filename: string
7
31
  title: string
@@ -1,6 +1,5 @@
1
- import { createCanvas } from 'canvas'
2
1
  import type { UniPool } from '../..'
3
- import type { Danmaku, SubtitleStyle } from '../types'
2
+ import type { CanvasCtx, Danmaku, SubtitleStyle } from '../types'
4
3
 
5
4
  import { DanmakuType, FontSize } from '../types'
6
5
  import { DanmakuList2UniPool, UniPool2DanmakuLists } from './danconvert'
@@ -77,8 +76,7 @@ const splitGrids = ({
77
76
  }
78
77
  }
79
78
 
80
- export const measureTextWidth = (() => {
81
- const canvasContext = createCanvas(50, 50).getContext('2d')
79
+ export const measureTextWidthConstructor = (canvasContext: CanvasCtx) => {
82
80
  const supportTextMeasure = !!canvasContext.measureText('中')
83
81
 
84
82
  if (supportTextMeasure) {
@@ -99,7 +97,7 @@ export const measureTextWidth = (() => {
99
97
  )
100
98
  return (_fontName: string, fontSize: number, _bold: boolean, text: string) =>
101
99
  text.length * fontSize
102
- })()
100
+ }
103
101
 
104
102
  // 找到能用的行
105
103
  const resolveAvailableFixGrid = (grids: FixGrid[], time: number) => {
@@ -138,7 +136,7 @@ const resolveAvailableScrollGrid = (
138
136
  return -1
139
137
  }
140
138
 
141
- const initializeLayout = (config: SubtitleStyle) => {
139
+ const initializeLayout = (config: SubtitleStyle, canvasCtx: CanvasCtx) => {
142
140
  const {
143
141
  playResX,
144
142
  playResY,
@@ -160,7 +158,12 @@ const initializeLayout = (config: SubtitleStyle) => {
160
158
  const targetGrids = grids[danmaku.type as keyof DanmakuGrids]
161
159
  const danmakuFontSize = fontSize[danmaku.fontSizeType]
162
160
  const rectWidth =
163
- measureTextWidth(fontName, danmakuFontSize, bold, danmaku.content) +
161
+ measureTextWidthConstructor(canvasCtx)(
162
+ fontName,
163
+ danmakuFontSize,
164
+ bold,
165
+ danmaku.content,
166
+ ) +
164
167
  paddingLeft +
165
168
  paddingRight
166
169
  const verticalOffset =
@@ -228,11 +231,12 @@ const initializeLayout = (config: SubtitleStyle) => {
228
231
  export const layoutDanmaku = (
229
232
  inputList: UniPool,
230
233
  config: SubtitleStyle,
234
+ canvasCtx: CanvasCtx,
231
235
  ): UniPool => {
232
236
  const list = [...UniPool2DanmakuLists(inputList)].sort(
233
237
  (x, y) => x.time - y.time,
234
238
  )
235
- const layout = initializeLayout(config)
239
+ const layout = initializeLayout(config, canvasCtx)
236
240
 
237
241
  return DanmakuList2UniPool(list.map(layout).filter((danmaku) => !!danmaku))
238
242
  }
package/src/index.test.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  //基于以下注释,根据vitest生成测试用例
2
+ import { createCanvas } from 'canvas'
2
3
  import { describe, it } from 'vitest'
3
4
 
4
5
  import { UniPool } from './index'
@@ -50,8 +51,9 @@ describe('转化自', () => {
50
51
  console.info(pool)
51
52
  })
52
53
  it('ass[双向]', () => {
54
+ const canvas = createCanvas(50, 50)
53
55
  const pool = UniPool.fromBiliXML(xml)
54
- const ass = pool.toASS()
56
+ const ass = pool.toASS(canvas.getContext('2d'))
55
57
  console.info(ass)
56
58
  console.info(UniPool.fromASS(ass))
57
59
  })
package/src/index.ts CHANGED
@@ -2,7 +2,7 @@ import 'reflect-metadata/lite'
2
2
 
3
3
  import { XMLBuilder, XMLParser } from 'fast-xml-parser'
4
4
  import JSONbig from 'json-bigint'
5
- import type { Options as AssGenOptions } from './ass-gen'
5
+ import type { Options as AssGenOptions, CanvasCtx } from './ass-gen'
6
6
  import type { CommandDm as DM_JSON_BiliCommandGrpc } from './proto/gen/bili/dm_pb'
7
7
 
8
8
  import { create, fromBinary, toBinary } from '@bufbuild/protobuf'
@@ -377,8 +377,8 @@ export class UniPool {
377
377
  return this.dans
378
378
  case 'danuni.bin':
379
379
  return this.toPb()
380
- // case 'bili.xml':
381
- // return this.toBiliXML()
380
+ case 'bili.xml':
381
+ return this.toBiliXML()
382
382
  // case 'bili.bin':
383
383
  // return this.toBiliBin()
384
384
  // case 'bili.cmd.bin':
@@ -389,8 +389,8 @@ export class UniPool {
389
389
  return this.toArtplayer()
390
390
  case 'ddplay.json':
391
391
  return this.toDDplay()
392
- case 'common.ass':
393
- return this.toASS()
392
+ // case 'common.ass':
393
+ // return this.toASS()
394
394
  default: {
395
395
  const message = '(err) Unknown format or unsupported now!'
396
396
  if (continue_on_error) return message
@@ -649,9 +649,15 @@ export class UniPool {
649
649
  static fromASS(ass: string) {
650
650
  return parseAssRawField(ass)
651
651
  }
652
- toASS(options: AssGenOptions = { substyle: {} }): string {
652
+ /**
653
+ * 转换为ASS字幕格式的弹幕,需播放器支持多行ASS渲染
654
+ */
655
+ toASS(
656
+ canvasCtx: CanvasCtx,
657
+ options: AssGenOptions = { substyle: {} },
658
+ ): string {
653
659
  const fn = this.shared.SOID
654
- return generateASS(this, { filename: fn, title: fn, ...options })
660
+ return generateASS(this, { filename: fn, title: fn, ...options }, canvasCtx)
655
661
  }
656
662
  }
657
663
 
@@ -1,4 +1,4 @@
1
- // @generated by protoc-gen-es v2.6.2 with parameter "target=ts"
1
+ // @generated by protoc-gen-es v2.6.3 with parameter "target=ts"
2
2
  // @generated from file bili/dm.proto (package bilibili.community.service.dm.v1, syntax proto3)
3
3
  /* eslint-disable */
4
4
 
@@ -1,4 +1,4 @@
1
- // @generated by protoc-gen-es v2.6.2 with parameter "target=ts"
1
+ // @generated by protoc-gen-es v2.6.3 with parameter "target=ts"
2
2
  // @generated from file danuni.proto (package danuni.danmaku.v1, syntax proto3)
3
3
  /* eslint-disable */
4
4