最近遇到一个业务需求,在小程序端定制预览功能,并在预览的图片中使用指定的外部字体。将预览的图片上传OSS,后端生成PDF,在管理系统中下载。
可是…………,通过实践发现,小程序尽管作了分包处理,依旧不能在本地存放字体包,把字体放OSS上返回,可是出现跨域,尽管配置了容许跨域,依旧不行。并且!!!小程序的canvas API
无法设置字体,没有h5中canvas中的context.font = '字体名称'
方法。最终决定曲线救国,放弃小程序端的预览生成canvas功能,将canvas引入字体,生成图片等操做放在管理系统中,采用原生canvas
来实现。前端
HTML
容器中,若是要实现文字的排版很容易。好比:实现文本超出自动换行,默认文本超出容器宽度就会自动换行,也可使用word-wrap:break-word
实现强制换行。
实现文字竖排,有几种方式:git
writing-mode
样式:(存在兼容性问题)writing-mode:vertical-rl;//垂直方向自右而左的书写方式。即 top-bottom-right-left 或者 writing-mode:vertical-lr;//垂直方向内内容从上到下,水平方向从左到右
具体效果如图:github
可是这个对于浏览器也存在必定兼容性问题,使用的时候须要注意。ajax
宽度
控制换行:(不存在兼容性,推荐方式)设置每行的宽度为一个字大小,利用文本超出默认换行的特性,或者设置超出强制换行,实现文本竖排。canvas
br
标签实现或者每一个文字存放一个标签实现换行:(很死板的写法,比较low,不推荐)给每一个文字后添加br
标签,或者每一个文字放一个标签,这样写灵活性不高,很是不推荐!小程序
canvas
中实现文字排版canvas
中,若是文本超出canvas
大小,并不会自动换行,会直接在超出的后面继续绘制成一排。canvas
中也没有直接能够设置换行的api,那该怎么实现换行呢?
能够经过js
控制,经过计算当前绘制文字的x
坐标,若是x坐标大于canvas
的宽度,将x
坐标赋值为0
(绘制的起始点x坐标),y
坐标累加一个文字的高度,从而实现文本换行。后端
竖排的逻辑和横排是同样的。文字竖排只是y
坐标累加,趟超过canvas
的高度时,将y
坐标赋值为0
(绘制的起始点y坐标),x
坐标累加一个文字的高度,从而实现竖排且文本换行。api
部分代码片断
/** * canvas绘制文字 * @param {CanvasRenderingContext2D对象} context * @param {绘制内容} text * @param {起始点x坐标制} x * @param {起始点y坐标制} y */ drawTextVertical(context, text, x, y) { let startX = x, startY = y; //记录开始的位置,用于文字换行赋值 let spaceCount = 0; let arrText = text.trim().split(''); let formatText = text.replace(/\//g, '').split(''); // 去掉单斜杠 let align = context.textAlign; let baseline = context.textBaseline; context.textAlign = 'center'; context.textBaseline = 'middle'; context.font = 'Pacifico' // 开始逐字绘制 arrText.forEach(function (letter, index) { // 肯定下一个字符的纵坐标位置 // 是否须要旋转判断 let code = letter.charCodeAt(0); // 计算文字间距 let letterWidth = 22 * 2.3; if (code <= 256) { context.translate(x, y); // 英文字符,旋转90° context.rotate(90 * Math.PI / 180); context.translate(-x, -y); } if (code !== 47) context.fillText(letter, x, y); // 旋转坐标系还原成初始态 context.setTransform(1, 0, 0, 1, 0, 0); // 单斜杠换行或者长度超过8 此处要过滤在第9字是换行的符号的状况 if ((code === 47 && !spaceCount) || (!spaceCount && index && index % 7 === 0)) { // 单斜杠/ 表明换行 charCode=47 spaceCount += 1; y = startY; x = index ? (startX + letterWidth) : x; startX = x; } else if (code !== 47) { // 若是是空格 减小字间距 if (code !== 32) { y = y + letterWidth; } else { y = y + letterWidth / 2 } } }); // 水平垂直对齐方式还原 context.textAlign = align; context.textBaseline = baseline; }
canvas
生成图片的时候能够指定图片格式(jpg,jpeg,png等),可是只能生成位图
(放大会失真)。若是想提升canvas
生成图片的质量,能够引入 hidpi-canvas-polyfill 插件,具体使用能够参考这篇文章 解决canvas生成图片模糊 。canvas
生成图片的背景默认是透明的,若是想单独设置背景颜色,可使用ctx.fillStyle
进行填充,可是设置文字颜色,则文字颜色会覆盖背景颜色,由于设置文字颜色也是使用ctx.fillStyle
。那么,这种状况可使用一下办法解决:
1.使用canvas.getImageData
复制画布上的像素数据
2.循环遍历复制的每一个像素点,而后给每一个像素设置rgb
值
3.将设置好的流数据经过putImageData
放回画布上。跨域
let imageData = ctx.getImageData(0, 0, width, height); for (let i = 0; i < imageData.data.length; i += 4) { // 当该像素是透明的,则设置成白色 if (imageData.data[i + 3] == 0) { imageData.data[i] = 255; imageData.data[i + 1] = 255; imageData.data[i + 2] = 255; imageData.data[i + 3] = 255; } } ctx.putImageData(imageData, 0, 0);
可是要注意背景和文字的绘制顺序,必须先绘制背景,再绘制文字
,若是顺序颠倒,则文字会出现很明显的锯齿状,有点模糊,这就和定位中z-index
原理相似。浏览器
1.首先引入字体库,为了节省本地空间,能够从服务端引入,可是须要注意跨域问题,
也能够将字体库放本地,直接相对路径引入。
// 从服务端引入 @font-face { font-family: "FZCUJINLJW"; src: url('https://www.xxxx.com/FZCUJINLJW.TTF') ; } // 本地引入 @font-face { font-family: "FZCUJINLJW"; src: url('../../assets/FZCUJINLJW.TTF') ; }
2.经过CanvasRenderingContext2D
对象设置字体,字号等
ctx.font = '24px FZCUJINLJW'; ctx.fillStyle = '#db9a00';//填充颜色
let canvas = document.getElementById('canvas'); let ctx = canvas.getContext('2d');//拿到一个CanvasRenderingContext2D对象 ctx.beginPath();// 开始绘制文字 ctx.font = `${FONT_SIZE}px FZCUJINLJW`; ctx.fillStyle = '#db9a00';//填充颜色 ctx.fillText('绘制的内容', /*绘制的x坐标*/, /*绘制的y坐标*/); let imgBase64 = canvas.toDataURL('image/png', 1); ctx.closePath(); ctx.save();// 保存当前画布内容 //若是须要在画布上循环绘制屡次,须要手动清除画布上已经保存的内容,若是不清除,则画布内容会叠加。 ctx.clearRect(0, 0, canvasObj.width, canvasObj.height);
经过ctx.toDataURL
能够获取到画布内容的base64编码
let imgBase64 = canvas.toDataURL('image/png', 1);
若是服务端支持使用base64
上传,则不用处理,此处由于后端须要file文件类型,因此须要将base64
转成file对象
,代码以下:
let file = dataURLtoFile(imgBase64, 'jpg'); // 将base转为file对象 function dataURLtoFile(urlData, fileName) { var bytes = window.atob(urlData.split(',')[1]); //去掉url的头,并转换为byte var mime = urlData.split(',')[0].match(/:(.*?);/)[1]; //处理异常,将ascii码小于0的转换为大于0 var ab = new ArrayBuffer(bytes.length); var ia = new Uint8Array(ab); for (var i = 0; i < bytes.length; i++) { ia[i] = bytes.charCodeAt(i); } return new File([ab], fileName, { type: mime }); }
转成file对象
后,经过FormData
格式上传
let formdata = new FormData(); formdata.append('multipartList', file); ajax.post(url,data:formdata).then()
此处须要注意,当canvas
生成的图片比较小时(好比5kb如下),有可能致使文件上传失败,我以前踩过此坑。
此处是和后端商量,将canvas
生成的图片上传服务端,并返回图片的OSS
地址,再将此地址做为参数传给后端,获取到PDF
的下载连接,前端经过window.open(url)
的方式实现文件下载。
let uploadUrl = window.interfercesPrefix + '/admin/goods/tbgoods/uploadImages'; let downLoadUrl = '/app/goods/tbgoods/downLoadPdf'; // 上传图片 ajaxUploderImg({ url: uploadUrl, data: formdata }).then(res => { // 将图片做为参数获取PDF下载地址 this.props.dispatch(downLoadPdf({ url: downLoadUrl, imgUrl: res.data })); }).catch(err => { if (err) { notification['error']({ message: err.message, description: '图片绘制出错,请重试!', }); } else { notification['error']({ message: '下载出错,请返回' }); } })