Add an API for Flutter

Introduction

The Flutter API for Apryse Mobile SDK includes all of the most used functions and methods for viewing, annotating and saving PDF documents. However, it is possible your app may need access to APIs that are available as part of the native API, but are not directly available to Flutter.

There are 2 different ways to use Apryse Flutter API on iOS or Android:

  • Present a document via a plugin.
  • Show an Apryse document view via a Widget.

This guide will demonstrate how to add a new functionality to both approaches.

The examples provided will show you how to add the following to the Flutter interface:

  • readOnly viewer configuration option that determines if the viewer will allow edits to the document.
  • getPageCropBox function that returns a PTRect object containing information about the crop box of a specified page.
  • startZoomChangedListener event listener that is raised when the zoom ratio is changed in the current document.

You can follow the same pattern to add new functions and viewer configuration options that your Flutter app may need. These additions could be simple ones, which expose one piece of functionality, or custom ones, that expose a series of native commands under the hood.

Prior to following this guide, we highly recommend you to go through the official guide here: Writing Platform-specific code to have a better understanding of the system.

1. Fork and clone Apryse's Flutter Repo

The source is hosted on GitHub here: https://github.com/ApryseSDK/pdftron-flutter

Fork the project and clone a copy of the repository to your disk.

Adding the readOnly viewer configuration option

2. Define the new viewer configuration option in Dart

The config.dart file is responsible for setting all of the viewer configuration options.

Dart

1var _readOnly;
2
3set readOnly(bool value) => _readOnly = value;
4
5Config.fromJson(Map<String, dynamic> json) :
6 _readOnly = json['readOnly'];
7
8Map<String, dynamic> toJson() => {
9 'readOnly': _readOnly,
10};

3. Define the new viewer configuration option in Objective C

First add the property to ios/Classes/PTFlutterDocumentController.h:

Obj-C

1@property (nonatomic, assign, getter=isReadOnly) BOOL readOnly;

Define a config constant for the property in ios/Classes/PdftronFlutterPlugin.h:

Obj-C

1// config
2static NSString * const PTReadOnlyKey = @"readOnly";

4. Set the property

In ios/Classes/PdftronFlutterPlugin.m, we iterate through the config and check for the presence of keys. Add the config constant that you defined in the previous step, and set the property:

Obj-C

1+ (void)configureDocumentController:(PTFlutterDocumentController*)documentController withConfig:(NSString*)config
2{
3 ...
4 for (NSString* key in configPairs.allKeys) {
5 if([key isEqualToString:PTDisabledToolsKey])
6 {
7 ...
8 } else if ([key isEqualToString:PTReadOnlyKey]) {
9 NSNumber* readOnlyNumber = [PdftronFlutterPlugin getConfigValue:configPairs configKey:PTReadOnlyKey class:[NSNumber class] error:&error];
10 if (!error && readOnlyNumber) {
11 [documentController setReadOnly:[readOnlyNumber boolValue]];
12 }
13 }

In this case, we call the default setter. Sometimes a custom setter must be defined in ios/Classes/PTFlutterDocumentController.m.

5. Use the property

In ios/Classes/PTFlutterDocumentController.m, give the property an initial value:

Obj-C

1- (void)initViewerSettings
2{
3 _base64 = NO;
4 _readOnly = NO;
5 ...
6}

In the same file, use the property to set the PTToolManager.readonly property.

Obj-C

1- (void)viewWillLayoutSubviews
2{
3 [super viewWillLayoutSubviews];
4
5 if (self.needsDocumentLoaded) {
6 ...
7 }
8
9 if (![self.toolManager isReadonly] && self.readOnly) {
10 self.toolManager.readonly = YES;
11 }
12
13 [self.plugin.tabbedDocumentViewController.tabManager saveItems];
14}

Now the document associated with the PTPDFViewCtrl is read-only.

## Adding the `getPageCropBox` function

2. Define the new function in Dart

The constants.dart file contains constants grouped by their purpose. The names of functions, parameters, buttons, toolbars, and other relevant constants are stored here. Add a constant to the Functions class:

Dart

1class Functions {
2 static const getPageCropBox = "getPageCropBox";
3}

The document_view.dart file handles function calls for the widget, while pdftron_flutter.dart handles function calls for the plugin. If you want to implement for both the widget and plugin then add functions to both files, otherwise add your function to whichever you prefer.

Note that since we use JSON to transfer the information between Flutter and the native code, one extra step of decoding is required here.

In document_view.dart, add the getPageCropBox method to the DocumentViewController class:

Dart

1Future<PTRect> getPageCropBox(int pageNumber) {
2 return _channel.invokeMethod(Functions.getPageCropBox, <String, dynamic>{'pageNumber': pageNumber}).then((value) => jsonDecode(value));
3}

In pdftron_flutter.dart, add the getPageCropBox method to the PdftronFlutter class:

Dart

1static Future<PTRect> getPageCropBox(int pageNumber) {
2 return _channel.invokeMethod(Functions.getPageCropBox, <String, dynamic>{'pageNumber': pageNumber}).then((value) => jsonDecode(value));
3}

The options.dart file contains constants and classes for convenience. In this exercise, we add a new class called PTRect to this file for easier access of information.

Dart

1class PTRect {
2 double x1, y1, x2, y2, width, height;
3 PTRect(this.x1, this.y1, this.x2, this.y2, this.width, this.height);
4
5 factory PTRect.fromJson(dynamic json) {
6 return PTRect(getDouble(json['x1']), getDouble(json['y1']), getDouble(json['x2']), getDouble(json['y2']), getDouble(json['width']), getDouble(json['height']));
7 }
8
9 // a helper for JSON number decoding
10 static getDouble(dynamic value) {
11 if (value is int) {
12 return value.toDouble();
13 } else {
14 return value;
15 }
16 }
17}

3. Receive the new function from method channel

First, define the function and argument constants in ios/Classes/PdftronFlutterPlugin.h:

Obj-C

1// function
2static NSString * const PTGetPageCropBoxKey = @"getPageCropBox";
3...
4
5// argument
6static NSString * const PTPageNumberArgumentKey = @"pageNumber";
7...

Sometimes the argument constants may already exist, so check before you define any constants.

Open the ios/Classes/PdftronFlutterPlugin.m file and add a new case for the new function to handleMethodCall:

Obj-C

1- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
2 if ([call.method isEqualToString:PTGetPlatformVersionKey]) {
3 ...
4 } else if ([call.method isEqualToString:PTGetPageCropBoxKey]) {
5 NSNumber *pageNumber = [PluginUtils PT_idAsNSNumber:call.arguments[PTPageNumberArgumentKey]];
6 [self getPageCropBox:pageNumber resultToken:result];
7 } else {
8 ...
9 }
10}

4. Implement the new function

In the same file ios/Classes/PdftronFlutterPlugin.m, implement the new function:

Obj-C

1- (void)getPageCropBox:(NSNumber *)pageNumber resultToken:(FlutterResult)result
2{
3 PTDocumentViewController *docVC = [self getDocumentViewController];
4
5 if(documentController.document == Nil)
6 {
7 // something is wrong, no document.
8 NSLog(@"Error: The document view controller has no document.");
9 flutterResult([FlutterError errorWithCode:@"get_page_crop_box" message:@"Failed to get page crop box" details:@"Error: The document view controller has no document."]);
10 return;
11 }
12
13 NSError *error;
14 [docVC.pdfViewCtrl DocLock:YES withBlock:^(PTPDFDoc * _Nullable doc) {
15 if([doc HasDownloader])
16 {
17 // too soon
18 NSLog(@"Error: The document is still being downloaded.");
19 return;
20 }
21
22 PTPage *page = [doc GetPage:(int)pageNumber];
23 if (page) {
24 PTPDFRect *rect = [page GetCropBox];
25 NSDictionary<NSString *, NSNumber *> *map = @{
26 PTX1Key: @([rect GetX1]),
27 PTY1Key: @([rect GetY1]),
28 PTX2Key: @([rect GetX2]),
29 PTY2Key: @([rect GetY2]),
30 PTWidthKey: @([rect Width]),
31 PTHeightKey: @([rect Height]),
32 };
33 NSData *jsonData = [NSJSONSerialization dataWithJSONObject:map options:0 error:nil];
34 NSString *res = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
35
36 result(res);
37 }
38
39 } error:&error];
40
41 if(error)
42 {
43 NSLog(@"Error: There was an error while trying to get the page crop box. %@", error.localizedDescription);
44 }
45}

The logic is to first get the current document view controller, lock the file and check whether the document is still being downloaded. If not, get the crop box and encode all the associated information into a JSON string as the result.

The actual implementation will depend on the actual functionality.

Adding startZoomChangedListener

2. Define the new listener in Dart

The events.dart file is where all event listeners are defined and implemented. In this file do the following:

  • add a constant to represent the event channel.
  • declare a type alias for the listener.
  • add an id to the enum eventSinkId.
  • add a method that sets up a stream for our event.

Dart

1const _zoomChangedChannel = const EventChannel('zoom_changed_event');
2
3typedef void ZoomChangedListener(dynamic zoom);
4
5enum eventSinkId {
6 zoomChangedId,
7}
8
9CancelListener startZoomChangedListener(ZoomChangedListener listener) {
10 var subscription = _zoomChangedChannel
11 .receiveBroadcastStream(eventSinkId.zoomChangedId.index)
12 .listen(listener, cancelOnError: true);
13
14 return () {
15 subscription.cancel();
16 };
17}

3. Define necessary constants and functions on iOS

In ios/Classes/PdftronFlutterPlugin.h do the following:

  • add an event key constant.
  • add an id to the enum eventSinkId.
  • add a method to the existing PdftronFlutterPlugin interface.

By the end of the guide, this method will send events to Dart and will be called whenever zoom changes.

Obj-C

1// event strings
2static NSString * const PTZoomChangedEventKey = @"zoom_changed_event";
3
4typedef enum {
5 ...
6 zoomChangedId,
7} EventSinkId;
8
9@interface PdftronFlutterPlugin : NSObject<FlutterPlugin, FlutterStreamHandler, FlutterPlatformView>
10
11-(void)documentController:(PTDocumentController *)docVC zoomChanged:(NSNumber*)zoom;
12
13@end

4. Initialise event channel and sink

In order to send events to Flutter, we need an event channel and sink.

Add a new event sink property to ios/Classes/PdftronFlutterPlugin.m:

Obj-C

1@interface PdftronFlutterPlugin () <PTTabbedDocumentViewControllerDelegate, PTDocumentControllerDelegate>
2...
3@property (nonatomic, strong) FlutterEventSink zoomChangedEventSink;
4...
5@end

In the same file, declare and initialize the event channel:

Obj-C

1- (void)registerEventChannels:(NSObject<FlutterBinaryMessenger> *)messenger
2{
3 ...
4 FlutterEventChannel* zoomChangedEventChannel = [FlutterEventChannel eventChannelWithName:PTZoomChangedEventKey binaryMessenger:messenger];
5 ...
6 [zoomChangedEventChannel setStreamHandler:self];
7}

The two methods below, onListenWithArguments:eventSink: and onCancelWithArguments:eventSink:, are used to start and cancel the event listener respectively.

In the same file, add a case for our event id to each method:

Obj-C

1- (FlutterError* _Nullable)onListenWithArguments:(id _Nullable)arguments eventSink:(FlutterEventSink)events
2{
3
4 int sinkId = [arguments intValue];
5
6 switch (sinkId)
7 {
8 ...
9 case zoomChangedId:
10 self.zoomChangedEventSink = events;
11 break;
12 }
13
14 return Nil;
15}
16
17- (FlutterError* _Nullable)onCancelWithArguments:(id _Nullable)arguments
18{
19 int sinkId = [arguments intValue];
20
21 switch (sinkId)
22 {
23 ...
24 case zoomChangedId:
25 self.zoomChangedEventSink = nil;
26 break;
27 }
28
29 return Nil;
30}

5. Send event to Dart

In the same file, ios/Classes/PdftronFlutterPlugin.m, implement the method to send the event to Dart, using the new event sink:

Obj-C

1#pragma mark - EventSinks
2-(void)documentController:(PTDocumentController *)docVC zoomChanged:(NSNumber*)zoom
3{
4 if (self.zoomChangedEventSink != nil)
5 {
6 self.zoomChangedEventSink(zoom);
7 }
8}

In the next step, we will call this method whenever the event occurs.

6. Receive event from PDFViewCtrl

The PTPDFViewCtrlDelegate protocol contains the method pdfViewCtrl:pdfScrollViewDidZoom:. This method allows adopting delegates to respond to the PTPDFViewCtrl class when the zoom event occurs.

Implement this method in ios/Classes/PTFlutterDocumentController.m:

Obj-C

1- (void)pdfViewCtrl:(PTPDFViewCtrl*)pdfViewCtrl pdfScrollViewDidZoom:(UIScrollView *)scrollView
2{
3 const double zoom = self.pdfViewCtrl.zoom * self.pdfViewCtrl.zoomScale;
4 [self.plugin documentController:self zoomChanged:[NSNumber numberWithDouble:zoom]];
5}

The actual implementation will depend on the actual functionality.

Finishing steps

1. Push the code and integrate the updated source

Now update your library with the new code.

The new functionality is now ready to use.

2. Access the new functionality

The app can now access the new APIs as follows:

Plugin version:

Dart

1var config = Config();
2config.readOnly = true;
3
4await PdftronFlutter.openDocument("https://pdftron.s3.amazonaws.com/downloads/pl/PDFTRON_mobile_about.pdf", config: config);
5
6var cropBox = await PdftronFlutter.getPageCropBox(1);
7print('The width of crop box for page 1 is: ' + cropBox.width.toString());
8
9var zoomChangedCancel = startZoomChangedListener((zoom) {
10 print("flutter zoom changed. Current zoom is: $zoom");
11});
12
13zoomChangedCancel(); // Cancels the listener

Or:

Widget version:

Dart

1var config = Config();
2config.readOnly = true;
3
4await controller.openDocument("https://pdftron.s3.amazonaws.com/downloads/pl/PDFTRON_mobile_about.pdf", config: config);
5
6var cropBox = await controller.getPageCropBox(1);
7print('The width of crop box for page 1 is: ' + cropBox.width.toString());
8
9var zoomChangedCancel = startZoomChangedListener((zoom) {
10 print("flutter zoom changed. Current zoom is: $zoom");
11});
12
13zoomChangedCancel(); // Cancels the listener

3. All done!

If you're only developing for iOS, then you're all done!

If you're also deploying on Android, you'll need to complete the necessary steps for Android.

If you're developing for both iOS and Android, please consider submitting a PR, as upstreaming the change will simplify your developing and make the APIs available for other Apryse customers.

Did you find this helpful?

Trial setup questions?

Ask experts on Discord

Need other help?

Contact Support

Pricing or product questions?

Contact Sales