证书是如何“炼”成的?

507 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第7天,点击查看活动详情

前言

在工作生活中,我们会获得大大小小的证书:小时候我们有“三好学生”、“优秀学生”的奖状;现在我们有“毕业证书”、“学位证书”、“创作先锋奖”等等;可以看到这些证书都有一个共同的底图(也就是背景图),然后需要修改的部分就是:姓名、公司、学位、专业等等;所以在证书打印之前肯定都是绘制好的模板然后进行打印,如果是在网站上我们正好可以使用canvas来生成这个模板;

模板

从网上我们找到一个证书模板,我们以这个证书为例;

可以看到需要修改的地方就是中间的“姓名”;左下方“日期”;右边的“签名”;还有最上面的“公司名称”;可以看到“姓名“、”日期“、”签名“都是特殊字体,我们第一个问题就是要解决特殊字体的加载1_webp.webp

加载特殊字体

首先需要理解@font-face的运行机制:当我们定义字体时,这个字体并不会立即下载,而是等到第一个使用这个字体的元素时才去下载字体文件,是一种”懒加载“;但是我们的canvas等不了那么长时间,等到字体加载完成,canvas早已生成了”系统字体“组成的图片

<canvas id="cvs"></canvas>
<script>
  const canvas = document.getElementById('cvs')
  const ctx = canvas.getContext('2d')
  ctx.font="caoshu 200px"
  ctx.fillText("看一下是不是草书",100,100)
</script>

1653700315404.jpg 显然字体”caoshu“被替换为系统字体,并且字体大小也不生效了

那么我们必须第一时间让字体进行加载,这里想到一个办法就是使用当前页面某个元素,设置一个伪元素,把它的font-family指向特殊字体,但是为了不显示出来,字体大小设置为0,content必须设置如果设置为空也不会触发加载

@font-face{
    font-family:'caoshu';
    src:url(/font/caoshu.ttf);
}
.xxxx:before{
    content:'.';
    font-family:'caoshu';
    font-size:0;
}

与CSS中@font-face对应的有一个JS FontFace API,利用它加载可以返回一个promise,使得字体加载更可控,但是我在实践过程中出现了各种报错,没有成功,大家有兴趣可以去研究一下

绘图

我们这里只讲一讲最主要的部分

  1. 图片预加载;只有所有背景图片加载完成之后我们才开始canvas绘制流程
const resources = []
function loadImg(url){
    return new Promise((resolve,reject)=>{
        const img = new Image()
        img.onload  = ()=>{
            resolve(img)
        }
        img.onerror = ()=>{
            reject()
        }
        img.src = url
    })
}
await Promise.all(resources.map(r=>loadImg(r)))
  1. 初始化画布大小,需要和背景图一样大,否则图片会被裁剪
const cavnasWidth = 1920
const canvasHeight = 1080
const initCanvas = (context) => {
  context.width = canvasWid;
  context.height = canvasHeight;
};
  1. 分区块绘制主体内容,不要把代码冗余到同一个函数中,可能会过长
functon drawDate(){}

funciton drawName(){}

function drawCompanyName(){}
  1. 缩放图片,我们的图片可能太大,需要缩放到我们需要的尺寸;这里我们先利用canvas.toDataURL;然后再创建一个canvas加载这张图片,利用drawImage进行剪裁
const base64 = canvas.toDataURL('image/jpeg', 1)
const finnalWidth = 480
const finnalHeight = 270
function transformPic(url){
   return new Promise((resolve,reject)=>{
       const image = new Image()
       image.onload = ()=>{
            const cvs = document.createElement('canvas')
            cvs.width = 480;
            cvs.height = 270;
            cvs..getContext('2d')
        ?.drawImage(image, 0, 0, finnalWidth, finnalHeight, 0, 0, finnalWidth, finnalHeight);
        cvx.toBlob(blob=>{
          resolve(blob)
        },'image/jpeg')
       }
   })
}
transformPic(base64)

canvas的结果产物

canvas可以转为Base64和Blob

Base64格式:canvas.toDataURL得到,是一种图片预览格式,无法上传,而且字符很多,占据很大的HTML,如果超出一定上限可能无法查看

Blob格式:canvas.toBlob(blob=>blob)得到,是一种文件格式,可以直接上传,是一个”全能选手“

Blob文件如何预览:URL.createObjectURL(canvasBlob)这种方式预览图片优于Base64,赶紧改掉之前使用Base64预览的习惯把(其实我也是今天才知道)

Blob文件读取:FileReader API

Blob文件截取:Blob.prototype.slice

图片下载终极方案

生成了证书:可如何下载呢?图片下载在各个浏览器表现都不一致,可怎么办呢?

我们类比一下附件下载,显然所有浏览器都支持附件下载,看一下请求和相应可以得出结论:

  1. 设置Content-disposition,设置这个响应头告知浏览器是附件,此时可以触发浏览器默认行为:下载

  2. 设置Content-type:application/octet-stream

如果是Nodejs可以这样写,但是我们这里需要资源服务器设置

// 这里设置基本的反馈头,"application/octet-stream" 为了适应多个类型的文件,如果是具体的类型就写相关类型即可
    ctx.Header("Content-Type", "application/octet-stream")

// 写入对应的文件名称
    ctx.Header("Content-Disposition", "attachment; filename="+arg.FileName)
    ctx.Header("Content-Transfer-Encoding", "binary")

但是如果使用OSS,那么很简单:只需要加一个后缀参数就可以了

href={`${prefix}?response-content-disposition=attachment&filename=`}
target='_blank'
download={`${data?.title}结营证书`}

总结:本文讲解了canvas绘制证书的关键环节、canvas的结果产物以及最后生成图片如何下载等一系列流程,希望下次再遇到同类需求不再惧怕