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.
@@ -0,0 +1,43 @@
1
+ import type { Layer } from 'ag-psd';
2
+ import type { IUI } from '@leafer-ui/interface';
3
+ import { type BuildContext } from './context';
4
+ import { Slicer } from './utils/slice';
5
+ import type { PsdParseProgress } from './types';
6
+ export interface TreeBuilderOptions {
7
+ /** 图层总数,用于进度 */
8
+ total: number;
9
+ onProgress?: (progress: PsdParseProgress) => void;
10
+ signal?: AbortSignal;
11
+ }
12
+ /**
13
+ * 把 ag-psd 的图层树翻译成 Leafer 元素树。
14
+ *
15
+ * 两条贯穿全流程的不变式:
16
+ *
17
+ * 1. **数组顺序就是绘制顺序**。`psd.children` 是自下而上的(已实测,说明见
18
+ * `utils/box.ts`),与 Leafer「先 add 的在下面」一致,所以不需要 reverse,
19
+ * 也不需要为了排序去摆弄 `zIndex`。
20
+ *
21
+ * 2. **每个元素的原点 = 自己那个盒的左上角**。内容元素放在它图层绝对盒的
22
+ * 原点上(换算到父容器的局部坐标),包装器(遮罩 / 剪贴蒙版)与被包装内容
23
+ * 共用同一个原点。往下进一层容器就重新设置 `origin`,保证绝对坐标只被
24
+ * 换算一次,不会出现层级越深偏得越多。
25
+ */
26
+ export declare class TreeBuilder {
27
+ private readonly slicer;
28
+ private readonly options;
29
+ private done;
30
+ constructor(slicer: Slicer, options: TreeBuilderOptions);
31
+ build(layers: Layer[], parent: IUI, ctx: BuildContext): Promise<void>;
32
+ /** 构建一个剪贴蒙版组,返回要挂到父容器的元素列表。 */
33
+ private buildClippingRun;
34
+ /**
35
+ * 单个图层 → 内容元素。
36
+ *
37
+ * 顺序很关键:先建内容与子层,再叠图层效果,最后套蒙版。这对应 PS 的
38
+ * 合成顺序 —— 图层效果先绘制,其结果再由图层蒙版裁剪。
39
+ */
40
+ private buildContent;
41
+ private shouldSkip;
42
+ private report;
43
+ }
@@ -0,0 +1,338 @@
1
+ import type { Layer, ReadOptions } from 'ag-psd';
2
+ /**
3
+ * 解析来源。
4
+ *
5
+ * - 浏览器:`File` / `Blob` / `ArrayBuffer`
6
+ * - Node:`Buffer` / `Uint8Array` / `ArrayBuffer`
7
+ */
8
+ export type PsdSource = ArrayBuffer | Uint8Array | Blob | File;
9
+ /**
10
+ * 图层内容模式。
11
+ *
12
+ * - `raster`:把图层像素烘焙成位图(`Image`)。还原度最高,但不可编辑。
13
+ * - `editable`:尝试还原为可编辑的 Leafer 元素(如文字层转 `Text`)。
14
+ * 几何/排版可能与 PS 有偏差,失败时自动降级为 `raster` 并给出警告。
15
+ */
16
+ export type ContentMode = 'raster' | 'editable';
17
+ export type PsdWarningCode =
18
+ /** PS 混合模式在 Leafer 中没有对应实现,已降级 */
19
+ 'blend-mode-unsupported'
20
+ /** 图层效果无法映射到 Leafer,已丢弃 */
21
+ | 'effect-unsupported'
22
+ /** 语义化还原失败,已降级为位图 */
23
+ | 'semantic-fallback'
24
+ /** 文字图层的某项属性尚无对应能力(字符级样式、文字变形、水平缩放、段间距等) */
25
+ | 'text-unsupported'
26
+ /** 文字图层的 leading 数值不可信,已退回自动行高 */
27
+ | 'text-leading-suspect'
28
+ /** 图层蒙版缺失或无法解析 */
29
+ | 'mask-skipped'
30
+ /** 图层没有任何可渲染内容 */
31
+ | 'empty-layer'
32
+ /** 被 filter 主动跳过 */
33
+ | 'filtered'
34
+ /** 文件声明的尺寸/效果参数超出配额,该图层或该项效果被跳过(见 `setCanvasLimits`) */
35
+ | 'limit-exceeded'
36
+ /** 读取 PSD 失败 */
37
+ | 'read-error';
38
+ export interface PsdParseWarning {
39
+ code: PsdWarningCode;
40
+ message: string;
41
+ layerName?: string;
42
+ layerId?: number;
43
+ }
44
+ export type PsdParsePhase =
45
+ /** 解码 PSD 二进制 */
46
+ 'read'
47
+ /** 构建 Leafer 元素树 */
48
+ | 'build' | 'done';
49
+ export interface PsdParseProgress {
50
+ phase: PsdParsePhase;
51
+ /** 已处理图层数 */
52
+ done: number;
53
+ /** 总图层数(含嵌套) */
54
+ total: number;
55
+ /** 0 ~ 1 */
56
+ ratio: number;
57
+ /** 当前图层名 */
58
+ name: string;
59
+ }
60
+ export interface PsdParseOptions {
61
+ /**
62
+ * 像素图层与形状图层的内容模式。
63
+ *
64
+ * ⚠️ 目前 `'editable'` 与 `'raster'` **等价**:ag-psd 没有暴露形状图层的路径
65
+ * 数据(`LayerAdditionalInfo.pathList` 仍是 TODO),所以形状图层做不到矢量化,
66
+ * 像素图层本来就是像素。显式传 `'editable'` 时会给出 `semantic-fallback` 诊断,
67
+ * 而不是静默地不生效。
68
+ *
69
+ * @default 'raster'
70
+ */
71
+ content?: ContentMode;
72
+ /**
73
+ * 文字图层的内容模式。
74
+ *
75
+ * - `'editable'`(默认):还原成可编辑的 Leafer `Text`(`adapter/text.ts`)。
76
+ * 定位锚在 PS 的基线上,几何/排版可能与 PS 有亚像素级偏差;无法还原时
77
+ * (例如拿不到字体、数据异常)自动降级为位图并报 `semantic-fallback`。
78
+ * 配合 `@leafer-in/text-editor` 可以直接双击改字。
79
+ * - `'raster'`:直接复用 PS 烘焙好的文字位图,像素最忠实但不可编辑。
80
+ *
81
+ * 两种模式的实测差异见 README「验证」:`test-1.psd` 上语义化更准
82
+ * (0.990 vs 1.039),`test-text.psd` 上烘焙更准(1.200 vs 1.773)。
83
+ *
84
+ * @default 'editable'
85
+ */
86
+ text?: ContentMode;
87
+ /** 是否解析图层蒙版(位图蒙版 + 矢量蒙版)。 @default true */
88
+ mask?: boolean;
89
+ /**
90
+ * 是否把矢量蒙版还原为可编辑的 `Path` 遮罩(`mask: 'path'`)。
91
+ *
92
+ * 关闭时改用 ag-psd 光栅化好的 `realMask` 做灰度遮罩 —— 像素结果一致,
93
+ * 但会失去矢量清晰度且多占一张位图。 @default true
94
+ */
95
+ vectorMask?: boolean;
96
+ /** 是否解析剪贴蒙版。 @default true */
97
+ clipping?: boolean;
98
+ /** 是否映射图层效果(投影/内阴影/描边/颜色叠加/渐变叠加)。 @default true */
99
+ effects?: boolean;
100
+ /** 是否映射混合模式。 @default true */
101
+ blendMode?: boolean;
102
+ /** 是否把 PSD 图层元数据挂到元素的 `ui.data.psd` 上。 @default true */
103
+ meta?: boolean;
104
+ /**
105
+ * 是否把解析出来的元素设为**可编辑**(`editable: true`)。
106
+ *
107
+ * 打开后配合 [`@leafer-in/editor`](https://www.npmjs.com/package/@leafer-in/editor)
108
+ * 就能直接选中、移动、缩放、旋转、编组。
109
+ *
110
+ * 可编辑只挂在**每个图层最外层的那个元素**上,所以一层就是一个可选中单元:
111
+ * 带蒙版的图层设在外层包装容器上,图层组设在整个组上并关闭 `hitChildren`
112
+ * (双击才进组)—— 与 leafer 编辑器自己编组时的约定一致。
113
+ *
114
+ * @default false
115
+ */
116
+ editable?: boolean;
117
+ /**
118
+ * 是否让解析出来的元素**可拖动**(`draggable: true`)。
119
+ *
120
+ * 只依赖 `leafer-ui` 自带的交互,**不需要**安装 `@leafer-in/editor`,
121
+ * 适合「让用户把图层挪一挪」这类轻量场景。
122
+ *
123
+ * 与 `editable` 独立,可以只开一个,也可以两个一起开
124
+ * (两个都开就能在纯 `leafer-ui` 里也拖得动)。
125
+ *
126
+ * 可拖动同样只挂在**每个图层最外层的那个元素**上,一层是一个拖动单元;
127
+ * 容器会关掉 `hitChildren`,所以拖组是整组一起动。
128
+ *
129
+ * @default false
130
+ */
131
+ draggable?: boolean;
132
+ /** 是否跳过隐藏图层(含其子树)。设为 false 时保留图层但置为不可见。 @default false */
133
+ skipHidden?: boolean;
134
+ /** 图层过滤器,返回 `false` 时跳过该图层及其子树。 */
135
+ filter?: (layer: Layer) => boolean;
136
+ /**
137
+ * 字体名映射。
138
+ *
139
+ * PS 存的是字体**全名**(`AdobeHeitiStd-Regular`、`MicrosoftYaHei`),而系统认识的
140
+ * 是**字体族名**(`Adobe Heiti Std`、`Microsoft YaHei`)。插件内置了一张常见别名表
141
+ * 先做归一化(见 `toFontFamily`),这个回调用来覆盖/补充它。
142
+ *
143
+ * 第二个参数是内置映射的结果,可以直接兜底:
144
+ *
145
+ * ```ts
146
+ * fontFamily: (psdName, builtin) => (psdName === 'MyFont-Regular' ? 'My Font' : builtin)
147
+ * ```
148
+ *
149
+ * 返回 `undefined` 表示沿用内置结果。
150
+ */
151
+ fontFamily?: (psdFontName: string, builtinFamily: string) => string | undefined;
152
+ /**
153
+ * PS 投影/内阴影的 `size` 与 Leafer `blur` 之间的换算系数。
154
+ *
155
+ * 两者的物理含义并不等价:PS 的 `size` 是高斯模糊半径,而 Leafer 的 `blur`
156
+ * 会传给画布的 `shadowBlur`(实现上 σ = shadowBlur / 2),各处的经验换算说法
157
+ * 相差很大(约 0.67× ~ 2×)。
158
+ *
159
+ * 默认值 **0.7** 是对着真实样例的参考合成图扫出来的:`0.55 ~ 0.75` 是一个平坦的
160
+ * 谷底,取 0.70;手算也吻合(参考图投影衰减拟合出 σ ≈ 6.33px,`2σ / 17(PS size) ≈ 0.745`)。
161
+ * 两条独立路径落在同一区间,不是过拟合。回归护栏见 `__tests__/calibrate.test.ts`。
162
+ *
163
+ * ```ts
164
+ * psdToFrame(file, { shadowBlurScale: 1 }) // 想回到恒等映射
165
+ * ```
166
+ *
167
+ * @default 0.7
168
+ */
169
+ shadowBlurScale?: number;
170
+ /** 进度回调。 */
171
+ onProgress?: (progress: PsdParseProgress) => void;
172
+ /** 诊断回调,用于收集降级/不支持的信息。 */
173
+ onWarning?: (warning: PsdParseWarning) => void;
174
+ /** 取消信号。 */
175
+ signal?: AbortSignal;
176
+ /**
177
+ * 分批构建的时间片预算(ms)。达到预算就让出主线程一帧,避免长时间阻塞 UI。
178
+ * 设为 `0` 表示不切分(一次性同步构建)。
179
+ *
180
+ * @default 12
181
+ */
182
+ sliceBudget?: number;
183
+ /**
184
+ * 解析完成后把根 Frame 的尺寸设为文档尺寸并裁剪溢出。
185
+ *
186
+ * @default true
187
+ */
188
+ fitRoot?: boolean;
189
+ /** 透传给 ag-psd `readPsd` 的选项。 */
190
+ readOptions?: ReadOptions;
191
+ }
192
+ /** 归一化后的完整选项,内部使用。 */
193
+ export interface ResolvedOptions extends Required<Omit<PsdParseOptions, 'onProgress' | 'onWarning' | 'filter' | 'signal' | 'readOptions' | 'fontFamily'>> {
194
+ onProgress?: (progress: PsdParseProgress) => void;
195
+ onWarning?: (warning: PsdParseWarning) => void;
196
+ filter?: (layer: Layer) => boolean;
197
+ signal?: AbortSignal;
198
+ readOptions?: ReadOptions;
199
+ fontFamily?: (psdFontName: string, builtinFamily: string) => string | undefined;
200
+ }
201
+ export interface PsdLayerBox {
202
+ left: number;
203
+ top: number;
204
+ right: number;
205
+ bottom: number;
206
+ }
207
+ /** 元素在解析结果中扮演的角色。 */
208
+ export type PsdElementRole =
209
+ /** 一个普通图层的可见内容 */
210
+ 'layer'
211
+ /** 图层蒙版/矢量蒙版(`mask` 属性所在元素) */
212
+ | 'mask'
213
+ /** 被包装出来的容器(用于承载蒙版或剪贴蒙版) */
214
+ | 'wrapper'
215
+ /** 剪贴蒙版的基底层 */
216
+ | 'clipping-base'
217
+ /** 剪贴蒙版中被裁剪的内容 */
218
+ | 'clipping-content';
219
+ /** 挂在 `ui.data.psd` 上的图层元数据,用于后续做选择同步或写回 PSD。 */
220
+ export interface PsdLayerMeta {
221
+ /** ag-psd 的图层 id */
222
+ id?: number;
223
+ name?: string;
224
+ /** 图层在文档坐标系下的绝对边界 */
225
+ box: PsdLayerBox;
226
+ /** ag-psd 原始混合模式值 */
227
+ blendMode?: string;
228
+ opacity: number;
229
+ hidden: boolean;
230
+ clipping: boolean;
231
+ isGroup: boolean;
232
+ role: PsdElementRole;
233
+ }
234
+ /**
235
+ * 跨平台画布的最小接口。
236
+ *
237
+ * 浏览器里是 `HTMLCanvasElement`,Node 端是 `@napi-rs/canvas` / `node-canvas`
238
+ * 的 `Canvas`,小程序是离屏画布。后三个编码方法都是**可选**的 ——
239
+ * 插件自身不做编码,只在附带的编码工具里按可用性挑一个用。
240
+ */
241
+ export interface AnyCanvas {
242
+ width: number;
243
+ height: number;
244
+ getContext(type: '2d'): any;
245
+ /** 浏览器:`HTMLCanvasElement.toDataURL` */
246
+ toDataURL?(type?: string, quality?: number): string;
247
+ /** 浏览器:`HTMLCanvasElement.toBlob` */
248
+ toBlob?(callback: (blob: Blob | null) => void, type?: string, quality?: number): void;
249
+ /** Node:`@napi-rs/canvas` / `node-canvas` 的 `toBuffer` */
250
+ toBuffer?(type?: string, quality?: number): Uint8Array;
251
+ /** Node:`@napi-rs/canvas` 的 `convertToBlob` */
252
+ convertToBlob?(options?: {
253
+ type?: string;
254
+ quality?: number;
255
+ }): Promise<Blob>;
256
+ }
257
+ /** 图片编码格式。 */
258
+ export type PsdResourceFormat = 'png' | 'webp' | 'jpeg';
259
+ /** 一张注册进 Leafer `Resource` 表的画布,是干什么用的。 */
260
+ export type PsdResourceKind =
261
+ /** 图层像素 */
262
+ 'layer'
263
+ /** 位图图层蒙版 */
264
+ | 'mask'
265
+ /** 降级为位图的矢量蒙版(`vectorMask: false` 时) */
266
+ | 'vector-mask'
267
+ /** 图层效果烘焙后的产物 */
268
+ | 'effects';
269
+ /** 资源来源图层的摘要,导出时用来组织存储路径。 */
270
+ export interface PsdResourceLayer {
271
+ /** ag-psd 的图层 id */
272
+ id?: number;
273
+ name?: string;
274
+ /** 图层在文档坐标系下的绝对边界 */
275
+ box: PsdLayerBox;
276
+ isGroup: boolean;
277
+ }
278
+ /**
279
+ * 一条资源记录。
280
+ *
281
+ * 解析出的每张画布都会登记一条 —— 它把「Leafer 资源符」和「它来自哪个 PSD 图层」
282
+ * 关联起来,`exportResources()` 靠它决定把哪张画布交给回调、以及回调能拿到什么元数据。
283
+ */
284
+ export interface PsdResource {
285
+ /** `Resource` 表里的 key,形如 `leafer://psd-plugin-resource-12.png`(前缀见 `RESOURCE_PREFIX`) */
286
+ key: string;
287
+ /** 这张画布的用途 */
288
+ kind: PsdResourceKind;
289
+ /** 来自哪个图层 */
290
+ layer?: PsdResourceLayer;
291
+ /** 画布本身(**注意**:它不是可序列化的,别塞进 `ui.data`) */
292
+ canvas: AnyCanvas;
293
+ width: number;
294
+ height: number;
295
+ }
296
+ /**
297
+ * 把画布换成 URL 的回调。
298
+ *
299
+ * 返回 `undefined` 表示这张不处理(保留 `leafer://` 资源符)。
300
+ */
301
+ export type PsdResourceResolver = (canvas: AnyCanvas, info: PsdResource) => string | undefined | Promise<string | undefined>;
302
+ export interface PsdExportOptions {
303
+ /**
304
+ * **唯一的必需回调** —— 上传到对象存储、转 dataURL、配合 zip 写相对路径,
305
+ * 都由它决定。插件不预设「上传」这件事。
306
+ *
307
+ * ```ts
308
+ * resolve: async (canvas, info) => {
309
+ * const blob = await canvasToBlob(canvas, 'webp', 0.9)
310
+ * const key = `psd/${info.layer?.name ?? 'unnamed'}-${info.key.split('-').pop()}`
311
+ * return (await myOss.put(key, blob)).url
312
+ * }
313
+ * ```
314
+ */
315
+ resolve: PsdResourceResolver;
316
+ /** 并发数。默认 4。 */
317
+ concurrency?: number;
318
+ /** 只处理这些用途,默认全部。 */
319
+ include?: PsdResourceKind[];
320
+ /** 每完成一张调一次。 */
321
+ onProgress?: (done: number, total: number, info: PsdResource) => void;
322
+ /**
323
+ * 某一张失败时怎么办。
324
+ *
325
+ * - `'throw'`(默认):中止并在结果里保留已替换的部分,然后抛出第一个错误
326
+ * - `'keep'`:保留 `leafer://` 资源符,记进 `skipped`
327
+ * - 传函数:自己决定,返回 URL 就用它,返回 `undefined` 则保留
328
+ */
329
+ onError?: 'throw' | 'keep' | ((error: unknown, info: PsdResource) => string | undefined);
330
+ }
331
+ export interface PsdExportResult {
332
+ /** 成功替换的数量 */
333
+ replaced: number;
334
+ /** 没被替换的(失败被保留、或被 `include` 过滤掉的) */
335
+ skipped: PsdResource[];
336
+ /** `leafer://` key → 新 URL,存档时可以另存一份清单 */
337
+ urls: Map<string, string>;
338
+ }
@@ -0,0 +1,9 @@
1
+ import type { BlendMode } from 'ag-psd';
2
+ import type { IBlendMode } from '@leafer-ui/interface';
3
+ export interface BlendModeResult {
4
+ /** 映射后的 Leafer 混合模式;未映射时为 `undefined` */
5
+ blendMode?: IBlendMode;
6
+ /** 未能映射的原始 PS 混合模式名 */
7
+ unsupported?: string;
8
+ }
9
+ export declare function toLeaferBlendMode(mode: BlendMode | string | undefined): BlendModeResult;
@@ -0,0 +1,29 @@
1
+ import type { Layer } from 'ag-psd';
2
+ export interface Box {
3
+ left: number;
4
+ top: number;
5
+ right: number;
6
+ bottom: number;
7
+ }
8
+ export declare function boxWidth(box: Box): number;
9
+ export declare function boxHeight(box: Box): number;
10
+ export declare function isEmptyBox(box: Box): boolean;
11
+ export declare function unionBox(a: Box, b: Box): Box;
12
+ /** 图层自身的坐标盒(不做任何递归)。 */
13
+ export declare function ownBox(layer: Layer): Box;
14
+ /**
15
+ * 计算图层在**文档坐标系**下的真实边界。
16
+ *
17
+ * 两个必须知道的 ag-psd 行为(均已实测确认):
18
+ *
19
+ * 1. 所有图层的 `left/top/right/bottom` —— **包括组内子层** —— 都是文档绝对坐标,
20
+ * 不是相对父组的坐标。所以把子层直接塞进一个带 `x/y` 的 Leafer `Group`
21
+ * 会造成双重偏移。本模块产出的绝对盒正是用来做坐标归一化的依据。
22
+ *
23
+ * 2. 图层组的 `right/bottom` **不可信**。实测写入 `(40,55,155,165)` 的组读回后
24
+ * 会塌缩成 `(40,55,40,55)`(宽高为 0)。因此分组一律通过子层递归求并集,
25
+ * 只有当组内没有任何可测量子层时才回退到自身盒。
26
+ */
27
+ export declare function measureLayer(layer: Layer): Box;
28
+ /** 统计图层树中的图层总数(含嵌套),用于进度计算。 */
29
+ export declare function countLayers(layers: Layer[]): number;
@@ -0,0 +1,9 @@
1
+ import type { Color } from 'ag-psd';
2
+ /**
3
+ * 把 ag-psd 的 `Color` 联合类型转为 CSS 颜色字符串。
4
+ *
5
+ * `Color = RGBA | RGB | FRGB | HSB | CMYK | LAB | Grayscale`,
6
+ * 这些分支靠字段名区分,判别顺序不能变(`LAB` 和 `RGB` 都有 `b`,
7
+ * `CMYK` 和 `Grayscale` 都有 `k`)。
8
+ */
9
+ export declare function colorToString(color: Color | undefined, opacity?: number): string;
@@ -0,0 +1,50 @@
1
+ import type { LayerEffectsInfo, UnitsValue } from 'ag-psd';
2
+ import { MAX_EFFECT_SIZE } from './limits';
3
+ /**
4
+ * 读取 ag-psd 参数、以及图层效果的几何换算。
5
+ *
6
+ * 这些函数原本在 `decorator/effects.ts` 与 `decorator/bake.ts` 里各写了一份
7
+ * (连注释都一样),现在收在一处 —— 两条路径的语义必须一致,
8
+ * 否则"位图烘焙"和"属性映射"会对同一份 PSD 给出不同的结果。
9
+ */
10
+ /** 取 `UnitsValue | number` 里的数值。 */
11
+ export declare function unitValue(value: UnitsValue | number | undefined, fallback?: number): number;
12
+ /**
13
+ * 效果半径/模糊值,已按 `MAX_EFFECT_SIZE` 钳制。
14
+ *
15
+ * 凡是这个值会进入**补白或画布尺寸**计算的地方都必须用它 —— 见 `utils/limits.ts`。
16
+ */
17
+ export declare function effectSize(value: UnitsValue | number | undefined): number;
18
+ /**
19
+ * PS 的 `choke` / spread 是**百分比**(0~100),不是像素。
20
+ *
21
+ * ⚠️ ag-psd 把它的单位标成了 `Pixels`,但那是错的 —— 真实 PSD 里存的就是 0~100 的
22
+ * 百分比。实测:把这个值当像素用,投影的扩散范围会大 4 倍以上,该区域的像素差从
23
+ * 8.2 劣化到 37.0。所以这里除以 100,并把结果一起钳到合法范围。
24
+ */
25
+ export declare function chokeToSpread(choke: UnitsValue | number | undefined, size: number): number;
26
+ /** 过滤掉 `enabled: false` 的效果项。 */
27
+ export declare function enabled<T extends {
28
+ enabled?: boolean;
29
+ }>(list: T[] | undefined): T[];
30
+ /**
31
+ * PS 角度逆时针为正、Y 轴朝上;画布 Y 轴朝下,所以纵向取反。
32
+ */
33
+ export declare function shadowOffset(shadow: {
34
+ angle?: number;
35
+ distance?: UnitsValue | number | undefined;
36
+ }): {
37
+ x: number;
38
+ y: number;
39
+ };
40
+ /** 便于调用方在告警信息里说明"被钳到了多少"。 */
41
+ export { MAX_EFFECT_SIZE };
42
+ /**
43
+ * 扫描一份图层效果数据里超出合理范围的参数。
44
+ *
45
+ * 钳制是"不静默"的:调用方拿到这里返回的文案就报一条 `limit-exceeded`,
46
+ * 而不是悄悄把 5000px 的描边画成 1000px。
47
+ *
48
+ * @returns 告警文案;参数都正常时返回 `undefined`
49
+ */
50
+ export declare function describeOutOfRangeEffects(effects: LayerEffectsInfo): string | undefined;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * PS 字体全名 → 可用字体族名。
3
+ *
4
+ * PS 存的是字体**全名**(`AdobeHeitiStd-Regular`、`MicrosoftYaHei`、`ArialMT`),
5
+ * 而系统/DOM 认识的是**字体族名**(`Adobe Heiti Std`、`Microsoft YaHei`、`Arial`)。
6
+ * 直接拿全名当 family 用会静默回退到默认字体 —— 实测 `AdobeHeitiStd-Regular`
7
+ * 在装了 `Adobe Heiti Std` 的机器上仍然回退,字宽差 16.4px;改成族名后命中。
8
+ *
9
+ * 这里只做「去掉样式后缀 + 查别名表」,**不做通用的驼峰拆词** ——
10
+ * 因为 `MicrosoftYaHei` 拆成 `Microsoft Ya Hei` 是错的(`YaHei` 是一个词)。
11
+ * 猜错的代价比不猜更大,所以宁可只处理已知的、可靠的映射。
12
+ */
13
+ /**
14
+ * 把 PS 的字体全名转成更适合当 CSS font-family 用的族名。
15
+ *
16
+ * 查不到时**原样返回**,不猜 —— 猜错会命中一个完全无关的字体,比回退到系统默认更糟。
17
+ */
18
+ export declare function toFontFamily(psdFontName: string): string;
19
+ /** 别名表是否认识这个字体(用于诊断)。 */
20
+ export declare function hasFontAlias(psdFontName: string): boolean;
@@ -0,0 +1,12 @@
1
+ import type { BezierPath } from 'ag-psd';
2
+ import type { IWindingRule } from '@leafer-ui/interface';
3
+ /**
4
+ * 把 ag-psd 的贝塞尔路径转成 SVG path 字符串。
5
+ *
6
+ * `BezierKnot.points` 是 `[x0, y0, x1, y1, x2, y2]`,依次为
7
+ * **入控制点、锚点、出控制点**。相邻锚点之间用三次贝塞尔连接,
8
+ * 控制点分别是前一个锚点的出点和后一个锚点的入点。
9
+ */
10
+ export declare function bezierPathsToSvg(paths: BezierPath[]): string;
11
+ /** 由贝塞尔路径推导填充规则,多个子路径时以第一条为准。 */
12
+ export declare function bezierWindingRule(paths: BezierPath[]): IWindingRule;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * 画布与图层效果参数的**硬上限(配额)**。
3
+ *
4
+ * PSD 是**不可信输入**:图层尺寸、蒙版框、效果的 `size` / `choke` 全都由文件内容决定。
5
+ * 不加限制的话,一个构造过的文件就能让插件申请一张几万像素见方的画布 ——
6
+ * 那是几百 MB 到几 GB 的一次性分配,宿主进程会被直接打爆(内存耗尽型 DoS,
7
+ * 而且 `readPsd` 是同步阻塞的,连卡死都一起给你)。
8
+ *
9
+ * 收口点有两个,共用同一份配额:
10
+ *
11
+ * 1. `platform.createCanvas` —— 插件自己算出来的画布(蒙版裁剪、效果烘焙、裸像素还原)
12
+ * 全部经过它,所以这里挡一次就等于全都挡住了;
13
+ * 2. `platform.ensureCanvasFactory` 装给 ag-psd 的解码期工厂 —— 解码阶段才是内存大头,
14
+ * 而且那些尺寸同样来自文件。
15
+ *
16
+ * 用 `setCanvasLimits()` 可以放宽(例如确实要处理 20000px 的巨幅海报),
17
+ * 但默认值刻意贴着浏览器 canvas 的安全上限(16384 单边),宁可明确报错也不去赌分配成功。
18
+ */
19
+ export interface CanvasLimits {
20
+ /** 单边最大像素。默认 16384 —— 浏览器 canvas 的安全上限就是 16384×16384 */
21
+ maxSide: number;
22
+ /** 单张画布最大总像素。默认 32M(约 128MB RGBA) */
23
+ maxPixels: number;
24
+ }
25
+ export declare const DEFAULT_CANVAS_LIMITS: CanvasLimits;
26
+ /**
27
+ * 图层效果 `size`(投影/内阴影/描边/发光的半径与模糊)的上限。
28
+ *
29
+ * PS 自己的 UI 上限是 250px,所以 1000 已经非常宽松;超出这个数量级的只可能是
30
+ * 损坏或构造过的数据。钳制它不只是省内存:`size` 会直接进补白计算,
31
+ * 而补白决定烘焙画布会有多大。
32
+ */
33
+ export declare const MAX_EFFECT_SIZE = 1000;
34
+ /**
35
+ * `choke` / spread 的上限。
36
+ *
37
+ * 它是**百分比**(0~100),见 `decorator/bake.ts` 里 ag-psd 单位标注错误的说明。
38
+ * 所以 `choke = 5000` 只会出现在坏数据里 —— 那会让扩散范围变成 `50 × size`。
39
+ */
40
+ export declare const MAX_EFFECT_PERCENT = 100;
41
+ /** 当前生效的配额(返回副本,改它不影响内部状态)。 */
42
+ export declare function getCanvasLimits(): CanvasLimits;
43
+ /**
44
+ * 调整配额。两边的值都必须是正的有限数,非法值被忽略(不会把配额设成 NaN 而"什么都不挡")。
45
+ *
46
+ * ```ts
47
+ * setCanvasLimits({ maxSide: 30000, maxPixels: 200 * 1024 * 1024 })
48
+ * ```
49
+ *
50
+ * @returns 调整后的配额
51
+ */
52
+ export declare function setCanvasLimits(next: Partial<CanvasLimits>): CanvasLimits;
53
+ /** 像素数的可读写法(用于错误信息与告警)。 */
54
+ export declare function describePixels(pixels: number): string;
55
+ /** 配额的可读描述。 */
56
+ export declare function describeCanvasLimits(current?: CanvasLimits): string;
57
+ /** 超限原因:`side` = 单边超了,`pixels` = 总像素超了,`undefined` = 没超。 */
58
+ export declare function canvasLimitReason(width: number, height: number, current?: CanvasLimits): 'side' | 'pixels' | undefined;
59
+ /** 这张画布是否在配额之内。 */
60
+ export declare function fitsCanvasLimits(width: number, height: number): boolean;
61
+ /** 超限时抛出的错误。宿主可以据此区分"文件有问题"与"真的解析失败"。 */
62
+ export declare class PsdCanvasLimitError extends Error {
63
+ readonly width: number;
64
+ readonly height: number;
65
+ readonly limits: CanvasLimits;
66
+ readonly reason: 'side' | 'pixels';
67
+ constructor(width: number, height: number, reason: 'side' | 'pixels', current?: CanvasLimits);
68
+ }
69
+ /**
70
+ * 校验一次画布分配,超限就抛 `PsdCanvasLimitError`。
71
+ *
72
+ * 这是唯一的收口点,所有 `createCanvas` 都必须先过这里。
73
+ */
74
+ export declare function assertCanvasSize(width: number, height: number): void;
75
+ /** 把效果 `size` 钳到 `[0, MAX_EFFECT_SIZE]`。 */
76
+ export declare function clampEffectSize(size: number): number;
77
+ /** 把 `choke` / spread 百分比钳到 `[0, MAX_EFFECT_PERCENT]`。 */
78
+ export declare function clampEffectPercent(percent: number): number;
@@ -0,0 +1,11 @@
1
+ import type { LayerMaskData, PixelData } from 'ag-psd';
2
+ import type { AnyCanvas } from '../types';
3
+ /**
4
+ * 把 ag-psd 的裸像素数据还原成一张画布。
5
+ *
6
+ * `readPsd` 默认产出 `canvas`;只有当宿主显式传了 `useImageData: true`
7
+ * 时才会拿到 `imageData`,此时需要在这里补回画布。
8
+ */
9
+ export declare function pixelDataToCanvas(pixel: PixelData): AnyCanvas;
10
+ /** 取得蒙版(图层蒙版或矢量蒙版的光栅化结果)对应的画布。 */
11
+ export declare function maskCanvas(mask: LayerMaskData | undefined): AnyCanvas | undefined;
@@ -0,0 +1,29 @@
1
+ /** 让出主线程一帧。浏览器用 rAF 以便真正有机会绘制,其他环境用定时器。 */
2
+ export declare function nextFrame(): Promise<void>;
3
+ /**
4
+ * 时间片调度器。
5
+ *
6
+ * 解析 PSD 时图层数量可能上千,一次性同步构建会长时间阻塞主线程。
7
+ * 但「每层都 `setTimeout(..., 0)`」同样不可取 —— 定时器最小间隔约 4ms,
8
+ * 1000 层就是 4 秒以上的纯等待,比同步还慢。
9
+ *
10
+ * 正确做法是**按时间片分批**:累计处理满 `budget` 毫秒才让出一帧。
11
+ */
12
+ export declare class Slicer {
13
+ private readonly budget;
14
+ private sliceStartedAt;
15
+ private yields;
16
+ constructor(budget: number);
17
+ /**
18
+ * 已经让出主线程的次数。
19
+ *
20
+ * 用于测试与诊断:让出与否不能靠「排个宏任务看它跑没跑」来判断 ——
21
+ * Leafer 内部自己也会用 `setTimeout` 调度渲染,那样测出来的是
22
+ * 「期间有没有任何宏任务执行过」,而不是本调度器有没有让出。
23
+ */
24
+ get yieldCount(): number;
25
+ /** 在处理完一个单元后调用,必要时让出主线程。 */
26
+ tick(): Promise<void>;
27
+ }
28
+ /** 在构建循环中检查取消信号。 */
29
+ export declare function throwIfAborted(signal: AbortSignal | undefined): void;