diff --git a/.gitattributes b/.gitattributes
index d93e4fa4f07b5ef94c94cd9ba1fa4e3486514f6f..eb41d37bed3d222f966bb43b667b0756d55e7c07 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -49,3 +49,47 @@ ocr_webapp/PaddleOCR/deploy/cpp_infer/docs/imgs/cmake_step3.png filter=lfs diff=
ocr_webapp/PaddleOCR/deploy/cpp_infer/docs/imgs/cmake_step4.png filter=lfs diff=lfs merge=lfs -text
ocr_webapp/PaddleOCR/deploy/cpp_infer/docs/imgs/result.jpg filter=lfs diff=lfs merge=lfs -text
ocr_webapp/PaddleOCR/deploy/cpp_infer/docs/imgs/vs_step1.png filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/SampleImages/general_ocr_002.jpg filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/Fixtures/ios_ocr_validation_reference.jpg filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/deploy/Jetson/images/00057937.jpg filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/deploy/Jetson/images/det_res_french_0.jpg filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/deploy/paddle2onnx/images/lite_demo_onnx.png filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/deploy/paddle2onnx/images/lite_demo_paddle.png filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/deploy/paddlecloud/images/architecture.jpeg filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/deploy/paddlecloud/images/paddlecloud.png filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/deploy/paddlejs/paddlejs_demo.gif filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/deploy/pdserving/imgs/demo.png filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/deploy/pdserving/imgs/pipeline_result.png filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/deploy/pdserving/imgs/results.png filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/deploy/pdserving/imgs/start_server.png filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/deploy/ppocr-android/ppocr-sdk/src/androidTest/res/raw/android_ocr_benchmark_reference.png filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/doc/banner.png filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/doc/datasets/ArT.jpg filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/doc/datasets/captcha_demo.png filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/doc/datasets/ccpd_demo.png filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/doc/datasets/CDLA_demo/val_0633.jpg filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/doc/datasets/CDLA_demo/val_0941.jpg filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/doc/datasets/ch_street_rec_1.png filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/doc/datasets/ch_street_rec_2.png filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/doc/datasets/doc.jpg filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/doc/datasets/funsd_demo/gt_train_00040534.jpg filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/doc/datasets/funsd_demo/gt_train_00070353.jpg filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/doc/datasets/icdar_rec.png filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/doc/datasets/labelme.jpg filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/doc/datasets/LSVT_1.jpg filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/doc/datasets/publaynet_demo/gt_PMC3724501_00006.jpg filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/doc/datasets/publaynet_demo/gt_PMC5086060_00002.jpg filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/doc/datasets/roLabelImg.png filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/doc/datasets/table_tal_demo/1.jpg filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/doc/datasets/table_tal_demo/2.jpg filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/doc/datasets/tablebank_demo/004.png filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/doc/datasets/tablebank_demo/005.png filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/doc/datasets/VoTT.jpg filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/doc/datasets/wildreceipt_demo/1bbe854b8817dedb8585e0732089fd1f752d2cec.jpeg filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/doc/datasets/wildreceipt_demo/2769.jpeg filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/doc/datasets/xfund_demo/gt_zh_train_0.jpg filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/doc/datasets/xfund_demo/gt_zh_train_1.jpg filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/doc/demo/error.png filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/doc/demo/proxy.png filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/doc/deployment.png filter=lfs diff=lfs merge=lfs -text
+ocr_webapp/PaddleOCR/doc/deployment_en.png filter=lfs diff=lfs merge=lfs -text
diff --git a/ocr_webapp/PaddleOCR/deploy/Jetson/images/00057937.jpg b/ocr_webapp/PaddleOCR/deploy/Jetson/images/00057937.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..461e09232ff89473cefae3416a5d41aa7b7ff4bb
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/Jetson/images/00057937.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c6132df199f3a548df8cccf512cbeaeb8533a2610798627b19bb2cc2a1ffca9f
+size 292585
diff --git a/ocr_webapp/PaddleOCR/deploy/Jetson/images/det_res_french_0.jpg b/ocr_webapp/PaddleOCR/deploy/Jetson/images/det_res_french_0.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..4b6e951df8e18563b0078244428bb6eb6367e8c2
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/Jetson/images/det_res_french_0.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:777690a87047fcf90f0bd5c520853fe6d75deb4bd0bbb68d6a36ceb2d33de875
+size 223382
diff --git a/ocr_webapp/PaddleOCR/deploy/Jetson/readme.md b/ocr_webapp/PaddleOCR/deploy/Jetson/readme.md
new file mode 100644
index 0000000000000000000000000000000000000000..14c88c3c3c8aa8e4bbfe6ce04cb51c58af762351
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/Jetson/readme.md
@@ -0,0 +1,84 @@
+English | [简体中文](readme_ch.md)
+
+# Jetson Deployment for PaddleOCR
+
+This section introduces the deployment of PaddleOCR on Jetson NX, TX2, nano, AGX and other series of hardware.
+
+
+## 1. Prepare Environment
+
+You need to prepare a Jetson development hardware. If you need TensorRT, you need to prepare the TensorRT environment. It is recommended to use TensorRT version 7.1.3;
+
+1. Install PaddlePaddle in Jetson
+
+The PaddlePaddle download [link](https://www.paddlepaddle.org.cn/inference/user_guides/download_lib.html#python)
+Please select the appropriate installation package for your Jetpack version, cuda version, and trt version. Here, we download paddlepaddle_gpu-2.3.0rc0-cp36-cp36m-linux_aarch64.whl.
+
+Install PaddlePaddle:
+```shell
+pip3 install -U paddlepaddle_gpu-2.3.0rc0-cp36-cp36m-linux_aarch64.whl
+```
+
+
+2. Download PaddleOCR code and install dependencies
+
+Clone the PaddleOCR code:
+```
+git clone https://github.com/PaddlePaddle/PaddleOCR
+```
+
+and install dependencies:
+```
+cd PaddleOCR
+pip3 install -r requirements.txt
+```
+
+*Note: Jetson hardware CPU is poor, dependency installation is slow, please wait patiently*
+
+## 2. Perform prediction
+
+Obtain the PPOCR model from the [document](https://github.com/PaddlePaddle/PaddleOCR/blob/dygraph/doc/doc_en/ppocr_introduction_en.md#6-model-zoo) model library. The following takes the PP-OCRv3 model as an example to introduce the use of the PPOCR model on Jetson:
+
+Download and unzip the PP-OCRv3 models.
+```
+wget https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/ch_PP-OCRv3_det_infer.tar
+wget https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/ch_PP-OCRv3_rec_infer.tar
+tar xf ch_PP-OCRv3_det_infer.tar
+tar xf ch_PP-OCRv3_rec_infer.tar
+```
+
+The text detection inference:
+```
+cd PaddleOCR
+python3 tools/infer/predict_det.py --det_model_dir=./inference/ch_PP-OCRv2_det_infer/ --image_dir=./doc/imgs/french_0.jpg --use_gpu=True
+```
+
+After executing the command, the predicted information will be printed out in the terminal, and the visualization results will be saved in the `./inference_results/` directory.
+
+
+
+The text recognition inference:
+```
+python3 tools/infer/predict_det.py --rec_model_dir=./inference/ch_PP-OCRv2_rec_infer/ --image_dir=./doc/imgs_words/en/word_2.png --use_gpu=True --rec_image_shape="3,48,320"
+```
+
+After executing the command, the predicted information will be printed on the terminal, and the output is as follows:
+```
+[2022/04/28 15:41:45] root INFO: Predicts of ./doc/imgs_words/en/word_2.png:('yourself', 0.98084533)
+```
+
+The text detection and text recognition inference:
+
+```
+python3 tools/infer/predict_system.py --det_model_dir=./inference/ch_PP-OCRv2_det_infer/ --rec_model_dir=./inference/ch_PP-OCRv2_rec_infer/ --image_dir=./doc/imgs/00057937.jpg --use_gpu=True --rec_image_shape="3,48,320"
+```
+
+After executing the command, the predicted information will be printed out in the terminal, and the visualization results will be saved in the `./inference_results/` directory.
+
+
+To enable TRT prediction, you only need to set `--use_tensorrt=True` on the basis of the above command:
+```
+python3 tools/infer/predict_system.py --det_model_dir=./inference/ch_PP-OCRv2_det_infer/ --rec_model_dir=./inference/ch_PP-OCRv2_rec_infer/ --image_dir=./doc/imgs/ --rec_image_shape="3,48,320" --use_gpu=True --use_tensorrt=True
+```
+
+For more ppocr model predictions, please refer to[document](../../doc/doc_en/models_list_en.md)
diff --git a/ocr_webapp/PaddleOCR/deploy/Jetson/readme_ch.md b/ocr_webapp/PaddleOCR/deploy/Jetson/readme_ch.md
new file mode 100644
index 0000000000000000000000000000000000000000..7b0a344ffe3b4c0abefc78e50371b5c92aa25001
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/Jetson/readme_ch.md
@@ -0,0 +1,86 @@
+[English](readme.md) | 简体中文
+
+# Jetson部署PaddleOCR模型
+
+本节介绍PaddleOCR在Jetson NX、TX2、nano、AGX等系列硬件的部署。
+
+
+## 1. 环境准备
+
+需要准备一台Jetson开发板,如果需要TensorRT预测,需准备好TensorRT环境,建议使用7.1.3版本的TensorRT;
+
+1. Jetson安装PaddlePaddle
+
+PaddlePaddle下载[链接](https://www.paddlepaddle.org.cn/inference/user_guides/download_lib.html#python)
+请选择适合的您Jetpack版本、cuda版本、trt版本的安装包。
+
+安装命令:
+```shell
+# 安装paddle,以paddlepaddle_gpu-2.3.0rc0-cp36-cp36m-linux_aarch64.whl 为例
+pip3 install -U paddlepaddle_gpu-2.3.0rc0-cp36-cp36m-linux_aarch64.whl
+```
+
+
+2. 下载PaddleOCR代码并安装依赖
+
+首先 clone PaddleOCR 代码:
+```
+git clone https://github.com/PaddlePaddle/PaddleOCR
+```
+
+然后,安装依赖:
+```
+cd PaddleOCR
+pip3 install -r requirements.txt
+```
+
+*注:jetson硬件CPU较差,依赖安装较慢,请耐心等待*
+
+
+## 2. 执行预测
+
+从[文档](https://github.com/PaddlePaddle/PaddleOCR/blob/dygraph/doc/doc_ch/ppocr_introduction.md#6-%E6%A8%A1%E5%9E%8B%E5%BA%93) 模型库中获取PPOCR模型,下面以PP-OCRv3模型为例,介绍在PPOCR模型在jetson上的使用方式:
+
+下载并解压PP-OCRv3模型
+```
+wget https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/ch_PP-OCRv3_det_infer.tar
+wget https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/ch_PP-OCRv3_rec_infer.tar
+tar xf ch_PP-OCRv3_det_infer.tar
+tar xf ch_PP-OCRv3_rec_infer.tar
+```
+
+执行文本检测预测:
+```
+cd PaddleOCR
+python3 tools/infer/predict_det.py --det_model_dir=./inference/ch_PP-OCRv2_det_infer/ --image_dir=./doc/imgs/french_0.jpg --use_gpu=True
+```
+
+执行命令后在终端会打印出预测的信息,并在 `./inference_results/` 下保存可视化结果。
+
+
+
+执行文本识别预测:
+```
+python3 tools/infer/predict_det.py --rec_model_dir=./inference/ch_PP-OCRv2_rec_infer/ --image_dir=./doc/imgs_words/en/word_2.png --use_gpu=True --rec_image_shape="3,48,320"
+```
+
+执行命令后在终端会打印出预测的信息,输出如下:
+```
+[2022/04/28 15:41:45] root INFO: Predicts of ./doc/imgs_words/en/word_2.png:('yourself', 0.98084533)
+```
+
+执行文本检测+文本识别串联预测:
+
+```
+python3 tools/infer/predict_system.py --det_model_dir=./inference/ch_PP-OCRv2_det_infer/ --rec_model_dir=./inference/ch_PP-OCRv2_rec_infer/ --image_dir=./doc/imgs/ --use_gpu=True --rec_image_shape="3,48,320"
+```
+
+执行命令后在终端会打印出预测的信息,并在 `./inference_results/` 下保存可视化结果。
+
+
+开启TRT预测只需要在以上命令基础上设置`--use_tensorrt=True`即可:
+```
+python3 tools/infer/predict_system.py --det_model_dir=./inference/ch_PP-OCRv2_det_infer/ --rec_model_dir=./inference/ch_PP-OCRv2_rec_infer/ --image_dir=./doc/imgs/00057937.jpg --use_gpu=True --use_tensorrt=True --rec_image_shape="3,48,320"
+```
+
+更多ppocr模型预测请参考[文档](../../doc/doc_ch/models_list.md)
diff --git a/ocr_webapp/PaddleOCR/deploy/README.md b/ocr_webapp/PaddleOCR/deploy/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..0cfb793f92547de1215277f5e2348bc157ec8503
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/README.md
@@ -0,0 +1,31 @@
+English | [简体中文](README_ch.md)
+
+# PP-OCR Deployment
+
+- [Paddle Deployment Introduction](#1)
+- [PP-OCR Deployment](#2)
+
+
+## Paddle Deployment Introduction
+
+Paddle provides a variety of deployment schemes to meet the deployment requirements of different scenarios. Please choose according to the actual situation:
+
+
+

+
+
+
+
+## PP-OCR Deployment
+
+PP-OCR has supported muti deployment schemes. Click the link to get the specific tutorial.
+
+- [Python Inference](../doc/doc_en/inference_ppocr_en.md)
+- [C++ Inference](./cpp_infer/readme.md)
+- [Serving (Python/C++)](./pdserving/README.md)
+- [Paddle-Lite (ARM CPU/OpenCL ARM GPU)](./lite/readme.md)
+- [Paddle.js](./paddlejs/README.md)
+- [Jetson Inference](https://github.com/PaddlePaddle/PaddleOCR/blob/dygraph/deploy/Jetson/readme.md)
+- [Paddle2ONNX](./paddle2onnx/readme.md)
+
+If you need the deployment tutorial of academic algorithm models other than PP-OCR, please directly enter the main page of corresponding algorithms, [entrance](../doc/doc_en/algorithm_overview_en.md)。
diff --git a/ocr_webapp/PaddleOCR/deploy/README_ch.md b/ocr_webapp/PaddleOCR/deploy/README_ch.md
new file mode 100644
index 0000000000000000000000000000000000000000..1773aedc2c757499bfc1f950e468a98c477636ac
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/README_ch.md
@@ -0,0 +1,31 @@
+[English](README.md) | 简体中文
+
+# PP-OCR 模型推理部署
+
+- [Paddle 推理部署方式简介](#1)
+- [PP-OCR 推理部署](#2)
+
+
+## Paddle 推理部署方式简介
+
+飞桨提供多种部署方案,以满足不同场景的部署需求,请根据实际情况进行选择:
+
+
+

+
+
+
+
+## PP-OCR 推理部署
+
+PP-OCR模型已打通多种场景部署方案,点击链接获取具体的使用教程。
+
+- [Python 推理](../doc/doc_ch/inference_ppocr.md)
+- [C++ 推理](./cpp_infer/readme_ch.md)
+- [Serving 服务化部署(Python/C++)](./pdserving/README_CN.md)
+- [Paddle-Lite 端侧部署(ARM CPU/OpenCL ARM GPU)](./lite/readme_ch.md)
+- [Paddle.js 部署](./paddlejs/README_ch.md)
+- [Jetson 推理](https://github.com/PaddlePaddle/PaddleOCR/blob/dygraph/deploy/Jetson/readme_ch.md)
+- [Paddle2ONNX 推理](./paddle2onnx/readme_ch.md)
+
+需要PP-OCR以外的学术算法模型的推理部署,请直接进入相应算法主页面,[入口](../doc/doc_ch/algorithm_overview.md)。
\ No newline at end of file
diff --git a/ocr_webapp/PaddleOCR/deploy/fastdeploy/sophgo/README.md b/ocr_webapp/PaddleOCR/deploy/fastdeploy/sophgo/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..9fd2e9563f48263cda37e98a6b4cf49ec5d53b4f
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/fastdeploy/sophgo/README.md
@@ -0,0 +1,102 @@
+[English](README.md) | 简体中文
+
+# PaddleOCR 模型在SOPHGO上部署方案-FastDeploy
+
+## 1. 说明
+PaddleOCR支持通过FastDeploy在SOPHGO上部署相关模型.
+
+## 2.支持模型列表
+
+下表中的模型下载链接由PaddleOCR模型库提供, 详见[PP-OCR系列模型列表](https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.6/doc/doc_ch/models_list.md)
+
+| PaddleOCR版本 | 文本框检测 | 方向分类模型 | 文字识别 |字典文件| 说明 |
+|:----|:----|:----|:----|:----|:--------|
+| ch_PP-OCRv3[推荐] |[ch_PP-OCRv3_det](https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/ch_PP-OCRv3_det_infer.tar) | [ch_ppocr_mobile_v2.0_cls](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_infer.tar) | [ch_PP-OCRv3_rec](https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/ch_PP-OCRv3_rec_infer.tar) | [ppocr_keys_v1.txt](https://bj.bcebos.com/paddlehub/fastdeploy/ppocr_keys_v1.txt) | OCRv3系列原始超轻量模型,支持中英文、多语种文本检测 |
+| en_PP-OCRv3[推荐] |[en_PP-OCRv3_det](https://paddleocr.bj.bcebos.com/PP-OCRv3/english/en_PP-OCRv3_det_infer.tar) | [ch_ppocr_mobile_v2.0_cls](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_infer.tar) | [en_PP-OCRv3_rec](https://paddleocr.bj.bcebos.com/PP-OCRv3/english/en_PP-OCRv3_rec_infer.tar) | [en_dict.txt](https://bj.bcebos.com/paddlehub/fastdeploy/en_dict.txt) | OCRv3系列原始超轻量模型,支持英文与数字识别,除检测模型和识别模型的训练数据与中文模型不同以外,无其他区别 |
+| ch_PP-OCRv2 |[ch_PP-OCRv2_det](https://paddleocr.bj.bcebos.com/PP-OCRv2/chinese/ch_PP-OCRv2_det_infer.tar) | [ch_ppocr_mobile_v2.0_cls](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_infer.tar) | [ch_PP-OCRv2_rec](https://paddleocr.bj.bcebos.com/PP-OCRv2/chinese/ch_PP-OCRv2_rec_infer.tar) | [ppocr_keys_v1.txt](https://bj.bcebos.com/paddlehub/fastdeploy/ppocr_keys_v1.txt) | OCRv2系列原始超轻量模型,支持中英文、多语种文本检测 |
+| ch_PP-OCRv2_mobile |[ch_ppocr_mobile_v2.0_det](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_det_infer.tar) | [ch_ppocr_mobile_v2.0_cls](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_infer.tar) | [ch_ppocr_mobile_v2.0_rec](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_rec_infer.tar) | [ppocr_keys_v1.txt](https://bj.bcebos.com/paddlehub/fastdeploy/ppocr_keys_v1.txt) | OCRv2系列原始超轻量模型,支持中英文、多语种文本检测,比PPOCRv2更加轻量 |
+| ch_PP-OCRv2_server |[ch_ppocr_server_v2.0_det](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_server_v2.0_det_infer.tar) | [ch_ppocr_mobile_v2.0_cls](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_infer.tar) | [ch_ppocr_server_v2.0_rec](https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_server_v2.0_rec_infer.tar) |[ppocr_keys_v1.txt](https://bj.bcebos.com/paddlehub/fastdeploy/ppocr_keys_v1.txt) | OCRv2服务器系列模型, 支持中英文、多语种文本检测,比超轻量模型更大,但效果更好|
+
+## 3. 准备PP-OCR推理模型以及转换模型
+
+PP-OCRv3包括文本检测模型(ch_PP-OCRv3_det)、方向分类模型(ch_ppocr_mobile_v2.0_cls)、文字识别模型(ch_PP-OCRv3_rec)
+SOPHGO-TPU部署模型前需要将以上Paddle模型转换成bmodel模型,我们以ch_PP-OCRv3_det模型为例,具体步骤如下:
+- 下载Paddle模型[ch_PP-OCRv3_det](https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/ch_PP-OCRv3_det_infer.tar)
+- Pddle模型转换为ONNX模型,请参考[Paddle2ONNX](https://github.com/PaddlePaddle/Paddle2ONNX)
+- ONNX模型转换bmodel模型的过程,请参考[TPU-MLIR](https://github.com/sophgo/tpu-mlir)
+下面我们提供一个example, 供用户参考,完成模型的转换.
+
+### 3.1 下载ch_PP-OCRv3_det模型,并转换为ONNX模型
+```shell
+wget https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/ch_PP-OCRv3_det_infer.tar
+tar xvf ch_PP-OCRv3_det_infer.tar
+
+# 修改ch_PP-OCRv3_det模型的输入shape,由动态输入变成固定输入
+python paddle_infer_shape.py --model_dir ch_PP-OCRv3_det_infer \
+ --model_filename inference.pdmodel \
+ --params_filename inference.pdiparams \
+ --save_dir ch_PP-OCRv3_det_infer_fix \
+ --input_shape_dict="{'x':[1,3,960,608]}"
+
+# 请用户自行安装最新发布版本的paddle2onnx, 转换模型到ONNX格式的模型
+paddle2onnx --model_dir ch_PP-OCRv3_det_infer_fix \
+ --model_filename inference.pdmodel \
+ --params_filename inference.pdiparams \
+ --save_file ch_PP-OCRv3_det_infer_fix.onnx \
+ --enable_dev_version True
+```
+
+### 3.2 导出bmodel模型
+
+以转换BM1684x的bmodel模型为例子,我们需要下载[TPU-MLIR](https://github.com/sophgo/tpu-mlir)工程,安装过程具体参见[TPU-MLIR文档](https://github.com/sophgo/tpu-mlir/blob/master/README.md)。
+#### 3.2.1 安装
+``` shell
+docker pull sophgo/tpuc_dev:latest
+
+# myname1234是一个示例,也可以设置其他名字
+docker run --privileged --name myname1234 -v $PWD:/workspace -it sophgo/tpuc_dev:latest
+
+source ./envsetup.sh
+./build.sh
+```
+
+#### 3.2.2 ONNX模型转换为bmodel模型
+``` shell
+mkdir ch_PP-OCRv3_det && cd ch_PP-OCRv3_det
+
+#在该文件中放入测试图片,同时将上一步转换的ch_PP-OCRv3_det_infer_fix.onnx放入该文件夹中
+cp -rf ${REGRESSION_PATH}/dataset/COCO2017 .
+cp -rf ${REGRESSION_PATH}/image .
+#放入onnx模型文件ch_PP-OCRv3_det_infer_fix.onnx
+
+mkdir workspace && cd workspace
+
+#将ONNX模型转换为mlir模型,其中参数--output_names可以通过NETRON查看
+model_transform.py \
+ --model_name ch_PP-OCRv3_det \
+ --model_def ../ch_PP-OCRv3_det_infer_fix.onnx \
+ --input_shapes [[1,3,960,608]] \
+ --mean 0.0,0.0,0.0 \
+ --scale 0.0039216,0.0039216,0.0039216 \
+ --keep_aspect_ratio \
+ --pixel_format rgb \
+ --output_names sigmoid_0.tmp_0 \
+ --test_input ../image/dog.jpg \
+ --test_result ch_PP-OCRv3_det_top_outputs.npz \
+ --mlir ch_PP-OCRv3_det.mlir
+
+#将mlir模型转换为BM1684x的F32 bmodel模型
+model_deploy.py \
+ --mlir ch_PP-OCRv3_det.mlir \
+ --quantize F32 \
+ --chip bm1684x \
+ --test_input ch_PP-OCRv3_det_in_f32.npz \
+ --test_reference ch_PP-OCRv3_det_top_outputs.npz \
+ --model ch_PP-OCRv3_det_1684x_f32.bmodel
+```
+最终获得可以在BM1684x上能够运行的bmodel模型ch_PP-OCRv3_det_1684x_f32.bmodel。按照上面同样的方法,可以将ch_ppocr_mobile_v2.0_cls,ch_PP-OCRv3_rec转换为bmodel的格式。如果需要进一步对模型进行加速,可以将ONNX模型转换为INT8 bmodel,具体步骤参见[TPU-MLIR文档](https://github.com/sophgo/tpu-mlir/blob/master/README.md)。
+
+
+## 4. 详细部署的部署示例
+- [Python部署](python)
+- [C++部署](cpp)
diff --git a/ocr_webapp/PaddleOCR/deploy/fastdeploy/sophgo/python/README.md b/ocr_webapp/PaddleOCR/deploy/fastdeploy/sophgo/python/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..27dbe2694cd7ca71b9f16960937fde8564a5b4d4
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/fastdeploy/sophgo/python/README.md
@@ -0,0 +1,52 @@
+[English](README.md) | 简体中文
+# PP-OCRv3 SOPHGO Python部署示例
+本目录下提供`infer.py`快速完成 PP-OCRv3 在SOPHGO TPU上部署的示例。
+
+## 1. 部署环境准备
+
+在部署前,需自行编译基于算能硬件的FastDeploy python wheel包并安装,参考文档[算能硬件部署环境](https://github.com/PaddlePaddle/FastDeploy/blob/develop/docs/cn/build_and_install#算能硬件部署环境)
+
+
+## 2.运行部署示例
+
+### 2.1 模型准备
+将Paddle模型转换为SOPHGO bmodel模型, 转换步骤参考[文档](../README.md)
+
+### 2.2 开始部署
+```bash
+# 下载部署示例代码
+git clone https://github.com/PaddlePaddle/FastDeploy.git
+cd FastDeploy/examples/vision/ocr/PP-OCR/sophgo/python
+
+# 如果您希望从PaddleOCR下载示例代码,请运行
+git clone https://github.com/PaddlePaddle/PaddleOCR.git
+# 注意:如果当前分支找不到下面的fastdeploy测试代码,请切换到dygraph分支
+git checkout dygraph
+cd PaddleOCR/deploy/fastdeploy/sophgo/python
+
+# 下载图片
+wget https://gitee.com/paddlepaddle/PaddleOCR/raw/release/2.6/doc/imgs/12.jpg
+
+#下载字典文件
+wget https://gitee.com/paddlepaddle/PaddleOCR/raw/release/2.6/ppocr/utils/ppocr_keys_v1.txt
+
+# 推理
+python3 infer.py --det_model ocr_bmodel/ch_PP-OCRv3_det_1684x_f32.bmodel \
+ --cls_model ocr_bmodel/ch_ppocr_mobile_v2.0_cls_1684x_f32.bmodel \
+ --rec_model ocr_bmodel/ch_PP-OCRv3_rec_1684x_f32.bmodel \
+ --rec_label_file ../ppocr_keys_v1.txt \
+ --image ../12.jpg
+
+# 运行完成后返回结果如下所示
+det boxes: [[42,413],[483,391],[484,428],[43,450]]rec text: 上海斯格威铂尔大酒店 rec score:0.952958 cls label: 0 cls score: 1.000000
+det boxes: [[187,456],[399,448],[400,480],[188,488]]rec text: 打浦路15号 rec score:0.897335 cls label: 0 cls score: 1.000000
+det boxes: [[23,507],[513,488],[515,529],[24,548]]rec text: 绿洲仕格维花园公寓 rec score:0.994589 cls label: 0 cls score: 1.000000
+det boxes: [[74,553],[427,542],[428,571],[75,582]]rec text: 打浦路252935号 rec score:0.900663 cls label: 0 cls score: 1.000000
+
+可视化结果保存在sophgo_result.jpg中
+```
+
+## 3. 其它文档
+- [PP-OCRv3 C++部署](../cpp)
+- [转换 PP-OCRv3 SOPHGO模型文档](../README.md)
+- 如果用户想要调整前后处理超参数、单独使用文字检测识别模型、使用其他模型等,更多详细文档与说明请参考[PP-OCR系列在CPU/GPU上的部署](../../cpu-gpu/cpp/README.md)
diff --git a/ocr_webapp/PaddleOCR/deploy/fastdeploy/web/README.md b/ocr_webapp/PaddleOCR/deploy/fastdeploy/web/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..5ca9628d30630d538479e6df90c744a1eea52261
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/fastdeploy/web/README.md
@@ -0,0 +1,33 @@
+[English](README.md) | 简体中文
+# PP-OCRv3 前端部署示例
+
+本节介绍部署PaddleOCR的PP-OCRv3模型在浏览器中运行,以及@paddle-js-models/ocr npm包中的js接口。
+
+
+## 1. 前端部署PP-OCRv3模型
+PP-OCRv3模型web demo使用[**参考文档**](https://github.com/PaddlePaddle/FastDeploy/tree/develop/examples/application/js/web_demo)
+
+## 2. PP-OCRv3 js接口
+
+```
+import * as ocr from "@paddle-js-models/ocr";
+await ocr.init(detConfig, recConfig);
+const res = await ocr.recognize(img, option, postConfig);
+```
+ocr模型加载和初始化,其中模型为Paddle.js模型格式,js模型转换方式参考[文档](https://github.com/PaddlePaddle/FastDeploy/tree/develop/examples/application/js/web_demo/README.md)
+
+**init函数参数**
+
+> * **detConfig**(dict): 文本检测模型配置参数,默认值为 {modelPath: 'https://js-models.bj.bcebos.com/PaddleOCR/PP-OCRv3/ch_PP-OCRv3_det_infer_js_960/model.json', fill: '#fff', mean: [0.485, 0.456, 0.406],std: [0.229, 0.224, 0.225]}; 其中,modelPath为文本检测模型路径,fill 为图像预处理padding的值,mean和std分别为预处理的均值和标准差
+> * **recConfig**(dict)): 文本识别模型配置参数,默认值为 {modelPath: 'https://js-models.bj.bcebos.com/PaddleOCR/PP-OCRv3/ch_PP-OCRv3_rec_infer_js/model.json', fill: '#000', mean: [0.5, 0.5, 0.5], std: [0.5, 0.5, 0.5]}; 其中,modelPath为文本检测模型路径,fill 为图像预处理padding的值,mean和std分别为预处理的均值和标准差
+
+
+**recognize函数参数**
+
+> * **img**(HTMLImageElement): 输入图像参数,类型为HTMLImageElement。
+> * **option**(dict): 可视化文本检测框的canvas参数,可不用设置。
+> * **postConfig**(dict): 文本检测后处理参数,默认值为:{shape: 960, thresh: 0.3, box_thresh: 0.6, unclip_ratio:1.5}; thresh是输出预测图的二值化阈值;box_thresh是输出框的阈值,低于此值的预测框会被丢弃,unclip_ratio是输出框扩大的比例。
+
+
+## 其它文档
+- [PP-OCRv3 微信小程序部署文档](https://github.com/PaddlePaddle/FastDeploy/tree/develop/examples/application/js/mini_program)
diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser/__init__.py b/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..c747d3e7aeca842933e083dffc01ef1fba3f4e85
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser/__init__.py
@@ -0,0 +1,13 @@
+# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
\ No newline at end of file
diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser/config.json b/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser/config.json
new file mode 100644
index 0000000000000000000000000000000000000000..b93a5f7161f6d612c90cf4c7160dcf050a31beb3
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser/config.json
@@ -0,0 +1,16 @@
+{
+ "modules_info": {
+ "kie_ser": {
+ "init_args": {
+ "version": "1.0.0",
+ "use_gpu": true
+ },
+ "predict_args": {
+ }
+ }
+ },
+ "port": 8871,
+ "use_multiprocess": false,
+ "workers": 2
+}
+
diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser/module.py b/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser/module.py
new file mode 100644
index 0000000000000000000000000000000000000000..f0ef3585d81bddca328d7c643687ef123ebccad4
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser/module.py
@@ -0,0 +1,145 @@
+# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+import os
+import sys
+sys.path.insert(0, ".")
+import copy
+
+import time
+import paddlehub
+from paddlehub.common.logger import logger
+from paddlehub.module.module import moduleinfo, runnable, serving
+import cv2
+import numpy as np
+import paddlehub as hub
+
+from tools.infer.utility import base64_to_cv2
+from ppstructure.kie.predict_kie_token_ser import SerPredictor
+from ppstructure.utility import parse_args
+
+from deploy.hubserving.kie_ser.params import read_params
+
+
+@moduleinfo(
+ name="kie_ser",
+ version="1.0.0",
+ summary="kie ser service",
+ author="paddle-dev",
+ author_email="paddle-dev@baidu.com",
+ type="cv/KIE_SER")
+class KIESer(hub.Module):
+ def _initialize(self, use_gpu=False, enable_mkldnn=False):
+ """
+ initialize with the necessary elements
+ """
+ cfg = self.merge_configs()
+
+ cfg.use_gpu = use_gpu
+ if use_gpu:
+ try:
+ _places = os.environ["CUDA_VISIBLE_DEVICES"]
+ int(_places[0])
+ print("use gpu: ", use_gpu)
+ print("CUDA_VISIBLE_DEVICES: ", _places)
+ cfg.gpu_mem = 8000
+ except:
+ raise RuntimeError(
+ "Environment Variable CUDA_VISIBLE_DEVICES is not set correctly. If you wanna use gpu, please set CUDA_VISIBLE_DEVICES via export CUDA_VISIBLE_DEVICES=cuda_device_id."
+ )
+ cfg.ir_optim = True
+ cfg.enable_mkldnn = enable_mkldnn
+
+ self.ser_predictor = SerPredictor(cfg)
+
+ def merge_configs(self, ):
+ # deafult cfg
+ backup_argv = copy.deepcopy(sys.argv)
+ sys.argv = sys.argv[:1]
+ cfg = parse_args()
+
+ update_cfg_map = vars(read_params())
+
+ for key in update_cfg_map:
+ cfg.__setattr__(key, update_cfg_map[key])
+
+ sys.argv = copy.deepcopy(backup_argv)
+ return cfg
+
+ def read_images(self, paths=[]):
+ images = []
+ for img_path in paths:
+ assert os.path.isfile(
+ img_path), "The {} isn't a valid file.".format(img_path)
+ img = cv2.imread(img_path)
+ if img is None:
+ logger.info("error in loading image:{}".format(img_path))
+ continue
+ images.append(img)
+ return images
+
+ def predict(self, images=[], paths=[]):
+ """
+ Get the chinese texts in the predicted images.
+ Args:
+ images (list(numpy.ndarray)): images data, shape of each is [H, W, C]. If images not paths
+ paths (list[str]): The paths of images. If paths not images
+ Returns:
+ res (list): The result of chinese texts and save path of images.
+ """
+ if images != [] and isinstance(images, list) and paths == []:
+ predicted_data = images
+ elif images == [] and isinstance(paths, list) and paths != []:
+ predicted_data = self.read_images(paths)
+ else:
+ raise TypeError("The input data is inconsistent with expectations.")
+
+ assert predicted_data != [], "There is not any image to be predicted. Please check the input data."
+
+ all_results = []
+ for img in predicted_data:
+ if img is None:
+ logger.info("error in loading image")
+ all_results.append([])
+ continue
+ starttime = time.time()
+ ser_res, _, elapse = self.ser_predictor(img)
+ elapse = time.time() - starttime
+ logger.info("Predict time: {}".format(elapse))
+ all_results.append(ser_res)
+ return all_results
+
+ @serving
+ def serving_method(self, images, **kwargs):
+ """
+ Run as a service.
+ """
+ images_decode = [base64_to_cv2(image) for image in images]
+ results = self.predict(images_decode, **kwargs)
+ return results
+
+
+if __name__ == '__main__':
+ ocr = OCRSystem()
+ ocr._initialize()
+ image_path = [
+ './doc/imgs/11.jpg',
+ './doc/imgs/12.jpg',
+ ]
+ res = ocr.predict(paths=image_path)
+ print(res)
diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser/params.py b/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser/params.py
new file mode 100644
index 0000000000000000000000000000000000000000..bdd04c72e5d68ef6b2f992d9797d2f3bb63399ac
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser/params.py
@@ -0,0 +1,38 @@
+# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+from deploy.hubserving.ocr_system.params import read_params as pp_ocr_read_params
+
+
+class Config(object):
+ pass
+
+
+def read_params():
+ cfg = pp_ocr_read_params()
+
+ # SER params
+ cfg.kie_algorithm = "LayoutXLM"
+ cfg.use_visual_backbone = False
+
+ cfg.ser_model_dir = "./inference/ser_vi_layoutxlm_xfund_infer"
+ cfg.ser_dict_path = "train_data/XFUND/class_list_xfun.txt"
+ cfg.vis_font_path = "./doc/fonts/simfang.ttf"
+ cfg.ocr_order_method = "tb-yx"
+
+ return cfg
diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser_re/__init__.py b/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser_re/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..c747d3e7aeca842933e083dffc01ef1fba3f4e85
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser_re/__init__.py
@@ -0,0 +1,13 @@
+# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
\ No newline at end of file
diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser_re/config.json b/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser_re/config.json
new file mode 100644
index 0000000000000000000000000000000000000000..4d796a860159541d73f9f799c9d3a249b48e479e
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser_re/config.json
@@ -0,0 +1,16 @@
+{
+ "modules_info": {
+ "kie_ser_re": {
+ "init_args": {
+ "version": "1.0.0",
+ "use_gpu": true
+ },
+ "predict_args": {
+ }
+ }
+ },
+ "port": 8872,
+ "use_multiprocess": false,
+ "workers": 2
+}
+
diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser_re/module.py b/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser_re/module.py
new file mode 100644
index 0000000000000000000000000000000000000000..5a63a8a1f18822db84600764e00309e4c5c993e7
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser_re/module.py
@@ -0,0 +1,147 @@
+# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+import os
+import sys
+sys.path.insert(0, ".")
+import copy
+
+import time
+import paddlehub
+from paddlehub.common.logger import logger
+from paddlehub.module.module import moduleinfo, runnable, serving
+import cv2
+import numpy as np
+import paddlehub as hub
+
+from tools.infer.utility import base64_to_cv2
+from ppstructure.kie.predict_kie_token_ser_re import SerRePredictor
+from ppstructure.utility import parse_args
+
+from deploy.hubserving.kie_ser_re.params import read_params
+
+
+@moduleinfo(
+ name="kie_ser_re",
+ version="1.0.0",
+ summary="kie ser re service",
+ author="paddle-dev",
+ author_email="paddle-dev@baidu.com",
+ type="cv/KIE_SER_RE")
+class KIESerRE(hub.Module):
+ def _initialize(self, use_gpu=False, enable_mkldnn=False):
+ """
+ initialize with the necessary elements
+ """
+ cfg = self.merge_configs()
+
+ cfg.use_gpu = use_gpu
+ if use_gpu:
+ try:
+ _places = os.environ["CUDA_VISIBLE_DEVICES"]
+ int(_places[0])
+ print("use gpu: ", use_gpu)
+ print("CUDA_VISIBLE_DEVICES: ", _places)
+ cfg.gpu_mem = 8000
+ except:
+ raise RuntimeError(
+ "Environment Variable CUDA_VISIBLE_DEVICES is not set correctly. If you wanna use gpu, please set CUDA_VISIBLE_DEVICES via export CUDA_VISIBLE_DEVICES=cuda_device_id."
+ )
+ cfg.ir_optim = True
+ cfg.enable_mkldnn = enable_mkldnn
+
+ self.ser_re_predictor = SerRePredictor(cfg)
+
+ def merge_configs(self, ):
+ # deafult cfg
+ backup_argv = copy.deepcopy(sys.argv)
+ sys.argv = sys.argv[:1]
+ cfg = parse_args()
+
+ update_cfg_map = vars(read_params())
+
+ for key in update_cfg_map:
+ cfg.__setattr__(key, update_cfg_map[key])
+
+ sys.argv = copy.deepcopy(backup_argv)
+ return cfg
+
+ def read_images(self, paths=[]):
+ images = []
+ for img_path in paths:
+ assert os.path.isfile(
+ img_path), "The {} isn't a valid file.".format(img_path)
+ img = cv2.imread(img_path)
+ if img is None:
+ logger.info("error in loading image:{}".format(img_path))
+ continue
+ images.append(img)
+ return images
+
+ def predict(self, images=[], paths=[]):
+ """
+ Get the chinese texts in the predicted images.
+ Args:
+ images (list(numpy.ndarray)): images data, shape of each is [H, W, C]. If images not paths
+ paths (list[str]): The paths of images. If paths not images
+ Returns:
+ res (list): The result of chinese texts and save path of images.
+ """
+ if images != [] and isinstance(images, list) and paths == []:
+ predicted_data = images
+ elif images == [] and isinstance(paths, list) and paths != []:
+ predicted_data = self.read_images(paths)
+ else:
+ raise TypeError("The input data is inconsistent with expectations.")
+
+ assert predicted_data != [], "There is not any image to be predicted. Please check the input data."
+
+ all_results = []
+ for img in predicted_data:
+ if img is None:
+ logger.info("error in loading image")
+ all_results.append([])
+ continue
+ print(img.shape)
+ starttime = time.time()
+ re_res, _ = self.ser_re_predictor(img)
+ print(re_res)
+ elapse = time.time() - starttime
+ logger.info("Predict time: {}".format(elapse))
+ all_results.append(re_res)
+ return all_results
+
+ @serving
+ def serving_method(self, images, **kwargs):
+ """
+ Run as a service.
+ """
+ images_decode = [base64_to_cv2(image) for image in images]
+ results = self.predict(images_decode, **kwargs)
+ return results
+
+
+if __name__ == '__main__':
+ ocr = OCRSystem()
+ ocr._initialize()
+ image_path = [
+ './doc/imgs/11.jpg',
+ './doc/imgs/12.jpg',
+ ]
+ res = ocr.predict(paths=image_path)
+ print(res)
diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser_re/params.py b/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser_re/params.py
new file mode 100644
index 0000000000000000000000000000000000000000..8c3214a5a78547d56cb0cc848bdbc35142193e84
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/hubserving/kie_ser_re/params.py
@@ -0,0 +1,40 @@
+# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+from deploy.hubserving.ocr_system.params import read_params as pp_ocr_read_params
+
+
+class Config(object):
+ pass
+
+
+def read_params():
+ cfg = pp_ocr_read_params()
+
+ # SER params
+ cfg.kie_algorithm = "LayoutXLM"
+ cfg.use_visual_backbone = False
+
+ cfg.ser_model_dir = "./inference/ser_vi_layoutxlm_xfund_infer"
+ cfg.re_model_dir = "./inference/re_vi_layoutxlm_xfund_infer"
+
+ cfg.ser_dict_path = "train_data/XFUND/class_list_xfun.txt"
+ cfg.vis_font_path = "./doc/fonts/simfang.ttf"
+ cfg.ocr_order_method = "tb-yx"
+
+ return cfg
diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_cls/__init__.py b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_cls/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..c747d3e7aeca842933e083dffc01ef1fba3f4e85
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_cls/__init__.py
@@ -0,0 +1,13 @@
+# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
\ No newline at end of file
diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_cls/config.json b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_cls/config.json
new file mode 100644
index 0000000000000000000000000000000000000000..2ced861b57efe3b9a426c129ff8a3f56e9d83a60
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_cls/config.json
@@ -0,0 +1,15 @@
+{
+ "modules_info": {
+ "ocr_cls": {
+ "init_args": {
+ "version": "1.0.0",
+ "use_gpu": true
+ },
+ "predict_args": {
+ }
+ }
+ },
+ "port": 8866,
+ "use_multiprocess": false,
+ "workers": 2
+}
diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_cls/module.py b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_cls/module.py
new file mode 100644
index 0000000000000000000000000000000000000000..8b70f0376e5ebf5a960a73115c65a7a1d3d0011e
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_cls/module.py
@@ -0,0 +1,151 @@
+# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+import os
+import sys
+sys.path.insert(0, ".")
+import copy
+import paddlehub
+from paddlehub.common.logger import logger
+from paddlehub.module.module import moduleinfo, runnable, serving
+import cv2
+import paddlehub as hub
+
+from tools.infer.utility import base64_to_cv2
+from tools.infer.predict_cls import TextClassifier
+from tools.infer.utility import parse_args
+from deploy.hubserving.ocr_cls.params import read_params
+
+
+@moduleinfo(
+ name="ocr_cls",
+ version="1.0.0",
+ summary="ocr angle cls service",
+ author="paddle-dev",
+ author_email="paddle-dev@baidu.com",
+ type="cv/text_angle_cls")
+class OCRCls(hub.Module):
+ def _initialize(self, use_gpu=False, enable_mkldnn=False):
+ """
+ initialize with the necessary elements
+ """
+ cfg = self.merge_configs()
+
+ cfg.use_gpu = use_gpu
+ if use_gpu:
+ try:
+ _places = os.environ["CUDA_VISIBLE_DEVICES"]
+ int(_places[0])
+ print("use gpu: ", use_gpu)
+ print("CUDA_VISIBLE_DEVICES: ", _places)
+ cfg.gpu_mem = 8000
+ except:
+ raise RuntimeError(
+ "Environment Variable CUDA_VISIBLE_DEVICES is not set correctly. If you wanna use gpu, please set CUDA_VISIBLE_DEVICES via export CUDA_VISIBLE_DEVICES=cuda_device_id."
+ )
+ cfg.ir_optim = True
+ cfg.enable_mkldnn = enable_mkldnn
+
+ self.text_classifier = TextClassifier(cfg)
+
+ def merge_configs(self, ):
+ # deafult cfg
+ backup_argv = copy.deepcopy(sys.argv)
+ sys.argv = sys.argv[:1]
+ cfg = parse_args()
+
+ update_cfg_map = vars(read_params())
+
+ for key in update_cfg_map:
+ cfg.__setattr__(key, update_cfg_map[key])
+
+ sys.argv = copy.deepcopy(backup_argv)
+ return cfg
+
+ def read_images(self, paths=[]):
+ images = []
+ for img_path in paths:
+ assert os.path.isfile(
+ img_path), "The {} isn't a valid file.".format(img_path)
+ img = cv2.imread(img_path)
+ if img is None:
+ logger.info("error in loading image:{}".format(img_path))
+ continue
+ images.append(img)
+ return images
+
+ def predict(self, images=[], paths=[]):
+ """
+ Get the text angle in the predicted images.
+ Args:
+ images (list(numpy.ndarray)): images data, shape of each is [H, W, C]. If images not paths
+ paths (list[str]): The paths of images. If paths not images
+ Returns:
+ res (list): The result of text detection box and save path of images.
+ """
+
+ if images != [] and isinstance(images, list) and paths == []:
+ predicted_data = images
+ elif images == [] and isinstance(paths, list) and paths != []:
+ predicted_data = self.read_images(paths)
+ else:
+ raise TypeError("The input data is inconsistent with expectations.")
+
+ assert predicted_data != [], "There is not any image to be predicted. Please check the input data."
+
+ img_list = []
+ for img in predicted_data:
+ if img is None:
+ continue
+ img_list.append(img)
+
+ rec_res_final = []
+ try:
+ img_list, cls_res, predict_time = self.text_classifier(img_list)
+ for dno in range(len(cls_res)):
+ angle, score = cls_res[dno]
+ rec_res_final.append({
+ 'angle': angle,
+ 'confidence': float(score),
+ })
+ except Exception as e:
+ print(e)
+ return [[]]
+
+ return [rec_res_final]
+
+ @serving
+ def serving_method(self, images, **kwargs):
+ """
+ Run as a service.
+ """
+ images_decode = [base64_to_cv2(image) for image in images]
+ results = self.predict(images_decode, **kwargs)
+ return results
+
+
+if __name__ == '__main__':
+ ocr = OCRCls()
+ ocr._initialize()
+ image_path = [
+ './doc/imgs_words/ch/word_1.jpg',
+ './doc/imgs_words/ch/word_2.jpg',
+ './doc/imgs_words/ch/word_3.jpg',
+ ]
+ res = ocr.predict(paths=image_path)
+ print(res)
diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_cls/params.py b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_cls/params.py
new file mode 100644
index 0000000000000000000000000000000000000000..fe4e84843a434e2ca712cdb68d026520de8bf635
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_cls/params.py
@@ -0,0 +1,37 @@
+# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+
+class Config(object):
+ pass
+
+
+def read_params():
+ cfg = Config()
+
+ #params for text classifier
+ cfg.cls_model_dir = "./inference/ch_ppocr_mobile_v2.0_cls_infer/"
+ cfg.cls_image_shape = "3, 48, 192"
+ cfg.label_list = ['0', '180']
+ cfg.cls_batch_num = 30
+ cfg.cls_thresh = 0.9
+
+ cfg.use_pdserving = False
+ cfg.use_tensorrt = False
+
+ return cfg
diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_det/__init__.py b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_det/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..c747d3e7aeca842933e083dffc01ef1fba3f4e85
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_det/__init__.py
@@ -0,0 +1,13 @@
+# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
\ No newline at end of file
diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_det/config.json b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_det/config.json
new file mode 100644
index 0000000000000000000000000000000000000000..6080d1c53ad065f47739a77e95bd15f792c63da0
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_det/config.json
@@ -0,0 +1,15 @@
+{
+ "modules_info": {
+ "ocr_det": {
+ "init_args": {
+ "version": "1.0.0",
+ "use_gpu": true
+ },
+ "predict_args": {
+ }
+ }
+ },
+ "port": 8865,
+ "use_multiprocess": false,
+ "workers": 2
+}
diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_det/module.py b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_det/module.py
new file mode 100644
index 0000000000000000000000000000000000000000..3dbaf161cdfd59528a36b5d5f645656cade2f1fc
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_det/module.py
@@ -0,0 +1,148 @@
+# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+import os
+import sys
+sys.path.insert(0, ".")
+
+import copy
+import paddlehub
+from paddlehub.common.logger import logger
+from paddlehub.module.module import moduleinfo, runnable, serving
+import cv2
+import numpy as np
+import paddlehub as hub
+
+from tools.infer.utility import base64_to_cv2
+from tools.infer.predict_det import TextDetector
+from tools.infer.utility import parse_args
+from deploy.hubserving.ocr_system.params import read_params
+
+
+@moduleinfo(
+ name="ocr_det",
+ version="1.0.0",
+ summary="ocr detection service",
+ author="paddle-dev",
+ author_email="paddle-dev@baidu.com",
+ type="cv/text_detection")
+class OCRDet(hub.Module):
+ def _initialize(self, use_gpu=False, enable_mkldnn=False):
+ """
+ initialize with the necessary elements
+ """
+ cfg = self.merge_configs()
+
+ cfg.use_gpu = use_gpu
+ if use_gpu:
+ try:
+ _places = os.environ["CUDA_VISIBLE_DEVICES"]
+ int(_places[0])
+ print("use gpu: ", use_gpu)
+ print("CUDA_VISIBLE_DEVICES: ", _places)
+ cfg.gpu_mem = 8000
+ except:
+ raise RuntimeError(
+ "Environment Variable CUDA_VISIBLE_DEVICES is not set correctly. If you wanna use gpu, please set CUDA_VISIBLE_DEVICES via export CUDA_VISIBLE_DEVICES=cuda_device_id."
+ )
+ cfg.ir_optim = True
+ cfg.enable_mkldnn = enable_mkldnn
+
+ self.text_detector = TextDetector(cfg)
+
+ def merge_configs(self, ):
+ # deafult cfg
+ backup_argv = copy.deepcopy(sys.argv)
+ sys.argv = sys.argv[:1]
+ cfg = parse_args()
+
+ update_cfg_map = vars(read_params())
+
+ for key in update_cfg_map:
+ cfg.__setattr__(key, update_cfg_map[key])
+
+ sys.argv = copy.deepcopy(backup_argv)
+ return cfg
+
+ def read_images(self, paths=[]):
+ images = []
+ for img_path in paths:
+ assert os.path.isfile(
+ img_path), "The {} isn't a valid file.".format(img_path)
+ img = cv2.imread(img_path)
+ if img is None:
+ logger.info("error in loading image:{}".format(img_path))
+ continue
+ images.append(img)
+ return images
+
+ def predict(self, images=[], paths=[]):
+ """
+ Get the text box in the predicted images.
+ Args:
+ images (list(numpy.ndarray)): images data, shape of each is [H, W, C]. If images not paths
+ paths (list[str]): The paths of images. If paths not images
+ Returns:
+ res (list): The result of text detection box and save path of images.
+ """
+
+ if images != [] and isinstance(images, list) and paths == []:
+ predicted_data = images
+ elif images == [] and isinstance(paths, list) and paths != []:
+ predicted_data = self.read_images(paths)
+ else:
+ raise TypeError("The input data is inconsistent with expectations.")
+
+ assert predicted_data != [], "There is not any image to be predicted. Please check the input data."
+
+ all_results = []
+ for img in predicted_data:
+ if img is None:
+ logger.info("error in loading image")
+ all_results.append([])
+ continue
+ dt_boxes, elapse = self.text_detector(img)
+ logger.info("Predict time : {}".format(elapse))
+
+ rec_res_final = []
+ for dno in range(len(dt_boxes)):
+ rec_res_final.append({
+ 'text_region': dt_boxes[dno].astype(np.int32).tolist()
+ })
+ all_results.append(rec_res_final)
+ return all_results
+
+ @serving
+ def serving_method(self, images, **kwargs):
+ """
+ Run as a service.
+ """
+ images_decode = [base64_to_cv2(image) for image in images]
+ results = self.predict(images_decode, **kwargs)
+ return results
+
+
+if __name__ == '__main__':
+ ocr = OCRDet()
+ ocr._initialize()
+ image_path = [
+ './doc/imgs/11.jpg',
+ './doc/imgs/12.jpg',
+ ]
+ res = ocr.predict(paths=image_path)
+ print(res)
diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_det/params.py b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_det/params.py
new file mode 100644
index 0000000000000000000000000000000000000000..c4aa13817f2dadb8225ec0112415094cce5165ad
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_det/params.py
@@ -0,0 +1,48 @@
+# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+
+class Config(object):
+ pass
+
+
+def read_params():
+ cfg = Config()
+
+ #params for text detector
+ cfg.det_algorithm = "DB"
+ cfg.det_model_dir = "./inference/ch_PP-OCRv3_det_infer/"
+ cfg.det_limit_side_len = 960
+ cfg.det_limit_type = 'max'
+
+ #DB parmas
+ cfg.det_db_thresh = 0.3
+ cfg.det_db_box_thresh = 0.6
+ cfg.det_db_unclip_ratio = 1.5
+ cfg.use_dilation = False
+ cfg.det_db_score_mode = "fast"
+
+ # #EAST parmas
+ # cfg.det_east_score_thresh = 0.8
+ # cfg.det_east_cover_thresh = 0.1
+ # cfg.det_east_nms_thresh = 0.2
+
+ cfg.use_pdserving = False
+ cfg.use_tensorrt = False
+
+ return cfg
diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_rec/__init__.py b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_rec/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..c747d3e7aeca842933e083dffc01ef1fba3f4e85
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_rec/__init__.py
@@ -0,0 +1,13 @@
+# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
\ No newline at end of file
diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_rec/config.json b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_rec/config.json
new file mode 100644
index 0000000000000000000000000000000000000000..59d54e6c7545f300f134c9c2efaf546308b9c360
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_rec/config.json
@@ -0,0 +1,15 @@
+{
+ "modules_info": {
+ "ocr_rec": {
+ "init_args": {
+ "version": "1.0.0",
+ "use_gpu": true
+ },
+ "predict_args": {
+ }
+ }
+ },
+ "port": 8867,
+ "use_multiprocess": false,
+ "workers": 2
+}
diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_rec/module.py b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_rec/module.py
new file mode 100644
index 0000000000000000000000000000000000000000..9fae54e2a317a7020543675d49fe0b7e07b4f7cd
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_rec/module.py
@@ -0,0 +1,151 @@
+# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+import os
+import sys
+sys.path.insert(0, ".")
+import copy
+import paddlehub
+from paddlehub.common.logger import logger
+from paddlehub.module.module import moduleinfo, runnable, serving
+import cv2
+import paddlehub as hub
+
+from tools.infer.utility import base64_to_cv2
+from tools.infer.predict_rec import TextRecognizer
+from tools.infer.utility import parse_args
+from deploy.hubserving.ocr_rec.params import read_params
+
+
+@moduleinfo(
+ name="ocr_rec",
+ version="1.0.0",
+ summary="ocr recognition service",
+ author="paddle-dev",
+ author_email="paddle-dev@baidu.com",
+ type="cv/text_recognition")
+class OCRRec(hub.Module):
+ def _initialize(self, use_gpu=False, enable_mkldnn=False):
+ """
+ initialize with the necessary elements
+ """
+ cfg = self.merge_configs()
+
+ cfg.use_gpu = use_gpu
+ if use_gpu:
+ try:
+ _places = os.environ["CUDA_VISIBLE_DEVICES"]
+ int(_places[0])
+ print("use gpu: ", use_gpu)
+ print("CUDA_VISIBLE_DEVICES: ", _places)
+ cfg.gpu_mem = 8000
+ except:
+ raise RuntimeError(
+ "Environment Variable CUDA_VISIBLE_DEVICES is not set correctly. If you wanna use gpu, please set CUDA_VISIBLE_DEVICES via export CUDA_VISIBLE_DEVICES=cuda_device_id."
+ )
+ cfg.ir_optim = True
+ cfg.enable_mkldnn = enable_mkldnn
+
+ self.text_recognizer = TextRecognizer(cfg)
+
+ def merge_configs(self, ):
+ # deafult cfg
+ backup_argv = copy.deepcopy(sys.argv)
+ sys.argv = sys.argv[:1]
+ cfg = parse_args()
+
+ update_cfg_map = vars(read_params())
+
+ for key in update_cfg_map:
+ cfg.__setattr__(key, update_cfg_map[key])
+
+ sys.argv = copy.deepcopy(backup_argv)
+ return cfg
+
+ def read_images(self, paths=[]):
+ images = []
+ for img_path in paths:
+ assert os.path.isfile(
+ img_path), "The {} isn't a valid file.".format(img_path)
+ img = cv2.imread(img_path)
+ if img is None:
+ logger.info("error in loading image:{}".format(img_path))
+ continue
+ images.append(img)
+ return images
+
+ def predict(self, images=[], paths=[]):
+ """
+ Get the text box in the predicted images.
+ Args:
+ images (list(numpy.ndarray)): images data, shape of each is [H, W, C]. If images not paths
+ paths (list[str]): The paths of images. If paths not images
+ Returns:
+ res (list): The result of text detection box and save path of images.
+ """
+
+ if images != [] and isinstance(images, list) and paths == []:
+ predicted_data = images
+ elif images == [] and isinstance(paths, list) and paths != []:
+ predicted_data = self.read_images(paths)
+ else:
+ raise TypeError("The input data is inconsistent with expectations.")
+
+ assert predicted_data != [], "There is not any image to be predicted. Please check the input data."
+
+ img_list = []
+ for img in predicted_data:
+ if img is None:
+ continue
+ img_list.append(img)
+
+ rec_res_final = []
+ try:
+ rec_res, predict_time = self.text_recognizer(img_list)
+ for dno in range(len(rec_res)):
+ text, score = rec_res[dno]
+ rec_res_final.append({
+ 'text': text,
+ 'confidence': float(score),
+ })
+ except Exception as e:
+ print(e)
+ return [[]]
+
+ return [rec_res_final]
+
+ @serving
+ def serving_method(self, images, **kwargs):
+ """
+ Run as a service.
+ """
+ images_decode = [base64_to_cv2(image) for image in images]
+ results = self.predict(images_decode, **kwargs)
+ return results
+
+
+if __name__ == '__main__':
+ ocr = OCRRec()
+ ocr._initialize()
+ image_path = [
+ './doc/imgs_words/ch/word_1.jpg',
+ './doc/imgs_words/ch/word_2.jpg',
+ './doc/imgs_words/ch/word_3.jpg',
+ ]
+ res = ocr.predict(paths=image_path)
+ print(res)
diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_rec/params.py b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_rec/params.py
new file mode 100644
index 0000000000000000000000000000000000000000..b8854c8c0718ddade7415e1d52b3deb0b2659868
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_rec/params.py
@@ -0,0 +1,41 @@
+# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+
+class Config(object):
+ pass
+
+
+def read_params():
+ cfg = Config()
+
+ #params for text recognizer
+ cfg.rec_algorithm = "CRNN"
+ cfg.rec_model_dir = "./inference/ch_PP-OCRv3_rec_infer/"
+
+ cfg.rec_image_shape = "3, 48, 320"
+ cfg.rec_batch_num = 6
+ cfg.max_text_length = 25
+
+ cfg.rec_char_dict_path = "./ppocr/utils/ppocr_keys_v1.txt"
+ cfg.use_space_char = True
+
+ cfg.use_pdserving = False
+ cfg.use_tensorrt = False
+
+ return cfg
diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_system/__init__.py b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_system/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..c747d3e7aeca842933e083dffc01ef1fba3f4e85
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_system/__init__.py
@@ -0,0 +1,13 @@
+# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
\ No newline at end of file
diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_system/config.json b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_system/config.json
new file mode 100644
index 0000000000000000000000000000000000000000..48e7e15425022e63a6622c6fe6394b8b249726cd
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_system/config.json
@@ -0,0 +1,16 @@
+{
+ "modules_info": {
+ "ocr_system": {
+ "init_args": {
+ "version": "1.0.0",
+ "use_gpu": true
+ },
+ "predict_args": {
+ }
+ }
+ },
+ "port": 8868,
+ "use_multiprocess": false,
+ "workers": 2
+}
+
diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_system/module.py b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_system/module.py
new file mode 100644
index 0000000000000000000000000000000000000000..192fff9650901df7889ae7b0620beea1d8b03e81
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_system/module.py
@@ -0,0 +1,156 @@
+# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+import os
+import sys
+sys.path.insert(0, ".")
+import copy
+
+import time
+import paddlehub
+from paddlehub.common.logger import logger
+from paddlehub.module.module import moduleinfo, runnable, serving
+import cv2
+import numpy as np
+import paddlehub as hub
+
+from tools.infer.utility import base64_to_cv2
+from tools.infer.predict_system import TextSystem
+from tools.infer.utility import parse_args
+from deploy.hubserving.ocr_system.params import read_params
+
+
+@moduleinfo(
+ name="ocr_system",
+ version="1.0.0",
+ summary="ocr system service",
+ author="paddle-dev",
+ author_email="paddle-dev@baidu.com",
+ type="cv/PP-OCR_system")
+class OCRSystem(hub.Module):
+ def _initialize(self, use_gpu=False, enable_mkldnn=False):
+ """
+ initialize with the necessary elements
+ """
+ cfg = self.merge_configs()
+
+ cfg.use_gpu = use_gpu
+ if use_gpu:
+ try:
+ _places = os.environ["CUDA_VISIBLE_DEVICES"]
+ int(_places[0])
+ print("use gpu: ", use_gpu)
+ print("CUDA_VISIBLE_DEVICES: ", _places)
+ cfg.gpu_mem = 8000
+ except:
+ raise RuntimeError(
+ "Environment Variable CUDA_VISIBLE_DEVICES is not set correctly. If you wanna use gpu, please set CUDA_VISIBLE_DEVICES via export CUDA_VISIBLE_DEVICES=cuda_device_id."
+ )
+ cfg.ir_optim = True
+ cfg.enable_mkldnn = enable_mkldnn
+
+ self.text_sys = TextSystem(cfg)
+
+ def merge_configs(self, ):
+ # deafult cfg
+ backup_argv = copy.deepcopy(sys.argv)
+ sys.argv = sys.argv[:1]
+ cfg = parse_args()
+
+ update_cfg_map = vars(read_params())
+
+ for key in update_cfg_map:
+ cfg.__setattr__(key, update_cfg_map[key])
+
+ sys.argv = copy.deepcopy(backup_argv)
+ return cfg
+
+ def read_images(self, paths=[]):
+ images = []
+ for img_path in paths:
+ assert os.path.isfile(
+ img_path), "The {} isn't a valid file.".format(img_path)
+ img = cv2.imread(img_path)
+ if img is None:
+ logger.info("error in loading image:{}".format(img_path))
+ continue
+ images.append(img)
+ return images
+
+ def predict(self, images=[], paths=[]):
+ """
+ Get the chinese texts in the predicted images.
+ Args:
+ images (list(numpy.ndarray)): images data, shape of each is [H, W, C]. If images not paths
+ paths (list[str]): The paths of images. If paths not images
+ Returns:
+ res (list): The result of chinese texts and save path of images.
+ """
+
+ if images != [] and isinstance(images, list) and paths == []:
+ predicted_data = images
+ elif images == [] and isinstance(paths, list) and paths != []:
+ predicted_data = self.read_images(paths)
+ else:
+ raise TypeError("The input data is inconsistent with expectations.")
+
+ assert predicted_data != [], "There is not any image to be predicted. Please check the input data."
+
+ all_results = []
+ for img in predicted_data:
+ if img is None:
+ logger.info("error in loading image")
+ all_results.append([])
+ continue
+ starttime = time.time()
+ dt_boxes, rec_res, _ = self.text_sys(img)
+ elapse = time.time() - starttime
+ logger.info("Predict time: {}".format(elapse))
+
+ dt_num = len(dt_boxes)
+ rec_res_final = []
+
+ for dno in range(dt_num):
+ text, score = rec_res[dno]
+ rec_res_final.append({
+ 'text': text,
+ 'confidence': float(score),
+ 'text_region': dt_boxes[dno].astype(np.int32).tolist()
+ })
+ all_results.append(rec_res_final)
+ return all_results
+
+ @serving
+ def serving_method(self, images, **kwargs):
+ """
+ Run as a service.
+ """
+ images_decode = [base64_to_cv2(image) for image in images]
+ results = self.predict(images_decode, **kwargs)
+ return results
+
+
+if __name__ == '__main__':
+ ocr = OCRSystem()
+ ocr._initialize()
+ image_path = [
+ './doc/imgs/11.jpg',
+ './doc/imgs/12.jpg',
+ ]
+ res = ocr.predict(paths=image_path)
+ print(res)
diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_system/params.py b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_system/params.py
new file mode 100644
index 0000000000000000000000000000000000000000..4df1979a103345c3d2089cd0b1872bad49dcb944
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/hubserving/ocr_system/params.py
@@ -0,0 +1,68 @@
+# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+
+class Config(object):
+ pass
+
+
+def read_params():
+ cfg = Config()
+
+ #params for text detector
+ cfg.det_algorithm = "DB"
+ cfg.det_model_dir = "./inference/ch_PP-OCRv3_det_infer/"
+ cfg.det_limit_side_len = 960
+ cfg.det_limit_type = 'max'
+
+ #DB parmas
+ cfg.det_db_thresh = 0.3
+ cfg.det_db_box_thresh = 0.5
+ cfg.det_db_unclip_ratio = 1.6
+ cfg.use_dilation = False
+ cfg.det_db_score_mode = "fast"
+
+ #EAST parmas
+ cfg.det_east_score_thresh = 0.8
+ cfg.det_east_cover_thresh = 0.1
+ cfg.det_east_nms_thresh = 0.2
+
+ #params for text recognizer
+ cfg.rec_algorithm = "CRNN"
+ cfg.rec_model_dir = "./inference/ch_PP-OCRv3_rec_infer/"
+
+ cfg.rec_image_shape = "3, 48, 320"
+ cfg.rec_batch_num = 6
+ cfg.max_text_length = 25
+
+ cfg.rec_char_dict_path = "./ppocr/utils/ppocr_keys_v1.txt"
+ cfg.use_space_char = True
+
+ #params for text classifier
+ cfg.use_angle_cls = True
+ cfg.cls_model_dir = "./inference/ch_ppocr_mobile_v2.0_cls_infer/"
+ cfg.cls_image_shape = "3, 48, 192"
+ cfg.label_list = ['0', '180']
+ cfg.cls_batch_num = 30
+ cfg.cls_thresh = 0.9
+
+ cfg.use_pdserving = False
+ cfg.use_tensorrt = False
+ cfg.drop_score = 0.5
+
+ return cfg
diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/readme.md b/ocr_webapp/PaddleOCR/deploy/hubserving/readme.md
new file mode 100644
index 0000000000000000000000000000000000000000..9302bad97edee5aeb6bc26888da0a9d1ad285325
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/hubserving/readme.md
@@ -0,0 +1,250 @@
+[English](readme_en.md) | 简体中文
+
+- [基于PaddleHub Serving的服务部署](#基于paddlehub-serving的服务部署)
+ - [1. 近期更新](#1-近期更新)
+ - [2. 快速启动服务](#2-快速启动服务)
+ - [2.1 安装PaddleHub](#21-安装PaddleHub)
+ - [2.2 下载推理模型](#22-下载推理模型)
+ - [2.3 安装服务模块](#23-安装服务模块)
+ - [2.4 启动服务](#24-启动服务)
+ - [2.4.1. 命令行命令启动(仅支持CPU)](#241-命令行命令启动仅支持cpu)
+ - [2.4.2 配置文件启动(支持CPU、GPU)](#242-配置文件启动支持cpugpu)
+ - [3. 发送预测请求](#3-发送预测请求)
+ - [4. 返回结果格式说明](#4-返回结果格式说明)
+ - [5. 自定义修改服务模块](#5-自定义修改服务模块)
+
+
+PaddleOCR提供2种服务部署方式:
+- 基于PaddleHub Serving的部署:代码路径为`./deploy/hubserving`,按照本教程使用;
+- 基于PaddleServing的部署:代码路径为`./deploy/pdserving`,使用方法参考[文档](../../deploy/pdserving/README_CN.md)。
+
+# 基于PaddleHub Serving的服务部署
+
+hubserving服务部署目录下包括文本检测、文本方向分类,文本识别、文本检测+文本方向分类+文本识别3阶段串联,版面分析、表格识别和PP-Structure七种服务包,请根据需求选择相应的服务包进行安装和启动。目录结构如下:
+```
+deploy/hubserving/
+ └─ ocr_cls 文本方向分类模块服务包
+ └─ ocr_det 文本检测模块服务包
+ └─ ocr_rec 文本识别模块服务包
+ └─ ocr_system 文本检测+文本方向分类+文本识别串联服务包
+ └─ structure_layout 版面分析服务包
+ └─ structure_table 表格识别服务包
+ └─ structure_system PP-Structure服务包
+ └─ kie_ser 关键信息抽取-SER服务包
+ └─ kie_ser_re 关键信息抽取-SER+RE服务包
+```
+
+每个服务包下包含3个文件。以2阶段串联服务包为例,目录如下:
+```
+deploy/hubserving/ocr_system/
+ └─ __init__.py 空文件,必选
+ └─ config.json 配置文件,可选,使用配置启动服务时作为参数传入
+ └─ module.py 主模块,必选,包含服务的完整逻辑
+ └─ params.py 参数文件,必选,包含模型路径、前后处理参数等参数
+```
+## 1. 近期更新
+
+* 2022.10.09 新增关键信息抽取服务。
+* 2022.08.23 新增版面分析服务。
+* 2022.05.05 新增PP-OCRv3检测和识别模型。
+* 2022.03.30 新增PP-Structure和表格识别两种服务。
+
+## 2. 快速启动服务
+以下步骤以检测+识别2阶段串联服务为例,如果只需要检测服务或识别服务,替换相应文件路径即可。
+### 2.1 安装PaddleHub
+paddlehub 需要 python>3.6.2
+```bash
+pip3 install paddlehub==2.1.0 --upgrade -i https://mirror.baidu.com/pypi/simple
+```
+
+### 2.2 下载推理模型
+安装服务模块前,需要准备推理模型并放到正确路径。默认使用的是PP-OCRv3模型,默认模型路径为:
+| 模型 | 路径 |
+| ------- | - |
+| 检测模型 | `./inference/ch_PP-OCRv3_det_infer/` |
+| 识别模型 | `./inference/ch_PP-OCRv3_rec_infer/` |
+| 方向分类器 | `./inference/ch_ppocr_mobile_v2.0_cls_infer/` |
+| 版面分析模型 | `./inference/picodet_lcnet_x1_0_fgd_layout_infer/` |
+| 表格结构识别模型 | `./inference/ch_ppstructure_mobile_v2.0_SLANet_infer/` |
+| 关键信息抽取SER模型 | `./inference/ser_vi_layoutxlm_xfund_infer/` |
+| 关键信息抽取RE模型 | `./inference/re_vi_layoutxlm_xfund_infer/` |
+
+**模型路径可在`params.py`中查看和修改。**
+
+更多模型可以从PaddleOCR提供的模型库[PP-OCR](../../doc/doc_ch/models_list.md)和[PP-Structure](../../ppstructure/docs/models_list.md)下载,也可以替换成自己训练转换好的模型。
+
+### 2.3 安装服务模块
+PaddleOCR提供5种服务模块,根据需要安装所需模块。
+
+在Linux环境(Windows环境请将`/`替换为`\`)下,安装模块命令如下表:
+| 服务模块 | 命令 |
+| ------- | - |
+| 检测 | `hub install deploy/hubserving/ocr_det` |
+| 分类 | `hub install deploy/hubserving/ocr_cls` |
+| 识别 | `hub install deploy/hubserving/ocr_rec` |
+| 检测+识别串联 | `hub install deploy/hubserving/ocr_system` |
+| 表格识别 | `hub install deploy/hubserving/structure_table` |
+| PP-Structure | `hub install deploy/hubserving/structure_system` |
+| 版面分析 | `hub install deploy/hubserving/structure_layout` |
+| 关键信息抽取SER | `hub install deploy/hubserving/kie_ser` |
+| 关键信息抽取SER+RE | `hub install deploy/hubserving/kie_ser_re` |
+
+### 2.4 启动服务
+#### 2.4.1. 命令行命令启动(仅支持CPU)
+**启动命令:**
+```bash
+hub serving start --modules Module1==Version1, Module2==Version2, ... \
+ --port 8866 \
+ --use_multiprocess \
+ --workers \
+```
+
+**参数:**
+|参数|用途|
+|---|---|
+|`--modules`/`-m`|PaddleHub Serving预安装模型,以多个Module==Version键值对的形式列出
**当不指定Version时,默认选择最新版本**|
+|`--port`/`-p`|服务端口,默认为8866|
+|`--use_multiprocess`|是否启用并发方式,默认为单进程方式,推荐多核CPU机器使用此方式
**Windows操作系统只支持单进程方式**|
+|`--workers`|在并发方式下指定的并发任务数,默认为`2*cpu_count-1`,其中`cpu_count`为CPU核数|
+
+如启动串联服务:
+```bash
+hub serving start -m ocr_system
+```
+
+这样就完成了一个服务化API的部署,使用默认端口号8866。
+
+#### 2.4.2 配置文件启动(支持CPU、GPU)
+**启动命令:**
+```bash
+hub serving start -c config.json
+```
+
+其中,`config.json`格式如下:
+```json
+{
+ "modules_info": {
+ "ocr_system": {
+ "init_args": {
+ "version": "1.0.0",
+ "use_gpu": true
+ },
+ "predict_args": {
+ }
+ }
+ },
+ "port": 8868,
+ "use_multiprocess": false,
+ "workers": 2
+}
+```
+
+- `init_args`中的可配参数与`module.py`中的`_initialize`函数接口一致。
+
+ **当`use_gpu`为`true`时,表示使用GPU启动服务。**
+- `predict_args`中的可配参数与`module.py`中的`predict`函数接口一致。
+
+**注意:**
+- 使用配置文件启动服务时,其他参数会被忽略。
+- 如果使用GPU预测(即,`use_gpu`置为`true`),则需要在启动服务之前,设置CUDA_VISIBLE_DEVICES环境变量,如:
+ ```bash
+ export CUDA_VISIBLE_DEVICES=0
+ ```
+- **`use_gpu`不可与`use_multiprocess`同时为`true`**。
+
+如,使用GPU 3号卡启动串联服务:
+```bash
+export CUDA_VISIBLE_DEVICES=3
+hub serving start -c deploy/hubserving/ocr_system/config.json
+```
+
+## 3. 发送预测请求
+配置好服务端,可使用以下命令发送预测请求,获取预测结果:
+```bash
+python tools/test_hubserving.py --server_url=server_url --image_dir=image_path
+```
+
+需要给脚本传递2个参数:
+- `server_url`:服务地址,格式为`http://[ip_address]:[port]/predict/[module_name]`
+
+ 例如,如果使用配置文件启动分类,检测、识别,检测+分类+识别3阶段,表格识别和PP-Structure服务
+
+ 并为每个服务修改了port,那么发送请求的url将分别是:
+ ```
+ http://127.0.0.1:8865/predict/ocr_det
+ http://127.0.0.1:8866/predict/ocr_cls
+ http://127.0.0.1:8867/predict/ocr_rec
+ http://127.0.0.1:8868/predict/ocr_system
+ http://127.0.0.1:8869/predict/structure_table
+ http://127.0.0.1:8870/predict/structure_system
+ http://127.0.0.1:8870/predict/structure_layout
+ http://127.0.0.1:8871/predict/kie_ser
+ http://127.0.0.1:8872/predict/kie_ser_re
+ ```
+- `image_dir`:测试图像路径,可以是单张图片路径,也可以是图像集合目录路径
+- `visualize`:是否可视化结果,默认为False
+- `output`:可视化结果保存路径,默认为`./hubserving_result`
+
+访问示例:
+```bash
+python tools/test_hubserving.py --server_url=http://127.0.0.1:8868/predict/ocr_system --image_dir=./doc/imgs/ --visualize=false
+```
+
+## 4. 返回结果格式说明
+返回结果为列表(list),列表中的每一项为词典(dict),词典一共可能包含3种字段,信息如下:
+|字段名称|数据类型|意义|
+|---|---|---|
+|angle|str|文本角度|
+|text|str|文本内容|
+|confidence|float| 文本识别置信度或文本角度分类置信度|
+|text_region|list|文本位置坐标|
+|html|str|表格的html字符串|
+|regions|list|版面分析+表格识别+OCR的结果,每一项为一个list
包含表示区域坐标的`bbox`,区域类型的`type`和区域结果的`res`三个字段|
+|layout|list|版面分析的结果,每一项一个dict,包含版面区域坐标的`bbox`,区域类型的`label`|
+
+不同模块返回的字段不同,如,文本识别服务模块返回结果不含`text_region`字段,具体信息如下:
+|字段名/模块名 |ocr_det |ocr_cls |ocr_rec |ocr_system |structure_table |structure_system |structure_layout |kie_ser |kie_re |
+|--- |--- |--- |--- |--- |--- |--- |--- |--- |--- |
+|angle | |✔ | |✔ | | | |
+|text | | |✔ |✔ | |✔ | |✔ |✔ |
+|confidence | |✔ |✔ |✔ | |✔ | |✔ |✔ |
+|text_region |✔ | | |✔ | |✔ | |✔ |✔ |
+|html | | | | |✔ |✔ | | | |
+|regions | | | | |✔ |✔ | | | |
+|layout | | | | | | |✔ | | |
+|ser_res | | | | | | | |✔ | |
+|re_res | | | | | | | | |✔ |
+
+**说明:** 如果需要增加、删除、修改返回字段,可在相应模块的`module.py`文件中进行修改,完整流程参考下一节自定义修改服务模块。
+
+## 5. 自定义修改服务模块
+如果需要修改服务逻辑,一般需要操作以下步骤(以修改`deploy/hubserving/ocr_system`为例):
+
+1. 停止服务:
+ ```bash
+ hub serving stop --port/-p XXXX
+ ```
+2. 到`deploy/hubserving/ocr_system`下的`module.py`和`params.py`等文件中根据实际需求修改代码。
+
+ 例如,如果需要替换部署服务所用模型,则需要到`params.py`中修改模型路径参数`det_model_dir`和`rec_model_dir`,如果需要关闭文本方向分类器,则将参数`use_angle_cls`置为`False`
+
+ 当然,同时可能还需要修改其他相关参数,请根据实际情况修改调试。
+
+ **强烈建议修改后先直接运行`module.py`调试,能正确运行预测后再启动服务测试。**
+
+ **注意:** PPOCR-v3识别模型使用的图片输入shape为`3,48,320`,因此需要修改`params.py`中的`cfg.rec_image_shape = "3, 48, 320"`,如果不使用PPOCR-v3识别模型,则无需修改该参数。
+3. (可选)如果想要重命名模块需要更改`module.py`文件中的以下行:
+ - [`from deploy.hubserving.ocr_system.params import read_params`中的`ocr_system`](https://github.com/PaddlePaddle/PaddleOCR/blob/a923f35de57b5e378f8dd16e54d0a3e4f51267fd/deploy/hubserving/ocr_system/module.py#L35)
+ - [`name="ocr_system",`中的`ocr_system`](https://github.com/PaddlePaddle/PaddleOCR/blob/a923f35de57b5e378f8dd16e54d0a3e4f51267fd/deploy/hubserving/ocr_system/module.py#L39)
+4. (可选)可能需要删除`__pycache__`目录以强制刷新CPython缓存:
+ ```bash
+ find deploy/hubserving/ocr_system -name '__pycache__' -exec rm -r {} \;
+ ```
+5. 安装修改后的新服务包:
+ ```bash
+ hub install deploy/hubserving/ocr_system
+ ```
+6. 重新启动服务:
+ ```bash
+ hub serving start -m ocr_system
+ ```
diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/readme_en.md b/ocr_webapp/PaddleOCR/deploy/hubserving/readme_en.md
new file mode 100644
index 0000000000000000000000000000000000000000..034e2786ce5e45186a7c3257a8e63a5872de144a
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/hubserving/readme_en.md
@@ -0,0 +1,249 @@
+English | [简体中文](readme.md)
+
+- [Service deployment based on PaddleHub Serving](#service-deployment-based-on-paddlehub-serving)
+ - [1. Update](#1-update)
+ - [2. Quick start service](#2-quick-start-service)
+ - [2.1 Install PaddleHub](#21-install-paddlehub)
+ - [2.2 Download inference model](#22-download-inference-model)
+ - [2.3 Install Service Module](#23-install-service-module)
+ - [2.4 Start service](#24-start-service)
+ - [2.4.1 Start with command line parameters (CPU only)](#241-start-with-command-line-parameters-cpu-only)
+ - [2.4.2 Start with configuration file(CPU and GPU)](#242-start-with-configuration-filecpugpu)
+ - [3. Send prediction requests](#3-send-prediction-requests)
+ - [4. Returned result format](#4-returned-result-format)
+ - [5. User-defined service module modification](#5-user-defined-service-module-modification)
+
+PaddleOCR provides 2 service deployment methods:
+- Based on **PaddleHub Serving**: Code path is `./deploy/hubserving`. Please follow this tutorial.
+- Based on **PaddleServing**: Code path is `./deploy/pdserving`. Please refer to the [tutorial](../../deploy/pdserving/README.md) for usage.
+
+# Service deployment based on PaddleHub Serving
+
+The hubserving service deployment directory includes seven service packages: text detection, text angle class, text recognition, text detection+text angle class+text recognition three-stage series connection, layout analysis, table recognition, and PP-Structure. Please select the corresponding service package to install and start the service according to your needs. The directory is as follows:
+```
+deploy/hubserving/
+ └─ ocr_det text detection module service package
+ └─ ocr_cls text angle class module service package
+ └─ ocr_rec text recognition module service package
+ └─ ocr_system text detection+text angle class+text recognition three-stage series connection service package
+ └─ structure_layout layout analysis service package
+ └─ structure_table table recognition service package
+ └─ structure_system PP-Structure service package
+ └─ kie_ser KIE(SER) service package
+ └─ kie_ser_re KIE(SER+RE) service package
+```
+
+Each service pack contains 3 files. Take the 2-stage series connection service package as an example, the directory is as follows:
+```
+deploy/hubserving/ocr_system/
+ └─ __init__.py Empty file, required
+ └─ config.json Configuration file, optional, passed in as a parameter when using configuration to start the service
+ └─ module.py Main module file, required, contains the complete logic of the service
+ └─ params.py Parameter file, required, including parameters such as model path, pre and post-processing parameters
+```
+## 1. Update
+
+* 2022.10.09 add KIE services.
+* 2022.08.23 add layout analysis services.
+* 2022.03.30 add PP-Structure and table recognition services.
+* 2022.05.05 add PP-OCRv3 text detection and recognition services.
+
+## 2. Quick start service
+The following steps take the 2-stage series service as an example. If only the detection service or recognition service is needed, replace the corresponding file path.
+
+### 2.1 Install PaddleHub
+```bash
+pip3 install paddlehub==2.1.0 --upgrade
+```
+
+### 2.2 Download inference model
+Before installing the service module, you need to prepare the inference model and put it in the correct path. By default, the PP-OCRv3 models are used, and the default model path is:
+| Model | Path |
+| ------- | - |
+| text detection model | ./inference/ch_PP-OCRv3_det_infer/ |
+| text recognition model | ./inference/ch_PP-OCRv3_rec_infer/ |
+| text angle classifier | ./inference/ch_ppocr_mobile_v2.0_cls_infer/ |
+| layout parse model | ./inference/picodet_lcnet_x1_0_fgd_layout_infer/ |
+| tanle recognition | ./inference/ch_ppstructure_mobile_v2.0_SLANet_infer/ |
+| KIE(SER) | ./inference/ser_vi_layoutxlm_xfund_infer/ |
+| KIE(SER+RE) | ./inference/re_vi_layoutxlm_xfund_infer/ |
+
+**The model path can be found and modified in `params.py`.**
+More models provided by PaddleOCR can be obtained from the [model library](../../doc/doc_en/models_list_en.md). You can also use models trained by yourself.
+
+### 2.3 Install Service Module
+PaddleOCR provides 5 kinds of service modules, install the required modules according to your needs.
+
+* On the Linux platform(replace `/` with `\` if using Windows), the examples are as the following table:
+| Service model | Command |
+| text detection | `hub install deploy/hubserving/ocr_det` |
+| text angle class: | `hub install deploy/hubserving/ocr_cls` |
+| text recognition: | `hub install deploy/hubserving/ocr_rec` |
+| 2-stage series: | `hub install deploy/hubserving/ocr_system` |
+| table recognition | `hub install deploy/hubserving/structure_table` |
+| PP-Structure | `hub install deploy/hubserving/structure_system` |
+| KIE(SER) | `hub install deploy/hubserving/kie_ser` |
+| KIE(SER+RE) | `hub install deploy/hubserving/kie_ser_re` |
+
+### 2.4 Start service
+#### 2.4.1 Start with command line parameters (CPU only)
+**start command:**
+```bash
+hub serving start --modules Module1==Version1, Module2==Version2, ... \
+ --port 8866 \
+ --use_multiprocess \
+ --workers \
+```
+
+**Parameters:**
+|parameters|usage|
+|---|---|
+|`--modules`/`-m`|PaddleHub Serving pre-installed model, listed in the form of multiple Module==Version key-value pairs
**When Version is not specified, the latest version is selected by default**|
+|`--port`/`-p`|Service port, default is 8866|
+|`--use_multiprocess`|Enable concurrent mode, by default using the single-process mode, this mode is recommended for multi-core CPU machines
**Windows operating system only supports single-process mode**|
+|`--workers`|The number of concurrent tasks specified in concurrent mode, the default is `2*cpu_count-1`, where `cpu_count` is the number of CPU cores|
+
+For example, start the 2-stage series service:
+```bash
+hub serving start -m ocr_system
+```
+
+This completes the deployment of a service API, using the default port number 8866.
+
+#### 2.4.2 Start with configuration file(CPU and GPU)
+**start command:**
+```bash
+hub serving start --config/-c config.json
+```
+
+In which the format of `config.json` is as follows:
+```json
+{
+ "modules_info": {
+ "ocr_system": {
+ "init_args": {
+ "version": "1.0.0",
+ "use_gpu": true
+ },
+ "predict_args": {
+ }
+ }
+ },
+ "port": 8868,
+ "use_multiprocess": false,
+ "workers": 2
+}
+```
+- The configurable parameters in `init_args` are consistent with the `_initialize` function interface in `module.py`.
+
+ **When `use_gpu` is `true`, it means that the GPU is used to start the service**.
+- The configurable parameters in `predict_args` are consistent with the `predict` function interface in `module.py`.
+
+ **Note:**
+ - When using the configuration file to start the service, other parameters will be ignored.
+ - If you use GPU prediction (that is, `use_gpu` is set to `true`), you need to set the environment variable CUDA_VISIBLE_DEVICES before starting the service, such as:
+ ```bash
+ export CUDA_VISIBLE_DEVICES=0
+ ```
+ - **`use_gpu` and `use_multiprocess` cannot be `true` at the same time.**
+
+For example, use GPU card No. 3 to start the 2-stage series service:
+```bash
+export CUDA_VISIBLE_DEVICES=3
+hub serving start -c deploy/hubserving/ocr_system/config.json
+```
+
+## 3. Send prediction requests
+After the service starts, you can use the following command to send a prediction request to obtain the prediction result:
+```bash
+python tools/test_hubserving.py --server_url=server_url --image_dir=image_path
+```
+
+Two parameters need to be passed to the script:
+- **server_url**:service address, the format of which is
+`http://[ip_address]:[port]/predict/[module_name]`
+
+ For example, if using the configuration file to start the text angle classification, text detection, text recognition, detection+classification+recognition 3 stages, table recognition and PP-Structure service,
+
+ also modified the port for each service, then the `server_url` to send the request will be:
+
+ ```
+ http://127.0.0.1:8865/predict/ocr_det
+ http://127.0.0.1:8866/predict/ocr_cls
+ http://127.0.0.1:8867/predict/ocr_rec
+ http://127.0.0.1:8868/predict/ocr_system
+ http://127.0.0.1:8869/predict/structure_table
+ http://127.0.0.1:8870/predict/structure_system
+ http://127.0.0.1:8870/predict/structure_layout
+ http://127.0.0.1:8871/predict/kie_ser
+ http://127.0.0.1:8872/predict/kie_ser_re
+ ```
+- **image_dir**:Test image path, which can be a single image path or an image directory path
+- **visualize**:Whether to visualize the results, the default value is False
+- **output**:The folder to save the Visualization result, the default value is `./hubserving_result`
+
+Example:
+```bash
+python tools/test_hubserving.py --server_url=http://127.0.0.1:8868/predict/ocr_system --image_dir=./doc/imgs/ --visualize=false`
+```
+
+## 4. Returned result format
+The returned result is a list. Each item in the list is a dictionary which may contain three fields. The information is as follows:
+
+|field name|data type|description|
+|----|----|----|
+|angle|str|angle|
+|text|str|text content|
+|confidence|float|text recognition confidence|
+|text_region|list|text location coordinates|
+|html|str|table HTML string|
+|regions|list|The result of layout analysis + table recognition + OCR, each item is a list
including `bbox` indicating area coordinates, `type` of area type and `res` of area results|
+|layout|list|The result of layout analysis, each item is a dict, including `bbox` indicating area coordinates, `label` of area type|
+
+The fields returned by different modules are different. For example, the results returned by the text recognition service module do not contain `text_region`, detailed table is as follows:
+
+|field name/module name |ocr_det |ocr_cls |ocr_rec |ocr_system |structure_table |structure_system |structure_layout |kie_ser |kie_re |
+|--- |--- |--- |--- |--- |--- |--- |--- |--- |--- |
+|angle | |✔ | |✔ | | | |
+|text | | |✔ |✔ | |✔ | |✔ |✔ |
+|confidence | |✔ |✔ |✔ | |✔ | |✔ |✔ |
+|text_region |✔ | | |✔ | |✔ | |✔ |✔ |
+|html | | | | |✔ |✔ | | | |
+|regions | | | | |✔ |✔ | | | |
+|layout | | | | | | |✔ | | |
+|ser_res | | | | | | | |✔ | |
+|re_res | | | | | | | | |✔ |
+
+**Note:** If you need to add, delete or modify the returned fields, you can modify the file `module.py` of the corresponding module. For the complete process, refer to the user-defined modification service module in the next section.
+
+## 5. User-defined service module modification
+If you need to modify the service logic, the following steps are generally required (take the modification of `deploy/hubserving/ocr_system` for example):
+
+1. Stop service:
+```bash
+hub serving stop --port/-p XXXX
+```
+2. Modify the code in the corresponding files under `deploy/hubserving/ocr_system`, such as `module.py` and `params.py`, to your actual needs.
+
+ For example, if you need to replace the model used by the deployed service, you need to modify model path parameters `det_model_dir` and `rec_model_dir` in `params.py`. If you want to turn off the text direction classifier, set the parameter `use_angle_cls` to `False`.
+
+ Of course, other related parameters may need to be modified at the same time. Please modify and debug according to the actual situation.
+
+ **It is suggested to run `module.py` directly for debugging after modification before starting the service test.**
+
+ **Note** The image input shape used by the PPOCR-v3 recognition model is `3, 48, 320`, so you need to modify `cfg.rec_image_shape = "3, 48, 320"` in `params.py`, if you do not use the PPOCR-v3 recognition model, then there is no need to modify this parameter.
+3. (Optional) If you want to rename the module, the following lines should be modified:
+ - [`ocr_system` within `from deploy.hubserving.ocr_system.params import read_params`](https://github.com/PaddlePaddle/PaddleOCR/blob/a923f35de57b5e378f8dd16e54d0a3e4f51267fd/deploy/hubserving/ocr_system/module.py#L35)
+ - [`ocr_system` within `name="ocr_system",`](https://github.com/PaddlePaddle/PaddleOCR/blob/a923f35de57b5e378f8dd16e54d0a3e4f51267fd/deploy/hubserving/ocr_system/module.py#L39)
+4. (Optional) It may require you to delete the directory `__pycache__` to force flush build cache of CPython:
+ ```bash
+ find deploy/hubserving/ocr_system -name '__pycache__' -exec rm -r {} \;
+ ```
+5. Install modified service module:
+ ```bash
+ hub install deploy/hubserving/ocr_system/
+ ```
+6. Restart service:
+ ```bash
+ hub serving start -m ocr_system
+ ```
diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/structure_layout/__init__.py b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_layout/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..c747d3e7aeca842933e083dffc01ef1fba3f4e85
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_layout/__init__.py
@@ -0,0 +1,13 @@
+# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
\ No newline at end of file
diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/structure_layout/config.json b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_layout/config.json
new file mode 100644
index 0000000000000000000000000000000000000000..bc52c1ab603d5659f90a5ed8a72cdb06638fb9e5
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_layout/config.json
@@ -0,0 +1,16 @@
+{
+ "modules_info": {
+ "structure_layout": {
+ "init_args": {
+ "version": "1.0.0",
+ "use_gpu": true
+ },
+ "predict_args": {
+ }
+ }
+ },
+ "port": 8871,
+ "use_multiprocess": false,
+ "workers": 2
+}
+
diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/structure_layout/module.py b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_layout/module.py
new file mode 100644
index 0000000000000000000000000000000000000000..7091f123fc0039e4886d8763096952d7c445184c
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_layout/module.py
@@ -0,0 +1,143 @@
+# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+import os
+import sys
+sys.path.insert(0, ".")
+import copy
+
+import time
+import paddlehub
+from paddlehub.common.logger import logger
+from paddlehub.module.module import moduleinfo, runnable, serving
+import cv2
+import paddlehub as hub
+
+from tools.infer.utility import base64_to_cv2
+from ppstructure.layout.predict_layout import LayoutPredictor as _LayoutPredictor
+from ppstructure.utility import parse_args
+from deploy.hubserving.structure_layout.params import read_params
+
+
+@moduleinfo(
+ name="structure_layout",
+ version="1.0.0",
+ summary="PP-Structure layout service",
+ author="paddle-dev",
+ author_email="paddle-dev@baidu.com",
+ type="cv/structure_layout")
+class LayoutPredictor(hub.Module):
+ def _initialize(self, use_gpu=False, enable_mkldnn=False):
+ """
+ initialize with the necessary elements
+ """
+ cfg = self.merge_configs()
+ cfg.use_gpu = use_gpu
+ if use_gpu:
+ try:
+ _places = os.environ["CUDA_VISIBLE_DEVICES"]
+ int(_places[0])
+ print("use gpu: ", use_gpu)
+ print("CUDA_VISIBLE_DEVICES: ", _places)
+ cfg.gpu_mem = 8000
+ except:
+ raise RuntimeError(
+ "Environment Variable CUDA_VISIBLE_DEVICES is not set correctly. If you wanna use gpu, please set CUDA_VISIBLE_DEVICES via export CUDA_VISIBLE_DEVICES=cuda_device_id."
+ )
+ cfg.ir_optim = True
+ cfg.enable_mkldnn = enable_mkldnn
+
+ self.layout_predictor = _LayoutPredictor(cfg)
+
+ def merge_configs(self):
+ # deafult cfg
+ backup_argv = copy.deepcopy(sys.argv)
+ sys.argv = sys.argv[:1]
+ cfg = parse_args()
+
+ update_cfg_map = vars(read_params())
+
+ for key in update_cfg_map:
+ cfg.__setattr__(key, update_cfg_map[key])
+
+ sys.argv = copy.deepcopy(backup_argv)
+ return cfg
+
+ def read_images(self, paths=[]):
+ images = []
+ for img_path in paths:
+ assert os.path.isfile(
+ img_path), "The {} isn't a valid file.".format(img_path)
+ img = cv2.imread(img_path)
+ if img is None:
+ logger.info("error in loading image:{}".format(img_path))
+ continue
+ images.append(img)
+ return images
+
+ def predict(self, images=[], paths=[]):
+ """
+ Get the chinese texts in the predicted images.
+ Args:
+ images (list(numpy.ndarray)): images data, shape of each is [H, W, C]. If images not paths
+ paths (list[str]): The paths of images. If paths not images
+ Returns:
+ res (list): The layout results of images.
+ """
+
+ if images != [] and isinstance(images, list) and paths == []:
+ predicted_data = images
+ elif images == [] and isinstance(paths, list) and paths != []:
+ predicted_data = self.read_images(paths)
+ else:
+ raise TypeError("The input data is inconsistent with expectations.")
+
+ assert predicted_data != [], "There is not any image to be predicted. Please check the input data."
+
+ all_results = []
+ for img in predicted_data:
+ if img is None:
+ logger.info("error in loading image")
+ all_results.append([])
+ continue
+ starttime = time.time()
+ res, _ = self.layout_predictor(img)
+ elapse = time.time() - starttime
+ logger.info("Predict time: {}".format(elapse))
+
+ for item in res:
+ item['bbox'] = item['bbox'].tolist()
+ all_results.append({'layout': res})
+ return all_results
+
+ @serving
+ def serving_method(self, images, **kwargs):
+ """
+ Run as a service.
+ """
+ images_decode = [base64_to_cv2(image) for image in images]
+ results = self.predict(images_decode, **kwargs)
+ return results
+
+
+if __name__ == '__main__':
+ layout = LayoutPredictor()
+ layout._initialize()
+ image_path = ['./ppstructure/docs/table/1.png']
+ res = layout.predict(paths=image_path)
+ print(res)
diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/structure_layout/params.py b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_layout/params.py
new file mode 100644
index 0000000000000000000000000000000000000000..448b66ac42dac555f084299f525ee9e91ad481d8
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_layout/params.py
@@ -0,0 +1,32 @@
+# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+
+class Config(object):
+ pass
+
+
+def read_params():
+ cfg = Config()
+
+ # params for layout analysis
+ cfg.layout_model_dir = './inference/picodet_lcnet_x1_0_fgd_layout_infer/'
+ cfg.layout_dict_path = './ppocr/utils/dict/layout_dict/layout_publaynet_dict.txt'
+ cfg.layout_score_threshold = 0.5
+ cfg.layout_nms_threshold = 0.5
+ return cfg
diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/structure_system/__init__.py b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_system/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..c747d3e7aeca842933e083dffc01ef1fba3f4e85
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_system/__init__.py
@@ -0,0 +1,13 @@
+# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
\ No newline at end of file
diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/structure_system/config.json b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_system/config.json
new file mode 100644
index 0000000000000000000000000000000000000000..642aa94a2a25759469f74280f6aab9a2495f493f
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_system/config.json
@@ -0,0 +1,16 @@
+{
+ "modules_info": {
+ "structure_system": {
+ "init_args": {
+ "version": "1.0.0",
+ "use_gpu": true
+ },
+ "predict_args": {
+ }
+ }
+ },
+ "port": 8870,
+ "use_multiprocess": false,
+ "workers": 2
+}
+
diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/structure_system/module.py b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_system/module.py
new file mode 100644
index 0000000000000000000000000000000000000000..61c93bb146ab11998bc7ed3350cb2686b73e3d3b
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_system/module.py
@@ -0,0 +1,149 @@
+# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+import os
+import sys
+sys.path.insert(0, ".")
+import copy
+
+import time
+import paddlehub
+from paddlehub.common.logger import logger
+from paddlehub.module.module import moduleinfo, runnable, serving
+import cv2
+import numpy as np
+import paddlehub as hub
+
+from tools.infer.utility import base64_to_cv2
+from ppstructure.predict_system import StructureSystem as PPStructureSystem
+from ppstructure.predict_system import save_structure_res
+from ppstructure.utility import parse_args
+from deploy.hubserving.structure_system.params import read_params
+
+
+@moduleinfo(
+ name="structure_system",
+ version="1.0.0",
+ summary="PP-Structure system service",
+ author="paddle-dev",
+ author_email="paddle-dev@baidu.com",
+ type="cv/structure_system")
+class StructureSystem(hub.Module):
+ def _initialize(self, use_gpu=False, enable_mkldnn=False):
+ """
+ initialize with the necessary elements
+ """
+ cfg = self.merge_configs()
+
+ cfg.use_gpu = use_gpu
+ if use_gpu:
+ try:
+ _places = os.environ["CUDA_VISIBLE_DEVICES"]
+ int(_places[0])
+ print("use gpu: ", use_gpu)
+ print("CUDA_VISIBLE_DEVICES: ", _places)
+ cfg.gpu_mem = 8000
+ except:
+ raise RuntimeError(
+ "Environment Variable CUDA_VISIBLE_DEVICES is not set correctly. If you wanna use gpu, please set CUDA_VISIBLE_DEVICES via export CUDA_VISIBLE_DEVICES=cuda_device_id."
+ )
+ cfg.ir_optim = True
+ cfg.enable_mkldnn = enable_mkldnn
+
+ self.table_sys = PPStructureSystem(cfg)
+
+ def merge_configs(self):
+ # deafult cfg
+ backup_argv = copy.deepcopy(sys.argv)
+ sys.argv = sys.argv[:1]
+ cfg = parse_args()
+
+ update_cfg_map = vars(read_params())
+
+ for key in update_cfg_map:
+ cfg.__setattr__(key, update_cfg_map[key])
+
+ sys.argv = copy.deepcopy(backup_argv)
+ return cfg
+
+ def read_images(self, paths=[]):
+ images = []
+ for img_path in paths:
+ assert os.path.isfile(
+ img_path), "The {} isn't a valid file.".format(img_path)
+ img = cv2.imread(img_path)
+ if img is None:
+ logger.info("error in loading image:{}".format(img_path))
+ continue
+ images.append(img)
+ return images
+
+ def predict(self, images=[], paths=[]):
+ """
+ Get the chinese texts in the predicted images.
+ Args:
+ images (list(numpy.ndarray)): images data, shape of each is [H, W, C]. If images not paths
+ paths (list[str]): The paths of images. If paths not images
+ Returns:
+ res (list): The result of chinese texts and save path of images.
+ """
+
+ if images != [] and isinstance(images, list) and paths == []:
+ predicted_data = images
+ elif images == [] and isinstance(paths, list) and paths != []:
+ predicted_data = self.read_images(paths)
+ else:
+ raise TypeError("The input data is inconsistent with expectations.")
+
+ assert predicted_data != [], "There is not any image to be predicted. Please check the input data."
+
+ all_results = []
+ for img in predicted_data:
+ if img is None:
+ logger.info("error in loading image")
+ all_results.append([])
+ continue
+ starttime = time.time()
+ res, _ = self.table_sys(img)
+ elapse = time.time() - starttime
+ logger.info("Predict time: {}".format(elapse))
+
+ # parse result
+ res_final = []
+ for region in res:
+ region.pop('img')
+ res_final.append(region)
+ all_results.append({'regions': res_final})
+ return all_results
+
+ @serving
+ def serving_method(self, images, **kwargs):
+ """
+ Run as a service.
+ """
+ images_decode = [base64_to_cv2(image) for image in images]
+ results = self.predict(images_decode, **kwargs)
+ return results
+
+
+if __name__ == '__main__':
+ structure_system = StructureSystem()
+ structure_system._initialize()
+ image_path = ['./ppstructure/docs/table/1.png']
+ res = structure_system.predict(paths=image_path)
+ print(res)
diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/structure_system/params.py b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_system/params.py
new file mode 100644
index 0000000000000000000000000000000000000000..fe691fbc2d172cc1ad32115abd5a4ee850d8ab2e
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_system/params.py
@@ -0,0 +1,33 @@
+# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+from deploy.hubserving.structure_table.params import read_params as table_read_params
+
+
+def read_params():
+ cfg = table_read_params()
+
+ # params for layout parser model
+ cfg.layout_model_dir = ''
+ cfg.layout_dict_path = './ppocr/utils/dict/layout_publaynet_dict.txt'
+ cfg.layout_score_threshold = 0.5
+ cfg.layout_nms_threshold = 0.5
+
+ cfg.mode = 'structure'
+ cfg.output = './output'
+ return cfg
diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/structure_table/__init__.py b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_table/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..c747d3e7aeca842933e083dffc01ef1fba3f4e85
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_table/__init__.py
@@ -0,0 +1,13 @@
+# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
\ No newline at end of file
diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/structure_table/config.json b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_table/config.json
new file mode 100644
index 0000000000000000000000000000000000000000..d0e3cb1523daaec85cc0dc6099969fcfb380184b
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_table/config.json
@@ -0,0 +1,16 @@
+{
+ "modules_info": {
+ "structure_table": {
+ "init_args": {
+ "version": "1.0.0",
+ "use_gpu": true
+ },
+ "predict_args": {
+ }
+ }
+ },
+ "port": 8869,
+ "use_multiprocess": false,
+ "workers": 2
+}
+
diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/structure_table/module.py b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_table/module.py
new file mode 100644
index 0000000000000000000000000000000000000000..b4432b2d7b8764bc0327e7b12fe7887530e825c4
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_table/module.py
@@ -0,0 +1,143 @@
+# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+import os
+import sys
+sys.path.insert(0, ".")
+import copy
+
+import time
+import paddlehub
+from paddlehub.common.logger import logger
+from paddlehub.module.module import moduleinfo, runnable, serving
+import cv2
+import numpy as np
+import paddlehub as hub
+
+from tools.infer.utility import base64_to_cv2
+from ppstructure.table.predict_table import TableSystem as _TableSystem
+from ppstructure.predict_system import save_structure_res
+from ppstructure.utility import parse_args
+from deploy.hubserving.structure_table.params import read_params
+
+
+@moduleinfo(
+ name="structure_table",
+ version="1.0.0",
+ summary="PP-Structure table service",
+ author="paddle-dev",
+ author_email="paddle-dev@baidu.com",
+ type="cv/structure_table")
+class TableSystem(hub.Module):
+ def _initialize(self, use_gpu=False, enable_mkldnn=False):
+ """
+ initialize with the necessary elements
+ """
+ cfg = self.merge_configs()
+ cfg.use_gpu = use_gpu
+ if use_gpu:
+ try:
+ _places = os.environ["CUDA_VISIBLE_DEVICES"]
+ int(_places[0])
+ print("use gpu: ", use_gpu)
+ print("CUDA_VISIBLE_DEVICES: ", _places)
+ cfg.gpu_mem = 8000
+ except:
+ raise RuntimeError(
+ "Environment Variable CUDA_VISIBLE_DEVICES is not set correctly. If you wanna use gpu, please set CUDA_VISIBLE_DEVICES via export CUDA_VISIBLE_DEVICES=cuda_device_id."
+ )
+ cfg.ir_optim = True
+ cfg.enable_mkldnn = enable_mkldnn
+
+ self.table_sys = _TableSystem(cfg)
+
+ def merge_configs(self):
+ # deafult cfg
+ backup_argv = copy.deepcopy(sys.argv)
+ sys.argv = sys.argv[:1]
+ cfg = parse_args()
+
+ update_cfg_map = vars(read_params())
+
+ for key in update_cfg_map:
+ cfg.__setattr__(key, update_cfg_map[key])
+
+ sys.argv = copy.deepcopy(backup_argv)
+ return cfg
+
+ def read_images(self, paths=[]):
+ images = []
+ for img_path in paths:
+ assert os.path.isfile(
+ img_path), "The {} isn't a valid file.".format(img_path)
+ img = cv2.imread(img_path)
+ if img is None:
+ logger.info("error in loading image:{}".format(img_path))
+ continue
+ images.append(img)
+ return images
+
+ def predict(self, images=[], paths=[]):
+ """
+ Get the chinese texts in the predicted images.
+ Args:
+ images (list(numpy.ndarray)): images data, shape of each is [H, W, C]. If images not paths
+ paths (list[str]): The paths of images. If paths not images
+ Returns:
+ res (list): The result of chinese texts and save path of images.
+ """
+
+ if images != [] and isinstance(images, list) and paths == []:
+ predicted_data = images
+ elif images == [] and isinstance(paths, list) and paths != []:
+ predicted_data = self.read_images(paths)
+ else:
+ raise TypeError("The input data is inconsistent with expectations.")
+
+ assert predicted_data != [], "There is not any image to be predicted. Please check the input data."
+
+ all_results = []
+ for img in predicted_data:
+ if img is None:
+ logger.info("error in loading image")
+ all_results.append([])
+ continue
+ starttime = time.time()
+ res, _ = self.table_sys(img)
+ elapse = time.time() - starttime
+ logger.info("Predict time: {}".format(elapse))
+
+ all_results.append({'html': res['html']})
+ return all_results
+
+ @serving
+ def serving_method(self, images, **kwargs):
+ """
+ Run as a service.
+ """
+ images_decode = [base64_to_cv2(image) for image in images]
+ results = self.predict(images_decode, **kwargs)
+ return results
+
+
+if __name__ == '__main__':
+ table_system = TableSystem()
+ table_system._initialize()
+ image_path = ['./ppstructure/docs/table/table.jpg']
+ res = table_system.predict(paths=image_path)
+ print(res)
diff --git a/ocr_webapp/PaddleOCR/deploy/hubserving/structure_table/params.py b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_table/params.py
new file mode 100644
index 0000000000000000000000000000000000000000..9632c2f70b794854d191e9f088f3f2e301a5dbb3
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/hubserving/structure_table/params.py
@@ -0,0 +1,30 @@
+# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+from deploy.hubserving.ocr_system.params import read_params as pp_ocr_read_params
+
+
+def read_params():
+ cfg = pp_ocr_read_params()
+
+ # params for table structure model
+ cfg.table_max_len = 488
+ cfg.table_model_dir = './inference/en_ppocr_mobile_v2.0_table_structure_infer/'
+ cfg.table_char_dict_path = './ppocr/utils/dict/table_structure_dict.txt'
+ cfg.show_log = False
+ return cfg
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/.gitignore b/ocr_webapp/PaddleOCR/deploy/ios_demo/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..66434d5ef4dce7cab1a3ed721df4e8e5bd529e8e
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/.gitignore
@@ -0,0 +1,21 @@
+# CocoaPods
+Pods/
+
+# Xcode
+PaddleOCRDemo.xcworkspace/xcuserdata/
+PaddleOCRDemo.xcodeproj/xcuserdata/
+PaddleOCRDemo.xcodeproj/project.xcworkspace/xcuserdata/
+*.xcuserstate
+DerivedData/
+build/
+
+# fetch_ios_demo_models.sh download cache
+.fetch_ios_demo_models_work/
+
+# Models — ignore all files under Models/; track only `.gitkeep` so det/, rec/, … stay in the tree
+PaddleOCRDemo/Models/**
+!PaddleOCRDemo/Models/*/
+!PaddleOCRDemo/Models/*/.gitkeep
+
+# Local validation reports
+out/
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/NOTICE b/ocr_webapp/PaddleOCR/deploy/ios_demo/NOTICE
new file mode 100644
index 0000000000000000000000000000000000000000..6545fc413c0f1ce4522205cd798698f88486d948
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/NOTICE
@@ -0,0 +1,25 @@
+PaddleOCR iOS demo — third-party notices
+===============================================================================
+
+This demo application is part of the PaddleOCR project (Apache License 2.0).
+It may bundle or link third-party components in addition to the PaddleOCR
+repository LICENSE. The following components are included in **source** form
+in this tree:
+
+Clipper / polyclipping (polygon clipping and offsetting)
+--------------------------------------------------------------------------------------------
+ • Upstream: polyclipping clipper 6.4.2 (Angus Johnson)
+ • Bundled: `PaddleOCRDemo/ThirdParty/Clipper1/` (clipper.cpp, clipper.hpp)
+ • SPDX: BSL-1.0 (Boost Software License 1.0)
+ • Full text: `PaddleOCRDemo/ThirdParty/Clipper1/LICENSE`
+
+CocoaPods dependencies (onnxruntime-objc, OpenCV, Yams, etc.)
+-------------------------------------------------------------
+ • Versions and upstream packages are recorded in `Podfile.lock` after
+ `pod install`. Each pod is subject to its own license as published by
+ the respective project; consult the Pods project and upstream repos
+ for notices required by those licenses.
+
+If you distribute binaries built from this demo, ensure your product’s
+open-source / third-party attribution (e.g. in-app “Licenses” or
+distribution packaging) satisfies your legal and policy requirements.
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo.xcodeproj/project.pbxproj b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo.xcodeproj/project.pbxproj
new file mode 100644
index 0000000000000000000000000000000000000000..a30617317d915c46dc5fa63ee598937457ce248f
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo.xcodeproj/project.pbxproj
@@ -0,0 +1,822 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 56;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 6F50EE661821421A98371746 /* Pods_PaddleOCRDemoTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0A24555277519FE15EF10A5D /* Pods_PaddleOCRDemoTests.framework */; };
+ A1000001 /* PaddleOCRDemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000001 /* PaddleOCRDemoApp.swift */; };
+ A1000002 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000002 /* ContentView.swift */; };
+ A1000003 /* Models in Resources */ = {isa = PBXBuildFile; fileRef = A2000006 /* Models */; };
+ A1000004 /* ModelConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000011 /* ModelConfig.swift */; };
+ A1000005 /* ORTSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000012 /* ORTSessionManager.swift */; };
+ A10000F0 /* ORTProfilingBridge.mm in Sources */ = {isa = PBXBuildFile; fileRef = A20000F1 /* ORTProfilingBridge.mm */; };
+ A1000007 /* InferenceConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000015 /* InferenceConfig.swift */; };
+ A1000008 /* Preprocessing.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000016 /* Preprocessing.swift */; };
+ A1000011 /* ORTSessionManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000014 /* ORTSessionManagerTests.swift */; };
+ A1000021 /* DBPostProcess.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000021 /* DBPostProcess.swift */; };
+ A1000022 /* OpenCVDBBridge.mm in Sources */ = {isa = PBXBuildFile; fileRef = A2000023 /* OpenCVDBBridge.mm */; };
+ A1000023 /* OpenCVImageBridge.mm in Sources */ = {isa = PBXBuildFile; fileRef = A2000025 /* OpenCVImageBridge.mm */; };
+ A10000E1 /* clipper.cpp in Sources */ = {isa = PBXBuildFile; fileRef = A20000E1 /* clipper.cpp */; };
+ A10000E2 /* PDBPolygonOffsetBridge.mm in Sources */ = {isa = PBXBuildFile; fileRef = A20000E2 /* PDBPolygonOffsetBridge.mm */; };
+ A10000C0 /* DemoSectionChrome.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000C0 /* DemoSectionChrome.swift */; };
+ A1000030 /* DetectionEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000030 /* DetectionEngine.swift */; };
+ A1000040 /* RecPreprocessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000040 /* RecPreprocessor.swift */; };
+ A1000050 /* CTCDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000050 /* CTCDecoder.swift */; };
+ A1000060 /* RecognitionEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000060 /* RecognitionEngine.swift */; };
+ A1000070 /* OCREngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000070 /* OCREngine.swift */; };
+ A1000071 /* BoxSorter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000071 /* BoxSorter.swift */; };
+ A1000072 /* QuadTextCrop.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000072 /* QuadTextCrop.swift */; };
+ A1000074 /* EncodedImageCodec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000074 /* EncodedImageCodec.swift */; };
+ A1000080 /* OCRViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000080 /* OCRViewModel.swift */; };
+ A1000090 /* ImagePickerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000090 /* ImagePickerSection.swift */; };
+ A1000091 /* SampleImages in Resources */ = {isa = PBXBuildFile; fileRef = A2000091 /* SampleImages */; };
+ A10000A0 /* ResultImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000A0 /* ResultImageView.swift */; };
+ A10000A1 /* TimingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000A1 /* TimingView.swift */; };
+ A10000A2 /* ResultsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000A2 /* ResultsListView.swift */; };
+ A10000A3 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000A3 /* ErrorView.swift */; };
+ A10000A4 /* OCRParametersPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000A4 /* OCRParametersPanel.swift */; };
+ A10000A5 /* OnnxRuntimeSettingsPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000A5 /* OnnxRuntimeSettingsPanel.swift */; };
+ A10000A6 /* EngineSettingsSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000A6 /* EngineSettingsSheet.swift */; };
+ A10000B3 /* OCRParameterResolverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000B3 /* OCRParameterResolverTests.swift */; };
+ A10000C5 /* OCRBenchmarkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000C5 /* OCRBenchmarkTests.swift */; };
+ A10000C6 /* BenchmarkPayloads.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000C6 /* BenchmarkPayloads.swift */; };
+ A10000D3 /* BenchmarkFixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000D3 /* BenchmarkFixtures.swift */; };
+ A10000C7 /* Fixtures in Resources */ = {isa = PBXBuildFile; fileRef = A20000C7 /* Fixtures */; };
+ A10000B1 /* OCRParameterResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000B1 /* OCRParameterResolver.swift */; };
+ A10000D0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A20000D0 /* Assets.xcassets */; };
+ A10000D1 /* Bundle+BundledImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000D1 /* Bundle+BundledImage.swift */; };
+ A10000D2 /* DemoAssets.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000D2 /* DemoAssets.swift */; };
+ F9888FA77A2AF8AF45B50193 /* Pods_PaddleOCRDemo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3D0FFA11A4E43E5BD0F80219 /* Pods_PaddleOCRDemo.framework */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ A5000001 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = A3000001 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = A4000001;
+ remoteInfo = PaddleOCRDemo;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXFileReference section */
+ 0A24555277519FE15EF10A5D /* Pods_PaddleOCRDemoTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_PaddleOCRDemoTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 3D0FFA11A4E43E5BD0F80219 /* Pods_PaddleOCRDemo.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_PaddleOCRDemo.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 49C7CEE60686CC98BE2EEE45 /* Pods-PaddleOCRDemoTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PaddleOCRDemoTests.release.xcconfig"; path = "Target Support Files/Pods-PaddleOCRDemoTests/Pods-PaddleOCRDemoTests.release.xcconfig"; sourceTree = ""; };
+ 520BC3C496ED955BBB302DC4 /* Pods-PaddleOCRDemo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PaddleOCRDemo.debug.xcconfig"; path = "Target Support Files/Pods-PaddleOCRDemo/Pods-PaddleOCRDemo.debug.xcconfig"; sourceTree = ""; };
+ 54F64B7C830508805D832F8A /* Pods-PaddleOCRDemo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PaddleOCRDemo.release.xcconfig"; path = "Target Support Files/Pods-PaddleOCRDemo/Pods-PaddleOCRDemo.release.xcconfig"; sourceTree = ""; };
+ 72FF5C269BF861629F89D6B5 /* Pods-PaddleOCRDemoTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PaddleOCRDemoTests.debug.xcconfig"; path = "Target Support Files/Pods-PaddleOCRDemoTests/Pods-PaddleOCRDemoTests.debug.xcconfig"; sourceTree = ""; };
+ A2000001 /* PaddleOCRDemoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaddleOCRDemoApp.swift; sourceTree = ""; };
+ A2000002 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
+ A2000003 /* PaddleOCRDemo-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "PaddleOCRDemo-Bridging-Header.h"; sourceTree = ""; };
+ A2000004 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ A2000005 /* PaddleOCRDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PaddleOCRDemo.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ A2000006 /* Models */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Models; sourceTree = ""; };
+ A2000009 /* PaddleOCRDemoTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PaddleOCRDemoTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ A2000011 /* ModelConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelConfig.swift; sourceTree = ""; };
+ A2000012 /* ORTSessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ORTSessionManager.swift; sourceTree = ""; };
+ A20000F0 /* ORTProfilingBridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ORTProfilingBridge.h; sourceTree = ""; };
+ A20000F1 /* ORTProfilingBridge.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ORTProfilingBridge.mm; sourceTree = ""; };
+ A2000014 /* ORTSessionManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ORTSessionManagerTests.swift; sourceTree = ""; };
+ A2000015 /* InferenceConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InferenceConfig.swift; sourceTree = ""; };
+ A2000016 /* Preprocessing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preprocessing.swift; sourceTree = ""; };
+ A2000021 /* DBPostProcess.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBPostProcess.swift; sourceTree = ""; };
+ A2000022 /* OpenCVDBBridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OpenCVDBBridge.h; sourceTree = ""; };
+ A2000023 /* OpenCVDBBridge.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = OpenCVDBBridge.mm; sourceTree = ""; };
+ A2000024 /* OpenCVImageBridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OpenCVImageBridge.h; sourceTree = ""; };
+ A2000025 /* OpenCVImageBridge.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = OpenCVImageBridge.mm; sourceTree = ""; };
+ A20000E1 /* clipper.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = clipper.cpp; sourceTree = ""; };
+ A20000E2 /* PDBPolygonOffsetBridge.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = PDBPolygonOffsetBridge.mm; sourceTree = ""; };
+ A20000E3 /* PDBPolygonOffsetBridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PDBPolygonOffsetBridge.h; sourceTree = ""; };
+ A2000030 /* DetectionEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectionEngine.swift; sourceTree = ""; };
+ A2000040 /* RecPreprocessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecPreprocessor.swift; sourceTree = ""; };
+ A2000050 /* CTCDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CTCDecoder.swift; sourceTree = ""; };
+ A2000060 /* RecognitionEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecognitionEngine.swift; sourceTree = ""; };
+ A2000070 /* OCREngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCREngine.swift; sourceTree = ""; };
+ A2000071 /* BoxSorter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxSorter.swift; sourceTree = ""; };
+ A2000072 /* QuadTextCrop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuadTextCrop.swift; sourceTree = ""; };
+ A2000074 /* EncodedImageCodec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncodedImageCodec.swift; sourceTree = ""; };
+ A2000080 /* OCRViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCRViewModel.swift; sourceTree = ""; };
+ A2000090 /* ImagePickerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePickerSection.swift; sourceTree = ""; };
+ A2000091 /* SampleImages */ = {isa = PBXFileReference; lastKnownFileType = folder; path = SampleImages; sourceTree = ""; };
+ A20000A0 /* ResultImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultImageView.swift; sourceTree = ""; };
+ A20000A1 /* TimingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimingView.swift; sourceTree = ""; };
+ A20000A2 /* ResultsListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultsListView.swift; sourceTree = ""; };
+ A20000A3 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = ""; };
+ A20000A4 /* OCRParametersPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCRParametersPanel.swift; sourceTree = ""; };
+ A20000A5 /* OnnxRuntimeSettingsPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnnxRuntimeSettingsPanel.swift; sourceTree = ""; };
+ A20000A6 /* EngineSettingsSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EngineSettingsSheet.swift; sourceTree = ""; };
+ A20000B3 /* OCRParameterResolverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCRParameterResolverTests.swift; sourceTree = ""; };
+ A20000C5 /* OCRBenchmarkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCRBenchmarkTests.swift; sourceTree = ""; };
+ A20000C6 /* BenchmarkPayloads.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BenchmarkPayloads.swift; sourceTree = ""; };
+ A20000D3 /* BenchmarkFixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BenchmarkFixtures.swift; sourceTree = ""; };
+ A20000C7 /* Fixtures */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Fixtures; sourceTree = ""; };
+ A20000B1 /* OCRParameterResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCRParameterResolver.swift; sourceTree = ""; };
+ A20000C0 /* DemoSectionChrome.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoSectionChrome.swift; sourceTree = ""; };
+ A20000D0 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ A20000D1 /* Bundle+BundledImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+BundledImage.swift"; sourceTree = ""; };
+ A20000D2 /* DemoAssets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoAssets.swift; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ A6000001 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ F9888FA77A2AF8AF45B50193 /* Pods_PaddleOCRDemo.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ A6000002 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 6F50EE661821421A98371746 /* Pods_PaddleOCRDemoTests.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 2D2CDE3432BBDFC183DD82E7 /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 3D0FFA11A4E43E5BD0F80219 /* Pods_PaddleOCRDemo.framework */,
+ 0A24555277519FE15EF10A5D /* Pods_PaddleOCRDemoTests.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+ 3E7A5973A61F2EB2D5542DE3 /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ 520BC3C496ED955BBB302DC4 /* Pods-PaddleOCRDemo.debug.xcconfig */,
+ 54F64B7C830508805D832F8A /* Pods-PaddleOCRDemo.release.xcconfig */,
+ 72FF5C269BF861629F89D6B5 /* Pods-PaddleOCRDemoTests.debug.xcconfig */,
+ 49C7CEE60686CC98BE2EEE45 /* Pods-PaddleOCRDemoTests.release.xcconfig */,
+ );
+ path = Pods;
+ sourceTree = "";
+ };
+ A7000001 = {
+ isa = PBXGroup;
+ children = (
+ A7000002 /* PaddleOCRDemo */,
+ A7000005 /* PaddleOCRDemoTests */,
+ A7000003 /* Products */,
+ 3E7A5973A61F2EB2D5542DE3 /* Pods */,
+ 2D2CDE3432BBDFC183DD82E7 /* Frameworks */,
+ );
+ sourceTree = "";
+ };
+ A7000002 /* PaddleOCRDemo */ = {
+ isa = PBXGroup;
+ children = (
+ A7000004 /* App */,
+ A7000007 /* Engine */,
+ A7000008 /* ViewModels */,
+ A7000009 /* Views */,
+ A7000006 /* Supporting */,
+ A7000010 /* Resources */,
+ A700000D /* ThirdParty */,
+ A2000006 /* Models */,
+ );
+ path = PaddleOCRDemo;
+ sourceTree = "";
+ };
+ A7000003 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ A2000005 /* PaddleOCRDemo.app */,
+ A2000009 /* PaddleOCRDemoTests.xctest */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ A7000004 /* App */ = {
+ isa = PBXGroup;
+ children = (
+ A20000D1 /* Bundle+BundledImage.swift */,
+ A20000D2 /* DemoAssets.swift */,
+ A2000001 /* PaddleOCRDemoApp.swift */,
+ A2000002 /* ContentView.swift */,
+ A20000C0 /* DemoSectionChrome.swift */,
+ );
+ path = App;
+ sourceTree = "";
+ };
+ A7000005 /* PaddleOCRDemoTests */ = {
+ isa = PBXGroup;
+ children = (
+ A2000014 /* ORTSessionManagerTests.swift */,
+ A20000B3 /* OCRParameterResolverTests.swift */,
+ A20000C5 /* OCRBenchmarkTests.swift */,
+ A20000C6 /* BenchmarkPayloads.swift */,
+ A20000D3 /* BenchmarkFixtures.swift */,
+ A20000C7 /* Fixtures */,
+ );
+ path = PaddleOCRDemoTests;
+ sourceTree = "";
+ };
+ A7000006 /* Supporting */ = {
+ isa = PBXGroup;
+ children = (
+ A2000003 /* PaddleOCRDemo-Bridging-Header.h */,
+ A2000004 /* Info.plist */,
+ );
+ path = Supporting;
+ sourceTree = "";
+ };
+ A7000007 /* Engine */ = {
+ isa = PBXGroup;
+ children = (
+ A2000071 /* BoxSorter.swift */,
+ A2000050 /* CTCDecoder.swift */,
+ A2000021 /* DBPostProcess.swift */,
+ A2000022 /* OpenCVDBBridge.h */,
+ A2000023 /* OpenCVDBBridge.mm */,
+ A2000024 /* OpenCVImageBridge.h */,
+ A2000025 /* OpenCVImageBridge.mm */,
+ A20000E2 /* PDBPolygonOffsetBridge.mm */,
+ A20000E3 /* PDBPolygonOffsetBridge.h */,
+ A2000030 /* DetectionEngine.swift */,
+ A2000015 /* InferenceConfig.swift */,
+ A20000B1 /* OCRParameterResolver.swift */,
+ A2000011 /* ModelConfig.swift */,
+ A2000070 /* OCREngine.swift */,
+ A2000012 /* ORTSessionManager.swift */,
+ A20000F0 /* ORTProfilingBridge.h */,
+ A20000F1 /* ORTProfilingBridge.mm */,
+ A2000072 /* QuadTextCrop.swift */,
+ A2000074 /* EncodedImageCodec.swift */,
+ A2000016 /* Preprocessing.swift */,
+ A2000040 /* RecPreprocessor.swift */,
+ A2000060 /* RecognitionEngine.swift */,
+ );
+ path = Engine;
+ sourceTree = "";
+ };
+ A7000008 /* ViewModels */ = {
+ isa = PBXGroup;
+ children = (
+ A2000080 /* OCRViewModel.swift */,
+ );
+ path = ViewModels;
+ sourceTree = "";
+ };
+ A7000009 /* Views */ = {
+ isa = PBXGroup;
+ children = (
+ A20000A3 /* ErrorView.swift */,
+ A2000090 /* ImagePickerSection.swift */,
+ A20000A5 /* OnnxRuntimeSettingsPanel.swift */,
+ A20000A4 /* OCRParametersPanel.swift */,
+ A20000A6 /* EngineSettingsSheet.swift */,
+ A20000A0 /* ResultImageView.swift */,
+ A20000A2 /* ResultsListView.swift */,
+ A20000A1 /* TimingView.swift */,
+ );
+ path = Views;
+ sourceTree = "";
+ };
+ A700000D /* ThirdParty */ = {
+ isa = PBXGroup;
+ children = (
+ A700000E /* Clipper1 */,
+ );
+ path = ThirdParty;
+ sourceTree = "";
+ };
+ A700000E /* Clipper1 */ = {
+ isa = PBXGroup;
+ children = (
+ A20000E1 /* clipper.cpp */,
+ );
+ path = Clipper1;
+ sourceTree = "";
+ };
+ A7000010 /* Resources */ = {
+ isa = PBXGroup;
+ children = (
+ A20000D0 /* Assets.xcassets */,
+ A2000091 /* SampleImages */,
+ );
+ path = Resources;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ A4000001 /* PaddleOCRDemo */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = A9000003 /* Build configuration list for PBXNativeTarget "PaddleOCRDemo" */;
+ buildPhases = (
+ A9D5C3BAF4FDCBE19D45BCA7 /* [CP] Check Pods Manifest.lock */,
+ A8000001 /* Sources */,
+ A6000001 /* Frameworks */,
+ A8000003 /* Resources */,
+ 6530B8008C9EFD4F162C3E86 /* [CP] Embed Pods Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = PaddleOCRDemo;
+ productName = PaddleOCRDemo;
+ productReference = A2000005 /* PaddleOCRDemo.app */;
+ productType = "com.apple.product-type.application";
+ };
+ A4000002 /* PaddleOCRDemoTests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = A9000006 /* Build configuration list for PBXNativeTarget "PaddleOCRDemoTests" */;
+ buildPhases = (
+ 9FD4B8CB3C3E1C27991BDE0B /* [CP] Check Pods Manifest.lock */,
+ A8000002 /* Sources */,
+ A6000002 /* Frameworks */,
+ A8000004 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ A5000002 /* PBXTargetDependency */,
+ );
+ name = PaddleOCRDemoTests;
+ productName = PaddleOCRDemoTests;
+ productReference = A2000009 /* PaddleOCRDemoTests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ A3000001 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ BuildIndependentTargetsInParallel = 1;
+ LastSwiftUpdateCheck = 1540;
+ LastUpgradeCheck = 2640;
+ TargetAttributes = {
+ A4000001 = {
+ CreatedOnToolsVersion = 15.4;
+ };
+ A4000002 = {
+ CreatedOnToolsVersion = 15.4;
+ TestTargetID = A4000001;
+ };
+ };
+ };
+ buildConfigurationList = A9000001 /* Build configuration list for PBXProject "PaddleOCRDemo" */;
+ compatibilityVersion = "Xcode 14.0";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = A7000001;
+ productRefGroup = A7000003 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ A4000001 /* PaddleOCRDemo */,
+ A4000002 /* PaddleOCRDemoTests */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ A8000003 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ A10000D0 /* Assets.xcassets in Resources */,
+ A1000003 /* Models in Resources */,
+ A1000091 /* SampleImages in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ A8000004 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ A10000C7 /* Fixtures in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 6530B8008C9EFD4F162C3E86 /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-PaddleOCRDemo/Pods-PaddleOCRDemo-frameworks-${CONFIGURATION}-input-files.xcfilelist",
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-PaddleOCRDemo/Pods-PaddleOCRDemo-frameworks-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-PaddleOCRDemo/Pods-PaddleOCRDemo-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 9FD4B8CB3C3E1C27991BDE0B /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-PaddleOCRDemoTests-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+ A9D5C3BAF4FDCBE19D45BCA7 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-PaddleOCRDemo-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ A8000001 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ A1000001 /* PaddleOCRDemoApp.swift in Sources */,
+ A10000D1 /* Bundle+BundledImage.swift in Sources */,
+ A10000D2 /* DemoAssets.swift in Sources */,
+ A1000002 /* ContentView.swift in Sources */,
+ A10000C0 /* DemoSectionChrome.swift in Sources */,
+ A1000021 /* DBPostProcess.swift in Sources */,
+ A1000022 /* OpenCVDBBridge.mm in Sources */,
+ A1000023 /* OpenCVImageBridge.mm in Sources */,
+ A10000E1 /* clipper.cpp in Sources */,
+ A10000E2 /* PDBPolygonOffsetBridge.mm in Sources */,
+ A1000030 /* DetectionEngine.swift in Sources */,
+ A1000004 /* ModelConfig.swift in Sources */,
+ A1000005 /* ORTSessionManager.swift in Sources */,
+ A10000F0 /* ORTProfilingBridge.mm in Sources */,
+ A1000007 /* InferenceConfig.swift in Sources */,
+ A10000B1 /* OCRParameterResolver.swift in Sources */,
+ A1000008 /* Preprocessing.swift in Sources */,
+ A1000040 /* RecPreprocessor.swift in Sources */,
+ A1000050 /* CTCDecoder.swift in Sources */,
+ A1000060 /* RecognitionEngine.swift in Sources */,
+ A1000070 /* OCREngine.swift in Sources */,
+ A1000071 /* BoxSorter.swift in Sources */,
+ A1000072 /* QuadTextCrop.swift in Sources */,
+ A1000074 /* EncodedImageCodec.swift in Sources */,
+ A1000080 /* OCRViewModel.swift in Sources */,
+ A1000090 /* ImagePickerSection.swift in Sources */,
+ A10000A0 /* ResultImageView.swift in Sources */,
+ A10000A1 /* TimingView.swift in Sources */,
+ A10000A2 /* ResultsListView.swift in Sources */,
+ A10000A3 /* ErrorView.swift in Sources */,
+ A10000A5 /* OnnxRuntimeSettingsPanel.swift in Sources */,
+ A10000A6 /* EngineSettingsSheet.swift in Sources */,
+ A10000A4 /* OCRParametersPanel.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ A8000002 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ A1000011 /* ORTSessionManagerTests.swift in Sources */,
+ A10000B3 /* OCRParameterResolverTests.swift in Sources */,
+ A10000C5 /* OCRBenchmarkTests.swift in Sources */,
+ A10000C6 /* BenchmarkPayloads.swift in Sources */,
+ A10000D3 /* BenchmarkFixtures.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ A5000002 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = A4000001 /* PaddleOCRDemo */;
+ targetProxy = A5000001 /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin XCBuildConfiguration section */
+ AA000001 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ STRING_CATALOG_GENERATE_SYMBOLS = YES;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ };
+ name = Debug;
+ };
+ AA000002 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ SDKROOT = iphoneos;
+ STRING_CATALOG_GENERATE_SYMBOLS = YES;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ AA000003 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 520BC3C496ED955BBB302DC4 /* Pods-PaddleOCRDemo.debug.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = 76MLN428C9;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = PaddleOCRDemo/Supporting/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = "PaddleOCR";
+ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+ INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ MARKETING_VERSION = 1.0;
+ OTHER_LDFLAGS = (
+ "$(inherited)",
+ "-Wl,-no_warn_duplicate_libraries",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.paddleocr.demo;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
+ SUPPORTS_MACCATALYST = NO;
+ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
+ SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_OBJC_BRIDGING_HEADER = "PaddleOCRDemo/Supporting/PaddleOCRDemo-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = 1;
+ USER_HEADER_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(SRCROOT)/PaddleOCRDemo/Engine",
+ "$(SRCROOT)/PaddleOCRDemo/ThirdParty/Clipper1",
+ "$(PODS_ROOT)/onnxruntime-objc/objectivec",
+ "$(PODS_ROOT)/onnxruntime-objc/objectivec/include",
+ );
+ };
+ name = Debug;
+ };
+ AA000004 /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 54F64B7C830508805D832F8A /* Pods-PaddleOCRDemo.release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = 76MLN428C9;
+ ENABLE_TESTABILITY = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = PaddleOCRDemo/Supporting/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = "PaddleOCR";
+ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+ INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ MARKETING_VERSION = 1.0;
+ OTHER_LDFLAGS = (
+ "$(inherited)",
+ "-Wl,-no_warn_duplicate_libraries",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.paddleocr.demo;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
+ SUPPORTS_MACCATALYST = NO;
+ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
+ SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_OBJC_BRIDGING_HEADER = "PaddleOCRDemo/Supporting/PaddleOCRDemo-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = 1;
+ USER_HEADER_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(SRCROOT)/PaddleOCRDemo/Engine",
+ "$(SRCROOT)/PaddleOCRDemo/ThirdParty/Clipper1",
+ "$(PODS_ROOT)/onnxruntime-objc/objectivec",
+ "$(PODS_ROOT)/onnxruntime-objc/objectivec/include",
+ );
+ };
+ name = Release;
+ };
+ AA000005 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 72FF5C269BF861629F89D6B5 /* Pods-PaddleOCRDemoTests.debug.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = 76MLN428C9;
+ GENERATE_INFOPLIST_FILE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.paddleocr.demo.tests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PaddleOCRDemo.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/PaddleOCRDemo";
+ };
+ name = Debug;
+ };
+ AA000006 /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 49C7CEE60686CC98BE2EEE45 /* Pods-PaddleOCRDemoTests.release.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = 76MLN428C9;
+ GENERATE_INFOPLIST_FILE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.paddleocr.demo.tests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PaddleOCRDemo.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/PaddleOCRDemo";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ A9000001 /* Build configuration list for PBXProject "PaddleOCRDemo" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ AA000001 /* Debug */,
+ AA000002 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ A9000003 /* Build configuration list for PBXNativeTarget "PaddleOCRDemo" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ AA000003 /* Debug */,
+ AA000004 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ A9000006 /* Build configuration list for PBXNativeTarget "PaddleOCRDemoTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ AA000005 /* Debug */,
+ AA000006 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = A3000001 /* Project object */;
+}
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo.xcodeproj/xcshareddata/xcschemes/PaddleOCRDemo.xcscheme b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo.xcodeproj/xcshareddata/xcschemes/PaddleOCRDemo.xcscheme
new file mode 100644
index 0000000000000000000000000000000000000000..2cae7330ba23a7ec1d8d5c4440114666a3a38ea7
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo.xcodeproj/xcshareddata/xcschemes/PaddleOCRDemo.xcscheme
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo.xcworkspace/contents.xcworkspacedata b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000000000000000000000000000000000000..c0bcb46a77c59930e73ae0dbae56d1100d0b16c7
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/App/Bundle+BundledImage.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/App/Bundle+BundledImage.swift
new file mode 100644
index 0000000000000000000000000000000000000000..7d4c4ccaaec79233be2212b7c7605d9283a01c66
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/App/Bundle+BundledImage.swift
@@ -0,0 +1,48 @@
+// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import Foundation
+
+extension Bundle {
+ /// Raster extensions tried when resolving a bundled image by stem, or after an explicit extension misses.
+ static let bundledRasterImageExtensions: [String] = [
+ "jpg", "jpeg", "png", "heic", "heif", "webp", "tiff", "tif", "bmp", "gif",
+ ]
+
+ /// Resolves a filesystem path for an image under `subdirectory`.
+ ///
+ /// `name` may be a resource stem (e.g. `photo`) or include a suffix (e.g. `photo.jpg`).
+ /// Extensions are matched case-insensitively when provided; otherwise each entry in
+ /// ``bundledRasterImageExtensions`` is tried in order.
+ func path(forBundledImageNamed name: String, subdirectory: String) -> String? {
+ let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines)
+ guard !trimmed.isEmpty else { return nil }
+ let ns = trimmed as NSString
+ let extGiven = ns.pathExtension.lowercased()
+ let stem = ns.deletingPathExtension
+ let resourceStem = stem.isEmpty ? trimmed : stem
+
+ if !extGiven.isEmpty {
+ if let path = path(forResource: resourceStem, ofType: extGiven, inDirectory: subdirectory) {
+ return path
+ }
+ }
+ for ext in Self.bundledRasterImageExtensions where ext != extGiven {
+ if let path = path(forResource: resourceStem, ofType: ext, inDirectory: subdirectory) {
+ return path
+ }
+ }
+ return nil
+ }
+}
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/App/ContentView.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/App/ContentView.swift
new file mode 100644
index 0000000000000000000000000000000000000000..e728c161474934832e23107820ff32c3ec61fcf7
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/App/ContentView.swift
@@ -0,0 +1,321 @@
+// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import PhotosUI
+import SwiftUI
+
+/// Root view: routes `AppState` and lays out **input (photo/sample)** vs **output (preview + text)**; engine tuning lives in ``EngineSettingsSheet``.
+struct ContentView: View {
+ @Environment(\.horizontalSizeClass) private var horizontalSizeClass
+ @StateObject private var viewModel = OCRViewModel()
+ @State private var selectedItem: PhotosPickerItem?
+ @State private var showEngineSettings = false
+
+ private var navigationTitleDisplayMode: NavigationBarItem.TitleDisplayMode {
+ switch viewModel.state {
+ case .ready: return .large
+ case .error(let err) where !err.isModelError: return .large
+ default: return .inline
+ }
+ }
+
+ private var showsAdvancedToolbarItem: Bool {
+ switch viewModel.state {
+ case .loadingModels, .processing: return false
+ case .ready, .results, .error: return true
+ }
+ }
+
+ var body: some View {
+ NavigationStack {
+ ScrollView {
+ VStack(spacing: 0) {
+ contentForState
+ }
+ .padding(.horizontal, 20)
+ .padding(.vertical, 20)
+ }
+ .background(Color(.systemGroupedBackground))
+ .navigationTitle("PaddleOCR Demo")
+ .navigationBarTitleDisplayMode(navigationTitleDisplayMode)
+ .toolbar {
+ if showsAdvancedToolbarItem {
+ ToolbarItem(placement: .topBarTrailing) {
+ Button {
+ showEngineSettings = true
+ } label: {
+ Image(systemName: "slider.horizontal.3")
+ }
+ .accessibilityLabel("Advanced settings")
+ }
+ }
+ }
+ }
+ .sheet(isPresented: $showEngineSettings) {
+ EngineSettingsSheet(viewModel: viewModel)
+ }
+ .task {
+ await viewModel.loadModels()
+ }
+ .onChange(of: selectedItem) { newItem in
+ guard let item = newItem else { return }
+ Task {
+ guard let data = try? await item.loadTransferable(type: Data.self),
+ let uiImage = UIImage(data: data) else {
+ viewModel.reportImageLoadFailed()
+ return
+ }
+ selectedItem = nil
+ await viewModel.processImage(uiImage)
+ }
+ }
+ }
+
+ private func imageInputSection(title: String, subtitle: String) -> some View {
+ VStack(alignment: .leading, spacing: 10) {
+ DemoSectionHeader(title: title, subtitle: subtitle)
+ ImagePickerSection(
+ selectedItem: $selectedItem,
+ sampleImageNames: viewModel.sampleImageNames,
+ onSampleSelected: { name in
+ Task { await viewModel.selectSampleImage(named: name) }
+ }
+ )
+ }
+ }
+
+ // MARK: - State Router
+
+ @ViewBuilder
+ private var contentForState: some View {
+ switch viewModel.state {
+ case .loadingModels:
+ loadingModelsView
+ case .ready:
+ readyView
+ case .processing(let image):
+ processingView(image: image)
+ case .results(let result, let image, _):
+ resultsView(result: result, image: image)
+ case .error(let error):
+ errorView(error: error)
+ }
+ }
+
+ // MARK: - Loading Models
+
+ private var loadingModelsView: some View {
+ VStack(spacing: 16) {
+ ProgressView()
+ .scaleEffect(1.3)
+ .tint(.accentColor)
+ Text("Loading models")
+ .font(.title3.weight(.semibold))
+ Text("Preparing detection and recognition…")
+ .font(.subheadline)
+ .foregroundStyle(.secondary)
+ .multilineTextAlignment(.center)
+ }
+ .frame(maxWidth: .infinity)
+ .padding(.top, 72)
+ .padding(.bottom, 40)
+ }
+
+ // MARK: - Ready
+
+ private var readyView: some View {
+ VStack(spacing: 22) {
+ statusPill(icon: "checkmark.circle.fill", tint: .green) {
+ VStack(alignment: .leading, spacing: 4) {
+ Text("Ready")
+ .font(.title3.weight(.semibold))
+ Text("Choose a photo or open a sample. Engine and OCR tuning are under Advanced in the toolbar.")
+ .font(.subheadline)
+ .foregroundStyle(.secondary)
+ .fixedSize(horizontal: false, vertical: true)
+ }
+ }
+
+ imageInputSection(
+ title: "Image",
+ subtitle: "Choose from your library or a bundled sample."
+ )
+ }
+ }
+
+ // MARK: - Processing
+
+ private func processingView(image: UIImage) -> some View {
+ VStack(alignment: .leading, spacing: 16) {
+ Text("Running OCR")
+ .font(.title3.weight(.semibold))
+ Text("Hold on — detection and recognition are in progress.")
+ .font(.subheadline)
+ .foregroundStyle(.secondary)
+
+ ZStack {
+ Image(uiImage: image)
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+ .frame(maxHeight: 360)
+ .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
+
+ RoundedRectangle(cornerRadius: 14, style: .continuous)
+ .fill(.ultraThinMaterial)
+
+ VStack(spacing: 10) {
+ ProgressView()
+ .scaleEffect(1.25)
+ .tint(.accentColor)
+ Text("Working…")
+ .font(.headline)
+ }
+ }
+ .frame(maxWidth: .infinity)
+ }
+ }
+
+ // MARK: - Results
+
+ private func resultsView(result: OCRRunResult, image: UIImage) -> some View {
+ // Only use two columns when explicitly regular (e.g. iPad); treat nil as compact.
+ let isCompact = horizontalSizeClass != .regular
+ return VStack(spacing: 22) {
+ if isCompact {
+ previewBlock(result: result, image: image)
+ TimingView(result: result)
+ .padding(.vertical, 4)
+ } else {
+ TimingView(result: result)
+ .padding(.vertical, 4)
+ HStack(alignment: .top, spacing: 16) {
+ previewBlock(result: result, image: image)
+ .frame(maxWidth: .infinity)
+ .layoutPriority(1)
+ resultsListBlock(result: result)
+ .frame(minWidth: 280, maxWidth: .infinity)
+ }
+ }
+
+ if isCompact {
+ resultsListBlock(result: result)
+ }
+
+ imageInputSection(
+ title: "New image",
+ subtitle: "Choose another photo or sample. Re-run uses the current image and OCR parameters."
+ )
+
+ rerunOCRButton()
+ }
+ }
+
+ @ViewBuilder
+ private func rerunOCRButton() -> some View {
+ if viewModel.hasPendingOcrChanges {
+ Button {
+ Task { await viewModel.rerunOCR() }
+ } label: {
+ Label("Re-run OCR on this image", systemImage: "arrow.clockwise.circle.fill")
+ .frame(maxWidth: .infinity)
+ .padding(.vertical, 4)
+ }
+ .buttonStyle(.borderedProminent)
+ .controlSize(.large)
+ } else {
+ Button {
+ Task { await viewModel.rerunOCR() }
+ } label: {
+ Label("Re-run OCR on this image", systemImage: "arrow.clockwise.circle.fill")
+ .frame(maxWidth: .infinity)
+ .padding(.vertical, 4)
+ }
+ .buttonStyle(.bordered)
+ .controlSize(.large)
+ }
+ }
+
+ private func previewBlock(result: OCRRunResult, image: UIImage) -> some View {
+ VStack(alignment: .leading, spacing: 10) {
+ DemoSectionHeader(
+ title: "Preview",
+ subtitle: "Accent overlay shows each detected text region."
+ )
+ DemoCard {
+ ResultImageView(image: image, results: result.results)
+ .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
+ }
+ }
+ }
+
+ private func resultsListBlock(result: OCRRunResult) -> some View {
+ VStack(alignment: .leading, spacing: 10) {
+ DemoSectionHeader(title: "Results", subtitle: nil)
+ DemoCard {
+ ResultsListView(
+ results: result.results,
+ copiedFeedback: viewModel.copiedFeedback,
+ onCopy: { viewModel.copyResultsToClipboard() }
+ )
+ }
+ }
+ }
+
+ // MARK: - Error
+
+ private func errorView(error: AppError) -> some View {
+ VStack(spacing: 22) {
+ ErrorView(error: error, onRetry: {
+ Task { await viewModel.retry() }
+ })
+
+ if !error.isModelError {
+ VStack(alignment: .leading, spacing: 10) {
+ DemoSectionHeader(title: "Try another image", subtitle: nil)
+ ImagePickerSection(
+ selectedItem: $selectedItem,
+ sampleImageNames: viewModel.sampleImageNames,
+ onSampleSelected: { name in
+ Task { await viewModel.selectSampleImage(named: name) }
+ }
+ )
+ }
+ }
+ }
+ .padding(.top, 24)
+ }
+
+ // MARK: - Small chrome
+
+ private func statusPill(icon: String, tint: Color, @ViewBuilder content: () -> Content) -> some View {
+ HStack(alignment: .top, spacing: 14) {
+ Image(systemName: icon)
+ .font(.system(size: 28))
+ .foregroundStyle(tint)
+ .symbolRenderingMode(.hierarchical)
+ content()
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+ .padding(16)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .background {
+ RoundedRectangle(cornerRadius: 14, style: .continuous)
+ .fill(Color(.secondarySystemGroupedBackground))
+ }
+ .overlay {
+ RoundedRectangle(cornerRadius: 14, style: .continuous)
+ .strokeBorder(Color(.separator).opacity(0.35), lineWidth: 0.5)
+ }
+ }
+}
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/App/DemoAssets.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/App/DemoAssets.swift
new file mode 100644
index 0000000000000000000000000000000000000000..e21a33d6a7991128e3256bd87f91aa408646b7a3
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/App/DemoAssets.swift
@@ -0,0 +1,21 @@
+// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import Foundation
+
+/// Built-in demo images (bundle resource stems).
+enum DemoAssets {
+ /// Default bundled sample for the image picker.
+ static let defaultSampleImageStem: String = "general_ocr_002"
+}
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/App/DemoSectionChrome.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/App/DemoSectionChrome.swift
new file mode 100644
index 0000000000000000000000000000000000000000..6230d0623190329db9989ad76701b8ae0251367e
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/App/DemoSectionChrome.swift
@@ -0,0 +1,62 @@
+// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import SwiftUI
+
+// MARK: - Section chrome (shared by ContentView + panels)
+
+/// Vertical section title + optional subtitle for scan-friendly hierarchy.
+struct DemoSectionHeader: View {
+ let title: String
+ var subtitle: String?
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 4) {
+ Text(title)
+ .font(.footnote.weight(.semibold))
+ .foregroundStyle(.secondary)
+ .textCase(.uppercase)
+ .tracking(0.4)
+ if let subtitle {
+ Text(subtitle)
+ .font(.subheadline)
+ .foregroundStyle(.secondary)
+ }
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+}
+
+/// Grouped card matching system settings style.
+struct DemoCard: View {
+ let content: Content
+
+ init(@ViewBuilder content: () -> Content) {
+ self.content = content()
+ }
+
+ var body: some View {
+ content
+ .padding(16)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .background {
+ RoundedRectangle(cornerRadius: 14, style: .continuous)
+ .fill(Color(.secondarySystemGroupedBackground))
+ }
+ .overlay {
+ RoundedRectangle(cornerRadius: 14, style: .continuous)
+ .strokeBorder(Color(.separator).opacity(0.35), lineWidth: 0.5)
+ }
+ }
+}
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/App/PaddleOCRDemoApp.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/App/PaddleOCRDemoApp.swift
new file mode 100644
index 0000000000000000000000000000000000000000..1f293e8d34d773863e437adf17aad704784669fb
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/App/PaddleOCRDemoApp.swift
@@ -0,0 +1,24 @@
+// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import SwiftUI
+
+@main
+struct PaddleOCRDemoApp: App {
+ var body: some Scene {
+ WindowGroup {
+ ContentView()
+ }
+ }
+}
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/BoxSorter.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/BoxSorter.swift
new file mode 100644
index 0000000000000000000000000000000000000000..a326daddd65cb9c11873eb77fbd0d29f322867d5
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/BoxSorter.swift
@@ -0,0 +1,72 @@
+// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import Foundation
+
+// MARK: - BoxSorter
+
+/// Sorts detection boxes in natural reading order: top-to-bottom, then
+/// left-to-right within the same text line.
+///
+/// Two-pass: sort by top-left (y then x), then swap same-line neighbors when
+/// the y-gap is below `yThreshold` but x is out of order.
+struct BoxSorter {
+
+ /// Y-coordinate threshold for same-line detection. Boxes whose top-left y
+ /// coordinates differ by less than this value are considered on the same line
+ /// and sorted left-to-right. Default: 10 pixels.
+ static let yThreshold: Int32 = 10
+
+ /// Sort detection boxes in reading order: top-to-bottom, then left-to-right
+ /// within the same line (defined by yThreshold).
+ ///
+ /// Algorithm:
+ /// 1. Sort all boxes by top-left corner: first by y, then by x.
+ /// 2. Insertion-sort pass: for each box, walk backward and swap with the
+ /// previous box if they are on the same line (y-difference < 10) and
+ /// the current box's x is smaller.
+ ///
+ /// - Parameter boxes: Unordered detection boxes from DBPostProcessor.
+ /// - Returns: Boxes sorted in reading order.
+ static func sortInReadingOrder(_ boxes: [DetectionBox]) -> [DetectionBox] {
+ guard boxes.count > 1 else { return boxes }
+
+ // Step 1: Sort by top-left point -- first by y, then by x
+ var sorted = boxes.sorted { a, b in
+ let ay = a.points[0][1], ax = a.points[0][0]
+ let by = b.points[0][1], bx = b.points[0][0]
+ if ay != by { return ay < by }
+ return ax < bx
+ }
+
+ // Step 2: Backward insertion sort for same-line boxes
+ for i in 0..<(sorted.count - 1) {
+ var j = i
+ while j >= 0 {
+ let nextY = sorted[j + 1].points[0][1]
+ let currY = sorted[j].points[0][1]
+ let nextX = sorted[j + 1].points[0][0]
+ let currX = sorted[j].points[0][0]
+ if abs(nextY - currY) < yThreshold && nextX < currX {
+ sorted.swapAt(j, j + 1)
+ j -= 1
+ } else {
+ break
+ }
+ }
+ }
+
+ return sorted
+ }
+}
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/CTCDecoder.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/CTCDecoder.swift
new file mode 100644
index 0000000000000000000000000000000000000000..2bf7867c449289169ab9c18d2baf80799065640a
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/CTCDecoder.swift
@@ -0,0 +1,199 @@
+// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import Foundation
+
+// MARK: - Errors
+
+enum CTCDecoderError: LocalizedError {
+ case emptyDictionary
+ case invalidOutputShape([Int])
+
+ var errorDescription: String? {
+ switch self {
+ case .emptyDictionary:
+ return "Character dictionary is empty"
+ case .invalidOutputShape(let shape):
+ return "Invalid recognition output shape: \(shape), expected [1, T, C]"
+ }
+ }
+}
+
+// MARK: - Recognition Result
+
+/// A single text recognition result from CTC decoding.
+struct RecognitionResult {
+ /// The decoded text string.
+ let text: String
+ /// Average confidence across all decoded characters (0.0 to 1.0).
+ let confidence: Float
+}
+
+// MARK: - CTCDecoder
+
+/// CTC label decoding for recognition output
+///
+/// Greedy CTC decoding with blank index 0 and duplicate collapse.
+/// Per timestep: take the best class and its score, then merge duplicates and drop blanks,
+/// then average the kept scores. Logits need not be softmax probabilities;
+/// `max` over the class dimension is used for confidence on exported logits.
+///
+/// The CTC (Connectionist Temporal Classification) decoding algorithm:
+/// 1. Argmax across the class dimension to get per-timestep predicted indices
+/// 2. Max across the class dimension to get per-timestep prediction probabilities
+/// 3. Remove consecutive duplicate indices (CTC merge rule)
+/// 4. Remove blank tokens (index 0)
+/// 5. Map remaining indices to characters via the dictionary
+/// 6. Confidence = mean of probabilities at selected positions
+///
+struct CTCDecoder {
+ private let characters: [String]
+
+ /// The blank token index (always 0 for CTC).
+ private let blankIndex: Int = 0
+
+ /// Creates a CTCDecoder from a recognition model's InferenceConfig.
+ ///
+ /// Reads `character_dict` from the model config file and prepends the blank token.
+ ///
+ /// - Parameter config: A parsed InferenceConfig from the recognition model's config file.
+ /// - Throws: `CTCDecoderError.emptyDictionary` if no character dict is found.
+ init(config: InferenceConfig) throws {
+ guard let dict = config.postProcess.characterDict, !dict.isEmpty else {
+ throw CTCDecoderError.emptyDictionary
+ }
+
+ // Build character list: dict from config, trailing space (default-on), blank at index 0.
+ var chars = ["blank"]
+ chars.append(contentsOf: dict)
+ chars.append(" ")
+ self.characters = chars
+ }
+
+ /// Decodes the raw recognition model output into text with confidence.
+ ///
+ /// The output tensor has shape `[1, T, C]` where:
+ /// - T = number of timesteps (sequence length)
+ /// - C = number of classes (character vocabulary size + 1 for blank)
+ ///
+ /// - Parameters:
+ /// - outputData: Flat float32 array of the model output.
+ /// - outputShape: Shape of the output tensor, expected `[1, T, C]`.
+ /// - Returns: A `RecognitionResult` with decoded text and confidence score.
+ func decode(outputData: [Float], outputShape: [Int]) throws -> RecognitionResult {
+ guard outputShape.count == 3, outputShape[0] == 1 else {
+ throw CTCDecoderError.invalidOutputShape(outputShape)
+ }
+
+ let timesteps = outputShape[1]
+ let numClasses = outputShape[2]
+
+ // Step 1 & 2: Argmax and max across the class dimension for each timestep
+ var predIndices = [Int](repeating: 0, count: timesteps)
+ var predProbs = [Float](repeating: 0, count: timesteps)
+
+ for t in 0.. maxVal {
+ maxVal = val
+ maxIdx = c
+ }
+ }
+
+ predIndices[t] = maxIdx
+ predProbs[t] = maxVal
+ }
+
+ // Step 3: Build selection mask
+ // - Remove consecutive duplicates (keep first of each run)
+ // - Remove blank tokens (index 0)
+ //
+ // Drop consecutive duplicate indices, then drop blank (0).
+ var selection = [Bool](repeating: true, count: timesteps)
+
+ // Remove consecutive duplicates
+ for t in 1.. [RecognitionResult] {
+ guard outputShape.count == 3 else {
+ throw CTCDecoderError.invalidOutputShape(outputShape)
+ }
+ let batchCount = outputShape[0]
+ let timesteps = outputShape[1]
+ let numClasses = outputShape[2]
+ guard batchCount >= 1, timesteps >= 1, numClasses >= 1 else {
+ throw CTCDecoderError.invalidOutputShape(outputShape)
+ }
+ let rowElements = timesteps * numClasses
+ let expected = batchCount * rowElements
+ guard outputData.count == expected else {
+ throw CTCDecoderError.invalidOutputShape(outputShape)
+ }
+ var results: [RecognitionResult] = []
+ results.reserveCapacity(batchCount)
+ for b in 0.. [DetectionBox] {
+ // 1. Extract probability map: pred[0, 0, :, :] from shape [1, 1, H, W]
+ let mapSize = tensorHeight * tensorWidth
+ let pred: [Float]
+ if outputTensor.count >= mapSize {
+ pred = Array(outputTensor.prefix(mapSize))
+ } else {
+ return []
+ }
+
+ // 2. Binary threshold: create mask where prob > thresh
+ var binaryMask = [UInt8](repeating: 0, count: mapSize)
+ for i in 0.. thresh ? 1 : 0
+ }
+
+ // 3. Find contours on the binary mask
+ let contours = findContours(
+ binaryMask: binaryMask,
+ width: tensorWidth,
+ height: tensorHeight
+ )
+
+ // 4. Process each contour
+ let numContours = min(contours.count, maxCandidates)
+ var results: [DetectionBox] = []
+
+ for i in 0.. score {
+ continue
+ }
+
+ // 4c. Expand polygon (unclip) via integer-grid offset.
+ let nsMini = miniBox.map { NSValue(cgPoint: CGPoint(x: CGFloat($0.x), y: CGFloat($0.y))) }
+ let area = Double(PDBOpenCVDBBridge.contourArea(fromPoints: nsMini))
+ let length = Double(PDBOpenCVDBBridge.arcLength(fromPoints: nsMini, closed: true))
+ guard length > 0 else { continue }
+ let distance = area * Double(unclipRatio) / length
+
+ let rings = PDBPolygonOffsetBridge.inflateClosedPolygon(with: nsMini, distance: distance) as [[NSValue]]
+ guard rings.count == 1 else {
+ continue
+ }
+ let firstRing = rings[0]
+
+ // 4d. Second getMiniBoxes on the expanded polygon
+ let expandedFloats = firstRing.map { FloatPoint(x: Float($0.cgPointValue.x), y: Float($0.cgPointValue.y)) }
+ guard let (finalBox, finalSside) = getMiniBoxes(contour: expandedFloats) else {
+ continue
+ }
+ if finalSside < minSize + 2 {
+ continue
+ }
+
+ // 4e. Scale to original image dimensions
+ let w = Float(tensorWidth)
+ let h = Float(tensorHeight)
+ let dw = Float(originalWidth)
+ let dh = Float(originalHeight)
+
+ var scaledPoints: [[Int32]] = []
+ for pt in finalBox {
+ let sx = Int32(min(max((pt.x / w * dw).rounded(), 0), dw))
+ let sy = Int32(min(max((pt.y / h * dh).rounded(), 0), dh))
+ scaledPoints.append([sx, sy])
+ }
+
+ results.append(DetectionBox(points: scaledPoints, score: score))
+ }
+
+ return results
+ }
+}
+
+// MARK: - Internal Point Type
+
+/// Float 2D point for `DBPostProcessor` geometry helpers.
+internal struct FloatPoint {
+ var x: Float
+ var y: Float
+}
+
+// MARK: - OpenCV (CocoaPods OpenCV 4.3.x)
+
+extension DBPostProcessor {
+
+ /// Foreground pixels as 255 on an 8-bit mask; runs `findContours` with `RETR_LIST` and `CHAIN_APPROX_SIMPLE`.
+ func findContours(binaryMask: [UInt8], width: Int, height: Int) -> [[FloatPoint]] {
+ let data = Data(binaryMask)
+ let raw = PDBOpenCVDBBridge.findContours(
+ binaryMask: data,
+ width: width,
+ height: height,
+ maxCandidates: maxCandidates
+ )
+ return raw.map { contour in
+ contour.map { v in
+ let p = v.cgPointValue
+ return FloatPoint(x: Float(p.x), y: Float(p.y))
+ }
+ }
+ }
+
+ /// Minimum-area rectangle: `minAreaRect` on contour points, `boxPoints` for corners, then TL/TR/BR/BL ordering.
+ func getMiniBoxes(contour: [FloatPoint]) -> (box: [FloatPoint], minSide: Float)? {
+ guard contour.count >= 2 else { return nil }
+ let values = contour.map { NSValue(cgPoint: CGPoint(x: CGFloat($0.x), y: CGFloat($0.y))) }
+ guard let result = PDBOpenCVDBBridge.miniBox(fromPoints: values) else { return nil }
+ let corners = result.corners.map { v in
+ let p = v.cgPointValue
+ return FloatPoint(x: Float(p.x), y: Float(p.y))
+ }
+ return (corners, result.minSide)
+ }
+}
+
+// MARK: - Box Scoring
+
+extension DBPostProcessor {
+
+ /// Compute the mean probability score within a box region of the probability map.
+ ///
+ /// Mean probability inside the quad: `fillPoly` mask on the score map, then `mean` over the masked region.
+ func boxScoreFast(
+ pred: [Float],
+ predWidth: Int,
+ predHeight: Int,
+ box: [[Float]]
+ ) -> Float {
+ let predData = pred.withUnsafeBufferPointer { Data(buffer: $0) }
+ let boxNS: [[NSNumber]] = box.map { [NSNumber(value: $0[0]), NSNumber(value: $0[1])] }
+ return PDBOpenCVImageBridge.meanPredInQuad(
+ predData,
+ width: predWidth,
+ height: predHeight,
+ box: boxNS
+ )
+ }
+}
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/DetectionEngine.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/DetectionEngine.swift
new file mode 100644
index 0000000000000000000000000000000000000000..6c2ded783fac14233a920135b854d82d886aaf4b
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/DetectionEngine.swift
@@ -0,0 +1,168 @@
+// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import CoreGraphics
+import Foundation
+
+// MARK: - Detection Result
+
+/// The result of running text detection on an image.
+///
+/// Contains the detected text boxes and per-stage timing metrics for performance analysis.
+struct DetectionResult {
+ /// Detected text region bounding polygons with confidence scores.
+ let boxes: [DetectionBox]
+ /// Seconds spent in preprocessing (resize, normalize, HWC-to-CHW).
+ let preprocessTime: TimeInterval
+ /// Seconds spent in ONNX Runtime inference.
+ let inferenceTime: TimeInterval
+ /// Seconds spent in DB postprocessing (threshold, contours, polygon offset / unclip).
+ let postprocessTime: TimeInterval
+ /// Total detection time (preprocess + inference + postprocess).
+ let totalTime: TimeInterval
+ /// Detection model input tensor shape, e.g. [1, 3, H, W].
+ let inputTensorShape: [Int]
+}
+
+// MARK: - Detection Engine Errors
+
+enum DetectionEngineError: LocalizedError {
+ case noOutputTensor
+ case unexpectedOutputShape([Int])
+
+ var errorDescription: String? {
+ switch self {
+ case .noOutputTensor:
+ return "Detection model produced no output tensor"
+ case .unexpectedOutputShape(let shape):
+ return "Unexpected detection output shape: \(shape), expected [1, 1, H, W]"
+ }
+ }
+}
+
+// MARK: - DetectionEngine
+
+/// Orchestrates text detection: CGImage → preprocess → ORT inference → postprocess.
+///
+/// This is the integration layer that composes `DetPreprocessor`, `ORTSessionManager`, and
+/// `DBPostProcessor` into a single callable unit.
+///
+/// Usage:
+/// ```swift
+/// let engine = try DetectionEngine(sessionManager: manager)
+/// let result = try await engine.detect(cgImage)
+/// print("Found \(result.boxes.count) text regions in \(result.totalTime)s")
+/// ```
+class DetectionEngine {
+ private let sessionManager: ORTSessionManager
+ private let preprocessor: DetPreprocessor
+ private(set) var modelConfig: InferenceConfig
+
+ /// Initialize with an existing ORTSessionManager (models must already be loaded).
+ ///
+ /// Loads the detection model config file from the bundle to configure
+ /// the preprocessor and postprocessor.
+ ///
+ /// - Parameter sessionManager: A loaded ORTSessionManager with detection model ready.
+ /// - Throws: If the model config cannot be loaded or required fields are missing.
+ init(sessionManager: ORTSessionManager) throws {
+ self.sessionManager = sessionManager
+
+ // Load detection model config
+ let detPaths = try ModelConfig.detection()
+ let cfg = try InferenceConfig.load(from: detPaths.configPath)
+ self.modelConfig = cfg
+ self.preprocessor = try DetPreprocessor(config: cfg)
+ }
+
+ private func makePostProcessor(effective: EffectiveOCRParams) -> DBPostProcessor {
+ DBPostProcessor(
+ thresh: effective.detThresh,
+ boxThresh: effective.detBoxThresh,
+ maxCandidates: modelConfig.postProcess.maxCandidates,
+ unclipRatio: effective.detUnclipRatio
+ )
+ }
+
+ /// Run detection on a CGImage, returning bounding polygons with confidence scores.
+ ///
+ /// Steps: DetResizeForTest → NormalizeImage → ToCHW → ORT inference → DBPostProcess
+ ///
+ /// - Parameters:
+ /// - image: The input image to detect text regions in.
+ /// - runtimeParams: Detection postprocess / resize overrides (see ``OCRRuntimeParams``).
+ /// - Returns: A `DetectionResult` with boxes and per-stage timing metrics.
+ func detect(_ image: CGImage, runtimeParams: OCRRuntimeParams = .noOverrides) async throws -> DetectionResult {
+ let eff = OCRParameterResolver.effective(modelConfig: modelConfig, runtime: runtimeParams)
+ return try await detect(effective: eff) {
+ try preprocessor.preprocess(image, detResize: eff.detResize)
+ }
+ }
+
+ private func detect(
+ effective: EffectiveOCRParams,
+ preprocess: () throws -> PreprocessResult
+ ) async throws -> DetectionResult {
+ let postprocessor = makePostProcessor(effective: effective)
+ // Step 1: Preprocess
+ let preprocessStart = CFAbsoluteTimeGetCurrent()
+ let preprocessed = try preprocess()
+ let preprocessTime = CFAbsoluteTimeGetCurrent() - preprocessStart
+
+ // Step 2: Run ORT inference
+ let inferenceStart = CFAbsoluteTimeGetCurrent()
+ let outputs = try await sessionManager.runDetection(
+ inputData: preprocessed.tensorData,
+ shape: preprocessed.tensorShape
+ )
+ let inferenceTime = CFAbsoluteTimeGetCurrent() - inferenceStart
+
+ // Step 3: Extract the probability map from ORT output
+ // The det model outputs a single tensor with shape [1, 1, H, W]
+ guard let firstOutput = outputs.values.first else {
+ throw DetectionEngineError.noOutputTensor
+ }
+ let outputData = firstOutput.data
+ let outputShape = firstOutput.shape
+
+ // Output shape should be [1, 1, H, W]
+ guard outputShape.count == 4 else {
+ throw DetectionEngineError.unexpectedOutputShape(outputShape)
+ }
+ let tensorHeight = outputShape[2]
+ let tensorWidth = outputShape[3]
+
+ // Step 4: Postprocess
+ let postprocessStart = CFAbsoluteTimeGetCurrent()
+ let boxes = postprocessor.process(
+ outputTensor: outputData,
+ tensorHeight: tensorHeight,
+ tensorWidth: tensorWidth,
+ originalWidth: preprocessed.originalSize.width,
+ originalHeight: preprocessed.originalSize.height
+ )
+ let postprocessTime = CFAbsoluteTimeGetCurrent() - postprocessStart
+
+ let totalTime = preprocessTime + inferenceTime + postprocessTime
+
+ return DetectionResult(
+ boxes: boxes,
+ preprocessTime: preprocessTime,
+ inferenceTime: inferenceTime,
+ postprocessTime: postprocessTime,
+ totalTime: totalTime,
+ inputTensorShape: preprocessed.tensorShape
+ )
+ }
+}
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/EncodedImageCodec.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/EncodedImageCodec.swift
new file mode 100644
index 0000000000000000000000000000000000000000..b4d704da03d3b355e30dedcfea211a8719a14f4c
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/EncodedImageCodec.swift
@@ -0,0 +1,51 @@
+// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import CoreGraphics
+import Foundation
+import ImageIO
+
+enum EncodedImageCodecError: LocalizedError {
+ case emptyData
+ case createImageSourceFailed
+ case createImageFailed
+
+ var errorDescription: String? {
+ switch self {
+ case .emptyData:
+ return "Image data is empty"
+ case .createImageSourceFailed:
+ return "Could not create image source from data"
+ case .createImageFailed:
+ return "Could not decode image from data"
+ }
+ }
+}
+
+/// Decodes PNG/JPEG/WebP and other formats supported by Image I/O into a ``CGImage``.
+enum EncodedImageCodec {
+ static func cgImage(fromEncodedData data: Data) throws -> CGImage {
+ guard !data.isEmpty else {
+ throw EncodedImageCodecError.emptyData
+ }
+ guard let source = CGImageSourceCreateWithData(data as CFData, nil) else {
+ throw EncodedImageCodecError.createImageSourceFailed
+ }
+ let options: [CFString: Any] = [kCGImageSourceShouldCache: true]
+ guard let cgImage = CGImageSourceCreateImageAtIndex(source, 0, options as CFDictionary) else {
+ throw EncodedImageCodecError.createImageFailed
+ }
+ return cgImage
+ }
+}
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/InferenceConfig.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/InferenceConfig.swift
new file mode 100644
index 0000000000000000000000000000000000000000..2568ef5e736d1b501ff6ae03d3c104c914a1ec44
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/InferenceConfig.swift
@@ -0,0 +1,322 @@
+// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import Foundation
+import Yams
+
+// MARK: - Errors
+
+enum InferenceConfigError: LocalizedError {
+ case fileNotFound(String)
+ case parseError(String)
+ case missingField(String)
+
+ var errorDescription: String? {
+ switch self {
+ case .fileNotFound(let path):
+ return "Model config file not found: \(path)"
+ case .parseError(let detail):
+ return "Failed to parse model config: \(detail)"
+ case .missingField(let field):
+ return "Missing required field in model config: \(field)"
+ }
+ }
+}
+
+// MARK: - Transform Operations
+
+/// Channel order implied by `DecodeImage` in the model config file (`img_mode`: BGR or RGB).
+/// When `DecodeImage` is absent, defaults to **BGR** (typical decode path for three-channel inputs).
+enum InferenceImageChannelOrder: Equatable, Sendable {
+ case bgr
+ case rgb
+
+ /// Parses `DecodeImage.img_mode`; empty/missing defaults to **BGR**.
+ static func fromDecodeImage(imgMode: String?) -> InferenceImageChannelOrder {
+ guard let raw = imgMode?.trimmingCharacters(in: .whitespacesAndNewlines), !raw.isEmpty else {
+ return .bgr
+ }
+ switch raw.lowercased() {
+ case "rgb": return .rgb
+ case "bgr": return .bgr
+ default:
+ return .bgr
+ }
+ }
+}
+
+/// Parameters for `DetResizeForTest` from the model config file.
+struct DetResizeParams: Equatable {
+ var limitSideLen: Int
+ var limitType: String
+ var maxSideLimit: Int
+}
+
+/// Represents a single preprocessing transform operation parsed from the model config file.
+/// Each case carries associated parameters read from the model config file.
+enum TransformOp {
+ /// `DecodeImage` in the model config file — selects BGR vs RGB for decoded pixels.
+ case decodeImage(channelOrder: InferenceImageChannelOrder)
+ case detResizeForTest(DetResizeParams)
+ case normalizeImage(scale: Float, mean: [Float], std: [Float], order: String)
+ case toCHWImage
+ case recResizeImg(imageShape: [Int])
+ case unknown(name: String)
+}
+
+// MARK: - Config Structures
+
+struct PreProcessConfig {
+ let transformOps: [TransformOp]
+}
+
+struct PostProcessConfig {
+ let name: String
+ let thresh: Float
+ let boxThresh: Float
+ let maxCandidates: Int
+ let unclipRatio: Float
+ let characterDict: [String]?
+}
+
+struct InferenceConfig {
+ let modelName: String
+ let preProcess: PreProcessConfig
+ let postProcess: PostProcessConfig
+
+ // MARK: - Loading
+
+ /// Loads and parses a model config file into a typed `InferenceConfig`.
+ ///
+ /// - Parameter path: Absolute filesystem path to the model config file.
+ /// - Returns: A fully parsed `InferenceConfig` with typed transform operations.
+ static func load(from path: String) throws -> InferenceConfig {
+ guard FileManager.default.fileExists(atPath: path) else {
+ throw InferenceConfigError.fileNotFound(path)
+ }
+
+ let configFileText: String
+ do {
+ configFileText = try String(contentsOfFile: path, encoding: .utf8)
+ } catch {
+ throw InferenceConfigError.parseError("Cannot read file: \(error.localizedDescription)")
+ }
+
+ guard let root = try Yams.load(yaml: configFileText) as? [String: Any] else {
+ throw InferenceConfigError.parseError("Root element is not a dictionary")
+ }
+
+ // Parse Global.model_name
+ guard let global = root["Global"] as? [String: Any],
+ let modelName = global["model_name"] as? String else {
+ throw InferenceConfigError.missingField("Global.model_name")
+ }
+
+ // Parse PreProcess
+ guard let preProcessDict = root["PreProcess"] as? [String: Any],
+ let transformOpsRaw = preProcessDict["transform_ops"] as? [[String: Any?]] else {
+ throw InferenceConfigError.missingField("PreProcess.transform_ops")
+ }
+
+ let transformOps = transformOpsRaw.map { parseTransformOp($0) }
+ let preProcess = PreProcessConfig(transformOps: transformOps)
+
+ // Parse PostProcess
+ guard let postProcessDict = root["PostProcess"] as? [String: Any] else {
+ throw InferenceConfigError.missingField("PostProcess")
+ }
+ let postProcess = parsePostProcess(postProcessDict)
+
+ return InferenceConfig(
+ modelName: modelName,
+ preProcess: preProcess,
+ postProcess: postProcess
+ )
+ }
+}
+
+extension InferenceConfig {
+ // MARK: - Private Parsing Helpers
+
+ private enum ParseDefaults {
+ static let postThresh: Float = 0.3
+ static let postBoxThresh: Float = 0.6
+ static let postMaxCandidates: Int = 1000
+ static let postUnclipRatio: Float = 2.0
+ static let detResizeLimitSideLen: Int = 960
+ static let detResizeLimitType: String = "max"
+ static let detResizeMaxSideLimit: Int = 4000
+ }
+
+ private static let defaultDetResizeParams = DetResizeParams(
+ limitSideLen: ParseDefaults.detResizeLimitSideLen,
+ limitType: ParseDefaults.detResizeLimitType,
+ maxSideLimit: ParseDefaults.detResizeMaxSideLimit
+ )
+
+ /// Parses a single transform operation dictionary (one key = op name, value = params or null).
+ private static func parseTransformOp(_ dict: [String: Any?]) -> TransformOp {
+ guard let opName = dict.keys.first else {
+ return .unknown(name: "empty")
+ }
+
+ switch opName {
+ case "DecodeImage":
+ let params = dict[opName] as? [String: Any] ?? [:]
+ let imgMode = params["img_mode"] as? String
+ return .decodeImage(channelOrder: InferenceImageChannelOrder.fromDecodeImage(imgMode: imgMode))
+
+ case "DetResizeForTest":
+ let params = dict[opName] as? [String: Any] ?? [:]
+ let limitSideLen =
+ optionalIntValue(params["resize_long"])
+ ?? optionalIntValue(params["limit_side_len"])
+ ?? ParseDefaults.detResizeLimitSideLen
+ let limitType = params["limit_type"] as? String ?? ParseDefaults.detResizeLimitType
+ let maxSideLimit =
+ intValueForKeyIfPresent(params, key: "max_side_limit")
+ ?? ParseDefaults.detResizeMaxSideLimit
+ return .detResizeForTest(
+ DetResizeParams(
+ limitSideLen: limitSideLen,
+ limitType: limitType,
+ maxSideLimit: maxSideLimit
+ )
+ )
+
+ case "NormalizeImage":
+ let params = dict[opName] as? [String: Any] ?? [:]
+ let scale = parseScale(params["scale"])
+ let mean = parseFloatArray(params["mean"]) ?? [0.485, 0.456, 0.406]
+ let std = parseFloatArray(params["std"]) ?? [0.229, 0.224, 0.225]
+ let order = params["order"] as? String ?? "hwc"
+ return .normalizeImage(scale: scale, mean: mean, std: std, order: order)
+
+ case "ToCHWImage":
+ return .toCHWImage
+
+ case "RecResizeImg":
+ let params = dict[opName] as? [String: Any] ?? [:]
+ let imageShape = (params["image_shape"] as? [Any])?.compactMap { toInt($0) } ?? [3, 48, 320]
+ return .recResizeImg(imageShape: imageShape)
+
+ default:
+ return .unknown(name: opName)
+ }
+ }
+
+ /// Parses the `scale` field which may be a numeric value or a fraction string like `"1./255."`.
+ /// Handles string division expressions by splitting on "/" and computing the result.
+ private static func parseScale(_ value: Any?) -> Float {
+ if let floatVal = value as? Double {
+ return Float(floatVal)
+ }
+ if let stringVal = value as? String {
+ return parseScaleString(stringVal)
+ }
+ // Default scale: 1/255
+ return 1.0 / 255.0
+ }
+
+ /// Evaluates a fraction string by splitting on `"/"` and dividing numerator by denominator.
+ private static func parseScaleString(_ s: String) -> Float {
+ if s.contains("/") {
+ let parts = s.split(separator: "/")
+ if parts.count == 2,
+ let numerator = Double(parts[0].trimmingCharacters(in: .init(charactersIn: "."))),
+ let denominator = Double(parts[1].trimmingCharacters(in: .init(charactersIn: "."))) {
+ return Float(numerator / denominator)
+ }
+ let rawParts = s.split(separator: "/")
+ if rawParts.count == 2,
+ let num = Double(rawParts[0]),
+ let den = Double(rawParts[1]) {
+ return Float(num / den)
+ }
+ }
+ if let val = Double(s) {
+ return Float(val)
+ }
+ return 1.0 / 255.0
+ }
+
+ private static func parseFloatArray(_ value: Any?) -> [Float]? {
+ guard let array = value as? [Any] else { return nil }
+ return array.compactMap { element -> Float? in
+ if let d = element as? Double { return Float(d) }
+ if let i = element as? Int { return Float(i) }
+ return nil
+ }
+ }
+
+ private static func parsePostProcess(_ dict: [String: Any]) -> PostProcessConfig {
+ let name = dict["name"] as? String ?? "Unknown"
+ let thresh = (dict["thresh"] as? Double).map { Float($0) } ?? ParseDefaults.postThresh
+ let boxThresh = (dict["box_thresh"] as? Double).map { Float($0) } ?? ParseDefaults.postBoxThresh
+ let maxCandidates = dict["max_candidates"] as? Int ?? ParseDefaults.postMaxCandidates
+ let unclipRatio = (dict["unclip_ratio"] as? Double).map { Float($0) } ?? ParseDefaults.postUnclipRatio
+ let characterDict = (dict["character_dict"] as? [Any])?.compactMap { $0 as? String }
+
+ return PostProcessConfig(
+ name: name,
+ thresh: thresh,
+ boxThresh: boxThresh,
+ maxCandidates: maxCandidates,
+ unclipRatio: unclipRatio,
+ characterDict: characterDict
+ )
+ }
+
+ private static func toInt(_ value: Any) -> Int? {
+ if let i = value as? Int { return i }
+ if let d = value as? Double { return Int(d) }
+ return nil
+ }
+
+ private static func optionalIntValue(_ value: Any?) -> Int? {
+ guard let value else { return nil }
+ return toInt(value)
+ }
+
+ /// Only returns a value when the key exists in `params` (a missing key yields `nil`).
+ private static func stringValueForKeyIfPresent(_ params: [String: Any], key: String) -> String? {
+ guard params.keys.contains(key) else { return nil }
+ return params[key] as? String
+ }
+
+ private static func intValueForKeyIfPresent(_ params: [String: Any], key: String) -> Int? {
+ guard params.keys.contains(key) else { return nil }
+ return optionalIntValue(params[key])
+ }
+
+ /// First `DetResizeForTest` block in the model config file next to the ONNX weights, if present.
+ var detResizeFromModel: DetResizeParams? {
+ for op in preProcess.transformOps {
+ if case .detResizeForTest(let p) = op { return p }
+ }
+ return nil
+ }
+
+ var detResizeForMerge: DetResizeParams {
+ detResizeFromModel ?? Self.defaultDetResizeParams
+ }
+
+ /// First `DecodeImage` entry in `transform_ops` and its `img_mode` (BGR or RGB). If absent, **BGR**.
+ var decodeImageChannelOrder: InferenceImageChannelOrder {
+ for op in preProcess.transformOps {
+ if case .decodeImage(let order) = op { return order }
+ }
+ return .bgr
+ }
+}
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/ModelConfig.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/ModelConfig.swift
new file mode 100644
index 0000000000000000000000000000000000000000..8242098cbdb9d7d85fca13c696553d771d9fd52c
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/ModelConfig.swift
@@ -0,0 +1,107 @@
+// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import Foundation
+
+/// Resolves exported ONNX or ORT-format models and their **model config files** in the app bundle (`Models/det`, `Models/rec`).
+/// Resource lookup uses the conventional shared basename for the weight file and the model config file in each folder.
+/// Weights: prefers `inference*.ort` in the model directory (see ``bundledOrtPath``), otherwise `inference.onnx`.
+struct ModelConfig {
+ let modelPath: String
+ let configPath: String
+ /// From `Global.model_name` in the model config file (same directory as the ONNX weights).
+ let name: String
+
+ /// Human-readable label from `Global.model_name` in the model config file; `fallback` if the file cannot be read.
+ private static func displayName(fromModelConfigPath configPath: String, fallback: String) -> String {
+ guard let cfg = try? InferenceConfig.load(from: configPath) else {
+ return fallback
+ }
+ return cfg.modelName
+ }
+
+ /// Bundled model weights: any `inference*.ort` in the subdirectory (``ortBundledFileMatchOrder``), else `inference.onnx`.
+ ///
+ /// ONNX Runtime’s `convert_onnx_models_to_ort` may emit `inference.with_runtime_opt.ort` instead of `inference.ort`
+ /// when using Runtime optimization style; the bundle cannot resolve that with `forResource: "inference"`.
+ private static let ortResourceStems: [String] = [
+ "inference",
+ "inference.with_runtime_opt",
+ ]
+
+ private static func bundledOrtPath(inDirectory directory: String) -> String? {
+ for stem in ortResourceStems {
+ if let p = Bundle.main.path(forResource: stem, ofType: "ort", inDirectory: directory) {
+ return p
+ }
+ }
+ // Other ORT export suffixes (e.g. non-default optim levels): one `inference*.ort` in the folder.
+ guard let urls = Bundle.main.urls(forResourcesWithExtension: "ort", subdirectory: directory) else {
+ return nil
+ }
+ let matches = urls.filter { url in
+ let c = url.lastPathComponent
+ return c.hasPrefix("inference") && c.hasSuffix(".ort")
+ }
+ guard !matches.isEmpty else { return nil }
+ if matches.count == 1 { return matches[0].path }
+ // If multiple, prefer lexicographic first (deterministic); avoid shipping several exports in one dir.
+ return matches.sorted { $0.lastPathComponent < $1.lastPathComponent }.first?.path
+ }
+
+ private static func bundledWeightsPath(inDirectory directory: String, notFoundMessage: String) throws -> String {
+ if let ort = bundledOrtPath(inDirectory: directory) {
+ return ort
+ }
+ if let onnx = Bundle.main.path(forResource: "inference", ofType: "onnx", inDirectory: directory) {
+ return onnx
+ }
+ throw ModelConfigError.modelNotFound(notFoundMessage)
+ }
+
+ static func detection() throws -> ModelConfig {
+ let modelPath = try Self.bundledWeightsPath(
+ inDirectory: "Models/det",
+ notFoundMessage: "det/inference.ort or det/inference.onnx"
+ )
+ guard let configPath = Bundle.main.path(forResource: "inference", ofType: "yml", inDirectory: "Models/det") else {
+ throw ModelConfigError.modelNotFound("det model config file")
+ }
+ let name = displayName(fromModelConfigPath: configPath, fallback: "text_detection")
+ return ModelConfig(modelPath: modelPath, configPath: configPath, name: name)
+ }
+
+ static func recognition() throws -> ModelConfig {
+ let modelPath = try Self.bundledWeightsPath(
+ inDirectory: "Models/rec",
+ notFoundMessage: "rec/inference.ort or rec/inference.onnx"
+ )
+ guard let configPath = Bundle.main.path(forResource: "inference", ofType: "yml", inDirectory: "Models/rec") else {
+ throw ModelConfigError.modelNotFound("rec model config file")
+ }
+ let name = displayName(fromModelConfigPath: configPath, fallback: "text_recognition")
+ return ModelConfig(modelPath: modelPath, configPath: configPath, name: name)
+ }
+}
+
+enum ModelConfigError: LocalizedError {
+ case modelNotFound(String)
+
+ var errorDescription: String? {
+ switch self {
+ case .modelNotFound(let path):
+ return "Model file not found in bundle: \(path)"
+ }
+ }
+}
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/OCREngine.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/OCREngine.swift
new file mode 100644
index 0000000000000000000000000000000000000000..ec6cee768b835d1320c5c5caa688741169edf618
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/OCREngine.swift
@@ -0,0 +1,293 @@
+// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import CoreGraphics
+import Foundation
+
+// MARK: - OCR run result types
+
+/// A single OCR result: one detected text region with its recognized text.
+struct OCRResult {
+ /// Four corner points of the bounding polygon [x, y] in original image coordinates.
+ /// Order: top-left, top-right, bottom-right, bottom-left.
+ let polygon: [[Int32]]
+ /// The recognized text string from CTC decoding.
+ let text: String
+ /// Recognition confidence score (0.0 to 1.0).
+ let confidence: Float
+}
+
+/// Result of one full OCR run on an image, with per-stage timing.
+struct OCRRunResult {
+ /// All detected and recognized text regions, in reading order.
+ let results: [OCRResult]
+ /// Total time spent in the detection stage (preprocess + inference + postprocess).
+ let detectionTime: TimeInterval
+ /// Seconds in detection preprocessing (resize, normalize, HWC-to-CHW).
+ let detectionPreprocessTime: TimeInterval
+ /// Seconds in detection ONNX inference.
+ let detectionInferenceTime: TimeInterval
+ /// Seconds in detection postprocessing (DB map, contours, polygons).
+ let detectionPostprocessTime: TimeInterval
+ /// Total time spent recognizing all text regions (sum of per-line totals).
+ let recognitionTime: TimeInterval
+ /// Sum of recognition preprocess time across all lines (batched work split per line).
+ let recognitionPreprocessTime: TimeInterval
+ /// Sum of recognition inference time across all lines.
+ let recognitionInferenceTime: TimeInterval
+ /// Sum of recognition postprocess (CTC decode) time across all lines.
+ let recognitionPostprocessTime: TimeInterval
+ /// Wall-clock time for the entire run (detect + sort + crop + recognize).
+ let totalTime: TimeInterval
+ /// Time not attributed to detection or recognition totals.
+ let pipelineOverheadTime: TimeInterval
+ /// Number of text lines from detection sent through recognition.
+ let recognitionLineCount: Int
+ /// Detection model input tensor shape for this OCR run, e.g. [1, 3, H, W].
+ let detectionInputTensorShape: [Int]
+ /// Recognition model input tensor shapes for each non-empty recognition batch.
+ let recognitionInputTensorShapes: [[Int]]
+ /// Per-line recognition ONNX inference times in **seconds**.
+ let lineRecognitionInferenceTimes: [TimeInterval]
+ /// Per-line recognition preprocess times in **seconds**.
+ let lineRecognitionPreprocessTimes: [TimeInterval]
+ /// Per-line recognition postprocess times in **seconds**.
+ let lineRecognitionPostprocessTimes: [TimeInterval]
+}
+
+// MARK: - OCR Engine Errors
+
+enum OCREngineError: LocalizedError {
+ case quadTextCropFailed(boxIndex: Int, underlying: Error)
+ case recognitionBatchSizeMismatch(expected: Int, actual: Int)
+
+ var errorDescription: String? {
+ switch self {
+ case .quadTextCropFailed(let idx, let err):
+ return "Quad text crop failed for box \(idx): \(err.localizedDescription)"
+ case .recognitionBatchSizeMismatch(let expected, let actual):
+ return "Recognition returned \(actual) results but \(expected) crops were sent"
+ }
+ }
+}
+
+// MARK: - OCREngine
+
+/// End-to-end OCR: detect → sort → crop → recognize.
+///
+/// Composes `DetectionEngine`, `BoxSorter`, `QuadTextCrop`, and `RecognitionEngine`
+/// into a single `run(CGImage)` call.
+///
+/// Runs entirely via async/await. Since `DetectionEngine` and
+/// `RecognitionEngine` delegate to `ORTSessionManager` (a Swift actor), all ORT
+/// calls are off the main thread.
+///
+/// Usage:
+/// ```swift
+/// let manager = ORTSessionManager()
+/// try await manager.loadModels()
+/// let engine = try OCREngine(sessionManager: manager)
+/// let result = try await engine.run(cgImage, params: .noOverrides)
+/// for item in result.results {
+/// print("\(item.text) (\(item.confidence))")
+/// }
+/// ```
+class OCREngine {
+ private let detectionEngine: DetectionEngine
+ private let recognitionEngine: RecognitionEngine
+
+ /// Initialize with an existing ORTSessionManager (models must already be loaded).
+ ///
+ /// Creates both DetectionEngine and RecognitionEngine.
+ ///
+ /// - Parameter sessionManager: A loaded ORTSessionManager.
+ /// - Throws: If either engine's model config cannot be loaded.
+ init(sessionManager: ORTSessionManager) throws {
+ self.detectionEngine = try DetectionEngine(sessionManager: sessionManager)
+ self.recognitionEngine = try RecognitionEngine(sessionManager: sessionManager)
+ }
+
+ func baselineRuntimeDefaults() -> ResolvedOCRRuntimeParams {
+ ResolvedOCRRuntimeParams.fromModelConfig(detectionEngine.modelConfig)
+ }
+
+ /// Run full OCR on an image.
+ ///
+ /// End-to-end OCR flow (detect → sort → crop → recognize):
+ /// 1. **Detect**: Run detection to get bounding polygons
+ /// 2. **Sort**: Sort boxes in reading order (top-to-bottom, left-to-right)
+ /// 3. **Crop + Recognize**: Crop each region, order crops by ascending width/height for batched
+ /// recognition, then map results back to reading order
+ ///
+ /// - Parameters:
+ /// - image: The input `CGImage` to process.
+ /// - params: Optional runtime parameter overrides (see ``OCRRuntimeParams``).
+ /// - Returns: `OCRRunResult` with all line results and timing.
+ func run(_ image: CGImage, params: OCRRuntimeParams = .noOverrides) async throws -> OCRRunResult {
+ let runStart = CFAbsoluteTimeGetCurrent()
+
+ let resolved = params.resolved(detectionEngine.modelConfig)
+
+ let detResult = try await detectionEngine.detect(image, runtimeParams: params)
+ let sortedBoxes = BoxSorter.sortInReadingOrder(detResult.boxes)
+ let (ocrResults, recTiming, perLine) = try await recognizeSortedBoxes(
+ sortedBoxes,
+ sourceImage: image,
+ resolved: resolved
+ )
+
+ let totalTime = CFAbsoluteTimeGetCurrent() - runStart
+ let overhead = totalTime - detResult.totalTime - recTiming.total
+
+ return OCRRunResult(
+ results: ocrResults,
+ detectionTime: detResult.totalTime,
+ detectionPreprocessTime: detResult.preprocessTime,
+ detectionInferenceTime: detResult.inferenceTime,
+ detectionPostprocessTime: detResult.postprocessTime,
+ recognitionTime: recTiming.total,
+ recognitionPreprocessTime: recTiming.preprocess,
+ recognitionInferenceTime: recTiming.inference,
+ recognitionPostprocessTime: recTiming.postprocess,
+ totalTime: totalTime,
+ pipelineOverheadTime: max(0, overhead),
+ recognitionLineCount: perLine.count,
+ detectionInputTensorShape: detResult.inputTensorShape,
+ recognitionInputTensorShapes: perLine.inputTensorShapes,
+ lineRecognitionInferenceTimes: perLine.inference,
+ lineRecognitionPreprocessTimes: perLine.preprocess,
+ lineRecognitionPostprocessTimes: perLine.postprocess
+ )
+ }
+
+ /// Aggregated recognition timing across all lines (sums of per-line split batch times).
+ private struct RecognitionTimingAggregate {
+ let preprocess: TimeInterval
+ let inference: TimeInterval
+ let postprocess: TimeInterval
+ let total: TimeInterval
+ }
+
+ /// Per-line rec timings
+ private struct PerLineRecognitionTimes {
+ let count: Int
+ let inputTensorShapes: [[Int]]
+ let inference: [TimeInterval]
+ let preprocess: [TimeInterval]
+ let postprocess: [TimeInterval]
+ }
+
+ private func recognizeSortedBoxes(
+ _ sortedBoxes: [DetectionBox],
+ sourceImage: CGImage,
+ resolved: ResolvedOCRRuntimeParams
+ ) async throws -> ([OCRResult], RecognitionTimingAggregate, PerLineRecognitionTimes) {
+ struct LineCrop {
+ let lineIndex: Int
+ let aspectRatio: Float
+ let crop: CGImage
+ let polygon: [[Int32]]
+ }
+
+ var lines: [LineCrop] = []
+ lines.reserveCapacity(sortedBoxes.count)
+
+ for (index, box) in sortedBoxes.enumerated() {
+ let croppedImage: CGImage
+ do {
+ croppedImage = try QuadTextCrop.crop(sourceImage, polygon: box.points)
+ } catch {
+ throw OCREngineError.quadTextCropFailed(boxIndex: index, underlying: error)
+ }
+ let h = max(croppedImage.height, 1)
+ let aspect = Float(croppedImage.width) / Float(h)
+ lines.append(LineCrop(lineIndex: index, aspectRatio: aspect, crop: croppedImage, polygon: box.points))
+ }
+
+ let sortedForInference = lines.sorted { a, b in
+ if a.aspectRatio != b.aspectRatio {
+ return a.aspectRatio < b.aspectRatio
+ }
+ return a.lineIndex < b.lineIndex
+ }
+
+ let batchSize = max(1, resolved.textRecBatchSize)
+ var perLine: [RecognitionEngineResult?] = Array(repeating: nil, count: lines.count)
+ var batchInputShapes: [[Int]] = []
+
+ var chunkStart = 0
+ while chunkStart < sortedForInference.count {
+ let chunkEnd = min(chunkStart + batchSize, sortedForInference.count)
+ let chunk = Array(sortedForInference[chunkStart.. ResolvedOCRRuntimeParams {
+ let base = ResolvedOCRRuntimeParams.fromModelConfig(modelConfig)
+ return ResolvedOCRRuntimeParams(
+ textDetLimitSideLen: textDetLimitSideLen ?? base.textDetLimitSideLen,
+ textDetLimitType: textDetLimitType ?? base.textDetLimitType,
+ textDetMaxSideLimit: textDetMaxSideLimit ?? base.textDetMaxSideLimit,
+ textDetThresh: textDetThresh ?? base.textDetThresh,
+ textDetBoxThresh: textDetBoxThresh ?? base.textDetBoxThresh,
+ textDetUnclipRatio: textDetUnclipRatio ?? base.textDetUnclipRatio,
+ textRecBatchSize: textRecBatchSize ?? base.textRecBatchSize,
+ textRecScoreThresh: textRecScoreThresh ?? base.textRecScoreThresh
+ )
+ }
+}
+
+extension ResolvedOCRRuntimeParams {
+ /// Baseline with no UI overrides, using ``OCRParameterResolver`` app-tier defaults.
+ static func fromModelConfig(_ modelConfig: InferenceConfig) -> ResolvedOCRRuntimeParams {
+ let o = OCRParameterResolver.effective(modelConfig: modelConfig, runtime: .noOverrides)
+ return ResolvedOCRRuntimeParams(
+ textDetLimitSideLen: o.mergedLimitSideLen,
+ textDetLimitType: o.mergedLimitType,
+ textDetMaxSideLimit: o.mergedMaxSideLimit,
+ textDetThresh: o.detThresh,
+ textDetBoxThresh: o.detBoxThresh,
+ textDetUnclipRatio: o.detUnclipRatio,
+ textRecBatchSize: o.textRecBatchSize,
+ textRecScoreThresh: o.recScoreThresh
+ )
+ }
+}
+
+// MARK: - Effective parameters (single merge pass)
+
+struct EffectiveOCRParams: Equatable, Sendable {
+ // Detection — preprocess
+ var detResize: DetResizeParams
+ var mergedLimitSideLen: Int
+ var mergedLimitType: String
+ var mergedMaxSideLimit: Int
+ // Detection — postprocess
+ var detThresh: Float
+ var detBoxThresh: Float
+ var detUnclipRatio: Float
+ // Recognition
+ var textRecBatchSize: Int
+ var recScoreThresh: Float
+}
+
+// MARK: - Resolver
+
+enum OCRParameterResolver {
+
+ // MARK: App tier
+
+ // Detection — preprocess
+ static let textDetLimitSideLenAppDefault: Int = 64
+ static let textDetLimitTypeAppDefault: String = "min"
+ static let textDetMaxSideLimitAppDefault: Int = 4000
+ // Detection — postprocess
+ static let textDetThreshAppDefault: Float = 0.3
+ static let textDetBoxThreshAppDefault: Float = 0.6
+ static let textDetUnclipRatioAppDefault: Float = 1.5
+ // Recognition
+ static let textRecBatchSizeAppDefault: Int = 1
+ static let textRecScoreThreshAppDefault: Float = 0.0
+
+ // MARK: Merge helpers
+
+ /// UI override if set; otherwise the app-tier default.
+ private static func mergeFloat(ui: Float?, app: Float) -> Float {
+ ui ?? app
+ }
+
+ private static func mergeInt(ui: Int?, app: Int) -> Int {
+ ui ?? app
+ }
+
+ private static func mergeString(ui: String?, app: String) -> String {
+ ui ?? app
+ }
+
+ private static func mergedLimitSideFields(runtime: OCRRuntimeParams) -> (
+ sideLen: Int, limitType: String, maxSide: Int
+ ) {
+ let sideLen = mergeInt(
+ ui: runtime.textDetLimitSideLen,
+ app: Self.textDetLimitSideLenAppDefault
+ )
+ let limitType = mergeString(
+ ui: runtime.textDetLimitType,
+ app: Self.textDetLimitTypeAppDefault
+ )
+ let maxSide = mergeInt(
+ ui: runtime.textDetMaxSideLimit,
+ app: Self.textDetMaxSideLimitAppDefault
+ )
+ return (sideLen, limitType, maxSide)
+ }
+
+ private static func limitSideParams(runtime: OCRRuntimeParams) -> DetResizeParams {
+ let m = mergedLimitSideFields(runtime: runtime)
+ return DetResizeParams(
+ limitSideLen: m.sideLen,
+ limitType: m.limitType,
+ maxSideLimit: m.maxSide
+ )
+ }
+
+ /// Merges ``OCRRuntimeParams`` with app-tier defaults (UI → app). The `modelConfig` argument is
+ /// unused at this moment.
+ static func effective(modelConfig _: InferenceConfig, runtime: OCRRuntimeParams) -> EffectiveOCRParams {
+ let detResize = limitSideParams(runtime: runtime)
+ let merged = mergedLimitSideFields(runtime: runtime)
+
+ let detThresh = mergeFloat(
+ ui: runtime.textDetThresh,
+ app: Self.textDetThreshAppDefault
+ )
+ let detBoxThresh = mergeFloat(
+ ui: runtime.textDetBoxThresh,
+ app: Self.textDetBoxThreshAppDefault
+ )
+ let detUnclipRatio = mergeFloat(
+ ui: runtime.textDetUnclipRatio,
+ app: Self.textDetUnclipRatioAppDefault
+ )
+
+ let textRecBatchSize = mergeInt(
+ ui: runtime.textRecBatchSize,
+ app: Self.textRecBatchSizeAppDefault
+ )
+ let recScoreThresh = runtime.textRecScoreThresh ?? Self.textRecScoreThreshAppDefault
+
+ return EffectiveOCRParams(
+ detResize: detResize,
+ mergedLimitSideLen: merged.sideLen,
+ mergedLimitType: merged.limitType,
+ mergedMaxSideLimit: merged.maxSide,
+ detThresh: detThresh,
+ detBoxThresh: detBoxThresh,
+ detUnclipRatio: detUnclipRatio,
+ textRecBatchSize: textRecBatchSize,
+ recScoreThresh: recScoreThresh
+ )
+ }
+}
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/ORTProfilingBridge.h b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/ORTProfilingBridge.h
new file mode 100644
index 0000000000000000000000000000000000000000..f52d78a41854c9c6fae8bdcb0c2448ed73b309c9
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/ORTProfilingBridge.h
@@ -0,0 +1,45 @@
+// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import
+
+@class ORTSession;
+@class ORTSessionOptions;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/// Bridges ONNX Runtime C++ profiling APIs that are not exposed on
+/// `onnxruntime-objc`'s public surface.
+///
+/// - Warning: `endProfilingOnSession` reads the private `_session` ivar on
+/// `ORTSession` (layout depends on the
+/// installed `onnxruntime-objc` version). Re-verify after pod upgrades.
+@interface ORTProfilingBridge : NSObject
+
++ (BOOL)enableProfilingOnSessionOptions:(ORTSessionOptions *)options
+ pathPrefix:(NSString *)pathPrefix
+ error:(NSError *_Nullable __autoreleasing *)
+ error
+ NS_SWIFT_NAME(enableProfiling(sessionOptions:pathPrefix:));
+
+/// Ends profiling and returns the path to the written JSON (see ORT
+/// `Session::EndProfilingAllocated`).
++ (nullable NSString *)
+ endProfilingOnSession:(ORTSession *)session
+ error:(NSError *_Nullable __autoreleasing *)error
+ NS_SWIFT_NAME(endProfiling(session:));
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/ORTProfilingBridge.mm b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/ORTProfilingBridge.mm
new file mode 100644
index 0000000000000000000000000000000000000000..046857d6b09da74fbcbe0a5a3f8f7052d0ee1da6
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/ORTProfilingBridge.mm
@@ -0,0 +1,72 @@
+// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "ORTProfilingBridge.h"
+
+#import "ort_session_internal.h"
+
+#import "cxx_api.h"
+#import "error_utils.h"
+
+#import
+
+#include
+
+@implementation ORTProfilingBridge
+
++ (BOOL)enableProfilingOnSessionOptions:(ORTSessionOptions *)options
+ pathPrefix:(NSString *)pathPrefix
+ error:(NSError **)error {
+ if (options == nil || pathPrefix.length == 0) {
+ ORTSaveCodeAndDescriptionToError(ORT_INVALID_ARGUMENT, @"ORTProfilingBridge: options and non-empty pathPrefix required", error);
+ return NO;
+ }
+ try {
+ [options CXXAPIOrtSessionOptions].EnableProfiling(pathPrefix.fileSystemRepresentation);
+ return YES;
+ }
+ ORT_OBJC_API_IMPL_CATCH_RETURNING_BOOL(error);
+}
+
++ (NSString *)endProfilingOnSession:(ORTSession *)session error:(NSError **)error {
+ if (session == nil) {
+ ORTSaveCodeAndDescriptionToError(ORT_INVALID_ARGUMENT, @"ORTProfilingBridge: session is nil", error);
+ return nil;
+ }
+ try {
+ Ivar ivar = class_getInstanceVariable([ORTSession class], "_session");
+ if (ivar == nullptr) {
+ ORTSaveCodeAndDescriptionToError(ORT_FAIL,
+ @"ORTProfilingBridge: ORTSession has no _session ivar — onnxruntime-objc mismatch?",
+ error);
+ return nil;
+ }
+ uintptr_t base = reinterpret_cast(session);
+ auto *opt = reinterpret_cast *>(base + ivar_getOffset(ivar));
+ if (opt == nullptr || !opt->has_value()) {
+ ORTSaveCodeAndDescriptionToError(ORT_FAIL, @"ORTProfilingBridge: ORTSession is not ready", error);
+ return nil;
+ }
+ Ort::AllocatorWithDefaultOptions allocator;
+ Ort::AllocatedStringPtr path = opt->value().EndProfilingAllocated(allocator);
+ const char *cstr = path.get();
+ if (cstr == nullptr) {
+ return nil;
+ }
+ return [NSString stringWithUTF8String:cstr];
+ }
+ ORT_OBJC_API_IMPL_CATCH_RETURNING_NULLABLE(error);
+}
+
+@end
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/ORTSessionManager.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/ORTSessionManager.swift
new file mode 100644
index 0000000000000000000000000000000000000000..842e5ada2ba3bfd7c75af60c727a6da10b6d5155
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/ORTSessionManager.swift
@@ -0,0 +1,338 @@
+// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import Foundation
+
+/// Preset for which ONNX Runtime execution provider is registered first on the session chain in this demo.
+enum ORTPrimaryExecutionProvider: String, CaseIterable, Identifiable, Sendable {
+ /// Default CPU execution. Default for this demo.
+ case cpu = "cpu"
+ /// Registers the XNNPACK EP; nodes it cannot run still use the CPU EP.
+ case xnnpack = "xnnpack"
+ /// Registers the Core ML EP; remaining subgraphs use other registered EPs (commonly CPU).
+ case coreML = "core_ml"
+
+ var id: String { rawValue }
+
+ var displayTitle: String {
+ switch self {
+ case .cpu: return "CPU"
+ case .xnnpack: return "XNNPACK"
+ case .coreML: return "Core ML"
+ }
+ }
+}
+
+enum ORTCoreMLProviderOption: String, CaseIterable, Identifiable, Sendable {
+ case enableOnSubgraphs
+ case createMLProgram
+ case staticInputShapes
+ case cpuOnly
+ case cpuAndGPU
+ case aneOnly
+
+ var id: String { rawValue }
+
+ var displayTitle: String {
+ switch self {
+ case .enableOnSubgraphs: return "Enable on subgraphs"
+ case .createMLProgram: return "Use ML program"
+ case .staticInputShapes: return "Fixed tensor shapes only"
+ case .cpuOnly: return "Core ML CPU only"
+ case .cpuAndGPU: return "Core ML CPU and GPU"
+ case .aneOnly: return "Only on devices with ANE"
+ }
+ }
+
+ var detailCaption: String {
+ switch self {
+ case .enableOnSubgraphs: return "Compile subgraphs independently for Core ML."
+ case .createMLProgram: return "Prefer MLProgram over NeuralNetwork (when supported)."
+ case .staticInputShapes:
+ return "Core ML tries to run only ops whose input tensors have fixed shapes at inference time; parts with varying dimensions typically stay on the CPU EP instead."
+ case .cpuOnly: return "Forces Core ML compute on CPU. Mutually exclusive with CPU and GPU."
+ case .cpuAndGPU: return "Allows Core ML to use CPU and GPU. Mutually exclusive with CPU only."
+ case .aneOnly: return "Only enable Core ML EP on devices that have Apple Neural Engine hardware."
+ }
+ }
+}
+
+enum ORTSessionManagerError: LocalizedError {
+ case modelNotFound(String)
+ case sessionCreationFailed(String)
+ case inferenceFailed(String)
+ case inputTensorElementCountMismatch(expected: Int, actual: Int, modelName: String)
+ case outputContainsNaN(String)
+ case ortProfilingFailed(String)
+
+ var errorDescription: String? {
+ switch self {
+ case .modelNotFound(let name): return "Model not found: \(name)"
+ case .sessionCreationFailed(let detail): return "Session creation failed: \(detail)"
+ case .inferenceFailed(let detail): return "Inference failed: \(detail)"
+ case .inputTensorElementCountMismatch(let expected, let actual, let modelName):
+ return "\(modelName): input tensor size mismatch (expected \(expected) floats, got \(actual))"
+ case .outputContainsNaN(let name): return "Output tensor '\(name)' contains NaN values"
+ case .ortProfilingFailed(let detail): return "ONNX Runtime profiling: \(detail)"
+ }
+ }
+}
+
+/// File URLs of ONNX Runtime JSON profiles after ``ORTSessionManager/finalizeORTProfiling()`` (separate file per sub-model).
+struct ORTProfilingOutput: Sendable {
+ var detectionProfileJSON: URL
+ var recognitionProfileJSON: URL
+}
+
+/// Tuning options for ORT session creation.
+struct ORTSessionTuningOptions: Sendable {
+ /// Session-level intra-op thread count. 0 = ORT default.
+ var intraOpThreads: Int = 0
+ /// XNNPACK EP thread count. 0 = ORT default.
+ var xnnpackThreads: Int = 0
+ /// CoreML EP flags: enableOnSubgraphs, createMLProgram, staticInputShapes, cpuOnly, cpuAndGPU, aneOnly.
+ var coreMLFlags: Set = []
+
+ static let `default` = ORTSessionTuningOptions()
+}
+
+extension ORTSessionTuningOptions: Equatable {}
+
+/// At most one placement-related Core ML EP option is applied. Priority matches
+/// ``OnnxRuntimeSettingsPanel`` when several keys appear (e.g. benchmark env / JSON).
+private func applyCoreMLPlacementFlags(from flags: Set, to coreml: ORTCoreMLExecutionProviderOptions) {
+ if flags.contains(ORTCoreMLProviderOption.cpuOnly.rawValue) {
+ coreml.useCPUOnly = true
+ } else if flags.contains(ORTCoreMLProviderOption.cpuAndGPU.rawValue) {
+ coreml.useCPUAndGPU = true
+ } else if flags.contains(ORTCoreMLProviderOption.aneOnly.rawValue) {
+ coreml.onlyEnableForDevicesWithANE = true
+ }
+}
+
+actor ORTSessionManager {
+ private struct SessionIONames {
+ let inputName: String
+ let outputNames: Set
+ }
+
+ private var env: ORTEnv?
+ private var detSession: ORTSession?
+ private var recSession: ORTSession?
+ private var detIO: SessionIONames?
+ private var recIO: SessionIONames?
+ private var ortProfilingPendingFinalize: Bool = false
+
+ /// Load both models. Creates one ORTEnv (per ORT docs: one per process),
+ /// configures execution providers per ``ORTPrimaryExecutionProvider``,
+ /// and creates sessions for detection and recognition models.
+ ///
+ /// - Parameter ortProfiling: When `true`, enables ONNX Runtime session profiling (written as JSON on finalize).
+ /// Profiling adds overhead; do not treat wall-clock times from the same run as a clean latency benchmark.
+ /// - Parameter tuning: Session tuning options (threads, EP flags). Default reads from environment.
+ func loadModels(executionProvider: ORTPrimaryExecutionProvider = .cpu, ortProfiling: Bool = false, tuning: ORTSessionTuningOptions = .default) async throws {
+ let env = try ORTEnv(loggingLevel: .warning)
+ self.env = env
+ ortProfilingPendingFinalize = ortProfiling
+
+ let detConfig = try ModelConfig.detection()
+ let recConfig = try ModelConfig.recognition()
+
+ if ortProfiling {
+ let baseDir = try ortProfilingDirectoryURL()
+ let detPrefix = baseDir.appendingPathComponent("paddle_ort_det", isDirectory: false).path
+ let recPrefix = baseDir.appendingPathComponent("paddle_ort_rec", isDirectory: false).path
+ let detOptions = try makeSessionOptions(executionProvider: executionProvider, ortProfilePathPrefix: detPrefix, tuning: tuning)
+ let recOptions = try makeSessionOptions(executionProvider: executionProvider, ortProfilePathPrefix: recPrefix, tuning: tuning)
+ detSession = try ORTSession(env: env, modelPath: detConfig.modelPath, sessionOptions: detOptions)
+ recSession = try ORTSession(env: env, modelPath: recConfig.modelPath, sessionOptions: recOptions)
+ } else {
+ let options = try makeSessionOptions(executionProvider: executionProvider, ortProfilePathPrefix: nil, tuning: tuning)
+ detSession = try ORTSession(env: env, modelPath: detConfig.modelPath, sessionOptions: options)
+ recSession = try ORTSession(env: env, modelPath: recConfig.modelPath, sessionOptions: options)
+ }
+ if let detSession {
+ detIO = try makeSessionIONames(session: detSession, modelName: "det")
+ }
+ if let recSession {
+ recIO = try makeSessionIONames(session: recSession, modelName: "rec")
+ }
+ }
+
+ /// Flushes ORT profiling and returns the JSON file paths. Call once after the last inference when
+ /// ``loadModels(ortProfiling:)`` was `true`. Safe to call when profiling was disabled (returns `nil`).
+ func finalizeORTProfiling() throws -> ORTProfilingOutput? {
+ guard ortProfilingPendingFinalize else { return nil }
+ ortProfilingPendingFinalize = false
+ guard let det = detSession, let rec = recSession else {
+ throw ORTSessionManagerError.sessionCreationFailed("Cannot finalize ORT profiling: session missing")
+ }
+ let detPath = try ORTProfilingBridge.endProfiling(session: det)
+ let recPath = try ORTProfilingBridge.endProfiling(session: rec)
+ if detPath.isEmpty || recPath.isEmpty {
+ throw ORTSessionManagerError.ortProfilingFailed("EndProfiling returned an empty path")
+ }
+ return ORTProfilingOutput(
+ detectionProfileJSON: URL(fileURLWithPath: detPath),
+ recognitionProfileJSON: URL(fileURLWithPath: recPath)
+ )
+ }
+
+ private func ortProfilingDirectoryURL() throws -> URL {
+ let base = try FileManager.default.url(
+ for: .cachesDirectory,
+ in: .userDomainMask,
+ appropriateFor: nil,
+ create: true
+ )
+ let dir = base.appendingPathComponent("PaddleOCRORTProfiling", isDirectory: true)
+ try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
+ return dir
+ }
+
+ private func makeSessionOptions(executionProvider: ORTPrimaryExecutionProvider, ortProfilePathPrefix: String?, tuning: ORTSessionTuningOptions) throws -> ORTSessionOptions {
+ let options = try ORTSessionOptions()
+ try options.setGraphOptimizationLevel(.all)
+ if tuning.intraOpThreads > 0 {
+ try options.setIntraOpNumThreads(Int32(tuning.intraOpThreads))
+ }
+ if let prefix = ortProfilePathPrefix {
+ try ORTProfilingBridge.enableProfiling(sessionOptions: options, pathPrefix: prefix)
+ }
+ switch executionProvider {
+ case .cpu:
+ break
+ case .xnnpack:
+ let xnnpackOptions = ORTXnnpackExecutionProviderOptions()
+ if tuning.xnnpackThreads > 0 {
+ xnnpackOptions.intra_op_num_threads = Int32(tuning.xnnpackThreads)
+ }
+ try options.appendXnnpackExecutionProvider(with: xnnpackOptions)
+ case .coreML:
+ let coremlOptions = ORTCoreMLExecutionProviderOptions()
+ let flags = tuning.coreMLFlags
+ if flags.contains("enableOnSubgraphs") { coremlOptions.enableOnSubgraphs = true }
+ if flags.contains("createMLProgram") { coremlOptions.createMLProgram = true }
+ if flags.contains("staticInputShapes") { coremlOptions.onlyAllowStaticInputShapes = true }
+ applyCoreMLPlacementFlags(from: flags, to: coremlOptions)
+ try options.appendCoreMLExecutionProvider(with: coremlOptions)
+ }
+ return options
+ }
+
+ private func makeSessionIONames(session: ORTSession, modelName: String) throws -> SessionIONames {
+ let inputNames = try session.inputNames()
+ let outputNamesList = try session.outputNames()
+ guard let firstInputName = inputNames.first else {
+ throw ORTSessionManagerError.inferenceFailed("\(modelName): no input names found")
+ }
+ return SessionIONames(inputName: firstInputName, outputNames: Set(outputNamesList))
+ }
+
+ /// Run detection inference with real preprocessed input data.
+ ///
+ /// - Parameters:
+ /// - inputData: Float32 array in CHW layout; length must equal the product of `shape`.
+ /// - shape: Tensor shape, e.g. [1, 3, 896, 960].
+ /// - Returns: Dictionary mapping output name to (data as [Float], shape as [Int]).
+ func runDetection(inputData: [Float], shape: [Int]) async throws -> [String: (data: [Float], shape: [Int])] {
+ guard let session = detSession else {
+ throw ORTSessionManagerError.sessionCreationFailed("Detection session not loaded")
+ }
+ guard let io = detIO else {
+ throw ORTSessionManagerError.sessionCreationFailed("Detection session IO names not loaded")
+ }
+ return try runInference(session: session, io: io, modelName: "det", inputData: inputData, shape: shape)
+ }
+
+ /// Run recognition inference with preprocessed input data.
+ ///
+ /// The recognition model accepts dynamic batch and width: shape `[N, 3, H, W]` with `N ≥ 1`,
+ /// fixed `H` (e.g. 48), and `W` shared after padding within each batch. A single line uses `N = 1`.
+ ///
+ /// - Parameters:
+ /// - inputData: Float32 row-major tensor; length must equal the product of `shape`.
+ /// - shape: e.g. `[1, 3, 48, 320]` or `[6, 3, 48, 640]` for a batch of six lines.
+ /// - Returns: Dictionary mapping output name to (data as [Float], shape as [Int]).
+ func runRecognition(inputData: [Float], shape: [Int]) async throws -> [String: (data: [Float], shape: [Int])] {
+ guard let session = recSession else {
+ throw ORTSessionManagerError.sessionCreationFailed("Recognition session not loaded")
+ }
+ guard let io = recIO else {
+ throw ORTSessionManagerError.sessionCreationFailed("Recognition session IO names not loaded")
+ }
+ return try runInference(session: session, io: io, modelName: "rec", inputData: inputData, shape: shape)
+ }
+
+ /// Shared inference logic used by both detection and recognition.
+ ///
+ /// Discovers input/output names at runtime, creates a float32 tensor from the
+ /// provided data and shape, runs the session, extracts output arrays, and
+ /// validates that no output contains NaN values.
+ private func runInference(
+ session: ORTSession,
+ io: SessionIONames,
+ modelName: String,
+ inputData: [Float],
+ shape: [Int]
+ ) throws -> [String: (data: [Float], shape: [Int])] {
+ let expectedElements = shape.reduce(1, *)
+ guard expectedElements == inputData.count else {
+ throw ORTSessionManagerError.inputTensorElementCountMismatch(
+ expected: expectedElements,
+ actual: inputData.count,
+ modelName: modelName
+ )
+ }
+
+ // Create ORT tensor from input data
+ let nsShape = shape.map { NSNumber(value: $0) }
+ var data = inputData
+ let tensorData = NSMutableData(
+ bytes: &data,
+ length: inputData.count * MemoryLayout.stride
+ )
+ let inputTensor = try ORTValue(
+ tensorData: tensorData,
+ elementType: .float,
+ shape: nsShape
+ )
+
+ let inputs: [String: ORTValue] = [io.inputName: inputTensor]
+ let outputs = try session.run(
+ withInputs: inputs,
+ outputNames: io.outputNames,
+ runOptions: nil
+ )
+
+ // Extract output tensors as Float arrays
+ var result: [String: (data: [Float], shape: [Int])] = [:]
+ for (name, value) in outputs {
+ let info = try value.tensorTypeAndShapeInfo()
+ let outputShape = info.shape.map { $0.intValue }
+ let outputData = try value.tensorData() as Data
+ let floats: [Float] = outputData.withUnsafeBytes { buffer in
+ Array(buffer.bindMemory(to: Float.self))
+ }
+
+ if floats.contains(where: \.isNaN) {
+ throw ORTSessionManagerError.outputContainsNaN(name)
+ }
+
+ result[name] = (data: floats, shape: outputShape)
+ }
+
+ return result
+ }
+}
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/OpenCVDBBridge.h b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/OpenCVDBBridge.h
new file mode 100644
index 0000000000000000000000000000000000000000..2ed51159549e581fb023ad1432973c11a83ab534
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/OpenCVDBBridge.h
@@ -0,0 +1,45 @@
+// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface PDBMiniBoxResult : NSObject
+@property(nonatomic) NSArray *corners;
+@property(nonatomic) float minSide;
+@end
+
+@interface PDBOpenCVDBBridge : NSObject
+
++ (NSArray *> *)findContoursWithBinaryMask:(NSData *)data
+ width:(NSInteger)width
+ height:(NSInteger)height
+ maxCandidates:
+ (NSInteger)maxCandidates
+ NS_SWIFT_NAME(findContours(binaryMask:width:height:maxCandidates:));
+
++ (nullable PDBMiniBoxResult *)miniBoxFromPoints:(NSArray *)points
+ NS_SWIFT_NAME(miniBox(fromPoints:));
+
++ (float)contourAreaFromPoints:(NSArray *)points
+ NS_SWIFT_NAME(contourArea(fromPoints:));
+
++ (float)arcLengthFromPoints:(NSArray *)points
+ closed:(BOOL)closed
+ NS_SWIFT_NAME(arcLength(fromPoints:closed:));
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/OpenCVDBBridge.mm b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/OpenCVDBBridge.mm
new file mode 100644
index 0000000000000000000000000000000000000000..55cfcceabb032bf659f85431845a04c36bdca443
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/OpenCVDBBridge.mm
@@ -0,0 +1,147 @@
+// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "OpenCVDBBridge.h"
+// Include imgproc before UIKit: Objective-C's `NO` macro breaks OpenCV enums named `NO` in opencv.hpp.
+#import
+#import
+#import
+#include
+
+@implementation PDBMiniBoxResult
+@end
+
+@implementation PDBOpenCVDBBridge
+
++ (NSArray *> *)findContoursWithBinaryMask:(NSData *)data
+ width:(NSInteger)width
+ height:(NSInteger)height
+ maxCandidates:(NSInteger)maxCandidates {
+ if (width <= 0 || height <= 0 || data.length < (NSUInteger)(width * height)) {
+ return @[];
+ }
+
+ const uint8_t *src = static_cast(data.bytes);
+ cv::Mat mat((int)height, (int)width, CV_8UC1);
+ for (int y = 0; y < (int)height; y++) {
+ uint8_t *row = mat.ptr(y);
+ for (int x = 0; x < (int)width; x++) {
+ row[x] = src[y * (int)width + x] ? 255 : 0;
+ }
+ }
+
+ std::vector> contours;
+ cv::findContours(mat, contours, cv::RETR_LIST, cv::CHAIN_APPROX_SIMPLE);
+
+ const NSInteger n = std::min((NSInteger)contours.size(), maxCandidates);
+ NSMutableArray *out = [NSMutableArray arrayWithCapacity:(NSUInteger)n];
+ for (NSInteger i = 0; i < n; i++) {
+ const auto &c = contours[(size_t)i];
+ NSMutableArray *pts = [NSMutableArray arrayWithCapacity:c.size()];
+ for (const auto &pt : c) {
+ [pts addObject:[NSValue valueWithCGPoint:CGPointMake(pt.x, pt.y)]];
+ }
+ [out addObject:pts];
+ }
+ return out;
+}
+
++ (nullable PDBMiniBoxResult *)miniBoxFromPoints:(NSArray *)points {
+ if (points.count < 2) {
+ return nil;
+ }
+
+ std::vector pts;
+ pts.reserve(points.count);
+ for (NSValue *v in points) {
+ CGPoint p = v.CGPointValue;
+ pts.emplace_back((float)p.x, (float)p.y);
+ }
+
+ cv::RotatedRect rr = cv::minAreaRect(pts);
+ float w = rr.size.width;
+ float h = rr.size.height;
+ if (w <= 0 && h <= 0) {
+ return nil;
+ }
+
+ cv::Point2f cvPts[4];
+ rr.points(cvPts);
+
+ // Stable sort by x only (min-area box corner order).
+ int ord[4] = {0, 1, 2, 3};
+ std::stable_sort(ord, ord + 4, [&](int a, int b) { return cvPts[a].x < cvPts[b].x; });
+ cv::Point2f sortedPts[4];
+ for (int i = 0; i < 4; i++) {
+ sortedPts[i] = cvPts[ord[i]];
+ }
+
+ NSInteger index1, index2, index3, index4;
+ cv::Point2f p0 = sortedPts[0];
+ cv::Point2f p1 = sortedPts[1];
+ if (p1.y > p0.y) {
+ index1 = 0;
+ index4 = 1;
+ } else {
+ index1 = 1;
+ index4 = 0;
+ }
+ cv::Point2f p2 = sortedPts[2];
+ cv::Point2f p3 = sortedPts[3];
+ if (p3.y > p2.y) {
+ index2 = 2;
+ index3 = 3;
+ } else {
+ index2 = 3;
+ index3 = 2;
+ }
+
+ PDBMiniBoxResult *res = [[PDBMiniBoxResult alloc] init];
+ res.corners = @[
+ [NSValue valueWithCGPoint:CGPointMake(sortedPts[index1].x, sortedPts[index1].y)],
+ [NSValue valueWithCGPoint:CGPointMake(sortedPts[index2].x, sortedPts[index2].y)],
+ [NSValue valueWithCGPoint:CGPointMake(sortedPts[index3].x, sortedPts[index3].y)],
+ [NSValue valueWithCGPoint:CGPointMake(sortedPts[index4].x, sortedPts[index4].y)],
+ ];
+ res.minSide = std::min(w, h);
+ return res;
+}
+
++ (float)contourAreaFromPoints:(NSArray *)points {
+ if (points.count < 2) {
+ return 0;
+ }
+ std::vector pts;
+ pts.reserve(points.count);
+ for (NSValue *v in points) {
+ CGPoint p = v.CGPointValue;
+ pts.emplace_back((float)p.x, (float)p.y);
+ }
+ return cv::contourArea(pts);
+}
+
++ (float)arcLengthFromPoints:(NSArray *)points closed:(BOOL)closed {
+ if (points.count < 2) {
+ return 0;
+ }
+ std::vector pts;
+ pts.reserve(points.count);
+ for (NSValue *v in points) {
+ CGPoint p = v.CGPointValue;
+ pts.emplace_back((float)p.x, (float)p.y);
+ }
+ return (float)cv::arcLength(pts, closed);
+}
+
+@end
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/OpenCVImageBridge.h b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/OpenCVImageBridge.h
new file mode 100644
index 0000000000000000000000000000000000000000..d66dde7e3916d73c5d56f93b5111873f098a8138
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/OpenCVImageBridge.h
@@ -0,0 +1,63 @@
+// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface PDBPerspectiveCropOutput : NSObject
+@property(nonatomic) NSData *rgbData;
+@property(nonatomic) NSInteger width;
+@property(nonatomic) NSInteger height;
+@end
+
+@interface PDBOpenCVImageBridge : NSObject
+
+/// `cv::resize(..., INTER_LINEAR)` on 8-bit **3-channel** row-major data
+/// (`CV_8UC3`; stores BGR channel order for OpenCV).
++ (NSData *)resizeRGBU8:(NSData *)data
+ srcWidth:(NSInteger)srcWidth
+ srcHeight:(NSInteger)srcHeight
+ dstWidth:(NSInteger)dstWidth
+ dstHeight:(NSInteger)dstHeight NS_SWIFT_NAME(resizeRGBU8(_:srcWidth:srcHeight:dstWidth:dstHeight:));
+
+/// Pad RGB image to `padWidth` x `padHeight` with zeros on bottom/right.
++ (NSData *)padRGBU8:(NSData *)data
+ width:(NSInteger)width
+ height:(NSInteger)height
+ padWidth:(NSInteger)padWidth
+ padHeight:(NSInteger)padHeight
+ NS_SWIFT_NAME(padRGBU8(_:width:height:padWidth:padHeight:));
+
+/// Minimum-area rectangle, corner ordering, perspective warp (`INTER_CUBIC`,
+/// `BORDER_REPLICATE`), then 90° CCW rotation when height/width ≥ 1.5. Input
+/// buffer is row-major **BGR**.
++ (nullable PDBPerspectiveCropOutput *)
+ quadTextLineCropBGR:(NSData *)src
+ srcWidth:(NSInteger)srcWidth
+ srcHeight:(NSInteger)srcHeight
+ quad:(NSArray *)quad
+ NS_SWIFT_NAME(quadTextLineCropBGR(_:srcWidth:srcHeight:quad:));
+
+/// Mean of the float probability map inside the quadrilateral: `fillPoly` mask
+/// then `cv::mean`.
++ (float)meanPredInQuad:(NSData *)pred
+ width:(NSInteger)width
+ height:(NSInteger)height
+ box:(NSArray *> *)box
+ NS_SWIFT_NAME(meanPredInQuad(_:width:height:box:));
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/OpenCVImageBridge.mm b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/OpenCVImageBridge.mm
new file mode 100644
index 0000000000000000000000000000000000000000..d608aa73b499a86ca8cb6f047fa95fbaab67a306
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/OpenCVImageBridge.mm
@@ -0,0 +1,205 @@
+// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "OpenCVImageBridge.h"
+#import
+#import
+#import
+#import
+#import
+#import
+
+@implementation PDBPerspectiveCropOutput
+@end
+
+@implementation PDBOpenCVImageBridge
+
++ (NSData *)resizeRGBU8:(NSData *)data
+ srcWidth:(NSInteger)srcWidth
+ srcHeight:(NSInteger)srcHeight
+ dstWidth:(NSInteger)dstWidth
+ dstHeight:(NSInteger)dstHeight {
+ if (srcWidth <= 0 || srcHeight <= 0 || dstWidth <= 0 || dstHeight <= 0) {
+ return [NSData data];
+ }
+ if (data.length < (NSUInteger)(srcWidth * srcHeight * 3)) {
+ return [NSData data];
+ }
+ cv::Mat src((int)srcHeight, (int)srcWidth, CV_8UC3, (void *)data.bytes);
+ cv::Mat dst;
+ cv::resize(src, dst, cv::Size((int)dstWidth, (int)dstHeight), 0, 0, cv::INTER_LINEAR);
+ return [NSData dataWithBytes:dst.data length:(NSUInteger)(dst.rows * dst.cols * 3)];
+}
+
++ (NSData *)padRGBU8:(NSData *)data
+ width:(NSInteger)width
+ height:(NSInteger)height
+ padWidth:(NSInteger)padWidth
+ padHeight:(NSInteger)padHeight {
+ if (width <= 0 || height <= 0 || padWidth < width || padHeight < height) {
+ return data ?: [NSData data];
+ }
+ if (data.length < (NSUInteger)(width * height * 3)) {
+ return [NSData data];
+ }
+ cv::Mat src((int)height, (int)width, CV_8UC3, (void *)data.bytes);
+ int bot = (int)(padHeight - height);
+ int right = (int)(padWidth - width);
+ cv::Mat dst;
+ cv::copyMakeBorder(src, dst, 0, bot, 0, right, cv::BORDER_CONSTANT, cv::Scalar(0, 0, 0));
+ return [NSData dataWithBytes:dst.data length:(NSUInteger)(dst.rows * dst.cols * 3)];
+}
+
++ (nullable PDBPerspectiveCropOutput *)quadTextLineCropBGR:(NSData *)src
+ srcWidth:(NSInteger)srcWidth
+ srcHeight:(NSInteger)srcHeight
+ quad:(NSArray *)quad {
+ if (srcWidth <= 0 || srcHeight <= 0) {
+ return nil;
+ }
+ if (quad.count != 4 || src.length < (NSUInteger)(srcWidth * srcHeight * 3)) {
+ return nil;
+ }
+
+ cv::Mat im((int)srcHeight, (int)srcWidth, CV_8UC3, (void *)src.bytes);
+
+ std::vector raw(4);
+ for (int i = 0; i < 4; i++) {
+ CGPoint p = [quad[i] CGPointValue];
+ raw[i] = cv::Point2f((float)(int32_t)p.x, (float)(int32_t)p.y);
+ }
+
+ cv::RotatedRect rr = cv::minAreaRect(raw);
+ cv::Point2f boxPts[4];
+ rr.points(boxPts);
+ std::vector sorted(boxPts, boxPts + 4);
+ std::sort(sorted.begin(), sorted.end(),
+ [](const cv::Point2f &a, const cv::Point2f &b) { return a.x < b.x; });
+
+ int index_a, index_b, index_c, index_d;
+ if (sorted[1].y > sorted[0].y) {
+ index_a = 0;
+ index_d = 1;
+ } else {
+ index_a = 1;
+ index_d = 0;
+ }
+ if (sorted[3].y > sorted[2].y) {
+ index_b = 2;
+ index_c = 3;
+ } else {
+ index_b = 3;
+ index_c = 2;
+ }
+
+ cv::Point2f srcPts[4] = {sorted[index_a], sorted[index_b], sorted[index_c], sorted[index_d]};
+
+ double w1 = cv::norm(srcPts[0] - srcPts[1]);
+ double w2 = cv::norm(srcPts[2] - srcPts[3]);
+ double h1 = cv::norm(srcPts[0] - srcPts[3]);
+ double h2 = cv::norm(srcPts[1] - srcPts[2]);
+
+ int img_crop_width = (int)std::max(w1, w2);
+ int img_crop_height = (int)std::max(h1, h2);
+ if (img_crop_width <= 0 || img_crop_height <= 0) {
+ return nil;
+ }
+
+ cv::Point2f dstPts[4] = {
+ cv::Point2f(0, 0),
+ cv::Point2f((float)img_crop_width, 0),
+ cv::Point2f((float)img_crop_width, (float)img_crop_height),
+ cv::Point2f(0, (float)img_crop_height),
+ };
+
+ cv::Mat M = cv::getPerspectiveTransform(srcPts, dstPts);
+ cv::Mat warped;
+ cv::warpPerspective(im, warped, M, cv::Size(img_crop_width, img_crop_height), cv::INTER_CUBIC,
+ cv::BORDER_REPLICATE);
+
+ cv::Mat outMat;
+ double ratio = (double)warped.rows / (double)warped.cols;
+ if (ratio >= 1.5) {
+ cv::rotate(warped, outMat, cv::ROTATE_90_COUNTERCLOCKWISE);
+ } else {
+ outMat = warped;
+ }
+
+ PDBPerspectiveCropOutput *result = [[PDBPerspectiveCropOutput alloc] init];
+ result.rgbData = [NSData dataWithBytes:outMat.data length:(NSUInteger)(outMat.rows * outMat.cols * 3)];
+ result.width = outMat.cols;
+ result.height = outMat.rows;
+ return result;
+}
+
++ (float)meanPredInQuad:(NSData *)pred
+ width:(NSInteger)width
+ height:(NSInteger)height
+ box:(NSArray *> *)box {
+ if (width <= 0 || height <= 0 || box.count != 4) {
+ return 0;
+ }
+ size_t need = (size_t)(width * height) * sizeof(float);
+ if (pred.length < need) {
+ return 0;
+ }
+
+ float xminF = FLT_MAX, xmaxF = -FLT_MAX, yminF = FLT_MAX, ymaxF = -FLT_MAX;
+ for (NSArray *pt in box) {
+ if (pt.count < 2) {
+ return 0;
+ }
+ float x = [pt[0] floatValue];
+ float y = [pt[1] floatValue];
+ xminF = std::min(xminF, x);
+ xmaxF = std::max(xmaxF, x);
+ yminF = std::min(yminF, y);
+ ymaxF = std::max(ymaxF, y);
+ }
+
+ int w = (int)width;
+ int h = (int)height;
+ // Clamp ROI to bitmap bounds before masking and mean (avoids out-of-range reads).
+ int xmin = std::max(0, std::min((int)std::floor(xminF), w - 1));
+ int xmax = std::max(0, std::min((int)std::ceil(xmaxF), w - 1));
+ int ymin = std::max(0, std::min((int)std::floor(yminF), h - 1));
+ int ymax = std::max(0, std::min((int)std::ceil(ymaxF), h - 1));
+ if (xmax < xmin || ymax < ymin) {
+ return 0;
+ }
+
+ const float *p = (const float *)pred.bytes;
+ cv::Mat bitmap(h, w, CV_32FC1, (void *)const_cast(p));
+ cv::Rect roi(xmin, ymin, xmax - xmin + 1, ymax - ymin + 1);
+ cv::Mat crop = bitmap(roi);
+
+ cv::Mat mask = cv::Mat::zeros(crop.rows, crop.cols, CV_8UC1);
+ std::vector pts;
+ pts.reserve(4);
+ for (NSArray *pt in box) {
+ float fx = [pt[0] floatValue] - static_cast(xmin);
+ float fy = [pt[1] floatValue] - static_cast(ymin);
+ int px = static_cast(fx);
+ int py = static_cast(fy);
+ pts.push_back(cv::Point(px, py));
+ }
+ std::vector> contours;
+ contours.push_back(pts);
+ cv::fillPoly(mask, contours, cv::Scalar(1));
+
+ cv::Scalar m = cv::mean(crop, mask);
+ return (float)m[0];
+}
+
+@end
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/PDBPolygonOffsetBridge.h b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/PDBPolygonOffsetBridge.h
new file mode 100644
index 0000000000000000000000000000000000000000..d8967f114767ed67a5da5149867a0f7213097800
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/PDBPolygonOffsetBridge.h
@@ -0,0 +1,29 @@
+// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+/// Polygon inflate (unclip) via Clipper 6.x (`ClipperLib`) for closed paths.
+@interface PDBPolygonOffsetBridge : NSObject
+
++ (NSArray *> *)inflateClosedPolygonWith:
+ (NSArray *)points
+ distance:(double)distance
+ NS_SWIFT_NAME(inflateClosedPolygon(with:distance:));
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/PDBPolygonOffsetBridge.mm b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/PDBPolygonOffsetBridge.mm
new file mode 100644
index 0000000000000000000000000000000000000000..ff6933dc6f774c044f6ab4f7b7826e0cdb0ccd7f
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/PDBPolygonOffsetBridge.mm
@@ -0,0 +1,49 @@
+// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "PDBPolygonOffsetBridge.h"
+#import
+#include
+#include "clipper.hpp"
+@implementation PDBPolygonOffsetBridge
++ (NSArray *> *)inflateClosedPolygonWith:(NSArray *)points
+ distance:(double)distance {
+ if (points.count < 2) {
+ return @[];
+ }
+ using namespace ClipperLib;
+ // Default miter limit and arc tolerance for ClipperOffset (round join).
+ ClipperOffset co(2.0, 0.25);
+ Path path;
+ path.reserve(points.count);
+ for (NSValue *v in points) {
+ CGPoint p = v.CGPointValue;
+ cInt ix = static_cast(std::lround((double)p.x));
+ cInt iy = static_cast(std::lround((double)p.y));
+ path.push_back(IntPoint(ix, iy));
+ }
+ co.AddPath(path, jtRound, etClosedPolygon);
+ Paths solution;
+ co.Execute(solution, distance);
+ if (solution.empty()) {
+ return @[];
+ }
+ const Path &out = solution[0];
+ NSMutableArray *ring = [NSMutableArray arrayWithCapacity:out.size()];
+ for (const IntPoint &pt : out) {
+ [ring addObject:[NSValue valueWithCGPoint:CGPointMake((CGFloat)pt.X, (CGFloat)pt.Y)]];
+ }
+ return @[ ring ];
+}
+@end
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/Preprocessing.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/Preprocessing.swift
new file mode 100644
index 0000000000000000000000000000000000000000..7e2358af63d2b2932167a128b159f8cd539cc64f
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/Preprocessing.swift
@@ -0,0 +1,386 @@
+// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import CoreGraphics
+import Foundation
+
+// MARK: - Errors
+
+enum DetPreprocessorError: LocalizedError {
+ case invalidImage
+ case configMissing(String)
+ case pixelExtractionFailed
+
+ var errorDescription: String? {
+ switch self {
+ case .invalidImage:
+ return "Invalid input image: width or height is zero"
+ case .configMissing(let detail):
+ return "Required preprocessing config missing: \(detail)"
+ case .pixelExtractionFailed:
+ return "Failed to extract pixel data from image"
+ }
+ }
+}
+
+// MARK: - Preprocessing Result
+
+/// The output of detection preprocessing: a float32 tensor and metadata needed for postprocessing.
+struct PreprocessResult {
+ /// Flat CHW array of shape [1, 3, resizedH, resizedW].
+ let tensorData: [Float]
+ /// Tensor dimensions: [1, 3, resizedH, resizedW].
+ let tensorShape: [Int]
+ /// Original image dimensions before any resizing.
+ let originalSize: (width: Int, height: Int)
+ /// Image dimensions after resize and padding to stride multiples.
+ let resizedSize: (width: Int, height: Int)
+ /// Vertical resize ratio: resizedH / originalH.
+ let ratioH: Float
+ /// Horizontal resize ratio: resizedW / originalW.
+ let ratioW: Float
+}
+
+// MARK: - DetPreprocessor
+
+/// Implements detection preprocessing: **DetResizeForTest** →
+/// **NormalizeImage** (from the model config file) -> **ToCHWImage**.
+///
+/// Rasterization uses CoreGraphics; resize/pad use OpenCV (`INTER_LINEAR`, `copyMakeBorder`).
+/// HWC channel order follows `DecodeImage.img_mode` (`InferenceConfig.decodeImageChannelOrder`).
+struct DetPreprocessor {
+ private enum ResizeRule {
+ case limitSide(len: Int, limitType: String, maxSideLimit: Int)
+ case longEdge(resizeLong: Int)
+ }
+
+ private let scale: Float
+ private let mean: [Float]
+ private let std: [Float]
+ private let channelOrder: InferenceImageChannelOrder
+
+ init(config: InferenceConfig) throws {
+ var foundScale: Float?
+ var foundMean: [Float]?
+ var foundStd: [Float]?
+
+ for op in config.preProcess.transformOps {
+ switch op {
+ case .decodeImage:
+ break
+ case .detResizeForTest(_):
+ break
+ case .normalizeImage(let s, let m, let st, _):
+ foundScale = s
+ foundMean = m
+ foundStd = st
+ case .toCHWImage, .recResizeImg, .unknown:
+ break
+ }
+ }
+
+ guard let scale = foundScale, let mean = foundMean, let std = foundStd else {
+ throw DetPreprocessorError.configMissing("NormalizeImage with scale, mean, std")
+ }
+ guard mean.count == 3, std.count == 3 else {
+ throw DetPreprocessorError.configMissing("NormalizeImage mean/std must have exactly 3 values")
+ }
+
+ self.scale = scale
+ self.mean = mean
+ self.std = std
+ self.channelOrder = config.decodeImageChannelOrder
+ }
+
+ private static func resizeRule(from params: DetResizeParams) throws -> ResizeRule {
+ .limitSide(
+ len: params.limitSideLen,
+ limitType: params.limitType,
+ maxSideLimit: params.maxSideLimit
+ )
+ }
+
+ /// Rasterizes ``CGImage`` to HWC uint8 (`BGR` or `RGB` per `DecodeImage.img_mode`).
+ func preprocess(_ image: CGImage, detResize: DetResizeParams) throws -> PreprocessResult {
+ let originalW = image.width
+ let originalH = image.height
+
+ guard originalW > 0, originalH > 0 else {
+ throw DetPreprocessorError.invalidImage
+ }
+
+ let pixelBytes = try rasterizeImageIntoHWCBuffer(
+ from: image, width: originalW, height: originalH, order: channelOrder
+ )
+ return try preprocessFromHWCPixelBuffer(
+ pixelBytes: pixelBytes,
+ originalWidth: originalW,
+ originalHeight: originalH,
+ detResize: detResize
+ )
+ }
+
+ private func preprocessFromHWCPixelBuffer(
+ pixelBytes initialPixels: [UInt8],
+ originalWidth: Int,
+ originalHeight: Int,
+ detResize: DetResizeParams
+ ) throws -> PreprocessResult {
+ let resizeRule = try Self.resizeRule(from: detResize)
+ var pixelBytes = initialPixels
+ var currentW = originalWidth
+ var currentH = originalHeight
+
+ if currentH + currentW < 64 {
+ let paddedH = max(32, currentH)
+ let paddedW = max(32, currentW)
+ pixelBytes = padImage(pixelBytes, fromW: currentW, fromH: currentH, toW: paddedW, toH: paddedH)
+ currentW = paddedW
+ currentH = paddedH
+ }
+
+ let (resizeW, resizeH, ratioW, ratioH): (Int, Int, Float, Float)
+ switch resizeRule {
+ case .limitSide(let len, let limitType, let maxSideLimit):
+ (resizeW, resizeH, ratioW, ratioH) = computeResizeLimitSide(
+ srcW: currentW,
+ srcH: currentH,
+ limitSideLen: len,
+ limitType: limitType,
+ maxSideLimit: maxSideLimit
+ )
+ case .longEdge(let resizeLong):
+ (resizeW, resizeH, ratioW, ratioH) = computeResizeLongEdge(
+ srcW: currentW,
+ srcH: currentH,
+ resizeLong: resizeLong
+ )
+ }
+
+ let resizedPixels = resizeImageBuffer(
+ pixelBytes, srcW: currentW, srcH: currentH, dstW: resizeW, dstH: resizeH
+ )
+
+ let normalizedHWC = normalizePixels(resizedPixels, width: resizeW, height: resizeH)
+ let chwData = hwcToCHW(normalizedHWC, width: resizeW, height: resizeH)
+
+ return PreprocessResult(
+ tensorData: chwData,
+ tensorShape: [1, 3, resizeH, resizeW],
+ originalSize: (width: originalWidth, height: originalHeight),
+ resizedSize: (width: resizeW, height: resizeH),
+ ratioH: ratioH,
+ ratioW: ratioW
+ )
+ }
+
+ // MARK: - Rasterization
+
+ /// Row-major HWC uint8: **BGR** or **RGB** per `DecodeImage.img_mode` in the model config file.
+ private func rasterizeImageIntoHWCBuffer(
+ from image: CGImage,
+ width: Int,
+ height: Int,
+ order: InferenceImageChannelOrder
+ ) throws -> [UInt8] {
+ let bytesPerPixel = 4
+ let bytesPerRow = width * bytesPerPixel
+ var rgbaData = [UInt8](repeating: 0, count: height * bytesPerRow)
+
+ guard let colorSpace = CGColorSpace(name: CGColorSpace.sRGB),
+ let context = CGContext(
+ data: &rgbaData,
+ width: width,
+ height: height,
+ bitsPerComponent: 8,
+ bytesPerRow: bytesPerRow,
+ space: colorSpace,
+ bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue
+ ) else {
+ throw DetPreprocessorError.pixelExtractionFailed
+ }
+
+ context.draw(image, in: CGRect(x: 0, y: 0, width: width, height: height))
+
+ var out = [UInt8](repeating: 0, count: height * width * 3)
+ for i in 0..<(height * width) {
+ let r = rgbaData[i * 4]
+ let g = rgbaData[i * 4 + 1]
+ let b = rgbaData[i * 4 + 2]
+ switch order {
+ case .bgr:
+ out[i * 3] = b
+ out[i * 3 + 1] = g
+ out[i * 3 + 2] = r
+ case .rgb:
+ out[i * 3] = r
+ out[i * 3 + 1] = g
+ out[i * 3 + 2] = b
+ }
+ }
+ return out
+ }
+
+ private func padImage(_ pixels: [UInt8], fromW: Int, fromH: Int, toW: Int, toH: Int) -> [UInt8] {
+ if fromW == toW && fromH == toH {
+ return pixels
+ }
+ let out = PDBOpenCVImageBridge.padRGBU8(
+ Data(pixels), width: fromW, height: fromH, padWidth: toW, padHeight: toH
+ )
+ let expected = toW * toH * 3
+ guard out.count == expected else {
+ return [UInt8](repeating: 0, count: expected)
+ }
+ return [UInt8](out)
+ }
+
+ // MARK: - Resize geometry
+
+ /// Limit-side resize with multiple-of-32 dimensions, optional cap on the larger side.
+ private func computeResizeLimitSide(
+ srcW: Int,
+ srcH: Int,
+ limitSideLen: Int,
+ limitType: String,
+ maxSideLimit: Int
+ ) -> (width: Int, height: Int, ratioW: Float, ratioH: Float) {
+ let h = Float(srcH)
+ let w = Float(srcW)
+ let ratio: Float
+ switch limitType {
+ case "max":
+ if max(srcH, srcW) > limitSideLen {
+ if srcH > srcW {
+ ratio = Float(limitSideLen) / h
+ } else {
+ ratio = Float(limitSideLen) / w
+ }
+ } else {
+ ratio = 1.0
+ }
+ case "min":
+ if min(srcH, srcW) < limitSideLen {
+ if srcH < srcW {
+ ratio = Float(limitSideLen) / h
+ } else {
+ ratio = Float(limitSideLen) / w
+ }
+ } else {
+ ratio = 1.0
+ }
+ case "resize_long":
+ ratio = Float(limitSideLen) / Float(max(srcH, srcW))
+ default:
+ ratio = 1.0
+ }
+
+ var resizeH = Int(Float(srcH) * ratio)
+ var resizeW = Int(Float(srcW) * ratio)
+
+ if max(resizeH, resizeW) > maxSideLimit {
+ let shrink = Float(maxSideLimit) / Float(max(resizeH, resizeW))
+ resizeH = Int(Float(resizeH) * shrink)
+ resizeW = Int(Float(resizeW) * shrink)
+ }
+
+ resizeH = max(Int((Double(resizeH) / 32.0).rounded(.toNearestOrEven)) * 32, 32)
+ resizeW = max(Int((Double(resizeW) / 32.0).rounded(.toNearestOrEven)) * 32, 32)
+
+ if resizeH == srcH && resizeW == srcW {
+ return (srcW, srcH, 1.0, 1.0)
+ }
+
+ let ratioH = Float(resizeH) / Float(srcH)
+ let ratioW = Float(resizeW) / Float(srcW)
+ return (resizeW, resizeH, ratioW, ratioH)
+ }
+
+ /// Long-edge resize with stride 128 on both sides.
+ private func computeResizeLongEdge(
+ srcW: Int,
+ srcH: Int,
+ resizeLong: Int
+ ) -> (width: Int, height: Int, ratioW: Float, ratioH: Float) {
+ let ratio: Float
+ if srcH > srcW {
+ ratio = Float(resizeLong) / Float(srcH)
+ } else {
+ ratio = Float(resizeLong) / Float(srcW)
+ }
+
+ var resizeH = Int(Float(srcH) * ratio)
+ var resizeW = Int(Float(srcW) * ratio)
+
+ let stride = 128
+ resizeH = ((resizeH + stride - 1) / stride) * stride
+ resizeW = ((resizeW + stride - 1) / stride) * stride
+
+ let ratioH = Float(resizeH) / Float(srcH)
+ let ratioW = Float(resizeW) / Float(srcW)
+ return (resizeW, resizeH, ratioW, ratioH)
+ }
+
+ private func resizeImageBuffer(
+ _ pixels: [UInt8], srcW: Int, srcH: Int, dstW: Int, dstH: Int
+ ) -> [UInt8] {
+ if srcW == dstW && srcH == dstH {
+ return pixels
+ }
+ let out = PDBOpenCVImageBridge.resizeRGBU8(
+ Data(pixels), srcWidth: srcW, srcHeight: srcH, dstWidth: dstW, dstHeight: dstH
+ )
+ let expected = dstW * dstH * 3
+ guard out.count == expected else {
+ return [UInt8](repeating: 0, count: expected)
+ }
+ return [UInt8](out)
+ }
+
+ // MARK: - NormalizeImage
+
+ private func normalizePixels(_ pixels: [UInt8], width: Int, height: Int) -> [Float] {
+ let pixelCount = width * height
+ var result = [Float](repeating: 0, count: pixelCount * 3)
+
+ let scaleOverStd = [scale / std[0], scale / std[1], scale / std[2]]
+ let meanOverStd = [mean[0] / std[0], mean[1] / std[1], mean[2] / std[2]]
+
+ for i in 0.. [Float] {
+ let channelSize = height * width
+ var chwData = [Float](repeating: 0, count: 3 * channelSize)
+
+ for y in 0.. CGImage {
+ let bytesPerPixel = 4
+ let bytesPerRow = width * bytesPerPixel
+
+ let pixelCount = width * height
+ guard pixels.count == pixelCount * 3 else {
+ throw QuadTextCropError.imageCreationFailed
+ }
+
+ var rgbaData = [UInt8](repeating: 255, count: pixelCount * bytesPerPixel)
+ for i in 0.. CGImage {
+ guard polygon.count == 4, polygon.allSatisfy({ $0.count == 2 }) else {
+ throw QuadTextCropError.invalidPolygon(
+ "Expected 4 points with 2 coordinates each, got \(polygon.count) points"
+ )
+ }
+
+ let srcPixels = try extractBGRPixels(from: image)
+ let srcW = image.width
+ let srcH = image.height
+
+ let polyValues: [NSValue] = polygon.map {
+ NSValue(cgPoint: CGPoint(x: CGFloat($0[0]), y: CGFloat($0[1])))
+ }
+ guard let out = PDBOpenCVImageBridge.quadTextLineCropBGR(
+ Data(srcPixels),
+ srcWidth: srcW,
+ srcHeight: srcH,
+ quad: polyValues
+ ) else {
+ throw QuadTextCropError.openCVFailed
+ }
+
+ let bytes = [UInt8](out.rgbData)
+ return try BGRImageCodec.makeCGImageFromBGR(pixels: bytes, width: Int(out.width), height: Int(out.height))
+ }
+}
+
+// MARK: - Pixel Extraction & Image Creation
+
+private extension QuadTextCrop {
+
+ /// Row-major HWC **BGR**
+ static func extractBGRPixels(from image: CGImage) throws -> [UInt8] {
+ let width = image.width
+ let height = image.height
+ let bytesPerPixel = 4
+ let bytesPerRow = width * bytesPerPixel
+ var rgbaData = [UInt8](repeating: 0, count: height * bytesPerRow)
+
+ guard let colorSpace = CGColorSpace(name: CGColorSpace.sRGB),
+ let context = CGContext(
+ data: &rgbaData,
+ width: width,
+ height: height,
+ bitsPerComponent: 8,
+ bytesPerRow: bytesPerRow,
+ space: colorSpace,
+ bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue
+ ) else {
+ throw QuadTextCropError.imageCreationFailed
+ }
+
+ context.draw(image, in: CGRect(x: 0, y: 0, width: width, height: height))
+
+ let pixelCount = width * height
+ var bgrData = [UInt8](repeating: 0, count: pixelCount * 3)
+ for i in 0..= 3 else {
+ throw RecPreprocessorError.configMissing("RecResizeImg with image_shape")
+ }
+
+ self.imgC = imageShape[0]
+ self.imgH = imageShape[1]
+ self.imgW = imageShape[2]
+ self.channelOrder = config.decodeImageChannelOrder
+ }
+
+ /// Runs recognition preprocessing on a `CGImage`.
+ ///
+ /// Steps: extract pixels → compute dimensions → resize → normalize → HWC-to-CHW → pad
+ ///
+ /// - Parameter image: The input image (typically a cropped text region).
+ /// - Returns: A `RecPreprocessResult` with the float32 CHW tensor and metadata.
+ func preprocess(_ image: CGImage) throws -> RecPreprocessResult {
+ let originalW = image.width
+ let originalH = image.height
+
+ guard originalW > 0, originalH > 0 else {
+ throw RecPreprocessorError.invalidImage
+ }
+
+ let pixelBytes = try extractHWCPixels(from: image, width: originalW, height: originalH, order: channelOrder)
+ let (paddedData, canvasW, resizedW) = try lineResizeNormalizeCHW(
+ pixelBytes: pixelBytes,
+ originalW: originalW,
+ originalH: originalH,
+ alignCanvasTo8: Self.isRecognitionCanvasWidthAlignedTo8Enabled()
+ )
+
+ return RecPreprocessResult(
+ tensorData: paddedData,
+ tensorShape: [1, imgC, imgH, canvasW],
+ originalSize: (width: originalW, height: originalH),
+ resizedWidth: resizedW
+ )
+ }
+
+ /// Builds a batched tensor: each line is resized and padded to its canvas width, then all rows
+ /// are padded on the right to the maximum canvas width in the batch (shape `[N, C, H, W]`).
+ func preprocessBatch(_ images: [CGImage]) throws -> RecBatchPreprocessResult {
+ guard !images.isEmpty else {
+ throw RecPreprocessorError.invalidImage
+ }
+
+ let align8 = Self.isRecognitionCanvasWidthAlignedTo8Enabled()
+ var rowCHW: [[Float]] = []
+ var canvasWidths: [Int] = []
+
+ for image in images {
+ let originalW = image.width
+ let originalH = image.height
+ guard originalW > 0, originalH > 0 else {
+ throw RecPreprocessorError.invalidImage
+ }
+ let pixelBytes = try extractHWCPixels(from: image, width: originalW, height: originalH, order: channelOrder)
+ let (chw, canvasW, _) = try lineResizeNormalizeCHW(
+ pixelBytes: pixelBytes,
+ originalW: originalW,
+ originalH: originalH,
+ alignCanvasTo8: align8
+ )
+ rowCHW.append(chw)
+ canvasWidths.append(canvasW)
+ }
+
+ guard let maxCanvasW = canvasWidths.max() else {
+ throw RecPreprocessorError.invalidImage
+ }
+
+ let n = rowCHW.count
+ let channelSize = imgH * maxCanvasW
+ var flat = [Float](repeating: 0, count: n * imgC * channelSize)
+
+ for (b, row) in rowCHW.enumerated() {
+ let w = canvasWidths[b]
+ let paddedRow: [Float]
+ if w == maxCanvasW {
+ paddedRow = row
+ } else {
+ let contentW = w
+ paddedRow = padToTargetWidth(
+ row,
+ contentWidth: contentW,
+ targetWidth: maxCanvasW,
+ height: imgH,
+ channels: imgC
+ )
+ }
+ let batchOffset = b * imgC * channelSize
+ for i in 0.. Bool {
+ let v = ProcessInfo.processInfo.environment["PADDLEOCR_REC_ALIGN_WIDTH_TO_8"]?.lowercased()
+ return v == "1" || v == "true" || v == "yes"
+ }
+
+ /// One text line: HWC uint8 → resize → normalize → CHW, padded to the computed canvas width.
+ private func lineResizeNormalizeCHW(
+ pixelBytes: [UInt8],
+ originalW: Int,
+ originalH: Int,
+ alignCanvasTo8: Bool
+ ) throws -> (chw: [Float], canvasW: Int, resizedW: Int) {
+ let w = originalW
+ let h = originalH
+ let whRatio = Float(w) / Float(h)
+ let defaultRatio = Float(imgW) / Float(imgH)
+ let maxWhRatio = max(defaultRatio, whRatio)
+ var canvasW = Int(Float(imgH) * maxWhRatio)
+ let resizedW: Int
+
+ if canvasW > maxImgW {
+ canvasW = maxImgW
+ resizedW = maxImgW
+ } else {
+ let ratio = Float(w) / Float(h)
+ if Int(ceil(Float(imgH) * ratio)) > canvasW {
+ resizedW = canvasW
+ } else {
+ resizedW = Int(ceil(Float(imgH) * ratio))
+ }
+ }
+
+ if alignCanvasTo8 {
+ canvasW = min(max(roundUpToMultipleOf(canvasW, 8), resizedW), maxImgW)
+ }
+
+ let resizedPixels = resizeWithOpenCV(pixelBytes, srcW: w, srcH: h, dstW: resizedW, dstH: imgH)
+ let normalizedHWC = normalizePixels(resizedPixels, width: resizedW, height: imgH)
+ let chwData = hwcToCHW(normalizedHWC, width: resizedW, height: imgH)
+ let paddedData = padToTargetWidth(
+ chwData,
+ contentWidth: resizedW,
+ targetWidth: canvasW,
+ height: imgH,
+ channels: imgC
+ )
+ return (paddedData, canvasW, resizedW)
+ }
+
+ // MARK: - Step 0: Pixel extraction (BGR / RGB)
+
+ private func extractHWCPixels(
+ from image: CGImage,
+ width: Int,
+ height: Int,
+ order: InferenceImageChannelOrder
+ ) throws -> [UInt8] {
+ let bytesPerPixel = 4
+ let bytesPerRow = width * bytesPerPixel
+ var rgbaData = [UInt8](repeating: 0, count: height * bytesPerRow)
+
+ guard let colorSpace = CGColorSpace(name: CGColorSpace.sRGB),
+ let context = CGContext(
+ data: &rgbaData,
+ width: width,
+ height: height,
+ bitsPerComponent: 8,
+ bytesPerRow: bytesPerRow,
+ space: colorSpace,
+ bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue
+ ) else {
+ throw RecPreprocessorError.pixelExtractionFailed
+ }
+
+ context.draw(image, in: CGRect(x: 0, y: 0, width: width, height: height))
+
+ var out = [UInt8](repeating: 0, count: height * width * 3)
+ for i in 0..<(height * width) {
+ let r = rgbaData[i * 4]
+ let g = rgbaData[i * 4 + 1]
+ let b = rgbaData[i * 4 + 2]
+ switch order {
+ case .bgr:
+ out[i * 3] = b
+ out[i * 3 + 1] = g
+ out[i * 3 + 2] = r
+ case .rgb:
+ out[i * 3] = r
+ out[i * 3 + 1] = g
+ out[i * 3 + 2] = b
+ }
+ }
+ return out
+ }
+
+ // MARK: - Resize (OpenCV)
+
+ private func resizeWithOpenCV(
+ _ pixels: [UInt8], srcW: Int, srcH: Int, dstW: Int, dstH: Int
+ ) -> [UInt8] {
+ if srcW == dstW && srcH == dstH {
+ return pixels
+ }
+ let out = PDBOpenCVImageBridge.resizeRGBU8(
+ Data(pixels), srcWidth: srcW, srcHeight: srcH, dstWidth: dstW, dstHeight: dstH
+ )
+ let expected = dstW * dstH * 3
+ guard out.count == expected else {
+ return [UInt8](repeating: 0, count: expected)
+ }
+ return [UInt8](out)
+ }
+
+ // MARK: - Normalize (recognition)
+
+ /// Maps pixel values from [0, 255] to [-1, 1] via `pixel / 127.5 - 1.0`.
+ private func normalizePixels(_ pixels: [UInt8], width: Int, height: Int) -> [Float] {
+ let count = width * height * 3
+ var result = [Float](repeating: 0, count: count)
+
+ for i in 0.. [Float] {
+ let channelSize = height * width
+ var chwData = [Float](repeating: 0, count: 3 * channelSize)
+
+ for y in 0.. [Float] {
+ if contentWidth == targetWidth {
+ return chwData
+ }
+
+ let targetChannelSize = height * targetWidth
+ let sourceChannelSize = height * contentWidth
+ var padded = [Float](repeating: 0, count: channels * targetChannelSize)
+
+ for c in 0.. Int {
+ guard multiple > 0 else { return value }
+ return (value + multiple - 1) / multiple * multiple
+}
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/RecognitionEngine.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/RecognitionEngine.swift
new file mode 100644
index 0000000000000000000000000000000000000000..5865ce238bc1d442cda3d8f0ab14c904658cd65a
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Engine/RecognitionEngine.swift
@@ -0,0 +1,165 @@
+// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import CoreGraphics
+import Foundation
+
+// MARK: - Recognition Engine Result
+
+/// The result of running text recognition on a cropped text image.
+///
+/// Contains the decoded text, confidence score, and per-stage timing metrics.
+struct RecognitionEngineResult {
+ /// The decoded text string from CTC decoding.
+ let text: String
+ /// Average confidence across all decoded characters (0.0 to 1.0).
+ let confidence: Float
+ /// Seconds spent in preprocessing (resize, normalize, HWC-to-CHW, pad).
+ let preprocessTime: TimeInterval
+ /// Seconds spent in ONNX Runtime inference.
+ let inferenceTime: TimeInterval
+ /// Seconds spent in CTC decoding (best-class pick, dedup, char mapping).
+ let postprocessTime: TimeInterval
+ /// Total recognition time (preprocess + inference + postprocess).
+ let totalTime: TimeInterval
+ /// Recognition model input tensor shape for the batch this line belonged to.
+ let inputTensorShape: [Int]
+}
+
+// MARK: - Recognition Engine Errors
+
+enum RecognitionEngineError: LocalizedError {
+ case noOutputTensor
+ case unexpectedOutputShape([Int])
+ case batchOutputCountMismatch(expectedBatch: Int, shape: [Int])
+
+ var errorDescription: String? {
+ switch self {
+ case .noOutputTensor:
+ return "Recognition model produced no output tensor"
+ case .unexpectedOutputShape(let shape):
+ return "Unexpected recognition output shape: \(shape), expected [1, T, C] or [N, T, C]"
+ case .batchOutputCountMismatch(let expectedBatch, let shape):
+ return "Recognition batch size mismatch: expected batch dimension \(expectedBatch), got shape \(shape)"
+ }
+ }
+}
+
+// MARK: - RecognitionEngine
+
+/// Orchestrates text recognition:
+/// CGImage → RecPreprocessor → ORT inference → CTCDecoder.
+///
+/// This is the integration layer that composes `RecPreprocessor`, `ORTSessionManager`,
+/// and `CTCDecoder` into a single callable unit. All preprocessing parameters are read
+/// from the recognition model config file at initialization time.
+///
+/// Usage:
+/// ```swift
+/// let engine = try RecognitionEngine(sessionManager: manager)
+/// let result = try await engine.recognize(croppedTextImage)
+/// print("Text: \(result.text) (confidence: \(result.confidence))")
+/// ```
+class RecognitionEngine {
+ private let sessionManager: ORTSessionManager
+ private let preprocessor: RecPreprocessor
+ private let decoder: CTCDecoder
+ private(set) var modelConfig: InferenceConfig
+
+ /// Initialize with an existing ORTSessionManager (models must already be loaded).
+ ///
+ /// Loads the recognition model config file from the bundle to configure
+ /// the preprocessor and CTC decoder.
+ ///
+ /// - Parameter sessionManager: A loaded ORTSessionManager with recognition model ready.
+ /// - Throws: If the model config cannot be loaded or required fields are missing.
+ init(sessionManager: ORTSessionManager) throws {
+ self.sessionManager = sessionManager
+
+ let recPaths = try ModelConfig.recognition()
+ let cfg = try InferenceConfig.load(from: recPaths.configPath)
+ self.modelConfig = cfg
+
+ self.preprocessor = try RecPreprocessor(config: cfg)
+ self.decoder = try CTCDecoder(config: cfg)
+ }
+
+ /// Run recognition on a cropped text region CGImage, returning decoded text with confidence.
+ ///
+ /// Steps: RecPreprocessor → ORT inference → CTCDecoder
+ ///
+ /// - Parameter image: A cropped text region image.
+ /// - Returns: A `RecognitionEngineResult` with text, confidence, and per-stage timing.
+ func recognize(_ image: CGImage) async throws -> RecognitionEngineResult {
+ let batch = try await recognizeBatch([image])
+ guard let first = batch.first else {
+ throw RecognitionEngineError.noOutputTensor
+ }
+ return first
+ }
+
+ /// Runs recognition on multiple cropped lines in one batched inference call.
+ ///
+ /// - Parameter images: Cropped text regions in inference order (caller controls ordering).
+ /// - Returns: One result per input image, same order.
+ func recognizeBatch(_ images: [CGImage]) async throws -> [RecognitionEngineResult] {
+ guard !images.isEmpty else { return [] }
+
+ let preprocessStart = CFAbsoluteTimeGetCurrent()
+ let batchPreprocessed = try preprocessor.preprocessBatch(images)
+ let preprocessTime = CFAbsoluteTimeGetCurrent() - preprocessStart
+
+ let inferenceStart = CFAbsoluteTimeGetCurrent()
+ let outputs = try await sessionManager.runRecognition(
+ inputData: batchPreprocessed.tensorData,
+ shape: batchPreprocessed.tensorShape
+ )
+ let inferenceTime = CFAbsoluteTimeGetCurrent() - inferenceStart
+
+ guard let firstOutput = outputs.values.first else {
+ throw RecognitionEngineError.noOutputTensor
+ }
+ let outputData = firstOutput.data
+ let outputShape = firstOutput.shape
+
+ guard outputShape.count == 3, outputShape[0] == images.count else {
+ if outputShape.count == 3 {
+ throw RecognitionEngineError.batchOutputCountMismatch(expectedBatch: images.count, shape: outputShape)
+ }
+ throw RecognitionEngineError.unexpectedOutputShape(outputShape)
+ }
+
+ let postprocessStart = CFAbsoluteTimeGetCurrent()
+ let decoded = try decoder.decodeBatch(outputData: outputData, outputShape: outputShape)
+ let postprocessTime = CFAbsoluteTimeGetCurrent() - postprocessStart
+
+ let n = Double(images.count)
+ let preprocessPer = preprocessTime / n
+ let inferencePer = inferenceTime / n
+ let postPer = postprocessTime / n
+ let totalPer = (preprocessTime + inferenceTime + postprocessTime) / n
+
+ return decoded.map { d in
+ RecognitionEngineResult(
+ text: d.text,
+ confidence: d.confidence,
+ preprocessTime: preprocessPer,
+ inferenceTime: inferencePer,
+ postprocessTime: postPer,
+ totalTime: totalPer,
+ inputTensorShape: batchPreprocessed.tensorShape
+ )
+ }
+ }
+}
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 0000000000000000000000000000000000000000..38004313122eb2687ddf7123c4038c54a29bd712
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.753",
+ "green" : "0.396",
+ "red" : "0.082"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000000000000000000000000000000000000..a65675bd40156303d975a8e87119db452526d6d9
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,62 @@
+{
+ "images": [
+ {
+ "filename": "Icon-20@2x.png",
+ "idiom": "iphone",
+ "scale": "2x",
+ "size": "20x20"
+ },
+ {
+ "filename": "Icon-20@3x.png",
+ "idiom": "iphone",
+ "scale": "3x",
+ "size": "20x20"
+ },
+ {
+ "filename": "Icon-29@2x.png",
+ "idiom": "iphone",
+ "scale": "2x",
+ "size": "29x29"
+ },
+ {
+ "filename": "Icon-29@3x.png",
+ "idiom": "iphone",
+ "scale": "3x",
+ "size": "29x29"
+ },
+ {
+ "filename": "Icon-40@2x.png",
+ "idiom": "iphone",
+ "scale": "2x",
+ "size": "40x40"
+ },
+ {
+ "filename": "Icon-40@3x.png",
+ "idiom": "iphone",
+ "scale": "3x",
+ "size": "40x40"
+ },
+ {
+ "filename": "Icon-60@2x.png",
+ "idiom": "iphone",
+ "scale": "2x",
+ "size": "60x60"
+ },
+ {
+ "filename": "Icon-60@3x.png",
+ "idiom": "iphone",
+ "scale": "3x",
+ "size": "60x60"
+ },
+ {
+ "filename": "Icon-1024.png",
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024"
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+}
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-1024.png b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-1024.png
new file mode 100644
index 0000000000000000000000000000000000000000..5cbc32ee10a372e8f01a854f712fa77960acc209
Binary files /dev/null and b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-1024.png differ
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-20@2x.png b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-20@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..8de1117ee6f654116f60089c250cf7da6cbaf125
Binary files /dev/null and b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-20@2x.png differ
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-20@3x.png b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-20@3x.png
new file mode 100644
index 0000000000000000000000000000000000000000..3aa6a12a5eb61b28bbc0418d9fd845ea695e49c6
Binary files /dev/null and b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-20@3x.png differ
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-29@2x.png b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-29@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..78920df7975a6584c8918c7c21b27528c0084b8c
Binary files /dev/null and b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-29@2x.png differ
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-29@3x.png b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-29@3x.png
new file mode 100644
index 0000000000000000000000000000000000000000..51615399542a8eb3bdb3d900c9e1761b4dbb8c56
Binary files /dev/null and b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-29@3x.png differ
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..3fb3e0eb8143c2a13a0b2a7411a732bb0661acba
Binary files /dev/null and b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png differ
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png
new file mode 100644
index 0000000000000000000000000000000000000000..efdf5d18959c1065b980525484c6fd25ec8cd5f0
Binary files /dev/null and b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png differ
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..efdf5d18959c1065b980525484c6fd25ec8cd5f0
Binary files /dev/null and b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png differ
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png
new file mode 100644
index 0000000000000000000000000000000000000000..01e2fc97ee32e65ef927d7f0ce8b9130e73519f0
Binary files /dev/null and b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png differ
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/Contents.json b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/Contents.json
new file mode 100644
index 0000000000000000000000000000000000000000..b2cf3951c5db3652a71b3c9a8735aa44423a4f26
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/SampleImages/general_ocr_002.jpg b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/SampleImages/general_ocr_002.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..70f5b428c2c21a775fdfbba0668506c4a770dfd6
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Resources/SampleImages/general_ocr_002.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4425af33dd163cf73bdff502bd35ee527e9bdd5725501db1da78bfdae9f538f4
+size 128713
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Supporting/Info.plist b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Supporting/Info.plist
new file mode 100644
index 0000000000000000000000000000000000000000..004308c3e9e21f8db04215119e687cb1a5e5c437
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Supporting/Info.plist
@@ -0,0 +1,37 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleDisplayName
+ PaddleOCR
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1
+ LSRequiresIPhoneOS
+
+ UIApplicationSceneManifest
+
+ UIApplicationSupportsMultipleScenes
+
+
+ UILaunchScreen
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+
+
+
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Supporting/PaddleOCRDemo-Bridging-Header.h b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Supporting/PaddleOCRDemo-Bridging-Header.h
new file mode 100644
index 0000000000000000000000000000000000000000..a444bbc1a28b03467529bdc3d441b638e27c8e71
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Supporting/PaddleOCRDemo-Bridging-Header.h
@@ -0,0 +1,24 @@
+// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef BridgingHeader_h
+#define BridgingHeader_h
+
+#import "ORTProfilingBridge.h"
+#import "OpenCVDBBridge.h"
+#import "OpenCVImageBridge.h"
+#import "PDBPolygonOffsetBridge.h"
+#import
+
+#endif
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/ThirdParty/Clipper1/LICENSE b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/ThirdParty/Clipper1/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..b3cf87ee31169994e14e679f54f7ba890a7e8330
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/ThirdParty/Clipper1/LICENSE
@@ -0,0 +1,23 @@
+Boost Software License - Version 1.0 - August 17th, 2003
+
+Permission is hereby granted, free of charge, to any person or organization
+obtaining a copy of the software and accompanying documentation covered by
+this license (the "Software") to use, reproduce, display, distribute,
+execute, and transmit the Software, and to prepare derivative works of the
+Software, and to permit third parties to whom the Software is furnished to
+do so, all subject to the following:
+
+The copyright notices in the Software and this entire statement, including
+the above license grant, this restriction and the following disclaimer,
+must be included in all copies of the Software, in whole or in part, and
+all derivative works of the Software, unless such copies or derivative
+works are solely in the form of machine-executable object code generated by
+a source language processor.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
+SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
+FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/ThirdParty/Clipper1/clipper.cpp b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/ThirdParty/Clipper1/clipper.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..d5d83764579216924e08bbb467dc414b8f4e0777
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/ThirdParty/Clipper1/clipper.cpp
@@ -0,0 +1,4377 @@
+/*******************************************************************************
+ * *
+ * Author : Angus Johnson * Version : 6.4.2 * Date : 27 February
+ *2017 * Website :
+ *http://www.angusj.com * Copyright :
+ *Angus Johnson 2010-2017 *
+ * *
+ * License: * Use, modification & distribution is subject to Boost Software
+ *License Ver 1. * http://www.boost.org/LICENSE_1_0.txt *
+ * *
+ * Attributions: * The code in this library is an extension of Bala Vatti's
+ *clipping algorithm: * "A generic solution to polygon clipping" *
+ * Communications of the ACM, Vol 35, Issue 7 (July 1992) pp 56-63. *
+ * http://portal.acm.org/citation.cfm?id=129906 *
+ * *
+ * Computer graphics and geometric modeling: implementation and algorithms * By
+ *Max K. Agoston *
+ * Springer; 1 edition (January 4, 2005) *
+ * http://books.google.com/books?q=vatti+clipping+agoston *
+ * *
+ * See also: * "Polygon Offsetting by Computing Winding Numbers" * Paper no.
+ *DETC2005-85513 pp. 565-575 * ASME 2005
+ *International Design Engineering Technical Conferences * and
+ *Computers and Information in Engineering Conference (IDETC/CIE2005) *
+ * September 24-28, 2005 , Long Beach, California, USA *
+ * http://www.me.berkeley.edu/~mcmains/pubs/DAC05OffsetPolygon.pdf *
+ * *
+ *******************************************************************************/
+
+/*******************************************************************************
+ * *
+ * This is a translation of the Delphi Clipper library and the naming style *
+ * used has retained a Delphi flavour. *
+ * *
+ *******************************************************************************/
+
+#include "clipper.hpp"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace ClipperLib {
+
+static double const pi = 3.141592653589793238;
+static double const two_pi = pi * 2;
+static double const def_arc_tolerance = 0.25;
+
+enum Direction { dRightToLeft, dLeftToRight };
+
+static int const Unassigned = -1; // edge not currently 'owning' a solution
+static int const Skip = -2; // edge that would otherwise close a path
+
+#define HORIZONTAL (-1.0E+40)
+#define TOLERANCE (1.0e-20)
+#define NEAR_ZERO(val) (((val) > -TOLERANCE) && ((val) < TOLERANCE))
+
+struct TEdge {
+ IntPoint Bot;
+ IntPoint Curr; // current (updated for every new scanbeam)
+ IntPoint Top;
+ double Dx;
+ PolyType PolyTyp;
+ EdgeSide Side; // side only refers to current side of solution poly
+ int WindDelta; // 1 or -1 depending on winding direction
+ int WindCnt;
+ int WindCnt2; // winding count of the opposite polytype
+ int OutIdx;
+ TEdge *Next;
+ TEdge *Prev;
+ TEdge *NextInLML;
+ TEdge *NextInAEL;
+ TEdge *PrevInAEL;
+ TEdge *NextInSEL;
+ TEdge *PrevInSEL;
+};
+
+struct IntersectNode {
+ TEdge *Edge1;
+ TEdge *Edge2;
+ IntPoint Pt;
+};
+
+struct LocalMinimum {
+ cInt Y;
+ TEdge *LeftBound;
+ TEdge *RightBound;
+};
+
+struct OutPt;
+
+// OutRec: contains a path in the clipping solution. Edges in the AEL will
+// carry a pointer to an OutRec when they are part of the clipping solution.
+struct OutRec {
+ int Idx;
+ bool IsHole;
+ bool IsOpen;
+ OutRec *FirstLeft; // see comments in clipper.pas
+ PolyNode *PolyNd;
+ OutPt *Pts;
+ OutPt *BottomPt;
+};
+
+struct OutPt {
+ int Idx;
+ IntPoint Pt;
+ OutPt *Next;
+ OutPt *Prev;
+};
+
+struct Join {
+ OutPt *OutPt1;
+ OutPt *OutPt2;
+ IntPoint OffPt;
+};
+
+struct LocMinSorter {
+ inline bool operator()(const LocalMinimum &locMin1,
+ const LocalMinimum &locMin2) {
+ return locMin2.Y < locMin1.Y;
+ }
+};
+
+//------------------------------------------------------------------------------
+//------------------------------------------------------------------------------
+
+inline cInt Round(double val) {
+ if ((val < 0))
+ return static_cast(val - 0.5);
+ else
+ return static_cast(val + 0.5);
+}
+//------------------------------------------------------------------------------
+
+inline cInt Abs(cInt val) { return val < 0 ? -val : val; }
+
+//------------------------------------------------------------------------------
+// PolyTree methods ...
+//------------------------------------------------------------------------------
+
+void PolyTree::Clear() {
+ for (PolyNodes::size_type i = 0; i < AllNodes.size(); ++i)
+ delete AllNodes[i];
+ AllNodes.resize(0);
+ Childs.resize(0);
+}
+//------------------------------------------------------------------------------
+
+PolyNode *PolyTree::GetFirst() const {
+ if (!Childs.empty())
+ return Childs[0];
+ else
+ return 0;
+}
+//------------------------------------------------------------------------------
+
+int PolyTree::Total() const {
+ int result = (int)AllNodes.size();
+ // with negative offsets, ignore the hidden outer polygon ...
+ if (result > 0 && Childs[0] != AllNodes[0])
+ result--;
+ return result;
+}
+
+//------------------------------------------------------------------------------
+// PolyNode methods ...
+//------------------------------------------------------------------------------
+
+PolyNode::PolyNode() : Parent(0), Index(0), m_IsOpen(false) {}
+//------------------------------------------------------------------------------
+
+int PolyNode::ChildCount() const { return (int)Childs.size(); }
+//------------------------------------------------------------------------------
+
+void PolyNode::AddChild(PolyNode &child) {
+ unsigned cnt = (unsigned)Childs.size();
+ Childs.push_back(&child);
+ child.Parent = this;
+ child.Index = cnt;
+}
+//------------------------------------------------------------------------------
+
+PolyNode *PolyNode::GetNext() const {
+ if (!Childs.empty())
+ return Childs[0];
+ else
+ return GetNextSiblingUp();
+}
+//------------------------------------------------------------------------------
+
+PolyNode *PolyNode::GetNextSiblingUp() const {
+ if (!Parent) // protects against PolyTree.GetNextSiblingUp()
+ return 0;
+ else if (Index == Parent->Childs.size() - 1)
+ return Parent->GetNextSiblingUp();
+ else
+ return Parent->Childs[Index + 1];
+}
+//------------------------------------------------------------------------------
+
+bool PolyNode::IsHole() const {
+ bool result = true;
+ PolyNode *node = Parent;
+ while (node) {
+ result = !result;
+ node = node->Parent;
+ }
+ return result;
+}
+//------------------------------------------------------------------------------
+
+bool PolyNode::IsOpen() const { return m_IsOpen; }
+//------------------------------------------------------------------------------
+
+#ifndef use_int32
+
+//------------------------------------------------------------------------------
+// Int128 class (enables safe math on signed 64bit integers)
+// eg Int128 val1((long64)9223372036854775807); //ie 2^63 -1
+// Int128 val2((long64)9223372036854775807);
+// Int128 val3 = val1 * val2;
+// val3.AsString => "85070591730234615847396907784232501249" (8.5e+37)
+//------------------------------------------------------------------------------
+
+class Int128 {
+public:
+ ulong64 lo;
+ long64 hi;
+
+ Int128(long64 _lo = 0) {
+ lo = (ulong64)_lo;
+ if (_lo < 0)
+ hi = -1;
+ else
+ hi = 0;
+ }
+
+ Int128(const Int128 &val) : lo(val.lo), hi(val.hi) {}
+
+ Int128(const long64 &_hi, const ulong64 &_lo) : lo(_lo), hi(_hi) {}
+
+ Int128 &operator=(const long64 &val) {
+ lo = (ulong64)val;
+ if (val < 0)
+ hi = -1;
+ else
+ hi = 0;
+ return *this;
+ }
+
+ bool operator==(const Int128 &val) const {
+ return (hi == val.hi && lo == val.lo);
+ }
+
+ bool operator!=(const Int128 &val) const { return !(*this == val); }
+
+ bool operator>(const Int128 &val) const {
+ if (hi != val.hi)
+ return hi > val.hi;
+ else
+ return lo > val.lo;
+ }
+
+ bool operator<(const Int128 &val) const {
+ if (hi != val.hi)
+ return hi < val.hi;
+ else
+ return lo < val.lo;
+ }
+
+ bool operator>=(const Int128 &val) const { return !(*this < val); }
+
+ bool operator<=(const Int128 &val) const { return !(*this > val); }
+
+ Int128 &operator+=(const Int128 &rhs) {
+ hi += rhs.hi;
+ lo += rhs.lo;
+ if (lo < rhs.lo)
+ hi++;
+ return *this;
+ }
+
+ Int128 operator+(const Int128 &rhs) const {
+ Int128 result(*this);
+ result += rhs;
+ return result;
+ }
+
+ Int128 &operator-=(const Int128 &rhs) {
+ *this += -rhs;
+ return *this;
+ }
+
+ Int128 operator-(const Int128 &rhs) const {
+ Int128 result(*this);
+ result -= rhs;
+ return result;
+ }
+
+ Int128 operator-() const // unary negation
+ {
+ if (lo == 0)
+ return Int128(-hi, 0);
+ else
+ return Int128(~hi, ~lo + 1);
+ }
+
+ operator double() const {
+ const double shift64 = 18446744073709551616.0; // 2^64
+ if (hi < 0) {
+ if (lo == 0)
+ return (double)hi * shift64;
+ else
+ return -(double)(~lo + ~hi * shift64);
+ } else
+ return (double)(lo + hi * shift64);
+ }
+};
+//------------------------------------------------------------------------------
+
+Int128 Int128Mul(long64 lhs, long64 rhs) {
+ bool negate = (lhs < 0) != (rhs < 0);
+
+ if (lhs < 0)
+ lhs = -lhs;
+ ulong64 int1Hi = ulong64(lhs) >> 32;
+ ulong64 int1Lo = ulong64(lhs & 0xFFFFFFFF);
+
+ if (rhs < 0)
+ rhs = -rhs;
+ ulong64 int2Hi = ulong64(rhs) >> 32;
+ ulong64 int2Lo = ulong64(rhs & 0xFFFFFFFF);
+
+ // nb: see comments in clipper.pas
+ ulong64 a = int1Hi * int2Hi;
+ ulong64 b = int1Lo * int2Lo;
+ ulong64 c = int1Hi * int2Lo + int1Lo * int2Hi;
+
+ Int128 tmp;
+ tmp.hi = long64(a + (c >> 32));
+ tmp.lo = long64(c << 32);
+ tmp.lo += long64(b);
+ if (tmp.lo < b)
+ tmp.hi++;
+ if (negate)
+ tmp = -tmp;
+ return tmp;
+};
+#endif
+
+//------------------------------------------------------------------------------
+// Miscellaneous global functions
+//------------------------------------------------------------------------------
+
+bool Orientation(const Path &poly) { return Area(poly) >= 0; }
+//------------------------------------------------------------------------------
+
+double Area(const Path &poly) {
+ int size = (int)poly.size();
+ if (size < 3)
+ return 0;
+
+ double a = 0;
+ for (int i = 0, j = size - 1; i < size; ++i) {
+ a += ((double)poly[j].X + poly[i].X) * ((double)poly[j].Y - poly[i].Y);
+ j = i;
+ }
+ return -a * 0.5;
+}
+//------------------------------------------------------------------------------
+
+double Area(const OutPt *op) {
+ const OutPt *startOp = op;
+ if (!op)
+ return 0;
+ double a = 0;
+ do {
+ a += (double)(op->Prev->Pt.X + op->Pt.X) *
+ (double)(op->Prev->Pt.Y - op->Pt.Y);
+ op = op->Next;
+ } while (op != startOp);
+ return a * 0.5;
+}
+//------------------------------------------------------------------------------
+
+double Area(const OutRec &outRec) { return Area(outRec.Pts); }
+//------------------------------------------------------------------------------
+
+bool PointIsVertex(const IntPoint &Pt, OutPt *pp) {
+ OutPt *pp2 = pp;
+ do {
+ if (pp2->Pt == Pt)
+ return true;
+ pp2 = pp2->Next;
+ } while (pp2 != pp);
+ return false;
+}
+//------------------------------------------------------------------------------
+
+// See "The Point in Polygon Problem for Arbitrary Polygons" by Hormann &
+// Agathos
+// http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.88.5498&rep=rep1&type=pdf
+int PointInPolygon(const IntPoint &pt, const Path &path) {
+ // returns 0 if false, +1 if true, -1 if pt ON polygon boundary
+ int result = 0;
+ size_t cnt = path.size();
+ if (cnt < 3)
+ return 0;
+ IntPoint ip = path[0];
+ for (size_t i = 1; i <= cnt; ++i) {
+ IntPoint ipNext = (i == cnt ? path[0] : path[i]);
+ if (ipNext.Y == pt.Y) {
+ if ((ipNext.X == pt.X) ||
+ (ip.Y == pt.Y && ((ipNext.X > pt.X) == (ip.X < pt.X))))
+ return -1;
+ }
+ if ((ip.Y < pt.Y) != (ipNext.Y < pt.Y)) {
+ if (ip.X >= pt.X) {
+ if (ipNext.X > pt.X)
+ result = 1 - result;
+ else {
+ double d = (double)(ip.X - pt.X) * (ipNext.Y - pt.Y) -
+ (double)(ipNext.X - pt.X) * (ip.Y - pt.Y);
+ if (!d)
+ return -1;
+ if ((d > 0) == (ipNext.Y > ip.Y))
+ result = 1 - result;
+ }
+ } else {
+ if (ipNext.X > pt.X) {
+ double d = (double)(ip.X - pt.X) * (ipNext.Y - pt.Y) -
+ (double)(ipNext.X - pt.X) * (ip.Y - pt.Y);
+ if (!d)
+ return -1;
+ if ((d > 0) == (ipNext.Y > ip.Y))
+ result = 1 - result;
+ }
+ }
+ }
+ ip = ipNext;
+ }
+ return result;
+}
+//------------------------------------------------------------------------------
+
+int PointInPolygon(const IntPoint &pt, OutPt *op) {
+ // returns 0 if false, +1 if true, -1 if pt ON polygon boundary
+ int result = 0;
+ OutPt *startOp = op;
+ for (;;) {
+ if (op->Next->Pt.Y == pt.Y) {
+ if ((op->Next->Pt.X == pt.X) ||
+ (op->Pt.Y == pt.Y && ((op->Next->Pt.X > pt.X) == (op->Pt.X < pt.X))))
+ return -1;
+ }
+ if ((op->Pt.Y < pt.Y) != (op->Next->Pt.Y < pt.Y)) {
+ if (op->Pt.X >= pt.X) {
+ if (op->Next->Pt.X > pt.X)
+ result = 1 - result;
+ else {
+ double d = (double)(op->Pt.X - pt.X) * (op->Next->Pt.Y - pt.Y) -
+ (double)(op->Next->Pt.X - pt.X) * (op->Pt.Y - pt.Y);
+ if (!d)
+ return -1;
+ if ((d > 0) == (op->Next->Pt.Y > op->Pt.Y))
+ result = 1 - result;
+ }
+ } else {
+ if (op->Next->Pt.X > pt.X) {
+ double d = (double)(op->Pt.X - pt.X) * (op->Next->Pt.Y - pt.Y) -
+ (double)(op->Next->Pt.X - pt.X) * (op->Pt.Y - pt.Y);
+ if (!d)
+ return -1;
+ if ((d > 0) == (op->Next->Pt.Y > op->Pt.Y))
+ result = 1 - result;
+ }
+ }
+ }
+ op = op->Next;
+ if (startOp == op)
+ break;
+ }
+ return result;
+}
+//------------------------------------------------------------------------------
+
+bool Poly2ContainsPoly1(OutPt *OutPt1, OutPt *OutPt2) {
+ OutPt *op = OutPt1;
+ do {
+ // nb: PointInPolygon returns 0 if false, +1 if true, -1 if pt on polygon
+ int res = PointInPolygon(op->Pt, OutPt2);
+ if (res >= 0)
+ return res > 0;
+ op = op->Next;
+ } while (op != OutPt1);
+ return true;
+}
+//----------------------------------------------------------------------
+
+bool SlopesEqual(const TEdge &e1, const TEdge &e2, bool UseFullInt64Range) {
+#ifndef use_int32
+ if (UseFullInt64Range)
+ return Int128Mul(e1.Top.Y - e1.Bot.Y, e2.Top.X - e2.Bot.X) ==
+ Int128Mul(e1.Top.X - e1.Bot.X, e2.Top.Y - e2.Bot.Y);
+ else
+#endif
+ return (e1.Top.Y - e1.Bot.Y) * (e2.Top.X - e2.Bot.X) ==
+ (e1.Top.X - e1.Bot.X) * (e2.Top.Y - e2.Bot.Y);
+}
+//------------------------------------------------------------------------------
+
+bool SlopesEqual(const IntPoint pt1, const IntPoint pt2, const IntPoint pt3,
+ bool UseFullInt64Range) {
+#ifndef use_int32
+ if (UseFullInt64Range)
+ return Int128Mul(pt1.Y - pt2.Y, pt2.X - pt3.X) ==
+ Int128Mul(pt1.X - pt2.X, pt2.Y - pt3.Y);
+ else
+#endif
+ return (pt1.Y - pt2.Y) * (pt2.X - pt3.X) ==
+ (pt1.X - pt2.X) * (pt2.Y - pt3.Y);
+}
+//------------------------------------------------------------------------------
+
+bool SlopesEqual(const IntPoint pt1, const IntPoint pt2, const IntPoint pt3,
+ const IntPoint pt4, bool UseFullInt64Range) {
+#ifndef use_int32
+ if (UseFullInt64Range)
+ return Int128Mul(pt1.Y - pt2.Y, pt3.X - pt4.X) ==
+ Int128Mul(pt1.X - pt2.X, pt3.Y - pt4.Y);
+ else
+#endif
+ return (pt1.Y - pt2.Y) * (pt3.X - pt4.X) ==
+ (pt1.X - pt2.X) * (pt3.Y - pt4.Y);
+}
+//------------------------------------------------------------------------------
+
+inline bool IsHorizontal(TEdge &e) { return e.Dx == HORIZONTAL; }
+//------------------------------------------------------------------------------
+
+inline double GetDx(const IntPoint pt1, const IntPoint pt2) {
+ return (pt1.Y == pt2.Y) ? HORIZONTAL
+ : (double)(pt2.X - pt1.X) / (pt2.Y - pt1.Y);
+}
+//---------------------------------------------------------------------------
+
+inline void SetDx(TEdge &e) {
+ cInt dy = (e.Top.Y - e.Bot.Y);
+ if (dy == 0)
+ e.Dx = HORIZONTAL;
+ else
+ e.Dx = (double)(e.Top.X - e.Bot.X) / dy;
+}
+//---------------------------------------------------------------------------
+
+inline void SwapSides(TEdge &Edge1, TEdge &Edge2) {
+ EdgeSide Side = Edge1.Side;
+ Edge1.Side = Edge2.Side;
+ Edge2.Side = Side;
+}
+//------------------------------------------------------------------------------
+
+inline void SwapPolyIndexes(TEdge &Edge1, TEdge &Edge2) {
+ int OutIdx = Edge1.OutIdx;
+ Edge1.OutIdx = Edge2.OutIdx;
+ Edge2.OutIdx = OutIdx;
+}
+//------------------------------------------------------------------------------
+
+inline cInt TopX(TEdge &edge, const cInt currentY) {
+ return (currentY == edge.Top.Y)
+ ? edge.Top.X
+ : edge.Bot.X + Round(edge.Dx * (currentY - edge.Bot.Y));
+}
+//------------------------------------------------------------------------------
+
+void IntersectPoint(TEdge &Edge1, TEdge &Edge2, IntPoint &ip) {
+#ifdef use_xyz
+ ip.Z = 0;
+#endif
+
+ double b1, b2;
+ if (Edge1.Dx == Edge2.Dx) {
+ ip.Y = Edge1.Curr.Y;
+ ip.X = TopX(Edge1, ip.Y);
+ return;
+ } else if (Edge1.Dx == 0) {
+ ip.X = Edge1.Bot.X;
+ if (IsHorizontal(Edge2))
+ ip.Y = Edge2.Bot.Y;
+ else {
+ b2 = Edge2.Bot.Y - (Edge2.Bot.X / Edge2.Dx);
+ ip.Y = Round(ip.X / Edge2.Dx + b2);
+ }
+ } else if (Edge2.Dx == 0) {
+ ip.X = Edge2.Bot.X;
+ if (IsHorizontal(Edge1))
+ ip.Y = Edge1.Bot.Y;
+ else {
+ b1 = Edge1.Bot.Y - (Edge1.Bot.X / Edge1.Dx);
+ ip.Y = Round(ip.X / Edge1.Dx + b1);
+ }
+ } else {
+ b1 = Edge1.Bot.X - Edge1.Bot.Y * Edge1.Dx;
+ b2 = Edge2.Bot.X - Edge2.Bot.Y * Edge2.Dx;
+ double q = (b2 - b1) / (Edge1.Dx - Edge2.Dx);
+ ip.Y = Round(q);
+ if (std::fabs(Edge1.Dx) < std::fabs(Edge2.Dx))
+ ip.X = Round(Edge1.Dx * q + b1);
+ else
+ ip.X = Round(Edge2.Dx * q + b2);
+ }
+
+ if (ip.Y < Edge1.Top.Y || ip.Y < Edge2.Top.Y) {
+ if (Edge1.Top.Y > Edge2.Top.Y)
+ ip.Y = Edge1.Top.Y;
+ else
+ ip.Y = Edge2.Top.Y;
+ if (std::fabs(Edge1.Dx) < std::fabs(Edge2.Dx))
+ ip.X = TopX(Edge1, ip.Y);
+ else
+ ip.X = TopX(Edge2, ip.Y);
+ }
+ // finally, don't allow 'ip' to be BELOW curr.Y (ie bottom of scanbeam) ...
+ if (ip.Y > Edge1.Curr.Y) {
+ ip.Y = Edge1.Curr.Y;
+ // use the more vertical edge to derive X ...
+ if (std::fabs(Edge1.Dx) > std::fabs(Edge2.Dx))
+ ip.X = TopX(Edge2, ip.Y);
+ else
+ ip.X = TopX(Edge1, ip.Y);
+ }
+}
+//------------------------------------------------------------------------------
+
+void ReversePolyPtLinks(OutPt *pp) {
+ if (!pp)
+ return;
+ OutPt *pp1, *pp2;
+ pp1 = pp;
+ do {
+ pp2 = pp1->Next;
+ pp1->Next = pp1->Prev;
+ pp1->Prev = pp2;
+ pp1 = pp2;
+ } while (pp1 != pp);
+}
+//------------------------------------------------------------------------------
+
+void DisposeOutPts(OutPt *&pp) {
+ if (pp == 0)
+ return;
+ pp->Prev->Next = 0;
+ while (pp) {
+ OutPt *tmpPp = pp;
+ pp = pp->Next;
+ delete tmpPp;
+ }
+}
+//------------------------------------------------------------------------------
+
+inline void InitEdge(TEdge *e, TEdge *eNext, TEdge *ePrev, const IntPoint &Pt) {
+ std::memset(e, 0, sizeof(TEdge));
+ e->Next = eNext;
+ e->Prev = ePrev;
+ e->Curr = Pt;
+ e->OutIdx = Unassigned;
+}
+//------------------------------------------------------------------------------
+
+void InitEdge2(TEdge &e, PolyType Pt) {
+ if (e.Curr.Y >= e.Next->Curr.Y) {
+ e.Bot = e.Curr;
+ e.Top = e.Next->Curr;
+ } else {
+ e.Top = e.Curr;
+ e.Bot = e.Next->Curr;
+ }
+ SetDx(e);
+ e.PolyTyp = Pt;
+}
+//------------------------------------------------------------------------------
+
+TEdge *RemoveEdge(TEdge *e) {
+ // removes e from double_linked_list (but without removing from memory)
+ e->Prev->Next = e->Next;
+ e->Next->Prev = e->Prev;
+ TEdge *result = e->Next;
+ e->Prev = 0; // flag as removed (see ClipperBase.Clear)
+ return result;
+}
+//------------------------------------------------------------------------------
+
+inline void ReverseHorizontal(TEdge &e) {
+ // swap horizontal edges' Top and Bottom x's so they follow the natural
+ // progression of the bounds - ie so their xbots will align with the
+ // adjoining lower edge. [Helpful in the ProcessHorizontal() method.]
+ std::swap(e.Top.X, e.Bot.X);
+#ifdef use_xyz
+ std::swap(e.Top.Z, e.Bot.Z);
+#endif
+}
+//------------------------------------------------------------------------------
+
+void SwapPoints(IntPoint &pt1, IntPoint &pt2) {
+ IntPoint tmp = pt1;
+ pt1 = pt2;
+ pt2 = tmp;
+}
+//------------------------------------------------------------------------------
+
+bool GetOverlapSegment(IntPoint pt1a, IntPoint pt1b, IntPoint pt2a,
+ IntPoint pt2b, IntPoint &pt1, IntPoint &pt2) {
+ // precondition: segments are Collinear.
+ if (Abs(pt1a.X - pt1b.X) > Abs(pt1a.Y - pt1b.Y)) {
+ if (pt1a.X > pt1b.X)
+ SwapPoints(pt1a, pt1b);
+ if (pt2a.X > pt2b.X)
+ SwapPoints(pt2a, pt2b);
+ if (pt1a.X > pt2a.X)
+ pt1 = pt1a;
+ else
+ pt1 = pt2a;
+ if (pt1b.X < pt2b.X)
+ pt2 = pt1b;
+ else
+ pt2 = pt2b;
+ return pt1.X < pt2.X;
+ } else {
+ if (pt1a.Y < pt1b.Y)
+ SwapPoints(pt1a, pt1b);
+ if (pt2a.Y < pt2b.Y)
+ SwapPoints(pt2a, pt2b);
+ if (pt1a.Y < pt2a.Y)
+ pt1 = pt1a;
+ else
+ pt1 = pt2a;
+ if (pt1b.Y > pt2b.Y)
+ pt2 = pt1b;
+ else
+ pt2 = pt2b;
+ return pt1.Y > pt2.Y;
+ }
+}
+//------------------------------------------------------------------------------
+
+bool FirstIsBottomPt(const OutPt *btmPt1, const OutPt *btmPt2) {
+ OutPt *p = btmPt1->Prev;
+ while ((p->Pt == btmPt1->Pt) && (p != btmPt1))
+ p = p->Prev;
+ double dx1p = std::fabs(GetDx(btmPt1->Pt, p->Pt));
+ p = btmPt1->Next;
+ while ((p->Pt == btmPt1->Pt) && (p != btmPt1))
+ p = p->Next;
+ double dx1n = std::fabs(GetDx(btmPt1->Pt, p->Pt));
+
+ p = btmPt2->Prev;
+ while ((p->Pt == btmPt2->Pt) && (p != btmPt2))
+ p = p->Prev;
+ double dx2p = std::fabs(GetDx(btmPt2->Pt, p->Pt));
+ p = btmPt2->Next;
+ while ((p->Pt == btmPt2->Pt) && (p != btmPt2))
+ p = p->Next;
+ double dx2n = std::fabs(GetDx(btmPt2->Pt, p->Pt));
+
+ if (std::max(dx1p, dx1n) == std::max(dx2p, dx2n) &&
+ std::min(dx1p, dx1n) == std::min(dx2p, dx2n))
+ return Area(btmPt1) > 0; // if otherwise identical use orientation
+ else
+ return (dx1p >= dx2p && dx1p >= dx2n) || (dx1n >= dx2p && dx1n >= dx2n);
+}
+//------------------------------------------------------------------------------
+
+OutPt *GetBottomPt(OutPt *pp) {
+ OutPt *dups = 0;
+ OutPt *p = pp->Next;
+ while (p != pp) {
+ if (p->Pt.Y > pp->Pt.Y) {
+ pp = p;
+ dups = 0;
+ } else if (p->Pt.Y == pp->Pt.Y && p->Pt.X <= pp->Pt.X) {
+ if (p->Pt.X < pp->Pt.X) {
+ dups = 0;
+ pp = p;
+ } else {
+ if (p->Next != pp && p->Prev != pp)
+ dups = p;
+ }
+ }
+ p = p->Next;
+ }
+ if (dups) {
+ // there appears to be at least 2 vertices at BottomPt so ...
+ while (dups != p) {
+ if (!FirstIsBottomPt(p, dups))
+ pp = dups;
+ dups = dups->Next;
+ while (dups->Pt != pp->Pt)
+ dups = dups->Next;
+ }
+ }
+ return pp;
+}
+//------------------------------------------------------------------------------
+
+bool Pt2IsBetweenPt1AndPt3(const IntPoint pt1, const IntPoint pt2,
+ const IntPoint pt3) {
+ if ((pt1 == pt3) || (pt1 == pt2) || (pt3 == pt2))
+ return false;
+ else if (pt1.X != pt3.X)
+ return (pt2.X > pt1.X) == (pt2.X < pt3.X);
+ else
+ return (pt2.Y > pt1.Y) == (pt2.Y < pt3.Y);
+}
+//------------------------------------------------------------------------------
+
+bool HorzSegmentsOverlap(cInt seg1a, cInt seg1b, cInt seg2a, cInt seg2b) {
+ if (seg1a > seg1b)
+ std::swap(seg1a, seg1b);
+ if (seg2a > seg2b)
+ std::swap(seg2a, seg2b);
+ return (seg1a < seg2b) && (seg2a < seg1b);
+}
+
+//------------------------------------------------------------------------------
+// ClipperBase class methods ...
+//------------------------------------------------------------------------------
+
+ClipperBase::ClipperBase() // constructor
+{
+ m_CurrentLM = m_MinimaList.begin(); // begin() == end() here
+ m_UseFullRange = false;
+}
+//------------------------------------------------------------------------------
+
+ClipperBase::~ClipperBase() // destructor
+{
+ Clear();
+}
+//------------------------------------------------------------------------------
+
+void RangeTest(const IntPoint &Pt, bool &useFullRange) {
+ if (useFullRange) {
+ if (Pt.X > hiRange || Pt.Y > hiRange || -Pt.X > hiRange || -Pt.Y > hiRange)
+ throw clipperException("Coordinate outside allowed range");
+ } else if (Pt.X > loRange || Pt.Y > loRange || -Pt.X > loRange ||
+ -Pt.Y > loRange) {
+ useFullRange = true;
+ RangeTest(Pt, useFullRange);
+ }
+}
+//------------------------------------------------------------------------------
+
+TEdge *FindNextLocMin(TEdge *E) {
+ for (;;) {
+ while (E->Bot != E->Prev->Bot || E->Curr == E->Top)
+ E = E->Next;
+ if (!IsHorizontal(*E) && !IsHorizontal(*E->Prev))
+ break;
+ while (IsHorizontal(*E->Prev))
+ E = E->Prev;
+ TEdge *E2 = E;
+ while (IsHorizontal(*E))
+ E = E->Next;
+ if (E->Top.Y == E->Prev->Bot.Y)
+ continue; // ie just an intermediate horz.
+ if (E2->Prev->Bot.X < E->Bot.X)
+ E = E2;
+ break;
+ }
+ return E;
+}
+//------------------------------------------------------------------------------
+
+TEdge *ClipperBase::ProcessBound(TEdge *E, bool NextIsForward) {
+ TEdge *Result = E;
+ TEdge *Horz = 0;
+
+ if (E->OutIdx == Skip) {
+ // if edges still remain in the current bound beyond the skip edge then
+ // create another LocMin and call ProcessBound once more
+ if (NextIsForward) {
+ while (E->Top.Y == E->Next->Bot.Y)
+ E = E->Next;
+ // don't include top horizontals when parsing a bound a second time,
+ // they will be contained in the opposite bound ...
+ while (E != Result && IsHorizontal(*E))
+ E = E->Prev;
+ } else {
+ while (E->Top.Y == E->Prev->Bot.Y)
+ E = E->Prev;
+ while (E != Result && IsHorizontal(*E))
+ E = E->Next;
+ }
+
+ if (E == Result) {
+ if (NextIsForward)
+ Result = E->Next;
+ else
+ Result = E->Prev;
+ } else {
+ // there are more edges in the bound beyond result starting with E
+ if (NextIsForward)
+ E = Result->Next;
+ else
+ E = Result->Prev;
+ MinimaList::value_type locMin;
+ locMin.Y = E->Bot.Y;
+ locMin.LeftBound = 0;
+ locMin.RightBound = E;
+ E->WindDelta = 0;
+ Result = ProcessBound(E, NextIsForward);
+ m_MinimaList.push_back(locMin);
+ }
+ return Result;
+ }
+
+ TEdge *EStart;
+
+ if (IsHorizontal(*E)) {
+ // We need to be careful with open paths because this may not be a
+ // true local minima (ie E may be following a skip edge).
+ // Also, consecutive horz. edges may start heading left before going right.
+ if (NextIsForward)
+ EStart = E->Prev;
+ else
+ EStart = E->Next;
+ if (IsHorizontal(*EStart)) // ie an adjoining horizontal skip edge
+ {
+ if (EStart->Bot.X != E->Bot.X && EStart->Top.X != E->Bot.X)
+ ReverseHorizontal(*E);
+ } else if (EStart->Bot.X != E->Bot.X)
+ ReverseHorizontal(*E);
+ }
+
+ EStart = E;
+ if (NextIsForward) {
+ while (Result->Top.Y == Result->Next->Bot.Y && Result->Next->OutIdx != Skip)
+ Result = Result->Next;
+ if (IsHorizontal(*Result) && Result->Next->OutIdx != Skip) {
+ // nb: at the top of a bound, horizontals are added to the bound
+ // only when the preceding edge attaches to the horizontal's left vertex
+ // unless a Skip edge is encountered when that becomes the top divide
+ Horz = Result;
+ while (IsHorizontal(*Horz->Prev))
+ Horz = Horz->Prev;
+ if (Horz->Prev->Top.X > Result->Next->Top.X)
+ Result = Horz->Prev;
+ }
+ while (E != Result) {
+ E->NextInLML = E->Next;
+ if (IsHorizontal(*E) && E != EStart && E->Bot.X != E->Prev->Top.X)
+ ReverseHorizontal(*E);
+ E = E->Next;
+ }
+ if (IsHorizontal(*E) && E != EStart && E->Bot.X != E->Prev->Top.X)
+ ReverseHorizontal(*E);
+ Result = Result->Next; // move to the edge just beyond current bound
+ } else {
+ while (Result->Top.Y == Result->Prev->Bot.Y && Result->Prev->OutIdx != Skip)
+ Result = Result->Prev;
+ if (IsHorizontal(*Result) && Result->Prev->OutIdx != Skip) {
+ Horz = Result;
+ while (IsHorizontal(*Horz->Next))
+ Horz = Horz->Next;
+ if (Horz->Next->Top.X == Result->Prev->Top.X ||
+ Horz->Next->Top.X > Result->Prev->Top.X)
+ Result = Horz->Next;
+ }
+
+ while (E != Result) {
+ E->NextInLML = E->Prev;
+ if (IsHorizontal(*E) && E != EStart && E->Bot.X != E->Next->Top.X)
+ ReverseHorizontal(*E);
+ E = E->Prev;
+ }
+ if (IsHorizontal(*E) && E != EStart && E->Bot.X != E->Next->Top.X)
+ ReverseHorizontal(*E);
+ Result = Result->Prev; // move to the edge just beyond current bound
+ }
+
+ return Result;
+}
+//------------------------------------------------------------------------------
+
+bool ClipperBase::AddPath(const Path &pg, PolyType PolyTyp, bool Closed) {
+#ifdef use_lines
+ if (!Closed && PolyTyp == ptClip)
+ throw clipperException("AddPath: Open paths must be subject.");
+#else
+ if (!Closed)
+ throw clipperException("AddPath: Open paths have been disabled.");
+#endif
+
+ int highI = (int)pg.size() - 1;
+ if (Closed)
+ while (highI > 0 && (pg[highI] == pg[0]))
+ --highI;
+ while (highI > 0 && (pg[highI] == pg[highI - 1]))
+ --highI;
+ if ((Closed && highI < 2) || (!Closed && highI < 1))
+ return false;
+
+ // create a new edge array ...
+ TEdge *edges = new TEdge[highI + 1];
+
+ bool IsFlat = true;
+ // 1. Basic (first) edge initialization ...
+ try {
+ edges[1].Curr = pg[1];
+ RangeTest(pg[0], m_UseFullRange);
+ RangeTest(pg[highI], m_UseFullRange);
+ InitEdge(&edges[0], &edges[1], &edges[highI], pg[0]);
+ InitEdge(&edges[highI], &edges[0], &edges[highI - 1], pg[highI]);
+ for (int i = highI - 1; i >= 1; --i) {
+ RangeTest(pg[i], m_UseFullRange);
+ InitEdge(&edges[i], &edges[i + 1], &edges[i - 1], pg[i]);
+ }
+ } catch (...) {
+ delete[] edges;
+ throw; // range test fails
+ }
+ TEdge *eStart = &edges[0];
+
+ // 2. Remove duplicate vertices, and (when closed) collinear edges ...
+ TEdge *E = eStart, *eLoopStop = eStart;
+ for (;;) {
+ // nb: allows matching start and end points when not Closed ...
+ if (E->Curr == E->Next->Curr && (Closed || E->Next != eStart)) {
+ if (E == E->Next)
+ break;
+ if (E == eStart)
+ eStart = E->Next;
+ E = RemoveEdge(E);
+ eLoopStop = E;
+ continue;
+ }
+ if (E->Prev == E->Next)
+ break; // only two vertices
+ else if (Closed &&
+ SlopesEqual(E->Prev->Curr, E->Curr, E->Next->Curr,
+ m_UseFullRange) &&
+ (!m_PreserveCollinear ||
+ !Pt2IsBetweenPt1AndPt3(E->Prev->Curr, E->Curr, E->Next->Curr))) {
+ // Collinear edges are allowed for open paths but in closed paths
+ // the default is to merge adjacent collinear edges into a single edge.
+ // However, if the PreserveCollinear property is enabled, only overlapping
+ // collinear edges (ie spikes) will be removed from closed paths.
+ if (E == eStart)
+ eStart = E->Next;
+ E = RemoveEdge(E);
+ E = E->Prev;
+ eLoopStop = E;
+ continue;
+ }
+ E = E->Next;
+ if ((E == eLoopStop) || (!Closed && E->Next == eStart))
+ break;
+ }
+
+ if ((!Closed && (E == E->Next)) || (Closed && (E->Prev == E->Next))) {
+ delete[] edges;
+ return false;
+ }
+
+ if (!Closed) {
+ m_HasOpenPaths = true;
+ eStart->Prev->OutIdx = Skip;
+ }
+
+ // 3. Do second stage of edge initialization ...
+ E = eStart;
+ do {
+ InitEdge2(*E, PolyTyp);
+ E = E->Next;
+ if (IsFlat && E->Curr.Y != eStart->Curr.Y)
+ IsFlat = false;
+ } while (E != eStart);
+
+ // 4. Finally, add edge bounds to LocalMinima list ...
+
+ // Totally flat paths must be handled differently when adding them
+ // to LocalMinima list to avoid endless loops etc ...
+ if (IsFlat) {
+ if (Closed) {
+ delete[] edges;
+ return false;
+ }
+ E->Prev->OutIdx = Skip;
+ MinimaList::value_type locMin;
+ locMin.Y = E->Bot.Y;
+ locMin.LeftBound = 0;
+ locMin.RightBound = E;
+ locMin.RightBound->Side = esRight;
+ locMin.RightBound->WindDelta = 0;
+ for (;;) {
+ if (E->Bot.X != E->Prev->Top.X)
+ ReverseHorizontal(*E);
+ if (E->Next->OutIdx == Skip)
+ break;
+ E->NextInLML = E->Next;
+ E = E->Next;
+ }
+ m_MinimaList.push_back(locMin);
+ m_edges.push_back(edges);
+ return true;
+ }
+
+ m_edges.push_back(edges);
+ bool leftBoundIsForward;
+ TEdge *EMin = 0;
+
+ // workaround to avoid an endless loop in the while loop below when
+ // open paths have matching start and end points ...
+ if (E->Prev->Bot == E->Prev->Top)
+ E = E->Next;
+
+ for (;;) {
+ E = FindNextLocMin(E);
+ if (E == EMin)
+ break;
+ else if (!EMin)
+ EMin = E;
+
+ // E and E.Prev now share a local minima (left aligned if horizontal).
+ // Compare their slopes to find which starts which bound ...
+ MinimaList::value_type locMin;
+ locMin.Y = E->Bot.Y;
+ if (E->Dx < E->Prev->Dx) {
+ locMin.LeftBound = E->Prev;
+ locMin.RightBound = E;
+ leftBoundIsForward = false; // Q.nextInLML = Q.prev
+ } else {
+ locMin.LeftBound = E;
+ locMin.RightBound = E->Prev;
+ leftBoundIsForward = true; // Q.nextInLML = Q.next
+ }
+
+ if (!Closed)
+ locMin.LeftBound->WindDelta = 0;
+ else if (locMin.LeftBound->Next == locMin.RightBound)
+ locMin.LeftBound->WindDelta = -1;
+ else
+ locMin.LeftBound->WindDelta = 1;
+ locMin.RightBound->WindDelta = -locMin.LeftBound->WindDelta;
+
+ E = ProcessBound(locMin.LeftBound, leftBoundIsForward);
+ if (E->OutIdx == Skip)
+ E = ProcessBound(E, leftBoundIsForward);
+
+ TEdge *E2 = ProcessBound(locMin.RightBound, !leftBoundIsForward);
+ if (E2->OutIdx == Skip)
+ E2 = ProcessBound(E2, !leftBoundIsForward);
+
+ if (locMin.LeftBound->OutIdx == Skip)
+ locMin.LeftBound = 0;
+ else if (locMin.RightBound->OutIdx == Skip)
+ locMin.RightBound = 0;
+ m_MinimaList.push_back(locMin);
+ if (!leftBoundIsForward)
+ E = E2;
+ }
+ return true;
+}
+//------------------------------------------------------------------------------
+
+bool ClipperBase::AddPaths(const Paths &ppg, PolyType PolyTyp, bool Closed) {
+ bool result = false;
+ for (Paths::size_type i = 0; i < ppg.size(); ++i)
+ if (AddPath(ppg[i], PolyTyp, Closed))
+ result = true;
+ return result;
+}
+//------------------------------------------------------------------------------
+
+void ClipperBase::Clear() {
+ DisposeLocalMinimaList();
+ for (EdgeList::size_type i = 0; i < m_edges.size(); ++i) {
+ TEdge *edges = m_edges[i];
+ delete[] edges;
+ }
+ m_edges.clear();
+ m_UseFullRange = false;
+ m_HasOpenPaths = false;
+}
+//------------------------------------------------------------------------------
+
+void ClipperBase::Reset() {
+ m_CurrentLM = m_MinimaList.begin();
+ if (m_CurrentLM == m_MinimaList.end())
+ return; // ie nothing to process
+ std::sort(m_MinimaList.begin(), m_MinimaList.end(), LocMinSorter());
+
+ m_Scanbeam = ScanbeamList(); // clears/resets priority_queue
+ // reset all edges ...
+ for (MinimaList::iterator lm = m_MinimaList.begin(); lm != m_MinimaList.end();
+ ++lm) {
+ InsertScanbeam(lm->Y);
+ TEdge *e = lm->LeftBound;
+ if (e) {
+ e->Curr = e->Bot;
+ e->Side = esLeft;
+ e->OutIdx = Unassigned;
+ }
+
+ e = lm->RightBound;
+ if (e) {
+ e->Curr = e->Bot;
+ e->Side = esRight;
+ e->OutIdx = Unassigned;
+ }
+ }
+ m_ActiveEdges = 0;
+ m_CurrentLM = m_MinimaList.begin();
+}
+//------------------------------------------------------------------------------
+
+void ClipperBase::DisposeLocalMinimaList() {
+ m_MinimaList.clear();
+ m_CurrentLM = m_MinimaList.begin();
+}
+//------------------------------------------------------------------------------
+
+bool ClipperBase::PopLocalMinima(cInt Y, const LocalMinimum *&locMin) {
+ if (m_CurrentLM == m_MinimaList.end() || (*m_CurrentLM).Y != Y)
+ return false;
+ locMin = &(*m_CurrentLM);
+ ++m_CurrentLM;
+ return true;
+}
+//------------------------------------------------------------------------------
+
+IntRect ClipperBase::GetBounds() {
+ IntRect result;
+ MinimaList::iterator lm = m_MinimaList.begin();
+ if (lm == m_MinimaList.end()) {
+ result.left = result.top = result.right = result.bottom = 0;
+ return result;
+ }
+ result.left = lm->LeftBound->Bot.X;
+ result.top = lm->LeftBound->Bot.Y;
+ result.right = lm->LeftBound->Bot.X;
+ result.bottom = lm->LeftBound->Bot.Y;
+ while (lm != m_MinimaList.end()) {
+ // todo - needs fixing for open paths
+ result.bottom = std::max(result.bottom, lm->LeftBound->Bot.Y);
+ TEdge *e = lm->LeftBound;
+ for (;;) {
+ TEdge *bottomE = e;
+ while (e->NextInLML) {
+ if (e->Bot.X < result.left)
+ result.left = e->Bot.X;
+ if (e->Bot.X > result.right)
+ result.right = e->Bot.X;
+ e = e->NextInLML;
+ }
+ result.left = std::min(result.left, e->Bot.X);
+ result.right = std::max(result.right, e->Bot.X);
+ result.left = std::min(result.left, e->Top.X);
+ result.right = std::max(result.right, e->Top.X);
+ result.top = std::min(result.top, e->Top.Y);
+ if (bottomE == lm->LeftBound)
+ e = lm->RightBound;
+ else
+ break;
+ }
+ ++lm;
+ }
+ return result;
+}
+//------------------------------------------------------------------------------
+
+void ClipperBase::InsertScanbeam(const cInt Y) { m_Scanbeam.push(Y); }
+//------------------------------------------------------------------------------
+
+bool ClipperBase::PopScanbeam(cInt &Y) {
+ if (m_Scanbeam.empty())
+ return false;
+ Y = m_Scanbeam.top();
+ m_Scanbeam.pop();
+ while (!m_Scanbeam.empty() && Y == m_Scanbeam.top()) {
+ m_Scanbeam.pop();
+ } // Pop duplicates.
+ return true;
+}
+//------------------------------------------------------------------------------
+
+void ClipperBase::DisposeAllOutRecs() {
+ for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i)
+ DisposeOutRec(i);
+ m_PolyOuts.clear();
+}
+//------------------------------------------------------------------------------
+
+void ClipperBase::DisposeOutRec(PolyOutList::size_type index) {
+ OutRec *outRec = m_PolyOuts[index];
+ if (outRec->Pts)
+ DisposeOutPts(outRec->Pts);
+ delete outRec;
+ m_PolyOuts[index] = 0;
+}
+//------------------------------------------------------------------------------
+
+void ClipperBase::DeleteFromAEL(TEdge *e) {
+ TEdge *AelPrev = e->PrevInAEL;
+ TEdge *AelNext = e->NextInAEL;
+ if (!AelPrev && !AelNext && (e != m_ActiveEdges))
+ return; // already deleted
+ if (AelPrev)
+ AelPrev->NextInAEL = AelNext;
+ else
+ m_ActiveEdges = AelNext;
+ if (AelNext)
+ AelNext->PrevInAEL = AelPrev;
+ e->NextInAEL = 0;
+ e->PrevInAEL = 0;
+}
+//------------------------------------------------------------------------------
+
+OutRec *ClipperBase::CreateOutRec() {
+ OutRec *result = new OutRec;
+ result->IsHole = false;
+ result->IsOpen = false;
+ result->FirstLeft = 0;
+ result->Pts = 0;
+ result->BottomPt = 0;
+ result->PolyNd = 0;
+ m_PolyOuts.push_back(result);
+ result->Idx = (int)m_PolyOuts.size() - 1;
+ return result;
+}
+//------------------------------------------------------------------------------
+
+void ClipperBase::SwapPositionsInAEL(TEdge *Edge1, TEdge *Edge2) {
+ // check that one or other edge hasn't already been removed from AEL ...
+ if (Edge1->NextInAEL == Edge1->PrevInAEL ||
+ Edge2->NextInAEL == Edge2->PrevInAEL)
+ return;
+
+ if (Edge1->NextInAEL == Edge2) {
+ TEdge *Next = Edge2->NextInAEL;
+ if (Next)
+ Next->PrevInAEL = Edge1;
+ TEdge *Prev = Edge1->PrevInAEL;
+ if (Prev)
+ Prev->NextInAEL = Edge2;
+ Edge2->PrevInAEL = Prev;
+ Edge2->NextInAEL = Edge1;
+ Edge1->PrevInAEL = Edge2;
+ Edge1->NextInAEL = Next;
+ } else if (Edge2->NextInAEL == Edge1) {
+ TEdge *Next = Edge1->NextInAEL;
+ if (Next)
+ Next->PrevInAEL = Edge2;
+ TEdge *Prev = Edge2->PrevInAEL;
+ if (Prev)
+ Prev->NextInAEL = Edge1;
+ Edge1->PrevInAEL = Prev;
+ Edge1->NextInAEL = Edge2;
+ Edge2->PrevInAEL = Edge1;
+ Edge2->NextInAEL = Next;
+ } else {
+ TEdge *Next = Edge1->NextInAEL;
+ TEdge *Prev = Edge1->PrevInAEL;
+ Edge1->NextInAEL = Edge2->NextInAEL;
+ if (Edge1->NextInAEL)
+ Edge1->NextInAEL->PrevInAEL = Edge1;
+ Edge1->PrevInAEL = Edge2->PrevInAEL;
+ if (Edge1->PrevInAEL)
+ Edge1->PrevInAEL->NextInAEL = Edge1;
+ Edge2->NextInAEL = Next;
+ if (Edge2->NextInAEL)
+ Edge2->NextInAEL->PrevInAEL = Edge2;
+ Edge2->PrevInAEL = Prev;
+ if (Edge2->PrevInAEL)
+ Edge2->PrevInAEL->NextInAEL = Edge2;
+ }
+
+ if (!Edge1->PrevInAEL)
+ m_ActiveEdges = Edge1;
+ else if (!Edge2->PrevInAEL)
+ m_ActiveEdges = Edge2;
+}
+//------------------------------------------------------------------------------
+
+void ClipperBase::UpdateEdgeIntoAEL(TEdge *&e) {
+ if (!e->NextInLML)
+ throw clipperException("UpdateEdgeIntoAEL: invalid call");
+
+ e->NextInLML->OutIdx = e->OutIdx;
+ TEdge *AelPrev = e->PrevInAEL;
+ TEdge *AelNext = e->NextInAEL;
+ if (AelPrev)
+ AelPrev->NextInAEL = e->NextInLML;
+ else
+ m_ActiveEdges = e->NextInLML;
+ if (AelNext)
+ AelNext->PrevInAEL = e->NextInLML;
+ e->NextInLML->Side = e->Side;
+ e->NextInLML->WindDelta = e->WindDelta;
+ e->NextInLML->WindCnt = e->WindCnt;
+ e->NextInLML->WindCnt2 = e->WindCnt2;
+ e = e->NextInLML;
+ e->Curr = e->Bot;
+ e->PrevInAEL = AelPrev;
+ e->NextInAEL = AelNext;
+ if (!IsHorizontal(*e))
+ InsertScanbeam(e->Top.Y);
+}
+//------------------------------------------------------------------------------
+
+bool ClipperBase::LocalMinimaPending() {
+ return (m_CurrentLM != m_MinimaList.end());
+}
+
+//------------------------------------------------------------------------------
+// TClipper methods ...
+//------------------------------------------------------------------------------
+
+Clipper::Clipper(int initOptions)
+ : ClipperBase() // constructor
+{
+ m_ExecuteLocked = false;
+ m_UseFullRange = false;
+ m_ReverseOutput = ((initOptions & ioReverseSolution) != 0);
+ m_StrictSimple = ((initOptions & ioStrictlySimple) != 0);
+ m_PreserveCollinear = ((initOptions & ioPreserveCollinear) != 0);
+ m_HasOpenPaths = false;
+#ifdef use_xyz
+ m_ZFill = 0;
+#endif
+}
+//------------------------------------------------------------------------------
+
+#ifdef use_xyz
+void Clipper::ZFillFunction(ZFillCallback zFillFunc) { m_ZFill = zFillFunc; }
+//------------------------------------------------------------------------------
+#endif
+
+bool Clipper::Execute(ClipType clipType, Paths &solution,
+ PolyFillType fillType) {
+ return Execute(clipType, solution, fillType, fillType);
+}
+//------------------------------------------------------------------------------
+
+bool Clipper::Execute(ClipType clipType, PolyTree &polytree,
+ PolyFillType fillType) {
+ return Execute(clipType, polytree, fillType, fillType);
+}
+//------------------------------------------------------------------------------
+
+bool Clipper::Execute(ClipType clipType, Paths &solution,
+ PolyFillType subjFillType, PolyFillType clipFillType) {
+ if (m_ExecuteLocked)
+ return false;
+ if (m_HasOpenPaths)
+ throw clipperException(
+ "Error: PolyTree struct is needed for open path clipping.");
+ m_ExecuteLocked = true;
+ solution.resize(0);
+ m_SubjFillType = subjFillType;
+ m_ClipFillType = clipFillType;
+ m_ClipType = clipType;
+ m_UsingPolyTree = false;
+ bool succeeded = ExecuteInternal();
+ if (succeeded)
+ BuildResult(solution);
+ DisposeAllOutRecs();
+ m_ExecuteLocked = false;
+ return succeeded;
+}
+//------------------------------------------------------------------------------
+
+bool Clipper::Execute(ClipType clipType, PolyTree &polytree,
+ PolyFillType subjFillType, PolyFillType clipFillType) {
+ if (m_ExecuteLocked)
+ return false;
+ m_ExecuteLocked = true;
+ m_SubjFillType = subjFillType;
+ m_ClipFillType = clipFillType;
+ m_ClipType = clipType;
+ m_UsingPolyTree = true;
+ bool succeeded = ExecuteInternal();
+ if (succeeded)
+ BuildResult2(polytree);
+ DisposeAllOutRecs();
+ m_ExecuteLocked = false;
+ return succeeded;
+}
+//------------------------------------------------------------------------------
+
+void Clipper::FixHoleLinkage(OutRec &outrec) {
+ // skip OutRecs that (a) contain outermost polygons or
+ //(b) already have the correct owner/child linkage ...
+ if (!outrec.FirstLeft ||
+ (outrec.IsHole != outrec.FirstLeft->IsHole && outrec.FirstLeft->Pts))
+ return;
+
+ OutRec *orfl = outrec.FirstLeft;
+ while (orfl && ((orfl->IsHole == outrec.IsHole) || !orfl->Pts))
+ orfl = orfl->FirstLeft;
+ outrec.FirstLeft = orfl;
+}
+//------------------------------------------------------------------------------
+
+bool Clipper::ExecuteInternal() {
+ bool succeeded = true;
+ try {
+ Reset();
+ m_Maxima = MaximaList();
+ m_SortedEdges = 0;
+
+ succeeded = true;
+ cInt botY, topY;
+ if (!PopScanbeam(botY))
+ return false;
+ InsertLocalMinimaIntoAEL(botY);
+ while (PopScanbeam(topY) || LocalMinimaPending()) {
+ ProcessHorizontals();
+ ClearGhostJoins();
+ if (!ProcessIntersections(topY)) {
+ succeeded = false;
+ break;
+ }
+ ProcessEdgesAtTopOfScanbeam(topY);
+ botY = topY;
+ InsertLocalMinimaIntoAEL(botY);
+ }
+ } catch (...) {
+ succeeded = false;
+ }
+
+ if (succeeded) {
+ // fix orientations ...
+ for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) {
+ OutRec *outRec = m_PolyOuts[i];
+ if (!outRec->Pts || outRec->IsOpen)
+ continue;
+ if ((outRec->IsHole ^ m_ReverseOutput) == (Area(*outRec) > 0))
+ ReversePolyPtLinks(outRec->Pts);
+ }
+
+ if (!m_Joins.empty())
+ JoinCommonEdges();
+
+ // unfortunately FixupOutPolygon() must be done after JoinCommonEdges()
+ for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) {
+ OutRec *outRec = m_PolyOuts[i];
+ if (!outRec->Pts)
+ continue;
+ if (outRec->IsOpen)
+ FixupOutPolyline(*outRec);
+ else
+ FixupOutPolygon(*outRec);
+ }
+
+ if (m_StrictSimple)
+ DoSimplePolygons();
+ }
+
+ ClearJoins();
+ ClearGhostJoins();
+ return succeeded;
+}
+//------------------------------------------------------------------------------
+
+void Clipper::SetWindingCount(TEdge &edge) {
+ TEdge *e = edge.PrevInAEL;
+ // find the edge of the same polytype that immediately preceeds 'edge' in AEL
+ while (e && ((e->PolyTyp != edge.PolyTyp) || (e->WindDelta == 0)))
+ e = e->PrevInAEL;
+ if (!e) {
+ if (edge.WindDelta == 0) {
+ PolyFillType pft =
+ (edge.PolyTyp == ptSubject ? m_SubjFillType : m_ClipFillType);
+ edge.WindCnt = (pft == pftNegative ? -1 : 1);
+ } else
+ edge.WindCnt = edge.WindDelta;
+ edge.WindCnt2 = 0;
+ e = m_ActiveEdges; // ie get ready to calc WindCnt2
+ } else if (edge.WindDelta == 0 && m_ClipType != ctUnion) {
+ edge.WindCnt = 1;
+ edge.WindCnt2 = e->WindCnt2;
+ e = e->NextInAEL; // ie get ready to calc WindCnt2
+ } else if (IsEvenOddFillType(edge)) {
+ // EvenOdd filling ...
+ if (edge.WindDelta == 0) {
+ // are we inside a subj polygon ...
+ bool Inside = true;
+ TEdge *e2 = e->PrevInAEL;
+ while (e2) {
+ if (e2->PolyTyp == e->PolyTyp && e2->WindDelta != 0)
+ Inside = !Inside;
+ e2 = e2->PrevInAEL;
+ }
+ edge.WindCnt = (Inside ? 0 : 1);
+ } else {
+ edge.WindCnt = edge.WindDelta;
+ }
+ edge.WindCnt2 = e->WindCnt2;
+ e = e->NextInAEL; // ie get ready to calc WindCnt2
+ } else {
+ // nonZero, Positive or Negative filling ...
+ if (e->WindCnt * e->WindDelta < 0) {
+ // prev edge is 'decreasing' WindCount (WC) toward zero
+ // so we're outside the previous polygon ...
+ if (Abs(e->WindCnt) > 1) {
+ // outside prev poly but still inside another.
+ // when reversing direction of prev poly use the same WC
+ if (e->WindDelta * edge.WindDelta < 0)
+ edge.WindCnt = e->WindCnt;
+ // otherwise continue to 'decrease' WC ...
+ else
+ edge.WindCnt = e->WindCnt + edge.WindDelta;
+ } else
+ // now outside all polys of same polytype so set own WC ...
+ edge.WindCnt = (edge.WindDelta == 0 ? 1 : edge.WindDelta);
+ } else {
+ // prev edge is 'increasing' WindCount (WC) away from zero
+ // so we're inside the previous polygon ...
+ if (edge.WindDelta == 0)
+ edge.WindCnt = (e->WindCnt < 0 ? e->WindCnt - 1 : e->WindCnt + 1);
+ // if wind direction is reversing prev then use same WC
+ else if (e->WindDelta * edge.WindDelta < 0)
+ edge.WindCnt = e->WindCnt;
+ // otherwise add to WC ...
+ else
+ edge.WindCnt = e->WindCnt + edge.WindDelta;
+ }
+ edge.WindCnt2 = e->WindCnt2;
+ e = e->NextInAEL; // ie get ready to calc WindCnt2
+ }
+
+ // update WindCnt2 ...
+ if (IsEvenOddAltFillType(edge)) {
+ // EvenOdd filling ...
+ while (e != &edge) {
+ if (e->WindDelta != 0)
+ edge.WindCnt2 = (edge.WindCnt2 == 0 ? 1 : 0);
+ e = e->NextInAEL;
+ }
+ } else {
+ // nonZero, Positive or Negative filling ...
+ while (e != &edge) {
+ edge.WindCnt2 += e->WindDelta;
+ e = e->NextInAEL;
+ }
+ }
+}
+//------------------------------------------------------------------------------
+
+bool Clipper::IsEvenOddFillType(const TEdge &edge) const {
+ if (edge.PolyTyp == ptSubject)
+ return m_SubjFillType == pftEvenOdd;
+ else
+ return m_ClipFillType == pftEvenOdd;
+}
+//------------------------------------------------------------------------------
+
+bool Clipper::IsEvenOddAltFillType(const TEdge &edge) const {
+ if (edge.PolyTyp == ptSubject)
+ return m_ClipFillType == pftEvenOdd;
+ else
+ return m_SubjFillType == pftEvenOdd;
+}
+//------------------------------------------------------------------------------
+
+bool Clipper::IsContributing(const TEdge &edge) const {
+ PolyFillType pft, pft2;
+ if (edge.PolyTyp == ptSubject) {
+ pft = m_SubjFillType;
+ pft2 = m_ClipFillType;
+ } else {
+ pft = m_ClipFillType;
+ pft2 = m_SubjFillType;
+ }
+
+ switch (pft) {
+ case pftEvenOdd:
+ // return false if a subj line has been flagged as inside a subj polygon
+ if (edge.WindDelta == 0 && edge.WindCnt != 1)
+ return false;
+ break;
+ case pftNonZero:
+ if (Abs(edge.WindCnt) != 1)
+ return false;
+ break;
+ case pftPositive:
+ if (edge.WindCnt != 1)
+ return false;
+ break;
+ default: // pftNegative
+ if (edge.WindCnt != -1)
+ return false;
+ }
+
+ switch (m_ClipType) {
+ case ctIntersection:
+ switch (pft2) {
+ case pftEvenOdd:
+ case pftNonZero:
+ return (edge.WindCnt2 != 0);
+ case pftPositive:
+ return (edge.WindCnt2 > 0);
+ default:
+ return (edge.WindCnt2 < 0);
+ }
+ break;
+ case ctUnion:
+ switch (pft2) {
+ case pftEvenOdd:
+ case pftNonZero:
+ return (edge.WindCnt2 == 0);
+ case pftPositive:
+ return (edge.WindCnt2 <= 0);
+ default:
+ return (edge.WindCnt2 >= 0);
+ }
+ break;
+ case ctDifference:
+ if (edge.PolyTyp == ptSubject)
+ switch (pft2) {
+ case pftEvenOdd:
+ case pftNonZero:
+ return (edge.WindCnt2 == 0);
+ case pftPositive:
+ return (edge.WindCnt2 <= 0);
+ default:
+ return (edge.WindCnt2 >= 0);
+ }
+ else
+ switch (pft2) {
+ case pftEvenOdd:
+ case pftNonZero:
+ return (edge.WindCnt2 != 0);
+ case pftPositive:
+ return (edge.WindCnt2 > 0);
+ default:
+ return (edge.WindCnt2 < 0);
+ }
+ break;
+ case ctXor:
+ if (edge.WindDelta == 0) // XOr always contributing unless open
+ switch (pft2) {
+ case pftEvenOdd:
+ case pftNonZero:
+ return (edge.WindCnt2 == 0);
+ case pftPositive:
+ return (edge.WindCnt2 <= 0);
+ default:
+ return (edge.WindCnt2 >= 0);
+ }
+ else
+ return true;
+ break;
+ default:
+ return true;
+ }
+}
+//------------------------------------------------------------------------------
+
+OutPt *Clipper::AddLocalMinPoly(TEdge *e1, TEdge *e2, const IntPoint &Pt) {
+ OutPt *result;
+ TEdge *e, *prevE;
+ if (IsHorizontal(*e2) || (e1->Dx > e2->Dx)) {
+ result = AddOutPt(e1, Pt);
+ e2->OutIdx = e1->OutIdx;
+ e1->Side = esLeft;
+ e2->Side = esRight;
+ e = e1;
+ if (e->PrevInAEL == e2)
+ prevE = e2->PrevInAEL;
+ else
+ prevE = e->PrevInAEL;
+ } else {
+ result = AddOutPt(e2, Pt);
+ e1->OutIdx = e2->OutIdx;
+ e1->Side = esRight;
+ e2->Side = esLeft;
+ e = e2;
+ if (e->PrevInAEL == e1)
+ prevE = e1->PrevInAEL;
+ else
+ prevE = e->PrevInAEL;
+ }
+
+ if (prevE && prevE->OutIdx >= 0 && prevE->Top.Y < Pt.Y && e->Top.Y < Pt.Y) {
+ cInt xPrev = TopX(*prevE, Pt.Y);
+ cInt xE = TopX(*e, Pt.Y);
+ if (xPrev == xE && (e->WindDelta != 0) && (prevE->WindDelta != 0) &&
+ SlopesEqual(IntPoint(xPrev, Pt.Y), prevE->Top, IntPoint(xE, Pt.Y),
+ e->Top, m_UseFullRange)) {
+ OutPt *outPt = AddOutPt(prevE, Pt);
+ AddJoin(result, outPt, e->Top);
+ }
+ }
+ return result;
+}
+//------------------------------------------------------------------------------
+
+void Clipper::AddLocalMaxPoly(TEdge *e1, TEdge *e2, const IntPoint &Pt) {
+ AddOutPt(e1, Pt);
+ if (e2->WindDelta == 0)
+ AddOutPt(e2, Pt);
+ if (e1->OutIdx == e2->OutIdx) {
+ e1->OutIdx = Unassigned;
+ e2->OutIdx = Unassigned;
+ } else if (e1->OutIdx < e2->OutIdx)
+ AppendPolygon(e1, e2);
+ else
+ AppendPolygon(e2, e1);
+}
+//------------------------------------------------------------------------------
+
+void Clipper::AddEdgeToSEL(TEdge *edge) {
+ // SEL pointers in PEdge are reused to build a list of horizontal edges.
+ // However, we don't need to worry about order with horizontal edge
+ // processing.
+ if (!m_SortedEdges) {
+ m_SortedEdges = edge;
+ edge->PrevInSEL = 0;
+ edge->NextInSEL = 0;
+ } else {
+ edge->NextInSEL = m_SortedEdges;
+ edge->PrevInSEL = 0;
+ m_SortedEdges->PrevInSEL = edge;
+ m_SortedEdges = edge;
+ }
+}
+//------------------------------------------------------------------------------
+
+bool Clipper::PopEdgeFromSEL(TEdge *&edge) {
+ if (!m_SortedEdges)
+ return false;
+ edge = m_SortedEdges;
+ DeleteFromSEL(m_SortedEdges);
+ return true;
+}
+//------------------------------------------------------------------------------
+
+void Clipper::CopyAELToSEL() {
+ TEdge *e = m_ActiveEdges;
+ m_SortedEdges = e;
+ while (e) {
+ e->PrevInSEL = e->PrevInAEL;
+ e->NextInSEL = e->NextInAEL;
+ e = e->NextInAEL;
+ }
+}
+//------------------------------------------------------------------------------
+
+void Clipper::AddJoin(OutPt *op1, OutPt *op2, const IntPoint OffPt) {
+ Join *j = new Join;
+ j->OutPt1 = op1;
+ j->OutPt2 = op2;
+ j->OffPt = OffPt;
+ m_Joins.push_back(j);
+}
+//------------------------------------------------------------------------------
+
+void Clipper::ClearJoins() {
+ for (JoinList::size_type i = 0; i < m_Joins.size(); i++)
+ delete m_Joins[i];
+ m_Joins.resize(0);
+}
+//------------------------------------------------------------------------------
+
+void Clipper::ClearGhostJoins() {
+ for (JoinList::size_type i = 0; i < m_GhostJoins.size(); i++)
+ delete m_GhostJoins[i];
+ m_GhostJoins.resize(0);
+}
+//------------------------------------------------------------------------------
+
+void Clipper::AddGhostJoin(OutPt *op, const IntPoint OffPt) {
+ Join *j = new Join;
+ j->OutPt1 = op;
+ j->OutPt2 = 0;
+ j->OffPt = OffPt;
+ m_GhostJoins.push_back(j);
+}
+//------------------------------------------------------------------------------
+
+void Clipper::InsertLocalMinimaIntoAEL(const cInt botY) {
+ const LocalMinimum *lm;
+ while (PopLocalMinima(botY, lm)) {
+ TEdge *lb = lm->LeftBound;
+ TEdge *rb = lm->RightBound;
+
+ OutPt *Op1 = 0;
+ if (!lb) {
+ // nb: don't insert LB into either AEL or SEL
+ InsertEdgeIntoAEL(rb, 0);
+ SetWindingCount(*rb);
+ if (IsContributing(*rb))
+ Op1 = AddOutPt(rb, rb->Bot);
+ } else if (!rb) {
+ InsertEdgeIntoAEL(lb, 0);
+ SetWindingCount(*lb);
+ if (IsContributing(*lb))
+ Op1 = AddOutPt(lb, lb->Bot);
+ InsertScanbeam(lb->Top.Y);
+ } else {
+ InsertEdgeIntoAEL(lb, 0);
+ InsertEdgeIntoAEL(rb, lb);
+ SetWindingCount(*lb);
+ rb->WindCnt = lb->WindCnt;
+ rb->WindCnt2 = lb->WindCnt2;
+ if (IsContributing(*lb))
+ Op1 = AddLocalMinPoly(lb, rb, lb->Bot);
+ InsertScanbeam(lb->Top.Y);
+ }
+
+ if (rb) {
+ if (IsHorizontal(*rb)) {
+ AddEdgeToSEL(rb);
+ if (rb->NextInLML)
+ InsertScanbeam(rb->NextInLML->Top.Y);
+ } else
+ InsertScanbeam(rb->Top.Y);
+ }
+
+ if (!lb || !rb)
+ continue;
+
+ // if any output polygons share an edge, they'll need joining later ...
+ if (Op1 && IsHorizontal(*rb) && m_GhostJoins.size() > 0 &&
+ (rb->WindDelta != 0)) {
+ for (JoinList::size_type i = 0; i < m_GhostJoins.size(); ++i) {
+ Join *jr = m_GhostJoins[i];
+ // if the horizontal Rb and a 'ghost' horizontal overlap, then convert
+ // the 'ghost' join to a real join ready for later ...
+ if (HorzSegmentsOverlap(jr->OutPt1->Pt.X, jr->OffPt.X, rb->Bot.X,
+ rb->Top.X))
+ AddJoin(jr->OutPt1, Op1, jr->OffPt);
+ }
+ }
+
+ if (lb->OutIdx >= 0 && lb->PrevInAEL &&
+ lb->PrevInAEL->Curr.X == lb->Bot.X && lb->PrevInAEL->OutIdx >= 0 &&
+ SlopesEqual(lb->PrevInAEL->Bot, lb->PrevInAEL->Top, lb->Curr, lb->Top,
+ m_UseFullRange) &&
+ (lb->WindDelta != 0) && (lb->PrevInAEL->WindDelta != 0)) {
+ OutPt *Op2 = AddOutPt(lb->PrevInAEL, lb->Bot);
+ AddJoin(Op1, Op2, lb->Top);
+ }
+
+ if (lb->NextInAEL != rb) {
+
+ if (rb->OutIdx >= 0 && rb->PrevInAEL->OutIdx >= 0 &&
+ SlopesEqual(rb->PrevInAEL->Curr, rb->PrevInAEL->Top, rb->Curr,
+ rb->Top, m_UseFullRange) &&
+ (rb->WindDelta != 0) && (rb->PrevInAEL->WindDelta != 0)) {
+ OutPt *Op2 = AddOutPt(rb->PrevInAEL, rb->Bot);
+ AddJoin(Op1, Op2, rb->Top);
+ }
+
+ TEdge *e = lb->NextInAEL;
+ if (e) {
+ while (e != rb) {
+ // nb: For calculating winding counts etc, IntersectEdges() assumes
+ // that param1 will be to the Right of param2 ABOVE the intersection
+ // ...
+ IntersectEdges(rb, e, lb->Curr); // order important here
+ e = e->NextInAEL;
+ }
+ }
+ }
+ }
+}
+//------------------------------------------------------------------------------
+
+void Clipper::DeleteFromSEL(TEdge *e) {
+ TEdge *SelPrev = e->PrevInSEL;
+ TEdge *SelNext = e->NextInSEL;
+ if (!SelPrev && !SelNext && (e != m_SortedEdges))
+ return; // already deleted
+ if (SelPrev)
+ SelPrev->NextInSEL = SelNext;
+ else
+ m_SortedEdges = SelNext;
+ if (SelNext)
+ SelNext->PrevInSEL = SelPrev;
+ e->NextInSEL = 0;
+ e->PrevInSEL = 0;
+}
+//------------------------------------------------------------------------------
+
+#ifdef use_xyz
+void Clipper::SetZ(IntPoint &pt, TEdge &e1, TEdge &e2) {
+ if (pt.Z != 0 || !m_ZFill)
+ return;
+ else if (pt == e1.Bot)
+ pt.Z = e1.Bot.Z;
+ else if (pt == e1.Top)
+ pt.Z = e1.Top.Z;
+ else if (pt == e2.Bot)
+ pt.Z = e2.Bot.Z;
+ else if (pt == e2.Top)
+ pt.Z = e2.Top.Z;
+ else
+ (*m_ZFill)(e1.Bot, e1.Top, e2.Bot, e2.Top, pt);
+}
+//------------------------------------------------------------------------------
+#endif
+
+void Clipper::IntersectEdges(TEdge *e1, TEdge *e2, IntPoint &Pt) {
+ bool e1Contributing = (e1->OutIdx >= 0);
+ bool e2Contributing = (e2->OutIdx >= 0);
+
+#ifdef use_xyz
+ SetZ(Pt, *e1, *e2);
+#endif
+
+#ifdef use_lines
+ // if either edge is on an OPEN path ...
+ if (e1->WindDelta == 0 || e2->WindDelta == 0) {
+ // ignore subject-subject open path intersections UNLESS they
+ // are both open paths, AND they are both 'contributing maximas' ...
+ if (e1->WindDelta == 0 && e2->WindDelta == 0)
+ return;
+
+ // if intersecting a subj line with a subj poly ...
+ else if (e1->PolyTyp == e2->PolyTyp && e1->WindDelta != e2->WindDelta &&
+ m_ClipType == ctUnion) {
+ if (e1->WindDelta == 0) {
+ if (e2Contributing) {
+ AddOutPt(e1, Pt);
+ if (e1Contributing)
+ e1->OutIdx = Unassigned;
+ }
+ } else {
+ if (e1Contributing) {
+ AddOutPt(e2, Pt);
+ if (e2Contributing)
+ e2->OutIdx = Unassigned;
+ }
+ }
+ } else if (e1->PolyTyp != e2->PolyTyp) {
+ // toggle subj open path OutIdx on/off when Abs(clip.WndCnt) == 1 ...
+ if ((e1->WindDelta == 0) && abs(e2->WindCnt) == 1 &&
+ (m_ClipType != ctUnion || e2->WindCnt2 == 0)) {
+ AddOutPt(e1, Pt);
+ if (e1Contributing)
+ e1->OutIdx = Unassigned;
+ } else if ((e2->WindDelta == 0) && (abs(e1->WindCnt) == 1) &&
+ (m_ClipType != ctUnion || e1->WindCnt2 == 0)) {
+ AddOutPt(e2, Pt);
+ if (e2Contributing)
+ e2->OutIdx = Unassigned;
+ }
+ }
+ return;
+ }
+#endif
+
+ // update winding counts...
+ // assumes that e1 will be to the Right of e2 ABOVE the intersection
+ if (e1->PolyTyp == e2->PolyTyp) {
+ if (IsEvenOddFillType(*e1)) {
+ int oldE1WindCnt = e1->WindCnt;
+ e1->WindCnt = e2->WindCnt;
+ e2->WindCnt = oldE1WindCnt;
+ } else {
+ if (e1->WindCnt + e2->WindDelta == 0)
+ e1->WindCnt = -e1->WindCnt;
+ else
+ e1->WindCnt += e2->WindDelta;
+ if (e2->WindCnt - e1->WindDelta == 0)
+ e2->WindCnt = -e2->WindCnt;
+ else
+ e2->WindCnt -= e1->WindDelta;
+ }
+ } else {
+ if (!IsEvenOddFillType(*e2))
+ e1->WindCnt2 += e2->WindDelta;
+ else
+ e1->WindCnt2 = (e1->WindCnt2 == 0) ? 1 : 0;
+ if (!IsEvenOddFillType(*e1))
+ e2->WindCnt2 -= e1->WindDelta;
+ else
+ e2->WindCnt2 = (e2->WindCnt2 == 0) ? 1 : 0;
+ }
+
+ PolyFillType e1FillType, e2FillType, e1FillType2, e2FillType2;
+ if (e1->PolyTyp == ptSubject) {
+ e1FillType = m_SubjFillType;
+ e1FillType2 = m_ClipFillType;
+ } else {
+ e1FillType = m_ClipFillType;
+ e1FillType2 = m_SubjFillType;
+ }
+ if (e2->PolyTyp == ptSubject) {
+ e2FillType = m_SubjFillType;
+ e2FillType2 = m_ClipFillType;
+ } else {
+ e2FillType = m_ClipFillType;
+ e2FillType2 = m_SubjFillType;
+ }
+
+ cInt e1Wc, e2Wc;
+ switch (e1FillType) {
+ case pftPositive:
+ e1Wc = e1->WindCnt;
+ break;
+ case pftNegative:
+ e1Wc = -e1->WindCnt;
+ break;
+ default:
+ e1Wc = Abs(e1->WindCnt);
+ }
+ switch (e2FillType) {
+ case pftPositive:
+ e2Wc = e2->WindCnt;
+ break;
+ case pftNegative:
+ e2Wc = -e2->WindCnt;
+ break;
+ default:
+ e2Wc = Abs(e2->WindCnt);
+ }
+
+ if (e1Contributing && e2Contributing) {
+ if ((e1Wc != 0 && e1Wc != 1) || (e2Wc != 0 && e2Wc != 1) ||
+ (e1->PolyTyp != e2->PolyTyp && m_ClipType != ctXor)) {
+ AddLocalMaxPoly(e1, e2, Pt);
+ } else {
+ AddOutPt(e1, Pt);
+ AddOutPt(e2, Pt);
+ SwapSides(*e1, *e2);
+ SwapPolyIndexes(*e1, *e2);
+ }
+ } else if (e1Contributing) {
+ if (e2Wc == 0 || e2Wc == 1) {
+ AddOutPt(e1, Pt);
+ SwapSides(*e1, *e2);
+ SwapPolyIndexes(*e1, *e2);
+ }
+ } else if (e2Contributing) {
+ if (e1Wc == 0 || e1Wc == 1) {
+ AddOutPt(e2, Pt);
+ SwapSides(*e1, *e2);
+ SwapPolyIndexes(*e1, *e2);
+ }
+ } else if ((e1Wc == 0 || e1Wc == 1) && (e2Wc == 0 || e2Wc == 1)) {
+ // neither edge is currently contributing ...
+
+ cInt e1Wc2, e2Wc2;
+ switch (e1FillType2) {
+ case pftPositive:
+ e1Wc2 = e1->WindCnt2;
+ break;
+ case pftNegative:
+ e1Wc2 = -e1->WindCnt2;
+ break;
+ default:
+ e1Wc2 = Abs(e1->WindCnt2);
+ }
+ switch (e2FillType2) {
+ case pftPositive:
+ e2Wc2 = e2->WindCnt2;
+ break;
+ case pftNegative:
+ e2Wc2 = -e2->WindCnt2;
+ break;
+ default:
+ e2Wc2 = Abs(e2->WindCnt2);
+ }
+
+ if (e1->PolyTyp != e2->PolyTyp) {
+ AddLocalMinPoly(e1, e2, Pt);
+ } else if (e1Wc == 1 && e2Wc == 1)
+ switch (m_ClipType) {
+ case ctIntersection:
+ if (e1Wc2 > 0 && e2Wc2 > 0)
+ AddLocalMinPoly(e1, e2, Pt);
+ break;
+ case ctUnion:
+ if (e1Wc2 <= 0 && e2Wc2 <= 0)
+ AddLocalMinPoly(e1, e2, Pt);
+ break;
+ case ctDifference:
+ if (((e1->PolyTyp == ptClip) && (e1Wc2 > 0) && (e2Wc2 > 0)) ||
+ ((e1->PolyTyp == ptSubject) && (e1Wc2 <= 0) && (e2Wc2 <= 0)))
+ AddLocalMinPoly(e1, e2, Pt);
+ break;
+ case ctXor:
+ AddLocalMinPoly(e1, e2, Pt);
+ }
+ else
+ SwapSides(*e1, *e2);
+ }
+}
+//------------------------------------------------------------------------------
+
+void Clipper::SetHoleState(TEdge *e, OutRec *outrec) {
+ TEdge *e2 = e->PrevInAEL;
+ TEdge *eTmp = 0;
+ while (e2) {
+ if (e2->OutIdx >= 0 && e2->WindDelta != 0) {
+ if (!eTmp)
+ eTmp = e2;
+ else if (eTmp->OutIdx == e2->OutIdx)
+ eTmp = 0;
+ }
+ e2 = e2->PrevInAEL;
+ }
+ if (!eTmp) {
+ outrec->FirstLeft = 0;
+ outrec->IsHole = false;
+ } else {
+ outrec->FirstLeft = m_PolyOuts[eTmp->OutIdx];
+ outrec->IsHole = !outrec->FirstLeft->IsHole;
+ }
+}
+//------------------------------------------------------------------------------
+
+OutRec *GetLowermostRec(OutRec *outRec1, OutRec *outRec2) {
+ // work out which polygon fragment has the correct hole state ...
+ if (!outRec1->BottomPt)
+ outRec1->BottomPt = GetBottomPt(outRec1->Pts);
+ if (!outRec2->BottomPt)
+ outRec2->BottomPt = GetBottomPt(outRec2->Pts);
+ OutPt *OutPt1 = outRec1->BottomPt;
+ OutPt *OutPt2 = outRec2->BottomPt;
+ if (OutPt1->Pt.Y > OutPt2->Pt.Y)
+ return outRec1;
+ else if (OutPt1->Pt.Y < OutPt2->Pt.Y)
+ return outRec2;
+ else if (OutPt1->Pt.X < OutPt2->Pt.X)
+ return outRec1;
+ else if (OutPt1->Pt.X > OutPt2->Pt.X)
+ return outRec2;
+ else if (OutPt1->Next == OutPt1)
+ return outRec2;
+ else if (OutPt2->Next == OutPt2)
+ return outRec1;
+ else if (FirstIsBottomPt(OutPt1, OutPt2))
+ return outRec1;
+ else
+ return outRec2;
+}
+//------------------------------------------------------------------------------
+
+bool OutRec1RightOfOutRec2(OutRec *outRec1, OutRec *outRec2) {
+ do {
+ outRec1 = outRec1->FirstLeft;
+ if (outRec1 == outRec2)
+ return true;
+ } while (outRec1);
+ return false;
+}
+//------------------------------------------------------------------------------
+
+OutRec *Clipper::GetOutRec(int Idx) {
+ OutRec *outrec = m_PolyOuts[Idx];
+ while (outrec != m_PolyOuts[outrec->Idx])
+ outrec = m_PolyOuts[outrec->Idx];
+ return outrec;
+}
+//------------------------------------------------------------------------------
+
+void Clipper::AppendPolygon(TEdge *e1, TEdge *e2) {
+ // get the start and ends of both output polygons ...
+ OutRec *outRec1 = m_PolyOuts[e1->OutIdx];
+ OutRec *outRec2 = m_PolyOuts[e2->OutIdx];
+
+ OutRec *holeStateRec;
+ if (OutRec1RightOfOutRec2(outRec1, outRec2))
+ holeStateRec = outRec2;
+ else if (OutRec1RightOfOutRec2(outRec2, outRec1))
+ holeStateRec = outRec1;
+ else
+ holeStateRec = GetLowermostRec(outRec1, outRec2);
+
+ // get the start and ends of both output polygons and
+ // join e2 poly onto e1 poly and delete pointers to e2 ...
+
+ OutPt *p1_lft = outRec1->Pts;
+ OutPt *p1_rt = p1_lft->Prev;
+ OutPt *p2_lft = outRec2->Pts;
+ OutPt *p2_rt = p2_lft->Prev;
+
+ // join e2 poly onto e1 poly and delete pointers to e2 ...
+ if (e1->Side == esLeft) {
+ if (e2->Side == esLeft) {
+ // z y x a b c
+ ReversePolyPtLinks(p2_lft);
+ p2_lft->Next = p1_lft;
+ p1_lft->Prev = p2_lft;
+ p1_rt->Next = p2_rt;
+ p2_rt->Prev = p1_rt;
+ outRec1->Pts = p2_rt;
+ } else {
+ // x y z a b c
+ p2_rt->Next = p1_lft;
+ p1_lft->Prev = p2_rt;
+ p2_lft->Prev = p1_rt;
+ p1_rt->Next = p2_lft;
+ outRec1->Pts = p2_lft;
+ }
+ } else {
+ if (e2->Side == esRight) {
+ // a b c z y x
+ ReversePolyPtLinks(p2_lft);
+ p1_rt->Next = p2_rt;
+ p2_rt->Prev = p1_rt;
+ p2_lft->Next = p1_lft;
+ p1_lft->Prev = p2_lft;
+ } else {
+ // a b c x y z
+ p1_rt->Next = p2_lft;
+ p2_lft->Prev = p1_rt;
+ p1_lft->Prev = p2_rt;
+ p2_rt->Next = p1_lft;
+ }
+ }
+
+ outRec1->BottomPt = 0;
+ if (holeStateRec == outRec2) {
+ if (outRec2->FirstLeft != outRec1)
+ outRec1->FirstLeft = outRec2->FirstLeft;
+ outRec1->IsHole = outRec2->IsHole;
+ }
+ outRec2->Pts = 0;
+ outRec2->BottomPt = 0;
+ outRec2->FirstLeft = outRec1;
+
+ int OKIdx = e1->OutIdx;
+ int ObsoleteIdx = e2->OutIdx;
+
+ e1->OutIdx =
+ Unassigned; // nb: safe because we only get here via AddLocalMaxPoly
+ e2->OutIdx = Unassigned;
+
+ TEdge *e = m_ActiveEdges;
+ while (e) {
+ if (e->OutIdx == ObsoleteIdx) {
+ e->OutIdx = OKIdx;
+ e->Side = e1->Side;
+ break;
+ }
+ e = e->NextInAEL;
+ }
+
+ outRec2->Idx = outRec1->Idx;
+}
+//------------------------------------------------------------------------------
+
+OutPt *Clipper::AddOutPt(TEdge *e, const IntPoint &pt) {
+ if (e->OutIdx < 0) {
+ OutRec *outRec = CreateOutRec();
+ outRec->IsOpen = (e->WindDelta == 0);
+ OutPt *newOp = new OutPt;
+ outRec->Pts = newOp;
+ newOp->Idx = outRec->Idx;
+ newOp->Pt = pt;
+ newOp->Next = newOp;
+ newOp->Prev = newOp;
+ if (!outRec->IsOpen)
+ SetHoleState(e, outRec);
+ e->OutIdx = outRec->Idx;
+ return newOp;
+ } else {
+ OutRec *outRec = m_PolyOuts[e->OutIdx];
+ // OutRec.Pts is the 'Left-most' point & OutRec.Pts.Prev is the 'Right-most'
+ OutPt *op = outRec->Pts;
+
+ bool ToFront = (e->Side == esLeft);
+ if (ToFront && (pt == op->Pt))
+ return op;
+ else if (!ToFront && (pt == op->Prev->Pt))
+ return op->Prev;
+
+ OutPt *newOp = new OutPt;
+ newOp->Idx = outRec->Idx;
+ newOp->Pt = pt;
+ newOp->Next = op;
+ newOp->Prev = op->Prev;
+ newOp->Prev->Next = newOp;
+ op->Prev = newOp;
+ if (ToFront)
+ outRec->Pts = newOp;
+ return newOp;
+ }
+}
+//------------------------------------------------------------------------------
+
+OutPt *Clipper::GetLastOutPt(TEdge *e) {
+ OutRec *outRec = m_PolyOuts[e->OutIdx];
+ if (e->Side == esLeft)
+ return outRec->Pts;
+ else
+ return outRec->Pts->Prev;
+}
+//------------------------------------------------------------------------------
+
+void Clipper::ProcessHorizontals() {
+ TEdge *horzEdge;
+ while (PopEdgeFromSEL(horzEdge))
+ ProcessHorizontal(horzEdge);
+}
+//------------------------------------------------------------------------------
+
+inline bool IsMinima(TEdge *e) {
+ return e && (e->Prev->NextInLML != e) && (e->Next->NextInLML != e);
+}
+//------------------------------------------------------------------------------
+
+inline bool IsMaxima(TEdge *e, const cInt Y) {
+ return e && e->Top.Y == Y && !e->NextInLML;
+}
+//------------------------------------------------------------------------------
+
+inline bool IsIntermediate(TEdge *e, const cInt Y) {
+ return e->Top.Y == Y && e->NextInLML;
+}
+//------------------------------------------------------------------------------
+
+TEdge *GetMaximaPair(TEdge *e) {
+ if ((e->Next->Top == e->Top) && !e->Next->NextInLML)
+ return e->Next;
+ else if ((e->Prev->Top == e->Top) && !e->Prev->NextInLML)
+ return e->Prev;
+ else
+ return 0;
+}
+//------------------------------------------------------------------------------
+
+TEdge *GetMaximaPairEx(TEdge *e) {
+ // as GetMaximaPair() but returns 0 if MaxPair isn't in AEL (unless it's
+ // horizontal)
+ TEdge *result = GetMaximaPair(e);
+ if (result &&
+ (result->OutIdx == Skip ||
+ (result->NextInAEL == result->PrevInAEL && !IsHorizontal(*result))))
+ return 0;
+ return result;
+}
+//------------------------------------------------------------------------------
+
+void Clipper::SwapPositionsInSEL(TEdge *Edge1, TEdge *Edge2) {
+ if (!(Edge1->NextInSEL) && !(Edge1->PrevInSEL))
+ return;
+ if (!(Edge2->NextInSEL) && !(Edge2->PrevInSEL))
+ return;
+
+ if (Edge1->NextInSEL == Edge2) {
+ TEdge *Next = Edge2->NextInSEL;
+ if (Next)
+ Next->PrevInSEL = Edge1;
+ TEdge *Prev = Edge1->PrevInSEL;
+ if (Prev)
+ Prev->NextInSEL = Edge2;
+ Edge2->PrevInSEL = Prev;
+ Edge2->NextInSEL = Edge1;
+ Edge1->PrevInSEL = Edge2;
+ Edge1->NextInSEL = Next;
+ } else if (Edge2->NextInSEL == Edge1) {
+ TEdge *Next = Edge1->NextInSEL;
+ if (Next)
+ Next->PrevInSEL = Edge2;
+ TEdge *Prev = Edge2->PrevInSEL;
+ if (Prev)
+ Prev->NextInSEL = Edge1;
+ Edge1->PrevInSEL = Prev;
+ Edge1->NextInSEL = Edge2;
+ Edge2->PrevInSEL = Edge1;
+ Edge2->NextInSEL = Next;
+ } else {
+ TEdge *Next = Edge1->NextInSEL;
+ TEdge *Prev = Edge1->PrevInSEL;
+ Edge1->NextInSEL = Edge2->NextInSEL;
+ if (Edge1->NextInSEL)
+ Edge1->NextInSEL->PrevInSEL = Edge1;
+ Edge1->PrevInSEL = Edge2->PrevInSEL;
+ if (Edge1->PrevInSEL)
+ Edge1->PrevInSEL->NextInSEL = Edge1;
+ Edge2->NextInSEL = Next;
+ if (Edge2->NextInSEL)
+ Edge2->NextInSEL->PrevInSEL = Edge2;
+ Edge2->PrevInSEL = Prev;
+ if (Edge2->PrevInSEL)
+ Edge2->PrevInSEL->NextInSEL = Edge2;
+ }
+
+ if (!Edge1->PrevInSEL)
+ m_SortedEdges = Edge1;
+ else if (!Edge2->PrevInSEL)
+ m_SortedEdges = Edge2;
+}
+//------------------------------------------------------------------------------
+
+TEdge *GetNextInAEL(TEdge *e, Direction dir) {
+ return dir == dLeftToRight ? e->NextInAEL : e->PrevInAEL;
+}
+//------------------------------------------------------------------------------
+
+void GetHorzDirection(TEdge &HorzEdge, Direction &Dir, cInt &Left,
+ cInt &Right) {
+ if (HorzEdge.Bot.X < HorzEdge.Top.X) {
+ Left = HorzEdge.Bot.X;
+ Right = HorzEdge.Top.X;
+ Dir = dLeftToRight;
+ } else {
+ Left = HorzEdge.Top.X;
+ Right = HorzEdge.Bot.X;
+ Dir = dRightToLeft;
+ }
+}
+//------------------------------------------------------------------------
+
+/*******************************************************************************
+ * Notes: Horizontal edges (HEs) at scanline intersections (ie at the Top or *
+ * Bottom of a scanbeam) are processed as if layered. The order in which HEs *
+ * are processed doesn't matter. HEs intersect with other HE Bot.Xs only [#] *
+ * (or they could intersect with Top.Xs only, ie EITHER Bot.Xs OR Top.Xs), * and
+ *with other non-horizontal edges [*]. Once these intersections are *
+ * processed, intermediate HEs then 'promote' the Edge above (NextInLML) into *
+ * the AEL. These 'promoted' edges may in turn intersect [%] with other HEs. *
+ *******************************************************************************/
+
+void Clipper::ProcessHorizontal(TEdge *horzEdge) {
+ Direction dir;
+ cInt horzLeft, horzRight;
+ bool IsOpen = (horzEdge->WindDelta == 0);
+
+ GetHorzDirection(*horzEdge, dir, horzLeft, horzRight);
+
+ TEdge *eLastHorz = horzEdge, *eMaxPair = 0;
+ while (eLastHorz->NextInLML && IsHorizontal(*eLastHorz->NextInLML))
+ eLastHorz = eLastHorz->NextInLML;
+ if (!eLastHorz->NextInLML)
+ eMaxPair = GetMaximaPair(eLastHorz);
+
+ MaximaList::const_iterator maxIt;
+ MaximaList::const_reverse_iterator maxRit;
+ if (m_Maxima.size() > 0) {
+ // get the first maxima in range (X) ...
+ if (dir == dLeftToRight) {
+ maxIt = m_Maxima.begin();
+ while (maxIt != m_Maxima.end() && *maxIt <= horzEdge->Bot.X)
+ maxIt++;
+ if (maxIt != m_Maxima.end() && *maxIt >= eLastHorz->Top.X)
+ maxIt = m_Maxima.end();
+ } else {
+ maxRit = m_Maxima.rbegin();
+ while (maxRit != m_Maxima.rend() && *maxRit > horzEdge->Bot.X)
+ maxRit++;
+ if (maxRit != m_Maxima.rend() && *maxRit <= eLastHorz->Top.X)
+ maxRit = m_Maxima.rend();
+ }
+ }
+
+ OutPt *op1 = 0;
+
+ for (;;) // loop through consec. horizontal edges
+ {
+
+ bool IsLastHorz = (horzEdge == eLastHorz);
+ TEdge *e = GetNextInAEL(horzEdge, dir);
+ while (e) {
+
+ // this code block inserts extra coords into horizontal edges (in output
+ // polygons) whereever maxima touch these horizontal edges. This helps
+ //'simplifying' polygons (ie if the Simplify property is set).
+ if (m_Maxima.size() > 0) {
+ if (dir == dLeftToRight) {
+ while (maxIt != m_Maxima.end() && *maxIt < e->Curr.X) {
+ if (horzEdge->OutIdx >= 0 && !IsOpen)
+ AddOutPt(horzEdge, IntPoint(*maxIt, horzEdge->Bot.Y));
+ maxIt++;
+ }
+ } else {
+ while (maxRit != m_Maxima.rend() && *maxRit > e->Curr.X) {
+ if (horzEdge->OutIdx >= 0 && !IsOpen)
+ AddOutPt(horzEdge, IntPoint(*maxRit, horzEdge->Bot.Y));
+ maxRit++;
+ }
+ }
+ };
+
+ if ((dir == dLeftToRight && e->Curr.X > horzRight) ||
+ (dir == dRightToLeft && e->Curr.X < horzLeft))
+ break;
+
+ // Also break if we've got to the end of an intermediate horizontal edge
+ // ... nb: Smaller Dx's are to the right of larger Dx's ABOVE the
+ // horizontal.
+ if (e->Curr.X == horzEdge->Top.X && horzEdge->NextInLML &&
+ e->Dx < horzEdge->NextInLML->Dx)
+ break;
+
+ if (horzEdge->OutIdx >= 0 && !IsOpen) // note: may be done multiple times
+ {
+#ifdef use_xyz
+ if (dir == dLeftToRight)
+ SetZ(e->Curr, *horzEdge, *e);
+ else
+ SetZ(e->Curr, *e, *horzEdge);
+#endif
+ op1 = AddOutPt(horzEdge, e->Curr);
+ TEdge *eNextHorz = m_SortedEdges;
+ while (eNextHorz) {
+ if (eNextHorz->OutIdx >= 0 &&
+ HorzSegmentsOverlap(horzEdge->Bot.X, horzEdge->Top.X,
+ eNextHorz->Bot.X, eNextHorz->Top.X)) {
+ OutPt *op2 = GetLastOutPt(eNextHorz);
+ AddJoin(op2, op1, eNextHorz->Top);
+ }
+ eNextHorz = eNextHorz->NextInSEL;
+ }
+ AddGhostJoin(op1, horzEdge->Bot);
+ }
+
+ // OK, so far we're still in range of the horizontal Edge but make sure
+ // we're at the last of consec. horizontals when matching with eMaxPair
+ if (e == eMaxPair && IsLastHorz) {
+ if (horzEdge->OutIdx >= 0)
+ AddLocalMaxPoly(horzEdge, eMaxPair, horzEdge->Top);
+ DeleteFromAEL(horzEdge);
+ DeleteFromAEL(eMaxPair);
+ return;
+ }
+
+ if (dir == dLeftToRight) {
+ IntPoint Pt = IntPoint(e->Curr.X, horzEdge->Curr.Y);
+ IntersectEdges(horzEdge, e, Pt);
+ } else {
+ IntPoint Pt = IntPoint(e->Curr.X, horzEdge->Curr.Y);
+ IntersectEdges(e, horzEdge, Pt);
+ }
+ TEdge *eNext = GetNextInAEL(e, dir);
+ SwapPositionsInAEL(horzEdge, e);
+ e = eNext;
+ } // end while(e)
+
+ // Break out of loop if HorzEdge.NextInLML is not also horizontal ...
+ if (!horzEdge->NextInLML || !IsHorizontal(*horzEdge->NextInLML))
+ break;
+
+ UpdateEdgeIntoAEL(horzEdge);
+ if (horzEdge->OutIdx >= 0)
+ AddOutPt(horzEdge, horzEdge->Bot);
+ GetHorzDirection(*horzEdge, dir, horzLeft, horzRight);
+
+ } // end for (;;)
+
+ if (horzEdge->OutIdx >= 0 && !op1) {
+ op1 = GetLastOutPt(horzEdge);
+ TEdge *eNextHorz = m_SortedEdges;
+ while (eNextHorz) {
+ if (eNextHorz->OutIdx >= 0 &&
+ HorzSegmentsOverlap(horzEdge->Bot.X, horzEdge->Top.X,
+ eNextHorz->Bot.X, eNextHorz->Top.X)) {
+ OutPt *op2 = GetLastOutPt(eNextHorz);
+ AddJoin(op2, op1, eNextHorz->Top);
+ }
+ eNextHorz = eNextHorz->NextInSEL;
+ }
+ AddGhostJoin(op1, horzEdge->Top);
+ }
+
+ if (horzEdge->NextInLML) {
+ if (horzEdge->OutIdx >= 0) {
+ op1 = AddOutPt(horzEdge, horzEdge->Top);
+ UpdateEdgeIntoAEL(horzEdge);
+ if (horzEdge->WindDelta == 0)
+ return;
+ // nb: HorzEdge is no longer horizontal here
+ TEdge *ePrev = horzEdge->PrevInAEL;
+ TEdge *eNext = horzEdge->NextInAEL;
+ if (ePrev && ePrev->Curr.X == horzEdge->Bot.X &&
+ ePrev->Curr.Y == horzEdge->Bot.Y && ePrev->WindDelta != 0 &&
+ (ePrev->OutIdx >= 0 && ePrev->Curr.Y > ePrev->Top.Y &&
+ SlopesEqual(*horzEdge, *ePrev, m_UseFullRange))) {
+ OutPt *op2 = AddOutPt(ePrev, horzEdge->Bot);
+ AddJoin(op1, op2, horzEdge->Top);
+ } else if (eNext && eNext->Curr.X == horzEdge->Bot.X &&
+ eNext->Curr.Y == horzEdge->Bot.Y && eNext->WindDelta != 0 &&
+ eNext->OutIdx >= 0 && eNext->Curr.Y > eNext->Top.Y &&
+ SlopesEqual(*horzEdge, *eNext, m_UseFullRange)) {
+ OutPt *op2 = AddOutPt(eNext, horzEdge->Bot);
+ AddJoin(op1, op2, horzEdge->Top);
+ }
+ } else
+ UpdateEdgeIntoAEL(horzEdge);
+ } else {
+ if (horzEdge->OutIdx >= 0)
+ AddOutPt(horzEdge, horzEdge->Top);
+ DeleteFromAEL(horzEdge);
+ }
+}
+//------------------------------------------------------------------------------
+
+bool Clipper::ProcessIntersections(const cInt topY) {
+ if (!m_ActiveEdges)
+ return true;
+ try {
+ BuildIntersectList(topY);
+ size_t IlSize = m_IntersectList.size();
+ if (IlSize == 0)
+ return true;
+ if (IlSize == 1 || FixupIntersectionOrder())
+ ProcessIntersectList();
+ else
+ return false;
+ } catch (...) {
+ m_SortedEdges = 0;
+ DisposeIntersectNodes();
+ throw clipperException("ProcessIntersections error");
+ }
+ m_SortedEdges = 0;
+ return true;
+}
+//------------------------------------------------------------------------------
+
+void Clipper::DisposeIntersectNodes() {
+ for (size_t i = 0; i < m_IntersectList.size(); ++i)
+ delete m_IntersectList[i];
+ m_IntersectList.clear();
+}
+//------------------------------------------------------------------------------
+
+void Clipper::BuildIntersectList(const cInt topY) {
+ if (!m_ActiveEdges)
+ return;
+
+ // prepare for sorting ...
+ TEdge *e = m_ActiveEdges;
+ m_SortedEdges = e;
+ while (e) {
+ e->PrevInSEL = e->PrevInAEL;
+ e->NextInSEL = e->NextInAEL;
+ e->Curr.X = TopX(*e, topY);
+ e = e->NextInAEL;
+ }
+
+ // bubblesort ...
+ bool isModified;
+ do {
+ isModified = false;
+ e = m_SortedEdges;
+ while (e->NextInSEL) {
+ TEdge *eNext = e->NextInSEL;
+ IntPoint Pt;
+ if (e->Curr.X > eNext->Curr.X) {
+ IntersectPoint(*e, *eNext, Pt);
+ if (Pt.Y < topY)
+ Pt = IntPoint(TopX(*e, topY), topY);
+ IntersectNode *newNode = new IntersectNode;
+ newNode->Edge1 = e;
+ newNode->Edge2 = eNext;
+ newNode->Pt = Pt;
+ m_IntersectList.push_back(newNode);
+
+ SwapPositionsInSEL(e, eNext);
+ isModified = true;
+ } else
+ e = eNext;
+ }
+ if (e->PrevInSEL)
+ e->PrevInSEL->NextInSEL = 0;
+ else
+ break;
+ } while (isModified);
+ m_SortedEdges = 0; // important
+}
+//------------------------------------------------------------------------------
+
+void Clipper::ProcessIntersectList() {
+ for (size_t i = 0; i < m_IntersectList.size(); ++i) {
+ IntersectNode *iNode = m_IntersectList[i];
+ {
+ IntersectEdges(iNode->Edge1, iNode->Edge2, iNode->Pt);
+ SwapPositionsInAEL(iNode->Edge1, iNode->Edge2);
+ }
+ delete iNode;
+ }
+ m_IntersectList.clear();
+}
+//------------------------------------------------------------------------------
+
+bool IntersectListSort(IntersectNode *node1, IntersectNode *node2) {
+ return node2->Pt.Y < node1->Pt.Y;
+}
+//------------------------------------------------------------------------------
+
+inline bool EdgesAdjacent(const IntersectNode &inode) {
+ return (inode.Edge1->NextInSEL == inode.Edge2) ||
+ (inode.Edge1->PrevInSEL == inode.Edge2);
+}
+//------------------------------------------------------------------------------
+
+bool Clipper::FixupIntersectionOrder() {
+ // pre-condition: intersections are sorted Bottom-most first.
+ // Now it's crucial that intersections are made only between adjacent edges,
+ // so to ensure this the order of intersections may need adjusting ...
+ CopyAELToSEL();
+ std::sort(m_IntersectList.begin(), m_IntersectList.end(), IntersectListSort);
+ size_t cnt = m_IntersectList.size();
+ for (size_t i = 0; i < cnt; ++i) {
+ if (!EdgesAdjacent(*m_IntersectList[i])) {
+ size_t j = i + 1;
+ while (j < cnt && !EdgesAdjacent(*m_IntersectList[j]))
+ j++;
+ if (j == cnt)
+ return false;
+ std::swap(m_IntersectList[i], m_IntersectList[j]);
+ }
+ SwapPositionsInSEL(m_IntersectList[i]->Edge1, m_IntersectList[i]->Edge2);
+ }
+ return true;
+}
+//------------------------------------------------------------------------------
+
+void Clipper::DoMaxima(TEdge *e) {
+ TEdge *eMaxPair = GetMaximaPairEx(e);
+ if (!eMaxPair) {
+ if (e->OutIdx >= 0)
+ AddOutPt(e, e->Top);
+ DeleteFromAEL(e);
+ return;
+ }
+
+ TEdge *eNext = e->NextInAEL;
+ while (eNext && eNext != eMaxPair) {
+ IntersectEdges(e, eNext, e->Top);
+ SwapPositionsInAEL(e, eNext);
+ eNext = e->NextInAEL;
+ }
+
+ if (e->OutIdx == Unassigned && eMaxPair->OutIdx == Unassigned) {
+ DeleteFromAEL(e);
+ DeleteFromAEL(eMaxPair);
+ } else if (e->OutIdx >= 0 && eMaxPair->OutIdx >= 0) {
+ if (e->OutIdx >= 0)
+ AddLocalMaxPoly(e, eMaxPair, e->Top);
+ DeleteFromAEL(e);
+ DeleteFromAEL(eMaxPair);
+ }
+#ifdef use_lines
+ else if (e->WindDelta == 0) {
+ if (e->OutIdx >= 0) {
+ AddOutPt(e, e->Top);
+ e->OutIdx = Unassigned;
+ }
+ DeleteFromAEL(e);
+
+ if (eMaxPair->OutIdx >= 0) {
+ AddOutPt(eMaxPair, e->Top);
+ eMaxPair->OutIdx = Unassigned;
+ }
+ DeleteFromAEL(eMaxPair);
+ }
+#endif
+ else
+ throw clipperException("DoMaxima error");
+}
+//------------------------------------------------------------------------------
+
+void Clipper::ProcessEdgesAtTopOfScanbeam(const cInt topY) {
+ TEdge *e = m_ActiveEdges;
+ while (e) {
+ // 1. process maxima, treating them as if they're 'bent' horizontal edges,
+ // but exclude maxima with horizontal edges. nb: e can't be a horizontal.
+ bool IsMaximaEdge = IsMaxima(e, topY);
+
+ if (IsMaximaEdge) {
+ TEdge *eMaxPair = GetMaximaPairEx(e);
+ IsMaximaEdge = (!eMaxPair || !IsHorizontal(*eMaxPair));
+ }
+
+ if (IsMaximaEdge) {
+ if (m_StrictSimple)
+ m_Maxima.push_back(e->Top.X);
+ TEdge *ePrev = e->PrevInAEL;
+ DoMaxima(e);
+ if (!ePrev)
+ e = m_ActiveEdges;
+ else
+ e = ePrev->NextInAEL;
+ } else {
+ // 2. promote horizontal edges, otherwise update Curr.X and Curr.Y ...
+ if (IsIntermediate(e, topY) && IsHorizontal(*e->NextInLML)) {
+ UpdateEdgeIntoAEL(e);
+ if (e->OutIdx >= 0)
+ AddOutPt(e, e->Bot);
+ AddEdgeToSEL(e);
+ } else {
+ e->Curr.X = TopX(*e, topY);
+ e->Curr.Y = topY;
+#ifdef use_xyz
+ e->Curr.Z =
+ topY == e->Top.Y ? e->Top.Z : (topY == e->Bot.Y ? e->Bot.Z : 0);
+#endif
+ }
+
+ // When StrictlySimple and 'e' is being touched by another edge, then
+ // make sure both edges have a vertex here ...
+ if (m_StrictSimple) {
+ TEdge *ePrev = e->PrevInAEL;
+ if ((e->OutIdx >= 0) && (e->WindDelta != 0) && ePrev &&
+ (ePrev->OutIdx >= 0) && (ePrev->Curr.X == e->Curr.X) &&
+ (ePrev->WindDelta != 0)) {
+ IntPoint pt = e->Curr;
+#ifdef use_xyz
+ SetZ(pt, *ePrev, *e);
+#endif
+ OutPt *op = AddOutPt(ePrev, pt);
+ OutPt *op2 = AddOutPt(e, pt);
+ AddJoin(op, op2, pt); // StrictlySimple (type-3) join
+ }
+ }
+
+ e = e->NextInAEL;
+ }
+ }
+
+ // 3. Process horizontals at the Top of the scanbeam ...
+ m_Maxima.sort();
+ ProcessHorizontals();
+ m_Maxima.clear();
+
+ // 4. Promote intermediate vertices ...
+ e = m_ActiveEdges;
+ while (e) {
+ if (IsIntermediate(e, topY)) {
+ OutPt *op = 0;
+ if (e->OutIdx >= 0)
+ op = AddOutPt(e, e->Top);
+ UpdateEdgeIntoAEL(e);
+
+ // if output polygons share an edge, they'll need joining later ...
+ TEdge *ePrev = e->PrevInAEL;
+ TEdge *eNext = e->NextInAEL;
+ if (ePrev && ePrev->Curr.X == e->Bot.X && ePrev->Curr.Y == e->Bot.Y &&
+ op && ePrev->OutIdx >= 0 && ePrev->Curr.Y > ePrev->Top.Y &&
+ SlopesEqual(e->Curr, e->Top, ePrev->Curr, ePrev->Top,
+ m_UseFullRange) &&
+ (e->WindDelta != 0) && (ePrev->WindDelta != 0)) {
+ OutPt *op2 = AddOutPt(ePrev, e->Bot);
+ AddJoin(op, op2, e->Top);
+ } else if (eNext && eNext->Curr.X == e->Bot.X &&
+ eNext->Curr.Y == e->Bot.Y && op && eNext->OutIdx >= 0 &&
+ eNext->Curr.Y > eNext->Top.Y &&
+ SlopesEqual(e->Curr, e->Top, eNext->Curr, eNext->Top,
+ m_UseFullRange) &&
+ (e->WindDelta != 0) && (eNext->WindDelta != 0)) {
+ OutPt *op2 = AddOutPt(eNext, e->Bot);
+ AddJoin(op, op2, e->Top);
+ }
+ }
+ e = e->NextInAEL;
+ }
+}
+//------------------------------------------------------------------------------
+
+void Clipper::FixupOutPolyline(OutRec &outrec) {
+ OutPt *pp = outrec.Pts;
+ OutPt *lastPP = pp->Prev;
+ while (pp != lastPP) {
+ pp = pp->Next;
+ if (pp->Pt == pp->Prev->Pt) {
+ if (pp == lastPP)
+ lastPP = pp->Prev;
+ OutPt *tmpPP = pp->Prev;
+ tmpPP->Next = pp->Next;
+ pp->Next->Prev = tmpPP;
+ delete pp;
+ pp = tmpPP;
+ }
+ }
+
+ if (pp == pp->Prev) {
+ DisposeOutPts(pp);
+ outrec.Pts = 0;
+ return;
+ }
+}
+//------------------------------------------------------------------------------
+
+void Clipper::FixupOutPolygon(OutRec &outrec) {
+ // FixupOutPolygon() - removes duplicate points and simplifies consecutive
+ // parallel edges by removing the middle vertex.
+ OutPt *lastOK = 0;
+ outrec.BottomPt = 0;
+ OutPt *pp = outrec.Pts;
+ bool preserveCol = m_PreserveCollinear || m_StrictSimple;
+
+ for (;;) {
+ if (pp->Prev == pp || pp->Prev == pp->Next) {
+ DisposeOutPts(pp);
+ outrec.Pts = 0;
+ return;
+ }
+
+ // test for duplicate points and collinear edges ...
+ if ((pp->Pt == pp->Next->Pt) || (pp->Pt == pp->Prev->Pt) ||
+ (SlopesEqual(pp->Prev->Pt, pp->Pt, pp->Next->Pt, m_UseFullRange) &&
+ (!preserveCol ||
+ !Pt2IsBetweenPt1AndPt3(pp->Prev->Pt, pp->Pt, pp->Next->Pt)))) {
+ lastOK = 0;
+ OutPt *tmp = pp;
+ pp->Prev->Next = pp->Next;
+ pp->Next->Prev = pp->Prev;
+ pp = pp->Prev;
+ delete tmp;
+ } else if (pp == lastOK)
+ break;
+ else {
+ if (!lastOK)
+ lastOK = pp;
+ pp = pp->Next;
+ }
+ }
+ outrec.Pts = pp;
+}
+//------------------------------------------------------------------------------
+
+int PointCount(OutPt *Pts) {
+ if (!Pts)
+ return 0;
+ int result = 0;
+ OutPt *p = Pts;
+ do {
+ result++;
+ p = p->Next;
+ } while (p != Pts);
+ return result;
+}
+//------------------------------------------------------------------------------
+
+void Clipper::BuildResult(Paths &polys) {
+ polys.reserve(m_PolyOuts.size());
+ for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) {
+ if (!m_PolyOuts[i]->Pts)
+ continue;
+ Path pg;
+ OutPt *p = m_PolyOuts[i]->Pts->Prev;
+ int cnt = PointCount(p);
+ if (cnt < 2)
+ continue;
+ pg.reserve(cnt);
+ for (int i = 0; i < cnt; ++i) {
+ pg.push_back(p->Pt);
+ p = p->Prev;
+ }
+ polys.push_back(pg);
+ }
+}
+//------------------------------------------------------------------------------
+
+void Clipper::BuildResult2(PolyTree &polytree) {
+ polytree.Clear();
+ polytree.AllNodes.reserve(m_PolyOuts.size());
+ // add each output polygon/contour to polytree ...
+ for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); i++) {
+ OutRec *outRec = m_PolyOuts[i];
+ int cnt = PointCount(outRec->Pts);
+ if ((outRec->IsOpen && cnt < 2) || (!outRec->IsOpen && cnt < 3))
+ continue;
+ FixHoleLinkage(*outRec);
+ PolyNode *pn = new PolyNode();
+ // nb: polytree takes ownership of all the PolyNodes
+ polytree.AllNodes.push_back(pn);
+ outRec->PolyNd = pn;
+ pn->Parent = 0;
+ pn->Index = 0;
+ pn->Contour.reserve(cnt);
+ OutPt *op = outRec->Pts->Prev;
+ for (int j = 0; j < cnt; j++) {
+ pn->Contour.push_back(op->Pt);
+ op = op->Prev;
+ }
+ }
+
+ // fixup PolyNode links etc ...
+ polytree.Childs.reserve(m_PolyOuts.size());
+ for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); i++) {
+ OutRec *outRec = m_PolyOuts[i];
+ if (!outRec->PolyNd)
+ continue;
+ if (outRec->IsOpen) {
+ outRec->PolyNd->m_IsOpen = true;
+ polytree.AddChild(*outRec->PolyNd);
+ } else if (outRec->FirstLeft && outRec->FirstLeft->PolyNd)
+ outRec->FirstLeft->PolyNd->AddChild(*outRec->PolyNd);
+ else
+ polytree.AddChild(*outRec->PolyNd);
+ }
+}
+//------------------------------------------------------------------------------
+
+void SwapIntersectNodes(IntersectNode &int1, IntersectNode &int2) {
+ // just swap the contents (because fIntersectNodes is a single-linked-list)
+ IntersectNode inode = int1; // gets a copy of Int1
+ int1.Edge1 = int2.Edge1;
+ int1.Edge2 = int2.Edge2;
+ int1.Pt = int2.Pt;
+ int2.Edge1 = inode.Edge1;
+ int2.Edge2 = inode.Edge2;
+ int2.Pt = inode.Pt;
+}
+//------------------------------------------------------------------------------
+
+inline bool E2InsertsBeforeE1(TEdge &e1, TEdge &e2) {
+ if (e2.Curr.X == e1.Curr.X) {
+ if (e2.Top.Y > e1.Top.Y)
+ return e2.Top.X < TopX(e1, e2.Top.Y);
+ else
+ return e1.Top.X > TopX(e2, e1.Top.Y);
+ } else
+ return e2.Curr.X < e1.Curr.X;
+}
+//------------------------------------------------------------------------------
+
+bool GetOverlap(const cInt a1, const cInt a2, const cInt b1, const cInt b2,
+ cInt &Left, cInt &Right) {
+ if (a1 < a2) {
+ if (b1 < b2) {
+ Left = std::max(a1, b1);
+ Right = std::min(a2, b2);
+ } else {
+ Left = std::max(a1, b2);
+ Right = std::min(a2, b1);
+ }
+ } else {
+ if (b1 < b2) {
+ Left = std::max(a2, b1);
+ Right = std::min(a1, b2);
+ } else {
+ Left = std::max(a2, b2);
+ Right = std::min(a1, b1);
+ }
+ }
+ return Left < Right;
+}
+//------------------------------------------------------------------------------
+
+inline void UpdateOutPtIdxs(OutRec &outrec) {
+ OutPt *op = outrec.Pts;
+ do {
+ op->Idx = outrec.Idx;
+ op = op->Prev;
+ } while (op != outrec.Pts);
+}
+//------------------------------------------------------------------------------
+
+void Clipper::InsertEdgeIntoAEL(TEdge *edge, TEdge *startEdge) {
+ if (!m_ActiveEdges) {
+ edge->PrevInAEL = 0;
+ edge->NextInAEL = 0;
+ m_ActiveEdges = edge;
+ } else if (!startEdge && E2InsertsBeforeE1(*m_ActiveEdges, *edge)) {
+ edge->PrevInAEL = 0;
+ edge->NextInAEL = m_ActiveEdges;
+ m_ActiveEdges->PrevInAEL = edge;
+ m_ActiveEdges = edge;
+ } else {
+ if (!startEdge)
+ startEdge = m_ActiveEdges;
+ while (startEdge->NextInAEL &&
+ !E2InsertsBeforeE1(*startEdge->NextInAEL, *edge))
+ startEdge = startEdge->NextInAEL;
+ edge->NextInAEL = startEdge->NextInAEL;
+ if (startEdge->NextInAEL)
+ startEdge->NextInAEL->PrevInAEL = edge;
+ edge->PrevInAEL = startEdge;
+ startEdge->NextInAEL = edge;
+ }
+}
+//----------------------------------------------------------------------
+
+OutPt *DupOutPt(OutPt *outPt, bool InsertAfter) {
+ OutPt *result = new OutPt;
+ result->Pt = outPt->Pt;
+ result->Idx = outPt->Idx;
+ if (InsertAfter) {
+ result->Next = outPt->Next;
+ result->Prev = outPt;
+ outPt->Next->Prev = result;
+ outPt->Next = result;
+ } else {
+ result->Prev = outPt->Prev;
+ result->Next = outPt;
+ outPt->Prev->Next = result;
+ outPt->Prev = result;
+ }
+ return result;
+}
+//------------------------------------------------------------------------------
+
+bool JoinHorz(OutPt *op1, OutPt *op1b, OutPt *op2, OutPt *op2b,
+ const IntPoint Pt, bool DiscardLeft) {
+ Direction Dir1 = (op1->Pt.X > op1b->Pt.X ? dRightToLeft : dLeftToRight);
+ Direction Dir2 = (op2->Pt.X > op2b->Pt.X ? dRightToLeft : dLeftToRight);
+ if (Dir1 == Dir2)
+ return false;
+
+ // When DiscardLeft, we want Op1b to be on the Left of Op1, otherwise we
+ // want Op1b to be on the Right. (And likewise with Op2 and Op2b.)
+ // So, to facilitate this while inserting Op1b and Op2b ...
+ // when DiscardLeft, make sure we're AT or RIGHT of Pt before adding Op1b,
+ // otherwise make sure we're AT or LEFT of Pt. (Likewise with Op2b.)
+ if (Dir1 == dLeftToRight) {
+ while (op1->Next->Pt.X <= Pt.X && op1->Next->Pt.X >= op1->Pt.X &&
+ op1->Next->Pt.Y == Pt.Y)
+ op1 = op1->Next;
+ if (DiscardLeft && (op1->Pt.X != Pt.X))
+ op1 = op1->Next;
+ op1b = DupOutPt(op1, !DiscardLeft);
+ if (op1b->Pt != Pt) {
+ op1 = op1b;
+ op1->Pt = Pt;
+ op1b = DupOutPt(op1, !DiscardLeft);
+ }
+ } else {
+ while (op1->Next->Pt.X >= Pt.X && op1->Next->Pt.X <= op1->Pt.X &&
+ op1->Next->Pt.Y == Pt.Y)
+ op1 = op1->Next;
+ if (!DiscardLeft && (op1->Pt.X != Pt.X))
+ op1 = op1->Next;
+ op1b = DupOutPt(op1, DiscardLeft);
+ if (op1b->Pt != Pt) {
+ op1 = op1b;
+ op1->Pt = Pt;
+ op1b = DupOutPt(op1, DiscardLeft);
+ }
+ }
+
+ if (Dir2 == dLeftToRight) {
+ while (op2->Next->Pt.X <= Pt.X && op2->Next->Pt.X >= op2->Pt.X &&
+ op2->Next->Pt.Y == Pt.Y)
+ op2 = op2->Next;
+ if (DiscardLeft && (op2->Pt.X != Pt.X))
+ op2 = op2->Next;
+ op2b = DupOutPt(op2, !DiscardLeft);
+ if (op2b->Pt != Pt) {
+ op2 = op2b;
+ op2->Pt = Pt;
+ op2b = DupOutPt(op2, !DiscardLeft);
+ };
+ } else {
+ while (op2->Next->Pt.X >= Pt.X && op2->Next->Pt.X <= op2->Pt.X &&
+ op2->Next->Pt.Y == Pt.Y)
+ op2 = op2->Next;
+ if (!DiscardLeft && (op2->Pt.X != Pt.X))
+ op2 = op2->Next;
+ op2b = DupOutPt(op2, DiscardLeft);
+ if (op2b->Pt != Pt) {
+ op2 = op2b;
+ op2->Pt = Pt;
+ op2b = DupOutPt(op2, DiscardLeft);
+ };
+ };
+
+ if ((Dir1 == dLeftToRight) == DiscardLeft) {
+ op1->Prev = op2;
+ op2->Next = op1;
+ op1b->Next = op2b;
+ op2b->Prev = op1b;
+ } else {
+ op1->Next = op2;
+ op2->Prev = op1;
+ op1b->Prev = op2b;
+ op2b->Next = op1b;
+ }
+ return true;
+}
+//------------------------------------------------------------------------------
+
+bool Clipper::JoinPoints(Join *j, OutRec *outRec1, OutRec *outRec2) {
+ OutPt *op1 = j->OutPt1, *op1b;
+ OutPt *op2 = j->OutPt2, *op2b;
+
+ // There are 3 kinds of joins for output polygons ...
+ // 1. Horizontal joins where Join.OutPt1 & Join.OutPt2 are vertices anywhere
+ // along (horizontal) collinear edges (& Join.OffPt is on the same
+ // horizontal).
+ // 2. Non-horizontal joins where Join.OutPt1 & Join.OutPt2 are at the same
+ // location at the Bottom of the overlapping segment (& Join.OffPt is above).
+ // 3. StrictSimple joins where edges touch but are not collinear and where
+ // Join.OutPt1, Join.OutPt2 & Join.OffPt all share the same point.
+ bool isHorizontal = (j->OutPt1->Pt.Y == j->OffPt.Y);
+
+ if (isHorizontal && (j->OffPt == j->OutPt1->Pt) &&
+ (j->OffPt == j->OutPt2->Pt)) {
+ // Strictly Simple join ...
+ if (outRec1 != outRec2)
+ return false;
+ op1b = j->OutPt1->Next;
+ while (op1b != op1 && (op1b->Pt == j->OffPt))
+ op1b = op1b->Next;
+ bool reverse1 = (op1b->Pt.Y > j->OffPt.Y);
+ op2b = j->OutPt2->Next;
+ while (op2b != op2 && (op2b->Pt == j->OffPt))
+ op2b = op2b->Next;
+ bool reverse2 = (op2b->Pt.Y > j->OffPt.Y);
+ if (reverse1 == reverse2)
+ return false;
+ if (reverse1) {
+ op1b = DupOutPt(op1, false);
+ op2b = DupOutPt(op2, true);
+ op1->Prev = op2;
+ op2->Next = op1;
+ op1b->Next = op2b;
+ op2b->Prev = op1b;
+ j->OutPt1 = op1;
+ j->OutPt2 = op1b;
+ return true;
+ } else {
+ op1b = DupOutPt(op1, true);
+ op2b = DupOutPt(op2, false);
+ op1->Next = op2;
+ op2->Prev = op1;
+ op1b->Prev = op2b;
+ op2b->Next = op1b;
+ j->OutPt1 = op1;
+ j->OutPt2 = op1b;
+ return true;
+ }
+ } else if (isHorizontal) {
+ // treat horizontal joins differently to non-horizontal joins since with
+ // them we're not yet sure where the overlapping is. OutPt1.Pt & OutPt2.Pt
+ // may be anywhere along the horizontal edge.
+ op1b = op1;
+ while (op1->Prev->Pt.Y == op1->Pt.Y && op1->Prev != op1b &&
+ op1->Prev != op2)
+ op1 = op1->Prev;
+ while (op1b->Next->Pt.Y == op1b->Pt.Y && op1b->Next != op1 &&
+ op1b->Next != op2)
+ op1b = op1b->Next;
+ if (op1b->Next == op1 || op1b->Next == op2)
+ return false; // a flat 'polygon'
+
+ op2b = op2;
+ while (op2->Prev->Pt.Y == op2->Pt.Y && op2->Prev != op2b &&
+ op2->Prev != op1b)
+ op2 = op2->Prev;
+ while (op2b->Next->Pt.Y == op2b->Pt.Y && op2b->Next != op2 &&
+ op2b->Next != op1)
+ op2b = op2b->Next;
+ if (op2b->Next == op2 || op2b->Next == op1)
+ return false; // a flat 'polygon'
+
+ cInt Left, Right;
+ // Op1 --> Op1b & Op2 --> Op2b are the extremites of the horizontal edges
+ if (!GetOverlap(op1->Pt.X, op1b->Pt.X, op2->Pt.X, op2b->Pt.X, Left, Right))
+ return false;
+
+ // DiscardLeftSide: when overlapping edges are joined, a spike will created
+ // which needs to be cleaned up. However, we don't want Op1 or Op2 caught up
+ // on the discard Side as either may still be needed for other joins ...
+ IntPoint Pt;
+ bool DiscardLeftSide;
+ if (op1->Pt.X >= Left && op1->Pt.X <= Right) {
+ Pt = op1->Pt;
+ DiscardLeftSide = (op1->Pt.X > op1b->Pt.X);
+ } else if (op2->Pt.X >= Left && op2->Pt.X <= Right) {
+ Pt = op2->Pt;
+ DiscardLeftSide = (op2->Pt.X > op2b->Pt.X);
+ } else if (op1b->Pt.X >= Left && op1b->Pt.X <= Right) {
+ Pt = op1b->Pt;
+ DiscardLeftSide = op1b->Pt.X > op1->Pt.X;
+ } else {
+ Pt = op2b->Pt;
+ DiscardLeftSide = (op2b->Pt.X > op2->Pt.X);
+ }
+ j->OutPt1 = op1;
+ j->OutPt2 = op2;
+ return JoinHorz(op1, op1b, op2, op2b, Pt, DiscardLeftSide);
+ } else {
+ // nb: For non-horizontal joins ...
+ // 1. Jr.OutPt1.Pt.Y == Jr.OutPt2.Pt.Y
+ // 2. Jr.OutPt1.Pt > Jr.OffPt.Y
+
+ // make sure the polygons are correctly oriented ...
+ op1b = op1->Next;
+ while ((op1b->Pt == op1->Pt) && (op1b != op1))
+ op1b = op1b->Next;
+ bool Reverse1 = ((op1b->Pt.Y > op1->Pt.Y) ||
+ !SlopesEqual(op1->Pt, op1b->Pt, j->OffPt, m_UseFullRange));
+ if (Reverse1) {
+ op1b = op1->Prev;
+ while ((op1b->Pt == op1->Pt) && (op1b != op1))
+ op1b = op1b->Prev;
+ if ((op1b->Pt.Y > op1->Pt.Y) ||
+ !SlopesEqual(op1->Pt, op1b->Pt, j->OffPt, m_UseFullRange))
+ return false;
+ };
+ op2b = op2->Next;
+ while ((op2b->Pt == op2->Pt) && (op2b != op2))
+ op2b = op2b->Next;
+ bool Reverse2 = ((op2b->Pt.Y > op2->Pt.Y) ||
+ !SlopesEqual(op2->Pt, op2b->Pt, j->OffPt, m_UseFullRange));
+ if (Reverse2) {
+ op2b = op2->Prev;
+ while ((op2b->Pt == op2->Pt) && (op2b != op2))
+ op2b = op2b->Prev;
+ if ((op2b->Pt.Y > op2->Pt.Y) ||
+ !SlopesEqual(op2->Pt, op2b->Pt, j->OffPt, m_UseFullRange))
+ return false;
+ }
+
+ if ((op1b == op1) || (op2b == op2) || (op1b == op2b) ||
+ ((outRec1 == outRec2) && (Reverse1 == Reverse2)))
+ return false;
+
+ if (Reverse1) {
+ op1b = DupOutPt(op1, false);
+ op2b = DupOutPt(op2, true);
+ op1->Prev = op2;
+ op2->Next = op1;
+ op1b->Next = op2b;
+ op2b->Prev = op1b;
+ j->OutPt1 = op1;
+ j->OutPt2 = op1b;
+ return true;
+ } else {
+ op1b = DupOutPt(op1, true);
+ op2b = DupOutPt(op2, false);
+ op1->Next = op2;
+ op2->Prev = op1;
+ op1b->Prev = op2b;
+ op2b->Next = op1b;
+ j->OutPt1 = op1;
+ j->OutPt2 = op1b;
+ return true;
+ }
+ }
+}
+//----------------------------------------------------------------------
+
+static OutRec *ParseFirstLeft(OutRec *FirstLeft) {
+ while (FirstLeft && !FirstLeft->Pts)
+ FirstLeft = FirstLeft->FirstLeft;
+ return FirstLeft;
+}
+//------------------------------------------------------------------------------
+
+void Clipper::FixupFirstLefts1(OutRec *OldOutRec, OutRec *NewOutRec) {
+ // tests if NewOutRec contains the polygon before reassigning FirstLeft
+ for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) {
+ OutRec *outRec = m_PolyOuts[i];
+ OutRec *firstLeft = ParseFirstLeft(outRec->FirstLeft);
+ if (outRec->Pts && firstLeft == OldOutRec) {
+ if (Poly2ContainsPoly1(outRec->Pts, NewOutRec->Pts))
+ outRec->FirstLeft = NewOutRec;
+ }
+ }
+}
+//----------------------------------------------------------------------
+
+void Clipper::FixupFirstLefts2(OutRec *InnerOutRec, OutRec *OuterOutRec) {
+ // A polygon has split into two such that one is now the inner of the other.
+ // It's possible that these polygons now wrap around other polygons, so check
+ // every polygon that's also contained by OuterOutRec's FirstLeft container
+ //(including 0) to see if they've become inner to the new inner polygon ...
+ OutRec *orfl = OuterOutRec->FirstLeft;
+ for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) {
+ OutRec *outRec = m_PolyOuts[i];
+
+ if (!outRec->Pts || outRec == OuterOutRec || outRec == InnerOutRec)
+ continue;
+ OutRec *firstLeft = ParseFirstLeft(outRec->FirstLeft);
+ if (firstLeft != orfl && firstLeft != InnerOutRec &&
+ firstLeft != OuterOutRec)
+ continue;
+ if (Poly2ContainsPoly1(outRec->Pts, InnerOutRec->Pts))
+ outRec->FirstLeft = InnerOutRec;
+ else if (Poly2ContainsPoly1(outRec->Pts, OuterOutRec->Pts))
+ outRec->FirstLeft = OuterOutRec;
+ else if (outRec->FirstLeft == InnerOutRec ||
+ outRec->FirstLeft == OuterOutRec)
+ outRec->FirstLeft = orfl;
+ }
+}
+//----------------------------------------------------------------------
+void Clipper::FixupFirstLefts3(OutRec *OldOutRec, OutRec *NewOutRec) {
+ // reassigns FirstLeft WITHOUT testing if NewOutRec contains the polygon
+ for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) {
+ OutRec *outRec = m_PolyOuts[i];
+ OutRec *firstLeft = ParseFirstLeft(outRec->FirstLeft);
+ if (outRec->Pts && firstLeft == OldOutRec)
+ outRec->FirstLeft = NewOutRec;
+ }
+}
+//----------------------------------------------------------------------
+
+void Clipper::JoinCommonEdges() {
+ for (JoinList::size_type i = 0; i < m_Joins.size(); i++) {
+ Join *join = m_Joins[i];
+
+ OutRec *outRec1 = GetOutRec(join->OutPt1->Idx);
+ OutRec *outRec2 = GetOutRec(join->OutPt2->Idx);
+
+ if (!outRec1->Pts || !outRec2->Pts)
+ continue;
+ if (outRec1->IsOpen || outRec2->IsOpen)
+ continue;
+
+ // get the polygon fragment with the correct hole state (FirstLeft)
+ // before calling JoinPoints() ...
+ OutRec *holeStateRec;
+ if (outRec1 == outRec2)
+ holeStateRec = outRec1;
+ else if (OutRec1RightOfOutRec2(outRec1, outRec2))
+ holeStateRec = outRec2;
+ else if (OutRec1RightOfOutRec2(outRec2, outRec1))
+ holeStateRec = outRec1;
+ else
+ holeStateRec = GetLowermostRec(outRec1, outRec2);
+
+ if (!JoinPoints(join, outRec1, outRec2))
+ continue;
+
+ if (outRec1 == outRec2) {
+ // instead of joining two polygons, we've just created a new one by
+ // splitting one polygon into two.
+ outRec1->Pts = join->OutPt1;
+ outRec1->BottomPt = 0;
+ outRec2 = CreateOutRec();
+ outRec2->Pts = join->OutPt2;
+
+ // update all OutRec2.Pts Idx's ...
+ UpdateOutPtIdxs(*outRec2);
+
+ if (Poly2ContainsPoly1(outRec2->Pts, outRec1->Pts)) {
+ // outRec1 contains outRec2 ...
+ outRec2->IsHole = !outRec1->IsHole;
+ outRec2->FirstLeft = outRec1;
+
+ if (m_UsingPolyTree)
+ FixupFirstLefts2(outRec2, outRec1);
+
+ if ((outRec2->IsHole ^ m_ReverseOutput) == (Area(*outRec2) > 0))
+ ReversePolyPtLinks(outRec2->Pts);
+
+ } else if (Poly2ContainsPoly1(outRec1->Pts, outRec2->Pts)) {
+ // outRec2 contains outRec1 ...
+ outRec2->IsHole = outRec1->IsHole;
+ outRec1->IsHole = !outRec2->IsHole;
+ outRec2->FirstLeft = outRec1->FirstLeft;
+ outRec1->FirstLeft = outRec2;
+
+ if (m_UsingPolyTree)
+ FixupFirstLefts2(outRec1, outRec2);
+
+ if ((outRec1->IsHole ^ m_ReverseOutput) == (Area(*outRec1) > 0))
+ ReversePolyPtLinks(outRec1->Pts);
+ } else {
+ // the 2 polygons are completely separate ...
+ outRec2->IsHole = outRec1->IsHole;
+ outRec2->FirstLeft = outRec1->FirstLeft;
+
+ // fixup FirstLeft pointers that may need reassigning to OutRec2
+ if (m_UsingPolyTree)
+ FixupFirstLefts1(outRec1, outRec2);
+ }
+
+ } else {
+ // joined 2 polygons together ...
+
+ outRec2->Pts = 0;
+ outRec2->BottomPt = 0;
+ outRec2->Idx = outRec1->Idx;
+
+ outRec1->IsHole = holeStateRec->IsHole;
+ if (holeStateRec == outRec2)
+ outRec1->FirstLeft = outRec2->FirstLeft;
+ outRec2->FirstLeft = outRec1;
+
+ if (m_UsingPolyTree)
+ FixupFirstLefts3(outRec2, outRec1);
+ }
+ }
+}
+
+//------------------------------------------------------------------------------
+// ClipperOffset support functions ...
+//------------------------------------------------------------------------------
+
+DoublePoint GetUnitNormal(const IntPoint &pt1, const IntPoint &pt2) {
+ if (pt2.X == pt1.X && pt2.Y == pt1.Y)
+ return DoublePoint(0, 0);
+
+ double Dx = (double)(pt2.X - pt1.X);
+ double dy = (double)(pt2.Y - pt1.Y);
+ double f = 1 * 1.0 / std::sqrt(Dx * Dx + dy * dy);
+ Dx *= f;
+ dy *= f;
+ return DoublePoint(dy, -Dx);
+}
+
+//------------------------------------------------------------------------------
+// ClipperOffset class
+//------------------------------------------------------------------------------
+
+ClipperOffset::ClipperOffset(double miterLimit, double arcTolerance) {
+ this->MiterLimit = miterLimit;
+ this->ArcTolerance = arcTolerance;
+ m_lowest.X = -1;
+}
+//------------------------------------------------------------------------------
+
+ClipperOffset::~ClipperOffset() { Clear(); }
+//------------------------------------------------------------------------------
+
+void ClipperOffset::Clear() {
+ for (int i = 0; i < m_polyNodes.ChildCount(); ++i)
+ delete m_polyNodes.Childs[i];
+ m_polyNodes.Childs.clear();
+ m_lowest.X = -1;
+}
+//------------------------------------------------------------------------------
+
+void ClipperOffset::AddPath(const Path &path, JoinType joinType,
+ EndType endType) {
+ int highI = (int)path.size() - 1;
+ if (highI < 0)
+ return;
+ PolyNode *newNode = new PolyNode();
+ newNode->m_jointype = joinType;
+ newNode->m_endtype = endType;
+
+ // strip duplicate points from path and also get index to the lowest point ...
+ if (endType == etClosedLine || endType == etClosedPolygon)
+ while (highI > 0 && path[0] == path[highI])
+ highI--;
+ newNode->Contour.reserve(highI + 1);
+ newNode->Contour.push_back(path[0]);
+ int j = 0, k = 0;
+ for (int i = 1; i <= highI; i++)
+ if (newNode->Contour[j] != path[i]) {
+ j++;
+ newNode->Contour.push_back(path[i]);
+ if (path[i].Y > newNode->Contour[k].Y ||
+ (path[i].Y == newNode->Contour[k].Y &&
+ path[i].X < newNode->Contour[k].X))
+ k = j;
+ }
+ if (endType == etClosedPolygon && j < 2) {
+ delete newNode;
+ return;
+ }
+ m_polyNodes.AddChild(*newNode);
+
+ // if this path's lowest pt is lower than all the others then update m_lowest
+ if (endType != etClosedPolygon)
+ return;
+ if (m_lowest.X < 0)
+ m_lowest = IntPoint(m_polyNodes.ChildCount() - 1, k);
+ else {
+ IntPoint ip = m_polyNodes.Childs[(int)m_lowest.X]->Contour[(int)m_lowest.Y];
+ if (newNode->Contour[k].Y > ip.Y ||
+ (newNode->Contour[k].Y == ip.Y && newNode->Contour[k].X < ip.X))
+ m_lowest = IntPoint(m_polyNodes.ChildCount() - 1, k);
+ }
+}
+//------------------------------------------------------------------------------
+
+void ClipperOffset::AddPaths(const Paths &paths, JoinType joinType,
+ EndType endType) {
+ for (Paths::size_type i = 0; i < paths.size(); ++i)
+ AddPath(paths[i], joinType, endType);
+}
+//------------------------------------------------------------------------------
+
+void ClipperOffset::FixOrientations() {
+ // fixup orientations of all closed paths if the orientation of the
+ // closed path with the lowermost vertex is wrong ...
+ if (m_lowest.X >= 0 &&
+ !Orientation(m_polyNodes.Childs[(int)m_lowest.X]->Contour)) {
+ for (int i = 0; i < m_polyNodes.ChildCount(); ++i) {
+ PolyNode &node = *m_polyNodes.Childs[i];
+ if (node.m_endtype == etClosedPolygon ||
+ (node.m_endtype == etClosedLine && Orientation(node.Contour)))
+ ReversePath(node.Contour);
+ }
+ } else {
+ for (int i = 0; i < m_polyNodes.ChildCount(); ++i) {
+ PolyNode &node = *m_polyNodes.Childs[i];
+ if (node.m_endtype == etClosedLine && !Orientation(node.Contour))
+ ReversePath(node.Contour);
+ }
+ }
+}
+//------------------------------------------------------------------------------
+
+void ClipperOffset::Execute(Paths &solution, double delta) {
+ solution.clear();
+ FixOrientations();
+ DoOffset(delta);
+
+ // now clean up 'corners' ...
+ Clipper clpr;
+ clpr.AddPaths(m_destPolys, ptSubject, true);
+ if (delta > 0) {
+ clpr.Execute(ctUnion, solution, pftPositive, pftPositive);
+ } else {
+ IntRect r = clpr.GetBounds();
+ Path outer(4);
+ outer[0] = IntPoint(r.left - 10, r.bottom + 10);
+ outer[1] = IntPoint(r.right + 10, r.bottom + 10);
+ outer[2] = IntPoint(r.right + 10, r.top - 10);
+ outer[3] = IntPoint(r.left - 10, r.top - 10);
+
+ clpr.AddPath(outer, ptSubject, true);
+ clpr.ReverseSolution(true);
+ clpr.Execute(ctUnion, solution, pftNegative, pftNegative);
+ if (solution.size() > 0)
+ solution.erase(solution.begin());
+ }
+}
+//------------------------------------------------------------------------------
+
+void ClipperOffset::Execute(PolyTree &solution, double delta) {
+ solution.Clear();
+ FixOrientations();
+ DoOffset(delta);
+
+ // now clean up 'corners' ...
+ Clipper clpr;
+ clpr.AddPaths(m_destPolys, ptSubject, true);
+ if (delta > 0) {
+ clpr.Execute(ctUnion, solution, pftPositive, pftPositive);
+ } else {
+ IntRect r = clpr.GetBounds();
+ Path outer(4);
+ outer[0] = IntPoint(r.left - 10, r.bottom + 10);
+ outer[1] = IntPoint(r.right + 10, r.bottom + 10);
+ outer[2] = IntPoint(r.right + 10, r.top - 10);
+ outer[3] = IntPoint(r.left - 10, r.top - 10);
+
+ clpr.AddPath(outer, ptSubject, true);
+ clpr.ReverseSolution(true);
+ clpr.Execute(ctUnion, solution, pftNegative, pftNegative);
+ // remove the outer PolyNode rectangle ...
+ if (solution.ChildCount() == 1 && solution.Childs[0]->ChildCount() > 0) {
+ PolyNode *outerNode = solution.Childs[0];
+ solution.Childs.reserve(outerNode->ChildCount());
+ solution.Childs[0] = outerNode->Childs[0];
+ solution.Childs[0]->Parent = outerNode->Parent;
+ for (int i = 1; i < outerNode->ChildCount(); ++i)
+ solution.AddChild(*outerNode->Childs[i]);
+ } else
+ solution.Clear();
+ }
+}
+//------------------------------------------------------------------------------
+
+void ClipperOffset::DoOffset(double delta) {
+ m_destPolys.clear();
+ m_delta = delta;
+
+ // if Zero offset, just copy any CLOSED polygons to m_p and return ...
+ if (NEAR_ZERO(delta)) {
+ m_destPolys.reserve(m_polyNodes.ChildCount());
+ for (int i = 0; i < m_polyNodes.ChildCount(); i++) {
+ PolyNode &node = *m_polyNodes.Childs[i];
+ if (node.m_endtype == etClosedPolygon)
+ m_destPolys.push_back(node.Contour);
+ }
+ return;
+ }
+
+ // see offset_triginometry3.svg in the documentation folder ...
+ if (MiterLimit > 2)
+ m_miterLim = 2 / (MiterLimit * MiterLimit);
+ else
+ m_miterLim = 0.5;
+
+ double y;
+ if (ArcTolerance <= 0.0)
+ y = def_arc_tolerance;
+ else if (ArcTolerance > std::fabs(delta) * def_arc_tolerance)
+ y = std::fabs(delta) * def_arc_tolerance;
+ else
+ y = ArcTolerance;
+ // see offset_triginometry2.svg in the documentation folder ...
+ double steps = pi / std::acos(1 - y / std::fabs(delta));
+ if (steps > std::fabs(delta) * pi)
+ steps = std::fabs(delta) * pi; // ie excessive precision check
+ m_sin = std::sin(two_pi / steps);
+ m_cos = std::cos(two_pi / steps);
+ m_StepsPerRad = steps / two_pi;
+ if (delta < 0.0)
+ m_sin = -m_sin;
+
+ m_destPolys.reserve(m_polyNodes.ChildCount() * 2);
+ for (int i = 0; i < m_polyNodes.ChildCount(); i++) {
+ PolyNode &node = *m_polyNodes.Childs[i];
+ m_srcPoly = node.Contour;
+
+ int len = (int)m_srcPoly.size();
+ if (len == 0 ||
+ (delta <= 0 && (len < 3 || node.m_endtype != etClosedPolygon)))
+ continue;
+
+ m_destPoly.clear();
+ if (len == 1) {
+ if (node.m_jointype == jtRound) {
+ double X = 1.0, Y = 0.0;
+ for (cInt j = 1; j <= steps; j++) {
+ m_destPoly.push_back(IntPoint(Round(m_srcPoly[0].X + X * delta),
+ Round(m_srcPoly[0].Y + Y * delta)));
+ double X2 = X;
+ X = X * m_cos - m_sin * Y;
+ Y = X2 * m_sin + Y * m_cos;
+ }
+ } else {
+ double X = -1.0, Y = -1.0;
+ for (int j = 0; j < 4; ++j) {
+ m_destPoly.push_back(IntPoint(Round(m_srcPoly[0].X + X * delta),
+ Round(m_srcPoly[0].Y + Y * delta)));
+ if (X < 0)
+ X = 1;
+ else if (Y < 0)
+ Y = 1;
+ else
+ X = -1;
+ }
+ }
+ m_destPolys.push_back(m_destPoly);
+ continue;
+ }
+ // build m_normals ...
+ m_normals.clear();
+ m_normals.reserve(len);
+ for (int j = 0; j < len - 1; ++j)
+ m_normals.push_back(GetUnitNormal(m_srcPoly[j], m_srcPoly[j + 1]));
+ if (node.m_endtype == etClosedLine || node.m_endtype == etClosedPolygon)
+ m_normals.push_back(GetUnitNormal(m_srcPoly[len - 1], m_srcPoly[0]));
+ else
+ m_normals.push_back(DoublePoint(m_normals[len - 2]));
+
+ if (node.m_endtype == etClosedPolygon) {
+ int k = len - 1;
+ for (int j = 0; j < len; ++j)
+ OffsetPoint(j, k, node.m_jointype);
+ m_destPolys.push_back(m_destPoly);
+ } else if (node.m_endtype == etClosedLine) {
+ int k = len - 1;
+ for (int j = 0; j < len; ++j)
+ OffsetPoint(j, k, node.m_jointype);
+ m_destPolys.push_back(m_destPoly);
+ m_destPoly.clear();
+ // re-build m_normals ...
+ DoublePoint n = m_normals[len - 1];
+ for (int j = len - 1; j > 0; j--)
+ m_normals[j] = DoublePoint(-m_normals[j - 1].X, -m_normals[j - 1].Y);
+ m_normals[0] = DoublePoint(-n.X, -n.Y);
+ k = 0;
+ for (int j = len - 1; j >= 0; j--)
+ OffsetPoint(j, k, node.m_jointype);
+ m_destPolys.push_back(m_destPoly);
+ } else {
+ int k = 0;
+ for (int j = 1; j < len - 1; ++j)
+ OffsetPoint(j, k, node.m_jointype);
+
+ IntPoint pt1;
+ if (node.m_endtype == etOpenButt) {
+ int j = len - 1;
+ pt1 = IntPoint((cInt)Round(m_srcPoly[j].X + m_normals[j].X * delta),
+ (cInt)Round(m_srcPoly[j].Y + m_normals[j].Y * delta));
+ m_destPoly.push_back(pt1);
+ pt1 = IntPoint((cInt)Round(m_srcPoly[j].X - m_normals[j].X * delta),
+ (cInt)Round(m_srcPoly[j].Y - m_normals[j].Y * delta));
+ m_destPoly.push_back(pt1);
+ } else {
+ int j = len - 1;
+ k = len - 2;
+ m_sinA = 0;
+ m_normals[j] = DoublePoint(-m_normals[j].X, -m_normals[j].Y);
+ if (node.m_endtype == etOpenSquare)
+ DoSquare(j, k);
+ else
+ DoRound(j, k);
+ }
+
+ // re-build m_normals ...
+ for (int j = len - 1; j > 0; j--)
+ m_normals[j] = DoublePoint(-m_normals[j - 1].X, -m_normals[j - 1].Y);
+ m_normals[0] = DoublePoint(-m_normals[1].X, -m_normals[1].Y);
+
+ k = len - 1;
+ for (int j = k - 1; j > 0; --j)
+ OffsetPoint(j, k, node.m_jointype);
+
+ if (node.m_endtype == etOpenButt) {
+ pt1 = IntPoint((cInt)Round(m_srcPoly[0].X - m_normals[0].X * delta),
+ (cInt)Round(m_srcPoly[0].Y - m_normals[0].Y * delta));
+ m_destPoly.push_back(pt1);
+ pt1 = IntPoint((cInt)Round(m_srcPoly[0].X + m_normals[0].X * delta),
+ (cInt)Round(m_srcPoly[0].Y + m_normals[0].Y * delta));
+ m_destPoly.push_back(pt1);
+ } else {
+ k = 1;
+ m_sinA = 0;
+ if (node.m_endtype == etOpenSquare)
+ DoSquare(0, 1);
+ else
+ DoRound(0, 1);
+ }
+ m_destPolys.push_back(m_destPoly);
+ }
+ }
+}
+//------------------------------------------------------------------------------
+
+void ClipperOffset::OffsetPoint(int j, int &k, JoinType jointype) {
+ // cross product ...
+ m_sinA = (m_normals[k].X * m_normals[j].Y - m_normals[j].X * m_normals[k].Y);
+ if (std::fabs(m_sinA * m_delta) < 1.0) {
+ // dot product ...
+ double cosA =
+ (m_normals[k].X * m_normals[j].X + m_normals[j].Y * m_normals[k].Y);
+ if (cosA > 0) // angle => 0 degrees
+ {
+ m_destPoly.push_back(
+ IntPoint(Round(m_srcPoly[j].X + m_normals[k].X * m_delta),
+ Round(m_srcPoly[j].Y + m_normals[k].Y * m_delta)));
+ return;
+ }
+ // else angle => 180 degrees
+ } else if (m_sinA > 1.0)
+ m_sinA = 1.0;
+ else if (m_sinA < -1.0)
+ m_sinA = -1.0;
+
+ if (m_sinA * m_delta < 0) {
+ m_destPoly.push_back(
+ IntPoint(Round(m_srcPoly[j].X + m_normals[k].X * m_delta),
+ Round(m_srcPoly[j].Y + m_normals[k].Y * m_delta)));
+ m_destPoly.push_back(m_srcPoly[j]);
+ m_destPoly.push_back(
+ IntPoint(Round(m_srcPoly[j].X + m_normals[j].X * m_delta),
+ Round(m_srcPoly[j].Y + m_normals[j].Y * m_delta)));
+ } else
+ switch (jointype) {
+ case jtMiter: {
+ double r = 1 + (m_normals[j].X * m_normals[k].X +
+ m_normals[j].Y * m_normals[k].Y);
+ if (r >= m_miterLim)
+ DoMiter(j, k, r);
+ else
+ DoSquare(j, k);
+ break;
+ }
+ case jtSquare:
+ DoSquare(j, k);
+ break;
+ case jtRound:
+ DoRound(j, k);
+ break;
+ }
+ k = j;
+}
+//------------------------------------------------------------------------------
+
+void ClipperOffset::DoSquare(int j, int k) {
+ double dx = std::tan(std::atan2(m_sinA, m_normals[k].X * m_normals[j].X +
+ m_normals[k].Y * m_normals[j].Y) /
+ 4);
+ m_destPoly.push_back(IntPoint(
+ Round(m_srcPoly[j].X + m_delta * (m_normals[k].X - m_normals[k].Y * dx)),
+ Round(m_srcPoly[j].Y +
+ m_delta * (m_normals[k].Y + m_normals[k].X * dx))));
+ m_destPoly.push_back(IntPoint(
+ Round(m_srcPoly[j].X + m_delta * (m_normals[j].X + m_normals[j].Y * dx)),
+ Round(m_srcPoly[j].Y +
+ m_delta * (m_normals[j].Y - m_normals[j].X * dx))));
+}
+//------------------------------------------------------------------------------
+
+void ClipperOffset::DoMiter(int j, int k, double r) {
+ double q = m_delta / r;
+ m_destPoly.push_back(
+ IntPoint(Round(m_srcPoly[j].X + (m_normals[k].X + m_normals[j].X) * q),
+ Round(m_srcPoly[j].Y + (m_normals[k].Y + m_normals[j].Y) * q)));
+}
+//------------------------------------------------------------------------------
+
+void ClipperOffset::DoRound(int j, int k) {
+ double a = std::atan2(m_sinA, m_normals[k].X * m_normals[j].X +
+ m_normals[k].Y * m_normals[j].Y);
+ int steps = std::max((int)Round(m_StepsPerRad * std::fabs(a)), 1);
+
+ double X = m_normals[k].X, Y = m_normals[k].Y, X2;
+ for (int i = 0; i < steps; ++i) {
+ m_destPoly.push_back(IntPoint(Round(m_srcPoly[j].X + X * m_delta),
+ Round(m_srcPoly[j].Y + Y * m_delta)));
+ X2 = X;
+ X = X * m_cos - m_sin * Y;
+ Y = X2 * m_sin + Y * m_cos;
+ }
+ m_destPoly.push_back(
+ IntPoint(Round(m_srcPoly[j].X + m_normals[j].X * m_delta),
+ Round(m_srcPoly[j].Y + m_normals[j].Y * m_delta)));
+}
+
+//------------------------------------------------------------------------------
+// Miscellaneous public functions
+//------------------------------------------------------------------------------
+
+void Clipper::DoSimplePolygons() {
+ PolyOutList::size_type i = 0;
+ while (i < m_PolyOuts.size()) {
+ OutRec *outrec = m_PolyOuts[i++];
+ OutPt *op = outrec->Pts;
+ if (!op || outrec->IsOpen)
+ continue;
+ do // for each Pt in Polygon until duplicate found do ...
+ {
+ OutPt *op2 = op->Next;
+ while (op2 != outrec->Pts) {
+ if ((op->Pt == op2->Pt) && op2->Next != op && op2->Prev != op) {
+ // split the polygon into two ...
+ OutPt *op3 = op->Prev;
+ OutPt *op4 = op2->Prev;
+ op->Prev = op4;
+ op4->Next = op;
+ op2->Prev = op3;
+ op3->Next = op2;
+
+ outrec->Pts = op;
+ OutRec *outrec2 = CreateOutRec();
+ outrec2->Pts = op2;
+ UpdateOutPtIdxs(*outrec2);
+ if (Poly2ContainsPoly1(outrec2->Pts, outrec->Pts)) {
+ // OutRec2 is contained by OutRec1 ...
+ outrec2->IsHole = !outrec->IsHole;
+ outrec2->FirstLeft = outrec;
+ if (m_UsingPolyTree)
+ FixupFirstLefts2(outrec2, outrec);
+ } else if (Poly2ContainsPoly1(outrec->Pts, outrec2->Pts)) {
+ // OutRec1 is contained by OutRec2 ...
+ outrec2->IsHole = outrec->IsHole;
+ outrec->IsHole = !outrec2->IsHole;
+ outrec2->FirstLeft = outrec->FirstLeft;
+ outrec->FirstLeft = outrec2;
+ if (m_UsingPolyTree)
+ FixupFirstLefts2(outrec, outrec2);
+ } else {
+ // the 2 polygons are separate ...
+ outrec2->IsHole = outrec->IsHole;
+ outrec2->FirstLeft = outrec->FirstLeft;
+ if (m_UsingPolyTree)
+ FixupFirstLefts1(outrec, outrec2);
+ }
+ op2 = op; // ie get ready for the Next iteration
+ }
+ op2 = op2->Next;
+ }
+ op = op->Next;
+ } while (op != outrec->Pts);
+ }
+}
+//------------------------------------------------------------------------------
+
+void ReversePath(Path &p) { std::reverse(p.begin(), p.end()); }
+//------------------------------------------------------------------------------
+
+void ReversePaths(Paths &p) {
+ for (Paths::size_type i = 0; i < p.size(); ++i)
+ ReversePath(p[i]);
+}
+//------------------------------------------------------------------------------
+
+void SimplifyPolygon(const Path &in_poly, Paths &out_polys,
+ PolyFillType fillType) {
+ Clipper c;
+ c.StrictlySimple(true);
+ c.AddPath(in_poly, ptSubject, true);
+ c.Execute(ctUnion, out_polys, fillType, fillType);
+}
+//------------------------------------------------------------------------------
+
+void SimplifyPolygons(const Paths &in_polys, Paths &out_polys,
+ PolyFillType fillType) {
+ Clipper c;
+ c.StrictlySimple(true);
+ c.AddPaths(in_polys, ptSubject, true);
+ c.Execute(ctUnion, out_polys, fillType, fillType);
+}
+//------------------------------------------------------------------------------
+
+void SimplifyPolygons(Paths &polys, PolyFillType fillType) {
+ SimplifyPolygons(polys, polys, fillType);
+}
+//------------------------------------------------------------------------------
+
+inline double DistanceSqrd(const IntPoint &pt1, const IntPoint &pt2) {
+ double Dx = ((double)pt1.X - pt2.X);
+ double dy = ((double)pt1.Y - pt2.Y);
+ return (Dx * Dx + dy * dy);
+}
+//------------------------------------------------------------------------------
+
+double DistanceFromLineSqrd(const IntPoint &pt, const IntPoint &ln1,
+ const IntPoint &ln2) {
+ // The equation of a line in general form (Ax + By + C = 0)
+ // given 2 points (x¹,y¹) & (x²,y²) is ...
+ //(y¹ - y²)x + (x² - x¹)y + (y² - y¹)x¹ - (x² - x¹)y¹ = 0
+ // A = (y¹ - y²); B = (x² - x¹); C = (y² - y¹)x¹ - (x² - x¹)y¹
+ // perpendicular distance of point (x³,y³) = (Ax³ + By³ + C)/Sqrt(A² + B²)
+ // see http://en.wikipedia.org/wiki/Perpendicular_distance
+ double A = double(ln1.Y - ln2.Y);
+ double B = double(ln2.X - ln1.X);
+ double C = A * ln1.X + B * ln1.Y;
+ C = A * pt.X + B * pt.Y - C;
+ return (C * C) / (A * A + B * B);
+}
+//---------------------------------------------------------------------------
+
+bool SlopesNearCollinear(const IntPoint &pt1, const IntPoint &pt2,
+ const IntPoint &pt3, double distSqrd) {
+ // this function is more accurate when the point that's geometrically
+ // between the other 2 points is the one that's tested for distance.
+ // ie makes it more likely to pick up 'spikes' ...
+ if (Abs(pt1.X - pt2.X) > Abs(pt1.Y - pt2.Y)) {
+ if ((pt1.X > pt2.X) == (pt1.X < pt3.X))
+ return DistanceFromLineSqrd(pt1, pt2, pt3) < distSqrd;
+ else if ((pt2.X > pt1.X) == (pt2.X < pt3.X))
+ return DistanceFromLineSqrd(pt2, pt1, pt3) < distSqrd;
+ else
+ return DistanceFromLineSqrd(pt3, pt1, pt2) < distSqrd;
+ } else {
+ if ((pt1.Y > pt2.Y) == (pt1.Y < pt3.Y))
+ return DistanceFromLineSqrd(pt1, pt2, pt3) < distSqrd;
+ else if ((pt2.Y > pt1.Y) == (pt2.Y < pt3.Y))
+ return DistanceFromLineSqrd(pt2, pt1, pt3) < distSqrd;
+ else
+ return DistanceFromLineSqrd(pt3, pt1, pt2) < distSqrd;
+ }
+}
+//------------------------------------------------------------------------------
+
+bool PointsAreClose(IntPoint pt1, IntPoint pt2, double distSqrd) {
+ double Dx = (double)pt1.X - pt2.X;
+ double dy = (double)pt1.Y - pt2.Y;
+ return ((Dx * Dx) + (dy * dy) <= distSqrd);
+}
+//------------------------------------------------------------------------------
+
+OutPt *ExcludeOp(OutPt *op) {
+ OutPt *result = op->Prev;
+ result->Next = op->Next;
+ op->Next->Prev = result;
+ result->Idx = 0;
+ return result;
+}
+//------------------------------------------------------------------------------
+
+void CleanPolygon(const Path &in_poly, Path &out_poly, double distance) {
+ // distance = proximity in units/pixels below which vertices
+ // will be stripped. Default ~= sqrt(2).
+
+ size_t size = in_poly.size();
+
+ if (size == 0) {
+ out_poly.clear();
+ return;
+ }
+
+ OutPt *outPts = new OutPt[size];
+ for (size_t i = 0; i < size; ++i) {
+ outPts[i].Pt = in_poly[i];
+ outPts[i].Next = &outPts[(i + 1) % size];
+ outPts[i].Next->Prev = &outPts[i];
+ outPts[i].Idx = 0;
+ }
+
+ double distSqrd = distance * distance;
+ OutPt *op = &outPts[0];
+ while (op->Idx == 0 && op->Next != op->Prev) {
+ if (PointsAreClose(op->Pt, op->Prev->Pt, distSqrd)) {
+ op = ExcludeOp(op);
+ size--;
+ } else if (PointsAreClose(op->Prev->Pt, op->Next->Pt, distSqrd)) {
+ ExcludeOp(op->Next);
+ op = ExcludeOp(op);
+ size -= 2;
+ } else if (SlopesNearCollinear(op->Prev->Pt, op->Pt, op->Next->Pt,
+ distSqrd)) {
+ op = ExcludeOp(op);
+ size--;
+ } else {
+ op->Idx = 1;
+ op = op->Next;
+ }
+ }
+
+ if (size < 3)
+ size = 0;
+ out_poly.resize(size);
+ for (size_t i = 0; i < size; ++i) {
+ out_poly[i] = op->Pt;
+ op = op->Next;
+ }
+ delete[] outPts;
+}
+//------------------------------------------------------------------------------
+
+void CleanPolygon(Path &poly, double distance) {
+ CleanPolygon(poly, poly, distance);
+}
+//------------------------------------------------------------------------------
+
+void CleanPolygons(const Paths &in_polys, Paths &out_polys, double distance) {
+ out_polys.resize(in_polys.size());
+ for (Paths::size_type i = 0; i < in_polys.size(); ++i)
+ CleanPolygon(in_polys[i], out_polys[i], distance);
+}
+//------------------------------------------------------------------------------
+
+void CleanPolygons(Paths &polys, double distance) {
+ CleanPolygons(polys, polys, distance);
+}
+//------------------------------------------------------------------------------
+
+void Minkowski(const Path &poly, const Path &path, Paths &solution, bool isSum,
+ bool isClosed) {
+ int delta = (isClosed ? 1 : 0);
+ size_t polyCnt = poly.size();
+ size_t pathCnt = path.size();
+ Paths pp;
+ pp.reserve(pathCnt);
+ if (isSum)
+ for (size_t i = 0; i < pathCnt; ++i) {
+ Path p;
+ p.reserve(polyCnt);
+ for (size_t j = 0; j < poly.size(); ++j)
+ p.push_back(IntPoint(path[i].X + poly[j].X, path[i].Y + poly[j].Y));
+ pp.push_back(p);
+ }
+ else
+ for (size_t i = 0; i < pathCnt; ++i) {
+ Path p;
+ p.reserve(polyCnt);
+ for (size_t j = 0; j < poly.size(); ++j)
+ p.push_back(IntPoint(path[i].X - poly[j].X, path[i].Y - poly[j].Y));
+ pp.push_back(p);
+ }
+
+ solution.clear();
+ solution.reserve((pathCnt + delta) * (polyCnt + 1));
+ for (size_t i = 0; i < pathCnt - 1 + delta; ++i)
+ for (size_t j = 0; j < polyCnt; ++j) {
+ Path quad;
+ quad.reserve(4);
+ quad.push_back(pp[i % pathCnt][j % polyCnt]);
+ quad.push_back(pp[(i + 1) % pathCnt][j % polyCnt]);
+ quad.push_back(pp[(i + 1) % pathCnt][(j + 1) % polyCnt]);
+ quad.push_back(pp[i % pathCnt][(j + 1) % polyCnt]);
+ if (!Orientation(quad))
+ ReversePath(quad);
+ solution.push_back(quad);
+ }
+}
+//------------------------------------------------------------------------------
+
+void MinkowskiSum(const Path &pattern, const Path &path, Paths &solution,
+ bool pathIsClosed) {
+ Minkowski(pattern, path, solution, true, pathIsClosed);
+ Clipper c;
+ c.AddPaths(solution, ptSubject, true);
+ c.Execute(ctUnion, solution, pftNonZero, pftNonZero);
+}
+//------------------------------------------------------------------------------
+
+void TranslatePath(const Path &input, Path &output, const IntPoint delta) {
+ // precondition: input != output
+ output.resize(input.size());
+ for (size_t i = 0; i < input.size(); ++i)
+ output[i] = IntPoint(input[i].X + delta.X, input[i].Y + delta.Y);
+}
+//------------------------------------------------------------------------------
+
+void MinkowskiSum(const Path &pattern, const Paths &paths, Paths &solution,
+ bool pathIsClosed) {
+ Clipper c;
+ for (size_t i = 0; i < paths.size(); ++i) {
+ Paths tmp;
+ Minkowski(pattern, paths[i], tmp, true, pathIsClosed);
+ c.AddPaths(tmp, ptSubject, true);
+ if (pathIsClosed) {
+ Path tmp2;
+ TranslatePath(paths[i], tmp2, pattern[0]);
+ c.AddPath(tmp2, ptClip, true);
+ }
+ }
+ c.Execute(ctUnion, solution, pftNonZero, pftNonZero);
+}
+//------------------------------------------------------------------------------
+
+void MinkowskiDiff(const Path &poly1, const Path &poly2, Paths &solution) {
+ Minkowski(poly1, poly2, solution, false, true);
+ Clipper c;
+ c.AddPaths(solution, ptSubject, true);
+ c.Execute(ctUnion, solution, pftNonZero, pftNonZero);
+}
+//------------------------------------------------------------------------------
+
+enum NodeType { ntAny, ntOpen, ntClosed };
+
+void AddPolyNodeToPaths(const PolyNode &polynode, NodeType nodetype,
+ Paths &paths) {
+ bool match = true;
+ if (nodetype == ntClosed)
+ match = !polynode.IsOpen();
+ else if (nodetype == ntOpen)
+ return;
+
+ if (!polynode.Contour.empty() && match)
+ paths.push_back(polynode.Contour);
+ for (int i = 0; i < polynode.ChildCount(); ++i)
+ AddPolyNodeToPaths(*polynode.Childs[i], nodetype, paths);
+}
+//------------------------------------------------------------------------------
+
+void PolyTreeToPaths(const PolyTree &polytree, Paths &paths) {
+ paths.resize(0);
+ paths.reserve(polytree.Total());
+ AddPolyNodeToPaths(polytree, ntAny, paths);
+}
+//------------------------------------------------------------------------------
+
+void ClosedPathsFromPolyTree(const PolyTree &polytree, Paths &paths) {
+ paths.resize(0);
+ paths.reserve(polytree.Total());
+ AddPolyNodeToPaths(polytree, ntClosed, paths);
+}
+//------------------------------------------------------------------------------
+
+void OpenPathsFromPolyTree(PolyTree &polytree, Paths &paths) {
+ paths.resize(0);
+ paths.reserve(polytree.Total());
+ // Open paths are top level only, so ...
+ for (int i = 0; i < polytree.ChildCount(); ++i)
+ if (polytree.Childs[i]->IsOpen())
+ paths.push_back(polytree.Childs[i]->Contour);
+}
+//------------------------------------------------------------------------------
+
+std::ostream &operator<<(std::ostream &s, const IntPoint &p) {
+ s << "(" << p.X << "," << p.Y << ")";
+ return s;
+}
+//------------------------------------------------------------------------------
+
+std::ostream &operator<<(std::ostream &s, const Path &p) {
+ if (p.empty())
+ return s;
+ Path::size_type last = p.size() - 1;
+ for (Path::size_type i = 0; i < last; i++)
+ s << "(" << p[i].X << "," << p[i].Y << "), ";
+ s << "(" << p[last].X << "," << p[last].Y << ")\n";
+ return s;
+}
+//------------------------------------------------------------------------------
+
+std::ostream &operator<<(std::ostream &s, const Paths &p) {
+ for (Paths::size_type i = 0; i < p.size(); i++)
+ s << p[i];
+ s << "\n";
+ return s;
+}
+//------------------------------------------------------------------------------
+
+} // namespace ClipperLib
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/ThirdParty/Clipper1/clipper.hpp b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/ThirdParty/Clipper1/clipper.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..68bc80bdc77fa810f99ae68487feb6a5af9f4a91
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/ThirdParty/Clipper1/clipper.hpp
@@ -0,0 +1,419 @@
+/*******************************************************************************
+ * *
+ * Author : Angus Johnson * Version : 6.4.2 * Date : 27 February
+ *2017 * Website :
+ *http://www.angusj.com * Copyright :
+ *Angus Johnson 2010-2017 *
+ * *
+ * License: * Use, modification & distribution is subject to Boost Software
+ *License Ver 1. * http://www.boost.org/LICENSE_1_0.txt *
+ * *
+ * Attributions: * The code in this library is an extension of Bala Vatti's
+ *clipping algorithm: * "A generic solution to polygon clipping" *
+ * Communications of the ACM, Vol 35, Issue 7 (July 1992) pp 56-63. *
+ * http://portal.acm.org/citation.cfm?id=129906 *
+ * *
+ * Computer graphics and geometric modeling: implementation and algorithms * By
+ *Max K. Agoston *
+ * Springer; 1 edition (January 4, 2005) *
+ * http://books.google.com/books?q=vatti+clipping+agoston *
+ * *
+ * See also: * "Polygon Offsetting by Computing Winding Numbers" * Paper no.
+ *DETC2005-85513 pp. 565-575 * ASME 2005
+ *International Design Engineering Technical Conferences * and
+ *Computers and Information in Engineering Conference (IDETC/CIE2005) *
+ * September 24-28, 2005 , Long Beach, California, USA *
+ * http://www.me.berkeley.edu/~mcmains/pubs/DAC05OffsetPolygon.pdf *
+ * *
+ *******************************************************************************/
+
+#ifndef clipper_hpp
+#define clipper_hpp
+
+#define CLIPPER_VERSION "6.4.2"
+
+// use_int32: When enabled 32bit ints are used instead of 64bit ints. This
+// improve performance but coordinate values are limited to the range +/- 46340
+// #define use_int32
+
+// use_xyz: adds a Z member to IntPoint. Adds a minor cost to perfomance.
+// #define use_xyz
+
+// use_lines: Enables line clipping. Adds a very minor cost to performance.
+#define use_lines
+
+// use_deprecated: Enables temporary support for the obsolete functions
+// #define use_deprecated
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace ClipperLib {
+
+enum ClipType { ctIntersection, ctUnion, ctDifference, ctXor };
+enum PolyType { ptSubject, ptClip };
+// By far the most widely used winding rules for polygon filling are
+// EvenOdd & NonZero (GDI, GDI+, XLib, OpenGL, Cairo, AGG, Quartz, SVG, Gr32)
+// Others rules include Positive, Negative and ABS_GTR_EQ_TWO (only in OpenGL)
+// see http://glprogramming.com/red/chapter11.html
+enum PolyFillType { pftEvenOdd, pftNonZero, pftPositive, pftNegative };
+
+#ifdef use_int32
+typedef int cInt;
+static cInt const loRange = 0x7FFF;
+static cInt const hiRange = 0x7FFF;
+#else
+typedef signed long long cInt;
+static cInt const loRange = 0x3FFFFFFF;
+static cInt const hiRange = 0x3FFFFFFFFFFFFFFFLL;
+typedef signed long long long64; // used by Int128 class
+typedef unsigned long long ulong64;
+
+#endif
+
+struct IntPoint {
+ cInt X;
+ cInt Y;
+#ifdef use_xyz
+ cInt Z;
+ IntPoint(cInt x = 0, cInt y = 0, cInt z = 0) : X(x), Y(y), Z(z){};
+#else
+ IntPoint(cInt x = 0, cInt y = 0) : X(x), Y(y){};
+#endif
+
+ friend inline bool operator==(const IntPoint &a, const IntPoint &b) {
+ return a.X == b.X && a.Y == b.Y;
+ }
+ friend inline bool operator!=(const IntPoint &a, const IntPoint &b) {
+ return a.X != b.X || a.Y != b.Y;
+ }
+};
+//------------------------------------------------------------------------------
+
+typedef std::vector Path;
+typedef std::vector Paths;
+
+inline Path &operator<<(Path &poly, const IntPoint &p) {
+ poly.push_back(p);
+ return poly;
+}
+inline Paths &operator<<(Paths &polys, const Path &p) {
+ polys.push_back(p);
+ return polys;
+}
+
+std::ostream &operator<<(std::ostream &s, const IntPoint &p);
+std::ostream &operator<<(std::ostream &s, const Path &p);
+std::ostream &operator<<(std::ostream &s, const Paths &p);
+
+struct DoublePoint {
+ double X;
+ double Y;
+ DoublePoint(double x = 0, double y = 0) : X(x), Y(y) {}
+ DoublePoint(IntPoint ip) : X((double)ip.X), Y((double)ip.Y) {}
+};
+//------------------------------------------------------------------------------
+
+#ifdef use_xyz
+typedef void (*ZFillCallback)(IntPoint &e1bot, IntPoint &e1top, IntPoint &e2bot,
+ IntPoint &e2top, IntPoint &pt);
+#endif
+
+enum InitOptions {
+ ioReverseSolution = 1,
+ ioStrictlySimple = 2,
+ ioPreserveCollinear = 4
+};
+enum JoinType { jtSquare, jtRound, jtMiter };
+enum EndType {
+ etClosedPolygon,
+ etClosedLine,
+ etOpenButt,
+ etOpenSquare,
+ etOpenRound
+};
+
+class PolyNode;
+typedef std::vector PolyNodes;
+
+class PolyNode {
+public:
+ PolyNode();
+ virtual ~PolyNode(){};
+ Path Contour;
+ PolyNodes Childs;
+ PolyNode *Parent;
+ PolyNode *GetNext() const;
+ bool IsHole() const;
+ bool IsOpen() const;
+ int ChildCount() const;
+
+private:
+ // PolyNode& operator =(PolyNode& other);
+ unsigned Index; // node index in Parent.Childs
+ bool m_IsOpen;
+ JoinType m_jointype;
+ EndType m_endtype;
+ PolyNode *GetNextSiblingUp() const;
+ void AddChild(PolyNode &child);
+ friend class Clipper; // to access Index
+ friend class ClipperOffset;
+};
+
+class PolyTree : public PolyNode {
+public:
+ ~PolyTree() { Clear(); };
+ PolyNode *GetFirst() const;
+ void Clear();
+ int Total() const;
+
+private:
+ // PolyTree& operator =(PolyTree& other);
+ PolyNodes AllNodes;
+ friend class Clipper; // to access AllNodes
+};
+
+bool Orientation(const Path &poly);
+double Area(const Path &poly);
+int PointInPolygon(const IntPoint &pt, const Path &path);
+
+void SimplifyPolygon(const Path &in_poly, Paths &out_polys,
+ PolyFillType fillType = pftEvenOdd);
+void SimplifyPolygons(const Paths &in_polys, Paths &out_polys,
+ PolyFillType fillType = pftEvenOdd);
+void SimplifyPolygons(Paths &polys, PolyFillType fillType = pftEvenOdd);
+
+void CleanPolygon(const Path &in_poly, Path &out_poly, double distance = 1.415);
+void CleanPolygon(Path &poly, double distance = 1.415);
+void CleanPolygons(const Paths &in_polys, Paths &out_polys,
+ double distance = 1.415);
+void CleanPolygons(Paths &polys, double distance = 1.415);
+
+void MinkowskiSum(const Path &pattern, const Path &path, Paths &solution,
+ bool pathIsClosed);
+void MinkowskiSum(const Path &pattern, const Paths &paths, Paths &solution,
+ bool pathIsClosed);
+void MinkowskiDiff(const Path &poly1, const Path &poly2, Paths &solution);
+
+void PolyTreeToPaths(const PolyTree &polytree, Paths &paths);
+void ClosedPathsFromPolyTree(const PolyTree &polytree, Paths &paths);
+void OpenPathsFromPolyTree(PolyTree &polytree, Paths &paths);
+
+void ReversePath(Path &p);
+void ReversePaths(Paths &p);
+
+struct IntRect {
+ cInt left;
+ cInt top;
+ cInt right;
+ cInt bottom;
+};
+
+// enums that are used internally ...
+enum EdgeSide { esLeft = 1, esRight = 2 };
+
+// forward declarations (for stuff used internally) ...
+struct TEdge;
+struct IntersectNode;
+struct LocalMinimum;
+struct OutPt;
+struct OutRec;
+struct Join;
+
+typedef std::vector PolyOutList;
+typedef std::vector EdgeList;
+typedef std::vector JoinList;
+typedef std::vector IntersectList;
+
+//------------------------------------------------------------------------------
+
+// ClipperBase is the ancestor to the Clipper class. It should not be
+// instantiated directly. This class simply abstracts the conversion of sets of
+// polygon coordinates into edge objects that are stored in a LocalMinima list.
+class ClipperBase {
+public:
+ ClipperBase();
+ virtual ~ClipperBase();
+ virtual bool AddPath(const Path &pg, PolyType PolyTyp, bool Closed);
+ bool AddPaths(const Paths &ppg, PolyType PolyTyp, bool Closed);
+ virtual void Clear();
+ IntRect GetBounds();
+ bool PreserveCollinear() { return m_PreserveCollinear; };
+ void PreserveCollinear(bool value) { m_PreserveCollinear = value; };
+
+protected:
+ void DisposeLocalMinimaList();
+ TEdge *AddBoundsToLML(TEdge *e, bool IsClosed);
+ virtual void Reset();
+ TEdge *ProcessBound(TEdge *E, bool IsClockwise);
+ void InsertScanbeam(const cInt Y);
+ bool PopScanbeam(cInt &Y);
+ bool LocalMinimaPending();
+ bool PopLocalMinima(cInt Y, const LocalMinimum *&locMin);
+ OutRec *CreateOutRec();
+ void DisposeAllOutRecs();
+ void DisposeOutRec(PolyOutList::size_type index);
+ void SwapPositionsInAEL(TEdge *edge1, TEdge *edge2);
+ void DeleteFromAEL(TEdge *e);
+ void UpdateEdgeIntoAEL(TEdge *&e);
+
+ typedef std::vector MinimaList;
+ MinimaList::iterator m_CurrentLM;
+ MinimaList m_MinimaList;
+
+ bool m_UseFullRange;
+ EdgeList m_edges;
+ bool m_PreserveCollinear;
+ bool m_HasOpenPaths;
+ PolyOutList m_PolyOuts;
+ TEdge *m_ActiveEdges;
+
+ typedef std::priority_queue ScanbeamList;
+ ScanbeamList m_Scanbeam;
+};
+//------------------------------------------------------------------------------
+
+class Clipper : public virtual ClipperBase {
+public:
+ Clipper(int initOptions = 0);
+ bool Execute(ClipType clipType, Paths &solution,
+ PolyFillType fillType = pftEvenOdd);
+ bool Execute(ClipType clipType, Paths &solution, PolyFillType subjFillType,
+ PolyFillType clipFillType);
+ bool Execute(ClipType clipType, PolyTree &polytree,
+ PolyFillType fillType = pftEvenOdd);
+ bool Execute(ClipType clipType, PolyTree &polytree, PolyFillType subjFillType,
+ PolyFillType clipFillType);
+ bool ReverseSolution() { return m_ReverseOutput; };
+ void ReverseSolution(bool value) { m_ReverseOutput = value; };
+ bool StrictlySimple() { return m_StrictSimple; };
+ void StrictlySimple(bool value) { m_StrictSimple = value; };
+ // set the callback function for z value filling on intersections (otherwise
+ // Z is 0)
+#ifdef use_xyz
+ void ZFillFunction(ZFillCallback zFillFunc);
+#endif
+protected:
+ virtual bool ExecuteInternal();
+
+private:
+ JoinList m_Joins;
+ JoinList m_GhostJoins;
+ IntersectList m_IntersectList;
+ ClipType m_ClipType;
+ typedef std::list MaximaList;
+ MaximaList m_Maxima;
+ TEdge *m_SortedEdges;
+ bool m_ExecuteLocked;
+ PolyFillType m_ClipFillType;
+ PolyFillType m_SubjFillType;
+ bool m_ReverseOutput;
+ bool m_UsingPolyTree;
+ bool m_StrictSimple;
+#ifdef use_xyz
+ ZFillCallback m_ZFill; // custom callback
+#endif
+ void SetWindingCount(TEdge &edge);
+ bool IsEvenOddFillType(const TEdge &edge) const;
+ bool IsEvenOddAltFillType(const TEdge &edge) const;
+ void InsertLocalMinimaIntoAEL(const cInt botY);
+ void InsertEdgeIntoAEL(TEdge *edge, TEdge *startEdge);
+ void AddEdgeToSEL(TEdge *edge);
+ bool PopEdgeFromSEL(TEdge *&edge);
+ void CopyAELToSEL();
+ void DeleteFromSEL(TEdge *e);
+ void SwapPositionsInSEL(TEdge *edge1, TEdge *edge2);
+ bool IsContributing(const TEdge &edge) const;
+ bool IsTopHorz(const cInt XPos);
+ void DoMaxima(TEdge *e);
+ void ProcessHorizontals();
+ void ProcessHorizontal(TEdge *horzEdge);
+ void AddLocalMaxPoly(TEdge *e1, TEdge *e2, const IntPoint &pt);
+ OutPt *AddLocalMinPoly(TEdge *e1, TEdge *e2, const IntPoint &pt);
+ OutRec *GetOutRec(int idx);
+ void AppendPolygon(TEdge *e1, TEdge *e2);
+ void IntersectEdges(TEdge *e1, TEdge *e2, IntPoint &pt);
+ OutPt *AddOutPt(TEdge *e, const IntPoint &pt);
+ OutPt *GetLastOutPt(TEdge *e);
+ bool ProcessIntersections(const cInt topY);
+ void BuildIntersectList(const cInt topY);
+ void ProcessIntersectList();
+ void ProcessEdgesAtTopOfScanbeam(const cInt topY);
+ void BuildResult(Paths &polys);
+ void BuildResult2(PolyTree &polytree);
+ void SetHoleState(TEdge *e, OutRec *outrec);
+ void DisposeIntersectNodes();
+ bool FixupIntersectionOrder();
+ void FixupOutPolygon(OutRec &outrec);
+ void FixupOutPolyline(OutRec &outrec);
+ bool IsHole(TEdge *e);
+ bool FindOwnerFromSplitRecs(OutRec &outRec, OutRec *&currOrfl);
+ void FixHoleLinkage(OutRec &outrec);
+ void AddJoin(OutPt *op1, OutPt *op2, const IntPoint offPt);
+ void ClearJoins();
+ void ClearGhostJoins();
+ void AddGhostJoin(OutPt *op, const IntPoint offPt);
+ bool JoinPoints(Join *j, OutRec *outRec1, OutRec *outRec2);
+ void JoinCommonEdges();
+ void DoSimplePolygons();
+ void FixupFirstLefts1(OutRec *OldOutRec, OutRec *NewOutRec);
+ void FixupFirstLefts2(OutRec *InnerOutRec, OutRec *OuterOutRec);
+ void FixupFirstLefts3(OutRec *OldOutRec, OutRec *NewOutRec);
+#ifdef use_xyz
+ void SetZ(IntPoint &pt, TEdge &e1, TEdge &e2);
+#endif
+};
+//------------------------------------------------------------------------------
+
+class ClipperOffset {
+public:
+ ClipperOffset(double miterLimit = 2.0, double roundPrecision = 0.25);
+ ~ClipperOffset();
+ void AddPath(const Path &path, JoinType joinType, EndType endType);
+ void AddPaths(const Paths &paths, JoinType joinType, EndType endType);
+ void Execute(Paths &solution, double delta);
+ void Execute(PolyTree &solution, double delta);
+ void Clear();
+ double MiterLimit;
+ double ArcTolerance;
+
+private:
+ Paths m_destPolys;
+ Path m_srcPoly;
+ Path m_destPoly;
+ std::vector m_normals;
+ double m_delta, m_sinA, m_sin, m_cos;
+ double m_miterLim, m_StepsPerRad;
+ IntPoint m_lowest;
+ PolyNode m_polyNodes;
+
+ void FixOrientations();
+ void DoOffset(double delta);
+ void OffsetPoint(int j, int &k, JoinType jointype);
+ void DoSquare(int j, int k);
+ void DoMiter(int j, int k, double r);
+ void DoRound(int j, int k);
+};
+//------------------------------------------------------------------------------
+
+class clipperException : public std::exception {
+public:
+ clipperException(const char *description) : m_descr(description) {}
+ virtual ~clipperException() throw() {}
+ virtual const char *what() const throw() { return m_descr.c_str(); }
+
+private:
+ std::string m_descr;
+};
+//------------------------------------------------------------------------------
+
+} // namespace ClipperLib
+
+#endif // clipper_hpp
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/ViewModels/OCRViewModel.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/ViewModels/OCRViewModel.swift
new file mode 100644
index 0000000000000000000000000000000000000000..5e405a8142045889992111a3a2e60b04e077a77a
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/ViewModels/OCRViewModel.swift
@@ -0,0 +1,275 @@
+// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import Foundation
+import UIKit
+
+// MARK: - App State
+
+/// The complete set of states the app can be in.
+/// Drives all UI rendering via a single `@Published` property on `OCRViewModel`.
+enum AppState: Equatable {
+ /// Models are being loaded from the bundle.
+ case loadingModels
+ /// Models are loaded and ready; waiting for the user to select an image.
+ case ready
+ /// An image is being processed by OCR.
+ case processing(UIImage)
+ /// OCR completed successfully with results and the processed image.
+ /// `runId` distinguishes successive successful runs for ``Equatable`` without requiring ``OCRRunResult`` to conform.
+ case results(OCRRunResult, UIImage, runId: UUID)
+ /// An error occurred during model loading or inference.
+ case error(AppError)
+
+ static func == (lhs: AppState, rhs: AppState) -> Bool {
+ switch (lhs, rhs) {
+ case (.loadingModels, .loadingModels): return true
+ case (.ready, .ready): return true
+ case (.processing, .processing): return true
+ case (.results(_, _, let runIdL), .results(_, _, let runIdR)): return runIdL == runIdR
+ case (.error(let a), .error(let b)): return a == b
+ default: return false
+ }
+ }
+}
+
+// MARK: - App Error
+
+/// User-facing errors with recovery guidance.
+enum AppError: Equatable, LocalizedError {
+ case modelLoadFailed(String)
+ case inferenceFailed(String)
+ case imageLoadFailed
+
+ var errorDescription: String? {
+ switch self {
+ case .modelLoadFailed(let msg): return msg
+ case .inferenceFailed(let msg): return msg
+ case .imageLoadFailed: return "Could not load the selected image. Try a different one."
+ }
+ }
+
+ /// Whether this error is recoverable by retrying model load (vs just selecting a new image).
+ var isModelError: Bool {
+ if case .modelLoadFailed = self { return true }
+ return false
+ }
+}
+
+// MARK: - OCRViewModel
+
+/// Central view model that manages the complete app lifecycle.
+///
+/// State transitions:
+/// ```
+/// loadingModels --> ready --> processing --> results
+/// | ^ | |
+/// v | v v
+/// error ----retry----> error ready (reset)
+/// ```
+///
+/// All state mutations happen on `@MainActor` through the single
+/// `@Published var state` property, giving SwiftUI a single source of truth.
+@MainActor
+class OCRViewModel: ObservableObject {
+ @Published var state: AppState = .loadingModels
+ @Published var copiedFeedback: Bool = false
+ @Published var runtimeParams = OCRRuntimeParams.noOverrides
+ /// ONNX Runtime presets and tuning shown in UI (draft). Use **Apply & reload** in Advanced settings to reload sessions (`loadModels()`).
+ @Published var ortPrimaryExecutionProvider: ORTPrimaryExecutionProvider = .cpu
+ @Published var ortSessionTuning = ORTSessionTuningOptions.default
+ /// Snapshot of Ort settings actually used by the loaded ``ocrEngine``.
+ private var ortLoadedPrimaryExecutionProvider: ORTPrimaryExecutionProvider = .cpu
+ private var ortLoadedSessionTuning: ORTSessionTuningOptions = .default
+ @Published private(set) var resolvedRuntimeBaseline: ResolvedOCRRuntimeParams?
+
+ private var sessionManager: ORTSessionManager?
+ private var ocrEngine: OCREngine?
+ /// Last image run through OCR (normalized orientation), for **Re-run OCR** without re-picking.
+ private var lastNormalizedImage: UIImage?
+ /// OCR runtime parameters used for the last successful run (drives ``hasPendingOcrChanges``).
+ private var lastRunRuntimeParams: OCRRuntimeParams?
+
+ /// Bundled sample resource names (stem or `stem.ext`) for ``selectSampleImage(named:)``.
+ let sampleImageNames: [String] = [DemoAssets.defaultSampleImageStem]
+
+ /// Draft Ort settings differ from the configuration used by loaded sessions.
+ var hasUnappliedOrtSettings: Bool {
+ ortPrimaryExecutionProvider != ortLoadedPrimaryExecutionProvider
+ || ortSessionTuning != ortLoadedSessionTuning
+ }
+
+ /// `runtimeParams` differs from the last successful OCR run (only meaningful in ``AppState/results``).
+ var hasPendingOcrChanges: Bool {
+ guard case .results = state else { return false }
+ guard let last = lastRunRuntimeParams else { return false }
+ return runtimeParams != last
+ }
+
+ /// When `false`, disable Apply/Revert (reload would conflict with loading or OCR in flight).
+ var allowsOrtApplyControls: Bool {
+ switch state {
+ case .loadingModels, .processing: return false
+ case .ready, .results, .error: return true
+ }
+ }
+
+ /// Restores ONNX Runtime controls to match the currently loaded sessions.
+ func revertOrtDraftToLoaded() {
+ ortPrimaryExecutionProvider = ortLoadedPrimaryExecutionProvider
+ ortSessionTuning = ortLoadedSessionTuning
+ }
+
+ /// Photo picker or file load produced unusable image data.
+ func reportImageLoadFailed() {
+ state = .error(.imageLoadFailed)
+ }
+
+ // MARK: - Lifecycle
+
+ /// Load ORT models from the app bundle.
+ ///
+ /// Creates an `ORTSessionManager`, loads both detection and recognition models,
+ /// then creates the `OCREngine` for end-to-end OCR.
+ func loadModels() async {
+ state = .loadingModels
+ do {
+ let manager = ORTSessionManager()
+ try await manager.loadModels(executionProvider: ortPrimaryExecutionProvider, tuning: ortSessionTuning)
+ let engine = try OCREngine(sessionManager: manager)
+ self.sessionManager = manager
+ self.ocrEngine = engine
+ self.resolvedRuntimeBaseline = engine.baselineRuntimeDefaults()
+ ortLoadedPrimaryExecutionProvider = ortPrimaryExecutionProvider
+ ortLoadedSessionTuning = ortSessionTuning
+ state = .ready
+ } catch {
+ state = .error(.modelLoadFailed(error.localizedDescription))
+ }
+ }
+
+ // MARK: - Image Processing
+
+ /// Run full OCR on a user-selected image.
+ ///
+ /// Normalizes orientation first (camera photos often have EXIF rotation),
+ /// then delegates to `OCREngine.run(_:)`.
+ func processImage(_ uiImage: UIImage) async {
+ let normalized = normalizeOrientation(uiImage)
+ lastNormalizedImage = normalized
+ state = .processing(normalized)
+ guard let cgImage = normalized.cgImage else {
+ state = .error(.imageLoadFailed)
+ return
+ }
+ guard let engine = ocrEngine else {
+ state = .error(.modelLoadFailed("OCR engine is not ready."))
+ return
+ }
+ do {
+ let result = try await engine.run(cgImage, params: runtimeParams)
+ lastRunRuntimeParams = runtimeParams
+ state = .results(result, normalized, runId: UUID())
+ } catch {
+ lastNormalizedImage = nil
+ state = .error(.inferenceFailed(error.localizedDescription))
+ }
+ }
+
+ /// Runs OCR again on the **last image** using the current `runtimeParams` (e.g. after tuning sliders).
+ func rerunOCR() async {
+ guard let image = lastNormalizedImage else { return }
+ await processImage(image)
+ }
+
+ /// Load a bundled sample image by name and run OCR on it.
+ func selectSampleImage(named name: String) async {
+ guard let path = Bundle.main.path(forBundledImageNamed: name, subdirectory: "SampleImages") else {
+ state = .error(.imageLoadFailed)
+ return
+ }
+ do {
+ guard let uiFromFile = UIImage(contentsOfFile: path) else {
+ state = .error(.imageLoadFailed)
+ return
+ }
+ let normalized = normalizeOrientation(uiFromFile)
+ lastNormalizedImage = normalized
+ state = .processing(normalized)
+ guard let cgImage = normalized.cgImage else {
+ state = .error(.imageLoadFailed)
+ return
+ }
+ guard let engine = ocrEngine else {
+ state = .error(.modelLoadFailed("OCR engine is not ready."))
+ return
+ }
+ let result = try await engine.run(cgImage, params: runtimeParams)
+ lastRunRuntimeParams = runtimeParams
+ state = .results(result, normalized, runId: UUID())
+ } catch {
+ lastNormalizedImage = nil
+ state = .error(.inferenceFailed(error.localizedDescription))
+ }
+ }
+
+ // MARK: - Result Actions
+
+ /// Copy all recognized text to the system clipboard, with brief visual feedback.
+ func copyResultsToClipboard() {
+ guard case .results(let result, _, _) = state else { return }
+ let allText = result.results.map(\.text).joined(separator: "\n")
+ UIPasteboard.general.string = allText
+ copiedFeedback = true
+ Task {
+ try? await Task.sleep(nanoseconds: 2_000_000_000)
+ copiedFeedback = false
+ }
+ }
+
+ // MARK: - Error Recovery
+
+ /// Retry after an error. Model errors reload models; other errors return to ready state.
+ func retry() async {
+ guard case .error(let error) = state else { return }
+ if error.isModelError {
+ await loadModels()
+ } else {
+ state = .ready
+ }
+ }
+
+ /// Return to the ready state (e.g. after viewing results, user wants to pick a new image).
+ func reset() {
+ lastNormalizedImage = nil
+ lastRunRuntimeParams = nil
+ state = .ready
+ }
+}
+
+// MARK: - Image Orientation
+
+/// Normalize EXIF orientation by redrawing the image in `.up` orientation.
+///
+/// Camera photos from the photo picker often carry non-`.up` orientation metadata.
+/// CoreGraphics ignores EXIF orientation, so we must flatten it before OCR processing.
+private func normalizeOrientation(_ image: UIImage) -> UIImage {
+ guard image.imageOrientation != .up else { return image }
+ let format = UIGraphicsImageRendererFormat()
+ format.scale = image.scale
+ let renderer = UIGraphicsImageRenderer(size: image.size, format: format)
+ return renderer.image { _ in
+ image.draw(in: CGRect(origin: .zero, size: image.size))
+ }
+}
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/EngineSettingsSheet.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/EngineSettingsSheet.swift
new file mode 100644
index 0000000000000000000000000000000000000000..3796d2d3067f6026fa993dbb0ca36a41c67c4e9d
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/EngineSettingsSheet.swift
@@ -0,0 +1,106 @@
+// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import SwiftUI
+
+/// Advanced ONNX Runtime and OCR parameters, presented as a sheet from the main screen toolbar.
+struct EngineSettingsSheet: View {
+ @Environment(\.dismiss) private var dismiss
+ @ObservedObject var viewModel: OCRViewModel
+ @State private var ortSessionReloadConfirmed = false
+
+ var body: some View {
+ NavigationStack {
+ ScrollView {
+ VStack(alignment: .leading, spacing: 20) {
+ DemoSectionHeader(
+ title: "ONNX Runtime",
+ subtitle: "Choose execution provider and options, then reload to apply."
+ )
+ DemoCard {
+ OnnxRuntimeSettingsPanel(
+ primaryExecutionProvider: $viewModel.ortPrimaryExecutionProvider,
+ tuning: $viewModel.ortSessionTuning
+ )
+ }
+
+ DemoSectionHeader(
+ title: "OCR parameters",
+ subtitle: "These apply to the next OCR run."
+ )
+ DemoCard {
+ OCRParametersPanel(
+ params: $viewModel.runtimeParams,
+ resolvedBaseline: viewModel.resolvedRuntimeBaseline ?? .fallbackForUI
+ )
+ }
+ }
+ .padding(20)
+ .padding(.bottom, viewModel.hasUnappliedOrtSettings ? 120 : 24)
+ }
+ .background(Color(.systemGroupedBackground))
+ .navigationTitle("Engine & parameters")
+ .navigationBarTitleDisplayMode(.inline)
+ .toolbar {
+ ToolbarItem(placement: .confirmationAction) {
+ Button("Done") {
+ dismiss()
+ }
+ }
+ }
+ .safeAreaInset(edge: .bottom, spacing: 0) {
+ if viewModel.hasUnappliedOrtSettings {
+ VStack(alignment: .leading, spacing: 12) {
+ Divider()
+ Text("Sessions still use the previous ONNX Runtime choices until you reload.")
+ .font(.footnote)
+ .foregroundStyle(Color.orange.opacity(0.95))
+ .fixedSize(horizontal: false, vertical: true)
+
+ HStack(spacing: 12) {
+ Button("Revert draft") {
+ viewModel.revertOrtDraftToLoaded()
+ }
+ .disabled(!viewModel.allowsOrtApplyControls || !viewModel.hasUnappliedOrtSettings)
+
+ Button {
+ ortSessionReloadConfirmed = true
+ } label: {
+ Label("Apply & reload", systemImage: "arrow.clockwise.circle")
+ .frame(maxWidth: .infinity)
+ }
+ .buttonStyle(.borderedProminent)
+ .disabled(!viewModel.allowsOrtApplyControls || !viewModel.hasUnappliedOrtSettings)
+ }
+ }
+ .padding(.horizontal, 20)
+ .padding(.vertical, 12)
+ .background(.ultraThinMaterial)
+ }
+ }
+ }
+ .confirmationDialog(
+ "Rebuild ONNX Runtime sessions?",
+ isPresented: $ortSessionReloadConfirmed,
+ titleVisibility: .visible
+ ) {
+ Button("Reload models") {
+ Task { await viewModel.loadModels() }
+ }
+ Button("Cancel", role: .cancel) {}
+ } message: {
+ Text("Sessions are recreated from the bundle. Expect a short pause.")
+ }
+ }
+}
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/ErrorView.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/ErrorView.swift
new file mode 100644
index 0000000000000000000000000000000000000000..529c87bf95fa8f37fa69b60502bf0f7cd8c738a5
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/ErrorView.swift
@@ -0,0 +1,60 @@
+// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import SwiftUI
+
+/// Displays an error state with icon, message, and retry button.
+struct ErrorView: View {
+ let error: AppError
+ let onRetry: () -> Void
+
+ var body: some View {
+ VStack(spacing: 8) {
+ Image(systemName: "exclamationmark.triangle.fill")
+ .font(.system(size: 48))
+ .symbolRenderingMode(.hierarchical)
+ .foregroundStyle(errorSeverityColor)
+
+ Text(errorTitle)
+ .font(.title2)
+ .fontWeight(.semibold)
+
+ Text(error.localizedDescription)
+ .font(.body)
+ .foregroundStyle(.secondary)
+ .multilineTextAlignment(.center)
+
+ Button(action: onRetry) {
+ Label("Retry", systemImage: "arrow.clockwise")
+ }
+ .buttonStyle(.borderedProminent)
+ .padding(.top, 8)
+ }
+ }
+
+ private var errorTitle: String {
+ switch error {
+ case .modelLoadFailed: return "Model Loading Failed"
+ case .inferenceFailed: return "OCR Failed"
+ case .imageLoadFailed: return "Image Error"
+ }
+ }
+
+ private var errorSeverityColor: Color {
+ switch error {
+ case .imageLoadFailed: return Color(.systemOrange)
+ case .modelLoadFailed, .inferenceFailed: return Color(.systemRed)
+ }
+ }
+}
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/ImagePickerSection.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/ImagePickerSection.swift
new file mode 100644
index 0000000000000000000000000000000000000000..259406c3799b87110b81c5766eef68f40fae2bbb
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/ImagePickerSection.swift
@@ -0,0 +1,83 @@
+// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import PhotosUI
+import SwiftUI
+
+/// Image selection UI: a prominent "Choose Photo" button backed by PhotosPicker,
+/// plus a row of sample image thumbnails for quick demo access.
+struct ImagePickerSection: View {
+ @Binding var selectedItem: PhotosPickerItem?
+ let sampleImageNames: [String]
+ let onSampleSelected: (String) -> Void
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 12) {
+ // Primary CTA: system photo picker
+ PhotosPicker(
+ selection: $selectedItem,
+ matching: .images,
+ photoLibrary: .shared()
+ ) {
+ Label("Choose Photo", systemImage: "photo.on.rectangle")
+ .frame(maxWidth: .infinity)
+ }
+ .buttonStyle(.borderedProminent)
+
+ // Sample images row
+ if !sampleImageNames.isEmpty {
+ VStack(alignment: .leading, spacing: 8) {
+ Text("Or try a sample:")
+ .font(.subheadline)
+ .foregroundStyle(.secondary)
+
+ HStack(spacing: 8) {
+ ForEach(sampleImageNames, id: \.self) { name in
+ Button {
+ onSampleSelected(name)
+ } label: {
+ sampleThumbnail(named: name)
+ }
+ .buttonStyle(.plain)
+ .frame(minWidth: 44, minHeight: 44)
+ .contentShape(Rectangle())
+ .accessibilityLabel("Sample image: \(name)")
+ }
+ }
+ }
+ }
+ }
+ }
+
+ @ViewBuilder
+ private func sampleThumbnail(named name: String) -> some View {
+ if let path = Bundle.main.path(forBundledImageNamed: name, subdirectory: "SampleImages"),
+ let uiImage = UIImage(contentsOfFile: path) {
+ Image(uiImage: uiImage)
+ .resizable()
+ .aspectRatio(contentMode: .fill)
+ .frame(width: 60, height: 60)
+ .clipShape(RoundedRectangle(cornerRadius: 8))
+ } else {
+ // Fallback for when image can't be loaded
+ RoundedRectangle(cornerRadius: 8)
+ .fill(Color(.tertiarySystemFill))
+ .frame(width: 60, height: 60)
+ .overlay(
+ Image(systemName: "photo")
+ .foregroundStyle(.secondary)
+ )
+ }
+ }
+}
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/OCRParametersPanel.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/OCRParametersPanel.swift
new file mode 100644
index 0000000000000000000000000000000000000000..942e6a13446615a25f6efaf27798003acc78f350
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/OCRParametersPanel.swift
@@ -0,0 +1,226 @@
+// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import SwiftUI
+
+/// Sliders for detection / recognition runtime parameters (content only; wrap with `DemoCard` in parent).
+struct OCRParametersPanel: View {
+ @Binding var params: OCRRuntimeParams
+ let resolvedBaseline: ResolvedOCRRuntimeParams
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 0) {
+ groupTitle("Detection preprocess")
+ intParamRow(
+ title: "Limit side length",
+ caption: "Side length limit for detection resize",
+ range: 32...2048,
+ value: intOverrideBinding(\.textDetLimitSideLen, fallback: resolvedBaseline.textDetLimitSideLen)
+ )
+ limitTypeRow(
+ selection: stringOverrideBinding(\.textDetLimitType, fallback: resolvedBaseline.textDetLimitType)
+ )
+ intParamRow(
+ title: "Max side length cap",
+ caption: "After limit-side resize, caps the longer side if it is still above this",
+ range: 500...8000,
+ value: intOverrideBinding(\.textDetMaxSideLimit, fallback: resolvedBaseline.textDetMaxSideLimit)
+ )
+
+ groupTitle("Detection postprocess")
+ paramRow(
+ title: "Map threshold",
+ caption: "Binarization on the detector heatmap",
+ range: 0.05...0.95,
+ value: floatOverrideBinding(\.textDetThresh, fallback: resolvedBaseline.textDetThresh)
+ )
+ paramRow(
+ title: "Box score threshold",
+ caption: "Minimum score to keep a box",
+ range: 0.1...0.95,
+ value: floatOverrideBinding(\.textDetBoxThresh, fallback: resolvedBaseline.textDetBoxThresh)
+ )
+ paramRow(
+ title: "Unclip ratio",
+ caption: "Expand recovered polygons",
+ range: 0.5...3.0,
+ value: floatOverrideBinding(\.textDetUnclipRatio, fallback: resolvedBaseline.textDetUnclipRatio)
+ )
+
+ Divider()
+ .padding(.vertical, 12)
+
+ groupTitle("Recognition")
+ intParamRow(
+ title: "Recognition batch size",
+ caption: "Number of text lines per recognition run",
+ range: 1...32,
+ value: intOverrideBinding(\.textRecBatchSize, fallback: resolvedBaseline.textRecBatchSize)
+ )
+ paramRow(
+ title: "Line confidence",
+ caption: "Discard lines below this score",
+ range: 0...1,
+ value: floatOverrideBinding(\.textRecScoreThresh, fallback: resolvedBaseline.textRecScoreThresh)
+ )
+
+ HStack {
+ Spacer()
+ Button("Reset to defaults") {
+ params = .noOverrides
+ }
+ .font(.subheadline.weight(.medium))
+ }
+ .padding(.top, 4)
+ }
+ }
+
+ private func floatOverrideBinding(
+ _ keyPath: WritableKeyPath,
+ fallback: Float
+ ) -> Binding {
+ Binding(
+ get: { params[keyPath: keyPath] ?? fallback },
+ set: { newValue in
+ var next = params
+ next[keyPath: keyPath] = newValue
+ params = next
+ }
+ )
+ }
+
+ private func intOverrideBinding(
+ _ keyPath: WritableKeyPath,
+ fallback: Int
+ ) -> Binding {
+ Binding(
+ get: { params[keyPath: keyPath] ?? fallback },
+ set: { newValue in
+ var next = params
+ next[keyPath: keyPath] = newValue
+ params = next
+ }
+ )
+ }
+
+ private func stringOverrideBinding(
+ _ keyPath: WritableKeyPath,
+ fallback: String
+ ) -> Binding {
+ Binding(
+ get: { params[keyPath: keyPath] ?? fallback },
+ set: { newValue in
+ var next = params
+ next[keyPath: keyPath] = newValue
+ params = next
+ }
+ )
+ }
+
+ private func groupTitle(_ text: String) -> some View {
+ Text(text)
+ .font(.subheadline.weight(.semibold))
+ .foregroundStyle(.primary)
+ .padding(.bottom, 8)
+ }
+
+ private func paramRow(
+ title: String,
+ caption: String,
+ range: ClosedRange,
+ value: Binding
+ ) -> some View {
+ VStack(alignment: .leading, spacing: 6) {
+ HStack(alignment: .firstTextBaseline) {
+ VStack(alignment: .leading, spacing: 2) {
+ Text(title)
+ .font(.subheadline)
+ Text(caption)
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ }
+ Spacer()
+ Text(String(format: "%.3f", value.wrappedValue))
+ .font(.caption.monospacedDigit())
+ .foregroundStyle(.secondary)
+ .padding(.horizontal, 8)
+ .padding(.vertical, 4)
+ .background(Color(.tertiarySystemFill), in: Capsule())
+ }
+ Slider(
+ value: Binding(
+ get: { Double(value.wrappedValue) },
+ set: { value.wrappedValue = Float($0) }
+ ),
+ in: Double(range.lowerBound)...Double(range.upperBound)
+ )
+ .tint(Color.accentColor)
+ }
+ .padding(.bottom, 14)
+ }
+
+ private func intParamRow(
+ title: String,
+ caption: String,
+ range: ClosedRange,
+ value: Binding
+ ) -> some View {
+ VStack(alignment: .leading, spacing: 6) {
+ HStack(alignment: .firstTextBaseline) {
+ VStack(alignment: .leading, spacing: 2) {
+ Text(title)
+ .font(.subheadline)
+ Text(caption)
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ }
+ Spacer()
+ Text("\(value.wrappedValue)")
+ .font(.caption.monospacedDigit())
+ .foregroundStyle(.secondary)
+ .padding(.horizontal, 8)
+ .padding(.vertical, 4)
+ .background(Color(.tertiarySystemFill), in: Capsule())
+ }
+ Slider(
+ value: Binding(
+ get: { Double(value.wrappedValue) },
+ set: { value.wrappedValue = Int($0.rounded()) }
+ ),
+ in: Double(range.lowerBound)...Double(range.upperBound),
+ step: 1
+ )
+ .tint(Color.accentColor)
+ }
+ .padding(.bottom, 14)
+ }
+
+ private func limitTypeRow(selection: Binding) -> some View {
+ VStack(alignment: .leading, spacing: 6) {
+ VStack(alignment: .leading, spacing: 2) {
+ Text("Limit type")
+ .font(.subheadline)
+ Text("min: shortest side ≥ limit length; max: longest side ≤ limit length")
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ }
+ Picker("Limit type", selection: selection) {
+ Text("min").tag("min")
+ Text("max").tag("max")
+ }
+ .pickerStyle(.segmented)
+ }
+ .padding(.bottom, 14)
+ }
+}
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/OnnxRuntimeSettingsPanel.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/OnnxRuntimeSettingsPanel.swift
new file mode 100644
index 0000000000000000000000000000000000000000..30f416444f804b30eec39fef53b54e5dad013eed
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/OnnxRuntimeSettingsPanel.swift
@@ -0,0 +1,225 @@
+// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import SwiftUI
+
+private enum CoreMLComputeMode: String, CaseIterable, Identifiable {
+ case automatic
+ case cpuOnly
+ case cpuAndGPU
+ case devicesWithANE
+
+ var id: String { rawValue }
+
+ var segmentLabel: String {
+ switch self {
+ case .automatic: return "Auto"
+ case .cpuOnly: return "CPU"
+ case .cpuAndGPU: return "CPU+GPU"
+ case .devicesWithANE: return "ANE"
+ }
+ }
+
+ static func from(flags: Set) -> CoreMLComputeMode {
+ let cpuOnly = ORTCoreMLProviderOption.cpuOnly.rawValue
+ let cpuAndGPU = ORTCoreMLProviderOption.cpuAndGPU.rawValue
+ let aneOnly = ORTCoreMLProviderOption.aneOnly.rawValue
+ if flags.contains(cpuOnly) { return .cpuOnly }
+ if flags.contains(cpuAndGPU) { return .cpuAndGPU }
+ if flags.contains(aneOnly) { return .devicesWithANE }
+ return .automatic
+ }
+
+ func merge(into flags: inout Set) {
+ let cpuOnly = ORTCoreMLProviderOption.cpuOnly.rawValue
+ let cpuAndGPU = ORTCoreMLProviderOption.cpuAndGPU.rawValue
+ let aneOnly = ORTCoreMLProviderOption.aneOnly.rawValue
+ flags.remove(cpuOnly)
+ flags.remove(cpuAndGPU)
+ flags.remove(aneOnly)
+ switch self {
+ case .automatic: break
+ case .cpuOnly: flags.insert(cpuOnly)
+ case .cpuAndGPU: flags.insert(cpuAndGPU)
+ case .devicesWithANE: flags.insert(aneOnly)
+ }
+ }
+}
+
+/// ONNX Runtime: preferred execution provider (EP) preset plus session tuning. Card content only.
+struct OnnxRuntimeSettingsPanel: View {
+ private static let coreMLExtraToggleOptions: [ORTCoreMLProviderOption] =
+ [.enableOnSubgraphs, .createMLProgram, .staticInputShapes]
+
+ @Binding var primaryExecutionProvider: ORTPrimaryExecutionProvider
+ @Binding var tuning: ORTSessionTuningOptions
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 14) {
+ Text("All inference uses ONNX Runtime. Choose which EP is preferred first for this demo preset; unsupported operators still run on the CPU EP.")
+ .font(.caption)
+ .foregroundStyle(.secondary)
+
+ Picker("Execution provider", selection: $primaryExecutionProvider) {
+ ForEach(ORTPrimaryExecutionProvider.allCases) { provider in
+ Text(provider.displayTitle).tag(provider)
+ }
+ }
+ .pickerStyle(.segmented)
+
+ Divider()
+ .padding(.vertical, 2)
+
+ Text("Session options")
+ .font(.subheadline.weight(.semibold))
+
+ labeledStepper(
+ title: "Intra-op threads",
+ caption: "Session-wide CPU parallelism. Use 0 to leave ONNX Runtime’s default (no explicit limit).",
+ value: intraOpThreadsBinding,
+ range: 0...8
+ )
+
+ if primaryExecutionProvider == .xnnpack {
+ labeledStepper(
+ title: "XNNPACK intra-op threads",
+ caption: "0 leaves the XNNPACK EP default.",
+ value: xnnpackThreadsBinding,
+ range: 0...8
+ )
+ }
+
+ if primaryExecutionProvider == .coreML {
+ VStack(alignment: .leading, spacing: 10) {
+ Text("Core ML EP options")
+ .font(.subheadline.weight(.semibold))
+ .padding(.top, 4)
+
+ VStack(alignment: .leading, spacing: 6) {
+ Text("Core ML placement")
+ .font(.body)
+ Picker("Core ML placement", selection: coreMLComputeModeBinding) {
+ ForEach(CoreMLComputeMode.allCases) { mode in
+ Text(mode.segmentLabel).tag(mode)
+ }
+ }
+ .pickerStyle(.segmented)
+ Text(coreMLComputeModeCaption)
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ }
+
+ ForEach(Self.coreMLExtraToggleOptions) { option in
+ Toggle(isOn: coreMLFlagBinding(option)) {
+ VStack(alignment: .leading, spacing: 2) {
+ Text(option.displayTitle)
+ Text(option.detailCaption)
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ }
+ }
+ }
+ }
+ }
+
+ HStack {
+ Spacer()
+ Button("Reset session options") {
+ tuning = .default
+ }
+ .font(.subheadline.weight(.medium))
+ }
+ .padding(.top, 4)
+ }
+ }
+
+ private var coreMLComputeModeCaption: String {
+ switch CoreMLComputeMode.from(flags: tuning.coreMLFlags) {
+ case .automatic:
+ return "ORT defaults — no cpuOnly/cpuAndGPU/device ANE restriction."
+ case .cpuOnly:
+ return "useCPUOnly — Core ML constrained to CPU."
+ case .cpuAndGPU:
+ return "useCPUAndGPU — CPU and GPU paths; excludes ANE in Core ML."
+ case .devicesWithANE:
+ return "Only enable Core ML EP on devices that have a Neural Engine."
+ }
+ }
+
+ private var intraOpThreadsBinding: Binding {
+ Binding(
+ get: { tuning.intraOpThreads },
+ set: { newValue in
+ var next = tuning
+ next.intraOpThreads = newValue
+ tuning = next
+ }
+ )
+ }
+
+ private var xnnpackThreadsBinding: Binding {
+ Binding(
+ get: { tuning.xnnpackThreads },
+ set: { newValue in
+ var next = tuning
+ next.xnnpackThreads = newValue
+ tuning = next
+ }
+ )
+ }
+
+ private var coreMLComputeModeBinding: Binding {
+ Binding(
+ get: { CoreMLComputeMode.from(flags: tuning.coreMLFlags) },
+ set: { mode in
+ var next = tuning
+ mode.merge(into: &next.coreMLFlags)
+ tuning = next
+ }
+ )
+ }
+
+ private func coreMLFlagBinding(_ option: ORTCoreMLProviderOption) -> Binding {
+ let key = option.rawValue
+ return Binding(
+ get: { tuning.coreMLFlags.contains(key) },
+ set: { on in
+ var next = tuning
+ if on {
+ next.coreMLFlags.insert(key)
+ } else {
+ next.coreMLFlags.remove(key)
+ }
+ tuning = next
+ }
+ )
+ }
+
+ private func labeledStepper(title: String, caption: String, value: Binding, range: ClosedRange) -> some View {
+ VStack(alignment: .leading, spacing: 6) {
+ HStack(alignment: .firstTextBaseline) {
+ Text(title)
+ Spacer()
+ Stepper(value: value, in: range) {
+ Text("\(value.wrappedValue)")
+ .font(.body.monospacedDigit())
+ .frame(minWidth: 24, alignment: .trailing)
+ }
+ }
+ Text(caption)
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ }
+ }
+}
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/ResultImageView.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/ResultImageView.swift
new file mode 100644
index 0000000000000000000000000000000000000000..3a90afac00542366c396d309412151e233376e63
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/ResultImageView.swift
@@ -0,0 +1,79 @@
+// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import SwiftUI
+
+/// Displays the source image with bounding box polygon overlays drawn on a Canvas.
+///
+/// Polygon coordinates from `OCRResult.polygon` are in original image pixel space.
+/// This view computes the scale factor and offset to map them to the displayed
+/// image area, accounting for `.aspectFit` rendering within the available frame.
+struct ResultImageView: View {
+ let image: UIImage
+ let results: [OCRResult]
+
+ var body: some View {
+ GeometryReader { geo in
+ let imageSize = image.size
+ let imageAspect = imageSize.width / imageSize.height
+ let viewAspect = geo.size.width / geo.size.height
+
+ // Compute the actual rendered image rect within the GeometryReader frame
+ let fitSize: CGSize = imageAspect > viewAspect
+ ? CGSize(width: geo.size.width, height: geo.size.width / imageAspect)
+ : CGSize(width: geo.size.height * imageAspect, height: geo.size.height)
+ let offset = CGPoint(
+ x: (geo.size.width - fitSize.width) / 2,
+ y: (geo.size.height - fitSize.height) / 2
+ )
+ let scale = fitSize.width / imageSize.width
+
+ ZStack {
+ Image(uiImage: image)
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+
+ Canvas { context, size in
+ for result in results {
+ let points = result.polygon.compactMap { coords -> CGPoint? in
+ guard coords.count >= 2 else { return nil }
+ return CGPoint(
+ x: CGFloat(coords[0]) * scale + offset.x,
+ y: CGFloat(coords[1]) * scale + offset.y
+ )
+ }
+ guard points.count >= 4 else { continue }
+
+ var path = Path()
+ path.move(to: points[0])
+ for pt in points.dropFirst() {
+ path.addLine(to: pt)
+ }
+ path.closeSubpath()
+
+ let strokeColor = Color.accentColor.opacity(0.9)
+ let fillColor = Color.accentColor.opacity(0.12)
+ context.stroke(path, with: .color(strokeColor), lineWidth: 2)
+ context.fill(path, with: .color(fillColor))
+ }
+ }
+ }
+ .frame(width: geo.size.width, height: geo.size.height)
+ }
+ .aspectRatio(image.size.width / image.size.height, contentMode: .fit)
+ .frame(maxHeight: 400)
+ .accessibilityElement(children: .combine)
+ .accessibilityLabel("Image preview with \(results.count) detected text regions")
+ }
+}
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/ResultsListView.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/ResultsListView.swift
new file mode 100644
index 0000000000000000000000000000000000000000..93b78ed9b65cca81574c998b918e546061323a37
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/ResultsListView.swift
@@ -0,0 +1,98 @@
+// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import SwiftUI
+
+/// Recognized lines + copy action (intended to sit inside `DemoCard` from the parent).
+struct ResultsListView: View {
+ let results: [OCRResult]
+ let copiedFeedback: Bool
+ let onCopy: () -> Void
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 0) {
+ HStack(alignment: .firstTextBaseline) {
+ Text("Recognized text")
+ .font(.headline)
+ Spacer()
+ Button(action: onCopy) {
+ Label(
+ copiedFeedback ? "Copied" : "Copy all",
+ systemImage: copiedFeedback ? "checkmark.circle.fill" : "doc.on.doc"
+ )
+ .labelStyle(.titleAndIcon)
+ }
+ .font(.subheadline.weight(.medium))
+ .buttonStyle(.bordered)
+ .controlSize(.small)
+ .disabled(results.isEmpty)
+ }
+ .padding(.bottom, 12)
+
+ if results.isEmpty {
+ emptyState
+ } else {
+ VStack(alignment: .leading, spacing: 0) {
+ ForEach(Array(results.enumerated()), id: \.offset) { index, result in
+ resultRow(index: index, result: result)
+ if index < results.count - 1 {
+ Divider()
+ .padding(.vertical, 10)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private var emptyState: some View {
+ VStack(spacing: 12) {
+ Image(systemName: "text.magnifyingglass")
+ .font(.system(size: 32))
+ .foregroundStyle(.secondary)
+ .symbolRenderingMode(.hierarchical)
+ Text("No text to show")
+ .font(.subheadline.weight(.semibold))
+ Text("Try lowering thresholds or use an image with clearer text.")
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ .multilineTextAlignment(.center)
+ }
+ .frame(maxWidth: .infinity)
+ .padding(.vertical, 20)
+ }
+
+ private func resultRow(index: Int, result: OCRResult) -> some View {
+ HStack(alignment: .top, spacing: 12) {
+ Text("\(index + 1)")
+ .font(.caption.weight(.bold))
+ .foregroundStyle(.secondary)
+ .frame(width: 22, height: 22)
+ .background(Color(.tertiarySystemFill), in: Circle())
+
+ Text(result.text)
+ .font(.body)
+ .foregroundStyle(.primary)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .lineLimit(5)
+
+ Text(String(format: "%.0f%%", result.confidence * 100))
+ .font(.caption.monospacedDigit())
+ .foregroundStyle(.secondary)
+ .padding(.horizontal, 6)
+ .padding(.vertical, 2)
+ .background(Color(.tertiarySystemFill), in: Capsule())
+ }
+ }
+}
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/TimingView.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/TimingView.swift
new file mode 100644
index 0000000000000000000000000000000000000000..f4f277db183fe74409e83713d2ce82fb56616aa0
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemo/Views/TimingView.swift
@@ -0,0 +1,54 @@
+// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import SwiftUI
+
+/// Compact per-stage timing chips (fits the “output” section below the preview).
+struct TimingView: View {
+ let result: OCRRunResult
+
+ var body: some View {
+ HStack(spacing: 10) {
+ chip(title: "Detect", ms: result.detectionTime)
+ chip(title: "Recognize", ms: result.recognitionTime)
+ chip(title: "Total", ms: result.totalTime, emphasized: true)
+ }
+ .frame(maxWidth: .infinity)
+ }
+
+ private func chip(title: String, ms: TimeInterval, emphasized: Bool = false) -> some View {
+ VStack(spacing: 6) {
+ Text(title)
+ .font(.caption2.weight(.semibold))
+ .foregroundStyle(.secondary)
+ Text(String(format: "%.0f", ms * 1000))
+ .font(.system(.body, design: .rounded).weight(emphasized ? .semibold : .regular))
+ .monospacedDigit()
+ Text("ms")
+ .font(.caption2)
+ .foregroundStyle(.tertiary)
+ }
+ .frame(maxWidth: .infinity)
+ .padding(.vertical, 12)
+ .padding(.horizontal, 8)
+ .background {
+ RoundedRectangle(cornerRadius: 12, style: .continuous)
+ .fill(emphasized ? Color.accentColor.opacity(0.12) : Color(.tertiarySystemFill))
+ }
+ .overlay {
+ RoundedRectangle(cornerRadius: 12, style: .continuous)
+ .strokeBorder(Color(.separator).opacity(0.25), lineWidth: 0.5)
+ }
+ }
+}
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/BenchmarkFixtures.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/BenchmarkFixtures.swift
new file mode 100644
index 0000000000000000000000000000000000000000..13cb6265fd04400e26526d7aa04d8d59e5474557
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/BenchmarkFixtures.swift
@@ -0,0 +1,25 @@
+// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import Foundation
+
+/// Default bundled image file names for benchmark tests.
+enum BenchmarkFixtures {
+ static let defaultReferenceImageStem: String = "ios_ocr_benchmark_reference"
+ static let defaultReferenceImageFileExtension: String = "jpg"
+
+ static var defaultReferenceImageFileName: String {
+ "\(defaultReferenceImageStem).\(defaultReferenceImageFileExtension)"
+ }
+}
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/BenchmarkPayloads.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/BenchmarkPayloads.swift
new file mode 100644
index 0000000000000000000000000000000000000000..7380e17bb1aade8b8bb36c84f368106f8915f84d
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/BenchmarkPayloads.swift
@@ -0,0 +1,95 @@
+// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+
+import Foundation
+
+@testable import PaddleOCRDemo
+
+/// Names for JSON written as `XCTAttachment`s; keep in sync with whatever exports attachments from test results.
+enum BenchmarkArtifact: String {
+ case iOSExport = "ios-ocr-export.json"
+ case onDevicePerformance = "on-device-performance.json"
+}
+
+// MARK: - Accuracy export payload
+
+struct OCRExportPayload: Codable {
+ var schemaVersion: Int = 1
+ var source: String
+ var items: [OCRExportItem]
+}
+
+struct OCRExportItem: Codable {
+ var polygon: [[Int]]
+ var text: String
+ var score: Double?
+}
+
+// MARK: - On-device performance payload
+
+struct TimingSummary: Codable {
+ var mean: Double
+ var stdev: Double
+ var p90: Double
+}
+
+/// Pooled: one sample per det line per measured run (all lines concatenated, ms).
+struct RecognitionPerLinePooledMs: Codable {
+ var count: Int
+ var inferenceMs: TimingSummary
+ var preprocessMs: TimingSummary
+ var postprocessMs: TimingSummary
+ var totalMs: TimingSummary
+}
+
+struct RecognitionPerLineBlock: Codable {
+ var pooled: RecognitionPerLinePooledMs
+}
+
+struct BenchmarkTensorShapeSample: Codable {
+ var shape: [Int]
+ var count: Int
+}
+
+struct BenchmarkInputShapeDistribution: Codable {
+ var detection: [BenchmarkTensorShapeSample]
+ var recognition: [BenchmarkTensorShapeSample]
+}
+
+struct OCRDeviceBenchmarkPayload: Codable {
+ var schemaVersion: Int = 1
+ var buildConfiguration: String
+ var deviceModel: String
+ var osVersion: String
+ var isSimulator: Bool
+ var ortExecutionProvider: String
+ var ortProfilingEnabled: Bool
+ var warmupIterations: Int
+ var measuredIterations: Int
+ var inputShapeDistribution: BenchmarkInputShapeDistribution
+ var firstMeasuredLineCount: Int
+ var coldLoadTimeMs: Double
+ var totalTimeMs: TimingSummary
+ var detectionTimeMs: TimingSummary
+ var detectionPreprocessTimeMs: TimingSummary
+ var detectionInferenceTimeMs: TimingSummary
+ var detectionPostprocessTimeMs: TimingSummary
+ var recognitionTimeMs: TimingSummary
+ var recognitionPreprocessTimeMs: TimingSummary
+ var recognitionInferenceTimeMs: TimingSummary
+ var recognitionPostprocessTimeMs: TimingSummary
+ var recognitionPerLine: RecognitionPerLineBlock?
+ var pipelineOverheadTimeMs: TimingSummary
+ var memoryFootprintBeforeLoadBytes: UInt64?
+ var memoryFootprintAfterLoadBytes: UInt64
+ var thermalState: String?
+}
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/Fixtures/.gitignore b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/Fixtures/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..3a4a7f7e62d4c6159278e300874f3e7c2d520e82
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/Fixtures/.gitignore
@@ -0,0 +1,2 @@
+# Ad-hoc copies from ./scripts/run_validation.sh --image (local-*); not committed.
+local-*
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/Fixtures/ios_ocr_validation_reference.jpg b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/Fixtures/ios_ocr_validation_reference.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..70f5b428c2c21a775fdfbba0668506c4a770dfd6
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/Fixtures/ios_ocr_validation_reference.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4425af33dd163cf73bdff502bd35ee527e9bdd5725501db1da78bfdae9f538f4
+size 128713
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/OCRBenchmarkTests.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/OCRBenchmarkTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..fbf3631bc8a9995d4186c4432f676840f944f106
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/OCRBenchmarkTests.swift
@@ -0,0 +1,562 @@
+// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import Darwin
+import Foundation
+
+import XCTest
+
+@testable import PaddleOCRDemo
+
+import UIKit
+
+// MARK: - Tests
+
+final class OCRBenchmarkTests: XCTestCase {
+
+ /// `PADDLEOCR_BENCHMARK_IMAGE_NAME`: bundled image stem or `stem.ext` (optional).
+ private static let imageNameEnvKey = "PADDLEOCR_BENCHMARK_IMAGE_NAME"
+
+ /// Optional non-negative int; default `3`. Used only by `testOnDevicePerformanceMetrics`.
+ private static let warmupIterationsEnvKey = "PADDLEOCR_BENCHMARK_WARMUP_ITERATIONS"
+
+ /// Optional non-negative int; default `10`. Used only by `testOnDevicePerformanceMetrics`.
+ private static let measuredIterationsEnvKey = "PADDLEOCR_BENCHMARK_MEASURED_ITERATIONS"
+
+ /// `CPU` (default), `XNNPACK`, or `CORE_ML` — ONNX Runtime execution provider preset for benchmarks.
+ private static let ortExecutionProviderEnvKey = "PADDLEOCR_BENCHMARK_ORT_EXECUTION_PROVIDER"
+
+ /// `1` / `true` / `yes` / `on` enables ONNX Runtime JSON profiling (see ``ORTSessionManager/finalizeORTProfiling()``).
+ /// When set, on-device performance timings are not representative of a clean latency benchmark.
+ private static let ortProfilingEnvKey = "PADDLEOCR_BENCHMARK_ORT_PROFILING"
+
+ private static let buildConfigurationEnvKey = "PADDLEOCR_BENCHMARK_BUILD_CONFIGURATION"
+
+ private static let intraOpThreadsEnvKey = "PADDLEOCR_BENCHMARK_INTRA_OP_THREADS"
+ private static let xnnpackThreadsEnvKey = "PADDLEOCR_BENCHMARK_XNNPACK_THREADS"
+ private static let coremlOptionsEnvKey = "PADDLEOCR_BENCHMARK_COREML_OPTIONS"
+ private static let recBatchSizeEnvKey = "PADDLEOCR_BENCHMARK_REC_BATCH_SIZE"
+
+ func testOCRExportJSONSchema() async throws {
+ let cgImage = try resolveBenchmarkImage()
+ let executionProvider = try resolveBenchmarkExecutionProvider()
+ let manager = ORTSessionManager()
+ try await manager.loadModels(executionProvider: executionProvider, ortProfiling: resolveORTProfilingEnabled(), tuning: resolveSessionTuningOptions())
+ let engine = try OCREngine(sessionManager: manager)
+ let run = try await engine.run(cgImage, params: .noOverrides)
+
+ let payload = OCRExportPayload(
+ source: "ios_ocr_demo",
+ items: run.results.map { r in
+ OCRExportItem(
+ polygon: r.polygon.map { $0.map { Int($0) } },
+ text: r.text,
+ score: Double(r.confidence)
+ )
+ }
+ )
+
+ let encoder = JSONEncoder()
+ encoder.outputFormatting = [.sortedKeys]
+ let data = try encoder.encode(payload)
+ _ = try JSONDecoder().decode(OCRExportPayload.self, from: data)
+ attachJSON(data, artifact: .iOSExport)
+
+ if resolveORTProfilingEnabled(), let out = try await manager.finalizeORTProfiling() {
+ attachProfileFile(out.detectionProfileJSON, name: "ort_profile_detection")
+ attachProfileFile(out.recognitionProfileJSON, name: "ort_profile_recognition")
+ }
+ }
+
+ func testOnDeviceLatencyBenchmark() async throws {
+ let executionProvider = try resolveBenchmarkExecutionProvider()
+ let ortProfiling = resolveORTProfilingEnabled()
+ let context = try await makeBenchmarkContext(executionProvider: executionProvider, ortProfiling: ortProfiling)
+ let warmup = try parseNonNegativeIntEnv(Self.warmupIterationsEnvKey, defaultValue: 3)
+ let iterations = max(try parseNonNegativeIntEnv(Self.measuredIterationsEnvKey, defaultValue: 10), 1)
+ try await runWarmup(engine: context.engine, image: context.cgImage, iterations: warmup)
+
+ var totals: [Double] = []
+ var dets: [Double] = []
+ var detPre: [Double] = []
+ var detInf: [Double] = []
+ var detPost: [Double] = []
+ var recs: [Double] = []
+ var recPre: [Double] = []
+ var recInf: [Double] = []
+ var recPost: [Double] = []
+ var overheads: [Double] = []
+ totals.reserveCapacity(iterations)
+ dets.reserveCapacity(iterations)
+ detPre.reserveCapacity(iterations)
+ detInf.reserveCapacity(iterations)
+ detPost.reserveCapacity(iterations)
+ recs.reserveCapacity(iterations)
+ recPre.reserveCapacity(iterations)
+ recInf.reserveCapacity(iterations)
+ recPost.reserveCapacity(iterations)
+ overheads.reserveCapacity(iterations)
+
+ var lineInferenceMsPooled: [Double] = []
+ var linePreprocessMsPooled: [Double] = []
+ var linePostprocessMsPooled: [Double] = []
+ var lineTotalMsPooled: [Double] = []
+ var firstMeasuredLineCount = 0
+ var detectionInputShapes: [[Int]] = []
+ var recognitionInputShapes: [[Int]] = []
+
+ let benchmarkParams = resolveBenchmarkRuntimeParams()
+
+ for index in 0.. BenchmarkContext {
+ let cgImage = try resolveBenchmarkImage()
+ let memoryBeforeLoad = physicalFootprintBytes()
+ let manager = ORTSessionManager()
+ let loadStart = CFAbsoluteTimeGetCurrent()
+ try await manager.loadModels(executionProvider: executionProvider, ortProfiling: ortProfiling, tuning: resolveSessionTuningOptions())
+ let coldLoadTime = CFAbsoluteTimeGetCurrent() - loadStart
+ let memoryAfterLoad = physicalFootprintBytes()
+ let engine = try OCREngine(sessionManager: manager)
+ return BenchmarkContext(
+ cgImage: cgImage,
+ manager: manager,
+ engine: engine,
+ memoryBeforeLoad: memoryBeforeLoad,
+ memoryAfterLoad: memoryAfterLoad,
+ coldLoadTime: coldLoadTime
+ )
+ }
+
+ private func summarizeShapes(_ shapes: [[Int]]) -> [BenchmarkTensorShapeSample] {
+ let counts = shapes.reduce(into: [[Int]: Int]()) { partial, shape in
+ partial[shape, default: 0] += 1
+ }
+ return counts
+ .map { BenchmarkTensorShapeSample(shape: $0.key, count: $0.value) }
+ .sorted { lhs, rhs in
+ if lhs.shape != rhs.shape {
+ return lhs.shape.lexicographicallyPrecedes(rhs.shape)
+ }
+ return lhs.count > rhs.count
+ }
+ }
+
+ private func runWarmup(engine: OCREngine, image: CGImage, iterations: Int) async throws {
+ let params = resolveBenchmarkRuntimeParams()
+ for _ in 0.. 0 {
+ let infMs = run.lineRecognitionInferenceTimes.map { $0 * 1000.0 }
+ let preMs = run.lineRecognitionPreprocessTimes.map { $0 * 1000.0 }
+ let postMs = run.lineRecognitionPostprocessTimes.map { $0 * 1000.0 }
+ lineInferenceMsPooled.append(contentsOf: infMs)
+ linePreprocessMsPooled.append(contentsOf: preMs)
+ linePostprocessMsPooled.append(contentsOf: postMs)
+ for i in 0.. String {
+ let raw =
+ ProcessInfo.processInfo.environment[Self.buildConfigurationEnvKey]?
+ .trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
+ return raw.isEmpty ? "Release" : raw
+ }
+
+ private func isRunningOnSimulator() -> Bool {
+ #if targetEnvironment(simulator)
+ return true
+ #else
+ return false
+ #endif
+ }
+
+ private func deviceModelName() -> String {
+ var systemInfo = utsname()
+ uname(&systemInfo)
+ let mirror = Mirror(reflecting: systemInfo.machine)
+ return mirror.children.reduce(into: "") { identifier, element in
+ guard let value = element.value as? Int8, value != 0 else { return }
+ identifier.append(String(UnicodeScalar(UInt8(value))))
+ }
+ }
+
+ private func resolveORTProfilingEnabled() -> Bool {
+ let raw =
+ ProcessInfo.processInfo.environment[Self.ortProfilingEnvKey]?
+ .trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
+ if raw.isEmpty { return false }
+ switch raw.lowercased() {
+ case "1", "true", "yes", "on":
+ return true
+ default:
+ return false
+ }
+ }
+
+ private func resolveSessionTuningOptions() -> ORTSessionTuningOptions {
+ let env = ProcessInfo.processInfo.environment
+ var opts = ORTSessionTuningOptions()
+ if let v = env[Self.intraOpThreadsEnvKey], let n = Int(v), n > 0 {
+ opts.intraOpThreads = n
+ }
+ if let v = env[Self.xnnpackThreadsEnvKey], let n = Int(v), n > 0 {
+ opts.xnnpackThreads = n
+ }
+ if let v = env[Self.coremlOptionsEnvKey], !v.isEmpty {
+ opts.coreMLFlags = Set(v.split(separator: ",").map { String($0).trimmingCharacters(in: .whitespaces) })
+ }
+ return opts
+ }
+
+ private func attachProfileFile(_ url: URL, name: String) {
+ let attachment = XCTAttachment(contentsOfFile: url)
+ attachment.name = name
+ attachment.lifetime = XCTAttachment.Lifetime.keepAlways
+ add(attachment)
+ }
+
+ private func resolveBenchmarkExecutionProvider() throws -> ORTPrimaryExecutionProvider {
+ let raw =
+ ProcessInfo.processInfo.environment[Self.ortExecutionProviderEnvKey]?
+ .trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
+ if raw.isEmpty {
+ return .cpu
+ }
+ if let exact = ORTPrimaryExecutionProvider(rawValue: raw) {
+ return exact
+ }
+ switch raw.lowercased() {
+ case "core_ml":
+ return .coreML
+ case "xnnpack":
+ return .xnnpack
+ case "cpu":
+ return .cpu
+ default:
+ throw NSError(
+ domain: "OCRBenchmarkTests",
+ code: 5,
+ userInfo: [
+ NSLocalizedDescriptionKey:
+ "Invalid \(Self.ortExecutionProviderEnvKey): \"\(raw)\". "
+ + "Use CPU (default), XNNPACK, CORE_ML, or Swift raw values "
+ + "\(ORTPrimaryExecutionProvider.cpu.rawValue) / "
+ + "\(ORTPrimaryExecutionProvider.xnnpack.rawValue) / "
+ + "\(ORTPrimaryExecutionProvider.coreML.rawValue).",
+ ]
+ )
+ }
+ }
+
+ private func resolveBenchmarkImage() throws -> CGImage {
+ let envRaw =
+ ProcessInfo.processInfo.environment[Self.imageNameEnvKey]?
+ .trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
+ let raw = envRaw.isEmpty ? BenchmarkFixtures.defaultReferenceImageStem : envRaw
+ let bundle = Bundle(for: Self.self)
+ guard let path = bundle.path(forBundledImageNamed: raw, subdirectory: "Fixtures") else {
+ throw NSError(
+ domain: "OCRBenchmarkTests",
+ code: 3,
+ userInfo: [
+ NSLocalizedDescriptionKey:
+ "Image \"\(raw)\" not found in the test bundle. Check the name and that the file is included in the test target resources (e.g. \"\(BenchmarkFixtures.defaultReferenceImageStem)\").",
+ ]
+ )
+ }
+ let url = URL(fileURLWithPath: path)
+ guard let ui = UIImage(contentsOfFile: url.path),
+ let cg = normalizeOrientation(ui).cgImage
+ else {
+ throw NSError(
+ domain: "OCRBenchmarkTests",
+ code: 4,
+ userInfo: [NSLocalizedDescriptionKey: "Could not decode image at \(url.path)."]
+ )
+ }
+ return cg
+ }
+
+ private func attachJSON(_ data: Data, artifact: BenchmarkArtifact) {
+ let attachment = XCTAttachment(data: data, uniformTypeIdentifier: "public.json")
+ attachment.name = artifact.rawValue
+ attachment.lifetime = .keepAlways
+ add(attachment)
+ }
+
+ private func waitForAsync(_ operation: @escaping () async throws -> T) throws -> T {
+ let semaphore = DispatchSemaphore(value: 0)
+ let lock = NSLock()
+ var capturedResult: Result?
+ Task {
+ do {
+ let value = try await operation()
+ lock.lock()
+ capturedResult = .success(value)
+ lock.unlock()
+ } catch {
+ lock.lock()
+ capturedResult = .failure(error)
+ lock.unlock()
+ }
+ semaphore.signal()
+ }
+ semaphore.wait()
+ lock.lock()
+ let result = capturedResult
+ lock.unlock()
+ switch result {
+ case .success(let value):
+ return value
+ case .failure(let error):
+ throw error
+ case .none:
+ throw NSError(
+ domain: "OCRBenchmarkTests",
+ code: 6,
+ userInfo: [NSLocalizedDescriptionKey: "Async operation completed without a result."]
+ )
+ }
+ }
+
+ /// Parses a non-negative integer from the environment, or returns `defaultValue` when unset/blank.
+ private func parseNonNegativeIntEnv(_ key: String, defaultValue: Int) throws -> Int {
+ let raw =
+ ProcessInfo.processInfo.environment[key]?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
+ if raw.isEmpty {
+ return defaultValue
+ }
+ guard let n = Int(raw), n >= 0 else {
+ throw NSError(
+ domain: "OCRBenchmarkTests",
+ code: 3,
+ userInfo: [
+ NSLocalizedDescriptionKey:
+ "Invalid \(key): expected a non-negative integer (default \(defaultValue) if unset), got \(raw).",
+ ]
+ )
+ }
+ return n
+ }
+
+ private func resolveBenchmarkRuntimeParams() -> OCRRuntimeParams {
+ let env = ProcessInfo.processInfo.environment
+ if let v = env[Self.recBatchSizeEnvKey], let n = Int(v), n >= 1 {
+ return OCRRuntimeParams(
+ textDetLimitSideLen: nil,
+ textDetLimitType: nil,
+ textDetMaxSideLimit: nil,
+ textDetThresh: nil,
+ textDetBoxThresh: nil,
+ textDetUnclipRatio: nil,
+ textRecBatchSize: n,
+ textRecScoreThresh: nil
+ )
+ }
+ return .noOverrides
+ }
+
+ /// Physical memory footprint (bytes) — matches Xcode Memory gauge more closely than RSS alone.
+ /// Uses `task_vm_info_data_t.phys_footprint` via `task_info(TASK_VM_INFO, ...)`.
+ private func physicalFootprintBytes() -> UInt64 {
+ var info = task_vm_info_data_t()
+ var count = mach_msg_type_number_t(MemoryLayout.size / MemoryLayout.size)
+ let kerr = withUnsafeMutablePointer(to: &info) { p in
+ p.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
+ task_info(mach_task_self_, task_flavor_t(TASK_VM_INFO), $0, &count)
+ }
+ }
+ guard kerr == KERN_SUCCESS else { return 0 }
+ return UInt64(info.phys_footprint)
+ }
+
+ private func summarizeMs(_ samples: [Double]) -> TimingSummary {
+ let sorted = samples.sorted()
+ let mean = samples.reduce(0, +) / Double(max(samples.count, 1))
+ let variance =
+ samples.isEmpty
+ ? 0
+ : samples.map { pow($0 - mean, 2) }.reduce(0, +) / Double(samples.count)
+ let stdev = sqrt(variance)
+ let p90Idx = max(0, min(sorted.count - 1, Int(floor(0.9 * Double(max(sorted.count - 1, 0))))))
+ let p90 = sorted.isEmpty ? 0 : sorted[p90Idx]
+ return TimingSummary(mean: mean, stdev: stdev, p90: p90)
+ }
+}
+
+// MARK: - Orientation (match OCRViewModel)
+
+private func normalizeOrientation(_ image: UIImage) -> UIImage {
+ guard image.imageOrientation != .up else { return image }
+ let format = UIGraphicsImageRendererFormat()
+ format.scale = image.scale
+ let renderer = UIGraphicsImageRenderer(size: image.size, format: format)
+ return renderer.image { _ in
+ image.draw(in: CGRect(origin: .zero, size: image.size))
+ }
+}
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/OCRParameterResolverTests.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/OCRParameterResolverTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..d75839484881d7aea2bd7f585e4274d8b6a38e4b
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/OCRParameterResolverTests.swift
@@ -0,0 +1,118 @@
+// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import XCTest
+@testable import PaddleOCRDemo
+
+/// Verifies ``OCRParameterResolver``
+final class OCRParameterResolverTests: XCTestCase {
+
+ func testDetResizeMergePrefersAppDefaultsOverModelWhenUINil() throws {
+ let dir = FileManager.default.temporaryDirectory
+ let url = dir.appendingPathComponent("merge_test_det_\(UUID().uuidString).yaml")
+ let yaml = """
+ Global:
+ model_name: PP-OCRv5_mobile_det
+ PreProcess:
+ transform_ops:
+ - DetResizeForTest:
+ resize_long: 960
+ PostProcess:
+ name: DBPostProcess
+ """
+ try yaml.write(to: url, atomically: true, encoding: .utf8)
+ defer { try? FileManager.default.removeItem(at: url) }
+
+ let det = try InferenceConfig.load(from: url.path)
+ let m = det.detResizeFromModel
+ XCTAssertNotNil(m)
+ XCTAssertEqual(m?.limitSideLen, 960)
+
+ let eff = OCRParameterResolver.effective(modelConfig: det, runtime: .noOverrides)
+ let app = OCRParameterResolver.textDetLimitSideLenAppDefault
+ XCTAssertEqual(eff.detResize.limitSideLen, app)
+ XCTAssertEqual(eff.mergedLimitSideLen, app)
+ }
+
+ func testEffectiveDetPostprocessMergesThresholdsOnly() throws {
+ let dir = FileManager.default.temporaryDirectory
+ let url = dir.appendingPathComponent("merge_test_post_\(UUID().uuidString).yaml")
+ let yaml = """
+ Global:
+ model_name: PP-OCRv5_mobile_det
+ PreProcess:
+ transform_ops:
+ - DetResizeForTest:
+ resize_long: 960
+ PostProcess:
+ name: DBPostProcess
+ thresh: 0.99
+ box_thresh: 0.99
+ unclip_ratio: 9.9
+ max_candidates: 7
+ """
+ try yaml.write(to: url, atomically: true, encoding: .utf8)
+ defer { try? FileManager.default.removeItem(at: url) }
+
+ let det = try InferenceConfig.load(from: url.path)
+ let post = OCRParameterResolver.effective(modelConfig: det, runtime: .noOverrides)
+ XCTAssertEqual(post.detThresh, 0.3, accuracy: 0.0001)
+ XCTAssertEqual(post.detBoxThresh, 0.6, accuracy: 0.0001)
+ XCTAssertEqual(post.detUnclipRatio, 1.5, accuracy: 0.0001)
+ XCTAssertEqual(det.postProcess.maxCandidates, 7)
+ }
+
+ func testDecodeImageImgModeParsedAsRGB() throws {
+ let dir = FileManager.default.temporaryDirectory
+ let url = dir.appendingPathComponent("decode_rgb_\(UUID().uuidString).yaml")
+ let yaml = """
+ Global:
+ model_name: PP-OCRv5_mobile_det
+ PreProcess:
+ transform_ops:
+ - DecodeImage:
+ channel_first: false
+ img_mode: RGB
+ - DetResizeForTest:
+ resize_long: 960
+ PostProcess:
+ name: DBPostProcess
+ """
+ try yaml.write(to: url, atomically: true, encoding: .utf8)
+ defer { try? FileManager.default.removeItem(at: url) }
+
+ let cfg = try InferenceConfig.load(from: url.path)
+ XCTAssertEqual(cfg.decodeImageChannelOrder, .rgb)
+ }
+
+ func testDecodeImageMissingDefaultsToBGR() throws {
+ let dir = FileManager.default.temporaryDirectory
+ let url = dir.appendingPathComponent("decode_default_bgr_\(UUID().uuidString).yaml")
+ let yaml = """
+ Global:
+ model_name: PP-OCRv5_mobile_det
+ PreProcess:
+ transform_ops:
+ - DetResizeForTest:
+ resize_long: 960
+ PostProcess:
+ name: DBPostProcess
+ """
+ try yaml.write(to: url, atomically: true, encoding: .utf8)
+ defer { try? FileManager.default.removeItem(at: url) }
+
+ let cfg = try InferenceConfig.load(from: url.path)
+ XCTAssertEqual(cfg.decodeImageChannelOrder, .bgr)
+ }
+}
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/ORTSessionManagerTests.swift b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/ORTSessionManagerTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..f940823ae7008f45bf529c372398b63137b21fdc
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/PaddleOCRDemoTests/ORTSessionManagerTests.swift
@@ -0,0 +1,55 @@
+// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import XCTest
+@testable import PaddleOCRDemo
+
+final class ORTSessionManagerTests: XCTestCase {
+
+ func testLoadModels() async throws {
+ let manager = ORTSessionManager()
+ try await manager.loadModels(executionProvider: .coreML)
+ }
+
+ func testLoadModelsCPU() async throws {
+ let manager = ORTSessionManager()
+ try await manager.loadModels(executionProvider: .cpu)
+ }
+
+ func testModelConfigResolution() throws {
+ do {
+ let detConfig = try ModelConfig.detection()
+ XCTAssertFalse(detConfig.modelPath.isEmpty, "Det model path should not be empty")
+ let expected = try InferenceConfig.load(from: detConfig.configPath).modelName
+ XCTAssertEqual(detConfig.name, expected, "Display name should equal Global.model_name in the det model config")
+ } catch let error as ModelConfigError {
+ switch error {
+ case .modelNotFound(let path):
+ XCTAssertTrue(path.contains("det"), "Error should reference det model path")
+ }
+ }
+
+ do {
+ let recConfig = try ModelConfig.recognition()
+ XCTAssertFalse(recConfig.modelPath.isEmpty, "Rec model path should not be empty")
+ let expected = try InferenceConfig.load(from: recConfig.configPath).modelName
+ XCTAssertEqual(recConfig.name, expected, "Display name should equal Global.model_name in the rec model config")
+ } catch let error as ModelConfigError {
+ switch error {
+ case .modelNotFound(let path):
+ XCTAssertTrue(path.contains("rec"), "Error should reference rec model path")
+ }
+ }
+ }
+}
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/Podfile b/ocr_webapp/PaddleOCR/deploy/ios_demo/Podfile
new file mode 100644
index 0000000000000000000000000000000000000000..b94d6792170cf52cc12737d7f2a132210421f5fa
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/Podfile
@@ -0,0 +1,30 @@
+platform :ios, '16.0'
+
+target 'PaddleOCRDemo' do
+ use_frameworks!
+
+ # ONNX Runtime with ObjC API — includes Core ML EP + XNNPACK EP pre-built
+ pod 'onnxruntime-objc', '~> 1.24'
+
+ # YAML parsing for model config files
+ pod 'Yams', '~> 5.0'
+
+ # OpenCV (imgproc): preprocessing resize/pad, recognition resize, quad crop, DB contours — see OpenCV*Bridge.
+ pod 'OpenCV', '~> 4.3.0'
+
+ # Unit tests `@testable import PaddleOCRDemo` need the same Swift / header search paths
+ # as the app without duplicating pod linkage.
+ target 'PaddleOCRDemoTests' do
+ inherit! :search_paths
+ end
+end
+
+# onnxruntime-objc ships headers that use #include "..." inside framework headers; newer Xcode
+# treats that as an error (CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER). Allow Pods to build.
+post_install do |installer|
+ installer.pods_project.targets.each do |target|
+ target.build_configurations.each do |config|
+ config.build_settings['CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER'] = 'NO'
+ end
+ end
+end
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/Podfile.lock b/ocr_webapp/PaddleOCR/deploy/ios_demo/Podfile.lock
new file mode 100644
index 0000000000000000000000000000000000000000..ded7f54f62f1379d80b58d7d4a71a38ba0716c0c
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/Podfile.lock
@@ -0,0 +1,30 @@
+PODS:
+ - onnxruntime-c (1.24.3)
+ - onnxruntime-objc (1.24.3):
+ - onnxruntime-objc/Core (= 1.24.3)
+ - onnxruntime-objc/Core (1.24.3):
+ - onnxruntime-c (= 1.24.3)
+ - OpenCV (4.3.0)
+ - Yams (5.0.6)
+
+DEPENDENCIES:
+ - onnxruntime-objc (~> 1.24)
+ - OpenCV (~> 4.3.0)
+ - Yams (~> 5.0)
+
+SPEC REPOS:
+ trunk:
+ - onnxruntime-c
+ - onnxruntime-objc
+ - OpenCV
+ - Yams
+
+SPEC CHECKSUMS:
+ onnxruntime-c: 4a1a1a81da2087869dc9b6357f182952db2df29c
+ onnxruntime-objc: f9310465f0a284682811d071b7eb8e93f10517b8
+ OpenCV: 681e0451c1e54c5930a8922277f817ed5d5ab459
+ Yams: e10dae147f517ed57ecae37c5e8681bdf8fcab65
+
+PODFILE CHECKSUM: 24111b5e94db1b6a72362b62148f587abc1a06be
+
+COCOAPODS: 1.16.2
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/README.md b/ocr_webapp/PaddleOCR/deploy/ios_demo/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..3000495e328eb396304001fff686cf9c4cbc8ab5
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/README.md
@@ -0,0 +1,3 @@
+# ios Demo
+
+参考 https://github.com/PaddlePaddle/Paddle-Lite-Demo/blob/develop/ocr/ios/ppocr_demo/ppocr_demo/README.md
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/README_en.md b/ocr_webapp/PaddleOCR/deploy/ios_demo/README_en.md
new file mode 100644
index 0000000000000000000000000000000000000000..540dbd6baa5201fcf2f84a5b45531902a5339bbc
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/README_en.md
@@ -0,0 +1,5 @@
+# PaddleOCR iOS Demo
+
+[中文](./README.md) | English
+
+Please refer to the [documentation](../../docs/version3.x/inference_deployment/cross_platform/ios_deployment.en.md).
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/requirements-accuracy.txt b/ocr_webapp/PaddleOCR/deploy/ios_demo/requirements-accuracy.txt
new file mode 100644
index 0000000000000000000000000000000000000000..302908ec31e34e9184e48263c6c47b5e2ba85a3b
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/requirements-accuracy.txt
@@ -0,0 +1,2 @@
+PyYAML>=6.0
+shapely>=2.0
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/requirements-build-calib.txt b/ocr_webapp/PaddleOCR/deploy/ios_demo/requirements-build-calib.txt
new file mode 100644
index 0000000000000000000000000000000000000000..917552454c9d8d2532b10ab5dc3a3aa5afc8ca02
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/requirements-build-calib.txt
@@ -0,0 +1,4 @@
+numpy
+paddlex>=3.5,<3.6
+onnxruntime>=1.18
+opencv-contrib-python>=4.5
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/requirements-onnx-convert-ort.txt b/ocr_webapp/PaddleOCR/deploy/ios_demo/requirements-onnx-convert-ort.txt
new file mode 100644
index 0000000000000000000000000000000000000000..a58c03cd1be779e60d95f21b0c8ba3610565185b
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/requirements-onnx-convert-ort.txt
@@ -0,0 +1,3 @@
+numpy>=1.20
+onnx>=1.16,<1.21
+onnxruntime>=1.18
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/requirements-onnx-quantize.txt b/ocr_webapp/PaddleOCR/deploy/ios_demo/requirements-onnx-quantize.txt
new file mode 100644
index 0000000000000000000000000000000000000000..dc405192303302dd29bd5d4e21e70f87b19778a3
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/requirements-onnx-quantize.txt
@@ -0,0 +1,5 @@
+# Keep onnx and onnxruntime within a range that works together (avoids IR / schema skew on older CPU wheels).
+# For best parity with the app, install a onnxruntime major version close to onnxruntime-objc in the Podfile.
+numpy>=1.20
+onnx>=1.16,<1.21
+onnxruntime>=1.18
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/scripts/build_onnx_calib_npy.py b/ocr_webapp/PaddleOCR/deploy/ios_demo/scripts/build_onnx_calib_npy.py
new file mode 100644
index 0000000000000000000000000000000000000000..d3cf1d0fd1a41e226c67ac6cd4c46a201c84a245
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/scripts/build_onnx_calib_npy.py
@@ -0,0 +1,397 @@
+#!/usr/bin/env python3
+# Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Build float32 NCHW ``.npy`` tensors for static ONNX quantization.
+
+Note:
+ Uses undocumented PaddleX predictor internals (e.g. ``pre_tfs``). May break when
+ upgrading PaddleX.
+
+Examples:
+
+ # Detection model
+ python3 scripts/build_onnx_calib_npy.py --task det \\
+ --model-dir PaddleOCRDemo/Models/det --image-dir /path/to/images --output-dir calib_det
+
+ # Recognition model (one tensor per full image)
+ python3 scripts/build_onnx_calib_npy.py --task rec \\
+ --model-dir PaddleOCRDemo/Models/rec --image-dir /path/to/images --output-dir calib_rec
+
+ # Recognition: full-page images — run det to crop line boxes, one tensor per crop
+ python3 scripts/build_onnx_calib_npy.py --task rec \\
+ --model-dir PaddleOCRDemo/Models/rec --det-model-dir PaddleOCRDemo/Models/det \\
+ --image-dir /path/to/pages --output-dir calib_rec_crops
+"""
+
+from __future__ import annotations
+
+import argparse
+import re
+import sys
+from pathlib import Path
+from typing import Any, Iterator
+
+import cv2
+import numpy as np
+from paddlex import create_predictor
+
+_script_dir = Path(__file__).resolve().parent
+if str(_script_dir) not in sys.path:
+ sys.path.insert(0, str(_script_dir))
+
+from utils import die
+
+
+def _read_model_name(model_dir: Path) -> str:
+ try:
+ import yaml
+ except ImportError as e:
+ die(f"PyYAML is required: {e}")
+ p = model_dir / "inference.yml"
+ if not p.is_file():
+ die(f"missing {p}")
+ with p.open("r", encoding="utf-8") as f:
+ data = yaml.safe_load(f)
+ name = (data or {}).get("Global", {}).get("model_name")
+ if not name:
+ die("Global.model_name not found in inference.yml")
+ return str(name)
+
+
+def _collect_images(image_dir: Path) -> list[Path]:
+ exts = {".png", ".jpg", ".jpeg", ".bmp", ".webp", ".tif", ".tiff"}
+ out: list[Path] = []
+ for p in sorted(image_dir.iterdir()):
+ if p.is_file() and p.suffix.lower() in exts:
+ out.append(p)
+ if not out:
+ die(f"no images found under {image_dir}")
+ return out
+
+
+def _safe_stem(p: Path, idx: int) -> str:
+ s = p.stem
+ s = re.sub(r"[^\w\-.]+", "_", s)[:80]
+ return f"{idx:04d}_{s}" if s else f"{idx:04d}"
+
+
+def _iter_text_boxes(dt_polys: Any) -> Iterator[np.ndarray]:
+ """Turn ``dt_polys`` from a TextDetResult into a sequence of (N,2) point arrays."""
+ if dt_polys is None:
+ return
+ if isinstance(dt_polys, np.ndarray):
+ if dt_polys.size == 0:
+ return
+ if dt_polys.ndim == 2 and dt_polys.shape[0] >= 3:
+ yield np.array(dt_polys, dtype=np.float32, copy=True)
+ elif dt_polys.ndim == 3:
+ for i in range(dt_polys.shape[0]):
+ box = np.array(dt_polys[i], dtype=np.float32, copy=True)
+ if box.shape[0] >= 3:
+ yield box
+ return
+ for box in dt_polys:
+ b = np.array(box, dtype=np.float32, copy=True)
+ if b.size >= 6 and b.ndim == 2 and b.shape[0] >= 3:
+ yield b
+
+
+def _minarea_rect_rotate_crop(img: np.ndarray, points: np.ndarray) -> np.ndarray:
+ """
+ Min-area rect crop aligned with PP-OCR (see PaddleX CropByPolys, quad).
+ *img* is RGB (same as TextDet Read output).
+ """
+ tmp_box = np.array(points, dtype=np.float32, copy=True)
+ bounding_box = cv2.minAreaRect(tmp_box.astype(np.int32))
+ pts = sorted(list(cv2.boxPoints(bounding_box)), key=lambda x: x[0])
+ if pts[1][1] > pts[0][1]:
+ index_a, index_d = 0, 1
+ else:
+ index_a, index_d = 1, 0
+ if pts[3][1] > pts[2][1]:
+ index_b, index_c = 2, 3
+ else:
+ index_b, index_c = 3, 2
+ box = [pts[index_a], pts[index_b], pts[index_c], pts[index_d]]
+ pts4 = [np.array(p, dtype=np.float32) for p in box]
+ assert len(pts4) == 4, "shape of points must be 4*2"
+ img_crop_width = int(
+ max(
+ np.linalg.norm(pts4[0] - pts4[1]),
+ np.linalg.norm(pts4[2] - pts4[3]),
+ )
+ )
+ img_crop_height = int(
+ max(
+ np.linalg.norm(pts4[0] - pts4[3]),
+ np.linalg.norm(pts4[1] - pts4[2]),
+ )
+ )
+ if img_crop_width < 1 or img_crop_height < 1:
+ raise ValueError("degenerate text crop (zero width/height)")
+ pts_std = np.float32(
+ [
+ [0, 0],
+ [img_crop_width, 0],
+ [img_crop_width, img_crop_height],
+ [0, img_crop_height],
+ ]
+ )
+ pstack = np.stack(pts4, axis=0)
+ m = cv2.getPerspectiveTransform(pstack, pts_std)
+ dst = cv2.warpPerspective(
+ img,
+ m,
+ (img_crop_width, img_crop_height),
+ borderMode=cv2.BORDER_REPLICATE,
+ flags=cv2.INTER_CUBIC,
+ )
+ dh, dw = dst.shape[0:2]
+ if dh * 1.0 / max(dw, 1) >= 1.5:
+ dst = np.rot90(dst)
+ return dst
+
+
+def _tensor_det(pred, image_path: str) -> np.ndarray:
+ """Mirror TextDetRunnerPredictor.process() preprocess, return (1,C,H,W) float32."""
+ batches = list(pred.batch_sampler([image_path]))
+ b = batches[0]
+ batch_raw_imgs = pred.pre_tfs["Read"](imgs=b.instances)
+ batch_imgs, _ = pred.pre_tfs["Resize"](
+ imgs=batch_raw_imgs,
+ limit_side_len=pred.limit_side_len,
+ limit_type=pred.limit_type,
+ max_side_limit=pred.max_side_limit,
+ )
+ batch_imgs = pred.pre_tfs["Normalize"](imgs=batch_imgs)
+ batch_imgs = pred.pre_tfs["ToCHW"](imgs=batch_imgs)
+ x = pred.pre_tfs["ToBatch"](imgs=batch_imgs)
+ if not x:
+ die("ToBatch returned empty (det)")
+ return np.ascontiguousarray(x[0], dtype=np.float32)
+
+
+def _tensor_rec(pred, image_path: str) -> np.ndarray:
+ from paddlex.inference.models.text_recognition.processors import (
+ validate_text_rec_image_array,
+ )
+
+ batches = list(pred.batch_sampler([image_path]))
+ b = batches[0]
+ batch_raw_imgs = pred.pre_tfs["Read"](imgs=b.instances)
+ for i, img in enumerate(batch_raw_imgs):
+ validate_text_rec_image_array(img, index=i)
+ batch_imgs = pred.pre_tfs["ReisizeNorm"](imgs=batch_raw_imgs)
+ x = pred.pre_tfs["ToBatch"](imgs=batch_imgs)
+ if not x:
+ die("ToBatch returned empty (rec)")
+ return np.ascontiguousarray(x[0], dtype=np.float32)
+
+
+def _tensor_rec_from_rgb_ndarray(rec_pred, img_rgb: np.ndarray) -> np.ndarray:
+ """Same preprocessing as *path* input, for an RGB array (e.g. det crop on full page)."""
+ from paddlex.inference.models.text_recognition.processors import (
+ validate_text_rec_image_array,
+ )
+
+ bgr = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2BGR)
+ batch_raw_imgs = rec_pred.pre_tfs["Read"](imgs=[bgr])
+ for i, img in enumerate(batch_raw_imgs):
+ validate_text_rec_image_array(img, index=i)
+ batch_imgs = rec_pred.pre_tfs["ReisizeNorm"](imgs=batch_raw_imgs)
+ x = rec_pred.pre_tfs["ToBatch"](imgs=batch_imgs)
+ if not x:
+ die("ToBatch returned empty (rec, ndarray)")
+ return np.ascontiguousarray(x[0], dtype=np.float32)
+
+
+def main() -> None:
+ ap = argparse.ArgumentParser(
+ description="Build float32 NCHW .npy files for static ONNX quantize."
+ )
+ ap.add_argument(
+ "--image-dir",
+ type=Path,
+ required=True,
+ help="Directory of input images.",
+ )
+ ap.add_argument(
+ "--output-dir",
+ type=Path,
+ required=True,
+ help="Output directory for .npy files (created if missing).",
+ )
+ ap.add_argument(
+ "--model-dir",
+ type=Path,
+ required=True,
+ help="Model directory.",
+ )
+ ap.add_argument(
+ "--task",
+ choices=("det", "rec"),
+ required=True,
+ help="Model role: det = text detection, rec = text recognition.",
+ )
+ ap.add_argument(
+ "--det-model-dir",
+ type=Path,
+ default=None,
+ help=(
+ "For --task rec only: text-detection model directory. "
+ "When set, each full image is run through det; each text box is cropped and one "
+ "rec calibration tensor is written per crop (instead of one tensor per file)."
+ ),
+ )
+ ap.add_argument(
+ "--max-crops-per-image",
+ type=int,
+ default=0,
+ help="With --det-model-dir, cap how many box crops to use per input image (0 = no cap).",
+ )
+ ap.add_argument("--device", default="cpu", help="Device string, e.g. cpu or gpu:0.")
+ args = ap.parse_args()
+
+ if not args.image_dir.is_dir():
+ die(f"not a directory: {args.image_dir}")
+ if not (args.model_dir / "inference.onnx").is_file():
+ die(f"missing {args.model_dir / 'inference.onnx'}")
+
+ if args.det_model_dir is not None:
+ if args.task != "rec":
+ die("--det-model-dir is only valid with --task rec")
+ if not (args.det_model_dir / "inference.onnx").is_file():
+ die(f"missing {args.det_model_dir / 'inference.onnx'}")
+
+ model_name = _read_model_name(args.model_dir)
+ if args.task == "det" and "_rec" in model_name and "_det" not in model_name:
+ print(
+ "warning: --task det but model_name looks like recognition; check --model-dir",
+ file=sys.stderr,
+ )
+ if args.task == "rec" and "_det" in model_name and "_rec" not in model_name:
+ print(
+ "warning: --task rec but model_name looks like detection; check --model-dir",
+ file=sys.stderr,
+ )
+
+ try:
+ pred = create_predictor(
+ model_name,
+ model_dir=str(args.model_dir.resolve()),
+ device=args.device,
+ engine="onnxruntime",
+ batch_size=1,
+ )
+ except Exception as e:
+ die(f"create_predictor failed: {e}")
+
+ det_pred = None
+ det_name = None
+ if args.det_model_dir is not None:
+ det_name = _read_model_name(args.det_model_dir)
+ if "_rec" in det_name and "_det" not in det_name:
+ print(
+ "warning: --det-model-dir model_name looks like recognition; check path",
+ file=sys.stderr,
+ )
+ try:
+ det_pred = create_predictor(
+ det_name,
+ model_dir=str(args.det_model_dir.resolve()),
+ device=args.device,
+ engine="onnxruntime",
+ batch_size=1,
+ )
+ except Exception as e:
+ die(f"create_predictor (det) failed: {e}")
+
+ out_dir = args.output_dir
+ out_dir.mkdir(parents=True, exist_ok=True)
+
+ images = _collect_images(args.image_dir)
+ written = 0
+ for idx, path in enumerate(images):
+ pstr = str(path.resolve())
+ if args.task == "det":
+ try:
+ arr = _tensor_det(pred, pstr)
+ except Exception as e:
+ die(f"preprocess failed for {path}: {e}")
+ if arr.dtype != np.float32:
+ arr = arr.astype(np.float32)
+ name = _safe_stem(path, idx) + ".npy"
+ out_path = out_dir / name
+ np.save(str(out_path), arr)
+ print(f"wrote {out_path} shape={arr.shape} dtype={arr.dtype}")
+ written += 1
+ continue
+
+ # rec
+ if det_pred is None:
+ try:
+ arr = _tensor_rec(pred, pstr)
+ except Exception as e:
+ die(f"preprocess failed for {path}: {e}")
+ if arr.dtype != np.float32:
+ arr = arr.astype(np.float32)
+ out_path = out_dir / (_safe_stem(path, idx) + ".npy")
+ np.save(str(out_path), arr)
+ print(f"wrote {out_path} shape={arr.shape} dtype={arr.dtype}")
+ written += 1
+ continue
+
+ # rec + det: crops from full page
+ try:
+ det_res = next(det_pred(pstr)) # type: ignore[union-attr]
+ except Exception as e:
+ die(f"detection failed for {path}: {e}")
+ polys = list(_iter_text_boxes(det_res["dt_polys"]))
+ if not polys:
+ print(
+ f"warning: no text boxes for {path}; skipping (no .npy written)",
+ file=sys.stderr,
+ )
+ continue
+ if args.max_crops_per_image > 0:
+ polys = polys[: args.max_crops_per_image]
+ full_rgb = det_res["input_img"]
+ if isinstance(full_rgb, (list, tuple)) and len(full_rgb) == 1:
+ full_rgb = full_rgb[0]
+ for box_i, quad in enumerate(polys):
+ try:
+ line_rgb = _minarea_rect_rotate_crop(full_rgb, quad)
+ except Exception as e:
+ print(
+ f"warning: skip box {box_i} in {path}: {e}",
+ file=sys.stderr,
+ )
+ continue
+ try:
+ arr = _tensor_rec_from_rgb_ndarray(pred, line_rgb)
+ except Exception as e:
+ die(f"rec preprocess failed for {path} box {box_i}: {e}")
+ if arr.dtype != np.float32:
+ arr = arr.astype(np.float32)
+ out_path = out_dir / (f"{_safe_stem(path, idx)}_box{box_i:03d}.npy")
+ np.save(str(out_path), arr)
+ print(f"wrote {out_path} shape={arr.shape} dtype={arr.dtype}")
+ written += 1
+
+ print(f"done. {written} .npy file(s) -> {out_dir}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/scripts/compare_ocr_json.py b/ocr_webapp/PaddleOCR/deploy/ios_demo/scripts/compare_ocr_json.py
new file mode 100644
index 0000000000000000000000000000000000000000..ad5d536f95f9239e8a271650d6986a03f2d1d914
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/scripts/compare_ocr_json.py
@@ -0,0 +1,223 @@
+#!/usr/bin/env python3
+# Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Compare two OCR JSON files (reference vs device) using polygon IoU + CER.
+
+Default thresholds (see argparse defaults) target a stricter CI gate than a loose
+0.5 IoU baseline: tighter box alignment, ~95% char-level headroom on mean CER,
+and limited reference-side misses.
+
+Example:
+ python compare_ocr_json.py ref.json ios.json \\
+ --iou-threshold 0.65 --cer-threshold 0.05 --max-unmatched-ratio 0.1
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import sys
+from pathlib import Path
+from typing import Any, Dict, List, Sequence, Tuple
+
+
+def _levenshtein(a: str, b: str) -> int:
+ if a == b:
+ return 0
+ if not a:
+ return len(b)
+ if not b:
+ return len(a)
+ prev = list(range(len(b) + 1))
+ for i, ca in enumerate(a, 1):
+ cur = [i]
+ for j, cb in enumerate(b, 1):
+ ins = cur[j - 1] + 1
+ delete = prev[j] + 1
+ sub = prev[j - 1] + (ca != cb)
+ cur.append(min(ins, delete, sub))
+ prev = cur
+ return prev[-1]
+
+
+def _cer(ref: str, hyp: str) -> float:
+ if not ref and not hyp:
+ return 0.0
+ if not ref:
+ return 1.0
+ return _levenshtein(ref, hyp) / max(len(ref), 1)
+
+
+def _polygon_iou(
+ poly_a: Sequence[Sequence[float]], poly_b: Sequence[Sequence[float]]
+) -> float:
+ try:
+ from shapely.geometry import Polygon
+ except ImportError as exc:
+ raise RuntimeError(
+ "compare_ocr_json.py requires `pip install shapely`"
+ ) from exc
+
+ def _to_poly(p: Sequence[Sequence[float]]) -> Polygon:
+ pts = [(float(x), float(y)) for x, y in p]
+ if len(pts) < 3:
+ return Polygon()
+ if pts[0] != pts[-1]:
+ pts = pts + [pts[0]]
+ return Polygon(pts)
+
+ p1 = _to_poly(poly_a)
+ p2 = _to_poly(poly_b)
+ if p1.is_empty or p2.is_empty or not p1.is_valid or not p2.is_valid:
+ return 0.0
+ inter = p1.intersection(p2).area
+ union = p1.union(p2).area
+ if union <= 0:
+ return 0.0
+ return float(inter / union)
+
+
+def _load_items(path: Path) -> List[Dict[str, Any]]:
+ with path.open("r", encoding="utf-8") as f:
+ data = json.load(f)
+ items = data.get("items")
+ if not isinstance(items, list):
+ raise ValueError(f"{path}: missing 'items' array")
+ return items
+
+
+def _greedy_match(
+ ref_items: List[Dict[str, Any]],
+ hyp_items: List[Dict[str, Any]],
+ iou_threshold: float,
+) -> Tuple[List[Tuple[int, int, float]], List[int], List[int]]:
+ """Return (pairs as ref_idx, hyp_idx, iou), unmatched_ref, unmatched_hyp."""
+ candidates: List[Tuple[float, int, int]] = []
+ for i, ri in enumerate(ref_items):
+ ra = ri.get("polygon")
+ if not isinstance(ra, list):
+ continue
+ for j, hj in enumerate(hyp_items):
+ ha = hj.get("polygon")
+ if not isinstance(ha, list):
+ continue
+ iou = _polygon_iou(ra, ha)
+ candidates.append((iou, i, j))
+
+ candidates.sort(key=lambda t: t[0], reverse=True)
+ used_r = set()
+ used_h = set()
+ pairs: List[Tuple[int, int, float]] = []
+ for iou, i, j in candidates:
+ if iou < iou_threshold:
+ break
+ if i in used_r or j in used_h:
+ continue
+ used_r.add(i)
+ used_h.add(j)
+ pairs.append((i, j, iou))
+
+ unmatched_r = [i for i in range(len(ref_items)) if i not in used_r]
+ unmatched_h = [j for j in range(len(hyp_items)) if j not in used_h]
+ return pairs, unmatched_r, unmatched_h
+
+
+def main(argv: Sequence[str] | None = None) -> int:
+ parser = argparse.ArgumentParser(description="Compare OCR JSON outputs")
+ parser.add_argument(
+ "reference", type=Path, help="Reference JSON (e.g. paddleocr_reference)"
+ )
+ parser.add_argument("hypothesis", type=Path, help="Device / second JSON")
+ parser.add_argument(
+ "--iou-threshold",
+ type=float,
+ default=0.65,
+ help="Min IoU to pair quadrilateral boxes (stricter than common 0.5 VOC-style cutoff)",
+ )
+ parser.add_argument(
+ "--cer-threshold",
+ type=float,
+ default=0.05,
+ help="Fail if mean CER on matched pairs exceeds this (e.g. 0.05 ≈ 95%% char accuracy headroom)",
+ )
+ parser.add_argument(
+ "--max-unmatched-ratio",
+ type=float,
+ default=0.1,
+ help="Fail if unmatched ref lines / len(ref) exceeds this",
+ )
+ parser.add_argument(
+ "--json-summary-out",
+ type=Path,
+ default=None,
+ help="Write the same JSON as stdout to this path (PASS or FAIL) for generate_benchmark_report.py",
+ )
+ args = parser.parse_args(list(argv) if argv is not None else None)
+
+ ref_items = _load_items(args.reference)
+ hyp_items = _load_items(args.hypothesis)
+
+ pairs, unmatched_r, unmatched_h = _greedy_match(
+ ref_items, hyp_items, args.iou_threshold
+ )
+
+ cers: List[float] = []
+ for ri, hj, _ in pairs:
+ rt = str(ref_items[ri].get("text", ""))
+ ht = str(hyp_items[hj].get("text", ""))
+ cers.append(_cer(rt, ht))
+
+ mean_cer = sum(cers) / len(cers) if cers else 0.0
+ nref = max(len(ref_items), 1)
+ unmatched_ratio = len(unmatched_r) / nref
+
+ report = {
+ "matched_pairs": len(pairs),
+ "reference_count": len(ref_items),
+ "hypothesis_count": len(hyp_items),
+ "unmatched_reference": len(unmatched_r),
+ "unmatched_hypothesis": len(unmatched_h),
+ "unmatched_reference_ratio": unmatched_ratio,
+ "mean_cer_matched": mean_cer,
+ "iou_threshold": args.iou_threshold,
+ "cer_threshold": args.cer_threshold,
+ "max_unmatched_ratio": args.max_unmatched_ratio,
+ }
+ failed = False
+ if mean_cer > args.cer_threshold:
+ print(f"FAIL: mean CER {mean_cer:.4f} > {args.cer_threshold}", file=sys.stderr)
+ failed = True
+ if unmatched_ratio > args.max_unmatched_ratio:
+ print(
+ f"FAIL: unmatched ref ratio {unmatched_ratio:.4f} > {args.max_unmatched_ratio}",
+ file=sys.stderr,
+ )
+ failed = True
+
+ report["pass"] = not failed
+ out_txt = json.dumps(report, indent=2, ensure_ascii=False)
+ print(out_txt)
+
+ if args.json_summary_out is not None:
+ args.json_summary_out.parent.mkdir(parents=True, exist_ok=True)
+ args.json_summary_out.write_text(out_txt + "\n", encoding="utf-8")
+
+ if not failed:
+ print("PASS", file=sys.stderr)
+ return 1 if failed else 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/ocr_webapp/PaddleOCR/deploy/ios_demo/scripts/convert_onnx_to_ort.sh b/ocr_webapp/PaddleOCR/deploy/ios_demo/scripts/convert_onnx_to_ort.sh
new file mode 100644
index 0000000000000000000000000000000000000000..3ccd8be21daa8d4d15f61a318978e38896cb7a36
--- /dev/null
+++ b/ocr_webapp/PaddleOCR/deploy/ios_demo/scripts/convert_onnx_to_ort.sh
@@ -0,0 +1,184 @@
+#!/usr/bin/env bash
+# Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Convert ONNX models to ORT format for ONNX Runtime.
+# See: https://onnxruntime.ai/docs/performance/model-optimizations/ort-format-models.html
+#
+# Requires: python3
+#
+# Usage:
+# ./scripts/convert_onnx_to_ort.sh
+# ./scripts/convert_onnx_to_ort.sh --output-dir ./out/ort_bundles
+# ./scripts/convert_onnx_to_ort.sh --input-dir /path/to/models --output-dir /tmp/ort
+# ./scripts/convert_onnx_to_ort.sh --optimization-style Runtime
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+IOS_DEMO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
+DEFAULT_INPUT_DIR="${IOS_DEMO_ROOT}/PaddleOCRDemo/Models"
+
+INPUT_DIR="${DEFAULT_INPUT_DIR}"
+ORT_OUTPUT_DIR=""
+KEEP_OPERATOR_CONFIG=false
+EXPLICIT_OPTIMIZATION_STYLE=false
+OPTIMIZATION_STYLE_VALUES=()
+
+usage() {
+ cat < Root directory to scan for *.onnx (recursive). Default: PaddleOCRDemo/Models.
+ --output-dir If set, write .ort files under this directory, mirroring relative paths from
+ the input tree; also copies inference.yml beside each converted bundle (same
+ layout as under --input-dir) so you can swap in the whole output folder. If
+ omitted, write next to each .onnx in-place.
+ --keep-operator-config
+ Keep required_operators*.config files emitted by the ORT converter (for
+ minimal ORT builds). By default these are removed after a successful run.
+ --optimization-style