使用PictureRecorder保存画布图片会导致空图像 [英] Saving picture of Canvas with PictureRecorder results in empty image

查看:1360
本文介绍了使用PictureRecorder保存画布图片会导致空图像的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

首先,该计划的目标是允许用户使用手机或平板电脑签署官方文件。
该程序必须将图像保存为png。

First things first, the goal of this program is to allow user to sign officials document with a phone or tablet. The program has to save the image as a png.

我使用Flutter(和dart)和VS Code开发此应用程序。

I use Flutter(and dart) and VS Code to develop this app.

什么有效:

-The user can draw on the canvas.

什么行不通:

-the image can't be saved as a png

什么我发现:

-The **Picture** get by ending the **PictureRecoder** of the canvas is empty (i tried to display it but no success)
-I tried to save it as a PNG using **PictureRecorder.EndRecording().toImage(100.0,100.0).toByteDate(EncodingFormat.png())** but the size is really small, and it can't be displayed.

如果你们中的一些人可以给我一些关于问题所在的暗示,那将是非常好的。

If some of you could give me some hint about where the problem could be, it would be really nice.

仅供参考:Flutter是开发频道的最新版本

FYI : Flutter is at the latest version on the dev channel

以下是完整代码:

import 'dart:ui' as ui;
import 'package:flutter/material.dart';

///This class is use just to check if the Image returned by
///the PictureRecorder of the first Canvas is not empty.
///FYI : The image is not displayed.
class CanvasImageDraw extends CustomPainter {

    ui.Picture picture;

    CanvasImageDraw(this.picture);

    @override
    void paint(ui.Canvas canvas, ui.Size size) {
      canvas.drawPicture(picture);
    }

    @override
    bool shouldRepaint(CustomPainter oldDelegate) {
      return true;
    }
}

///Class used to display the second canvas
class SecondCanvasView extends StatelessWidget {

    ui.Picture picture;

    var canvas;
    var pictureRecorder= new ui.PictureRecorder();

    SecondCanvasView(this.picture) {
      canvas = new Canvas(pictureRecorder);
    }

    @override
    Widget build(BuildContext context) {
      return new Scaffold(
        appBar: new AppBar(
          title: new Text('Image Debug'),
        ),
        body: new Column(
          children: <Widget>[
            new Text('Top'),
            CustomPaint(
              painter: new CanvasImageDraw(picture),
            ),
            new Text('Bottom'),
          ],
        ));
    }
}

///This is the CustomPainter of the first Canvas which is used
///to draw to display the users draw/sign.
class SignaturePainter extends CustomPainter {
  final List<Offset> points;
  Canvas canvas;
  ui.PictureRecorder pictureRecorder;

  SignaturePainter(this.points, this.canvas, this.pictureRecorder);

  void paint(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;
}

///Classes used to get the user draw/sign, send it to the first canvas, 
///then display it.
///'points' list of position returned by the GestureDetector
class Signature extends StatefulWidget {
  ui.PictureRecorder pictureRecorder = new ui.PictureRecorder();
  Canvas canvas;
  List<Offset> points = [];

  Signature() {
    canvas = new Canvas(pictureRecorder);
  }

  SignatureState createState() => new SignatureState(canvas, points);
}

class SignatureState extends State<Signature> {
  List<Offset> points = <Offset>[];
  Canvas canvas;

  SignatureState(this.canvas, this.points);

  Widget build(BuildContext context) {
    return new Stack(
      children: [
        GestureDetector(
          onPanUpdate: (DragUpdateDetails details) {
            RenderBox referenceBox = context.findRenderObject();
            Offset localPosition =
            referenceBox.globalToLocal(details.globalPosition);

            setState(() {
              points = new List.from(points)..add(localPosition);
            });
          },
          onPanEnd: (DragEndDetails details) => points.add(null),
        ),
        new Text('data'),
        CustomPaint(
            painter:
                new SignaturePainter(points, canvas, widget.pictureRecorder)),
        new Text('data')
      ],
    );
  }
}


///The main class which display the first Canvas, Drawing/Signig area
///
///Floating action button used to stop the PictureRecorder's recording,
///then send the Picture to the next view/Canvas which should display it
class DemoApp extends StatelessWidget {
  Signature sign = new Signature();
  Widget build(BuildContext context) => new Scaffold(
        body: sign,
        floatingActionButton: new FloatingActionButton(
          child: new Icon(Icons.save),
          onPressed: () async {
            ui.Picture picture = sign.pictureRecorder.endRecording();
            Navigator.push(context, new MaterialPageRoute(builder: (context) => new SecondCanvasView(picture)));
          },
        ),
  );
}

void main() => runApp(new MaterialApp(home: new DemoApp()));


推荐答案

首先,感谢有趣的问题和自我 - 包含答案。这令人耳目一新,并且更容易提供帮助!

First off, thanks for the interesting question and the self-contained answer. That is refreshing to see and a lot easier to help out with!

您的代码存在一些问题。首先,你不应该通过画布&从Signature到SignatureState的点;这是一个颤动的反模式。

There's a few issues with your code. The first is that you shouldn't be passing the canvas & points from Signature to SignatureState; that's an antipattern in flutter.

此代码可行。我已经完成了一些对于清理它的答案没有必要的一些小事情,对不起那个= D。

This code here works. I've done a few little things that were unnecessary to the answer to clean it up as well, sorry about that =D.

import 'dart:ui' as ui;

import 'package:flutter/material.dart';

///This class is use just to check if the Image returned by
///the PictureRecorder of the first Canvas is not empty.
class CanvasImageDraw extends CustomPainter {
  ui.Image image;

  CanvasImageDraw(this.image);

  @override
  void paint(ui.Canvas canvas, ui.Size size) {
    // simple aspect fit for the image
    var hr = size.height / image.height;
    var wr = size.width / image.width;

    double ratio;
    double translateX;
    double translateY;
    if (hr < wr) {
      ratio = hr;
      translateX = (size.width - (ratio * image.width)) / 2;
      translateY = 0.0;
    } else {
      ratio = wr;
      translateX = 0.0;
      translateY = (size.height - (ratio * image.height)) / 2;
    }

    canvas.translate(translateX, translateY);
    canvas.scale(ratio, ratio);
    canvas.drawImage(image, new Offset(0.0, 0.0), new Paint());
  }

  @override
  bool shouldRepaint(CanvasImageDraw oldDelegate) {
    return image != oldDelegate.image;
  }
}

///Class used to display the second canvas
class SecondView extends StatelessWidget {
  ui.Image image;

  var pictureRecorder = new ui.PictureRecorder();

  SecondView({this.image});

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
        appBar: new AppBar(
          title: new Text('Image Debug'),
        ),
        body: new Column(
          children: <Widget>[
            new Text('Top'),
            CustomPaint(
              painter: new CanvasImageDraw(image),
              size: new Size(200.0, 200.0),
            ),
            new Text('Bottom'),
          ],
        ));
  }
}

///This is the CustomPainter of the first Canvas which is used
///to draw to display the users draw/sign.
class SignaturePainter extends CustomPainter {
  final List<Offset> points;
  final int revision;

  SignaturePainter(this.points, [this.revision = 0]);

  void paint(canvas, Size size) {
    if (points.length < 2) return;

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

  // simplified this, but if you ever modify points instead of changing length you'll
  // have to revise.
  bool shouldRepaint(SignaturePainter other) => other.revision != revision;
}

///Classes used to get the user draw/sign, send it to the first canvas,
///then display it.
///'points' list of position returned by the GestureDetector
class Signature extends StatefulWidget {
  Signature({Key key}): super(key: key);

  @override
  State<StatefulWidget> createState()=> new SignatureState();
}

class SignatureState extends State<Signature> {
  List<Offset> _points = <Offset>[];
  int _revision = 0;

  ui.Image get rendered {
    var pictureRecorder = new ui.PictureRecorder();
    Canvas canvas = new Canvas(pictureRecorder);
    SignaturePainter painter = new SignaturePainter(_points);

    var size = context.size;
    // if you pass a smaller size here, it cuts off the lines
    painter.paint(canvas, size);
    // if you use a smaller size for toImage, it also cuts off the lines - so I've
    // done that in here as well, as this is the only place it's easy to get the width & height.
    return pictureRecorder.endRecording().toImage(size.width.floor(), size.height.floor());
  }

  void _addToPoints(Offset position) {
    _revision++;
    _points.add(position);
  }

  Widget build(BuildContext context) {
    return new Stack(
      children: [
        GestureDetector(
          onPanStart: (DragStartDetails details) {
            RenderBox referenceBox = context.findRenderObject();
            Offset localPosition = referenceBox.globalToLocal(details.globalPosition);
            setState(() {
              _addToPoints(localPosition);
            });
          },
          onPanUpdate: (DragUpdateDetails details) {
            RenderBox referenceBox = context.findRenderObject();
            Offset localPosition = referenceBox.globalToLocal(details.globalPosition);
            setState(() {
              _addToPoints(localPosition);
            });
          },
          onPanEnd: (DragEndDetails details) => setState(() => _addToPoints(null)),
        ),
        CustomPaint(painter: new SignaturePainter(_points, _revision)),
      ],
    );
  }
}

///The main class which display the first Canvas, Drawing/Signing area
///
///Floating action button used to stop the PictureRecorder's recording,
///then send the Picture to the next view/Canvas which should display it
class DemoApp extends StatelessWidget {
  GlobalKey<SignatureState> signatureKey = new GlobalKey();

  Widget build(BuildContext context) => new Scaffold(
        body: new Signature(key: signatureKey),
        floatingActionButton: new FloatingActionButton(
          child: new Icon(Icons.save),
          onPressed: () async {
            var image = signatureKey.currentState.rendered;
            Navigator.push(context, new MaterialPageRoute(builder: (context) => new SecondView(image: image)));
          },
        ),
      );
}

void main() => runApp(new MaterialApp(home: new DemoApp()));

你遇到的最大问题是你持有自己的画布遍布整个地方的物品 - 这不是你应该做的事情。 CustomPainter提供自己的画布。当你画画时,我认为它实际上并没有画到正确的位置。

The biggest issue you had is that you were holding your own canvas objects all over the place - that's not something you should ever do really. CustomPainter provides its own canvas. When you were painting, I don't think it was actually painting to the right place.

此外,每次添加一个点时,你都在重新创建列表。我将假设这是因为你的画布不会因为 other.points!= points 而绘制。我已将其更改为采用修订版int,每次将某些内容添加到列表中时都会增加。更好的方法是使用你自己的列表子类,它自己做了这个,但这个例子有点多= D。您也可以使用 other.points.length!= points.length 如果您确定永远不会修改列表中的任何元素。

Also, you were recreating the list each time you added a point. I'm going to assume that's because your canvas wouldn't draw otherwise due to the other.points != points. I've changed it to take a revision int which is incremented each time something is added to the list. Even better would be to use your own list subclass that did this on its own, but that's a bit much for this example =D. You could also just use other.points.length != points.length if you're for sure never going to modify any of the elements in the list.

考虑到图像的大小,还有一些事情要做。我已经走了最简单的路线,并且使得画布的尺寸与其父版相同,因此渲染更容易,因为它可以再次使用相同的尺寸(因此渲染图像的大小与手机的屏幕相同) )。如果您不想这样做,而是渲染自己的大小,那么就可以完成。你必须将一些东西传递给SignaturePainter,这样它才能知道如何缩放点以使它们适合新的大小(如果你愿意的话,可以在CanvasImageDraw中调整宽高比适合代码)。

There were a few things to do considering the size of the image as well. I've gone the easiest route and made it so that the canvas is just the same size as its parent, so that the rendering is easier as it can just use that same size again (and therefore renders an image the size of the phone's screen). If you didn't want to do this but rather render your own size, it can be done. You'd have to pass in something to the SignaturePainter so it would know how to scale the points so they fit within the new size (you could adapt the aspect fit code in CanvasImageDraw for that if you so wished).

如果您对我的所作所为有任何其他疑问,请随时提出。

If you have any other questions about what I did, feel free to ask.

这篇关于使用PictureRecorder保存画布图片会导致空图像的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆