Flutter: How would one save a Canvas/CustomPainter to an image file?

22,641

Solution 1

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

Solution 2

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

Solution 3

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;
  }
Share:
22,641

Related videos on Youtube

Jus10
Author by

Jus10

Updated on December 16, 2020

Comments

  • Jus10
    Jus10 over 3 years

    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
    Jus10 almost 6 years
    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?
  • jspcal
    jspcal almost 6 years
    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).
  • speedyGonzales
    speedyGonzales almost 6 years
    Actually I am having a strange error with your provided solution. I got Format exception, any idea why ? Here is the code gist.github.com/speedyGonzales/51d8cd1d94ffe568dddb20759dc9b‌​383
  • Kamlesh
    Kamlesh over 3 years
    how can we create jpg image? png image is transparent and mixed with background. Please share any suggestion.
  • ChildhoodAndy
    ChildhoodAndy about 3 years
    If we want to save as gif or continuous pictures, how to do that?
  • Omar Abdelazeem
    Omar Abdelazeem over 2 years
    @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); } }
  • Arslan Kaleem
    Arslan Kaleem over 2 years
    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
  • Hamza Muazzam
    Hamza Muazzam about 2 years
    how we can get it's reverse ? like ui.Image to canvas?