@empty-sekai/renderer-wasm 0.1.0
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/LICENSE +235 -0
- package/LICENSE-EXCEPTION +32 -0
- package/README.md +123 -0
- package/dist/allium_renderer_wasm.js +14 -0
- package/dist/allium_renderer_wasm.wasm +0 -0
- package/dist/emscripten.d.ts +26 -0
- package/dist/emscripten.js +9 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +21 -0
- package/dist/protocol.d.ts +71 -0
- package/dist/protocol.js +8 -0
- package/dist/renderer.d.ts +115 -0
- package/dist/renderer.js +316 -0
- package/dist/worker-client.d.ts +51 -0
- package/dist/worker-client.js +105 -0
- package/dist/worker.d.ts +14 -0
- package/dist/worker.js +96 -0
- package/package.json +39 -0
- package/src/emscripten.ts +37 -0
- package/src/index.ts +24 -0
- package/src/lib.rs +528 -0
- package/src/main.rs +38 -0
- package/src/protocol.ts +47 -0
- package/src/renderer.ts +449 -0
- package/src/worker-client.ts +141 -0
- package/src/worker.ts +122 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* emscripten 运行时模块类型(MODULARIZE + EXPORT_ES6 产物)。
|
|
3
|
+
*
|
|
4
|
+
* 构建产物 `allium_renderer_wasm.js` 默认导出一个工厂函数
|
|
5
|
+
* `createAlliumRenderer(moduleArg?) => Promise<EmscriptenModule>`。
|
|
6
|
+
* 其导出函数集合与 `.cargo/config.toml` 的 EXPORTED_FUNCTIONS /
|
|
7
|
+
* EXPORTED_RUNTIME_METHODS 必须保持一致。
|
|
8
|
+
*/
|
|
9
|
+
export interface EmscriptenModule {
|
|
10
|
+
/** wasm 线性内存的字节视图。ALLOW_MEMORY_GROWTH 下增长后会被替换,每次用前重读。 */
|
|
11
|
+
HEAPU8: Uint8Array;
|
|
12
|
+
HEAPU32: Uint32Array;
|
|
13
|
+
_malloc(size: number): number;
|
|
14
|
+
_free(ptr: number): void;
|
|
15
|
+
getValue(ptr: number, type: "i32" | "i8" | "i16" | "float" | "double" | "*"): number;
|
|
16
|
+
setValue(ptr: number, value: number, type: "i32" | "i8" | "i16" | "float" | "double" | "*"): void;
|
|
17
|
+
lengthBytesUTF8(str: string): number;
|
|
18
|
+
stringToUTF8(str: string, outPtr: number, maxBytes: number): void;
|
|
19
|
+
UTF8ToString(ptr: number, maxBytes?: number): string;
|
|
20
|
+
cwrap(name: string, returnType: "number" | "void" | null, argTypes: Array<"number" | "string" | "array">): (...args: number[]) => number;
|
|
21
|
+
_alr_alloc(size: number): number;
|
|
22
|
+
_alr_free(ptr: number, size: number): void;
|
|
23
|
+
}
|
|
24
|
+
export type EmscriptenModuleFactory = (moduleArg?: Partial<EmscriptenModule> & {
|
|
25
|
+
locateFile?: (path: string, prefix: string) => string;
|
|
26
|
+
}) => Promise<EmscriptenModule>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* emscripten 运行时模块类型(MODULARIZE + EXPORT_ES6 产物)。
|
|
3
|
+
*
|
|
4
|
+
* 构建产物 `allium_renderer_wasm.js` 默认导出一个工厂函数
|
|
5
|
+
* `createAlliumRenderer(moduleArg?) => Promise<EmscriptenModule>`。
|
|
6
|
+
* 其导出函数集合与 `.cargo/config.toml` 的 EXPORTED_FUNCTIONS /
|
|
7
|
+
* EXPORTED_RUNTIME_METHODS 必须保持一致。
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @allium/renderer-wasm — 浏览器名片渲染。
|
|
3
|
+
*
|
|
4
|
+
* 入口导出核心封装与类型。Worker 调度见子路径 `@allium/renderer-wasm/worker`。
|
|
5
|
+
*
|
|
6
|
+
* 最小用法(主线程,注意 skia 光栅化同步阻塞):
|
|
7
|
+
* ```ts
|
|
8
|
+
* import createAlliumRenderer from "@allium/renderer-wasm/allium_renderer_wasm.js";
|
|
9
|
+
* import { AlliumRenderer, ImageFormat } from "@allium/renderer-wasm";
|
|
10
|
+
*
|
|
11
|
+
* const r = await AlliumRenderer.create(createAlliumRenderer);
|
|
12
|
+
* r.registerFont("FZLanTingHei-DB-GBK", await fetchBytes("/fonts/lanting.ttf"));
|
|
13
|
+
* for (const [name, json] of tables) r.loadMasterData(name, json);
|
|
14
|
+
* r.init();
|
|
15
|
+
* for (const key of r.collectAssetKeys(cardJson)) {
|
|
16
|
+
* r.putAsset(key, await fetchBytes(assetUrl(key)));
|
|
17
|
+
* }
|
|
18
|
+
* const jpeg = r.render(cardJson, ImageFormat.Jpeg);
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export { AlliumRenderer, ImageFormat, AlliumRenderError } from "./renderer.js";
|
|
22
|
+
export type { CroppedLayerOutput, LayerCrop } from "./renderer.js";
|
|
23
|
+
export type { EmscriptenModule, EmscriptenModuleFactory } from "./emscripten.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @allium/renderer-wasm — 浏览器名片渲染。
|
|
3
|
+
*
|
|
4
|
+
* 入口导出核心封装与类型。Worker 调度见子路径 `@allium/renderer-wasm/worker`。
|
|
5
|
+
*
|
|
6
|
+
* 最小用法(主线程,注意 skia 光栅化同步阻塞):
|
|
7
|
+
* ```ts
|
|
8
|
+
* import createAlliumRenderer from "@allium/renderer-wasm/allium_renderer_wasm.js";
|
|
9
|
+
* import { AlliumRenderer, ImageFormat } from "@allium/renderer-wasm";
|
|
10
|
+
*
|
|
11
|
+
* const r = await AlliumRenderer.create(createAlliumRenderer);
|
|
12
|
+
* r.registerFont("FZLanTingHei-DB-GBK", await fetchBytes("/fonts/lanting.ttf"));
|
|
13
|
+
* for (const [name, json] of tables) r.loadMasterData(name, json);
|
|
14
|
+
* r.init();
|
|
15
|
+
* for (const key of r.collectAssetKeys(cardJson)) {
|
|
16
|
+
* r.putAsset(key, await fetchBytes(assetUrl(key)));
|
|
17
|
+
* }
|
|
18
|
+
* const jpeg = r.render(cardJson, ImageFormat.Jpeg);
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export { AlliumRenderer, ImageFormat, AlliumRenderError } from "./renderer.js";
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker 协议:主线程 ↔ 渲染 Worker 的消息类型。
|
|
3
|
+
*
|
|
4
|
+
* 渲染(skia CPU 光栅化)是同步阻塞,放进 Worker 避免卡主线程 UI。
|
|
5
|
+
* 字节经 Transferable(ArrayBuffer)传递,避免结构化克隆复制大缓冲。
|
|
6
|
+
*/
|
|
7
|
+
import { ImageFormat } from "./renderer.js";
|
|
8
|
+
export { ImageFormat };
|
|
9
|
+
export type { CroppedLayerOutput, LayerCrop } from "./renderer.js";
|
|
10
|
+
/** 创建 Worker 时传入的初始化参数。 */
|
|
11
|
+
export interface InitPayload {
|
|
12
|
+
/** wasm 工厂模块 URL(Worker 内 `import()` 加载)。 */
|
|
13
|
+
moduleUrl: string;
|
|
14
|
+
/** `.wasm` 文件 URL(可选,默认相对 moduleUrl 解析)。 */
|
|
15
|
+
wasmUrl?: string;
|
|
16
|
+
}
|
|
17
|
+
/** 一次渲染请求的全部输入(一次性注入,渲染后 Worker 状态可复用)。 */
|
|
18
|
+
export interface RenderRequest {
|
|
19
|
+
cardJson: string;
|
|
20
|
+
profileJson?: string;
|
|
21
|
+
format?: ImageFormat;
|
|
22
|
+
/** 分层裁剪渲染的 WebP 质量(0-100,默认 80)。仅 renderLayerCropped / renderAllLayers 使用。 */
|
|
23
|
+
quality?: number;
|
|
24
|
+
/** renderAllLayers 是否填充每层 properties(默认 true)。 */
|
|
25
|
+
includeProperties?: boolean;
|
|
26
|
+
/** masterdata 表:name → JSON 文本。 */
|
|
27
|
+
masterData: Record<string, string>;
|
|
28
|
+
/** 字体:family → 字节。 */
|
|
29
|
+
fonts: Array<{
|
|
30
|
+
family: string;
|
|
31
|
+
bytes: Uint8Array;
|
|
32
|
+
}>;
|
|
33
|
+
/** 素材:key → 字节。未提供的 key 渲染时按缺素材处理。 */
|
|
34
|
+
assets: Array<{
|
|
35
|
+
key: string;
|
|
36
|
+
bytes: Uint8Array;
|
|
37
|
+
}>;
|
|
38
|
+
}
|
|
39
|
+
export type RequestMessage = {
|
|
40
|
+
id: number;
|
|
41
|
+
kind: "init";
|
|
42
|
+
payload: InitPayload;
|
|
43
|
+
} | {
|
|
44
|
+
id: number;
|
|
45
|
+
kind: "render";
|
|
46
|
+
payload: RenderRequest;
|
|
47
|
+
} | {
|
|
48
|
+
id: number;
|
|
49
|
+
kind: "renderLayerCropped";
|
|
50
|
+
payload: RenderRequest;
|
|
51
|
+
} | {
|
|
52
|
+
id: number;
|
|
53
|
+
kind: "renderAllLayers";
|
|
54
|
+
payload: RenderRequest;
|
|
55
|
+
} | {
|
|
56
|
+
id: number;
|
|
57
|
+
kind: "collectAssetKeys";
|
|
58
|
+
payload: {
|
|
59
|
+
cardJson: string;
|
|
60
|
+
masterData: Record<string, string>;
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
export type ResponseMessage = {
|
|
64
|
+
id: number;
|
|
65
|
+
ok: true;
|
|
66
|
+
result?: unknown;
|
|
67
|
+
} | {
|
|
68
|
+
id: number;
|
|
69
|
+
ok: false;
|
|
70
|
+
error: string;
|
|
71
|
+
};
|
package/dist/protocol.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* allium-renderer-wasm 核心封装。
|
|
3
|
+
*
|
|
4
|
+
* 直接在当前线程持有 emscripten 模块并调用 C ABI。skia CPU 光栅化是
|
|
5
|
+
* 同步阻塞调用——主线程使用会卡 UI,故浏览器中建议用 `./worker` 的
|
|
6
|
+
* Worker 客户端(本类在 Worker 内运行)。两者共享下方调用约定。
|
|
7
|
+
*
|
|
8
|
+
* 资源注入责任在使用方:本包不内嵌任何字体 / masterdata / 素材。
|
|
9
|
+
* - 字体:`registerFont(family, bytes)`,缺字体的文本元素不渲染。
|
|
10
|
+
* - masterdata:`loadMasterData(name, json)` 逐表注入后 `init()`。
|
|
11
|
+
* - 素材:`putAsset(key, bytes)`(key 由 `collectAssetKeys` 给出)。
|
|
12
|
+
*/
|
|
13
|
+
import type { EmscriptenModuleFactory } from "./emscripten.js";
|
|
14
|
+
/** 输出图片格式。 */
|
|
15
|
+
export declare enum ImageFormat {
|
|
16
|
+
Jpeg = 0,
|
|
17
|
+
Png = 1,
|
|
18
|
+
PngTransparent = 2
|
|
19
|
+
}
|
|
20
|
+
/** C ABI 错误(携带来自 `alr_last_error` 的引擎错误文本)。 */
|
|
21
|
+
export declare class AlliumRenderError extends Error {
|
|
22
|
+
constructor(message: string);
|
|
23
|
+
}
|
|
24
|
+
/** {@link AlliumRenderer.renderLayerCropped} 的输出:WebP 字节 + 画布坐标系裁剪框。 */
|
|
25
|
+
export interface CroppedLayerOutput {
|
|
26
|
+
/** 裁剪后的 WebP 编码字节。 */
|
|
27
|
+
data: Uint8Array;
|
|
28
|
+
/** 裁剪框左上角 X(原画布坐标系)。 */
|
|
29
|
+
x: number;
|
|
30
|
+
/** 裁剪框左上角 Y(原画布坐标系)。 */
|
|
31
|
+
y: number;
|
|
32
|
+
/** 裁剪框宽度(像素)。完全透明时为 0。 */
|
|
33
|
+
width: number;
|
|
34
|
+
/** 裁剪框高度(像素)。完全透明时为 0。 */
|
|
35
|
+
height: number;
|
|
36
|
+
}
|
|
37
|
+
/** {@link AlliumRenderer.renderAllLayers} 单层输出(WebP 字节 + 元数据)。 */
|
|
38
|
+
export interface LayerCrop {
|
|
39
|
+
/** layer 升序的 0-based 序号。 */
|
|
40
|
+
z: number;
|
|
41
|
+
/** 元素类型 "text" / "card_member" / ...。 */
|
|
42
|
+
type: string;
|
|
43
|
+
/** 原始可见性(调用方可自行覆盖)。不可见层 `data` 为空。 */
|
|
44
|
+
original_visible: boolean;
|
|
45
|
+
/** 裁剪后的 WebP 字节;不可见层为空 Uint8Array。 */
|
|
46
|
+
data: Uint8Array;
|
|
47
|
+
/** 裁剪框(原画布坐标系);不可见层全 0。 */
|
|
48
|
+
x: number;
|
|
49
|
+
y: number;
|
|
50
|
+
width: number;
|
|
51
|
+
height: number;
|
|
52
|
+
/** 元素属性(字体名/颜色 hex/文本等),仅 `includeProperties=true` 时存在。 */
|
|
53
|
+
properties?: Record<string, unknown>;
|
|
54
|
+
}
|
|
55
|
+
export declare class AlliumRenderer {
|
|
56
|
+
private readonly mod;
|
|
57
|
+
private readonly ex;
|
|
58
|
+
private constructor();
|
|
59
|
+
/**
|
|
60
|
+
* 加载 wasm 模块并构造封装。
|
|
61
|
+
*
|
|
62
|
+
* @param factory 构建产物 `allium_renderer_wasm.js` 的默认导出。
|
|
63
|
+
* @param wasmUrl `.wasm` 文件 URL(默认让 emscripten 在 .js 旁解析;
|
|
64
|
+
* 打包/CDN 场景显式传入更可靠)。
|
|
65
|
+
*/
|
|
66
|
+
static create(factory: EmscriptenModuleFactory, wasmUrl?: string | URL): Promise<AlliumRenderer>;
|
|
67
|
+
/** 注入一张 masterdata 表(JSON 文本)。须在 {@link init} 前调用。 */
|
|
68
|
+
loadMasterData(name: string, json: string): void;
|
|
69
|
+
/** 注册内存字体(family 名 + 字体文件字节)。本包不内嵌字体,必须显式注入。 */
|
|
70
|
+
registerFont(family: string, bytes: Uint8Array): void;
|
|
71
|
+
/** 用已注入的表构建渲染器。重复调用等效热替换 masterdata。 */
|
|
72
|
+
init(): void;
|
|
73
|
+
/** 收集名片所需素材 key({@link init} 之后调用)。 */
|
|
74
|
+
collectAssetKeys(cardJson: string): string[];
|
|
75
|
+
/** 注入素材(key + 编码图片字节)。 */
|
|
76
|
+
putAsset(key: string, bytes: Uint8Array): void;
|
|
77
|
+
/**
|
|
78
|
+
* 渲染名片,返回编码图片字节。
|
|
79
|
+
*
|
|
80
|
+
* @param cardJson `CustomProfileCard` 或 `UserCustomProfileCard[]`(取首张)JSON。
|
|
81
|
+
* @param format 输出格式(默认 JPEG)。
|
|
82
|
+
* @param profileJson 可选 profile API 响应 JSON(注入 generals / 称号等级)。
|
|
83
|
+
*/
|
|
84
|
+
render(cardJson: string, format?: ImageFormat, profileJson?: string): Uint8Array;
|
|
85
|
+
/**
|
|
86
|
+
* 分层裁剪渲染:所有可见元素绘到透明画布,裁剪到不透明像素的紧凑包围盒,
|
|
87
|
+
* 编码为 WebP,并返回裁剪框在原画布坐标系的偏移。
|
|
88
|
+
*
|
|
89
|
+
* @param cardJson `CustomProfileCard` 或 `UserCustomProfileCard[]`(取首张)JSON。
|
|
90
|
+
* @param quality WebP 质量(0-100,默认 80)。
|
|
91
|
+
* @param profileJson 可选 profile API 响应 JSON(注入 generals / 称号等级)。
|
|
92
|
+
*/
|
|
93
|
+
renderLayerCropped(cardJson: string, quality?: number, profileJson?: string): CroppedLayerOutput;
|
|
94
|
+
/**
|
|
95
|
+
* 批量分层裁剪渲染:把名片按 layer 升序逐元素渲成裁剪 WebP,一次 FFI
|
|
96
|
+
* 拿全部 N 层。
|
|
97
|
+
*
|
|
98
|
+
* @param cardJson `CustomProfileCard` 或 `UserCustomProfileCard[]`(取首张)JSON。
|
|
99
|
+
* @param quality WebP 质量 0-100(默认 80)。
|
|
100
|
+
* @param includeProperties 是否填充每层 `properties`(字体名/颜色 hex/文本等);
|
|
101
|
+
* 不需要时关掉省一遍 masterdata 查询。
|
|
102
|
+
* @param profileJson 可选 profile API 响应 JSON(注入 generals / 称号等级)。
|
|
103
|
+
*
|
|
104
|
+
* 返回数组顺序 = layer 升序 = z 序号;不可见元素也在结果中(`data` 为空、
|
|
105
|
+
* rect 全 0),便于完整重建图层列表。
|
|
106
|
+
*/
|
|
107
|
+
renderAllLayers(cardJson: string, quality?: number, includeProperties?: boolean, profileJson?: string): LayerCrop[];
|
|
108
|
+
private pushStr;
|
|
109
|
+
private pushBytes;
|
|
110
|
+
private popBuf;
|
|
111
|
+
/** 读取 `*out_ptr`/`*out_len` 指向的引擎输出缓冲并复制出来,随后 alr_free 释放。 */
|
|
112
|
+
private takeOutput;
|
|
113
|
+
private check;
|
|
114
|
+
private readLastError;
|
|
115
|
+
}
|
package/dist/renderer.js
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* allium-renderer-wasm 核心封装。
|
|
3
|
+
*
|
|
4
|
+
* 直接在当前线程持有 emscripten 模块并调用 C ABI。skia CPU 光栅化是
|
|
5
|
+
* 同步阻塞调用——主线程使用会卡 UI,故浏览器中建议用 `./worker` 的
|
|
6
|
+
* Worker 客户端(本类在 Worker 内运行)。两者共享下方调用约定。
|
|
7
|
+
*
|
|
8
|
+
* 资源注入责任在使用方:本包不内嵌任何字体 / masterdata / 素材。
|
|
9
|
+
* - 字体:`registerFont(family, bytes)`,缺字体的文本元素不渲染。
|
|
10
|
+
* - masterdata:`loadMasterData(name, json)` 逐表注入后 `init()`。
|
|
11
|
+
* - 素材:`putAsset(key, bytes)`(key 由 `collectAssetKeys` 给出)。
|
|
12
|
+
*/
|
|
13
|
+
/** 输出图片格式。 */
|
|
14
|
+
export var ImageFormat;
|
|
15
|
+
(function (ImageFormat) {
|
|
16
|
+
ImageFormat[ImageFormat["Jpeg"] = 0] = "Jpeg";
|
|
17
|
+
ImageFormat[ImageFormat["Png"] = 1] = "Png";
|
|
18
|
+
ImageFormat[ImageFormat["PngTransparent"] = 2] = "PngTransparent";
|
|
19
|
+
})(ImageFormat || (ImageFormat = {}));
|
|
20
|
+
/** C ABI 错误(携带来自 `alr_last_error` 的引擎错误文本)。 */
|
|
21
|
+
export class AlliumRenderError extends Error {
|
|
22
|
+
constructor(message) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = "AlliumRenderError";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const textEncoder = new TextEncoder();
|
|
28
|
+
const textDecoder = new TextDecoder("utf-8");
|
|
29
|
+
export class AlliumRenderer {
|
|
30
|
+
mod;
|
|
31
|
+
ex;
|
|
32
|
+
constructor(mod, ex) {
|
|
33
|
+
this.mod = mod;
|
|
34
|
+
this.ex = ex;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 加载 wasm 模块并构造封装。
|
|
38
|
+
*
|
|
39
|
+
* @param factory 构建产物 `allium_renderer_wasm.js` 的默认导出。
|
|
40
|
+
* @param wasmUrl `.wasm` 文件 URL(默认让 emscripten 在 .js 旁解析;
|
|
41
|
+
* 打包/CDN 场景显式传入更可靠)。
|
|
42
|
+
*/
|
|
43
|
+
static async create(factory, wasmUrl) {
|
|
44
|
+
const mod = await factory(wasmUrl
|
|
45
|
+
? { locateFile: (path) => (path.endsWith(".wasm") ? String(wasmUrl) : path) }
|
|
46
|
+
: undefined);
|
|
47
|
+
const ex = {
|
|
48
|
+
alloc: mod.cwrap("alr_alloc", "number", ["number"]),
|
|
49
|
+
free: mod.cwrap("alr_free", "void", ["number", "number"]),
|
|
50
|
+
lastError: mod.cwrap("alr_last_error", "number", ["number"]),
|
|
51
|
+
loadMasterdata: mod.cwrap("alr_load_masterdata", "number", [
|
|
52
|
+
"number",
|
|
53
|
+
"number",
|
|
54
|
+
"number",
|
|
55
|
+
"number",
|
|
56
|
+
]),
|
|
57
|
+
registerFont: mod.cwrap("alr_register_font", "number", [
|
|
58
|
+
"number",
|
|
59
|
+
"number",
|
|
60
|
+
"number",
|
|
61
|
+
"number",
|
|
62
|
+
]),
|
|
63
|
+
init: mod.cwrap("alr_init", "number", []),
|
|
64
|
+
collectAssetKeys: mod.cwrap("alr_collect_asset_keys", "number", [
|
|
65
|
+
"number",
|
|
66
|
+
"number",
|
|
67
|
+
"number",
|
|
68
|
+
"number",
|
|
69
|
+
]),
|
|
70
|
+
putAsset: mod.cwrap("alr_put_asset", "number", [
|
|
71
|
+
"number",
|
|
72
|
+
"number",
|
|
73
|
+
"number",
|
|
74
|
+
"number",
|
|
75
|
+
]),
|
|
76
|
+
render: mod.cwrap("alr_render", "number", [
|
|
77
|
+
"number",
|
|
78
|
+
"number",
|
|
79
|
+
"number",
|
|
80
|
+
"number",
|
|
81
|
+
"number",
|
|
82
|
+
"number",
|
|
83
|
+
"number",
|
|
84
|
+
]),
|
|
85
|
+
renderLayerCropped: mod.cwrap("alr_render_layer_cropped", "number", [
|
|
86
|
+
"number",
|
|
87
|
+
"number",
|
|
88
|
+
"number",
|
|
89
|
+
"number",
|
|
90
|
+
"number",
|
|
91
|
+
"number",
|
|
92
|
+
"number",
|
|
93
|
+
"number",
|
|
94
|
+
]),
|
|
95
|
+
renderAllLayers: mod.cwrap("alr_render_all_layers", "number", [
|
|
96
|
+
"number",
|
|
97
|
+
"number",
|
|
98
|
+
"number",
|
|
99
|
+
"number",
|
|
100
|
+
"number",
|
|
101
|
+
"number",
|
|
102
|
+
"number",
|
|
103
|
+
"number",
|
|
104
|
+
"number",
|
|
105
|
+
"number",
|
|
106
|
+
]),
|
|
107
|
+
};
|
|
108
|
+
return new AlliumRenderer(mod, ex);
|
|
109
|
+
}
|
|
110
|
+
/** 注入一张 masterdata 表(JSON 文本)。须在 {@link init} 前调用。 */
|
|
111
|
+
loadMasterData(name, json) {
|
|
112
|
+
const n = this.pushStr(name);
|
|
113
|
+
const j = this.pushStr(json);
|
|
114
|
+
try {
|
|
115
|
+
this.check(this.ex.loadMasterdata(n.ptr, n.len, j.ptr, j.len));
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
this.popBuf(j);
|
|
119
|
+
this.popBuf(n);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/** 注册内存字体(family 名 + 字体文件字节)。本包不内嵌字体,必须显式注入。 */
|
|
123
|
+
registerFont(family, bytes) {
|
|
124
|
+
const f = this.pushStr(family);
|
|
125
|
+
const b = this.pushBytes(bytes);
|
|
126
|
+
try {
|
|
127
|
+
this.check(this.ex.registerFont(f.ptr, f.len, b.ptr, b.len));
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
this.popBuf(b);
|
|
131
|
+
this.popBuf(f);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/** 用已注入的表构建渲染器。重复调用等效热替换 masterdata。 */
|
|
135
|
+
init() {
|
|
136
|
+
this.check(this.ex.init());
|
|
137
|
+
}
|
|
138
|
+
/** 收集名片所需素材 key({@link init} 之后调用)。 */
|
|
139
|
+
collectAssetKeys(cardJson) {
|
|
140
|
+
const c = this.pushStr(cardJson);
|
|
141
|
+
const outPtr = this.mod._malloc(4);
|
|
142
|
+
const outLen = this.mod._malloc(4);
|
|
143
|
+
try {
|
|
144
|
+
this.check(this.ex.collectAssetKeys(c.ptr, c.len, outPtr, outLen));
|
|
145
|
+
const json = this.takeOutput(outPtr, outLen);
|
|
146
|
+
return JSON.parse(textDecoder.decode(json));
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
this.mod._free(outLen);
|
|
150
|
+
this.mod._free(outPtr);
|
|
151
|
+
this.popBuf(c);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/** 注入素材(key + 编码图片字节)。 */
|
|
155
|
+
putAsset(key, bytes) {
|
|
156
|
+
const k = this.pushStr(key);
|
|
157
|
+
const b = this.pushBytes(bytes);
|
|
158
|
+
try {
|
|
159
|
+
this.check(this.ex.putAsset(k.ptr, k.len, b.ptr, b.len));
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
this.popBuf(b);
|
|
163
|
+
this.popBuf(k);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* 渲染名片,返回编码图片字节。
|
|
168
|
+
*
|
|
169
|
+
* @param cardJson `CustomProfileCard` 或 `UserCustomProfileCard[]`(取首张)JSON。
|
|
170
|
+
* @param format 输出格式(默认 JPEG)。
|
|
171
|
+
* @param profileJson 可选 profile API 响应 JSON(注入 generals / 称号等级)。
|
|
172
|
+
*/
|
|
173
|
+
render(cardJson, format = ImageFormat.Jpeg, profileJson) {
|
|
174
|
+
const c = this.pushStr(cardJson);
|
|
175
|
+
const p = profileJson ? this.pushStr(profileJson) : { ptr: 0, len: 0 };
|
|
176
|
+
const outPtr = this.mod._malloc(4);
|
|
177
|
+
const outLen = this.mod._malloc(4);
|
|
178
|
+
try {
|
|
179
|
+
this.check(this.ex.render(c.ptr, c.len, p.ptr, p.len, format, outPtr, outLen));
|
|
180
|
+
return this.takeOutput(outPtr, outLen);
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
this.mod._free(outLen);
|
|
184
|
+
this.mod._free(outPtr);
|
|
185
|
+
if (p.ptr)
|
|
186
|
+
this.popBuf(p);
|
|
187
|
+
this.popBuf(c);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* 分层裁剪渲染:所有可见元素绘到透明画布,裁剪到不透明像素的紧凑包围盒,
|
|
192
|
+
* 编码为 WebP,并返回裁剪框在原画布坐标系的偏移。
|
|
193
|
+
*
|
|
194
|
+
* @param cardJson `CustomProfileCard` 或 `UserCustomProfileCard[]`(取首张)JSON。
|
|
195
|
+
* @param quality WebP 质量(0-100,默认 80)。
|
|
196
|
+
* @param profileJson 可选 profile API 响应 JSON(注入 generals / 称号等级)。
|
|
197
|
+
*/
|
|
198
|
+
renderLayerCropped(cardJson, quality = 80, profileJson) {
|
|
199
|
+
const c = this.pushStr(cardJson);
|
|
200
|
+
const p = profileJson ? this.pushStr(profileJson) : { ptr: 0, len: 0 };
|
|
201
|
+
const outPtr = this.mod._malloc(4);
|
|
202
|
+
const outLen = this.mod._malloc(4);
|
|
203
|
+
const outRect = this.mod._malloc(16); // 4 × u32: x, y, width, height
|
|
204
|
+
try {
|
|
205
|
+
this.check(this.ex.renderLayerCropped(c.ptr, c.len, p.ptr, p.len, quality, outPtr, outLen, outRect));
|
|
206
|
+
const data = this.takeOutput(outPtr, outLen);
|
|
207
|
+
return {
|
|
208
|
+
data,
|
|
209
|
+
x: this.mod.getValue(outRect, "i32") >>> 0,
|
|
210
|
+
y: this.mod.getValue(outRect + 4, "i32") >>> 0,
|
|
211
|
+
width: this.mod.getValue(outRect + 8, "i32") >>> 0,
|
|
212
|
+
height: this.mod.getValue(outRect + 12, "i32") >>> 0,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
this.mod._free(outRect);
|
|
217
|
+
this.mod._free(outLen);
|
|
218
|
+
this.mod._free(outPtr);
|
|
219
|
+
if (p.ptr)
|
|
220
|
+
this.popBuf(p);
|
|
221
|
+
this.popBuf(c);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* 批量分层裁剪渲染:把名片按 layer 升序逐元素渲成裁剪 WebP,一次 FFI
|
|
226
|
+
* 拿全部 N 层。
|
|
227
|
+
*
|
|
228
|
+
* @param cardJson `CustomProfileCard` 或 `UserCustomProfileCard[]`(取首张)JSON。
|
|
229
|
+
* @param quality WebP 质量 0-100(默认 80)。
|
|
230
|
+
* @param includeProperties 是否填充每层 `properties`(字体名/颜色 hex/文本等);
|
|
231
|
+
* 不需要时关掉省一遍 masterdata 查询。
|
|
232
|
+
* @param profileJson 可选 profile API 响应 JSON(注入 generals / 称号等级)。
|
|
233
|
+
*
|
|
234
|
+
* 返回数组顺序 = layer 升序 = z 序号;不可见元素也在结果中(`data` 为空、
|
|
235
|
+
* rect 全 0),便于完整重建图层列表。
|
|
236
|
+
*/
|
|
237
|
+
renderAllLayers(cardJson, quality = 80, includeProperties = true, profileJson) {
|
|
238
|
+
const c = this.pushStr(cardJson);
|
|
239
|
+
const p = profileJson ? this.pushStr(profileJson) : { ptr: 0, len: 0 };
|
|
240
|
+
const outMetaPtr = this.mod._malloc(4);
|
|
241
|
+
const outMetaLen = this.mod._malloc(4);
|
|
242
|
+
const outBlobPtr = this.mod._malloc(4);
|
|
243
|
+
const outBlobLen = this.mod._malloc(4);
|
|
244
|
+
try {
|
|
245
|
+
this.check(this.ex.renderAllLayers(c.ptr, c.len, p.ptr, p.len, quality, includeProperties ? 1 : 0, outMetaPtr, outMetaLen, outBlobPtr, outBlobLen));
|
|
246
|
+
const metaBytes = this.takeOutput(outMetaPtr, outMetaLen);
|
|
247
|
+
const blobBytes = this.takeOutput(outBlobPtr, outBlobLen);
|
|
248
|
+
const meta = JSON.parse(textDecoder.decode(metaBytes));
|
|
249
|
+
// 按 meta 切 blob。slice() 复制一份独立 Uint8Array(blobBytes 是 takeOutput
|
|
250
|
+
// 复制出来的,引用切片虽然便宜但生命周期不直观;这里就显式复制)。
|
|
251
|
+
return meta.map((m) => ({
|
|
252
|
+
z: m.z,
|
|
253
|
+
type: m.type,
|
|
254
|
+
original_visible: m.original_visible,
|
|
255
|
+
x: m.x, y: m.y, width: m.width, height: m.height,
|
|
256
|
+
data: m.byte_length > 0
|
|
257
|
+
? blobBytes.slice(m.byte_offset, m.byte_offset + m.byte_length)
|
|
258
|
+
: new Uint8Array(0),
|
|
259
|
+
properties: m.properties,
|
|
260
|
+
}));
|
|
261
|
+
}
|
|
262
|
+
finally {
|
|
263
|
+
this.mod._free(outBlobLen);
|
|
264
|
+
this.mod._free(outBlobPtr);
|
|
265
|
+
this.mod._free(outMetaLen);
|
|
266
|
+
this.mod._free(outMetaPtr);
|
|
267
|
+
if (p.ptr)
|
|
268
|
+
this.popBuf(p);
|
|
269
|
+
this.popBuf(c);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// ---- 内部 marshalling ----
|
|
273
|
+
pushStr(s) {
|
|
274
|
+
const bytes = textEncoder.encode(s);
|
|
275
|
+
return this.pushBytes(bytes);
|
|
276
|
+
}
|
|
277
|
+
pushBytes(bytes) {
|
|
278
|
+
const len = bytes.length;
|
|
279
|
+
// 长度 0 也分配 1 字节,避免 0 长度指针歧义。
|
|
280
|
+
const ptr = this.ex.alloc(Math.max(len, 1));
|
|
281
|
+
if (ptr === 0)
|
|
282
|
+
throw new AlliumRenderError("alr_alloc 返回空指针(内存不足)");
|
|
283
|
+
this.mod.HEAPU8.set(bytes, ptr);
|
|
284
|
+
return { ptr, len, cap: Math.max(len, 1) };
|
|
285
|
+
}
|
|
286
|
+
popBuf(buf) {
|
|
287
|
+
this.ex.free(buf.ptr, buf.cap);
|
|
288
|
+
}
|
|
289
|
+
/** 读取 `*out_ptr`/`*out_len` 指向的引擎输出缓冲并复制出来,随后 alr_free 释放。 */
|
|
290
|
+
takeOutput(outPtrPtr, outLenPtr) {
|
|
291
|
+
const dataPtr = this.mod.getValue(outPtrPtr, "*");
|
|
292
|
+
const dataLen = this.mod.getValue(outLenPtr, "*") >>> 0;
|
|
293
|
+
// 复制出线性内存(HEAPU8 可能在后续调用因内存增长失效)。
|
|
294
|
+
const copy = this.mod.HEAPU8.slice(dataPtr, dataPtr + dataLen);
|
|
295
|
+
this.ex.free(dataPtr, dataLen);
|
|
296
|
+
return copy;
|
|
297
|
+
}
|
|
298
|
+
check(code) {
|
|
299
|
+
if (code === 0)
|
|
300
|
+
return;
|
|
301
|
+
throw new AlliumRenderError(this.readLastError());
|
|
302
|
+
}
|
|
303
|
+
readLastError() {
|
|
304
|
+
const lenPtr = this.mod._malloc(4);
|
|
305
|
+
try {
|
|
306
|
+
const ptr = this.ex.lastError(lenPtr);
|
|
307
|
+
const len = this.mod.getValue(lenPtr, "*") >>> 0;
|
|
308
|
+
if (ptr === 0 || len === 0)
|
|
309
|
+
return "未知错误";
|
|
310
|
+
return textDecoder.decode(this.mod.HEAPU8.slice(ptr, ptr + len));
|
|
311
|
+
}
|
|
312
|
+
finally {
|
|
313
|
+
this.mod._free(lenPtr);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker 客户端:在主线程驱动渲染 Worker,返回 Promise。
|
|
3
|
+
*
|
|
4
|
+
* 这是浏览器中的推荐入口——skia 光栅化在 Worker 内同步执行,不阻塞 UI。
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { AlliumWorkerClient } from "@allium/renderer-wasm/worker";
|
|
8
|
+
*
|
|
9
|
+
* const client = await AlliumWorkerClient.spawn({
|
|
10
|
+
* workerUrl: new URL("@allium/renderer-wasm/worker.js", import.meta.url),
|
|
11
|
+
* moduleUrl: new URL("@allium/renderer-wasm/allium_renderer_wasm.js", import.meta.url).href,
|
|
12
|
+
* });
|
|
13
|
+
* const jpeg = await client.render({ cardJson, masterData, fonts, assets });
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
import { ImageFormat } from "./renderer.js";
|
|
17
|
+
import type { RenderRequest, CroppedLayerOutput, LayerCrop } from "./protocol.js";
|
|
18
|
+
export { ImageFormat };
|
|
19
|
+
export type { RenderRequest, CroppedLayerOutput, LayerCrop } from "./protocol.js";
|
|
20
|
+
export interface SpawnOptions {
|
|
21
|
+
/** Worker 脚本 URL(指向打包后的 `worker.js`)。 */
|
|
22
|
+
workerUrl: string | URL;
|
|
23
|
+
/** wasm 工厂模块 URL(Worker 内 import)。 */
|
|
24
|
+
moduleUrl: string;
|
|
25
|
+
/** `.wasm` 文件 URL(可选)。 */
|
|
26
|
+
wasmUrl?: string;
|
|
27
|
+
}
|
|
28
|
+
export declare class AlliumWorkerClient {
|
|
29
|
+
private readonly worker;
|
|
30
|
+
private nextId;
|
|
31
|
+
private readonly pending;
|
|
32
|
+
private constructor();
|
|
33
|
+
/** 启动 Worker 并完成初始化握手。 */
|
|
34
|
+
static spawn(opts: SpawnOptions): Promise<AlliumWorkerClient>;
|
|
35
|
+
/** 渲染名片,返回编码图片字节。 */
|
|
36
|
+
render(req: RenderRequest): Promise<Uint8Array>;
|
|
37
|
+
/**
|
|
38
|
+
* 分层裁剪渲染,返回裁剪后的 WebP 字节及其在原画布的偏移。
|
|
39
|
+
*/
|
|
40
|
+
renderLayerCropped(req: RenderRequest): Promise<CroppedLayerOutput>;
|
|
41
|
+
/**
|
|
42
|
+
* 批量分层裁剪渲染:一次返回所有元素的 WebP 字节 + 元数据。
|
|
43
|
+
*/
|
|
44
|
+
renderAllLayers(req: RenderRequest): Promise<LayerCrop[]>;
|
|
45
|
+
/** 收集名片所需素材 key。 */
|
|
46
|
+
collectAssetKeys(cardJson: string, masterData: Record<string, string>): Promise<string[]>;
|
|
47
|
+
/** 终止 Worker,拒绝所有在途请求。 */
|
|
48
|
+
terminate(): void;
|
|
49
|
+
private collectTransfer;
|
|
50
|
+
private post;
|
|
51
|
+
}
|