H5 canvas生成圖片並上傳文件轉成PDF下載canvas文字排版

H5 canvas生成圖片並上傳文件轉成PDF下載

最近遇到一個業務需求,在小程序端定製預覽功能,並在預覽的圖片中使用指定的外部字體。將預覽的圖片上傳OSS,後端生成PDF,在管理系統中下載。
但是…………,經過實踐發現,小程序儘管做了分包處理,依舊不能在本地存放字體包,把字體放OSS上返回,但是出現跨域,儘管配置了允許跨域,依舊不行。而且!!!小程序的canvas API沒法設置字體,沒有h5中canvas中的context.font = '字體名稱'方法。最終決定曲線救國,放棄小程序端的預覽生成canvas功能,將canvas引入字體,生成圖片等操作放在管理系統中,採用原生canvas來實現。

clipboard.png

技術要點

  • canvas文字排版
  • canvas設置指定背景顏色
  • canvas引入外部字體
  • canvas繪製文字圖片
  • 將canvas生成的base64圖片轉成file上傳(這裏根據後端協商,此處後端要求)
  • 將圖片生成PDF,並點擊批量下載

實現步驟

canvas文字排版

在一般HTML容器中,如果要實現文字的排版很容易。比如:

實現文本超出自動換行,默認文本超出容器寬度就會自動換行,也可以使用word-wrap:break-word實現強制換行。
實現文字豎排,有幾種方式:

  • 給文本容器設置writing-mode樣式:(存在兼容性問題)
writing-mode:vertical-rl;//垂直方向自右而左的書寫方式。即 top-bottom-right-left
或者
writing-mode:vertical-lr;//垂直方向內內容從上到下,水平方向從左到右

具體效果如圖:

clipboard.png

但是這個對於瀏覽器也存在一定兼容性問題,使用的時候需要注意。

clipboard.png

  • 使用寬度控制換行:(不存在兼容性,推薦方式)

設置每行的寬度爲一個字大小,利用文本超出默認換行的特性,或者設置超出強制換行,實現文本豎排。

  • 利用br標籤實現或者每個文字存放一個標籤實現換行:(很死板的寫法,比較low,不推薦)

給每個文字後添加br標籤,或者每個文字放一個標籤,這樣寫靈活性不高,非常不推薦!

clipboard.png

canvas中實現文字排版

  • 實現文字橫排

canvas中,如果文本超出canvas大小,並不會自動換行,會直接在超出的後面繼續繪製成一排。
canvas中也沒有直接可以設置換行的api,那該怎麼實現換行呢?
可以通過js控制,通過計算當前繪製文字的x座標,如果x座標大於canvas的寬度,將x座標賦值爲0(繪製的起始點x座標),y座標累加一個文字的高度,從而實現文本換行。

  • 實現文字豎排

豎排的邏輯和橫排是一樣的。文字豎排只是y座標累加,趟超過canvas的高度時,將y座標賦值爲0(繪製的起始點y座標),x座標累加一個文字的高度,從而實現豎排且文本換行。

部分代碼片段
  /**
   * 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;
  }

clipboard.png

canvas設置背景顏色

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原理類似。

canvas引入外部字體

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';//填充顏色 

canvas繪製文字圖片

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);

clipboard.png

canvas生成圖片並上傳服務端

通過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以下),有可能導致文件上傳失敗,我之前踩過此坑。
clipboard.png

將圖片生成PDF,並點擊批量下載

此處是和後端商量,將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: '下載出錯,請返回'
          });
        }
      })

clipboard.png

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章