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