30

I am trying to collect a signature from the user and save it to an image. I have made it far enough that I can draw on the screen, but now I'd like to click a button to save to an image and store in my database.

This is what I have so far:

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

Not sure where to go from there...

Jus10
  • 14,519
  • 21
  • 52
  • 77

5 Answers5

32

You can capture the output of a CustomPainter with PictureRecorder. Pass your PictureRecorder instance to the constructor for your Canvas. The Picture returned by PictureRecorder.endRecording can then be converted to an Image with Picture.toImage. Finally, extract the image bytes using Image.toByteData.

Here's an example: https://github.com/rxlabz/flutter_canvas_to_image

jspcal
  • 50,847
  • 7
  • 72
  • 76
  • 6
    In my code I'm using `paint(Canvas canvas, Size size)` so there is no Canvas contructor. How would I incorporate that in my situation? – Jus10 May 13 '18 at 21:13
  • You can add a `Canvas` to your layout or have paint update both the recording `Canvas` and the one that was passed in (paint to both or use `drawPicture` to copy from one to another). – jspcal May 13 '18 at 21:31
  • 1
    Actually I am having a strange error with your provided solution. I got Format exception, any idea why ? Here is the code https://gist.github.com/speedyGonzales/51d8cd1d94ffe568dddb20759dc9b383 – speedyGonzales Jun 29 '18 at 08:42
  • If we want to save as gif or continuous pictures, how to do that? – ChildhoodAndy Mar 26 '21 at 00:49
  • @Jus10 Define another canvas and recorder inside paint function and define global recorder and then make global recorder equal to local inside paint function . 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 Jan 11 '22 at 13:28
  • I am using the picture recorder but its seems that it drops the image and canvas quality so much. Although I have set `filterQualtiy` to `FilterQuality.high` but still image I get is pixelated and blur even the circle I draw is very pixelated as compared to CustomPaint canvas – Arslan Kaleem Jan 25 '22 at 07:17
16

Add the rendered method in your widget

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

Then using state fetch the rendered image

var image = signatureKey.currentState.rendered

Now, you can produce png Image using toByteData(format: ui.ImageByteFormat.png) and store using asInt8List()

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

For complete example, on how to export canvas as png check out this example https://github.com/vemarav/signature

log0
  • 10,489
  • 4
  • 28
  • 62
Aravind Vemula
  • 1,571
  • 17
  • 22
5

The existing solutions worked for me, but the images I captured with PictureRecorder were always blurry vs. what was rendering on-screen. I eventually realized I could use some elementary Canvas tricks to pull this off. Basically, after you create the PictureRecorder's Canvas, set its size to multiple times your desired scale (here I have it set to 4x). Then just canvas.scale it. Boom - your generated images are no longer blurry vs. what appears on screens with modern resolutions!

You may want to crank the _overSampleScale value higher for printed or images that may be blown up/expanded, or lower if you're using this a ton and want to improve image preview loading performance. Using it on-screen, you'll need to constrain your Image.memory Widget with a Container of the actual width and height, as with the other solutions. Ideally this number would be the ratio between Flutter's DPI in its fake "pixels" (i.e. what PictureRecorder captures) and the actual DPI of the screen.

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;
  }
WarDoctor113
  • 141
  • 2
  • 5
3

Given all the data that you need to paint your custom painter, this is all you need to do (in this example, "points" were needed for my customer painter,of course this will change based on your usecase):

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

Answer based on https://gist.github.com/OPY-bbt/a5418127d8444393a2ef25ad2d966dc0

yoni keren
  • 300
  • 2
  • 14
1

Follow the complete class to draw a PNG image using Flutter > 3.0.0

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;
  }
}
Roger Gusmao
  • 3,788
  • 1
  • 20
  • 17