水印技术是一种在数字内容中嵌入标识信息的方法,用于涉密保护、版权保护、品牌推广或内容防伪。在前端开发中,水印技术可以通过多种方式实现。本文将介绍几种常用的前端水印实现方法,包括使用 CSS 伪元素、背景图像、SVG 叠加、Canvas 以及 WebGL等等。
废话不多说,有一句话叫Talk is cheap. Show me the code.
下面让我们一起步入正题。
1. 使用 CSS 伪元素实现水印
方法概述:
通过 CSS 的 ::before
或 ::after
伪元素,可以在网页元素的前面或后面添加水印。此方法适合于静态或简单的水印需求,但也可以通过 JavaScript 动态修改水印内容。
实现示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Repeating Text Watermark</title>
<style>
.watermarked-content {
position: relative;
width: 100%;
height: 400px;
background-color: #eaeaea;
margin: 20px auto;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
text-align: center;
--watermark-text: 'Default Watermark'; /* 默认水印文本 */
/* 使用 ::before 伪元素创建水印文字 */
background-image: repeating-linear-gradient(
rgba(0, 0, 0, 0.1) 1px,
transparent 1px
),
repeating-linear-gradient(
90deg,
rgba(0, 0, 0, 0.1) 1px,
transparent 1px
);
background-size: 200px 200px;
background-position: center;
}
/* 设置水印伪元素 */
.watermarked-content::before {
content: var(--watermark-text); /* 动态使用 CSS 变量作为水印文本 */
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: block;
color: rgba(0, 0, 0, 0.1);
font-size: 20px;
transform: rotate(-30deg); /* 旋转水印 */
pointer-events: none;
white-space: nowrap;
z-index: 1;
opacity: 1;
background-repeat: repeat;
background-size: 200px 100px;
}
</style>
</head>
<body>
<div class="watermarked-content" id="watermarkedElement">
<!-- 内容区域 -->
Your Content Here
</div>
<script>
// JavaScript 动态修改水印文本
function changeWatermark(text) {
const watermarkedElement =
document.getElementById('watermarkedElement');
// 使用CSS变量来改变伪元素中的文本
watermarkedElement.style.setProperty('--watermark-text', `"${text}"`);
}
// 默认设置水印
changeWatermark('Initial Watermark');
</script>
</body>
</html>
这种方法的优点有两点,一是实现简单,易于调整样式;二是支持通过 JavaScript 动态修改水印内容。但它也有一定的局限性,主要适用于静态水印,动态更新较为有限。
2. 使用背景图像实现水印
通过设置背景图像的方式,在网页元素中创建重复的水印。可以使用 SVG 图像来生成水印,并通过 JavaScript 动态修改背景图像的 URL 来改变水印内容。
实现示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Background Image Watermark with Dynamic Content</title>
<style>
.watermarked-content {
position: relative;
width: 100%;
height: 400px;
background-color: #f5f5f5;
background-repeat: repeat;
background-size: 200px 100px;
}
</style>
</head>
<body>
<div class="watermarked-content" id="watermarkedContent">content here</div>
<script>
function generateSvgWatermark(text) {
return `data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200"><text x="50%" y="50%" dy="0.35em" text-anchor="middle" fill="rgba(0,0,0,0.1)" font-size="16" transform="rotate(-30)">${text}</text></svg>`;
}
function updateWatermark(newText) {
document.getElementById(
'watermarkedContent'
).style.backgroundImage = `url('${generateSvgWatermark(newText)}')`;
}
// Initial watermark
updateWatermark('12345');
</script>
</body>
</html>
这种方式的优点是支持大面积背景水印,样式可复杂;同时可以动态生成和更新水印图像。
它的缺点是可能对页面性能产生影响,尤其是频繁更新时;同时水印更新可能需要较高的计算开销。
3. 使用 <video>
元素和水印层叠
在 <video>
元素上叠加一个透明的水印层,可以利用 CSS 和 HTML 来实现。适用于需要对视频内容进行水印处理的场景。
实现示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Video Watermark</title>
<style>
.video-container {
position: relative;
width: 100%;
height: auto;
}
.video-container video {
width: 100%;
height: auto;
}
.watermark-overlay {
position: absolute;
top: 10px;
left: 10px;
color: rgba(255, 255, 255, 0.5);
font-size: 24px;
pointer-events: none;
}
</style>
</head>
<body>
<div class="video-container">
<video src="your-video.mp4" controls></video>
<div class="watermark-overlay" id="watermarkOverlay">Watermark</div>
</div>
<script>
function updateWatermark(newText) {
document.getElementById('watermarkOverlay').textContent = newText;
}
updateWatermark('Updated Watermark');
</script>
</body>
</html>
这种方式适用于视频内容的水印添加。实现简单,通过 CSS 调整水印样式。
但同时,它对视频内容的动态处理不如其他方法灵活,还可能需要处理视频播放器的兼容性问题。
4. 使用 SVG 叠加在 HTML 元素上
直接将 SVG 元素叠加在 HTML 元素上进行水印。SVG 可以很容易地进行图形处理和样式调整。
示例如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SVG Overlay Watermark</title>
<style>
.content {
position: relative;
width: 100%;
height: 400px;
background-color: #f5f5f5;
}
.watermark-svg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
opacity: 0.1;
}
</style>
</head>
<body>
<div class="content">
<svg class="watermark-svg" xmlns="http://www.w3.org/2000/svg">
<text x="50%" y="50%" text-anchor="middle" fill="black" font-size="48" transform="rotate(-30, 50, 50)">Watermark</text>
</svg>
Your content goes here
</div>
</body>
</html>
这种方式它的优势在于 SVG 提供了矢量图形的高质量显示。同时,可以精确控制水印的外观和位置。
但也会因此而带来一些问题,比如对复杂 SVG 可能有性能影响,以及动态更新可能需要额外处理。
5、使用 Canvas 生成水印
利用 HTML5 的 Canvas API 动态生成水印图像,适合需要高度自定义的水印效果。通过 JavaScript 可以动态修改水印内容。
示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Dynamic Canvas Watermark</title>
<style>
.content {
position: relative;
width: 100%;
height: 400px;
background-color: #f5f5f5;
overflow: hidden;
}
.dynamic-watermark {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none; /* Prevent interaction with the watermark canvas */
}
</style>
</head>
<body>
<div id="targetElement" class="content">
<div class="inner-content">Your content goes here.</div>
</div>
<script>
function createAndInsertCanvas() {
// Get the target element where the Canvas will be inserted
const targetElement = document.getElementById('targetElement');
// Create a Canvas element
const canvas = document.createElement('canvas');
canvas.className = 'dynamic-watermark';
canvas.width = targetElement.offsetWidth;
canvas.height = targetElement.offsetHeight;
// Append the Canvas to the target element
targetElement.appendChild(canvas);
// Get the Canvas 2D context
const ctx = canvas.getContext('2d');
// Draw the watermark
function drawWatermark(text) {
const width = canvas.width;
const height = canvas.height;
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = 'rgba(0, 0, 0, 0.3)';
ctx.font = '30px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.save();
// Draw repeated watermark text
const textWidth = ctx.measureText(text).width;
const spacing = 100; // Space between repeated watermarks
for (let x = -spacing; x < width; x += textWidth + spacing) {
for (let y = -spacing; y < height; y += 60 + spacing) {
// Adjust 60 to the text height
ctx.translate(x, y);
ctx.rotate(-Math.PI / 6); // Rotate text if needed
ctx.fillText(text, 0, 0);
ctx.rotate(Math.PI / 6);
ctx.translate(-x, -y);
}
}
ctx.restore();
}
// Draw the watermark initially
drawWatermark('hello world');
// Redraw watermark on window resize
window.onresize = () => {
canvas.width = targetElement.offsetWidth;
canvas.height = targetElement.offsetHeight;
drawWatermark();
};
}
window.onload = createAndInsertCanvas;
</script>
</body>
</html>
这种方式的优点在于高度自定义,支持动态生成和更新。同时可以在复杂背景上绘制水印。
但它也有一定的缺陷,一是实现相对复杂,需要处理 Canvas 的尺寸和内容刷新。二是性能开销较大,尤其是频繁更新的场景。
由于灵活性高且ROI相对其他方案较好,因此它是目前主要流行的解决方案。
6. 使用 WebGL 实现水印
通过 WebGL 绘制水印,这种方法适用于需要高性能和复杂水印效果的场景,例如 3D 水印。
简单示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WebGL Watermark with Text</title>
<style>
.watermarked-content {
position: relative;
width: 100%;
height: 400px;
background-color: #f5f5f5;
}
canvas {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div class="watermarked-content">
<canvas id="webglCanvas"></canvas>
Your content goes here
</div>
<script>
function initWebGLWatermark() {
const canvas = document.getElementById('webglCanvas');
const gl = canvas.getContext('webgl');
if (!gl) {
console.error('WebGL not supported');
return;
}
canvas.width = canvas.parentElement.offsetWidth;
canvas.height = canvas.parentElement.offsetHeight;
// Create the text texture using a 2D canvas
const textCanvas = document.createElement('canvas');
const textCtx = textCanvas.getContext('2d');
textCanvas.width = 512;
textCanvas.height = 128;
textCtx.fillStyle = 'rgba(0, 0, 0, 0.5)';
textCtx.font = '48px Arial';
textCtx.textAlign = 'center';
textCtx.textBaseline = 'middle';
textCtx.fillText(
'Watermark',
textCanvas.width / 2,
textCanvas.height / 2
);
// Create texture
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
gl.RGBA,
gl.UNSIGNED_BYTE,
textCanvas
);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
// Vertex shader program
const vsSource = `
attribute vec2 a_position;
attribute vec2 a_texCoord;
varying vec2 v_texCoord;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
v_texCoord = a_texCoord;
}
`;
// Fragment shader program
const fsSource = `
precision mediump float;
varying vec2 v_texCoord;
uniform sampler2D u_texture;
void main() {
gl_FragColor = texture2D(u_texture, v_texCoord);
}
`;
// Create shader
function createShader(type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error(
'An error occurred compiling the shaders: ' +
gl.getShaderInfoLog(shader)
);
gl.deleteShader(shader);
return null;
}
return shader;
}
const vertexShader = createShader(gl.VERTEX_SHADER, vsSource);
const fragmentShader = createShader(gl.FRAGMENT_SHADER, fsSource);
// Create program
function createProgram(vs, fs) {
const program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error(
'Unable to initialize the shader program: ' +
gl.getProgramInfoLog(program)
);
return null;
}
return program;
}
const program = createProgram(vertexShader, fragmentShader);
gl.useProgram(program);
// Define the positions and texture coordinates for the rectangle (full-screen quad)
const vertices = new Float32Array([
-1.0, -1.0, 0.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 0.0, 0.0, -1.0,
1.0, 0.0, 0.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0,
]);
// Create buffer
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
// Get attribute and uniform locations
const positionLocation = gl.getAttribLocation(program, 'a_position');
const texCoordLocation = gl.getAttribLocation(program, 'a_texCoord');
const textureLocation = gl.getUniformLocation(program, 'u_texture');
// Enable attributes
gl.enableVertexAttribArray(positionLocation);
gl.enableVertexAttribArray(texCoordLocation);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 16, 0);
gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 16, 8);
// Bind the texture
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.uniform1i(textureLocation, 0);
// Clear the canvas
gl.clearColor(1.0, 1.0, 1.0, 1.0); // White
gl.clear(gl.COLOR_BUFFER_BIT);
// Draw
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
window.onload = initWebGLWatermark;
</script>
</body>
</html>
它的优点是高度灵活,支持复杂的图形和效果。同时高性能,适合需要处理大量数据的场景。
但它也有一个比较明显的缺陷,就是学习成本较高,实现复杂,需要深入了解 WebGL。同时对不支持 WebGL 的设备或浏览器可能需要降级处理。
面临的技术挑战与总结
每种水印技术都有其独特的优势和适用场景:
- CSS 伪元素:实现简单,适合静态内容,但动态更新能力有限。
- 背景图像:适合大面积水印,支持动态内容生成,但可能影响性能。
- Canvas:高度自定义,适用于复杂效果,但实现和性能管理较复杂。
- SVG 叠加:高质量图形,适用于精确控制,但复杂 SVG 可能影响性能。
- WebGL:适合高性能和复杂场景,但实现难度大。
选择合适的水印技术取决于你的具体需求和场景。大多数场景下,我们多采用Canvas方案,因其高度自定义且难度适中。
然而实际场景中也会面临各种技术挑战,如下:
1)删除元素:恶意用户可能会尝试删除水印元素,以去除水印标识。
2)修改元素属性:用户可以通过开发者工具或 JavaScript 修改页面中的水印元素属性,比如隐藏。
3)修改内容图层:用户放弃修改水印元素,转而修改内容元素的图层,覆盖水印图层。
上述皆是明印,用户很容易通过肉眼察觉而进行某些破坏行为。然而在某些场景下,我们不希望水印影响用户体验,因此也会采用暗印,比如涉密等场景中,而暗印就会涉及编码和解码等一系列问题。尽管如此,暗印虽然难以察觉,但也可能被恶意用户破解或去除。
关注我,下一篇文章,我们将一起探讨下如何防止水印被用户恶意去除问题,打造更安全的前端水印:防破解技术与暗水印应用。