RN 绘制水印

61 阅读1分钟
import { forwardRef, useCallback, useImperativeHandle, useRef } from 'react'
import Canvas, { Image as CanvasImage } from 'react-native-canvas'
import RNFS from 'react-native-fs'
import { Dimensions } from 'react-native'
import { useUserStore } from '@/store'
import { getAddressByLocation } from '@/utils/location'
import dayjs from 'dayjs'
import { formatError, sectcutArr } from '@/utils'

export type WatermarkArgs = {
	url?: string
	handleCustomDraw?: (ctx: any) => void
	maskText?: string[]
	width?: number
	height?: number
	uri?: string
}

export interface WatermarkRefType {
	addWatermark: (args: WatermarkArgs) => Promise<string>
	removeTask: (uri?: string) => void
}

type DrawImageParams = {
	args: WatermarkArgs
	resolve: (url: string) => void
	reject: (error?: any) => void
}

/*** 获取公共水印 */
export const getCommonMask = async () => {
	const address = await getAddressByLocation()
	const userInfo = useUserStore.getState().userInfo
	return [`${userInfo.nickName}  ${dayjs().format('YYYY-MM-DD HH:mm')}`, address]
}

const scale = Dimensions.get('window').scale

const WatermarkRoot = forwardRef<WatermarkRefType>((_, ref) => {
	const canvasRef = useRef<Canvas | null>(null)
	const taskQueue = useRef<DrawImageParams[]>([])

	const addWatermark = async (args: WatermarkArgs) => {
		return new Promise<string>((resolve, reject) => {
			if (!canvasRef.current) {
				console.error('Canvas not initialized')
				return reject(new Error('Canvas not initialized'))
			}
			const task = { args, resolve, reject }
			if (!taskQueue.current.find((t) => t.args.url === args.url)) {
				taskQueue.current.push(task)
				if (taskQueue.current.length === 1) {
					drawImage(task)
				} else {
					handleEventTracking({
						tip: 'canvas绘制任务被挂起--Watermark/index.tsx--55',
						data: {
							taskQueue: taskQueue.current,
						},
					})
				}
			}
		})
	}

	/*** 处理任务队列 */
	const processNextTask = () => {
		taskQueue.current.shift()
		if (taskQueue.current.length > 0) {
			drawImage(taskQueue.current[0])
		}
	}

	/*** 使用Canvas绘制水印 */
	const drawImage = ({ args, resolve, reject }: DrawImageParams) => {
		const canvas = canvasRef.current
		if (!canvas) return reject(new Error('Canvas instance not available'))

		const { handleCustomDraw, maskText = [], width = 1080, height = 1440 } = args
		const imageWidth = width / scale
		const imageHeight = height / scale

		canvas.width = imageWidth
		canvas.height = imageHeight
		const ctx = canvas.getContext('2d')
		const image = new CanvasImage(canvas)
		image.crossOrigin = 'Anonymous'
		image.addEventListener('load', async () => {
			try {
				ctx.drawImage(image, 0, 0, imageWidth, imageHeight)
				ctx.fillStyle = '#fff'
				const size = Math.floor(imageWidth / 34)
				ctx.font = `${size}px PingFang SC`

				const commonText = await getCommonMask()
				const handlerMaskText = sectcutArr([...maskText, ...commonText])
				const bgHeight = handlerMaskText.length * 1.6 * size
				const maskHeight = imageHeight - bgHeight

				handlerMaskText.forEach((item, index) => {
					ctx.fillText(item, size, maskHeight + (index + 1) * 1.4 * size)
				})

				if (handleCustomDraw) handleCustomDraw(ctx)

				const dataURL = await canvas.toDataURL('image/jpeg', 0.8)
				const url = await base64ToFile(dataURL)

				handleEventTracking({
					tip: '图片水印绘制成功--Watermark/index.tsx--96',
					data: {
						taskQueue: taskQueue.current.length,
						url,
					},
				})

				resolve(url)
			} catch (error) {
				reject(error)
				handleEventTracking({
					tip: '捕获图片水印绘制失败--Watermark/index.tsx--129',
					data: {
						errorObj: formatError(error),
						taskQueue: taskQueue.current,
					},
				})
			} finally {
				processNextTask()
			}
		})

		image.addEventListener('error', (error: any) => {
			handleEventTracking({
				tip: '绘制水印时加载图片失败--Watermark/index.tsx--77',
				data: { errorObj: formatError(error), base64: image.src },
			})
			reject(error)
			processNextTask()
		})

		image.src = `data:image/png;base64,${args.url}`
	}

	/*** base64 转文件路径 */
	const base64ToFile = async (base64: string) => {
		const imageData = base64.split('data:image/jpeg;base64,')[1]
		const imagePath = `${RNFS.TemporaryDirectoryPath}/${Date.now()}.jpg`
		await RNFS.writeFile(imagePath, imageData, 'base64')
		return `file://${imagePath}`
	}

	/*** 删除绘制中的任务 */
	const removeTask = (taskUri?: string) => {
		const flag = taskQueue.current.find((task) => task.args.uri === taskUri)
		if (flag) {
			taskQueue.current = taskQueue.current.filter((task) => task.args.uri !== taskUri)
			if (taskQueue.current.length > 0) {
				drawImage(taskQueue.current[0])
			}
		}
	}

	useImperativeHandle(ref, () => ({ addWatermark, removeTask }), [])
	return <Canvas ref={canvasRef} style={{ position: 'absolute', left: -9999, top: -9999 }} />
})

export default WatermarkRoot