@sakitam-gis/sprite-cli 1.0.0 → 1.0.1

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/dist/index.js ADDED
@@ -0,0 +1,901 @@
1
+ // src/sprite.ts
2
+ import fs2 from "fs/promises";
3
+ import path2 from "path";
4
+ import stringify from "json-stable-stringify";
5
+
6
+ // src/core/layout.ts
7
+ import { DOMParser } from "@xmldom/xmldom";
8
+
9
+ // src/core/shelf-pack.ts
10
+ var Bin = class {
11
+ /**
12
+ * @param id Unique id of the bin
13
+ * @param x Left coordinate of the bin
14
+ * @param y Top coordinate of the bin
15
+ * @param w Width of the bin
16
+ * @param h Height of the bin
17
+ * @param maxw Max width of the bin (defaults to `w` if not provided)
18
+ * @param maxh Max height of the bin (defaults to `h` if not provided)
19
+ */
20
+ constructor(id, x, y, w, h, maxw, maxh) {
21
+ this.id = id;
22
+ this.x = x;
23
+ this.y = y;
24
+ this.w = w;
25
+ this.h = h;
26
+ this.maxw = maxw || w;
27
+ this.maxh = maxh || h;
28
+ this.refcount = 0;
29
+ }
30
+ };
31
+ var Shelf = class {
32
+ /**
33
+ * @param y Top coordinate of the new shelf
34
+ * @param w Width of the new shelf
35
+ * @param h Height of the new shelf
36
+ */
37
+ constructor(y, w, h) {
38
+ this.x = 0;
39
+ this.y = y;
40
+ this.w = this.free = w;
41
+ this.h = h;
42
+ }
43
+ /**
44
+ * 在货架上分配一个 bin
45
+ *
46
+ * @param w Width of the bin to allocate
47
+ * @param h Height of the bin to allocate
48
+ * @param id Unique id of the bin to allocate
49
+ * @returns Bin object or null if allocation failed
50
+ */
51
+ alloc(w, h, id) {
52
+ if (w > this.free || h > this.h) {
53
+ return null;
54
+ }
55
+ const x = this.x;
56
+ this.x += w;
57
+ this.free -= w;
58
+ return new Bin(id, x, this.y, w, h, w, this.h);
59
+ }
60
+ /**
61
+ * 调整货架宽度
62
+ *
63
+ * @param w Requested new width of the shelf
64
+ * @returns true
65
+ */
66
+ resize(w) {
67
+ this.free += w - this.w;
68
+ this.w = w;
69
+ return true;
70
+ }
71
+ };
72
+ var ShelfPack = class {
73
+ /**
74
+ * @param w Initial width of the sprite
75
+ * @param h Initial height of the sprite
76
+ * @param options Configuration options
77
+ */
78
+ constructor(w = 64, h = 64, options = {}) {
79
+ this.w = w;
80
+ this.h = h;
81
+ this.autoResize = !!options.autoResize;
82
+ this.shelves = [];
83
+ this.freebins = [];
84
+ this.stats = {};
85
+ this.bins = {};
86
+ this.maxId = 0;
87
+ }
88
+ /**
89
+ * 批量打包多个 bins
90
+ *
91
+ * @param bins Array of requested bins
92
+ * @param options Pack options
93
+ * @returns Array of allocated Bins
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * const bins = [
98
+ * { id: 1, w: 12, h: 12 },
99
+ * { id: 2, w: 12, h: 16 },
100
+ * ];
101
+ * const results = sprite.pack(bins, { inPlace: false });
102
+ * ```
103
+ */
104
+ pack(bins, options = {}) {
105
+ const binArray = [].concat(bins);
106
+ const results = [];
107
+ for (let i = 0; i < binArray.length; i++) {
108
+ const w = binArray[i].w || binArray[i].width;
109
+ const h = binArray[i].h || binArray[i].height;
110
+ const id = binArray[i].id;
111
+ if (w && h) {
112
+ const allocation = this.packOne(w, h, id);
113
+ if (!allocation) {
114
+ continue;
115
+ }
116
+ if (options.inPlace) {
117
+ binArray[i].x = allocation.x;
118
+ binArray[i].y = allocation.y;
119
+ binArray[i].id = allocation.id;
120
+ }
121
+ results.push(allocation);
122
+ }
123
+ }
124
+ this.shrink();
125
+ return results;
126
+ }
127
+ /**
128
+ * 打包单个 bin
129
+ *
130
+ * 每个 bin 都有唯一标识符。如果未提供 id,将自动创建一个。
131
+ * Bins 自动进行引用计数。
132
+ *
133
+ * @param w Width of the bin to allocate
134
+ * @param h Height of the bin to allocate
135
+ * @param id Unique identifier for this bin
136
+ * @returns Bin object or null if allocation failed
137
+ *
138
+ * @example
139
+ * ```ts
140
+ * const bin = sprite.packOne(12, 16, 'a');
141
+ * ```
142
+ */
143
+ packOne(w, h, id) {
144
+ const best = { freebin: -1, shelf: -1, waste: Infinity };
145
+ let y = 0;
146
+ let bin;
147
+ let shelf;
148
+ let waste;
149
+ let i;
150
+ if (typeof id === "string" || typeof id === "number") {
151
+ bin = this.getBin(id);
152
+ if (bin) {
153
+ this.ref(bin);
154
+ return bin;
155
+ }
156
+ if (typeof id === "number") {
157
+ this.maxId = Math.max(id, this.maxId);
158
+ }
159
+ } else {
160
+ id = ++this.maxId;
161
+ }
162
+ for (i = 0; i < this.freebins.length; i++) {
163
+ bin = this.freebins[i];
164
+ if (h === bin.maxh && w === bin.maxw) {
165
+ return this.allocFreebin(i, w, h, id);
166
+ }
167
+ if (h > bin.maxh || w > bin.maxw) {
168
+ continue;
169
+ }
170
+ if (h <= bin.maxh && w <= bin.maxw) {
171
+ waste = bin.maxw * bin.maxh - w * h;
172
+ if (waste < best.waste) {
173
+ best.waste = waste;
174
+ best.freebin = i;
175
+ }
176
+ }
177
+ }
178
+ for (i = 0; i < this.shelves.length; i++) {
179
+ shelf = this.shelves[i];
180
+ y += shelf.h;
181
+ if (w > shelf.free) {
182
+ continue;
183
+ }
184
+ if (h === shelf.h) {
185
+ return this.allocShelf(i, w, h, id);
186
+ }
187
+ if (h > shelf.h) {
188
+ continue;
189
+ }
190
+ if (h < shelf.h) {
191
+ waste = (shelf.h - h) * w;
192
+ if (waste < best.waste) {
193
+ best.freebin = -1;
194
+ best.waste = waste;
195
+ best.shelf = i;
196
+ }
197
+ }
198
+ }
199
+ if (best.freebin !== -1) {
200
+ return this.allocFreebin(best.freebin, w, h, id);
201
+ }
202
+ if (best.shelf !== -1) {
203
+ return this.allocShelf(best.shelf, w, h, id);
204
+ }
205
+ if (h <= this.h - y && w <= this.w) {
206
+ shelf = new Shelf(y, this.w, h);
207
+ return this.allocShelf(this.shelves.push(shelf) - 1, w, h, id);
208
+ }
209
+ if (this.autoResize) {
210
+ const h1 = this.h;
211
+ let h2 = this.h;
212
+ const w1 = this.w;
213
+ let w2 = this.w;
214
+ if (w1 <= h1 || w > w1) {
215
+ w2 = Math.max(w, w1) * 2;
216
+ }
217
+ if (h1 < w1 || h > h1) {
218
+ h2 = Math.max(h, h1) * 2;
219
+ }
220
+ this.resize(w2, h2);
221
+ return this.packOne(w, h, id);
222
+ }
223
+ return null;
224
+ }
225
+ /**
226
+ * 通过重用现有 freebin 分配一个 bin
227
+ *
228
+ * @param index Index into the `this.freebins` array
229
+ * @param w Width of the bin to allocate
230
+ * @param h Height of the bin to allocate
231
+ * @param id Unique identifier for this bin
232
+ * @returns Bin object
233
+ */
234
+ allocFreebin(index, w, h, id) {
235
+ const bin = this.freebins.splice(index, 1)[0];
236
+ bin.id = id;
237
+ bin.w = w;
238
+ bin.h = h;
239
+ bin.refcount = 0;
240
+ this.bins[id] = bin;
241
+ this.ref(bin);
242
+ return bin;
243
+ }
244
+ /**
245
+ * 在现有货架上分配 bin
246
+ *
247
+ * @param index Index into the `this.shelves` array
248
+ * @param w Width of the bin to allocate
249
+ * @param h Height of the bin to allocate
250
+ * @param id Unique identifier for this bin
251
+ * @returns Bin object
252
+ */
253
+ allocShelf(index, w, h, id) {
254
+ const shelf = this.shelves[index];
255
+ const bin = shelf.alloc(w, h, id);
256
+ if (bin) {
257
+ this.bins[id] = bin;
258
+ this.ref(bin);
259
+ }
260
+ return bin;
261
+ }
262
+ /**
263
+ * 将 sprite 的宽度/高度缩小到最小
264
+ *
265
+ * 由于 shelf-pack 在空间不足时会先加倍宽度再加倍高度,
266
+ * 如果这发生在打包结束时,可能会导致较大的未使用空间
267
+ */
268
+ shrink() {
269
+ if (this.shelves.length > 0) {
270
+ let w2 = 0;
271
+ let h2 = 0;
272
+ for (let j = 0; j < this.shelves.length; j++) {
273
+ const shelf = this.shelves[j];
274
+ h2 += shelf.h;
275
+ w2 = Math.max(shelf.w - shelf.free, w2);
276
+ }
277
+ this.resize(w2, h2);
278
+ }
279
+ }
280
+ /**
281
+ * 根据 id 返回已打包的 bin
282
+ *
283
+ * @param id Unique identifier for this bin
284
+ * @returns The requested bin, or undefined if not found
285
+ *
286
+ * @example
287
+ * ```ts
288
+ * const bin = sprite.getBin('a');
289
+ * ```
290
+ */
291
+ getBin(id) {
292
+ return this.bins[id];
293
+ }
294
+ /**
295
+ * 增加 bin 的引用计数并更新统计信息
296
+ *
297
+ * @param bin Bin instance
298
+ * @returns New refcount of the bin
299
+ *
300
+ * @example
301
+ * ```ts
302
+ * const bin = sprite.getBin('a');
303
+ * if (bin) sprite.ref(bin);
304
+ * ```
305
+ */
306
+ ref(bin) {
307
+ if (++bin.refcount === 1) {
308
+ const h = bin.h;
309
+ this.stats[h] = (this.stats[h] || 0) + 1;
310
+ }
311
+ return bin.refcount;
312
+ }
313
+ /**
314
+ * 减少 bin 的引用计数并更新统计信息
315
+ * 当引用计数达到 0 时,bin 将自动标记为空闲空间
316
+ *
317
+ * @param bin Bin instance
318
+ * @returns New refcount of the bin
319
+ *
320
+ * @example
321
+ * ```ts
322
+ * const bin = sprite.getBin('a');
323
+ * if (bin) sprite.unref(bin);
324
+ * ```
325
+ */
326
+ unref(bin) {
327
+ if (bin.refcount === 0) {
328
+ return 0;
329
+ }
330
+ if (--bin.refcount === 0) {
331
+ this.stats[bin.h]--;
332
+ delete this.bins[bin.id];
333
+ this.freebins.push(bin);
334
+ }
335
+ return bin.refcount;
336
+ }
337
+ /**
338
+ * 清除 sprite,重置所有内容和统计信息
339
+ *
340
+ * @example
341
+ * ```ts
342
+ * sprite.clear();
343
+ * ```
344
+ */
345
+ clear() {
346
+ this.shelves = [];
347
+ this.freebins = [];
348
+ this.stats = {};
349
+ this.bins = {};
350
+ this.maxId = 0;
351
+ }
352
+ /**
353
+ * 调整 sprite 尺寸
354
+ *
355
+ * @param w Requested new sprite width
356
+ * @param h Requested new sprite height
357
+ * @returns `true` if resize succeeded
358
+ *
359
+ * @example
360
+ * ```ts
361
+ * sprite.resize(256, 256);
362
+ * ```
363
+ */
364
+ resize(w, h) {
365
+ this.w = w;
366
+ this.h = h;
367
+ for (let i = 0; i < this.shelves.length; i++) {
368
+ this.shelves[i].resize(w);
369
+ }
370
+ return true;
371
+ }
372
+ };
373
+
374
+ // src/core/metadata.ts
375
+ import { optimize } from "svgo";
376
+ import computePathBounds from "svg-boundings";
377
+ var metadataPrefix = "mapbox-";
378
+ function strongRound(value, precision = 3) {
379
+ const error = +Math.pow(0.1, precision).toFixed(precision);
380
+ if (+value.toFixed(precision) !== value) {
381
+ const rounded = +value.toFixed(precision - 1);
382
+ value = +Math.abs(rounded - value).toFixed(precision + 1) >= error ? +value.toFixed(precision) : rounded;
383
+ }
384
+ return value;
385
+ }
386
+ async function extractMetadata(svg, pixelRatio = 1) {
387
+ const svgString = svg.toString("utf-8");
388
+ if (!svgString.includes("mapbox-stretch") && !svgString.includes("mapbox-text-placeholder")) {
389
+ return {};
390
+ }
391
+ try {
392
+ const optimized = optimize(svgString, {
393
+ plugins: [
394
+ {
395
+ name: "convertShapeToPath",
396
+ params: {
397
+ convertArcs: true
398
+ }
399
+ },
400
+ {
401
+ name: "convertTransform"
402
+ },
403
+ {
404
+ name: "cleanupIds",
405
+ params: {
406
+ preservePrefixes: ["mapbox-"]
407
+ }
408
+ }
409
+ ]
410
+ });
411
+ const { DOMParser: DOMParser2 } = await import("@xmldom/xmldom");
412
+ const doc = new DOMParser2().parseFromString(optimized.data, "image/svg+xml");
413
+ const metadata = {};
414
+ const paths = doc.getElementsByTagName("path");
415
+ for (let i = 0; i < paths.length; i++) {
416
+ const path3 = paths[i];
417
+ const id = path3.getAttribute("id");
418
+ const d = path3.getAttribute("d");
419
+ if (!id || !id.startsWith(metadataPrefix) || !d) {
420
+ continue;
421
+ }
422
+ try {
423
+ const bounds = computePathBounds.path({ d }, true);
424
+ if (!Number.isFinite(bounds.left) || !Number.isFinite(bounds.right) || !Number.isFinite(bounds.top) || !Number.isFinite(bounds.bottom)) {
425
+ continue;
426
+ }
427
+ const left = strongRound(bounds.left * pixelRatio);
428
+ const top = strongRound(bounds.top * pixelRatio);
429
+ const right = strongRound(bounds.right * pixelRatio);
430
+ const bottom = strongRound(bounds.bottom * pixelRatio);
431
+ const metadataType = id.substring(metadataPrefix.length);
432
+ if (metadataType.startsWith("stretch-x") || metadataType === "stretch-x") {
433
+ metadata.stretchX = metadata.stretchX || [];
434
+ metadata.stretchX.push([left, right]);
435
+ } else if (metadataType.startsWith("stretch-y") || metadataType === "stretch-y") {
436
+ metadata.stretchY = metadata.stretchY || [];
437
+ metadata.stretchY.push([top, bottom]);
438
+ } else if (metadataType === "stretch") {
439
+ metadata.stretchX = metadata.stretchX || [];
440
+ metadata.stretchX.push([left, right]);
441
+ metadata.stretchY = metadata.stretchY || [];
442
+ metadata.stretchY.push([top, bottom]);
443
+ } else if (metadataType === "content") {
444
+ metadata.content = [left, top, right, bottom];
445
+ } else if (metadataType === "text-placeholder") {
446
+ metadata.placeholder = [left, top, right, bottom];
447
+ }
448
+ } catch (err) {
449
+ console.warn(`Failed to compute bounds for ${id}: ${err}`);
450
+ }
451
+ }
452
+ if (metadata.stretchX) {
453
+ metadata.stretchX.sort((a, b) => a[0] - b[0]);
454
+ }
455
+ if (metadata.stretchY) {
456
+ metadata.stretchY.sort((a, b) => a[0] - b[0]);
457
+ }
458
+ return metadata;
459
+ } catch (err) {
460
+ console.error(`Failed to extract metadata: ${err}`);
461
+ return {};
462
+ }
463
+ }
464
+ function hasMetadata(svg) {
465
+ const svgString = svg.toString("utf-8");
466
+ return svgString.includes("mapbox-stretch") || svgString.includes("mapbox-text-placeholder");
467
+ }
468
+
469
+ // src/core/layout.ts
470
+ function heightAndIdComparator(a, b) {
471
+ const heightDiff = b.height - a.height;
472
+ if (heightDiff !== 0) {
473
+ return heightDiff;
474
+ }
475
+ return a.id.localeCompare(b.id);
476
+ }
477
+ function packImages(images) {
478
+ const sorted = [...images].sort(heightAndIdComparator);
479
+ const sprite = new ShelfPack(1, 1, { autoResize: true });
480
+ sprite.pack(sorted, { inPlace: true });
481
+ const packed = sorted.map((img) => ({
482
+ id: img.id,
483
+ x: img.x,
484
+ y: img.y,
485
+ w: img.width,
486
+ h: img.height
487
+ }));
488
+ return {
489
+ packed,
490
+ width: sprite.w,
491
+ height: sprite.h
492
+ };
493
+ }
494
+ var svgDimensionsCache = /* @__PURE__ */ new Map();
495
+ async function getImageDimensions(img, pixelRatio, useCache = true) {
496
+ if (img.width && img.height) {
497
+ return {
498
+ width: Math.ceil(img.width * pixelRatio),
499
+ height: Math.ceil(img.height * pixelRatio)
500
+ };
501
+ }
502
+ if (img.png) {
503
+ const { createCanvas, Image } = await import("canvas");
504
+ return new Promise((resolve, reject) => {
505
+ const image = new Image();
506
+ image.onload = () => {
507
+ resolve({
508
+ width: Math.ceil(image.width * pixelRatio),
509
+ height: Math.ceil(image.height * pixelRatio)
510
+ });
511
+ };
512
+ image.onerror = reject;
513
+ image.src = img.png;
514
+ });
515
+ }
516
+ if (img.svg) {
517
+ let cachedDims;
518
+ let cacheKey;
519
+ if (useCache) {
520
+ cacheKey = img.svg.toString("base64");
521
+ cachedDims = svgDimensionsCache.get(cacheKey);
522
+ if (cachedDims) {
523
+ return {
524
+ width: Math.ceil(cachedDims.width * pixelRatio),
525
+ height: Math.ceil(cachedDims.height * pixelRatio)
526
+ };
527
+ }
528
+ }
529
+ try {
530
+ const svgString = img.svg.toString("utf8");
531
+ const doc = new DOMParser().parseFromString(svgString, "image/svg+xml");
532
+ const svgElement = doc.documentElement;
533
+ const parseError = doc.getElementsByTagName("parsererror");
534
+ if (parseError && parseError.length > 0) {
535
+ throw new Error(`Failed to parse SVG for ${img.id}: Invalid XML`);
536
+ }
537
+ let width = 0;
538
+ let height = 0;
539
+ const widthAttr = svgElement.getAttribute("width");
540
+ const heightAttr = svgElement.getAttribute("height");
541
+ if (widthAttr && heightAttr) {
542
+ width = parseFloat(widthAttr);
543
+ height = parseFloat(heightAttr);
544
+ } else {
545
+ const viewBox = svgElement.getAttribute("viewBox");
546
+ if (viewBox) {
547
+ const parts = viewBox.split(/\s+/);
548
+ if (parts.length === 4) {
549
+ width = parseFloat(parts[2]);
550
+ height = parseFloat(parts[3]);
551
+ }
552
+ }
553
+ }
554
+ if (!width || !height) {
555
+ console.warn(`No dimensions found for ${img.id}, using default 24x24`);
556
+ width = 24;
557
+ height = 24;
558
+ }
559
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
560
+ throw new Error(`Invalid dimensions for ${img.id}: ${width}x${height}`);
561
+ }
562
+ if (useCache && cacheKey) {
563
+ svgDimensionsCache.set(cacheKey, { width, height });
564
+ }
565
+ return {
566
+ width: Math.ceil(width * pixelRatio),
567
+ height: Math.ceil(height * pixelRatio)
568
+ };
569
+ } catch (err) {
570
+ throw new Error(`Failed to process SVG ${img.id}: ${err}`);
571
+ }
572
+ }
573
+ return {
574
+ width: Math.ceil(24 * pixelRatio),
575
+ height: Math.ceil(24 * pixelRatio)
576
+ };
577
+ }
578
+ function clearDimensionsCache() {
579
+ svgDimensionsCache.clear();
580
+ }
581
+ async function generateLayout(options) {
582
+ const {
583
+ imgs,
584
+ pixelRatio = 1,
585
+ format = false,
586
+ extractMetadata: shouldExtractMetadata = true
587
+ } = options;
588
+ const imagesWithDims = await Promise.all(
589
+ imgs.map(async (img) => ({
590
+ id: img.id,
591
+ svg: img.svg,
592
+ png: img.png,
593
+ ...await getImageDimensions(img, pixelRatio)
594
+ }))
595
+ );
596
+ const { packed } = packImages(imagesWithDims);
597
+ const layout = {};
598
+ for (const item of packed) {
599
+ const originalImg = imgs.find((i) => i.id === item.id);
600
+ if (format) {
601
+ layout[item.id] = {
602
+ width: item.w,
603
+ height: item.h,
604
+ x: item.x,
605
+ y: item.y,
606
+ pixelRatio
607
+ };
608
+ if (shouldExtractMetadata && originalImg && originalImg.svg && hasMetadata(originalImg.svg)) {
609
+ try {
610
+ const metadata = await extractMetadata(originalImg.svg, pixelRatio);
611
+ Object.assign(layout[item.id], metadata);
612
+ } catch (err) {
613
+ console.warn(`Failed to extract metadata for ${item.id}: ${err}`);
614
+ }
615
+ }
616
+ } else {
617
+ const img = imagesWithDims.find((i) => i.id === item.id);
618
+ if (img) {
619
+ layout[item.id] = {
620
+ width: item.w,
621
+ height: item.h,
622
+ x: item.x,
623
+ y: item.y,
624
+ pixelRatio,
625
+ svg: img.svg,
626
+ png: img.png
627
+ };
628
+ }
629
+ }
630
+ }
631
+ return layout;
632
+ }
633
+ async function generateLayoutUnique(options) {
634
+ const { imgs, pixelRatio = 1, format = false } = options;
635
+ const imgMap = /* @__PURE__ */ new Map();
636
+ for (const img of imgs) {
637
+ const imgString = (img.svg || img.png)?.toString("base64");
638
+ if (!imgString) continue;
639
+ const existing = imgMap.get(imgString);
640
+ if (existing) {
641
+ existing.push(img.id);
642
+ } else {
643
+ imgMap.set(imgString, [img.id]);
644
+ }
645
+ }
646
+ const uniqueImgs = [];
647
+ const idMapping = /* @__PURE__ */ new Map();
648
+ for (const [imgString, ids] of imgMap.entries()) {
649
+ const primaryId = ids[0];
650
+ const buffer = Buffer.from(imgString, "base64");
651
+ const originalImg = imgs.find((i) => i.id === primaryId);
652
+ if (originalImg.svg) {
653
+ uniqueImgs.push({ id: primaryId, svg: buffer });
654
+ } else {
655
+ uniqueImgs.push({ id: primaryId, png: buffer });
656
+ }
657
+ for (const id of ids) {
658
+ idMapping.set(id, primaryId);
659
+ }
660
+ }
661
+ const baseLayout = await generateLayout({
662
+ imgs: uniqueImgs,
663
+ pixelRatio,
664
+ format: false
665
+ });
666
+ const layout = {};
667
+ for (const img of imgs) {
668
+ const primaryId = idMapping.get(img.id);
669
+ if (primaryId && baseLayout[primaryId]) {
670
+ const baseItem = baseLayout[primaryId];
671
+ layout[img.id] = format ? {
672
+ width: baseItem.width,
673
+ height: baseItem.height,
674
+ x: baseItem.x,
675
+ y: baseItem.y,
676
+ pixelRatio
677
+ } : baseItem;
678
+ }
679
+ }
680
+ return layout;
681
+ }
682
+
683
+ // src/core/image.ts
684
+ import sharp from "sharp";
685
+ async function renderImage(imageBuffer, isPNG, ctx, x, y, width, height, pixelRatio) {
686
+ const { Image } = await import("canvas");
687
+ return new Promise((resolve, reject) => {
688
+ const img = new Image();
689
+ img.onload = () => {
690
+ try {
691
+ ctx.drawImage(img, x, y, width, height);
692
+ resolve();
693
+ } catch (error) {
694
+ reject(error);
695
+ }
696
+ };
697
+ img.onerror = (error) => {
698
+ reject(new Error(`Failed to load image: ${error}`));
699
+ };
700
+ if (isPNG) {
701
+ img.src = imageBuffer;
702
+ } else {
703
+ const svgString = imageBuffer.toString("utf8");
704
+ const svgDataUrl = `data:image/svg+xml;base64,${Buffer.from(svgString).toString("base64")}`;
705
+ img.src = svgDataUrl;
706
+ }
707
+ });
708
+ }
709
+ function getCanvasDimensions(layout) {
710
+ let maxX = 0;
711
+ let maxY = 0;
712
+ for (const item of Object.values(layout)) {
713
+ maxX = Math.max(maxX, item.x + item.width);
714
+ maxY = Math.max(maxY, item.y + item.height);
715
+ }
716
+ return {
717
+ width: maxX || 1,
718
+ height: maxY || 1
719
+ };
720
+ }
721
+ async function generateImage(layout) {
722
+ const { createCanvas } = await import("canvas");
723
+ const layoutKeys = Object.keys(layout);
724
+ if (layoutKeys.length === 0) {
725
+ throw new Error("Cannot generate image from empty layout");
726
+ }
727
+ const { width, height } = getCanvasDimensions(layout);
728
+ if (width === 0 || height === 0) {
729
+ throw new Error("Invalid canvas dimensions: width and height must be greater than 0");
730
+ }
731
+ const canvas = createCanvas(width, height);
732
+ const ctx = canvas.getContext("2d");
733
+ ctx.clearRect(0, 0, width, height);
734
+ const renderPromises = [];
735
+ for (const [id, item] of Object.entries(layout)) {
736
+ const svg = item.svg;
737
+ const png = item.png;
738
+ if (!svg && !png) {
739
+ console.warn(`Warning: No image data for '${id}', skipping...`);
740
+ continue;
741
+ }
742
+ const imageBuffer = svg || png;
743
+ const isPNG = !!png;
744
+ const promise = renderImage(
745
+ imageBuffer,
746
+ isPNG,
747
+ ctx,
748
+ item.x,
749
+ item.y,
750
+ item.width,
751
+ item.height,
752
+ item.pixelRatio
753
+ );
754
+ renderPromises.push(promise);
755
+ }
756
+ await Promise.all(renderPromises);
757
+ return canvas.toBuffer("image/png");
758
+ }
759
+ async function generateOptimizedImage(layout, options = {}) {
760
+ const { colors = 256, quality = 80, palette = true } = options;
761
+ const standardPng = await generateImage(layout);
762
+ const optimized = await sharp(standardPng).png({
763
+ palette,
764
+ quality,
765
+ colors,
766
+ compressionLevel: 9,
767
+ // 最高压缩级别
768
+ adaptiveFiltering: true,
769
+ force: true
770
+ }).toBuffer();
771
+ return optimized;
772
+ }
773
+
774
+ // src/utils.ts
775
+ import fs from "fs/promises";
776
+ import path from "path";
777
+ async function getFilePaths(dir) {
778
+ const entries = await fs.readdir(dir);
779
+ return entries.filter((entry) => !entry.startsWith(".") && (entry.endsWith(".svg") || entry.endsWith(".png"))).map((entry) => path.join(dir, entry));
780
+ }
781
+ async function loadFile(filePath) {
782
+ const buffer = await fs.readFile(filePath);
783
+ const ext = path.extname(filePath).toLowerCase();
784
+ const id = path.basename(filePath).replace(/\.(svg|png)$/i, "");
785
+ if (ext === ".svg") {
786
+ return { svg: buffer, id };
787
+ } else if (ext === ".png") {
788
+ return { png: buffer, id };
789
+ } else {
790
+ throw new Error(`Unsupported file format: ${ext}`);
791
+ }
792
+ }
793
+ function sortById(a, b) {
794
+ return b.id < a.id ? 1 : -1;
795
+ }
796
+
797
+ // src/sprite.ts
798
+ async function ensureDir(filePath) {
799
+ const dir = path2.dirname(filePath);
800
+ try {
801
+ await fs2.access(dir);
802
+ } catch {
803
+ await fs2.mkdir(dir, { recursive: true });
804
+ }
805
+ }
806
+ async function saveLayout(outfile, layout, format = true) {
807
+ const jsonPath = `${outfile}.json`;
808
+ await ensureDir(jsonPath);
809
+ const formattedLayout = format ? stringify(layout, { space: " " }) || "{}" : JSON.stringify(layout);
810
+ await fs2.writeFile(jsonPath, formattedLayout, "utf8");
811
+ }
812
+ async function saveImage(outfile, layout) {
813
+ const pngPath = `${outfile}.png`;
814
+ await ensureDir(pngPath);
815
+ const image = await generateImage(layout);
816
+ await fs2.writeFile(pngPath, image);
817
+ }
818
+ async function createLayout(buffers, ratio, format, unique) {
819
+ const genLayout = unique ? generateLayoutUnique : generateLayout;
820
+ return await genLayout({ imgs: buffers, pixelRatio: ratio, format });
821
+ }
822
+ async function generateSprite(options) {
823
+ const { output, input, ratio = 1, unique = false, format = true } = options;
824
+ let outfile = output;
825
+ if (outfile.endsWith("/")) {
826
+ const defaultName = ratio === 1 ? "sprite" : `sprite@${ratio}x`;
827
+ outfile = outfile + defaultName;
828
+ }
829
+ const filePaths = await getFilePaths(input);
830
+ if (filePaths.length === 0) {
831
+ throw new Error(`No SVG or PNG files found in directory: ${input}`);
832
+ }
833
+ const buffers = await Promise.all(filePaths.map((file) => loadFile(file)));
834
+ buffers.sort(sortById);
835
+ const layoutForJson = await createLayout(buffers, ratio, format, unique);
836
+ const layoutForImage = await createLayout(buffers, ratio, false, unique);
837
+ await Promise.all([
838
+ saveLayout(outfile, layoutForJson, format),
839
+ saveImage(outfile, layoutForImage)
840
+ ]);
841
+ return {
842
+ jsonPath: `${outfile}.json`,
843
+ pngPath: `${outfile}.png`,
844
+ layout: layoutForJson,
845
+ imageCount: buffers.length
846
+ };
847
+ }
848
+ async function generateSpriteMultiple(input, output, ratios = [1, 2], unique = false) {
849
+ const results = await Promise.all(
850
+ ratios.map((ratio) => {
851
+ const outputPath = ratio === 1 ? output : `${output}@${ratio}x`;
852
+ return generateSprite({
853
+ output: outputPath,
854
+ input,
855
+ ratio,
856
+ unique
857
+ });
858
+ })
859
+ );
860
+ return results;
861
+ }
862
+ async function generateSpriteFromBuffers(buffers, output, options = {}) {
863
+ const { ratio = 1, unique = false, format = true } = options;
864
+ if (buffers.length === 0) {
865
+ throw new Error("No image buffers provided");
866
+ }
867
+ buffers.sort(sortById);
868
+ let outfile = output;
869
+ if (outfile.endsWith("/")) {
870
+ const defaultName = ratio === 1 ? "sprite" : `sprite@${ratio}x`;
871
+ outfile = outfile + defaultName;
872
+ }
873
+ const layoutForJson = await createLayout(buffers, ratio, format, unique);
874
+ const layoutForImage = await createLayout(buffers, ratio, false, unique);
875
+ await Promise.all([
876
+ saveLayout(outfile, layoutForJson, format),
877
+ saveImage(outfile, layoutForImage)
878
+ ]);
879
+ return {
880
+ jsonPath: `${outfile}.json`,
881
+ pngPath: `${outfile}.png`,
882
+ layout: layoutForJson,
883
+ imageCount: buffers.length
884
+ };
885
+ }
886
+ export {
887
+ clearDimensionsCache,
888
+ extractMetadata,
889
+ generateImage,
890
+ generateLayout,
891
+ generateLayoutUnique,
892
+ generateOptimizedImage,
893
+ generateSprite,
894
+ generateSpriteFromBuffers,
895
+ generateSpriteMultiple,
896
+ getFilePaths,
897
+ hasMetadata,
898
+ loadFile,
899
+ sortById
900
+ };
901
+ //# sourceMappingURL=index.js.map