使用C++实现Windows和Linux的Flutter文档扫描插件

news2024/10/6 22:23:52

文档扫描应用大都是移动应用。结合手机摄像头可以实现文档拍摄,边缘检测,自动剪裁,透视矫正,滤镜等功能。但是桌面办公也少不了文档处理。这里分享下如何使用Dynamsoft Document Normalizer C++ SDK实现用于桌面文档处理的Flutter插件。

Flutter文档扫描插件下载

需要快速体验可以直接访问
https://pub.dev/packages/flutter_document_scan_sdk

生成配置Flutter桌面插件工程

因为C++ SDK只提供了Windows和Linux的库,所以我们创建一个支持Windows和Linux的Flutter插件工程:

flutter create --org com.dynamsoft --template=plugin --platforms=windows,linux .

我们看到Windows和Linux插件代码都是通过CMake编译的。所以需要打开CMakeLists.txt文件配置下编译环境。

Windows

link_directories("${PROJECT_SOURCE_DIR}/lib/") 

target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin "DynamsoftCorex64" "DynamsoftDocumentNormalizerx64")

set(flutter_document_scan_sdk_bundled_libraries
  "${PROJECT_SOURCE_DIR}/bin/"
  PARENT_SCOPE
)

Linux

link_directories("${PROJECT_SOURCE_DIR}/lib/") 

target_link_libraries(${PLUGIN_NAME} PRIVATE flutter "DynamsoftCore" "DynamsoftDocumentNormalizer")

set(flutter_document_scan_sdk_bundled_libraries
  
  PARENT_SCOPE
)

两个平台有一些差异。用于Windows和Linux的库名字有一些不同。Windows上因为链接的是*.lib文件,最后还需要指定*.dll文件的路径用于打包。

Flutter和C++接口实现

Flutter和底层代码是通过method channel通信的。

首先在flutter_document_scan_sdk_method_channel.dart中定义上层接口:

class MethodChannelFlutterDocumentScanSdk
    extends FlutterDocumentScanSdkPlatform {
  
  final methodChannel = const MethodChannel('flutter_document_scan_sdk');

  
  Future<String?> getPlatformVersion() async {
    final version =
        await methodChannel.invokeMethod<String>('getPlatformVersion');
    return version;
  }

  
  Future<int?> init(String path, String key) async {
    return await methodChannel
        .invokeMethod<int>('init', {'path': path, 'key': key});
  }

  
  Future<int?> setParameters(String params) async {
    return await methodChannel
        .invokeMethod<int>('setParameters', {'params': params});
  }

  
  Future<String?> getParameters() async {
    return await methodChannel.invokeMethod<String>('getParameters');
  }

  
  Future<List<DocumentResult>> detect(String file) async {
    List? results = await methodChannel.invokeListMethod<dynamic>(
      'detect',
      {'file': file},
    );

    return _resultWrapper(results);
  }

  List<DocumentResult> _resultWrapper(List<dynamic>? results) {
    List<DocumentResult> output = [];

    if (results != null) {
      for (var result in results) {
        int confidence = result['confidence'];
        List<Offset> offsets = [];
        int x1 = result['x1'];
        int y1 = result['y1'];
        int x2 = result['x2'];
        int y2 = result['y2'];
        int x3 = result['x3'];
        int y3 = result['y3'];
        int x4 = result['x4'];
        int y4 = result['y4'];
        offsets.add(Offset(x1.toDouble(), y1.toDouble()));
        offsets.add(Offset(x2.toDouble(), y2.toDouble()));
        offsets.add(Offset(x3.toDouble(), y3.toDouble()));
        offsets.add(Offset(x4.toDouble(), y4.toDouble()));
        DocumentResult documentResult = DocumentResult(confidence, offsets, []);
        output.add(documentResult);
      }
    }

    return output;
  }

  
  Future<NormalizedImage?> normalize(String file, dynamic points) async {
    Offset offset = points[0];
    int x1 = offset.dx.toInt();
    int y1 = offset.dy.toInt();

    offset = points[1];
    int x2 = offset.dx.toInt();
    int y2 = offset.dy.toInt();

    offset = points[2];
    int x3 = offset.dx.toInt();
    int y3 = offset.dy.toInt();

    offset = points[3];
    int x4 = offset.dx.toInt();
    int y4 = offset.dy.toInt();
    Map? result = await methodChannel.invokeMapMethod<String, dynamic>(
      'normalize',
      {
        'file': file,
        'x1': x1,
        'y1': y1,
        'x2': x2,
        'y2': y2,
        'x3': x3,
        'y3': y3,
        'x4': x4,
        'y4': y4
      },
    );

    if (result != null) {
      return NormalizedImage(
        result['data'],
        result['width'],
        result['height'],
      );
    }

    return null;
  }

  
  Future<int?> save(String filename) async {
    return await methodChannel
        .invokeMethod<int>('save', {'filename': filename});
  }
}

然后在C++中解析接口名和参数名。Windows上C++的入口函数是FlutterDocumentScanSdkPlugin::HandleMethodCall,而Linux上是flutter_document_scan_sdk_plugin_handle_method_call

Windows

#include "flutter_document_scan_sdk_plugin.h"

#include <windows.h>

#include <VersionHelpers.h>

#include <flutter/method_channel.h>
#include <flutter/plugin_registrar_windows.h>
#include <flutter/standard_method_codec.h>

#include <memory>
#include <sstream>

void FlutterDocumentScanSdkPlugin::HandleMethodCall(
      const flutter::MethodCall<flutter::EncodableValue> &method_call,
      std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result){}

Linux

#include "include/flutter_document_scan_sdk/flutter_document_scan_sdk_plugin.h"

#include <flutter_linux/flutter_linux.h>
#include <gtk/gtk.h>
#include <sys/utsname.h>

#include <cstring>

static void flutter_document_scan_sdk_plugin_handle_method_call(
    FlutterDocumentScanSdkPlugin *self,
    FlMethodCall *method_call){}

因为Windows和Linux的头文件和数据结构不同,所以对应的解析代码也有区别。

Windows

const auto *arguments = std::get_if<EncodableMap>(method_call.arguments());

if (method_call.method_name().compare("init") == 0)
{
  std::string license;
  int ret = 0;

  if (arguments)
  {
    auto license_it = arguments->find(EncodableValue("key"));
    if (license_it != arguments->end())
    {
      license = std::get<std::string>(license_it->second);
    }
    ret = DocumentManager::SetLicense(license.c_str());
  }

  result->Success(EncodableValue(ret));
}
...

Linux

if (strcmp(method, "init") == 0)
{
  if (fl_value_get_type(args) != FL_VALUE_TYPE_MAP)
  {
    return;
  }

  FlValue *value = fl_value_lookup_string(args, "key");
  if (value == nullptr)
  {
    return;
  }
  const char *license = fl_value_get_string(value);

  int ret = DocumentManager::SetLicense(license);
  g_autoptr(FlValue) result = fl_value_new_int(ret);
  response = FL_METHOD_RESPONSE(fl_method_success_response_new(result));
}
...

接下来创建一个document_manager.h文件来实现所有的底层接口:

  • 导入Dynamsoft Document Normalizer的头文件:

    #include "DynamsoftCore.h"
    #include "DynamsoftDocumentNormalizer.h"
    
    using namespace std;
    using namespace dynamsoft::ddn;
    using namespace dynamsoft::core;
    
  • 创建和销毁Dynamsoft Document Normalizer:

    class DocumentManager
    {
    public:
        ~DocumentManager()
        {
            if (normalizer != NULL)
            {
                DDN_DestroyInstance(normalizer);
                normalizer = NULL;
            }
    
            FreeImage();
        };
    
        const char *GetVersion()
        {
            return DDN_GetVersion();
    }
    
    void Init()
    {
        normalizer = DDN_CreateInstance();
        imageResult = NULL;
    }
    private:
        void *normalizer;
        NormalizedImageResult *imageResult;
    
        void FreeImage()
        {
            if (imageResult != NULL)
            {
                DDN_FreeNormalizedImageResult(&imageResult);
                imageResult = NULL;
            }
        }
    };
    
  • 设置license:

    static int SetLicense(const char *license)
    {
        char errorMsgBuffer[512];
        // Click https://www.dynamsoft.com/customer/license/trialLicense/?product=ddn to get a trial license.
        int ret = DC_InitLicense(license, errorMsgBuffer, 512);
        if (ret != DM_OK)
        {
            cout << errorMsgBuffer << endl;
        }
        return ret;
    }
    
  • 文档边缘检测:
    Windows

    EncodableList Detect(const char *filename)
    {
        EncodableList out;
        if (normalizer == NULL)
            return out;
    
        DetectedQuadResultArray *pResults = NULL;
    
        int ret = DDN_DetectQuadFromFile(normalizer, filename, "", &pResults);
        if (ret)
        {
            printf("Detection error: %s\n", DC_GetErrorString(ret));
        }
    
        if (pResults)
        {
            int count = pResults->resultsCount;
    
            for (int i = 0; i < count; i++)
            {
                EncodableMap map;
    
                DetectedQuadResult *quadResult = pResults->detectedQuadResults[i];
                int confidence = quadResult->confidenceAsDocumentBoundary;
                DM_Point *points = quadResult->location->points;
                int x1 = points[0].coordinate[0];
                int y1 = points[0].coordinate[1];
                int x2 = points[1].coordinate[0];
                int y2 = points[1].coordinate[1];
                int x3 = points[2].coordinate[0];
                int y3 = points[2].coordinate[1];
                int x4 = points[3].coordinate[0];
                int y4 = points[3].coordinate[1];
    
                map[EncodableValue("confidence")] = EncodableValue(confidence);
                map[EncodableValue("x1")] = EncodableValue(x1);
                map[EncodableValue("y1")] = EncodableValue(y1);
                map[EncodableValue("x2")] = EncodableValue(x2);
                map[EncodableValue("y2")] = EncodableValue(y2);
                map[EncodableValue("x3")] = EncodableValue(x3);
                map[EncodableValue("y3")] = EncodableValue(y3);
                map[EncodableValue("x4")] = EncodableValue(x4);
                map[EncodableValue("y4")] = EncodableValue(y4);
                out.push_back(map);
            }
        }
    
        if (pResults != NULL)
            DDN_FreeDetectedQuadResultArray(&pResults);
    
        return out;
    }
    

    Linux

    FlValue* Detect(const char *filename)
    {
        FlValue* out = fl_value_new_list();
        if (normalizer == NULL)
            return out;
    
        DetectedQuadResultArray *pResults = NULL;
    
        int ret = DDN_DetectQuadFromFile(normalizer, filename, "", &pResults);
        if (ret)
        {
            printf("Detection error: %s\n", DC_GetErrorString(ret));
        }
    
        if (pResults)
        {
            int count = pResults->resultsCount;
    
            for (int i = 0; i < count; i++)
            {
                FlValue* result = fl_value_new_map ();
    
                DetectedQuadResult *quadResult = pResults->detectedQuadResults[i];
                int confidence = quadResult->confidenceAsDocumentBoundary;
                DM_Point *points = quadResult->location->points;
                int x1 = points[0].coordinate[0];
                int y1 = points[0].coordinate[1];
                int x2 = points[1].coordinate[0];
                int y2 = points[1].coordinate[1];
                int x3 = points[2].coordinate[0];
                int y3 = points[2].coordinate[1];
                int x4 = points[3].coordinate[0];
                int y4 = points[3].coordinate[1];
    
                fl_value_set_string_take (result, "confidence", fl_value_new_int(confidence));
                fl_value_set_string_take (result, "x1", fl_value_new_int(x1));
                fl_value_set_string_take (result, "y1", fl_value_new_int(y1));
                fl_value_set_string_take (result, "x2", fl_value_new_int(x2));
                fl_value_set_string_take (result, "y2", fl_value_new_int(y2));
                fl_value_set_string_take (result, "x3", fl_value_new_int(x3));
                fl_value_set_string_take (result, "y3", fl_value_new_int(y3));
                fl_value_set_string_take (result, "x4", fl_value_new_int(x4));
                fl_value_set_string_take (result, "y4", fl_value_new_int(y4));
    
                fl_value_append_take (out, result);
            }
        }
    
        if (pResults != NULL)
            DDN_FreeDetectedQuadResultArray(&pResults);
    
        return out;
    }
    
  • 文档剪裁,实现标准化效果:

    Quadrilateral quad;
    quad.points[0].coordinate[0] = x1;
    quad.points[0].coordinate[1] = y1;
    quad.points[1].coordinate[0] = x2;
    quad.points[1].coordinate[1] = y2;
    quad.points[2].coordinate[0] = x3;
    quad.points[2].coordinate[1] = y3;
    quad.points[3].coordinate[0] = x4;
    quad.points[3].coordinate[1] = y4;
    
    int errorCode = DDN_NormalizeFile(normalizer, filename, "", &quad, &imageResult);
    if (errorCode != DM_OK)
        printf("%s\r\n", DC_GetErrorString(errorCode));
    
    if (imageResult)
    {
        ImageData *imageData = imageResult->image;
        int width = imageData->width;
        int height = imageData->height;
        int stride = imageData->stride;
        int format = (int)imageData->format;
        unsigned char* data = imageData->bytes;
        int orientation = imageData->orientation;
        int length = imageData->bytesLength;
    }
    

    如果得到的数据是黑白图,1字节保存了8个像素值。为了显示,需要把每个bit的值转成0或255:

    unsigned char *grayscale = new unsigned char[width * height];
    binary2grayscale(data, grayscale, width, height, stride, length);
    
    void binary2grayscale(unsigned char* data, unsigned char* output, int width, int height, int stride, int length) 
    {
        int index = 0;
    
        int skip = stride * 8 - width;
        int shift = 0;
        int n = 1;
    
        for (int i = 0; i < length; ++i)
        {
            unsigned char b = data[i];
            int byteCount = 7;
            while (byteCount >= 0)
            {
                int tmp = (b & (1 << byteCount)) >> byteCount;
    
                if (shift < stride * 8 * n - skip) {
                    if (tmp == 1)
                        output[index] = 255;
                    else
                        output[index] = 0;
                    index += 1;
                }
    
                byteCount -= 1;
                shift += 1;
            }
    
            if (shift == stride * 8 * n) {
                n += 1;
            }
        }
    }
    

    我们调用Flutter的decodeImageFromPixels()接口来创建一个显示用的图像,这个接口只支持rgba8888bgra8888。因此返回的数据要加上alpha通道。

    unsigned char *rgba = new unsigned char[width * height * 4];
    memset(rgba, 0, width * height * 4);
    if (format == IPF_RGB_888)
    {
        int dataIndex = 0;
        for (int i = 0; i < height; i++)
        {
            for (int j = 0; j < width; j++)
            {
                int index = i * width + j;
    
                rgba[index * 4] = data[dataIndex + 2];     // red
                rgba[index * 4 + 1] = data[dataIndex + 1]; // green
                rgba[index * 4 + 2] = data[dataIndex];     // blue
                rgba[index * 4 + 3] = 255;                 // alpha
                dataIndex += 3;
            }
        }
    }
    else if (format == IPF_GRAYSCALED)
    {
        int dataIndex = 0;
        for (int i = 0; i < height; i++)
        {
            for (int j = 0; j < width; j++)
            {
                int index = i * width + j;
                rgba[index * 4] = data[dataIndex];
                rgba[index * 4 + 1] = data[dataIndex];
                rgba[index * 4 + 2] = data[dataIndex];
                rgba[index * 4 + 3] = 255;
                dataIndex += 1;
            }
        }
    }
    else if (format == IPF_BINARY)
    {
        unsigned char *grayscale = new unsigned char[width * height];
        binary2grayscale(data, grayscale, width, height, stride, length);
    
        int dataIndex = 0;
        for (int i = 0; i < height; i++)
        {
            for (int j = 0; j < width; j++)
            {
                int index = i * width + j;
                rgba[index * 4] = grayscale[dataIndex];
                rgba[index * 4 + 1] = grayscale[dataIndex];
                rgba[index * 4 + 2] = grayscale[dataIndex];
                rgba[index * 4 + 3] = 255;
                dataIndex += 1;
            }
        }
    
        free(grayscale);
    }
    

Flutter桌面文档扫描应用

流程:

  1. 读取文件。
  2. 自动边缘检测。
  3. 根据坐标做图片剪裁以及透视矫正。
  4. 保存文档。

你需要自己申请一个30天试用密钥,也可以使用我提供的一天可用密钥。

代码:

import 'dart:ui';

import 'package:file_selector/file_selector.dart';
import 'package:flutter/material.dart';
import 'dart:async';

import 'package:flutter/services.dart';
import 'package:flutter_document_scan_sdk/document_result.dart';
import 'package:flutter_document_scan_sdk/flutter_document_scan_sdk.dart';
import 'package:flutter_document_scan_sdk/template.dart';
import 'package:flutter_document_scan_sdk/normalized_image.dart';
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart' show kIsWeb;

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

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

  
  State<MyApp> createState() => _MyAppState();
}

class ImagePainter extends CustomPainter {
  ImagePainter(this.image, this.results);
  final ui.Image image;
  final List<DocumentResult> results;

  
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.red
      ..strokeWidth = 5
      ..style = PaintingStyle.stroke;

    canvas.drawImage(image, Offset.zero, paint);
    for (var result in results) {
      canvas.drawLine(result.points[0], result.points[1], paint);
      canvas.drawLine(result.points[1], result.points[2], paint);
      canvas.drawLine(result.points[2], result.points[3], paint);
      canvas.drawLine(result.points[3], result.points[0], paint);
    }
  }

  
  bool shouldRepaint(ImagePainter oldDelegate) =>
      image != oldDelegate.image || results != oldDelegate.results;
}

class _MyAppState extends State<MyApp> {
  String _platformVersion = 'Unknown';
  final _flutterDocumentScanSdkPlugin = FlutterDocumentScanSdk();
  String file = '';
  ui.Image? image;
  ui.Image? normalizedUiImage;
  NormalizedImage? normalizedImage;
  List<DocumentResult>? detectionResults = [];
  String _pixelFormat = 'grayscale';

  
  void initState() {
    super.initState();
    initPlatformState();
  }

  Future<ui.Image> loadImage(XFile file) async {
    final data = await file.readAsBytes();
    return await decodeImageFromList(data);
  }

  // Platform messages are asynchronous, so we initialize in an async method.
  Future<void> initPlatformState() async {
    String platformVersion;
    // Platform messages may fail, so we use a try/catch PlatformException.
    // We also handle the message potentially returning null.
    try {
      platformVersion =
          await _flutterDocumentScanSdkPlugin.getPlatformVersion() ??
              'Unknown platform version';
    } on PlatformException {
      platformVersion = 'Failed to get platform version.';
    }

    int? ret = await _flutterDocumentScanSdkPlugin.init(
        "https://cdn.jsdelivr.net/npm/dynamsoft-document-normalizer@1.0.11/dist/",
        "DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ==");
    String? params = await _flutterDocumentScanSdkPlugin.getParameters();
    print(params);

    ret = await _flutterDocumentScanSdkPlugin.setParameters(Template.grayscale);
    if (ret != 0) {
      print("setParameters failed");
    }
    // If the widget was removed from the tree while the asynchronous platform
    // message was in flight, we want to discard the reply rather than calling
    // setState to update our non-existent appearance.
    if (!mounted) return;

    setState(() {
      _platformVersion = platformVersion;
    });
  }

  Widget createCustomImage(ui.Image image, List<DocumentResult> results) {
    return SizedBox(
      width: image.width.toDouble(),
      height: image.height.toDouble(),
      child: CustomPaint(
        painter: ImagePainter(image, results),
      ),
    );
  }

  Future<void> normalizeFile(String file, dynamic points) async {
    normalizedImage = await _flutterDocumentScanSdkPlugin.normalize(
        file, detectionResults![0].points);
    if (normalizedImage != null) {

      decodeImageFromPixels(normalizedImage!.data, normalizedImage!.width,
          normalizedImage!.height, PixelFormat.rgba8888, (ui.Image img) {
        normalizedUiImage = img;
        setState(() {});
      });
    }
  }

  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Dynamsoft Document Normalizer'),
        ),
        body: Stack(children: <Widget>[
          Center(
            child: GridView.count(
              padding: const EdgeInsets.all(30.0),
              crossAxisSpacing: 10.0,
              mainAxisSpacing: 10.0,
              crossAxisCount: 2,
              children: <Widget>[
                SingleChildScrollView(
                    child: SingleChildScrollView(
                  scrollDirection: Axis.horizontal,
                  child: image == null
                      ? Image.asset('images/default.png')
                      : createCustomImage(image!, detectionResults!),
                )),
                SingleChildScrollView(
                  child: SingleChildScrollView(
                    scrollDirection: Axis.horizontal,
                    child: normalizedUiImage == null
                        ? Image.asset('images/default.png')
                        : createCustomImage(normalizedUiImage!, []),
                  ),
                )
              ],
            ),
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Radio(
                value: 'binary',
                groupValue: _pixelFormat,
                onChanged: (String? value) async {
                  setState(() {
                    _pixelFormat = value!;
                  });

                  await _flutterDocumentScanSdkPlugin
                      .setParameters(Template.binary);

                  if (detectionResults!.isNotEmpty) {
                    await normalizeFile(file, detectionResults![0].points);
                  }
                },
              ),
              const Text('Binary'),
              Radio(
                value: 'grayscale',
                groupValue: _pixelFormat,
                onChanged: (String? value) async {
                  setState(() {
                    _pixelFormat = value!;
                  });

                  await _flutterDocumentScanSdkPlugin
                      .setParameters(Template.grayscale);

                  if (detectionResults!.isNotEmpty) {
                    await normalizeFile(file, detectionResults![0].points);
                  }
                },
              ),
              const Text('Gray'),
              Radio(
                value: 'color',
                groupValue: _pixelFormat,
                onChanged: (String? value) async {
                  setState(() {
                    _pixelFormat = value!;
                  });

                  await _flutterDocumentScanSdkPlugin
                      .setParameters(Template.color);

                  if (detectionResults!.isNotEmpty) {
                    await normalizeFile(file, detectionResults![0].points);
                  }
                },
              ),
              const Text('Color'),
            ],
          ),
          Column(
            mainAxisAlignment: MainAxisAlignment.end,
            children: [
              SizedBox(
                height: 100,
                child: Row(
                    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                    children: <Widget>[
                      MaterialButton(
                          textColor: Colors.white,
                          color: Colors.blue,
                          onPressed: () async {
                            const XTypeGroup typeGroup = XTypeGroup(
                              label: 'images',
                              extensions: <String>['jpg', 'png'],
                            );
                            final XFile? pickedFile = await openFile(
                                acceptedTypeGroups: <XTypeGroup>[typeGroup]);
                            if (pickedFile != null) {
                              image = await loadImage(pickedFile);
                              file = pickedFile.path;
                              detectionResults =
                                  await _flutterDocumentScanSdkPlugin
                                      .detect(file);
                              setState(() {});
                              if (detectionResults!.isEmpty) {
                                print("No document detected");
                              } else {
                                setState(() {});
                                print("Document detected");
                                await normalizeFile(
                                    file, detectionResults![0].points);
                              }
                            }
                          },
                          child: const Text('Load Document')),
                      MaterialButton(
                          textColor: Colors.white,
                          color: Colors.blue,
                          onPressed: () async {
                            const String fileName = 'normalized.png';

                            if (kIsWeb) {
                              await _flutterDocumentScanSdkPlugin
                                  .save(fileName);

                              String path = 'normalized.webp';

                              if (normalizedUiImage != null) {
                                const String mimeType = 'image/webp';
                                ByteData? data = await normalizedUiImage!
                                    .toByteData(format: ui.ImageByteFormat.png);
                                if (data != null) {
                                  final XFile imageFile = XFile.fromData(
                                    data.buffer.asUint8List(),
                                    mimeType: mimeType,
                                  );
                                  await imageFile.saveTo(path);
                                }
                              }
                            } else {
                              String? path =
                                  await getSavePath(suggestedName: fileName);

                              path ??= fileName;

                              await _flutterDocumentScanSdkPlugin.save(path);

                              path = '${path.split('.png')[0]}.webp';

                              if (normalizedUiImage != null) {
                                const String mimeType = 'image/webp';
                                ByteData? data = await normalizedUiImage!
                                    .toByteData(format: ui.ImageByteFormat.png);
                                if (data != null) {
                                  final XFile imageFile = XFile.fromData(
                                    data.buffer.asUint8List(),
                                    mimeType: mimeType,
                                  );
                                  await imageFile.saveTo(path);
                                }
                              }
                            }
                          },
                          child: const Text("Save Document"))
                    ]),
              ),
            ],
          )
        ]),
      ),
    );
  }
}

程序可以在Windows, Linux和web上运行。

测试环境:统信UOS

flutter run -d linux

Flutter文档扫描

源码

https://github.com/yushulx/flutter_document_scan_sdk

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/71512.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

抖音最新版抓包方案,修改so绕过ssl

dy过ssl charles抓包 及xposed的justtrustme安装到手机上只能过系统的ssl。 抖音写了一个非系统的ssl所以需要反编译so来处理。 第一步&#xff0c;charles 我用的是magisk手机&#xff0c;先重charles把证书下载到pc端 选项1&#xff0c;安装证书到本地 选项2&#xff0c…

Android桌面图标快捷方式

一、背景 长按桌面图标实现快捷方式最早是iOS提供的功能,而Android最早在Android 7.1版本也提供了对这方面的支持,于是在短时间内,像微信,支付宝,头条等流量级应用都提供了这方面的支持,如下图。 现在,长按桌面图标快捷方式已经是很成熟的功能,实现上也比较简单,主…

知识图谱-KGE-语义匹配-双线性模型(打分函数用到了双线性函数)-2011:RESCAL【双线性模型的开山之作】【把每个关系对应的邻接矩阵进行了矩阵的分解】

【paper】 A Three-Way Model for Collective Learning on Multi-Relational Data 【简介】 这篇文章应该算是双线性模型的开山之作。是德国的一个团队发表在 ICML 2011 上的工作&#xff0c;比较老了&#xff0c;主要思想是三维张量分解。 想研究啥&#xff0c;啥就很重要 Re…

Activity7-流程初体验

流程符号详解&#xff1a; 创建Activiti工作流的主要步骤包含以下几步: 1. 定义流程。按照BPMN的规范&#xff0c;使用流程定义工具&#xff0c;将整个流程描述出来。 2. 部署流程。把画好的BPMN流程定义文件加载到数据库中&#xff0c;生成相关的表数据。 3. 启动流程。使用…

基于粒子群优化算法的微电网调度(光伏、储能、电动车、电网交互)(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️❤️&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清…

目标检测3

还是目标检测相关~这次是Box Size置信度偏差会损害目标检测器 检测器的置信度预测在目标大小和位置方面存在偏差&#xff0c;但目前尚不清楚这种偏差与受影响的目标检测器的性能有何关系。 无数应用依赖于目标检测器的可靠置信度估计的准确预测。然而&#xff0c;众所周知&am…

[附源码]Python计算机毕业设计Django在线招聘网站

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

突破40%!新能源汽车L2级辅助驾驶搭载率创新高

新能源汽车正在成为智能化的主力军。 高工智能汽车研究院监测数据显示&#xff0c;2022年1-10月中国市场&#xff08;不含进出口&#xff09;新能源汽车前装标配搭载L2级辅助驾驶交付上险167.51万辆&#xff0c;前装搭载率首次突破40%&#xff0c;达到41.93%&#xff1b;同期&…

[Java反序列化]CommonsBeanutils1利用链学习

0x01 前篇shiro的利用&#xff0c;需要动态字节码 &#xff0c;而这种方式需要我们自己添加依赖&#xff0c;所以很局限&#xff0c;而CommonsBeanutils 是shiro的依赖&#xff0c; CommonsBeanutils 是应用于 javabean 的工具 javabean的定义 类必须是具体的和公共的&#…

传统服务器与云服务器

传统服务器 传统服务器即物理服务器&#xff0c;是指独立服务器&#xff0c;也就是指物理上的单独服务器&#xff0c;物理服务器的构成包括处理器、硬盘、内存、系统总线等&#xff0c;和通用的计算机架构类似。 裸机物理服务器(BMS) 裸机服务器的官方定义是&#xff1a;裸机…

【嵌入式硬件芯片开发笔记】4-2 mADAC芯片AD5421配置流程

【嵌入式硬件芯片开发笔记】4-2 mADAC芯片AD5421配置流程 16位、串行输入、环路供电、4 mA至20 mA DAC 可用于HART协议相关电路 同AD5700配合使用 AD5421的SPI和普通的不一样 回读时要发两段 CS中间拉高一次 数据在SCLK上升沿逐个输出&#xff0c;而且在 SCLK下降沿有效 固CPO…

微服务框架 SpringCloud微服务架构 26 数据聚合 26.2 DSL 实现Bucket聚合

微服务框架 【SpringCloudRabbitMQDockerRedis搜索分布式&#xff0c;系统详解springcloud微服务技术栈课程|黑马程序员Java微服务】 SpringCloud微服务架构 文章目录微服务框架SpringCloud微服务架构26 数据聚合26.2 DSL 实现Bucket聚合26.2.1 DSL 实现Bucket聚合26.2.2 Buc…

【设计模式】享元模式(Flyweight Pattern)

享元模式属于结构型模式&#xff0c;主要解决系统需要使用大量相似对象&#xff08;细粒度对象&#xff09;而消耗大量内存资源的问题。享元模式运用共享技术有效地支持大量细粒度的对象&#xff0c;其通过提取对象共同的信息抽象出享元对象&#xff0c;实现共享功能&#xff0…

mysql详解之B树的查询时间复杂度

前言 本文是我在看了这篇文章之后&#xff08;这篇文章对b树的时间复杂度总结的很全面&#xff09;&#xff0c;关于B树&#xff08;或B树&#xff09;时间复杂度做的进一步思考&#xff08;如果对解题过程不感兴趣&#xff0c;可以直接看最后的总结&#xff09;。 正题 在这…

Java继承(extends)简明教程

继承是面向对象的三大特征之一。继承和现实生活中的“继承”的相似之处是保留一些父辈的特性&#xff0c;从而减少代码冗余&#xff0c;提高程序运行效率。 Java 中的继承就是在已经存在类的基础上进行扩展&#xff0c;从而产生新的类。已经存在的类称为父类、基类或超类&…

物联网和大数据可应用在哪些领域?

物联网和大数据可应用在哪些领域&#xff1f;物联网和大数据是近年来最受媒体和企业关注的两大宏观技术趋势。两者也并驾齐驱&#xff0c;物联网旨在特定组织或环境中创建一个互联网络&#xff0c;使用该网络来收集数据并集中执行特定功能。物联网部署会生成大量以前未开发的数…

【MR】C++ bullet客户端基于MR的动力学仿真

程序基于&#xff08;A minimal example showing how to use PyBullet from C over shared memory.一个展示如何在共享内存上从c使用PyBullet的最小示例。https://github.com/erwincoumans/pybullet_cpp_sharedmemory_example&#xff09;实现与pybullet服务器通信交互。加上MR…

MySql按条件插入数据,MySQL插入语句写where条件,MySQL在插入时做幂等

文章目录写在前面根据条件插入数据1、先准备测试数据2、正常的插入语句3、有条件的插入语句&#xff08;重点&#xff09;4、查看最终结果总结分析写在前面 使用MySQL的删、改、查功能时&#xff0c;我们都可以根据where条件来对指定数据进行操作。 插入语句如何通过where条件…

Git —— 关于创建多对非对称公钥时对不同服务器的匹配问题

Git —— 关于创建多对非对称密钥时对不同服务器的匹配问题 《工欲善其事&#xff0c;必先利其器》—— 既然点进来了&#xff0c;麻烦你看下去&#xff0c;希望你有不一样的收获~ 《 前言 》 大概是半年前&#xff0c;我写过一篇关于 git 连接托管平台提示 permission denied…

经典算法冒泡排序之标志位优化版

前言 今天总结一下优化版的经典算法——冒泡排序&#xff0c;不同于以往的暴力二重for循环&#xff0c;这里的冒泡排序增加了一个标志位。我们要理解该冒泡排序的概念&#xff0c;算法流程与算法思想&#xff0c;探讨时间复杂度。 冒泡排序算法解析 一、理解冒泡排序思想 1…