草庐IT

image - 使用 PictureRecorder 保存 Canvas 的图片导致图像为空

coder 2023-07-22 原文

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

我使用 Flutter(和 dart)和 VS Code 开发这个应用。

什么有效:

-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.

如果你们中的一些人能给我一些关于问题可能出在哪里的提示,那就太好了。

仅供引用:Flutter 是开发 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()));

最佳答案

首先,感谢您提出有趣的问题和独立的答案。这令人耳目一新,而且更容易帮助解决问题!

您的代码存在一些问题。首先是您不应该将 Canvas 和点从 Signature 传递到 SignatureState;这是 flutter 中的反模式。

这里的代码有效。我也做了一些对清理它的答案来说不必要的小事情,对此很抱歉 =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()));

您遇到的最大问题是您到处都是自己的 canvas 对象 - 这不是您应该做的事情。 CustomPainter 提供自己的 Canvas 。你画的时候,我觉得不是真的画对地方了。

此外,每次添加一个点时都会重新创建列表。我假设那是因为由于 other.points != points,您的 Canvas 不会以其他方式绘制。我已将其更改为采用修订 int,每次将某些内容添加到列表时该修订 int 都会递增。更好的方法是使用自己的列表子类,它自己完成此操作,但这对于这个示例来说有点过分了 =D。如果您确定永远不会修改列表中的任何元素,您也可以只使用 other.points.length != points.length

考虑到图像的大小,还有一些事情要做。我走了最简单的路线,使 Canvas 与其父级大小相同,这样渲染就更容易了,因为它可以再次使用相同的大小(因此渲染图像的大小与手机屏幕相同) ).如果您不想这样做而是渲染自己的尺寸,则可以做到。您必须向 SignaturePainter 传递一些内容,这样它才能知道如何缩放点,使它们适合新的大小(如果您愿意,可以为此调整 CanvasImageDraw 中的纵横比适合代码)。

如果您对我所做的还有任何其他问题,请随时提问。

关于image - 使用 PictureRecorder 保存 Canvas 的图片导致图像为空,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/49929983/

有关image - 使用 PictureRecorder 保存 Canvas 的图片导致图像为空的更多相关文章

  1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

    我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

  2. ruby - 使用 RubyZip 生成 ZIP 文件时设置压缩级别 - 2

    我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看ruby​​zip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d

  3. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

    类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

  4. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

    很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

  5. ruby - 在 Ruby 中使用匿名模块 - 2

    假设我做了一个模块如下:m=Module.newdoclassCendend三个问题:除了对m的引用之外,还有什么方法可以访问C和m中的其他内容?我可以在创建匿名模块后为其命名吗(就像我输入“module...”一样)?如何在使用完匿名模块后将其删除,使其定义的常量不再存在? 最佳答案 三个答案:是的,使用ObjectSpace.此代码使c引用你的类(class)C不引用m:c=nilObjectSpace.each_object{|obj|c=objif(Class===objandobj.name=~/::C$/)}当然这取决于

  6. ruby - 使用 ruby​​ 和 savon 的 SOAP 服务 - 2

    我正在尝试使用ruby​​和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我

  7. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  8. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

    我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

  9. ruby - 使用 ruby​​ 将 HTML 转换为纯文本并维护结构/格式 - 2

    我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h

  10. ruby - 在 64 位 Snow Leopard 上使用 rvm、postgres 9.0、ruby 1.9.2-p136 安装 pg gem 时出现问题 - 2

    我想为Heroku构建一个Rails3应用程序。他们使用Postgres作为他们的数据库,所以我通过MacPorts安装了postgres9.0。现在我需要一个postgresgem并且共识是出于性能原因你想要pggem。但是我对我得到的错误感到非常困惑当我尝试在rvm下通过geminstall安装pg时。我已经非常明确地指定了所有postgres目录的位置可以找到但仍然无法完成安装:$envARCHFLAGS='-archx86_64'geminstallpg--\--with-pg-config=/opt/local/var/db/postgresql90/defaultdb/po

随机推荐