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

Posted

技术标签:

【中文标题】Flutter:如何将 Canvas/CustomPainter 保存到图像文件中?【英文标题】:Flutter: How would one save a Canvas/CustomPainter to an image file? 【发布时间】:2018-10-23 12:47:31 【问题描述】:

我正在尝试从用户那里收集签名并将其保存到图像中。我已经做到了可以在屏幕上绘图的程度,但现在我想单击一个按钮以保存到图像并存储在我的数据库中。

这是我目前所拥有的:

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;

不知道从那里去哪里......

【问题讨论】:

【参考方案1】:

您可以使用PictureRecorder 捕获CustomPainter 的输出。将PictureRecorder 实例传递给Canvas 的构造函数。然后,PictureRecorder.endRecording 返回的 Picture 可以通过 Picture.toImage 转换为 Image。最后,使用Image.toByteData提取图像字节。

这是一个例子:https://github.com/rxlabz/flutter_canvas_to_image

【讨论】:

在我的代码中,我使用的是paint(Canvas canvas, Size size),所以没有 Canvas 构造函数。我将如何将其融入我的情况? 您可以在布局中添加Canvas 或让绘制更新记录Canvas 和传入的记录(绘制到两者或使用drawPicture 从一个复制到另一个)。 实际上,您提供的解决方案出现了一个奇怪的错误。我得到了格式异常,知道为什么吗?这是代码gist.github.com/speedyGonzales/51d8cd1d94ffe568dddb20759dc9b383 如果我们要保存为gif或者连续图片,怎么做呢? @Jus10 在绘图函数中定义另一个画布和记录器并定义全局记录器,然后使全局记录器等于本地内部绘图函数。 void paint(Canvas canvas, Size size) PictureRecorder recorder = PictureRecorder();外部记录器 = 记录器; tempCanvas = 画布(录音机); 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, 画家); 【参考方案2】:

在您的小部件中添加渲染方法

  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());
  

然后使用状态获取渲染图像

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 图像是透明的并与背景混合。请分享任何建议。【参考方案3】:

现有的解决方案对我有用,但我使用 PictureRecorder 捕获的图像与屏幕上呈现的图像相比总是模糊不清。我最终意识到我可以使用一些基本的 Canvas 技巧来实现这一目标。基本上,在创建PictureRecorderCanvas 之后,将其大小设置为所需比例的数倍(这里我将其设置为4x)。然后只需canvas.scale它。 Boom - 与现代分辨率屏幕上显示的图像相比,您生成的图像不再模糊!

您可能希望将_overSampleScale 的值调高以用于打印或可能会被放大/扩展的图像,或者如果您大量使用此值并希望提高图像预览加载性能,则可以调低该值。在屏幕上使用它,您需要使用 实际 宽度和高度的 Container 约束您的 Image.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;
  

【讨论】:

以上是关于Flutter:如何将 Canvas/CustomPainter 保存到图像文件中?的主要内容,如果未能解决你的问题,请参考以下文章

将 Flutter 应用迁移到 Flutter Web 时如何显示图像资产?

Flutter Desktop - flutter-desktop-embedding 如何将文件保存到硬盘

如何将 Flutter 与 Genymotion 连接起来?

如何将flutter_web迁移到蜂鸟中颤动?

Flutter:如何将列表转换为 JSON

Flutter:如何将变量从 StatelessWidget 传递到 StatefulWidget