Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow flutter to save widget to image even if widget is offscreen #40064

Open
tapizquent opened this issue Sep 8, 2019 · 70 comments
Open

Allow flutter to save widget to image even if widget is offscreen #40064

tapizquent opened this issue Sep 8, 2019 · 70 comments
Labels
a: images Loading, displaying, rendering images c: proposal A detailed proposal for a change to Flutter framework flutter/packages/flutter repository. See also f: labels. P3 Issues that are less important to the Flutter project team-framework Owned by Framework team triaged-framework Triaged by Framework team

Comments

@tapizquent
Copy link

tapizquent commented Sep 8, 2019

When you have an application, sometimes it's important to save widgets as images even if they're not on screen.

An example of how this would be necessary: having a customized widget displayed in a thumbnail in the app but you wish to save as an image a full size/full screen version of that same widget.

Steps to Reproduce

  1. ... Following documentation, use RepaintBoundary with a global key in the widget to save as image.
  2. ... Wrap the previous widget in an OffStage widget.
  3. ... Try to call RenderRepaintBoundary boundary =
    _exportLayoutKey.currentContext.findRenderObject();
    ui.Image image = await boundary.toImage();

Logs

It will throw an error

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

running flutter doctor -v

[✓] Flutter (Channel dev, v1.9.7, on Mac OS X 10.14.6 18G95, locale en-US)
    • Flutter version 1.9.7 at /Users/tapizquent/Projects/tools/flutter
    • Framework revision 4984d1a33d (11 days ago), 2019-08-28 17:04:07 -0700
    • Engine revision f52c0b9270
    • Dart version 2.5.0

 
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.0)
    • Android SDK at /Users/tapizquent/Library/Android/sdk
    • Android NDK location not configured (optional; useful for native profiling support)
    • Platform android-29, build-tools 29.0.0
    • Java binary at: /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 1.8.0_152-release-1343-b01)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 10.3)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Xcode 10.3, Build version 10G8
    • CocoaPods version 1.7.4

[✓] Android Studio (version 3.4)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin version 36.1.1
    • Dart plugin version 183.6270
    • Java version OpenJDK Runtime Environment (build 1.8.0_152-release-1343-b01)

[✓] VS Code (version 1.38.0)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.4.1

[✓] Connected device (1 available)
    • Tapizquent • e75fc710ec1404ac1c323311b0ba25049db7e7d4 • ios • iOS 12.4.1

• No issues found!

Update:

Minimal project showcasing scenario:

import 'dart:convert';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

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

  Future<void> _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);
      return pngBytes;
    } catch (e) {
      print(e);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Widget To Image demo'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'Tapping button below should capture placeholder image capture the placeholder image',
            ),
            RepaintBoundary(
              key: _globalKey,
              child: Offstage(
                child: Container(
                  width: MediaQuery.of(context).size.width * 0.8,
                  child: Placeholder(),
                ),
              ),
            ),
            RaisedButton(
              child: Text('capture Image'),
              onPressed: _capturePng,
            ),
          ],
        ),
      ),
    );
  }
}

I have tried doing it like this, and also switching Offstage and RepaintBoundary:

RepaintBoundary(
    key: _globalKey,
    child: Offstage(
        child: Container(
            width: MediaQuery.of(context).size.width * 0.8,
            child: Placeholder(),
         ),
     ),
),
@BondarenkoStas
Copy link

@tapizquent Please provide a small self-contained app that demonstrates the problem. You can include small examples inline if they're enclosed by backticks, or just provide a public link to a github "gist" - https://gist.github.com/.
It's needed to make sure to reproduce it correctly.

@BondarenkoStas BondarenkoStas added the waiting for customer response The Flutter team cannot make further progress on this issue until the original reporter responds label Sep 9, 2019
@HansMuller HansMuller added the a: images Loading, displaying, rendering images label Sep 9, 2019
@tapizquent
Copy link
Author

tapizquent commented Sep 9, 2019

@BondarenkoStas I apologize, I have added a main.dart you can run simulating the scenario. Container with Placeholder act as any widget you'd like to "photograph".

I added the Offstage option as a recommendation coming from: https://stackoverflow.com/questions/55134343/renderrepaintboundary-to-image-without-adding-widget-to-screen#comment102118492_55134343

which should "technically" work , but I also tried it hiding the widget by setting Opacity to 0 but that doesn't work either.

@no-response no-response bot removed the waiting for customer response The Flutter team cannot make further progress on this issue until the original reporter responds label Sep 9, 2019
@VladyslavBondarenko VladyslavBondarenko added the framework flutter/packages/flutter repository. See also f: labels. label Jan 15, 2020
@porfirioribeiro
Copy link

This also bothers me, i want to create thumbnails using Widgets but they wont be visible, so i tought about using Offstage but then it gives error with: '!debugNeedsPaint': is not true.

There should be a way to do this

@TahaTesser TahaTesser added the c: proposal A detailed proposal for a change to Flutter label Feb 28, 2020
@christian-muertz
Copy link
Contributor

The problem lies in the way the Offstage widget works. It just lays out the child but does not paint it. For the RepaintBoundary.toImage to work the render object has to be painted at least once.

You could create a separate PipelineOwner and BuildOwner to manage a separate element/render tree, then inflate the tree, and finally flush the pipeline and then call toImage.

@christian-muertz
Copy link
Contributor

You could create a separate PipelineOwner and BuildOwner to manage a separate element/render tree, then inflate the tree, and finally flush the pipeline and then call toImage.

I'll try to come up with a function like createImageFromWidget that does exactly that.

@AliYar-Khan
Copy link

Any luck guys ?? I also want content of a container to be saved as image. found an article here if anyone of you can understand whats happening ?

@christian-muertz
Copy link
Contributor

I'll try to come up with a function like createImageFromWidget that does exactly that.

Here we go:

/// Creates an image from the given widget by first spinning up a element and render tree,
/// then waiting for the given [wait] amount of time and then creating an image via a [RepaintBoundary].
/// 
/// The final image will be of size [imageSize] and the the widget will be layout, ... with the given [logicalSize].
Future<Uint8List> createImageFromWidget(Widget widget, {Duration wait, Size logicalSize, Size imageSize}) async {
  final RenderRepaintBoundary repaintBoundary = RenderRepaintBoundary();

  logicalSize ??= ui.window.physicalSize / ui.window.devicePixelRatio;
  imageSize ??= ui.window.physicalSize;

  assert(logicalSize.aspectRatio == imageSize.aspectRatio);

  final RenderView renderView = RenderView(
    window: null,
    child: RenderPositionedBox(alignment: Alignment.center, child: repaintBoundary),
    configuration: ViewConfiguration(
      size: logicalSize,
      devicePixelRatio: 1.0,
    ),
  );

  final PipelineOwner pipelineOwner = PipelineOwner();
  final BuildOwner buildOwner = BuildOwner();

  pipelineOwner.rootNode = renderView;
  renderView.prepareInitialFrame();

  final RenderObjectToWidgetElement<RenderBox> rootElement = RenderObjectToWidgetAdapter<RenderBox>(
    container: repaintBoundary,
    child: widget,
  ).attachToRenderTree(buildOwner);

  buildOwner.buildScope(rootElement);

  if (wait != null) {
    await Future.delayed(wait);
  }

  buildOwner.buildScope(rootElement);
  buildOwner.finalizeTree();

  pipelineOwner.flushLayout();
  pipelineOwner.flushCompositingBits();
  pipelineOwner.flushPaint();

  final ui.Image image = await repaintBoundary.toImage(pixelRatio: imageSize.width / logicalSize.width);
  final ByteData byteData = await image.toByteData(format: ui.ImageByteFormat.png);

  return byteData.buffer.asUint8List();
}

@abdulllrashid
Copy link

@christian-muertz Thanks, It worked fine.
Added textdirection to Text,Column Widgets

@KieranLafferty
Copy link

KieranLafferty commented Apr 28, 2020

Getting the code @christian-muertz but I'm getting the following error at the line

  final ui.Image image = await repaintBoundary.toImage(pixelRatio: imageSize.width / logicalSize.width);

Error:

Exception has occurred.
_AssertionError ('package:flutter/src/rendering/proxy_box.dart': Failed assertion: line 2961 pos 12: '!debugNeedsPaint': is not true.)

@christian-muertz
Copy link
Contributor

@KieranLafferty please share your flutter doctor output as well as a small sample app.

@shameelsadaka
Copy link

@christian-muertz Wow.. It is working..
If the widget contains directional widgets like text or row, It will show the error No Directionality widget found
So wrapped the widget with a Directionality widget inside the function.

   final RenderObjectToWidgetElement<RenderBox> rootElement = RenderObjectToWidgetAdapter<RenderBox>(
      container: repaintBoundary,
      child: Directionality(
          textDirection: TextDirection.ltr,
          child:widget,
      )
    ).attachToRenderTree(buildOwner);

@abdulllrashid
Copy link

abdulllrashid commented May 3, 2020

I have a padding widget with a row and two image.asset inside it.
when it's rendered for the first time whole Widget thing except image.asset does'nt load.
the second time when our function is called the widget get rendred also.
Any tweeks in function to fix this @christian-muertz

Padding( padding: const EdgeInsets.fromLTRB(10, 10, 10, 20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, textDirection: TextDirection.ltr, children: <Widget>[ Image.asset( "assets/store_download_badges/google-play-badge.png", height: 30, ), Image.asset( "assets/logo.png", height: 30, ), ], ),

@christian-muertz
Copy link
Contributor

christian-muertz commented May 3, 2020

@abdulllrashid What do you mean by the whole Widget thing except the image does not load? I don't see anything that should be shown except the two images in a row with some padding. Could you add the screenshots of the first call and the second?

@abdulllrashid
Copy link

@christian-muertz I'm expecting two images to be rendered but unfortunately, it doesn't show on the first render. On second call it does.

the issue got fixed when I added a 1-sec wait in the function, earlier it was null

@pacifio
Copy link

pacifio commented Jun 3, 2020

I can only see a part of image being generated ... which is frustrating ..

@christian-muertz
Copy link
Contributor

I can only see a part of image being generated ... which is frustrating ..

More detailed information would be very helpful

@Virczek
Copy link

Virczek commented Jun 27, 2020

Hello, I've got a problem with images too, too. I'll try to create a swipe card but is laggy. I try to change the widget to raster object (image widget) when it is dragging to eliminate lags. But my images disappear (background of card and button). Or is loading after start pan not earlier (at the build widget moment). Do you have maybe any solution for this?
Here's code:
https://pastebin.pl/view/94fdb146
Animation how it looks:
https://giphy.com/gifs/ftBVdqhvK1jbf523tm

@cms103
Copy link

cms103 commented Aug 24, 2020

I've been partially successful with the createImageFromWidget function above (thank you!), but now I've hit a problem where the Focus (onKey: ...) method is no longer called!

Context: This is a desktop application running on Mac OS. The keyboard interactions call the Focus onKey method in my displayed view fine, but as soon as I pass even the simplest widget to createImageFromWidget() the onKey method is no longer called on keyboard input. TextFields are working, Focus is working (onFocusChange works), but not onKey.

I've tried re-building the screen after the call with no luck - only a restart of the application bring back onKey. Anyone any ideas?

@cms103
Copy link

cms103 commented Aug 24, 2020

OK, I found the problem with the keyboard. createImageFromWidget creates a BuildOwner. BuildOwner creates a new FocusManager, which in it's constructor calls: RawKeyboard.instance.keyEventHandler = _handleRawKeyEvent; (

RawKeyboard.instance.keyEventHandler = _handleRawKeyEvent;
)

To work around this you can save the original keyEventHandler (var oldKeyboardHandler = RawKeyboard.instance.keyEventHandler;) just before BuildOwner() is called and then restore it before returning with RawKeyboard.instance.keyEventHandler = oldKeyboardHandler;

It feels like a hack, but it fixes the issue!

@1ka
Copy link

1ka commented Oct 2, 2020

Thanks for this @christian-muertz

What is the correct way of disposing of the widget that is passed into createImageFromWidget? It appears that this whole build pipeline continues to exist after function has finished so I suspect we need to tear it down manually somehow.

@cntseesharp
Copy link

I'll try to come up with a function like createImageFromWidget that does exactly that.

Here we go:

/// Creates an image from the given widget by first spinning up a element and render tree,
/// then waiting for the given [wait] amount of time and then creating an image via a [RepaintBoundary].
/// 
/// The final image will be of size [imageSize] and the the widget will be layout, ... with the given [logicalSize].
Future<Uint8List> createImageFromWidget(Widget widget, {Duration wait, Size logicalSize, Size imageSize}) async {
  final RenderRepaintBoundary repaintBoundary = RenderRepaintBoundary();

  logicalSize ??= ui.window.physicalSize / ui.window.devicePixelRatio;
  imageSize ??= ui.window.physicalSize;

  assert(logicalSize.aspectRatio == imageSize.aspectRatio);

  final RenderView renderView = RenderView(
    window: null,
    child: RenderPositionedBox(alignment: Alignment.center, child: repaintBoundary),
    configuration: ViewConfiguration(
      size: logicalSize,
      devicePixelRatio: 1.0,
    ),
  );

  final PipelineOwner pipelineOwner = PipelineOwner();
  final BuildOwner buildOwner = BuildOwner();

  pipelineOwner.rootNode = renderView;
  renderView.prepareInitialFrame();

  final RenderObjectToWidgetElement<RenderBox> rootElement = RenderObjectToWidgetAdapter<RenderBox>(
    container: repaintBoundary,
    child: widget,
  ).attachToRenderTree(buildOwner);

  buildOwner.buildScope(rootElement);

  if (wait != null) {
    await Future.delayed(wait);
  }

  buildOwner.buildScope(rootElement);
  buildOwner.finalizeTree();

  pipelineOwner.flushLayout();
  pipelineOwner.flushCompositingBits();
  pipelineOwner.flushPaint();

  final ui.Image image = await repaintBoundary.toImage(pixelRatio: imageSize.width / logicalSize.width);
  final ByteData byteData = await image.toByteData(format: ui.ImageByteFormat.png);

  return byteData.buffer.asUint8List();
}

This code works like a charm until i try to pass a SvgPicture as a widget. Those Svgs have been a pain in the ass for a long time, maybe you could advice something?

@heshengyahuai
Copy link

If the child widget is SingleChildScrollView, it won't work

@yasinarik
Copy link

I am using interactive view widget and it only exports the part which is visible on viewport.

In other words, if a widget or part of a widget is out of viewport (or not rendered yet) it won't print out the image as a whole big picture.

Thumbs up if you have the same problem (for ex: list view, page view, stack)

@yasinarik
Copy link

There is a package called "Screenshot".

It can truly get the screenshot of a widget even if the some parts of the widget is out of the screen (or not rendered yet).

I basically wrapped my InteractiveView widget with it. The screen width is 1440px but the InteractiveView has a child with width 10000px. So it can handle this.

Easy to set up.

I used it combining with PDF and Printing packages to export as image and pdf.

@tehsunnliu
Copy link

tehsunnliu commented Jan 25, 2021

I'll try to come up with a function like createImageFromWidget that does exactly that.

Here we go:

/// Creates an image from the given widget by first spinning up a element and render tree,
/// then waiting for the given [wait] amount of time and then creating an image via a [RepaintBoundary].
/// 
/// The final image will be of size [imageSize] and the the widget will be layout, ... with the given [logicalSize].
Future<Uint8List> createImageFromWidget(Widget widget, {Duration wait, Size logicalSize, Size imageSize}) async {
  final RenderRepaintBoundary repaintBoundary = RenderRepaintBoundary();

  logicalSize ??= ui.window.physicalSize / ui.window.devicePixelRatio;
  imageSize ??= ui.window.physicalSize;

  assert(logicalSize.aspectRatio == imageSize.aspectRatio);

  final RenderView renderView = RenderView(
    window: null,
    child: RenderPositionedBox(alignment: Alignment.center, child: repaintBoundary),
    configuration: ViewConfiguration(
      size: logicalSize,
      devicePixelRatio: 1.0,
    ),
  );

  final PipelineOwner pipelineOwner = PipelineOwner();
  final BuildOwner buildOwner = BuildOwner();

  pipelineOwner.rootNode = renderView;
  renderView.prepareInitialFrame();

  final RenderObjectToWidgetElement<RenderBox> rootElement = RenderObjectToWidgetAdapter<RenderBox>(
    container: repaintBoundary,
    child: widget,
  ).attachToRenderTree(buildOwner);

  buildOwner.buildScope(rootElement);

  if (wait != null) {
    await Future.delayed(wait);
  }

  buildOwner.buildScope(rootElement);
  buildOwner.finalizeTree();

  pipelineOwner.flushLayout();
  pipelineOwner.flushCompositingBits();
  pipelineOwner.flushPaint();

  final ui.Image image = await repaintBoundary.toImage(pixelRatio: imageSize.width / logicalSize.width);
  final ByteData byteData = await image.toByteData(format: ui.ImageByteFormat.png);

  return byteData.buffer.asUint8List();
}

Hi, I tried this, it works and captures screenshots of widgets that are not rendered on the screen. However, when I add QrImage() widget from qr_flutter: ^3.2.0, the function is not able to capture the QrCode. The QrImage displays the Qr Code in the widget as expected.

There is a package called "Screenshot".

It can truly get the screenshot of a widget even if the some parts of the widget is out of the screen (or not rendered yet).

I basically wrapped my InteractiveView widget with it. The screen width is 1440px but the InteractiveView has a child with width 10000px. So it can handle this.

Easy to set up.

I used it combining with PDF and Printing packages to export as image and pdf.

I tried using Screenshot package too, but the screenshot captures only the widgets which are rendered on the screen and throws a null exception for those which are out of the screen, even If I use ListView() (not ListView.builder()). Using this package the screenshot does have the QR Code in it.

In my case I need each ListView Item to be an individual screenshot and not the ListView as a whole.

@fernando-s97
Copy link

@LaysDragon So removing the final oldKeyboardHandler = RawKeyboard.instance.keyEventHandler; and the RawKeyboard.instance.keyEventHandler = oldKeyboardHandler; makes everything work fine?

@LaysDragon
Copy link

LaysDragon commented Oct 26, 2021

@LaysDragon So removing the final oldKeyboardHandler = RawKeyboard.instance.keyEventHandler; and the RawKeyboard.instance.keyEventHandler = oldKeyboardHandler; makes everything work fine?

Till now didn't encounter any other problem. It seems the bug is caused by a old fix code not work on a new version flutter. It even trigger more warning message about shouldn't access this internal field that only reserved for compatibility reason.
I still have no idea how its made so many errors .But yeah as long as I removed two line related to RawKeyboard.instance.keyEventHandler the keyboard function back to normal.

@Chetan-3110
Copy link

  1. I have tried using delay of 1 seconds but It is still not loading images on first run.
  2. After using it multiple times with different images, It is giving the older image in the captured image even after clearing the ImagesCache and deleting the file from local storage after each run.

@fernando-s97
Copy link

About item 1, after some time using my implementation, the100ms delay I use doesn't work always, but I still have no clue on why's that. Also, I didn't figure out any workarounds yet.

@Wizzel1
Copy link

Wizzel1 commented Nov 23, 2021

I am trying to reference the widget by Key like so :

    final currentWidgetSize = key.currentContext!.size!;
    final keyWidget = key.currentWidget!;
    final pngBytes = await createImageFromWidget(
      keyWidget,
      logicalSize: Size(currentWidgetSize.width, currentWidgetSize.height),
      imageSize: Size(currentWidgetSize.width, currentWidgetSize.height),
    );

but

  final rootElement = RenderObjectToWidgetAdapter<RenderBox>(
   container: repaintBoundary,
   child: widget,
 ).attachToRenderTree(buildOwner);

throws an error :

flutter: 'package:flutter/src/widgets/framework.dart': Failed assertion: line 1921 pos 12:
flutter: '_elements.contains(element)': is not true.

It seems like the referenced widget gets taken out of the original widget tree.
How can I fix this?

@bobatsar
Copy link

I am using the screenshot package and adapted it a bit (it is based on these code examples here). I would like to capture only the child but the rendered image is either the ScreenSize or the passed ImageSize.
Is there any way to calculate the ImageSize from the rendered size of the widget so the image size matches the widget to be captured?

I tried setting another RepaintBoundary and to get it I used:

RenderRepaintBoundary boundary = _globalKey.currentContext.findRenderObject();

but the currentContext is always null.

Any hints how to handle my problem?

@DhananjayB
Copy link

On flutter web it throwing below exception.

ServicesBinding.instance!.keyEventManager.keyMessageHandler == null
is not true

@Haseebarshad849
Copy link

it just making white image

did you find the solution??

@Haseebarshad849
Copy link

Haseebarshad849 commented Mar 25, 2022

I'll try to come up with a function like createImageFromWidget that does exactly that.

I am capturing a widget of QR with Logo in the center but when i capture the widget either with plugin screenshot or by using Future createImageFromWidget(Widget widget, {Duration wait, Size logicalSize, Size imageSize}) i am getting white screen..
But when i remove the network image or logo image in the center i am getting correct screenshot of the widget without logo
My code is :

final qrView = MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: Center(
          child: Padding(
            padding: const EdgeInsets.all(200),
            child: QrImage(
      data: 'www.google.com',
      gapless: false,
      version: QrVersions.auto,
       // i have tried all imageproviders e.g [AssetImage], [CachedNetworkImageProvider], [NetworkImage]
      embeddedImage: NetworkImage(
        'https://images.crazygames.com/colorpixelartclassic.png?auto=format,compress&q=75&cs=strip&ch=DPR&w=1200&h=630&fit=crop,  // any network image
      ),
      embeddedImageEmitsError: true,
      errorStateBuilder: (context, error) => Center(
        child: Text(
          error.toString(),
        ),
      ),
    ),
          ),
        ),
      ),
    );
    final capturedImage = await screenshotController.captureFromWidget(
      qrView,
      delay: const Duration(seconds: 1),
    );

@christian-muertz

@shuaihuzhou
Copy link

Ability to capture the widget which is outside the viewport. Imgkl/davinci#1

I am using the screenshot package and adapted it a bit (it is based on these code examples here). I would like to capture only the child but the rendered image is either the ScreenSize or the passed ImageSize. Is there any way to calculate the ImageSize from the rendered size of the widget so the image size matches the widget to be captured?

I tried setting another RepaintBoundary and to get it I used:

RenderRepaintBoundary boundary = _globalKey.currentContext.findRenderObject();

but the currentContext is always null.

Any hints how to handle my problem?

have you found a solution of get the image size

@goderbauer goderbauer added the P3 Issues that are less important to the Flutter project label Jan 17, 2023
@Bahrom2101
Copy link

Bahrom2101 commented Jan 27, 2023

According to now January 27, 2023 working code is below

static Future<Uint8List?> createImageFromWidget(Widget widget, {Duration? wait, Size? logicalSize, Size? imageSize}) async {
    final repaintBoundary = RenderRepaintBoundary();

    logicalSize ??= ui.window.physicalSize / ui.window.devicePixelRatio;
    imageSize ??= ui.window.physicalSize;

    assert(logicalSize.aspectRatio == imageSize.aspectRatio, 'logicalSize and imageSize must not be the same');

    final renderView = RenderView(
      window: ui.window,
      child: RenderPositionedBox(alignment: Alignment.center, child: repaintBoundary),
      configuration: ViewConfiguration(
        size: logicalSize,
        devicePixelRatio: 1,
      ),
    );

    final pipelineOwner = PipelineOwner();
    final buildOwner = BuildOwner(focusManager: FocusManager());

    pipelineOwner.rootNode = renderView;
    renderView.prepareInitialFrame();

    final rootElement = RenderObjectToWidgetAdapter<RenderBox>(
        container: repaintBoundary,
        child: Directionality(
          textDirection: TextDirection.ltr,
          child:widget,
        )
    ).attachToRenderTree(buildOwner);

    // final rootElement = RenderObjectToWidgetAdapter<RenderBox>(
    //   container: repaintBoundary,
    //   child: widget,
    // ).attachToRenderTree(buildOwner);

    buildOwner.buildScope(rootElement);

    if (wait != null) {
      await Future.delayed(wait);
    }

    buildOwner..buildScope(rootElement)
    ..finalizeTree();

    pipelineOwner..flushLayout()
    ..flushCompositingBits()
    ..flushPaint();

    final image = await repaintBoundary.toImage(pixelRatio: imageSize.width / logicalSize.width);
    final byteData = await image.toByteData(format: ui.ImageByteFormat.png);

    return byteData?.buffer.asUint8List();
  }

@FluffyBunniesTasteTheBest

This appears to be one of the most underrated features requests there are. Beside @tapizquent usecase, there's another very common one:

Sooner or later, every app needs some screenshots for its store listings. Creating those is quite labor intense:

  1. Take a screenshot from the app at a certain resolution.
  2. Put the screenshot onto an image of a matching Android/iOS device.
  3. Scale the image down to create some space to be able to...
  4. place a sales pitch onto it.
  5. And finally some optional, but usually necessary steps to make it look spectacular.

Now imagine you need five screenshots, in two languages, shown on six devices: 60 screenshots to create - that's quite
a lot of work in Gimp/Photoshop. A way more efficient approach is to let the app generate the store listing screenshots.

The snippet above is a start, and distinguishing between logical and image size makes it incredible versatile. @christian-muertz thank you very much for providing it!

Unfortunately it has its limitations (on some apps the first screenshot fails - subsequent screenshots succeed, assertions in debug mode, context loses translations/providers, difficulties with global keys, ...) that require quite some workarounds before it delivers the expected results...

Hence my vote to increase the priority of this feature request. What do you think?

@NaderMnsr
Copy link

An updated version, that works on Flutter 3.10.x, and which removes the dependance on ui.window, since soon it will be deprecated

static Future<Uint8List?> createImageFromWidget(
      BuildContext context, Widget widget,
      {Duration? wait, Size? logicalSize, Size? imageSize}) async {
    final repaintBoundary = RenderRepaintBoundary();

    logicalSize ??=
        View.of(context).physicalSize / View.of(context).devicePixelRatio;
    imageSize ??= View.of(context).physicalSize;

    assert(logicalSize.aspectRatio == imageSize.aspectRatio,
        'logicalSize and imageSize must not be the same');

    final renderView = RenderView(
        child: RenderPositionedBox(
            alignment: Alignment.center, child: repaintBoundary),
        configuration: ViewConfiguration(
          size: logicalSize,
          devicePixelRatio: 1,
        ),
        view: View.of(context) //PlatformDispatcher.instance.views.first,
        );

    final pipelineOwner = PipelineOwner();
    final buildOwner = BuildOwner(focusManager: FocusManager());

    pipelineOwner.rootNode = renderView;
    renderView.prepareInitialFrame();

    final rootElement = RenderObjectToWidgetAdapter<RenderBox>(
        container: repaintBoundary,
        child: Directionality(
          textDirection: TextDirection.ltr,
          child: widget,
        )).attachToRenderTree(buildOwner);

    buildOwner.buildScope(rootElement);

    if (wait != null) {
      await Future.delayed(wait);
    }

    buildOwner
      ..buildScope(rootElement)
      ..finalizeTree();

    pipelineOwner
      ..flushLayout()
      ..flushCompositingBits()
      ..flushPaint();

    final image = await repaintBoundary.toImage(
        pixelRatio: imageSize.width / logicalSize.width);
    final byteData = await image.toByteData(format: ImageByteFormat.png);

    return byteData?.buffer.asUint8List();
  }

@bilaldbank
Copy link

bilaldbank commented May 23, 2023

Not working for me on Flutter 3.10 (tested on Android Emulator). My custom widget is a SingleChildScrollView

@zhuhean
Copy link

zhuhean commented May 26, 2023

Also, please note that if you intend to invoke this method many times, make sure that the PipelineOwner object was created only once. Otherwise, your app will crash.

@lukehutch
Copy link
Contributor

Also, please note that if you intend to invoke this method many times, make sure that the PipelineOwner object was created only once. Otherwise, your app will crash.

@zhuhean Are you saying that pipelineOwner should be declared as a static or global variable?

Why does the app crash if you declare a new PipelineOwner every time you run this method?

@lukehutch
Copy link
Contributor

Why does the RenderView have to be tied to the BuildContext (Element) of the caller, if the target is offscreen rendering?

@lukehutch
Copy link
Contributor

Just wanted to drop this code here in case anyone needs to render to a Canvas off-screen, using graphics primitives rather than widgets:

var recorder = PictureRecorder();
var canvas = Canvas(recorder);
canvas.drawImage(Offset.zero, myImage);
canvas.transform( ... some transformation ...)
var picture = recorder.endRecording();
picture.toImage(100, 100).then((ui.Image) {
  var png = ui.Image.toByteData(format: ... )
}

Thanks to @jonahwilliams for the example code.

@flutter-triage-bot flutter-triage-bot bot added team-framework Owned by Framework team triaged-framework Triaged by Framework team labels Jul 8, 2023
@hongquang198
Copy link

hongquang198 commented Sep 11, 2023

An updated version, that works on Flutter 3.10.x, and which removes the dependance on ui.window, since soon it will be deprecated

static Future<Uint8List?> createImageFromWidget(
      BuildContext context, Widget widget,
      {Duration? wait, Size? logicalSize, Size? imageSize}) async {
    final repaintBoundary = RenderRepaintBoundary();

    logicalSize ??=
        View.of(context).physicalSize / View.of(context).devicePixelRatio;
    imageSize ??= View.of(context).physicalSize;

    assert(logicalSize.aspectRatio == imageSize.aspectRatio,
        'logicalSize and imageSize must not be the same');

    final renderView = RenderView(
        child: RenderPositionedBox(
            alignment: Alignment.center, child: repaintBoundary),
        configuration: ViewConfiguration(
          size: logicalSize,
          devicePixelRatio: 1,
        ),
        view: View.of(context) //PlatformDispatcher.instance.views.first,
        );

    final pipelineOwner = PipelineOwner();
    final buildOwner = BuildOwner(focusManager: FocusManager());

    pipelineOwner.rootNode = renderView;
    renderView.prepareInitialFrame();

    final rootElement = RenderObjectToWidgetAdapter<RenderBox>(
        container: repaintBoundary,
        child: Directionality(
          textDirection: TextDirection.ltr,
          child: widget,
        )).attachToRenderTree(buildOwner);

    buildOwner.buildScope(rootElement);

    if (wait != null) {
      await Future.delayed(wait);
    }

    buildOwner
      ..buildScope(rootElement)
      ..finalizeTree();

    pipelineOwner
      ..flushLayout()
      ..flushCompositingBits()
      ..flushPaint();

    final image = await repaintBoundary.toImage(
        pixelRatio: imageSize.width / logicalSize.width);
    final byteData = await image.toByteData(format: ImageByteFormat.png);

    return byteData?.buffer.asUint8List();
  }

To take screenshots with 'ListView' widget inside, just simply wrap a MediaQuery widget around the Directionality widget. Tested and it works fine on Flutter 3.13.2 (Since ListView behaves differently on Flutter 3.13, it does a lookup for View.of(context).devicePixelRatio in Scrollable widget (One of ListView dependency widget)

  @override
  void didChangeDependencies() {
    _mediaQueryGestureSettings = MediaQuery.maybeGestureSettingsOf(context);
    _devicePixelRatio = MediaQuery.maybeDevicePixelRatioOf(context) ?? View.of(context).devicePixelRatio;
    _updatePosition();
    super.didChangeDependencies();
  }

if we wrap MediaQuery it does not need to lookup for View.of(context) widget anymore hence make it work fine

For example:

 final RenderObjectToWidgetElement<RenderBox> rootElement =
      RenderObjectToWidgetAdapter<RenderBox>(
    container: repaintBoundary,
    child: MediaQuery(
      data: MediaQuery.of(
              getIt<INavigationService>().navigatorKey.currentContext!)
          .copyWith(),
      child: Directionality(textDirection: ui.TextDirection.ltr, child: widget),
    ),
  ).attachToRenderTree(buildOwner);

@saadzarif
Copy link

On flutter web it throwing below exception.

ServicesBinding.instance!.keyEventManager.keyMessageHandler == null is not true

did you find any solution?

@bambinoua
Copy link
Contributor

bambinoua commented Dec 5, 2023

Hi, @christian-muertz. I am just curious how do you create this code or from where did you take it? :)

Here we go:

/// Creates an image from the given widget by first spinning up a element and render tree,
/// then waiting for the given [wait] amount of time and then creating an image via a [RepaintBoundary].
/// 
/// The final image will be of size [imageSize] and the the widget will be layout, ... with the given [logicalSize].
Future<Uint8List> createImageFromWidget(Widget widget, {Duration wait, Size logicalSize, Size imageSize}) async {

@rivella50
Copy link

rivella50 commented Jan 1, 2024

Using the method from @christian-muertz with a StatefulWidget as widget parameter there is a new instance created which loses the current state, i.e. the generated image always reflects the initial state.
Is there a possibility where the current state of the passed widget is kept for the image creation?

@Bahrom2101
Copy link

Guys, how can we increase image quality in this?

@JeromeGsq
Copy link

An updated version, that works on Flutter 3.10.x, and which removes the dependance on ui.window, since soon it will be deprecated

static Future<Uint8List?> createImageFromWidget(
      BuildContext context, Widget widget,
      {Duration? wait, Size? logicalSize, Size? imageSize}) async {
    final repaintBoundary = RenderRepaintBoundary();

    logicalSize ??=
        View.of(context).physicalSize / View.of(context).devicePixelRatio;
    imageSize ??= View.of(context).physicalSize;

    assert(logicalSize.aspectRatio == imageSize.aspectRatio,
        'logicalSize and imageSize must not be the same');

    final renderView = RenderView(
        child: RenderPositionedBox(
            alignment: Alignment.center, child: repaintBoundary),
        configuration: ViewConfiguration(
          size: logicalSize,
          devicePixelRatio: 1,
        ),
        view: View.of(context) //PlatformDispatcher.instance.views.first,
        );

    final pipelineOwner = PipelineOwner();
    final buildOwner = BuildOwner(focusManager: FocusManager());

    pipelineOwner.rootNode = renderView;
    renderView.prepareInitialFrame();

    final rootElement = RenderObjectToWidgetAdapter<RenderBox>(
        container: repaintBoundary,
        child: Directionality(
          textDirection: TextDirection.ltr,
          child: widget,
        )).attachToRenderTree(buildOwner);

    buildOwner.buildScope(rootElement);

    if (wait != null) {
      await Future.delayed(wait);
    }

    buildOwner
      ..buildScope(rootElement)
      ..finalizeTree();

    pipelineOwner
      ..flushLayout()
      ..flushCompositingBits()
      ..flushPaint();

    final image = await repaintBoundary.toImage(
        pixelRatio: imageSize.width / logicalSize.width);
    final byteData = await image.toByteData(format: ImageByteFormat.png);

    return byteData?.buffer.asUint8List();
  }

To take screenshots with 'ListView' widget inside, just simply wrap a MediaQuery widget around the Directionality widget. Tested and it works fine on Flutter 3.13.2 (Since ListView behaves differently on Flutter 3.13, it does a lookup for View.of(context).devicePixelRatio in Scrollable widget (One of ListView dependency widget)

  @override
  void didChangeDependencies() {
    _mediaQueryGestureSettings = MediaQuery.maybeGestureSettingsOf(context);
    _devicePixelRatio = MediaQuery.maybeDevicePixelRatioOf(context) ?? View.of(context).devicePixelRatio;
    _updatePosition();
    super.didChangeDependencies();
  }

if we wrap MediaQuery it does not need to lookup for View.of(context) widget anymore hence make it work fine

For example:

 final RenderObjectToWidgetElement<RenderBox> rootElement =
      RenderObjectToWidgetAdapter<RenderBox>(
    container: repaintBoundary,
    child: MediaQuery(
      data: MediaQuery.of(
              getIt<INavigationService>().navigatorKey.currentContext!)
          .copyWith(),
      child: Directionality(textDirection: ui.TextDirection.ltr, child: widget),
    ),
  ).attachToRenderTree(buildOwner);

Thank you very much for your help!
Here is my example with GoRouter to get a key with a context:

import 'dart:typed_data';
import 'dart:ui';

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:jpeg_encode/jpeg_encode.dart';

Future<Uint8List?> capture(
  Widget child, {
  required Size size,
}) async {
  final logicalSize = size;
  final imageSize = size;

  final repaintBoundary = RenderRepaintBoundary();

  final renderView = RenderView(
    child: RenderPositionedBox(alignment: Alignment.center, child: repaintBoundary),
    configuration: ViewConfiguration(
      size: logicalSize,
      devicePixelRatio: 1,
    ),
    view: PlatformDispatcher.instance.views.first,
  );

  final pipelineOwner = PipelineOwner();
  final buildOwner = BuildOwner(focusManager: FocusManager());

  pipelineOwner.rootNode = renderView;
  renderView.prepareInitialFrame();

  final rootElement = RenderObjectToWidgetAdapter<RenderBox>(
    container: repaintBoundary,
    child: Directionality(
      textDirection: TextDirection.ltr,
      child: MediaQuery(
        data: MediaQuery.of(rootNavigatorKey.currentContext!).copyWith(),
        child: Container(
          color: Colors.white,
          width: logicalSize.width,
          height: logicalSize.height,
          child: child,
        ),
      ),
    ),
  ).attachToRenderTree(
    buildOwner,
  );

  buildOwner.buildScope(rootElement);

  buildOwner
    ..buildScope(rootElement)
    ..finalizeTree();

  pipelineOwner
    ..flushLayout()
    ..flushCompositingBits()
    ..flushPaint();

  final image = await repaintBoundary.toImage(pixelRatio: imageSize.width / logicalSize.width);
  final byteData = await image.toByteData(format: ImageByteFormat.rawRgba);
  print('imageSize.height : ${imageSize.height}');
  print('image.height : ${image.height}');
  return JpegEncoder().compress(byteData!.buffer.asUint8List(), image.width, image.height, 100);
}

And with the GoRouter key available:

import 'package:go_router/go_router.dart';

final rootNavigatorKey = GlobalKey<NavigatorState>();

// GoRouter configuration
final router = GoRouter(
  navigatorKey: rootNavigatorKey,
  initialLocation: '/',
  routes: <RouteBase>[
    StatefulShellRoute.indexedStack(
      builder: (context, state, navigationShell) {
        return HomeView(navigationShell: navigationShell);
      },
 // ...

Then use it like this:

final image = await capture(widget, size: const Size(2048, 512));

@JeromeGsq
Copy link

Guys, how can we increase image quality in this?

Hi @Bahrom2101, you can use Provider/Riverpod or something like this to create a separated class that hold your changes.
Don't forget to provide your provider when you build your widget:

class YourNotifierProvider extends ChangeNotifier {
  double _itemWidth = 1;
  double get itemWidth => _itemWidth;
  set itemWidth(double value) {
    _itemWidth = value;
    notifyListeners();
  }
}

/// Your stateful widget
class YourWidget extends StatefulWidget {
  const YourWidget({super.key});

  @override
  State<YourWidget> createState() => _YourWidgetState();
}

class _YourWidgetState extends State<YourWidget> {
  @override
  Widget build(BuildContext context) {
    final notifier = context.watch<YourNotifierProvider>();
    final itemWidth = notifier.itemWidth;

    return IconButton(
      onPressed: () {
        context.read<YourNotifierProvider>().itemWidth = itemWidth + 1;
      },
      icon: Text(notifier.itemWidth.toString()),
    );
  }
}

/// Your async capture method
Future<Uint8List> captureMyWidget() async {
  final widget = MultiProvider(
    providers: [
      ChangeNotifierProvider<YourNotifierProvider>.value(value: yourNotifierProviderInstance),
    ],
    child: YourWidget(),
  );

  // Capture the widget
  return capture(widget, size: const Size(2048, 512));
}

When you press the IconButton, you should increment the value of itemWidth, then your provider should hold the changes and you can now screenshot it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
a: images Loading, displaying, rendering images c: proposal A detailed proposal for a change to Flutter framework flutter/packages/flutter repository. See also f: labels. P3 Issues that are less important to the Flutter project team-framework Owned by Framework team triaged-framework Triaged by Framework team
Projects
None yet
Development

No branches or pull requests