@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,105 @@
|
|
|
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
|
+
export { ImageFormat };
|
|
18
|
+
export class AlliumWorkerClient {
|
|
19
|
+
worker;
|
|
20
|
+
nextId = 1;
|
|
21
|
+
pending = new Map();
|
|
22
|
+
constructor(worker) {
|
|
23
|
+
this.worker = worker;
|
|
24
|
+
this.worker.onmessage = (ev) => {
|
|
25
|
+
const msg = ev.data;
|
|
26
|
+
const entry = this.pending.get(msg.id);
|
|
27
|
+
if (!entry)
|
|
28
|
+
return;
|
|
29
|
+
this.pending.delete(msg.id);
|
|
30
|
+
if (msg.ok)
|
|
31
|
+
entry.resolve(msg.result);
|
|
32
|
+
else
|
|
33
|
+
entry.reject(new Error(msg.error));
|
|
34
|
+
};
|
|
35
|
+
this.worker.onerror = (ev) => {
|
|
36
|
+
const err = new Error(ev.message || "Worker 错误");
|
|
37
|
+
for (const { reject } of this.pending.values())
|
|
38
|
+
reject(err);
|
|
39
|
+
this.pending.clear();
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** 启动 Worker 并完成初始化握手。 */
|
|
43
|
+
static async spawn(opts) {
|
|
44
|
+
const worker = new Worker(opts.workerUrl, { type: "module" });
|
|
45
|
+
const client = new AlliumWorkerClient(worker);
|
|
46
|
+
await client.post({
|
|
47
|
+
id: client.nextId++,
|
|
48
|
+
kind: "init",
|
|
49
|
+
payload: { moduleUrl: opts.moduleUrl, wasmUrl: opts.wasmUrl },
|
|
50
|
+
});
|
|
51
|
+
return client;
|
|
52
|
+
}
|
|
53
|
+
/** 渲染名片,返回编码图片字节。 */
|
|
54
|
+
async render(req) {
|
|
55
|
+
const result = await this.post({ id: this.nextId++, kind: "render", payload: req }, this.collectTransfer(req));
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* 分层裁剪渲染,返回裁剪后的 WebP 字节及其在原画布的偏移。
|
|
60
|
+
*/
|
|
61
|
+
async renderLayerCropped(req) {
|
|
62
|
+
const result = await this.post({ id: this.nextId++, kind: "renderLayerCropped", payload: req }, this.collectTransfer(req));
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 批量分层裁剪渲染:一次返回所有元素的 WebP 字节 + 元数据。
|
|
67
|
+
*/
|
|
68
|
+
async renderAllLayers(req) {
|
|
69
|
+
const result = await this.post({ id: this.nextId++, kind: "renderAllLayers", payload: req }, this.collectTransfer(req));
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
/** 收集名片所需素材 key。 */
|
|
73
|
+
async collectAssetKeys(cardJson, masterData) {
|
|
74
|
+
const result = await this.post({
|
|
75
|
+
id: this.nextId++,
|
|
76
|
+
kind: "collectAssetKeys",
|
|
77
|
+
payload: { cardJson, masterData },
|
|
78
|
+
});
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
/** 终止 Worker,拒绝所有在途请求。 */
|
|
82
|
+
terminate() {
|
|
83
|
+
this.worker.terminate();
|
|
84
|
+
for (const { reject } of this.pending.values()) {
|
|
85
|
+
reject(new Error("Worker 已终止"));
|
|
86
|
+
}
|
|
87
|
+
this.pending.clear();
|
|
88
|
+
}
|
|
89
|
+
collectTransfer(req) {
|
|
90
|
+
// 转移字体/素材的 ArrayBuffer,避免结构化克隆复制大缓冲。
|
|
91
|
+
// 注意:转移后调用方的 Uint8Array 会失效(detached);如需复用请先复制。
|
|
92
|
+
const transfer = [];
|
|
93
|
+
for (const f of req.fonts)
|
|
94
|
+
transfer.push(f.bytes.buffer);
|
|
95
|
+
for (const a of req.assets)
|
|
96
|
+
transfer.push(a.bytes.buffer);
|
|
97
|
+
return transfer;
|
|
98
|
+
}
|
|
99
|
+
post(msg, transfer = []) {
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
this.pending.set(msg.id, { resolve, reject });
|
|
102
|
+
this.worker.postMessage(msg, transfer);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
package/dist/worker.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 渲染 Worker 入口。在 Worker 线程加载 wasm 并串行处理请求。
|
|
3
|
+
*
|
|
4
|
+
* 用法(主线程通过 `./worker-client` 间接使用,或手动):
|
|
5
|
+
* ```ts
|
|
6
|
+
* const worker = new Worker(
|
|
7
|
+
* new URL("@allium/renderer-wasm/worker.js", import.meta.url),
|
|
8
|
+
* { type: "module" },
|
|
9
|
+
* );
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* 协议见 `./protocol`。请求严格串行(单 AlliumRenderer 实例)。
|
|
13
|
+
*/
|
|
14
|
+
export {};
|
package/dist/worker.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 渲染 Worker 入口。在 Worker 线程加载 wasm 并串行处理请求。
|
|
3
|
+
*
|
|
4
|
+
* 用法(主线程通过 `./worker-client` 间接使用,或手动):
|
|
5
|
+
* ```ts
|
|
6
|
+
* const worker = new Worker(
|
|
7
|
+
* new URL("@allium/renderer-wasm/worker.js", import.meta.url),
|
|
8
|
+
* { type: "module" },
|
|
9
|
+
* );
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* 协议见 `./protocol`。请求严格串行(单 AlliumRenderer 实例)。
|
|
13
|
+
*/
|
|
14
|
+
import { AlliumRenderer, ImageFormat } from "./renderer.js";
|
|
15
|
+
let renderer = null;
|
|
16
|
+
let moduleUrl = null;
|
|
17
|
+
let wasmUrl;
|
|
18
|
+
async function ensureRenderer() {
|
|
19
|
+
if (renderer)
|
|
20
|
+
return renderer;
|
|
21
|
+
if (!moduleUrl)
|
|
22
|
+
throw new Error("Worker 未初始化(先发送 init 消息)");
|
|
23
|
+
const factoryModule = (await import(/* @vite-ignore */ moduleUrl));
|
|
24
|
+
renderer = await AlliumRenderer.create(factoryModule.default, wasmUrl);
|
|
25
|
+
return renderer;
|
|
26
|
+
}
|
|
27
|
+
function handleInit(payload) {
|
|
28
|
+
moduleUrl = payload.moduleUrl;
|
|
29
|
+
wasmUrl = payload.wasmUrl;
|
|
30
|
+
renderer = null; // 下次请求时按新 URL 重建
|
|
31
|
+
}
|
|
32
|
+
function applyInputs(r, req) {
|
|
33
|
+
for (const { family, bytes } of req.fonts)
|
|
34
|
+
r.registerFont(family, bytes);
|
|
35
|
+
for (const [name, json] of Object.entries(req.masterData)) {
|
|
36
|
+
r.loadMasterData(name, json);
|
|
37
|
+
}
|
|
38
|
+
r.init();
|
|
39
|
+
for (const { key, bytes } of req.assets)
|
|
40
|
+
r.putAsset(key, bytes);
|
|
41
|
+
}
|
|
42
|
+
async function handle(msg) {
|
|
43
|
+
switch (msg.kind) {
|
|
44
|
+
case "init": {
|
|
45
|
+
handleInit(msg.payload);
|
|
46
|
+
return { transfer: [] };
|
|
47
|
+
}
|
|
48
|
+
case "render": {
|
|
49
|
+
const r = await ensureRenderer();
|
|
50
|
+
applyInputs(r, msg.payload);
|
|
51
|
+
const out = r.render(msg.payload.cardJson, msg.payload.format ?? ImageFormat.Jpeg, msg.payload.profileJson);
|
|
52
|
+
return { result: out, transfer: [out.buffer] };
|
|
53
|
+
}
|
|
54
|
+
case "renderLayerCropped": {
|
|
55
|
+
const r = await ensureRenderer();
|
|
56
|
+
applyInputs(r, msg.payload);
|
|
57
|
+
const out = r.renderLayerCropped(msg.payload.cardJson, msg.payload.quality ?? 80, msg.payload.profileJson);
|
|
58
|
+
return { result: out, transfer: [out.data.buffer] };
|
|
59
|
+
}
|
|
60
|
+
case "renderAllLayers": {
|
|
61
|
+
const r = await ensureRenderer();
|
|
62
|
+
applyInputs(r, msg.payload);
|
|
63
|
+
const layers = r.renderAllLayers(msg.payload.cardJson, msg.payload.quality ?? 80, msg.payload.includeProperties ?? true, msg.payload.profileJson);
|
|
64
|
+
// 每层 data 是独立 Uint8Array,全部 transfer 回主线程,避免大缓冲克隆。
|
|
65
|
+
const transfer = layers
|
|
66
|
+
.map((l) => l.data.buffer)
|
|
67
|
+
.filter((b) => b.byteLength > 0);
|
|
68
|
+
return { result: layers, transfer };
|
|
69
|
+
}
|
|
70
|
+
case "collectAssetKeys": {
|
|
71
|
+
const r = await ensureRenderer();
|
|
72
|
+
for (const [name, json] of Object.entries(msg.payload.masterData)) {
|
|
73
|
+
r.loadMasterData(name, json);
|
|
74
|
+
}
|
|
75
|
+
r.init();
|
|
76
|
+
const keys = r.collectAssetKeys(msg.payload.cardJson);
|
|
77
|
+
return { result: keys, transfer: [] };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
self.onmessage = async (ev) => {
|
|
82
|
+
const msg = ev.data;
|
|
83
|
+
try {
|
|
84
|
+
const { result, transfer } = await handle(msg);
|
|
85
|
+
const res = { id: msg.id, ok: true, result };
|
|
86
|
+
self.postMessage(res, transfer);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
const res = {
|
|
90
|
+
id: msg.id,
|
|
91
|
+
ok: false,
|
|
92
|
+
error: err instanceof Error ? err.message : String(err),
|
|
93
|
+
};
|
|
94
|
+
self.postMessage(res);
|
|
95
|
+
}
|
|
96
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@empty-sekai/renderer-wasm",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Browser wasm build of the allium custom-profile-card renderer (skia CPU + FreeType).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "AGPL-3.0-only",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist/",
|
|
9
|
+
"src/",
|
|
10
|
+
"LICENSE",
|
|
11
|
+
"LICENSE-EXCEPTION",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"module": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./worker": {
|
|
23
|
+
"types": "./dist/worker-client.d.ts",
|
|
24
|
+
"import": "./dist/worker-client.js"
|
|
25
|
+
},
|
|
26
|
+
"./allium_renderer_wasm.wasm": "./dist/allium_renderer_wasm.wasm"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build:wasm": "bash ./build.sh",
|
|
30
|
+
"build:ts": "tsc -p tsconfig.json && node ./scripts/copy-artifacts.mjs",
|
|
31
|
+
"build": "npm run build:wasm && npm run build:ts"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"typescript": "^5.6.0"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
|
|
10
|
+
export interface EmscriptenModule {
|
|
11
|
+
/** wasm 线性内存的字节视图。ALLOW_MEMORY_GROWTH 下增长后会被替换,每次用前重读。 */
|
|
12
|
+
HEAPU8: Uint8Array;
|
|
13
|
+
HEAPU32: Uint32Array;
|
|
14
|
+
|
|
15
|
+
_malloc(size: number): number;
|
|
16
|
+
_free(ptr: number): void;
|
|
17
|
+
|
|
18
|
+
getValue(ptr: number, type: "i32" | "i8" | "i16" | "float" | "double" | "*"): number;
|
|
19
|
+
setValue(ptr: number, value: number, type: "i32" | "i8" | "i16" | "float" | "double" | "*"): void;
|
|
20
|
+
lengthBytesUTF8(str: string): number;
|
|
21
|
+
stringToUTF8(str: string, outPtr: number, maxBytes: number): void;
|
|
22
|
+
UTF8ToString(ptr: number, maxBytes?: number): string;
|
|
23
|
+
|
|
24
|
+
cwrap(
|
|
25
|
+
name: string,
|
|
26
|
+
returnType: "number" | "void" | null,
|
|
27
|
+
argTypes: Array<"number" | "string" | "array">,
|
|
28
|
+
): (...args: number[]) => number;
|
|
29
|
+
|
|
30
|
+
// alr_* 导出(直接引用形式,cwrap 是更安全的调用入口)
|
|
31
|
+
_alr_alloc(size: number): number;
|
|
32
|
+
_alr_free(ptr: number, size: number): void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type EmscriptenModuleFactory = (
|
|
36
|
+
moduleArg?: Partial<EmscriptenModule> & { locateFile?: (path: string, prefix: string) => string },
|
|
37
|
+
) => Promise<EmscriptenModule>;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @empty-sekai/renderer-wasm — 浏览器名片渲染。
|
|
3
|
+
*
|
|
4
|
+
* 入口导出核心封装与类型。Worker 调度见子路径 `@empty-sekai/renderer-wasm/worker`。
|
|
5
|
+
*
|
|
6
|
+
* 最小用法(主线程,注意 skia 光栅化同步阻塞):
|
|
7
|
+
* ```ts
|
|
8
|
+
* import createAlliumRenderer from "@empty-sekai/renderer-wasm/allium_renderer_wasm.js";
|
|
9
|
+
* import { AlliumRenderer, ImageFormat } from "@empty-sekai/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
|
+
|
|
22
|
+
export { AlliumRenderer, ImageFormat, AlliumRenderError } from "./renderer.js";
|
|
23
|
+
export type { CroppedLayerOutput, LayerCrop } from "./renderer.js";
|
|
24
|
+
export type { EmscriptenModule, EmscriptenModuleFactory } from "./emscripten.js";
|