syncfusion / flutter-widgets

Syncfusion Flutter widgets libraries include high quality UI widgets and file-format packages to help you create rich, high-quality applications for iOS, Android, and web from a single code base.
1.6k stars 782 forks source link

[syncfusion_flutter_pdf] Issue with Exporting Tall Widgets to PDF Using Syncfusion in Flutter #2170

Open azazadev opened 3 days ago

azazadev commented 3 days ago

Bug description

In our Flutter-based dashboarding app, users can view and interact with charts (gauge, bar, line, data grid, etc.) and custom cards that may contain text, trends, or other components. Users can select these charts and export them to a PDF using Syncfusion's PDF functionality.

The issue arises when exporting charts or cards taller than the screen height (e.g., more than 100% of the viewport height). The exported PDF only captures the visible portion of the widget, while the rest is truncated, leading to incomplete exports for tall widgets

Steps to reproduce

  1. Create a widget (e.g., a chart or card) that exceeds the screen height (e.g., 200% of the viewport).
  2. Add the widget to the app with the ability to scroll to view the full content.
  3. Use Syncfusion's PDF functionality to export the widget to a PDF. Check the exported PDF.

Expected Behavior: The entire widget, including all scrollable or tall content, should be fully included in the exported PDF.

Actual Behavior: Only the visible portion of the widget (within the screen bounds) is exported, and the rest is truncated.

Code sample

  Future<void> exportSelectedCharts(BuildContext context) async {
    try {
      PdfDocument document = PdfDocument();

      // Add Title and Body to the PDF
      PdfPage titlePage = document.pages.add();
      titlePage.graphics.drawString(
        'MobileApp - Shared Charts',
        PdfStandardFont(PdfFontFamily.helvetica, 24),
        bounds: Rect.fromLTWH(0, 0, titlePage.getClientSize().width, 50),
      );
      titlePage.graphics.drawString(
        'Explore the charts ...',
        PdfStandardFont(PdfFontFamily.helvetica, 16),
        bounds: Rect.fromLTWH(0, 60, titlePage.getClientSize().width, 30),
      );

      bool isFirstChart = true;

      for (var identifier in _selectedCharts) {
        final card = getFavoriteCards()[identifier];
        if (card != null) {
          GlobalKey boundaryKey = GlobalKey();
          final imageBytes = await _captureWidgetAsImage(
            context,
            boundaryKey,
            Padding(
              padding: const EdgeInsets.only(top: 80, bottom: 20),
              child: MyCustomCard(
                // this is my card that can contain any syncfusion chart and height can be dynamic
                key: UniqueKey(),
              ),
            ),
            pixelRatio: 3.0,
          );

          if (imageBytes != null) {
            PdfPage page;

            if (isFirstChart) {
              // Use the existing title page for the first chart
              page = titlePage;
              isFirstChart = false;
            } else {
              // Add a new page for each subsequent chart
              page = document.pages.add();
            }

            final PdfBitmap bitmap = PdfBitmap(imageBytes);

            // Scale the image to its original pixel size, and center it on the page
            double originalImageWidth = bitmap.width.toDouble();
            double originalImageHeight = bitmap.height.toDouble();

            // Check if the image fits within the page, otherwise scale down to fit
            double pageWidth = page.getClientSize().width;
            double pageHeight = page.getClientSize().height;

            double scale = 1.0;
            if (originalImageWidth > pageWidth || originalImageHeight > pageHeight) {
              double widthScale = pageWidth / originalImageWidth;
              double heightScale = pageHeight / originalImageHeight;
              scale = widthScale < heightScale ? widthScale : heightScale;
            }

            double scaledWidth = originalImageWidth * scale;
            double scaledHeight = originalImageHeight * scale;

            double x = (pageWidth - scaledWidth) / 2;
            double y = (pageHeight - scaledHeight) / 2;

            page.graphics.drawImage(
              bitmap,
              Rect.fromLTWH(x, y, scaledWidth, scaledHeight),
            );
          } else {
            debugPrint('Error capturing image for $identifier: imageBytes is null');
          }
        }
      }

      final directory = await getApplicationDocumentsDirectory();
      final path = '${directory.path}/selected_charts.pdf';
      File file = File(path);
      await file.writeAsBytes(await document.save());
      document.dispose();
      final xFile = XFile(path);

      await Share.shareXFiles(
        [xFile],
        text: 'MobileApp',
      ).whenComplete(() {
        _selectedCharts.clear();
        _selectionNotifiers.clear();
        _isSelectionMode = false;
        notifyListeners();
      });
    } catch (e) {
      debugPrint('Error during export: $e');
    }
  }

  Future<Uint8List?> _captureWidgetAsImage(
    BuildContext context,
    GlobalKey key,
    Widget widget, {
    double pixelRatio = 1.0,
  }) async {
    Completer<Uint8List?> completer = Completer();
    OverlayEntry overlayEntry = OverlayEntry(
      builder: (context) => MaterialApp(
        home: MediaQuery(
          data: MediaQueryData.fromView(WidgetsBinding.instance.window),
          child: Scaffold(
            body: RepaintBoundary(
              key: key,
              child: Container(
                padding: const EdgeInsets.all(16),
                child: widget,
              ),
            ),
          ),
        ),
      ),
    );

    Overlay.of(context).insert(overlayEntry);
    await _ensureWidgetIsPainted();

    final boundary = key.currentContext?.findRenderObject() as RenderRepaintBoundary?;
    if (boundary != null) {
      final image = await boundary.toImage(pixelRatio: pixelRatio);
      final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
      completer.complete(byteData?.buffer.asUint8List());
    } else {
      completer.complete(null);
    }

    overlayEntry.remove();
    return completer.future;
  }

  Future<void> _ensureWidgetIsPainted() async {
    await Future.delayed(const Duration(milliseconds: 500));
  }

Screenshots or Video

Screenshots / Video demonstration [Upload media here]

Stack Traces

Stack Traces ```dart [Add the Stack Traces here] ```

On which target platforms have you observed this bug?

Android, iOS

Flutter Doctor output

• Flutter version 3.24.3 on channel stable at /Users/...
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision 2663184aa7 (10 weeks ago), 2024-09-11 16:27:48 -0500
• Engine revision 36335019a8
• Dart version 3.5.3
• DevTools version 2.37.3
irfanajaffer commented 18 hours ago

Hi ,

We have attempted to reproduce the reported issue on our end, but the data is being exported to PDF as expected. For your reference, we have prepared a sample and attached it. Kindly try running this sample on your end and let us know if you require any further assistance.

Sample: https://www.syncfusion.com/downloads/support/directtrac/general/ze/export_chart_sample891922016

For more details, you can also refer to the following documentation: Export Cartesian Chart to PDF

If the issue persists, we kindly request you to modify the sample and share it with us. This will help us analyze the problem more effectively and provide you with a prompt solution.

Regards,

Irfana J.