joseph-grabinger / flutter_to_pdf

Create PDFs but work with normal Flutter Widgets.
MIT License
17 stars 15 forks source link

Support "unsupported" widgets e.g. Painter, Charts, ... #62

Closed Vera-Spoettl closed 3 months ago

Vera-Spoettl commented 5 months ago

Great package! Unfortunately, I have to use quite some "unsupported" widgets like charts from fl_chart, diagrams painted via custom_painter, ... . I'm wondering, if it would be possible (and with normal effort realizable) to support these widgets via screenshot and image. Have you ever spend a thought on this or do you have a better idea? Thanks, Vera

Vera-Spoettl commented 5 months ago

I gave it a try today.

I added a widget for "wrapping" unsupported flutter widgets, that I want to capture and printed:

import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';

class CaptureWrapper extends StatelessWidget {
  final Widget child;

  const CaptureWrapper({
    super.key,
    required this.child,
  });

  @override
  Widget build(BuildContext context) {
    return RepaintBoundary(
      child: child,
    );
  }
}

I expanded export_instance.dart with this case:

      case CaptureWrapper:
        if (context != null) {
          if (widget.key == null) {
            throw Exception('Capture must have a key to be exported');
          }
          Element? contextElement =
              findElement(context, (CaptureWrapper e) => e.key == widget.key);

          assert(contextElement != null);
          RenderRepaintBoundary? boundary;
          RenderObject? renderObject = contextElement!.renderObject;

          if (renderObject is RenderRepaintBoundary) {
            boundary = renderObject;
          } else {
            renderObject?.visitChildren((child) {
              if (child is RenderRepaintBoundary) {
                boundary = child;
              }
            });
          }

          assert(boundary != null);
          final ui.Image uiImage = await boundary!.toImage(pixelRatio: 2.0);
          final pngBytes =
              await uiImage.toByteData(format: ImageByteFormat.png);

          return [
            await Image.memory(Uint8List.view(pngBytes!.buffer)).toPdfWidget()
          ];
        }
        return [];

Up to now, it seems to work with fl_chart and CustomPainter widgets.

Unfortunately I'm not experienced enough in flutter to know if this is an elegant, efficient, ... solution. But I'm open to discussions. :-)

image
joseph-grabinger commented 5 months ago

Hey, thanks for your detailed explanation :) So I think supporting fl_charts boils down to supporting flutters CustomPainter. The straight forward way to do this would be to create an extension on CustomPaint to access the paint(Canvas canvas, Size size) method. The problem with this approach is that having access to the paint method itself doesn't help much. One would need access to the actual Canvas methods used (e.g. canvas.drawColor(...), canvas.drawArc(...)), since these could be converted to their pdf equivalent.

But since this is not possible, the only way (I could think of) to support it would be your approach by converting said widgets to images. πŸ‘

Feel free to open a pull request so I can take a closer look and actually run and test your solution. Also since your approach seems very suitable, a PR would be best for further discussion.

Thanks for your involvement! πŸ₯‡

Vera-Spoettl commented 5 months ago

I'll prepare a PR but it'll take me some days (lots of things on my plate atm). πŸ™ˆ

I was wondering, if I could use PictureRecorder to use for CustomPaint stuff. Maybe I can give it a try ...

Vera-Spoettl commented 1 month ago

Hi @joseph-grabinger ,

I have a very strange behaviour of the CaptureWrapper which occurs only in my code but not in your example and I can't find the point that causes the bug:

When I give a GlobalKey to a captured widget, I get the following error during runtime:

════════ Exception caught by widgets library ═══════════════════════════════════
The following assertion was thrown building CaptureWrapper-[<'someFrame'>]:
'package:flutter/src/widgets/framework.dart': Failed assertion: line 2116 pos 12: '_elements.contains(element)': is not true.

Either the assertion indicates an error in the framework itself, or we should provide substantially more information in this error message to help you determine and fix the underlying cause.
In either case, please report this assertion by filing a bug on GitHub:
  https://github.com/flutter/flutter/issues/new?template=2_bug.yml

The relevant error-causing widget was:
    CaptureWrapper-[<'someFrame'>] CaptureWrapper:file:///Users/vsz/Development/projects/stuff/report_generator/lib/main.dart:31:15

followed by

════════ Exception caught by widgets library ═══════════════════════════════════
Duplicate GlobalKey detected in widget tree.
════════════════════════════════════════════════════════════════════════════════

Here is my minimal code example:

import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:flutter_to_pdf/flutter_to_pdf.dart';
import 'package:printing/printing.dart';

void main() {
  runApp(const MainApp());
}

class MainApp extends StatefulWidget {
  const MainApp({super.key});

  @override
  State<MainApp> createState() => _MainAppState();
}

class _MainAppState extends State<MainApp> {
  final ExportDelegate exportDelegate = ExportDelegate();
  GlobalKey scatterChartKey = GlobalKey();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: ExportFrame(
          frameId: 'someFrameId',
          exportDelegate: exportDelegate,
          child: Column(
            children: [
              const SizedBox(height: 10),
              CaptureWrapper(
                key: const Key('someFrame'),
                child: SizedBox(
                  width: 400,
                  height: 200,
                  child: ScatterChart(
                    key: scatterChartKey,
                    ScatterChartData(
                      scatterSpots: [
                        ScatterSpot(1, 1),
                        ScatterSpot(1, 2),
                        ScatterSpot(2, 3),
                        ScatterSpot(3, 2),
                        ScatterSpot(4, 5),
                        ScatterSpot(5, 4),
                        ScatterSpot(6, 6),
                      ],
                      titlesData: const FlTitlesData(
                        topTitles: AxisTitles(
                          sideTitles: SideTitles(showTitles: false),
                        ),
                      ),
                    ),
                  ),
                ),
              ),
              ElevatedButton(
                onPressed: () async {
                  final ExportOptions overrideOptions = ExportOptions(
                    textFieldOptions: TextFieldOptions.uniform(
                      interactive: false,
                    ),
                    checkboxOptions: CheckboxOptions.uniform(
                      interactive: false,
                    ),
                  );
                  final pdf = await exportDelegate.exportToPdfDocument(
                    'someFrameId',
                    overrideOptions: overrideOptions,
                  );
                  Printing.layoutPdf(onLayout: (format) => pdf.save());
                },
                child: const Text('Export'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

and my pubspec.yaml

name: report_generator
description: "A new Flutter project."
publish_to: "none"
version: 0.1.0

environment:
  sdk: ">=3.4.4 <4.0.0"

dependencies:
  fl_chart: ^0.68.0
  flutter:
    sdk: flutter
  flutter_to_pdf: ^0.2.1
  printing: ^5.13.1

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^3.0.0

flutter:
  uses-material-design: true

When I replace the chart from your example app with the following code:

return ScatterChart(
      key: GlobalKey(),
      chartRendererKey: rendererKey,
      ScatterChartData(
          scatterSpots: [
            ScatterSpot(1, 1),
            ScatterSpot(1, 2),
            ScatterSpot(2, 3),
            ScatterSpot(3, 2),
            ScatterSpot(4, 5),
            ScatterSpot(5, 4),
            ScatterSpot(6, 6),
          ],
          titlesData: const FlTitlesData(
              topTitles:
                  AxisTitles(sideTitles: SideTitles(showTitles: false)))),
    );

everything works still fine.

Do you have any idea? What's going on here? I think I already spent 2 days now on trying many things to make it work but besides isolating the problem to this minimal example, I didn't make any progress ... :-(

BTW: just tried the same with a Text widget and a GlobalKey and had the same problem, so it doesn't seem to be a problem of ScatterChart.

joseph-grabinger commented 1 month ago

Does the error happen once you export a frame or already before exporting (when rendering the widget on screen) ?

Vera-Spoettl commented 1 month ago

It happens during export.

Gesendet von Outlook fΓΌr iOShttps://aka.ms/o0ukef


Von: Joseph Grabinger @.> Gesendet: Saturday, August 3, 2024 8:28:40 PM An: joseph-grabinger/flutter_to_pdf @.> Cc: Vera SpΓΆttl-Zeisberg @.>; Author @.> Betreff: Re: [joseph-grabinger/flutter_to_pdf] Support "unsupported" widgets e.g. Painter, Charts, ... (Issue #62)

Does the error happen once you export a frame or already before exporting (when rendering the widget on screen) ?

β€” Reply to this email directly, view it on GitHubhttps://github.com/joseph-grabinger/flutter_to_pdf/issues/62#issuecomment-2267095151, or unsubscribehttps://github.com/notifications/unsubscribe-auth/AXW4SU6DOMRLUVXCKVACLXDZPUONRAVCNFSM6AAAAABFTC3WJCVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDENRXGA4TKMJVGE. You are receiving this because you authored the thread.Message ID: @.***>

joseph-grabinger commented 1 month ago

I was able to reproduce the issue! It seems that the issue only occurs if a widget inside of a CaptureWrapper uses a GlobalKey outside of the CaptureWrapper's scope.

This works:

 CaptureWrapper(
    key: const Key('CustomPaint'),
    child: CustomPaint(
      key: GlobalKey(),
      size: const Size(300, 300),
      painter: HousePainter(),
    ),
  ),

But this doesn't work:

final gKey = GlabalKey();

 CaptureWrapper(
    key: const Key('CustomPaint'),
    child: CustomPaint(
      key: gKey,
      size: const Size(300, 300),
      painter: HousePainter(),
    ),
  ),

Not completely sure why this is the case :/

Vera-Spoettl commented 1 month ago

Unfortunately, in my minimal example moving the GlobalKey into a child widget of CaptureWrapper makes no difference. In both cases, I get the error. And it makes no difference if it is a complex widget like the ScatterChart or a quite simple one like placeholder:

image

This seems to be the place where the problem appears:

image

API-Documentation of GlobalKey says: "Widgets that have global keys reparent their subtrees when they are moved from one location in the tree to another location in the tree."

I guess this is somehow the problem. But I don't understand and can't explain it yet.

joseph-grabinger commented 1 month ago

And it makes no difference if it is a complex widget like the ScatterChart or a quite simple one like placeholder

I agree that it doesn't depend on the widget itself but rather on a GlobalKey being present.

However, your example with the Placeholder(key: GlobalKey()) (or also a Text("someText", key: GlobalKey())) doesn't produce any errors for me and the entire widget is exported correctly.

Since there is seems to be some difference in the output.... What flutter version are you using?

For me the examples run just fine on Flutter (Channel stable, 3.22.0, on macOS 14.6 23G80 darwin-x64, locale de-DE)

Vera-Spoettl commented 1 month ago

Very strange behaviour!

That’s my flutter doctor

[βœ“] Flutter (Channel stable, 3.22.3, on macOS 14.5 23F79 darwin-arm64, locale de-DE) [βœ“] Android toolchain - develop for Android devices (Android SDK version 34.0.0) [βœ“] Xcode - develop for iOS and macOS (Xcode 15.4) [βœ“] Chrome - develop for the web [βœ“] Android Studio (version 2023.1) [βœ“] VS Code (version 1.91.1) [βœ“] Connected device (4 available) [βœ“] Network resources

β€’ No issues found!

Not much different. Guess the MacOS version shouldn’t cause the problem. I’ll try to downgrade to Flutter 3.22 later this afternoon and let you know the result.

Von: Joseph Grabinger @.> Datum: Sonntag, 4. August 2024 um 12:52 An: joseph-grabinger/flutter_to_pdf @.> Cc: Vera SpΓΆttl-Zeisberg @.>, Author @.> Betreff: Re: [joseph-grabinger/flutter_to_pdf] Support "unsupported" widgets e.g. Painter, Charts, ... (Issue #62)

And it makes no difference if it is a complex widget like the ScatterChart or a quite simple one like placeholder

I agree that it doesn't depend on the widget itself but rather on a GlobalKey being present.

However, your example with the Placeholder(key: GlobalKey()) (or also a Text("someText", key: GlobalKey())) doesn't produce any errors for me and the entire widget is exported correctly.

Since there is seems to be some difference in the output.... What flutter version are you using?

For me the examples run just fine on Flutter (Channel stable, 3.22.0, on macOS 14.6 23G80 darwin-x64, locale de-DE)

β€” Reply to this email directly, view it on GitHubhttps://github.com/joseph-grabinger/flutter_to_pdf/issues/62#issuecomment-2267498020, or unsubscribehttps://github.com/notifications/unsubscribe-auth/AXW4SUY4OM3MEOXQI5OAZULZPYBVXAVCNFSM6AAAAABFTC3WJCVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDENRXGQ4TQMBSGA. You are receiving this because you authored the thread.Message ID: @.***>

joseph-grabinger commented 1 month ago

Okay I just upgraded to Flutter 3.22.3 and now I have the same issue. So it seems that there has been some kind of change to the GlobalKey between Flutter 3.22.0 and 3.22.3.

Vera-Spoettl commented 1 month ago

What's the best way to handle this? I can't find anything in the Flutter change logs related to GlobalKey. I could downgrade to 3.22 but I guess that just postpones the problem to the next update of Flutter.

joseph-grabinger commented 1 month ago

What's the best way to handle this?

I created a separate Issue (#81) so we could move the discussion to there. I'll create a branch with an example that runs on my machine, since I am now also running Flutter 3.22.3. You could then check-out the branch and we'll go from there.