【Vue】Pdf转图片功能+多张图片拼接封装

Pdf转图片功能+多张图片拼接封装

HTML页面

typescript 复制代码
<template>
  <div class="main-marge">
    <div class="box">
      <van-uploader accept="image/*,.pdf" :before-read="(file) => beforeRead(file, '-1')">
        <div class="upImg">
          <img src="./assets/upload.png" />
          <span>请上传文件第{{ imglength }}页</span>
        </div>
      </van-uploader>
      <div class="upImgtip">单文件大小不超过2MB,格式仅限PNG、JPG、JPEG、PDF</div>
      <div class="imglist">
        <div class="title">上传材料</div>
        <div class="listform" v-if="filelist.length">
          <div class="listbar" v-for="(item, index) in filelist" :key="index">
            <div class="before">
              <img :src="item.url" />
              <div class="cont">{{ item.name }}</div>
            </div>
            <div class="bargroup">
              <span @click="delImg(index)">删除</span>
              <van-uploader accept="image/*,.pdf" :before-read="(file) => beforeRead(file, index)">
                <span>重新上传</span>
              </van-uploader>
            </div>
          </div>
        </div>
        <div class="nolist" v-else>
          <img src="./assets/nolist.png" />
          <span>暂未上传材料,请上传</span>
        </div>
      </div>
    </div>
    <div class="btngroup">
      <div class="cancle" @click="cancle">取消</div>
      <div class="confirm" @click="mergeImg">确定</div>
    </div>
    <!-- pdf绘制区域 -->
    <div class="canvasPDF">
      <div>
        <canvas
          id="canvas"
          :style="{border: '1px solid #eeeeee' }"
        ></canvas>
      </div>
      <div class="pdfbar" v-for="(item, i) in imgFiles" :key="i">
        <canvas :id="`pdf_canvas_${item}`" style="border: 1px solid #eeeeee"></canvas>
      </div>
    </div>
    <!-- 图片绘制合并区域 -->
    <div class="canvasPDF">
      <canvas
        id="myCanvas"
        :style="{ border: '1px solid #eeeeee' }"
      ></canvas>
    </div>
  </div>
</template>

<script>
import { getPdfnum, PdfToImg, MeargeImg } from '../../utils/tools.js';
export default {
  data() {
    return {
      // pdf
      newPrototype: [],
      newPrototypeValue: [],
      imgFiles: [], //pdf页数列表
      filelist: [], //列表
      //img
      realWidth: 720,
      dpr: '',
      loading: false,
    };
  },
  computed: {
    imglength() {
      return this.filelist.length + 1;
    },
  },
  created() {
  	//此功能是为了pdf.js内部,有时候会报for....in的错误,原因是原型方法被其他地方改变,这里需要改回来
    for (let key in Array.prototype) {
      if (!Array.prototype.hasOwnProperty(key)) continue;
      this.newPrototype.push(key);
    }
    // 存放原始键 原始方法
    this.newPrototypeValue = this.newPrototype.map((v) => ({ [v]: Array.prototype[v] }));
    // 删除直接属性
    this.newPrototype.forEach((v) => delete Array.prototype[v]);
  },
  beforeDestroy() {
    if (Array.isArray(this.newPrototypeValue) && this.newPrototypeValue.length > 0) {
      for (const key in this.newPrototypeValue) {
        const method = this.newPrototypeValue[key];
        // 确保该属性是函数(即方法)
        if (typeof method === 'function') {
          // 将方法重新赋值到Array.prototype上
          Array.prototype[key] = method;
        }
      }
    }
  },
  methods: {
    //pdf转图片
    async beforeRead(file, fileindex) {
      if (!file) {
        return false;
      }
      let loading = this.$Toast.loading({
        message: '加载中...',
        forbidClick: true,
        duration: 0,
      });
      try {
        this.imgFiles = await getPdfnum(file);
        const res = await PdfToImg(file);
        if (fileindex === '-1') {
          this.filelist.push(res);
        } else {
          this.filelist.splice(fileindex, 1, res);
        }
        loading.clear();
      } catch (error) {
        loading.clear();
        this.$confirm({
          title: '提示',
          message: error,
          showCancelButton: false,
          closeOnClickModal: false,
        });
      }
    },
    //合并图片
    async mergeImg() {
      let loading = this.$Toast.loading({
        message: '加载中...',
        forbidClick: true,
        duration: 0,
      });
      try {
        const res = await MeargeImg(this.filelist, this.realWidth);
        this.$emit('confirm-Merge', res.data);
        loading.clear();
      } catch (error) {
        loading.clear();
        this.$confirm({
          title: '提示',
          message: error,
          showCancelButton: false,
          closeOnClickModal: false,
        });
      }
    },
    //删除所选照片
    delImg(index) {
      this.filelist.splice(index, 1);
    },
    //关闭弹窗
    cancle() {
      this.$emit('hidden-cancle');
    },
  },
};
</script>

<style scoped lang="scss">
.main-marge {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 99;
  height: 100vh;
  width: 100%;
  background: #fff;
  .tip {
    display: flex;
    padding: 9px 9px 9px 12px;
    background: rgba(253, 171, 77, 0.1);
    img {
      margin-right: 8px;
      width: 16px;
      height: 16px;
    }
    p {
      flex: 1;
      color: #fc7a43;
      font-family: 'PingFang SC';
      font-size: 12px;
      font-style: normal;
      font-weight: 400;
      line-height: 22px; /* 157.143% */
    }
  }
  .box {
    padding: 0 16px;
  }
  .upImg {
    margin-top: 16px;
    display: flex;
    padding: 12px 0px;
    flex-direction: column;
    align-items: center;
    border-radius: 8px;
    border: 1px dashed rgba(203, 180, 134, 0.3);
    background: rgba(203, 180, 134, 0.06);
    color: #b8926b;
    text-align: center;
    font-family: 'PingFang SC';
    font-size: 14px;
    font-style: normal;
    font-weight: 400;
    line-height: 22px; /* 157.143% */
    span {
      margin-top: 8px;
    }
  }
  .upImgtip {
    margin-top: 8px;
    color: rgba(0, 0, 0, 0.4);
    font-family: 'PingFang SC';
    font-size: 12px;
    font-style: normal;
    font-weight: 400;
    line-height: 20px; /* 166.667% */
  }
  .imglist {
    margin-top: 28px;
    .title {
      color: #000;
      font-family: 'PingFang SC';
      font-size: 18px;
      font-style: normal;
      font-weight: 500;
      line-height: 26px; /* 144.444% */
    }
    .listform {
      .listbar {
        display: flex;
        align-items: center;
        justify-content: space-between;
        border-bottom: 1px solid #e5e5e5;
        padding: 12px 0;
        .before {
          display: flex;
          align-items: center;
          img {
            margin-right: 20px;
            width: 48px;
            height: 48px;
            border-radius: 6px;
          }
          .cont {
            width: 175px;
            color: rgba(0, 0, 0, 0.8);
            font-family: 'PingFang SC';
            font-size: 16px;
            font-style: normal;
            font-weight: 500;
            line-height: 24px; /* 150% */
          }
        }
        .bargroup {
          width: 100px;
          color: #cbb486;
          text-align: center;
          font-family: 'PingFang SC';
          font-size: 14px;
          font-style: normal;
          font-weight: 400;
          line-height: 22px; /* 157.143% */
        }
        &:last-child {
          border-bottom: none;
        }
      }
    }
    .nolist {
      margin-top: 24px;
      display: flex;
      flex-direction: column;
      align-items: center;
      img {
        width: 160px;
        height: 160px;
      }
      span {
        margin-top: 16px;
        color: rgba(0, 0, 0, 0.6);
        text-align: center;
        font-family: 'PingFang SC';
        font-size: 14px;
        font-style: normal;
        font-weight: 400;
        line-height: normal;
      }
    }
  }
  .btngroup {
    position: fixed;
    z-index: 2;
    bottom: 0;
    left: 0;
    width: 100%;
    padding: 12px 0;
    display: flex;
    align-items: center;
    justify-content: space-around;
    background: #fff;
    box-shadow: 0px -2px 8px 0px rgba(191, 191, 191, 0.15), 0px -2px 8px 0px rgba(191, 191, 191, 0.15);
    .cancle {
      width: 160px;
      padding: 11px 16px;
      border-radius: 8px;
      border: 1px solid #e5e5e5;
      background: #fff;
      color: rgba(0, 0, 0, 0.6);
      font-family: 'PingFang SC';
      font-size: 16px;
      font-style: normal;
      font-weight: 400;
      line-height: 26px; /* 144.444% */
      text-align: center;
    }
    .confirm {
      width: 160px;
      padding: 11px 16px;
      border-radius: 8px;
      background: linear-gradient(135deg, #e4c995 0%, #b9916a 100%);
      color: #fff;
      font-family: 'PingFang SC';
      font-size: 16px;
      font-style: normal;
      font-weight: 400;
      line-height: 26px; /* 144.444% */
      text-align: center;
    }
  }
  .canvasPDF {
    position: fixed;
    top: 0;
    left: -9999px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    .pdfbar {
      position: relative;
      margin-top: 10px;
      z-index: 1;
    }
  }
}
</style>

tools.js文件

typescript 复制代码
import * as pdfjs from 'pdfjs-dist';
import * as pdfjsWorker from 'pdfjs-dist/build/pdf.worker.entry';
/**
 * pdf转图片获取pdf页数,用于渲染页面
 * @param {file} file - 文件
 */
export function getPdfnum(file) {
  return new Promise((resolve, reject) => {
    const imgFiles = [];
    if (file.type === 'application/pdf') {
      let reader = new FileReader();
      reader.readAsDataURL(file); //将文件读取为 DataURL
      reader.onload = function () {
        //文件读取成功完成时触发
        const loadingTask = pdfjs.getDocument(reader.result);
        loadingTask.promise.then(async (pdf) => {
          const pageNum = pdf.numPages;
          if (pageNum > 10) {
            reject('此pdf页数过多,请线下合并');
          }
          //准备图片
          for (let i = 1; i <= pageNum; i++) {
            imgFiles.push(i);
          }
          resolve(imgFiles);
        });
      };
      reader.onerror = function () {
        // 如果文件读取失败,拒绝Promise
        reject(new Error('Failed to read the file'));
      };
    } else {
      resolve([]);
    }
  });
}
//pdf转图片
export function PdfToImg(file) {
  return new Promise((resolve, reject) => {
    const newimgList = [];
    const fileName = file.name.substring(0, file.name.lastIndexOf('.'));
    if (file.type === 'application/pdf') {
      let reader = new FileReader();
      reader.readAsDataURL(file); //将文件读取为 DataURL
      reader.onload = function () {
        //文件读取成功完成时触发
        const loadingTask = pdfjs.getDocument(reader.result);
        loadingTask.promise.then(async (pdf) => {
          let pageNum = pdf.numPages;
          // 处理
          for (let i = 1; i <= pageNum; i++) {
            let canvasItem = '';
            pdf.getPage(i).then(async (page) => {
              const canvas = document.getElementById('pdf_canvas_' + i);
              const ctx = canvas.getContext('2d');
              const viewport = page.getViewport({ scale: 4 });
              canvas.height = viewport.height;
              canvas.width = viewport.width;
              const destWidth = 298;
              const destheight = destWidth * (viewport.height / viewport.width);
              canvas.style.width = destWidth + 'px';
              canvas.style.height = destWidth * (viewport.height / viewport.width) + 'px';
              newimgList.push(canvas);
              await page.render({ canvasContext: ctx, viewport });
              // 使用file对象进行后续操作
              if (i === pageNum) {
                setTimeout(async () => {
                  const res = await savePdfImage(newimgList, destWidth, destheight, fileName);
                  resolve(res);
                }, 500);
              }
            });
          }
        });
      };
      reader.onerror = function () {
        // 如果文件读取失败,拒绝Promise
        reject(new Error('Failed to read the file'));
      };
    } else {
      const reader = new FileReader();
      reader.onload = function (e) {
        let pngData = e.target.result;
        let obj = {
          url: pngData,
          name: fileName,
        };
        resolve(obj);
      };
      // 开始读取文件
      reader.readAsDataURL(file);
    }
  });
}
/**
 * pdf保存图片
 * @param {Array} newimgList - canvas列表
 * @param {Number} x - 页面展示宽
 * @param {Number} y - 页面展示高
 * @param {String}fileName - 文件名称
 */
function savePdfImage(newimgList, x, y, fileName) {
  return new Promise((resolve, reject) => {
    let allcanvas = document.getElementById('canvas');
    let allctx = allcanvas.getContext('2d');
    allcanvas.style.width = x;
    allcanvas.style.height = y * newimgList.length;
    const subCanvasWidth = newimgList[0].width;
    const subCanvasHeight = newimgList[0].height;
    allcanvas.width = subCanvasWidth;
    allcanvas.height = subCanvasHeight * newimgList.length;
    newimgList.forEach((subCanvas, index) => {
      // 计算当前图片在Canvas中的垂直位置
      const yPosition = index * subCanvasHeight;
      // 绘制图片
      // 使用drawImage的四个参数版本来指定绘制的源图像区域和目标区域
      allctx.drawImage(subCanvas, 0, yPosition, subCanvasWidth, subCanvasHeight);
      if (index === newimgList.length - 1) {
        let pngData = allcanvas.toDataURL('image/png');
        let obj = {
          url: pngData,
          name: fileName,
        };
        resolve(obj);
      }
    });
  });
}
/**
 * 合并图片
 * @param {file} filelist - 文件列表
 * @param {Number} realWidth - 合并后的文件宽度
 *
 */
export function MeargeImg(filelist, realWidth) {
  return new Promise(async (resolve, reject) => {
    if (!filelist.length) {
      reject('材料不能为空,请检查');
    }
    let canvas = document.getElementById('myCanvas');
    const ctx = canvas.getContext('2d');
    let x = 0;
    let y = 0;
    //获取图片信息
    const dpr = window.devicePixelRatio;
    const res = await getImginfo(filelist, realWidth);
    //展示高度
    canvas.style.width = realWidth;
    canvas.style.height = res;
    canvas.width = realWidth * dpr;
    canvas.height = res * dpr;
    for (let i = 0; i < filelist.length; i++) {
      const img = new Image();
      img.src = filelist[i].url;
      img.crossOrigin = 'anonymous';
      img.onload = async () => {
        let { imgWidth, imgHeight } = await setPosition(x, y, img, realWidth);
        //获取比例
        ctx.drawImage(img, x, y, imgWidth, imgHeight);
        y += imgHeight;
        if (i === filelist.length - 1) {
          // 下载图片
          let pngData = canvas.toDataURL('image/png');
          let filename = filelist[0].name + '.png' || '合并图片.png';
          let fileimg = await base64ToFile(pngData, filename);
          let obj = {
            target: {
              files: [fileimg],
            },
          };
          resolve({ data: obj });
        }
      };
    }
    ctx.scale(dpr, dpr);
  });
}
//获取实际位置,宽高
function setPosition(x, y, img, realWidth) {
  return new Promise((resolve, reject) => {
    const scaledpr = img.width / realWidth;
    let imgWidth = img.width;
    let imgHeight = img.height;
    if (scaledpr > 1) {
      imgWidth = img.width / scaledpr;
      imgHeight = img.height / scaledpr;
    }
    resolve({
      imgWidth: imgWidth,
      imgHeight: imgHeight,
    });
  });
}
//获取所有图片的总高度
function getImginfo(filelist, realWidth) {
  return new Promise((resolve, reject) => {
    let allHeight = 0;
    let loadedCount = 0; // 用于追踪已加载图片数量
    let totalImages = filelist.length; // 图片总数
    for (let i = 0; i < totalImages; i++) {
      const img = new Image();
      img.crossOrigin = 'anonymous';
      img.src = filelist[i].url;
      img.onload = () => {
        const scaledpr = img.width / realWidth;
        let imgHeight = img.height;
        if (scaledpr > 1) {
          imgHeight = img.height / scaledpr;
        }
        allHeight += imgHeight;
        loadedCount++; // 增加已加载图片计数
        if (loadedCount === totalImages) {
          resolve(allHeight);
        }
      };
      img.onerror = (error) => {
        reject(error);
      };
    }
  });
}
//base64转file文件
function base64ToFile(base64, filename) {
  filename = filename || String(new Date().getTime());
  let arr = base64.split(',');
  let mime = arr[0].match(/:(.*?);/)[1];
  let bstr = atob(arr[1]);
  let n = bstr.length;
  let u8arr = new Uint8Array(n);
  while (n--) {
    u8arr[n] = bstr.charCodeAt(n);
  }
  return new File([u8arr], filename, { type: mime });
}
相关推荐
Myli_ing4 分钟前
HTML的自动定义倒计时,这个配色存一下
前端·javascript·html
I_Am_Me_35 分钟前
【JavaEE进阶】 JavaScript
开发语言·javascript·ecmascript
℘团子এ1 小时前
vue3中如何上传文件到腾讯云的桶(cosbrowser)
前端·javascript·腾讯云
学习前端的小z1 小时前
【前端】深入理解 JavaScript 逻辑运算符的优先级与短路求值机制
开发语言·前端·javascript
星星会笑滴1 小时前
vue+node+Express+xlsx+emements-plus实现导入excel,并且将数据保存到数据库
vue.js·excel·express
前端百草阁1 小时前
【TS简单上手,快速入门教程】————适合零基础
javascript·typescript
彭世瑜1 小时前
ts: TypeScript跳过检查/忽略类型检查
前端·javascript·typescript
Backstroke fish1 小时前
Token刷新机制
前端·javascript·vue.js·typescript·vue
zwjapple1 小时前
typescript里面正则的使用
开发语言·javascript·正则表达式
小五Five1 小时前
TypeScript项目中Axios的封装
开发语言·前端·javascript