-
Notifications
You must be signed in to change notification settings - Fork 58
/
ispriter.js
1483 lines (1235 loc) · 43.5 KB
/
ispriter.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
var fs = require('fs'),
path = require('path'),
EventProxy = require('eventproxy'),
us = require('underscore'),
CSSOM = require('cssom'),
PNG = require('pngjs').PNG,
CleanCSS = require('clean-css'),
GrowingPacker = require('./GrowingPacker'),
BI = require('./BackgroundInterpreter'),
nf = require('./node-file'),
zTool = require('./ztool');
//****************************************************************
// 0. 声明和配置一些常量
//****************************************************************
//默认情况下的图片版本号
var DEFAULT_VERSION = Date.now();
var CURRENT_DIR = path.resolve('./');
/**
* 默认配置
* 注意: 所有配置中, 跟路径有关的都必须使用 linux 的目录分隔符 "/", 不能使用 windows 的 "\".
*/
var DEFAULT_CONFIG = {
/**
* 调试时使用, 输出调试日志
*/
"debug": false,
/**
* 精灵图合并算法, 目前只有 growingpacker
*
* @optional
* @default "growingpacker"
*/
"algorithm": "growingpacker",
/**
* 工作目录, 可以是相对路径或者绝对路径
*
* @optional
* @default 运行 ispriter 命令时所在的目录
* @example
* "./": 当前运行目录, 默认值
* "../": 当前目录的上一级
* "/data": 根目录下的 data 目录
* "D:\\sprite": D 盘下的 sprite 目录
*/
"workspace": CURRENT_DIR,
"input": {
/**
* 原 cssRoot
* 需要进行精灵图合并的 css 文件路径或文件列表, 单个时使用字符串, 多个时使用数组.
* 路径可使用 ant 风格的路径写法
*
* @required
* @example
* "cssSource": "../css/";
* "cssSource": ["../css/style.css", "../css2/*.css"]
*/
"cssSource": null,
/**
* 排除不想合并的图片, 可使用通配符
* 也可以直接在 css 文件中, 在不想合并的图片 url 后面添加 #unsprite, iSpriter 会排除该图片, 并把 #unsprite 删除
*
* @optional
* @example
* "ignoreImages": "icons/*"
* "ignoreImages": ["icons/*", "loading.png"]
*/
"ignoreImages": null,
/**
* 输出的精灵图的格式, 目前只支持输出 png 格式,
* 如果是其他格式, 也是以PNG格式输出, 仅仅把后缀改为所指定后缀
*
* @optional
* @default "png"
*/
"format": "png"
},
"output": {
/**
* 原 cssRoot
* 精灵图合并之后, css 文件的输出目录
*
* @optional
* @default "./sprite/css/"
*/
"cssDist": "./sprite/css/",
/**
* 原 imageRoot
* 生成的精灵图相对于 cssDist 的路径, 最终会变成合并后的的图片路径写在 css 文件中
*
* @optional
* @default "./img/"
* @example
* 如果指定 imageDist 为 "./images/sprite/", 则在输出的 css 中会显示为
* background: url("./images/sprite/sprite_1.png");
*
*/
"imageDist": "./img/",
/**
* 原 maxSize
* 单个精灵图的最大大小, 单位 KB,
* 如果合并之后图片的大小超过 maxSingleSize, 则会对图片进行拆分
*
* @optional
* @default 0
* @example
* 如指定 "maxSingleSize": 60, 而生成的精灵图(sprite_all.png)的容量为 80KB,
* 则会把精灵图拆分为 sprite_0.png 和 sprite_1.png 两张
*
*/
"maxSingleSize": 0,
/**
* 合成之后, 图片间的空隙, 单位 px
*
* @optional
* @default 0
*/
"margin": 0,
/**
* 配置生成的精灵图的前缀
*
* @optional
* @default "sprite_"
*/
"prefix": "sprite_",
/**
* 精灵图的输出格式
*
* @optional
* @default "png"
*/
"format": "png",
/**
* 配置是否要将所有精灵图合并成为一张, 当有很多 css 文件输入的时候可以使用.
* 为 true 时将所有图片合并为一张, 同时所有 css 文件合并为一个文件.
* 注意: 此时 maxSingleSize 仍然生效, 超过限制时也会进行图片拆分
*
* @optional
* @default false
*/
"combine": false,
/**
* 配置是否把合并了图片的样式整合成一条规则, 统一设置 background-image, 例如:
* .cls1, .cls2{
* background-image: url(xxx);
* }
*
* @optional
* @default true
*/
"combineCSSRule": true,
/**
* 配置是否压缩 css 文件, 将使用 clean-css 进行压缩, 其值如下:
* false: 不进行压缩;
* true: 用 clean-css 的默认配置进行压缩;
* Object{"keepBreaks": true, ... }: 用指定的配置进行压缩.
*
* @optional
* @default false
*/
"compress": false,
/**
* 配置是否把没有合并的图片进行拷贝, 默认是不不会进行拷贝的
*
* @optional
* @default false
*/
"copyUnspriteImage": false
}
};
var DEFAULT_COMBINE_CSS_NAME = 'all.css';
function debug(msg) {
if (spriteConfig.debug) {
console.log('>>>', +new Date, msg, '\n<<<===================');
}
}
function info(msg) {
console.info.apply(console, arguments);
}
//****************************************************************
// 1. 读取配置
// 把传入的配置(最简配置或者完整配置等)进行适配和整理
//****************************************************************
/**
* 读取配置, 支持config 为配置文件名或者为配置对象
*
* @param {Object|String} config 配置文件或者配置对象
* @return {Config} 读取并解析完成的配置对象
*/
function readConfig(config) {
if (us.isString(config)) {
if (!fs.existsSync(config)) {
throw 'place give in a sprite config or config file!';
}
var content = fs.readFileSync(config).toString();
config = zTool.jsonParse(content);
} else if (us.isArray(config)) {
config = {
input: config
};
}
config = config || {};
// 适配最简配置
if (us.isString(config.input) || us.isArray(config.input)) {
config.input = {
cssSource: config.input
};
}
if (!config.output) {
config.output = {};
} else if (us.isString(config.output)) {
config.output = {
cssDist: config.output
}
}
// 对旧的配置项进行兼容
config = adjustOldProperty(config);
//
config = zTool.merge({}, DEFAULT_CONFIG, config);
var cssSource = config.input.cssSource;
if (!cssSource) {
throw 'there is no cssSource specific!';
} else if (us.isString(cssSource)) {
cssSource = [cssSource];
}
// 读取所有指定的 css 文件
var cssFiles = [],
cssPattern, queryResult;
for (var i = 0; i < cssSource.length; i++) {
cssPattern = path.normalize(cssSource[i]).replace(/\\/g, '\\\\');
if (zTool.endsWith(cssPattern, path.sep)) {
cssPattern += '*.css';
} else if (!zTool.endsWith(cssPattern, '.css')) {
cssPattern += '/*.css';
}
queryResult = nf.query(config.workspace, cssPattern);
cssFiles = cssFiles.concat(queryResult);
}
if (!cssFiles.length) {
throw 'there is no any css file contain!';
}
// 去重
cssFiles = us.unique(cssFiles);
config.input.cssSource = cssFiles;
// 解析要排除的图片规则
var ignoreImages = config.input.ignoreImages;
if (ignoreImages) {
if (!us.isArray(ignoreImages)) {
ignoreImages = [ignoreImages];
}
ignoreImages.forEach(function(pattern, i) {
ignoreImages[i] = zTool.wildcardToPattern(pattern);
});
}
// 确保输出路径是个目录
if (!zTool.endsWith(config.output.cssDist, '/')) {
config.output.cssDist += '/';
}
config.output.cssDist = path.normalize(config.output.cssDist);
if (!zTool.endsWith(config.output.imageDist, '/')) {
config.output.imageDist += '/';
}
// KB 换算成 B
config.output.maxSingleSize *= 1024;
// 确保 margin 是整数
config.output.margin = parseInt(config.output.margin);
// debug(config);
return config;
}
/**
* 对旧的配置项做兼容
* @param {Config} config
* @return {Config}
*/
function adjustOldProperty(config) {
if (!config.input.cssSource && 'cssRoot' in config.input) {
config.input.cssSource = config.input.cssRoot;
delete config.input.cssRoot;
}
if (!config.output.cssDist && 'cssRoot' in config.output) {
config.output.cssDist = config.output.cssRoot;
delete config.output.cssRoot;
}
if (!config.output.imageDist && 'imageRoot' in config.output) {
config.output.imageDist = config.output.imageRoot;
delete config.output.imageRoot;
}
if (!config.output.maxSingleSize && 'maxSize' in config.output) {
config.output.maxSingleSize = config.output.maxSize;
delete config.output.maxSize;
}
return config;
}
//****************************************************************
// 2. CSS 样式处理
//****************************************************************
/**
* 读取并解析样式表文件
* @return {CSSStyleSheet}
* @example
* CSSStyleSheet: {
* cssRules: [
* { // CSSStyleDeclaration
* selectorText: "img",
* style: {
* 0: "border",
* length: 1,
* border: "none"
* }
* }
* ]
* }
*/
function readStyleSheet(fileName) {
fileName = path.join(spriteConfig.workspace, fileName);
if (!fs.existsSync(fileName)) {
return null;
}
var content = fs.readFileSync(fileName);
var styleSheet = CSSOM.parse(content.toString());
return styleSheet;
};
/**
* CSS Style Declaration 的通用方法定义
* @type {Object}
* @example
* CSSStyleDeclaration: {
* 0: "border",
* 1: "color",
* length: 2,
* border: "none",
* color: "#333"
* }
*/
var BaseCSSStyleDeclaration = {
/**
* 把background 属性拆分
* e.g. background: #fff url('...') repeat-x 0px top;
*/
splitBackground: function() {
var background,
value;
if (!this['background']) {
// 有 background 属性的 style 才能拆分 background
return;
}
// 撕裂 background-position
if (value = this['background-position']) {
value = value.trim().replace(/\s{2}/g, '').split(' ');
if (!value[1]) {
value[1] = value[0];
}
this['background-position-x'] = value[0];
this['background-position-y'] = value[1];
}
background = BI.analyse(this['background']);
if (background.length != 1) {
// FIXME 暂时跳过多背景的属性
return;
}
background = background[0];
if (background['background-image']) {
// 把原来缩写的 background 属性删掉
this.removeProperty('background');
this.extend(background);
}
},
/**
* 把 style 里面的 background 属性转换成简写形式, 用于减少代码
*/
mergeBackgound: function() {
var background = '',
style = this;
if(style.getPropertyValue('background')) {
return;
}
var positionText = this.removeProperty('background-position-x') + ' ' +
this.removeProperty('background-position-y');
style.setProperty('background-position', positionText.trim(), null);
var toMergeAttrs = [
'background-color', 'background-image', 'background-position',
'background-repeat', 'background-attachment',
'background-origin', 'background-clip'
];
for (var i = 0, item; item = toMergeAttrs[i]; i++) {
if (style.hasOwnProperty(item)) {
background += this.removeProperty(item) + ' ';
}
}
if(background.trim()) {
style.setProperty('background', background.trim(), null);
}
},
/**
* 把 obj 的属性和属性值扩展合并过来, 并调整下标, 方法将被忽略
* @param {Object} obj
* @param {Boolean} override 是否覆盖已有属性
*/
extend: function(obj, override) {
for (var i in obj) {
if (us.isFunction(obj[i])) {
continue;
} else if (this[i] && !override) {
continue;
}
this.setProperty(i, obj[i], null);
}
}
}
/**
* 所用到的一些正则
*/
var regexp = {
ignoreNetwork: /^(https?|ftp):\/\//i,
ignorePosition: /right|center|bottom/i,
ignoreRepeat: /^(repeat-x|repeat-y|repeat)$/i,
image: /\(['"]?(.+\.(png|jpg|jpeg|gif|bmp))((\?|#).*?)?['"]?\)/i,
css: /(.+\.css).*/i,
ignoreImage: /#unsprite\b/i
}
/**
* 收集需要合并的样式和图片
* @param {CSSStyleSheet} styleSheet
* @param {Object} result StyleObjList
* @return {Object}
* @example
* result: { // StyleObjList
* length: 1,
* "./img/icon1.png": { // StyleObj
* imageUrl: "./img/icon1.png",
* imageAbsUrl: "./img/icon1.png", //相对于 workspace 的路径
* cssRules: []
* }
* }
*/
function collectStyleRules(styleSheet, result, styleSheetUrl) {
if (!result) {
result = { // an StyleObjList
length: 0
}
}
if (!styleSheet.cssRules.length) {
return result;
}
var styleSheetDir = path.dirname(styleSheetUrl);
// 遍历所有 css 规则收集进行图片合并的样式规则
styleSheet.cssRules.forEach(function(rule, i) {
// typeof rule === 'CSSStyleRule'
if (rule.href && rule.styleSheet) {
// @import 引入的样式表, 把 css 文件读取进来继续处理
var fileName = rule.href;
// 忽略掉链接到网络上的文件
if (!fileName || regexp.ignoreNetwork.test(fileName)) {
return;
}
var match = fileName.match(regexp.css);
if (!match) {
return;
}
fileName = match[1];
var url = path.join(styleSheetDir, fileName);
var styleSheet = readStyleSheet(url);
debug('read import style: ' + url + ' , has styleSheet == : ' + !!styleSheet);
if (!styleSheet) {
return;
}
rule.styleSheet = styleSheet;
debug('collect import style: ' + fileName);
// 继续收集 import 的样式
collectStyleRules(styleSheet, result, url);
return;
}
if (rule.cssRules && rule.cssRules.length) {
if (rule instanceof CSSOM.CSSKeyframesRule) {
return; // FIXME 先跳过 keyframes 的属性
}
// 遇到有子样式的,比如 @media, 递归收集
collectStyleRules(rule, result, styleSheetUrl);
return;
}
if (!rule.style) {
// 有可能 @media 等中没有任何样式, 如: @media xxx {}
return;
}
/*
* typeof style === 'CSSStyleDeclaration'
* 给 style 对象扩展基本的方法
*/
var style = us.extend(rule.style, BaseCSSStyleDeclaration);
if (style['background-size']) {
/*
* 跳过有 background-size 的样式, 因为:
* 1. background-size 不能简写在 background 里面, 而拆分 background 之后再组装,
* background 就变成在 background-size 后面了, 会导致 background-size 被 background 覆盖;
* 2. 拥有 background-size 的背景图片一般都涉及到拉伸, 这类图片是不能合并的
*/
return;
}
if (style['background']) {
// 有 background 属性的 style 就先把 background 简写拆分出来
style.splitBackground();
} else if (style['background-position']) {
var value = style['background-position'];
value = value.trim().replace(/\s{2}/g, '').split(' ');
if (!value[1]) {
value[1] = value[0];
}
style.setProperty('background-position-x', value[0]);
style.setProperty('background-position-y', value[1]);
}
if (regexp.ignorePosition.test(style['background-position-x']) ||
regexp.ignorePosition.test(style['background-position-y'])) {
/*
* background 定位是 right center bottom 的图片不合并
* 因为这三个的定位方式比较特殊, 浏览器有个自动适应的特性
* 把刚刚拆分的 background 属性合并并返回
*/
style.mergeBackgound();
return;
}
if (regexp.ignoreRepeat.test(style['background-repeat']) ||
regexp.ignoreRepeat.test(style['background-repeat-x']) ||
regexp.ignoreRepeat.test(style['background-repeat-y'])) {
// 显式的使用了平铺的图片, 也不进行合并
style.mergeBackgound();
return;
}
var imageUrl = getImageUrl(style, styleSheetDir),
imageAbsUrl,
fileName;
if (imageUrl) {
imageAbsUrl = path.join(styleSheetDir, imageUrl);
fileName = path.join(spriteConfig.workspace, imageAbsUrl);
if (!fs.existsSync(fileName)) {
// 如果这个图片是不存在的, 就直接返回了, 进行容错
info('>>Skip: "' + fileName + '" is not exist');
return;
}
// 把用了同一个文件的样式汇集在一起
if (!result[imageUrl]) {
result[imageUrl] = { // an StyleObj
imageUrl: imageUrl,
imageAbsUrl: imageAbsUrl,
cssRules: []
};
result.length++;
}
result[imageUrl].cssRules.push(style);
}
else{
//图片找不到css_backgound合并还原 20150405
style.mergeBackgound();
}
});
return result;
}
/**
* 从background-image 的值中提取图片的路径
* @return {String} url
*/
function getImageUrl(style, dir) {
var format = spriteConfig.input.format,
ignoreImages = spriteConfig.input.ignoreImages,
backgroundImage = style['background-image'],
url = null,
ext,
match;
if (!backgroundImage) {
return null;
}
if (backgroundImage.indexOf(',') > -1) {
// FIXME 暂时忽略掉多背景的属性
// FIXME 提取 url 进行拷贝
return null;
}
match = backgroundImage.match(regexp.image);
if (match) {
url = match[1];
ext = match[2];
if (format.indexOf(ext) == -1) { // 去掉非指定后缀的图片
unspriteImageArray.push(path.join(dir, url));
return null;
}
if (regexp.ignoreImage.test(backgroundImage)) { // 去掉不需要合并图片
unspriteImageArray.push(path.join(dir, url));
info('>>Skip: Unsprite image "' + url + '"');
url = backgroundImage.replace(regexp.ignoreImage, '');
style.setProperty('background-image', url, null);
return null;
}
} else {
debug('not match image bg: ' + backgroundImage);
return null;
}
// 遇到网络图片就跳过
if (regexp.ignoreNetwork.test(url)) {
// 这里直接返回了, 因为一个style里面是不会同时存在两个 background-image 的
info('>>Skip: Network image "' + url + '"');
return null;
}
if (ignoreImages) {
for (var i = 0; i < ignoreImages.length; i++) {
if (ignoreImages[i].test(url)) {
info('>>Skip: Unsprite image "' + url + '"');
return null;
}
}
}
return url;
}
//****************************************************************
// 3. 收集图片相关信息
//****************************************************************
/**
* 读取图片的内容和大小
* @param {StyleObjList} styleObjList
* @param {Function} onDone
*/
function readImagesInfo(styleObjList, onDone) {
// pngjs 没有提供同步 api, 所以只能用异步的方式读取图片信息
zTool.forEach(styleObjList, function(styleObj, url, next) {
if (url === 'length') {
return next(); // 跳过 styleObjList 的 length 字段
}
var imageInfo = imageInfoCache[url];
var onGetImageInfo = function(imageInfo) {
if (imageInfo) {
imageInfoCache[url] = imageInfo;
// 从所有style里面,选取图片宽高最大的作为图片宽高
setImageWidthHeight(styleObj, imageInfo);
styleObj.imageInfo = imageInfo;
} else { // 没有读取到图片信息, 可能是图片签名或格式不对, 读取出错了
delete imageInfoCache[url];
delete styleObjList[url];
styleObj.cssRules.forEach(function(style) {
style.mergeBackgound();
});
}
next();
}
if (imageInfo) {
onGetImageInfo(imageInfo);
} else {
readImageInfo(styleObj.imageAbsUrl, onGetImageInfo);
}
}, onDone);
}
/**
* 读取单个图片的内容和信息
* @param {String} fileName
* @param {Function} callback callback(ImageInfo)
* { // ImageInfo
* image: null, // 图片数据
* width: 0,
* height: 0,
* size: 0 // 图片数据的大小
* }
*/
function readImageInfo(fileName, callback) {
fileName = path.join(spriteConfig.workspace, fileName);
fs.createReadStream(fileName).pipe(new PNG())
.on('parsed', function() {
var imageInfo = {
image: this,
width: this.width,
height: this.height
};
getImageSize(this, function(size) {
imageInfo.size = size;
callback(imageInfo);
});
})
.on('error', function(e) {
info('>>Skip: ' + e.message + ' of "' + fileName + '"');
callback(null);
});
}
/**
* 读取图片内容所占硬盘空间的大小
* @param {PNG} image
* @param {Function} callback callback(Number)
*/
function getImageSize(image, callback) {
var size = 0;
/*
* 这里读取图片大小的范式比较折腾, pngjs 没有提供直接获取 size 的通用方法,
* 同时它只提供了文件流的方式读取, 所以只能一段一段的读取数据时把长度相加
*/
image.pack().on('data', function(chunk) {
size += chunk.length;
}).on('end', function() {
callback(size);
});
}
/**
* 把用了同一个图片的样式里写的大小 (with, height) 跟图片的大小相比较, 取最大值,
* 防止有些样式写的宽高比较大, 导致合并后显示到了周边的图片内容
* @param {StyleObj} styleObj
* @param {ImageInfo} imageInfo
*/
function setImageWidthHeight(styleObj, imageInfo) {
var w = 0,
h = 0,
mw = imageInfo.width,
mh = imageInfo.height;
// 遍历所有规则, 取最大值
styleObj.cssRules.forEach(function(style) {
w = getPxValue(style.width),
h = getPxValue(style.height);
// TODO 这一步有必要么? // 没有设置宽高的样式, 用图片的大小来设置
// if(!style.hasOwnProperty('width')){
// style.setProperty('width', imageInfo.width + 'px', null);
// }
// if(!style.hasOwnProperty('height')){
// style.setProperty('height', imageInfo.height + 'px', null);
// }
if (w > mw) {
mw = w;
}
if (h > mh) {
mh = h;
}
});
/*
* 最后的大小还要加上 config 中配置的 margin 值
* 这里之所以用 w / h 来表示宽高, 而不是用 with / height
* 是因为 packer 算法限定死了, 值读取传入元素的 w / h 值
*/
styleObj.w = mw + spriteConfig.output.margin;
styleObj.h = mh + spriteConfig.output.margin;
}
/**
* 把像素值转换成数字, 如果没有该值则设置为 0,
* 非 px 的值会忽略, 当成 0 来处理
* @param {String} cssValue
*/
function getPxValue(cssValue) {
if (cssValue && cssValue.indexOf('px') > -1) {
return parseInt(cssValue);
}
return 0;
}
//****************************************************************
// 4. 对图片进行坐标定位
//****************************************************************
/**
* 对需要合并的图片进行布局定位
* @param {StyleObjList} styleObjList
* @return {Array} 返回 spriteArrayay 的数组,
* SpriteImageArray 的每个元素都是 StyleObj 数组,
* 一个 StyleObj 数组包含了一张精灵图的所有小图片
*/
function positionImages(styleObjList) {
var styleObj,
spriteArray = [],
arr = [],
existArr = [], // 保存已经合并过的图片的样式
maxSize = spriteConfig.output.maxSingleSize,
packer = new GrowingPacker();
// 把已经合并了并已输出的图片先排除掉
for (var i in styleObjList) {
if (i === 'length') {
continue;
}
styleObj = styleObjList[i];
if (styleObj.imageInfo.drew) {
existArr.push(styleObj);
} else {
arr.push(styleObj);
}
}
// 如果限制了输出图片的大小, 则进行分组
if (maxSize) {
/*
* 限制图片大小的算法是:
* 1. 先把图片按从大到小排序
* 2. 顺序叠加图片 size , 超过maxSize 时, 另起一个数组
* 3. 最终把一个数组, 拆成 N 个 总 szie 小于 maxSize 的数组
*/
arr.sort(function(a, b) {
return b.imageInfo.size - a.imageInfo.size;
});
var total = 0,
ret = [];
arr.forEach(function(styleObj) {
total += styleObj.imageInfo.size;
if (total > maxSize) {
if (ret.length) { // 避免出现空图片
spriteArray.push(ret);
ret = [];
total = styleObj.imageInfo.size;
}
}
ret.push(styleObj);
});
if (ret.length) {
spriteArray.push(ret);
}
} else {
spriteArray.push(arr);
}
spriteArray.forEach(function(arr) {
/*
* packer 算法需要把最大的一个放在首位...
* 排序算法会对结果造成比较大的影响
*/
arr.sort(function(a, b) {
return b.w * b.h - a.w * a.h;
});
// 用 packer 对数组元素进行定位
packer.fit(arr);
/*
* root 的值就是 packer 定位的结果
* root.w / root.h 表示图片排列后的总宽高
* 各个小图片的坐标这在 arr 的元素中, 新增了一个 fit 属性
* fit.x / fit.y 表示定位后元素的坐标
*/
arr.root = packer.root;
});
if (existArr.length) {
spriteArray.push(existArr);
}
return spriteArray;
}
//****************************************************************
// 5. 根据定位合并图片并输出, 同时修改样式表里面的background
//****************************************************************
function drawImageAndPositionBackground(spriteTask, callback) {
var combinedCssRules,
spriteArray = spriteTask.spriteArray,
fileversion = spriteTask.fileversion;
// 保存用了同一张精灵图的选择器, 用于最后输出 css 文件的时候统一设置 background-image
combinedCssRules = {
// './sprite_output/sprite_1.png': {imageName: '', selectors: []}
};
spriteTask.combinedCssRules = combinedCssRules;
if (!spriteArray[spriteArray.length - 1].root) {
/*
* 若最后一个元素, 没有root 属性, 说明它的样式都是复用已合并的图片的,
* 直接替换样式即可
*/
var styleObjArr = spriteArray.pop();
styleObjArr.forEach(function(styleObj) {
var imageInfo = styleObj.imageInfo,
imageName = imageInfo.imageName;
styleObj.fit = imageInfo.fit;
if (!combinedCssRules[imageName]) {
combinedCssRules[imageName] = [];
}
// 修改 background 属性