21

package:flutter/src/rendering/proxy_box.dart': Failed assertion: line 2813 pos 12: '!debugNeedsPaint': is not true.

I am trying to take screenshot in flutter, but I am getting exception. I visited many links but nothing worked.

Future<Uint8List> _capturePng() async {
    try {
        print('inside');
        RenderRepaintBoundary boundary = _globalKey.currentContext.findRenderObject();
        ui.Image image = await boundary.toImage(pixelRatio: 3.0);
        ByteData byteData = await image.toByteData(format: ui.ImageByteFormat.png);
        var pngBytes = byteData.buffer.asUint8List();
        var bs64 = base64Encode(pngBytes);
        print(pngBytes);
        print(bs64);
        setState(() {});
        return pngBytes;
    } catch (e) {
        print(e);
    }
}
Valentin Vignal
  • 6,151
  • 2
  • 33
  • 73
xbadal
  • 1,284
  • 2
  • 11
  • 24
  • 1
    Where is this function invoked? It will most likely not work if it is invoked within `initState` of a `StatefulWidget` because the `RenderObject` you are retrieving from the context of `_globalKey` has not been drawn/painted yet. – Ovidiu Sep 30 '19 at 15:08

6 Answers6

27

You can find official toImage example here. But it looks like it doesn't work without delay between button tap and toImage call.

There is an issue in the official repository: https://github.com/flutter/flutter/issues/22308

The cause for this: your tap initializes animation for a button and RenderObject.markNeedsPaint gets called recursively including parents, so you should wait while debugNeedsPaint will be false again. toImage function just throws assertion error in this case:

  Future<ui.Image> toImage({ double pixelRatio = 1.0 }) {
    assert(!debugNeedsPaint);
    final OffsetLayer offsetLayer = layer;
    return offsetLayer.toImage(Offset.zero & size, pixelRatio: pixelRatio);
  }

https://github.com/flutter/flutter/blob/f0553ba58e6455aa63fafcdca16100b81ff5c3ce/packages/flutter/lib/src/rendering/proxy_box.dart#L2857

  bool get debugNeedsPaint {
    bool result;
    assert(() {
      result = _needsPaint;
      return true;
    }());
    return result;
  }

https://github.com/flutter/flutter/blob/0ca5e71f281cd549f1b5284e339523ad93544c60/packages/flutter/lib/src/rendering/object.dart#L2011

Actually assert function is used only in development, so as you can see you will not have the trouble with the error in production. But I don't know what kind of troubles you can get instead, probably no ).

The next code is not great but it works:

class _MyHomePageState extends State<MyHomePage> {
  GlobalKey globalKey = GlobalKey();

  Future<Uint8List> _capturePng() async {
    RenderRepaintBoundary boundary = globalKey.currentContext.findRenderObject();

    if (boundary.debugNeedsPaint) {
      print("Waiting for boundary to be painted.");
      await Future.delayed(const Duration(milliseconds: 20));
      return _capturePng();
    }

    var image = await boundary.toImage();
    var byteData = await image.toByteData(format: ImageByteFormat.png);
    return byteData.buffer.asUint8List();
  }

  void _printPngBytes() async {
    var pngBytes = await _capturePng();
    var bs64 = base64Encode(pngBytes);
    print(pngBytes);
    print(bs64);
  }

  @override
  Widget build(BuildContext context) {
    return RepaintBoundary(
      key: globalKey,
      child: Center(
        child: FlatButton(
          color: Color.fromARGB(255, 255, 255, 255),
          child: Text('Capture Png', textDirection: TextDirection.ltr),
          onPressed: _printPngBytes
        ),
      ),
    );
  }
}
exxbrain
  • 596
  • 4
  • 11
5

Your code is already fine, you shouldn't face any problem in release mode because from docs:

debugNeedsPaint: Whether this render object's paint information is dirty.

This is only set in debug mode. In general, render objects should not need to condition their runtime behavior on whether they are dirty or not, since they should only be marked dirty immediately prior to being laid out and painted.

It is intended to be used by tests and asserts.

It is possible (and indeed, quite common) for debugNeedsPaint to be false and debugNeedsLayout to be true. The render object will still be repainted in the next frame when this is the case, because the markNeedsPaint method is implicitly called by the framework after a render object is laid out, prior to the paint phase.

However, if you still need a solution, you can try this:

Future<Uint8List> _capturePng() async {
  try {
    print('inside');
    RenderRepaintBoundary boundary = _globalKey.currentContext.findRenderObject();
    
    // if it needs repaint, we paint it.
    if (boundary.debugNeedsPaint) {
      Timer(Duration(seconds: 1), () => _capturePng());
      return null;
    }
    
    ui.Image image = await boundary.toImage(pixelRatio: 3.0);
    ByteData byteData = await image.toByteData(format: ui.ImageByteFormat.png);
    var pngBytes = byteData.buffer.asUint8List();
    var bs64 = base64Encode(pngBytes);
    print(pngBytes);
    print(bs64);
    setState(() {});
    return pngBytes;
  } catch (e) {
    print(e);
    return null;
  }
}
Community
  • 1
  • 1
CopsOnRoad
  • 237,138
  • 77
  • 654
  • 440
  • I think this code will not work because `_capturePng` will return `null`, and then in one second bytes will be printed in console. I mean `if (boundary.debugNeedsPaint) { Timer(Duration(seconds: 1), () => _capturePng()); return null; }` – exxbrain Oct 07 '19 at 20:32
  • @DenisZakharov It won't happen that way, give it a run and let me know. – CopsOnRoad Oct 07 '19 at 20:39
  • Your `Timer` scheduled asynchronously, callback will be executed in one second after `_capturePng` returned null. Please check my solution I'm sure you will understand what I mean. – exxbrain Oct 07 '19 at 20:48
  • I hope you will vote for my solution then )). BTW I checked your code. It works as I said. – exxbrain Oct 07 '19 at 20:54
  • @DenisZakharov Sorry but I am not able to test your code as of now but I can see you used my logic – CopsOnRoad Oct 07 '19 at 21:02
  • `await Future.delayed(const Duration(milliseconds: 20));` is another logic and it works. Guys from flutter issue https://github.com/flutter/flutter/issues/22308 also use Timer but their function returns nothing. So their solution works too. But not yours. It's strange that you don't see it. – exxbrain Oct 07 '19 at 21:04
  • Your function should return Future but returns null (in debug). This is the problem. – exxbrain Oct 07 '19 at 21:08
  • @DenisZakharov I will be happy to see if you can create a reproducible code with error using the solution I provided. You probably don't know what `return null` is doing there. – CopsOnRoad Oct 07 '19 at 21:48
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/200533/discussion-between-denis-zakharov-and-copsonroad). – exxbrain Oct 07 '19 at 21:55
2

I have changed logic of return from null to function _capturePng() again.

Reff: @CopsOnRoad

Check the below code

    Future<Uint8List> _capturePng() async {
  try {
    print('inside');
    RenderRepaintBoundary boundary = _globalKey.currentContext.findRenderObject();
    
    // if it needs repaint, we paint it.
    if (boundary.debugNeedsPaint) {

//if debugNeedsPaint return to function again.. and loop again until boundary have paint.
return _capturePng()); 
    }
    
    ui.Image image = await boundary.toImage(pixelRatio: 3.0);
    ByteData byteData = await image.toByteData(format: ui.ImageByteFormat.png);
    var pngBytes = byteData.buffer.asUint8List();
    var bs64 = base64Encode(pngBytes);
    print(pngBytes);
    print(bs64);
    setState(() {});
    return pngBytes;
  } catch (e) {
    print(e);
    return null;
  }
}
Adam Rabbani
  • 21
  • 1
  • 5
2

This is an important addition to the accepted answer. As per documentation of debugNeedsPaint, checking this flag in release mode will crash the app (In release builds, this throws.)

So we need to only check this flag in debug mode, using kDebugMode from import 'package:flutter/foundation.dart';

    var debugNeedsPaint = false;

    //https://stackoverflow.com/questions/49707028/how-to-check-flutter-application-is-running-in-debug
    //In release builds, this throws (boundary.debugNeedsPaint)
    if (kDebugMode) debugNeedsPaint = boundary.debugNeedsPaint;

    if (debugNeedsPaint) {
      print("Waiting for boundary to be painted.");
      await Future.delayed(const Duration(milliseconds: 20));
      return _capturePng();
    }
adrianvintu
  • 1,041
  • 10
  • 7
0

I have face the same issue

After a long period of research

I finally find the problem

if the widget you want to capture is in a ListView,and the listview is very long,so your widget is out of screen,the capture will faild.

user3044484
  • 463
  • 5
  • 7
0

Here's the solution for Flutter 2.0+ method for screenshot and share it on social media.

import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:flutter_datetime_picker/flutter_datetime_picker.dart';

class selClass extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: sel(),
    );
  }
}

class sel extends StatefulWidget {
  @override
  _selState createState() => _selState();
}

class _selState extends State<sel> {
  String _date = "Not set";

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(child:SingleChildScrollView(
    child: Column(
    children: <Widget>[
    Container(
    color: Colors.grey,
      width: MediaQuery
          .of(context)
          .size
          .width,
      height: MediaQuery
          .of(context)
          .size
          .height / 10,
      child: Center(child: Text("Attendance",
        style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20.0),),),
    ),
    SizedBox(height: 30.0,),
    Padding(
    padding: const EdgeInsets.all(16.0),
    child: Container(
    child: Column(
    mainAxisSize: MainAxisSize.max,
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
    RaisedButton(
    shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(5.0)),
    elevation: 4.0,
    onPressed: () {
    DatePicker.showDatePicker(context,
    theme: DatePickerTheme(
    containerHeight: 210.0,
    ),
    showTitleActions: true,
    minTime: DateTime(2000, 1, 1),
    maxTime: DateTime(2022, 12, 31),
    onConfirm: (date) {
    print('confirm $date');
    _date = '${date.year} - ${date.month} - ${date.day}';
    setState(() {});
    },
    currentTime: DateTime.now(),
    locale: LocaleType.en);
    },
    child: Container(
    alignment: Alignment.center,
    height: 50.0,
    child: Row(
    mainAxisAlignment: MainAxisAlignment.spaceBetween,
    children: <Widget>[
    Row(
    children: <Widget>[
    Container(
    child: Row(
    children: <Widget>[
    Icon(
    Icons.date_range,
    size: 18.0,
    color: Colors.teal,
    ),
    Text(
    " $_date",
    style: TextStyle(
    color: Colors.teal,
    fontWeight: FontWeight.bold,
    fontSize: 18.0),
    ),
    ],
    ),
    )
    ],
    ),
    Text(
    "  Change",
    style: TextStyle(
    color: Colors.teal,
    fontWeight: FontWeight.bold,
    fontSize: 18.0),
    ),
    ],
    ),
    ),
    color: Colors.white,
    ),
    SizedBox(
    height: 20.0,
    ),
    ]
    ,
    )
    ,
    )
    )
    ]))));
  }
}
m4n0
  • 29,823
  • 27
  • 76
  • 89
sanchit
  • 149
  • 2
  • 3