6.7.0 - Alpha16 - 打包应用支持自带离线 Paddle OCR 引擎, 避免依赖插件安装

This commit is contained in:
SuperMonster003
2026-01-18 23:33:18 +08:00
parent 52be56318f
commit a46d78b704
54 changed files with 2745 additions and 253 deletions

View File

@@ -0,0 +1,123 @@
package com.baidu.paddle.lite.ocr;
import android.graphics.Bitmap;
import android.util.Log;
import org.opencv.android.OpenCVLoader;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author PaddleOCR
* Modified by TonyJiangWJ
* @since 2023-08-06
*/
public class OCRPredictorNative {
private static final AtomicBoolean isSOLoaded = new AtomicBoolean();
private static final ReentrantLock lock = new ReentrantLock();
public static void loadLibrary() throws RuntimeException {
if (!isSOLoaded.get() && isSOLoaded.compareAndSet(false, true)) {
try {
// 可能和 AJ 中的 OpenCV 冲突, 直接初始化一遍
OpenCVLoader.initDebug();
System.loadLibrary("Native");
} catch (Throwable e) {
throw new RuntimeException(
"Load libNative.so failed, please check it exists in apk file.", e);
}
}
}
private long nativePointer;
public OCRPredictorNative(Config config) {
lock.lock();
try {
loadLibrary();
nativePointer = init(config.detModelFilename, config.recModelFilename, config.clsModelFilename, config.useOpenCL,
config.cpuThreadNum, config.cpuPower);
Log.i("OCRPredictorNative", "load success " + nativePointer);
} finally {
lock.unlock();
}
}
public ArrayList<OcrResultModel> runImage(Bitmap originalImage, int max_size_len, int run_det, int run_cls, int run_rec) {
lock.lock();
try {
Log.i("OCRPredictorNative", "begin to run image");
float[] rawResults = forward(nativePointer, originalImage, max_size_len, run_det, run_cls, run_rec);
return postprocess(rawResults);
} finally {
lock.unlock();
}
}
public static class Config {
public int useOpenCL;
public int cpuThreadNum;
public String cpuPower;
public String detModelFilename;
public String recModelFilename;
public String clsModelFilename;
}
public void destroy() {
if (nativePointer != 0) {
release(nativePointer);
nativePointer = 0;
}
}
protected native long init(String detModelPath, String recModelPath, String clsModelPath, int useOpenCL, int threadNum, String cpuMode);
protected native float[] forward(long pointer, Bitmap originalImage, int max_size_len, int run_det, int run_cls, int run_rec);
protected native void release(long pointer);
private ArrayList<OcrResultModel> postprocess(float[] raw) {
ArrayList<OcrResultModel> results = new ArrayList<>();
int begin = 0;
while (begin < raw.length) {
int point_num = Math.round(raw[begin]);
int word_num = Math.round(raw[begin + 1]);
OcrResultModel model = parse(raw, begin + 2, point_num, word_num);
begin += 2 + 1 + point_num * 2 + word_num + 2;
results.add(model);
}
return results;
}
private OcrResultModel parse(float[] raw, int begin, int pointNum, int wordNum) {
int current = begin;
OcrResultModel model = new OcrResultModel();
model.setConfidence(raw[current]);
current++;
for (int i = 0; i < pointNum; i++) {
model.addPoints(Math.round(raw[current + i * 2]), Math.round(raw[current + i * 2 + 1]));
}
current += (pointNum * 2);
for (int i = 0; i < wordNum; i++) {
int index = Math.round(raw[current + i]);
model.addWordIndex(index);
}
current += wordNum;
model.setClsIdx(raw[current]);
model.setClsConfidence(raw[current + 1]);
// Log.i("OCRPredictorNative", "word finished " + wordNum);
return model;
}
@Override
protected void finalize() throws Throwable {
super.finalize();
destroy();
}
}

View File

@@ -0,0 +1,134 @@
package com.baidu.paddle.lite.ocr;
import android.graphics.Point;
import android.graphics.Rect;
import java.util.ArrayList;
import java.util.List;
/**
* @author PaddleOCR
* Modified by TonyJiangWJ
* @since 2023-08-06
*/
public class OcrResult implements Comparable<OcrResult> {
private String label;
private float confidence;
private Rect bounds;
private final List<OcrResult> elements = new ArrayList<>();
public OcrResult() {
}
public OcrResult(OcrResultModel resultModel) {
this.label = resultModel.getLabel();
this.confidence = resultModel.getConfidence();
int left = -1, right = -1, top = -1, bottom = -1;
for (Point point : resultModel.getPoints()) {
if (point.x < left || left == -1) {
left = point.x;
}
if (point.x > right || right == -1) {
right = point.x;
}
if (point.y < top || top == -1) {
top = point.y;
}
if (point.y > bottom || bottom == -1) {
bottom = point.y;
}
}
this.bounds = new Rect(left, top, right, bottom);
}
public OcrResult(String label, float confidence, Rect bounds) {
this.label = label;
this.confidence = confidence;
this.bounds = bounds;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
public float getConfidence() {
return confidence;
}
public void setConfidence(float confidence) {
this.confidence = confidence;
}
public Rect getBounds() {
return bounds;
}
public void setBounds(Rect bounds) {
this.bounds = bounds;
}
public RectLocation getLocation() {
return new RectLocation(bounds);
}
public String getWords() {
return label.trim().replace("\r", "");
}
public List<OcrResult> getElements() {
return this.elements;
}
public void addElements(OcrResult element) {
this.elements.add(element);
}
@Override
public int compareTo(OcrResult o) {
// 上下差距小于二分之一的高度 判定为同一行
int deviation = Math.max(this.bounds.height(), o.bounds.height()) / 2;
// 通过垂直中心点的距离判定
if (Math.abs((this.bounds.top + this.bounds.bottom) / 2 - (o.bounds.top + o.bounds.bottom) / 2) < deviation) {
return this.bounds.left - o.bounds.left;
} else {
return this.bounds.bottom - o.bounds.bottom;
}
}
@Override
public String toString() {
return "OcrResult{" + "label='" + label + '\'' +
", confidence=" + confidence +
", bounds=" + bounds +
", elements=" + elements +
'}';
}
public static class RectLocation {
public int left;
public int top;
public int width;
public int height;
public RectLocation() {
}
public RectLocation(int left, int top, int width, int height) {
this.left = left;
this.top = top;
this.width = width;
this.height = height;
}
public RectLocation(Rect rect) {
left = rect.left;
top = rect.top;
width = rect.right - rect.left;
height = rect.bottom - rect.top;
}
}
}

View File

@@ -0,0 +1,97 @@
package com.baidu.paddle.lite.ocr;
import android.graphics.Point;
import java.util.ArrayList;
import java.util.List;
/**
* @author PaddleOCR
* Modified by TonyJiangWJ
* @since 2023-08-06
*/
public class OcrResultModel {
private final List<Point> points;
private final List<Integer> wordIndex;
private String label;
private float confidence;
private float clsIdx;
private String clsLabel;
private float clsConfidence;
public OcrResultModel() {
super();
points = new ArrayList<>();
wordIndex = new ArrayList<>();
}
public void addPoints(int x, int y) {
Point point = new Point(x, y);
points.add(point);
}
public void addWordIndex(int index) {
wordIndex.add(index);
}
public List<Point> getPoints() {
return points;
}
public List<Integer> getWordIndex() {
return wordIndex;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
public float getConfidence() {
return confidence;
}
public void setConfidence(float confidence) {
this.confidence = confidence;
}
public float getClsIdx() {
return clsIdx;
}
public void setClsIdx(float idx) {
this.clsIdx = idx;
}
public String getClsLabel() {
return clsLabel;
}
public void setClsLabel(String label) {
this.clsLabel = label;
}
public float getClsConfidence() {
return clsConfidence;
}
public void setClsConfidence(float confidence) {
this.clsConfidence = confidence;
}
@Override
public String toString() {
return "OcrResultModel{" +
"points=" + points +
", wordIndex=" + wordIndex +
", label='" + label + '\'' +
", confidence=" + confidence +
", clsIdx=" + clsIdx +
", clsLabel='" + clsLabel + '\'' +
", clsConfidence=" + clsConfidence +
'}';
}
}

View File

@@ -0,0 +1,155 @@
package com.baidu.paddle.lite.ocr;
import android.content.Context;
import android.os.Build;
import android.util.Log;
public final class OpenCLGuard {
private static final String TAG = "OpenCLGuard";
private static final String SP = "paddle_opencl_probe";
private static final String KEY_CACHED_RES = "res_";
private static final String KEY_CACHED_AT = "at_";
// Timestamp of crash fuse.
// zh-CN: 崩溃保险丝时间戳.
private static final String KEY_LAST_FUSE = "fuse_";
private static final long CACHE_TTL_MS = 24L * 60 * 60 * 1000; // 24h
private static final long FUSE_MUTE_MS = 7L * 24 * 60 * 60 * 1000; // 7d
private static final String FINGERPRINT = android.os.Build.FINGERPRINT;
private static final String CACHE_KEY_RES = KEY_CACHED_RES + FINGERPRINT;
private static final String CACHE_KEY_AT = KEY_CACHED_AT + FINGERPRINT;
private static final String FUSE_KEY = KEY_LAST_FUSE + FINGERPRINT;
private static final String[] CANDIDATES = new String[]{
// Common Treble partitions.
// zh-CN: 常见 Treble 分区.
"/vendor/lib64/libOpenCL.so",
"/system/lib64/libOpenCL.so",
"/system/vendor/lib64/libOpenCL.so",
"/odm/lib64/libOpenCL.so",
// Some vendors put OpenCL in GPU APEX/extension area (not standard, just try).
// zh-CN: 部分厂商会把 OpenCL 放到 GPU APEX/扩展区 (并不标准, 仅做尝试).
"/apex/com.android.hwext/lib64/libOpenCL.so",
// Some devices put ICD in a proprietary directory (rare).
// zh-CN: 部分设备把 ICD 放在专有目录 (罕见).
"/vendor/lib64/egl/libOpenCL.so"
};
/**
* Mark: About to initialize OpenCL (if APP crashes, it can be detected next time).
* zh-CN: 标记: 准备开始初始化 OpenCL (若 APP 崩溃, 下次就能检测到).
*/
public static void markInitStart(Context ctx) {
ctx.getSharedPreferences(SP, 0).edit().putLong(FUSE_KEY, System.currentTimeMillis()).apply();
}
/**
* Mark: OpenCL initialization has safely ended (regardless of success or failure).
* zh-CN: 标记: OpenCL 初始化已安全结束 (无论成功或失败).
*/
public static void markInitEnd(Context ctx) {
ctx.getSharedPreferences(SP, 0).edit().remove(FUSE_KEY).apply();
}
/**
* Whether to recommend enabling OpenCL (with cache + fuse + absolute path loading attempt).
* zh-CN: 是否建议启用 OpenCL (带缓存 + 保险丝 + 绝对路径加载尝试).
*/
public static boolean isOpenCLRuntimeAvailable(Context ctx) {
// Crash fuse: last initialization did not end normally -> pause for 7 days.
// zh-CN: 崩溃保险丝: 上次初始化未正常结束 -> 暂停 7 天.
long fuseTs = ctx.getSharedPreferences(SP, 0).getLong(FUSE_KEY, 0L);
if (fuseTs > 0 && (System.currentTimeMillis() - fuseTs) < FUSE_MUTE_MS) {
Log.w(TAG, "[OpenCL] Fuse active, skip probing.");
return false;
}
// Only try in 64-bit process + arm64 device.
// zh-CN: 只在 64-bit 进程 + arm64 设备尝试.
boolean isArm64Device = false;
try {
String[] abis64 = Build.SUPPORTED_64_BIT_ABIS;
if (abis64 != null) for (String abi : abis64) {
if ("arm64-v8a".equalsIgnoreCase(abi)) {
isArm64Device = true;
break;
}
}
} catch (Throwable ignore) {
/* Ignored. */
}
boolean is64Process = System.getProperty("os.arch", "").contains("64");
if (!(isArm64Device && is64Process)) {
Log.i(TAG, "[OpenCL] Not arm64/64-bit process, skip. dev=" + isArm64Device + " proc=" + is64Process);
return false;
}
// Read cache.
// zh-CN: 读取缓存.
final var sp = ctx.getSharedPreferences(SP, 0);
long cachedAt = sp.getLong(CACHE_KEY_AT, 0L);
if (cachedAt > 0 && (System.currentTimeMillis() - cachedAt) < CACHE_TTL_MS) {
boolean cached = sp.getBoolean(CACHE_KEY_RES, false);
Log.i(TAG, "[OpenCL] use cached=" + cached);
return cached;
}
// 1) Absolute path existence.
// zh-CN: 1) 绝对路径存在性.
String hitPath = null;
for (String p : CANDIDATES) {
try {
if (new java.io.File(p).exists()) {
hitPath = p;
break;
}
} catch (Throwable ignore) {
}
}
// 2) Try loading (absolute path first, then loadLibrary("OpenCL")).
// zh-CN: 2) 尝试加载 (绝对路径优先, 其次 loadLibrary("OpenCL")).
boolean loadOk = false;
// 2.1 Absolute path dlopen.
// zh-CN: 2.1 绝对路径 dlopen.
if (hitPath != null) {
try {
System.load(hitPath);
loadOk = true;
Log.i(TAG, "[OpenCL] System.load hit: " + hitPath);
} catch (Throwable t) {
Log.i(TAG, "[OpenCL] System.load failed: " + hitPath + " -> " + t.getClass().getSimpleName());
}
}
// 2.2 Regular link name.
// zh-CN: 2.2 常规链接名.
if (!loadOk) {
try {
System.loadLibrary("OpenCL");
loadOk = true;
Log.i(TAG, "[OpenCL] loadLibrary(\"OpenCL\") ok.");
} catch (Throwable t) {
Log.i(TAG, "[OpenCL] loadLibrary(\"OpenCL\") failed: " + t.getMessage());
}
}
int probe = -999;
if (loadOk) {
try {
probe = OpenCLProbe.nativeProbeOpenCL();
} catch (Throwable t) {
Log.i(TAG, "[OpenCL] nativeProbeOpenCL error: " + t.getMessage());
}
}
// At least 1 platform.
// zh-CN: 至少 1 个平台.
boolean available = loadOk && probe >= 1;
Log.i(TAG, "[OpenCL] available=" + available + " (loadOk=" + loadOk + ", platforms=" + probe + ")");
// Write cache.
// zh-CN: 写缓存.
sp.edit().putBoolean(CACHE_KEY_RES, available).putLong(CACHE_KEY_AT, System.currentTimeMillis()).apply();
return available;
}
}

View File

@@ -0,0 +1,8 @@
package com.baidu.paddle.lite.ocr;
public final class OpenCLProbe {
static {
try { System.loadLibrary("opencl_probe"); } catch (Throwable ignore) {}
}
public static native int nativeProbeOpenCL();
}

View File

@@ -0,0 +1,286 @@
package com.baidu.paddle.lite.ocr
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import org.autojs.plugin.paddle.ocr.api.OcrOptions
import org.autojs.plugin.paddle.ocr.api.OcrResult
import java.io.FileNotFoundException
/**
* A unified embedded Paddle OCR engine API for both host (INRT) and plugin APK.
* zh-CN: 面向宿主 (INRT) 与插件 APK 的统一 Paddle OCR 内置引擎 API.
*
* Created by JetBrains AI Assistant (GPT-5.2) on Jan 17, 2026.
* Modified by SuperMonster003 as of Jan 18, 2026.
*/
class PaddleOcrEngine(
private val appContext: Context,
private val variant: VariantSpec,
private val bridge: NativeBridge = PredictorNativeBridge(),
) {
@Volatile
private var initialized: Boolean = false
private val lock = Any()
/**
* Initialize native libs and load models.
* zh-CN: 初始化 native 库并加载模型.
*/
fun ensureInitialized(options: OcrOptions) {
if (initialized) return
synchronized(lock) {
if (initialized) return
// Resolve model profile by options + variant.
// zh-CN: 根据 options + variant 决定模型组合.
val profile = variant.resolveProfile(options)
// Ensure required assets exist.
// zh-CN: 确保所需 assets 存在.
variant.assertAssetsExist(appContext, profile)
// Prepare checking bitmap from drawable resource.
// zh-CN: 从 drawable 资源准备检查用 bitmap.
val checkingBitmap = decodeDrawable(appContext, variant.checkingDrawableRes)
// Delegate to bridge for real initialization.
// zh-CN: 委托给 bridge 执行真实初始化.
bridge.init(
context = appContext,
profile = profile,
checkingBitmap = checkingBitmap,
)
initialized = true
}
}
/**
* Recognize text only.
* zh-CN: 仅识别文本.
*/
fun recognizeText(bitmap: Bitmap, options: OcrOptions): List<String> {
ensureInitialized(options)
return bridge.recognizeText(bitmap, options)
}
/**
* Detect with boxes.
* zh-CN: 检测并返回文本框信息.
*/
fun detect(bitmap: Bitmap, options: OcrOptions): List<OcrResult> {
ensureInitialized(options)
return bridge.detect(bitmap, options)
}
private fun decodeDrawable(context: Context, resId: Int): Bitmap {
return BitmapFactory.decodeResource(context.resources, resId)
?: throw IllegalStateException(
context.getString(R.string.error_failed_to_decode_checking_drawable_resource)
)
}
}
/**
* A single point for real predictor/native invocation.
* zh-CN: 真实 predictor/native 调用的单点封装.
*/
interface NativeBridge {
/**
* Initialize predictor with model paths.
* zh-CN: 用模型路径初始化 predictor.
*/
fun init(
context: Context,
profile: ModelProfile,
checkingBitmap: Bitmap,
)
/**
* Recognize text.
* zh-CN: 识别文本.
*/
fun recognizeText(bitmap: Bitmap, options: OcrOptions): List<String>
/**
* Detect results with boxes.
* zh-CN: 检测并返回带框结果.
*/
fun detect(bitmap: Bitmap, options: OcrOptions): List<OcrResult>
}
/**
* Resolved model paths for one run configuration.
* zh-CN: 单次运行配置解析出的模型路径集合.
*/
data class ModelProfile(
val variantName: String,
val labelAssetPath: String,
// v5 uses Predictor's internal default dirs; v3 uses explicit dir.
// zh-CN: v5 由 Predictor 内部默认目录决定, v3 使用显式目录.
val modelDir: String?,
val useSlim: Boolean,
val useOpenCL: Boolean,
val cpuThreadNum: Int,
val detModelFile: String,
val recModelFile: String,
val clsModelFile: String,
val assetsToCheck: List<String>,
)
/**
* Engine variant specification (v3/v5).
* zh-CN: 引擎变体规范 (v3/v5).
*/
data class VariantSpec(
val name: String,
val supportsOpenCL: Boolean,
val labelAssetPath: String,
// Explicit model directories.
// zh-CN: 显式模型目录配置.
val modelDirCpu: String,
val modelDirCpuSlim: String? = null,
val modelDirOpenCL: String? = null,
val modelDirOpenCLSlim: String? = null,
val detModelFile: String,
val recModelFile: String,
val clsModelFile: String,
val checkingDrawableRes: Int,
) {
fun resolveProfile(options: OcrOptions): ModelProfile {
val useSlim = options.useSlim
val useOpenCLRequested = options.useOpenCL && supportsOpenCL
val resolvedDir: String? = if (name == NAME_V5) {
// v5: let Predictor decide between CPU/OpenCL + fallback internally.
// zh-CN: v5: 让 Predictor 内部决定 CPU/OpenCL 并自动回退.
null
} else {
// v3: choose deterministic dir here (OpenCL ignored).
// zh-CN: v3: 在此确定性选择目录 (OpenCL 被忽略).
if (useSlim) (modelDirCpuSlim ?: modelDirCpu) else modelDirCpu
}
// Assets to check:
// - Always check label file.
// - For v3: check the resolved model dir files.
// - For v5: check CPU + slim dirs always, and additionally OpenCL dirs if requested.
// zh-CN:
// - 总是检查 label 文件.
// - v3: 检查解析出的模型目录及其文件.
// - v5: 总是检查 CPU/INT8 目录, 若用户请求 OpenCL 再额外检查 OpenCL 目录.
val assets = buildList {
add(labelAssetPath)
val cpuDir = modelDirCpu
add("$cpuDir/$detModelFile")
add("$cpuDir/$recModelFile")
add("$cpuDir/$clsModelFile")
modelDirCpuSlim?.let { slimDir ->
add("$slimDir/$detModelFile")
add("$slimDir/$recModelFile")
add("$slimDir/$clsModelFile")
}
if (useOpenCLRequested) {
modelDirOpenCL?.let { clDir ->
add("$clDir/$detModelFile")
add("$clDir/$recModelFile")
add("$clDir/$clsModelFile")
}
modelDirOpenCLSlim?.let { clSlimDir ->
add("$clSlimDir/$detModelFile")
add("$clSlimDir/$recModelFile")
add("$clSlimDir/$clsModelFile")
}
}
// v3 deterministic dir check (override list to minimal set).
// zh-CN: v3 确定性目录检查 (覆盖为最小集合).
if (name != NAME_V5) {
clear()
add(labelAssetPath)
val dir = requireNotNull(resolvedDir)
add("$dir/$detModelFile")
add("$dir/$recModelFile")
add("$dir/$clsModelFile")
}
}
return ModelProfile(
variantName = name,
labelAssetPath = labelAssetPath,
modelDir = resolvedDir,
useSlim = useSlim,
useOpenCL = useOpenCLRequested,
cpuThreadNum = options.cpuThreadNum,
detModelFile = detModelFile,
recModelFile = recModelFile,
clsModelFile = clsModelFile,
assetsToCheck = assets.distinct(),
)
}
fun assertAssetsExist(context: Context, profile: ModelProfile) {
fun assertOne(path: String) {
try {
context.assets.open(path).use { }
} catch (e: FileNotFoundException) {
throw IllegalStateException(context.getString(R.string.error_missing_required_paddle_ocr_asset, path), e)
}
}
profile.assetsToCheck.forEach(::assertOne)
}
companion object {
const val NAME_V5 = "v5"
const val NAME_V3 = "v3"
fun v5(): VariantSpec = VariantSpec(
name = NAME_V5,
supportsOpenCL = true,
labelAssetPath = "labels/ppocr_keys_ocrv5.txt",
modelDirCpu = "models/pp-ocrv5-arm",
modelDirCpuSlim = "models/pp-ocrv5-arm-int8",
modelDirOpenCL = "models/pp-ocrv5-arm-opencl",
modelDirOpenCLSlim = "models/pp-ocrv5-arm-opencl-int8",
detModelFile = "PP-OCRv5_mobile_det.nb",
recModelFile = "PP-OCRv5_mobile_rec.nb",
clsModelFile = "PP-LCNet_x1_0_textline_ori.nb",
checkingDrawableRes = R.drawable.paddle_ocr_test,
)
fun v3(): VariantSpec = VariantSpec(
name = NAME_V3,
supportsOpenCL = false,
labelAssetPath = "labels/ppocr_keys_v1.txt",
modelDirCpu = "models/ocr_v3_for_cpu",
modelDirCpuSlim = "models/ocr_v3_for_cpu(slim)",
modelDirOpenCL = null,
modelDirOpenCLSlim = null,
detModelFile = "det_opt.nb",
recModelFile = "rec_opt.nb",
clsModelFile = "cls_opt.nb",
checkingDrawableRes = R.drawable.paddle_ocr_test,
)
}
}

View File

@@ -0,0 +1,582 @@
package com.baidu.paddle.lite.ocr;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Build;
import android.util.Base64;
import android.util.Log;
import androidx.preference.PreferenceManager;
import org.opencv.BuildConfig;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
/**
* Predictor for Paddle-Lite OCR engine, managing model loading, runtime configuration,
* and end-to-end inference flow.
* zh-CN: 面向 Paddle-Lite OCR 引擎的预测器, 负责模型加载/运行时配置以及端到端推理流程.
*
* @author <a href="https://github.com/TonyJiangWJ">TonyJiangWJ</a>
* @see <a href="https://github.com/PaddlePaddle/PaddleOCR/blob/main/deploy/android_demo/app/src/main/java/com/baidu/paddle/lite/demo/ocr/Predictor.java">
* PaddlePaddle/PaddleOCR (Predictor.java)</a>
* @since Aug 6, 2023
*
* <p> Modified by TonyJiangWJ as of Aug 7, 2023. </p>
* <p> Modified by JetBrains AI Assistant (GPT-5.2) as of Jan 17, 2026. </p>
* <p> Modified by SuperMonster003 as of Jan 18, 2026. </p>
*/
@SuppressWarnings("unused")
public class Predictor {
public static final int DEFAULT_CPU_THREAD_NUM = 4;
public static final boolean DEFAULT_USE_SLIM = true;
public static final boolean DEFAULT_USE_OPENCL = false;
private static final String TAG = Predictor.class.getSimpleName();
/**
* Probe bitmap cache for init-check.
* zh-CN: 初始化校验使用的探测位图缓存.
*/
private static Bitmap checkingBitmap;
/**
* Default label file path.
* zh-CN: 默认字典文件路径.
*/
private final String defaultLabelPath = "labels/ppocr_keys_ocrv5.txt";
/**
* Default CPU model directory (standard).
* zh-CN: 默认 CPU 标准模型目录.
*/
private final String defaultModelPath = "models/pp-ocrv5-arm";
/**
* Default OpenCL model directory (standard).
* zh-CN: 默认 OpenCL 标准模型目录.
*/
private final String defaultModelPathOpenCL = "models/pp-ocrv5-arm-opencl";
/**
* Default CPU model directory (INT8 slim).
* zh-CN: 默认 CPU INT8 slim 模型目录.
*/
private final String defaultModelPathSlim = "models/pp-ocrv5-arm-int8";
/**
* Default OpenCL model directory (INT8 slim).
* zh-CN: 默认 OpenCL INT8 slim 模型目录.
*/
private final String defaultModelPathOpenCLSlim = "models/pp-ocrv5-arm-opencl-int8";
/** Detection model. [zh-CN: 检测模型]. */
public String detModelFilename = "PP-OCRv5_mobile_det.nb";
/** Recognition model. [zh-CN: 识别模型]. */
public String recModelFilename = "PP-OCRv5_mobile_rec.nb";
/** Text direction (cls) model. [zh-CN: 方向分类 (cls) 模型]. */
public String clsModelFilename = "PP-LCNet_x1_0_textline_ori.nb";
/** Whether the model is loaded. [zh-CN: 模型是否已加载]. */
public boolean isLoaded = false;
/** Warm-up iteration count. [zh-CN: 预热迭代次数]. */
public int warmupIterNum = 1;
/** Inference iteration count for timing. [zh-CN: 用于计时的推理迭代次数]. */
public int inferIterNum = 1;
/** CPU thread count. [zh-CN: CPU 线程数]. */
public int cpuThreadNum = DEFAULT_CPU_THREAD_NUM;
/** CPU power mode string (Lite power hint). [zh-CN: CPU 能耗模式字符串 (Lite 电源提示)]. */
public String cpuPowerMode = "LITE_POWER_HIGH";
/** Selected model resolved absolute path. [zh-CN: 选定模型解析后的绝对路径]. */
public String modelPath = "";
/** Selected model directory name. [zh-CN: 选定模型目录名]. */
public String modelName = "";
/** Use slim (INT8) model. [zh-CN: 是否使用 slim (INT8) 模型]. */
public boolean useSlim = DEFAULT_USE_SLIM;
/** Use OpenCL backend (if available). [zh-CN: 是否启用 OpenCL 后端 (若可用)]. */
public boolean useOpenCL = DEFAULT_USE_OPENCL;
/** Validate initialization with a preset image. [zh-CN: 是否通过预设图片校验初始化]. */
public boolean checkModelLoaded = BuildConfig.DEBUG;
/** Enable classification (cls). [zh-CN: 启用方向分类 (cls)]. */
public boolean isClassificationEnabled = false;
/** Enable detection (det). [zh-CN: 启用文本检测 (det)]. */
public boolean isDetectionEnabled = false;
/** Enable recognition (rec). [zh-CN: 启用文本识别 (rec)]. */
public boolean isRecognitionEnabled = true;
/** Score threshold for filtering results. [zh-CN: 结果过滤的置信度阈值]. */
public float scoreThreshold = 0.1f;
/** Max long side for det input resize. [zh-CN: 检测输入缩放的最长边]. */
protected int detLongSize = 960;
/** Native predictor bridge. [zh-CN: Native 预测器桥接对象]. */
protected OCRPredictorNative paddlePredictor = null;
/** Inference time in milliseconds. [zh-CN: 推理耗时 (毫秒)]. */
protected float inferenceTime = 0;
/** Labels for recognition post-processing. [zh-CN: 识别后处理所需的字典标签]. */
protected List<String> wordLabels = new ArrayList<>();
/** Input image buffer (ARGB_8888). [zh-CN: 输入图像缓冲 (ARGB_8888)]. */
protected Bitmap inputImage = null;
/** Preprocess time in milliseconds. [zh-CN: 预处理耗时 (毫秒)]. */
protected float preprocessTime = 0;
/** Validation attempt counter. [zh-CN: 初始化校验重试计数器]. */
private int validationAttempt = 1;
/** Initialization attempt counter. [zh-CN: 初始化尝试计数器]. */
private int initializationAttempt = 1;
// @Archived by SuperMonster003 on Nov 7, 2025.
// ! Legacy default paths and filenames for PP-OCRv3 are archived here.
// ! zh-CN: 旧版 PP-OCRv3 的默认路径与文件名在此归档保留.
// # private final String defaultLabelPath = "labels/ppocr_keys_v1.txt";
// # private final String defaultModelPath = "models/ocr_v3_for_cpu";
// # public String detModelFilename = "det_opt.nb";
// # public String recModelFilename = "rec_opt.nb";
// # public String clsModelFilename = "cls_opt.nb";
// # // Slim model converted by opt 2.10; 2.11 had issues.
// # // zh-CN: Slim 模型由 2.10 版 opt 转换; 2.11 存在兼容问题.
// # private final String defaultModelPathSlim = "models/ocr_v3_for_cpu(slim)";
public Predictor() {
/* Empty body. */
}
public static String md5(String text) {
MessageDigest md;
byte[] bytesOfMessage = text.getBytes();
try {
md = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
return Base64.encodeToString(md.digest(bytesOfMessage), Base64.DEFAULT);
}
public boolean init(Context appCtx) {
return this.init(appCtx, defaultModelPath, defaultLabelPath);
}
public boolean init(Context appCtx, boolean useSlim) {
return this.init(appCtx, useSlim, DEFAULT_USE_OPENCL);
}
public boolean init(Context appCtx, boolean useSlim, boolean useOpenCL) {
Log.d(TAG, "use slim: " + useSlim);
Log.d(TAG, "use opencl: " + useOpenCL);
// If already loaded and switches are consistent, reuse directly.
// zh-CN: 若已加载且开关一致, 直接复用.
if (this.isLoaded && this.useSlim == useSlim && this.useOpenCL == useOpenCL) {
return true;
}
boolean openclAvailable = false;
if (useOpenCL) {
try {
openclAvailable = OpenCLGuard.isOpenCLRuntimeAvailable(appCtx);
if (!openclAvailable) {
Log.w(TAG, "[OpenCL] Unavailable or fused, fallback to CPU.");
}
} catch (Throwable t) {
Log.w(TAG, "[OpenCL] Probe exception, fallback to CPU: " + t.getMessage());
}
}
this.useSlim = useSlim;
this.useOpenCL = openclAvailable;
String modelDir;
if (this.useSlim) {
modelDir = this.useOpenCL ? defaultModelPathOpenCLSlim : defaultModelPathSlim;
} else {
modelDir = this.useOpenCL ? defaultModelPathOpenCL : defaultModelPath;
}
boolean ok;
if (this.useOpenCL) {
OpenCLGuard.markInitStart(appCtx);
try {
ok = this.init(appCtx, modelDir, defaultLabelPath);
} finally {
OpenCLGuard.markInitEnd(appCtx);
}
if (!ok) {
Log.w(TAG, "[OpenCL] Init failed without crash, fallback to CPU.");
this.useOpenCL = false;
String cpuModelDir = this.useSlim ? defaultModelPathSlim : defaultModelPath;
ok = this.init(appCtx, cpuModelDir, defaultLabelPath);
}
return ok;
} else {
return this.init(appCtx, modelDir, defaultLabelPath);
}
}
public boolean init(Context appCtx, String modelPath, String labelPath) {
Log.d(TAG, "init whit model: " + modelPath + " label: " + labelPath);
isLoaded = loadModel(appCtx, modelPath, cpuThreadNum, cpuPowerMode);
if (!isLoaded) {
return false;
}
isLoaded = loadLabel(appCtx, labelPath);
if (!checkModelLoadedSuccess(appCtx)) {
if (initializationAttempt++ < 3) {
return init(appCtx, modelPath, labelPath);
} else {
return false;
}
}
return isLoaded;
}
/**
* Initialize and validate models by running inference on a preset test image.
* Retry up to several times as a workaround (deeper cause needs investigation).
* zh-CN:
* 初始化模型后通过识别预设图片校验是否初始化成功.
* 曲线救国, 深层的失败原因需要后续排查.
*/
private boolean checkModelLoadedSuccess(Context context) {
if (!checkModelLoaded) {
return true;
}
if (!isLoaded) {
return false;
}
List<OcrResult> results = runOcr(getCheckingBitmap(context));
StringBuilder sb = new StringBuilder();
for (OcrResult result : results) {
sb.append(result.getLabel());
}
// The image contains a single recognizable text string "测试" (Chinese word "test").
// zh-CN: 图片中包含唯一可识别文本 "测试".
boolean check = sb.toString().contains("测试");
Log.d(TAG, "Validation attempt " + validationAttempt + ": { initialized: " + check + ", result: " + sb + " }");
boolean result = check || validationAttempt++ >= 5;
if (!check && validationAttempt >= 5) {
Log.e(TAG, "Model initialization failed");
}
return result;
}
private Bitmap getCheckingBitmap(Context context) {
if (checkingBitmap == null) {
checkingBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.paddle_ocr_test);
}
return checkingBitmap;
}
public boolean init(Context appCtx, String modelPath, String labelPath, int cpuThreadNum, String cpuPowerMode) {
isLoaded = loadModel(appCtx, modelPath, cpuThreadNum, cpuPowerMode);
if (!isLoaded) {
return false;
}
isLoaded = loadLabel(appCtx, labelPath);
return isLoaded;
}
public boolean init(Context appCtx, String modelPath, String labelPath, int cpuThreadNum, String cpuPowerMode, int detLongSize, float scoreThreshold) {
boolean isLoaded = init(appCtx, modelPath, labelPath, cpuThreadNum, cpuPowerMode);
if (!isLoaded) {
return false;
}
this.detLongSize = detLongSize;
this.scoreThreshold = scoreThreshold;
return true;
}
protected boolean loadModel(Context appCtx, String modelPath, int cpuThreadNum, String cpuPowerMode) {
// Release model if exists.
// zh-CN: 释放模型如果存在.
releaseModel();
// Load model.
// zh-CN: 加载模型.
if (modelPath.isEmpty()) {
return false;
}
String realPath = modelPath;
if (modelPath.charAt(0) != '/') {
// Read model files from custom path if the first character of mode path is '/'
// otherwise copy model to cache from assets.
// zh-CN: 如果模型路径首字符为 '/' 则从自定义路径读取模型文件, 否则从 assets 复制模型到缓存.
realPath = appCtx.getCacheDir() + File.separator + modelPath;
// @SectionBegin("copyModelAssets") by TonyJiangWJ on Aug 7, 2023.
String key = "PADDLE_MODEL_LOADED" + md5(modelPath);
// Model has been updated, force override the old model.
// zh-CN: 进行了模型更新, 需要强制覆盖旧模型.
boolean loaded = PreferenceManager.getDefaultSharedPreferences(appCtx).getBoolean(key, false);
if (loaded) {
// No need to copy every time.
// zh-CN: 没有必要每次都复制.
Utils.copyDirectoryFromAssetsIfNeeded(appCtx, modelPath, realPath);
} else {
Utils.copyDirectoryFromAssets(appCtx, modelPath, realPath);
PreferenceManager.getDefaultSharedPreferences(appCtx).edit().putBoolean(key, true).apply();
}
// @SectionEnd("copyModelAssets")
}
OCRPredictorNative.Config config = new OCRPredictorNative.Config();
// Whether to use GPU (OpenCL), only set to 1 when useOpenCL is confirmed available at Java level.
// zh-CN: 是否使用 GPU (OpenCL), 只有在 Java 层确认 useOpenCL 可用时才真正置 1.
config.useOpenCL = useOpenCL ? 1 : 0;
config.cpuThreadNum = cpuThreadNum;
config.detModelFilename = realPath + File.separator + detModelFilename;
config.recModelFilename = realPath + File.separator + recModelFilename;
config.clsModelFilename = realPath + File.separator + clsModelFilename;
Log.i("Predictor", "model path" + config.detModelFilename + " ; " + config.recModelFilename + ";" + config.clsModelFilename);
config.cpuPower = cpuPowerMode;
paddlePredictor = new OCRPredictorNative(config);
this.cpuThreadNum = cpuThreadNum;
this.cpuPowerMode = cpuPowerMode;
this.modelPath = realPath;
this.modelName = realPath.substring(realPath.lastIndexOf(File.separator) + 1);
return true;
}
public void releaseModel() {
if (paddlePredictor != null) {
paddlePredictor.destroy();
paddlePredictor = null;
}
isLoaded = false;
modelPath = "";
modelName = "";
}
protected boolean loadLabel(Context appCtx, String labelPath) {
wordLabels.clear();
wordLabels.add("black");
// Load word labels from file.
// zh-CN: 从文件中加载字典标签.
try {
InputStream labelInputStream;
if (labelPath.startsWith(File.separator)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
labelInputStream = Files.newInputStream(Paths.get(labelPath));
} else {
labelInputStream = new FileInputStream(labelPath);
}
} else {
labelInputStream = appCtx.getAssets().open(labelPath);
}
int available = labelInputStream.available();
byte[] lines = new byte[available];
if (labelInputStream.read(lines) <= 0) {
Log.e(TAG, "Failed to read label");
return false;
}
labelInputStream.close();
String words = new String(lines);
// Compatible with \r\n line endings on Windows.
// zh-CN: 兼容 Windows 系统下的 \r\n 换行符.
String[] contents = words.split("(\r)?\n");
wordLabels.addAll(Arrays.asList(contents));
wordLabels.add(" ");
Log.i(TAG, "Word label size: " + wordLabels.size());
} catch (Exception e) {
Log.e(TAG, e.getMessage(), e);
return false;
}
return true;
}
public List<OcrResult> runOcr(Bitmap inputImage) {
if (inputImage == null || !isLoaded()) {
return Collections.emptyList();
}
int run_det = isDetectionEnabled ? 1 : 0;
int run_cls = isClassificationEnabled ? 1 : 0;
int run_rec = isRecognitionEnabled ? 1 : 0;
// Warm up.
// zh-CN: 预热.
for (int i = 0; i < warmupIterNum; i++) {
paddlePredictor.runImage(inputImage, detLongSize, run_det, run_cls, run_rec);
}
// Do not need warm.
// zh-CN: 不需要预热.
warmupIterNum = 0;
// Run inference.
// zh-CN: 执行推理.
Date start = new Date();
ArrayList<OcrResultModel> results = paddlePredictor.runImage(inputImage, detLongSize, run_det, run_cls, run_rec);
Date end = new Date();
inferenceTime = (end.getTime() - start.getTime()) / (float) inferIterNum;
postProcess(results);
Log.i(TAG, "[stat] Preprocess Time: " + preprocessTime + "; Inference Time: " + inferenceTime + "; Box Size: " + results.size());
List<OcrResult> ocrResults = new ArrayList<>();
for (OcrResultModel resultModel : results) {
// Log.d(TAG, "Recognize: " + resultModel);
if (resultModel.getConfidence() >= scoreThreshold) {
ocrResults.add(new OcrResult(resultModel));
} else {
// Log.d(TAG, "Discard: " + resultModel);
}
}
Collections.sort(ocrResults);
return ocrResults;
}
/**
* Whether model is loaded and predictor is valid.
* zh-CN: 模型是否已加载且预测器有效.
*/
public boolean isLoaded() {
return paddlePredictor != null && isLoaded;
}
public String modelPath() {
return modelPath;
}
public String modelName() {
return modelName;
}
public int cpuThreadNum() {
return cpuThreadNum;
}
public String cpuPowerMode() {
return cpuPowerMode;
}
public float inferenceTime() {
return inferenceTime;
}
public Bitmap inputImage() {
return inputImage;
}
public float preprocessTime() {
return preprocessTime;
}
public String getDefaultLabelPath() {
return defaultLabelPath;
}
public String getDefaultModelPath() {
return defaultModelPath;
}
/**
* Get default OpenCL model directory (standard).
* zh-CN: 获取默认的 OpenCL 标准模型目录.
*/
public String getDefaultModelPathOpenCL() {
return defaultModelPathOpenCL;
}
/**
* Get default CPU model directory (INT8 slim).
* zh-CN: 获取默认的 CPU INT8 slim 模型目录.
*/
public String getDefaultModelPathSlim() {
return defaultModelPathSlim;
}
/**
* Get default OpenCL model directory (INT8 slim).
* zh-CN: 获取默认的 OpenCL INT8 slim 模型目录.
*/
public String getDefaultModelPathOpenCLSlim() {
return defaultModelPathOpenCLSlim;
}
public boolean isUseSlim() {
return useSlim;
}
public boolean isUseOpenCL() {
return useOpenCL;
}
/**
* Set input image buffer (copy to ARGB_8888).
* zh-CN: 设置输入图像缓冲 (复制为 ARGB_8888).
*/
public void setInputImage(Bitmap image) {
if (image != null) {
this.inputImage = image.copy(Bitmap.Config.ARGB_8888, true);
}
}
/**
* Convert raw recognition outputs to text labels and metadata.
* zh-CN: 将识别原始输出转换为文本标签及元数据.
*/
private void postProcess(ArrayList<OcrResultModel> results) {
for (OcrResultModel r : results) {
StringBuilder word = new StringBuilder();
for (int index : r.getWordIndex()) {
if (index >= 0 && index < wordLabels.size()) {
word.append(wordLabels.get(index));
} else {
Log.e(TAG, "Word index is not in label list:" + index);
word.append(" ");
}
}
r.setLabel(word.toString());
r.setClsLabel(r.getClsIdx() == 1 ? "180" : "0");
}
}
/**
* Enable/disable classification (cls).
* zh-CN: 启用/禁用方向分类 (cls).
*/
public void setClassificationEnabled(boolean enable) {
this.isClassificationEnabled = enable;
}
/**
* Enable/disable detection (det).
* zh-CN: 启用/禁用文本检测 (det).
*/
public void setDetectionEnabled(boolean enable) {
this.isDetectionEnabled = enable;
}
/**
* Enable/disable recognition (rec).
* zh-CN: 启用/禁用文本识别 (rec).
*/
public void setRecognitionEnabled(boolean enable) {
this.isRecognitionEnabled = enable;
}
/**
* Set max long side for detection input (smaller is usually faster, e.g., 736-960).
* zh-CN: 设置检测输入的最长边 (越小通常越快, 例如 736-960).
*/
public void setDetLongSize(int detLongSize) {
this.detLongSize = detLongSize;
}
/**
* Set score threshold (slightly improves speed by pruning noisy boxes).
* zh-CN: 设置置信度阈值 (通过剔除噪声框可略微提速).
*/
public void setScoreThreshold(float scoreThreshold) {
this.scoreThreshold = scoreThreshold;
}
}

View File

@@ -0,0 +1,138 @@
package com.baidu.paddle.lite.ocr
import android.content.Context
import android.graphics.Bitmap
import android.os.Bundle
import android.os.Looper
import android.util.Log
import org.autojs.plugin.paddle.ocr.api.OcrOptions
import org.autojs.plugin.paddle.ocr.api.OcrResult
/**
* Native bridge based on com.baidu.paddle.lite.ocr.Predictor.
* zh-CN: 基于 com.baidu.paddle.lite.ocr.Predictor 的 native bridge.
*
* Created by JetBrains AI Assistant (GPT-5.2) on Jan 17, 2026.
* Modified by SuperMonster003 as of Jan 18, 2026.
*/
class PredictorNativeBridge : NativeBridge {
private val predictor = Predictor()
@Volatile
private var lastProfileKey: String? = null
override fun init(context: Context, profile: ModelProfile, checkingBitmap: Bitmap) {
val desiredThreadNum = profile.cpuThreadNum
val desiredUseSlim = profile.useSlim
val desiredUseOpenCL = profile.useOpenCL
// Compute a simple cache key to avoid redundant re-init.
// zh-CN: 计算简单缓存 key, 避免重复初始化.
val key = "${profile.variantName}|t=$desiredThreadNum|slim=$desiredUseSlim|opencl=$desiredUseOpenCL|dir=${profile.modelDir ?: "-"}"
if (predictor.isLoaded && lastProfileKey == key) return
// Predictor.init() may do heavy work and should not block main thread.
// zh-CN: Predictor.init() 可能较耗时, 不应阻塞主线程.
val ok = if (Looper.getMainLooper() == Looper.myLooper()) {
val lock = Object()
val completed = booleanArrayOf(false)
val initResult = booleanArrayOf(false)
Thread {
initResult[0] = initInternal(context, profile)
synchronized(lock) {
completed[0] = true
lock.notifyAll()
}
}.start()
val deadline = System.currentTimeMillis() + 60_000
var interrupted = false
synchronized(lock) {
try {
while (!completed[0]) {
val remaining = deadline - System.currentTimeMillis()
if (remaining <= 0) break
lock.wait(remaining)
}
} catch (_: InterruptedException) {
Thread.currentThread().interrupt()
interrupted = true
}
}
!interrupted && completed[0] && initResult[0]
} else {
initInternal(context, profile)
}
if (!ok) {
throw IllegalStateException(context.getString(R.string.error_failed_to_initialize_paddle_ocr_predictor))
}
lastProfileKey = key
}
private fun initInternal(context: Context, profile: ModelProfile): Boolean {
// Ensure cpuThreadNum updates take effect.
// zh-CN: 确保 cpuThreadNum 更新生效.
if (predictor.cpuThreadNum != profile.cpuThreadNum) {
predictor.releaseModel()
predictor.cpuThreadNum = profile.cpuThreadNum
}
// Apply per-variant model file names (both v3/v5).
// zh-CN: 应用按变体区分的模型文件名 (同时覆盖 v3/v5).
predictor.detModelFilename = profile.detModelFile
predictor.recModelFilename = profile.recModelFile
predictor.clsModelFilename = profile.clsModelFile
return if (profile.variantName == VariantSpec.NAME_V5) {
// Use v5 built-in OpenCLGuard + fallback logic.
// zh-CN: 使用 v5 内置的 OpenCLGuard + fallback 逻辑.
predictor.init(
context.applicationContext,
profile.useSlim,
profile.useOpenCL,
)
} else {
// v3: OpenCL is ignored at variant level; init by resolved modelDir/labelPath.
// zh-CN: v3: OpenCL 在变体层被忽略; 按解析后的 modelDir/labelPath 初始化.
predictor.init(
context.applicationContext,
requireNotNull(profile.modelDir) {
context.getString(R.string.error_missing_modeldir_for_variant, profile.variantName)
},
profile.labelAssetPath,
)
}
}
override fun recognizeText(bitmap: Bitmap, options: OcrOptions): List<String> {
val results = predictor.runOcr(bitmap)
val out = ArrayList<String>(results.size)
for (r in results) out.add(r.label)
// Only print summary or first several items to avoid heavy I/O.
// zh-CN: 仅打印摘要或前若干条, 避免大量 I/O.
Log.i("PaddleOcrEngine", "recognized ${out.size} items")
for (i in 0 until minOf(5, out.size)) {
Log.d("PaddleOcrEngine", "item[$i]: ${out[i]}")
}
return out
}
override fun detect(bitmap: Bitmap, options: OcrOptions): List<OcrResult> {
val results = predictor.runOcr(bitmap)
return results.map { r ->
OcrResult().apply {
text = r.label
confidence = r.confidence
bounds = r.bounds
extras = Bundle()
}
}
}
}

View File

@@ -0,0 +1,199 @@
package com.baidu.paddle.lite.ocr;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.media.ExifInterface;
import android.os.Environment;
import android.util.Log;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* @author PaddleOCR
* @since Aug 6, 2023
*
* <p> Modified by TonyJiangWJ as of Aug 7, 2023. </p>
* <p> Modified by SuperMonster003 as of Jan 18, 2026. </p>
*/
@SuppressWarnings({"ResultOfMethodCallIgnored", "CallToPrintStackTrace", "unused"})
public class Utils {
private static final String TAG = Utils.class.getSimpleName();
public static void copyFileFromAssets(Context appCtx, String srcPath, String dstPath) {
if (srcPath.isEmpty() || dstPath.isEmpty()) {
return;
}
try (InputStream is = new BufferedInputStream(appCtx.getAssets().open(srcPath)); OutputStream os = new BufferedOutputStream(new FileOutputStream(dstPath))) {
try {
byte[] buffer = new byte[1024];
int length;
while ((length = is.read(buffer)) != -1) {
os.write(buffer, 0, length);
}
} catch (IOException e) {
e.printStackTrace();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void copyDirectoryFromAssets(Context appCtx, String srcDir, String dstDir) {
if (srcDir.isEmpty() || dstDir.isEmpty()) {
return;
}
try {
if (!new File(dstDir).exists()) {
new File(dstDir).mkdirs();
}
String[] list = appCtx.getAssets().list(srcDir);
if (list == null) {
return;
}
for (String fileName : list) {
String srcSubPath = srcDir + File.separator + fileName;
String dstSubPath = dstDir + File.separator + fileName;
if (new File(srcSubPath).isDirectory()) {
copyDirectoryFromAssets(appCtx, srcSubPath, dstSubPath);
} else {
Log.d(TAG, "Copy asset file: " + srcSubPath + " -> " + dstSubPath);
copyFileFromAssets(appCtx, srcSubPath, dstSubPath);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void copyDirectoryFromAssetsIfNeeded(Context appCtx, String srcDir, String dstDir) {
if (srcDir.isEmpty() || dstDir.isEmpty()) {
return;
}
try {
if (!new File(dstDir).exists()) {
new File(dstDir).mkdirs();
}
String[] list = appCtx.getAssets().list(srcDir);
if (list == null) {
return;
}
for (String fileName : list) {
String srcSubPath = srcDir + File.separator + fileName;
String dstSubPath = dstDir + File.separator + fileName;
if (new File(srcSubPath).isDirectory()) {
copyDirectoryFromAssetsIfNeeded(appCtx, srcSubPath, dstSubPath);
} else {
if (new File(dstSubPath).exists()) {
return;
}
Log.d(TAG, "Copy asset file: " + srcSubPath + " -> " + dstSubPath);
copyFileFromAssets(appCtx, srcSubPath, dstSubPath);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static float[] parseFloatsFromString(String string, String delimiter) {
String[] pieces = string.trim().toLowerCase().split(delimiter);
float[] floats = new float[pieces.length];
for (int i = 0; i < pieces.length; i++) {
floats[i] = Float.parseFloat(pieces[i].trim());
}
return floats;
}
public static long[] parseLongsFromString(String string, String delimiter) {
String[] pieces = string.trim().toLowerCase().split(delimiter);
long[] longs = new long[pieces.length];
for (int i = 0; i < pieces.length; i++) {
longs[i] = Long.parseLong(pieces[i].trim());
}
return longs;
}
public static String getSDCardDirectory() {
return Environment.getExternalStorageDirectory().getAbsolutePath();
}
public static boolean isSupportedNPU() {
return false;
// String hardware = android.os.Build.HARDWARE;
// return hardware.equalsIgnoreCase("kirin810") || hardware.equalsIgnoreCase("kirin990");
}
public static Bitmap resizeWithStep(Bitmap bitmap, int maxLength, int step) {
int width = bitmap.getWidth();
int height = bitmap.getHeight();
int maxWH = Math.max(width, height);
float ratio;
int newWidth = width;
int newHeight = height;
if (maxWH > maxLength) {
ratio = maxLength * 1.0f / maxWH;
newWidth = (int) Math.floor(ratio * width);
newHeight = (int) Math.floor(ratio * height);
}
newWidth = newWidth - newWidth % step;
if (newWidth == 0) {
newWidth = step;
}
newHeight = newHeight - newHeight % step;
if (newHeight == 0) {
newHeight = step;
}
return Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true);
}
public static Bitmap rotateBitmap(Bitmap bitmap, int orientation) {
Matrix matrix = new Matrix();
switch (orientation) {
case ExifInterface.ORIENTATION_NORMAL:
return bitmap;
case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
matrix.setScale(-1, 1);
break;
case ExifInterface.ORIENTATION_ROTATE_180:
matrix.setRotate(180);
break;
case ExifInterface.ORIENTATION_FLIP_VERTICAL:
matrix.setRotate(180);
matrix.postScale(-1, 1);
break;
case ExifInterface.ORIENTATION_TRANSPOSE:
matrix.setRotate(90);
matrix.postScale(-1, 1);
break;
case ExifInterface.ORIENTATION_ROTATE_90:
matrix.setRotate(90);
break;
case ExifInterface.ORIENTATION_TRANSVERSE:
matrix.setRotate(-90);
matrix.postScale(-1, 1);
break;
case ExifInterface.ORIENTATION_ROTATE_270:
matrix.setRotate(-90);
break;
default:
return bitmap;
}
try {
Bitmap bmRotated = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
bitmap.recycle();
return bmRotated;
} catch (OutOfMemoryError e) {
e.printStackTrace();
return null;
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="error_failed_to_initialize_paddle_ocr_predictor">تعذّرت تهيئة Paddle OCR predictor.</string>
<string name="error_missing_modeldir_for_variant">modelDir مفقود لـ variant=%1$s.</string>
<string name="error_failed_to_decode_checking_drawable_resource">تعذّر فك ترميز مورد drawable الخاص بالتحقق.</string>
<string name="error_missing_required_paddle_ocr_asset">مورد Paddle OCR المطلوب مفقود: %1$s.</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="error_failed_to_initialize_paddle_ocr_predictor">Failed to initialize Paddle OCR predictor.</string>
<string name="error_missing_modeldir_for_variant">Missing modelDir for variant=%1$s.</string>
<string name="error_failed_to_decode_checking_drawable_resource">Failed to decode checking drawable resource.</string>
<string name="error_missing_required_paddle_ocr_asset">Missing required Paddle OCR asset: %1$s.</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="error_failed_to_initialize_paddle_ocr_predictor">No se pudo inicializar el predictor de Paddle OCR.</string>
<string name="error_missing_modeldir_for_variant">Falta modelDir para variant=%1$s.</string>
<string name="error_failed_to_decode_checking_drawable_resource">No se pudo decodificar el recurso drawable de verificación.</string>
<string name="error_missing_required_paddle_ocr_asset">Falta el recurso requerido de Paddle OCR: %1$s.</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="error_failed_to_initialize_paddle_ocr_predictor">Échec de l\'initialisation du prédicteur Paddle OCR.</string>
<string name="error_missing_modeldir_for_variant">modelDir manquant pour variant=%1$s.</string>
<string name="error_failed_to_decode_checking_drawable_resource">Échec du décodage de la ressource drawable de vérification.</string>
<string name="error_missing_required_paddle_ocr_asset">Ressource Paddle OCR requise manquante: %1$s.</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="error_failed_to_initialize_paddle_ocr_predictor">Paddle OCR predictor の初期化に失敗しました.</string>
<string name="error_missing_modeldir_for_variant">variant=%1$s の modelDir が見つかりません.</string>
<string name="error_failed_to_decode_checking_drawable_resource">確認用 drawable リソースのデコードに失敗しました.</string>
<string name="error_missing_required_paddle_ocr_asset">必要な Paddle OCR asset が見つかりません: %1$s.</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="error_failed_to_initialize_paddle_ocr_predictor">Paddle OCR predictor 초기화에 실패했습니다.</string>
<string name="error_missing_modeldir_for_variant">variant=%1$s에 대한 modelDir가 없습니다.</string>
<string name="error_failed_to_decode_checking_drawable_resource">검사용 drawable 리소스 디코딩에 실패했습니다.</string>
<string name="error_missing_required_paddle_ocr_asset">필수 Paddle OCR asset이 없습니다: %1$s.</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="error_failed_to_initialize_paddle_ocr_predictor">Не удалось инициализировать predictor Paddle OCR.</string>
<string name="error_missing_modeldir_for_variant">Отсутствует modelDir для variant=%1$s.</string>
<string name="error_failed_to_decode_checking_drawable_resource">Не удалось декодировать проверочный ресурс drawable.</string>
<string name="error_missing_required_paddle_ocr_asset">Отсутствует требуемый ресурс Paddle OCR: %1$s.</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="error_failed_to_initialize_paddle_ocr_predictor">初始化 Paddle OCR predictor 失敗.</string>
<string name="error_missing_modeldir_for_variant">變體缺少 modelDir: variant=%1$s.</string>
<string name="error_failed_to_decode_checking_drawable_resource">解碼檢查用 drawable 資源失敗.</string>
<string name="error_missing_required_paddle_ocr_asset">缺少 Paddle OCR 必要資源文件: %1$s.</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="error_failed_to_initialize_paddle_ocr_predictor">初始化 Paddle OCR predictor 失敗.</string>
<string name="error_missing_modeldir_for_variant">變體缺少 modelDir: variant=%1$s.</string>
<string name="error_failed_to_decode_checking_drawable_resource">解碼檢查用 drawable 資源失敗.</string>
<string name="error_missing_required_paddle_ocr_asset">缺少 Paddle OCR 必要資原始檔: %1$s.</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="error_failed_to_initialize_paddle_ocr_predictor">初始化 Paddle OCR predictor 失败.</string>
<string name="error_missing_modeldir_for_variant">变体缺少 modelDir: variant=%1$s.</string>
<string name="error_failed_to_decode_checking_drawable_resource">解码检查用 drawable 资源失败.</string>
<string name="error_missing_required_paddle_ocr_asset">缺少 Paddle OCR 必要资源文件: %1$s.</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="error_failed_to_initialize_paddle_ocr_predictor">Failed to initialize Paddle OCR predictor.</string>
<string name="error_missing_modeldir_for_variant">Missing modelDir for variant=%1$s.</string>
<string name="error_failed_to_decode_checking_drawable_resource">Failed to decode checking drawable resource.</string>
<string name="error_missing_required_paddle_ocr_asset">Missing required Paddle OCR asset: %1$s.</string>
</resources>