koishi-plugin-vercel-satori-png-service 0.1.9 → 0.2.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/lib/Satori.d.ts CHANGED
@@ -1,10 +1,9 @@
1
1
  import { Readable } from "node:stream";
2
2
  import * as resvg from "@resvg/resvg-wasm";
3
+ import Vips from "wasm-vips";
3
4
  import { ReactElement } from "react";
4
- import { ImageOptions } from "./og";
5
+ import { ImageOptions, Logger } from "./og";
5
6
  export declare const initSatori: () => Promise<void>;
6
- export declare const createNodejsStream: (element: ReactElement<any, any>, options: ImageOptions) => Promise<Readable>;
7
- export declare const renderSvg: (element: ReactElement<any, any>, options: ImageOptions) => Promise<string>;
8
7
  export declare const getResvg: () => new (svg: Uint8Array | string, options?: resvg.ResvgRenderOptions) => {
9
8
  free(): void;
10
9
  render(): {
@@ -41,3 +40,7 @@ export declare const getResvg: () => new (svg: Uint8Array | string, options?: re
41
40
  readonly height: number;
42
41
  readonly width: number;
43
42
  };
43
+ export declare const getVips: () => typeof Vips;
44
+ export declare const createNodejsStream: (element: ReactElement<any, any>, options: ImageOptions, logger: Logger) => Promise<Readable>;
45
+ export declare const svgToPng: (svg: string, options: ImageOptions, logger: Logger) => Promise<Readable>;
46
+ export declare const renderSvg: (element: ReactElement<any, any>, options: ImageOptions, logger: Logger) => Promise<string>;
package/lib/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Context, Schema, Service } from "koishi";
2
2
  import { ReactElement } from "react";
3
+ import SkiaCanvas from "skia-canvas";
3
4
  import { Readable } from "stream";
4
5
  import { Font, ImageOptions } from "./og";
5
6
  export { Font, ImageOptions } from "./og";
@@ -14,15 +15,8 @@ declare class VercelSatoriPngService extends Service {
14
15
  private fonts;
15
16
  constructor(ctx: Context, config: VercelSatoriPngService.Config);
16
17
  start(): Promise<void>;
17
- jsxToReactElement(jsxCode: string, data?: Record<any, any>): Promise<ReactElement<any, any>>;
18
- htmlToReactElement(htmlCode: string): ReactElement<any, any>;
19
18
  addFont(fonts: Font[]): void;
20
19
  removeFont(fonts: Font[]): void;
21
- private buildOptions;
22
- jsxToPng(jsxCode: string, options?: ImageOptions, data?: Record<any, any>): Promise<Readable>;
23
- htmlToPng(htmlCode: string, options?: ImageOptions): Promise<Readable>;
24
- reactElementToPng(reactElement: ReactElement<any, any>, options?: ImageOptions): Promise<Readable>;
25
- reactElementToSvg(reactElement: ReactElement<any, any>, options?: ImageOptions): Promise<string>;
26
20
  getResvg(): new (svg: Uint8Array | string, options?: import("@resvg/resvg-wasm").ResvgRenderOptions) => {
27
21
  free(): void;
28
22
  render(): {
@@ -59,6 +53,16 @@ declare class VercelSatoriPngService extends Service {
59
53
  readonly height: number;
60
54
  readonly width: number;
61
55
  };
56
+ getVips(): typeof import("wasm-vips");
57
+ getSkiaCanvas(): typeof SkiaCanvas;
58
+ jsxToReactElement(jsxCode: string, data?: Record<any, any>): Promise<ReactElement<any, any>>;
59
+ htmlToReactElement(htmlCode: string): ReactElement<any, any>;
60
+ jsxToPng(jsxCode: string, options?: ImageOptions, data?: Record<any, any>): Promise<Readable>;
61
+ htmlToPng(htmlCode: string, options?: ImageOptions): Promise<Readable>;
62
+ private buildOptions;
63
+ reactElementToPng(reactElement: ReactElement<any, any>, options?: ImageOptions): Promise<Readable>;
64
+ svgToPng(svg: string, options?: ImageOptions): Promise<Readable>;
65
+ reactElementToSvg(reactElement: ReactElement<any, any>, options?: ImageOptions): Promise<string>;
62
66
  }
63
67
  declare namespace VercelSatoriPngService {
64
68
  const usage: string;
package/lib/index.js CHANGED
@@ -37,6 +37,7 @@ var import_koishi = require("koishi");
37
37
  var import_react = __toESM(require("react"));
38
38
  var import_sucrase = require("sucrase");
39
39
  var import_html_react_parser = __toESM(require("html-react-parser"));
40
+ var import_skia_canvas2 = __toESM(require("skia-canvas"));
40
41
 
41
42
  // src/Satori.ts
42
43
  var import_node_path = __toESM(require("node:path"));
@@ -44,6 +45,7 @@ var import_promises = __toESM(require("node:fs/promises"));
44
45
  var import_node_module = require("node:module");
45
46
  var import_node_stream = require("node:stream");
46
47
  var resvg = __toESM(require("@resvg/resvg-wasm"));
48
+ var import_wasm_vips = __toESM(require("wasm-vips"));
47
49
 
48
50
  // src/emoji.ts
49
51
  var U200D = String.fromCharCode(8205);
@@ -192,6 +194,9 @@ var languageFontMap = {
192
194
  };
193
195
 
194
196
  // src/og.ts
197
+ var import_skia_canvas = __toESM(require("skia-canvas"));
198
+ var import_canvg = require("canvg");
199
+ var import_jsdom = require("jsdom");
195
200
  async function loadGoogleFont(font, text) {
196
201
  if (!font || !text) return;
197
202
  const API = `https://fonts.googleapis.com/css2?family=${font}&text=${encodeURIComponent(
@@ -250,18 +255,9 @@ var loadDynamicAsset = /* @__PURE__ */ __name(({ emoji }) => {
250
255
  return asset;
251
256
  };
252
257
  }, "loadDynamicAsset");
253
- function mergeOptions(opts) {
254
- return Object.assign(
255
- {
256
- debug: false
257
- },
258
- opts
259
- );
260
- }
261
- __name(mergeOptions, "mergeOptions");
262
- async function renderSvg(satori2, opts, defaultFonts, element) {
263
- const options = mergeOptions(opts);
264
- return await satori2(element, {
258
+ async function renderSvg(satori2, logger, options, defaultFonts, element) {
259
+ const startTime = options.showLog ? Date.now() : void 0;
260
+ const svg = await satori2(element, {
265
261
  width: options.width,
266
262
  height: options.height,
267
263
  debug: options.debug,
@@ -270,38 +266,80 @@ async function renderSvg(satori2, opts, defaultFonts, element) {
270
266
  emoji: options.emoji
271
267
  })
272
268
  });
269
+ if (options.showLog) {
270
+ logger(
271
+ `renderSvg time: ${Date.now() - startTime}ms; svg size: ${svg.length}`
272
+ );
273
+ }
274
+ return svg;
273
275
  }
274
276
  __name(renderSvg, "renderSvg");
275
- async function render(satori2, resvg2, opts, defaultFonts, element) {
276
- const options = mergeOptions(opts);
277
- const svg = await satori2(element, {
278
- width: options.width,
279
- height: options.height,
280
- debug: options.debug,
281
- fonts: options.fonts || defaultFonts,
282
- loadAdditionalAsset: loadDynamicAsset({
283
- emoji: options.emoji
284
- })
285
- });
286
- const resvgJS = new resvg2.Resvg(svg, {
287
- fitTo: {
288
- mode: "width",
289
- value: options.width
277
+ async function svgToPng(resvg2, vips2, logger, options, svg) {
278
+ const startTime = options.showLog ? Date.now() : void 0;
279
+ let pngBuffer;
280
+ switch (options.converter) {
281
+ case "skia-canvas-canvg": {
282
+ const canvas = new import_skia_canvas.default.Canvas(1, 1);
283
+ const ctx = canvas.getContext("2d");
284
+ const dom = new import_jsdom.JSDOM();
285
+ const v = import_canvg.Canvg.fromString(ctx, svg, {
286
+ window: dom.window,
287
+ DOMParser: dom.window.DOMParser,
288
+ createCanvas: /* @__PURE__ */ __name((w, h) => new import_skia_canvas.default.Canvas(w, h), "createCanvas"),
289
+ createImage: import_skia_canvas.default.Image,
290
+ ignoreDimensions: false
291
+ });
292
+ await v.render();
293
+ pngBuffer = await canvas.toBuffer("png");
294
+ break;
290
295
  }
291
- });
292
- const pngData = resvgJS.render();
293
- const pngBuffer = pngData.asPng();
294
- pngData.free();
295
- resvgJS.free();
296
+ case "skia-canvas": {
297
+ const img = await import_skia_canvas.default.loadImage(Buffer.from(svg));
298
+ const canvas = new import_skia_canvas.default.Canvas(img.width, img.height);
299
+ const ctx = canvas.getContext("2d");
300
+ ctx.drawImage(img, 0, 0);
301
+ pngBuffer = await canvas.toBuffer("png");
302
+ break;
303
+ }
304
+ case "vips": {
305
+ const img = vips2.Image.svgloadBuffer(Buffer.from(svg));
306
+ pngBuffer = img.pngsaveBuffer();
307
+ img.delete();
308
+ break;
309
+ }
310
+ case "resvg":
311
+ default: {
312
+ const resvgJS = new resvg2.Resvg(svg);
313
+ const pngData = resvgJS.render();
314
+ pngBuffer = pngData.asPng();
315
+ pngData.free();
316
+ resvgJS.free();
317
+ break;
318
+ }
319
+ }
320
+ if (options.showLog) {
321
+ logger(
322
+ `svgToPng time: ${Date.now() - startTime}ms; converter:${options.converter}; png size: ${pngBuffer.length}`
323
+ );
324
+ }
296
325
  return pngBuffer;
297
326
  }
327
+ __name(svgToPng, "svgToPng");
328
+ async function render(satori2, resvg2, vips2, logger, options, defaultFonts, element) {
329
+ const svg = await renderSvg(satori2, logger, options, defaultFonts, element);
330
+ return svgToPng(resvg2, vips2, logger, options, svg);
331
+ }
298
332
  __name(render, "render");
299
333
 
300
334
  // src/Satori.ts
301
335
  var fontData;
302
336
  var satori;
337
+ var vips;
303
338
  var initSatori = /* @__PURE__ */ __name(async () => {
304
339
  satori = (await import("satori")).default;
340
+ vips = await (0, import_wasm_vips.default)({
341
+ dynamicLibraries: ["vips-resvg.wasm"]
342
+ });
305
343
  const require2 = (0, import_node_module.createRequire)("file:///" + __filename);
306
344
  const reSvgWasm = import_node_path.default.join(
307
345
  import_node_path.default.dirname(require2.resolve("@resvg/resvg-wasm")),
@@ -312,6 +350,12 @@ var initSatori = /* @__PURE__ */ __name(async () => {
312
350
  require2.resolve("../noto-sans-v27-latin-regular.ttf")
313
351
  );
314
352
  }, "initSatori");
353
+ var getResvg = /* @__PURE__ */ __name(() => {
354
+ return resvg.Resvg;
355
+ }, "getResvg");
356
+ var getVips = /* @__PURE__ */ __name(() => {
357
+ return vips;
358
+ }, "getVips");
315
359
  var getDefaultFonts = /* @__PURE__ */ __name(() => [
316
360
  {
317
361
  name: "sans serif",
@@ -320,22 +364,25 @@ var getDefaultFonts = /* @__PURE__ */ __name(() => [
320
364
  style: "normal"
321
365
  }
322
366
  ], "getDefaultFonts");
323
- var createNodejsStream = /* @__PURE__ */ __name(async (element, options) => {
367
+ var createNodejsStream = /* @__PURE__ */ __name(async (element, options, logger) => {
324
368
  const result = await render(
325
369
  satori,
326
370
  resvg,
371
+ vips,
372
+ logger,
327
373
  options,
328
374
  getDefaultFonts(),
329
375
  element
330
376
  );
331
377
  return import_node_stream.Readable.from(Buffer.from(result));
332
378
  }, "createNodejsStream");
333
- var renderSvg2 = /* @__PURE__ */ __name(async (element, options) => {
334
- return renderSvg(satori, options, getDefaultFonts(), element);
379
+ var svgToPng2 = /* @__PURE__ */ __name(async (svg, options, logger) => {
380
+ const result = await svgToPng(resvg, vips, logger, options, svg);
381
+ return import_node_stream.Readable.from(Buffer.from(result));
382
+ }, "svgToPng");
383
+ var renderSvg2 = /* @__PURE__ */ __name(async (element, options, logger) => {
384
+ return renderSvg(satori, logger, options, getDefaultFonts(), element);
335
385
  }, "renderSvg");
336
- var getResvg = /* @__PURE__ */ __name(() => {
337
- return resvg.Resvg;
338
- }, "getResvg");
339
386
 
340
387
  // src/index.ts
341
388
  var serviceName = "vercelSatoriPngService";
@@ -360,6 +407,30 @@ var VercelSatoriPngService = class extends import_koishi.Service {
360
407
  await initSatori();
361
408
  initialized = true;
362
409
  }
410
+ addFont(fonts) {
411
+ this.fonts.push(...fonts);
412
+ this.ctx.on("dispose", () => {
413
+ this.removeFont(fonts);
414
+ });
415
+ }
416
+ removeFont(fonts) {
417
+ fonts.forEach((font) => {
418
+ const index = this.fonts.indexOf(font);
419
+ if (index === -1) {
420
+ return;
421
+ }
422
+ this.fonts.splice(index, 1);
423
+ });
424
+ }
425
+ getResvg() {
426
+ return getResvg();
427
+ }
428
+ getVips() {
429
+ return getVips();
430
+ }
431
+ getSkiaCanvas() {
432
+ return import_skia_canvas2.default;
433
+ }
363
434
  async jsxToReactElement(jsxCode, data) {
364
435
  const hCode = (0, import_sucrase.transform)(jsxCode, {
365
436
  transforms: ["jsx"],
@@ -387,20 +458,14 @@ var VercelSatoriPngService = class extends import_koishi.Service {
387
458
  htmlToReactElement(htmlCode) {
388
459
  return (0, import_html_react_parser.default)(htmlCode);
389
460
  }
390
- addFont(fonts) {
391
- this.fonts.push(...fonts);
392
- this.ctx.on("dispose", () => {
393
- this.removeFont(fonts);
394
- });
461
+ async jsxToPng(jsxCode, options, data) {
462
+ return this.reactElementToPng(
463
+ await this.jsxToReactElement(jsxCode, data),
464
+ options
465
+ );
395
466
  }
396
- removeFont(fonts) {
397
- fonts.forEach((font) => {
398
- const index = this.fonts.indexOf(font);
399
- if (index === -1) {
400
- return;
401
- }
402
- this.fonts.splice(index, 1);
403
- });
467
+ htmlToPng(htmlCode, options) {
468
+ return this.reactElementToPng(this.htmlToReactElement(htmlCode), options);
404
469
  }
405
470
  buildOptions(options) {
406
471
  options ||= {};
@@ -410,27 +475,30 @@ var VercelSatoriPngService = class extends import_koishi.Service {
410
475
  }
411
476
  return options;
412
477
  }
413
- async jsxToPng(jsxCode, options, data) {
414
- return this.reactElementToPng(
415
- await this.jsxToReactElement(jsxCode, data),
416
- options
478
+ async reactElementToPng(reactElement, options) {
479
+ return createNodejsStream(
480
+ reactElement,
481
+ this.buildOptions(options),
482
+ this._ctx.logger.info.bind(this._ctx.logger)
417
483
  );
418
484
  }
419
- htmlToPng(htmlCode, options) {
420
- return this.reactElementToPng(this.htmlToReactElement(htmlCode), options);
421
- }
422
- async reactElementToPng(reactElement, options) {
423
- return createNodejsStream(reactElement, this.buildOptions(options));
485
+ async svgToPng(svg, options) {
486
+ return svgToPng2(
487
+ svg,
488
+ this.buildOptions(options),
489
+ this._ctx.logger.info.bind(this._ctx.logger)
490
+ );
424
491
  }
425
492
  async reactElementToSvg(reactElement, options) {
426
- return renderSvg2(reactElement, this.buildOptions(options));
427
- }
428
- getResvg() {
429
- return getResvg();
493
+ return renderSvg2(
494
+ reactElement,
495
+ this.buildOptions(options),
496
+ this._ctx.logger.info.bind(this._ctx.logger)
497
+ );
430
498
  }
431
499
  };
432
500
  ((VercelSatoriPngService2) => {
433
- VercelSatoriPngService2.usage = 'html to ReactElement <a target="_blank" href="https://www.npmjs.com/package/html-react-parser">html-react-parser</a> \njsx to ReactElement <a target="_blank" href="https://www.npmjs.com/package/sucrase">sucrase</a> \nReactElement to svg <a target="_blank" href="https://github.com/vercel/satori#overview">vercel/satori</a> \n<a target="_blank" href="https://og-playground.vercel.app/">og-playground</a> \nsvg to png <a target="_blank" href="https://www.npmjs.com/package/@resvg/resvg-wasm">@resvg/resvg-wasm</a>';
501
+ VercelSatoriPngService2.usage = 'html to ReactElement <a target="_blank" href="https://www.npmjs.com/package/html-react-parser">html-react-parser</a><br/>jsx to ReactElement <a target="_blank" href="https://www.npmjs.com/package/sucrase">sucrase</a><br/>ReactElement to svg <a target="_blank" href="https://github.com/vercel/satori#overview">vercel/satori</a> <a target="_blank" href="https://og-playground.vercel.app/">og-playground</a><br/><hr/>svg to png<br/><a target="_blank" href="https://www.npmjs.com/package/@resvg/resvg-wasm">@resvg/resvg-wasm</a><br/><a target="_blank" href="https://www.npmjs.com/package/wasm-vips">wasm-vips</a><br/><a target="_blank" href="https://www.npmjs.com/package/skia-canvas">skia-canvas</a> <a target="_blank" href="https://www.npmjs.com/package/canvg">canvg</a> <a target="_blank" href="https://www.npmjs.com/package/jsdom">jsdom</a><br/>';
434
502
  VercelSatoriPngService2.Config = import_koishi.Schema.object({});
435
503
  })(VercelSatoriPngService || (VercelSatoriPngService = {}));
436
504
  var src_default = VercelSatoriPngService;
package/lib/og.d.ts CHANGED
@@ -3,6 +3,7 @@ import * as Resvg from "@resvg/resvg-wasm";
3
3
  import type Satori from "satori";
4
4
  import type { SatoriOptions } from "satori";
5
5
  import type { ReactElement } from "react";
6
+ import type Vips from "wasm-vips";
6
7
  export type ImageOptions = {
7
8
  /**
8
9
  * The width of the image.
@@ -37,7 +38,16 @@ export type ImageOptions = {
37
38
  * @default 'twemoji'
38
39
  */
39
40
  emoji?: EmojiType;
41
+ /**
42
+ * The converter to use.
43
+ *
44
+ * @default 'resvg'
45
+ */
46
+ converter?: "resvg" | "vips" | "skia-canvas" | "skia-canvas-canvg";
47
+ showLog?: boolean;
40
48
  };
41
49
  export type Font = ImageOptions["fonts"][number];
42
- export declare function renderSvg(satori: typeof Satori, opts: ImageOptions, defaultFonts: Font[], element: ReactElement<any, any>): Promise<string>;
43
- export default function render(satori: typeof Satori, resvg: typeof Resvg, opts: ImageOptions, defaultFonts: Font[], element: ReactElement<any, any>): Promise<Uint8Array<ArrayBufferLike>>;
50
+ export type Logger = (msg: string) => void;
51
+ export declare function renderSvg(satori: typeof Satori, logger: Logger, options: ImageOptions, defaultFonts: Font[], element: ReactElement<any, any>): Promise<string>;
52
+ export declare function svgToPng(resvg: typeof Resvg, vips: typeof Vips, logger: Logger, options: ImageOptions, svg: string): Promise<Uint8Array<ArrayBufferLike>>;
53
+ export default function render(satori: typeof Satori, resvg: typeof Resvg, vips: typeof Vips, logger: Logger, options: ImageOptions, defaultFonts: Font[], element: ReactElement<any, any>): Promise<Uint8Array<ArrayBufferLike>>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-vercel-satori-png-service",
3
3
  "description": "Use Vercel Satori and Resvg.js to convert html to png",
4
- "version": "0.1.9",
4
+ "version": "0.2.1",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "scripts": {
@@ -41,10 +41,14 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@resvg/resvg-wasm": "^2.6.3-alpha.2",
44
+ "canvg": "^4.0.3",
44
45
  "html-react-parser": "^5.2.11",
46
+ "jsdom": "27.0.1",
45
47
  "react": "^19.3.0-canary-fd524fe0-20251121",
46
48
  "satori": "^0.16.2",
47
- "sucrase": "^3.35.1"
49
+ "skia-canvas": "^3.0.8",
50
+ "sucrase": "^3.35.1",
51
+ "wasm-vips": "^0.0.16"
48
52
  },
49
53
  "koishi": {
50
54
  "service": {
@@ -54,6 +58,7 @@
54
58
  }
55
59
  },
56
60
  "devDependencies": {
61
+ "@types/jsdom": "^27",
57
62
  "@types/react": "^19"
58
63
  }
59
64
  }
package/readme.md CHANGED
@@ -11,7 +11,16 @@ html to ReactElement [html-react-parser](https://www.npmjs.com/package/html-reac
11
11
  jsx to ReactElement [sucrase](https://www.npmjs.com/package/sucrase)
12
12
 
13
13
  ReactElement to svg [vercel/satori](https://github.com/vercel/satori#overview)
14
-
15
14
  [og-playground](https://og-playground.vercel.app/)
16
15
 
17
- svg to png [@resvg/resvg-wasm](https://www.npmjs.com/package/@resvg/resvg-wasm)
16
+ ---
17
+
18
+ svg to png
19
+
20
+ [@resvg/resvg-wasm](https://www.npmjs.com/package/@resvg/resvg-wasm)
21
+
22
+ [wasm-vips](https://www.npmjs.com/package/wasm-vips)
23
+
24
+ [skia-canvas](https://www.npmjs.com/package/skia-canvas)
25
+ [canvg](https://www.npmjs.com/package/canvg)
26
+ [jsdom](https://www.npmjs.com/package/jsdom)
package/lib/emoji.d.ts DELETED
@@ -1,15 +0,0 @@
1
- /**
2
- * Modified version of https://unpkg.com/twemoji@13.1.0/dist/twemoji.esm.js.
3
- */
4
- export declare function getIconCode(char: string): string;
5
- declare const apis: {
6
- twemoji: (code: any) => string;
7
- openmoji: string;
8
- blobmoji: string;
9
- noto: string;
10
- fluent: (code: any) => string;
11
- fluentFlat: (code: any) => string;
12
- };
13
- export type EmojiType = keyof typeof apis;
14
- export declare function loadEmoji(code: string, type?: EmojiType): Promise<Response>;
15
- export {};
package/lib/language.d.ts DELETED
@@ -1,28 +0,0 @@
1
- export declare class FontDetector {
2
- private rangesByLang;
3
- detect(text: string, fonts: string[]): Promise<{
4
- [lang: string]: string;
5
- }>;
6
- private detectSegment;
7
- private load;
8
- private addDetectors;
9
- }
10
- export declare const languageFontMap: {
11
- 'ja-JP': string;
12
- 'ko-KR': string;
13
- 'zh-CN': string;
14
- 'zh-TW': string;
15
- 'zh-HK': string;
16
- 'th-TH': string;
17
- 'bn-IN': string;
18
- 'ar-AR': string;
19
- 'ta-IN': string;
20
- 'ml-IN': string;
21
- 'he-IL': string;
22
- 'te-IN': string;
23
- devanagari: string;
24
- kannada: string;
25
- symbol: string[];
26
- math: string;
27
- unknown: string;
28
- };