Skip to main content

Command Palette

Search for a command to run...

Canvas之于图片妙用

使用Canvas给图片增加水印

Published
3 min read
T

A simple developer.

在C端业务中,我们经常需要对用户上传的图片添加水印,这里提供一个灵活便捷的文字水印添加工具:

调用时仅需要:

waterMarkImg = waterMarkGen.init({ textArr: ['自定义水印文字'] });

源码:

const HALF_DEG = 180;
const TWO = 2;

export default {
    settings:{ // 默认配置
        textArr: ['默认水印文字', '多行的,我是第二行'],
        textAlign: 'left',
        font: '12px "Microsoft Yahei"',
        fillStyle: 'rgba(0,0,0,0.2)', // 文字填充颜色
        maxWidth: 100, // 单行文字最大宽度,超出会自动换一行
        minWidth: 60,
        lineHeight: 16, // 文字行高
        deg: -45, // 文字旋转角度
        marginRight: 10, // 水印区块右边距(这里其实对内是边距,输出后对外其实是内边距padding)
        marginBottom: 10,
        left: 0,
        top: 0,
    },
    init: funciton (options) { // 水印生成:输出一张水印database64图片
        let textArr;
        this.settings = Object.assign(this.settings, options || {}); // 合并选项
        this.settings.minWidth = Math.min(this.settings.maxWidth, this.settings.minWidth); // 防止出现maxWidth < minWidth的奇怪情况,做兼容处理
        textArr = this.settings.textArr;

        if (Object.prototype.toString.call(textArr) !== '[Object Array]') { // 当需要渲染的水印文字非数组时返回错误
            throw Error('textArr 水印文本必须是数组类型');
        }

        const c = this._createCanvas(); // 创建Canvas对象
        this._draw(c, this.settings); // 按照配置绘制水印内容

        return this._convertCanvasToImage(c); // 将当前Canvas对象输出为图片database64 - url
    },
    _createCanvas: function () { // 创建一个不可见的canvas对象并返回
        let c = document.createElement('canvas');
        c.style.display = 'none';
        document.body.appendChild(c);
        return c;
    },
    _draw: function (c, settings) {
        let ctx = c.getContext('2d'),
            textArr = settings.textArr || [],
            wordBreakTextArr = [],
            maxWidthArr = [];

        textArr.forEach(function (text) {

            // 计算在当前所设定最大宽度/字体下 当前文字需要的行数,与最大宽度
            let result = this._breakLinesForCanvas(ctx, text + '', settings.maxWidth, settings.font);

            wordBreakTextArr.push(...result);
            maxWidthArr.push(result.maxWidth);
        });

        maxWidthArr.sort(function (a, b) { return b - a; });

        // 根据切割结果动态修改canvas宽高
        const maxWidth = Math.max(maxWidthArr[0], settings.minWidth),
            lineHeight = settings.lineHeight,
            height = workBreakTextArr.length * lineHeight, // 高度 = 渲染文字行数 × 行高
            degToPI = Math.PI * settings.deg / HALF_DEG, // 角度换算
            absDeg = Math.abs(degToPI),
            hSinDeg = height * Math.sin(absDeg),
            hCosDeg = height * Math.cos(absDeg),
            wSinDeg = maxWidth * Math.sin(absDeg),
            wCosDeg = maxWidth * Math.cos(absDeg);

        c.width = parseInt(hSinDeg + wCosDeg + settings.marginRight, 10); // 计算canvas宽度
        c.height = parseInt(wSinDeg + hCosDeg + settings.marginBottom, 10);

        // 同时重绘样式
        ctx.font = settings.font;
        ctx.fillStyle = settings.fillStyle;
        ctx.textBaseline = 'hanging'; // 默认为alphabetic,这里改为基准线为贴着线的方式

        // 移动并旋转画布
        ctx.translate(0, wSinDeg);
        ctx.rotate(degToPI);

        // 绘制文本到画布
        wordBreakTextArr.forEach(function (wordInfo, index) {

            // 居中情况需要动态的去计算文本起始点的位置(x, y)
            let x = null;
            switch (settings.textAlign) {
                case 'left':
                    x = 0; // 从最左开始
                    break;
                case 'right':
                    x = maxWidth - wordInfo.maxWidth; // 最右边 - 本身文字内容的宽度
                    break;
                case 'center':
                    x = (maxWidth - wordInfo.maxWidth) / TWO;
                    break;
                default:
                    x = 0;
                    break;
            };
            ctx.fillText(wordInfo.textArr[0], x, lineHeight * index); // 绘制到画布指定位置
        });
    },

    //转换canvas --> dabase64URL
    _convertCanvasToImage: function (canvas, type = 'image/png') {
        return canvas.toDataURL(type);
    },

    // 根据给定的每行最大宽度,切割文字(自动换行功能)
    _breakLinesForCanvas: function (ctx, text, width, font) {
        let textArr = [],
            maxWidth = 0;

        font && (ctx.font = font);

        let breakPoint = this._findBreakPoint(text, width, ctx); // 寻找换行断点,返回indexindex
        while (breakPoint !== -1) { // 循环,知道当前text全部都无需换行
            textArr.push(text.substr(0, breakPoint)); // 将单行最大宽度可容纳 文本 截取,然后继续循环换行
            text = text.substr(breakPoint);
            maxWidth = width;
            breakPoint = this._findBreakPoint(text, width, ctx);
        }

        // (最后剩下的部分无需换行文本 | 当前文本无需换行未走循环情况),加入数组
        if (text) {
            textArr.push(text);
            const lastTextWidth = context.measureText(text).width;
            !maxWidth && (maxWidth = lastTextWidth); // maxWidth为0时(未换过行),最大宽度即为当前文本宽度
        }

        return {
            textArr,
            maxWidth
        };
    },

    _findBreakPoint: function (text, width, ctx) {
        let min = 0,
            max = text.length - 1;

        // 二分查找
        while (min <= max) {
            const middle = Math.floor((min + max) / TWO),
                middleWidth = ctx.measureText(text.substr(0, middle)).width,
                onCharWiderThanMiddleWidth = ctx.measureText(text.substr(0, middle + 1)).width; // 计算比middle多一个字符的宽度

            // 已找到断点
            if (middleWidth <= width && oneCharWiderThanMiddleWidth > width) {
                return middle;
            }
            if (middleWidth < width) {
                min = middle + 1; // 后移middle中点,继续寻找
            } else {
                max = middle - 1; // 前移middle中点,继续查找
            }
        }

        return -1; // 无需分割
    }
};
T
Taoist4y ago

搞得不错,别搞了

1

More from this blog

Git 无法正常访问问题解决 —— DNS无法解析git地址

在国内网络经常会无法访问github,一般呈现错误为: github.com 打不开 git 操作 clone/push/pull/fetch 超时等 这普遍由于国内DNS解析github相关域名的问题,国内DNS解析污染会导致,无法解析github相关域名,使得我们无法获悉正确ip而无法访问。 偏方一剂:修改Host,固定绑定 domain - ip 查询相关域名的解析 IPAddress github.com github.global.ssl.fastly.net asse...

Sep 22, 20221 min read

Vue SSR实战小练

思想 开发环境: 在webpack dev与pro的前端打包构建基础上, 添加webpack server compiler的服务,其为单独创建的node服务,用于渲染html代码并返回给客户端。(其他JS...则仍旧交由webpack dev server来构建) 所以在获取html后要再自行将dev客户端渲染的js加入到html中 在生产环境则不需如此: 由dev与server打包好的文件,将其组合 客户端则访问node服务来获取文件 基本用法 安装:npm install v...

Sep 5, 20229 min read

Jsonp 跨域解决方案解析

由来背景 As we all know,浏览器同源策略会将非同源请求(跨域)抛弃,而许多时候我们可能并未将前端资源与服务端服务放在一个服务器,此时就需要一个跨域的手段了。 基于此,针对同源策略,衍生了一种跨域的方法Jsonp。 Jsonp简单讲,即利用了script标签不受浏览器同源策略影响的特性,从而利用script想服务端跨域请求的方式。 原理解析 图下图所示 封装一个Jsonp函数,Promise化 // jsonp.ts type JsonpOptions<T extends Reco...

Sep 2, 20222 min read
Jsonp 跨域解决方案解析

唯在一心

15 posts

A simple developer.