leafer-x-psd 0.0.1-beta.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/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "leafer-x-psd",
3
+ "version": "0.0.1-beta.0",
4
+ "description": "Parse PSD files into leaferjs Frames - a leafer-x plugin.",
5
+ "author": "Tian",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "./dist/psd.umd.cjs",
9
+ "module": "./dist/psd.mjs",
10
+ "types": "./types/index.d.ts",
11
+ "unpkg": "./dist/psd.umd.cjs",
12
+ "jsdelivr": "./dist/psd.umd.cjs",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./types/index.d.ts",
16
+ "import": "./dist/psd.mjs",
17
+ "require": "./dist/psd.umd.cjs"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "types"
23
+ ],
24
+ "sideEffects": false,
25
+ "scripts": {
26
+ "build": "npm run build:types && vite build",
27
+ "build:types": "tsc -p tsconfig.build.json",
28
+ "test": "vitest run",
29
+ "test:watch": "vitest",
30
+ "test:artifact": "vitest run __tests__/package.test.ts",
31
+ "typecheck": "tsc --noEmit",
32
+ "inspect": "node scripts/inspect-psd.mjs",
33
+ "verify:artifact": "npm run build && npm run test:artifact",
34
+ "prepublishOnly": "npm run verify:artifact"
35
+ },
36
+ "keywords": [
37
+ "leafer",
38
+ "leafer-x",
39
+ "leaferjs",
40
+ "psd",
41
+ "photoshop",
42
+ "parser"
43
+ ],
44
+ "comment:dependencies": "ag-psd 已被打包进 dist(保证 UMD/CDN 全局用法可用),因此不再声明为运行时依赖,避免消费者多装一份用不到的东西。",
45
+ "peerDependencies": {
46
+ "@leafer-ui/core": "^2.2.11",
47
+ "@leafer-ui/interface": "^2.2.11"
48
+ },
49
+ "devDependencies": {
50
+ "@leafer-ui/core": "^2.2.11",
51
+ "@leafer-ui/interface": "^2.2.11",
52
+ "@leafer-ui/node": "^2.2.11",
53
+ "@napi-rs/canvas": "^1.0.9",
54
+ "@types/node": "^26.6.1",
55
+ "ag-psd": "^31.0.2",
56
+ "typescript": "^5.9.3",
57
+ "vite": "^8.3.0",
58
+ "vitest": "^5.0.1"
59
+ }
60
+ }
@@ -0,0 +1,33 @@
1
+ import type { Layer } from 'ag-psd';
2
+ import type { IUI, IUIInputData } from '@leafer-ui/interface';
3
+ import type { BuildContext } from '../context';
4
+ import type { AnyCanvas, PsdElementRole } from '../types';
5
+ /**
6
+ * 图层内容元素的公共输入数据。
7
+ *
8
+ * 位置一律用 `measureLayer` 得到的文档绝对盒换算到父容器局部坐标,
9
+ * 这是避免组内子层双重偏移的唯一入口。
10
+ */
11
+ export declare function buildBaseData(layer: Layer, ctx: BuildContext, role: PsdElementRole): IUIInputData;
12
+ /** 图层的尺寸(文档绝对盒的宽高)。 */
13
+ export declare function layerSize(layer: Layer): {
14
+ width: number;
15
+ height: number;
16
+ };
17
+ /**
18
+ * 包装容器的输入数据。
19
+ *
20
+ * 包装器(遮罩、剪贴蒙版)与被包装的内容**共用同一个原点**:包装器放在内容
21
+ * 原本的位置上,内容被移到 (0, 0)。这样蒙版元素可以用 `0,0` 起算的局部坐标,
22
+ * 整棵子树的绝对位置不变。
23
+ */
24
+ export declare function buildWrapperData(layer: Layer, ctx: BuildContext, role: PsdElementRole, x: number, y: number): IUIInputData;
25
+ /**
26
+ * 把图层像素统一取成一张画布。
27
+ *
28
+ * `readPsd` 默认产出 `layer.canvas`;如果宿主传了 `useImageData: true`,
29
+ * 这里负责把裸像素还原成画布。
30
+ */
31
+ export declare function layerCanvas(layer: Layer): AnyCanvas | undefined;
32
+ /** 把元素沿自身原点平移,用于放进与自身同原点的包装容器。 */
33
+ export declare function moveToOrigin(element: IUI): void;
@@ -0,0 +1,8 @@
1
+ import type { LayerAdapter } from './types';
2
+ /**
3
+ * 图层组适配器。
4
+ *
5
+ * 组自身不设宽高,边界由子树推导;位置取子层递归求并集得到的原点,
6
+ * 子层坐标在 tree 里会减去这个原点(ag-psd 给的是文档绝对坐标)。
7
+ */
8
+ export declare const groupAdapter: LayerAdapter;
@@ -0,0 +1,11 @@
1
+ import type { LayerAdapter } from './types';
2
+ /**
3
+ * 像素图层适配器:把 `layer.canvas` 烘焙成 `Image`。
4
+ *
5
+ * 走 `Resource.setImage` 把画布直接注册为资源,`Image.url` 用资源符引用,
6
+ * 全程没有 base64 编解码。同时登记一份资源记录(key → 图层信息),
7
+ * 供 `exportResources()` 之后把这些资源换成真实 URL。
8
+ *
9
+ * 优先级最低,作为所有无法语义化还原的图层的兜底。
10
+ */
11
+ export declare const imageAdapter: LayerAdapter;
@@ -0,0 +1,36 @@
1
+ import type { Layer } from 'ag-psd';
2
+ import type { IUI } from '@leafer-ui/interface';
3
+ import type { BuildContext } from '../context';
4
+ import type { LayerAdapter } from './types';
5
+ /**
6
+ * 注册图层适配器。同名会覆盖,按 `priority` 降序匹配。
7
+ * 宿主可以注册自己的适配器来接管特定图层(例如 PSD 里的智能对象)。
8
+ */
9
+ export declare function registerAdapter(adapter: LayerAdapter): void;
10
+ export declare function unregisterAdapter(name: string): void;
11
+ export declare function listAdapters(): readonly LayerAdapter[];
12
+ /** 返回第一个匹配上的适配器(不尝试创建)。 */
13
+ export declare function resolveAdapter(layer: Layer, ctx: BuildContext): LayerAdapter | undefined;
14
+ export interface AdapterResolution {
15
+ /** 成功创建的图层内容 */
16
+ element?: IUI;
17
+ /** 实际生效的适配器 */
18
+ adapter?: LayerAdapter;
19
+ /** 优先级更高但创建失败的适配器,用于给出降级诊断 */
20
+ degraded?: {
21
+ adapter: LayerAdapter;
22
+ error?: unknown;
23
+ };
24
+ }
25
+ /**
26
+ * 按优先级依次尝试适配器,直到某一层成功创建出元素。
27
+ *
28
+ * 这是「语义化优先、失败降级」的落点:例如文字适配器认领了图层却没能还原,
29
+ * 就会继续往下试,最终由兜底的位图适配器接手,同时把降级的事实报出去,
30
+ * 而不是让整个解析失败或静默产生错误结果。
31
+ *
32
+ * 适配器抛出的异常也会被吞掉并记为降级原因 —— 单个图层的数据异常不应该
33
+ * 让整份 PSD 解析崩掉。
34
+ */
35
+ export declare function createLayerContent(layer: Layer, ctx: BuildContext): AdapterResolution;
36
+ export type { LayerAdapter };
@@ -0,0 +1,12 @@
1
+ import type { LayerAdapter } from './types';
2
+ /**
3
+ * 文字图层适配器。
4
+ *
5
+ * 定位模型:把 Leafer `Text` 的**首行基线**对齐到 PS 的基线。
6
+ *
7
+ * Leafer 内部算出首行基线距盒子顶部的偏移是
8
+ * `__baseLine = lineHeight - (lineHeight - fontSize * 0.7) / 2 = lineHeight / 2 + 0.35 * fontSize`
9
+ * (见 `@leafer-ui/draw` 的文本样式计算),再把 `around` 设为 `{ x: 0, y: 基线偏移 }`,
10
+ * 就能让「元素内部的基线起点」正好落在元素的 `(x, y)` 上,旋转也自然绕基线进行。
11
+ */
12
+ export declare const textAdapter: LayerAdapter;
@@ -0,0 +1,17 @@
1
+ import type { Layer } from 'ag-psd';
2
+ import type { IUI } from '@leafer-ui/interface';
3
+ import type { BuildContext } from '../context';
4
+ /**
5
+ * 图层适配器:把一类 PSD 图层还原成 Leafer 元素。
6
+ *
7
+ * 按 `priority` 从高到低匹配,第一个 `match` 返回 true 的适配器负责创建内容。
8
+ * 语义化还原失败时应返回 `undefined`,由调用方降级到下一个适配器。
9
+ */
10
+ export interface LayerAdapter {
11
+ /** 适配器名,用于诊断。 */
12
+ readonly name: string;
13
+ /** 匹配优先级,越大越先尝试。 */
14
+ readonly priority: number;
15
+ match(layer: Layer, ctx: BuildContext): boolean;
16
+ create(layer: Layer, ctx: BuildContext): IUI | undefined;
17
+ }
@@ -0,0 +1,37 @@
1
+ import type { Layer, Psd } from 'ag-psd';
2
+ import type { ResourceCollector } from './resources';
3
+ import type { PsdWarningCode, ResolvedOptions } from './types';
4
+ export interface Point {
5
+ x: number;
6
+ y: number;
7
+ }
8
+ /**
9
+ * 构建期的共享上下文。
10
+ *
11
+ * 里面最要紧的是 `origin`:它是**当前父容器在文档坐标系中的原点**。
12
+ * ag-psd 给出的图层坐标全是文档绝对坐标,所以每往下进一层容器,
13
+ * 都要用 `localX/localY` 把绝对坐标换算成父容器局部坐标,否则就会
14
+ * 出现层级越深偏得越多的双重偏移。
15
+ */
16
+ export interface BuildContext {
17
+ readonly psd: Psd;
18
+ readonly options: ResolvedOptions;
19
+ /** 当前父容器原点(文档绝对坐标) */
20
+ readonly origin: Point;
21
+ /**
22
+ * 本次解析登记的资源记录(key → 图层信息 + 画布)。
23
+ *
24
+ * `Resource` 是全局表且不会自动回收,把记录挂在这里,解析完成后可以交给
25
+ * `releaseResources()` / `exportResources()` 统一处理,避免反复加载 PSD 时
26
+ * 内存持续增长。收集器在所有派生上下文之间共享。
27
+ */
28
+ readonly resources: ResourceCollector;
29
+ /** 文档 X 绝对坐标 → 父容器局部坐标 */
30
+ localX(absolute: number): number;
31
+ /** 文档 Y 绝对坐标 → 父容器局部坐标 */
32
+ localY(absolute: number): number;
33
+ warn(code: PsdWarningCode, message: string, layer?: Layer): void;
34
+ }
35
+ export declare function createContext(psd: Psd, options: ResolvedOptions, resources: ResourceCollector): BuildContext;
36
+ /** 进入子容器时派生一个新上下文,原点切换为该容器的绝对原点。 */
37
+ export declare function withOrigin(ctx: BuildContext, origin: Point): BuildContext;
@@ -0,0 +1,14 @@
1
+ import type { LayerEffectsInfo } from 'ag-psd';
2
+ import type { AnyCanvas } from '../types';
3
+ export interface BakedEffects {
4
+ canvas: AnyCanvas;
5
+ /** 烘焙画布原点相对原图层盒原点的偏移(≤ 0) */
6
+ offsetX: number;
7
+ offsetY: number;
8
+ }
9
+ /**
10
+ * 把图层效果烘焙进位图。
11
+ *
12
+ * @returns 新的画布与偏移;没有任何可烘焙的效果时返回 `undefined`。
13
+ */
14
+ export declare function bakeRasterEffects(source: AnyCanvas, effects: LayerEffectsInfo, blurScale: number): BakedEffects | undefined;
@@ -0,0 +1,34 @@
1
+ import type { Layer } from 'ag-psd';
2
+ import type { IUI } from '@leafer-ui/interface';
3
+ import type { BuildContext } from '../context';
4
+ /**
5
+ * 一个剪贴蒙版组。
6
+ *
7
+ * PS 的剪贴蒙版在 ag-psd 的图层数组里表现为:基底层(`clipping: false`)
8
+ * 后面紧跟连续的若干个 `clipping: true` 的图层。数组顺序是**自下而上**的,
9
+ * 所以「后面紧跟」正好对应 PS 里「压在基底上面的那些层」。
10
+ */
11
+ export interface ClippingRun {
12
+ /** 基底层,决定裁剪形状与整个组的透明度 */
13
+ base: Layer;
14
+ /** 被基底裁剪的图层,自下而上 */
15
+ contents: Layer[];
16
+ /** 该剪贴组结束的下标(不含) */
17
+ end: number;
18
+ }
19
+ /**
20
+ * 从 `start` 开始探测一个剪贴蒙版组。
21
+ *
22
+ * 孤立出现的 `clipping: true`(下方没有基底层)会返回 `undefined`,
23
+ * 交由普通图层逻辑处理,避免把无效数据放大成错误结构。
24
+ */
25
+ export declare function findClippingRun(layers: Layer[], start: number): ClippingRun | undefined;
26
+ /**
27
+ * 把基底层与被裁剪层组装成 Leafer 的剪贴遮罩。
28
+ *
29
+ * Leafer 的 `mask: 'clipping'` 官方描述就是「和 PS 中的剪贴蒙版一样的效果:
30
+ * 使用每个像素的透明度,并会显示自身」,所以语义完全对齐,不需要自己合成。
31
+ *
32
+ * 结构上:Group 内第一个子元素是带 `mask: 'clipping'` 的基底层,其余层压在它上面。
33
+ */
34
+ export declare function buildClippingGroup(run: ClippingRun, baseElement: IUI, contentElements: IUI[], ctx: BuildContext): IUI;
@@ -0,0 +1,25 @@
1
+ import type { Layer } from 'ag-psd';
2
+ import type { IUI } from '@leafer-ui/interface';
3
+ import type { BuildContext } from '../context';
4
+ /**
5
+ * 应用图层效果。
6
+ *
7
+ * 分两条路径,因为 PS 与 Leafer 的能力边界不同:
8
+ *
9
+ * 1. **位图图层(`Image`)→ 自己烘焙进位图**。
10
+ * PS 的描边、颜色叠加、内发光等都是沿图层 **alpha 轮廓** 生效的,而 Leafer 的
11
+ * `stroke` / `fill` 对 `Image` 要么作用在包围盒上、要么被图片画笔占用
12
+ * (`Image` 内部就用 `fill` 承载图片)。所以这类图层必须按 alpha 合成,
13
+ * 见 `bake.ts`。
14
+ *
15
+ * 2. **文字与形状图层 → 映射成 Leafer 属性**。
16
+ * 这类元素自身的形状就是内容形状,`stroke` / `fill` 语义与 PS 一致,直接映射即可,
17
+ * 而且保持了可编辑性。
18
+ *
19
+ * 无法映射的(斜面浮雕 / 光泽 / 图案叠加)一律丢弃并告警,绝不静默产生错误画面。
20
+ *
21
+ * ⚠️ 文件里带着离谱的效果参数(`size` / `choke` 超出 PS 的正常量级)时,
22
+ * 数值会被钳制并报 `limit-exceeded`;需要临时画布的效果若因此超出画布配额,
23
+ * 则保留未烘焙的内容并报同一条诊断 —— 都不让整份 PSD 解析失败。
24
+ */
25
+ export declare function applyEffects(layer: Layer, content: IUI, ctx: BuildContext): IUI;
@@ -0,0 +1,17 @@
1
+ import type { Layer } from 'ag-psd';
2
+ import type { IUI } from '@leafer-ui/interface';
3
+ import type { BuildContext } from '../context';
4
+ /**
5
+ * 给图层内容套上图层蒙版。
6
+ *
7
+ * 两个容易被忽略的点:
8
+ *
9
+ * 1. **PS 的矢量蒙版与像素图层蒙版可以同时存在,两者取交集。**
10
+ * 所以这里不是「矢量优先、否则位图」,而是把可用的蒙版**逐个嵌套**套上去
11
+ * (Leafer 每个容器只认一个遮罩元素,嵌套容器天然求交)。
12
+ * 实测样例 `图层 1 拷贝 2` 就属于这种双蒙版图层。
13
+ *
14
+ * 2. 遮罩元素必须是**同一父 Group 下的兄弟节点**,并且自身不渲染,
15
+ * 因此每个蒙版都要包一层容器,把遮罩放在内容下面。
16
+ */
17
+ export declare function decorateWithMask(layer: Layer, content: IUI, ctx: BuildContext): IUI;
@@ -0,0 +1,37 @@
1
+ import type { IUI } from '@leafer-ui/interface';
2
+ import type { PsdExportOptions, PsdExportResult } from './types';
3
+ /**
4
+ * 把解析结果里的会话内资源符(`RESOURCE_PREFIX` 开头的那些)换成真实 URL,
5
+ * 让 Frame 变成**可持久化**的。
6
+ *
7
+ * 为什么要有这一步:解析出来的 `Image.url` 是会话内的资源符
8
+ * (Leafer `Resource` 表里的一个 key),`frame.toJSON()` 会把它原样序列化 ——
9
+ * 存进数据库、换个客户端、分享链接,图片全都加载不出来。
10
+ *
11
+ * 为什么是独立的一步、而不是塞进 `psdToFrame`:
12
+ *
13
+ * - 解析目前全程**零编码**(画布直通),这是它最大的性能优势。一旦要求上传就必须先编码,
14
+ * 这个优势就没了。
15
+ * - 上传会慢、会失败、需要并发限流与重试 —— 那是存储与网络问题,不是解析问题。
16
+ * 混在一起会让 `psdToFrame` 的语义变浑(到底是「解析」还是「解析并上传」)。
17
+ * - 有些用户根本不需要(只在当前会话里编辑,最后导出一张图),强制走编码是纯损失。
18
+ *
19
+ * ```ts
20
+ * const frame = await psdToFrame(file)
21
+ *
22
+ * await exportResources(frame, {
23
+ * resolve: async (canvas, info) => {
24
+ * const blob = await canvasToBlob(canvas, 'webp', 0.9)
25
+ * const name = `${info.layer?.name ?? 'unnamed'}-${info.kind}.webp`
26
+ * return (await myOss.put(`psd/${name}`, blob)).url
27
+ * },
28
+ * onProgress: (done, total) => console.log(`${done}/${total}`),
29
+ * })
30
+ *
31
+ * const json = frame.toJSON() // 这时候才是可持久化的
32
+ * ```
33
+ *
34
+ * 成功替换后会把旧的 `leafer://` 从 Leafer `Resource` 表里**摘掉** —— 那张画布
35
+ * 已经没人引用了,摘掉能立刻省下一份内存。
36
+ */
37
+ export declare function exportResources(target: IUI, options: PsdExportOptions): Promise<PsdExportResult>;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * leafer-x-psd —— 把 PSD 文件解析成 leaferjs 的 `Frame`。
3
+ *
4
+ * 设计取向是「混合路线」:
5
+ * - 图层结构、图层蒙版、剪贴蒙版、混合模式尽量**语义化**还原成 Leafer 原生能力;
6
+ * - 像素图层与形状图层烘焙成位图(ag-psd 没有暴露形状路径数据,做不到矢量还原);
7
+ * - 无法映射的部分不静默失败,而是通过 `onWarning` 报出来。
8
+ */
9
+ export { DEFAULT_SHADOW_BLUR_SCALE, PsdParser, psdToContainer, psdToFrame, releaseAllResources, resolveOptions, } from './parser';
10
+ export { readPsdFromSource } from './reader';
11
+ export { ResourceCollector, attachCollector, getCollector, getResources, registerResource, releaseResources, unregisterResource, } from './resources';
12
+ export { exportResources } from './exporter';
13
+ export { RESOURCE_PREFIX, canvasToBlob, canvasToBuffer } from './platform';
14
+ export { listAdapters, registerAdapter, resolveAdapter, unregisterAdapter, } from './adapter';
15
+ export type { LayerAdapter } from './adapter/types';
16
+ export { toLeaferBlendMode } from './utils/blendMode';
17
+ export { boxHeight, boxWidth, countLayers, measureLayer, ownBox, unionBox } from './utils/box';
18
+ export { DEFAULT_CANVAS_LIMITS, PsdCanvasLimitError, describeCanvasLimits, getCanvasLimits, setCanvasLimits, } from './utils/limits';
19
+ export type { CanvasLimits } from './utils/limits';
20
+ export { bezierPathsToSvg, bezierWindingRule } from './utils/geometry';
21
+ export { colorToString } from './utils/color';
22
+ export { hasFontAlias, toFontFamily } from './utils/font';
23
+ export type { Box } from './utils/box';
24
+ export type * from './types';
@@ -0,0 +1,67 @@
1
+ import { Frame } from '@leafer-ui/core';
2
+ import type { Psd } from 'ag-psd';
3
+ import type { IUI } from '@leafer-ui/interface';
4
+ import { releaseAllResources } from './platform';
5
+ import type { PsdParseOptions, PsdSource, ResolvedOptions } from './types';
6
+ /**
7
+ * PS 投影/内阴影的 `size` 与画布 `shadowBlur` 之间的换算系数。
8
+ *
9
+ * 对着真实 PSD 的参考合成图扫描出来的:0.55 ~ 0.75 是一个平坦的谷底,取 0.70。
10
+ * 手算也吻合 —— 参考图的投影衰减曲线拟合出 σ ≈ 6.33px,而画布 `shadowBlur = 2σ ≈ 12.66`,
11
+ * `12.66 / 17(PS size) ≈ 0.745`。两条独立路径落在同一个区间,不是过拟合。
12
+ */
13
+ export declare const DEFAULT_SHADOW_BLUR_SCALE = 0.7;
14
+ export declare function resolveOptions(options?: PsdParseOptions): ResolvedOptions;
15
+ /** 把一个已解码的 `Psd` 构建成 Leafer 元素树。 */
16
+ export declare function psdToContainer(psd: Psd, root: IUI, options?: PsdParseOptions): Promise<IUI>;
17
+ /** 清空本插件注册过的全部资源(不知道具体清单时的兜底,例如热重载)。 */
18
+ export { releaseAllResources };
19
+ /**
20
+ * 解析 PSD 文件,返回一个 Leafer `Frame`。
21
+ *
22
+ * ```ts
23
+ * import { Leafer } from 'leafer-ui'
24
+ * import { psdToFrame } from 'leafer-x-psd'
25
+ *
26
+ * const frame = await psdToFrame(fileInput.files[0])
27
+ * new Leafer({ view: window }).add(frame)
28
+ * ```
29
+ */
30
+ export declare function psdToFrame(source: PsdSource, options?: PsdParseOptions): Promise<Frame>;
31
+ /**
32
+ * 解析器。相比 `psdToFrame`,它把「解码」与「构建」拆开,
33
+ * 于是可以复用同一份 `Psd` 反复构建,也可以中途取消。
34
+ *
35
+ * ```ts
36
+ * const parser = new PsdParser(file)
37
+ * const psd = await parser.ready // 只解码,拿到文档尺寸/图层树
38
+ * const frame = await parser.toFrame() // 需要时再构建
39
+ * parser.abort() // 取消
40
+ * ```
41
+ */
42
+ export declare class PsdParser {
43
+ private _psd;
44
+ private failure;
45
+ private readonly controller;
46
+ private readonly options;
47
+ private built;
48
+ /** 解码完成的 `Psd`,未完成时抛错。 */
49
+ get psd(): Psd;
50
+ get isReady(): boolean;
51
+ get width(): number;
52
+ get height(): number;
53
+ /** 顶层图层数组(自下而上) */
54
+ get layers(): import("ag-psd").Layer[];
55
+ /** 解码完成的 Promise,可安全地重复 await。 */
56
+ readonly ready: Promise<Psd>;
57
+ constructor(source: PsdSource, options?: PsdParseOptions);
58
+ private decode;
59
+ /** 构建 `Frame`。可传入已有容器以复用。 */
60
+ toFrame(root?: Frame): Promise<Frame>;
61
+ /** 是否已经构建过 Frame。 */
62
+ get isBuilt(): boolean;
63
+ /** 解析失败时的原始错误。 */
64
+ get error(): unknown;
65
+ /** 取消进行中的解析。 */
66
+ abort(reason?: unknown): void;
67
+ }
@@ -0,0 +1,89 @@
1
+ import type { AnyCanvas, PsdResourceFormat } from './types';
2
+ /** 注册到 Leafer `Resource` 里的资源符前缀,用于识别与回收本插件产生的资源。 */
3
+ export declare const RESOURCE_PREFIX = "leafer://psd-plugin-resource-";
4
+ /**
5
+ * 确保 ag-psd 有可用的画布工厂。
6
+ *
7
+ * ag-psd 是在**模块求值时**检测 `typeof document !== 'undefined'` 来决定画布工厂的
8
+ * (见 `ag-psd/dist/helpers.js`),所以浏览器里开箱即用,Node / 小程序里则必须显式
9
+ * 调用 `initializeCanvas`。
10
+ *
11
+ * 关键点:ag-psd 被本插件打包进产物后,产物里有**自己的一份副本**,宿主对独立安装的
12
+ * `ag-psd` 调用 `initializeCanvas` 影响不到它。所以这里直接从 Leafer 的平台适配
13
+ * (`Platform.origin.createCanvas`)取工厂自动接上 —— 宿主既然已经能跑 Leafer,
14
+ * 这一步就不该再让用户操心。
15
+ *
16
+ * ⚠️ 解码期的画布同样要过配额(`utils/limits.ts`):这里的尺寸**全部来自文件内容**
17
+ * (文档尺寸、图层尺寸、蒙版尺寸),而解码又是整个流程里内存占用最大的一步。
18
+ * 超限时抛 `PsdCanvasLimitError`,由 `reader.ts` 转成可读的错误信息。
19
+ */
20
+ export declare function ensureCanvasFactory(): void;
21
+ /**
22
+ * 手动指定 ag-psd 的画布工厂。
23
+ *
24
+ * 只有在自动接管不适用时才需要(例如小程序里用自定义离屏画布)。
25
+ *
26
+ * ```ts
27
+ * useCanvas(
28
+ * (width, height) => myCreateCanvas(width, height),
29
+ * (width, height) => myCreateImageData(width, height),
30
+ * )
31
+ * ```
32
+ */
33
+ export declare function useCanvas(createCanvasMethod: (width: number, height: number) => AnyCanvas, createImageDataMethod?: (width: number, height: number) => ImageData): void;
34
+ /**
35
+ * 创建一张画布。优先走 Leafer 平台适配,其次回退到 DOM。
36
+ *
37
+ * ⚠️ **这是本插件唯一分配画布的地方,也是配额(`utils/limits.ts`)的唯一收口点。**
38
+ * PSD 里的图层尺寸、蒙版框、效果半径都会变成本函数的入参,所以在这里校验一次,
39
+ * 就等于所有分配路径都挡上了。超限抛 `PsdCanvasLimitError`;调用方(蒙版、烘焙、
40
+ * 图片适配器)负责把它降级成 `limit-exceeded` 告警,而不是让整份 PSD 解析失败。
41
+ */
42
+ export declare function createCanvas(width: number, height: number): AnyCanvas;
43
+ /**
44
+ * 把画布注册成 Leafer 资源,返回可以直接喂给 `Image.url` 的资源符。
45
+ *
46
+ * 这是本插件性能上最关键的一步:`Resource.setImage` 内部把画布存成
47
+ * `{ url, view }`,Leafer 渲染时**直接使用这张画布**,没有编码、
48
+ * 没有 base64、没有二次解码。相比之下 `canvas.toDataURL()` 会做一次
49
+ * 完整的 PNG 编码 + base64 展开,再在渲染时解码回来 —— 对大 PSD 是灾难。
50
+ *
51
+ * 资源记录(key → 图层信息)由 `resources.ts` 的 `registerResource` 负责,
52
+ * 这里只管 Leafer 那一侧。
53
+ */
54
+ export declare function registerCanvas(canvas: AnyCanvas): string;
55
+ /** 取得 2D 上下文,失败时抛出可读的错误。 */
56
+ export declare function get2dContext(canvas: AnyCanvas): any;
57
+ /**
58
+ * 回收一批资源符。
59
+ *
60
+ * `Resource` 是一张全局表,本插件注册进去的画布**不会**自动释放。
61
+ * 反复加载 PSD(例如做一个能来回切换文件的预览器)时,旧画布会一直留在
62
+ * 表里,内存持续增长。所以解析结果上记了一份资源清单,用完调这里清掉。
63
+ */
64
+ export declare function releaseResourceKeys(keys: readonly string[]): number;
65
+ /**
66
+ * 清空本插件注册过的全部资源(不知道具体清单时的兜底,例如热重载)。
67
+ *
68
+ * `Resource.map` 是 Leafer **公开**的资源表(`IResource.map`,只是声明成了 `any`,
69
+ * 形如 `{ [key]: { url, view } }`),所以这里直接给它一个具体类型即可 ——
70
+ * 不需要绕过类型系统去猜内部结构。
71
+ */
72
+ export declare function releaseAllResources(): number;
73
+ /**
74
+ * 把画布编码成字节。
75
+ *
76
+ * 依次尝试:`toBuffer`(node-canvas / @napi-rs)→ `convertToBlob`(@napi-rs)
77
+ * → `toBlob`(浏览器)→ `toDataURL` 手工解 base64。
78
+ *
79
+ * @param format 默认 `'png'`;照片类图层用 `'webp'` 体积小很多
80
+ * @param quality 0~1,仅对 `webp` / `jpeg` 有效,默认 0.92
81
+ */
82
+ export declare function canvasToBuffer(canvas: AnyCanvas, format?: PsdResourceFormat, quality?: number): Promise<Uint8Array>;
83
+ /**
84
+ * 把画布编码成 `Blob`(浏览器上传首选:比 dataURL 少 33% 的 base64 膨胀)。
85
+ *
86
+ * 依次尝试:`convertToBlob`(@napi-rs)→ `toBlob`(浏览器)→ `toBuffer` 包一层
87
+ * → `toDataURL` 手工解 base64。
88
+ */
89
+ export declare function canvasToBlob(canvas: AnyCanvas, format?: PsdResourceFormat, quality?: number): Promise<Blob>;
@@ -0,0 +1,13 @@
1
+ import type { Psd, ReadOptions } from 'ag-psd';
2
+ import type { PsdSource } from './types';
3
+ /**
4
+ * 解码 PSD 二进制。
5
+ *
6
+ * ⚠️ `readPsd` 是**同步阻塞**的:文件越大卡得越久。本插件不做 Worker 隔离
7
+ * (按既定方案),但宿主如果处理很大的 PSD,建议自己在 Worker 里调用本函数,
8
+ * 再把得到的 `Psd` 传进来构建元素树(画布对象可跨 Worker 传输 ImageBitmap)。
9
+ *
10
+ * 解码期的画布分配同样受配额约束(`utils/limits.ts`):文件声明的文档尺寸/图层尺寸
11
+ * 超出上限时会抛 `PsdCanvasLimitError`(被包装成可读信息),而不是去申请一张巨图。
12
+ */
13
+ export declare function readPsdFromSource(source: PsdSource, options?: ReadOptions): Promise<Psd>;
@@ -0,0 +1,62 @@
1
+ import type { Layer } from 'ag-psd';
2
+ import type { IUI } from '@leafer-ui/interface';
3
+ import type { AnyCanvas, PsdResource, PsdResourceKind } from './types';
4
+ /**
5
+ * 本次解析登记的资源记录。
6
+ *
7
+ * 每张注册进 Leafer `Resource` 表的画布都会记一条,把「资源符」和「它来自哪个 PSD 图层」
8
+ * 关联起来 —— `exportResources()` 靠它决定把哪张画布交给回调、以及回调能拿到什么元数据。
9
+ *
10
+ * 生命周期是「一次解析一份」:会连同被替换掉的条目一起维护,不残留脏数据。
11
+ */
12
+ export declare class ResourceCollector {
13
+ private readonly entries;
14
+ private readonly byKey;
15
+ add(entry: PsdResource): void;
16
+ get(key: string): PsdResource | undefined;
17
+ remove(key: string): void;
18
+ list(): readonly PsdResource[];
19
+ keys(): string[];
20
+ clear(): void;
21
+ get size(): number;
22
+ }
23
+ /**
24
+ * 注册一张画布,并登记它的来源。
25
+ *
26
+ * 兜底走 dataURL 的情况(平台没适配 canvas 资源)**不登记** —— 那种 url 本身
27
+ * 就是可直接使用的,不需要再被 `exportResources` 处理。
28
+ */
29
+ export declare function registerResource(collector: ResourceCollector, canvas: AnyCanvas, kind: PsdResourceKind, layer?: Layer): string;
30
+ /** 撤掉一张资源:同时从记录里和 Leafer 的 `Resource` 表里摘掉。 */
31
+ export declare function unregisterResource(collector: ResourceCollector, key: string): number;
32
+ export declare function attachCollector(target: IUI, collector: ResourceCollector): void;
33
+ /** 从任意元素向上找到解析结果的根,取出它的资源记录。 */
34
+ export declare function getCollector(target: IUI): ResourceCollector | undefined;
35
+ /**
36
+ * 读出解析结果登记的全部资源。
37
+ *
38
+ * 传 `Frame` 或它的任意子元素都可以。
39
+ *
40
+ * ```ts
41
+ * for (const { key, kind, layer, width, height } of getResources(frame)) {
42
+ * console.log(kind, layer?.name, `${width}x${height}`, key)
43
+ * }
44
+ * ```
45
+ */
46
+ export declare function getResources(target: IUI): readonly PsdResource[];
47
+ /**
48
+ * 释放某次解析注册进 Leafer `Resource` 的画布。
49
+ *
50
+ * `Resource` 是一张全局表,本插件注册进去的画布**不会自动释放**。做「能反复打开不同
51
+ * PSD」的界面时,旧画布会一直留在表里,内存持续增长。所以处理完一份 PSD
52
+ * (比如用户切换了文件)就调用一次:
53
+ *
54
+ * ```ts
55
+ * const frame = await psdToFrame(file)
56
+ * // ... 用户打开另一份文件时
57
+ * releaseResources(frame) // 返回实际释放的数量
58
+ * ```
59
+ *
60
+ * @returns 实际释放的资源数量
61
+ */
62
+ export declare function releaseResources(target: IUI): number;