图片处理是 Web 开发中的常见需求。无论是用户头像裁剪、图片水印保护,还是格式转换优化,掌握图片处理技术都是开发者的必备技能。本文将深入讲解图片处理的核心原理和实现方法。
图片基础知识
数字图像的本质
数字图像本质上是一个像素矩阵,每个像素包含颜色信息:
图像 = 像素矩阵[宽度 × 高度]
像素 = (R, G, B, A) // 红、绿、蓝、透明度
常见图片格式对比
| 格式 | 压缩类型 | 透明度 | 动画 | 适用场景 |
|---|---|---|---|---|
| JPEG | 有损 | ❌ | ❌ | 照片、复杂图像 |
| PNG | 无损 | ✅ | ❌ | 图标、需要透明的图像 |
| GIF | 无损 | ✅ | ✅ | 简单动画、图标 |
| WebP | 有损/无损 | ✅ | ✅ | 现代 Web 应用 |
| SVG | 矢量 | ✅ | ✅ | 图标、Logo、插图 |
图片质量与文件大小
图片质量和文件大小的关系:
文件大小 ≈ 宽度 × 高度 × 色深 × (1 - 压缩率)
图片裁剪技术
裁剪原理
图片裁剪的本质是从原图像素矩阵中提取一个子矩阵:
原图: Image[W × H]
裁剪区域: (x, y, width, height)
结果: SubImage[width × height]
Canvas 实现裁剪
class ImageCropper {
constructor(image) {
this.image = image;
this.canvas = document.createElement('canvas');
this.ctx = this.canvas.getContext('2d');
}
crop(x, y, width, height) {
this.canvas.width = width;
this.canvas.height = height;
this.ctx.drawImage(
this.image,
x, y, width, height, // 源图裁剪区域
0, 0, width, height // 目标画布位置
);
return this.canvas.toDataURL('image/png');
}
cropWithAspectRatio(aspectRatio) {
const imgRatio = this.image.width / this.image.height;
let cropWidth, cropHeight, x, y;
if (imgRatio > aspectRatio) {
cropHeight = this.image.height;
cropWidth = cropHeight * aspectRatio;
x = (this.image.width - cropWidth) / 2;
y = 0;
} else {
cropWidth = this.image.width;
cropHeight = cropWidth / aspectRatio;
x = 0;
y = (this.image.height - cropHeight) / 2;
}
return this.crop(x, y, cropWidth, cropHeight);
}
cropCircle(centerX, centerY, radius) {
const diameter = radius * 2;
this.canvas.width = diameter;
this.canvas.height = diameter;
this.ctx.beginPath();
this.ctx.arc(radius, radius, radius, 0, Math.PI * 2);
this.ctx.closePath();
this.ctx.clip();
this.ctx.drawImage(
this.image,
centerX - radius, centerY - radius, diameter, diameter,
0, 0, diameter, diameter
);
return this.canvas.toDataURL('image/png');
}
}
// 使用示例
const img = new Image();
img.onload = () => {
const cropper = new ImageCropper(img);
// 矩形裁剪
const cropped = cropper.crop(100, 100, 300, 200);
// 按比例裁剪 (16:9)
const ratio16x9 = cropper.cropWithAspectRatio(16/9);
// 圆形裁剪
const circle = cropper.cropCircle(200, 200, 100);
};
img.src = 'image.jpg';
Python 实现裁剪
from PIL import Image
import numpy as np
class ImageCropper:
def __init__(self, image_path):
self.image = Image.open(image_path)
def crop(self, x, y, width, height):
"""矩形裁剪"""
box = (x, y, x + width, y + height)
return self.image.crop(box)
def crop_center(self, width, height):
"""中心裁剪"""
img_width, img_height = self.image.size
x = (img_width - width) // 2
y = (img_height - height) // 2
return self.crop(x, y, width, height)
def crop_with_aspect_ratio(self, aspect_ratio):
"""按比例裁剪"""
img_width, img_height = self.image.size
img_ratio = img_width / img_height
if img_ratio > aspect_ratio:
new_width = int(img_height * aspect_ratio)
new_height = img_height
x = (img_width - new_width) // 2
y = 0
else:
new_width = img_width
new_height = int(img_width / aspect_ratio)
x = 0
y = (img_height - new_height) // 2
return self.crop(x, y, new_width, new_height)
def crop_circle(self):
"""圆形裁剪"""
size = min(self.image.size)
mask = Image.new('L', (size, size), 0)
from PIL import ImageDraw
draw = ImageDraw.Draw(mask)
draw.ellipse((0, 0, size, size), fill=255)
# 先裁剪为正方形
square = self.crop_center(size, size)
# 应用圆形蒙版
output = Image.new('RGBA', (size, size), (0, 0, 0, 0))
output.paste(square, mask=mask)
return output
# 使用示例
cropper = ImageCropper('photo.jpg')
cropped = cropper.crop_with_aspect_ratio(16/9)
cropped.save('cropped_16x9.jpg')
图片水印技术
水印类型
- 文字水印:添加版权文字
- 图片水印:叠加 Logo 图片
- 盲水印:隐藏在图像数据中,肉眼不可见
文字水印实现
class WatermarkGenerator {
constructor(image) {
this.image = image;
this.canvas = document.createElement('canvas');
this.ctx = this.canvas.getContext('2d');
}
addTextWatermark(text, options = {}) {
const {
fontSize = 24,
fontFamily = 'Arial',
color = 'rgba(255, 255, 255, 0.5)',
position = 'bottom-right',
padding = 20,
rotation = 0
} = options;
this.canvas.width = this.image.width;
this.canvas.height = this.image.height;
// 绘制原图
this.ctx.drawImage(this.image, 0, 0);
// 设置水印样式
this.ctx.font = `${fontSize}px ${fontFamily}`;
this.ctx.fillStyle = color;
this.ctx.textBaseline = 'middle';
// 计算位置
const textMetrics = this.ctx.measureText(text);
const textWidth = textMetrics.width;
const textHeight = fontSize;
let x, y;
switch (position) {
case 'top-left':
x = padding;
y = padding + textHeight / 2;
break;
case 'top-right':
x = this.canvas.width - textWidth - padding;
y = padding + textHeight / 2;
break;
case 'bottom-left':
x = padding;
y = this.canvas.height - padding - textHeight / 2;
break;
case 'bottom-right':
x = this.canvas.width - textWidth - padding;
y = this.canvas.height - padding - textHeight / 2;
break;
case 'center':
x = (this.canvas.width - textWidth) / 2;
y = this.canvas.height / 2;
break;
}
// 应用旋转
if (rotation !== 0) {
this.ctx.save();
this.ctx.translate(x + textWidth / 2, y);
this.ctx.rotate(rotation * Math.PI / 180);
this.ctx.fillText(text, -textWidth / 2, 0);
this.ctx.restore();
} else {
this.ctx.fillText(text, x, y);
}
return this.canvas.toDataURL('image/png');
}
addTiledWatermark(text, options = {}) {
const {
fontSize = 16,
color = 'rgba(128, 128, 128, 0.3)',
spacing = 100,
rotation = -30
} = options;
this.canvas.width = this.image.width;
this.canvas.height = this.image.height;
this.ctx.drawImage(this.image, 0, 0);
this.ctx.font = `${fontSize}px Arial`;
this.ctx.fillStyle = color;
// 平铺水印
const textWidth = this.ctx.measureText(text).width;
const stepX = textWidth + spacing;
const stepY = fontSize + spacing;
this.ctx.save();
this.ctx.rotate(rotation * Math.PI / 180);
// 扩展绘制范围以覆盖旋转后的区域
const diagonal = Math.sqrt(
this.canvas.width ** 2 + this.canvas.height ** 2
);
for (let y = -diagonal; y < diagonal * 2; y += stepY) {
for (let x = -diagonal; x < diagonal * 2; x += stepX) {
this.ctx.fillText(text, x, y);
}
}
this.ctx.restore();
return this.canvas.toDataURL('image/png');
}
}
// 使用示例
const img = new Image();
img.onload = () => {
const watermark = new WatermarkGenerator(img);
// 单个水印
const single = watermark.addTextWatermark('© 2024 MyCompany', {
position: 'bottom-right',
color: 'rgba(255, 255, 255, 0.7)'
});
// 平铺水印
const tiled = watermark.addTiledWatermark('CONFIDENTIAL', {
rotation: -45,
spacing: 150
});
};
img.src = 'photo.jpg';
图片水印实现
addImageWatermark(watermarkImage, options = {}) {
const {
position = 'bottom-right',
padding = 20,
opacity = 0.7,
scale = 0.2 // 水印相对于原图的比例
} = options;
this.canvas.width = this.image.width;
this.canvas.height = this.image.height;
this.ctx.drawImage(this.image, 0, 0);
// 计算水印尺寸
const wmWidth = this.image.width * scale;
const wmHeight = (watermarkImage.height / watermarkImage.width) * wmWidth;
// 计算位置
let x, y;
switch (position) {
case 'bottom-right':
x = this.canvas.width - wmWidth - padding;
y = this.canvas.height - wmHeight - padding;
break;
// ... 其他位置
}
// 设置透明度
this.ctx.globalAlpha = opacity;
this.ctx.drawImage(watermarkImage, x, y, wmWidth, wmHeight);
this.ctx.globalAlpha = 1;
return this.canvas.toDataURL('image/png');
}
Python 水印实现
from PIL import Image, ImageDraw, ImageFont
import math
class WatermarkGenerator:
def __init__(self, image_path):
self.image = Image.open(image_path).convert('RGBA')
def add_text_watermark(self, text, position='bottom-right',
font_size=36, color=(255, 255, 255, 128)):
"""添加文字水印"""
# 创建透明图层
txt_layer = Image.new('RGBA', self.image.size, (255, 255, 255, 0))
draw = ImageDraw.Draw(txt_layer)
try:
font = ImageFont.truetype('arial.ttf', font_size)
except:
font = ImageFont.load_default()
# 获取文字尺寸
bbox = draw.textbbox((0, 0), text, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
# 计算位置
padding = 20
positions = {
'top-left': (padding, padding),
'top-right': (self.image.width - text_width - padding, padding),
'bottom-left': (padding, self.image.height - text_height - padding),
'bottom-right': (self.image.width - text_width - padding,
self.image.height - text_height - padding),
'center': ((self.image.width - text_width) // 2,
(self.image.height - text_height) // 2)
}
x, y = positions.get(position, positions['bottom-right'])
draw.text((x, y), text, font=font, fill=color)
# 合并图层
return Image.alpha_composite(self.image, txt_layer)
def add_tiled_watermark(self, text, spacing=100, rotation=-30,
font_size=24, color=(128, 128, 128, 80)):
"""添加平铺水印"""
# 创建单个水印
try:
font = ImageFont.truetype('arial.ttf', font_size)
except:
font = ImageFont.load_default()
# 创建足够大的水印图层
diagonal = int(math.sqrt(self.image.width**2 + self.image.height**2))
txt_layer = Image.new('RGBA', (diagonal * 2, diagonal * 2), (0, 0, 0, 0))
draw = ImageDraw.Draw(txt_layer)
# 绘制平铺文字
bbox = draw.textbbox((0, 0), text, font=font)
text_width = bbox[2] - bbox[0]
step_x = text_width + spacing
step_y = font_size + spacing
for y in range(0, diagonal * 2, step_y):
for x in range(0, diagonal * 2, step_x):
draw.text((x, y), text, font=font, fill=color)
# 旋转水印图层
txt_layer = txt_layer.rotate(rotation, expand=False)
# 裁剪到原图大小
center_x = txt_layer.width // 2
center_y = txt_layer.height // 2
crop_box = (
center_x - self.image.width // 2,
center_y - self.image.height // 2,
center_x + self.image.width // 2,
center_y + self.image.height // 2
)
txt_layer = txt_layer.crop(crop_box)
return Image.alpha_composite(self.image, txt_layer)
# 使用示例
wm = WatermarkGenerator('photo.jpg')
result = wm.add_text_watermark('© 2024 MyCompany')
result.save('watermarked.png')
图片压缩与优化
压缩原理
图片压缩主要通过以下方式实现:
- 有损压缩:丢弃人眼不敏感的信息
- 无损压缩:利用数据冗余进行压缩
- 尺寸缩放:减少像素数量
JavaScript 压缩实现
class ImageCompressor {
compress(image, options = {}) {
const {
maxWidth = 1920,
maxHeight = 1080,
quality = 0.8,
format = 'image/jpeg'
} = options;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// 计算新尺寸
let { width, height } = this.calculateSize(
image.width, image.height, maxWidth, maxHeight
);
canvas.width = width;
canvas.height = height;
// 使用双线性插值
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(image, 0, 0, width, height);
return new Promise((resolve) => {
canvas.toBlob(resolve, format, quality);
});
}
calculateSize(width, height, maxWidth, maxHeight) {
if (width <= maxWidth && height <= maxHeight) {
return { width, height };
}
const ratio = Math.min(maxWidth / width, maxHeight / height);
return {
width: Math.round(width * ratio),
height: Math.round(height * ratio)
};
}
async compressToTargetSize(image, targetSizeKB) {
let quality = 0.9;
let result;
while (quality > 0.1) {
result = await this.compress(image, { quality });
const sizeKB = result.size / 1024;
if (sizeKB <= targetSizeKB) {
return result;
}
quality -= 0.1;
}
return result;
}
}
// 使用示例
const compressor = new ImageCompressor();
// 文件选择处理
document.getElementById('fileInput').addEventListener('change', async (e) => {
const file = e.target.files[0];
const image = await loadImage(file);
const compressed = await compressor.compress(image, {
maxWidth: 1200,
quality: 0.7
});
console.log(`原始大小: ${file.size / 1024}KB`);
console.log(`压缩后: ${compressed.size / 1024}KB`);
});
function loadImage(file) {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => resolve(img);
img.src = URL.createObjectURL(file);
});
}
格式转换
class ImageConverter {
async convert(image, targetFormat) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = image.width;
canvas.height = image.height;
ctx.drawImage(image, 0, 0);
const mimeTypes = {
'jpg': 'image/jpeg',
'jpeg': 'image/jpeg',
'png': 'image/png',
'webp': 'image/webp',
'gif': 'image/gif'
};
const mimeType = mimeTypes[targetFormat.toLowerCase()];
return new Promise((resolve) => {
canvas.toBlob(resolve, mimeType, 0.9);
});
}
async toWebP(image, quality = 0.8) {
return this.convert(image, 'webp');
}
async toPNG(image) {
return this.convert(image, 'png');
}
async toJPEG(image, quality = 0.9) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = image.width;
canvas.height = image.height;
// JPEG 不支持透明,填充白色背景
ctx.fillStyle = '#FFFFFF';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(image, 0, 0);
return new Promise((resolve) => {
canvas.toBlob(resolve, 'image/jpeg', quality);
});
}
}
SVG 优化
SVG 优化原理
SVG 是基于 XML 的矢量图格式,优化主要包括:
- 移除不必要的元数据
- 简化路径数据
- 合并相同样式
- 压缩数值精度
SVG 优化实现
class SVGOptimizer {
optimize(svgString, options = {}) {
const {
removeComments = true,
removeMetadata = true,
removeEmptyAttrs = true,
cleanupNumericValues = true,
precision = 2
} = options;
let result = svgString;
if (removeComments) {
result = result.replace(/<!--[\s\S]*?-->/g, '');
}
if (removeMetadata) {
result = result.replace(/<metadata[\s\S]*?<\/metadata>/gi, '');
result = result.replace(/<\?xml[\s\S]*?\?>/gi, '');
result = result.replace(/<!DOCTYPE[\s\S]*?>/gi, '');
}
if (removeEmptyAttrs) {
result = result.replace(/\s+[a-z-]+=""/gi, '');
}
if (cleanupNumericValues) {
result = this.cleanupNumbers(result, precision);
}
// 移除多余空白
result = result.replace(/\s+/g, ' ');
result = result.replace(/>\s+</g, '><');
return result.trim();
}
cleanupNumbers(svg, precision) {
return svg.replace(
/(\d+\.\d{3,})/g,
(match) => parseFloat(match).toFixed(precision)
);
}
simplifyPaths(svg) {
// 简化路径命令
return svg.replace(
/d="([^"]+)"/g,
(match, path) => {
const simplified = path
.replace(/\s+/g, ' ')
.replace(/([MmLlHhVvCcSsQqTtAaZz])\s*/g, '$1')
.replace(/\s+([MmLlHhVvCcSsQqTtAaZz])/g, '$1');
return `d="${simplified}"`;
}
);
}
}
// 使用示例
const optimizer = new SVGOptimizer();
const optimized = optimizer.optimize(svgContent, {
precision: 1,
removeMetadata: true
});
实际应用场景
1. 用户头像处理
class AvatarProcessor {
async processAvatar(file, size = 200) {
const image = await this.loadImage(file);
// 1. 裁剪为正方形
const cropper = new ImageCropper(image);
const squareSize = Math.min(image.width, image.height);
const croppedData = cropper.cropCircle(
image.width / 2,
image.height / 2,
squareSize / 2
);
// 2. 缩放到目标尺寸
const croppedImage = await this.loadImage(croppedData);
const compressor = new ImageCompressor();
return compressor.compress(croppedImage, {
maxWidth: size,
maxHeight: size,
quality: 0.9
});
}
loadImage(src) {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => resolve(img);
img.src = typeof src === 'string' ? src : URL.createObjectURL(src);
});
}
}
2. 批量水印处理
import os
from concurrent.futures import ThreadPoolExecutor
def batch_watermark(input_dir, output_dir, watermark_text):
"""批量添加水印"""
os.makedirs(output_dir, exist_ok=True)
image_files = [
f for f in os.listdir(input_dir)
if f.lower().endswith(('.jpg', '.jpeg', '.png'))
]
def process_image(filename):
input_path = os.path.join(input_dir, filename)
output_path = os.path.join(output_dir, filename)
wm = WatermarkGenerator(input_path)
result = wm.add_text_watermark(watermark_text)
result.convert('RGB').save(output_path, 'JPEG', quality=90)
return filename
with ThreadPoolExecutor(max_workers=4) as executor:
results = list(executor.map(process_image, image_files))
return results
# 使用
processed = batch_watermark('./images', './output', '© 2024 MyCompany')
print(f'处理完成: {len(processed)} 张图片')
3. 响应式图片生成
async function generateResponsiveImages(image, sizes = [320, 640, 1024, 1920]) {
const compressor = new ImageCompressor();
const results = {};
for (const width of sizes) {
const blob = await compressor.compress(image, {
maxWidth: width,
quality: 0.8,
format: 'image/webp'
});
results[width] = blob;
}
return results;
}
// 生成 srcset
function generateSrcSet(images) {
return Object.entries(images)
.map(([width, blob]) => `${URL.createObjectURL(blob)} ${width}w`)
.join(', ');
}
性能优化建议
1. 使用 Web Workers
// worker.js
self.onmessage = async (e) => {
const { imageData, operation, options } = e.data;
const canvas = new OffscreenCanvas(imageData.width, imageData.height);
const ctx = canvas.getContext('2d');
ctx.putImageData(imageData, 0, 0);
// 执行图片处理操作
// ...
const blob = await canvas.convertToBlob(options);
self.postMessage({ blob });
};
// 主线程
const worker = new Worker('worker.js');
worker.postMessage({ imageData, operation: 'compress', options });
2. 渐进式加载
function loadImageProgressive(url, onProgress) {
return new Promise((resolve, reject) => {
const img = new Image();
// 先加载缩略图
const thumbUrl = url.replace('.jpg', '_thumb.jpg');
const thumb = new Image();
thumb.onload = () => onProgress(thumb, 0.3);
thumb.src = thumbUrl;
// 加载完整图片
img.onload = () => resolve(img);
img.onerror = reject;
img.src = url;
});
}
总结
图片处理是 Web 开发中的重要技能,核心要点包括:
- 裁剪:通过 Canvas 提取图像子区域
- 水印:在图像上叠加文字或图片保护版权
- 压缩:平衡质量和文件大小
- 格式转换:选择合适的格式优化传输
如需快速处理图片,可以使用我们的在线工具: