1. 介绍
标题: 如何在 Flutter 中为内存中的图片添加水印
引言:
在移动应用开发中,往往需要在图片上添加水印,比如在显示或保存之前对用户上传的图片进行标记。本文将详细介绍如何在 Flutter 中使用 dart:ui
库,在内存中的图片上添加水印,并将处理后的图片导出为 Uint8List
。我们将通过一步一步的代码实现,帮助你掌握这一技术。
样式:
2. 背景知识
什么是 Uint8List
和 ByteData
?
Uint8List
是 Dart 中的一种无符号8位整数列表,通常用于处理原始二进制数据,比如图像数据。ByteData
提供了对字节数据的视图,可以用于读取和写入不同类型的数据(如int
,float
等)。
为什么要将图片处理在内存中?
- 在内存中处理图片可以避免 UI 层叠造成的额外开销。
- 在需要保存图片时,直接在内存中处理并保存,效率更高。
3. 代码实现详解
3.1 准备工作:加载图片
首先,我们需要将图片数据从 Uint8List
转换为 ui.Image
。这可以通过以下函数实现:
/// 通过[Uint8List]获取图片
static Future<ui.Image> loadImageByUint8List(Uint8List list) async {
ui.Codec codec = await ui.instantiateImageCodec(list);
ui.FrameInfo frame = await codec.getNextFrame();
return frame.image;
}
解析:
ui.instantiateImageCodec
:该方法用于将Uint8List
中的二进制数据转换为图片的编解码器 (Codec
)。codec.getNextFrame()
:获取图片的帧。虽然我们只需要单张图片,但Codec
支持多帧图像(如 GIF)。
3.2 在图片上添加水印
接下来,我们在内存中的图片上添加水印:
第一种覆盖明显的:
/// 图片加文字
static imageAddWaterMark(Uint8List imagelist, String textStr) async {
int width, height;
// 创建画布和图片
ui.PictureRecorder recorder = ui.PictureRecorder();
Canvas canvas = Canvas(recorder);
// 加载图片对象
ui.Image image = await loadImageByUint8List(imagelist);
width = image.width;
height = image.height;
// 计算对角线长度
double dimension =
math.sqrt(math.pow(image.width, 2) + math.pow(image.height, 2));
// 绘制原始图片到画布上
canvas.drawImage(image, const Offset(0, 0), Paint());
// 准备水印文本
canvas.saveLayer(
Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()),
Paint()..blendMode = BlendMode.multiply);
var text = textStr;
// 计算水印文本重复次数
var rectSize = math.pow(dimension, 2);
int textRepeating = ((rectSize / math.pow(30, 2) * 2) / (text.length + 8))
.round();
// 设置水印文本位置和样式
math.Point pivotPoint = math.Point(dimension / 2, dimension / 2);
canvas.translate(pivotPoint.x.toDouble(), pivotPoint.y.toDouble());
canvas.rotate(-25 * math.pi / 180);
canvas.translate(
-pivotPoint.distanceTo(math.Point(0, image.height)),
-pivotPoint.distanceTo(
const math.Point(0, 0)));
var textPainter = TextPainter(
text: TextSpan(
text: (text.padRight(text.length + 8)) * textRepeating,
style: const TextStyle(
fontSize: 30, color: Color.fromRGBO(0, 0, 0, .3), height: 2)),
maxLines: null,
textDirection: ui.TextDirection.ltr,
textAlign: TextAlign.start,
);
textPainter.layout(maxWidth: dimension);
textPainter.paint(canvas, Offset.zero);
canvas.restore();
// 获取处理后的图片
ui.Picture picture = recorder.endRecording();
final img = await picture.toImage(width.toInt(), height.toInt());
// 转换为 Uint8List
final pngBytes = await img.toByteData(format: ui.ImageByteFormat.png);
return pngBytes!.buffer.asUint8List();
}
解析:
ui.PictureRecorder
:用于记录绘制指令。最终生成的Picture
可以转化为ui.Image
。Canvas
:代表一个绘制对象,可以在其上绘制图形、图片、文本等。TextPainter
:用于测量和绘制文本。BlendMode.multiply
:应用了混合模式,使得水印叠加在图片上更加自然。
第二种在图片上添加覆盖不明显带有渐变效果的水印:
static Future<Uint8List?> imageAddWaterMark(
Uint8List imagelist,
String textStr, {
int rows = 4,
int columns = 2,
double angle = 45,
double opacity = 0.1,
int blockRows = 31,
int blockColumns = 31,
}) async {
try {
int width, height;
// 拿到Canvas
ui.PictureRecorder recorder = ui.PictureRecorder();
Canvas canvas = Canvas(recorder);
// 拿到Image对象
ui.Image image = await loadImageByUint8List(imagelist);
width = image.width;
height = image.height;
// 绘制原始图片到Canvas
canvas.drawImage(image, const Offset(0, 0), Paint());
// 计算单位宽高
final double unitWidth = width / blockColumns;
final double unitHeight = height / blockRows;
// 定义颜色渐变
List<Color> colors = [
Colors.blue.withOpacity(opacity),
Colors.red.withOpacity(opacity),
Colors.green.withOpacity(opacity),
Colors.orange.withOpacity(opacity),
Colors.purple.withOpacity(opacity),
];
// 文本样式
final textStyle = TextStyle(
fontSize: width / 20,
);
final textSpan = TextSpan(
text: textStr,
style: textStyle,
);
final textPainter = TextPainter(
text: textSpan,
textDirection: TextDirection.ltr,
);
textPainter.layout();
final double stepX = width / (columns + 1);
final double stepY = height / (rows + 1);
for (int row = 1; row <= rows; row++) {
for (int col = 1; col <= columns; col++) {
final dx = col * stepX;
final dy = row * stepY;
final offset = Offset(dx, dy);
canvas.save();
canvas.translate(offset.dx, offset.dy);
// 创建线性渐变
final gradient = LinearGradient(
colors: colors,
);
final Paint paint = Paint()
..shader = gradient.createShader(
Rect.fromLTWH(-textPainter.width / 2, -textPainter.height / 2,
textPainter.width, textPainter.height),
);
final textStyleWithGradient = textStyle.copyWith(foreground: paint);
final textSpanWithGradient = TextSpan(
text: textStr,
style: textStyleWithGradient,
);
final textPainterWithGradient = TextPainter(
text: textSpanWithGradient,
textDirection: TextDirection.ltr,
);
textPainterWithGradient.layout();
// 应用旋转
canvas.rotate(angle * math.pi / 180);
textPainterWithGradient.paint(
canvas, Offset(-textPainterWithGradient.width / 2, -textPainterWithGradient.height / 2));
canvas.restore();
}
}
// 结束绘制
ui.Picture picture = recorder.endRecording();
final img = await picture.toImage(width.toInt(), height.toInt());
// 转换为 Uint8List
final pngBytes = await img.toByteData(format: ui.ImageByteFormat.png);
return pngBytes!.buffer.asUint8List();
} catch (e) {
Log.e(tag: "imageAddWaterMark", msg: e.toString());
return null;
}
}
- 定义参数:函数签名中添加了
rows
,columns
,angle
,opacity
,blockRows
, 和blockColumns
参数,控制水印文本的行数、列数、旋转角度、透明度以及网格布局。 - 颜色渐变:使用
LinearGradient
为水印文本创建颜色渐变,增加视觉效果。 - 文本样式:水印文本的字体大小与图片宽度相关,确保在不同大小的图片上有合适的显示效果。
- 旋转文本:通过
canvas.rotate
使水印以一定的角度显示,增加难以去除的效果。 - 导出图片:最终返回处理后的图片数据,格式为
Uint8List
,可以直接用于显示或保存。
4. 代码实现总结
通过上面的步骤,我们实现了一个方法 imageAddWaterMark
,它接受一个 Uint8List
格式的图片和一个水印文本,然后将水印添加到图片中并返回带水印的图片数据。该方法在内存中直接处理图像数据,避免了 UI 层的额外开销。
5. 使用案例
接下来,你可以在实际应用中使用这个功能,例如在用户上传图片之前为图片添加水印,或者在用户下载图片前添加版权信息。
void main() async {
// 示例:加载图片,添加水印并显示
Uint8List imageBytes = ...; // 从文件或网络加载图片数据
String watermarkText = "CONFIDENTIAL";
Uint8List watermarkedImage = await imageAddWaterMark(imageBytes, watermarkText);
// 显示处理后的图片
// 可以将 watermarkedImage 传递给 Image.memory() 等组件
}
6. 优化与扩展
你可以进一步扩展这个功能,例如:
- 动态设置水印的颜色和透明度。
- 支持更多的文本样式。
- 支持在图片的不同位置添加多段水印。
7. 结论
在 Flutter 中,使用 dart:ui
库,你可以轻松地在内存中处理图片并添加水印。这不仅提高了应用的性能,也为你的应用提供了更多的图片处理可能性。