在前端开发中,处理用户上传的图片并进行裁剪是常见的需求。使用 Vue.js 结合 vue-cropper
组件可以方便地实现这一功能。然而,在实际应用中,开发者可能会遇到一个问题:选择带有透明或白色背景的图片后,裁剪后的图片背景变成了黑色。本文将深入探讨这一问题,并提供详细的解决方案。
问题描述
在使用 vue-cropper
组件进行图片裁剪时,用户选择的图片如果原本具有透明区域或白色背景,裁剪后这些透明区域或白色背景会自动变为黑色。这不仅影响了用户体验,还可能导致图像质量问题。
上传签名代码组件
展示了如何使用 vue-cropper
进行图片上传和裁剪:
<template>
<div>
<div class="user-info-head" :class="notLoginUser ? 'disable' : ''" @click="editCropper()">
<img :src="imgUrl2" title="点击上传签名" class="img-circle img-lg">
</div>
<el-dialog
:title="title"
:visible.sync="open"
width="800px"
append-to-body
:close-on-click-modal="false"
@opened="modalOpened"
@close="closeDialog"
>
<el-row>
<el-col :xs="24" :md="12" :style="{ height: '350px' }">
<vue-cropper
v-if="visible"
ref="cropper"
:img="options.img"
:info="true"
:auto-crop="options.autoCrop"
:auto-crop-width="options.autoCropWidth"
:auto-crop-height="options.autoCropHeight"
:fixed-box="options.fixedBox"
@realTime="realTime"
/>
</el-col>
<el-col
:xs="24"
:md="12"
class="preview"
>
<div class="avatar-upload-preview">
<img :src="previews.url" :style="previews.img">
</div>
</el-col>
</el-row>
<br>
<el-row class="row-btn">
<el-col :lg="2" :md="2">
<el-upload
action="#"
:show-file-list="false"
:before-upload="beforeUpload"
>
<el-button>
选择
<i class="el-icon-upload el-icon--right"/>
</el-button>
</el-upload>
</el-col>
<el-col :lg="{ span: 1, offset: 2 }" :md="2">
<el-button icon="el-icon-plus" @click="changeScale(1)"/>
</el-col>
<el-col :lg="{ span: 1, offset: 1 }" :md="2">
<el-button icon="el-icon-minus" @click="changeScale(-1)"/>
</el-col>
<el-col :lg="{ span: 1, offset: 1 }" :md="2">
<el-button icon="el-icon-refresh-left" @click="rotateLeft()"/>
</el-col>
<el-col :lg="{ span: 1, offset: 1 }" :md="2">
<el-button icon="el-icon-refresh-right" @click="rotateRight()"/>
</el-col>
<el-col :lg="{ span: 2, offset: 6 }" :md="2">
<el-button type="primary" @click="uploadImg()"
v-hasPermi="['scm:system:scmCommonBusinessFile:add']"
>
提 交
</el-button>
</el-col>
</el-row>
</el-dialog>
</div>
</template>
<script>
import store from "@/store";
import { VueCropper } from "vue-cropper";
import imgUrl from "@/assets/signature.png"; // 引入图片方法一
import { downSignatureFile, uploadFile } from "@/api/file.js";
import { getImgUrl } from "@/utils/fileDownload.js";
import { uploadSignature, getOssByUserId } from "@/api/system/user.js";
export default {
name: "UserAvatar",
components: { VueCropper },
props: {
value: {
type: Object
}
},
data() {
return {
imgUrl1: imgUrl, // 引入图片方法一
imgUrl2: imgUrl, // 引入图片方法二
// 是否显示弹出层
open: false,
// 是否显示cropper
visible: false,
// 弹出层标题
title: "上传签名",
options: {
img: undefined, // 裁剪图片的地址
autoCrop: true, // 是否默认生成截图框
autoCropWidth: 200, // 默认生成截图框宽度
autoCropHeight: 200, // 默认生成截图框高度
fixedBox: true // 固定截图框大小 不允许改变
},
previews: {},
avatarImg: "",
fileInfo: {},
userInfoData: {}
};
},
computed: {
/**
* 用户信息非当前登录用户
*/
notLoginUser() {
return store.getters["user/userId"] !== this.user.userId;
},
user: {
get() {
return this.value;
},
set(value) {
this.$emit("input", value);
}
}
},
watch: {
user: {
handler(val) {
// let currentRouter = this.$route
// if (JSON.stringify(val) !== "{}" && currentRouter.path ==='/system/e-signature/info') {
this.initAvatar(val);
// }
},
immediate: true,
deep: true
}
},
methods: {
/**
* 初始化图片
*/
initAvatar(user) {
this.userInfoData = user;
let businessId = user.userId
getOssByUserId(businessId)
.then((v) =>
downSignatureFile(v.data.fileId))
.then((v) => getImgUrl(v))
.then((v) => {
this.imgUrl2 = v;
});
},
// 编辑头像
editCropper() {
this.options.img = this.imgUrl2;
this.open = true;
},
// 打开弹出层结束时的回调
modalOpened() {
this.visible = true;
},
// 向左旋转
rotateLeft() {
this.$refs.cropper.rotateLeft();
},
// 向右旋转
rotateRight() {
this.$refs.cropper.rotateRight();
},
// 图片缩放
changeScale(num) {
num = num || 1;
this.$refs.cropper.changeScale(num);
},
// 上传预处理
beforeUpload(file) {
//限制严格点,支持JPG、PNG格式
if (file.type !== "image/png" && file.type !== "image/jpeg") {
this.$modal.msgError("文件格式错误,请上传图片类型,如:JPG,PNG后缀的文件。");
} else {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
this.options.img = reader.result;
this.fileInfo = file;
this.options.filename = file.name;
};
}
},
uploadImg() {
this.$refs.cropper.getCropBlob((data) => {
const formdata = new FormData();
if (Object.keys(this.fileInfo).length !== 0) {
formdata.append("file", data ,this.options.filename);
formdata.append("bizPath", "picture");
formdata.append("uploadType", "minio");
uploadFile(formdata)
.then((f) => uploadSignature(
{
url: f.data.url,
fileName: f.data.fileName,
fileId: f.data.ossId,
businessId: this.userInfoData.userId
}
))
.then((res) => {
this.imgUrl2 = decodeURIComponent(res.data);
// this.user.avatar = decodeURIComponent(res.data);
})
.then(() => {
this.open = false;
this.$modal.msgSuccess("上传成功");
this.visible = false;
});
} else {
this.$modal.msgWarning("请先上传签名图片!");
}
});
},
// 实时预览
realTime(data) {
this.previews = data;
},
// 关闭窗口
closeDialog() {
this.options.img = "/assets/profile.jpg";
this.visible = false;
}
}
};
</script>
<style scoped lang="less">
.user-info-head {
position: relative;
display: inline-block;
height: 200px;
width: 200px;
img {
width: 200px;
height: 200px;
}
&.disable {
pointer-events: none;
}
}
.user-info-head:hover::after {
content: "+";
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
color: #EEEEEE;
background: rgba(0, 0, 0, 0.5);
font-size: 24px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
cursor: pointer;
line-height: 200px;
/* border-radius: 50%; */
}
::v-deep .row-btn {
padding-bottom: 15px;
.el-upload {
margin-top: 0;
}
}
.preview {
height: 350px;
position: relative;
display: flex;
justify-content: center;
align-items: center;
.avatar-upload-preview {
width: 200px;
height: 200px;
overflow: hidden;
}
}
</style>
问题分析
根据上述代码,vue-cropper
被用于处理用户上传的签名图片。用户选择图片后,图片会被裁剪并显示在预览区域。然而,问题在于如果上传的图片具有透明背景或白色背景,裁剪后的图片背景却变成了黑色。
可能的原因
-
默认输出格式为 JPEG:
- JPEG 格式不支持透明通道。如果裁剪后的图片被导出为 JPEG,任何透明区域都会被填充为黑色或其他默认颜色。
-
画布背景色设置:
- 在生成最终的画布或 Blob 时,如果没有明确指定背景颜色,某些库或工具可能会默认使用黑色填充。
-
样式覆盖:
- CSS 样式可能在某些层级覆盖了透明背景,导致看起来像是背景变成了黑色。
-
后端处理:
- 上传到后端后,服务器可能会对图片进行二次处理,如格式转换,导致透明背景丢失。
解决方案
针对上述问题,我们可以从以下几个方面入手:
1. 设置 vue-cropper
的输出格式为 PNG
vue-cropper
组件默认的输出格式可能是 JPEG。由于 JPEG 不支持透明背景,我们需要将输出格式设置为 PNG,以保留透明通道。
修改后的代码:
<vue-cropper
v-if="visible"
ref="cropper"
:img="options.img"
:info="true"
:auto-crop="options.autoCrop"
:auto-crop-width="options.autoCropWidth"
:auto-crop-height="options.autoCropHeight"
:fixed-box="options.fixedBox"
@realTime="realTime"
:output-type="'png'" <!-- 强制使用 PNG 格式 -->
/>
注意:根据 vue-cropper
的版本,属性名可能为 outputType
或 output-type
。请参考您使用的版本的文档进行确认。
2. 确保生成的 Blob/Canvas 保持透明通道
在获取裁剪后的 Blob 时,明确指定 MIME 类型为 image/png
,以确保透明背景被保留。
修改后的代码:
uploadImg() {
this.$refs.cropper.getCropBlob((data) => {
const formdata = new FormData();
if (Object.keys(this.fileInfo).length !== 0) {
formdata.append("file", data, this.options.filename);
formdata.append("bizPath", "picture");
formdata.append("uploadType", "minio");
uploadFile(formdata)
.then((f) => uploadSignature(
{
url: f.data.url,
fileName: f.data.fileName,
fileId: f.data.ossId,
businessId: this.userInfoData.userId
}
))
.then((res) => {
this.imgUrl2 = decodeURIComponent(res.data);
})
.then(() => {
this.open = false;
this.$modal.msgSuccess("上传成功");
this.visible = false;
});
} else {
this.$modal.msgWarning("请先上传签名图片!");
}
}, 'image/png'); // 指定 MIME 类型为 PNG
}
说明:getCropBlob
方法的第二个参数设置为 'image/png'
,确保生成的 Blob 保留透明通道。
3. 检查样式确保没有覆盖背景
确保所有相关的 CSS 样式没有为裁剪后的图片或其容器设置背景色,特别是避免设置为黑色。
示例检查:
.preview {
height: 350px;
position: relative;
display: flex;
justify-content: center;
align-items: center;
.avatar-upload-preview {
width: 200px;
height: 200px;
overflow: hidden;
/* 确保没有设置背景色 */
background-color: transparent;
}
}
说明:在 .avatar-upload-preview
类中,明确设置 background-color
为 transparent
,以避免任何意外的背景覆盖。
4. 确认后端处理不改变图片格式
确保上传到后端的图片保持 PNG 格式,避免服务器在处理时将其转换为 JPEG 或其他不支持透明通道的格式。
检查步骤:
- 上传接口:
- 确认
uploadFile
和uploadSignature
接口在处理图片时不进行格式转换。
- 确认
- 存储服务:
- 如果使用 OSS(对象存储服务)或其他存储服务,确保上传后图片格式未被改变。
- 返回图片 URL:
- 确认
this.imgUrl2
指向的 URL 返回的仍然是 PNG 格式的图片。
- 确认
详细代码实现
综合上述解决方案,以下是修改后的完整代码示例:
<template>
<div>
<div class="user-info-head" :class="notLoginUser ? 'disable' : ''" @click="editCropper()">
<img :src="imgUrl2" title="点击上传签名" class="img-circle img-lg">
</div>
<el-dialog
:title="title"
:visible.sync="open"
width="800px"
append-to-body
:close-on-click-modal="false"
@opened="modalOpened"
@close="closeDialog"
>
<el-row>
<el-col :xs="24" :md="12" :style="{ height: '350px' }">
<vue-cropper
v-if="visible"
ref="cropper"
:img="options.img"
:info="true"
:auto-crop="options.autoCrop"
:auto-crop-width="options.autoCropWidth"
:auto-crop-height="options.autoCropHeight"
:fixed-box="options.fixedBox"
@realTime="realTime"
:output-type="'png'" <!-- 设置输出类型为 PNG -->
/>
</el-col>
<el-col
:xs="24"
:md="12"
class="preview"
>
<div class="avatar-upload-preview">
<img :src="previews.url" :style="previews.img">
</div>
</el-col>
</el-row>
<br>
<el-row class="row-btn">
<el-col :lg="2" :md="2">
<el-upload
action="#"
:show-file-list="false"
:before-upload="beforeUpload"
>
<el-button>
选择
<i class="el-icon-upload el-icon--right"/>
</el-button>
</el-upload>
</el-col>
<el-col :lg="{ span: 1, offset: 2 }" :md="2">
<el-button icon="el-icon-plus" @click="changeScale(1)"/>
</el-col>
<el-col :lg="{ span: 1, offset: 1 }" :md="2">
<el-button icon="el-icon-minus" @click="changeScale(-1)"/>
</el-col>
<el-col :lg="{ span: 1, offset: 1 }" :md="2">
<el-button icon="el-icon-refresh-left" @click="rotateLeft()"/>
</el-col>
<el-col :lg="{ span: 1, offset: 1 }" :md="2">
<el-button icon="el-icon-refresh-right" @click="rotateRight()"/>
</el-col>
<el-col :lg="{ span: 2, offset: 6 }" :md="2">
<el-button type="primary" @click="uploadImg()"
v-hasPermi="['scm:system:scmCommonBusinessFile:add']"
>
提 交
</el-button>
</el-col>
</el-row>
</el-dialog>
</div>
</template>
<script>
import store from "@/store";
import { VueCropper } from "vue-cropper";
import imgUrl from "@/assets/signature.png"; // 引入图片方法一
import { downSignatureFile, uploadFile } from "@/api/file.js";
import { getImgUrl } from "@/utils/fileDownload.js";
import { uploadSignature, getOssByUserId } from "@/api/system/user.js";
export default {
name: "UserAvatar",
components: { VueCropper },
props: {
value: {
type: Object
}
},
data() {
return {
imgUrl1: imgUrl, // 引入图片方法一
imgUrl2: imgUrl, // 引入图片方法二
// 是否显示弹出层
open: false,
// 是否显示cropper
visible: false,
// 弹出层标题
title: "上传签名",
options: {
img: undefined, // 裁剪图片的地址
autoCrop: true, // 是否默认生成截图框
autoCropWidth: 200, // 默认生成截图框宽度
autoCropHeight: 200, // 默认生成截图框高度
fixedBox: true, // 固定截图框大小 不允许改变
filename: '' // 新增 filename
},
previews: {},
avatarImg: "",
fileInfo: {},
userInfoData: {}
};
},
computed: {
/**
* 用户信息非当前登录用户
*/
notLoginUser() {
return store.getters["user/userId"] !== this.user.userId;
},
user: {
get() {
return this.value;
},
set(value) {
this.$emit("input", value);
}
}
},
watch: {
user: {
handler(val) {
// 初始化头像
this.initAvatar(val);
},
immediate: true,
deep: true
}
},
methods: {
/**
* 初始化图片
*/
initAvatar(user) {
this.userInfoData = user;
let businessId = user.userId;
getOssByUserId(businessId)
.then((v) => downSignatureFile(v.data.fileId))
.then((v) => getImgUrl(v))
.then((v) => {
this.imgUrl2 = v;
});
},
// 编辑头像
editCropper() {
this.options.img = this.imgUrl2;
this.open = true;
},
// 打开弹出层结束时的回调
modalOpened() {
this.visible = true;
},
// 向左旋转
rotateLeft() {
this.$refs.cropper.rotateLeft();
},
// 向右旋转
rotateRight() {
this.$refs.cropper.rotateRight();
},
// 图片缩放
changeScale(num) {
num = num || 1;
this.$refs.cropper.changeScale(num);
},
// 上传预处理
beforeUpload(file) {
//限制严格点,支持JPG、PNG格式
if (file.type !== "image/png" && file.type !== "image/jpeg") {
this.$modal.msgError("文件格式错误,请上传图片类型,如:JPG,PNG后缀的文件。");
} else {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
this.options.img = reader.result;
this.fileInfo = file;
this.options.filename = file.name;
};
}
},
uploadImg() {
this.$refs.cropper.getCropBlob((data) => {
const formdata = new FormData();
if (Object.keys(this.fileInfo).length !== 0) {
formdata.append("file", data, this.options.filename);
formdata.append("bizPath", "picture");
formdata.append("uploadType", "minio");
uploadFile(formdata)
.then((f) => uploadSignature(
{
url: f.data.url,
fileName: f.data.fileName,
fileId: f.data.ossId,
businessId: this.userInfoData.userId
}
))
.then((res) => {
this.imgUrl2 = decodeURIComponent(res.data);
})
.then(() => {
this.open = false;
this.$modal.msgSuccess("上传成功");
this.visible = false;
});
} else {
this.$modal.msgWarning("请先上传签名图片!");
}
}, 'image/png'); // 指定 MIME 类型为 PNG
},
// 实时预览
realTime(data) {
this.previews = data;
},
// 关闭窗口
closeDialog() {
this.options.img = "/assets/profile.jpg";
this.visible = false;
}
}
};
</script>
<style scoped lang="less">
.user-info-head {
position: relative;
display: inline-block;
height: 200px;
width: 200px;
img {
width: 200px;
height: 200px;
}
&.disable {
pointer-events: none;
}
}
.user-info-head:hover::after {
content: "+";
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
color: #EEEEEE;
background: rgba(0, 0, 0, 0.5);
font-size: 24px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
cursor: pointer;
line-height: 200px;
/* border-radius: 50%; */
}
::v-deep .row-btn {
padding-bottom: 15px;
.el-upload {
margin-top: 0;
}
}
.preview {
height: 350px;
position: relative;
display: flex;
justify-content: center;
align-items: center;
.avatar-upload-preview {
width: 200px;
height: 200px;
overflow: hidden;
background-color: transparent; /* 确保背景透明 */
}
}
</style>
总结
在处理带有透明背景的图片裁剪时,确保输出格式支持透明通道(如 PNG)是关键。此外,明确指定生成 Blob 的 MIME 类型,检查样式避免背景覆盖,以及确保后端处理不改变图片格式,都是确保图片透明背景不被填充为黑色的重要步骤。通过上述方法,可以有效解决在 vue-cropper
组件中透明或白色背景变黑的问题,提升用户体验和图片质量。
参考资料
希望这篇技术博客能帮助到遇到类似问题的开发者,顺利解决图片裁剪中的背景问题。
评论区