Flutter:如何将Canvas/CustomPainter保存为图像文件?

30
我正在尝试从用户那里收集签名并保存为图像。我已经做到能够在屏幕上绘制,但现在我想点击一个按钮将其保存为图像并存储在我的数据库中。
以下是我目前所拥有的内容:
import 'package:flutter/material.dart';

class SignaturePadPage extends StatefulWidget {
  SignaturePadPage({Key key}) : super(key: key);

  @override
  _SignaturePadPage createState() => new _SignaturePadPage();
}
class _SignaturePadPage extends State<SignaturePadPage> {

  List<Offset> _points = <Offset>[];

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.white,
      child: GestureDetector(
        onPanUpdate: (DragUpdateDetails details) {
          setState(() {
            RenderBox referenceBox = context.findRenderObject();
            Offset localPosition =
            referenceBox.globalToLocal(details.globalPosition);
            _points = new List.from(_points)..add(localPosition);
          });
        },
        onPanEnd: (DragEndDetails details) => _points.add(null),
        child: new CustomPaint(painter: new SignaturePainter(_points)),
      ),
    );
  }
}

class SignaturePainter extends CustomPainter {
  SignaturePainter(this.points);
  final List<Offset> points;
  void paint(Canvas canvas, Size size) {
    Paint paint = new Paint()
      ..color = Colors.black
      ..strokeCap = StrokeCap.round
      ..strokeWidth = 5.0;
    for (int i = 0; i < points.length - 1; i++) {
      if (points[i] != null && points[i + 1] != null)
        canvas.drawLine(points[i], points[i + 1], paint);
    }
  }
  bool shouldRepaint(SignaturePainter other) => other.points != points;
}

不确定接下来该怎么做...

5个回答

32

6
我在我的代码中使用了 paint(Canvas canvas, Size size),因此没有 Canvas 构造函数。在这种情况下,我该怎么做才能将它整合进来? - Jus10
您可以将Canvas添加到您的布局中,或者让绘画更新记录的Canvas和传入的Canvas(同时绘制或使用drawPicture从一个复制到另一个)。 - jspcal
1
实际上,我在使用您提供的解决方案时遇到了一个奇怪的错误。我得到了格式异常,你有什么想法吗?这是代码 https://gist.github.com/speedyGonzales/51d8cd1d94ffe568dddb20759dc9b383 - speedyGonzales
如果我们想要保存为gif或连续图片,该怎么做? - ChildhoodAndy
@Jus10 在paint函数内定义另一个画布和记录器,并定义全局记录器,然后将全局记录器设置为局部记录器。 void paint(Canvas canvas, Size size) { PictureRecorder recorder = PictureRecorder(); outerRecorder = recorder; tempCanvas = Canvas(recorder); canvas.drawImage(image, Offset(0.0, 0.0), Paint()); tempCanvas.drawImage(image, Offset(0.0, 0.0), Paint()); for (Offset offset in points) { canvas.drawCircle(offset, 10, painter); tempCanvas.drawCircle(offset, 10, painter); } } - Omar Abdelazeem
我正在使用图片记录器,但它似乎会大大降低图像和画布的质量。尽管我已将filterQuality设置为FilterQuality.high,但我得到的图像仍然是像素化和模糊的,即使我绘制的圆形与CustomPaint画布相比也非常像素化。 - Arslan Kaleem

16
在您的小部件中添加渲染方法。
  ui.Image get rendered {
    // [CustomPainter] has its own @canvas to pass our
    // [ui.PictureRecorder] object must be passed to [Canvas]#contructor
    // to capture the Image. This way we can pass @recorder to [Canvas]#contructor
    // using @painter[SignaturePainter] we can call [SignaturePainter]#paint
    // with the our newly created @canvas
    ui.PictureRecorder recorder = ui.PictureRecorder();
    Canvas canvas = Canvas(recorder);
    SignaturePainter painter = SignaturePainter(points: _points);
    var size = context.size;
    painter.paint(canvas, size);
    return recorder.endRecording()
        .toImage(size.width.floor(), size.height.floor());
  }

然后使用 state 获取渲染后的图片

var image = signatureKey.currentState.rendered

现在,您可以使用toByteData(format: ui.ImageByteFormat.png)生成png图像,并使用asInt8List()储存。

var pngBytes = await image.toByteData(format: ui.ImageByteFormat.png);
File('your-path/filename.png')
    .writeAsBytesSync(pngBytes.buffer.asInt8List());

要完整地了解如何将画布导出为 PNG 的示例,请查看此示例https://github.com/vemarav/signature


我们如何创建JPG图像?PNG图像是透明的并且与背景混合。请分享任何建议。 - Kamlesh
我们如何获取它的反向?例如将ui.Image转换为canvas? - Hamza Muazzam

5
现有的解决方案对我来说可行,但是用PictureRecorder捕获的图像与屏幕上呈现的内容相比总是模糊的。最终,我意识到可以使用一些基本的画布技巧来解决这个问题。基本上,在创建PictureRecorderCanvas之后,将其大小设置为所需倍数(这里我将其设置为4倍)。然后只需缩放canvas.scale它。嘭——你生成的图像不再与现代分辨率屏幕上出现的内容模糊不清!
您可能希望将_overSampleScale值增加到更高的级别,以供打印或可能被放大/扩展的图像使用,或者如果您经常使用它并希望提高图像预览加载性能,则将其降低。在屏幕上使用它时,您需要使用ContainerImage.memory小部件限制在实际宽度和高度上,就像其他解决方案一样。理想情况下,这个数字应该是Flutter在其虚假“像素”中的DPI(即PictureRecorder捕获的内容)与屏幕实际DPI之间的比率。
static const double _overSampleScale = 4;
Future<ui.Image> get renderedScoreImage async {
    final recorder = ui.PictureRecorder();
    Canvas canvas = Canvas(recorder);
    final size = Size(widget.width * _overSampleScale, widget.height * _overSampleScale);
    final painter = SignaturePainter(points: _points);
    canvas.save();
    canvas.scale(_overSampleScale);
    painter.paint(canvas, size);
    canvas.restore();
    final data = recorder.endRecording()
      .toImage(size.width.floor(), size.height.floor());
    return data;
  }

3

如果您需要绘制自定义画笔所需的所有数据,那么您只需要执行以下操作(在本示例中,我的客户画笔需要“点数”,当然,这将根据您的用例而变化):

Future<void> _handleSavePressed() async {
    PictureRecorder recorder = PictureRecorder();
    Canvas canvas = Canvas(recorder);
    var painter = MyCustomPainter(points: points);
    var size = _containerKey.currentContext.size;

    painter.paint(canvas, size);
    ui.Image renderedImage = await recorder
        .endRecording()
        .toImage(size.width.floor(), size.height.floor());

    var pngBytes =
    await renderedImage.toByteData(format: ui.ImageByteFormat.png);

    Directory saveDir = await getApplicationDocumentsDirectory();
    String path = '${saveDir.path}/custom_image.jpg';
    File saveFile = File(path);

    if (!saveFile.existsSync()) {
      saveFile.createSync(recursive: true);
    }
    saveFile.writeAsBytesSync(pngBytes.buffer.asUint8List(), flush: true);
    await GallerySaver.saveImage(path, albumName: 'iDream');



    print('Image was saved!');
  }

基于https://gist.github.com/OPY-bbt/a5418127d8444393a2ef25ad2d966dc0的答案。


1

跟随完整的课程学习,使用Flutter > 3.0.0绘制PNG图像。

import 'dart:typed_data';
import 'dart:ui';
import 'dart:ui' as ui;

import 'package:flutter/material.dart';

class BitmapUtils {
  Future<Uint8List> generateImagePngAsBytes(String text) async {
    ByteData? image = await generateSquareWithText(text);
    return image!.buffer.asUint8List();
  }

  Future<ByteData?> generateSquareWithText(String text) async {
    final recorder = PictureRecorder();
    final canvas = Canvas(
        recorder, Rect.fromPoints(Offset(0.0, 0.0), Offset(200.0, 200.0)));

    final stroke = Paint()
      ..color = Colors.grey
      ..style = PaintingStyle.stroke;

    canvas.drawRect(Rect.fromLTWH(0.0, 0.0, 200.0, 200.0), stroke);

    final textPainter = TextPainter(
        text: TextSpan(
          text: text,
          style: TextStyle(
            color: Colors.black,
            fontSize: 30,
          ),
        ),
        textDirection: TextDirection.ltr,
        textAlign: TextAlign.center);
    textPainter.layout();

// Draw the text centered around the point (50, 100) for instance
    final offset =
        Offset(50 - (textPainter.width / 2), 100 - (textPainter.height / 2));
    textPainter.paint(canvas, offset);

    final picture = recorder.endRecording();
    ui.Image img = await picture.toImage(200, 200);
    final ByteData? pngBytes =
        await img.toByteData(format: ImageByteFormat.png);

    return pngBytes;
  }
}


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接