之前分享过一篇 PHP 生成海报图像的文章,详情请戳这里>>> WordPress 添加海报分享功能 。该功能是通过 PHP 的 GD 库生成的,由于是后端生成,无疑会增加服务器压力,同时由于部分小伙伴配置服务器的种种问题可能会导致图像生成失败,前端在加载海报图片的过程中同样会由于图片太大导致加载缓慢的问题。由于 HTML 5 中引入 Canvas 元素,我们可以在 JavaScript 中获取 Canvas 元素来生成需要的图片。那么,什么是 Canvas ?

HTML5 的 canvas 元素使用 JavaScript 在网页上绘制图像。

画布是一个矩形区域,您可以控制其每一像素。

canvas 拥有多种绘制路径、矩形、圆形、字符以及添加图像的方法。

关于 Canvas 的使用,可参考 W3CSchool 的这篇文章:https://www.w3school.com.cn/html5/html_5_canvas.asp。

下面说下具体的实现方式:

引入海报生成文件

该文件是根据 Overbool 的工程项目修改的,原项目生成的海报内容个人不太喜欢,实际使用过程中发现几处代码报错,所以修改优化了下,修改的内容挺多的,具体的就不说了。修改之后的内容为:

/*! @overbool/poster v1.0.1 | (c) 2019 Overbool | https://github.com/overbool/poster */
// 蝈蝈要安静 2019年10月01日 修改版(https://blog.quietguoguo.com/)

define(function (){
    "use strict";
    // post class
    var poster = function (){
        var DEBUG = false;
        var WIDTH = 600;
        var HEIGHT = 1200;
        function init(config){
	    var $container = document.querySelector(config.selector);
	    var $wrapper = createDom('div', 'id', 'wrapper');
	    var $canvas = createDom('canvas', 'id', 'canvas', 'block');
	    var $day = createDom('canvas', 'id', 'day');
	    var $date = createDom('canvas', 'id', 'date');
	    var $title = createDom('canvas', 'id', 'title');
	    var $content = createDom('canvas', 'id', 'content');
	    var $postmeta = createDom('canvas', 'id', 'postmeta');
	    var $description = createDom('canvas', 'id', 'description');
	    appendChilds($wrapper, $canvas, $day, $date, $title, $content, $postmeta, $description);
	    $container ? $container.appendChild($wrapper) : "";
			
	    var date = new Date(); 
	    // 日
	    var dayStyle = {
	        font: 'italic bold 80px Arial',
	        color: 'rgba(255, 255, 255, 1)',
	        position: 'left'
	    };
	    drawOneline($day, dayStyle, date.getDate()); 
	    // 年月
	    var dateStyle = {
	        font: 'normal 30px Arial',
	        color: 'rgba(255, 255, 255, 1)',
	        position: 'left'
	    };
	    drawOneline($date, dateStyle, date.getFullYear() + ' / ' + (date.getMonth() + 1)+ ' / '); 
	    // 文章标题
	    var titleStyle = {
	        font: 'normal 24px Arial',
	        color: 'rgba(66, 66, 66, 1)',
	        position: 'left'
	    };
	    titleStyle.font = config.titleStyle && config.titleStyle.font || titleStyle.font;
	    titleStyle.color = config.titleStyle && config.titleStyle.color || titleStyle.color;
	    titleStyle.position = config.titleStyle && config.titleStyle.position || titleStyle.position;
	    drawOneline($title, titleStyle, config.title); 
	    // 文章摘要
	    var contentStyle = {
	        font: '24px Arial',
	        color: 'rgba(88, 88, 88, 1)',
	        position: 'left',
	        lineHeight: 1.5,
	        maxHeight: 210
	    };
	    contentStyle.font = config.contentStyle && config.contentStyle.font || contentStyle.font;
	    contentStyle.color = config.contentStyle && config.contentStyle.color || contentStyle.color;
	    contentStyle.position = config.contentStyle && config.contentStyle.position || contentStyle.position;
	    contentStyle.lineHeight = config.contentStyle && config.contentStyle.lineHeight || contentStyle.lineHeight;
	    contentStyle.maxHeight = config.contentStyle && config.contentStyle.maxHeight || contentStyle.maxHeight;
	    drawMoreLines($content, contentStyle, config.content); 
	    // 文章Meta
	    var postmetaStyle = {
	        font: 'normal 24px Arial',
		color: 'rgba(66, 200, 120, 1)',
		position: 'left',
		lineHeight: 1.5,
		maxHeight: 72
	    };
	    postmetaStyle.font = config.postmetaStyle && config.postmetaStyle.font || postmetaStyle.font;
	    postmetaStyle.color = config.postmetaStyle && config.postmetaStyle.color || postmetaStyle.color;
	    postmetaStyle.position = config.postmetaStyle && config.postmetaStyle.position || postmetaStyle.position;
	    postmetaStyle.lineHeight = config.postmetaStyle && config.postmetaStyle.lineHeight || postmetaStyle.lineHeight;
	    postmetaStyle.maxHeight = config.postmetaStyle && config.postmetaStyle.maxHeight || postmetaStyle.maxHeight;
	    drawMoreLines($postmeta, postmetaStyle, config.postmeta); 
	    // 宣传标语
	    var descriptionStyle = {
		font: 'normal 36px Arial',
		color: 'rgba(180, 180, 180, 1)',
		position: 'center'
	    };
	    descriptionStyle.font = config.descriptionStyle && config.descriptionStyle.font || descriptionStyle.font;
	    descriptionStyle.color = config.descriptionStyle && config.descriptionStyle.color || descriptionStyle.color;
	    descriptionStyle.position = config.descriptionStyle && config.descriptionStyle.position || descriptionStyle.position;
	    drawOneline($description, descriptionStyle, config.description); 
			
	    // Logo 图标
	    var logo = new Image();
	    logo.setAttribute("crossOrigin",'Anonymous');
	    logo.src = config.logo;
	    // 特色图像
	    var banner = new Image();
	    banner.setAttribute("crossOrigin",'Anonymous');
	    banner.src = config.banner;
	    //二维码图片
	    var qrcode = new Image();
	    qrcode.src = config.qrcode;
	    // 生成海报图片
	    var onload = function onload(){
		$canvas.width = WIDTH;
		$canvas.height = HEIGHT;
				
		banner.onload = function (){
		    var ctx = $canvas.getContext('2d');
		    ctx.fillStyle = 'rgba(255, 255, 255, 1)';
		    ctx.fillRect(0, 0, $canvas.width, $canvas.height);
		    // 绘制 Banner 图片
		    ctx.drawImage(banner, 0, 0, $canvas.width, $canvas.height / 3);
		    // 绘制灰色遮罩
		    ctx.fillStyle="#00000052";	
		    ctx.globalCompositeOperation="source-over";
		    ctx.fillRect(0, 0, $canvas.width, $canvas.height / 3);
		    // 绘制 Logo 图片
		    ctx.drawImage(logo, $canvas.width-300, 20, 280, 64);
		    // 绘制当前日期
		    ctx.drawImage($day, 150, $canvas.height / 3 - 120);
		    ctx.drawImage($date, 0, $canvas.height / 3 - 80);
		    ctx.lineWidth = 3;
		    ctx.strokeStyle = '#fff';
		    ctx.moveTo(30, $canvas.height / 3 - 40);
		    ctx.lineTo(170, $canvas.height / 3 -40);
		    ctx.stroke(); 
		    ctx.beginPath();
		    ctx.lineWidth = 5;
		    ctx.strokeStyle = '#fff';
		    ctx.moveTo(30, $canvas.height / 3 - 30);
		    ctx.lineTo(240, $canvas.height / 3 -30);
		    ctx.stroke(); 
		    // 绘制文章内容
		    ctx.drawImage($title, 10, $canvas.height / 3 + 50);
		    ctx.drawImage($content, 10, $canvas.height / 3 + 130);
		    ctx.drawImage($postmeta, 10, $canvas.height / 3 + 330);
		    // 绘制装饰线条
		    ctx.beginPath();
		    ctx.strokeStyle = '#eee';
		    //ctx.setLineDash([5, 6]);
		    ctx.moveTo(0, $canvas.height / 3 + 420);
		    ctx.lineTo(600, $canvas.height / 3 + 420);
		    ctx.stroke();
		    // 绘制二维码图片
		    ctx.drawImage(qrcode, $canvas.width/2-100, $canvas.height-320,200,200);
		    // 绘制整站描述
		    ctx.drawImage($description, 0, $canvas.height - 80,);
		    var img = new Image();
		    img.src = $canvas.toDataURL('image/png');
		    var radio = config.radio || 0.7;
		    img.width = WIDTH * radio;
		    img.height = HEIGHT * radio;
		    ctx.clearRect(0, 0, $canvas.width, $canvas.height);
		    $canvas.style.display = 'none';
		    $container ? $container.appendChild(img) : "";
		    $container ? $container.removeChild($wrapper) : "";
		    if (config.callback) {
		        config.callback($container);
		    }
		};
            };
            onload();
        }
        function createDom(name, key, value) {
	    var display = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 'none';
	    var $dom = document.createElement(name);
	    $dom.setAttribute(key, value);
	    $dom.style.display = display;
	    $dom.width = WIDTH;
	    return $dom;
	}
	function appendChilds(parent) {
	    for (var _len = arguments.length, doms = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
	        doms[_key - 1] = arguments[_key];
	    }

	    doms.forEach(function (dom) {
		parent.appendChild(dom);
	    });
	}
	function drawOneline(canvas, style, content) {
	    var ctx = canvas.getContext('2d');
	    canvas.height = parseInt(style.font.match(/\d+/), 10) + 20;
	    ctx.font = style.font;
	    ctx.fillStyle = style.color;
	    ctx.textBaseline = 'top';
	    var lineWidth = 0;
	    var idx = 0;
	    var truncated = false;
	    for (var i = 0; i < content.length; i++) {
		lineWidth += ctx.measureText(content[i]).width;
		if (lineWidth > canvas.width - 60) {
		    truncated = true;
		    idx = i;
	            break;
		}
	    }
	    var padding = 30;
	    if (truncated) {
		content = content.substring(0, idx);
		padding = canvas.width / 2 - lineWidth / 2;
	    }
	    if (DEBUG) {
		ctx.strokeStyle = "#6fda92";
		ctx.strokeRect(0, 0, canvas.width, canvas.height);
	    }
	    if (style.position === 'center') {
		ctx.textAlign = 'center';
		ctx.fillText(content, canvas.width / 2, 0);
	    } else if (style.position === 'left') {
		ctx.fillText(content, padding, 0);
	    } else {
		ctx.textAlign = 'right';
		ctx.fillText(content, canvas.width - padding, 0);
	    }
	}

	function drawMoreLines(canvas, style, content) {
	    var ctx = canvas.getContext('2d');
	    var fontHeight = parseInt(style.font.match(/\d+/), 10);
	    canvas.height = style.maxHeight ? style.maxHeight : 200;
	    if (DEBUG) {
		ctx.strokeStyle = "#6fda92";
		ctx.strokeRect(0, 0, canvas.width, canvas.height);
	    }
	    ctx.font = style.font;
	    ctx.fillStyle = style.color;
	    ctx.textBaseline = 'top';
	    ctx.textAlign = 'center';
	    var alignX = 0;
	    if (style.position === 'center') {
		alignX = canvas.width / 2;
	    } else if (style.position === 'left') {
		ctx.textAlign = 'left';
		alignX = 20;
	    } else {
		ctx.textAlign = 'right';
		alignX = canvas.width - 60;
	    }
	    var lineWidth = 0;
	    var lastSubStrIndex = 0;
	    var offsetY = 0;
	    for (var i = 0; i < content.length; i++) {
		// 累加字体长度(px)
		lineWidth += ctx.measureText(content[i]).width;
		// 字体长度满一行后绘制
		if (lineWidth > canvas.width - 80) {
		    ctx.fillText(content.substring(lastSubStrIndex, i), alignX, offsetY);
		    offsetY += fontHeight * style.lineHeight;
		    lineWidth = 0;
		    lastSubStrIndex = i;
		}
		// 字体长度不足一行时绘制
		    if (i === content.length - 1) {
			ctx.fillText(content.substring(lastSubStrIndex, i + 1), alignX, offsetY);
		    }
		}
	    }
	    return {
		init: init
	    };
	}();
    return poster;
})

文末有下载链接,直接下载即可。

配置参数生成海报

生成海报的代码已经封装好了,在网站中引入该 js 文件然后调用即可。配置JS 代码如下:

// 海报按钮 JS 代码
$('[data-event="poster-popover"]').on('click', function(){
    $('.poster-popover-mask, .poster-popover-box').fadeIn()
});
$('[data-event="poster-close"]').on('click', function(){
    $('.poster-popover-mask, .poster-popover-box').fadeOut()
});
// 海报生成
if( $('.post-poster').length ){
    require(['poster','qrcode'],function(poster,qrcode){            // 使用了 require.min.js 按需加载 JS ,如果主题未使用 RequireJS 修改为其他方式加载 jquery.qrcode.min.js 即可
        // 通过 jquery.qrcode.min.js 生成二维码
        $('.poster-qrcode').qrcode({
	    render: "canvas", 
	    width: 200,
	    height: 200,
	    text: window.location.href
        });
        var qrcanvas = $('.poster-qrcode canvas')[0];//二维码所在的canvas
        var qrcode_img = convertCanvasToImage(qrcanvas)
	function convertCanvasToImage(canvas) {
	    var canvas;
	    var image = new Image();
	    canvas ? image.src = canvas.toDataURL("image/png"): "" ;
	    return image;
	}
	// 获取文章标题
	post_title = document.title;            // 自动获取文档标题
	// 获取文章摘要信息
	post_desc = jsui.excerpt ? jsui.excerpt : '暂时没有描述信息!';            // 自动获取文章摘要信息,根据自己网站修改获取摘要方式即可
	// 获取文章Meta信息
	post_meta = '本文由『'+jsui.author+'』于〔'+jsui.update+'〕更新至《'+jsui.cat_name+'》分类目录下';            // 自动获取文章Meta信息,根据自己网站修改获取Meta方式即可
	// 获取文章特色图像
	banner_link = jsui.att_img ? jsui.att_img : document.images[0].src;            // 自动获取文章特色图像,根据自己网站修改获取特色图像方式即可
	// 获取站点描述信息
	site_desc = jsui.site_brand ? jsui.site_brand : '记录成长 分享快乐';            // 自动获取网站描述信息,根据自己网站修改获取网站描述方式即可
	poster.init({
	    selector: '.poster-popover-box',
	    // 图片链接
	    banner: banner_link,       
	    logo: jsui.logo_pure,            // 自动获取网站 Logo 图标,修改为自己网站图标地址即可
	    qrcode: qrcode_img['src'],
	    // 文章标题
	    title: post_title,
	    titleStyle:{
		font: 'bold normal 36px Arial',
		color: 'rgba(66, 66, 66, 1)',
		position: 'left',
	    },
	    //文章摘要
	    content: post_desc,
	    contentStyle:{
		font: 'italic 24px Arial',
		color: 'rgba(88, 88, 88, 1)',
		position: 'left',
		lineHeight: 1.5,
		maxHeight: 174,
	    },
	    // 文章Meta
	    postmeta: post_meta,
	    postmetaStyle:{
		font: 'normal 24px Arial',
		color: 'rgba(66, 200, 120, 1)',
		position: 'left',
		lineHeight: 1.5,
		maxHeight: 72,
	    },
	    // logo标语
	    description: site_desc,
	    descriptionStyle:{
		font: 'normal 36px Arial',
		color: 'rgba(200, 200, 200, 1)',
		position: 'center'
	    },
	    
	    callback: posterDownload
	});
		
	function posterDownload(container){
	    if(container == null) {return;}
	    const $btn = container.querySelector('.poster-download')
	    const $img = container.querySelector('img')
	    $btn.setAttribute('href', $img.getAttribute('src'));
	};
    });
};

代码中基本已经注释每个配置项目的含义了,就不再细说了。需要注意的是由于海报中的二维码使用了 jquery 的 qrcode 插件生成,所以你需要确保网页中引入了 jquery.qrcode.min.js 文件。

前端调用弹窗海报

配置好上面步骤后基本上就可以生成海报图片了,但是我们需要将其从前端显示出来,在需要显示的地方添加如下代码即可:

<!-- 海报 -->
<div class="post-poster action action-poster">
    <a href="javascript:;" class="item" data-event="poster-popover">
	<i class="fa fa-paper-plane"></i><span>&nbsp;生成海报</span>
    </a>
    <div class="poster-qrcode" style="display:none;"></div>
    <div class="poster-popover-mask" data-event="poster-close"></div>
    <div class="poster-popover-box">
	<a class="poster-download btn btn-default" download="<?php echo get_the_id();?>.jpg">    // 这里是下载图片的名称,为了方便我通过 WordPress 的 get_the_id() 函数自动获取的文章ID,你可能需要修改为固定值
	    <span>下载海报</span>
	</a>
    </div>		
</div>

最后放一张生成的海报样式。