wd-watermark.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  1. <!--
  2. * @Author: weisheng
  3. * @Date: 2023-04-05 21:32:56
  4. * @LastEditTime: 2025-04-28 19:41:17
  5. * @LastEditors: weisheng
  6. * @Description: 水印组件
  7. * @FilePath: /wot-design-uni/src/uni_modules/wot-design-uni/components/wd-watermark/wd-watermark.vue
  8. * 记得注释
  9. -->
  10. <template>
  11. <view :class="rootClass" :style="rootStyle">
  12. <canvas
  13. v-if="!canvasOffScreenable && showCanvas"
  14. type="2d"
  15. :style="{ height: canvasHeight + 'px', width: canvasWidth + 'px', visibility: 'hidden' }"
  16. :canvas-id="canvasId"
  17. :id="canvasId"
  18. />
  19. </view>
  20. </template>
  21. <script lang="ts">
  22. export default {
  23. name: 'wd-watermark',
  24. options: {
  25. addGlobalClass: true,
  26. virtualHost: true,
  27. styleIsolation: 'shared'
  28. }
  29. }
  30. </script>
  31. <script lang="ts" setup>
  32. import { computed, onMounted, ref, watch, nextTick, type CSSProperties } from 'vue'
  33. import { addUnit, buildUrlWithParams, isBase64Image, objToStyle, uuid } from '../common/util'
  34. import { watermarkProps } from './types'
  35. const props = defineProps(watermarkProps)
  36. watch(
  37. () => props,
  38. () => {
  39. doReset()
  40. },
  41. { deep: true }
  42. )
  43. const canvasId = ref<string>(`water${uuid()}`) // canvas 组件的唯一标识符
  44. const waterMarkUrl = ref<string>('') // canvas生成base64水印
  45. const canvasOffScreenable = ref<boolean>(uni.canIUse('createOffscreenCanvas') && Boolean(uni.createOffscreenCanvas)) // 是否可以使用离屏canvas
  46. const pixelRatio = ref<number>(uni.getSystemInfoSync().pixelRatio) // 像素比
  47. const canvasHeight = ref<number>((props.height + props.gutterY) * pixelRatio.value) // canvas画布高度
  48. const canvasWidth = ref<number>((props.width + props.gutterX) * pixelRatio.value) // canvas画布宽度
  49. const showCanvas = ref<boolean>(true) // 是否展示canvas
  50. /**
  51. * 水印css类
  52. */
  53. const rootClass = computed(() => {
  54. let classess: string = 'wd-watermark'
  55. if (props.fullScreen) {
  56. classess = `${classess} is-fullscreen`
  57. }
  58. return `${classess} ${props.customClass}`
  59. })
  60. /**
  61. * 水印样式
  62. */
  63. const rootStyle = computed(() => {
  64. const style: CSSProperties = {
  65. opacity: props.opacity,
  66. backgroundSize: addUnit(props.width + props.gutterX)
  67. }
  68. if (waterMarkUrl.value) {
  69. style['backgroundImage'] = `url('${waterMarkUrl.value}')`
  70. }
  71. return `${objToStyle(style)}${props.customStyle}`
  72. })
  73. onMounted(() => {
  74. doInit()
  75. })
  76. function doReset() {
  77. showCanvas.value = true
  78. canvasHeight.value = (props.height + props.gutterY) * pixelRatio.value
  79. canvasWidth.value = (props.width + props.gutterX) * pixelRatio.value
  80. nextTick(() => {
  81. doInit()
  82. })
  83. }
  84. function doInit() {
  85. // #ifdef H5
  86. // h5使用document.createElement创建canvas,不用展示canvas标签
  87. showCanvas.value = false
  88. // #endif
  89. const { width, height, color, size, fontStyle, fontWeight, fontFamily, content, rotate, gutterX, gutterY, image, imageHeight, imageWidth } = props
  90. // 创建水印
  91. createWaterMark(width, height, color, size, fontStyle, fontWeight, fontFamily, content, rotate, gutterX, gutterY, image, imageHeight, imageWidth)
  92. }
  93. /**
  94. * 创建水印图片
  95. * @param width canvas宽度
  96. * @param height canvas高度
  97. * @param color canvas字体颜色
  98. * @param size canvas字体大小
  99. * @param fontStyle canvas字体样式
  100. * @param fontWeight canvas字体字重
  101. * @param fontFamily canvas字体系列
  102. * @param content canvas内容
  103. * @param rotate 倾斜角度
  104. * @param gutterX X轴间距
  105. * @param gutterY Y轴间距
  106. * @param image canvas图片
  107. * @param imageHeight canvas图片高度
  108. * @param imageWidth canvas图片宽度
  109. */
  110. function createWaterMark(
  111. width: number,
  112. height: number,
  113. color: string,
  114. size: number,
  115. fontStyle: string,
  116. fontWeight: number | string,
  117. fontFamily: string,
  118. content: string,
  119. rotate: number,
  120. gutterX: number,
  121. gutterY: number,
  122. image: string,
  123. imageHeight: number,
  124. imageWidth: number
  125. ) {
  126. const canvasHeight = (height + gutterY) * pixelRatio.value
  127. const canvasWidth = (width + gutterX) * pixelRatio.value
  128. const contentWidth = width * pixelRatio.value
  129. const contentHeight = height * pixelRatio.value
  130. const fontSize = size * pixelRatio.value
  131. // #ifndef H5
  132. if (canvasOffScreenable.value) {
  133. createOffscreenCanvas(
  134. canvasHeight,
  135. canvasWidth,
  136. contentWidth,
  137. contentHeight,
  138. rotate,
  139. fontSize,
  140. fontFamily,
  141. fontStyle,
  142. fontWeight,
  143. color,
  144. content,
  145. image,
  146. imageHeight,
  147. imageWidth
  148. )
  149. } else {
  150. createCanvas(canvasHeight, contentWidth, rotate, fontSize, color, content, image, imageHeight, imageWidth)
  151. }
  152. // #endif
  153. // #ifdef H5
  154. createH5Canvas(
  155. canvasHeight,
  156. canvasWidth,
  157. contentWidth,
  158. contentHeight,
  159. rotate,
  160. fontSize,
  161. fontFamily,
  162. fontStyle,
  163. fontWeight,
  164. color,
  165. content,
  166. image,
  167. imageHeight,
  168. imageWidth
  169. )
  170. // #endif
  171. }
  172. /**
  173. * 创建离屏canvas
  174. * @param canvasHeight canvas高度
  175. * @param canvasWidth canvas宽度
  176. * @param contentWidth 内容宽度
  177. * @param contentHeight 内容高度
  178. * @param rotate 内容倾斜角度
  179. * @param fontSize 字体大小
  180. * @param fontFamily 字体系列
  181. * @param fontStyle 字体样式
  182. * @param fontWeight 字体字重
  183. * @param color 字体颜色
  184. * @param content 内容
  185. * @param image canvas图片
  186. * @param imageHeight canvas图片高度
  187. * @param imageWidth canvas图片宽度
  188. */
  189. function createOffscreenCanvas(
  190. canvasHeight: number,
  191. canvasWidth: number,
  192. contentWidth: number,
  193. contentHeight: number,
  194. rotate: number,
  195. fontSize: number,
  196. fontFamily: string,
  197. fontStyle: string,
  198. fontWeight: string | number,
  199. color: string,
  200. content: string,
  201. image: string,
  202. imageHeight: number,
  203. imageWidth: number
  204. ) {
  205. // 创建离屏canvas
  206. const canvas: any = uni.createOffscreenCanvas({ height: canvasHeight, width: canvasWidth, type: '2d' })
  207. const ctx: any = canvas.getContext('2d')
  208. if (ctx) {
  209. if (image) {
  210. const img = canvas.createImage() as HTMLImageElement
  211. drawImageOffScreen(ctx, img, image, imageHeight, imageWidth, rotate, contentWidth, contentHeight, canvas)
  212. } else {
  213. drawTextOffScreen(ctx, content, contentWidth, contentHeight, rotate, fontSize, fontFamily, fontStyle, fontWeight, color, canvas)
  214. }
  215. } else {
  216. console.error('无法获取canvas上下文,请确认当前环境是否支持canvas')
  217. }
  218. }
  219. /**
  220. * 非H5创建canvas
  221. * 不支持创建离屏canvas时调用
  222. * @param contentHeight 内容高度
  223. * @param contentWidth 内容宽度
  224. * @param rotate 内容倾斜角度
  225. * @param fontSize 字体大小
  226. * @param color 字体颜色
  227. * @param content 内容
  228. * @param image canvas图片
  229. * @param imageHeight canvas图片高度
  230. * @param imageWidth canvas图片宽度
  231. */
  232. function createCanvas(
  233. contentHeight: number,
  234. contentWidth: number,
  235. rotate: number,
  236. fontSize: number,
  237. color: string,
  238. content: string,
  239. image: string,
  240. imageHeight: number,
  241. imageWidth: number
  242. ) {
  243. const ctx = uni.createCanvasContext(canvasId.value)
  244. if (ctx) {
  245. if (image) {
  246. drawImageOnScreen(ctx, image, imageHeight, imageWidth, rotate, contentWidth, contentHeight)
  247. } else {
  248. drawTextOnScreen(ctx, content, contentWidth, rotate, fontSize, color)
  249. }
  250. } else {
  251. console.error('无法获取canvas上下文,请确认当前环境是否支持canvas')
  252. }
  253. }
  254. /**
  255. * h5创建canvas
  256. * @param canvasHeight canvas高度
  257. * @param canvasWidth canvas宽度
  258. * @param contentWidth 水印内容宽度
  259. * @param contentHeight 水印内容高度
  260. * @param rotate 水印内容倾斜角度
  261. * @param fontSize 水印字体大小
  262. * @param fontFamily 水印字体系列
  263. * @param fontStyle 水印字体样式
  264. * @param fontWeight 水印字体字重
  265. * @param color 水印字体颜色
  266. * @param content 水印内容
  267. */
  268. function createH5Canvas(
  269. canvasHeight: number,
  270. canvasWidth: number,
  271. contentWidth: number,
  272. contentHeight: number,
  273. rotate: number,
  274. fontSize: number,
  275. fontFamily: string,
  276. fontStyle: string,
  277. fontWeight: string | number,
  278. color: string,
  279. content: string,
  280. image: string,
  281. imageHeight: number,
  282. imageWidth: number
  283. ) {
  284. const canvas = document.createElement('canvas')
  285. const ctx = canvas.getContext('2d')
  286. canvas.setAttribute('width', `${canvasWidth}px`)
  287. canvas.setAttribute('height', `${canvasHeight}px`)
  288. if (ctx) {
  289. if (image) {
  290. const img = new Image()
  291. drawImageOffScreen(ctx, img, image, imageHeight, imageWidth, rotate, contentWidth, contentHeight, canvas)
  292. } else {
  293. drawTextOffScreen(ctx, content, contentWidth, contentHeight, rotate, fontSize, fontFamily, fontStyle, fontWeight, color, canvas)
  294. }
  295. } else {
  296. console.error('无法获取canvas上下文,请确认当前环境是否支持canvas')
  297. }
  298. }
  299. /**
  300. * 绘制离屏文字canvas
  301. * @param ctx canvas上下文
  302. * @param content 水印内容
  303. * @param contentWidth 水印宽度
  304. * @param contentHeight 水印高度
  305. * @param rotate 水印内容倾斜角度
  306. * @param fontSize 水印字体大小
  307. * @param fontFamily 水印字体系列
  308. * @param fontStyle 水印字体样式
  309. * @param fontWeight 水印字体字重
  310. * @param color 水印字体颜色
  311. * @param canvas canvas实例
  312. */
  313. function drawTextOffScreen(
  314. ctx: CanvasRenderingContext2D,
  315. content: string,
  316. contentWidth: number,
  317. contentHeight: number,
  318. rotate: number,
  319. fontSize: number,
  320. fontFamily: string,
  321. fontStyle: string,
  322. fontWeight: string | number,
  323. color: string,
  324. canvas: HTMLCanvasElement
  325. ) {
  326. ctx.textBaseline = 'middle'
  327. ctx.textAlign = 'center'
  328. ctx.translate(contentWidth / 2, contentWidth / 2)
  329. ctx.rotate((Math.PI / 180) * rotate)
  330. ctx.font = `${fontStyle} normal ${fontWeight} ${fontSize}px/${contentHeight}px ${fontFamily}`
  331. ctx.fillStyle = color
  332. ctx.fillText(content, 0, 0)
  333. ctx.restore()
  334. waterMarkUrl.value = canvas.toDataURL()
  335. }
  336. /**
  337. * 绘制在屏文字canvas
  338. * @param ctx canvas上下文
  339. * @param content 水印内容
  340. * @param contentWidth 水印宽度
  341. * @param rotate 水印内容倾斜角度
  342. * @param fontSize 水印字体大小
  343. * @param color 水印字体颜色
  344. */
  345. function drawTextOnScreen(ctx: UniApp.CanvasContext, content: string, contentWidth: number, rotate: number, fontSize: number, color: string) {
  346. ctx.setTextBaseline('middle')
  347. ctx.setTextAlign('center')
  348. ctx.translate(contentWidth / 2, contentWidth / 2)
  349. ctx.rotate((Math.PI / 180) * rotate)
  350. ctx.setFillStyle(color)
  351. ctx.setFontSize(fontSize)
  352. ctx.fillText(content, 0, 0)
  353. ctx.restore()
  354. ctx.draw()
  355. // #ifdef MP-DINGTALK
  356. // 钉钉小程序的canvasToTempFilePath接口与其他平台不一样
  357. ;(ctx as any).toTempFilePath({
  358. success(res: any) {
  359. showCanvas.value = false
  360. waterMarkUrl.value = res.filePath
  361. }
  362. })
  363. // #endif
  364. // #ifndef MP-DINGTALK
  365. uni.canvasToTempFilePath({
  366. canvasId: canvasId.value,
  367. success: (res) => {
  368. showCanvas.value = false
  369. waterMarkUrl.value = res.tempFilePath
  370. }
  371. })
  372. // #endif
  373. }
  374. /**
  375. * 绘制离屏图片canvas
  376. * @param ctx canvas上下文
  377. * @param img 水印图片对象
  378. * @param image 水印图片地址
  379. * @param imageHeight 水印图片高度
  380. * @param imageWidth 水印图片宽度
  381. * @param rotate 水印内容倾斜角度
  382. * @param contentWidth 水印宽度
  383. * @param contentHeight 水印高度
  384. * @param canvas canvas实例
  385. */
  386. async function drawImageOffScreen(
  387. ctx: CanvasRenderingContext2D,
  388. img: HTMLImageElement,
  389. image: string,
  390. imageHeight: number,
  391. imageWidth: number,
  392. rotate: number,
  393. contentWidth: number,
  394. contentHeight: number,
  395. canvas: HTMLCanvasElement
  396. ) {
  397. ctx.translate(contentWidth / 2, contentHeight / 2)
  398. ctx.rotate((Math.PI / 180) * Number(rotate))
  399. img.crossOrigin = 'anonymous'
  400. img.referrerPolicy = 'no-referrer'
  401. if (isBase64Image(image)) {
  402. img.src = image
  403. } else {
  404. img.src = buildUrlWithParams(image, {
  405. timestamp: `${new Date().getTime()}`
  406. })
  407. }
  408. img.onload = () => {
  409. ctx.drawImage(
  410. img,
  411. (-imageWidth * pixelRatio.value) / 2,
  412. (-imageHeight * pixelRatio.value) / 2,
  413. imageWidth * pixelRatio.value,
  414. imageHeight * pixelRatio.value
  415. )
  416. ctx.restore()
  417. waterMarkUrl.value = canvas.toDataURL()
  418. }
  419. }
  420. /**
  421. * 绘制在屏图片canvas
  422. * @param ctx canvas上下文
  423. * @param image 水印图片地址
  424. * @param imageHeight 水印图片高度
  425. * @param imageWidth 水印图片宽度
  426. * @param rotate 水印内容倾斜角度
  427. * @param contentWidth 水印宽度
  428. * @param contentHeight 水印高度
  429. */
  430. function drawImageOnScreen(
  431. ctx: UniApp.CanvasContext,
  432. image: string,
  433. imageHeight: number,
  434. imageWidth: number,
  435. rotate: number,
  436. contentWidth: number,
  437. contentHeight: number
  438. ) {
  439. ctx.translate(contentWidth / 2, contentHeight / 2)
  440. ctx.rotate((Math.PI / 180) * Number(rotate))
  441. ctx.drawImage(
  442. image,
  443. (-imageWidth * pixelRatio.value) / 2,
  444. (-imageHeight * pixelRatio.value) / 2,
  445. imageWidth * pixelRatio.value,
  446. imageHeight * pixelRatio.value
  447. )
  448. ctx.restore()
  449. ctx.draw(false, () => {
  450. // #ifdef MP-DINGTALK
  451. // 钉钉小程序的canvasToTempFilePath接口与其他平台不一样
  452. ;(ctx as any).toTempFilePath({
  453. success(res: any) {
  454. showCanvas.value = false
  455. waterMarkUrl.value = res.filePath
  456. }
  457. })
  458. // #endif
  459. // #ifndef MP-DINGTALK
  460. uni.canvasToTempFilePath({
  461. canvasId: canvasId.value,
  462. success: (res) => {
  463. showCanvas.value = false
  464. waterMarkUrl.value = res.tempFilePath
  465. }
  466. })
  467. // #endif
  468. })
  469. }
  470. </script>
  471. <style lang="scss" scoped>
  472. @import './index.scss';
  473. </style>