Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/web/api/watermark.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,8 @@ spline: data
### Graylevel watermark

{{ graylevel }}

### Different Layout watermark
set layout to use different layout.

{{ layout }}
6 changes: 5 additions & 1 deletion docs/web/api/watermark.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ spline: data

{{ movingImage }}


### 图片灰阶水印

{{ graylevel }}

### 不同布局的水印
通过设置 layout 使用不同的布局。

{{ layout }}
214 changes: 178 additions & 36 deletions js/watermark/generateBase64Url.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -11,94 +47,200 @@ 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<WatermarkText | WatermarkImage>,
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;
pixels[i] = lightness;
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);
}
}
2 changes: 2 additions & 0 deletions js/watermark/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,5 @@ export interface WatermarkImage {
*/
url?: string;
}

export type WatermarkLayout = 'rectangular' | 'hexagonal';