Bootstrap

vue3 使用canvas将图片和文字合成并下载。

vue3 使用canvas将图片和文字合成并下载。

<template>
  <div class="my-certificate-item">
    <div class="certificate-img" v-if="item.style == 'horizontal'">
      <div v-if="item.type == 0">
        <img :src="item.url" alt="" width="240px" height="170px" />
      </div>
      <div v-else style="position: relative">
        <img :src="item.url" alt="" width="240px" height="170px" />
        <div class="smalll-horizontal">
          <div class="horizontal-name">{{ item.name }}</div>
          <div class="horizontal-content">{{ item.content }}</div>
        </div>
      </div>
    </div>
    <div class="certificate-img" v-else>
      <div v-if="item.type == 0">
        <img :src="item.url" alt="" width="170px" height="240px" />
      </div>
      <div v-else style="position: relative">
        <img :src="item.url" alt="" width="170px" height="240px" />
        <div class="smalll-vertical">
          <div class="vertical-name">{{ item.name }}</div>
          <div class="vertical-content">{{ item.content }}</div>
        </div>
      </div>
    </div>
    <div class="certificate-info">
      <div class="certificate-name">{{ item.projectName }}</div>
      <div class="download">
        <span>获取时间:{{ item.time }}</span>
        <el-button type="primary" plain @click="download(item)">下载</el-button>
      </div>
    </div>
  </div>
</template>
<script setup lang="ts">
import { ref } from "vue";
defineProps<{
  item: {
    url: string;  //图片
    name: string;
    content: string;
    style: string;
    type: number;
    time: string;
    projectName: string;
  };
}>();

const downloadData = ref();
const download = (data: any) => {
  downloadData.value = data;
  if (data && data.url) {
    urlToBase64(data.url).then((res) => {
      // 转化后的base64图片地址
      let aLink = document.createElement("a");
      let blob = base64ToBlob(res); //new Blob([content]);
      
      aLink.download = "我的证书.png";
      aLink.href = URL.createObjectURL(blob);;
      aLink.click();
    });
  } else {
    console.error("Invalid data for download function");
  }
};

const urlToBase64 = (url) => {
  return new Promise((resolve, reject) => {
    // 确保downloadData在作用域内可用,或者作为参数传入
    let canvas = document.createElement("canvas");
    let ctx = canvas.getContext("2d");
    let image = new Image();

    image.onload = function () {
      if (downloadData.value.style == "horizontal") {
        canvas.width = 720;
        canvas.height = 510;
        ctx?.drawImage(image, 0, 0);
        if (downloadData.value.type == 1 && ctx) {
          ctx.textAlign = "center"; // 设置文本水平对齐方式
          ctx.textBaseline = "middle"; // 设置文本垂直对齐方式
          // 假设我们想要在画布中心显示文本
          ctx.font = "20px Arial"; // 设置 name 的字体大小为 20px
          let nameTextX = canvas.width / 2;
          let nameTextY = 217;
          ctx.fillText(downloadData.value.name, nameTextX, nameTextY);
          let contentFontSize = 16;
          ctx.font = `${contentFontSize}px Arial`;
          ctx.textAlign = "left"; // 换行文本通常左对齐
          ctx.textBaseline = "top"; // 从顶部基线开始绘制

          // content绘制的起始位置
          let contentTextX = (canvas.width - 450) / 2; // 水平居中,但限制在450px宽度内
          let contentTextY = nameTextY + 30; // 紧接在name文本下方

          // 实现content的自动换行逻辑
          let contentLines = wrapText(ctx, "    " + downloadData.value.content, 440);

          // 绘制换行后的content
          for (let i = 0; i < contentLines.length; i++) {
            let lineTextY = contentTextY + i * (contentFontSize + 5); // 更新每行的Y坐标
            ctx.fillText(contentLines[i], contentTextX, lineTextY);
          }
        }
      } else {
        canvas.width = 510;
        canvas.height = 720;
        ctx?.drawImage(image, 0, 0);
        if (downloadData.value.type == 1 && ctx) {
          ctx.textAlign = "center"; // 设置文本水平对齐方式
          ctx.textBaseline = "middle"; // 设置文本垂直对齐方式
          // 假设我们想要在画布中心显示文本
          ctx.font = "20px Arial"; // 设置 name 的字体大小为 20px
          let nameTextX = canvas.width / 2;
          let nameTextY = 295;
          ctx.fillText(downloadData.value.name, nameTextX, nameTextY);
          let contentFontSize = 16;
          ctx.font = `${contentFontSize}px Arial`;
          ctx.textAlign = "left"; // 换行文本通常左对齐
          ctx.textBaseline = "top"; // 从顶部基线开始绘制

          // content绘制的起始位置
          let contentTextX = (canvas.width - 360) / 2; // 水平居中,但限制在450px宽度内
          let contentTextY = nameTextY + 30; // 紧接在name文本下方

          // 实现content的自动换行逻辑
          let contentLines = wrapText(ctx, "    " + downloadData.value.content, 340);

          // 绘制换行后的content
          for (let i = 0; i < contentLines.length; i++) {
            let lineTextY = contentTextY + i * (contentFontSize + 5); // 更新每行的Y坐标
            ctx.fillText(contentLines[i], contentTextX, lineTextY);
          }
        }
      }

      // 将图片插入画布并开始绘制

      // 生成结果数据URL
      let result = canvas.toDataURL("image/png");
      resolve(result);
    };

    image.setAttribute("crossOrigin", "Anonymous");
    image.src = url;

    // 图片加载失败的错误处理
    image.onerror = () => {
      reject(new Error("urlToBase64 error"));
    };
  });
};

// 文本自动换行辅助函数
const wrapText = (ctx, text, maxWidth) => {
  let words = text.split("");
  let lines = [] as any;
  let currentLine = "";

  for (let word of words) {
    let testLine = currentLine + word + "";
    console.log(testLine);
    if (ctx.measureText(testLine).width > maxWidth) {
      lines.push(currentLine);
      currentLine = word + " ";
    } else {
      currentLine = testLine;
    }
  }
  if (currentLine.trim() !== "") {
    lines.push(currentLine);
  }

  console.log(lines, 123);
  return lines;
};

//base64转blob
const base64ToBlob = (code) => {
  let parts = code.split(";base64,");
  let contentType = parts[0].split(":")[1];
  let raw = window.atob(parts[1]);
  let rawLength = raw.length;

  let uInt8Array = new Uint8Array(rawLength);

  for (let i = 0; i < rawLength; ++i) {
    uInt8Array[i] = raw.charCodeAt(i);
  }
  return new Blob([uInt8Array], { type: contentType });
};
</script>
<style lang="scss" scoped>
.my-certificate-item {
  width: 256px;
  height: 370px;
  border-radius: 12px;
  border: 1px solid #dadada;
  margin: 8px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  .certificate-img {
    width: 254px;
    height: 256px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgb(245, 248, 255);
    border-top-left-radius: 12px;
    border-top-right-radius: 12px;

    .smalll-horizontal {
      position: absolute;
      width: 150px;
      left: 45px;
      text-align: center;
      top: 60px;
      scale: 0.5;
      display: flex;

      flex-direction: column;

      align-items: center;

      .horizontal-name {
        font-size: 12px;
        transform: scale(1);
        padding-bottom: 9px;
      }

      .horizontal-content {
        font-size: 12px;
        width: 300px;
      }
    }

    .smalll-vertical {
      position: absolute;
      width: 120px;
      left: 25px;
      text-align: center;
      top: 95px;
      scale: 0.5;
      display: flex;

      flex-direction: column;

      align-items: center;

      .vertical-name {
        font-size: 12px;
        transform: scale(1);
        padding-bottom: 9px;
      }

      .vertical-content {
        font-size: 12px;
        width: 210px;
      }
    }
  }
  .certificate-info {
    flex: 1;
    width: 100%;
    padding: 12px 12px 16px 12px;
    display: flex;
    flex-direction: column;

    .certificate-name {
      font-size: 16px;
      font-weight: 600;
      flex: 1;
    }
    .download {
      height: 32px;
      display: flex;
      align-items: center;
      justify-content: space-between;
    }
  }
}
</style>

;