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

@@ -2,6 +2,22 @@
* @author TonyJiangWJ
*/
!function originalClassesForPaddleOcr() {
let hintForAutoJs670AndAbove = '自 AutoJs6 6.7.0 起, Paddle OCR 已进行功能插件化, 因此无法再使用内置原始类.\n\n' +
'如需了解插件 API 使用示例, 可参考 "PaddleOCR (内置 API).js" 示例代码.';
dialogs.build({
title: R.string.text_prompt,
content: hintForAutoJs670AndAbove,
positiveText: R.string.text_exit,
stubborn: true,
}).on('positive', (d) => {
d.dismiss();
exit();
}).show();
exit();
const Predictor = com.baidu.paddle.lite.ocr.Predictor;
console.show();

View File

@@ -0,0 +1,232 @@
/**
* Create by TonyJiangWJ on Nov 15, 2023.
* Modified by SuperMonster003 as of Jan 18, 2026.
*/
!function sampleForPaddleOcr() {
let currentEngine = engines.myEngine();
let runningEngines = engines.all();
let currentSource = `${currentEngine.getSource()}`;
if (runningEngines.length > 1) {
runningEngines.forEach(compareEngine => {
let compareSource = `${compareEngine.getSource()}`;
if (currentEngine.getId() !== compareEngine.getId() && compareSource === currentSource) {
// Force stop scripts with the same name.
// zh-CN: 强制关闭同名的脚本.
compareEngine.forceStop();
}
});
}
if (!requestScreenCapture()) {
// Failed to request screenshot permission.
toastError('请求截图权限失败');
exit();
}
sleep(500);
// Use slim model for faster inference.
// zh-CN: 使用 slim 模型以获得更快速度.
let useSlim = true;
// CPU thread count.
// zh-CN: CPU 线程数量.
let cpuThreadNum = 4;
// Whether to request OpenCL.
// zh-CN: 是否请求 OpenCL.
let useOpenCL = false;
// Recognition results and screenshot information.
// zh-CN: 识别结果和截图信息.
let result = [];
let img = null;
let running = true;
let capturing = false;
// Get status bar height offset.
// zh-CN: 获取状态栏高度偏移.
let offset = /* -getStatusBarHeightCompat(); */ 0;
// Draw OCR results.
// zh-CN: 绘制 OCR 结果.
let window = floaty.rawWindow(
<canvas id="canvas" layout_weight="1"/>,
);
// Set float window position.
// zh-CN: 设置悬浮窗位置.
ui.post(() => {
window.setPosition(0, offset);
window.setSize(WIDTH, HEIGHT);
window.setTouchable(false);
window.exitOnClose();
});
// Operation buttons.
// zh-CN: 操作按钮.
let clickButtonWindow = floaty.rawWindow(
<vertical>
{/* Screenshot OCR */}
<button id="captureAndOcr" text="截图识别"/>
{/* Exit */}
<button id="closeBtn" text="退出"/>
</vertical>,
);
// OCR worker.
// zh-CN: OCR 工作线程.
function captureAndOcrAsync() {
if (capturing) {
// Busy, please wait.
toastLog('正在处理中, 请稍候.');
return;
}
capturing = true;
// Clear last result immediately to avoid drawing stale boxes after capture.
// zh-CN: 立即清空上一次结果, 避免截取完成后仍显示旧框.
result = [];
// Hide overlay windows before capturing to avoid them being captured into screenshot.
// zh-CN: 截图前隐藏悬浮窗, 避免悬浮窗内容被截图到.
ui.run(function () {
clickButtonWindow.setPosition(WIDTH, HEIGHT);
window.setPosition(WIDTH, HEIGHT);
});
threads.start(function () {
try {
// Wait for UI to apply window position changes (1-2 frames).
// zh-CN: 等待 UI 应用窗口位置变化 (1-2 帧).
sleep(150);
img && img.recycle();
img = images.captureScreen();
if (!img) {
// Failed to capture screenshot.
toastError('截图失败');
return;
}
let start = new Date();
result = ocr.paddle.detect(img, { useSlim, cpuThreadNum, useOpenCL });
// OCR cost time.
toastLog(`OCR 耗时 ${new Date() - start}ms`);
} catch (e) {
toastError(e);
exit();
} finally {
capturing = false;
// Restore overlay windows.
// zh-CN: 恢复悬浮窗位置.
ui.run(function () {
window.setPosition(0, offset);
window.setSize(WIDTH, HEIGHT);
window.setTouchable(false);
clickButtonWindow.setPosition(
cX(0.5) - ~~(clickButtonWindow.getWidth() / 2),
cY(0.65),
);
});
}
});
}
clickButtonWindow['captureAndOcr'].click(function () {
captureAndOcrAsync();
});
clickButtonWindow['closeBtn'].click(function () {
exit();
});
clickButtonWindow.exitOnClose();
let paint = new Paint();
paint.setStrokeWidth(1);
paint.setTypeface(Typeface.DEFAULT_BOLD);
paint.setTextAlign(Paint.Align.LEFT);
paint.setAntiAlias(true);
paint.setStrokeJoin(Paint.Join.ROUND);
paint.setDither(true);
window.canvas.on('draw', function (canvas) {
if (!running || capturing) return;
// Clear canvas.
// zh-CN: 清空画布.
canvas.drawColor(0xFFFFFF, android.graphics.PorterDuff.Mode.CLEAR);
if (result && result.length > 0) {
for (let i = 0; i < result.length; i++) {
let ocrResult = result[i];
drawRectAndText(ocrResult.label, ocrResult.bounds, '#00ff00', canvas, paint);
}
}
});
// Keep script alive.
// zh-CN: 保持脚本运行.
keepAlive();
events.on('exit', () => {
// Mark stop to avoid canvas crash.
// zh-CN: 标记停止, 避免 canvas 导致闪退.
running = false;
// Remove all listeners.
// zh-CN: 撤销监听.
window.canvas.removeAllListeners();
// Recycle image.
// zh-CN: 回收图片.
img && img.recycle();
});
/**
* Draw text and rectangle.
* zh-CN: 绘制文本和方框.
*/
function drawRectAndText(desc, rect, colorStr, canvas, paint) {
let color = colors.parseColor(colorStr);
paint.setStrokeWidth(1);
paint.setStyle(Paint.Style.STROKE);
// Invert color.
// zh-CN: 反色.
paint.setARGB(255, 255 - (color >> 16 & 0xff), 255 - (color >> 8 & 0xff), 255 - (color & 0xff));
canvas.drawRect(rect, paint);
paint.setARGB(255, color >> 16 & 0xff, color >> 8 & 0xff, color & 0xff);
paint.setStrokeWidth(1);
paint.setTextSize(20);
paint.setStyle(Paint.Style.FILL);
canvas.drawText(desc, rect.left, rect.top, paint);
paint.setTextSize(10);
paint.setStrokeWidth(1);
paint.setARGB(255, 0, 0, 0);
}
/**
* Get status bar height.
* zh-CN: 获取状态栏高度.
*/
function getStatusBarHeightCompat() {
let result = 0;
let resId = context.getResources().getIdentifier('status_bar_height', 'dimen', 'android');
if (resId > 0) {
result = context.getResources().getDimensionPixelOffset(resId);
}
if (result <= 0) {
result = context.getResources().getDimensionPixelOffset(R.dimen.dimen_25dp);
}
return result;
}
// Run once at start.
// zh-CN: 启动后先执行一次.
captureAndOcrAsync();
}();

View File

@@ -1,198 +0,0 @@
/**
* create by TonyJiangWJ
*/
!function sampleForPaddleOcr() {
const Predictor = com.baidu.paddle.lite.ocr.Predictor;
let currentEngine = engines.myEngine();
let runningEngines = engines.all();
let currentSource = `${currentEngine.getSource()}`;
if (runningEngines.length > 1) {
runningEngines.forEach(compareEngine => {
let compareSource = `${compareEngine.getSource()}`;
if (currentEngine.getId() !== compareEngine.getId() && compareSource === currentSource) {
// 强制关闭同名的脚本
compareEngine.forceStop();
}
});
}
if (!requestScreenCapture()) {
toastLog('请求截图权限失败');
exit();
}
sleep(1000);
// 指定是否用精简版模型 速度较快
let useSlim = true;
// 创建检测器
let predictor = new Predictor();
// predictor.cpuThreadNum = 4 // 可以自定义使用 CPU 的线程数
// predictor.checkModelLoaded = false // 可以自定义是否需要校验模型是否成功加载 默认开启 使用内置 Base64 图片进行校验 识别测试文本来校验模型是否加载成功
// 初始化模型 首次运行时会比较耗时
let loading = threads.disposable();
// 建议在新线程中初始化模型
threads.start(function () {
loading.setAndNotify(predictor.init(context, useSlim));
// loading.setAndNotify(predictor.init(context)) 为默认不使用精简版
// 内置默认 modelPath 为 models/ocr_v3_for_cpu, 初始化自定义模型请写绝对路径否则无法获取到
// 内置默认 labelPath 为 labels/ppocr_keys_v1.txt
// let modelPath = files.path('./models/customize') // 指定自定义模型路径
// let labelPath = files.path('./models/customize') // 指定自定义 label 路径
// 使用自定义模型时 det rec cls 三个模型文件名称需要手动指定
// predictor.detModelFilename = 'det_opt.nb'
// predictor.recModelFilename = 'rec_opt.nb'
// predictor.clsModelFilename = 'cls_opt.nb'
// loading.setAndNotify(predictor.init(context, modelPath, labelPath))
});
let loadSuccess = loading.blockedGet();
if (!loadSuccess) {
toastLog('初始化 ocr 失败');
exit();
}
// 识别结果和截图信息
let result = [];
let img = null;
let running = true;
let capturing = true;
/**
* 截图并识别 OCR 文本信息
*/
function captureAndOcr() {
capturing = true;
img && img.recycle();
img = captureScreen();
if (!img) {
toastLog('截图失败');
}
let start = new Date();
result = predictor.runOcr(img.getBitmap());
toastLog(`耗时 ${new Date() - start}ms`);
capturing = false;
}
captureAndOcr();
// 获取状态栏高度
let offset = -getStatusBarHeightCompat();
// 绘制识别结果
let window = floaty.rawWindow(
<canvas id="canvas" layout_weight="1"/>,
);
// 设置悬浮窗位置
ui.post(() => {
window.setPosition(0, offset);
window.setSize(device.width, device.height);
window.setTouchable(false);
});
// 操作按钮
let clickButtonWindow = floaty.rawWindow(
<vertical>
<button id="captureAndOcr" text="截图识别"/>
<button id="closeBtn" text="退出"/>
</vertical>,
);
ui.run(function () {
clickButtonWindow.setPosition(device.width / 2 - ~~(clickButtonWindow.getWidth() / 2), device.height * 0.65);
});
// 点击识别
clickButtonWindow['captureAndOcr'].click(function () {
result = [];
ui.run(function () {
clickButtonWindow.setPosition(device.width, device.height);
});
setTimeout(() => {
captureAndOcr();
ui.run(function () {
clickButtonWindow.setPosition(device.width / 2 - ~~(clickButtonWindow.getWidth() / 2), device.height * 0.65);
});
}, 500);
});
// 点击关闭
clickButtonWindow['closeBtn'].click(function () {
exit();
});
let Typeface = android.graphics.Typeface;
let paint = new Paint();
paint.setStrokeWidth(1);
paint.setTypeface(Typeface.DEFAULT_BOLD);
paint.setTextAlign(Paint.Align.LEFT);
paint.setAntiAlias(true);
paint.setStrokeJoin(Paint.Join.ROUND);
paint.setDither(true);
window.canvas.on('draw', function (canvas) {
if (!running || capturing) {
return;
}
// 清空内容
canvas.drawColor(0xFFFFFF, android.graphics.PorterDuff.Mode.CLEAR);
if (result && result.length > 0) {
for (let i = 0; i < result.length; i++) {
let ocrResult = result[i];
drawRectAndText(ocrResult.label, ocrResult.bounds, '#00ff00', canvas, paint);
}
}
});
setInterval(() => {
/* Empty body. */
}, 10000);
events.on('exit', () => {
// 标记停止 避免 canvas 导致闪退
running = false;
// 撤销监听
window.canvas.removeAllListeners();
// 回收图片
img && img.recycle();
});
/**
* 绘制文本和方框
*/
function drawRectAndText(desc, rect, colorStr, canvas, paint) {
let color = colors.parseColor(colorStr);
paint.setStrokeWidth(1);
paint.setStyle(Paint.Style.STROKE);
// 反色
paint.setARGB(255, 255 - (color >> 16 & 0xff), 255 - (color >> 8 & 0xff), 255 - (color & 0xff));
canvas.drawRect(rect, paint);
paint.setARGB(255, color >> 16 & 0xff, color >> 8 & 0xff, color & 0xff);
paint.setStrokeWidth(1);
paint.setTextSize(20);
paint.setStyle(Paint.Style.FILL);
canvas.drawText(desc, rect.left, rect.top, paint);
paint.setTextSize(10);
paint.setStrokeWidth(1);
paint.setARGB(255, 0, 0, 0);
}
/**
* 获取状态栏高度
*/
function getStatusBarHeightCompat() {
let result = 0;
let resId = context.getResources().getIdentifier('status_bar_height', 'dimen', 'android');
if (resId > 0) {
result = context.getResources().getDimensionPixelOffset(resId);
}
if (result <= 0) {
result = context.getResources().getDimensionPixelOffset(R.dimen.dimen_25dp);
}
return result;
}
}();

View File

@@ -1,17 +1,25 @@
package org.autojs.autojs.apkbuilder
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.content.pm.PackageManager.ApplicationInfoFlags
import android.content.pm.PackageManager.GET_SHARED_LIBRARY_FILES
import android.content.pm.ServiceInfo
import android.content.res.AssetManager
import android.graphics.Bitmap
import android.os.Build
import android.os.IBinder
import android.util.Log
import com.mcal.apksigner.ApkSigner
import com.reandroid.arsc.chunk.TableBlock
import org.autojs.autojs.AbstractAutoJs.Companion.isInrt
import org.autojs.autojs.apkbuilder.keystore.AESUtils
import org.autojs.autojs.app.GlobalAppContext
import org.autojs.autojs.core.plugin.center.PluginEnableStore
import org.autojs.autojs.engine.encryption.AdvancedEncryptionStandard
import org.autojs.autojs.pio.PFiles
import org.autojs.autojs.project.BuildInfo
@@ -23,6 +31,7 @@ import org.autojs.autojs.script.JavaScriptFileSource
import org.autojs.autojs.util.FileUtils.TYPE.JAVASCRIPT
import org.autojs.autojs.util.MD5Utils
import org.autojs.autojs6.R
import org.autojs.plugin.paddle.ocr.api.IOcrPlugin
import pxb.android.StringItem
import pxb.android.axml.AxmlWriter
import zhao.arsceditor.ResDecoder.ARSCDecoder
@@ -33,10 +42,15 @@ import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
/**
* Created by Stardust on Oct 24, 2017.
* Modified by SuperMonster003 as of Jul 8, 2022.
* Modified by JetBrains AI Assistant (GPT-5.2) as of Jan 17, 2026.
* Modified by SuperMonster003 as of Jan 18, 2026.
*/
open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File, private val buildPath: String) {
@@ -52,9 +66,9 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File
private val mAssetManager: AssetManager by lazy { globalContext.assets }
private var mLibsIncludes = Libs.defaultLibsToInclude.toMutableList()
private var mAssetsFileIncludes = Libs.defaultAssetFilesToInclude.toMutableList()
private var mAssetsDirExcludes = Libs.defaultAssetDirsToExclude.toMutableList()
private var mLibsIncludes = Lib.defaultLibsToInclude.toMutableList()
private var mAssetsFileIncludes = Lib.defaultAssetFilesToInclude.toMutableList()
private var mAssetsDirExcludes = Lib.defaultAssetDirsToExclude.toMutableList()
private var mSplashThemeId: Int = 0
private var mNoSplashThemeId: Int = 0
@@ -231,11 +245,11 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File
mKey = MD5Utils.md5(projectConfig.run { packageName + versionName + mainScriptFileName })
mInitVector = MD5Utils.md5(projectConfig.run { buildInfo.buildId + name }).take(16)
Libs.entries.forEach { entry ->
Lib.entries.forEach { entry ->
if (config.libs.contains(entry.label)) {
mLibsIncludes += entry.libsToInclude.toSet()
mAssetsFileIncludes += entry.assetFilesToInclude.toSet()
mAssetsDirExcludes -= entry.assetDirsToExclude.toSet()
mAssetsDirExcludes -= entry.assetDirsToInclude.toSet()
}
}
}
@@ -418,6 +432,10 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File
"armeabi-v7a" to "arm",
)
// Try extracting native libraries from installed plugin APKs if needed.
// zh-CN: 如有需要, 尝试从已安装插件 APK 中解压 native 库文件.
ensureAndExtractPluginLibrariesIfNeeded(config, potentialAbiAliasList)
config.abis.forEach { abiCanonicalName ->
copyLibrariesByAbi(abiCanonicalName, abiCanonicalName)
potentialAbiAliasList[abiCanonicalName]?.let { abiAliasName ->
@@ -426,6 +444,322 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File
}
}
private fun ensureAndExtractPluginLibrariesIfNeeded(
config: ProjectConfig,
potentialAbiAliasList: Map<String, String>,
) {
Lib.entries.mapNotNull {
if (it.isPlugin && config.libs.contains(it.label)) it.toPluginPair() else null
}.forEach { (lib, plugin) ->
// Select plugin service by variant.
// zh-CN: 通过 variant 选择插件服务.
val (serviceInfo, selectedVariant) = selectPluginServiceOrThrow(
lib = lib,
action = plugin.action,
)
Log.i(TAG, "Selected ${lib.label} plugin: variant=${selectedVariant.variant}, pkg=${serviceInfo.packageName}")
// Extract libraries from installed plugin APK (variant-aware).
// zh-CN: 从已安装插件 APK 中解压 so 文件, 并按变体裁剪.
extractLibrariesFromPluginApkOrThrow(
config = config,
requiredLibNames = selectedVariant.libsToInclude,
serviceInfo = serviceInfo,
potentialAbiAliasList = potentialAbiAliasList,
)
// Extract assets (models/labels) from installed plugin APK (variant-aware).
// zh-CN: 从已安装插件 APK 中解压 assets 资源 (models/labels), 并按变体裁剪.
extractAssetsFromPluginApkOrThrow(
serviceInfo = serviceInfo,
pluginLibVariant = selectedVariant,
)
}
}
private fun selectPluginServiceOrThrow(
lib: Lib,
action: String,
): Pair<ServiceInfo, PluginLibVariant> {
val pm = globalContext.packageManager
val moduleLabel = lib.label
val pluginPair = lib.toPluginPair()
val plugin = pluginPair.second
val services = findServicesByAction(pm, action)
if (services.isEmpty()) {
throw IllegalStateException(
globalContext.getString(R.string.error_missing_required_plugin_for_module_label, moduleLabel)
)
}
// Only consider enabled plugins in packaging phase.
// zh-CN: 打包阶段仅考虑已启用的插件.
val enabledServices = services.filter { si ->
PluginEnableStore.isEnabled(globalContext, si.packageName, defaultEnabled = true)
}
if (enabledServices.isEmpty()) {
throw IllegalStateException(
globalContext.getString(R.string.error_no_enabled_plugin_for_module_label, moduleLabel)
)
}
val infos = enabledServices.mapNotNull { si ->
val info = runCatching { queryPluginInfoBlocking(globalContext, si, pluginPair) }.getOrNull()
info?.let { si to it }
}
val variantList = plugin.variants.map { it.variant?.lowercase() }
infos.filter { (_, pi) ->
pi.variant?.lowercase() in variantList
}.maxByOrNull { (_, pi) -> pi.variant?.replace(Regex("\\D"), "")?.toIntOrNull() ?: 0 }?.let {
return it
}
infos.firstOrNull()?.let { return it }
throw IllegalStateException(
globalContext.getString(R.string.error_no_available_enabled_plugin_variants_found, moduleLabel, variantList.joinToString("/"))
)
}
private fun queryPluginInfoBlocking(context: Context, serviceInfo: ServiceInfo, pluginPair: Pair<Lib, PluginLib>): PluginLibVariant {
// Bind service and call getInfo() synchronously (packaging-time only).
// zh-CN: 同步绑定服务并调用 getInfo() (仅打包阶段使用).
val latch = CountDownLatch(1)
var selectedVariant: String? = null
var error: Throwable? = null
val (lib, plugin) = pluginPair
val intent = Intent(plugin.action).apply {
component = ComponentName(serviceInfo.packageName, serviceInfo.name)
}
val conn = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, binder: IBinder) {
try {
selectedVariant = plugin.onServiceConnected(binder)
} catch (t: Throwable) {
error = t
} finally {
latch.countDown()
}
}
override fun onServiceDisconnected(name: ComponentName) {
// Ignored.
}
}
val bound = runCatching {
context.bindService(intent, conn, Context.BIND_AUTO_CREATE)
}.getOrDefault(false)
if (!bound) {
throw IllegalStateException(
globalContext.getString(R.string.error_failed_to_bind_plugin_service, lib.label)
)
}
try {
val ok = latch.await(15, TimeUnit.SECONDS)
if (!ok) {
throw IllegalStateException(
globalContext.getString(R.string.error_timeout_while_querying_plugin_info, lib.label)
)
}
error?.let { throw it }
selectedVariant ?: throw IllegalStateException(
globalContext.getString(R.string.error_plugin_returned_empty_info, lib.label)
)
return plugin.variants.firstOrNull {
it.variant.equals(selectedVariant, ignoreCase = true)
} ?: throw IllegalStateException(
globalContext.getString(R.string.error_plugin_returned_invalid_variant, lib.label, selectedVariant)
)
} finally {
runCatching { context.unbindService(conn) }
}
}
private fun extractAssetsFromPluginApkOrThrow(
serviceInfo: ServiceInfo,
pluginLibVariant: PluginLibVariant,
) {
val pm = globalContext.packageManager
val appInfo = getApplicationInfoCompat(pm, serviceInfo.packageName)
val apkPaths = buildList {
add(appInfo.sourceDir)
appInfo.splitSourceDirs?.forEach { add(it) }
}.distinct()
val (requiredPrefixes, optionalPrefixes) = pluginLibVariant.assetsToInclude
// Extract required prefixes.
// zh-CN: 解压必需前缀.
val missingRequired = mutableListOf<String>()
requiredPrefixes.forEach { prefix ->
val ok = extractAssetsByPrefixFromApks(
apkPaths = apkPaths,
assetPrefix = prefix,
)
if (!ok) missingRequired += prefix
}
if (missingRequired.isNotEmpty()) {
val detail = missingRequired.joinToString(", ")
throw IllegalStateException(
globalContext.getString(R.string.error_plugin_apk_does_not_contain_required_assets_for_variant, pluginLibVariant.variant, detail)
)
}
// Extract optional prefixes (best-effort).
// zh-CN: 解压可选前缀 (尽力而为).
optionalPrefixes.forEach { prefix ->
extractAssetsByPrefixFromApks(
apkPaths = apkPaths,
assetPrefix = prefix,
)
}
}
private fun extractAssetsByPrefixFromApks(
apkPaths: List<String>,
assetPrefix: String,
): Boolean {
var extractedAny = false
apkPaths.forEach { apkPath ->
runCatching {
ZipFile(apkPath).use { zip ->
val entries = zip.entries()
while (entries.hasMoreElements()) {
val entry: ZipEntry = entries.nextElement()
val name = entry.name
if (!name.startsWith(assetPrefix)) continue
if (entry.isDirectory) continue
val relative = name.removePrefix("assets/")
val outFile = File(buildPath, "assets/$relative").apply {
parentFile?.let { parent -> if (!parent.exists()) parent.mkdirs() }
}
zip.getInputStream(entry).use { input ->
FileOutputStream(outFile, false).use { output ->
input.copyTo(output, bufferSize = 16 * 1024)
output.fd.sync()
}
}
extractedAny = true
}
}
}.onFailure {
// Ignore and try next apk path.
// zh-CN: 忽略异常并尝试下一个 apk 路径.
it.printStackTrace()
}
}
return extractedAny
}
private fun extractLibrariesFromPluginApkOrThrow(
config: ProjectConfig,
requiredLibNames: List<String>,
serviceInfo: ServiceInfo,
potentialAbiAliasList: Map<String, String>,
) {
val pm = globalContext.packageManager
val appInfo = getApplicationInfoCompat(pm, serviceInfo.packageName)
val apkPaths = buildList {
add(appInfo.sourceDir)
appInfo.splitSourceDirs?.forEach { add(it) }
}.distinct()
val missingPairs = mutableListOf<Pair<String, String>>() // (abi, soName)
config.abis.forEach { abiCanonicalName ->
val abiCandidates = buildList {
add(abiCanonicalName)
potentialAbiAliasList[abiCanonicalName]?.let { add(it) }
}.distinct()
requiredLibNames.forEach { soName ->
val ok = extractFirstMatchedSoFromApks(
apkPaths = apkPaths,
abiCandidates = abiCandidates,
soName = soName,
abiDestName = abiCanonicalName,
)
if (!ok) missingPairs += abiCanonicalName to soName
}
}
if (missingPairs.isNotEmpty()) {
val detail = missingPairs.joinToString(", ") { (abi, so) -> "$abi/$so" }
throw IllegalStateException(
globalContext.getString(R.string.error_plugin_apk_does_not_contain_required_native_libraries, detail)
)
}
}
private fun findServicesByAction(pm: PackageManager, action: String): List<ServiceInfo> {
val resolveList = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
pm.queryIntentServices(Intent(action), PackageManager.ResolveInfoFlags.of(0))
} else {
@Suppress("DEPRECATION")
pm.queryIntentServices(Intent(action), 0)
}
return resolveList.mapNotNull { it.serviceInfo }
}
private fun extractFirstMatchedSoFromApks(
apkPaths: List<String>,
abiCandidates: List<String>,
soName: String,
abiDestName: String,
): Boolean {
apkPaths.forEach { apkPath ->
runCatching {
ZipFile(apkPath).use { zip ->
abiCandidates.forEach { abiInApk ->
val entryName = "lib/$abiInApk/$soName"
val entry = zip.getEntry(entryName) ?: return@forEach
val outFile = File(buildPath, "lib/$abiDestName/$soName").apply {
parentFile?.let { parent -> if (!parent.exists()) parent.mkdirs() }
}
zip.getInputStream(entry).use { input ->
FileOutputStream(outFile, false).use { output ->
input.copyTo(output, bufferSize = 16 * 1024)
output.fd.sync()
}
}
Log.i(TAG, "Extracted so from plugin apk: $apkPath!/$entryName -> ${outFile.path}")
return true
}
}
}.onFailure {
// Ignore and try next apk path.
// zh-CN: 忽略异常并尝试下一个 apk 路径.
it.printStackTrace()
}
}
return false
}
private fun getApplicationInfoCompat(pm: PackageManager, packageName: String): ApplicationInfo {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
pm.getApplicationInfo(packageName, ApplicationInfoFlags.of(0))
} else {
@Suppress("DEPRECATION")
pm.getApplicationInfo(packageName, 0)
}
}
private fun copyLibrariesByAbi(abiSrcName: String, abiDestName: String) {
// @Reference to LZX284 (https://github.com/LZX284) by SuperMonster003 on Dec 11, 2023.
@@ -466,12 +800,15 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File
}
@Suppress("SpellCheckingInspection")
enum class Libs(
enum class Lib(
@JvmField val label: String,
@JvmField val aliases: List<String> = emptyList(),
@JvmField val enumerable: Boolean = true,
internal val plugin: PluginLib? = null,
internal val libsToInclude: List<String> = emptyList(),
internal val assetFilesToInclude: List<String> = emptyList(),
// Select, then include into packaging APK. (选择后, 会被打包进 APK 中.)
internal val assetDirsToInclude: List<String> = emptyList(),
internal val assetDirsToExclude: List<String> = emptyList(),
) {
@@ -499,7 +836,7 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File
libsToInclude = listOf(
"libmlkit_google_ocr_pipeline.so",
),
assetDirsToExclude = listOf(
assetDirsToInclude = listOf(
"mlkit-google-ocr-models",
),
),
@@ -507,14 +844,51 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File
PADDLE_OCR(
label = "Paddle OCR",
aliases = listOf("paddle", "paddleocr", "paddle-ocr", "paddle_ocr"),
libsToInclude = listOf(
"libc++_shared.so",
"libpaddle_light_api_shared.so",
"libNative.so",
),
libsToInclude = emptyList(),
assetDirsToInclude = emptyList(),
assetDirsToExclude = listOf(
"models",
)
),
plugin = PluginLib(
action = "org.autojs.plugin.PADDLE_OCR",
onServiceConnected = { binder: IBinder ->
IOcrPlugin.Stub.asInterface(binder).info.variant
},
variants = listOf(
PluginLibVariant(
variant = "v3",
assetsToInclude = listOf(
"assets/labels/ppocr_keys_v1.txt",
"assets/models/ocr_v3_for_cpu/",
) to listOf(
"assets/models/ocr_v3_for_cpu(slim)/",
),
libsToInclude = listOf(
"libc++_shared.so",
"libpaddle_light_api_shared.so",
"libNative.so",
"libopencv_java4.so",
),
),
PluginLibVariant(
variant = "v5",
assetsToInclude = listOf(
"assets/labels/ppocr_keys_ocrv5.txt",
"assets/models/pp-ocrv5-arm/",
"assets/models/pp-ocrv5-arm-int8/",
"assets/models/pp-ocrv5-arm-opencl/",
"assets/models/pp-ocrv5-arm-opencl-int8/",
) to emptyList(),
libsToInclude = listOf(
"libc++_shared.so",
"libpaddle_light_api_shared.so",
"libNative.so",
"libopencv_java4.so",
"libopencl_probe.so",
),
),
),
),
),
RAPID_OCR(
@@ -524,7 +898,7 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File
"libRapidOcr.so",
"libonnxruntime.so",
),
assetDirsToExclude = listOf(
assetDirsToInclude = listOf(
"labels",
),
),
@@ -535,7 +909,7 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File
libsToInclude = listOf(
"libChineseConverter.so",
),
assetDirsToExclude = listOf(
assetDirsToInclude = listOf(
"openccdata",
),
),
@@ -557,7 +931,7 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File
libsToInclude = listOf(
"libbarhopper_v3.so",
),
assetDirsToExclude = listOf(
assetDirsToInclude = listOf(
"mlkit_barcode_models",
),
),
@@ -582,6 +956,11 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File
;
val isPlugin: Boolean
get() = plugin != null
fun toPluginPair(): Pair<Lib, PluginLib> = this to plugin!!
fun ensureLibFiles(moduleName: String = label) {
if (!isInrt) return
val nativeLibraryDir = File(globalContext.applicationInfo.nativeLibraryDir)
@@ -604,10 +983,23 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File
val defaultAssetDirsToExclude = listOf(
"doc", "docs", "editor", "indices", "js-beautify", "sample", "stored-locales",
) + Libs.entries.flatMap { it.assetDirsToExclude }
) + entries.flatMap { it.assetDirsToInclude } + entries.flatMap { it.assetDirsToExclude }
}
}
}
data class PluginLib(
val action: String,
val onServiceConnected: (IBinder) -> String,
val variants: List<PluginLibVariant>,
)
data class PluginLibVariant(
val variant: String? = null,
/** <Required List> to <Optional List>. */
val assetsToInclude: Pair<List<String>, List<String>> = emptyList<String>() to emptyList(),
val libsToInclude: List<String> = emptyList(),
)
}

View File

@@ -7,7 +7,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.autojs.autojs.core.plugin.ocr.PaddleOcrPluginHost
import org.autojs.autojs6.R
import org.autojs.plugin.paddle.ocr.PluginInfo
import org.autojs.plugin.paddle.ocr.api.PluginInfo
import java.io.File
/**

View File

@@ -21,10 +21,10 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import org.autojs.autojs.core.plugin.center.PluginEnableStore
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 org.autojs.plugin.paddle.ocr.api.IOcrPlugin
import org.autojs.plugin.paddle.ocr.api.OcrOptions
import org.autojs.plugin.paddle.ocr.api.OcrResult
import org.autojs.plugin.paddle.ocr.api.PluginInfo
import java.io.File
import java.io.FileOutputStream

View File

@@ -41,7 +41,7 @@ class OcrMLKit(private val scriptRuntime: ScriptRuntime) : Augmentable(scriptRun
// ! Reserved param `options`.
// ! zh-CN: 预留参数 `options`.
fun recognizeTextImpl(scriptRuntime: ScriptRuntime, image: ImageWrapper, options: NativeObject): List<String> {
ApkBuilder.Libs.MLKIT_OCR.ensureLibFiles(OcrMode.MLKIT.value)
ApkBuilder.Lib.MLKIT_OCR.ensureLibFiles(OcrMode.MLKIT.value)
return scriptRuntime.ocrMLKit.recognizeText(image)
}
@@ -49,7 +49,7 @@ class OcrMLKit(private val scriptRuntime: ScriptRuntime) : Augmentable(scriptRun
// ! Reserved param `options`.
// ! zh-CN: 预留参数 `options`.
fun detectImpl(scriptRuntime: ScriptRuntime, image: ImageWrapper, options: NativeObject): List<OcrResult> {
ApkBuilder.Libs.MLKIT_OCR.ensureLibFiles(OcrMode.MLKIT.value)
ApkBuilder.Lib.MLKIT_OCR.ensureLibFiles(OcrMode.MLKIT.value)
return scriptRuntime.ocrMLKit.detect(image)
}

View File

@@ -1,6 +1,7 @@
package org.autojs.autojs.runtime.api.augment.ocr
import kotlinx.coroutines.runBlocking
import org.autojs.autojs.AbstractAutoJs.Companion.isInrt
import org.autojs.autojs.annotation.RhinoRuntimeFunctionInterface
import org.autojs.autojs.apkbuilder.ApkBuilder
import org.autojs.autojs.core.image.ImageWrapper
@@ -15,7 +16,7 @@ import org.autojs.autojs.runtime.exception.WrappedIllegalArgumentException
import org.autojs.autojs.util.RhinoUtils.coerceBoolean
import org.autojs.autojs.util.RhinoUtils.coerceIntNumber
import org.autojs.autojs6.R
import org.autojs.plugin.paddle.ocr.OcrOptions
import org.autojs.plugin.paddle.ocr.api.OcrOptions
import org.mozilla.javascript.NativeArray
import org.mozilla.javascript.NativeObject
@@ -50,33 +51,47 @@ class OcrPaddle(private val scriptRuntime: ScriptRuntime) : Augmentable(scriptRu
}
fun recognizeTextImpl(scriptRuntime: ScriptRuntime, image: ImageWrapper, options: NativeObject): List<String> {
ApkBuilder.Libs.PADDLE_OCR.ensureLibFiles(OcrMode.PADDLE.value)
ApkBuilder.Lib.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(globalContext.getString(R.string.error_no_paddle_ocr_plugins_available))
PaddleOcrPluginHost.recognizeText(globalContext, target, image.bitmap, ocrOptions)
return if (!isInrt) {
runBlocking(scriptRuntime.coroutineContext) {
val target = PaddleOcrPluginHost.select(globalContext)
?: throw WrappedIllegalArgumentException(globalContext.getString(R.string.error_no_paddle_ocr_plugins_available))
PaddleOcrPluginHost.recognizeText(globalContext, target, image.bitmap, ocrOptions)
}
} else {
// Use embedded engine in packaged (INRT) app.
// zh-CN: 打包应用 (INRT) 使用内置引擎 (本地推理), 不依赖插件.
PaddleOcrEmbeddedEngine.recognizeText(globalContext, image.bitmap, ocrOptions)
}
}
fun detectImpl(scriptRuntime: ScriptRuntime, image: ImageWrapper, options: NativeObject): List<OcrResult> {
ApkBuilder.Libs.PADDLE_OCR.ensureLibFiles(OcrMode.PADDLE.value)
ApkBuilder.Lib.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(globalContext.getString(R.string.error_no_paddle_ocr_plugins_available))
PaddleOcrPluginHost.detect(globalContext, target, image.bitmap, ocrOptions)
}.map { OcrResult(it.text, it.confidence, it.bounds) }
return if (!isInrt) {
runBlocking(scriptRuntime.coroutineContext) {
val target = PaddleOcrPluginHost.select(globalContext)
?: throw WrappedIllegalArgumentException(globalContext.getString(R.string.error_no_paddle_ocr_plugins_available))
PaddleOcrPluginHost.detect(globalContext, target, image.bitmap, ocrOptions)
}.map { OcrResult(it.text, it.confidence, it.bounds) }
} else {
// Use embedded engine in packaged (INRT) app.
// zh-CN: 打包应用 (INRT) 使用内置引擎 (本地推理), 不依赖插件.
PaddleOcrEmbeddedEngine.detect(globalContext, image.bitmap, ocrOptions).map {
OcrResult(it.text, it.confidence, it.bounds)
}
}
}
private fun getOcrOptions(options: NativeObject): OcrOptions {

View File

@@ -40,7 +40,7 @@ class OcrRapid(private val scriptRuntime: ScriptRuntime) : Augmentable(scriptRun
// ! Reserved param `options`.
// ! zh-CN: 预留参数 `options`.
fun recognizeTextImpl(scriptRuntime: ScriptRuntime, image: ImageWrapper, @Suppress("UNUSED_PARAMETER") options: NativeObject): List<String> {
ApkBuilder.Libs.RAPID_OCR.ensureLibFiles(OcrMode.RAPID.value)
ApkBuilder.Lib.RAPID_OCR.ensureLibFiles(OcrMode.RAPID.value)
return scriptRuntime.ocrRapid.recognizeText(image)
}
@@ -48,7 +48,7 @@ class OcrRapid(private val scriptRuntime: ScriptRuntime) : Augmentable(scriptRun
// ! Reserved param `options`.
// ! zh-CN: 预留参数 `options`.
fun detectImpl(scriptRuntime: ScriptRuntime, image: ImageWrapper, @Suppress("UNUSED_PARAMETER") options: NativeObject): List<OcrResult> {
ApkBuilder.Libs.RAPID_OCR.ensureLibFiles(OcrMode.RAPID.value)
ApkBuilder.Lib.RAPID_OCR.ensureLibFiles(OcrMode.RAPID.value)
return scriptRuntime.ocrRapid.detect(image)
}

View File

@@ -0,0 +1,82 @@
package org.autojs.autojs.runtime.api.augment.ocr
import android.content.Context
import android.content.res.AssetManager
import android.graphics.Bitmap
import com.baidu.paddle.lite.ocr.PaddleOcrEngine
import com.baidu.paddle.lite.ocr.VariantSpec
import org.autojs.autojs6.R
import org.autojs.plugin.paddle.ocr.api.OcrOptions
import org.autojs.plugin.paddle.ocr.api.OcrResult
internal object PaddleOcrEmbeddedEngine {
@Volatile
private var engine: PaddleOcrEngine? = null
@Volatile
private var resolvedVariant: VariantSpec? = null
private fun getEngine(context: Context): PaddleOcrEngine {
// Lazily initialize embedded engine.
// zh-CN: 懒初始化内置引擎.
val cached = engine
if (cached != null) return cached
return synchronized(this) {
engine ?: run {
val appCtx = context.applicationContext
val variant = resolveVariantOrThrow(appCtx, appCtx.assets).also { resolvedVariant = it }
PaddleOcrEngine(
appContext = appCtx,
variant = variant,
).also { engine = it }
}
}
}
private fun resolveVariantOrThrow(context: Context, assets: AssetManager): VariantSpec {
// Mirror the packaging-time selection rule:
// - If neither exists -> throw.
// - If both exist -> v5 first.
// - If only one exists -> choose it.
// zh-CN:
// 复刻打包阶段的选择规则:
// - 两者都不存在 -> 抛异常.
// - 两者都存在 -> v5 优先.
// - 仅存在一个 -> 选择唯一项.
val hasV5 = assets.hasAsset("labels/ppocr_keys_ocrv5.txt") &&
assets.hasAsset("models/pp-ocrv5-arm/PP-OCRv5_mobile_det.nb")
val hasV3 = assets.hasAsset("labels/ppocr_keys_v1.txt") &&
assets.hasAsset("models/ocr_v3_for_cpu/det_opt.nb")
return when {
hasV5 -> VariantSpec.v5()
hasV3 -> VariantSpec.v3()
else -> throw IllegalStateException(
context.getString(R.string.error_no_embedded_paddle_ocr_assets_found)
)
}
}
private fun AssetManager.hasAsset(path: String): Boolean {
return try {
open(path).use { true }
} catch (_: Throwable) {
false
}
}
fun recognizeText(context: Context, bitmap: Bitmap, options: OcrOptions): List<String> {
// Run OCR with embedded engine.
// zh-CN: 使用内置引擎执行 OCR.
return getEngine(context).recognizeText(bitmap, options)
}
fun detect(context: Context, bitmap: Bitmap, options: OcrOptions): List<OcrResult> {
// Run OCR detection with embedded engine.
// zh-CN: 使用内置引擎执行 OCR 检测.
return getEngine(context).detect(bitmap, options)
}
}

View File

@@ -240,7 +240,7 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa
private KeyStoreViewModel mKeyStoreViewModel;
static {
for (ApkBuilder.Libs entry : ApkBuilder.Libs.getEntries()) {
for (ApkBuilder.Lib entry : ApkBuilder.Lib.getEntries()) {
if (entry.enumerable) {
SUPPORTED_LIBS.add(entry.label);
LIB_ALIASES.put(entry.label, entry.aliases);

View File

@@ -1181,5 +1181,15 @@
<string name="text_sort_by_name">فرز حسب الاسم</string>
<string name="text_sort_by_last_update_time">فرز حسب آخر تحديث</string>
<string name="text_sort_by_package_size">فرز حسب حجم الحزمة</string>
<string name="error_missing_required_plugin_for_module_label">المكوّن الإضافي المطلوب لـ \"%1$s\" مفقود. يُرجى تثبيت المكوّن الإضافي ثم إعادة المحاولة.</string>
<string name="error_no_enabled_plugin_for_module_label">لا يوجد مكوّن إضافي مفعّل لـ \"%1$s\". يُرجى تفعيل مكوّن إضافي ثم إعادة المحاولة.</string>
<string name="error_no_available_enabled_plugin_variants_found">لم يتم العثور على أي متغيرات مفعّلة ومتاحة للمكوّن الإضافي %1$s (%2$s).</string>
<string name="error_plugin_apk_does_not_contain_required_assets_for_variant">ملف APK للمكوّن الإضافي لا يحتوي على الموارد المطلوبة لـ variant=\"%1$s\": %2$s.</string>
<string name="error_plugin_apk_does_not_contain_required_native_libraries">ملف APK للمكوّن الإضافي لا يحتوي على مكتبات native المطلوبة: %1$s.</string>
<string name="error_failed_to_bind_plugin_service">تعذّر ربط خدمة المكوّن الإضافي %1$s.</string>
<string name="error_timeout_while_querying_plugin_info">انتهت مهلة الاستعلام عن معلومات المكوّن الإضافي %1$s.</string>
<string name="error_plugin_returned_empty_info">أعاد المكوّن الإضافي %1$s معلومات فارغة.</string>
<string name="error_plugin_returned_invalid_variant">أعاد المكوّن الإضافي %1$s variant غير صالح: %2$s.</string>
<string name="error_no_embedded_paddle_ocr_assets_found">لم يتم العثور على موارد Paddle OCR المضمّنة. يُرجى إعادة الحزم مع تفعيل Paddle OCR.</string>
</resources>

View File

@@ -1176,5 +1176,15 @@
<string name="text_sort_by_name">Sort by name</string>
<string name="text_sort_by_last_update_time">Sort by last update time</string>
<string name="text_sort_by_package_size">Sort by package size</string>
<string name="error_missing_required_plugin_for_module_label">Missing required plugin for \"%1$s\". Please install the plugin and try again.</string>
<string name="error_no_enabled_plugin_for_module_label">No enabled plugin for \"%1$s\". Please enable a plugin and try again.</string>
<string name="error_no_available_enabled_plugin_variants_found">No available enabled %1$s plugin variants found (%2$s).</string>
<string name="error_plugin_apk_does_not_contain_required_assets_for_variant">Plugin APK does not contain required assets for variant=\"%1$s\": %2$s.</string>
<string name="error_plugin_apk_does_not_contain_required_native_libraries">Plugin APK does not contain required native libraries: %1$s.</string>
<string name="error_failed_to_bind_plugin_service">Failed to bind %1$s plugin service.</string>
<string name="error_timeout_while_querying_plugin_info">Timeout while querying %1$s plugin info.</string>
<string name="error_plugin_returned_empty_info">%1$s plugin returned empty info.</string>
<string name="error_plugin_returned_invalid_variant">%1$s plugin returned invalid variant: %2$s.</string>
<string name="error_no_embedded_paddle_ocr_assets_found">No embedded Paddle OCR assets found. Please re-package with Paddle OCR enabled.</string>
</resources>

View File

@@ -1179,5 +1179,15 @@
<string name="text_sort_by_name">Ordenar por nombre</string>
<string name="text_sort_by_last_update_time">Ordenar por última actualización</string>
<string name="text_sort_by_package_size">Ordenar por tamaño del paquete</string>
<string name="error_missing_required_plugin_for_module_label">Falta el plugin requerido para \"%1$s\". Instala el plugin e inténtalo de nuevo.</string>
<string name="error_no_enabled_plugin_for_module_label">No hay ningún plugin habilitado para \"%1$s\". Habilita un plugin e inténtalo de nuevo.</string>
<string name="error_no_available_enabled_plugin_variants_found">No se encontraron variantes de plugin %1$s habilitadas y disponibles (%2$s).</string>
<string name="error_plugin_apk_does_not_contain_required_assets_for_variant">El APK del plugin no contiene los recursos necesarios para variant=\"%1$s\": %2$s.</string>
<string name="error_plugin_apk_does_not_contain_required_native_libraries">El APK del plugin no contiene las bibliotecas nativas requeridas: %1$s.</string>
<string name="error_failed_to_bind_plugin_service">No se pudo vincular el servicio del plugin %1$s.</string>
<string name="error_timeout_while_querying_plugin_info">Tiempo de espera agotado al consultar la información del plugin %1$s.</string>
<string name="error_plugin_returned_empty_info">El plugin %1$s devolvió información vacía.</string>
<string name="error_plugin_returned_invalid_variant">El plugin %1$s devolvió una variante no válida: %2$s.</string>
<string name="error_no_embedded_paddle_ocr_assets_found">No se encontraron recursos integrados de Paddle OCR. Vuelve a empaquetar con Paddle OCR habilitado.</string>
</resources>

View File

@@ -1179,5 +1179,15 @@
<string name="text_sort_by_name">Trier par nom</string>
<string name="text_sort_by_last_update_time">Trier par dernière mise à jour</string>
<string name="text_sort_by_package_size">Trier par taille du paquet</string>
<string name="error_missing_required_plugin_for_module_label">Plugin requis manquant pour \"%1$s\". Veuillez installer le plugin et réessayer.</string>
<string name="error_no_enabled_plugin_for_module_label">Aucun plugin activé pour \"%1$s\". Veuillez activer un plugin et réessayer.</string>
<string name="error_no_available_enabled_plugin_variants_found">Aucune variante de plugin %1$s activée et disponible n\'a été trouvée (%2$s).</string>
<string name="error_plugin_apk_does_not_contain_required_assets_for_variant">L\'APK du plugin ne contient pas les ressources requises pour variant=\"%1$s\": %2$s.</string>
<string name="error_plugin_apk_does_not_contain_required_native_libraries">L\'APK du plugin ne contient pas les bibliothèques natives requises: %1$s.</string>
<string name="error_failed_to_bind_plugin_service">Échec de la liaison du service du plugin %1$s.</string>
<string name="error_timeout_while_querying_plugin_info">Délai d\'attente dépassé lors de la requête des informations du plugin %1$s.</string>
<string name="error_plugin_returned_empty_info">Le plugin %1$s a renvoyé des informations vides.</string>
<string name="error_plugin_returned_invalid_variant">Le plugin %1$s a renvoyé une variante invalide: %2$s.</string>
<string name="error_no_embedded_paddle_ocr_assets_found">Aucune ressource Paddle OCR intégrée n\'a été trouvée. Veuillez reconditionner avec Paddle OCR activé.</string>
</resources>

View File

@@ -1180,5 +1180,15 @@
<string name="text_sort_by_name">名前で並べ替え</string>
<string name="text_sort_by_last_update_time">最終更新日で並べ替え</string>
<string name="text_sort_by_package_size">パッケージサイズで並べ替え</string>
<string name="error_missing_required_plugin_for_module_label">\"%1$s\" に必要なプラグインが見つかりません. プラグインをインストールしてから再試行してください.</string>
<string name="error_no_enabled_plugin_for_module_label">\"%1$s\" に有効化されたプラグインがありません. プラグインを有効化してから再試行してください.</string>
<string name="error_no_available_enabled_plugin_variants_found">利用可能で有効化された %1$s プラグインの variant が見つかりません (%2$s).</string>
<string name="error_plugin_apk_does_not_contain_required_assets_for_variant">プラグイン APK に variant=\"%1$s\" に必要な assets が含まれていません: %2$s.</string>
<string name="error_plugin_apk_does_not_contain_required_native_libraries">プラグイン APK に必要な native ライブラリが含まれていません: %1$s.</string>
<string name="error_failed_to_bind_plugin_service">%1$s プラグインサービスのバインドに失敗しました.</string>
<string name="error_timeout_while_querying_plugin_info">%1$s プラグイン情報の取得がタイムアウトしました.</string>
<string name="error_plugin_returned_empty_info">%1$s プラグインが空の info を返しました.</string>
<string name="error_plugin_returned_invalid_variant">%1$s プラグインが無効な variant を返しました: %2$s.</string>
<string name="error_no_embedded_paddle_ocr_assets_found">埋め込みの Paddle OCR assets が見つかりません. Paddle OCR を有効にして再パッケージしてください.</string>
</resources>

View File

@@ -1181,5 +1181,15 @@
<string name="text_sort_by_name">이름순 정렬</string>
<string name="text_sort_by_last_update_time">최근 업데이트순 정렬</string>
<string name="text_sort_by_package_size">패키지 크기순 정렬</string>
<string name="error_missing_required_plugin_for_module_label">\"%1$s\"에 필요한 플러그인이 없습니다. 플러그인을 설치한 후 다시 시도해 주세요.</string>
<string name="error_no_enabled_plugin_for_module_label">\"%1$s\"에 활성화된 플러그인이 없습니다. 플러그인을 활성화한 후 다시 시도해 주세요.</string>
<string name="error_no_available_enabled_plugin_variants_found">사용 가능하며 활성화된 %1$s 플러그인 variant를 찾을 수 없습니다 (%2$s).</string>
<string name="error_plugin_apk_does_not_contain_required_assets_for_variant">플러그인 APK에 variant=\"%1$s\"에 필요한 assets가 포함되어 있지 않습니다: %2$s.</string>
<string name="error_plugin_apk_does_not_contain_required_native_libraries">플러그인 APK에 필요한 native 라이브러리가 포함되어 있지 않습니다: %1$s.</string>
<string name="error_failed_to_bind_plugin_service">%1$s 플러그인 서비스 바인딩에 실패했습니다.</string>
<string name="error_timeout_while_querying_plugin_info">%1$s 플러그인 정보 조회가 시간 초과되었습니다.</string>
<string name="error_plugin_returned_empty_info">%1$s 플러그인이 빈 info를 반환했습니다.</string>
<string name="error_plugin_returned_invalid_variant">%1$s 플러그인이 잘못된 variant를 반환했습니다: %2$s.</string>
<string name="error_no_embedded_paddle_ocr_assets_found">내장된 Paddle OCR assets를 찾을 수 없습니다. Paddle OCR을 활성화하여 다시 패키징해 주세요.</string>
</resources>

View File

@@ -1179,5 +1179,15 @@
<string name="text_sort_by_name">Сортировать по имени</string>
<string name="text_sort_by_last_update_time">Сортировать по времени последнего обновления</string>
<string name="text_sort_by_package_size">Сортировать по размеру пакета</string>
<string name="error_missing_required_plugin_for_module_label">Отсутствует требуемый плагин для \"%1$s\". Установите плагин и повторите попытку.</string>
<string name="error_no_enabled_plugin_for_module_label">Для \"%1$s\" нет включённого плагина. Включите плагин и повторите попытку.</string>
<string name="error_no_available_enabled_plugin_variants_found">Не найдено доступных и включённых вариантов плагина %1$s (%2$s).</string>
<string name="error_plugin_apk_does_not_contain_required_assets_for_variant">APK плагина не содержит требуемые ресурсы для variant=\"%1$s\": %2$s.</string>
<string name="error_plugin_apk_does_not_contain_required_native_libraries">APK плагина не содержит требуемые native-библиотеки: %1$s.</string>
<string name="error_failed_to_bind_plugin_service">Не удалось привязаться к службе плагина %1$s.</string>
<string name="error_timeout_while_querying_plugin_info">Тайм-аут при запросе информации о плагине %1$s.</string>
<string name="error_plugin_returned_empty_info">Плагин %1$s вернул пустую информацию.</string>
<string name="error_plugin_returned_invalid_variant">Плагин %1$s вернул недопустимый variant: %2$s.</string>
<string name="error_no_embedded_paddle_ocr_assets_found">Не найдены встроенные ресурсы Paddle OCR. Перепакуйте приложение с включённым Paddle OCR.</string>
</resources>

View File

@@ -71,7 +71,7 @@
<string name="description_all_files_access" tools:ignore="TypographyEllipsis">\"管理所有文件\" (或 \"所有文件訪問\") 權限允許 AutoJs6 在共享存儲空間中通過常規文件路徑直接 [ 創建 / 讀取 / 修改 / 刪除 ] 文件, 使腳本可以訪問 \"內部存儲 (Internal Storage)\", 使文件管理器可以正常顯示及管理文件.\n\n在 Android 11+ 設備上, 這是實現全盤文件讀寫能力的主要方式.</string>
<string name="description_app_language_preference">設置選項用於修改 AutoJs6 應用的文本內容顯示語言, 同時包括腳本運行產生的錯誤消息等.\n\n注: 部分內容可能需要重啓應用才能完成語言切換.</string>
<string name="description_auto_night_mode">自動夜間模式開啓後, AutoJs6 將根據系統設置自動切換夜間模式.\n\注: 自動夜間模式開關與夜間模式開關互相關聯且互相影響.</string>
<string name="description_background_popup_permission" tools:ignore="TypographyEllipsis">\"後台彈出界面\" (也稱 \"後台啓動界面\" 或 \"後台彈出頁面\") 權限允許 AutoJs6 在應用位於後台或未顯示界面時, 仍可主動啓動界面(Activity) 或打開特定設置頁, 適合 [ 定時任務觸發時打開 UI / 在鎖屏或待機後恢復交互 / 由通知或快捷方式喚起腳本配置頁 ] 等場景.\n\n注: 在 Xiaomi (MIUI/HyperOS), Vivo (OriginOS/Funtouch OS) 等系統上, 該權限可能默認關閉. 未授予時, 腳本嘗試從後台打開頁面可能會被系統攔截, 表現為 [ 無響應 / 僅後台執行但不顯示界面 / 跳轉失敗 ].\n\n授予後仍可能受到系統其他策略影響, 如 [ 電池優化 / 自啓動限制 / 後台凍結 / 應用待機 ] 等, 可結合相關權限或白名單設置一起調整.</string>
<string name="description_background_popup_permission" tools:ignore="TypographyEllipsis">\"後台彈出界面\" (也稱 \"後台啓動界面\" 或 \"後台彈出頁面\") 權限允許 AutoJs6 在應用位於後台或未顯示界面時, 仍可主動啓動界面 (Activity) 或打開特定設置頁, 適合 [ 定時任務觸發時打開 UI / 在鎖屏或待機後恢復交互 / 由通知或快捷方式喚起腳本配置頁 ] 等場景.\n\n注: 在 Xiaomi (MIUI/HyperOS), Vivo (OriginOS/Funtouch OS) 等系統上, 該權限可能默認關閉. 未授予時, 腳本嘗試從後台打開頁面可能會被系統攔截, 表現為 [ 無響應 / 僅後台執行但不顯示界面 / 跳轉失敗 ].\n\n授予後仍可能受到系統其他策略影響, 如 [ 電池優化 / 自啓動限制 / 後台凍結 / 應用待機 ] 等, 可結合相關權限或白名單設置一起調整.</string>
<string name="description_change_working_dir_preference">更改包含腳本的文件夾路徑</string>
<string name="description_check_for_updates_preference">AutoJs6 從 GitHub 獲取並下載更新.</string>
<string name="description_client_mode">客户端模式用於讓 AutoJs6 主動連接到遠端服務端, 以便進行 [ 腳本傳輸 / 打印日誌 / 遠程控制 ] 等.\n\n通常需要設備與服務端處於同一局域網或可互相訪問的網絡環境.</string>
@@ -1177,5 +1177,15 @@
<string name="text_sort_by_name">按名稱排序</string>
<string name="text_sort_by_last_update_time">按最近更新排序</string>
<string name="text_sort_by_package_size">按安裝包大小排序</string>
<string name="error_missing_required_plugin_for_module_label">缺少 \"%1$s\" 所需的插件. 請先安裝插件, 然後重試.</string>
<string name="error_no_enabled_plugin_for_module_label">\"%1$s\" 沒有已啓用的插件. 請先啓用插件, 然後重試.</string>
<string name="error_no_available_enabled_plugin_variants_found">未找到可用且已啓用的 %1$s 插件變體 (%2$s).</string>
<string name="error_plugin_apk_does_not_contain_required_assets_for_variant">插件 APK 不包含變體 \"%1$s\" 所需的資源文件: %2$s.</string>
<string name="error_plugin_apk_does_not_contain_required_native_libraries">插件 APK 不包含所需的 native 庫文件: %1$s.</string>
<string name="error_failed_to_bind_plugin_service">綁定 %1$s 插件服務失敗.</string>
<string name="error_timeout_while_querying_plugin_info">查詢 %1$s 插件信息超時.</string>
<string name="error_plugin_returned_empty_info">%1$s 插件返回的 info 為空.</string>
<string name="error_plugin_returned_invalid_variant">%1$s 插件返回的 variant 無效: %2$s.</string>
<string name="error_no_embedded_paddle_ocr_assets_found">未找到內置 Paddle OCR 資源, 請在打包時勾選並注入 Paddle OCR 後重試.</string>
</resources>

View File

@@ -71,7 +71,7 @@
<string name="description_all_files_access" tools:ignore="TypographyEllipsis">\"管理所有檔案\" (或 \"所有檔案訪問\") 許可權允許 AutoJs6 在共享儲存空間中透過常規檔案路徑直接 [ 建立 / 讀取 / 修改 / 刪除 ] 檔案, 使指令碼可以訪問 \"內部儲存 (Internal Storage)\", 使檔案管理器可以正常顯示及管理檔案.\n\n在 Android 11+ 裝置上, 這是實現全盤檔案讀寫能力的主要方式.</string>
<string name="description_app_language_preference">設定選項用於修改 AutoJs6 應用的文字內容顯示語言, 同時包括指令碼執行產生的錯誤訊息等.\n\n注: 部分內容可能需要重啟應用才能完成語言切換.</string>
<string name="description_auto_night_mode">自動夜間模式開啟後, AutoJs6 將根據系統設定自動切換夜間模式.\n\注: 自動夜間模式開關與夜間模式開關互相關聯且互相影響.</string>
<string name="description_background_popup_permission" tools:ignore="TypographyEllipsis">\"後臺彈出介面\" (也稱 \"後臺啟動介面\" 或 \"後臺彈出頁面\") 許可權允許 AutoJs6 在應用位於後臺或未顯示介面時, 仍可主動啟動介面(Activity) 或開啟特定設定頁, 適合 [ 定時任務觸發時開啟 UI / 在鎖屏或待機後恢復互動 / 由通知或快捷方式喚起指令碼配置頁 ] 等場景.\n\n注: 在 Xiaomi (MIUI/HyperOS), Vivo (OriginOS/Funtouch OS) 等系統上, 該許可權可能預設關閉. 未授予時, 指令碼嘗試從後臺開啟頁面可能會被系統攔截, 表現為 [ 無響應 / 僅後臺執行但不顯示介面 / 跳轉失敗 ].\n\n授予後仍可能受到系統其他策略影響, 如 [ 電池最佳化 / 自啟動限制 / 後臺凍結 / 應用待機 ] 等, 可結合相關許可權或白名單設定一起調整.</string>
<string name="description_background_popup_permission" tools:ignore="TypographyEllipsis">\"後臺彈出介面\" (也稱 \"後臺啟動介面\" 或 \"後臺彈出頁面\") 許可權允許 AutoJs6 在應用位於後臺或未顯示介面時, 仍可主動啟動介面 (Activity) 或開啟特定設定頁, 適合 [ 定時任務觸發時開啟 UI / 在鎖屏或待機後恢復互動 / 由通知或快捷方式喚起指令碼配置頁 ] 等場景.\n\n注: 在 Xiaomi (MIUI/HyperOS), Vivo (OriginOS/Funtouch OS) 等系統上, 該許可權可能預設關閉. 未授予時, 指令碼嘗試從後臺開啟頁面可能會被系統攔截, 表現為 [ 無響應 / 僅後臺執行但不顯示介面 / 跳轉失敗 ].\n\n授予後仍可能受到系統其他策略影響, 如 [ 電池最佳化 / 自啟動限制 / 後臺凍結 / 應用待機 ] 等, 可結合相關許可權或白名單設定一起調整.</string>
<string name="description_change_working_dir_preference">更改包含指令碼的資料夾路徑</string>
<string name="description_check_for_updates_preference">AutoJs6 從 GitHub 獲取並下載更新.</string>
<string name="description_client_mode">客戶端模式用於讓 AutoJs6 主動連線到遠端服務端, 以便進行 [ 指令碼傳輸 / 列印日誌 / 遠端控制 ] 等.\n\n通常需要裝置與服務端處於同一區域網或可互相訪問的網路環境.</string>
@@ -1177,5 +1177,15 @@
<string name="text_sort_by_name">按名稱排序</string>
<string name="text_sort_by_last_update_time">按最近更新排序</string>
<string name="text_sort_by_package_size">按安裝包大小排序</string>
<string name="error_missing_required_plugin_for_module_label">缺少 \"%1$s\" 所需的外掛. 請先安裝外掛, 然後重試.</string>
<string name="error_no_enabled_plugin_for_module_label">\"%1$s\" 沒有已啟用的外掛. 請先啟用外掛, 然後重試.</string>
<string name="error_no_available_enabled_plugin_variants_found">未找到可用且已啟用的 %1$s 外掛變體 (%2$s).</string>
<string name="error_plugin_apk_does_not_contain_required_assets_for_variant">外掛 APK 不包含變體 \"%1$s\" 所需的資原始檔: %2$s.</string>
<string name="error_plugin_apk_does_not_contain_required_native_libraries">外掛 APK 不包含所需的 native 庫檔案: %1$s.</string>
<string name="error_failed_to_bind_plugin_service">繫結 %1$s 外掛服務失敗.</string>
<string name="error_timeout_while_querying_plugin_info">查詢 %1$s 外掛資訊超時.</string>
<string name="error_plugin_returned_empty_info">%1$s 外掛返回的 info 為空.</string>
<string name="error_plugin_returned_invalid_variant">%1$s 外掛返回的 variant 無效: %2$s.</string>
<string name="error_no_embedded_paddle_ocr_assets_found">未找到內建 Paddle OCR 資源, 請在打包時勾選並注入 Paddle OCR 後重試.</string>
</resources>

View File

@@ -70,7 +70,7 @@
<string name="description_all_files_access" tools:ignore="TypographyEllipsis">\"管理所有文件\" (或 \"所有文件访问\") 权限允许 AutoJs6 在共享存储空间中通过常规文件路径直接 [ 创建 / 读取 / 修改 / 删除 ] 文件, 使脚本可以访问 \"内部存储 (Internal Storage)\", 使文件管理器可以正常显示及管理文件.\n\n在 Android 11+ 设备上, 这是实现全盘文件读写能力的主要方式.</string>
<string name="description_app_language_preference">设置选项用于修改 AutoJs6 应用的文本内容显示语言, 同时包括脚本运行产生的错误消息等.\n\n注: 部分内容可能需要重启应用才能完成语言切换.</string>
<string name="description_auto_night_mode">自动夜间模式开启后, AutoJs6 将根据系统设置自动切换夜间模式.\n\注: 自动夜间模式开关与夜间模式开关互相关联且互相影响.</string>
<string name="description_background_popup_permission" tools:ignore="TypographyEllipsis">\"后台弹出界面\" (也称 \"后台启动界面\" 或 \"后台弹出页面\") 权限允许 AutoJs6 在应用位于后台或未显示界面时, 仍可主动启动界面(Activity) 或打开特定设置页, 适合 [ 定时任务触发时打开 UI / 在锁屏或待机后恢复交互 / 由通知或快捷方式唤起脚本配置页 ] 等场景.\n\n注: 在 Xiaomi (MIUI/HyperOS), Vivo (OriginOS/Funtouch OS) 等系统上, 该权限可能默认关闭. 未授予时, 脚本尝试从后台打开页面可能会被系统拦截, 表现为 [ 无响应 / 仅后台执行但不显示界面 / 跳转失败 ].\n\n授予后仍可能受到系统其他策略影响, 如 [ 电池优化 / 自启动限制 / 后台冻结 / 应用待机 ] 等, 可结合相关权限或白名单设置一起调整.</string>
<string name="description_background_popup_permission" tools:ignore="TypographyEllipsis">\"后台弹出界面\" (也称 \"后台启动界面\" 或 \"后台弹出页面\") 权限允许 AutoJs6 在应用位于后台或未显示界面时, 仍可主动启动界面 (Activity) 或打开特定设置页, 适合 [ 定时任务触发时打开 UI / 在锁屏或待机后恢复交互 / 由通知或快捷方式唤起脚本配置页 ] 等场景.\n\n注: 在 Xiaomi (MIUI/HyperOS), Vivo (OriginOS/Funtouch OS) 等系统上, 该权限可能默认关闭. 未授予时, 脚本尝试从后台打开页面可能会被系统拦截, 表现为 [ 无响应 / 仅后台执行但不显示界面 / 跳转失败 ].\n\n授予后仍可能受到系统其他策略影响, 如 [ 电池优化 / 自启动限制 / 后台冻结 / 应用待机 ] 等, 可结合相关权限或白名单设置一起调整.</string>
<string name="description_change_working_dir_preference">更改包含脚本的文件夹路径</string>
<string name="description_check_for_updates_preference">AutoJs6 从 GitHub 获取并下载更新.</string>
<string name="description_client_mode">客户端模式用于让 AutoJs6 主动连接到远端服务端, 以便进行 [ 脚本传输 / 打印日志 / 远程控制 ] 等.\n\n通常需要设备与服务端处于同一局域网或可互相访问的网络环境.</string>
@@ -1177,5 +1177,15 @@
<string name="text_sort_by_name">按名称排序</string>
<string name="text_sort_by_last_update_time">按最近更新排序</string>
<string name="text_sort_by_package_size">按安装包大小排序</string>
<string name="error_missing_required_plugin_for_module_label">缺少 \"%1$s\" 所需的插件. 请先安装插件, 然后重试.</string>
<string name="error_no_enabled_plugin_for_module_label">\"%1$s\" 没有已启用的插件. 请先启用插件, 然后重试.</string>
<string name="error_no_available_enabled_plugin_variants_found">未找到可用且已启用的 %1$s 插件变体 (%2$s).</string>
<string name="error_plugin_apk_does_not_contain_required_assets_for_variant">插件 APK 不包含变体 \"%1$s\" 所需的资源文件: %2$s.</string>
<string name="error_plugin_apk_does_not_contain_required_native_libraries">插件 APK 不包含所需的 native 库文件: %1$s.</string>
<string name="error_failed_to_bind_plugin_service">绑定 %1$s 插件服务失败.</string>
<string name="error_timeout_while_querying_plugin_info">查询 %1$s 插件信息超时.</string>
<string name="error_plugin_returned_empty_info">%1$s 插件返回的 info 为空.</string>
<string name="error_plugin_returned_invalid_variant">%1$s 插件返回的 variant 无效: %2$s.</string>
<string name="error_no_embedded_paddle_ocr_assets_found">未找到内置 Paddle OCR 资源, 请在打包时勾选并注入 Paddle OCR 后重试.</string>
</resources>

View File

@@ -1434,5 +1434,15 @@
<string name="text_sort_by_name">Sort by name</string>
<string name="text_sort_by_last_update_time">Sort by last update time</string>
<string name="text_sort_by_package_size">Sort by package size</string>
<string name="error_missing_required_plugin_for_module_label">Missing required plugin for \"%1$s\". Please install the plugin and try again.</string>
<string name="error_no_enabled_plugin_for_module_label">No enabled plugin for \"%1$s\". Please enable a plugin and try again.</string>
<string name="error_no_available_enabled_plugin_variants_found">No available enabled %1$s plugin variants found (%2$s).</string>
<string name="error_plugin_apk_does_not_contain_required_assets_for_variant">Plugin APK does not contain required assets for variant=\"%1$s\": %2$s.</string>
<string name="error_plugin_apk_does_not_contain_required_native_libraries">Plugin APK does not contain required native libraries: %1$s.</string>
<string name="error_failed_to_bind_plugin_service">Failed to bind %1$s plugin service.</string>
<string name="error_timeout_while_querying_plugin_info">Timeout while querying %1$s plugin info.</string>
<string name="error_plugin_returned_empty_info">%1$s plugin returned empty info.</string>
<string name="error_plugin_returned_invalid_variant">%1$s plugin returned invalid variant: %2$s.</string>
<string name="error_no_embedded_paddle_ocr_assets_found">No embedded Paddle OCR assets found. Please re-package with Paddle OCR enabled.</string>
</resources>