diff --git a/docs/web/api/watermark.en-US.md b/docs/web/api/watermark.en-US.md index a746466678..c1c7afa589 100644 --- a/docs/web/api/watermark.en-US.md +++ b/docs/web/api/watermark.en-US.md @@ -29,3 +29,8 @@ spline: data ### Graylevel watermark {{ graylevel }} + +### Different Layout watermark +set layout to use different layout. + +{{ layout }} diff --git a/docs/web/api/watermark.md b/docs/web/api/watermark.md index f7eacc7704..2adeee4708 100644 --- a/docs/web/api/watermark.md +++ b/docs/web/api/watermark.md @@ -26,7 +26,11 @@ spline: data {{ movingImage }} - ### 图片灰阶水印 {{ graylevel }} + +### 不同布局的水印 +通过设置 layout 使用不同的布局。 + +{{ layout }} diff --git a/js/watermark/generateBase64Url.ts b/js/watermark/generateBase64Url.ts index 1f7170986a..00c1a1e441 100644 --- a/js/watermark/generateBase64Url.ts +++ b/js/watermark/generateBase64Url.ts @@ -1,4 +1,40 @@ -import { WatermarkText, WatermarkImage } from './type'; +import { WatermarkText, WatermarkImage, WatermarkLayout } from './type'; + +const ratio = window.devicePixelRatio || 1; + +// 元素中心为旋转点执行旋转 +const drawRotate = ( + ctx: CanvasRenderingContext2D, + x: number, + y: number, + rotate: number +) => { + ctx.translate(x, y); + ctx.rotate((Math.PI / 180) * Number(rotate)); + ctx.translate(-x, -y); +}; + +// 绘制文字 +const drawText = ( + ctx: CanvasRenderingContext2D, + x: number, + y: number, + markHeight: number, + text: string, + fontWeight: string, + fontSize: number, + fontFamily: string, + fillStyle: string +) => { + ctx.font = `normal normal ${fontWeight} ${ + fontSize * ratio + }px/${markHeight}px ${fontFamily}`; + ctx.fillStyle = fillStyle; + ctx.textAlign = 'start'; + ctx.textBaseline = 'top'; + + ctx.fillText(text, x, y); +}; export default function generateBase64Url({ width, @@ -11,64 +47,120 @@ export default function generateBase64Url({ alpha, watermarkContent, lineSpace, - fontColor = 'rgba(0,0,0,0.1)' + fontColor = 'rgba(0,0,0,0.1)', + layout, }: { width: number, height: number, - gapX:number, + gapX: number, gapY: number, - offsetLeft:number, - offsetTop:number, - rotate:number, - alpha:number, + offsetLeft: number, + offsetTop: number, + rotate: number, + alpha: number, watermarkContent: WatermarkText | WatermarkImage | Array, - lineSpace:number, - fontColor?:string -}, onFinish: (url: string) => void): string { + lineSpace: number, + fontColor?: string, + layout?: WatermarkLayout, +}, onFinish: (url: string, backgroundSize?: { width: number }) => void): string { + const isHexagonal = layout === 'hexagonal'; + const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); + if (!ctx) { // eslint-disable-next-line no-console console.warn('当前环境不支持Canvas, 无法绘制水印'); onFinish(''); return; } - const ratio = window.devicePixelRatio || 1; + + let actualBackgroundSize = { + width: gapX + width, + }; + const canvasWidth = (gapX + width) * ratio; const canvasHeight = (gapY + height) * ratio; + const markWidth = width * ratio; + const markHeight = height * ratio; + + const dislocationRotateX = canvasWidth; + const dislocationRotateY = canvasHeight; + const dislocationDrawX = (gapX + width) * ratio; + const dislocationDrawY = (gapY + height) * ratio; + canvas.width = canvasWidth; canvas.height = canvasHeight; canvas.style.width = `${gapX + width}px`; canvas.style.height = `${gapY + height}px`; + if (isHexagonal) { + canvas.style.width = `${canvasWidth * 2}px`; + canvas.style.height = `${canvasHeight * 2}px`; + canvas.width = canvasWidth * 2; + canvas.height = canvasHeight * 2; + + // 两倍宽度+间距 + actualBackgroundSize = { + width: gapX + width * 2 + width / 2, + }; + } + ctx.translate(offsetLeft * ratio, offsetTop * ratio); - ctx.rotate((Math.PI / 180) * Number(rotate)); ctx.globalAlpha = alpha; - const markWidth = width * ratio; - const markHeight = height * ratio; - ctx.fillStyle = 'transparent'; ctx.fillRect(0, 0, markWidth, markHeight); - const contents = Array.isArray(watermarkContent) ? watermarkContent : [{ ...watermarkContent }]; + const contents = Array.isArray(watermarkContent) + ? watermarkContent + : [{ ...watermarkContent }]; + let top = 0; + let imageLoadCount = 0; + let totalImages = 0; + + // 预处理 contents.forEach((item: WatermarkText & WatermarkImage & { top: number }) => { + // eslint-disable-next-line no-param-reassign + item.top = top; if (item.url) { - const { url, isGrayscale = false } = item; - // eslint-disable-next-line no-param-reassign - item.top = top; top += height; + totalImages += isHexagonal ? 2 : 1; // hexagonal布局需要绘制两次 + } else if (item.text) { + top += lineSpace; + } + }); + + // 绘制水印内容 + const renderWatermarkItem = ( + item: WatermarkText & WatermarkImage & { top: number }, + offsetX: number = 0, + offsetY: number = 0, + rotateX: number = 0, + rotateY: number = 0 + ) => { + if (item.url) { + const { url, isGrayscale = false } = item; const img = new Image(); img.crossOrigin = 'anonymous'; img.referrerPolicy = 'no-referrer'; img.src = url; img.onload = () => { - // ctx.filter = 'grayscale(1)'; - ctx.drawImage(img, 0, item.top * ratio, width * ratio, height * ratio); + ctx.save?.(); + drawRotate(ctx, rotateX, rotateY, rotate); + + // fix: 灰度效果只影响图片,不影响文字 if (isGrayscale) { - const imgData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height); + const tempCanvas = document.createElement('canvas'); + const tempCtx = tempCanvas.getContext('2d'); + tempCanvas.width = width * ratio; + tempCanvas.height = height * ratio; + + tempCtx.drawImage(img, 0, 0, width * ratio, height * ratio); + + const imgData = tempCtx.getImageData(0, 0, width * ratio, height * ratio); const pixels = imgData.data; for (let i = 0; i < pixels.length; i += 4) { const lightness = (pixels[i] + pixels[i + 1] + pixels[i + 2]) / 3; @@ -76,29 +168,79 @@ export default function generateBase64Url({ pixels[i + 1] = lightness; pixels[i + 2] = lightness; } - ctx.putImageData(imgData, 0, 0); + tempCtx.putImageData(imgData, 0, 0); + + ctx.drawImage( + tempCanvas, + offsetX, + offsetY + item.top * ratio, + width * ratio, + height * ratio + ); + } else { + ctx.drawImage( + img, + offsetX, + offsetY + item.top * ratio, + width * ratio, + height * ratio + ); + } + + ctx.restore?.(); + + // 图片加载完成再返回 + imageLoadCount += 1; + if (imageLoadCount === totalImages) { + onFinish(canvas.toDataURL(), actualBackgroundSize); } - onFinish(canvas.toDataURL()); }; } else if (item.text) { const { text, fontSize = 16, - fontFamily = undefined, + fontFamily = 'normal', fontWeight = 'normal', } = item; const fillStyle = item?.fontColor || fontColor; - // eslint-disable-next-line no-param-reassign - item.top = top; - top += lineSpace; - const markSize = Number(fontSize) * ratio; - // TODO 后续完善font 渲染控制 目前font-family 暂时为 undefined - ctx.font = `normal normal ${fontWeight} ${markSize}px/${markHeight}px ${fontFamily}`; - ctx.textAlign = 'start'; - ctx.textBaseline = 'top'; - ctx.fillStyle = fillStyle; - ctx.fillText(text, 0, item.top * ratio); + + ctx.save?.(); + drawRotate(ctx, rotateX, rotateY, rotate); + drawText( + ctx, + offsetX, + offsetY + item.top * ratio, + markHeight, + text, + fontWeight, + fontSize, + fontFamily, + fillStyle + ); + ctx.restore?.(); } + }; + + // 矩形水印 + contents.forEach((item: WatermarkText & WatermarkImage & { top: number }) => { + renderWatermarkItem(item, 0, 0, 0, 0); }); - onFinish(canvas.toDataURL()); + + // 六边形水印 + if (isHexagonal) { + contents.forEach((item: WatermarkText & WatermarkImage & { top: number }) => { + renderWatermarkItem( + item, + dislocationDrawX, + dislocationDrawY, + dislocationRotateX, + dislocationRotateY + ); + }); + } + + // 没有图片 + if (totalImages === 0) { + onFinish(canvas.toDataURL(), actualBackgroundSize); + } } diff --git a/js/watermark/type.ts b/js/watermark/type.ts index de4b2aa95f..417451c357 100644 --- a/js/watermark/type.ts +++ b/js/watermark/type.ts @@ -38,3 +38,5 @@ export interface WatermarkImage { */ url?: string; } + +export type WatermarkLayout = 'rectangular' | 'hexagonal';