it('should return a stream with two sprite objects and the appropriate nameing of sprites', function (done) { var count = 0; opts.dimension = [{ratio: 1, dpi: 72}, {ratio: 2, dpi: 192}]; mockLayouts.name = 'first'; var l2 = layout('top-down'); l2.addItem({height: 50, width: 50, meta: {}}); var mockLayouts2 = { name: 'second', layout: mockLayout.export() }; os.fromArray([mockLayouts, mockLayouts2]) .pipe(sprite(opts)) .pipe(spy(function (res) { res.sprites.length.should.equal(2); if (count === 0) { res.should.have.deep.property('sprites[0].name', 'sprite-first'); res.should.have.deep.property('sprites[1].name', 'sprite-first@2x'); } if (count === 1) { res.should.have.deep.property('sprites[0].name', 'sprite-second'); res.should.have.deep.property('sprites[1].name', 'sprite-second@2x'); } count++; })) .on('data', noop) .on('finish', function () { count.should.equal(2); done(); }); });
beforeEach(function () { opts = { name: 'sprite', base64: false, format: 'png', cssPath: '../images', engine: mockedImageProcessor, dimension: [{ratio: 1, dpi: 72}], logger: { log: noop, warn: noop, debug: noop, error: noop, success: noop } }; mockLayout = layout('top-down'); mockLayout.addItem({ height: 50, width: 50, meta: { width: 42, height: 42, x: 0, y: 0, type: 'png', offset: 4 } }); mockLayouts = { name: 'default', layout: mockLayout.export() }; });
constructor(algorithm = "binary-tree", background = "transparent", margin = 4) { this.layer = layout(algorithm, { sort: false, // todo support sort }); this.tiles = []; this.background = background; this.margin = margin; }
appendCreateImageRequests(images, requests) { const layer = boxLayout('left-right'); // TODO - Configurable? for(let image of images) { debug(`Slide #${this.slide.index}: adding inline image ${image.url}`); layer.addItem({ width: image.width + image.padding * 2, height: image.height + image.padding * 2, meta: image}); } const box = this.getBodyBoundingBox(false); const computedLayout = layer.export(); let scaleRatio = Math.min( box.width / computedLayout.width, box.height / computedLayout.height); let scaledWidth = computedLayout.width * scaleRatio; let scaledHeight = computedLayout.height * scaleRatio; let translateX = box.x + ((box.width - scaledWidth) / 2); let translateY = box.y + ((box.height - scaledHeight) / 2); for(let item of computedLayout.items) { let width = item.meta.width * scaleRatio; let height = item.meta.height * scaleRatio; requests.push({ createImage: { elementProperties: { pageObjectId: this.slide.objectId, size: { height: { magnitude: height, unit: 'EMU' }, width: { magnitude: width, unit: 'EMU' } }, transform: { scaleX: 1, scaleY: 1, translateX: translateX + (item.x + item.meta.padding) * scaleRatio, translateY: translateY + (item.y + item.meta.padding) * scaleRatio, shearX: 0, shearY: 0, unit: 'EMU' } }, url: item.meta.url } }); } }
layout: function() { var positions = Layout( this.composers.length, this.container.getBoundingClientRect() ); // Applying positions using CSS `left` and `top`. this.composers.forEach(function ( composer, index ) { var pos = positions[index]; composer.element.style.left = pos.left + "px"; composer.element.style.top = pos.top + "px"; }); }
beforeEach(function () { opts = { 'style-indent-char': 'space', 'style-indent-size': 2, 'processor': 'css', 'style': 'style', 'logger': { log: noop, warn: noop, debug: noop, error: noop, success: noop } }; var l = layout('top-down'); l.addItem({height: 50, width: 50, meta: {}}); layouts = { name: 'default', classname: 'icon', layout: l.export(), sprites: [{ name: 'sprite', url: '../images/sprite.png', type: 'png', dpi: null, ratio: null, width: 50, height: 300 }, { name: 'sprite@1.5x', url: '../images/sprite@1.5x.png', type: 'png', dpi: 144, ratio: 1.5, width: 50, height: 300 }, { name: 'sprite@2x', url: '../images/sprite@2x.png', type: 'png', dpi: 192, ratio: 2, width: 50, height: 300 }] }; });
function updateLayer() { var layoutOrientation = opts.orientation === 'vertical' ? 'top-down' : opts.orientation === 'horizontal' ? 'left-right' : 'binary-tree'; function appendLayer(list, layer) { _.forEach(list, function(image) { layer.addItem({ 'height': image.height, 'width': image.width, 'meta': { buffer: image.buffer, path: image.path, baseDir: image.baseDir, baseName: image.baseName, dirPath: image.dirPath, name: image.name } }); }); } var images = getImages(opts); var singleList = [], multiLists = []; images.forEach(function(value, key) { singleList = singleList.concat(value); multiLists[key] = layout(layoutOrientation); appendLayer(value, multiLists[key]); multiLists[key] = multiLists[key].export(); }); var singleLayer = layout(layoutOrientation); appendLayer(singleList, singleLayer); switch (opts.bundleMode) { case opts.oneBundle: return singleLayer.export(); break; case opts.multipleBundle: return multiLists; default: return singleLayer.export(); } }
module.exports = function (opt) { opt = lodash.extend({}, {name: 'sprite', format: 'png', margin: 4, processor: 'css', cssPath: '../images', orientation: 'vertical', sort: true, interpolation: 'grid'}, opt); opt.styleExtension = (opt.processor === 'stylus') ? 'styl' : opt.processor; var layoutOrientation = opt.orientation === 'vertical' ? 'top-down' : opt.orientation === 'horizontal' ? 'left-right' : 'binary-tree'; var layer = layout(layoutOrientation, {'sort': opt.sort}); if (opt.opacity === 0 && opt.format === 'jpg') { opt.opacity = 1; } var color = new Color(opt.background); opt.color = color.rgbArray(); opt.color.push(opt.opacity); function queue (file, img) { var spriteName = replaceExtension(file.relative, '').replace(/\/|\\|\ /g, '-'); layer.addItem({ height: img.height() + 2 * opt.margin, width: img.width() + 2 * opt.margin, meta: { name: spriteName, img: img, image: opt.cssPath + '/' + opt.name } }); } function queueImages (file, enc, cb) { if (file.isNull()) { cb(); return; // ignore } if (file.isStream()) { cb(new Error('Streaming not supported')); return; // ignore } if (imageinfo(file.contents)) { lwip.open(file.contents, imageinfo(file.contents).format.toLowerCase(), function(err, img) { if (!err) { queue(file, img); cb(); } else { console.log('Ignoring ' + file.relative + ' -> ' + err.toString()); cb(); } }); } else { console.log('Ignoring ' + file.relative + ' -> no image info'); cb(); } } function createCanvas (layerInfo, cb) { lwip.create(layerInfo.width, layerInfo.height, opt.color, function (err, image) { async.eachSeries(layerInfo.items, function (sprite, callback) { image.paste(sprite.x + opt.margin, sprite.y + opt.margin, sprite.meta.img, callback); }, function () { cb(image); }); }); } function createNonRetinaCanvas (retinaCanvas, cb) { var width = Math.floor(retinaCanvas.width() / 2); var height = Math.floor(retinaCanvas.height() / 2); retinaCanvas.clone(function(err, clone){ // tell lwip to use the 'grid' interpolation method when resizing - it makes the resized image look much better clone.resize(width, height, opt.interpolation, function (err, image) { cb(image); }); }); } function createStyle (layerInfo, sprite, retinaSprite) { var sprites = []; lodash.forEach(layerInfo.items, function (sprite) { sprites.push({ 'name': sprite.meta.name, 'x': sprite.x + opt.margin, 'y': sprite.y + opt.margin, 'width': sprite.width - opt.margin * 2, 'height': sprite.height - opt.margin * 2, 'total_width': layerInfo.width, 'total_height': layerInfo.height, 'image': sprite.meta.image }); }); var cachebuster = ''; if (opt.cachebuster === 'random') { cachebuster = '?' + crypto.randomBytes(20).toString('hex'); } if (retinaSprite) { sprites.unshift({ name: retinaSprite.relative, type: 'retina', image: (!opt.base64) ? url.resolve(opt.cssPath.replace(/\\/g, '/'), retinaSprite.relative) + cachebuster : 'data:' + imageinfo(retinaSprite.buffer).mimeType + ';base64,' + retinaSprite.buffer.toString('base64'), total_width: sprite.canvas.width(), total_height: sprite.canvas.height() }); lodash.forEach(sprites, function (sprite, i) { sprites[i].x = Math.floor(sprite.x / 2); sprites[i].y = Math.floor(sprite.y / 2); sprites[i].width = Math.floor(sprite.width / 2); sprites[i].height = Math.floor(sprite.height / 2); }); } sprites.unshift({ name: sprite.relative, type: 'sprite', image: (!opt.base64) ? url.resolve(opt.cssPath.replace(/\\/g, '/'), sprite.relative) + cachebuster : 'data:' + imageinfo(sprite.buffer).mimeType + ';base64,' + sprite.buffer.toString('base64'), total_width: sprite.canvas.width, total_height: sprite.canvas.height }); return json2css(sprites, {'format': 'sprite', formatOpts: {'cssClass': opt.prefix, 'processor': opt.processor, 'template': opt.template}}); } function createSprite (cb) { var _this = this; var layerInfo = layer.export(); var sprite, nonRetinaSprite, style; if (layerInfo.items.length === 0) { cb(); return; // ignore } async.waterfall([ function (callback) { createCanvas(layerInfo, function (canvas) { sprite = { base: opt.out, relative: opt.name + '.' + opt.format, path: path.join(opt.out, opt.name + '.' + opt.format), canvas: canvas }; callback(null, sprite); }); }, function (sprite, callback) { if (opt.retina) { sprite.path = replaceExtension(sprite.path, '') + '@2x.' + opt.format; sprite.relative = replaceExtension(sprite.relative, '') + '@2x.' + opt.format; createNonRetinaCanvas(sprite.canvas, function (canvas) { nonRetinaSprite = { base: opt.out, relative: opt.name + '.' + opt.format, path: path.join(opt.out, opt.name + '.' + opt.format), canvas: canvas }; callback(null, sprite, nonRetinaSprite); }); } else { callback(null, sprite, null); } }, function (sprite, nonRetinaSprite, callback) { if (nonRetinaSprite) { nonRetinaSprite.canvas.toBuffer(opt.format, {}, function (err, buf) { nonRetinaSprite.buffer = buf; callback(null, sprite, nonRetinaSprite); }); } else { callback(null, sprite, nonRetinaSprite); } }, function (sprite, nonRetinaSprite, callback) { sprite.canvas.toBuffer(opt.format, {}, function (err, buf) { sprite.buffer = buf; callback(null, sprite, nonRetinaSprite); }); }, function (sprite, nonRetinaSprite, callback) { if (!opt.base64) { if (nonRetinaSprite) { _this.push(new File({ base: nonRetinaSprite.base, path: nonRetinaSprite.path, contents: nonRetinaSprite.buffer })); } _this.push(new File({ base: sprite.base, path: sprite.path, contents: sprite.buffer })); } callback(null, sprite, nonRetinaSprite); }, function (sprite, nonRetinaSprite, callback) { if (opt.style || opt.base64) { style = opt.retina ? createStyle(layerInfo, nonRetinaSprite, sprite) : createStyle(layerInfo, sprite); _this.push(new File({ base: !opt.base64 ? path.dirname(opt.style) : opt.out, path: opt.style ? opt.style : path.join(opt.out, replaceExtension(opt.name, '.' + opt.styleExtension)), contents: new Buffer(style) })); } callback(null); } ], function () { cb(); }); } return through2.obj(queueImages, createSprite); };
return through2.obj(function (css, enc, callback) { var color = new Color(opt.background) opt.color = color.rgbArray() opt.color.push(opt.opacity) var layoutOrientation = opt.orientation === 'vertical' ? 'top-down' : opt.orientation === 'horizontal' ? 'left-right' : 'binary-tree' var layer = layout(layoutOrientation, {'sort': opt.sort}) var layer2x = layout(layoutOrientation, {'sort': opt.sort}) var layer3x = layout(layoutOrientation, {'sort': opt.sort}) var cssContent var originCssContent var images = [] var image1x = null var image2x = null var image3x = null RegExp.escape = function (s) { return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') } //保存CSS名称 var cssBaseName = path.basename(css.path, '.css') var _this = this async.waterfall([ //找出需要进行合并的图片 function (cb) { originCssContent = cssContent = css.contents.toString() var sliceRegex = new RegExp('background-image:[\\s]*url\\(["\']?(?!http[s]?|/)[^)]*?(' + opt.slicePath + '/[\\w\\d\\s!./\\-\\_@]*\\.[\\w?#]+)["\']?\\)\\s?(\\!important)?[^;]*?', 'ig') var codelines = cssContent.match(sliceRegex) if (!codelines || codelines.length === 0) { _this.push(new File({ base: opt.cssOut, path: path.join(opt.cssOut, cssBaseName + ".css"), contents: new Buffer(cssContent) })) return callback(null) } var index = opt.slicePath.lastIndexOf('/') if (index === -1) { opt.spriteOut = 'sprite/' } else { opt.spriteOut = opt.slicePath.substring(0, index + 1) + 'sprite/' } async.eachSeries(codelines, function (backgroundCodeLine, eachCb) { // 统一为无引号 var fixedQuoteReg = new RegExp('url\\(\\\\?["\']{1}(' + opt.slicePath.replace(/\./g, '\\.') + '\\/[^\\\\"\']*)\\\\?["\']{1}\\)', 'i') var m = backgroundCodeLine.match(fixedQuoteReg); var backgroundCodeLineFixedQuote = backgroundCodeLine; if(m && m.length) { backgroundCodeLineFixedQuote = 'background-image: url(' + m[1] + ')' cssContent = cssContent.split(backgroundCodeLine).join(backgroundCodeLineFixedQuote) } var relativePath = backgroundCodeLine.replace(sliceRegex, '$1') var absolutePath = path.join(path.dirname(css.path), relativePath) if (lodash.includes(images, absolutePath)) { return eachCb() } images.push(absolutePath) var meta = { backgroundCodeLine: backgroundCodeLineFixedQuote,//匹配出的代码行内容 fileName: path.basename(relativePath),//文件名 relativePath: relativePath,//相对路径 absolutePath: absolutePath,//绝对路径 has2x: fs.existsSync(absolutePath.replace('.png', '@2x.png')),//是否有 @2x 图 absolute2xPath: absolutePath.replace('.png', '@2x.png'), has3x: fs.existsSync(absolutePath.replace('.png', '@3x.png')),//是否有 @3x 图 absolute3xPath: absolutePath.replace('.png', '@3x.png'), margin: opt.margin } //如果直接引用 @2x 图 if (backgroundCodeLine.indexOf('@2x') > 0) { meta.has2x = true meta.absolute2xPath = absolutePath if (absolutePath.indexOf('@2x') > 0) { meta.has3x = fs.existsSync(absolutePath.replace('@2x.png', '@3x.png')) meta.absolute3xPath = absolutePath.replace('@2x.png', '@3x.png') } } //如果直接引用 @3x 图 if (backgroundCodeLine.indexOf('@3x') > 0) { meta.has3x = true meta.absolute3xPath = absolutePath if (absolutePath.indexOf('@3x')) { meta.has2x = fs.existsSync(absolutePath.replace('@3x.png', '@2x.png')) meta.absolute2xPath = absolutePath.replace('@3x.png', '@2x.png') } } //通过正则,匹配出 className var regexClassNameString = '(\\.?[^}]*?)\\s?{\\s?[^}]*?' + RegExp.escape(backgroundCodeLine) var regexClassName = new RegExp(regexClassNameString, 'ig') var classNameResult = cssContent.match(regexClassName) meta.className2x = [] meta.className3x = [] async.series([ function (next) { layerAddItem(layer, absolutePath, meta, next) }, function (next) { if (meta.has2x) { for (var key in classNameResult) { meta.className2x.push(classNameResult[key].replace(regexClassName, '$1')) } meta.margin = opt.margin * 2 layerAddItem(layer2x, meta.absolute2xPath, meta, next) } else { next() } }, function (next) { if (meta.has3x) { for (var key in classNameResult) { meta.className3x.push(classNameResult[key].replace(regexClassName, '$1')) } meta.margin = opt.margin * 3 layerAddItem(layer3x, meta.absolute3xPath, meta, next) } else { next() } } ], function () { eachCb() }) }, function () { cb(null, cssContent) }) }, //雪碧图布局排列 //CSS替换 function (cssContent, cb) { var layerInfo = layer.export() if (layerInfo.items.length > 0) { lwip.create(layerInfo.width, layerInfo.height, opt.color, function (err, image) { async.eachSeries(layerInfo.items, function (sprite, callback) { //图片合并 image.paste(sprite.x + sprite.meta.margin, sprite.y + sprite.meta.margin, sprite.meta.img, callback) //CSS替换 var code = 'background-image: url("' + opt.spriteOut + cssBaseName + '.png");' code += ' background-position: -' + (sprite.x + sprite.meta.margin) + 'px -' + (sprite.y + sprite.meta.margin) + 'px;' cssContent = cssContent.split(sprite.meta.backgroundCodeLine).join(code) }, function () { image1x = image cb(null, cssContent) }) }) } else { cb(null, cssContent) } }, //2x function (cssContent, cb) { if (typeof cssContent === 'function') { cssContent = originCssContent } var retinaLayerInfo = layer2x.export() //存在 @2x 图 if (retinaLayerInfo.items.length > 0) { var retinaCssContent = '\n\n@media only screen and (-webkit-min-device-pixel-ratio: 2),only screen and (min--moz-device-pixel-ratio: 2),only screen and (min-resolution: 240dpi) {' lwip.create(retinaLayerInfo.width, retinaLayerInfo.height, opt.color, function (err, image) { async.eachSeries(retinaLayerInfo.items, function (sprite, callback) { //图片合并 image.paste(sprite.x + sprite.meta.margin, sprite.y + sprite.meta.margin, sprite.meta.img, callback) cssContent = cssContent.split(sprite.meta.backgroundCodeLine).join(''); //添加 media query lodash.each(sprite.meta.className2x, function (item) { retinaCssContent += item retinaCssContent += '{background-image:url("' + opt.spriteOut + cssBaseName + '@2x.png");' retinaCssContent += '-webkit-background-size:' + retinaLayerInfo.width / 2 + 'px;' retinaCssContent += 'background-size:' + retinaLayerInfo.width / 2 + 'px;' retinaCssContent += 'background-position: -' + ((sprite.x + sprite.meta.margin) / 2) + 'px -' + ((sprite.y + sprite.meta.margin) / 2) + 'px;}' }) }, function () { retinaCssContent += "}" cssContent += retinaCssContent image2x = image cb(null, cssContent) }) }) } else { if (image1x) { cb(null, cssContent) } else { cb(null) } } }, //@3x function (cssContent, cb) { if (typeof cssContent === 'function') { cssContent = originCssContent } var retinaLayerInfo = layer3x.export() //存在 @3x 图 if (retinaLayerInfo.items.length > 0) { var retinaCssContent = '\n\n@media only screen and (min-device-width: 414px) and (-webkit-min-device-pixel-ratio: 3) {' lwip.create(retinaLayerInfo.width, retinaLayerInfo.height, opt.color, function (err, image) { async.eachSeries(retinaLayerInfo.items, function (sprite, callback) { //图片合并 image.paste(sprite.x + sprite.meta.margin, sprite.y + sprite.meta.margin, sprite.meta.img, callback) cssContent = cssContent.replace(sprite.meta.backgroundCodeLine, '') //添加 media query lodash.each(sprite.meta.className3x, function (item) { retinaCssContent += item retinaCssContent += '{background-image:url("' + opt.spriteOut + cssBaseName + '@3x.png");' retinaCssContent += '-webkit-background-size:' + retinaLayerInfo.width / 3 + 'px;' retinaCssContent += 'background-size:' + retinaLayerInfo.width / 3 + 'px;' retinaCssContent += 'background-position: -' + ((sprite.x + sprite.meta.margin) / 3) + 'px -' + ((sprite.y + sprite.meta.margin) / 3) + 'px;}' }) }, function () { retinaCssContent += "}" cssContent += retinaCssContent image3x = image cb(null, cssContent) }) }) } else { if (image2x || image1x) { cb(null, cssContent) } else { cb(null) } } }, // png to buffer // css to buffer function (cssContent, cb) { _this.push(new File({ base: opt.cssOut, path: path.join(opt.cssOut, cssBaseName + ".css"), contents: new Buffer(cssContent) })) async.series([ function (next) { if (image1x) { image1x.toBuffer('png', {}, function (err, spriteBuffer) { _this.push(new File({ base: opt.spriteOut, path: path.join(opt.spriteOut, cssBaseName + '.png'), contents: spriteBuffer })) next() }) } else { next() } }, function (next) { if (image2x) { image2x.toBuffer('png', {}, function (err, retinaSpriteBuffer) { _this.push(new File({ base: opt.spriteOut, path: path.join(opt.spriteOut, cssBaseName + '@2x.png'), contents: retinaSpriteBuffer })) next() }) } else { next() } }, function (next) { if (image3x) { image3x.toBuffer('png', {}, function (err, retinaSpriteBuffer) { _this.push(new File({ base: opt.spriteOut, path: path.join(opt.spriteOut, cssBaseName + '@3x.png'), contents: retinaSpriteBuffer })) next() }) } else { next() } } ], function () { cb(null) }) } ], function () { callback() }) })
images.forEach(function(value, key) { singleList = singleList.concat(value); multiLists[key] = layout(layoutOrientation); appendLayer(value, multiLists[key]); multiLists[key] = multiLists[key].export(); });