一.需求描述
在h5页面,调用手机摄像头拍照后,给照片添加水印(文本、图片)
水印大概内容是:
第一行是“拍摄人:xxx”、“拍摄时间:2023.11.22”; 第二行是“所在单位:xxxx有限公司”、“所在部门:万xxxx”; 第三行是“logo: 公司的logo图片”,图片尺寸是50px*50px;
样式布局要求: 水印在照片底部; 距离照片边缘留30px的空白;
每行的两组文字两边对齐;
左右侧文字不能重叠,中间最小间隔是30,文字多自动换行;
文字大小是20,行高24,颜色是#333;
二.需求难点分析
前端添加水印,本质是图片处理,必然是通过canvas来实现。
难点在于样式布局,据我了解,canvas并没有主流的针对于前端的封装库(类似jquery之于js),使用canvas来实现类似css的布局(见一.背景-样式布局要求)是很繁琐的过程。
三.核心方案
方案一,先使用html+css渲染水印dom1,然后使用css定位将dom1定位到照片dom2的上层,然后使用html2Canvas将dom1+dom2整体转为图片。
该方案优点是水印部分使用css布局,简单高效;
缺点是水印存在清晰度问题,因为水印是先渲染到页面上的,受屏幕分辨率影响(类似于截屏);同时,html2Canvas插件生成的图片比较大。
方案二,使用canvas将富文本逐条绘制到照片上面,需要较为繁琐的canvas操作(使用canvas代替css布局),需要对canvas的api较熟悉。
四.详细示例(方案二)
需求描述:在照片上添加5组文字,分为3行排列。
第一行是“拍摄人:xxx”、“拍摄时间:2023.11.22”;
第二行是“所在单位:xxxx有限公司”、“所在部门:xxxx”;
第三行是“logo: 公司的logo图片”,图片尺寸是50px*50px
样式要求:
水印在照片底部,
距离照片边缘留20的空白
每行的两组文字两边对齐,
左右侧文字不能重叠,中间最小间隔是30,文字多自动换行
文字大小是20,行高24,颜色是#333,
// js部分
const padding = 30
const logoSize = {w: 50, h:50}
const fontSize = 20;
const lineHeight = fontSize * 1.2;
ctx.fillStyle = '#333';
let maxWidth // 左右文字换行的宽度,
let y // 每次绘制时的纵坐标
document.getElementById('upload').addEventListener('change', async function(e) {
if (e.target.files && e.target.files[0]) {
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = async function(event) {
const img = await loadImage(event.target.result);
const logoImg = await loadImage('/img/logo.png'); // logo图片的路径
const canvas = document.getElementById('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
// 截止到这里,和方案一一致
// 设置水印样式
maxWidth = img.width - (padding * 2) - padding; // 考虑两侧30的间距,左右文字中间30
// 计算初始y坐标
y = img.height - padding; // 距底部30px
// 第三行水印 - 文本和图片
y -= logoSize.h; // 考虑logo高度
const textWidth = ctx.measureText('店铺logo: ').width;
ctx.fillText('店铺logo: ', padding, y);
ctx.drawImage(logoImg, padding + textWidth, y, logoSize.w, logoSize.h);
// 第二行水印
y -= lineHeight; // 暂时不考虑第三行文字换行且比logo还高
let leftLines = getTextLineNum(ctx, '所在单位:xxxx有限公司')
let rightLines = getTextLineNum(ctx, '所在部门:xxxx')
// 取leftLines rightLines较大的来计算起始y
y -= (Math.max(leftLines, rightLines) - 1) * lineHeight
wrapText(ctx, leftLines, padding, y, maxWidth / 2, lineHeight, 'left');
wrapText(ctx, rightLines, img.width - padding, y, maxWidth / 2, lineHeight, 'right');
y -= lineHeight;
leftLines = getTextLineNum(ctx, '拍摄人:xxx')
rightLines = getTextLineNum(ctx, '拍摄时间:2023-11-22')
y -= (Math.max(leftLines, rightLines) - 1) * lineHeight
wrapText(ctx, leftLines, padding, y, maxWidth / 2, lineHeight, 'left');
wrapText(ctx, rightLines, img.width - padding, y, maxWidth / 2, lineHeight, 'right');
// 显示水印图片
const watermarkedImage = document.getElementById('watermarkedImage');
watermarkedImage.src = canvas.toDataURL('image/jpeg');
canvas.style.display = 'none'; // 隐藏canvas元素
}
reader.readAsDataURL(file);
}
});
// 计算当前文字需要几行绘制
function getTextLineNum(ctx, text) {
let words = text.split(' ');
let line = '';
let lines = []; // 每个元素是一行文字
for (let n = 0; n < words.length; n++) {
let testLine = line + words[n] + ' ';
let metrics = ctx.measureText(testLine);
let testWidth = metrics.width;
if (testWidth > maxWidth && n > 0) {
lines.push(line);
line = words[n] + ' ';
} else {
line = testLine;
}
}
lines.push(line);
return lines.length // 返回需要绘制几行
}
// 绘制文字
function wrapText(ctx, lines, x, y, lineHeight, align = 'left') {
ctx.textAlign = align;
// 把lines按行绘制
y -= (lines.length - 1) * lineHeight // 假如需要绘制3行,那y需要在原基础上再往上移动2个lineHeight
for (let i = 0; i < lines.length; i++) {
ctx.fillText(lines[i], x, y);
y += lineHeight;
}
}
// html部分
<!DOCTYPE html>
<html>
<head>
<title>图片添加多行文本水印</title>
</head>
<body>
<input type="file" accept="image/*" id="upload">
<canvas id="canvas" style="display: none;"></canvas>
<img id="watermarkedImage" style="max-width: 100%;">
<script src="path_to_your_script.js"></script>
</body>
</html>
五.拓展
上述是添加富文本水印,有些情况是添加图片水印。
这个相对于富文本要简单一些,因为不需要考虑自定义排版,本质是将两张图片合并为一张,logo图片显示在上层。
document.getElementById('upload').addEventListener('change', function(e) {
if (e.target.files && e.target.files[0]) {
const file = e.target.files[0]; // 拿到上传的图片
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = function(event) {
const img = new Image();
img.src = event.target.result;
img.onload = function() {
const canvas = document.getElementById('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
// 到这里,上传的照片已经绘制到canvas
// 加载水印图片
const watermark = new Image();
watermark.src = 'xxx'; // 水印图片的路径
watermark.onload = function() {
// 水印尺寸
const watermarkWidth = 50;
const watermarkHeight = 30;
// 水印位置
const x = 20;
const y = img.height - watermarkHeight - 20;
// 绘制水印图片
ctx.drawImage(watermark, x, y, watermarkWidth, watermarkHeight);
};
}
}
}
});