6.7.0 - Alpha16 - 打包应用支持自带离线 Paddle OCR 引擎, 避免依赖插件安装
This commit is contained in:
@@ -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();
|
||||
232
app/src/main/assets-app/sample/OCR/PaddleOCR (截图识别) [v6.7.0+].js
Normal file
232
app/src/main/assets-app/sample/OCR/PaddleOCR (截图识别) [v6.7.0+].js
Normal 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();
|
||||
|
||||
}();
|
||||
@@ -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;
|
||||
}
|
||||
}();
|
||||
@@ -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(),
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user