6.6.3 - Alpha5 - 新增 images.compressToBytes/downsample; 修复 images.compress; ImageWrapper#saveTo 支持相对路径

This commit is contained in:
SuperMonster003
2025-05-21 00:41:15 +08:00
parent 4c7ba32646
commit 501c01a632
17 changed files with 563 additions and 231 deletions

View File

@@ -1,13 +1,15 @@
{ {
"$data": { "$data": {
"v6.6.3": { "v6.6.3": {
"released_date": "2025/05/14", "released_date": "2025/05/21",
"feature": [ "feature": [
"版本历史功能, 可查看发行版本历史更新记录 (多语言) 与统计数据", "版本历史功能, 可查看发行版本历史更新记录 (多语言) 与统计数据",
"timers.keepAlive 方法 (已全局化), 用于保持脚本活跃状态", "timers.keepAlive 方法 (已全局化), 用于保持脚本活跃状态",
"engines.on('start/stop/error', callback) 等事件监听方法, 用于监听脚本引擎全局事件", "engines.on('start/stop/error', callback) 等事件监听方法, 用于监听脚本引擎全局事件",
"images.detectMultiColors 方法, 用于多点颜色校验 _[`issue #374`](http://issues.autojs6.com/374)_", "images.detectMultiColors 方法, 用于多点颜色校验 _[`issue #374`](http://issues.autojs6.com/374)_",
"images.matchFeatures/detectAndComputeFeatures 方法, 支持全分辨率找图 (Ref to [Auto.js Pro](https://g.pro.autojs.org/)) _[`issue #366`](http://issues.autojs6.com/366)_" "images.matchFeatures/detectAndComputeFeatures 方法, 支持全分辨率找图 (Ref to [Auto.js Pro](https://g.pro.autojs.org/)) _[`issue #366`](http://issues.autojs6.com/366)_",
"images.compressToBytes 方法, 用于压缩图像并生成字节数组",
"images.downsample 方法, 用于像素降采样并生成新的 ImageWrapper"
], ],
"fix": [ "fix": [
"主页文档标签显示在线文档时部分内容被系统导航栏遮挡的问题", "主页文档标签显示在线文档时部分内容被系统导航栏遮挡的问题",
@@ -21,6 +23,8 @@
"使用 JavaAdapter 时导致 ClassLoader 调用栈溢出的问题 _[`issue #376`](http://issues.autojs6.com/376)_", "使用 JavaAdapter 时导致 ClassLoader 调用栈溢出的问题 _[`issue #376`](http://issues.autojs6.com/376)_",
"console.setContentTextColor 方法导致日志字体颜色丢失默认值的问题 _[`issue #346`](http://issues.autojs6.com/346)_", "console.setContentTextColor 方法导致日志字体颜色丢失默认值的问题 _[`issue #346`](http://issues.autojs6.com/346)_",
"console.setContentBackgroundColor 方法无法接受颜色名称参数的问题 _[`issue #384`](http://issues.autojs6.com/384)_", "console.setContentBackgroundColor 方法无法接受颜色名称参数的问题 _[`issue #384`](http://issues.autojs6.com/384)_",
"images.compress 方法实现原理由像素降采样修正为编码质量变化",
"images.resize 方法无法正常使用的问题",
"README.md 中部分语言日期格式不正确的问题", "README.md 中部分语言日期格式不正确的问题",
"Gradle 构建脚本可能因获取到无效库档案文件长度而导致构建失败的问题 _[`issue #389`](http://issues.autojs6.com/389)_" "Gradle 构建脚本可能因获取到无效库档案文件长度而导致构建失败的问题 _[`issue #389`](http://issues.autojs6.com/389)_"
], ],
@@ -34,6 +38,7 @@
"文件管理器浮动按钮展开后点击其他区域可自动隐藏", "文件管理器浮动按钮展开后点击其他区域可自动隐藏",
"打包单文件时自动读取并勾选已安装应用的声明权限 _[`issue #362`](http://issues.autojs6.com/362)_", "打包单文件时自动读取并勾选已安装应用的声明权限 _[`issue #362`](http://issues.autojs6.com/362)_",
"意图相关操作 (编辑/查看/安装/发送/播放等) 增加操作异常提示", "意图相关操作 (编辑/查看/安装/发送/播放等) 增加操作异常提示",
"ImageWrapper#saveTo 方法的路径参数支持相对路径",
"images.save 方法使用 quality 参数时支持 png 格式的文件体积压缩 _[`issue #367`](http://issues.autojs6.com/367)_", "images.save 方法使用 quality 参数时支持 png 格式的文件体积压缩 _[`issue #367`](http://issues.autojs6.com/367)_",
"已忽略更新记录及客户端模式连接地址记录支持清空操作", "已忽略更新记录及客户端模式连接地址记录支持清空操作",
"版本更新信息支持多语言显示 (与当前显示语言同步)", "版本更新信息支持多语言显示 (与当前显示语言同步)",

View File

@@ -242,7 +242,7 @@ class SimpleActionAutomator(private val accessibilityBridge: AccessibilityBridge
val bitmap = hardwareBuffer.copy(Bitmap.Config.ARGB_8888, true) val bitmap = hardwareBuffer.copy(Bitmap.Config.ARGB_8888, true)
hardwareBuffer.recycle() hardwareBuffer.recycle()
promiseAdapter.resolve(ImageWrapper.ofBitmap(bitmap)) promiseAdapter.resolve(ImageWrapper.ofBitmap(scriptRuntime, bitmap))
} }
mPromiseAdapter = null mPromiseAdapter = null
} }

View File

@@ -11,11 +11,10 @@ import android.graphics.PorterDuff;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.RectF; import android.graphics.RectF;
import android.graphics.Region; import android.graphics.Region;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.autojs.autojs.core.image.ImageWrapper; import org.autojs.autojs.core.image.ImageWrapper;
import org.autojs.autojs.runtime.ScriptRuntime;
/** /**
* Created by Stardust on Mar 22, 2018. * Created by Stardust on Mar 22, 2018.
@@ -23,24 +22,27 @@ import org.autojs.autojs.core.image.ImageWrapper;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class ScriptCanvas { public class ScriptCanvas {
private final ScriptRuntime mScriptRuntime;
private Canvas mCanvas; private Canvas mCanvas;
private Bitmap mBitmap; private Bitmap mBitmap;
public ScriptCanvas(int width, int height) { public ScriptCanvas(ScriptRuntime scriptRuntime, int width, int height) {
this(Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)); this(scriptRuntime, Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888));
} }
public ScriptCanvas(@NonNull Bitmap bitmap) { public ScriptCanvas(ScriptRuntime scriptRuntime, @NonNull Bitmap bitmap) {
mScriptRuntime = scriptRuntime;
mCanvas = new Canvas(bitmap); mCanvas = new Canvas(bitmap);
mBitmap = bitmap; mBitmap = bitmap;
} }
public ScriptCanvas(@NonNull ImageWrapper image) { public ScriptCanvas(ScriptRuntime scriptRuntime, @NonNull ImageWrapper image) {
this(image.getBitmap().copy(image.getBitmap().getConfig(), true)); this(scriptRuntime, image.getBitmap().copy(image.getBitmap().getConfig(), true));
} }
public ScriptCanvas() { public ScriptCanvas(ScriptRuntime scriptRuntime) {
/* Empty body. */ mScriptRuntime = scriptRuntime;
} }
public Canvas getAndroidCanvas() { public Canvas getAndroidCanvas() {
@@ -52,7 +54,7 @@ public class ScriptCanvas {
} }
public ImageWrapper toImage() { public ImageWrapper toImage() {
return ImageWrapper.ofBitmap(mBitmap.copy(mBitmap.getConfig(), true)); return ImageWrapper.ofBitmap(mScriptRuntime, mBitmap.copy(mBitmap.getConfig(), true));
} }
public boolean isHardwareAccelerated() { public boolean isHardwareAccelerated() {

View File

@@ -1,12 +1,14 @@
package org.autojs.autojs.core.image package org.autojs.autojs.core.image
import android.media.Image import android.media.Image
import org.autojs.autojs.runtime.ScriptRuntime
/** /**
* Created by SuperMonster003 on Dec 15, 2023. * Created by SuperMonster003 on Dec 15, 2023.
* Modified by SuperMonster003 as of May 20, 2025.
*/ */
// @Reference to Auto.js Pro 9.3.11 by SuperMonster003 on Dec 15, 2023. // @Reference to Auto.js Pro 9.3.11 by SuperMonster003 on Dec 15, 2023.
class CapturedImage(image: Image) : ImageWrapper(image) { class CapturedImage(scriptRuntime: ScriptRuntime, image: Image) : ImageWrapper(scriptRuntime, image) {
override fun recycle() { override fun recycle() {
/* Doing nothing to suppress default recycle method. */ /* Doing nothing to suppress default recycle method. */

View File

@@ -4,12 +4,15 @@ import android.graphics.Bitmap
import android.graphics.Bitmap.CompressFormat import android.graphics.Bitmap.CompressFormat
import android.graphics.Color import android.graphics.Color
import android.media.Image import android.media.Image
import androidx.core.graphics.createBitmap
import androidx.core.graphics.get
import org.autojs.autojs.annotation.ScriptInterface import org.autojs.autojs.annotation.ScriptInterface
import org.autojs.autojs.core.opencv.Mat import org.autojs.autojs.core.opencv.Mat
import org.autojs.autojs.core.opencv.OpenCVHelper import org.autojs.autojs.core.opencv.OpenCVHelper
import org.autojs.autojs.core.ref.MonitorResource import org.autojs.autojs.core.ref.MonitorResource
import org.autojs.autojs.core.ref.NativeObjectReference import org.autojs.autojs.core.ref.NativeObjectReference
import org.autojs.autojs.pio.UncheckedIOException import org.autojs.autojs.pio.UncheckedIOException
import org.autojs.autojs.runtime.ScriptRuntime
import org.autojs.autojs.runtime.api.Images import org.autojs.autojs.runtime.api.Images
import org.autojs.autojs.util.StringUtils.str import org.autojs.autojs.util.StringUtils.str
import org.autojs.autojs6.R import org.autojs.autojs6.R
@@ -31,6 +34,8 @@ import java.util.concurrent.atomic.AtomicLong
// @Reference to Auto.js Pro 9.3.11 by SuperMonster003 on Dec 20, 2023. // @Reference to Auto.js Pro 9.3.11 by SuperMonster003 on Dec 20, 2023.
open class ImageWrapper : Recyclable, MonitorResource { open class ImageWrapper : Recyclable, MonitorResource {
private var mScriptRuntime: ScriptRuntime
private var mMat: Mat? = null private var mMat: Mat? = null
private var mBgrMat: Mat? = null private var mBgrMat: Mat? = null
private var mBitmap: Bitmap? = null private var mBitmap: Bitmap? = null
@@ -61,7 +66,7 @@ open class ImageWrapper : Recyclable, MonitorResource {
ensureNotRecycled() ensureNotRecycled()
if (mBitmap == null) { if (mBitmap == null) {
if (mMat != null) { if (mMat != null) {
mBitmap = Bitmap.createBitmap(mMat!!.width(), mMat!!.height(), Bitmap.Config.ARGB_8888) mBitmap = createBitmap(mMat!!.width(), mMat!!.height())
Utils.matToBitmap(mMat, mBitmap) Utils.matToBitmap(mMat, mBitmap)
} else { } else {
mBitmap = mediaImage?.let { toBitmap(it) } mBitmap = mediaImage?.let { toBitmap(it) }
@@ -100,23 +105,26 @@ open class ImageWrapper : Recyclable, MonitorResource {
} }
get() = mPlane ?: mediaImage?.planes?.get(0) get() = mPlane ?: mediaImage?.planes?.get(0)
constructor(width: Int, height: Int) : this(Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)) constructor(scriptRuntime: ScriptRuntime, width: Int, height: Int) : this(scriptRuntime, createBitmap(width, height))
constructor(bitmap: Bitmap) { constructor(scriptRuntime: ScriptRuntime, bitmap: Bitmap) {
mScriptRuntime = scriptRuntime
mId = mNextId.incrementAndGet() mId = mNextId.incrementAndGet()
mBitmap = bitmap.also { addToList(it) } mBitmap = bitmap.also { addToList(it) }
mWidth = bitmap.width mWidth = bitmap.width
mHeight = bitmap.height mHeight = bitmap.height
} }
constructor(mat: Mat) { constructor(scriptRuntime: ScriptRuntime, mat: Mat) {
mScriptRuntime = scriptRuntime
mId = mNextId.incrementAndGet() mId = mNextId.incrementAndGet()
mMat = mat.also { addToList(it) } mMat = mat.also { addToList(it) }
mWidth = mat.cols() mWidth = mat.cols()
mHeight = mat.rows() mHeight = mat.rows()
} }
constructor(mat: org.opencv.core.Mat) { constructor(scriptRuntime: ScriptRuntime, mat: org.opencv.core.Mat) {
mScriptRuntime = scriptRuntime
mId = mNextId.incrementAndGet() mId = mNextId.incrementAndGet()
mMat = when (mat.nativeObj != 0L) { mMat = when (mat.nativeObj != 0L) {
true -> Mat(mat.nativeObj) true -> Mat(mat.nativeObj)
@@ -126,7 +134,8 @@ open class ImageWrapper : Recyclable, MonitorResource {
mHeight = mat.rows() mHeight = mat.rows()
} }
constructor(bitmap: Bitmap, mat: Mat?) { constructor(scriptRuntime: ScriptRuntime, bitmap: Bitmap, mat: Mat?) {
mScriptRuntime = scriptRuntime
mId = mNextId.incrementAndGet() mId = mNextId.incrementAndGet()
mMat = mat?.also { addToList(it) } mMat = mat?.also { addToList(it) }
mBitmap = bitmap.also { addToList(it) } mBitmap = bitmap.also { addToList(it) }
@@ -134,7 +143,8 @@ open class ImageWrapper : Recyclable, MonitorResource {
mHeight = bitmap.height mHeight = bitmap.height
} }
constructor(mediaImage: Image) { constructor(scriptRuntime: ScriptRuntime, mediaImage: Image) {
mScriptRuntime = scriptRuntime
mId = mNextId.incrementAndGet() mId = mNextId.incrementAndGet()
this.mediaImage = mediaImage.also { addToList(it) } this.mediaImage = mediaImage.also { addToList(it) }
mWidth = mediaImage.width mWidth = mediaImage.width
@@ -149,27 +159,23 @@ open class ImageWrapper : Recyclable, MonitorResource {
imageList.add(WeakReference(image)) imageList.add(WeakReference(image))
} }
fun saveTo(path: String?): Boolean { fun saveTo(path: String): Boolean {
ensureNotRecycled() ensureNotRecycled()
path ?: return false val fullPath = mScriptRuntime.files.nonNullPath(path)
if (mBitmap == null) { if (mBitmap == null) {
if (mMat != null) { if (mMat != null) {
return Imgcodecs.imwrite(path, mMat) return Imgcodecs.imwrite(fullPath, mMat)
} }
bitmap /* Getter, for initializing `mBitmap`. */ bitmap /* Getter, for initializing `mBitmap`. */
} }
return try { return runCatching { saveWithBitmap(fullPath) }.isSuccess
true.also { saveWithBitmap(path) }
} catch (_: Exception) {
false
}
} }
private fun saveWithBitmap(path: String?) { private fun saveWithBitmap(fullPath: String?) {
try { try {
path ?: throw Exception("Argument \"path\" cannot be null") fullPath ?: throw Exception("Argument \"path\" cannot be null")
mBitmap ?: throw Exception("Member \"bitmap\" cannot be null") mBitmap ?: throw Exception("Member \"bitmap\" cannot be null")
mBitmap!!.compress(CompressFormat.PNG, 100, FileOutputStream(path)) mBitmap!!.compress(CompressFormat.PNG, 100, FileOutputStream(fullPath))
} catch (e: FileNotFoundException) { } catch (e: FileNotFoundException) {
throw UncheckedIOException(e) throw UncheckedIOException(e)
} }
@@ -181,7 +187,7 @@ open class ImageWrapper : Recyclable, MonitorResource {
throw ArrayIndexOutOfBoundsException("Point ($x, $y) out of bounds of $this") throw ArrayIndexOutOfBoundsException("Point ($x, $y) out of bounds of $this")
} }
mBitmap?.let { oBitmap -> mBitmap?.let { oBitmap ->
return oBitmap.getPixel(x, y) return oBitmap[x, y]
} }
mMat?.let { oMat -> mMat?.let { oMat ->
@@ -282,8 +288,8 @@ open class ImageWrapper : Recyclable, MonitorResource {
fun clone(): ImageWrapper { fun clone(): ImageWrapper {
ensureNotRecycled() ensureNotRecycled()
return when (val bitmap = mBitmap) { return when (val bitmap = mBitmap) {
null -> ofMat(mat.clone()) null -> ofMat(mScriptRuntime, mat.clone())
else -> ofBitmap(bitmap.copy(bitmap.config ?: Bitmap.Config.ARGB_8888, true)) else -> ofBitmap(mScriptRuntime, bitmap.copy(bitmap.config ?: Bitmap.Config.ARGB_8888, true))
} }
} }
@@ -312,16 +318,16 @@ open class ImageWrapper : Recyclable, MonitorResource {
} }
@JvmStatic @JvmStatic
fun ofImage(image: Image) = ImageWrapper(image) fun ofImage(scriptRuntime: ScriptRuntime, image: Image) = ImageWrapper(scriptRuntime, image)
@JvmStatic @JvmStatic
fun ofMat(mat: Mat) = ImageWrapper(mat) fun ofMat(scriptRuntime: ScriptRuntime, mat: Mat) = ImageWrapper(scriptRuntime, mat)
@JvmStatic @JvmStatic
fun ofMat(mat: org.opencv.core.Mat) = ImageWrapper(mat) fun ofMat(scriptRuntime: ScriptRuntime, mat: org.opencv.core.Mat) = ImageWrapper(scriptRuntime, mat)
@JvmStatic @JvmStatic
fun ofBitmap(bitmap: Bitmap) = ImageWrapper(bitmap) fun ofBitmap(scriptRuntime: ScriptRuntime, bitmap: Bitmap) = ImageWrapper(scriptRuntime, bitmap)
@ScriptInterface @ScriptInterface
fun toBitmap(image: Image): Bitmap { fun toBitmap(image: Image): Bitmap {
@@ -329,11 +335,7 @@ open class ImageWrapper : Recyclable, MonitorResource {
val buffer = plane.buffer.apply { position(0) } val buffer = plane.buffer.apply { position(0) }
val pixelStride = plane.pixelStride val pixelStride = plane.pixelStride
val rowPadding = plane.rowStride - pixelStride * image.width val rowPadding = plane.rowStride - pixelStride * image.width
val bitmap = Bitmap.createBitmap( val bitmap = createBitmap(image.width + rowPadding / pixelStride, image.height).apply { copyPixelsFromBuffer(buffer) }
image.width + rowPadding / pixelStride,
image.height,
Bitmap.Config.ARGB_8888,
).apply { copyPixelsFromBuffer(buffer) }
return when (rowPadding == 0) { return when (rowPadding == 0) {
true -> bitmap true -> bitmap
else -> Bitmap.createBitmap(bitmap, 0, 0, image.width, image.height) else -> Bitmap.createBitmap(bitmap, 0, 0, image.width, image.height)

View File

@@ -62,7 +62,7 @@ class JsCanvasView : TextureView, TextureView.SurfaceTextureListener {
execute { execute {
var canvas: Canvas? = null var canvas: Canvas? = null
var time = SystemClock.uptimeMillis() var time = SystemClock.uptimeMillis()
val scriptCanvas = ScriptCanvas() val scriptCanvas = ScriptCanvas(mScriptRuntime)
try { try {
while (mDrawing) { while (mDrawing) {
canvas = lockCanvas() canvas = lockCanvas()

View File

@@ -1,5 +1,6 @@
package org.autojs.autojs.extension package org.autojs.autojs.extension
import androidx.core.net.toUri
import java.text.Normalizer import java.text.Normalizer
object StringExtensions { object StringExtensions {
@@ -59,4 +60,12 @@ object StringExtensions {
else -> false else -> false
} }
@JvmStatic
fun String.isUri(): Boolean {
if (isBlank()) return false
val uri = runCatching { this.toUri() }.getOrNull() ?: return false
uri.scheme?.lowercase() ?: return false
return true
}
} }

View File

@@ -62,9 +62,37 @@ import static org.autojs.autojs.util.RhinoUtils.isMainThread;
import static org.autojs.autojs.util.StringUtils.str; import static org.autojs.autojs.util.StringUtils.str;
/** /**
* Quick Reference Image Size / Quality Helpers<br>
* zh-CN: 快速参考 图像尺寸 / 质量相关辅助方法
* <hr>
* <b>scale</b><br>
* Multiply both width and height of an <b>already-decoded</b> Bitmap by
* the given scale factor(s).<br>
* zh-CN: 在 <b>已解码</b> 的 Bitmap 上, 按照给定比例同时缩放宽高.
* <p>
* <b>resize</b><br>
* Force an <b>already-decoded</b> Bitmap to the specified width/height.
* Aspect ratio may be kept or ignored depending on the overload.<br>
* zh-CN: 将 <b>已解码</b> Bitmap 调整为指定宽高; 可选择保持或忽略纵横比.
* <p>
* <b>downsample</b><br>
* Shrink <b>before decoding</b> by setting <code>inSampleSize</code>; pixels are
* skipped while reading, dramatically reducing memory.<br>
* zh-CN: <b>解码前</b> 通过设置 <code>inSampleSize</code> 跳读像素, 显著降低解码分辨率与内存.
* <p>
* <b>compress</b><br>
* Re-encode an existing Bitmap (or byte stream) into JPEG/PNG/WebP
* with the given format & quality to reduce <b>disk</b> footprint.<br>
* zh-CN: 使用指定格式与质量重新编码 Bitmap (或字节流), 以减小 <b>磁盘</b> 体积.
* <p>
* <b>save</b><br>
* Convenience wrapper that internally invokes <code>compress</code> (with
* default or user-supplied options) and then writes to storage.<br>
* zh-CN: 便捷封装, 内部调用 <code>compress</code> (默认或自定义参数) 后写入存储.
* <hr>
* Created by Stardust on May 20, 2017. * Created by Stardust on May 20, 2017.
* Modified by SuperMonster003 as of Dec 1, 2021.
*/ */
@SuppressWarnings("unused")
public class Images { public class Images {
private static final String TAG = Images.class.getSimpleName(); private static final String TAG = Images.class.getSimpleName();
@@ -130,7 +158,7 @@ public class Images {
return pixel; return pixel;
} }
public static ImageWrapper concat(ImageWrapper imgA, ImageWrapper imgB, int direction) { public static ImageWrapper concat(ScriptRuntime scriptRuntime, ImageWrapper imgA, ImageWrapper imgB, int direction) {
if (!Arrays.asList(Gravity.START, Gravity.END, Gravity.TOP, Gravity.BOTTOM).contains(direction)) { if (!Arrays.asList(Gravity.START, Gravity.END, Gravity.TOP, Gravity.BOTTOM).contains(direction)) {
throw new IllegalArgumentException(str(R.string.error_illegal_argument, "direction", direction)); throw new IllegalArgumentException(str(R.string.error_illegal_argument, "direction", direction));
} }
@@ -160,7 +188,7 @@ public class Images {
} }
imgA.shoot(); imgA.shoot();
imgB.shoot(); imgB.shoot();
return ImageWrapper.ofBitmap(bitmap); return ImageWrapper.ofBitmap(scriptRuntime, bitmap);
} }
public static void saveBitmap(@NonNull Bitmap bitmap, String path) { public static void saveBitmap(@NonNull Bitmap bitmap, String path) {
@@ -184,8 +212,8 @@ public class Images {
return Bitmap.createBitmap(origin, 0, 0, width, height, matrix, false); return Bitmap.createBitmap(origin, 0, 0, width, height, matrix, false);
} }
protected static void setImageCaptureCallback(OnScreenCaptureAvailableListener onScreenCaptureAvailableListener, Image image) { protected static void setImageCaptureCallback(ScriptRuntime scriptRuntime, OnScreenCaptureAvailableListener onScreenCaptureAvailableListener, Image image) {
ImageWrapper ofImage = ImageWrapper.ofImage(image); ImageWrapper ofImage = ImageWrapper.ofImage(scriptRuntime, image);
onScreenCaptureAvailableListener.onCaptureAvailable(ofImage); onScreenCaptureAvailableListener.onCaptureAvailable(ofImage);
// ofImage.recycle(); // ofImage.recycle();
} }
@@ -238,7 +266,7 @@ public class Images {
mPreCaptureImage.recycleInternal(); mPreCaptureImage.recycleInternal();
} }
if (capture != null) { if (capture != null) {
mPreCaptureImage = new CapturedImage(capture); mPreCaptureImage = new CapturedImage(mScriptRuntime, capture);
} }
} }
return mPreCaptureImage; return mPreCaptureImage;
@@ -247,7 +275,7 @@ public class Images {
public boolean captureScreen(String path) { public boolean captureScreen(String path) {
ImageWrapper image = captureScreen(); ImageWrapper image = captureScreen();
return image != null && image.saveTo(mScriptRuntime.files.nonNullPath(path)); return image != null && image.saveTo(path);
} }
public ImageWrapper copy(@NonNull ImageWrapper image) { public ImageWrapper copy(@NonNull ImageWrapper image) {
@@ -256,18 +284,12 @@ public class Images {
return imageWrapper; return imageWrapper;
} }
public boolean save(@NonNull ImageWrapper image, public boolean save(@NonNull ImageWrapper image, @NonNull String path, @NonNull String format, int quality) throws IOException {
@NonNull String path,
@NonNull String format,
int quality) throws IOException {
Bitmap bitmap = image.getBitmap(); Bitmap bitmap = image.getBitmap();
Bitmap.CompressFormat compressFormat = parseImageFormat(format); Bitmap.CompressFormat compressFormat = parseImageFormat(format);
if (compressFormat == Bitmap.CompressFormat.PNG && quality != 100) { if (compressFormat == Bitmap.CompressFormat.PNG && quality != 100) {
// ARGB_8888 -> RGBA[] byte[] compressed = PngQuantBridge.quantize(bitmap, quality);
byte[] rgba = BitmapUtils.bitmapToRgba(bitmap);
byte[] compressed = PngQuantBridge.quantize(rgba, bitmap.getWidth(), bitmap.getHeight(), quality);
if (compressed != null) { if (compressed != null) {
try (FileOutputStream fos = new FileOutputStream(path)) { try (FileOutputStream fos = new FileOutputStream(path)) {
fos.write(compressed); fos.write(compressed);
@@ -290,6 +312,36 @@ public class Images {
} }
} }
public byte[] compressToBytes(@NotNull ImageWrapper image, @NotNull String format, int quality) {
Bitmap bitmap = image.getBitmap();
Bitmap.CompressFormat compressFormat = parseImageFormat(format);
if (compressFormat == Bitmap.CompressFormat.PNG && quality != 100) {
byte[] compressed = PngQuantBridge.quantize(bitmap, quality);
image.shoot();
if (compressed != null) {
return compressed;
}
throw new WrappedRuntimeException("PNG quantization failed");
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (compressFormat == Bitmap.CompressFormat.WEBP_LOSSLESS && quality != 100) {
image.shoot();
throw new IllegalArgumentException(mContext.getString(R.string.error_webp_lossless_quality_not_supported));
}
}
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
bitmap.compress(compressFormat, quality, outputStream);
image.shoot();
return outputStream.toByteArray();
}
public ImageWrapper compress(@NotNull ImageWrapper image, @NotNull String format, int quality) throws BitmapUtils.DecodeException {
return fromBytes(compressToBytes(image, format, quality));
}
// public EventEmitter select() { // public EventEmitter select() {
// EventEmitter eventEmitter = new EventEmitter(scriptRuntime.bridges, scriptRuntime.timers.getTimerForCurrentThread()); // EventEmitter eventEmitter = new EventEmitter(scriptRuntime.bridges, scriptRuntime.timers.getTimerForCurrentThread());
// StartForResultActivity.start(mContext, new SelectImageCallback(this, eventEmitter, mContext, mContext.getString(R.string.text_select_image))); // StartForResultActivity.start(mContext, new SelectImageCallback(this, eventEmitter, mContext, mContext.getString(R.string.text_select_image)));
@@ -303,7 +355,7 @@ public class Images {
public ImageWrapper rotate(@NonNull ImageWrapper img, float x, float y, float degree) { public ImageWrapper rotate(@NonNull ImageWrapper img, float x, float y, float degree) {
Matrix matrix = new Matrix(); Matrix matrix = new Matrix();
matrix.postRotate(degree, x, y); matrix.postRotate(degree, x, y);
ImageWrapper imageWrapper = ImageWrapper.ofBitmap(Bitmap.createBitmap(img.getBitmap(), 0, 0, img.getWidth(), img.getHeight(), matrix, true)); ImageWrapper imageWrapper = ImageWrapper.ofBitmap(mScriptRuntime, Bitmap.createBitmap(img.getBitmap(), 0, 0, img.getWidth(), img.getHeight(), matrix, true));
img.shoot(); img.shoot();
return imageWrapper; return imageWrapper;
} }
@@ -313,23 +365,27 @@ public class Images {
Bitmap original = img.getBitmap(); Bitmap original = img.getBitmap();
Matrix matrix = new Matrix(); Matrix matrix = new Matrix();
// 根据传入参数设置缩放比例 // Set scaling ratio according to input parameters.
// zh-CN: 根据传入参数设置缩放比例.
float sx = horizontal ? -1.0f : 1.0f; float sx = horizontal ? -1.0f : 1.0f;
float sy = vertical ? -1.0f : 1.0f; float sy = vertical ? -1.0f : 1.0f;
matrix.preScale(sx, sy); matrix.preScale(sx, sy);
// 水平方向翻转后, 平移到图像宽度的位置 // After horizontal flip, translate to image width position.
// zh-CN: 水平方向翻转后, 平移到图像宽度的位置.
if (horizontal) matrix.postTranslate(original.getWidth(), 0); if (horizontal) matrix.postTranslate(original.getWidth(), 0);
// 垂直方向翻转后, 平移到图像高度的位置
// After vertical flip, translate to image height position.
// zh-CN: 垂直方向翻转后, 平移到图像高度的位置.
if (vertical) matrix.postTranslate(0, original.getHeight()); if (vertical) matrix.postTranslate(0, original.getHeight());
Bitmap flipped = Bitmap.createBitmap(original, 0, 0, original.getWidth(), original.getHeight(), matrix, true); Bitmap flipped = Bitmap.createBitmap(original, 0, 0, original.getWidth(), original.getHeight(), matrix, true);
img.shoot(); img.shoot();
return ImageWrapper.ofBitmap(flipped); return ImageWrapper.ofBitmap(mScriptRuntime, flipped);
} }
public ImageWrapper clip(@NonNull ImageWrapper img, int x, int y, int w, int h) { public ImageWrapper clip(@NonNull ImageWrapper img, int x, int y, int w, int h) {
ImageWrapper imageWrapper = ImageWrapper.ofBitmap(Bitmap.createBitmap(img.getBitmap(), x, y, w, h)); ImageWrapper imageWrapper = ImageWrapper.ofBitmap(mScriptRuntime, Bitmap.createBitmap(img.getBitmap(), x, y, w, h));
img.shoot(); img.shoot();
return imageWrapper; return imageWrapper;
} }
@@ -346,7 +402,7 @@ public class Images {
if (!isStrict) return null; if (!isStrict) return null;
throw new RuntimeException(mContext.getString(R.string.error_file_in_path_does_not_exist, fullPath)); throw new RuntimeException(mContext.getString(R.string.error_file_in_path_does_not_exist, fullPath));
} }
return ImageWrapper.ofBitmap(bitmap); return ImageWrapper.ofBitmap(mScriptRuntime, bitmap);
} }
@NotNull @NotNull
@@ -357,7 +413,7 @@ public class Images {
} }
public ImageWrapper fromBase64(String data) { public ImageWrapper fromBase64(String data) {
return ImageWrapper.ofBitmap(Drawables.loadBase64Data(data)); return ImageWrapper.ofBitmap(mScriptRuntime, Drawables.loadBase64Data(data));
} }
public String toBase64(ImageWrapper img, String format, int quality) { public String toBase64(ImageWrapper img, String format, int quality) {
@@ -375,8 +431,8 @@ public class Images {
return outputStream.toByteArray(); return outputStream.toByteArray();
} }
public ImageWrapper fromBytes(byte[] bytes) { public ImageWrapper fromBytes(byte[] bytes) throws BitmapUtils.DecodeException {
return ImageWrapper.ofBitmap(BitmapFactory.decodeByteArray(bytes, 0, bytes.length)); return ImageWrapper.ofBitmap(mScriptRuntime, BitmapUtils.bitmapFromByteArrayOrThrow(bytes));
} }
private Bitmap.CompressFormat parseImageFormat(String format) { private Bitmap.CompressFormat parseImageFormat(String format) {
@@ -405,7 +461,7 @@ public class Images {
HttpURLConnection connection = (HttpURLConnection) new URL(src).openConnection(); HttpURLConnection connection = (HttpURLConnection) new URL(src).openConnection();
connection.setDoInput(true); connection.setDoInput(true);
connection.connect(); connection.connect();
return ImageWrapper.ofBitmap(BitmapFactory.decodeStream(connection.getInputStream())); return ImageWrapper.ofBitmap(mScriptRuntime, BitmapFactory.decodeStream(connection.getInputStream()));
} catch (IOException e) { } catch (IOException e) {
return null; return null;
} }
@@ -441,7 +497,7 @@ public class Images {
channels.add(3, alphaChannel); // 将原来的 alpha 通道放回最后一个位置 channels.add(3, alphaChannel); // 将原来的 alpha 通道放回最后一个位置
org.opencv.core.Mat destMat = new org.opencv.core.Mat(); org.opencv.core.Mat destMat = new org.opencv.core.Mat();
Core.merge(channels, destMat); // 合并 4 个通道BGR + 原始 alpha Core.merge(channels, destMat); // 合并 4 个通道 (BGR + 原始 alpha)
// 转换反色后的 Mat 回 Bitmap // 转换反色后的 Mat 回 Bitmap
Bitmap invertedBitmap = Bitmap.createBitmap(originalWidth, originalHeight, Bitmap.Config.ARGB_8888); Bitmap invertedBitmap = Bitmap.createBitmap(originalWidth, originalHeight, Bitmap.Config.ARGB_8888);
@@ -449,7 +505,7 @@ public class Images {
image.shoot(); image.shoot();
return ImageWrapper.ofBitmap(invertedBitmap); return ImageWrapper.ofBitmap(mScriptRuntime, invertedBitmap);
} }
public void releaseScreenCapturer() { public void releaseScreenCapturer() {
@@ -521,18 +577,20 @@ public class Images {
shouldReleaseMat = true; shouldReleaseMat = true;
} }
@Nullable @Nullable
org.opencv.core.Point point = TemplateMatching.singleTemplateMatching( org.opencv.core.Point point;
src, try {
template.getMat(), point = TemplateMatching.singleTemplateMatching(
new TemplateMatching.Options(-1, weakThreshold, strictThreshold, maxLevel) src,
); template.getMat(),
new TemplateMatching.Options(-1, weakThreshold, strictThreshold, maxLevel)
if (shouldReleaseMat) { );
OpenCVHelper.release(src); } finally {
if (shouldReleaseMat) {
OpenCVHelper.release(src);
}
image.shoot();
template.shoot();
} }
image.shoot();
template.shoot();
if (point != null) { if (point != null) {
if (rect != null) { if (rect != null) {
point.x += rect.x; point.x += rect.x;
@@ -605,7 +663,7 @@ public class Images {
} }
public void setImageCaptureCallback(OnScreenCaptureAvailableListener onScreenCaptureAvailableListener) { public void setImageCaptureCallback(OnScreenCaptureAvailableListener onScreenCaptureAvailableListener) {
mOnScreenCaptureAvailableListener = new ScreenCaptureAvailableHandler(onScreenCaptureAvailableListener); mOnScreenCaptureAvailableListener = new ScreenCaptureAvailableHandler(mScriptRuntime, onScreenCaptureAvailableListener);
if (mScreenCapturer != null) { if (mScreenCapturer != null) {
mScreenCapturer.setImageCaptureCallback(mOnScreenCaptureAvailableListener); mScreenCapturer.setImageCaptureCallback(mOnScreenCaptureAvailableListener);
} }

View File

@@ -1,5 +1,7 @@
package org.autojs.autojs.runtime.api; package org.autojs.autojs.runtime.api;
import android.graphics.Bitmap;
public class PngQuantBridge { public class PngQuantBridge {
static { static {
@@ -35,12 +37,8 @@ public class PngQuantBridge {
* <li>PNG 规范不支持在压缩阶段引入有损处理, 因此不会像 JPEG 那样再出现马赛克等失真.</li> * <li>PNG 规范不支持在压缩阶段引入有损处理, 因此不会像 JPEG 那样再出现马赛克等失真.</li>
* </ul> * </ul>
* *
* @param rgba contiguous RGBA8888 buffer; length must equal {@code width * height * 4}
* @param width image width in pixels
* @param height image height in pixels
* @param quality libimagequant quality (0100, lower = smaller file &amp; potentially higher error)
* @return palette-based PNG bytes (may contain transparency) * @return palette-based PNG bytes (may contain transparency)
*/ */
public static native byte[] quantize(byte[] rgba, int width, int height, int quality); public static native byte[] quantize(Bitmap bitmap, int quality);
} }

View File

@@ -4,18 +4,17 @@ import android.content.Context;
import android.media.Image; import android.media.Image;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.autojs.autojs.core.floaty.BaseResizableFloatyWindow; import org.autojs.autojs.core.floaty.BaseResizableFloatyWindow;
import org.autojs.autojs.core.image.capture.ScreenCapturer; import org.autojs.autojs.core.image.capture.ScreenCapturer;
import org.autojs.autojs.runtime.ScriptRuntime;
/** /**
* Created by SuperMonster003 on Dec 18, 2023. * Created by SuperMonster003 on Dec 18, 2023.
*/ */
// @Reference to Auto.js Pro 9.3.11 by SuperMonster003 on Dec 18, 2023. // @Reference to Auto.js Pro 9.3.11 by SuperMonster003 on Dec 18, 2023.
public record ScreenCaptureAvailableHandler(Images.OnScreenCaptureAvailableListener listener) implements BaseResizableFloatyWindow.ViewSupplier, ScreenCapturer.OnScreenCaptureAvailableListener { public record ScreenCaptureAvailableHandler(ScriptRuntime scriptRuntime, Images.OnScreenCaptureAvailableListener listener) implements BaseResizableFloatyWindow.ViewSupplier, ScreenCapturer.OnScreenCaptureAvailableListener {
@NonNull @NonNull
@Override @Override
@@ -25,6 +24,6 @@ public record ScreenCaptureAvailableHandler(Images.OnScreenCaptureAvailableListe
@Override @Override
public void onCaptureAvailable(Image image) { public void onCaptureAvailable(Image image) {
Images.setImageCaptureCallback(listener, image); Images.setImageCaptureCallback(scriptRuntime, listener, image);
} }
} }

View File

@@ -3,8 +3,10 @@ package org.autojs.autojs.runtime.api.augment.images
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.net.Uri
import android.util.Log import android.util.Log
import android.view.Gravity import android.view.Gravity
import androidx.core.net.toUri
import org.autojs.autojs.annotation.RhinoRuntimeFunctionInterface import org.autojs.autojs.annotation.RhinoRuntimeFunctionInterface
import org.autojs.autojs.core.image.ColorDetector import org.autojs.autojs.core.image.ColorDetector
import org.autojs.autojs.core.image.ImageWrapper import org.autojs.autojs.core.image.ImageWrapper
@@ -18,6 +20,7 @@ import org.autojs.autojs.extension.ArrayExtensions.toNativeObject
import org.autojs.autojs.extension.ScriptableExtensions.hasProp import org.autojs.autojs.extension.ScriptableExtensions.hasProp
import org.autojs.autojs.extension.ScriptableExtensions.prop import org.autojs.autojs.extension.ScriptableExtensions.prop
import org.autojs.autojs.extension.ScriptableObjectExtensions.inquire import org.autojs.autojs.extension.ScriptableObjectExtensions.inquire
import org.autojs.autojs.extension.StringExtensions.isUri
import org.autojs.autojs.runtime.ScriptRuntime import org.autojs.autojs.runtime.ScriptRuntime
import org.autojs.autojs.runtime.api.ImageFeatureMatching import org.autojs.autojs.runtime.api.ImageFeatureMatching
import org.autojs.autojs.runtime.api.ImageSimilarity import org.autojs.autojs.runtime.api.ImageSimilarity
@@ -30,6 +33,8 @@ import org.autojs.autojs.runtime.api.augment.colors.Colors
import org.autojs.autojs.runtime.api.augment.s13n.S13n import org.autojs.autojs.runtime.api.augment.s13n.S13n
import org.autojs.autojs.runtime.exception.ShouldNeverHappenException import org.autojs.autojs.runtime.exception.ShouldNeverHappenException
import org.autojs.autojs.runtime.exception.WrappedIllegalArgumentException import org.autojs.autojs.runtime.exception.WrappedIllegalArgumentException
import org.autojs.autojs.runtime.exception.WrappedRuntimeException
import org.autojs.autojs.util.BitmapUtils
import org.autojs.autojs.util.RhinoUtils.callFunction import org.autojs.autojs.util.RhinoUtils.callFunction
import org.autojs.autojs.util.RhinoUtils.coerceBoolean import org.autojs.autojs.util.RhinoUtils.coerceBoolean
import org.autojs.autojs.util.RhinoUtils.coerceFloatNumber import org.autojs.autojs.util.RhinoUtils.coerceFloatNumber
@@ -47,16 +52,9 @@ import org.mozilla.javascript.ScriptableObject
import org.opencv.core.CvType import org.opencv.core.CvType
import org.opencv.features2d.DescriptorMatcher import org.opencv.features2d.DescriptorMatcher
import org.opencv.imgproc.Imgproc import org.opencv.imgproc.Imgproc
import java.io.ByteArrayOutputStream import java.net.URL
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.collections.component3
import kotlin.collections.contains
import kotlin.math.floor
import kotlin.math.ln
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import kotlin.math.pow
import kotlin.math.round import kotlin.math.round
import kotlin.math.roundToInt import kotlin.math.roundToInt
import kotlin.math.sqrt import kotlin.math.sqrt
@@ -101,7 +99,7 @@ class Images(scriptRuntime: ScriptRuntime) : Augmentable(scriptRuntime), AsEmitt
::bilateralFilter.name, ::bilateralFilter.name,
::cvtColor.name, ::cvtColor.name,
::findCircles.name, ::findCircles.name,
::resizeInternal.name, ::resize.name,
::scale.name, ::scale.name,
::rotate.name, ::rotate.name,
::flip.name, ::flip.name,
@@ -135,6 +133,8 @@ class Images(scriptRuntime: ScriptRuntime) : Augmentable(scriptRuntime), AsEmitt
::isRecycled.name, ::isRecycled.name,
::recycle.name, ::recycle.name,
::compress.name, ::compress.name,
::compressToBytes.name,
::downsample.name,
::getSize.name, ::getSize.name,
::getWidth.name, ::getWidth.name,
::getHeight.name, ::getHeight.name,
@@ -160,6 +160,9 @@ class Images(scriptRuntime: ScriptRuntime) : Augmentable(scriptRuntime), AsEmitt
@JvmField @JvmField
val DEFAULT_IMAGE_SAVE_QUALITY = 100 val DEFAULT_IMAGE_SAVE_QUALITY = 100
@JvmField
val DEFAULT_IMAGE_COMPRESS_QUALITY = 60
@JvmField @JvmField
val DEFAULT_IMAGE_TO_BYTES_QUALITY = 100 val DEFAULT_IMAGE_TO_BYTES_QUALITY = 100
@@ -308,7 +311,12 @@ class Images(scriptRuntime: ScriptRuntime) : Augmentable(scriptRuntime), AsEmitt
val image = if (o is String) read(scriptRuntime, arrayOf<Any>(o, true)) else o val image = if (o is String) read(scriptRuntime, arrayOf<Any>(o, true)) else o
require(image is ImageWrapper) { "Argument image for images.save must be a ImageWrapper" } require(image is ImageWrapper) { "Argument image for images.save must be a ImageWrapper" }
require(!path.isJsNullish()) { "Argument path for images.save must be non-nullish" } require(!path.isJsNullish()) { "Argument path for images.save must be non-nullish" }
scriptRuntime.images.save(image, scriptRuntime.files.nonNullPath(coerceString(path)), parseImageFormat(format), parseQuality(quality, DEFAULT_IMAGE_SAVE_QUALITY)) scriptRuntime.images.save(
/* image = */ image,
/* path = */ scriptRuntime.files.nonNullPath(coerceString(path)),
/* format = */ parseImageFormat(format),
/* quality = */ parseQuality(quality, DEFAULT_IMAGE_SAVE_QUALITY),
)
} }
@JvmStatic @JvmStatic
@@ -318,7 +326,12 @@ class Images(scriptRuntime: ScriptRuntime) : Augmentable(scriptRuntime), AsEmitt
val image = if (o is String) read(scriptRuntime, arrayOf<Any>(o, true)) else o val image = if (o is String) read(scriptRuntime, arrayOf<Any>(o, true)) else o
require(image is ImageWrapper) { "Argument image for images.saveImage must be a ImageWrapper" } require(image is ImageWrapper) { "Argument image for images.saveImage must be a ImageWrapper" }
require(!path.isJsNullish()) { "Argument path for images.saveImage must be non-nullish" } require(!path.isJsNullish()) { "Argument path for images.saveImage must be non-nullish" }
scriptRuntime.images.save(image, scriptRuntime.files.nonNullPath(coerceString(path)), parseImageFormat(format), parseQuality(quality, DEFAULT_IMAGE_SAVE_QUALITY)) scriptRuntime.images.save(
/* image = */ image,
/* path = */ scriptRuntime.files.nonNullPath(coerceString(path)),
/* format = */ parseImageFormat(format),
/* quality = */ parseQuality(quality, DEFAULT_IMAGE_SAVE_QUALITY),
)
} }
@JvmStatic @JvmStatic
@@ -658,7 +671,7 @@ class Images(scriptRuntime: ScriptRuntime) : Augmentable(scriptRuntime), AsEmitt
val imageB = if (oB is String) read(scriptRuntime, arrayOf<Any>(oB, true)) else oB val imageB = if (oB is String) read(scriptRuntime, arrayOf<Any>(oB, true)) else oB
require(imageB is ImageWrapper) { "Argument imageB for images.concat must be a ImageWrapper" } require(imageB is ImageWrapper) { "Argument imageB for images.concat must be a ImageWrapper" }
initOpenCvIfNeeded() initOpenCvIfNeeded()
ApiImages.concat(imageA, imageB, directionToGravityToConcat(direction)) ApiImages.concat(scriptRuntime, imageA, imageB, directionToGravityToConcat(direction))
} }
@JvmStatic @JvmStatic
@@ -988,8 +1001,8 @@ class Images(scriptRuntime: ScriptRuntime) : Augmentable(scriptRuntime), AsEmitt
fun matToImage(scriptRuntime: ScriptRuntime, args: Array<out Any?>): ImageWrapper = ensureArgumentsOnlyOne(args) { fun matToImage(scriptRuntime: ScriptRuntime, args: Array<out Any?>): ImageWrapper = ensureArgumentsOnlyOne(args) {
initOpenCvIfNeeded() initOpenCvIfNeeded()
when (it) { when (it) {
is AutoJsMat -> ImageWrapper.ofMat(it) is AutoJsMat -> ImageWrapper.ofMat(scriptRuntime, it)
is OpencvMat -> ImageWrapper.ofMat(it) is OpencvMat -> ImageWrapper.ofMat(scriptRuntime, it)
else -> throw WrappedIllegalArgumentException("Argument mat ${it.jsBrief()} is invalid for images.matToImage()") else -> throw WrappedIllegalArgumentException("Argument mat ${it.jsBrief()} is invalid for images.matToImage()")
} }
} }
@@ -1077,18 +1090,71 @@ class Images(scriptRuntime: ScriptRuntime) : Augmentable(scriptRuntime), AsEmitt
@JvmStatic @JvmStatic
@RhinoRuntimeFunctionInterface @RhinoRuntimeFunctionInterface
fun compress(scriptRuntime: ScriptRuntime, args: Array<out Any?>): ImageWrapper = ensureArgumentsLength(args, 2) { fun compress(scriptRuntime: ScriptRuntime, args: Array<out Any?>): ImageWrapper = ensureArgumentsLengthInRange(args, 1..3) {
val (o, compressLevelArg) = it
// @Comment by SuperMonster003 on May 19, 2025.
// ! Option `inSampleSize` performs pixel downsampling during decoding
// ! by discarding pixels to reduce resolution, rather than changing encoding quality.
// ! zh-CN:
// ! 选项 `inSampleSize` 在解码阶段做像素降采样,
// ! 通过降低分辨率丢弃像素的方式来实现文件压缩, 而非改变编码质量.
// !
// # val (o, compressLevelArg) = it
// # val image = if (o is String) read(scriptRuntime, arrayOf<Any>(o, true)) else o
// # require(image is ImageWrapper) { "Argument image for images.compress must be a ImageWrapper" }
// # val compressLevel = coerceNumber(compressLevelArg, 1.0)
// # val level = 2.0.pow(floor(ln(compressLevel.coerceAtLeast(1.0)) / ln(2.0))).toInt()
// # val outputStream = ByteArrayOutputStream()
// # image.bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
// # val byteArray = outputStream.toByteArray()
// # val options = BitmapFactory.Options().apply { inSampleSize = level }
// # val bitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size, options)
// # ImageWrapper.ofBitmap(bitmap).also { image.shoot() }
val (o, format, quality) = it
val image = if (o is String) read(scriptRuntime, arrayOf<Any>(o, true)) else o val image = if (o is String) read(scriptRuntime, arrayOf<Any>(o, true)) else o
require(image is ImageWrapper) { "Argument image for images.compress must be a ImageWrapper" } require(image is ImageWrapper) { "Argument image for images.compress must be a ImageWrapper" }
val compressLevel = coerceNumber(compressLevelArg, 1.0) scriptRuntime.images.compress(
val level = 2.0.pow(floor(ln(compressLevel.coerceAtLeast(1.0)) / ln(2.0))).toInt() /* image = */ image,
val outputStream = ByteArrayOutputStream() /* format = */ parseImageFormat(format),
image.bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream) /* quality = */ parseQuality(quality, DEFAULT_IMAGE_COMPRESS_QUALITY),
val byteArray = outputStream.toByteArray() )
val options = BitmapFactory.Options().apply { inSampleSize = level } }
val bitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size, options)
ImageWrapper.ofBitmap(bitmap).also { image.shoot() } @JvmStatic
@RhinoRuntimeFunctionInterface
fun compressToBytes(scriptRuntime: ScriptRuntime, args: Array<out Any?>): ByteArray = ensureArgumentsLengthInRange(args, 1..3) {
val (o, format, quality) = it
val image = if (o is String) read(scriptRuntime, arrayOf<Any>(o, true)) else o
require(image is ImageWrapper) { "Argument image for images.compressToBytes must be a ImageWrapper" }
scriptRuntime.images.compressToBytes(
/* image = */ image,
/* format = */ parseImageFormat(format),
/* quality = */ parseQuality(quality, DEFAULT_IMAGE_COMPRESS_QUALITY),
)
}
@JvmStatic
@RhinoRuntimeFunctionInterface
fun downsample(scriptRuntime: ScriptRuntime, args: Array<out Any?>): ImageWrapper = ensureArgumentsLengthInRange(args, 3..4) { argList ->
val (src, reqWidthArg, reqHeightArg, withAlphaArg) = argList
val reqWidth = coerceIntNumber(reqWidthArg)
val reqHeight = coerceIntNumber(reqHeightArg)
val withAlpha = coerceBoolean(withAlphaArg, true)
when (src) {
is ByteArray -> BitmapUtils.downsample(src, reqWidth, reqHeight, withAlpha)
is String -> when {
src.isUri() -> BitmapUtils.downsample(scriptRuntime.uiHandler.applicationContext, src.toUri(), reqWidth, reqHeight, withAlpha)
else -> BitmapUtils.downsample(scriptRuntime.files.nonNullPath(src), reqWidth, reqHeight, withAlpha)
}
is URL -> BitmapUtils.downsample(src, reqWidth, reqHeight, withAlpha)
is Uri -> BitmapUtils.downsample(scriptRuntime.uiHandler.applicationContext, src, reqWidth, reqHeight, withAlpha)
is Bitmap -> BitmapUtils.downsample(src, reqWidth, reqHeight, withAlpha)
is ImageWrapper -> BitmapUtils.downsample(src.bitmap, reqWidth, reqHeight, withAlpha).also { src.shoot() }
else -> throw WrappedIllegalArgumentException("Argument src ${src.jsBrief()} is invalid for images.downsample()")
}?.let {
ImageWrapper.ofBitmap(scriptRuntime, it)
} ?: throw WrappedRuntimeException("Failed to downsample image from ${src.jsBrief()}")
} }
@JvmStatic @JvmStatic

View File

@@ -1,14 +1,20 @@
package org.autojs.autojs.util package org.autojs.autojs.util
import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Matrix import android.graphics.Matrix
import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.net.Uri
import androidx.core.graphics.createBitmap import androidx.core.graphics.createBitmap
import androidx.core.graphics.scale
import java.net.URL
/** /**
* Created by Stardust on Apr 22, 2017. * Created by Stardust on Apr 22, 2017.
* Modified by SuperMonster003 as of Jan 21, 2023.
*/ */
object BitmapUtils { object BitmapUtils {
@@ -58,4 +64,164 @@ object BitmapUtils {
return rgba return rgba
} }
@JvmStatic
@Throws(DecodeException::class)
fun bitmapFromByteArrayOrThrow(bytes: ByteArray): Bitmap {
return bitmapFromByteArray(bytes) ?: throw DecodeException("Failed to decode bitmap from bytes")
}
@JvmStatic
fun bitmapFromByteArray(bytes: ByteArray): Bitmap? {
return BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
}
/**
* Decode and downsample based on inSampleSize (zh-CN: 解码并按 inSampleSize 降采样)
*
* @param data Compressed image byte stream (zh-CN: 压缩格式图片字节流)
* @param reqWidth Target max width (zh-CN: 目标最大宽)
* @param reqHeight Target max height (zh-CN: 目标最大高)
* @param withAlpha Whether to keep alpha channel; default true=ARGB_8888, false=RGB_565.<br>
* zh-CN: 是否保留透明通道; 默认 true=ARGB_8888, false=RGB_565.
*/
@JvmStatic
@JvmOverloads
fun downsample(
data: ByteArray,
reqWidth: Int,
reqHeight: Int,
withAlpha: Boolean = true,
): Bitmap? {
val opts = BitmapFactory.Options().apply { inJustDecodeBounds = true }
BitmapFactory.decodeByteArray(data, 0, data.size, opts)
opts.inSampleSize = computeSampleSize(opts.outWidth, opts.outHeight, reqWidth, reqHeight)
opts.inJustDecodeBounds = false
opts.inPreferredConfig = if (withAlpha) Bitmap.Config.ARGB_8888 else Bitmap.Config.RGB_565
val bmp = BitmapFactory.decodeByteArray(data, 0, data.size, opts)
if (!withAlpha) bmp?.setHasAlpha(false)
return bmp
}
@JvmStatic
@JvmOverloads
fun downsample(
filePath: String,
reqWidth: Int,
reqHeight: Int,
withAlpha: Boolean = true,
): Bitmap? {
val opts = BitmapFactory.Options().apply { inJustDecodeBounds = true }
BitmapFactory.decodeFile(filePath, opts)
opts.inSampleSize = computeSampleSize(opts.outWidth, opts.outHeight, reqWidth, reqHeight)
opts.inJustDecodeBounds = false
opts.inPreferredConfig = if (withAlpha) Bitmap.Config.ARGB_8888 else Bitmap.Config.RGB_565
val bmp = BitmapFactory.decodeFile(filePath, opts)
if (!withAlpha) bmp?.setHasAlpha(false)
return bmp
}
@JvmStatic
@JvmOverloads
fun downsample(
url: URL,
reqWidth: Int,
reqHeight: Int,
withAlpha: Boolean = true,
): Bitmap? = runCatching {
// Need to run in coroutine or thread in production to avoid blocking main thread.
// zh-CN: 生产环境需放到协程或线程中, 避免阻塞主线程.
val bytes = url.openStream().use { it.readBytes() }
downsample(bytes, reqWidth, reqHeight, withAlpha)
}.getOrNull()
@JvmStatic
@JvmOverloads
fun downsample(
src: Bitmap,
reqWidth: Int,
reqHeight: Int,
withAlpha: Boolean = true,
): Bitmap {
// Already a Bitmap, cannot use inSampleSize, can only scale proportionally.
// zh-CN: 已经是 Bitmap, 无法再用 inSampleSize, 只能等比缩放.
val scale = calcScale(src.width, src.height, reqWidth, reqHeight)
val dstW = (src.width / scale).coerceAtLeast(1)
val dstH = (src.height / scale).coerceAtLeast(1)
val scaled = src.scale(dstW, dstH)
// If need to adjust pixel format / alpha channel.
// zh-CN: 如需调整像素格式 / 透明通道.
return when (withAlpha) {
scaled.hasAlpha() -> scaled
else -> scaled.copy(
if (withAlpha) Bitmap.Config.ARGB_8888 else Bitmap.Config.RGB_565,
/* isMutable = */ false,
).apply { if (!withAlpha) setHasAlpha(false) }
}
}
@JvmStatic
@JvmOverloads
fun downsample(
context: Context,
uri: Uri,
reqWidth: Int,
reqHeight: Int,
withAlpha: Boolean = true,
): Bitmap? = runCatching {
val opts = BitmapFactory.Options().apply { inJustDecodeBounds = true }
context.contentResolver.openInputStream(uri)?.use {
BitmapFactory.decodeStream(it, null, opts)
} ?: return@runCatching null
opts.inSampleSize = computeSampleSize(opts.outWidth, opts.outHeight, reqWidth, reqHeight)
opts.inJustDecodeBounds = false
opts.inPreferredConfig = if (withAlpha) Bitmap.Config.ARGB_8888 else Bitmap.Config.RGB_565
val bmp = context.contentResolver.openInputStream(uri)?.use {
BitmapFactory.decodeStream(it, null, opts)
}
if (!withAlpha) bmp?.setHasAlpha(false)
bmp
}.getOrNull()
private fun calcScale(
srcW: Int, srcH: Int,
dstW: Int, dstH: Int,
): Int {
var scale = 1
if (srcH > dstH || srcW > dstW) {
while ((srcH / scale) > dstH || (srcW / scale) > dstW) {
scale++
}
}
return scale
}
@JvmStatic
fun computeSampleSize(outWidth: Int, outHeight: Int, reqWidth: Int, reqHeight: Int): Int {
var inSampleSize = 1
if (outHeight > reqHeight || outWidth > reqWidth) {
val halfHeight = outHeight / 2
val halfWidth = outWidth / 2
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
// zh-CN: 计算满足所需高度和宽度的2的幂次方的最大采样率.
while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
inSampleSize *= 2
}
}
return inSampleSize
}
class DecodeException(message: String) : Exception(message)
} }

View File

@@ -1,6 +1,5 @@
package org.autojs.autojs.util package org.autojs.autojs.util
import android.graphics.BitmapFactory
import org.opencv.core.Core import org.opencv.core.Core
import org.opencv.core.CvType import org.opencv.core.CvType
import org.opencv.core.Mat import org.opencv.core.Mat
@@ -8,29 +7,6 @@ import org.opencv.imgproc.Imgproc
object ImageUtils { object ImageUtils {
@JvmStatic
fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
// Raw height and width of image (zh-CN: 图像的原始高度和宽度)
val height = options.outHeight
val width = options.outWidth
var inSampleSize = 1
if (height > reqHeight || width > reqWidth) {
val halfHeight = height / 2
val halfWidth = width / 2
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
// zh-CN: 计算满足所需高度和宽度的2的幂次方的最大采样率.
while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
inSampleSize *= 2
}
}
return inSampleSize
}
@JvmStatic @JvmStatic
fun Mat.to8UC3(): Mat { fun Mat.to8UC3(): Mat {
val src = this val src = this

View File

@@ -1,110 +1,156 @@
#include <jni.h> #include <jni.h>
#include <android/bitmap.h>
#include <android/log.h>
#include <vector> #include <vector>
#include <setjmp.h>
#include "libimagequant.h" #include "libimagequant.h"
#include "png.h" #include "png.h"
#include <android/log.h>
#define ALOGE(fmt, ...) __android_log_print(ANDROID_LOG_ERROR, "PNGQ", fmt, ##__VA_ARGS__) #define ALOGE(fmt, ...) __android_log_print(ANDROID_LOG_ERROR, "PNGQ", fmt, ##__VA_ARGS__)
/* RAII: 保证函数结束时一定会 unlockPixels(). */
class BitmapLocker {
public:
BitmapLocker(JNIEnv *env, jobject bmp)
: mEnv(env), mBmp(bmp), mPixels(nullptr), mLocked(false) {}
bool lock() {
if (AndroidBitmap_lockPixels(mEnv, mBmp, &mPixels) != 0 || !mPixels) {
return false;
}
mLocked = true;
return true;
}
void *pixels() const { return mPixels; }
~BitmapLocker() {
if (mLocked) {
AndroidBitmap_unlockPixels(mEnv, mBmp);
}
}
private:
JNIEnv *mEnv;
jobject mBmp;
void *mPixels;
bool mLocked;
};
extern "C" extern "C"
JNIEXPORT jbyteArray JNICALL JNIEXPORT jbyteArray
JNICALL
Java_org_autojs_autojs_runtime_api_PngQuantBridge_quantize( Java_org_autojs_autojs_runtime_api_PngQuantBridge_quantize(
JNIEnv *env, jclass, jbyteArray srcRgba, JNIEnv *env, jclass /*cls*/, jobject jbitmap, jint quality) {
jint width, jint height, jint quality) {
/* 读取像素. */ /* 参数检查 & Bitmap 信息. */
if (!jbitmap) {
env->ThrowNew(env->FindClass("java/lang/IllegalArgumentException"), "Bitmap is null");
return nullptr;
}
AndroidBitmapInfo info{};
if (AndroidBitmap_getInfo(env, jbitmap, &info) != 0 ||
info.format != ANDROID_BITMAP_FORMAT_RGBA_8888) {
env->ThrowNew(env->FindClass("java/lang/IllegalArgumentException"),
"Bitmap must be RGBA_8888");
return nullptr;
}
const int width = static_cast<int>(info.width);
const int height = static_cast<int>(info.height);
jsize len = env->GetArrayLength(srcRgba); /* 提前声明所有资源. */
std::vector<uint8_t> rgba(len); BitmapLocker locker(env, jbitmap);
env->GetByteArrayRegion(srcRgba, 0, len, reinterpret_cast<jbyte *>(rgba.data())); liq_attr *attr = nullptr;
liq_image *image = nullptr;
liq_result *res = nullptr;
std::vector <uint8_t> indexed; // 后面 resize()
const liq_palette *pal = nullptr;
std::vector <uint8_t> compressed; // 输出 PNG
if (len != width * height * 4) { /* 锁定像素. */
ALOGE("rgba length mismatch, len=%d, expect=%d", len, width * height * 4); if (!locker.lock()) {
ALOGE("AndroidBitmap_lockPixels failed");
env->ThrowNew(env->FindClass("java/io/IOException"),
"AndroidBitmap_lockPixels failed");
return nullptr; return nullptr;
} }
/* libimagequant 量化. */ /* libimagequant. */
attr = liq_attr_create();
liq_attr *attr = liq_attr_create();
if (!attr) { if (!attr) {
ALOGE("liq_attr_create failed"); ALOGE("liq_attr_create failed");
return nullptr; goto fail;
} }
if (quality < 0) quality = 0;
if (quality > 100) quality = 100;
liq_set_speed(attr, 8); liq_set_speed(attr, 8);
liq_set_quality(attr, quality, quality); liq_set_quality(attr, quality, quality);
liq_set_max_colors(attr, 256); liq_set_max_colors(attr, 256);
liq_image *image = liq_image_create_rgba(attr, rgba.data(), width, height, 0); image = liq_image_create_rgba(attr,
liq_result *res; static_cast<uint8_t *>(locker.pixels()),
width, height, 0);
liq_error err = liq_image_quantize(image, attr, &res); if (!image) {
if (err != LIQ_OK) { ALOGE("liq_image_create_rgba failed");
ALOGE("liq_image_quantize failed, code=%d", err); goto fail;
liq_image_destroy(image); }
liq_attr_destroy(attr); if (liq_image_quantize(image, attr, &res) != LIQ_OK || !res) {
return nullptr; ALOGE("liq_image_quantize failed");
goto fail;
} }
size_t outSize = width * height; /* Remap. */
std::vector<uint8_t> indexed(outSize); indexed.resize(static_cast<size_t>(width) * height);
liq_write_remapped_image(res, image, indexed.data(), outSize); liq_write_remapped_image(res, image, indexed.data(), indexed.size());
const liq_palette *pal = liq_get_palette(res);
/* 写索引色 PNG 到内存. */ pal = liq_get_palette(res);
if (!pal || pal->count == 0 || pal->count > 256) {
ALOGE("invalid palette, count=%d", pal ? pal->count : -1);
goto fail;
}
std::vector<uint8_t> compressed; /* libpng 写 PNG 到内存. */
do { {
/* palette 检查. */
if (!pal || pal->count == 0 || pal->count > 256) {
ALOGE("invalid palette count=%d", pal ? pal->count : -1);
goto png_fail;
}
/* libpng 结构体. */
png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING,
nullptr, nullptr,
[](png_structp png_ptr, png_const_charp msg) { [](png_structp png_ptr, png_const_charp msg) {
ALOGE("libpng error: %s", msg); __android_log_print(ANDROID_LOG_ERROR, "PNGQ", "libpng error: %s", msg);
longjmp(png_jmpbuf(png_ptr), 1); longjmp(png_jmpbuf(png_ptr), 1);
}, },
nullptr); nullptr);
if (!png_ptr) { if (!png_ptr) {
ALOGE("png_create_write_struct failed"); ALOGE("png_create_write_struct failed");
goto png_fail; goto fail;
} }
png_infop info_ptr = png_create_info_struct(png_ptr); png_infop info_ptr = png_create_info_struct(png_ptr);
if (!info_ptr) { if (!info_ptr) {
png_destroy_write_struct(&png_ptr, nullptr); png_destroy_write_struct(&png_ptr, nullptr);
goto png_fail; ALOGE("png_create_info_struct failed");
goto fail;
} }
if (setjmp(png_jmpbuf(png_ptr))) { // libpng error
/* longjmp error handler. */
if (setjmp(png_jmpbuf(png_ptr))) {
png_destroy_write_struct(&png_ptr, &info_ptr); png_destroy_write_struct(&png_ptr, &info_ptr);
goto png_fail; ALOGE("libpng longjmp error");
goto fail;
} }
/* custom mem writer. */ /* 自定义写函数 => vector. */
struct Writer { struct Writer {
static void PNGAPI write(png_structp png_ptr, png_bytep data, png_size_t len) { static void PNGAPI write(png_structp png_ptr, png_bytep data, png_size_t len) {
auto *vec = static_cast<std::vector<uint8_t> *>(png_get_io_ptr(png_ptr)); auto *vec = static_cast<std::vector <uint8_t> *>(png_get_io_ptr(png_ptr));
vec->insert(vec->end(), data, data + len); vec->insert(vec->end(), data, data + len);
} }
static void PNGAPI flush(png_structp) {} static void PNGAPI flush(png_structp) {}
}; };
std::vector<uint8_t> tmpBuf; png_set_write_fn(png_ptr, &compressed, Writer::write, Writer::flush);
png_set_write_fn(png_ptr, &tmpBuf, Writer::write, Writer::flush);
/* IHDR. */ /* IHDR. */
png_set_IHDR(png_ptr, info_ptr, png_set_IHDR(png_ptr, info_ptr, width, height,
static_cast<png_uint_32>(width), 8, PNG_COLOR_TYPE_PALETTE, PNG_INTERLACE_NONE,
static_cast<png_uint_32>(height), PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
8, // bit depth
PNG_COLOR_TYPE_PALETTE, PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
png_set_compression_level(png_ptr, 3); png_set_compression_level(png_ptr, 3);
png_set_filter(png_ptr, 0, PNG_FILTER_NONE); png_set_filter(png_ptr, 0, PNG_FILTER_NONE);
@@ -124,36 +170,38 @@ Java_org_autojs_autojs_runtime_api_PngQuantBridge_quantize(
png_write_info(png_ptr, info_ptr); png_write_info(png_ptr, info_ptr);
/* rows. */ /* Rows. */
std::vector<png_bytep> rows(height); std::vector <png_bytep> rows(height);
for (int y = 0; y < height; ++y) { for (int y = 0; y < height; ++y) {
rows[y] = indexed.data() + y * width; rows[y] = indexed.data() + y * width;
} }
png_write_image(png_ptr, rows.data()); png_write_image(png_ptr, rows.data());
png_write_end(png_ptr, info_ptr); png_write_end(png_ptr, info_ptr);
compressed.swap(tmpBuf);
/* 无论是否成功, 均销毁. */
png_destroy_write_struct(&png_ptr, &info_ptr); png_destroy_write_struct(&png_ptr, &info_ptr);
} while (false);
png_fail:
if (compressed.empty()) {
ALOGE("compress failed, return null to Java");
env->ThrowNew(env->FindClass("java/io/IOException"),
"pngquant native: compress failed");
return nullptr;
} }
/* 清理. */ /* 成功: 回传 Java. */
liq_result_destroy(res); if (compressed.empty()) {
liq_image_destroy(image); ALOGE("compression produced empty data");
liq_attr_destroy(attr); goto fail;
}
{
jbyteArray out = env->NewByteArray(static_cast<jsize>(compressed.size()));
env->SetByteArrayRegion(out, 0, static_cast<jsize>(compressed.size()),
reinterpret_cast<const jbyte *>(compressed.data()));
/* 释放 libimagequant 资源. */
liq_result_destroy(res);
liq_image_destroy(image);
liq_attr_destroy(attr);
return out;
}
/* 回 JNI. */ fail:
jbyteArray out = env->NewByteArray(compressed.size()); /* 统一错误处理. */
env->SetByteArrayRegion(out, 0, compressed.size(), if (res) liq_result_destroy(res);
reinterpret_cast<jbyte *>(compressed.data())); if (image) liq_image_destroy(image);
return out; if (attr) liq_attr_destroy(attr);
env->ThrowNew(env->FindClass("java/io/IOException"), "pngquant native failed");
return nullptr;
} }

View File

@@ -40,6 +40,7 @@ android {
externalNativeBuild { externalNativeBuild {
cmake { cmake {
cppFlags "-std=c++17" cppFlags "-std=c++17"
cFlags "-ljnigraphics"
} }
} }

View File

@@ -204,8 +204,8 @@ pluginManagement {
/* Print concerned info by `System.getProperties()`. */ /* Print concerned info by `System.getProperties()`. */
val isShowConcernedSystemProperties = true val isShowConcernedSystemProperties = true
val isCleanupPaddleOcr = true val isCleanupPaddleOcr = false
val isCleanupRapidOcr = true val isCleanupRapidOcr = false
val fallbackAgpVersion = "7.4.2" val fallbackAgpVersion = "7.4.2"
val fallbackKotlinVersion = "1.7.10" val fallbackKotlinVersion = "1.7.10"

View File

@@ -1,5 +1,5 @@
#Sun May 18 20:20:48 CST 2025 #Tue May 20 22:30:54 CST 2025
BUILD_TIME=1747570848298 BUILD_TIME=1747751454685
COMPILE_SDK_VERSION=35 COMPILE_SDK_VERSION=35
IMAGE_QUANT_CMAKE_VERSION=3.22.1 IMAGE_QUANT_CMAKE_VERSION=3.22.1
IMAGE_QUANT_NDK_VERSION=26.1.10909125 IMAGE_QUANT_NDK_VERSION=26.1.10909125
@@ -19,6 +19,6 @@ RAPID_OCR_OPENCV_MOBILE_LABEL_VERSION=13
RAPID_OCR_OPENCV_MOBILE_VERSION=4.5.3 RAPID_OCR_OPENCV_MOBILE_VERSION=4.5.3
TARGET_SDK_VERSION=35 TARGET_SDK_VERSION=35
TARGET_SDK_VERSION_INRT=29 TARGET_SDK_VERSION_INRT=29
VERSION_BUILD=3234 VERSION_BUILD=3238
VERSION_NAME=6.6.3 Alpha5 VERSION_NAME=6.6.3 Alpha5
VSCODE_EXT_REQUIRED_VERSION=1.0.8 VSCODE_EXT_REQUIRED_VERSION=1.0.8