@sakitam-gis/sprite-cli 1.0.2 → 1.0.4
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/QUICK_START.md +34 -80
- package/README.md +1 -13
- package/dist/cli.js +157 -9
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +21 -1
- package/dist/index.js +158 -9
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
package/QUICK_START.md
CHANGED
|
@@ -90,6 +90,24 @@ icons/
|
|
|
90
90
|
- ✅ SVG(推荐)- 矢量图形,自动优化
|
|
91
91
|
- ✅ PNG - 光栅图像
|
|
92
92
|
|
|
93
|
+
<a id="svg-class-styles"></a>
|
|
94
|
+
|
|
95
|
+
#### SVG 图标的 class 样式
|
|
96
|
+
|
|
97
|
+
设计工具导出的 SVG 可以直接作为输入。例如,下图在雪碧图中显示红色填充和浅色描边。
|
|
98
|
+
|
|
99
|
+
```svg
|
|
100
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
|
|
101
|
+
<style>.st0 { fill: #FF0000; stroke: #F9F7F7; stroke-width: 2 }</style>
|
|
102
|
+
<path class="st0" d="M2 2h12v12H2z"/>
|
|
103
|
+
</svg>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
- CLI 和 Node.js API 在内存中补齐缺少的展示属性。元素上已有的同名属性和内联 `style` 优先。
|
|
107
|
+
- 支持 `.st0`、`.st0, .st1` 以及一个元素带多个 class。匹配的类声明之间先比较 `!important`,再取样式表中靠后的声明。
|
|
108
|
+
- 支持填充、描边、线宽与虚线、透明度、`color`、渐变色标和填充/裁剪规则。可通过 `fill: url(#gradientId)` 引用 SVG 内部渐变,也可写成 `url(#gradientId) hsl(120,100%,50%)` 指定引用失效时的备用色。半透明颜色支持 `rgba()`。
|
|
109
|
+
- 预处理只展开内嵌样式表中顶层的简单类选择器(类名使用 ASCII 字母、数字、下划线或连字符)。后代选择器、复合选择器、伪类、条件规则、外部样式表及 CSS 变量均不在展开范围内。依赖这些写法的图标,建议在导出时把最终颜色写入元素属性。
|
|
110
|
+
|
|
93
111
|
### 2. 生成雪碧图
|
|
94
112
|
|
|
95
113
|
最简单的用法:
|
|
@@ -148,9 +166,6 @@ sprite-maker [output] [input] [options]
|
|
|
148
166
|
| `--retina` | 生成 2x 像素比率(等同于 --ratio=2) | boolean | `false` | `--retina` |
|
|
149
167
|
| `--ratio=<n>` | 指定像素比率 | number | `1` | `--ratio=3` |
|
|
150
168
|
| `--unique` | 去重模式,相同图像只存储一次 | boolean | `false` | `--unique` |
|
|
151
|
-
| `--colors=<n>` | PNG 颜色数量(2-256) | number | `256` | `--colors=128` |
|
|
152
|
-
| `--quality=<n>` | PNG 质量(0-100) | number | `80` | `--quality=90` |
|
|
153
|
-
| `--palette` | 使用调色板模式压缩 PNG | boolean | `false` | `--palette` |
|
|
154
169
|
| `--help, -h` | 显示帮助信息 | - | - | `--help` |
|
|
155
170
|
|
|
156
171
|
## 🎨 高级功能
|
|
@@ -309,77 +324,20 @@ sprite-maker sprites/ icons/ --retina
|
|
|
309
324
|
|
|
310
325
|
## 🗜️ PNG 优化
|
|
311
326
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
### 优化选项
|
|
315
|
-
|
|
316
|
-
#### 1. 颜色量化
|
|
317
|
-
|
|
318
|
-
减少颜色数量以降低文件大小:
|
|
319
|
-
|
|
320
|
-
```bash
|
|
321
|
-
# 使用 128 色调色板(默认 256 色)
|
|
322
|
-
sprite-maker output/sprite icons/ --colors=128
|
|
323
|
-
```
|
|
324
|
-
|
|
325
|
-
**推荐值:**
|
|
326
|
-
- `256` - 最高质量(默认)
|
|
327
|
-
- `128` - 高质量,文件较小
|
|
328
|
-
- `64` - 中等质量,文件更小
|
|
329
|
-
- `32` - 低质量,最小文件
|
|
330
|
-
|
|
331
|
-
#### 2. 质量设置
|
|
332
|
-
|
|
333
|
-
调整 PNG 压缩质量:
|
|
334
|
-
|
|
335
|
-
```bash
|
|
336
|
-
# 90% 质量(默认 80%)
|
|
337
|
-
sprite-maker output/sprite icons/ --quality=90
|
|
338
|
-
```
|
|
339
|
-
|
|
340
|
-
**推荐值:**
|
|
341
|
-
- `90-100` - 高质量,文件较大
|
|
342
|
-
- `80-89` - 平衡质量和大小(推荐)
|
|
343
|
-
- `60-79` - 较小文件,质量略降
|
|
344
|
-
- `< 60` - 不推荐
|
|
345
|
-
|
|
346
|
-
#### 3. 调色板模式
|
|
347
|
-
|
|
348
|
-
启用调色板模式进一步压缩:
|
|
349
|
-
|
|
350
|
-
```bash
|
|
351
|
-
sprite-maker output/sprite icons/ --palette
|
|
352
|
-
```
|
|
353
|
-
|
|
354
|
-
**效果:**
|
|
355
|
-
- ✅ 大幅减小文件大小(通常 30-50%)
|
|
356
|
-
- ⚠️ 可能轻微影响颜色精度
|
|
357
|
-
- ✅ 适用于图标和简单图形
|
|
358
|
-
|
|
359
|
-
#### 4. 组合使用
|
|
327
|
+
在 Node.js 中调用 `generateOptimizedImage` 可设置调色板、颜色数和质量。传入包含图像数据的布局(`format: false`),将返回的 PNG Buffer 写入文件。
|
|
360
328
|
|
|
361
|
-
|
|
329
|
+
```js
|
|
330
|
+
import { writeFile } from 'node:fs/promises';
|
|
331
|
+
import {
|
|
332
|
+
getFilePaths, loadFile, generateLayout, generateOptimizedImage,
|
|
333
|
+
} from '@sakitam-gis/sprite-cli';
|
|
362
334
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
# 高质量压缩(适用于复杂图形)
|
|
368
|
-
sprite-maker output/sprite icons/ --colors=256 --quality=90
|
|
369
|
-
|
|
370
|
-
# 平衡模式(推荐)
|
|
371
|
-
sprite-maker output/sprite icons/ --palette --colors=128 --quality=80
|
|
335
|
+
const imgs = await Promise.all((await getFilePaths('icons/')).map(loadFile));
|
|
336
|
+
const layout = await generateLayout({ imgs, pixelRatio: 1, format: false });
|
|
337
|
+
const png = await generateOptimizedImage(layout, { palette: true, colors: 128, quality: 80 });
|
|
338
|
+
await writeFile('sprite.png', png);
|
|
372
339
|
```
|
|
373
340
|
|
|
374
|
-
### 优化效果对比
|
|
375
|
-
|
|
376
|
-
| 配置 | 文件大小 | 质量 | 适用场景 |
|
|
377
|
-
|------|----------|------|----------|
|
|
378
|
-
| 默认 | 100% | 优秀 | 通用 |
|
|
379
|
-
| `--palette --colors=128` | ~60% | 良好 | 图标、简单图形 |
|
|
380
|
-
| `--palette --colors=64` | ~40% | 中等 | 图标(不追求完美) |
|
|
381
|
-
| `--quality=90` | ~110% | 极佳 | 需要高保真的场景 |
|
|
382
|
-
|
|
383
341
|
## 💡 实际应用场景
|
|
384
342
|
|
|
385
343
|
### 场景 1: Web 应用图标
|
|
@@ -450,7 +408,7 @@ sprite-maker map-styles/sprite@2x map-icons/ --retina
|
|
|
450
408
|
|
|
451
409
|
```bash
|
|
452
410
|
# 生成游戏角色精灵表(PNG 输入)
|
|
453
|
-
sprite-maker game/sprites/characters characters
|
|
411
|
+
sprite-maker game/sprites/characters characters/ --unique
|
|
454
412
|
```
|
|
455
413
|
|
|
456
414
|
### 场景 4: 移动应用资源
|
|
@@ -471,8 +429,8 @@ sprite-maker mobile/assets/icons@3x icons/ --ratio=3
|
|
|
471
429
|
"scripts": {
|
|
472
430
|
"prebuild": "npm run build:sprites",
|
|
473
431
|
"build:sprites": "npm run sprites:1x && npm run sprites:2x",
|
|
474
|
-
"sprites:1x": "sprite-maker public/sprites/icon assets/icons/ --
|
|
475
|
-
"sprites:2x": "sprite-maker public/sprites/icon@2x assets/icons/ --retina --
|
|
432
|
+
"sprites:1x": "sprite-maker public/sprites/icon assets/icons/ --unique",
|
|
433
|
+
"sprites:2x": "sprite-maker public/sprites/icon@2x assets/icons/ --retina --unique",
|
|
476
434
|
"watch:sprites": "chokidar 'assets/icons/**/*.svg' -c 'npm run build:sprites'"
|
|
477
435
|
}
|
|
478
436
|
}
|
|
@@ -482,11 +440,7 @@ sprite-maker mobile/assets/icons@3x icons/ --ratio=3
|
|
|
482
440
|
|
|
483
441
|
### Q1: 生成的 PNG 文件太大,如何优化?
|
|
484
442
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
```bash
|
|
488
|
-
sprite-maker output/sprite icons/ --palette --colors=128 --quality=80
|
|
489
|
-
```
|
|
443
|
+
使用 Node.js API 调整调色板颜色数和质量,示例见 [PNG 优化](#-png-优化)。
|
|
490
444
|
|
|
491
445
|
### Q2: 支持哪些图像格式?
|
|
492
446
|
|
|
@@ -503,8 +457,8 @@ sprite-maker output/sprite icons/ --palette --colors=128 --quality=80
|
|
|
503
457
|
- 可处理 500+ 图标
|
|
504
458
|
|
|
505
459
|
```bash
|
|
506
|
-
# 大批量图标建议使用 unique
|
|
507
|
-
sprite-maker output/sprite icons/ --unique
|
|
460
|
+
# 大批量图标建议使用 unique 模式
|
|
461
|
+
sprite-maker output/sprite icons/ --unique
|
|
508
462
|
```
|
|
509
463
|
|
|
510
464
|
### Q4: unique 模式什么时候使用?
|
package/README.md
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
### 🎨 图像支持
|
|
17
17
|
- ✅ **SVG 支持** - 原生支持矢量图形,自动优化
|
|
18
|
+
- ✅ **SVG 类样式** - 自动读取内嵌 `<style>` 中的简单类规则,保留图标的填充和描边颜色。[支持范围与示例](./QUICK_START.md#svg-class-styles)
|
|
18
19
|
- ✅ **PNG 支持** - 支持光栅图像输入
|
|
19
20
|
- ✅ **多种像素比率** - 支持 1x、2x、3x 等 Retina 显示屏
|
|
20
21
|
- ✅ **智能去重** - unique 模式,自动识别相同图像
|
|
@@ -114,16 +115,6 @@ sprite-maker output/sprite@3x icons/ --ratio=3 # 3x
|
|
|
114
115
|
sprite-maker output/sprite icons/ --unique
|
|
115
116
|
```
|
|
116
117
|
|
|
117
|
-
#### 5️⃣ PNG 优化选项
|
|
118
|
-
|
|
119
|
-
```bash
|
|
120
|
-
# 调色板模式(减小文件大小)
|
|
121
|
-
sprite-maker output/sprite icons/ --palette --colors=128
|
|
122
|
-
|
|
123
|
-
# 自定义质量
|
|
124
|
-
sprite-maker output/sprite icons/ --quality=90
|
|
125
|
-
```
|
|
126
|
-
|
|
127
118
|
## 📖 详细文档
|
|
128
119
|
|
|
129
120
|
查看 [快速开始指南](./QUICK_START.md) 获取更多详细示例和高级用法。
|
|
@@ -135,9 +126,6 @@ sprite-maker output/sprite icons/ --quality=90
|
|
|
135
126
|
| `--retina` | 生成 2x 像素比率(等同于 --ratio=2) | `false` |
|
|
136
127
|
| `--ratio=<n>` | 指定像素比率(1x, 2x, 3x 等) | `1` |
|
|
137
128
|
| `--unique` | 去重模式,相同图像只存储一次 | `false` |
|
|
138
|
-
| `--colors=<n>` | PNG 颜色数量(2-256) | `256` |
|
|
139
|
-
| `--quality=<n>` | PNG 质量(0-100) | `80` |
|
|
140
|
-
| `--palette` | 使用调色板模式压缩 PNG | `false` |
|
|
141
129
|
| `--help, -h` | 显示帮助信息 | - |
|
|
142
130
|
|
|
143
131
|
## 🛠️ 开发
|
package/dist/cli.js
CHANGED
|
@@ -110,7 +110,7 @@ var ShelfPack = class {
|
|
|
110
110
|
* ```
|
|
111
111
|
*/
|
|
112
112
|
pack(bins, options = {}) {
|
|
113
|
-
const binArray =
|
|
113
|
+
const binArray = Array.isArray(bins) ? bins : [bins];
|
|
114
114
|
const results = [];
|
|
115
115
|
for (let i = 0; i < binArray.length; i++) {
|
|
116
116
|
const w = binArray[i].w || binArray[i].width;
|
|
@@ -416,8 +416,8 @@ async function extractMetadata(svg, pixelRatio = 1) {
|
|
|
416
416
|
}
|
|
417
417
|
]
|
|
418
418
|
});
|
|
419
|
-
const { DOMParser:
|
|
420
|
-
const doc = new
|
|
419
|
+
const { DOMParser: DOMParser3 } = await import("@xmldom/xmldom");
|
|
420
|
+
const doc = new DOMParser3().parseFromString(optimized.data, "image/svg+xml");
|
|
421
421
|
const metadata = {};
|
|
422
422
|
const paths = doc.getElementsByTagName("path");
|
|
423
423
|
for (let i = 0; i < paths.length; i++) {
|
|
@@ -454,7 +454,7 @@ async function extractMetadata(svg, pixelRatio = 1) {
|
|
|
454
454
|
metadata.placeholder = [left, top, right, bottom];
|
|
455
455
|
}
|
|
456
456
|
} catch (err) {
|
|
457
|
-
console.warn(`Failed to compute bounds for ${id}: ${err}`);
|
|
457
|
+
console.warn(`Failed to compute bounds for ${id}: ${String(err)}`);
|
|
458
458
|
}
|
|
459
459
|
}
|
|
460
460
|
if (metadata.stretchX) {
|
|
@@ -465,7 +465,7 @@ async function extractMetadata(svg, pixelRatio = 1) {
|
|
|
465
465
|
}
|
|
466
466
|
return metadata;
|
|
467
467
|
} catch (err) {
|
|
468
|
-
console.error(`Failed to extract metadata: ${err}`);
|
|
468
|
+
console.error(`Failed to extract metadata: ${String(err)}`);
|
|
469
469
|
return {};
|
|
470
470
|
}
|
|
471
471
|
}
|
|
@@ -483,7 +483,9 @@ function heightAndIdComparator(a, b) {
|
|
|
483
483
|
return a.id.localeCompare(b.id);
|
|
484
484
|
}
|
|
485
485
|
function packImages(images) {
|
|
486
|
-
const sorted = [...images].sort(
|
|
486
|
+
const sorted = [...images].sort(
|
|
487
|
+
heightAndIdComparator
|
|
488
|
+
);
|
|
487
489
|
const sprite = new ShelfPack(1, 1, { autoResize: true });
|
|
488
490
|
sprite.pack(sorted, { inPlace: true });
|
|
489
491
|
const packed = sorted.map((img) => ({
|
|
@@ -571,7 +573,7 @@ async function getImageDimensions(img, pixelRatio, useCache = true) {
|
|
|
571
573
|
height: Math.ceil(height * pixelRatio)
|
|
572
574
|
};
|
|
573
575
|
} catch (err) {
|
|
574
|
-
throw new Error(`Failed to process SVG ${img.id}: ${err}`);
|
|
576
|
+
throw new Error(`Failed to process SVG ${img.id}: ${String(err)}`);
|
|
575
577
|
}
|
|
576
578
|
}
|
|
577
579
|
return {
|
|
@@ -611,7 +613,7 @@ async function generateLayout(options) {
|
|
|
611
613
|
const metadata = await extractMetadata(originalImg.svg, pixelRatio);
|
|
612
614
|
Object.assign(layout[item.id], metadata);
|
|
613
615
|
} catch (err) {
|
|
614
|
-
console.warn(`Failed to extract metadata for ${item.id}: ${err}`);
|
|
616
|
+
console.warn(`Failed to extract metadata for ${item.id}: ${String(err)}`);
|
|
615
617
|
}
|
|
616
618
|
}
|
|
617
619
|
} else {
|
|
@@ -684,13 +686,156 @@ async function generateLayoutUnique(options) {
|
|
|
684
686
|
// src/core/image.ts
|
|
685
687
|
import sharp from "sharp";
|
|
686
688
|
import { createCanvas, loadImage as loadImage2 } from "@napi-rs/canvas";
|
|
689
|
+
|
|
690
|
+
// src/core/svg-styles.ts
|
|
691
|
+
import { DOMParser as DOMParser2, XMLSerializer } from "@xmldom/xmldom";
|
|
692
|
+
import postcss from "postcss";
|
|
693
|
+
var PRESENTATION = /* @__PURE__ */ new Set([
|
|
694
|
+
"fill",
|
|
695
|
+
"fill-opacity",
|
|
696
|
+
"fill-rule",
|
|
697
|
+
"stroke",
|
|
698
|
+
"stroke-width",
|
|
699
|
+
"stroke-opacity",
|
|
700
|
+
"stroke-linecap",
|
|
701
|
+
"stroke-linejoin",
|
|
702
|
+
"stroke-miterlimit",
|
|
703
|
+
"stroke-dasharray",
|
|
704
|
+
"stroke-dashoffset",
|
|
705
|
+
"opacity",
|
|
706
|
+
"color",
|
|
707
|
+
"stop-color",
|
|
708
|
+
"stop-opacity",
|
|
709
|
+
"clip-rule"
|
|
710
|
+
]);
|
|
711
|
+
function parseCss(css) {
|
|
712
|
+
try {
|
|
713
|
+
return postcss.parse(css);
|
|
714
|
+
} catch (error) {
|
|
715
|
+
if (error instanceof postcss.CssSyntaxError) return void 0;
|
|
716
|
+
throw error;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
function select(target, prop, next) {
|
|
720
|
+
const prev = target.get(prop);
|
|
721
|
+
if (!prev || (next.important !== prev.important ? next.important : next.order >= prev.order)) {
|
|
722
|
+
target.set(prop, next);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
function collectRules(doc) {
|
|
726
|
+
const byClass = /* @__PURE__ */ new Map();
|
|
727
|
+
let order = 0;
|
|
728
|
+
for (const style of Array.from(doc.getElementsByTagNameNS("*", "style"))) {
|
|
729
|
+
if (style.namespaceURI !== doc.documentElement.namespaceURI) continue;
|
|
730
|
+
const type = style.getAttribute("type")?.trim().toLowerCase();
|
|
731
|
+
const media = style.getAttribute("media")?.trim().toLowerCase();
|
|
732
|
+
if (type && type !== "text/css" || media && media !== "all") continue;
|
|
733
|
+
for (const rule of parseCss(style.textContent || "")?.nodes ?? []) {
|
|
734
|
+
if (rule.type !== "rule") continue;
|
|
735
|
+
const properties = /* @__PURE__ */ new Map();
|
|
736
|
+
for (const decl of rule.nodes) {
|
|
737
|
+
if (decl.type !== "decl") continue;
|
|
738
|
+
const prop = decl.prop.toLowerCase();
|
|
739
|
+
if (PRESENTATION.has(prop)) {
|
|
740
|
+
select(properties, prop, {
|
|
741
|
+
value: decl.value,
|
|
742
|
+
important: !!decl.important,
|
|
743
|
+
order: order++
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
for (const selector of new Set(rule.selectors)) {
|
|
748
|
+
if (!/^\.[\w-]+$/.test(selector)) continue;
|
|
749
|
+
const name = selector.slice(1);
|
|
750
|
+
const declarations = byClass.get(name) ?? /* @__PURE__ */ new Map();
|
|
751
|
+
for (const [prop, value] of properties) select(declarations, prop, value);
|
|
752
|
+
byClass.set(name, declarations);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return byClass;
|
|
757
|
+
}
|
|
758
|
+
function simplifyImportant(style) {
|
|
759
|
+
const winners = /* @__PURE__ */ new Map();
|
|
760
|
+
for (const decl of style.nodes) {
|
|
761
|
+
if (decl.type === "decl" && decl.important && PRESENTATION.has(decl.prop.toLowerCase())) {
|
|
762
|
+
winners.set(decl.prop.toLowerCase(), decl);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
if (!winners.size) return false;
|
|
766
|
+
style.walkDecls((decl) => {
|
|
767
|
+
const winner = winners.get(decl.prop.toLowerCase());
|
|
768
|
+
if (winner === decl) decl.important = false;
|
|
769
|
+
else if (winner) decl.remove();
|
|
770
|
+
});
|
|
771
|
+
return true;
|
|
772
|
+
}
|
|
773
|
+
function inlineSvgClassStyles(svg) {
|
|
774
|
+
if (!/<(?:[^\s<>:]+:)?style[\s>]/.test(svg) && !/!\s*important/i.test(svg)) return svg;
|
|
775
|
+
const doc = new DOMParser2().parseFromString(svg, "image/svg+xml");
|
|
776
|
+
const byClass = collectRules(doc);
|
|
777
|
+
let changed = false;
|
|
778
|
+
for (const element of Array.from(doc.getElementsByTagName("*"))) {
|
|
779
|
+
if (element.namespaceURI !== doc.documentElement.namespaceURI) continue;
|
|
780
|
+
const properties = /* @__PURE__ */ new Map();
|
|
781
|
+
for (const name of new Set((element.getAttribute("class") || "").split(/\s+/))) {
|
|
782
|
+
for (const [prop, value] of byClass.get(name) ?? []) select(properties, prop, value);
|
|
783
|
+
}
|
|
784
|
+
const inline = element.getAttribute("style");
|
|
785
|
+
if (inline && (properties.size || inline.includes("!"))) {
|
|
786
|
+
const style = parseCss(inline);
|
|
787
|
+
if (!style) continue;
|
|
788
|
+
style.walkDecls((decl) => {
|
|
789
|
+
properties.delete(decl.prop.toLowerCase());
|
|
790
|
+
});
|
|
791
|
+
if (simplifyImportant(style)) {
|
|
792
|
+
element.setAttribute("style", style.toString());
|
|
793
|
+
changed = true;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
for (const [prop, { value }] of properties) {
|
|
797
|
+
if (!element.hasAttribute(prop)) {
|
|
798
|
+
element.setAttribute(prop, value);
|
|
799
|
+
changed = true;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
return changed ? new XMLSerializer().serializeToString(doc) : svg;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// src/core/colors.ts
|
|
807
|
+
import { colord, extend } from "colord";
|
|
808
|
+
import namesPlugin from "colord/plugins/names";
|
|
809
|
+
extend([namesPlugin]);
|
|
810
|
+
var COLOR_ATTR_RE = /\b(?:fill|stroke|stop-color|flood-color|color|lighting-color)\s*=\s*(["'])(.*?)\1/gi;
|
|
811
|
+
var STYLE_COLOR_PROP_RE = /(?:fill|stroke|stop-color|flood-color|color|lighting-color|background-color)\s*:\s*([^;"'}{]+)/gi;
|
|
812
|
+
function normalizePaint(value) {
|
|
813
|
+
const trimmed = value.trim();
|
|
814
|
+
const url = /^url\((?:"[^"]*"|'[^']*'|[^)]*)\)\s*/i.exec(trimmed)?.[0] ?? "";
|
|
815
|
+
const color = colord(trimmed.slice(url.length));
|
|
816
|
+
if (!color.isValid()) return value;
|
|
817
|
+
const normalized = color.toRgb().a === 1 ? color.toHex() : color.toRgbString();
|
|
818
|
+
return url + normalized;
|
|
819
|
+
}
|
|
820
|
+
function normalizeSvgColors(svgString) {
|
|
821
|
+
if (!/(?:hsl|rgb|color|#[\da-f]{4}\b|#[\da-f]{8}\b)/i.test(svgString)) return svgString;
|
|
822
|
+
return svgString.replace(
|
|
823
|
+
STYLE_COLOR_PROP_RE,
|
|
824
|
+
(full, value) => full.replace(value, () => normalizePaint(value))
|
|
825
|
+
).replace(
|
|
826
|
+
COLOR_ATTR_RE,
|
|
827
|
+
(full, quote, value) => full.replace(quote + value + quote, () => quote + normalizePaint(value) + quote)
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// src/core/image.ts
|
|
687
832
|
async function renderImage(imageBuffer, isPNG, ctx, x, y, width, height) {
|
|
688
833
|
return new Promise((resolve, reject) => {
|
|
689
834
|
let src;
|
|
690
835
|
if (isPNG) {
|
|
691
836
|
src = imageBuffer;
|
|
692
837
|
} else {
|
|
693
|
-
const svgString = imageBuffer.toString("utf8");
|
|
838
|
+
const svgString = normalizeSvgColors(inlineSvgClassStyles(imageBuffer.toString("utf8")));
|
|
694
839
|
const svgDataUrl = `data:image/svg+xml;base64,${Buffer.from(svgString).toString("base64")}`;
|
|
695
840
|
src = svgDataUrl;
|
|
696
841
|
}
|
|
@@ -727,7 +872,10 @@ async function generateImage(layout) {
|
|
|
727
872
|
const ctx = canvas.getContext("2d");
|
|
728
873
|
ctx.clearRect(0, 0, width, height);
|
|
729
874
|
const renderPromises = [];
|
|
875
|
+
const rendered = /* @__PURE__ */ new Set();
|
|
730
876
|
for (const [id, item] of Object.entries(layout)) {
|
|
877
|
+
if (rendered.has(item)) continue;
|
|
878
|
+
rendered.add(item);
|
|
731
879
|
const svg = item.svg;
|
|
732
880
|
const png = item.png;
|
|
733
881
|
if (!svg && !png) {
|