如何构建Flutter Android应用程序从Airprint MFPS扫描文档
#javascript #网络开发人员 #flutter #dart

Airprint MFPS(多功能打印机)是与Apple Airprint技术兼容的打印机。 ESCL(可扩展的扫描仪控制语言)协议是一种无人驾驶扫描协议,是Airprint技术的一部分。它促进了Airprint MFP上的无线扫描功能。用户可以在同一网络上发现Airprint MFP,并使用其移动设备启动扫描操作。本文着重于构建一个使用Web视图和本机代码从Airprint MFPS扫描文档的混合动力Android应用程序。

支持Airprint的MFP列表

  • 佳能Imageclass MF743CDW
  • HP办公室250
  • HP办公室3830
  • 佳能Pixma IX6820
  • HP OfficeJet Pro 9025E
  • 佳能Pixma TR8620
  • pixma tr150

先决条件

  • 与Android设备同一网络中的空气打击设备
  • 从Google Play安装Dynamsoft Service

免费的在线文档扫描演示

DynamSoft Service应用程序是一种Android背景服务,可在Web浏览器和无线扫描仪之间提供通信通道。

Android Dynamsoft Service

您可以单击“ Online Demo”按钮以在移动Web浏览器中启动免费的online document scanning demo。该演示还与 Windows linux macOS 系统兼容,通过安装适当的DynamSoft服务。对于基本的文档扫描需求,在线演示就足够了。但是,如果您需要具有高级功能的更强大的文档扫描解决方案,请考虑使用Dynamic Web TWAIN SDK进行编码,该Dynamic Web TWAIN SDK提供30-day free trial

为什么要创建混合应用程序而不是Web浏览器?

在Web浏览器中使用在线演示时,保存和共享文档可能会带来不便。混合应用程序提供了更好的用户体验。通过将演示加载到Web视图中,混合应用程序提供了Web技术和本机功能的无缝集成。通过使用本机代码,该应用程序可以将扫描的图像直接存储到本地存储中,从而为用户提供更高效,更灵活的处理选项。 Web和本地功能的这种组合确保了更光滑,更易于用户友好的文档扫描体验。

用颤音构建Android文档扫描应用程序

在以下各节中,我们将向您展示如何使用Flutter WebView加载在线文档扫描演示以及如何组合JavaScript和Dart代码以处理扫描的图像。

依赖的扑面包

入门之前,您需要将以下软件包添加到pubspec.yaml文件:

Android许可

该应用程序需要两个权限:

<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
  • android.permission.CAMERA-访问网络视图中的相机所需的必需。
  • android.permission.QUERY_ALL_PACKAGES-启动外部Dynamsoft服务应用所需

Flutter Web视图:加载URL,并处理导航和权限请求

根据 WebView_flutter 软件包的示例代码,可以将在线文档扫描演示加载到Android Web视图中:

_controller = WebViewController()
  ..setJavaScriptMode(JavaScriptMode.unrestricted)
  ..setBackgroundColor(const Color(0x00000000))
  ..setNavigationDelegate(
    NavigationDelegate(
      onProgress: (int progress) {},
      onPageStarted: (String url) {},
      onPageFinished: (String url) {},
      onWebResourceError: (WebResourceError error) {},
      onNavigationRequest: (NavigationRequest request) {
        return NavigationDecision.navigate;
      },
    ),
  )
  ..loadRequest(Uri.parse('https://demo3.dynamsoft.com/web-twain/mobile-online-camera-scanner/'));

如果在线演示无法连接到背景DynamSoft服务,它将提示您安装它:

Dynamsoft service not launched

使用Android Web浏览器时,只需单击Open Service按钮即可安装或启动DynamSoft Service应用程序。但是,在Web视图中操作时,您可能会遇到404页。要解决此问题,我们需要在onNavigationRequest回调中处理该请求。 DynamSoft服务活动可以由其packagecomponentName启动。如果设备未安装DynamSoft Service应用程序,我们可以通过打开Goog​​le Play商店采取适当的操作。

Future<void> launchURL(String url) async {
  await launchUrlString(url);
}

Future<void> launchIntent() async {
  if (Platform.isAndroid) {
    try {
      AndroidIntent intent = const AndroidIntent(
        componentName: 'com.dynamsoft.mobilescan.MainActivity',
        package: 'com.dynamsoft.mobilescan',
      );
      await intent.launch();
    } catch (e) {
      // If the app is not installed, open the Google Play store to install the app.
      launchURL(
          'https://play.google.com/store/apps/details?id=com.dynamsoft.mobilescan');
    }
  }
}

onNavigationRequest: (NavigationRequest request) {
  if (request.url.startsWith('intent://')) {
    launchIntent();
    return NavigationDecision.prevent;
  } else if (request.url.endsWith('.apk')) {
    launchURL(
        'https://play.google.com/store/apps/details?id=com.dynamsoft.mobilescan');
    return NavigationDecision.prevent;
  }
  return NavigationDecision.navigate;
},

您可能会遇到的另一个问题是Android Web视图中的相机访问。在线演示使您可以捕获扫描仪和相机的文档。默认情况下,在Android Web视图中禁用了相机访问。您必须在本机代码中授予摄像机权限。

camera access in Android web view

解决方法是处理onPermissionRequest回调中的WebViewPermissionRequest。我们可以如下更改WebViewController创建代码:


Future<void> requestCameraPermission() async {
  final status = await Permission.camera.request();
  if (status == PermissionStatus.granted) {
  } else if (status == PermissionStatus.denied) {
  } else if (status == PermissionStatus.permanentlyDenied) {
  }
}

@override
void initState() {
  super.initState();

  late final PlatformWebViewControllerCreationParams params;
  params = const PlatformWebViewControllerCreationParams();

  _controller = WebViewController.fromPlatformCreationParams(
    params,
    onPermissionRequest: (WebViewPermissionRequest request) {
      request.grant();
    },
  )
    ..setJavaScriptMode(JavaScriptMode.unrestricted)

  ...

  requestCameraPermission();
}

现在,演示应在扑动网络视图中按预期工作。下一步是通过添加一些飞镖代码来优化用户体验。

扑面选项卡栏

与原始在线演示的UI相比,您可能已经观察到我们已经通过隐藏了HTML5中实现的AboutContact Us选项卡进行了一些更改。

document scan demo in web view

在应用程序的底部添加了一个用于切换Web视图和其他本机视图的flutter选项卡栏。

Flutter web view and native view

如何将HTML元素隐藏在JavaScript中?

  1. 在Web视图中启用Debugging选项。

    if (_controller.platform is AndroidWebViewController) {
      AndroidWebViewController.enableDebugging(true);
    }
    
  2. 在边缘或Chrome中打开edge://inspect/#deviceschrome://inspect/#devices以检查HTML元素。

    debug and inspect HTML elements

  3. 在控制台中,执行以下JavaScript代码以隐藏AboutContact Us选项卡:

    let parentElement = document.getElementsByClassName('dcs-main-footer')[0];
    let tags = parentElement .getElementsByTagName('div');
    tags[0].remove();
    tags[6].remove();
    

    此外,隐藏结果面板,该面板由本地历史记录页面代替:

    document.getElementsByClassName('dcs-main-content')[0].style.display = 'none';
    
  4. 使用onProgress回调执行JavaScript代码:

    NavigationDelegate(
        onProgress: (int progress) {
          if (progress == 100) {
            String jscode = '''
          let parentElement = document.getElementsByClassName('dcs-main-footer')[0];
          let tags = parentElement .getElementsByTagName('div');
          tags[0].remove();
          tags[6].remove();
          document.getElementsByClassName('dcs-main-content')[0].style.display = 'none';
          ''';
            _controller
                .runJavaScript(jscode)
                .then((value) => {setState(() {})});
          }
        },
    )
    

如何在颤音中创建一个标签栏?

class _MyAppPageState extends State<MyAppPage>
    with SingleTickerProviderStateMixin {
  late TabController _tabController;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(vsync: this, length: 3);

    ...
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: TabBarView(
        controller: _tabController,
        children: [
          HomeView(title: 'Web TWAIN Demo', controller: _controller),
          const HistoryView(title: 'History'),
          const AboutView(title: 'About the SDK'),
        ],
      ),
      bottomNavigationBar: TabBar(
        labelColor: Colors.blue,
        controller: _tabController,
        tabs: const [
          Tab(icon: Icon(Icons.home), text: 'Home'),
          Tab(icon: Icon(Icons.history_sharp), text: 'History'),
          Tab(icon: Icon(Icons.info), text: 'About'),
        ],
      ),
    );
  }
}

将base64图像从Web视图保存到本地存储

作为在Web视图中获取文档,可以将图像数据检索为base64字符串。

DWObject.ConvertToBase64([DWObject.CurrentImageIndexInBuffer], 1, (result, indices, type) =>{})

我们使用addJavaScriptChannel方法在Web视图和本机代码之间添加了通信通道。

_controller = WebViewController.fromPlatformCreationParams(
    params,
    onPermissionRequest: (WebViewPermissionRequest request) {
      request.grant();
    },
  )
    ..addJavaScriptChannel(
      'ImageData',
      onMessageReceived: (JavaScriptMessage message) {
        saveImage(message.message).then((value) {
          showToast(value);
        });
      },
    )

然后,JavaScript代码可以致电postMessage将消息发送到DART代码。当JavaScript映像缓冲区没有空时,我们创建一个按钮来触发图像保存操作。接收到消息时调用onMessageReceived回调。

IconButton(
  icon: const Icon(Icons.save),
  onPressed: () async {
    widget.controller.runJavaScript(
        'DWObject.ConvertToBase64([DWObject.CurrentImageIndexInBuffer], 1, (result, indices, type) =>{ImageData.postMessage(result._content)})');
  },
),

如何将base64字符串保存到flutter中的本地文件?

String getImageName() {
  // Get the current date and time.
  DateTime now = DateTime.now();

  // Format the date and time to create a timestamp.
  String timestamp =
      '${now.year}${now.month}${now.day}_${now.hour}${now.minute}${now.second}';

  // Create the image file name with the timestamp.
  String imageName = 'image_$timestamp.jpg';

  return imageName;
}

Future<String> saveImage(String base64String) async {
  Uint8List bytes = base64Decode(base64String);

  // Get the app directory
  final directory = await getApplicationDocumentsDirectory();

  // Create the file path
  String imageName = getImageName();
  final filePath = '${directory.path}/$imageName';

  // Write the bytes to the file
  await File(filePath).writeAsBytes(bytes);

  return filePath;
}

使用特定于平台的代码实现了showToast()方法:

  1. MainActivity.kt中添加以下Kotlin代码:

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
      super.configureFlutterEngine(flutterEngine)
      MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "AndroidNative")
        .setMethodCallHandler { call, result ->
          when (call.method) {
            "showToast" -> {
              val message = call.argument<String>("message")
              showToast(message!!)
              result.success(null)
            }
            else -> {
              result.notImplemented()
            }
          }
        }
    }
    
    private fun showToast(message: String) {
        Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
      }
    
  2. 调用DART代码中的showToast()方法:

    void showToast(String message) {
      const platform = MethodChannel('AndroidNative');
      platform.invokeMethod('showToast', {'message': message});
    }
    

查看并共享扫描的图像

将扫描的图像保存到本地存储后,我们可以获取文件列表如下:

Future<List<String>> getImages() async {
  // Get the app directory
  final directory = await getApplicationDocumentsDirectory();

  // Get the list of files in the app directory
  List<FileSystemEntity> files = directory.listSync();

  // Get the file paths
  List<String> filePaths = [];
  for (FileSystemEntity file in files) {
    if (file.path.endsWith('.jpg')) {
      filePaths.add(file.path);
    }
  }

  return filePaths;
}

要查看图像,我们创建了一个新的无状态小部件,并将文件路径传递为参数:

class DocumentView extends StatelessWidget {
  final String filePath;
  const DocumentView({super.key, required this.filePath});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Document Viewer'),
      ),
      body: Center(child: Image.file(File(filePath))),
    );
  }
}

flutter image view

可以与Share.shareXFiles()方法共享图像文件:

IconButton(
  icon: const Icon(Icons.share),
  onPressed: () async {
    if (selectedValue == -1) {
      return;
    }

    await Share.shareXFiles([XFile(_results[selectedValue])],
        text: 'Check out this image!');
  },
),

源代码

https://github.com/yushulx/flutter-android-document-scan