Poor image quality in saving image from a screenshot ( flutter )

Viewed 1115

I have a screen which lets users import images and drag n drop them to reposition, resize and rotate them. Each object is a widget and the parent of all these objects is a Stack widget. I have used matrix_gesture_detector 0.1.0 library to handle the transformations. I have saved this Stack widget as an image using the screenshot library. However, if the pixelRatio is low, I get pixelated images, but if its high, the image size increases by a lot (around 25MB). I'd like to know how I can improve the quality of the image without increasing the image size. If there are some other approaches I'd like to know about them too. My images can be AssetImage or NetWorkImage.

My code for taking screenshot:

....
....

Screenshot(
   controller: screenshotController,
   child: Container(
      key: Key("canvas"),
      decoration: BoxDecoration(
        color: currentColor,
        borderRadius: BorderRadius.all(
          Radius.circular(dp2),
        ),
      ),
      child: Stack(
        alignment: Alignment.center,
        children: _selectedWidgets, // here selected widgets is a lis of widgets
      ),
    ),
  )

//save image method
saveImage() async {
    

    String fileName = DateTime.now().toIso8601String();
    var path = '$directoryPath/$fileName.png';

    screenshotController
        .capture(
      pixelRatio: 5,
      path: path,
      delay: Duration(milliseconds: 10),
    ).then((File image) async {
      image = await compressImage(image);
     
      ........
      .........
      
    }).catchError((onError, stack) {
      log("stack = $stack");
    });
  }

// compressing function compress based on the size of the image
Future<File> compressImage(File image) async {

  int fileSize = image.lengthSync();

  int percentage = getPercentage(fileSize);

  return FlutterNativeImage.compressImage(image.path, quality: 25, percentage: percentage);
}

int getPercentage(int fileSize) {
  if(fileSize <= 1073741824)
    return 75;
  if(fileSize <= 2147483648)
    return 60;
  if(fileSize <= 3221225472)
    return 50;
  if(fileSize <= 4294967296)
    return 40;
  if(fileSize <= 5368709120)
    return 25;

  return 25;
}


2 Answers

So the whole goal of your app is to create a collage off images and save them to the disk? (maybe useful if you'd explain this first)

I would suggest either limiting the size of the "canvas" area, or resize it before saving it. You could also experiment with various compression algorithms, like JPEG, and their parameters. Not sure about the quality off FlutterNativeImage. Maybe you could look into some other packages? I see the flutter_image_compress package has a lot of likes.

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