侧边栏壁纸
  • 累计撰写 74 篇文章
  • 累计创建 31 个标签
  • 累计收到 5 条评论

目 录CONTENT

文章目录
vue

如何解决 Vue-Cropper 组件中透明或白色背景变黑的问题

PeakGao
2025-01-24 / 0 评论 / 0 点赞 / 2 阅读 / 0 字 / 正在检测是否收录...
温馨提示:
部分素材可能会来自网络,若不小心影响到您的利益,请联系我们删除。

在前端开发中,处理用户上传的图片并进行裁剪是常见的需求。使用 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 被用于处理用户上传的签名图片。用户选择图片后,图片会被裁剪并显示在预览区域。然而,问题在于如果上传的图片具有透明背景或白色背景,裁剪后的图片背景却变成了黑色。

可能的原因

  1. 默认输出格式为 JPEG

    • JPEG 格式不支持透明通道。如果裁剪后的图片被导出为 JPEG,任何透明区域都会被填充为黑色或其他默认颜色。
  2. 画布背景色设置

    • 在生成最终的画布或 Blob 时,如果没有明确指定背景颜色,某些库或工具可能会默认使用黑色填充。
  3. 样式覆盖

    • CSS 样式可能在某些层级覆盖了透明背景,导致看起来像是背景变成了黑色。
  4. 后端处理

    • 上传到后端后,服务器可能会对图片进行二次处理,如格式转换,导致透明背景丢失。

解决方案

针对上述问题,我们可以从以下几个方面入手:

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 的版本,属性名可能为 outputTypeoutput-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-colortransparent,以避免任何意外的背景覆盖。

4. 确认后端处理不改变图片格式

确保上传到后端的图片保持 PNG 格式,避免服务器在处理时将其转换为 JPEG 或其他不支持透明通道的格式。

检查步骤

  1. 上传接口
    • 确认 uploadFileuploadSignature 接口在处理图片时不进行格式转换。
  2. 存储服务
    • 如果使用 OSS(对象存储服务)或其他存储服务,确保上传后图片格式未被改变。
  3. 返回图片 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 组件中透明或白色背景变黑的问题,提升用户体验和图片质量。

参考资料

希望这篇技术博客能帮助到遇到类似问题的开发者,顺利解决图片裁剪中的背景问题。

0
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin

评论区