How to add text markup annotation in Flutter PDF Viewer using Syncfusion PDF package?
This article explains how to add highlight, underline, and strikethrough annotation in Syncfusion® Flutter PDF Viewer using the Syncfusion® PDF Package. Follow these steps to proceed with,
Step 1: Load the PDF document in which annotations need to be added in the SfPdfViewer.
Uint8List? _documentBytes; @override void initState() { getPdfBytes(); super.initState(); } ///Get the PDF document as bytes. void getPdfBytes() async { _documentBytes = await http.readBytes(Uri.parse( 'https://cdn.syncfusion.com/content/PDFViewer/flutter-succinctly.pdf')); setState(() {}); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Syncfusion Flutter PDF Viewer'), ), body: _documentBytes != null ? SfPdfViewer.memory( _documentBytes!, ) : Container(), ); }
Step 2: Subscribe the onTextSelectionChanged callback. The onTextSelectionChanged callback triggers when the text is selected or deselected in the SfPdfViewer. The PdfTextSelectionChangedDetails holds the globalSelectedRegion, which represents the global bounds information of the selected text region and the selected text represents the selected text value. We have shown a context menu whenever there is a selection in the onTextSelectionChanged callback.
onTextSelectionChanged: (PdfTextSelectionChangedDetails details) { if (details.selectedText == null && _overlayEntry != null) { _checkAndCloseContextMenu(); } else if (details.selectedText != null && _overlayEntry == null) { _showContextMenu(context, details); } },
Step 3: Create a customized context menu with a Highlighting option, an Underline option, and a strikethrough option. In the _showContextMenu() method, the context menu inside the OverlayEntry widget has been created. The _checkAndCloseContextMenu() method is used for close the context menu.
/// Show Context menu with annotation options. void _showContextMenu( BuildContext context, PdfTextSelectionChangedDetails? details, ) { final RenderBox? renderBoxContainer =context.findRenderObject()! as RenderBox; if (renderBoxContainer != null) { final double _kContextMenuHeight = 90; final double _kContextMenuWidth = 100; final double _kHeight = 18; final Offset containerOffset = renderBoxContainer.localToGlobal( renderBoxContainer.paintBounds.topLeft, ); if (details != null && containerOffset.dy < details.globalSelectedRegion!.topLeft.dy || (containerOffset.dy < details!.globalSelectedRegion!.center.dy – (_kContextMenuHeight / 2) && details.globalSelectedRegion!.height > _kContextMenuWidth)) { double top = 0.0; double left = 0.0; final Rect globalSelectedRect = details.globalSelectedRegion!; if ((globalSelectedRect.top) > MediaQuery.of(context).size.height / 2) { top = globalSelectedRect.topLeft.dy + details.globalSelectedRegion!.height + _kHeight; left = globalSelectedRect.bottomLeft.dx; } else { top = globalSelectedRect.height > _kContextMenuWidth ? globalSelectedRect.center.dy - (_kContextMenuHeight / 2) : globalSelectedRect.topLeft.dy + details.globalSelectedRegion!.height + _kHeight; left = globalSelectedRect.height > _kContextMenuWidth ? globalSelectedRect.center.dx - (_kContextMenuWidth / 2) : globalSelectedRect.bottomLeft.dx; } final OverlayState? _overlayState = Overlay.of(context, rootOverlay: true); _overlayEntry = OverlayEntry( builder: (context) => Positioned( top: top, left: left, child: Container( decoration: BoxDecoration( color: _contextMenuColor, boxShadow: [ BoxShadow( color: Color.fromRGBO(0, 0, 0, 0.14), blurRadius: 2, offset: Offset(0, 0), ), BoxShadow( color: Color.fromRGBO(0, 0, 0, 0.12), blurRadius: 2, offset: Offset(0, 2), ), BoxShadow( color: Color.fromRGBO(0, 0, 0, 0.2), blurRadius: 3, offset: Offset(0, 1), ), ], ), constraints: BoxConstraints.tightFor( width: _kContextMenuWidth, height: _kContextMenuHeight), child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _addAnnotation('Highlight', details.selectedText), _addAnnotation('Underline', details.selectedText), _addAnnotation('Strikethrough', details.selectedText), ], ), ), ), ); _overlayState?.insert(_overlayEntry!); } } } /// Check and close the context menu. void _checkAndCloseContextMenu() { if (_overlayEntry != null) { _overlayEntry?.remove(); _overlayEntry = null; } }
Step 4: Add the highlight, strikethrough, and underline annotation for selected text in the SfPdfViewer. The getSelectedTextLines () API is used for extracting the text of the selected line, bounds information of the selected text, and to retrieve the page number of the selected text in a PDF document.
///Add the annotation in a PDF document. Widget _addAnnotation(String? annotationType, String? selectedText) { return Container( height: 30, width: 100, child: RawMaterialButton( onPressed: () async { _checkAndCloseContextMenu(); await Clipboard.setData(ClipboardData(text: selectedText)); _drawAnnotation(annotationType); }, child: Text( annotationType!, style: TextStyle( color: _textColor, fontSize: 10, fontFamily: 'Roboto', fontWeight: FontWeight.w400), ), ), ); } ///Draw the annotation in a PDF document. void _drawAnnotation(String? annotationType) { final PdfDocument document = PdfDocument(inputBytes: _documentBytes); switch (annotationType) { case 'Highlight': { _pdfViewerKey.currentState! .getSelectedTextLines() .forEach((pdfTextLine) { final PdfPage _page = document.pages[pdfTextLine.pageNumber]; final PdfRectangleAnnotation rectangleAnnotation = PdfRectangleAnnotation( pdfTextLine.bounds, 'Highlight Annotation', author: 'Syncfusion', color: PdfColor.fromCMYK(0, 0, 255, 0), innerColor: PdfColor.fromCMYK(0, 0, 255, 0), opacity: 0.5); _page.annotations.add(rectangleAnnotation); _page.annotations.flattenAllAnnotations(); xOffset = _pdfViewerController.scrollOffset.dx; yOffset = _pdfViewerController.scrollOffset.dy; }); final List<int> bytes = document.saveSync(); setState(() { _documentBytes = Uint8List.fromList(bytes); }); } break; case 'Underline': { _pdfViewerKey.currentState! .getSelectedTextLines() .forEach((pdfTextLine) { final PdfPage _page = document.pages[pdfTextLine.pageNumber]; final PdfLineAnnotation lineAnnotation = PdfLineAnnotation( [ pdfTextLine.bounds.left.toInt(), (document.pages[pdfTextLine.pageNumber].size.height – pdfTextLine.bounds.bottom) .toInt(), pdfTextLine.bounds.right.toInt(), (document.pages[pdfTextLine.pageNumber].size.height – pdfTextLine.bounds.bottom) .toInt() ], 'Underline Annotation', author: 'Syncfusion', innerColor: PdfColor(0, 255, 0), color: PdfColor(0, 255, 0), ); _page.annotations.add(lineAnnotation); _page.annotations.flattenAllAnnotations(); xOffset = _pdfViewerController.scrollOffset.dx; yOffset = _pdfViewerController.scrollOffset.dy; }); final List<int> bytes = document.saveSync(); setState(() { _documentBytes = Uint8List.fromList(bytes); }); } break; case 'Strikethrough': { _pdfViewerKey.currentState! .getSelectedTextLines() .forEach((pdfTextLine) { final PdfPage _page = document.pages[pdfTextLine.pageNumber]; final PdfLineAnnotation lineAnnotation = PdfLineAnnotation( [ pdfTextLine.bounds.left.toInt(), ((document.pages[pdfTextLine.pageNumber].size.height – pdfTextLine.bounds.bottom) + (pdfTextLine.bounds.height / 2)) .toInt(), pdfTextLine.bounds.right.toInt(), ((document.pages[pdfTextLine.pageNumber].size.height – pdfTextLine.bounds.bottom) + (pdfTextLine.bounds.height / 2)) .toInt() ], 'Strikethrough Annotation', author: 'Syncfusion', innerColor: PdfColor(255, 0, 0), color: PdfColor(255, 0, 0), ); _page.annotations.add(lineAnnotation); _page.annotations.flattenAllAnnotations(); xOffset = _pdfViewerController.scrollOffset.dx; yOffset = _pdfViewerController.scrollOffset.dy; }); final List<int> bytes = document.saveSync(); setState(() { _documentBytes = Uint8List.fromList(bytes); }); } break; } }
A complete working sample can be downloaded from here.
Conclusion
I hope you enjoyed learning about how to add text markup annotation in Flutter PDF Viewer using Syncfusion® PDF package.
You can refer to our Flutter PDF Viewer page to know about its other groundbreaking feature representations. You can also explore our documentation to understand how to create and manipulate data.
For current customers, you can check out our components from the License and Downloads page. If you are new to Syncfusion, you can try our 30-day free trial to check out our other controls.
If you have any queries or require clarifications, please let us know in the comments section below. You can also contact us through our support forums, Direct-Trac, or feedback portal. We are always happy to assist you!