html转pdf

975 阅读2分钟

缘由:因业务原因,本公司做教育产品的,需要对试题进行转化,重组成试卷的方式。。。由前端将html转成pdf----本文针对产生截断问题记录一下。

前沿:

试题涉及到数学公式和化学公式,试卷涉及密封线等一些试卷属性。并且试题结构比较复杂。


一:

因为导出的是a4纸,我们一开始的方案是计算试题的高度与每页a4的高度做比较,如果超过a4高度,在对于试题上方加一个白色空白元素。对当前页面进行填充。余下的(挤下去)放在下一页。。但是由于试题的复杂性。多题嵌套,并且可能存在一道小题就超出一张a4纸的高度。。。故这个方式不好计算。

二: 在我们公司前端伙伴的帮助下。用了一个方法。识别颜色的办法。。。文字的颜色和背景色进行比对。


/**
 * pdf管理对象
 * @param {String} selector class、id选择器
 * @param {Object} options 配置参数
 * @property {String} options.type 尺寸类型 a3/a4 对应尺寸为[297mm, 420mm]/[210mm,297mm]
 * @property {Array} options.bgColors 背景色数组,用于判断此色号是否作为可分割线,需要传入6位hex数值或rgba,如#ffffff,rgab(255,255,255,1)
 * @property {Array} options.padding 内容与页面边距,单位mm,支持参数合并,例[10, 15]/[10]/[10,15,10,15]
 * @returns {Object} manage pdf管理对象
 * @property {Function} manage.save 导出pdf
 */
export function pdfManage(selector, options = {}) {
  let { bgColors, type, padding } = options;
  let paddingTop, paddingLeft, paddingRight, paddingBottom;
  bgColors = bgColors || ['#ffffff']; //过滤背景色数组默认赋值
  let types = ['a3', 'a4'];
  //类型对应尺寸mm
  let typeRect = {
    a3: {
      width: 297,
      height: 420
    },
    a4: {
      width: 210,
      height: 297
    }
  };
  type = type || 'a4'; //类型默认赋值
  //校验类型是否符合
  if (!types.includes(type)) {
    throw new Error('type只能是' + types.toString() + '中的一种');
  }
  padding = padding || [0]; //padding默认赋值
  //校验padding格式是否符合
  if (!Array.isArray(padding) || ![1, 2, 4].includes(padding.length)) {
    throw new Error('padding不符合格式');
  }
  //校验padding必须为0或正数
  if (padding.some(v => v < 0 || (!v && v != 0))) {
    throw new Error('padding不小于0');
  }
  //根据padding参数数量,分配四个位置对应的padding值
  switch (padding.length) {
    //四个位置padding相同
    case 1:
      paddingTop = paddingLeft = paddingRight = paddingBottom = padding[0];
      break;
    //上下padding取数组第一位,左右padding取数组第二位
    case 2:
      paddingTop = paddingBottom = padding[0];
      paddingLeft = paddingRight = padding[1];
      break;
    //数组位置分别对应上、右、下、左的padding
    case 4:
      [paddingTop, paddingRight, paddingBottom, paddingLeft] = padding;
      break;
    default:
      break;
  }
  const pdf = new JsPDF('p', 'mm', type); // A4纸,纵向
  let bgColorsList = [];
  bgColors.forEach(color => {
    bgColorsList.push(JSON.stringify(getRgbaData(color)));
  });
  let managePromise = new Promise(resolve => {
    makeHtmlToCanvas(selector).then(canvas => {
      /* 新增pdf下载-start */
      var ctx = canvas.getContext('2d');
      var typeW = typeRect[type].width - paddingLeft - paddingRight;
      var typeH = typeRect[type].height - paddingTop - paddingBottom; // A4大小,210mm x 297mm,四边各保留10mm的边距,显示区域190x277
      var imgHeight = Math.ceil((typeH * canvas.width) / typeW); // 按A4显示比例换算一页图像的像素高度
      var renderedHeight = 0;
      pdf.page = 0;
      while (renderedHeight < canvas.height) {
        //创建canvas存放每页的图片
        var page = document.createElement('canvas');
        page.width = canvas.width;
        page.height = Math.min(imgHeight, canvas.height - renderedHeight); // 可能内容不足一页
        var pageCtx = page.getContext('2d');
        // 用getImageData剪裁指定区域,并画到前面建立的canvas对象中
        let minHeight = Math.min(imgHeight, canvas.height - renderedHeight);
        let imgData = ctx.getImageData(
          0,
          renderedHeight,
          canvas.width,
          minHeight
        );
        let loopIndex = minHeight;
        //从下往上一行行数据做遍历,查找可以作为截断的行号(当整行的像素点都与背景色相符,则代表此行可以用来截断)
        while (loopIndex > 0 && bgColorsList.length > 0) {
          let bgFitCount = 0;
          //每四个数字,分别代表r,g,b,a的值
          for (
            let i = (loopIndex - 1) * 4 * imgData.width;
            i < loopIndex * 4 * imgData.width;
            i += 4
          ) {
            let imgDataI = [
              imgData.data[i],
              imgData.data[i + 1],
              imgData.data[i + 2],
              imgData.data[i + 3]
            ];
            //当前可用于过滤的背景色列表包含了此像素点
            if (bgColorsList.includes(JSON.stringify(imgDataI))) {
              bgFitCount += 1;
            } else {
              break;
            }
          }
          //找到符合条件的行,退出遍历
          if (bgFitCount === imgData.width) {
            break;
          } else {
            //未找到,继续遍历上一行
            loopIndex -= 1;
          }
        }
        //遍历完整页数据,还未找到可以用于截断的行,则不做处理,按默认情况截断
        if (loopIndex <= 0) {
          loopIndex = minHeight;
        }
        imgData = ctx.getImageData(0, renderedHeight, canvas.width, loopIndex);
        pageCtx.putImageData(imgData, 0, 0);
        //当前页被截断过,则需要在某尾添加被截断高度的填充色(bgColors[0])作为补充
        if (loopIndex < minHeight) {
          let fillBgHeight = minHeight - loopIndex;
          pageCtx.beginPath();
          pageCtx.fillStyle = bgColors[0];
          pageCtx.fillRect(0, loopIndex, canvas.width, fillBgHeight);
        }
        //将处理好的图片正式输出到pdf中
        pdf.addImage(
          page.toDataURL('image/jpeg', 1.0),
          'JPEG',
          paddingLeft,
          paddingTop,
          typeW,
          Math.min(typeH, (typeW * page.height) / page.width)
        );
        //本页渲染完成,继续处理下一页
        renderedHeight += loopIndex;
        if (renderedHeight < canvas.height) {
          pdf.addPage();
        } // 若是后面还有内容,添加一个空页
        ++pdf.page;
        // delete page
      }
      console.log(pdf, 'pdf');
      resolve();
    });
  });
  let manage = {
    el: selector,
    //保存pdf
    save(pdfFileName) {
      managePromise.then(() => {
        pdf.save(pdfFileName);
      });
    },
    //输出pdf
    output(type, options) {
      managePromise.then(() => {
        pdf.output(type, options);
      });
    },
    //获取pdf宽度
    getPageWidth() {
      pdf.getPageWidth();
    },
    //获取pdf高度
    getPageHeight() {
      pdf.getPageHeight();
    },
    //添加元素
    addContent(el, options = {}) {
      managePromise = new Promise(resolve => {
        makeHtmlToCanvas(el).then(canvas => {
          let { x, y, pages } = options;
          x = x || 0;
          y = y || 0;
          pages = pages || 'all';
          if (pages !== 'all' && !Array.isArray(pages)) {
            throw new Error('options参数pages只能为all或者数字型数组');
          }
          for (let i = 0; i < pdf.page; i++) {
            if (pages !== 'all' && !pages.includes(i + 1)) {
              continue;
            }
            pdf.setPage(i + 1);
            pdf.addImage(
              canvas.toDataURL('image/jpeg', 1.0),
              'JPEG',
              x,
              y,
              canvas.width / 2,
              canvas.height / 2
            );
          }
          resolve();
          // pdf.save('123');
        });
      });
    }
  };
  return manage;
}