Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

assetgraph-sprite

Package Overview
Dependencies
Maintainers
2
Versions
46
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

assetgraph-sprite - npm Package Compare versions

Comparing version 3.0.0 to 3.0.1

36

lib/packers/horizontal.js

@@ -1,19 +0,19 @@

exports.pack = function (imageInfos) {
let previousRightPadding = 0;
const packingData = {
width: 0,
height: 0,
imageInfos: []
};
for (const existingImageInfo of imageInfos) {
const imageInfo = {...existingImageInfo};
packingData.width += Math.max(previousRightPadding, imageInfo.padding[3]);
imageInfo.x = packingData.width;
imageInfo.y = 0;
packingData.width += imageInfo.width;
previousRightPadding = imageInfo.padding[1];
packingData.height = Math.max(packingData.height, imageInfo.height);
packingData.imageInfos.push(imageInfo);
}
return packingData;
exports.pack = imageInfos => {
let previousRightPadding = 0;
const packingData = {
width: 0,
height: 0,
imageInfos: []
};
for (const existingImageInfo of imageInfos) {
const imageInfo = { ...existingImageInfo };
packingData.width += Math.max(previousRightPadding, imageInfo.padding[3]);
imageInfo.x = packingData.width;
imageInfo.y = 0;
packingData.width += imageInfo.width;
previousRightPadding = imageInfo.padding[1];
packingData.height = Math.max(packingData.height, imageInfo.height);
packingData.imageInfos.push(imageInfo);
}
return packingData;
};
// Install getters for all packers in this directory:
for (const fileName of require('fs').readdirSync(__dirname)) {
if (/\.js$/.test(fileName) && fileName !== 'index.js') {
Object.defineProperty(exports, fileName.replace(/\.js$/, ''), {
get: () => require('./' + fileName)
});
}
if (/\.js$/.test(fileName) && fileName !== 'index.js') {
Object.defineProperty(exports, fileName.replace(/\.js$/, ''), {
get: () => require('./' + fileName)
});
}
}

@@ -11,90 +11,96 @@ /*

function findCoords(node, width, height) {
// If we are not at a leaf then go deeper
if (node.lft) {
// Check first the left branch if not found then go by the right
return findCoords(node.lft, width, height) || findCoords(node.rgt, width, height);
} else {
// If already used or it's too big then return
if (node.used || width > node.width || height > node.height) {
return;
}
// If we are not at a leaf then go deeper
if (node.lft) {
// Check first the left branch if not found then go by the right
return (
findCoords(node.lft, width, height) || findCoords(node.rgt, width, height)
);
} else {
// If already used or it's too big then return
if (node.used || width > node.width || height > node.height) {
return;
}
// If it fits perfectly then use this gap
if (width === node.width && height === node.height) {
node.used = true;
return {
x: node.x,
y: node.y
};
}
}
// If it fits perfectly then use this gap
if (width === node.width && height === node.height) {
node.used = true;
return {
x: node.x,
y: node.y
};
}
// Partition vertically or horizontally:
if (node.width - width > node.height - height) {
node.lft = {
x: node.x,
y: node.y,
width: width,
height: node.height
};
node.rgt = {
x: node.x + width,
y: node.y,
width: node.width - width,
height: node.height
};
} else {
node.lft = {
x: node.x,
y: node.y,
width: node.width,
height: height
};
node.rgt = {
x: node.x,
y: node.y + height,
width: node.width,
height: node.height - height
};
}
return findCoords(node.lft, width, height);
// Partition vertically or horizontally:
if (node.width - width > node.height - height) {
node.lft = {
x: node.x,
y: node.y,
width,
height: node.height
};
node.rgt = {
x: node.x + width,
y: node.y,
width: node.width - width,
height: node.height
};
} else {
node.lft = {
x: node.x,
y: node.y,
width: node.width,
height
};
node.rgt = {
x: node.x,
y: node.y + height,
width: node.width,
height: node.height - height
};
}
return findCoords(node.lft, width, height);
}
exports.pack = function (imageInfos, config) {
config = config || {};
const root = {
x: 0,
y: 0,
width: config.maxWidth || 999999,
height: config.maxHeight || 999999
};
exports.pack = (imageInfos, config) => {
config = config || {};
const root = {
x: 0,
y: 0,
width: config.maxWidth || 999999,
height: config.maxHeight || 999999
};
// Sort by area, descending:
imageInfos.sort(
(a, b) => (b.width * b.height) - (a.width * a.height)
);
// Sort by area, descending:
imageInfos.sort((a, b) => b.width * b.height - a.width * a.height);
const packingData = {
imageInfos: [],
width: 0,
height: 0
};
const packingData = {
imageInfos: [],
width: 0,
height: 0
};
for (const existingImageInfo of imageInfos) {
const imageInfo = {...existingImageInfo};
if (imageInfo.padding && imageInfo.padding.some(v => v > 0)) {
throw new Error('jimScott.pack: Sprite padding not supported');
}
// Perform the search
const coords = findCoords(root, imageInfo.width, imageInfo.height);
// If fitted then recalculate the used dimensions
if (coords) {
packingData.width = Math.max(packingData.width, coords.x + imageInfo.width);
packingData.height = Math.max(packingData.height, coords.y + imageInfo.height);
} else {
throw new Error('jimScott.pack: Cannot fit image');
}
Object.assign(imageInfo, coords);
packingData.imageInfos.push(imageInfo);
for (const existingImageInfo of imageInfos) {
const imageInfo = { ...existingImageInfo };
if (imageInfo.padding && imageInfo.padding.some(v => v > 0)) {
throw new Error('jimScott.pack: Sprite padding not supported');
}
return packingData;
// Perform the search
const coords = findCoords(root, imageInfo.width, imageInfo.height);
// If fitted then recalculate the used dimensions
if (coords) {
packingData.width = Math.max(
packingData.width,
coords.x + imageInfo.width
);
packingData.height = Math.max(
packingData.height,
coords.y + imageInfo.height
);
} else {
throw new Error('jimScott.pack: Cannot fit image');
}
Object.assign(imageInfo, coords);
packingData.imageInfos.push(imageInfo);
}
return packingData;
};
const packers = ['./horizontal', './vertical', './jimScott'].map(require);
exports.pack = imageInfos => {
let bestPacking;
for (const packer of packers) {
let packing;
try {
packing = packer.pack(imageInfos);
} catch (e) {
// The Jim Scott packer doesn't support sprite padding, just skip to the next packer if we get an exception.
continue;
}
if (!bestPacking || (packing.width * packing.height) < (bestPacking.width * bestPacking.height)) {
bestPacking = packing;
}
let bestPacking;
for (const packer of packers) {
let packing;
try {
packing = packer.pack(imageInfos);
} catch (e) {
// The Jim Scott packer doesn't support sprite padding, just skip to the next packer if we get an exception.
continue;
}
return bestPacking;
if (
!bestPacking ||
packing.width * packing.height < bestPacking.width * bestPacking.height
) {
bestPacking = packing;
}
}
return bestPacking;
};

@@ -1,20 +0,20 @@

exports.pack = function (imageInfos) {
let previousBottomPadding = 0;
const packingData = {
width: 0,
height: 0,
imageInfos: []
};
exports.pack = imageInfos => {
let previousBottomPadding = 0;
const packingData = {
width: 0,
height: 0,
imageInfos: []
};
for (const existingImageInfo of imageInfos) {
const imageInfo = {...existingImageInfo};
packingData.height += Math.max(previousBottomPadding, imageInfo.padding[0]);
imageInfo.y = packingData.height;
imageInfo.x = 0;
packingData.height += imageInfo.height;
previousBottomPadding = imageInfo.padding[2];
packingData.width = Math.max(packingData.width, imageInfo.width);
packingData.imageInfos.push(imageInfo);
}
return packingData;
for (const existingImageInfo of imageInfos) {
const imageInfo = { ...existingImageInfo };
packingData.height += Math.max(previousBottomPadding, imageInfo.padding[0]);
imageInfo.y = packingData.height;
imageInfo.x = 0;
packingData.height += imageInfo.height;
previousBottomPadding = imageInfo.padding[2];
packingData.width = Math.max(packingData.width, imageInfo.width);
packingData.imageInfos.push(imageInfo);
}
return packingData;
};
const queryString = require('querystring');
const { promisify } = require('util');
const packers = require('./packers');
let Canvas;
const { Canvas, Image } = require('canvas-prebuilt');
try {
Canvas = require('canvas');
} catch (e) {}
// Helper for extracting all nodes defining a specific property from a postcss rule
function getProperties(container, propertyName) {
return container.nodes.filter(
node => node.prop === propertyName
);
return container.nodes.filter(node => node.prop === propertyName);
}
async function getCanvasImageFromImageAsset(imageAsset) {
const canvasImage = new Canvas.Image();
await new Promise((resolve, reject) => {
canvasImage.onerror = err => {
err.message += ' (' + imageAsset.urlOrDescription + ')';
reject(err);
};
canvasImage.onload = resolve;
canvasImage.src = imageAsset.rawSrc;
});
return canvasImage;
const canvasImage = new Image();
await new Promise((resolve, reject) => {
canvasImage.onerror = err => {
if (err.message.includes('node-canvas was built without SVG support')) {
err.message = 'Adding SVG images to a sprite is not possible';
}
err.message += ` (${imageAsset.urlOrDescription})`;
reject(err);
};
canvasImage.onload = resolve;
canvasImage.src = imageAsset.rawSrc;
});
return canvasImage;
}
async function getImageAssetFromCanvas(canvas, assetType, assetGraph) {
if (assetType === 'Png') {
const rawSrc = await promisify(cb => canvas.toBuffer(cb))();
return {
type: 'Png',
rawSrc
};
} else {
const rawSrc = await promisify(cb => {
const jpegChunks = [];
canvas.createJPEGStream().on('data', function (chunk) {
jpegChunks.push(chunk);
})
.on('end', () => cb(null, Buffer.concat(jpegChunks)))
.on('error', cb);
})();
return {
type: 'Jpeg',
rawSrc
};
}
if (assetType === 'Png') {
const rawSrc = await promisify(cb => canvas.toBuffer(cb))();
return {
type: 'Png',
rawSrc
};
} else {
const rawSrc = await promisify(cb => {
const jpegChunks = [];
canvas
.createJPEGStream()
.on('data', chunk => {
jpegChunks.push(chunk);
})
.on('end', () => cb(null, Buffer.concat(jpegChunks)))
.on('error', cb);
})();
return {
type: 'Jpeg',
rawSrc
};
}
}
function calculateSpritePadding(paddingStr, asset) {
var padding;
if (paddingStr) {
// Strip units ('px' assumed)
var tokens = [];
paddingStr.split(/[,+]|\s+/).forEach(function (token) {
var num = parseInt(token.replace(/[a-z]+$/, ''), 10);
if (!isNaN(num)) {
tokens.push(num);
}
});
if (tokens.length === 4) {
padding = tokens;
} else if (tokens.length === 3) {
padding = [tokens[0], tokens[1], tokens[2], tokens[1]]; // T, L+R, B
} else if (tokens.length === 2) {
padding = [tokens[0], tokens[1], tokens[0], tokens[1]]; // T+B, L+R
} else if (tokens.length === 1) {
padding = [tokens[0], tokens[0], tokens[0], tokens[0]];
}
} else {
padding = [0, 0, 0, 0];
let padding;
if (paddingStr) {
// Strip units ('px' assumed)
const tokens = [];
paddingStr.split(/[,+]|\s+/).forEach(token => {
const num = parseInt(token.replace(/[a-z]+$/, ''), 10);
if (!isNaN(num)) {
tokens.push(num);
}
});
if (tokens.length === 4) {
padding = tokens;
} else if (tokens.length === 3) {
padding = [tokens[0], tokens[1], tokens[2], tokens[1]]; // T, L+R, B
} else if (tokens.length === 2) {
padding = [tokens[0], tokens[1], tokens[0], tokens[1]]; // T+B, L+R
} else if (tokens.length === 1) {
padding = [tokens[0], tokens[0], tokens[0], tokens[0]];
}
} else {
padding = [0, 0, 0, 0];
}
return padding.map(
size => Math.max(size, Math.max(asset.devicePixelRatio) - 1)
);
return padding.map(size =>
Math.max(size, Math.max(asset.devicePixelRatio) - 1)
);
}
function getRelationSpriteInfoFromIncomingRelation(incomingRelation) {
const parsedQueryString = queryString.parse(incomingRelation.href.match(/\?([^#]*)/)[1]);
return {
groupName: parsedQueryString.sprite || 'default',
noGroup: 'spriteNoGroup' in parsedQueryString,
padding: calculateSpritePadding(parsedQueryString.padding, incomingRelation.to),
asset: incomingRelation.to
};
const parsedQueryString = queryString.parse(
incomingRelation.href.match(/\?([^#]*)/)[1]
);
return {
groupName: parsedQueryString.sprite || 'default',
noGroup: 'spriteNoGroup' in parsedQueryString,
padding: calculateSpritePadding(
parsedQueryString.padding,
incomingRelation.to
),
asset: incomingRelation.to
};
}
function extractInfoFromCssRule(cssRule, propertyNamePrefix) {
const info = {};
cssRule.walkDecls(decl => {
if (!propertyNamePrefix || decl.prop.startsWith(propertyNamePrefix)) {
const keyName = decl.prop
.substr(propertyNamePrefix.length)
.replace(
/-([a-z])/g, ($0, $1) => $1.toUpperCase()
);
info[keyName] = decl.value.replace(/^([\'\"])(.*)\1$/, '$2');
}
});
return info;
const info = {};
cssRule.walkDecls(decl => {
if (!propertyNamePrefix || decl.prop.startsWith(propertyNamePrefix)) {
const keyName = decl.prop
.substr(propertyNamePrefix.length)
.replace(/-([a-z])/g, ($0, $1) => $1.toUpperCase());
info[keyName] = decl.value.replace(/^(['"])(.*)\1$/, '$2');
}
});
return info;
}
module.exports = () =>
async function spriteBackgroundImages(assetGraph) {
const spriteGroups = {};
module.exports = () => {
return async function spriteBackgroundImages(assetGraph) {
if (!Canvas) {
assetGraph.warn(new Error('assetgraph-sprite: Canvas not found, skipping'));
return;
// Find sprite annotated images and create a data structure with their information
for (const relation of assetGraph.findRelations({
type: 'CssImage',
to: { isImage: true },
href: /\?(?:|[^#]*&)sprite(?:[=&#]|$)/
})) {
const relationSpriteInfo = getRelationSpriteInfoFromIncomingRelation(
relation
);
const spriteGroup = (spriteGroups[
relationSpriteInfo.groupName
] = spriteGroups[relationSpriteInfo.groupName] || {
imageInfosById: {}
});
const imageInfo = spriteGroup.imageInfosById[relationSpriteInfo.asset.id];
if (!imageInfo) {
relationSpriteInfo.incomingRelations = [relation];
spriteGroup.imageInfosById[
relationSpriteInfo.asset.id
] = relationSpriteInfo;
} else {
imageInfo.incomingRelations.push(relation);
for (var i = 0; i < 4; i += 1) {
imageInfo.padding[i] = Math.max(
relationSpriteInfo.padding[i],
imageInfo.padding[i]
);
}
}
}
// Waiting for https://github.com/LearnBoost/node-canvas/issues/52
const cairoVersion = Canvas.cairoVersion.split('.').map(
str => parseInt(str, 10)
);
if (cairoVersion[0] < 1 || cairoVersion[1] < 10) {
assetGraph.warn(new Error('assetgraph-sprite: Cannot create sprites due to missing canvas.getContext("2d").drawImage() support. Please compile node-canvas with Cairo version 1.10.0 or above.'));
return;
const redefinitionErrors = {};
// Extract sprite grouping information va -sprite- prefixed properties in stylesheets
for (const cssAsset of assetGraph.findAssets({
type: 'Css',
isLoaded: true
})) {
cssAsset.eachRuleInParseTree(cssRule => {
if (cssRule.type !== 'rule') {
return;
}
if (getProperties(cssRule, '-sprite-selector-for-group').length > 0) {
const spriteInfo = extractInfoFromCssRule(cssRule, '-sprite-');
const spriteGroupName = spriteInfo.selectorForGroup;
if (spriteInfo.location) {
const matchLocation = spriteInfo.location.match(
/^url\((['"]|)(.*?)\1\)$/
);
if (matchLocation) {
spriteInfo.location = matchLocation[2];
}
}
const group = spriteGroups[spriteGroupName];
if (group) {
if (!Array.isArray(group.placeHolders)) {
group.placeHolders = [];
}
const spriteGroups = {};
if (group.placeHolders.length > 0) {
let err;
// Find sprite annotated images and create a data structure with their information
for (const relation of assetGraph.findRelations({type: 'CssImage', to: {isImage: true}, href: /\?(?:|[^#]*&)sprite(?:[=&#]|$)/})) {
const relationSpriteInfo = getRelationSpriteInfoFromIncomingRelation(relation);
const spriteGroup = (spriteGroups[relationSpriteInfo.groupName] = spriteGroups[relationSpriteInfo.groupName] || {
imageInfosById: {}
});
const imageInfo = spriteGroup.imageInfosById[relationSpriteInfo.asset.id];
if (!imageInfo) {
relationSpriteInfo.incomingRelations = [relation];
spriteGroup.imageInfosById[relationSpriteInfo.asset.id] = relationSpriteInfo;
if (
Object.keys(group.placeHolders[0]).every(key => {
if (['asset', 'cssRule'].includes(key)) {
return true;
}
return group.placeHolders[0][key] === spriteInfo[key];
})
) {
// Queue up these errors as they tend to come in quite big bunches
if (!Array.isArray(redefinitionErrors[spriteGroupName])) {
redefinitionErrors[spriteGroupName] = [];
}
redefinitionErrors[spriteGroupName].push(cssAsset);
group.placeHolders.push({
...spriteInfo,
asset: cssAsset,
cssRule
});
} else {
err = new Error(
`assetgraph-sprite: Multiple differing definitions of ${spriteGroupName} sprite.\nThis is most likely an error.`
);
err.asset = cssAsset;
assetGraph.warn(err);
}
} else {
imageInfo.incomingRelations.push(relation);
for (var i = 0 ; i < 4 ; i += 1) {
imageInfo.padding[i] = Math.max(relationSpriteInfo.padding[i], imageInfo.padding[i]);
}
group.placeHolders.push({
...spriteInfo,
asset: cssAsset,
cssRule
});
}
}
}
});
}
const redefinitionErrors = {};
for (const spriteGroupName of Object.keys(redefinitionErrors)) {
const message = [
`assetgraph-sprite: Multiple identical definitions of ${spriteGroupName} sprite.`,
'This might happen if you duplicate CSS using a preprocessor.',
...redefinitionErrors[spriteGroupName].map(
asset => ' ' + asset.urlOrDescription
)
].join('\n');
// Extract sprite grouping information va -sprite- prefixed properties in stylesheets
for (const cssAsset of assetGraph.findAssets({type: 'Css', isLoaded: true})) {
cssAsset.eachRuleInParseTree(cssRule => {
if (cssRule.type !== 'rule') {
return;
}
if (getProperties(cssRule, '-sprite-selector-for-group').length > 0) {
const spriteInfo = extractInfoFromCssRule(cssRule, '-sprite-');
const spriteGroupName = spriteInfo.selectorForGroup;
if (spriteInfo.location) {
const matchLocation = spriteInfo.location.match(/^url\((['"]|)(.*?)\1\)$/);
if (matchLocation) {
spriteInfo.location = matchLocation[2];
}
}
const group = spriteGroups[spriteGroupName];
if (group) {
if (!Array.isArray(group.placeHolders)) {
group.placeHolders = [];
}
const err = new Error(message);
if (group.placeHolders.length > 0) {
let err;
assetGraph.info(err);
}
if (Object.keys(group.placeHolders[0]).every(key => {
if (['asset', 'cssRule'].includes(key)) {
return true;
}
return group.placeHolders[0][key] === spriteInfo[key];
})) {
// Queue up these errors as they tend to come in quite big bunches
if (!Array.isArray(redefinitionErrors[spriteGroupName])) {
redefinitionErrors[spriteGroupName] = [];
}
redefinitionErrors[spriteGroupName].push(cssAsset);
for (const spriteGroupName of Object.keys(spriteGroups)) {
const spriteGroup = spriteGroups[spriteGroupName];
let imageInfos = Object.values(spriteGroup.imageInfosById);
const spriteInfo =
(spriteGroup.placeHolders && spriteGroup.placeHolders[0]) || {};
group.placeHolders.push({
...spriteInfo,
asset: cssAsset,
cssRule
});
} else {
err = new Error(`assetgraph-sprite: Multiple differing definitions of ${spriteGroupName} sprite.\nThis is most likely an error.`);
err.asset = cssAsset;
const canvasImages = await Promise.all(
imageInfos.map(imageInfo =>
getCanvasImageFromImageAsset(imageInfo.asset)
)
);
for (const [i, imageInfo] of imageInfos.entries()) {
const canvasImage = canvasImages[i];
Object.assign(imageInfo, {
canvasImage,
width: canvasImage.width,
height: canvasImage.height
});
}
assetGraph.warn(err);
}
const packerName =
{
'jim-scott': 'jimScott',
horizontal: 'horizontal',
vertical: 'vertical'
}[spriteInfo.packer] || 'tryAll';
} else {
group.placeHolders.push({
...spriteInfo,
asset: cssAsset,
cssRule
});
}
const packingData = packers[packerName].pack(imageInfos);
const canvas = new Canvas(packingData.width, packingData.height);
const ctx = canvas.getContext('2d');
}
}
});
}
if ('backgroundColor' in spriteInfo) {
ctx.fillStyle = spriteInfo.backgroundColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
for (const spriteGroupName of Object.keys(redefinitionErrors)) {
const message = [
`assetgraph-sprite: Multiple identical definitions of ${spriteGroupName} sprite.`,
'This might happen if you duplicate CSS using a preprocessor.',
...redefinitionErrors[spriteGroupName].map(
asset => ' ' + asset.urlOrDescription
)
].join('\n');
imageInfos = packingData.imageInfos;
for (const imageInfo of imageInfos) {
ctx.drawImage(
imageInfo.canvasImage,
imageInfo.x,
imageInfo.y,
imageInfo.width,
imageInfo.height
);
}
const spriteImageType = /^jpe?g$/.test(spriteInfo.imageFormat)
? 'Jpeg'
: 'Png';
const spriteAssetConfig = await getImageAssetFromCanvas(
canvas,
spriteImageType,
assetGraph
);
const err = new Error(message);
assetGraph.info(err);
let fileName = `sprite-${spriteGroupName}-${imageInfos.length}${
assetGraph[spriteImageType].prototype.defaultExtension
}`;
if (Array.isArray(spriteGroup.placeHolders)) {
let location = spriteGroup.placeHolders[0].location;
if (location) {
let href;
if (/^\?/.test(location)) {
href = fileName + location;
} else {
href = location;
}
spriteAssetConfig.url = assetGraph.resolveUrl(assetGraph.root, href);
}
}
if (!spriteAssetConfig.url) {
spriteAssetConfig.fileName = fileName;
}
for (const spriteGroupName of Object.keys(spriteGroups)) {
const spriteGroup = spriteGroups[spriteGroupName];
let imageInfos = Object.values(spriteGroup.imageInfosById);
const spriteInfo = spriteGroup.placeHolders && spriteGroup.placeHolders[0] || {};
const canvasImages = await Promise.all(imageInfos.map(
imageInfo => getCanvasImageFromImageAsset(imageInfo.asset)
));
for (const [i, imageInfo] of imageInfos.entries()) {
const canvasImage = canvasImages[i];
Object.assign(imageInfo, {
canvasImage,
width: canvasImage.width,
height: canvasImage.height
});
const spriteAsset = assetGraph.addAsset(spriteAssetConfig);
if (Array.isArray(spriteGroup.placeHolders)) {
for (const spriteInfo of spriteGroup.placeHolders) {
const cssRule = spriteInfo.cssRule;
let propertyName;
let propertyNode;
let tokenNumber;
for (const candidatePropertyName of [
'background-image',
'background'
]) {
const decls = getProperties(cssRule, candidatePropertyName);
if (!propertyName && decls.length > 0) {
propertyName = candidatePropertyName;
let propertyValue = decls[0].value;
if (propertyValue === '!important') {
// Hack so that an existing value of "!important" will DTRT
decls[0].important = true;
propertyValue = 'url(...)';
} else if (/^\s*$/.test(propertyValue)) {
propertyValue = 'url(...)';
} else {
const existingUrlTokens = propertyValue.match(
assetGraph.CssImage.prototype.tokenRegExp
);
if (existingUrlTokens) {
tokenNumber = existingUrlTokens.length;
}
propertyValue += ' url(...)';
}
decls[0].value = propertyValue;
}
}
if (propertyName) {
propertyNode = getProperties(cssRule, propertyName)[0];
} else {
cssRule.append('background-image: url(...)');
propertyNode = cssRule.last;
}
// I can't see why the ordering of CssImage relations should be significant...
const relation = spriteInfo.asset.addRelation(
{
type: 'CssImage',
node: cssRule,
to: spriteAsset,
propertyName,
propertyNode,
tokenNumber
},
'last'
);
relation.refreshHref();
spriteInfo.cssRule.walkDecls(decl => {
if (
[
'-sprite-selector-for-group',
'-sprite-packer',
'-sprite-image-format',
'-sprite-background-color',
'-sprite-important'
].includes(decl.prop)
) {
decl.remove();
}
});
const packerName = {
'jim-scott': 'jimScott',
horizontal: 'horizontal',
vertical: 'vertical'
}[spriteInfo.packer] || 'tryAll';
// If background-size is set, we should update it, The correct size is now the size of the sprite:
const backgroundSizeDecls = getProperties(
spriteInfo.cssRule,
'background-size'
);
if (backgroundSizeDecls.length > 0) {
backgroundSizeDecls[0].value =
packingData.width + 'px ' + packingData.height + 'px';
}
}
}
const packingData = packers[packerName].pack(imageInfos);
const canvas = new Canvas(packingData.width, packingData.height);
const ctx = canvas.getContext('2d');
if ('backgroundColor' in spriteInfo) {
ctx.fillStyle = spriteInfo.backgroundColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
for (const imageInfo of imageInfos) {
for (const incomingRelation of imageInfo.incomingRelations) {
incomingRelation.from.markDirty();
const relationSpriteInfo = getRelationSpriteInfoFromIncomingRelation(
incomingRelation
);
const node = incomingRelation.node;
const existingBackgroundPositionDecls = getProperties(
node,
'background-position'
);
const existingBackgroundDecls = getProperties(node, 'background');
const offsets = [
Math.round(imageInfo.x / imageInfo.asset.devicePixelRatio), // FIXME: Rounding issues?
Math.round(imageInfo.y / imageInfo.asset.devicePixelRatio)
];
let backgroundOffsetsWereUpdated = false;
let existingOffsets;
if (existingBackgroundDecls.length > 0) {
// Warn if there's more than one?
const backgroundTokens = existingBackgroundDecls[0].value.split(
/\s+/
);
const positionTokenIndices = [];
existingOffsets = [];
for (const [
i,
existingBackgroundValueToken
] of backgroundTokens.entries()) {
if (/^(?:-?\d+px|0)$/i.test(existingBackgroundValueToken)) {
positionTokenIndices.push(i);
existingOffsets.push(
parseInt(existingBackgroundValueToken, 10)
);
}
}
if (existingOffsets.length === 2) {
// Patch up the existing background property by replacing the old offsets with corrected ones:
for (let [i, offset] of offsets.entries()) {
offset -= existingOffsets[i];
backgroundTokens.splice(
positionTokenIndices[i],
1,
offset ? -offset + 'px' : '0'
);
}
imageInfos = packingData.imageInfos;
for (const imageInfo of imageInfos) {
ctx.drawImage(
imageInfo.canvasImage,
imageInfo.x,
imageInfo.y,
imageInfo.width,
imageInfo.height
);
existingBackgroundDecls[0].value = backgroundTokens.join(' ');
backgroundOffsetsWereUpdated = true;
}
const spriteImageType = /^jpe?g$/.test(spriteInfo.imageFormat) ? 'Jpeg' : 'Png';
const spriteAssetConfig = await getImageAssetFromCanvas(canvas, spriteImageType, assetGraph);
}
let fileName = `sprite-${spriteGroupName}-${imageInfos.length}${assetGraph[spriteImageType].prototype.defaultExtension}`;
if (Array.isArray(spriteGroup.placeHolders)) {
let location = spriteGroup.placeHolders[0].location;
if (location) {
let href;
if (/^\?/.test(location)) {
href = fileName + location;
} else {
href = location;
}
spriteAssetConfig.url = assetGraph.resolveUrl(
assetGraph.root,
href
);
if (!backgroundOffsetsWereUpdated) {
// There was no 'background' property, or it didn't contain something that looked like offsets.
// Create or update the background-position property instead:
let backgroundPositionImportant = false;
if (existingBackgroundPositionDecls.length === 1) {
// FIXME: Silently ignores other units than px
backgroundPositionImportant =
existingBackgroundPositionDecls[0].value === '!important' ||
existingBackgroundPositionDecls[0].important;
if (existingBackgroundPositionDecls[0].value !== '!important') {
existingOffsets = existingBackgroundPositionDecls[0].value
.split(' ')
.map(item => parseInt(item, 10));
if (
existingOffsets.length !== 2 ||
isNaN(existingOffsets[0]) ||
isNaN(existingOffsets[1])
) {
const err = new Error(
'WARNING: trying to sprite ' +
imageInfo.asset.url +
' with background-position: ' +
existingBackgroundPositionDecls[0].value
);
assetGraph.warn(err);
} else {
offsets[0] -= existingOffsets[0];
offsets[1] -= existingOffsets[1];
}
}
}
if (!spriteAssetConfig.url) {
spriteAssetConfig.fileName = fileName;
const newBackgroundPositionValue = offsets
.map(item => (item ? -item + 'px' : '0'))
.join(' ');
if (existingBackgroundPositionDecls.length > 0) {
existingBackgroundPositionDecls[0].value = newBackgroundPositionValue;
existingBackgroundPositionDecls[0].important = backgroundPositionImportant;
} else {
node.append(
'background-position: ' +
newBackgroundPositionValue +
(backgroundPositionImportant ? ' !important' : '')
);
}
}
const spriteAsset = assetGraph.addAsset(spriteAssetConfig);
if (Array.isArray(spriteGroup.placeHolders)) {
for (const spriteInfo of spriteGroup.placeHolders) {
const cssRule = spriteInfo.cssRule;
let propertyName;
let propertyNode;
let tokenNumber;
for (const candidatePropertyName of ['background-image', 'background']) {
const decls = getProperties(cssRule, candidatePropertyName);
if (!propertyName && decls.length > 0) {
propertyName = candidatePropertyName;
var propertyValue = decls[0].value;
if (propertyValue === '!important') {
// Hack so that an existing value of "!important" will DTRT
decls[0].important = true;
propertyValue = 'url(...)';
} else if (/^\s*$/.test(propertyValue)) {
propertyValue = 'url(...)';
} else {
const existingUrlTokens = propertyValue.match(assetGraph.CssImage.prototype.tokenRegExp);
if (existingUrlTokens) {
tokenNumber = existingUrlTokens.length;
}
propertyValue += ' url(...)';
}
decls[0].value = propertyValue;
}
}
if (propertyName) {
propertyNode = getProperties(cssRule, propertyName)[0];
} else {
cssRule.append('background-image: url(...)');
propertyNode = cssRule.last;
}
// I can't see why the ordering of CssImage relations should be significant...
const relation = spriteInfo.asset.addRelation({
type: 'CssImage',
node: cssRule,
to: spriteAsset,
propertyName,
propertyNode,
tokenNumber
}, 'last');
relation.refreshHref();
spriteInfo.cssRule.walkDecls(decl => {
if (['-sprite-selector-for-group', '-sprite-packer', '-sprite-image-format', '-sprite-background-color', '-sprite-important'].includes(decl.prop)) {
decl.remove();
}
});
node.walkDecls(decl => {
if (
[
'-sprite-group',
'-sprite-padding',
'-sprite-no-group-selector',
'-sprite-important'
].includes(decl.prop)
) {
decl.remove();
}
});
// If background-size is set, we should update it, The correct size is now the size of the sprite:
const backgroundSizeDecls = getProperties(spriteInfo.cssRule, 'background-size');
if (backgroundSizeDecls.length > 0) {
backgroundSizeDecls[0].value = packingData.width + 'px ' + packingData.height + 'px';
}
}
// Background-sizes change when spriting, upadte appropriately
if (imageInfo.asset.devicePixelRatio === 1) {
// Device pixel ratio is default. Remove property and let the defaults rule
for (const backgroundSizeDecl of getProperties(
incomingRelation.node,
'background-size'
)) {
backgroundSizeDecl.remove();
}
} else {
// Device pixel ratio is non-default, Set it explicitly with the ratio applied
const dpr = incomingRelation.to.devicePixelRatio;
for (const imageInfo of imageInfos) {
for (const incomingRelation of imageInfo.incomingRelations) {
incomingRelation.from.markDirty();
const relationSpriteInfo = getRelationSpriteInfoFromIncomingRelation(incomingRelation);
const node = incomingRelation.node;
const existingBackgroundPositionDecls = getProperties(node, 'background-position');
const existingBackgroundDecls = getProperties(node, 'background');
const offsets = [
Math.round(imageInfo.x / imageInfo.asset.devicePixelRatio), // FIXME: Rounding issues?
Math.round(imageInfo.y / imageInfo.asset.devicePixelRatio)
];
let backgroundOffsetsWereUpdated = false;
let existingOffsets;
if (existingBackgroundDecls.length > 0) {
// Warn if there's more than one?
const backgroundTokens = existingBackgroundDecls[0].value.split(/\s+/);
const positionTokenIndices = [];
existingOffsets = [];
for (const [i, existingBackgroundValueToken] of backgroundTokens.entries()) {
if (/^(?:-?\d+px|0)$/i.test(existingBackgroundValueToken)) {
positionTokenIndices.push(i);
existingOffsets.push(parseInt(existingBackgroundValueToken, 10));
}
}
if (existingOffsets.length === 2) {
// Patch up the existing background property by replacing the old offsets with corrected ones:
for (let [i, offset] of offsets.entries()) {
offset -= existingOffsets[i];
backgroundTokens.splice(positionTokenIndices[i], 1, offset ? -offset + 'px' : '0');
}
// TODO: Figure out if rounding might become a problem
const width = packingData.width / dpr;
const height = packingData.height / dpr;
const existingBackgroundSizeDecls = getProperties(
incomingRelation.node,
'background-size'
);
if (existingBackgroundSizeDecls.length > 0) {
existingBackgroundSizeDecls[0].value = `${width}px ${height}px`;
} else {
incomingRelation.node.append(
`background-size: ${width}px ${height}px`
);
}
}
existingBackgroundDecls[0].value = backgroundTokens.join(' ');
backgroundOffsetsWereUpdated = true;
}
}
if (relationSpriteInfo.noGroup || !spriteGroup.placeHolders) {
// The user specified that this selector needs its own background-image/background
// property pointing at the sprite rather than relying on the Html elements also being
// matched by the sprite group's "main" selector, which would have been preferable.
const relation = incomingRelation.from.addRelation(
{
type: 'CssImage',
node: incomingRelation.node,
propertyNode: incomingRelation.propertyNode,
to: spriteAsset
},
'before',
incomingRelation
);
relation.refreshHref();
incomingRelation.remove();
} else {
incomingRelation.detach();
}
if (!backgroundOffsetsWereUpdated) {
// There was no 'background' property, or it didn't contain something that looked like offsets.
// Create or update the background-position property instead:
let backgroundPositionImportant = false;
if (existingBackgroundPositionDecls.length === 1) {
// FIXME: Silently ignores other units than px
backgroundPositionImportant = existingBackgroundPositionDecls[0].value === '!important' || existingBackgroundPositionDecls[0].important;
if (existingBackgroundPositionDecls[0].value !== '!important') {
existingOffsets = existingBackgroundPositionDecls[0].value.split(' ').map(
item => parseInt(item, 10)
);
if (existingOffsets.length !== 2 || isNaN(existingOffsets[0]) || isNaN(existingOffsets[1])) {
const err = new Error('WARNING: trying to sprite ' + imageInfo.asset.url + ' with background-position: ' + existingBackgroundPositionDecls[0].value);
assetGraph.warn(err);
} else {
offsets[0] -= existingOffsets[0];
offsets[1] -= existingOffsets[1];
}
}
}
const newBackgroundPositionValue = offsets.map(
item => item ? -item + 'px' : '0'
).join(' ');
if (existingBackgroundPositionDecls.length > 0) {
existingBackgroundPositionDecls[0].value = newBackgroundPositionValue;
existingBackgroundPositionDecls[0].important = backgroundPositionImportant;
} else {
node.append('background-position: ' + newBackgroundPositionValue + (backgroundPositionImportant ? ' !important' : ''));
}
}
node.walkDecls(decl => {
if (['-sprite-group', '-sprite-padding', '-sprite-no-group-selector', '-sprite-important'].includes(decl.prop)) {
decl.remove();
}
});
// Background-sizes change when spriting, upadte appropriately
if (imageInfo.asset.devicePixelRatio === 1) {
// Device pixel ratio is default. Remove property and let the defaults rule
for (const backgroundSizeDecl of getProperties(incomingRelation.node, 'background-size')) {
backgroundSizeDecl.remove();
}
} else {
// Device pixel ratio is non-default, Set it explicitly with the ratio applied
const dpr = incomingRelation.to.devicePixelRatio;
// TODO: Figure out if rounding might become a problem
const width = packingData.width / dpr;
const height = packingData.height / dpr;
const existingBackgroundSizeDecls = getProperties(incomingRelation.node, 'background-size');
if (existingBackgroundSizeDecls.length > 0) {
existingBackgroundSizeDecls[0].value = `${width}px ${height}px`;
} else {
incomingRelation.node.append(`background-size: ${width}px ${height}px`);
}
}
if (relationSpriteInfo.noGroup || !spriteGroup.placeHolders) {
// The user specified that this selector needs its own background-image/background
// property pointing at the sprite rather than relying on the Html elements also being
// matched by the sprite group's "main" selector, which would have been preferable.
const relation = incomingRelation.from.addRelation({
type: 'CssImage',
node: incomingRelation.node,
propertyNode: incomingRelation.propertyNode,
to: spriteAsset
}, 'before', incomingRelation);
relation.refreshHref();
incomingRelation.remove();
} else {
incomingRelation.detach();
}
// Remove the original image if it has become an orphan:
if (!assetGraph.findRelations({to: incomingRelation.to}).length) {
assetGraph.removeAsset(incomingRelation.to);
}
}
}
// Remove the original image if it has become an orphan:
if (!assetGraph.findRelations({ to: incomingRelation.to }).length) {
assetGraph.removeAsset(incomingRelation.to);
}
}
};
};
}
}
};

@@ -5,7 +5,8 @@ {

"repository": "git://github.com/One-com/assetgraph-sprite.git",
"version": "3.0.0",
"version": "3.0.1",
"license": "BSD-3-Clause",
"maintainers": [
{
"name": "Andreas Lind",
"email": "andreas@one.com"
"email": "andreaslindpetersen@gmail.com"
},

@@ -17,21 +18,17 @@ {

],
"publishConfig": {
"registry": "http://registry.npmjs.org/"
},
"files": [
"lib"
],
"optionalDependencies": {
"canvas": "1.6.5"
},
"devDependencies": {
"assetgraph": "4.0.0",
"coveralls": "^2.11.6",
"eslint": "4.16.0",
"eslint-config-onelint": "3.0.0",
"istanbul": "^0.4.2",
"lodash.pluck": "3.1.2",
"mocha": "^3.0.0",
"assetgraph": "^5.4.0",
"coveralls": "^3.0.0",
"eslint": "^5.7.0",
"eslint-config-pretty-standard": "^2.0.0",
"eslint-plugin-prettier": "^3.0.0",
"lodash.pluck": "^3.1.2",
"mocha": "^5.1.0",
"nyc": "^13.1.0",
"prettier": "^1.14.3",
"unexpected": "^10.26.0",
"urltools": "0.3.1"
"urltools": "^0.4.1"
},

@@ -43,7 +40,15 @@ "directories": {

"lint": "eslint .",
"test": "npm run lint && mocha",
"coverage": "istanbul cover _mocha",
"test": "mocha",
"coverage": "NODE_ENV=development nyc --reporter=lcov --reporter=text --all -- npm run test && echo google-chrome coverage/lcov-report/index.html",
"travis": "npm run lint && npm run coverage"
},
"main": "lib/spriteBackgroundImages.js"
"main": "lib/spriteBackgroundImages.js",
"dependencies": {
"canvas-prebuilt": "2.0.0-alpha.14"
},
"nyc": {
"include": [
"lib/**"
]
}
}

@@ -163,16 +163,3 @@ AssetGraph-sprite

For creating the sprite images themselves AssetGraph-sprite uses <a
href="http://github.com/LearnBoost/node-canvas">node-canvas</a>, which
is not a pure-node module and requires the Cairo development sources
(version 1.10 or later), `libjpeg` (version 8 or later) and
`libgif`. On Ubuntu 10.10 and above you should be able to get them
like this:
```
$ sudo apt-get install libcairo2-dev libgif-dev libjpeg8-dev
```
Now you can proceed to install AssetGraph-sprite:
```
$ npm install assetgraph-sprite

@@ -179,0 +166,0 @@ ```

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc