Skip to main content

pp_ocr 0.3.2 copy "pp_ocr: ^0.3.2" to clipboard
pp_ocr: ^0.3.2 copied to clipboard

Flutter plugin for offline OCR using PP-OCRv6 (PaddleOCR) with ONNX Runtime C++ inference. Supports text detection and recognition on Windows and Linux.

example/lib/main.dart

import 'dart:io';
import 'dart:math' as math;
import 'dart:ui' as ui;
import 'dart:developer' as developer;

import 'package:material_ui/material_ui.dart';
import 'package:pp_ocr/pp_ocr.dart';

/// 应用版本号(与根 pubspec.yaml 的 version 保持同步)。
const String kAppVersion = '0.3.2';

/// 应用程序入口函数
void main() {
  runApp(const OcrExampleApp());
}

/// OCR 示例应用根组件
class OcrExampleApp extends StatelessWidget {
  const OcrExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'PP-OCRv6 Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
      ),
      home: const OcrDemoPage(),
    );
  }
}

/// OCR 演示页面(有状态组件)
class OcrDemoPage extends StatefulWidget {
  const OcrDemoPage({super.key});

  @override
  State<OcrDemoPage> createState() => _OcrDemoPageState();
}

/// OCR 演示页面的状态管理类
class _OcrDemoPageState extends State<OcrDemoPage> {
  // PaddleOcr 引擎实例
  final PaddleOcr _ocr = PaddleOcr();

  // 模型路径配置 - 自动解析项目根目录下 model/ 文件夹的绝对路径,
  // 避免硬编码绝对路径,使项目可在任意路径(含中文)下运行。
  // 如模型位于其他位置,可在界面输入框中修改。
  final TextEditingController _detModelCtrl = TextEditingController(
    text: _modelPath('det.onnx'),
  );
  final TextEditingController _recModelCtrl = TextEditingController(
    text: _modelPath('inference.onnx'),
  );
  final TextEditingController _dictPathCtrl = TextEditingController(
    text: _modelPath('ppocr_v6_dict.txt'),
  );

  /// 解析模型文件的绝对路径。
  ///
  /// 按以下顺序探测,命中即用:
  /// 1. 上一级目录的 `model/`(工作目录为 example/ 或部署机 exe 目录时,
  ///    模型位于其上一级,如 `C:\flutter_build\model\det.onnx`);
  /// 2. 可执行文件同级的 `model/`(部署形态,如
  ///    `C:\flutter_build\pp_ocr_example\model\det.onnx`);
  /// 3. 都未命中时返回上一级形态路径作为默认值(可在界面修改)。
  static String _modelPath(String name) {
    final sep = Platform.isWindows ? r'\' : '/';
    final parent = File('${Directory.current.path}$sep..${sep}model$sep$name');
    if (parent.existsSync()) return parent.absolute.path;
    final exeDir = File(Platform.resolvedExecutable).parent.path;
    final besideExe = File('$exeDir${sep}model$sep$name');
    if (besideExe.existsSync()) return besideExe.absolute.path;
    return parent.absolute.path;
  }

  // OCR 引擎是否已初始化
  bool _initialized = false;
  // 是否正在执行加载/识别操作
  bool _loading = false;
  // 状态信息(显示在底部状态栏)
  String? _statusMessage;
  // 当前选择的图片路径
  String? _imagePath;
  // OCR 识别结果列表
  List<OcrResult> _results = [];
  // 原始图片宽度(用于精确绘制检测框)
  double _imageWidth = 0;
  // 原始图片高度(用于精确绘制检测框)
  double _imageHeight = 0;
  // 原生后端信息(FFI 时为 "pp_ocr x.y.z",MethodChannel 时为平台名)。
  // 与 kAppVersion 对照可发现 Dart 包与原生库版本错配(如部署残留旧 DLL)。
  String? _backendInfo;

  @override
  void initState() {
    super.initState();
    _checkModelFiles();
    _maybeStartSelfTest();
    _loadBackendInfo();
  }

  /// 获取原生后端版本信息(异步,不阻塞界面)。
  ///
  /// FFI 可用时 [PaddleOcr.getPlatformVersion] 返回原生库版本字符串
  /// (形如 "pp_ocr 0.3.2");FFI 不可用回退 MethodChannel 时返回
  /// 平台名("Windows 10+" / "Linux")。据此可判断当前生效的后端。
  Future<void> _loadBackendInfo() async {
    try {
      final v = await _ocr.getPlatformVersion();
      if (!mounted) return;
      setState(() => _backendInfo = (v == null || v.isEmpty) ? null : v);
    } catch (_) {
      // 获取失败不影响主流程;保持 null 即状态栏不显示。
    }
  }

  /// 自动化自检入口。
  ///
  /// 当环境变量 `PP_OCR_SELFTEST_IMAGE` 指向一个可读的图片时,
  /// 启动后自动执行 initialize → recognizeImage,完全跳过需要人工
  /// 交互的 pickImage 分支,便于无头环境复现问题。
  void _maybeStartSelfTest() {
    final selfTestImage = Platform.environment['PP_OCR_SELFTEST_IMAGE'];
    if (selfTestImage == null || selfTestImage.isEmpty) return;
    // ignore: avoid_print
    print('[SELFTEST] image=$selfTestImage');
    WidgetsBinding.instance.addPostFrameCallback((_) async {
      await _initOcr();
      await _runSelfTestOnImage(selfTestImage);
    });
  }

  Future<void> _runSelfTestOnImage(String path) async {
    // 可选:先走一遍 pickImage 路径,复现“dialog → respond → 后续 FFI
    // 调用”的完整时序(需搭配 PP_OCR_PICKIMAGE_FAKE_PATH)。默认不开启,
    // 避免 dialog 阻塞无 X server 的环境。
    final viaPick = Platform.environment['PP_OCR_SELFTEST_VIA_PICK'] == '1';
    try {
      if (viaPick) {
        // ignore: avoid_print
        print('[SELFTEST] pickImage step 0: _ocr.pickImage()');
        final picked = await _ocr.pickImage();
        // ignore: avoid_print
        print('[SELFTEST] pickImage step 0 done: "$picked"');
        if (picked == null || picked.isEmpty) {
          // ignore: avoid_print
          print('[SELFTEST] pickImage returned empty, abort self-test');
          return;
        }
      }

      // ignore: avoid_print
      print('[SELFTEST] step 1: stat file');
      final f = File(path);
      final stat = await f.stat();
      // ignore: avoid_print
      print('[SELFTEST] file size=${stat.size} bytes');

      // ignore: avoid_print
      print('[SELFTEST] step 2: readAsBytes');
      final bytes = await f.readAsBytes();
      // ignore: avoid_print
      print('[SELFTEST] read ${bytes.length} bytes');

      // ignore: avoid_print
      print('[SELFTEST] step 3: decodeImageFromList');
      final decoded = await decodeImageFromList(bytes);
      // ignore: avoid_print
      print('[SELFTEST] decoded ${decoded.width}x${decoded.height}');

      // ignore: avoid_print
      print('[SELFTEST] step 4: _ocr.recognizeImage');
      final results = await _ocr.recognizeImage(path);
      // ignore: avoid_print
      print('[SELFTEST] step 4 done: ${results.length} results');

      // ignore: avoid_print
      print('[SELFTEST] step 5: _ocr.recognizeImageBytes');
      final results2 = await _ocr.recognizeImageBytes(bytes);
      // ignore: avoid_print
      print('[SELFTEST] step 5 done: ${results2.length} results');

      setState(() {
        _imagePath = path;
        _imageWidth = decoded.width.toDouble();
        _imageHeight = decoded.height.toDouble();
        _results = results;
        _statusMessage =
            '[SELFTEST] OK: '
            'file=${bytes.length}B decoded=${decoded.width}x${decoded.height} '
            'results=${results.length}/${results2.length}';
      });
      // ignore: avoid_print
      print('[SELFTEST] ALL STEPS PASSED');
    } catch (e, st) {
      // ignore: avoid_print
      print('[SELFTEST] FAILED: $e');
      // ignore: avoid_print
      print(st);
      setState(() => _statusMessage = '[SELFTEST] FAILED: $e');
    }
  }

  /// 检查默认模型文件是否存在,缺失时在状态栏给出明确提示。
  void _checkModelFiles() {
    final paths = [
      _detModelCtrl.text,
      _recModelCtrl.text,
      _dictPathCtrl.text,
    ].map((p) => p.trim()).where((p) => p.isNotEmpty).toList();
    final missing = paths.where((p) => !File(p).existsSync()).map((p) {
      final idx = p.lastIndexOf(RegExp(r'[\\/]'));
      return idx > 0 ? p.substring(idx + 1) : p;
    }).toList();
    if (missing.isEmpty) return;
    _statusMessage =
        '未找到模型文件:${missing.join('、')}。'
        '请将 model 文件夹放到程序目录的上一级,'
        '或在上方输入框中修改路径。';
  }

  @override
  void dispose() {
    // 释放所有文本控制器
    _detModelCtrl.dispose();
    _recModelCtrl.dispose();
    _dictPathCtrl.dispose();
    super.dispose();
  }

  /// 初始化 OCR 引擎
  ///
  /// 使用用户配置的模型路径调用 [PaddleOcr.initialize],
  /// 成功后会将 [_initialized] 置为 true。
  Future<void> _initOcr() async {
    setState(() {
      _loading = true;
      _statusMessage = 'Initializing OCR engine...';
    });
    // 入口防呆:模型文件不存在时直接提示,不进入原生初始化。
    final missing = [
      _detModelCtrl.text.trim(),
      _recModelCtrl.text.trim(),
      _dictPathCtrl.text.trim(),
    ].where((p) => p.isNotEmpty && !File(p).existsSync()).toList();
    if (missing.isNotEmpty) {
      setState(() {
        _loading = false;
        _statusMessage = '初始化失败:模型文件不存在——${missing.join('\n')}';
      });
      return;
    }
    try {
      final ok = await _ocr.initialize(
        detModelPath: _detModelCtrl.text.trim(),
        recModelPath: _recModelCtrl.text.trim(),
        dictPath: _dictPathCtrl.text.trim(),
      );
      // 初始化后刷新原生后端信息(FFI 引擎此时才真正创建)。
      await _loadBackendInfo();
      setState(() {
        _initialized = ok;
        _statusMessage = ok
            ? 'OCR engine initialized successfully.'
            : 'Failed to initialize OCR engine. Check model paths.';
      });
    } catch (e, st) {
      // 记录初始化异常到开发者日志
      developer.log('Init error', error: e, stackTrace: st);
      setState(() {
        _statusMessage =
            'Initialization error: $e\n'
            'Type: ${e.runtimeType}\n'
            'Stack: ${st.toString().split('\n').take(5).join(' | ')}';
      });
    } finally {
      setState(() => _loading = false);
    }
  }

  /// 选择图片并执行 OCR 识别
  ///
  /// 流程:
  /// 1. 检查 OCR 引擎是否已初始化
  /// 2. 调用 [PaddleOcr.pickImage] 让用户选择图片
  /// 3. 解码图片获取原始尺寸(用于检测框叠加)
  /// 4. 调用 [PaddleOcr.recognizeImage] 执行识别
  Future<void> _pickAndRecognize() async {
    if (!_initialized) {
      // 未初始化时提示用户
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Please initialize OCR engine first')),
      );
      return;
    }

    setState(() {
      _loading = true;
      _statusMessage = 'Selecting image...';
      // 重置上次识别的状态
      _results = [];
      _imagePath = null;
      _imageWidth = 0;
      _imageHeight = 0;
    });

    try {
      // ignore: avoid_print
      print('[PICK] step A: await _ocr.pickImage()');
      final path = await _ocr.pickImage();
      // ignore: avoid_print
      print('[PICK] step A done: path="$path"');
      if (path == null || path.isEmpty) {
        // 用户未选择图片
        setState(() {
          _statusMessage = 'No image selected.';
          _loading = false;
        });
        return;
      }

      // ignore: avoid_print
      print('[PICK] step B: readAsBytes');
      // 加载图片尺寸,用于精确叠加检测框
      final imageFile = File(path);
      final bytes = await imageFile.readAsBytes();
      // ignore: avoid_print
      print('[PICK] step B done: ${bytes.length} bytes');

      // ignore: avoid_print
      print('[PICK] step C: decodeImageFromList');
      final decodedImage = await decodeImageFromList(bytes);
      // ignore: avoid_print
      print('[PICK] step C done: ${decodedImage.width}x${decodedImage.height}');

      setState(() {
        _imagePath = path;
        _imageWidth = decodedImage.width.toDouble();
        _imageHeight = decodedImage.height.toDouble();
        _statusMessage = 'Recognizing text... This may take a moment.';
      });

      // ignore: avoid_print
      print('[PICK] step D: _ocr.recognizeImage');
      // 执行 OCR 识别
      final results = await _ocr.recognizeImage(path);
      // ignore: avoid_print
      print('[PICK] step D done: ${results.length} results');

      setState(() {
        _results = results;
        _statusMessage =
            'Recognition complete. Found ${results.length} text region(s).';
        _loading = false;
      });
    } on FormatException catch (e) {
      // 处理 UTF-8 解码异常(通常来自原生层返回的无效数据)
      developer.log('FormatException in pickAndRecognize', error: e);
      setState(() {
        _statusMessage =
            'FormatException: ${e.message}\n'
            'Offset: ${e.offset}\n'
            'Type: ${e.runtimeType}\n'
            'This indicates invalid UTF-8 data from native code.\n'
            'Check native stderr logs for hex dumps.';
        _loading = false;
      });
    } catch (e, st) {
      // 处理其他未预期的异常
      developer.log('pickAndRecognize error', error: e, stackTrace: st);
      setState(() {
        _statusMessage =
            'Error: $e\n'
            'Type: ${e.runtimeType}\n'
            'Stack: ${st.toString().split('\n').take(5).join(' | ')}';
        _loading = false;
      });
    }
  }

  /// 状态栏常驻信息行:后端与版本、原生库版本、最近一次识别的图尺寸。
  String get _infoLine {
    final parts = <String>[
      'app v$kAppVersion',
      if (_backendInfo != null) 'native: $_backendInfo',
      if (_imageWidth > 0 && _imageHeight > 0)
        'image: ${_imageWidth.round()}x${_imageHeight.round()}',
    ];
    return parts.join('  |  ');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            const Text('PP-OCRv6 Offline OCR Demo'),
            const SizedBox(width: 12),
            Text(
              'v$kAppVersion',
              style: TextStyle(
                fontSize: 13,
                fontWeight: FontWeight.w500,
                color: Theme.of(context).colorScheme.onPrimaryContainer,
              ),
            ),
          ],
        ),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: Column(
        children: [
          // 模型配置面板
          _buildConfigPanel(),
          const Divider(height: 1),
          // 主内容区域
          Expanded(
            child: Row(
              children: [
                // 图片显示区域
                Expanded(flex: 3, child: _buildImagePanel()),
                const VerticalDivider(width: 1),
                // 识别结果列表
                Expanded(flex: 2, child: _buildResultsPanel()),
              ],
            ),
          ),
          // 底部状态栏:常驻信息行(后端/版本/图尺寸)+ 临时消息(如有)。
          // 后端信息与 Dart 包版本对照,可发现 Dart 与原生库版本错配。
          Container(
            width: double.infinity,
            constraints: const BoxConstraints(maxHeight: 140),
            padding: const EdgeInsets.all(8),
            color: Colors.grey[200],
            child: SingleChildScrollView(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisSize: MainAxisSize.min,
                children: [
                  SelectableText(
                    _infoLine,
                    style: const TextStyle(
                        fontSize: 12, fontFamily: 'monospace'),
                  ),
                  if (_statusMessage != null) ...[
                    const SizedBox(height: 4),
                    SelectableText(
                      _statusMessage!,
                      style: const TextStyle(
                          fontSize: 12, fontFamily: 'monospace'),
                    ),
                  ],
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }

  /// 构建模型配置面板
  ///
  /// 包含检测模型路径、识别模型路径、字典路径的输入框,
  /// 以及"初始化"和"选择图片并识别"两个按钮。
  Widget _buildConfigPanel() {
    return Padding(
      padding: const EdgeInsets.all(12),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text(
            'Model Configuration',
            style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
          ),
          const SizedBox(height: 8),
          Row(
            children: [
              // 检测模型路径输入框
              Expanded(
                child: TextField(
                  controller: _detModelCtrl,
                  decoration: const InputDecoration(
                    labelText: 'Det Model Path (.onnx)',
                    isDense: true,
                    border: OutlineInputBorder(),
                  ),
                  style: const TextStyle(fontSize: 12),
                ),
              ),
              const SizedBox(width: 8),
              // 识别模型路径输入框
              Expanded(
                child: TextField(
                  controller: _recModelCtrl,
                  decoration: const InputDecoration(
                    labelText: 'Rec Model Path (.onnx)',
                    isDense: true,
                    border: OutlineInputBorder(),
                  ),
                  style: const TextStyle(fontSize: 12),
                ),
              ),
              const SizedBox(width: 8),
              // 字典路径输入框
              Expanded(
                child: TextField(
                  controller: _dictPathCtrl,
                  decoration: const InputDecoration(
                    labelText: 'Dict Path (.txt)',
                    isDense: true,
                    border: OutlineInputBorder(),
                  ),
                  style: const TextStyle(fontSize: 12),
                ),
              ),
              const SizedBox(width: 8),
              // 初始化按钮(加载中显示进度指示器)
              ElevatedButton.icon(
                onPressed: _loading ? null : _initOcr,
                icon: _loading && !_initialized
                    ? const SizedBox(
                        width: 16,
                        height: 16,
                        child: CircularProgressIndicator(strokeWidth: 2),
                      )
                    : const Icon(Icons.settings),
                label: Text(_initialized ? 'Re-init' : 'Initialize'),
              ),
              const SizedBox(width: 8),
              // 选择图片并识别按钮(仅在已初始化且非加载状态时可用)
              ElevatedButton.icon(
                onPressed: _loading || !_initialized ? null : _pickAndRecognize,
                icon: const Icon(Icons.image_search),
                label: const Text('Pick & Recognize'),
              ),
            ],
          ),
        ],
      ),
    );
  }

  /// 构建图片显示面板
  ///
  /// 未选择图片时显示占位提示;
  /// 选择图片后显示图片,并通过 [OcrBoxPainter] 叠加绘制检测框。
  Widget _buildImagePanel() {
    if (_imagePath == null) {
      return const Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(Icons.image_outlined, size: 80, color: Colors.grey),
            SizedBox(height: 16),
            Text(
              'Click "Pick & Recognize" to select an image',
              style: TextStyle(color: Colors.grey),
            ),
          ],
        ),
      );
    }

    return Column(
      children: [
        Expanded(
          child: Padding(
            padding: const EdgeInsets.all(8),
            child: Stack(
              children: [
                // 显示选中的图片
                Center(
                  child: Image.file(File(_imagePath!), fit: BoxFit.contain),
                ),
                // 检测框叠加层
                if (_results.isNotEmpty)
                  Positioned.fill(
                    child: CustomPaint(
                      painter: OcrBoxPainter(
                        imagePath: _imagePath!,
                        results: _results,
                        imageWidth: _imageWidth,
                        imageHeight: _imageHeight,
                      ),
                    ),
                  ),
              ],
            ),
          ),
        ),
      ],
    );
  }

  /// 构建识别结果列表面板
  ///
  /// 显示所有识别到的文本区域,每项包含序号、文本内容和置信度。
  Widget _buildResultsPanel() {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Padding(
          padding: const EdgeInsets.all(12),
          child: Text(
            'Recognition Results (${_results.length})',
            style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
          ),
        ),
        Expanded(
          child: _results.isEmpty
              ? const Center(
                  child: Text(
                    'No results yet',
                    style: TextStyle(color: Colors.grey),
                  ),
                )
              : ListView.builder(
                  itemCount: _results.length,
                  padding: const EdgeInsets.symmetric(horizontal: 8),
                  itemBuilder: (context, index) {
                    final r = _results[index];
                    return Card(
                      margin: const EdgeInsets.symmetric(vertical: 2),
                      child: ListTile(
                        dense: true,
                        // 序号头像
                        leading: CircleAvatar(
                          radius: 14,
                          child: Text(
                            '${index + 1}',
                            style: const TextStyle(fontSize: 11),
                          ),
                        ),
                        // 识别文本
                        title: Text(
                          r.text,
                          style: const TextStyle(fontSize: 13),
                        ),
                        // 置信度(百分比形式)
                        subtitle: Text(
                          'Confidence: ${(r.confidence * 100).toStringAsFixed(1)}%',
                          style: const TextStyle(fontSize: 11),
                        ),
                      ),
                    );
                  },
                ),
        ),
      ],
    );
  }
}

/// 自定义画笔,用于在图片上绘制 OCR 检测框
///
/// 根据 [imageWidth] 和 [imageHeight] 将检测框坐标从图片空间
/// 转换到画布空间,确保检测框与显示的图片精确对齐。
class OcrBoxPainter extends CustomPainter {
  /// 图片路径
  final String imagePath;

  /// OCR 识别结果列表
  final List<OcrResult> results;

  /// 原始图片宽度
  final double imageWidth;

  /// 原始图片高度
  final double imageHeight;

  OcrBoxPainter({
    required this.imagePath,
    required this.results,
    required this.imageWidth,
    required this.imageHeight,
  });

  @override
  void paint(Canvas canvas, Size size) {
    if (imageWidth <= 0 || imageHeight <= 0) return;

    // 检测框画笔配置:红色半透明描边
    final paint = Paint()
      ..color = Colors.red.withValues(alpha: 0.7)
      ..style = PaintingStyle.stroke
      ..strokeWidth = 2.0;

    // 计算将图片等比缩放到画布的缩放比例。
    //
    // 图片的实际显示控件是 Center(child: Image.file(fit: BoxFit.contain))。
    // RenderImage 在宽松约束下通过 constrainSizeAndAttemptToPreserveAspectRatio
    // 计算显示尺寸——只会把原图"夹小"到约束内,**不会放大**小于面板的图。
    // 因此这里的 scale 必须加上 1.0 上限,与显示行为严格一致:
    //   * 图 >= 面板:scale = min(sx, sy)(等比缩小铺满,与 contain 一致)
    //   * 图 <  面板:scale = 1(原图尺寸显示,框也 1:1 映射)
    // 否则小图会被此公式"虚拟放大",框整体溢出图片显示区(画框错位 bug)。
    final double scaleX = size.width / imageWidth;
    final double scaleY = size.height / imageHeight;
    final double scale =
        math.min(1.0, math.min(scaleX, scaleY));

    // 实际显示的图片尺寸(宽高比不同时会出现留白)
    final double displayedW = imageWidth * scale;
    final double displayedH = imageHeight * scale;
    // 图片在画布中的居中偏移量
    final double offsetX = (size.width - displayedW) / 2;
    final double offsetY = (size.height - displayedH) / 2;

    // 文本标签绘制器
    final textPainter = TextPainter(textDirection: TextDirection.ltr);

    for (final result in results) {
      // 检测框至少需要 4 个点
      if (result.box.length < 4) continue;

      // 将检测框坐标从图片空间转换到画布空间
      final path = ui.Path();
      path.moveTo(
        offsetX + result.box[0].dx * scale,
        offsetY + result.box[0].dy * scale,
      );
      for (int i = 1; i < result.box.length; i++) {
        path.lineTo(
          offsetX + result.box[i].dx * scale,
          offsetY + result.box[i].dy * scale,
        );
      }
      path.close();
      canvas.drawPath(path, paint);

      // 绘制文本标签(位于检测框左上角上方)
      textPainter.text = TextSpan(
        text: result.text,
        style: const TextStyle(
          color: Colors.red,
          fontSize: 10,
          backgroundColor: Colors.white70,
        ),
      );
      textPainter.layout(maxWidth: size.width);
      textPainter.paint(
        canvas,
        Offset(
          offsetX + result.box[0].dx * scale,
          offsetY + result.box[0].dy * scale - 14,
        ),
      );
    }
  }

  @override
  bool shouldRepaint(covariant OcrBoxPainter oldDelegate) {
    // 当结果列表、图片宽高发生变化时需要重绘
    return oldDelegate.results != results ||
        oldDelegate.imageWidth != imageWidth ||
        oldDelegate.imageHeight != imageHeight;
  }
}
2
likes
0
points
445
downloads

Publisher

unverified uploader

Weekly Downloads

Flutter plugin for offline OCR using PP-OCRv6 (PaddleOCR) with ONNX Runtime C++ inference. Supports text detection and recognition on Windows and Linux.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

ffi, flutter, plugin_platform_interface

More

Packages that depend on pp_ocr

Packages that implement pp_ocr