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>