This commit is contained in:
yulixing 2019-06-19 20:08:24 +08:00
parent 8f4d203f71
commit 16d4cc09da
15 changed files with 674 additions and 12 deletions

BIN
fonts/muyao.ttf Normal file

Binary file not shown.

BIN
fonts/shoushuti.ttf Normal file

Binary file not shown.

BIN
fonts/siyuan.otf Normal file

Binary file not shown.

BIN
fonts/yangrendong.ttf Normal file

Binary file not shown.

View File

@ -20,6 +20,7 @@
"body-parser": "^1.19.0",
"bson": "^4.0.2",
"bunyan": "^1.8.12",
"canvas": "^2.5.0",
"compression": "^1.7.4",
"connect-mongo": "^2.0.3",
"cookie-parser": "^1.4.4",
@ -29,6 +30,7 @@
"express-session": "^1.16.1",
"express-validator": "^5.3.1",
"file-stream-rotator": "^0.4.1",
"form-data": "^2.3.3",
"fs-extra": "^8.0.0",
"glob": "^7.1.4",
"helmet": "^3.18.0",

View File

@ -1,21 +1,16 @@
import { Router } from 'express';
import gamesRouter from './games'
import settingsRouter from './settings'
import platformsRouter from './platforms'
import shareRouter from './share'
import gamesRouter from './games';
import settingsRouter from './settings';
import platformsRouter from './platforms';
import shareRouter from './share';
import mpShareRouter from './mp_share';
const router = new Router();
router.use('/settings', settingsRouter);
router.use('/platforms', platformsRouter);
router.use('/share', shareRouter);
router.use('/mp_share', mpShareRouter);
router.use('/', gamesRouter);
export default router;

View File

@ -0,0 +1,130 @@
import { Router } from 'express';
import FormData from 'form-data';
import request from 'request';
import fs from 'fs';
import path from 'path';
import painter from '../../utils/painter';
import config from '../../../config/config';
const router = new Router();
router.get('/test', async (req, res, next) => {
var dataBuffer = new Buffer(base64, 'base64');
// var imgBuffer = streamifier.createReadStream(dataBuffer);
var imgpath = path.join(__dirname, '../../../temp/flower.jpg');
console.log(path.join(__dirname, '../../../temp/logo.png'));
var formData = {
'image-file': fs.createReadStream(imgpath),
// 'image-file': dataBuffer,
sub_path: '/mp-share/',
file_type: 'mp_share'
};
request.post(
{
url: 'http://localhost:2333/api/common/upload',
formData: formData,
headers: {
authorization:
'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Inl1bGl4aW5nIiwiaWF0IjoxNTYwOTIyOTEyLCJleHAiOjE1NjEwMDkzMTJ9.EAVRtbGxBIp4nDaWUAoO0IqGb6OSqDG5a_GXwbfjkBI'
}
},
function(err, res1, body) {
if (err) {
console.log(err);
res.send(err);
return;
}
fs.unlink(imgpath, function(error) {
if (error) {
console.log(error);
return false;
}
console.log('删除文件成功');
});
res.send(body);
}
);
});
router.get('/test1', async (req, res, next) => {
const opt = {
baseInfo: {
width: '250px',
height: '736px',
fonts: [
{
name: 'shoushuti',
path: 'fonts/shoushuti.ttf'
}
]
},
views: [
{
type: 'image',
url:
'https://client-1256832210.cos.ap-beijing.myqcloud.com/mp-share/5d09e26a62bb32868763dba3.jpeg',
style: {
top: '50px',
left: '20px',
width: '210px', //可选
height: '210px', // 可选
'border-radius': '105px', // 可选
border: '1px solid #fff' // 可选
}
},
{
type: 'text',
text:
'人间四月芳菲尽,山寺桃花始盛开。\n长恨春归无觅处不知转入此中来。\n——白居易·大林寺桃花',
style: {
left: '165px',
top: '300px',
color: '#fae',
'max-width': '693px',
'font-size': '24px',
'font-family': 'shoushuti',
'line-height': '50px',
'writing-mode': 'vertical-rl'
}
}
]
};
try {
const imgTempName = await painter(opt);
const imgTempPath = path.join(__dirname, '../../../temp/' + imgTempName);
// 生成后上传、删除
const formData = {
'image-file': fs.createReadStream(imgTempPath),
sub_path: '/mp-share/',
file_type: 'mp_share'
};
request.post(
{
url: config.host + '/api/common/upload',
formData: formData,
headers: {
authorization: `${req.headers.authorization}`
}
},
function(uploadErr, uploadRes, uploadBody) {
fs.unlink(imgTempPath, function(err) {
if (err) {
console.log(err);
}
});
if (uploadErr) {
next(uploadErr);
return;
}
res.send(uploadBody);
}
);
} catch (err) {
next(err);
}
});
export default router;

View File

@ -34,7 +34,7 @@ const GameShareImage = new mongoose.Schema({
ad_count: {type: Number, default: 0},
// 广告间隔时间
ad_cd: {type: Number, default: 0},
// 0: 分享图, 1: 广告
// 0: 分享图优先, 1: 广告优先, 2: 只分享, 3: 只广告
type: {type: Number, default: 0},
// 备注
comment: {type: String},

300
src/utils/painter/index.js Normal file
View File

@ -0,0 +1,300 @@
const { registerFont, createCanvas, loadImage } = require('canvas');
const drawRoundRect = require('./round_rect');
const wrapText = require('./wrap_text');
const fillTextVertical = require('./vertical_text');
const drawShadow = require('./shadow');
const transformBase64 = require('./transfer_base64');
const config = require('../../../config/config');
module.exports = async function paint(opt) {
// 获取画布基本信息
const baseInfo = opt.baseInfo;
const views = opt.views;
const canvasW = parseInt(baseInfo.width);
const canvasH = parseInt(baseInfo.height);
//引入字体
const fonts = baseInfo.fonts;
if (fonts.length > 0) {
for (let i = 0; i < fonts.length; i++) {
const fontName = fonts[i].name;
registerFont(fonts[i].path, { family: `${fontName}` });
}
}
// 提取图片
const imgs = [];
views.map((view, index) => {
if (view.type === 'image' && view.url) {
imgs.push({
url: view.url,
index: index
});
}
});
const imgsInfo = await _absorbImgs(imgs);
// 创建画布与画笔
const canvas = createCanvas(canvasW, canvasH);
const ctx = canvas.getContext('2d');
// 画笔功能扩展
ctx.wrapText = wrapText;
ctx.fillTextVertical = fillTextVertical;
ctx.drawRoundRect = drawRoundRect;
// 绘制背景
_drawBg(ctx, baseInfo);
//绘制元素
for (let i = 0; i < views.length; i++) {
const view = views[i];
_drawView(ctx, view, i, imgsInfo);
}
// 生成图片
const tempName = `${new Date().getTime()}.png`;
const tempPath = config.root + `/temp/${tempName}`;
transformBase64(canvas.toDataURL(), tempPath);
return tempName;
};
function _drawBg(ctx, info) {
ctx.save();
const w = parseInt(info.width);
const h = parseInt(info.height);
const bg = info['back-ground'];
let bdr = info['border-radius'] || 0;
// 圆角剪切
if (bdr && bdr.endsWith('px')) {
bdr = parseInt(bdr);
} else if (bdr && bdr.endsWith('%')) {
bdr = (w * parseInt(bdr)) / 100;
}
ctx.drawRoundRect(0, 0, w, h, bdr, false, false);
ctx.clip();
if (!bg) {
// 默认背景
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, w, h);
} else if (
bg.startsWith('#') ||
bg.startsWith('rgba') ||
bg.startsWith('rgb') ||
bg.toLowerCase() === 'transparent'
) {
// 纯色填充
ctx.fillStyle = bg;
ctx.fillRect(0, 0, w, h);
}
// TODO: 渐变填充
ctx.restore();
}
function _drawView(ctx, view, index, imgsInfo) {
switch (view.type) {
case 'image':
_drawImg(ctx, view, index, imgsInfo);
break;
case 'text':
_drawText(ctx, view);
break;
case 'rect':
_drawRect(ctx, view);
break;
// case 'qrcode':
// _drawQRCode(ctx, view);
// break;
default:
break;
}
ctx.beginPath();
ctx.arc(100, 75, 50, 0, 2 * Math.PI);
}
function _drawImg(ctx, view, index, imgsInfo) {
const img = imgsInfo[index];
const style = view.style;
const w = parseInt(style.width);
const h = parseInt(style.height);
let x = parseInt(style.left);
let y = parseInt(style.top);
const hasBd = style['border'] ? true : false;
let bdr = style['border-radius'] || 0;
if (!img) return;
ctx.save();
// 圆角剪切
if (bdr && bdr.endsWith('px')) {
bdr = parseInt(bdr);
} else if (bdr && bdr.endsWith('%')) {
bdr = (w * parseInt(bdr)) / 100;
}
// 是否旋转
if (style['transform']) {
ctx.translate(x + w / 2, y + h / 2);
const rotateDeg = _rotateDeg(style['transform']);
x = -w / 2;
y = -h / 2;
ctx.rotate(rotateDeg);
}
if (hasBd) {
const borderInfo = style['border'].split(' ');
ctx.lineWidth = parseInt(borderInfo[0]);
ctx.strokeStyle = borderInfo[2];
}
ctx.drawRoundRect(x, y, w, h, bdr, false, hasBd);
drawShadow(ctx, style);
ctx.clip();
// 绘画区域比例
const cp = w / h;
// 原图比例
const op = img.width / img.height;
if (cp >= op) {
const r = img.width / w;
const nW = img.width / r;
const nH = img.height / r;
const nY = y - (nH - h) / 2;
ctx.drawImage(img, x, nY, nW, nH);
} else {
const r = img.height / h;
const nW = img.width / r;
const nH = img.height / r;
const nX = x - (nW - w) / 2;
ctx.drawImage(img, nX, y, nW, nH);
}
ctx.restore();
}
function _drawText(ctx, view) {
const style = view.style;
let x = parseInt(style.left);
let y = parseInt(style.top);
const maxWidth = parseInt(style['max-width']);
const lineHeight = parseInt(style['line-height']);
const fontWeight = parseInt(style['font-weight']) || 500;
ctx.save();
// 字体颜色
ctx.fillStyle = style.color || '#000';
// 字体对齐方式
ctx.textAlign = style['text-align'] || 'left';
// 字体大小和fontface
ctx.font = style['font-family']
? `${fontWeight} ${style['font-size']} "${style['font-family']}"`
: `${fontWeight} ${style['font-size']} "Sans"`;
// 文字对齐方式
if (style['writing-mode'] === 'vertical-rl') {
// 竖排文字
ctx.fillTextVertical(
view.text,
x,
y,
parseInt(style['max-height']),
parseInt(style['line-height']),
view
);
} else {
// 横排文字
// 是否旋转
if (style['transform']) {
ctx.translate(x + maxWidth / 2, y + lineHeight / 2);
const rotateDeg = _rotateDeg(style['transform']);
x = -maxWidth / 2;
y = -lineHeight / 2;
ctx.rotate(rotateDeg);
}
ctx.wrapText(
view.text,
x,
y,
parseInt(style['max-width']),
parseInt(style['line-height']),
view
);
}
ctx.restore();
}
function _drawRect(ctx, view) {
const style = view.style;
const w = parseInt(style.width);
const h = parseInt(style.height);
let x = parseInt(style.left);
let y = parseInt(style.top);
const hasBd = style['border'] ? true : false;
let bdr = style['border-radius'] || 0;
ctx.save();
// 圆角剪切
if (bdr && bdr.endsWith('px')) {
bdr = parseInt(bdr);
} else if (bdr && bdr.endsWith('%')) {
bdr = (w * parseInt(bdr)) / 100;
}
// 是否旋转
if (style['transform']) {
ctx.translate(x + w / 2, y + h / 2);
const rotateDeg = _rotateDeg(style['transform']);
x = -w / 2;
y = -h / 2;
ctx.rotate(rotateDeg);
}
if (hasBd) {
const borderInfo = style['border'].split(' ');
ctx.lineWidth = parseInt(borderInfo[0]);
ctx.strokeStyle = borderInfo[2];
}
ctx.fillStyle = style['back-ground'];
ctx.drawRoundRect(x, y, w, h, bdr, true, hasBd);
drawShadow(ctx, style);
ctx.fill();
ctx.clip();
ctx.restore();
}
function _absorbImgs(imgs) {
return new Promise(async (resolve, reject) => {
const imgsNum = imgs.length;
const result = {};
try {
for (let i = 0; i < imgsNum; i++) {
const imgInfo = imgs[i];
const imgData = await loadImage(imgInfo.url);
result[imgInfo.index] = imgData;
}
resolve(result);
} catch (err) {
reject(err);
}
});
}
// 提取旋转角度
function _rotateDeg(str) {
const reg = /^rotate\((-?\d*)deg\)$/;
const result = reg.exec(str)[1];
return (parseInt(result) * Math.PI) / 180 || 0;
}

View File

@ -0,0 +1,13 @@
module.exports = function roundPath(x, y, w, h, r) {
var min_size = Math.min(w, h);
if (r > min_size / 2) r = min_size / 2;
// 开始绘制
this.beginPath();
this.moveTo(x + r, y);
this.arcTo(x + w, y, x + w, y + h, r);
this.arcTo(x + w, y + h, x, y + h, r);
this.arcTo(x, y + h, x, y, r);
this.arcTo(x, y, x + w, y, r);
this.closePath();
return this;
};

View File

@ -0,0 +1,16 @@
module.exports = function drawRoundRect(x, y, width, height, r, fill, stroke) {
this.save();
this.beginPath(); // draw top and top right corner
this.moveTo(x + r, y);
this.arcTo(x + width, y, x + width, y + r, r); // draw right side and bottom right corner
this.arcTo(x + width, y + height, x + width - r, y + height, r); // draw bottom and bottom left corner
this.arcTo(x, y + height, x, y + height - r, r); // draw left and top left corner
this.arcTo(x, y, x + r, y, r);
if (fill) {
this.fill();
}
if (stroke) {
this.stroke();
}
this.restore();
};

View File

@ -0,0 +1,22 @@
// shadow 支持 (x, y, blur, color), 不支持 spread
// shadow:0px 0px 10px rgba(0,0,0,0.1);
module.exports = function drawShadow(ctx, style) {
if (!style || (!style['box-shadow'] && !style['text-shadow'])) {
return;
}
let box;
if (style['text-shadow']) {
box = style['text-shadow'].replace(/,\s+/g, ',').split(' ');
} else {
box = style['box-shadow'].replace(/,\s+/g, ',').split(' ');
}
if (box.length > 4) {
console.error("shadow don't spread option");
return;
}
ctx.shadowOffsetX = parseInt(box[0], 10);
ctx.shadowOffsetY = parseInt(box[1], 10);
ctx.shadowBlur = parseInt(box[2], 10);
ctx.shadowColor = box[3];
};

View File

@ -0,0 +1,7 @@
const fs = require('fs');
module.exports = function transformBase64(data, path) {
const base64 = data.replace(/^data:image\/\w+;base64,/, '');
const dataBuffer = new Buffer(base64, 'base64');
fs.writeFileSync(path, dataBuffer);
};

View File

@ -0,0 +1,106 @@
/**
* @author zhangxinxu(.com)
* @licence MIT
* @description http://www.zhangxinxu.com/wordpress/?p=7362
*/
var drawShadow = require('./shadow');
module.exports = function fillTextVertical(
text,
x,
y,
maxHeight,
lineHeight,
view
) {
var context = this;
var canvas = context.canvas;
var arrText = text.split('');
var arrWidth = arrText.map(function(letter) {
return context.measureText(letter).width;
});
var align = context.textAlign;
var baseline = context.textBaseline;
var style = view.style;
var hasStroke = style['-webkit-text-stroke'] ? true : false;
var strokeInfo = [];
var fontSize = parseInt(style['font-size']);
if (hasStroke) {
strokeInfo = style['-webkit-text-stroke'].split(' ');
context.lineWidth = parseInt(strokeInfo[0]);
context.strokeStyle = strokeInfo[1];
}
// if (align == 'left') {
// x = x + Math.max.apply(null, arrWidth) / 2;
// } else if (align == 'right') {
// x = x - Math.max.apply(null, arrWidth) / 2;
// }
if (
baseline == 'bottom' ||
baseline == 'alphabetic' ||
baseline == 'ideographic'
) {
y = y - arrWidth[0] / 2;
} else if (baseline == 'top' || baseline == 'hanging') {
y = y + arrWidth[0] / 2;
}
context.textAlign = 'left';
context.textBaseline = 'top';
// 开始逐字绘制
var curHeihgt = 0;
var initialY = y;
arrText.forEach(function(letter, index) {
// 确定下一个字符的纵坐标位置
var letterWidth = arrWidth[index];
curHeihgt += letterWidth;
// 换行
if (curHeihgt > maxHeight || letter === '\n') {
x -= lineHeight;
y = initialY;
curHeihgt = 0;
}
// 是否需要旋转判断
var code = letter.charCodeAt(0);
if (code <= 256 || code == 8212) {
context.translate(x + fontSize / 2, y + fontSize / 2);
// 英文字符旋转90°
context.rotate((90 * Math.PI) / 180);
context.translate(-x - fontSize / 2, -y - fontSize / 2);
} else if (index > 0 && text.charCodeAt(index - 1) < 256) {
// y修正
y = y + arrWidth[index - 1] / 2;
}
// 绘制阴影
if (style['text-shadow']) {
drawShadow(context, view.style);
}
// 描边
if (hasStroke) {
context.strokeText(letter, x, y);
}
context.fillText(letter, x, y);
// 旋转坐标系还原成初始态
context.setTransform(1, 0, 0, 1, 0, 0);
// 确定下一个字符的纵坐标位置
var letterWidth = arrWidth[index];
y = y + letterWidth;
});
// 水平垂直对齐方式还原
context.textAlign = align;
context.textBaseline = baseline;
};

View File

@ -0,0 +1,71 @@
/**
* @author zhangxinxu(.com)
* @licence MIT
* @description http://www.zhangxinxu.com/wordpress/?p=7362
*/
var drawShadow = require('./shadow');
module.exports = function wrapText(text, x, y, maxWidth, lineHeight, view) {
if (
typeof text != 'string' ||
typeof x != 'number' ||
typeof y != 'number' ||
typeof maxWidth != 'number' ||
typeof lineHeight != 'number'
) {
console.log('必填参数有误', text, x, y);
return;
}
var context = this;
var canvas = context.canvas;
// 字符分隔为数组
var arrText = text.split('');
var line = '';
var style = view.style;
var hasStroke = style['-webkit-text-stroke'] ? true : false;
var strokeInfo = [];
context.textBaseline = 'top';
if (hasStroke) {
strokeInfo = style['-webkit-text-stroke'].split(' ');
context.lineWidth = parseInt(strokeInfo[0]);
context.strokeStyle = strokeInfo[1];
}
for (var n = 0; n < arrText.length; n++) {
var testLine = line + arrText[n];
var metrics = context.measureText(testLine);
var testWidth = metrics.width;
if ((testWidth > maxWidth && n > 0) || arrText[n] === '\n') {
// 绘制阴影
if (style['text-shadow']) {
drawShadow(context, view.style);
}
// 描边
if (hasStroke) {
context.strokeText(line, x, y);
}
context.fillText(line, x, y);
line = arrText[n] === '\n' ? '' : arrText[n];
y += lineHeight;
} else {
line = testLine;
}
}
// 绘制阴影
if (style['text-shadow']) {
drawShadow(context, view.style);
}
// 描边
if (hasStroke) {
context.strokeText(line, x, y);
}
context.fillText(line, x, y);
};