h5海报图片合成实践

249 阅读2分钟

在项目开发中我们经常会用到图片合成这个功能,最近也是闲着没事儿干将这个功能封装了一下,只需要传递一个图片路径二维码配置便可得到一张海报图,使用示例如下

// 生成海报图片
const posterImgPath = await generatePoster({
    // 海报背景图片
	posterBgImgPath: '/img/vue.jpg',
    // 二维码选项配置
	qrCodeOptions: {
		width: 140,
		height: 140,
                // 二维码的纠错级别
		correctLevel: 0
	}
})

是不是很简单方便,完全符合海报图片合成较多的场景,如果你想使用该函数的话,那么请先安装以下包

// 安装 合成图片 和 生成二维码 的依赖包
pnpm i html2canvas qrcodejs2-fix

// 安装ts类型工具包
pnpm i @dyb-dev/ts-config -D

安装完成后还需要在tsconfig.json文件中的compilerOptions.types选项添加 @dyb-dev/ts-config/types 声明

示例代码如下:

"compilerOptions": {
    "types": ["@dyb-dev/ts-config/types"]
}

前置步骤完成后就可放心使用以下核心代码了:

import html2canvas from 'html2canvas'
// @ts-ignore
import QRCode from 'qrcodejs2-fix'

import type { Options as IHtml2canvasOptions } from 'html2canvas'

/** TPositionItem 类型表示位置,可以是数字或空值 */
type TPositionItem = number | null

/** IQrCodeOptions 接口表示 QRCode 配置选项 */
interface IQrCodeOptions {
    /** QRCode 链接数据 */
    text: string
    /** QRCode 图片宽度 */
    width?: number
    /** QRCode 图片高度 */
    height?: number
    /** QRCode 的深色部分颜色 */
    colorDark?: string
    /** QRCode 的浅色部分颜色 */
    colorLight?: string
    /** QRCode 的纠错级别 0 | 1 | 2 | 3 默认: 2 */
    correctLevel?: RangeOfNumbers<0, 3>
    /** 二维码位置信息 top,right,bottom,left 默认:[null,null,0,0] */
    position?: GenerateTuple<TPositionItem, 4>
    /** 二维码边框颜色 */
    borderColor?: string
    /** 二维码边框宽度,单位 px */
    borderWidth?: number
    /** 二维码圆角,单位 px */
    borderRadius?: number
}

/** 创建海报选项基础类型 */
interface IGeneratePosterOptionsBase {
    /** 海报盒子 DOM 元素 */
    posterBox: HTMLElement
    /** 海报底图绝对路径 */
    posterBgImgPath: string
    /** 生成海报的图片类型 默认: png */
    posterImgType?: 'jpg' | 'jpeg' | 'png' | 'webp' | 'bmp' | 'gif'
    /** QRCode 配置选项 */
    qrCodeOptions: IQrCodeOptions
    /** html2canvas 配置选项(可选) */
    html2canvasOptions?: IHtml2canvasOptions
}

/** 生成海报的联合类型 */
type TGeneratePosterOptions =
    | ModifyProperties<IGeneratePosterOptionsBase, 'posterBox'>
    | ModifyProperties<IGeneratePosterOptionsBase, 'posterBgImgPath'>


/** 重载类型 */
interface IGeneratePosterFn {
    (
        options: ModifyProperties<IGeneratePosterOptionsBase, 'posterBox'>
    ): Promise<string | void>
    (
        options: ModifyProperties<IGeneratePosterOptionsBase, 'posterBgImgPath'>
    ): Promise<string | void>
}

// 生成海报图片
const generatePoster: IGeneratePosterFn = async(
    options: TGeneratePosterOptions
): Promise<string | void> => {
    try {
        const {
            posterBox,
            posterBgImgPath,
            posterImgType = 'png',
            qrCodeOptions,
            html2canvasOptions = {}
        } = options

        const {
            position = [null, 0, 0, null],
            borderColor = '#ffffff',
            borderWidth = 10,
            borderRadius = 0
        } = qrCodeOptions

        /** 二维码占位图 div */
        const _qrCodeBox = document.createElement('div')
        _qrCodeBox.style.position = 'absolute'
        _qrCodeBox.style.boxSizing = 'border-box'
        _qrCodeBox.style.backgroundColor = borderColor
        _qrCodeBox.style.padding = `${borderWidth}px`
        _qrCodeBox.style.borderRadius = `${borderRadius}px`
        _qrCodeBox.style.inset = position
            .map((item) => typeof item === 'number' ? `${item}px` : 'auto')
            .join(' ')
        // 为了解决二维码图片(行内元素)与外部div(块元素)有间隔,导致二维码下边框高度与其他不一致的问题,设置字体大小为0
        _qrCodeBox.style.fontSize = '0'

        /** 默认的 QRCode 配置 */
        const _defaultQrCodeOptions: Partial<IQrCodeOptions> = {
            correctLevel: 2
        }
        // 生成二维码
        new QRCode(_qrCodeBox, {
            ..._defaultQrCodeOptions,
            ...qrCodeOptions
        })

        /** 海报盒子 */
        let _posterBox = posterBox

        // 如果未传入海报盒子,则创建一个
        if (!_posterBox) {
            _posterBox = document.createElement('div')
            _posterBox.style.position = 'absolute'
            _posterBox.style.top = '-9999px'
            _posterBox.style.left = '-9999px'
        }

        /** 海报底图 img */
        let _posterBgImg: HTMLImageElement | null = null

        // 如果传入了海报底图路径,则创建一个 img 元素
        if (posterBgImgPath) {
            _posterBgImg = document.createElement('img')
            _posterBgImg.src = posterBgImgPath
            _posterBgImg.style.display = 'block'
            _posterBox.appendChild(_posterBgImg)
        }

        _posterBox.appendChild(_qrCodeBox)
        document.body.appendChild(_posterBox)

        // 默认的 html2canvas 配置
        const _defaultHtml2canvasOptions: Partial<IHtml2canvasOptions> = {
            backgroundColor: null, // 生成出来的图片有白色边框 所以设置为 null
            allowTaint: true, // 允许跨域图像使用
            useCORS: true // 是否使用 CORS(跨域资源共享)头来加载跨域图像
        }

        // 使用 html2canvas 生成海报图
        const _canvas = await html2canvas(_posterBox, {
            ..._defaultHtml2canvasOptions,
            ...html2canvasOptions
        })

        // 将 canvas 转换为 data URL
        const _dataURL = _canvas.toDataURL(`image/${posterImgType}`)

        // 如果传入了海报盒子,则手动移除添加的子元素
        if (posterBox) {
            _posterBox.removeChild(_qrCodeBox)
            _posterBgImg && _posterBox.removeChild(_posterBgImg)
        } else {
            _posterBox.remove()
        }

        return _dataURL
    } catch (error) {
        console.error('generatePoster() error =>>', error)
        throw error
    }
}

希望能够帮助到大家!