6.7.0 - Alpha20 - Paddle OCR 支持原始图像传输以提升性能, 并支持更灵活的选项配置
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
package org.autojs.autojs.core.plugin.ocr
|
package org.autojs.autojs.core.plugin.ocr
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@@ -10,24 +11,31 @@ import android.content.pm.ServiceInfo
|
|||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Bitmap.CompressFormat
|
import android.graphics.Bitmap.CompressFormat
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
|
import android.os.Looper
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
|
import android.os.RemoteException
|
||||||
|
import android.os.SharedMemory
|
||||||
import android.os.SystemClock.uptimeMillis
|
import android.os.SystemClock.uptimeMillis
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.CancellableContinuation
|
||||||
import org.autojs.autojs.core.plugin.center.PluginEnableStore
|
import org.autojs.autojs.core.plugin.center.PluginEnableStore
|
||||||
import org.autojs.plugin.paddle.ocr.api.IOcrPlugin
|
import org.autojs.plugin.paddle.ocr.api.IOcrPlugin
|
||||||
import org.autojs.plugin.paddle.ocr.api.OcrOptions
|
import org.autojs.plugin.paddle.ocr.api.OcrOptions
|
||||||
import org.autojs.plugin.paddle.ocr.api.OcrResult
|
import org.autojs.plugin.paddle.ocr.api.OcrResult
|
||||||
import org.autojs.plugin.paddle.ocr.api.PluginInfo
|
import org.autojs.plugin.paddle.ocr.api.PluginInfo
|
||||||
import java.io.File
|
import java.io.FileDescriptor
|
||||||
import java.io.FileOutputStream
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.ByteOrder
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.resumeWithException
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modified by JetBrains AI Assistant (GPT-5.2-Codex (xhigh)) as of Feb 13, 2026.
|
* Modified by JetBrains AI Assistant (GPT-5.2-Codex (xhigh)) as of Feb 13, 2026.
|
||||||
@@ -40,9 +48,20 @@ object PaddleOcrPluginHost {
|
|||||||
|
|
||||||
private const val DEFAULT_BIND_TIMEOUT_MS = 60_000L
|
private const val DEFAULT_BIND_TIMEOUT_MS = 60_000L
|
||||||
private const val DEFAULT_CALL_TIMEOUT_MS = 60_000L
|
private const val DEFAULT_CALL_TIMEOUT_MS = 60_000L
|
||||||
|
private const val EXTRA_IMAGE_FORMAT = "imageFormat"
|
||||||
|
private const val EXTRA_IMAGE_QUALITY = "imageQuality"
|
||||||
|
private const val EXTRA_RAW_IMAGE = "rawImage"
|
||||||
|
private const val EXTRA_RAW_WIDTH = "rawWidth"
|
||||||
|
private const val EXTRA_RAW_HEIGHT = "rawHeight"
|
||||||
|
private const val EXTRA_RAW_STRIDE = "rawStride"
|
||||||
|
private const val EXTRA_RAW_CONFIG = "rawConfig"
|
||||||
|
private const val IDLE_UNBIND_MS = 30_000L
|
||||||
|
|
||||||
private val externalServiceFlag = runCatching { ServiceInfo::class.java.getField("FLAG_EXTERNAL_SERVICE").getInt(null) }.getOrNull() ?: 0
|
private val externalServiceFlag = runCatching { ServiceInfo::class.java.getField("FLAG_EXTERNAL_SERVICE").getInt(null) }.getOrNull() ?: 0
|
||||||
private val bindExternalServiceFlag = runCatching { Context::class.java.getField("BIND_EXTERNAL_SERVICE").getInt(null) }.getOrNull() ?: 0
|
private val bindExternalServiceFlag = runCatching { Context::class.java.getField("BIND_EXTERNAL_SERVICE").getInt(null) }.getOrNull() ?: 0
|
||||||
|
private val poolHandler = Handler(Looper.getMainLooper())
|
||||||
|
private val poolLock = Any()
|
||||||
|
private val connectionPool = LinkedHashMap<ComponentName, PooledConnection>()
|
||||||
|
|
||||||
data class Discovered(
|
data class Discovered(
|
||||||
val serviceInfo: ServiceInfo,
|
val serviceInfo: ServiceInfo,
|
||||||
@@ -78,8 +97,9 @@ object PaddleOcrPluginHost {
|
|||||||
options: OcrOptions = OcrOptions(),
|
options: OcrOptions = OcrOptions(),
|
||||||
callTimeoutMs: Long = DEFAULT_CALL_TIMEOUT_MS,
|
callTimeoutMs: Long = DEFAULT_CALL_TIMEOUT_MS,
|
||||||
): List<String> {
|
): List<String> {
|
||||||
|
ensureRawSupport(target, options)
|
||||||
val start = uptimeMillis()
|
val start = uptimeMillis()
|
||||||
return createTempPfd(context, bitmap).use { pfd ->
|
return createTempPfd(bitmap, options).use { pfd ->
|
||||||
withService(context, target.serviceInfo, DEFAULT_BIND_TIMEOUT_MS) { proxy ->
|
withService(context, target.serviceInfo, DEFAULT_BIND_TIMEOUT_MS) { proxy ->
|
||||||
val remain = callTimeoutMs - (uptimeMillis() - start)
|
val remain = callTimeoutMs - (uptimeMillis() - start)
|
||||||
if (remain <= 0) error("AIDL call timeout in ${callTimeoutMs / 1000} seconds")
|
if (remain <= 0) error("AIDL call timeout in ${callTimeoutMs / 1000} seconds")
|
||||||
@@ -97,8 +117,9 @@ object PaddleOcrPluginHost {
|
|||||||
options: OcrOptions = OcrOptions(),
|
options: OcrOptions = OcrOptions(),
|
||||||
callTimeoutMs: Long = DEFAULT_CALL_TIMEOUT_MS,
|
callTimeoutMs: Long = DEFAULT_CALL_TIMEOUT_MS,
|
||||||
): List<OcrResult> {
|
): List<OcrResult> {
|
||||||
|
ensureRawSupport(target, options)
|
||||||
val start = uptimeMillis()
|
val start = uptimeMillis()
|
||||||
return createTempPfd(context, bitmap).use { pfd ->
|
return createTempPfd(bitmap, options).use { pfd ->
|
||||||
withService(context, target.serviceInfo, DEFAULT_BIND_TIMEOUT_MS) { proxy ->
|
withService(context, target.serviceInfo, DEFAULT_BIND_TIMEOUT_MS) { proxy ->
|
||||||
val remain = callTimeoutMs - (uptimeMillis() - start)
|
val remain = callTimeoutMs - (uptimeMillis() - start)
|
||||||
if (remain <= 0) error("AIDL call timeout")
|
if (remain <= 0) error("AIDL call timeout")
|
||||||
@@ -139,17 +160,147 @@ object PaddleOcrPluginHost {
|
|||||||
|
|
||||||
// Convert temporary file to read-only FD.
|
// Convert temporary file to read-only FD.
|
||||||
// zh-CN: 临时文件转换为只读 FD.
|
// zh-CN: 临时文件转换为只读 FD.
|
||||||
private fun createTempPfd(context: Context, bmp: Bitmap): ParcelFileDescriptor {
|
private fun createTempPfd(bmp: Bitmap, options: OcrOptions): ParcelFileDescriptor {
|
||||||
val dir = File(context.cacheDir, "ocr_ipc").apply { if (!exists()) mkdirs() }
|
val useRaw = options.extras?.getBoolean(EXTRA_RAW_IMAGE, false) == true
|
||||||
val f = File.createTempFile("img_", ".bin", dir)
|
if (useRaw && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||||
FileOutputStream(f).use { fos ->
|
return runCatching { createRawPfd(bmp, options) }
|
||||||
val format = when {
|
.getOrElse {
|
||||||
bmp.hasAlpha() -> CompressFormat.PNG
|
Log.w(TAG, "raw image transfer failed: ${it.message}")
|
||||||
else -> CompressFormat.JPEG
|
disableRawExtras(options)
|
||||||
}
|
createEncodedPfd(bmp, options)
|
||||||
require(bmp.compress(format, 100, fos)) { "Failed to encode bitmap" }
|
}
|
||||||
|
}
|
||||||
|
if (useRaw) {
|
||||||
|
disableRawExtras(options)
|
||||||
|
}
|
||||||
|
return createEncodedPfd(bmp, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createEncodedPfd(bmp: Bitmap, options: OcrOptions): ParcelFileDescriptor {
|
||||||
|
val (format, quality) = resolveEncodeOptions(bmp, options)
|
||||||
|
val pipe = ParcelFileDescriptor.createPipe()
|
||||||
|
val readFd = pipe[0]
|
||||||
|
val writeFd = pipe[1]
|
||||||
|
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
runCatching {
|
||||||
|
ParcelFileDescriptor.AutoCloseOutputStream(writeFd).use { out ->
|
||||||
|
require(bmp.compress(format, quality, out)) { "Failed to encode bitmap" }
|
||||||
|
}
|
||||||
|
}.onFailure { t ->
|
||||||
|
Log.e(TAG, "encode bitmap failed: ${t.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return readFd
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O_MR1)
|
||||||
|
private fun createRawPfd(bmp: Bitmap, options: OcrOptions): ParcelFileDescriptor {
|
||||||
|
val src = if (bmp.config == Bitmap.Config.ARGB_8888) bmp else bmp.copy(Bitmap.Config.ARGB_8888, false)
|
||||||
|
val shouldRecycle = src !== bmp
|
||||||
|
val rowBytes = src.rowBytes
|
||||||
|
val size = rowBytes * src.height
|
||||||
|
|
||||||
|
val extras = options.extras ?: Bundle().also { options.extras = it }
|
||||||
|
extras.putBoolean(EXTRA_RAW_IMAGE, true)
|
||||||
|
extras.putInt(EXTRA_RAW_WIDTH, src.width)
|
||||||
|
extras.putInt(EXTRA_RAW_HEIGHT, src.height)
|
||||||
|
extras.putInt(EXTRA_RAW_STRIDE, rowBytes)
|
||||||
|
extras.putString(EXTRA_RAW_CONFIG, Bitmap.Config.ARGB_8888.name)
|
||||||
|
|
||||||
|
runCatching {
|
||||||
|
val shm = SharedMemory.create("ocr_ipc_raw", size)
|
||||||
|
val buffer = shm.mapReadWrite()
|
||||||
|
buffer.order(ByteOrder.nativeOrder())
|
||||||
|
src.copyPixelsToBuffer(buffer)
|
||||||
|
SharedMemory.unmap(buffer)
|
||||||
|
|
||||||
|
val pfd = dupSharedMemoryFd(shm)
|
||||||
|
shm.close()
|
||||||
|
if (pfd != null) {
|
||||||
|
if (shouldRecycle) {
|
||||||
|
src.recycle()
|
||||||
|
}
|
||||||
|
return pfd
|
||||||
|
}
|
||||||
|
}.onFailure { t ->
|
||||||
|
Log.w(TAG, "shared memory unavailable, fallback to raw pipe: ${t.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
return createRawPipePfd(src, size, shouldRecycle)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createRawPipePfd(
|
||||||
|
bmp: Bitmap,
|
||||||
|
size: Int,
|
||||||
|
shouldRecycle: Boolean,
|
||||||
|
): ParcelFileDescriptor {
|
||||||
|
val pipe = ParcelFileDescriptor.createPipe()
|
||||||
|
val readFd = pipe[0]
|
||||||
|
val writeFd = pipe[1]
|
||||||
|
val raw = ByteArray(size)
|
||||||
|
val buffer = ByteBuffer.wrap(raw).order(ByteOrder.nativeOrder())
|
||||||
|
bmp.copyPixelsToBuffer(buffer)
|
||||||
|
|
||||||
|
if (shouldRecycle) {
|
||||||
|
bmp.recycle()
|
||||||
|
}
|
||||||
|
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
runCatching {
|
||||||
|
ParcelFileDescriptor.AutoCloseOutputStream(writeFd).use { out ->
|
||||||
|
out.write(raw, 0, size)
|
||||||
|
}
|
||||||
|
}.onFailure { t ->
|
||||||
|
Log.e(TAG, "write raw pipe failed: ${t.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return readFd
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SoonBlockedPrivateApi")
|
||||||
|
private fun dupSharedMemoryFd(shm: SharedMemory): ParcelFileDescriptor? {
|
||||||
|
val method = shm.javaClass.methods.firstOrNull { it.name == "getFdDup" && it.parameterCount == 0 }
|
||||||
|
?: shm.javaClass.methods.firstOrNull { it.name == "getFileDescriptor" && it.parameterCount == 0 }
|
||||||
|
val result = method?.let { runCatching { it.invoke(shm) }.getOrNull() }
|
||||||
|
return when (result) {
|
||||||
|
is ParcelFileDescriptor -> result
|
||||||
|
is FileDescriptor -> ParcelFileDescriptor.dup(result)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveEncodeOptions(bmp: Bitmap, options: OcrOptions): Pair<CompressFormat, Int> {
|
||||||
|
val extras = options.extras
|
||||||
|
val formatName = extras?.getString(EXTRA_IMAGE_FORMAT)?.trim()?.lowercase().orEmpty()
|
||||||
|
val quality = extras?.getInt(EXTRA_IMAGE_QUALITY, 100) ?: 100
|
||||||
|
val resolvedQuality = quality.coerceIn(1, 100)
|
||||||
|
val resolvedFormat = when (formatName) {
|
||||||
|
"png" -> CompressFormat.PNG
|
||||||
|
"jpg", "jpeg" -> CompressFormat.JPEG
|
||||||
|
"webp" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) CompressFormat.WEBP_LOSSY else CompressFormat.WEBP
|
||||||
|
"webp_lossless" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) CompressFormat.WEBP_LOSSLESS else CompressFormat.WEBP
|
||||||
|
else -> if (bmp.hasAlpha()) CompressFormat.PNG else CompressFormat.JPEG
|
||||||
|
}
|
||||||
|
return resolvedFormat to resolvedQuality
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun disableRawExtras(options: OcrOptions) {
|
||||||
|
val extras = options.extras ?: return
|
||||||
|
extras.remove(EXTRA_RAW_IMAGE)
|
||||||
|
extras.remove(EXTRA_RAW_WIDTH)
|
||||||
|
extras.remove(EXTRA_RAW_HEIGHT)
|
||||||
|
extras.remove(EXTRA_RAW_STRIDE)
|
||||||
|
extras.remove(EXTRA_RAW_CONFIG)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ensureRawSupport(target: Discovered, options: OcrOptions) {
|
||||||
|
val extras = options.extras ?: return
|
||||||
|
if (!extras.getBoolean(EXTRA_RAW_IMAGE, false)) return
|
||||||
|
val supportsRaw = target.pluginInfo?.capabilities?.getBoolean("supportsRawImage", false) == true
|
||||||
|
if (!supportsRaw) {
|
||||||
|
disableRawExtras(options)
|
||||||
}
|
}
|
||||||
return ParcelFileDescriptor.open(f, ParcelFileDescriptor.MODE_READ_ONLY)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun queryOcrServices(context: Context, packageName: String? = null): List<ServiceInfo> {
|
private fun queryOcrServices(context: Context, packageName: String? = null): List<ServiceInfo> {
|
||||||
@@ -191,93 +342,268 @@ object PaddleOcrPluginHost {
|
|||||||
serviceInfo: ServiceInfo,
|
serviceInfo: ServiceInfo,
|
||||||
bindTimeoutMs: Long = DEFAULT_BIND_TIMEOUT_MS,
|
bindTimeoutMs: Long = DEFAULT_BIND_TIMEOUT_MS,
|
||||||
block: suspend (IOcrPlugin) -> T,
|
block: suspend (IOcrPlugin) -> T,
|
||||||
): T = suspendCancellableCoroutine { cont ->
|
): T {
|
||||||
val cn = ComponentName(serviceInfo.packageName, serviceInfo.name)
|
|
||||||
val intent = Intent().setComponent(cn)
|
|
||||||
val appCtx = context.applicationContext
|
val appCtx = context.applicationContext
|
||||||
|
val entry = getOrCreateEntry(appCtx, serviceInfo)
|
||||||
|
val proxy = awaitProxy(entry, bindTimeoutMs)
|
||||||
|
return try {
|
||||||
|
block(proxy)
|
||||||
|
} catch (e: RemoteException) {
|
||||||
|
retryAfterBinderFailure(entry, bindTimeoutMs, e, block)
|
||||||
|
} finally {
|
||||||
|
touch(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var resolved = false
|
private suspend fun <T> retryAfterBinderFailure(
|
||||||
var jobRef: Job? = null
|
entry: PooledConnection,
|
||||||
|
bindTimeoutMs: Long,
|
||||||
|
error: RemoteException,
|
||||||
|
block: suspend (IOcrPlugin) -> T,
|
||||||
|
): T {
|
||||||
|
invalidateEntry(entry, error)
|
||||||
|
val proxy = awaitProxy(entry, bindTimeoutMs)
|
||||||
|
return block(proxy)
|
||||||
|
}
|
||||||
|
|
||||||
val conn = object : ServiceConnection {
|
private fun touch(entry: PooledConnection) {
|
||||||
|
synchronized(entry.lock) {
|
||||||
|
entry.lastUsed = uptimeMillis()
|
||||||
|
}
|
||||||
|
scheduleIdleUnbind(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun awaitProxy(entry: PooledConnection, bindTimeoutMs: Long): IOcrPlugin =
|
||||||
|
suspendCancellableCoroutine { cont ->
|
||||||
|
var shouldBind = false
|
||||||
|
synchronized(entry.lock) {
|
||||||
|
entry.lastUsed = uptimeMillis()
|
||||||
|
cancelIdleUnbindLocked(entry)
|
||||||
|
val existing = entry.proxy
|
||||||
|
if (existing != null && entry.bound) {
|
||||||
|
cont.resume(existing)
|
||||||
|
return@suspendCancellableCoroutine
|
||||||
|
}
|
||||||
|
entry.waiters.add(cont)
|
||||||
|
if (!entry.connecting) {
|
||||||
|
entry.connecting = true
|
||||||
|
shouldBind = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (shouldBind) {
|
||||||
|
startBind(entry, bindTimeoutMs)
|
||||||
|
}
|
||||||
|
cont.invokeOnCancellation {
|
||||||
|
synchronized(entry.lock) {
|
||||||
|
entry.waiters.remove(cont)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startBind(entry: PooledConnection, bindTimeoutMs: Long) {
|
||||||
|
val intent = Intent().setComponent(entry.cn)
|
||||||
|
val bindFlags = buildBindFlags(entry.serviceInfo)
|
||||||
|
var ok = try {
|
||||||
|
Log.i(TAG, "bindService: ${entry.cn}")
|
||||||
|
entry.appCtx.bindService(intent, entry.connection, bindFlags)
|
||||||
|
} catch (se: SecurityException) {
|
||||||
|
failWaiters(
|
||||||
|
entry,
|
||||||
|
IllegalStateException(
|
||||||
|
"bindService SecurityException: ${entry.cn}. Please make sure the plugin declares <uses-permission android:name=\"org.autojs.permission.PLUGIN\"/> and the Service uses this permission.",
|
||||||
|
se
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!ok && bindFlags != Context.BIND_AUTO_CREATE) {
|
||||||
|
ok = try {
|
||||||
|
entry.appCtx.bindService(intent, entry.connection, Context.BIND_AUTO_CREATE)
|
||||||
|
} catch (se: SecurityException) {
|
||||||
|
failWaiters(
|
||||||
|
entry,
|
||||||
|
IllegalStateException(
|
||||||
|
"bindService SecurityException: ${entry.cn}. Please make sure the plugin declares <uses-permission android:name=\"org.autojs.permission.PLUGIN\"/> and the Service uses this permission.",
|
||||||
|
se
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!ok) {
|
||||||
|
val msg = buildBindFailureMessage(entry.appCtx, entry.serviceInfo, entry.cn)
|
||||||
|
Log.e(TAG, msg)
|
||||||
|
failWaiters(entry, IllegalStateException(msg))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val timeout = Runnable {
|
||||||
|
val pending = synchronized(entry.lock) {
|
||||||
|
entry.connecting = false
|
||||||
|
val list = entry.waiters.toList()
|
||||||
|
entry.waiters.clear()
|
||||||
|
entry.bindTimeoutRunnable = null
|
||||||
|
list
|
||||||
|
}
|
||||||
|
if (pending.isNotEmpty()) {
|
||||||
|
Log.e(TAG, "bindService timeout: ${entry.cn}")
|
||||||
|
pending.forEach { waiter ->
|
||||||
|
if (!waiter.isCompleted) {
|
||||||
|
waiter.resumeWithException(TimeoutException("bindService timeout: ${entry.cn}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
runCatching { entry.appCtx.unbindService(entry.connection) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
synchronized(entry.lock) {
|
||||||
|
entry.bindTimeoutRunnable?.let { poolHandler.removeCallbacks(it) }
|
||||||
|
entry.bindTimeoutRunnable = timeout
|
||||||
|
}
|
||||||
|
poolHandler.postDelayed(timeout, bindTimeoutMs)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun invalidateEntry(entry: PooledConnection, error: Throwable) {
|
||||||
|
val shouldUnbind = synchronized(entry.lock) {
|
||||||
|
entry.proxy = null
|
||||||
|
entry.bound = false
|
||||||
|
entry.connecting = false
|
||||||
|
entry.bindTimeoutRunnable?.let { poolHandler.removeCallbacks(it) }
|
||||||
|
entry.bindTimeoutRunnable = null
|
||||||
|
entry.idleRunnable?.let { poolHandler.removeCallbacks(it) }
|
||||||
|
entry.idleRunnable = null
|
||||||
|
true
|
||||||
|
}
|
||||||
|
if (shouldUnbind) {
|
||||||
|
poolHandler.post {
|
||||||
|
runCatching { entry.appCtx.unbindService(entry.connection) }
|
||||||
|
Log.w(TAG, "invalidateEntry: ${entry.cn} | ${error.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun failWaiters(entry: PooledConnection, error: Throwable) {
|
||||||
|
val pending = synchronized(entry.lock) {
|
||||||
|
entry.connecting = false
|
||||||
|
entry.bindTimeoutRunnable?.let { poolHandler.removeCallbacks(it) }
|
||||||
|
entry.bindTimeoutRunnable = null
|
||||||
|
val list = entry.waiters.toList()
|
||||||
|
entry.waiters.clear()
|
||||||
|
list
|
||||||
|
}
|
||||||
|
pending.forEach { waiter ->
|
||||||
|
if (!waiter.isCompleted) {
|
||||||
|
waiter.resumeWithException(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scheduleIdleUnbind(entry: PooledConnection) {
|
||||||
|
val runnable = Runnable {
|
||||||
|
val shouldUnbind = synchronized(entry.lock) {
|
||||||
|
val idleEnough = uptimeMillis() - entry.lastUsed >= IDLE_UNBIND_MS
|
||||||
|
idleEnough && entry.bound && entry.proxy != null && !entry.connecting
|
||||||
|
}
|
||||||
|
if (shouldUnbind) {
|
||||||
|
runCatching { entry.appCtx.unbindService(entry.connection) }
|
||||||
|
synchronized(entry.lock) {
|
||||||
|
entry.proxy = null
|
||||||
|
entry.bound = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
synchronized(entry.lock) {
|
||||||
|
entry.idleRunnable?.let { poolHandler.removeCallbacks(it) }
|
||||||
|
entry.idleRunnable = runnable
|
||||||
|
}
|
||||||
|
poolHandler.postDelayed(runnable, IDLE_UNBIND_MS)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cancelIdleUnbindLocked(entry: PooledConnection) {
|
||||||
|
entry.idleRunnable?.let { poolHandler.removeCallbacks(it) }
|
||||||
|
entry.idleRunnable = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getOrCreateEntry(appCtx: Context, serviceInfo: ServiceInfo): PooledConnection {
|
||||||
|
val cn = ComponentName(serviceInfo.packageName, serviceInfo.name)
|
||||||
|
synchronized(poolLock) {
|
||||||
|
return connectionPool.getOrPut(cn) {
|
||||||
|
PooledConnection(appCtx, serviceInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class PooledConnection(
|
||||||
|
val appCtx: Context,
|
||||||
|
val serviceInfo: ServiceInfo,
|
||||||
|
) {
|
||||||
|
val cn: ComponentName = ComponentName(serviceInfo.packageName, serviceInfo.name)
|
||||||
|
val lock = Any()
|
||||||
|
val waiters = ArrayList<CancellableContinuation<IOcrPlugin>>()
|
||||||
|
var proxy: IOcrPlugin? = null
|
||||||
|
var bound: Boolean = false
|
||||||
|
var connecting: Boolean = false
|
||||||
|
var lastUsed: Long = 0L
|
||||||
|
var idleRunnable: Runnable? = null
|
||||||
|
var bindTimeoutRunnable: Runnable? = null
|
||||||
|
|
||||||
|
val connection = object : ServiceConnection {
|
||||||
override fun onServiceConnected(name: ComponentName, binder: IBinder) {
|
override fun onServiceConnected(name: ComponentName, binder: IBinder) {
|
||||||
Log.i(TAG, "onServiceConnected: $name")
|
Log.i(TAG, "onServiceConnected: $name")
|
||||||
val proxy = IOcrPlugin.Stub.asInterface(binder)
|
val proxy = IOcrPlugin.Stub.asInterface(binder)
|
||||||
val self = this
|
val pending = synchronized(lock) {
|
||||||
|
this@PooledConnection.proxy = proxy
|
||||||
jobRef = CoroutineScope(cont.context + Dispatchers.IO).launch {
|
bound = true
|
||||||
val result = runCatching { block(proxy) }
|
connecting = false
|
||||||
try {
|
lastUsed = uptimeMillis()
|
||||||
resolved = true
|
bindTimeoutRunnable?.let { poolHandler.removeCallbacks(it) }
|
||||||
if (!cont.isCompleted) cont.resumeWith(result)
|
bindTimeoutRunnable = null
|
||||||
Log.i(TAG, "resume continuation: success=${result.isSuccess}")
|
val list = waiters.toList()
|
||||||
} finally {
|
waiters.clear()
|
||||||
withContext(Dispatchers.Main) {
|
list
|
||||||
runCatching { appCtx.unbindService(self) }
|
}
|
||||||
Log.i(TAG, "unbindService: $name")
|
pending.forEach { waiter ->
|
||||||
}
|
if (!waiter.isCompleted) {
|
||||||
|
waiter.resume(proxy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onServiceDisconnected(name: ComponentName) {
|
override fun onServiceDisconnected(name: ComponentName) {
|
||||||
Log.w(TAG, "onServiceDisconnected: $name")
|
Log.w(TAG, "onServiceDisconnected: $name")
|
||||||
|
handleDisconnect(name, "onServiceDisconnected")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindingDied(name: ComponentName) {
|
||||||
|
Log.w(TAG, "onBindingDied: $name")
|
||||||
|
handleDisconnect(name, "onBindingDied")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNullBinding(name: ComponentName) {
|
||||||
|
Log.w(TAG, "onNullBinding: $name")
|
||||||
|
handleDisconnect(name, "onNullBinding")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val bindFlags = buildBindFlags(serviceInfo)
|
private fun handleDisconnect(name: ComponentName, reason: String) {
|
||||||
var ok = try {
|
val pending = synchronized(lock) {
|
||||||
Log.i(TAG, "bindService: $cn")
|
proxy = null
|
||||||
appCtx.bindService(intent, conn, bindFlags)
|
bound = false
|
||||||
} catch (se: SecurityException) {
|
connecting = false
|
||||||
Log.e(TAG, "bindService SecurityException: $cn | ${se.message}")
|
bindTimeoutRunnable?.let { poolHandler.removeCallbacks(it) }
|
||||||
cont.resumeWith(
|
bindTimeoutRunnable = null
|
||||||
Result.failure(
|
val list = waiters.toList()
|
||||||
IllegalStateException(
|
waiters.clear()
|
||||||
"bindService SecurityException: $cn. Please make sure the plugin declares <uses-permission android:name=\"org.autojs.permission.PLUGIN\"/> and the Service uses this permission.", se
|
list
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return@suspendCancellableCoroutine
|
|
||||||
}
|
|
||||||
if (!ok && bindFlags != Context.BIND_AUTO_CREATE) {
|
|
||||||
ok = try {
|
|
||||||
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 <uses-permission android:name=\"org.autojs.permission.PLUGIN\"/> and the Service uses this permission.", se
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return@suspendCancellableCoroutine
|
|
||||||
}
|
}
|
||||||
}
|
if (pending.isNotEmpty()) {
|
||||||
if (!ok) {
|
val error = IllegalStateException("$reason: $name")
|
||||||
val msg = buildBindFailureMessage(appCtx, serviceInfo, cn)
|
pending.forEach { waiter ->
|
||||||
Log.e(TAG, msg)
|
if (!waiter.isCompleted) {
|
||||||
cont.resumeWith(Result.failure(IllegalStateException(msg)))
|
waiter.resumeWithException(error)
|
||||||
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)
|
private class TimeoutException(msg: String) : RuntimeException(msg)
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package org.autojs.autojs.runtime.api.augment.ocr
|
package org.autojs.autojs.runtime.api.augment.ocr
|
||||||
|
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.autojs.autojs.AbstractAutoJs.Companion.isInrt
|
import org.autojs.autojs.AbstractAutoJs.Companion.isInrt
|
||||||
import org.autojs.autojs.annotation.RhinoRuntimeFunctionInterface
|
import org.autojs.autojs.annotation.RhinoRuntimeFunctionInterface
|
||||||
@@ -14,12 +17,19 @@ import org.autojs.autojs.runtime.api.augment.Invokable
|
|||||||
import org.autojs.autojs.runtime.api.augment.ocr.Ocr.Companion.OcrMode
|
import org.autojs.autojs.runtime.api.augment.ocr.Ocr.Companion.OcrMode
|
||||||
import org.autojs.autojs.runtime.exception.WrappedIllegalArgumentException
|
import org.autojs.autojs.runtime.exception.WrappedIllegalArgumentException
|
||||||
import org.autojs.autojs.util.RhinoUtils.coerceBoolean
|
import org.autojs.autojs.util.RhinoUtils.coerceBoolean
|
||||||
|
import org.autojs.autojs.util.RhinoUtils.coerceFloatNumber
|
||||||
import org.autojs.autojs.util.RhinoUtils.coerceIntNumber
|
import org.autojs.autojs.util.RhinoUtils.coerceIntNumber
|
||||||
|
import org.autojs.autojs.util.RhinoUtils.coerceString
|
||||||
import org.autojs.autojs6.R
|
import org.autojs.autojs6.R
|
||||||
import org.autojs.plugin.paddle.ocr.api.OcrOptions
|
import org.autojs.plugin.paddle.ocr.api.OcrOptions
|
||||||
import org.mozilla.javascript.NativeArray
|
import org.mozilla.javascript.NativeArray
|
||||||
import org.mozilla.javascript.NativeObject
|
import org.mozilla.javascript.NativeObject
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modified by JetBrains AI Assistant (GPT-5.2-Codex (xhigh)) as of Feb 13, 2026.
|
||||||
|
*/
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
class OcrPaddle(private val scriptRuntime: ScriptRuntime) : Augmentable(scriptRuntime), Invokable {
|
class OcrPaddle(private val scriptRuntime: ScriptRuntime) : Augmentable(scriptRuntime), Invokable {
|
||||||
|
|
||||||
@@ -35,8 +45,14 @@ class OcrPaddle(private val scriptRuntime: ScriptRuntime) : Augmentable(scriptRu
|
|||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val DEFAULT_CPU_THREAD_NUM = 4
|
private const val DEFAULT_CPU_THREAD_NUM = 4
|
||||||
|
|
||||||
private const val DEFAULT_USE_SLIM = true
|
private const val DEFAULT_USE_SLIM = true
|
||||||
|
private const val DEFAULT_USE_RAW = true
|
||||||
|
|
||||||
private const val DEFAULT_USE_OPENCL = false
|
private const val DEFAULT_USE_OPENCL = false
|
||||||
|
private const val DEFAULT_MERGE_LINE = false
|
||||||
|
|
||||||
|
private const val EXTRA_RAW_IMAGE = "rawImage"
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@RhinoRuntimeFunctionInterface
|
@RhinoRuntimeFunctionInterface
|
||||||
@@ -52,11 +68,15 @@ class OcrPaddle(private val scriptRuntime: ScriptRuntime) : Augmentable(scriptRu
|
|||||||
|
|
||||||
fun recognizeTextImpl(scriptRuntime: ScriptRuntime, image: ImageWrapper, options: NativeObject): List<String> {
|
fun recognizeTextImpl(scriptRuntime: ScriptRuntime, image: ImageWrapper, options: NativeObject): List<String> {
|
||||||
ApkBuilder.Lib.PADDLE_OCR.ensureLibFiles(OcrMode.PADDLE.value)
|
ApkBuilder.Lib.PADDLE_OCR.ensureLibFiles(OcrMode.PADDLE.value)
|
||||||
val (cpuThreadNum, useSlim, useOpenCL) = getOcrOptions(options)
|
val parsed = getOcrOptions(options)
|
||||||
|
val (cpuThreadNum, useSlim, useOpenCL) = parsed
|
||||||
val ocrOptions = OcrOptions().apply {
|
val ocrOptions = OcrOptions().apply {
|
||||||
this.cpuThreadNum = cpuThreadNum
|
this.cpuThreadNum = cpuThreadNum
|
||||||
this.useSlim = useSlim
|
this.useSlim = useSlim
|
||||||
this.useOpenCL = useOpenCL
|
this.useOpenCL = useOpenCL
|
||||||
|
this.detLongSize = parsed.detLongSize
|
||||||
|
this.scoreThreshold = parsed.scoreThreshold
|
||||||
|
this.extras = parsed.extras
|
||||||
}
|
}
|
||||||
return if (!isInrt) {
|
return if (!isInrt) {
|
||||||
runBlocking(scriptRuntime.coroutineContext) {
|
runBlocking(scriptRuntime.coroutineContext) {
|
||||||
@@ -73,13 +93,17 @@ class OcrPaddle(private val scriptRuntime: ScriptRuntime) : Augmentable(scriptRu
|
|||||||
|
|
||||||
fun detectImpl(scriptRuntime: ScriptRuntime, image: ImageWrapper, options: NativeObject): List<OcrResult> {
|
fun detectImpl(scriptRuntime: ScriptRuntime, image: ImageWrapper, options: NativeObject): List<OcrResult> {
|
||||||
ApkBuilder.Lib.PADDLE_OCR.ensureLibFiles(OcrMode.PADDLE.value)
|
ApkBuilder.Lib.PADDLE_OCR.ensureLibFiles(OcrMode.PADDLE.value)
|
||||||
val (cpuThreadNum, useSlim, useOpenCL) = getOcrOptions(options)
|
val parsed = getOcrOptions(options)
|
||||||
|
val (cpuThreadNum, useSlim, useOpenCL) = parsed
|
||||||
val ocrOptions = OcrOptions().apply {
|
val ocrOptions = OcrOptions().apply {
|
||||||
this.cpuThreadNum = cpuThreadNum
|
this.cpuThreadNum = cpuThreadNum
|
||||||
this.useSlim = useSlim
|
this.useSlim = useSlim
|
||||||
this.useOpenCL = useOpenCL
|
this.useOpenCL = useOpenCL
|
||||||
|
this.detLongSize = parsed.detLongSize
|
||||||
|
this.scoreThreshold = parsed.scoreThreshold
|
||||||
|
this.extras = parsed.extras
|
||||||
}
|
}
|
||||||
return if (!isInrt) {
|
val results = if (!isInrt) {
|
||||||
runBlocking(scriptRuntime.coroutineContext) {
|
runBlocking(scriptRuntime.coroutineContext) {
|
||||||
val target = PaddleOcrPluginHost.select(globalContext)
|
val target = PaddleOcrPluginHost.select(globalContext)
|
||||||
?: throw WrappedIllegalArgumentException(globalContext.getString(R.string.error_no_paddle_ocr_plugins_available))
|
?: throw WrappedIllegalArgumentException(globalContext.getString(R.string.error_no_paddle_ocr_plugins_available))
|
||||||
@@ -92,17 +116,106 @@ class OcrPaddle(private val scriptRuntime: ScriptRuntime) : Augmentable(scriptRu
|
|||||||
OcrResult(it.text, it.confidence, it.bounds)
|
OcrResult(it.text, it.confidence, it.bounds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return if (parsed.mergeLine) mergeByLine(results) else results
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getOcrOptions(options: NativeObject): OcrOptions {
|
private fun getOcrOptions(options: NativeObject): ParsedOptions {
|
||||||
val cpuThreadNum = options.inquire("cpuThreadNum", ::coerceIntNumber, DEFAULT_CPU_THREAD_NUM)
|
val cpuThreadNum = options.inquire("cpuThreadNum", ::coerceIntNumber, DEFAULT_CPU_THREAD_NUM)
|
||||||
val useSlim = options.inquire("useSlim", ::coerceBoolean, DEFAULT_USE_SLIM)
|
val useSlim = options.inquire("useSlim", ::coerceBoolean, DEFAULT_USE_SLIM)
|
||||||
val useOpenCL = options.inquire("useOpenCL", ::coerceBoolean, DEFAULT_USE_OPENCL)
|
val useOpenCL = options.inquire("useOpenCL", ::coerceBoolean, DEFAULT_USE_OPENCL)
|
||||||
return OcrOptions(cpuThreadNum, useSlim, useOpenCL)
|
val detLongSize = options.inquire("detLongSize", ::coerceIntNumber, 0)
|
||||||
|
val scoreThreshold = options.inquire("scoreThreshold", ::coerceFloatNumber, -1f)
|
||||||
|
val mergeLine = options.inquire("mergeLine", ::coerceBoolean, DEFAULT_MERGE_LINE)
|
||||||
|
val splitWords = options.inquire("splitWords", ::coerceBoolean, false)
|
||||||
|
val useWordSegmentation = options.inquire("useWordSegmentation", ::coerceBoolean, false)
|
||||||
|
val useRaw = options.inquire("useRaw", ::coerceBoolean, DEFAULT_USE_RAW) ||
|
||||||
|
options.inquire("raw", ::coerceBoolean, false)
|
||||||
|
val imageQuality = options.inquire("imageQuality", ::coerceIntNumber, -1)
|
||||||
|
val imageFormat = options.inquire("imageFormat", ::coerceString, "")
|
||||||
|
|
||||||
|
val extras = Bundle()
|
||||||
|
if (useRaw && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||||
|
extras.putBoolean(EXTRA_RAW_IMAGE, true)
|
||||||
|
}
|
||||||
|
if (imageQuality > 0) {
|
||||||
|
extras.putInt("imageQuality", imageQuality)
|
||||||
|
}
|
||||||
|
if (imageFormat.isNotBlank()) {
|
||||||
|
extras.putString("imageFormat", imageFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ParsedOptions(
|
||||||
|
cpuThreadNum = cpuThreadNum,
|
||||||
|
useSlim = useSlim,
|
||||||
|
useOpenCL = useOpenCL,
|
||||||
|
detLongSize = detLongSize,
|
||||||
|
scoreThreshold = scoreThreshold,
|
||||||
|
mergeLine = mergeLine && !splitWords && !useWordSegmentation,
|
||||||
|
extras = extras.takeIf { !it.isEmpty } ?: Bundle(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class OcrOptions(val cpuThreadNum: Int, val useSlim: Boolean, val useOpenCL: Boolean)
|
private data class ParsedOptions(
|
||||||
|
val cpuThreadNum: Int,
|
||||||
|
val useSlim: Boolean,
|
||||||
|
val useOpenCL: Boolean,
|
||||||
|
val detLongSize: Int,
|
||||||
|
val scoreThreshold: Float,
|
||||||
|
val mergeLine: Boolean,
|
||||||
|
val extras: Bundle,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun mergeByLine(results: List<OcrResult>): List<OcrResult> {
|
||||||
|
if (results.size <= 1) return results
|
||||||
|
val sorted = results.sorted()
|
||||||
|
val merged = ArrayList<OcrResult>(sorted.size)
|
||||||
|
|
||||||
|
var text = StringBuilder()
|
||||||
|
var bounds = Rect()
|
||||||
|
var weightSum = 0
|
||||||
|
var confidenceSum = 0f
|
||||||
|
var last = sorted.first()
|
||||||
|
|
||||||
|
fun add(r: OcrResult) {
|
||||||
|
if (text.isEmpty()) {
|
||||||
|
bounds = Rect(r.bounds)
|
||||||
|
} else {
|
||||||
|
bounds.union(r.bounds)
|
||||||
|
}
|
||||||
|
text.append(r.text)
|
||||||
|
val weight = max(1, r.text.length)
|
||||||
|
weightSum += weight
|
||||||
|
confidenceSum += r.confidence * weight
|
||||||
|
last = r
|
||||||
|
}
|
||||||
|
|
||||||
|
fun flush() {
|
||||||
|
if (text.isNotEmpty()) {
|
||||||
|
val confidence = if (weightSum > 0) confidenceSum / weightSum else 0f
|
||||||
|
merged.add(OcrResult(text.toString(), confidence, Rect(bounds)))
|
||||||
|
}
|
||||||
|
text = StringBuilder()
|
||||||
|
weightSum = 0
|
||||||
|
confidenceSum = 0f
|
||||||
|
}
|
||||||
|
|
||||||
|
add(last)
|
||||||
|
for (i in 1 until sorted.size) {
|
||||||
|
val r = sorted[i]
|
||||||
|
val deviation = max(last.bounds.height(), r.bounds.height()) / 2
|
||||||
|
val lastCenter = (last.bounds.top + last.bounds.bottom) / 2
|
||||||
|
val rCenter = (r.bounds.top + r.bounds.bottom) / 2
|
||||||
|
if (abs(lastCenter - rCenter) < deviation) {
|
||||||
|
add(r)
|
||||||
|
} else {
|
||||||
|
flush()
|
||||||
|
add(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flush()
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import org.autojs.plugin.paddle.ocr.api.OcrResult
|
|||||||
*
|
*
|
||||||
* Created by JetBrains AI Assistant (GPT-5.2) on Jan 17, 2026.
|
* Created by JetBrains AI Assistant (GPT-5.2) on Jan 17, 2026.
|
||||||
* Modified by SuperMonster003 as of Jan 18, 2026.
|
* Modified by SuperMonster003 as of Jan 18, 2026.
|
||||||
|
* Modified by JetBrains AI Assistant (GPT-5.2-Codex (xhigh)) as of Feb 13, 2026.
|
||||||
*/
|
*/
|
||||||
class PredictorNativeBridge : NativeBridge {
|
class PredictorNativeBridge : NativeBridge {
|
||||||
|
|
||||||
@@ -110,6 +111,7 @@ class PredictorNativeBridge : NativeBridge {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun recognizeText(bitmap: Bitmap, options: OcrOptions): List<String> {
|
override fun recognizeText(bitmap: Bitmap, options: OcrOptions): List<String> {
|
||||||
|
applyRuntimeOptions(options)
|
||||||
val results = predictor.runOcr(bitmap)
|
val results = predictor.runOcr(bitmap)
|
||||||
val out = ArrayList<String>(results.size)
|
val out = ArrayList<String>(results.size)
|
||||||
for (r in results) out.add(r.label)
|
for (r in results) out.add(r.label)
|
||||||
@@ -124,6 +126,7 @@ class PredictorNativeBridge : NativeBridge {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun detect(bitmap: Bitmap, options: OcrOptions): List<OcrResult> {
|
override fun detect(bitmap: Bitmap, options: OcrOptions): List<OcrResult> {
|
||||||
|
applyRuntimeOptions(options)
|
||||||
val results = predictor.runOcr(bitmap)
|
val results = predictor.runOcr(bitmap)
|
||||||
return results.map { r ->
|
return results.map { r ->
|
||||||
OcrResult().apply {
|
OcrResult().apply {
|
||||||
@@ -135,4 +138,15 @@ class PredictorNativeBridge : NativeBridge {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun applyRuntimeOptions(options: OcrOptions) {
|
||||||
|
val detLongSize = options.detLongSize
|
||||||
|
if (detLongSize > 0) {
|
||||||
|
predictor.setDetLongSize(detLongSize)
|
||||||
|
}
|
||||||
|
val scoreThreshold = options.scoreThreshold
|
||||||
|
if (scoreThreshold >= 0f) {
|
||||||
|
predictor.setScoreThreshold(scoreThreshold)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#Fri Feb 13 15:23:40 CST 2026
|
#Sat Feb 14 14:01:58 CST 2026
|
||||||
BUILD_TIME=1770967420765
|
BUILD_TIME=1771048918130
|
||||||
COMPILE_SDK_VERSION=36
|
COMPILE_SDK_VERSION=36
|
||||||
IMAGE_QUANT_CMAKE_VERSION=3.22.1
|
IMAGE_QUANT_CMAKE_VERSION=3.22.1
|
||||||
IMAGE_QUANT_NDK_VERSION=26.1.10909125
|
IMAGE_QUANT_NDK_VERSION=26.1.10909125
|
||||||
@@ -27,6 +27,6 @@ RAPID_OCR_OPENCV_MOBILE_LABEL_VERSION=13
|
|||||||
RAPID_OCR_OPENCV_MOBILE_VERSION=4.5.3
|
RAPID_OCR_OPENCV_MOBILE_VERSION=4.5.3
|
||||||
TARGET_SDK_VERSION=36
|
TARGET_SDK_VERSION=36
|
||||||
TARGET_SDK_VERSION_INRT=29
|
TARGET_SDK_VERSION_INRT=29
|
||||||
VERSION_BUILD=3738
|
VERSION_BUILD=3742
|
||||||
VERSION_NAME=6.7.0 Alpha20
|
VERSION_NAME=6.7.0 Alpha20
|
||||||
VSCODE_EXT_REQUIRED_VERSION=1.0.13
|
VSCODE_EXT_REQUIRED_VERSION=1.0.13
|
||||||
|
|||||||
Reference in New Issue
Block a user