museav-cli 2.10.1 → 2.10.2
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/CHANGELOG.md +18 -1
- package/dist/local-slideshow.js +37 -9
- package/package.json +1 -1
- package/src/local-slideshow.ts +42 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 2.10.
|
|
3
|
+
## 2.10.2 (2026-08-28)
|
|
4
|
+
|
|
5
|
+
### 修复:排版模板的图层顺序失效,图片会盖住它下面该有的文字
|
|
6
|
+
|
|
7
|
+
`slot` / `image` 图层被统一提到最后合成,导致**所有图片永远压在所有矢量层之上**,
|
|
8
|
+
DSL 里 `layers` 的先后顺序当场失效。「图片铺满画面 + 文字压在上面」这类版式
|
|
9
|
+
渲染出来是文字被图片盖掉。
|
|
10
|
+
|
|
11
|
+
内置的粉青版式没受影响(那里图与文字不重叠),所以 2.10.1 发出去时没被发现 ——
|
|
12
|
+
是拿一个结构完全不同的版式(满屏叠字)实测才暴露的。
|
|
13
|
+
|
|
14
|
+
改成按 `layers` 顺序分段渲染:连续的矢量层攒成一段 SVG,遇到图片层就断开,
|
|
15
|
+
按原顺序逐段叠加。
|
|
16
|
+
|
|
17
|
+
## 2.10.1 (2026-08-28)
|
|
18
|
+
|
|
19
|
+
> 这一版同时发布了另一条线的 `feedback` 命令(命令行提 bug/需求),
|
|
20
|
+
> 下面记的是 slideshow 这条线的改动。
|
|
4
21
|
|
|
5
22
|
### `slideshow` 的版面改由**排版模板**描述,模板库在中台
|
|
6
23
|
|
package/dist/local-slideshow.js
CHANGED
|
@@ -54,16 +54,29 @@ function resolveText(layer, content) {
|
|
|
54
54
|
return typeof v === 'string' ? v : '';
|
|
55
55
|
}
|
|
56
56
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
57
|
+
* 把一页拆成有序的渲染步骤。
|
|
58
|
+
*
|
|
59
|
+
* 关键点:**不能把图片层统一提到最后 composite**。sharp 的 composite 一律贴在底图之上,
|
|
60
|
+
* 那样所有图片就永远压在所有矢量层上面,DSL 里的图层顺序当场失效 ——
|
|
61
|
+
* 「图片铺满 + 文字压在上面」这种版式会渲染成文字被图片盖住(实测踩到)。
|
|
62
|
+
* 所以这里把连续的矢量层攒成一段 SVG,遇到图片层就断开,按原顺序逐段叠。
|
|
63
|
+
*
|
|
64
|
+
* 图片不嵌进 SVG 是另一回事:base64 内联会让 SVG 爆大且慢,走 composite 更划算。
|
|
59
65
|
*/
|
|
60
66
|
function buildPage(layout, content, scale, onWarn) {
|
|
61
67
|
const W = Math.round(layout.canvas.w * scale);
|
|
62
68
|
const H = Math.round(layout.canvas.h * scale);
|
|
63
69
|
const s = (n) => n * scale;
|
|
64
|
-
const
|
|
65
|
-
|
|
70
|
+
const steps = [];
|
|
71
|
+
let parts = [];
|
|
66
72
|
const unknown = new Set();
|
|
73
|
+
// 攒够的矢量层收成一段 SVG(透明底 —— 背景色由 base 图承担,否则后一段会盖掉前一段)
|
|
74
|
+
const flush = () => {
|
|
75
|
+
if (!parts.length)
|
|
76
|
+
return;
|
|
77
|
+
steps.push({ kind: 'svg', svg: `<svg width="${W}" height="${H}" xmlns="http://www.w3.org/2000/svg">${parts.join('')}</svg>` });
|
|
78
|
+
parts = [];
|
|
79
|
+
};
|
|
67
80
|
for (const layer of layout.layers) {
|
|
68
81
|
const op = layer.opacity !== undefined ? ` opacity="${layer.opacity}"` : '';
|
|
69
82
|
if (layer.type === 'rect' && layer.box) {
|
|
@@ -89,17 +102,19 @@ function buildPage(layout, content, scale, onWarn) {
|
|
|
89
102
|
}
|
|
90
103
|
else if ((layer.type === 'slot' || layer.type === 'image') && layer.box) {
|
|
91
104
|
const [x, y, w, h] = layer.box;
|
|
92
|
-
|
|
105
|
+
flush(); // 断开:此前的矢量层要落在这张图**下面**
|
|
106
|
+
steps.push({ kind: 'image', layer, box: [s(x), s(y), s(w), s(h)] });
|
|
93
107
|
}
|
|
94
108
|
else {
|
|
95
109
|
// 不认识的层类型:跳过,不中断。老 CLI 碰上新层类型该少画一层,不是出不了片。
|
|
96
110
|
unknown.add(String(layer.type));
|
|
97
111
|
}
|
|
98
112
|
}
|
|
113
|
+
flush();
|
|
99
114
|
if (unknown.size && onWarn) {
|
|
100
115
|
onWarn(`模板里有本版本不认识的图层类型(${[...unknown].join(', ')}),已跳过。升级试试:npm i -g museav-cli`);
|
|
101
116
|
}
|
|
102
|
-
return {
|
|
117
|
+
return { steps, W, H };
|
|
103
118
|
}
|
|
104
119
|
function run(cmd, args) {
|
|
105
120
|
return new Promise((resolve) => {
|
|
@@ -147,9 +162,15 @@ export async function makeSlideshow(opts) {
|
|
|
147
162
|
footer: opts.footer,
|
|
148
163
|
image: opts.images[i],
|
|
149
164
|
};
|
|
150
|
-
const {
|
|
165
|
+
const { steps, W, H } = buildPage(layout, content, scale, warn);
|
|
151
166
|
const composites = [];
|
|
152
|
-
for (const
|
|
167
|
+
for (const step of steps) {
|
|
168
|
+
// 矢量段按原顺序插进来,图片压在它之前的段之上、之后的段之下
|
|
169
|
+
if (step.kind === 'svg') {
|
|
170
|
+
composites.push({ input: Buffer.from(step.svg), top: 0, left: 0 });
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const { layer, box } = step;
|
|
153
174
|
const [bx, by, bw, bh] = box;
|
|
154
175
|
let src = null;
|
|
155
176
|
if (layer.type === 'slot') {
|
|
@@ -198,8 +219,15 @@ export async function makeSlideshow(opts) {
|
|
|
198
219
|
left: Math.max(0, Math.round(bx + (bw - art.info.width) / 2)),
|
|
199
220
|
});
|
|
200
221
|
}
|
|
222
|
+
// 底图只负责画布背景色,所有内容(含第一段矢量层)都按顺序 composite 上去
|
|
201
223
|
const page = join(work, `p${String(i).padStart(3, '0')}.png`);
|
|
202
|
-
|
|
224
|
+
const bg = layout.canvas.bg ?? '#ffffff';
|
|
225
|
+
await sharp({
|
|
226
|
+
create: { width: W, height: H, channels: 4, background: bg === 'transparent' ? { r: 0, g: 0, b: 0, alpha: 0 } : bg },
|
|
227
|
+
})
|
|
228
|
+
.composite(composites)
|
|
229
|
+
.png()
|
|
230
|
+
.toFile(page);
|
|
203
231
|
pages.push(page);
|
|
204
232
|
}
|
|
205
233
|
// concat demuxer:末页要重复列一次,否则它的 duration 会被忽略
|
package/package.json
CHANGED
package/src/local-slideshow.ts
CHANGED
|
@@ -116,18 +116,36 @@ function resolveText(layer: Layer, content: PageContent): string {
|
|
|
116
116
|
return typeof v === 'string' ? v : ''
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/** 渲染步骤:一段矢量图层(SVG)或一个图片图层。**顺序就是 DSL 里 layers 的顺序。** */
|
|
120
|
+
type Step =
|
|
121
|
+
| { kind: 'svg'; svg: string }
|
|
122
|
+
| { kind: 'image'; layer: Layer; box: [number, number, number, number] }
|
|
123
|
+
|
|
119
124
|
/**
|
|
120
|
-
*
|
|
121
|
-
*
|
|
125
|
+
* 把一页拆成有序的渲染步骤。
|
|
126
|
+
*
|
|
127
|
+
* 关键点:**不能把图片层统一提到最后 composite**。sharp 的 composite 一律贴在底图之上,
|
|
128
|
+
* 那样所有图片就永远压在所有矢量层上面,DSL 里的图层顺序当场失效 ——
|
|
129
|
+
* 「图片铺满 + 文字压在上面」这种版式会渲染成文字被图片盖住(实测踩到)。
|
|
130
|
+
* 所以这里把连续的矢量层攒成一段 SVG,遇到图片层就断开,按原顺序逐段叠。
|
|
131
|
+
*
|
|
132
|
+
* 图片不嵌进 SVG 是另一回事:base64 内联会让 SVG 爆大且慢,走 composite 更划算。
|
|
122
133
|
*/
|
|
123
134
|
function buildPage(layout: Layout, content: PageContent, scale: number, onWarn?: (m: string) => void) {
|
|
124
135
|
const W = Math.round(layout.canvas.w * scale)
|
|
125
136
|
const H = Math.round(layout.canvas.h * scale)
|
|
126
137
|
const s = (n: number) => n * scale
|
|
127
|
-
const
|
|
128
|
-
|
|
138
|
+
const steps: Step[] = []
|
|
139
|
+
let parts: string[] = []
|
|
129
140
|
const unknown = new Set<string>()
|
|
130
141
|
|
|
142
|
+
// 攒够的矢量层收成一段 SVG(透明底 —— 背景色由 base 图承担,否则后一段会盖掉前一段)
|
|
143
|
+
const flush = () => {
|
|
144
|
+
if (!parts.length) return
|
|
145
|
+
steps.push({ kind: 'svg', svg: `<svg width="${W}" height="${H}" xmlns="http://www.w3.org/2000/svg">${parts.join('')}</svg>` })
|
|
146
|
+
parts = []
|
|
147
|
+
}
|
|
148
|
+
|
|
131
149
|
for (const layer of layout.layers) {
|
|
132
150
|
const op = layer.opacity !== undefined ? ` opacity="${layer.opacity}"` : ''
|
|
133
151
|
|
|
@@ -152,17 +170,19 @@ function buildPage(layout: Layout, content: PageContent, scale: number, onWarn?:
|
|
|
152
170
|
)
|
|
153
171
|
} else if ((layer.type === 'slot' || layer.type === 'image') && layer.box) {
|
|
154
172
|
const [x, y, w, h] = layer.box
|
|
155
|
-
|
|
173
|
+
flush() // 断开:此前的矢量层要落在这张图**下面**
|
|
174
|
+
steps.push({ kind: 'image', layer, box: [s(x), s(y), s(w), s(h)] })
|
|
156
175
|
} else {
|
|
157
176
|
// 不认识的层类型:跳过,不中断。老 CLI 碰上新层类型该少画一层,不是出不了片。
|
|
158
177
|
unknown.add(String(layer.type))
|
|
159
178
|
}
|
|
160
179
|
}
|
|
180
|
+
flush()
|
|
161
181
|
|
|
162
182
|
if (unknown.size && onWarn) {
|
|
163
183
|
onWarn(`模板里有本版本不认识的图层类型(${[...unknown].join(', ')}),已跳过。升级试试:npm i -g museav-cli`)
|
|
164
184
|
}
|
|
165
|
-
return {
|
|
185
|
+
return { steps, W, H }
|
|
166
186
|
}
|
|
167
187
|
|
|
168
188
|
function run(cmd: string, args: string[]): Promise<{ code: number; err: string }> {
|
|
@@ -211,10 +231,16 @@ export async function makeSlideshow(opts: SlideshowOpts): Promise<{ out: string;
|
|
|
211
231
|
footer: opts.footer,
|
|
212
232
|
image: opts.images[i],
|
|
213
233
|
}
|
|
214
|
-
const {
|
|
234
|
+
const { steps, W, H } = buildPage(layout, content, scale, warn)
|
|
215
235
|
const composites: { input: Buffer; top: number; left: number }[] = []
|
|
216
236
|
|
|
217
|
-
for (const
|
|
237
|
+
for (const step of steps) {
|
|
238
|
+
// 矢量段按原顺序插进来,图片压在它之前的段之上、之后的段之下
|
|
239
|
+
if (step.kind === 'svg') {
|
|
240
|
+
composites.push({ input: Buffer.from(step.svg), top: 0, left: 0 })
|
|
241
|
+
continue
|
|
242
|
+
}
|
|
243
|
+
const { layer, box } = step
|
|
218
244
|
const [bx, by, bw, bh] = box
|
|
219
245
|
let src: string | null = null
|
|
220
246
|
|
|
@@ -259,8 +285,15 @@ export async function makeSlideshow(opts: SlideshowOpts): Promise<{ out: string;
|
|
|
259
285
|
})
|
|
260
286
|
}
|
|
261
287
|
|
|
288
|
+
// 底图只负责画布背景色,所有内容(含第一段矢量层)都按顺序 composite 上去
|
|
262
289
|
const page = join(work, `p${String(i).padStart(3, '0')}.png`)
|
|
263
|
-
|
|
290
|
+
const bg = layout.canvas.bg ?? '#ffffff'
|
|
291
|
+
await sharp({
|
|
292
|
+
create: { width: W, height: H, channels: 4, background: bg === 'transparent' ? { r: 0, g: 0, b: 0, alpha: 0 } : bg },
|
|
293
|
+
})
|
|
294
|
+
.composite(composites)
|
|
295
|
+
.png()
|
|
296
|
+
.toFile(page)
|
|
264
297
|
pages.push(page)
|
|
265
298
|
}
|
|
266
299
|
|