6.7.0 - Alpha12 - 剥离 Paddle OCR (PP-OCRv3) 功能; 定义 Paddle OCR 功能的 AIDL 接口约定

This commit is contained in:
SuperMonster003
2025-11-28 14:26:54 +08:00
parent a2b47cf6c8
commit 42ee329092
68 changed files with 357 additions and 16644 deletions

View File

@@ -166,9 +166,6 @@ dependencies /* Unclassified */ {
// OpenCV
implementation(project(":libs:org-opencv-4_8_0"))
// PaddleOCR
implementation(project(":libs:paddleocr"))
// RapidOCR
implementation(project(":libs:rapidocr"))
@@ -252,6 +249,9 @@ dependencies /* Unclassified */ {
// ICU4J
implementation(libs.icu4j)
// Plugin API: Paddle OCR
implementation(project(":plugin-api:paddle-ocr"))
}
dependencies /* MIME */ {

View File

@@ -126,6 +126,10 @@
android:protectionLevel="signature"
tools:ignore="ProtectedPermissions" />
<uses-permission
android:name="org.autojs.permission.PLUGIN"
android:protectionLevel="normal" />
<!-- 非 AutoJs6 运行必需, 不会主动申请, 脚本可自行申请 -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

View File

@@ -0,0 +1,217 @@
package org.autojs.autojs.core.plugin.ocr
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.graphics.Bitmap
import android.graphics.Bitmap.CompressFormat
import android.os.Build
import android.os.Handler
import android.os.IBinder
import android.os.ParcelFileDescriptor
import android.os.SystemClock.uptimeMillis
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import org.autojs.plugin.paddle.ocr.IOcrPlugin
import org.autojs.plugin.paddle.ocr.OcrOptions
import org.autojs.plugin.paddle.ocr.OcrResult
import org.autojs.plugin.paddle.ocr.PluginInfo
import java.io.File
import java.io.FileOutputStream
object PaddleOcrPluginHost {
private const val TAG = "PaddleOcrPluginHost"
const val ACTION_OCR = "org.autojs.plugin.PADDLE_OCR"
private const val DEFAULT_BIND_TIMEOUT_MS = 60_000L
private const val DEFAULT_CALL_TIMEOUT_MS = 60_000L
data class Discovered(
val serviceInfo: ServiceInfo,
val pluginInfo: PluginInfo?,
)
suspend fun discover(context: Context): List<Discovered> {
val pm = context.packageManager
val resolveList = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
pm.queryIntentServices(Intent(ACTION_OCR), PackageManager.ResolveInfoFlags.of(0))
} else {
pm.queryIntentServices(Intent(ACTION_OCR), 0)
}
val services = resolveList.mapNotNull { it.serviceInfo }
Log.i(TAG, "discover: services=${services.size}")
return services.map { svc ->
Log.i(TAG, "discover: ${svc.packageName}/${svc.name}")
val info = runCatching { withService(context, svc, DEFAULT_BIND_TIMEOUT_MS) { it.getInfo() } }
.onFailure { e -> Log.w(TAG, "getInfo failed: ${e.message}") }
.getOrNull()
Discovered(svc, info)
}
}
suspend fun recognizeText(
context: Context,
target: Discovered,
bitmap: Bitmap,
options: OcrOptions = OcrOptions(),
callTimeoutMs: Long = DEFAULT_CALL_TIMEOUT_MS,
): List<String> {
val start = uptimeMillis()
return createTempPfd(context, bitmap).use { pfd ->
withService(context, target.serviceInfo, DEFAULT_BIND_TIMEOUT_MS) { proxy ->
val remain = callTimeoutMs - (uptimeMillis() - start)
if (remain <= 0) error("AIDL call timeout in ${callTimeoutMs / 1000} seconds")
val lines = proxy.recognizeText(pfd, options)
Log.i(TAG, "recognizeTextAidl: got ${lines.size} lines")
lines
}
}
}
suspend fun detect(
context: Context,
target: Discovered,
bitmap: Bitmap,
options: OcrOptions = OcrOptions(),
callTimeoutMs: Long = DEFAULT_CALL_TIMEOUT_MS,
): List<OcrResult> {
val start = uptimeMillis()
return createTempPfd(context, bitmap).use { pfd ->
withService(context, target.serviceInfo, DEFAULT_BIND_TIMEOUT_MS) { proxy ->
val remain = callTimeoutMs - (uptimeMillis() - start)
if (remain <= 0) error("AIDL call timeout")
val results = proxy.detect(pfd, options)
Log.i(TAG, "detectAidl: got ${results.size} items")
results
}
}
}
suspend fun select(
context: Context,
// e.g. "paddle-ocr-v5"
engineId: String? = null,
// e.g. "paddle-ocr"
engine: String? = null,
// e.g. "v5"
variant: String? = null,
): Discovered? {
val list = discover(context).filter { it.pluginInfo != null }
if (list.isEmpty()) return null
if (engineId != null) {
list.firstOrNull { d -> d.pluginInfo?.id == engineId }?.let { return it }
}
if (engine != null && variant != null) {
list.firstOrNull { d -> d.pluginInfo?.engine == engine && d.pluginInfo.variant == variant }?.let { return it }
}
if (engine != null) {
list.firstOrNull { d -> d.pluginInfo?.engine == engine }?.let { return it }
}
return list.first()
}
// Convert temporary file to read-only FD.
// zh-CN: 临时文件转换为只读 FD.
private fun createTempPfd(context: Context, bmp: Bitmap): ParcelFileDescriptor {
val dir = File(context.cacheDir, "ocr_ipc").apply { if (!exists()) mkdirs() }
val f = File.createTempFile("img_", ".bin", dir)
FileOutputStream(f).use { fos ->
val format = when {
bmp.hasAlpha() -> CompressFormat.PNG
else -> CompressFormat.JPEG
}
require(bmp.compress(format, 100, fos)) { "Failed to encode bitmap" }
}
return ParcelFileDescriptor.open(f, ParcelFileDescriptor.MODE_READ_ONLY)
}
private suspend fun <T> withService(
context: Context,
serviceInfo: ServiceInfo,
bindTimeoutMs: Long = DEFAULT_BIND_TIMEOUT_MS,
block: suspend (IOcrPlugin) -> T,
): T = suspendCancellableCoroutine { cont ->
val cn = ComponentName(serviceInfo.packageName, serviceInfo.name)
val intent = Intent().setComponent(cn)
val appCtx = context.applicationContext
var resolved = false
var jobRef: Job? = null
val conn = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, binder: IBinder) {
Log.i(TAG, "onServiceConnected: $name")
val proxy = IOcrPlugin.Stub.asInterface(binder)
val self = this
jobRef = CoroutineScope(cont.context + Dispatchers.IO).launch {
val result = runCatching { block(proxy) }
try {
resolved = true
if (!cont.isCompleted) cont.resumeWith(result)
Log.i(TAG, "resume continuation: success=${result.isSuccess}")
} finally {
withContext(Dispatchers.Main) {
runCatching { appCtx.unbindService(self) }
Log.i(TAG, "unbindService: $name")
}
}
}
}
override fun onServiceDisconnected(name: ComponentName) {
Log.w(TAG, "onServiceDisconnected: $name")
}
}
val ok = try {
Log.i(TAG, "bindService: $cn")
appCtx.bindService(intent, conn, Context.BIND_AUTO_CREATE)
} catch (se: SecurityException) {
Log.e(TAG, "bindService SecurityException: $cn | ${se.message}")
cont.resumeWith(
Result.failure(
IllegalStateException(
"bindService SecurityException: $cn. Please make sure the plugin declares <permission android:name=\"org.autojs.permission.PLUGIN\"/> and the Service uses this permission.", se
)
)
)
return@suspendCancellableCoroutine
}
if (!ok) {
Log.e(TAG, "bindService failed: $cn")
cont.resumeWith(Result.failure(IllegalStateException("bindService failed: $cn")))
return@suspendCancellableCoroutine
}
val cancel = Runnable {
if (!resolved && !cont.isCompleted) {
Log.e(TAG, "bindService timeout: $cn")
cont.resumeWith(Result.failure(TimeoutException("bindService timeout: $cn")))
runCatching { appCtx.unbindService(conn) }
}
}
val h = Handler(appCtx.mainLooper)
h.postDelayed(cancel, bindTimeoutMs)
cont.invokeOnCancellation {
Log.w(TAG, "continuation cancelled: $cn")
h.removeCallbacks(cancel)
jobRef?.cancel()
runCatching { appCtx.unbindService(conn) }
}
}
private class TimeoutException(msg: String) : RuntimeException(msg)
}

View File

@@ -3,6 +3,9 @@ package org.autojs.autojs.runtime
import android.content.Context
import android.os.Build
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import org.autojs.autojs.AutoJs
import org.autojs.autojs.annotation.ScriptInterface
import org.autojs.autojs.annotation.ScriptVariable
@@ -128,6 +131,7 @@ import java.io.PrintWriter
import java.io.StringWriter
import java.lang.ref.WeakReference
import java.util.concurrent.ConcurrentHashMap
import kotlin.coroutines.cancellation.CancellationException
import org.autojs.autojs.core.accessibility.UiSelector as CoreUiSelector
import org.autojs.autojs.core.crypto.Crypto as CoreCrypto
import org.autojs.autojs.core.image.Colors as CoreColors
@@ -149,7 +153,6 @@ import org.autojs.autojs.runtime.api.Mime as ApiMime
import org.autojs.autojs.runtime.api.Notice as ApiNotice
import org.autojs.autojs.runtime.api.Ocr as ApiOcr
import org.autojs.autojs.runtime.api.OcrMLKit as ApiOcrMLKit
import org.autojs.autojs.runtime.api.OcrPaddle as ApiOcrPaddle
import org.autojs.autojs.runtime.api.OcrRapid as ApiOcrRapid
import org.autojs.autojs.runtime.api.Plugins as ApiPlugins
import org.autojs.autojs.runtime.api.Recorder as ApiRecorder
@@ -180,6 +183,10 @@ import org.autojs.autojs.runtime.api.augment.util.VersionCodes as UtilVersionCod
@Suppress("unused", "PropertyName", "PrivatePropertyName")
class ScriptRuntime private constructor(builder: Builder) {
private val mJob = SupervisorJob()
val coroutineScope = CoroutineScope(Dispatchers.Default + mJob)
val coroutineContext = coroutineScope.coroutineContext
private var mUiHandlerAppContext: Context
private var mRootShell: AbstractShell? = null
private val mProperties = ConcurrentHashMap<String, Any>()
@@ -335,10 +342,6 @@ class ScriptRuntime private constructor(builder: Builder) {
@ScriptVariable
val ocrMLKit: ApiOcrMLKit
@JvmField
@ScriptVariable
val ocrPaddle: ApiOcrPaddle
@JvmField
@ScriptVariable
val ocrRapid: ApiOcrRapid
@@ -474,7 +477,6 @@ class ScriptRuntime private constructor(builder: Builder) {
http = ApiHttp()
ocrMLKit = ApiOcrMLKit()
ocrPaddle = ApiOcrPaddle()
ocrRapid = ApiOcrRapid()
barcode = ApiBarcode()
@@ -579,6 +581,10 @@ class ScriptRuntime private constructor(builder: Builder) {
@Deprecated("ScriptRuntime#stop is deprecated", ReplaceWith("exit()"))
fun stop() = exit()
fun cancelScriptJobs() {
mJob.cancel(CancellationException(mUiHandlerAppContext.getString(R.string.error_script_is_on_exiting)))
}
fun onExit() {
Log.d(TAG, "on exit")
this.isExiting = true
@@ -620,7 +626,6 @@ class ScriptRuntime private constructor(builder: Builder) {
ignoresException({ images.releaseScreenCapturer() })
ignoresException({ images.stopScreenCapturerForegroundService() })
ignoresException({ ocrMLKit.release() })
ignoresException({ ocrPaddle.release() })
ignoresException({ sensors.unregisterAll() })
ignoresException({ timers.recycle() })
ignoresException({ ui.recycle() })

View File

@@ -1,87 +0,0 @@
package org.autojs.autojs.runtime.api;
import android.graphics.Bitmap;
import android.os.Looper;
import android.util.Log;
import com.baidu.paddle.lite.ocr.OcrResult;
import com.baidu.paddle.lite.ocr.Predictor;
import org.autojs.autojs.app.GlobalAppContext;
import org.autojs.autojs.concurrent.VolatileDispose;
import org.autojs.autojs.core.image.ImageWrapper;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* @author TonyJiangWJ
* @since 2023-08-06
*/
public class OcrPaddle {
private final Predictor mPredictor = new Predictor();
public synchronized boolean init(boolean useSlim) {
if (!mPredictor.isLoaded || useSlim != mPredictor.isUseSlim()) {
if (Looper.getMainLooper() == Looper.myLooper()) {
VolatileDispose<Boolean> result = new VolatileDispose<>();
new Thread(() -> result.setAndNotify(mPredictor.init(GlobalAppContext.get(), useSlim))).start();
return Boolean.TRUE.equals(result.blockedGet(60_000));
} else {
return mPredictor.init(GlobalAppContext.get(), useSlim);
}
}
return mPredictor.isLoaded;
}
public void release() {
mPredictor.releaseModel();
}
public List<OcrResult> detect(ImageWrapper image, int cpuThreadNum, boolean useSlim) {
if (image == null) {
return Collections.emptyList();
}
Bitmap bitmap = image.getBitmap();
if (bitmap.isRecycled()) {
return Collections.emptyList();
}
if (mPredictor.cpuThreadNum != cpuThreadNum) {
mPredictor.releaseModel();
mPredictor.cpuThreadNum = cpuThreadNum;
}
init(useSlim);
return mPredictor.runOcr(bitmap);
}
public List<OcrResult> detect(ImageWrapper image, int cpuThreadNum) {
return detect(image, cpuThreadNum, true);
}
public List<OcrResult> detect(ImageWrapper image) {
return detect(image, 4, true);
}
public List<String> recognizeText(ImageWrapper image, int cpuThreadNum, boolean useSlim) {
List<OcrResult> words_result = detect(image, cpuThreadNum, useSlim);
Collections.sort(words_result);
List<String> outputResult = Arrays.asList(new String[words_result.size()]);
for (int i = 0; i < words_result.size(); i++) {
outputResult.set(i, words_result.get(i).getLabel());
// show LOG in Logcat panel
Log.i("outputResult", outputResult.get(i));
}
return outputResult;
}
public List<String> recognizeText(ImageWrapper image, int cpuThreadNum) {
return recognizeText(image, cpuThreadNum, true);
}
public List<String> recognizeText(ImageWrapper image) {
return recognizeText(image, 4, true);
}
}

View File

@@ -4,7 +4,7 @@ import kotlinx.coroutines.runBlocking
import org.autojs.autojs.annotation.RhinoRuntimeFunctionInterface
import org.autojs.autojs.apkbuilder.ApkBuilder
import org.autojs.autojs.core.image.ImageWrapper
import org.autojs.autojs.core.plugin.ocr.OcrPluginHost
import org.autojs.autojs.core.plugin.ocr.PaddleOcrPluginHost
import org.autojs.autojs.extension.ScriptableObjectExtensions.inquire
import org.autojs.autojs.runtime.ScriptRuntime
import org.autojs.autojs.runtime.api.OcrResult
@@ -14,6 +14,7 @@ import org.autojs.autojs.runtime.api.augment.ocr.Ocr.Companion.OcrMode
import org.autojs.autojs.runtime.exception.WrappedIllegalArgumentException
import org.autojs.autojs.util.RhinoUtils.coerceBoolean
import org.autojs.autojs.util.RhinoUtils.coerceIntNumber
import org.autojs.plugin.paddle.ocr.OcrOptions
import org.mozilla.javascript.NativeArray
import org.mozilla.javascript.NativeObject
@@ -48,32 +49,35 @@ class OcrPaddle(private val scriptRuntime: ScriptRuntime) : Augmentable(scriptRu
}
fun recognizeTextImpl(scriptRuntime: ScriptRuntime, image: ImageWrapper, options: NativeObject): List<String> {
return performOcr(scriptRuntime, image, options).map {
it.text
}
}
fun detectImpl(scriptRuntime: ScriptRuntime, image: ImageWrapper, options: NativeObject): List<OcrResult> {
return performOcr(scriptRuntime, image, options).map {
OcrResult(it.text, it.confidence, it.bounds)
}
}
private fun performOcr(scriptRuntime: ScriptRuntime, image: ImageWrapper, options: NativeObject): List<org.autojs.plugin.ocr.OcrResult> {
ApkBuilder.Libs.PADDLE_OCR.ensureLibFiles(OcrMode.PADDLE.value)
val (cpuThreadNum, useSlim, useOpenCL) = getOcrOptions(options)
val ocrOptions = org.autojs.plugin.ocr.OcrOptions().apply {
this.threads = cpuThreadNum
val ocrOptions = OcrOptions().apply {
this.cpuThreadNum = cpuThreadNum
this.useSlim = useSlim
this.useOpenCL = useOpenCL
}
return runBlocking(scriptRuntime.coroutineContext) {
val target = OcrPluginHost.select(globalContext)
val target = PaddleOcrPluginHost.select(globalContext)
?: throw WrappedIllegalArgumentException("No Paddle OCR plugin matched")
OcrPluginHost.detect(globalContext, target, image.bitmap, ocrOptions)
PaddleOcrPluginHost.recognizeText(globalContext, target, image.bitmap, ocrOptions)
}
}
fun detectImpl(scriptRuntime: ScriptRuntime, image: ImageWrapper, options: NativeObject): List<OcrResult> {
ApkBuilder.Libs.PADDLE_OCR.ensureLibFiles(OcrMode.PADDLE.value)
val (cpuThreadNum, useSlim, useOpenCL) = getOcrOptions(options)
val ocrOptions = OcrOptions().apply {
this.cpuThreadNum = cpuThreadNum
this.useSlim = useSlim
this.useOpenCL = useOpenCL
}
return runBlocking(scriptRuntime.coroutineContext) {
val target = PaddleOcrPluginHost.select(globalContext)
?: throw WrappedIllegalArgumentException("No Paddle OCR plugin matched")
PaddleOcrPluginHost.detect(globalContext, target, image.bitmap, ocrOptions)
}.map { OcrResult(it.text, it.confidence, it.bounds) }
}
private fun getOcrOptions(options: NativeObject): OcrOptions {
val cpuThreadNum = options.inquire("cpuThreadNum", ::coerceIntNumber, DEFAULT_CPU_THREAD_NUM)
val useSlim = options.inquire("useSlim", ::coerceBoolean, DEFAULT_USE_SLIM)