diff --git a/fonts/muyao.ttf b/fonts/muyao.ttf new file mode 100644 index 0000000..39eee21 Binary files /dev/null and b/fonts/muyao.ttf differ diff --git a/fonts/shoushuti.ttf b/fonts/shoushuti.ttf new file mode 100644 index 0000000..f861505 Binary files /dev/null and b/fonts/shoushuti.ttf differ diff --git a/fonts/siyuan.otf b/fonts/siyuan.otf new file mode 100644 index 0000000..be89fcb Binary files /dev/null and b/fonts/siyuan.otf differ diff --git a/fonts/yangrendong.ttf b/fonts/yangrendong.ttf new file mode 100644 index 0000000..ed589cf Binary files /dev/null and b/fonts/yangrendong.ttf differ diff --git a/package.json b/package.json index 134cbe5..ab3f9f5 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/controllers/games/index.js b/src/controllers/games/index.js index 02bcfc2..35a97c5 100644 --- a/src/controllers/games/index.js +++ b/src/controllers/games/index.js @@ -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; diff --git a/src/controllers/games/mp_share.js b/src/controllers/games/mp_share.js new file mode 100644 index 0000000..7e338eb --- /dev/null +++ b/src/controllers/games/mp_share.js @@ -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; diff --git a/src/models/snoopy/GameShareImage.js b/src/models/snoopy/GameShareImage.js index bf023a6..86adae5 100644 --- a/src/models/snoopy/GameShareImage.js +++ b/src/models/snoopy/GameShareImage.js @@ -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}, diff --git a/src/utils/painter/index.js b/src/utils/painter/index.js new file mode 100644 index 0000000..635acfe --- /dev/null +++ b/src/utils/painter/index.js @@ -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; +} diff --git a/src/utils/painter/round_path.js b/src/utils/painter/round_path.js new file mode 100644 index 0000000..2c15a8f --- /dev/null +++ b/src/utils/painter/round_path.js @@ -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; +}; diff --git a/src/utils/painter/round_rect.js b/src/utils/painter/round_rect.js new file mode 100644 index 0000000..2f32b72 --- /dev/null +++ b/src/utils/painter/round_rect.js @@ -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(); +}; diff --git a/src/utils/painter/shadow.js b/src/utils/painter/shadow.js new file mode 100644 index 0000000..dab9306 --- /dev/null +++ b/src/utils/painter/shadow.js @@ -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]; +}; diff --git a/src/utils/painter/transfer_base64.js b/src/utils/painter/transfer_base64.js new file mode 100644 index 0000000..4a74ee4 --- /dev/null +++ b/src/utils/painter/transfer_base64.js @@ -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); +}; diff --git a/src/utils/painter/vertical_text.js b/src/utils/painter/vertical_text.js new file mode 100644 index 0000000..f4cf9d3 --- /dev/null +++ b/src/utils/painter/vertical_text.js @@ -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; +}; diff --git a/src/utils/painter/wrap_text.js b/src/utils/painter/wrap_text.js new file mode 100644 index 0000000..01798a5 --- /dev/null +++ b/src/utils/painter/wrap_text.js @@ -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); +};