如何在 Flutter 中为内存中的图片添加水印

1. 介绍

标题: 如何在 Flutter 中为内存中的图片添加水印

引言:
在移动应用开发中,往往需要在图片上添加水印,比如在显示或保存之前对用户上传的图片进行标记。本文将详细介绍如何在 Flutter 中使用 dart:ui 库,在内存中的图片上添加水印,并将处理后的图片导出为 Uint8List。我们将通过一步一步的代码实现,帮助你掌握这一技术。

样式

2. 背景知识

什么是 Uint8ListByteData?

  • 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 库,你可以轻松地在内存中处理图片并添加水印。这不仅提高了应用的性能,也为你的应用提供了更多的图片处理可能性。

8. 参考与资源


Flutter图片水印的步骤如下: 1. 将图片载到内存,可以使用`ImageProvider`或`File`类来载。 2. 创建一个新的画布,并将原始图片绘制到画布上。 3. 在画布上绘制水印,可以使用`TextPainter`类来绘制文字水印,或使用`ImageProvider`类来图片水印。 4. 将绘制完水印的画布保存为新的图片文件。 下面是一个简单的示例代码: ```dart import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:path_provider/path_provider.dart'; import 'package:image/image.dart' as img; class WatermarkImage extends StatefulWidget { final String imagePath; final String watermarkText; WatermarkImage({required this.imagePath, required this.watermarkText}); @override _WatermarkImageState createState() => _WatermarkImageState(); } class _WatermarkImageState extends State<WatermarkImage> { late String _watermarkImagePath; @override void initState() { super.initState(); _addWatermarkToImage(); } Future<void> _addWatermarkToImage() async { final directory = await getTemporaryDirectory(); final imageFile = File(widget.imagePath); final image = img.decodeImage(imageFile.readAsBytesSync())!; final watermark = img.Image.fromBytes( 100, 50, img.encodePng(img.copyResize( img.decodeImage( (await rootBundle.load('assets/images/watermark.png')).buffer.asUint8List(), )!, width: 100, height: 50, )), ); final textPainter = TextPainter( text: TextSpan( text: widget.watermarkText, style: TextStyle( fontSize: 20, color: Colors.white, ), ), textDirection: TextDirection.ltr, )..layout(); final canvas = img.Canvas(image); canvas.drawImage(watermark, 0, 0); canvas.drawImage( img.copyResize( img.Image.fromBytes( textPainter.width.toInt(), textPainter.height.toInt(), img.encodePng( (await textPainter.toPicture().toImage(textPainter.width.toInt(), textPainter.height.toInt())) .toByteData(format: img.PixelFormat.rgba8888)! .buffer .asUint8List(), ), ), width: 100, height: 50, ), image.width - 100, image.height - 50, ); final watermarkImageFile = File('${directory.path}/watermark_${DateTime.now().millisecondsSinceEpoch}.jpg'); watermarkImageFile.writeAsBytesSync(img.encodeJpg(image)); setState(() { _watermarkImagePath = watermarkImageFile.path; }); } @override Widget build(BuildContext context) { return _watermarkImagePath != null ? Image.file( File(_watermarkImagePath), fit: BoxFit.cover, ) : Container(); } } ``` 这个示例代码将载指定路径的图片文件,并在右下角添加一个水印图标和文字水印。最终生成的带有水印的新图片文件将保存在应用程序的临时目录
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值