1 Star 0 Fork 0

tianyu/merge-image

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
文件
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
ImageController.js 13.35 KB
一键复制 编辑 原始数据 按行查看 历史
zhangtianyu 提交于 2023-06-27 21:27 . feat: 完美版本1
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584
/**
* 图像控制类,用于处理图片相关操作
* 合并图片、创建背景图片,创建文字等
*/
const path = require('path');
const sharp = require('sharp');
const TextToSVG = require('text-to-svg');
const axios = require('axios');
const Utils = require('./utils/index');
const request = axios.create({ timeout: 5000 });
const pickData = res => res.data;
const EMPTY_TEXT = ' ';
const textToSVG = TextToSVG.loadSync(
path.join(process.cwd(), 'fonts/SimHei.ttf')
);
// sharp图像像素上限
// const limitInputPixels = 1000000000;
// 楼层划分函数算法
// 根据传入数组元素个数,生成拼接户型图图片行数和列数
const divideLevel = arr => {
const length = arr.length;
let cols = 0, rows = 0;
if (length === 2) {
rows = 1;
cols = 2;
} else if (length === 3) {
rows = 1;
cols = 3;
} else if (length === 5) {
rows = 2;
cols = 3;
} else if (length === 6) {
rows = 2;
cols = 3;
} else if (length === 7) {
rows = 2;
cols = 4;
} else if (length === 8) {
rows = 2;
cols = 4;
} else {
rows = Math.ceil(Math.sqrt(length));
cols = Math.ceil(length / rows);
}
let index = 0;
const result = [];
for (let i = 0; i < rows; i++) {
const row = [];
for (let j = 0; j < cols; j++) {
row.push(arr[index] ? arr[index] : null);
index++;
}
result.push(row);
}
return result;
};
const divideLevel2 = arr => {
const length = arr.length;
let cols = 0, rows = 0;
if (length === 2) {
rows = 1;
cols = 2;
} else if (length === 3) {
rows = 1;
cols = 3;
} else if (length === 5) {
rows = 2;
cols = 3;
} else if (length === 6) {
rows = 2;
cols = 3;
} else if (length === 7) {
rows = 2;
cols = 4;
} else if (length === 8) {
rows = 2;
cols = 4;
} else {
rows = Math.ceil(Math.sqrt(length));
cols = Math.ceil(length / rows);
}
let index = 0;
const result = [];
for (let i = 0; i < rows; i++) {
const row = [];
for (let j = 0; j < cols; j++) {
row.push(arr[index] ? arr[index] : null);
index++;
}
result.push(row);
}
return {
rows,
cols,
data: result
};
};
module.exports = class ImageController {
constructor() {
this.imageFormats = [
'jpeg', 'png', 'webp',
'gif', 'jp2', 'tiff', 'avif',
'heif', 'jxl', 'raw'
];
}
async getMetadata(image) {
return await sharp(image).metadata();
}
async getBuffer(filepath) {
return await sharp(filepath).toBuffer();
}
async createBackground(options) {
const { width, height, channels = 4, background = '#ffffff' } = options;
[width, height, channels].forEach(props => {
if (typeof props !== 'number') {
throw new TypeError('width, height, channels should "number"');
}
});
return sharp({
create: {
width,
height,
channels,
background
}
});
}
async createText(text, options = {}) {
if (Utils.isFalseValue(text)) text = EMPTY_TEXT;
const {
fill = 'black', stroke = 'black',
x = 0, y = 0, fontSize = 36, anchor = 'top'
} = options;
const attributes = { fill, stroke };
const svgOptions = {
x, y,
fontSize,
anchor,
attributes
};
const svgBuffer = Buffer.from(textToSVG.getSVG(text, svgOptions));
return svgBuffer;
}
async trim(image) {
return await sharp(image).trim().toBuffer();
}
// 给图片添加文字, 不改变原始尺寸
async mergeText(text, image, options = {}) {
if (Utils.isFalseValue(text)) text = EMPTY_TEXT;
const { position = 'bottom', offset = 0, fontSize = 24 } = options;
const { width, height } = await this.getMetadata(image);
const textBuffer = await this.createText(text, {
fontSize
});
const extendProp = {
top: 0,
right: 0,
bottom: 0,
left: 0
};
const flag = Object.keys(extendProp).includes(position);
flag
? extendProp[position] = Math.ceil(offset)
: extendProp['bottom'] = Math.ceil(offset);
const extendImage = await this.extend(image, extendProp);
const buffer = await sharp(extendImage)
.composite([{
input: textBuffer,
top: Math.ceil(height + offset / 2) - 50,
left: Math.ceil(width / 2) - 30
}])
.jpeg()
.toBuffer();
return await this.resize(buffer, { width, height });
}
// 给图片拼接第三方图片
async mergeImage(originImage, spliceImage, options) {
const { position = 'bottom' } = options;
const { height: originHeight, width: originWidth } =
await this.getMetadata(originImage);
const { height: spliceImageHeight, width: spliceImageWidth } =
await this.getMetadata(spliceImage);
if (position !== 'bottom') {
throw new Error('merge image position param is error');
}
// TODO: 支持多个位置拼接
const [backgroundWidth, backgroundHeight] = [
Math.max(originWidth, spliceImageWidth),
Math.ceil(originHeight + spliceImageHeight)
];
spliceImage = await this.resize(spliceImage, {
width: backgroundWidth,
height: spliceImageHeight
});
const background =
await this.createBackground({
width: backgroundWidth, height: backgroundHeight
});
const buffer = await background.composite([{
input: originImage,
top: 0,
left: 0
}, {
input: spliceImage,
top: originHeight,
left: 0
}]).jpeg().toBuffer();
return await this.resize(buffer, {
width: originWidth,
height: originHeight
});
}
// 给图片加背景
async addBackground(color = '#ffffff', image) {
const { width, height } = await this.getMetadata(image);
const background = await this.createBackground({ width, height, background: color });
return await background.composite([{
input: image,
blend: 'atop',
top: 0,
left: 0
}]).jpeg().toBuffer();
}
/**
* 合并户型图图片
* @param {*Array} images
* @param {*Object} options
* @returns
*/
async merge(images, options = {}) {
// 楼层数量
const totalLevel = images.length > 5 ? 5 : images.length;
// 对原始数据分组处理
const imageGroupList = divideLevel(images);
let {
channels = 4, background = '#ffffff', marginLeft = 0, marginTop = 0
} = options;
const imageInfoList = [];
// 单张图片横向距离间距
if (totalLevel === 4) {
marginLeft = 200;
marginTop = 200;
}
if (totalLevel === 5) {
marginLeft = 400;
marginTop = 400;
}
// 遍历
for (const singleImage of images) {
const { width, height } = await this.getMetadata(singleImage);
imageInfoList.push({ width, height });
}
// 计算最大宽高
const maxImageWidth = imageInfoList.reduce((prev, curr) => {
return curr.width > prev.width ? curr : prev;
}).width;
const maxImageHeight = imageInfoList.reduce((prev, curr) => {
return curr.height > prev.height ? curr : prev;
}).height;
// 计算整张图片的长度和宽度
const row = imageGroupList.length;
const column = imageGroupList[0].length;
const maxBackgroundWidth = maxImageWidth * column + (column - 1) * marginLeft;
const maxBackgroundHeight = maxImageHeight * row + (row - 1) * marginTop;
const compositeList = [];
for (let [i, group] of imageGroupList.entries()) {
const top = i > 0 ? maxImageHeight * i + marginTop * i : 0;
for (let [j, image] of group.entries()) {
if (!image) {
const blockBackground = await this.createBackground({
width: maxImageWidth,
height: maxImageHeight,
channels: 4,
background: '#ffffff'
});
image = await blockBackground.jpeg().toBuffer();
}
const left = j > 0 ? maxImageWidth * j + marginLeft * j : 0;
const { width, height } = await this.getMetadata(image);
const padding = {
top: 0,
bottom: 0,
left: 0,
right: 0
};
// 根据最大宽高,扩展每张图片
if (maxImageHeight > height) {
padding.top = Math.floor((maxImageHeight - height) / 2);
padding.bottom = Math.floor((maxImageHeight - height) / 2);
}
if (maxImageWidth > width) {
padding.left = Math.floor((maxImageWidth - width) / 2);
padding.right = Math.floor((maxImageWidth - width) / 2);
}
image = await this.extend(image, { ...padding });
// image = await this.addBorder(image)
image = await this.resize(image, { width: 1440, height: 1080 })
console.log()
const input = {
input: image,
blend: 'atop',
left,
top,
};
compositeList.push(input);
}
}
const maxBackground = await this.createBackground({
width: maxBackgroundWidth,
height: maxBackgroundHeight,
channels,
background
});
return await maxBackground.composite(compositeList).jpeg().toBuffer();
}
/**
* 合并户型图图片2
* @param {*} image
* @param {*} options
* @returns
*/
async merge2(images, options) {
const { cols, rows, data: imageGroupList } = divideLevel2(images);
const finalWidth = 1440;
const finalHeight = 1080;
// 计算每张图片的大小
let resizeWidth = 0, resizeHeight = 0;
let marginLeft = 0, marginTop = 0;
resizeWidth = Math.floor(finalWidth / cols);
resizeHeight = Math.floor(finalHeight / rows);
const compositeList = [];
// 第一层遍历,每一行
for (let [i, group] of imageGroupList.entries()) {
// 设置纵向偏移量
const top = i > 0 ? resizeHeight * i + marginTop * i : 0;
// 第二层遍历,每一列
for (let [j, image] of group.entries()) {
// 如果不存在图片对象,则使用空白背景代替
if (!image) {
const blockBackground = await this.createBackground({
width: resizeWidth,
height: resizeHeight,
channels: 4,
background: '#ffffff'
});
image = await blockBackground.jpeg().toBuffer();
}
// 设置横向偏移量
const left = j > 0 ? resizeWidth * j + marginLeft * j : 0;
// 转换图片大小
image = await this.resize(image, {
width: resizeWidth,
height: resizeHeight
});
// 输出图片对象
const input = {
input: image,
blend: 'atop',
left,
top,
};
compositeList.push(input);
}
}
// 创建整体背景图
const maxBackground = await this.createBackground({
width: finalWidth,
height: finalHeight,
channels: 4,
background: '#ffffff'
});
// 输出拼接后的图片
return await maxBackground.composite(compositeList).jpeg().toBuffer();
}
// 美化图片: 给图片加背景/padding、文字等操作
async beautify(image, options = {}) {
const defaultBackgroundProps = {
channels: 4,
background: '#ffffff'
};
const {
width, height,
name = ' ',
paddingBottom = 100
} = options;
const background = await this.createBackground({
width,
height,
...defaultBackgroundProps
});
const beautifyImage = await background.composite([{
input: image,
blend: 'atop',
left: 0,
top: 0,
}])
.jpeg()
.toBuffer();
if (name !== EMPTY_TEXT && !Utils.isFalseValue(name)) {
const text = await this.createText(name);
return await sharp(beautifyImage)
.extend({
top: 0,
right: 0,
bottom: paddingBottom,
left: 0,
background: '#ffffff'
})
.composite([{
input: text,
blend: 'atop',
left: width / 2,
top: height + 20,
}])
.jpeg()
.toBuffer();
}
return beautifyImage;
}
// 调整尺寸
async resize(image, options = {}) {
const {
width = 1200, height = 900,
fit = 'contain', background = '#ffffff'
} = options;
const buffer = await sharp(image)
.resize({
width,
height,
fit,
background
})
.toBuffer();
return buffer;
}
// 生成图片
async toFile(buffer, filepath) {
return await sharp(buffer).toFile(filepath);
}
// 扩展图片
async extend(image, options = {}) {
const {
top = 0, right = 0, bottom = 0, left = 0, background = '#ffffff'
} = options;
if ([top, right, bottom, left].every(v => v === 0)) return image;
const buffer = await sharp(image)
.extend({
top,
right,
bottom,
left,
background
})
.toBuffer();
return buffer;
}
// 添加边框
async addBorder(image, options = {}) {
const { border = 1, color = '#000000' } = options;
const { width, height } = this.getMetadata(image);
const buffer = await this.extend(image, {
top: border,
left: border,
right: border,
bottom: border,
background: color
});
return await this.resize(buffer, { width, height });
}
// 读取远程图片
async readRemoteImage(imageURL) {
return await request
.get(imageURL, { responseType: 'arraybuffer' })
.then(pickData);
}
};
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
JavaScript
1
https://gitee.com/Tianyu201809/merge-image.git
git@gitee.com:Tianyu201809/merge-image.git
Tianyu201809
merge-image
merge-image
master

搜索帮助