pixeli
Version:
A lightweight command-line tool for merging multiple images into customizable grid layouts.
157 lines (128 loc) • 3.78 kB
JavaScript
import sharp from 'sharp';
export const calculateAvgHeight = async (images) => {
let totalHeight = 0;
for (const img of images) {
const meta = await img.metadata();
totalHeight += meta.height;
}
return Math.floor(totalHeight / images.length);
};
export const calculateAvgWidth = async (images) => {
let totalWidth = 0;
for (const img of images) {
const meta = await img.metadata();
totalWidth += meta.width;
}
return Math.floor(totalWidth / images.length);
};
export const scaleImages = async (images, { width = null, height = null }) => {
if (!width && !height) {
throw new Error('You must provide either width or height.');
}
const scaledImages = await Promise.all(
images.map(async (image) => {
const meta = await image.metadata();
let targetWidth, targetHeight;
if (width) {
const f = width / meta.width;
targetWidth = width;
targetHeight = Math.floor(meta.height * f);
} else {
const f = height / meta.height;
targetHeight = height;
targetWidth = Math.floor(meta.width * f);
}
const buffer = await image.resize(targetWidth, targetHeight).toBuffer();
return sharp(buffer);
})
);
return scaledImages;
};
export const getSmallestImageDimensions = async (images) => {
const metas = await Promise.all(images.map((img) => img.metadata()));
return metas.reduce(
(acc, meta) => ({
smallestWidth: Math.min(acc.smallestWidth, meta.width),
smallestHeight: Math.min(acc.smallestHeight, meta.height),
}),
{ smallestWidth: Infinity, smallestHeight: Infinity }
);
};
export const getFontSize = async ({
text,
maxWidth,
maxHeight,
initialFontSize = 100,
minFontSize = 2,
fontFamily = 'sans-serif',
}) => {
const THRESHOLD = 200;
const SMALL_CHANGE = 2;
const LARGE_CHANGE = 5;
let fontSize = initialFontSize;
while (fontSize >= minFontSize) {
// No width or viewport given so that the actual size can be determined after rasterization
const svg = `
<svg xmlns="http://www.w3.org/2000/svg">
<text
x="${maxWidth / 2}"
y="10"
font-size="${fontSize}"
font-family="${fontFamily}"
fill="#000000"
text-anchor="middle"
dominant-baseline="middle">
${escapeXML(text)}
</text>
</svg>
`;
// Rasterize SVG: measure actual rendered size
const raster = await sharp(Buffer.from(svg)).png().toBuffer();
const meta = await sharp(raster).metadata();
if (meta.width <= maxWidth && meta.height <= maxHeight) {
return fontSize;
}
// If the difference is greater than the threshold, use large change
if (maxWidth - meta.width > THRESHOLD || maxHeight - meta.height) {
fontSize -= LARGE_CHANGE;
} else {
fontSize -= SMALL_CHANGE;
}
}
return minFontSize;
};
export const createSvgTextBuffer = ({ text, maxWidth, maxHeight, fontSize, fill = '#000000', fontFamily = 'sans-serif' }) => {
// Width and viewport are assigned to this svg
const svg = `
<svg xmlns="http://www.w3.org/2000/svg"
width="${maxWidth}" height="${maxHeight}"
viewBox="0 0 ${maxWidth} ${maxHeight}">
<text
x="${maxWidth / 2}"
y="${maxHeight / 2}"
font-size="${fontSize}"
font-family="${fontFamily}"
fill="${fill}"
text-anchor="middle"
dominant-baseline="middle">
${escapeXML(text)}
</text>
</svg>
`;
return Buffer.from(svg);
};
const escapeXML = (str) => {
return str.replace(
/[<>&'"]/g,
(c) =>
({
'<': '<',
'>': '>',
'&': '&',
'"': '"',
"'": ''',
}[c])
);
};
/*
*/