diff --git a/.changelog/lang_zh-Hans.json b/.changelog/lang_zh-Hans.json
index 2e2ff768..557e762b 100644
--- a/.changelog/lang_zh-Hans.json
+++ b/.changelog/lang_zh-Hans.json
@@ -1,13 +1,15 @@
{
"$data": {
"v6.6.3": {
- "released_date": "2025/05/14",
+ "released_date": "2025/05/21",
"feature": [
"版本历史功能, 可查看发行版本历史更新记录 (多语言) 与统计数据",
"timers.keepAlive 方法 (已全局化), 用于保持脚本活跃状态",
"engines.on('start/stop/error', callback) 等事件监听方法, 用于监听脚本引擎全局事件",
"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": [
"主页文档标签显示在线文档时部分内容被系统导航栏遮挡的问题",
@@ -21,6 +23,8 @@
"使用 JavaAdapter 时导致 ClassLoader 调用栈溢出的问题 _[`issue #376`](http://issues.autojs6.com/376)_",
"console.setContentTextColor 方法导致日志字体颜色丢失默认值的问题 _[`issue #346`](http://issues.autojs6.com/346)_",
"console.setContentBackgroundColor 方法无法接受颜色名称参数的问题 _[`issue #384`](http://issues.autojs6.com/384)_",
+ "images.compress 方法实现原理由像素降采样修正为编码质量变化",
+ "images.resize 方法无法正常使用的问题",
"README.md 中部分语言日期格式不正确的问题",
"Gradle 构建脚本可能因获取到无效库档案文件长度而导致构建失败的问题 _[`issue #389`](http://issues.autojs6.com/389)_"
],
@@ -34,6 +38,7 @@
"文件管理器浮动按钮展开后点击其他区域可自动隐藏",
"打包单文件时自动读取并勾选已安装应用的声明权限 _[`issue #362`](http://issues.autojs6.com/362)_",
"意图相关操作 (编辑/查看/安装/发送/播放等) 增加操作异常提示",
+ "ImageWrapper#saveTo 方法的路径参数支持相对路径",
"images.save 方法使用 quality 参数时支持 png 格式的文件体积压缩 _[`issue #367`](http://issues.autojs6.com/367)_",
"已忽略更新记录及客户端模式连接地址记录支持清空操作",
"版本更新信息支持多语言显示 (与当前显示语言同步)",
diff --git a/app/src/main/java/org/autojs/autojs/core/accessibility/SimpleActionAutomator.kt b/app/src/main/java/org/autojs/autojs/core/accessibility/SimpleActionAutomator.kt
index d2026247..393fb424 100644
--- a/app/src/main/java/org/autojs/autojs/core/accessibility/SimpleActionAutomator.kt
+++ b/app/src/main/java/org/autojs/autojs/core/accessibility/SimpleActionAutomator.kt
@@ -242,7 +242,7 @@ class SimpleActionAutomator(private val accessibilityBridge: AccessibilityBridge
val bitmap = hardwareBuffer.copy(Bitmap.Config.ARGB_8888, true)
hardwareBuffer.recycle()
- promiseAdapter.resolve(ImageWrapper.ofBitmap(bitmap))
+ promiseAdapter.resolve(ImageWrapper.ofBitmap(scriptRuntime, bitmap))
}
mPromiseAdapter = null
}
diff --git a/app/src/main/java/org/autojs/autojs/core/graphics/ScriptCanvas.java b/app/src/main/java/org/autojs/autojs/core/graphics/ScriptCanvas.java
index 49a75dd2..9a0eae36 100644
--- a/app/src/main/java/org/autojs/autojs/core/graphics/ScriptCanvas.java
+++ b/app/src/main/java/org/autojs/autojs/core/graphics/ScriptCanvas.java
@@ -11,11 +11,10 @@ import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
-
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-
import org.autojs.autojs.core.image.ImageWrapper;
+import org.autojs.autojs.runtime.ScriptRuntime;
/**
* Created by Stardust on Mar 22, 2018.
@@ -23,24 +22,27 @@ import org.autojs.autojs.core.image.ImageWrapper;
@SuppressWarnings("unused")
public class ScriptCanvas {
+ private final ScriptRuntime mScriptRuntime;
+
private Canvas mCanvas;
private Bitmap mBitmap;
- public ScriptCanvas(int width, int height) {
- this(Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888));
+ public ScriptCanvas(ScriptRuntime scriptRuntime, int width, int height) {
+ 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);
mBitmap = bitmap;
}
- public ScriptCanvas(@NonNull ImageWrapper image) {
- this(image.getBitmap().copy(image.getBitmap().getConfig(), true));
+ public ScriptCanvas(ScriptRuntime scriptRuntime, @NonNull ImageWrapper image) {
+ this(scriptRuntime, image.getBitmap().copy(image.getBitmap().getConfig(), true));
}
- public ScriptCanvas() {
- /* Empty body. */
+ public ScriptCanvas(ScriptRuntime scriptRuntime) {
+ mScriptRuntime = scriptRuntime;
}
public Canvas getAndroidCanvas() {
@@ -52,7 +54,7 @@ public class ScriptCanvas {
}
public ImageWrapper toImage() {
- return ImageWrapper.ofBitmap(mBitmap.copy(mBitmap.getConfig(), true));
+ return ImageWrapper.ofBitmap(mScriptRuntime, mBitmap.copy(mBitmap.getConfig(), true));
}
public boolean isHardwareAccelerated() {
diff --git a/app/src/main/java/org/autojs/autojs/core/image/CapturedImage.kt b/app/src/main/java/org/autojs/autojs/core/image/CapturedImage.kt
index 4406722a..bfba107d 100644
--- a/app/src/main/java/org/autojs/autojs/core/image/CapturedImage.kt
+++ b/app/src/main/java/org/autojs/autojs/core/image/CapturedImage.kt
@@ -1,12 +1,14 @@
package org.autojs.autojs.core.image
import android.media.Image
+import org.autojs.autojs.runtime.ScriptRuntime
/**
* 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.
-class CapturedImage(image: Image) : ImageWrapper(image) {
+class CapturedImage(scriptRuntime: ScriptRuntime, image: Image) : ImageWrapper(scriptRuntime, image) {
override fun recycle() {
/* Doing nothing to suppress default recycle method. */
diff --git a/app/src/main/java/org/autojs/autojs/core/image/ImageWrapper.kt b/app/src/main/java/org/autojs/autojs/core/image/ImageWrapper.kt
index c5c5b1cc..86342825 100644
--- a/app/src/main/java/org/autojs/autojs/core/image/ImageWrapper.kt
+++ b/app/src/main/java/org/autojs/autojs/core/image/ImageWrapper.kt
@@ -4,12 +4,15 @@ import android.graphics.Bitmap
import android.graphics.Bitmap.CompressFormat
import android.graphics.Color
import android.media.Image
+import androidx.core.graphics.createBitmap
+import androidx.core.graphics.get
import org.autojs.autojs.annotation.ScriptInterface
import org.autojs.autojs.core.opencv.Mat
import org.autojs.autojs.core.opencv.OpenCVHelper
import org.autojs.autojs.core.ref.MonitorResource
import org.autojs.autojs.core.ref.NativeObjectReference
import org.autojs.autojs.pio.UncheckedIOException
+import org.autojs.autojs.runtime.ScriptRuntime
import org.autojs.autojs.runtime.api.Images
import org.autojs.autojs.util.StringUtils.str
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.
open class ImageWrapper : Recyclable, MonitorResource {
+ private var mScriptRuntime: ScriptRuntime
+
private var mMat: Mat? = null
private var mBgrMat: Mat? = null
private var mBitmap: Bitmap? = null
@@ -61,7 +66,7 @@ open class ImageWrapper : Recyclable, MonitorResource {
ensureNotRecycled()
if (mBitmap == 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)
} else {
mBitmap = mediaImage?.let { toBitmap(it) }
@@ -100,23 +105,26 @@ open class ImageWrapper : Recyclable, MonitorResource {
}
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()
mBitmap = bitmap.also { addToList(it) }
mWidth = bitmap.width
mHeight = bitmap.height
}
- constructor(mat: Mat) {
+ constructor(scriptRuntime: ScriptRuntime, mat: Mat) {
+ mScriptRuntime = scriptRuntime
mId = mNextId.incrementAndGet()
mMat = mat.also { addToList(it) }
mWidth = mat.cols()
mHeight = mat.rows()
}
- constructor(mat: org.opencv.core.Mat) {
+ constructor(scriptRuntime: ScriptRuntime, mat: org.opencv.core.Mat) {
+ mScriptRuntime = scriptRuntime
mId = mNextId.incrementAndGet()
mMat = when (mat.nativeObj != 0L) {
true -> Mat(mat.nativeObj)
@@ -126,7 +134,8 @@ open class ImageWrapper : Recyclable, MonitorResource {
mHeight = mat.rows()
}
- constructor(bitmap: Bitmap, mat: Mat?) {
+ constructor(scriptRuntime: ScriptRuntime, bitmap: Bitmap, mat: Mat?) {
+ mScriptRuntime = scriptRuntime
mId = mNextId.incrementAndGet()
mMat = mat?.also { addToList(it) }
mBitmap = bitmap.also { addToList(it) }
@@ -134,7 +143,8 @@ open class ImageWrapper : Recyclable, MonitorResource {
mHeight = bitmap.height
}
- constructor(mediaImage: Image) {
+ constructor(scriptRuntime: ScriptRuntime, mediaImage: Image) {
+ mScriptRuntime = scriptRuntime
mId = mNextId.incrementAndGet()
this.mediaImage = mediaImage.also { addToList(it) }
mWidth = mediaImage.width
@@ -149,27 +159,23 @@ open class ImageWrapper : Recyclable, MonitorResource {
imageList.add(WeakReference(image))
}
- fun saveTo(path: String?): Boolean {
+ fun saveTo(path: String): Boolean {
ensureNotRecycled()
- path ?: return false
+ val fullPath = mScriptRuntime.files.nonNullPath(path)
if (mBitmap == null) {
if (mMat != null) {
- return Imgcodecs.imwrite(path, mMat)
+ return Imgcodecs.imwrite(fullPath, mMat)
}
bitmap /* Getter, for initializing `mBitmap`. */
}
- return try {
- true.also { saveWithBitmap(path) }
- } catch (_: Exception) {
- false
- }
+ return runCatching { saveWithBitmap(fullPath) }.isSuccess
}
- private fun saveWithBitmap(path: String?) {
+ private fun saveWithBitmap(fullPath: String?) {
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!!.compress(CompressFormat.PNG, 100, FileOutputStream(path))
+ mBitmap!!.compress(CompressFormat.PNG, 100, FileOutputStream(fullPath))
} catch (e: FileNotFoundException) {
throw UncheckedIOException(e)
}
@@ -181,7 +187,7 @@ open class ImageWrapper : Recyclable, MonitorResource {
throw ArrayIndexOutOfBoundsException("Point ($x, $y) out of bounds of $this")
}
mBitmap?.let { oBitmap ->
- return oBitmap.getPixel(x, y)
+ return oBitmap[x, y]
}
mMat?.let { oMat ->
@@ -282,8 +288,8 @@ open class ImageWrapper : Recyclable, MonitorResource {
fun clone(): ImageWrapper {
ensureNotRecycled()
return when (val bitmap = mBitmap) {
- null -> ofMat(mat.clone())
- else -> ofBitmap(bitmap.copy(bitmap.config ?: Bitmap.Config.ARGB_8888, true))
+ null -> ofMat(mScriptRuntime, mat.clone())
+ else -> ofBitmap(mScriptRuntime, bitmap.copy(bitmap.config ?: Bitmap.Config.ARGB_8888, true))
}
}
@@ -312,16 +318,16 @@ open class ImageWrapper : Recyclable, MonitorResource {
}
@JvmStatic
- fun ofImage(image: Image) = ImageWrapper(image)
+ fun ofImage(scriptRuntime: ScriptRuntime, image: Image) = ImageWrapper(scriptRuntime, image)
@JvmStatic
- fun ofMat(mat: Mat) = ImageWrapper(mat)
+ fun ofMat(scriptRuntime: ScriptRuntime, mat: Mat) = ImageWrapper(scriptRuntime, mat)
@JvmStatic
- fun ofMat(mat: org.opencv.core.Mat) = ImageWrapper(mat)
+ fun ofMat(scriptRuntime: ScriptRuntime, mat: org.opencv.core.Mat) = ImageWrapper(scriptRuntime, mat)
@JvmStatic
- fun ofBitmap(bitmap: Bitmap) = ImageWrapper(bitmap)
+ fun ofBitmap(scriptRuntime: ScriptRuntime, bitmap: Bitmap) = ImageWrapper(scriptRuntime, bitmap)
@ScriptInterface
fun toBitmap(image: Image): Bitmap {
@@ -329,11 +335,7 @@ open class ImageWrapper : Recyclable, MonitorResource {
val buffer = plane.buffer.apply { position(0) }
val pixelStride = plane.pixelStride
val rowPadding = plane.rowStride - pixelStride * image.width
- val bitmap = Bitmap.createBitmap(
- image.width + rowPadding / pixelStride,
- image.height,
- Bitmap.Config.ARGB_8888,
- ).apply { copyPixelsFromBuffer(buffer) }
+ val bitmap = createBitmap(image.width + rowPadding / pixelStride, image.height).apply { copyPixelsFromBuffer(buffer) }
return when (rowPadding == 0) {
true -> bitmap
else -> Bitmap.createBitmap(bitmap, 0, 0, image.width, image.height)
diff --git a/app/src/main/java/org/autojs/autojs/core/ui/widget/JsCanvasView.kt b/app/src/main/java/org/autojs/autojs/core/ui/widget/JsCanvasView.kt
index 819e02bf..4dcc7288 100644
--- a/app/src/main/java/org/autojs/autojs/core/ui/widget/JsCanvasView.kt
+++ b/app/src/main/java/org/autojs/autojs/core/ui/widget/JsCanvasView.kt
@@ -62,7 +62,7 @@ class JsCanvasView : TextureView, TextureView.SurfaceTextureListener {
execute {
var canvas: Canvas? = null
var time = SystemClock.uptimeMillis()
- val scriptCanvas = ScriptCanvas()
+ val scriptCanvas = ScriptCanvas(mScriptRuntime)
try {
while (mDrawing) {
canvas = lockCanvas()
diff --git a/app/src/main/java/org/autojs/autojs/extension/StringExtensions.kt b/app/src/main/java/org/autojs/autojs/extension/StringExtensions.kt
index aee8c628..bb5a08de 100644
--- a/app/src/main/java/org/autojs/autojs/extension/StringExtensions.kt
+++ b/app/src/main/java/org/autojs/autojs/extension/StringExtensions.kt
@@ -1,5 +1,6 @@
package org.autojs.autojs.extension
+import androidx.core.net.toUri
import java.text.Normalizer
object StringExtensions {
@@ -59,4 +60,12 @@ object StringExtensions {
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
+ }
+
}
diff --git a/app/src/main/java/org/autojs/autojs/runtime/api/Images.java b/app/src/main/java/org/autojs/autojs/runtime/api/Images.java
index fb4a580c..210e0e49 100644
--- a/app/src/main/java/org/autojs/autojs/runtime/api/Images.java
+++ b/app/src/main/java/org/autojs/autojs/runtime/api/Images.java
@@ -62,9 +62,37 @@ import static org.autojs.autojs.util.RhinoUtils.isMainThread;
import static org.autojs.autojs.util.StringUtils.str;
/**
+ * Quick Reference – Image Size / Quality Helpers
+ * zh-CN: 快速参考 – 图像尺寸 / 质量相关辅助方法
+ *
+ * scale
+ * Multiply both width and height of an already-decoded Bitmap by
+ * the given scale factor(s).
+ * zh-CN: 在 已解码 的 Bitmap 上, 按照给定比例同时缩放宽高.
+ *
+ * resize
+ * Force an already-decoded Bitmap to the specified width/height.
+ * Aspect ratio may be kept or ignored depending on the overload.
+ * zh-CN: 将 已解码 Bitmap 调整为指定宽高; 可选择保持或忽略纵横比.
+ *
+ * downsample
+ * Shrink before decoding by setting inSampleSize; pixels are
+ * skipped while reading, dramatically reducing memory.
+ * zh-CN: 解码前 通过设置 inSampleSize 跳读像素, 显著降低解码分辨率与内存.
+ *
+ * compress
+ * Re-encode an existing Bitmap (or byte stream) into JPEG/PNG/WebP
+ * with the given format & quality to reduce disk footprint.
+ * zh-CN: 使用指定格式与质量重新编码 Bitmap (或字节流), 以减小 磁盘 体积.
+ *
+ * save
+ * Convenience wrapper that internally invokes compress (with
+ * default or user-supplied options) and then writes to storage.
+ * zh-CN: 便捷封装, 内部调用 compress (默认或自定义参数) 后写入存储.
+ *
* Created by Stardust on May 20, 2017.
+ * Modified by SuperMonster003 as of Dec 1, 2021.
*/
-@SuppressWarnings("unused")
public class Images {
private static final String TAG = Images.class.getSimpleName();
@@ -130,7 +158,7 @@ public class Images {
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)) {
throw new IllegalArgumentException(str(R.string.error_illegal_argument, "direction", direction));
}
@@ -160,7 +188,7 @@ public class Images {
}
imgA.shoot();
imgB.shoot();
- return ImageWrapper.ofBitmap(bitmap);
+ return ImageWrapper.ofBitmap(scriptRuntime, bitmap);
}
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);
}
- protected static void setImageCaptureCallback(OnScreenCaptureAvailableListener onScreenCaptureAvailableListener, Image image) {
- ImageWrapper ofImage = ImageWrapper.ofImage(image);
+ protected static void setImageCaptureCallback(ScriptRuntime scriptRuntime, OnScreenCaptureAvailableListener onScreenCaptureAvailableListener, Image image) {
+ ImageWrapper ofImage = ImageWrapper.ofImage(scriptRuntime, image);
onScreenCaptureAvailableListener.onCaptureAvailable(ofImage);
// ofImage.recycle();
}
@@ -238,7 +266,7 @@ public class Images {
mPreCaptureImage.recycleInternal();
}
if (capture != null) {
- mPreCaptureImage = new CapturedImage(capture);
+ mPreCaptureImage = new CapturedImage(mScriptRuntime, capture);
}
}
return mPreCaptureImage;
@@ -247,7 +275,7 @@ public class Images {
public boolean captureScreen(String path) {
ImageWrapper image = captureScreen();
- return image != null && image.saveTo(mScriptRuntime.files.nonNullPath(path));
+ return image != null && image.saveTo(path);
}
public ImageWrapper copy(@NonNull ImageWrapper image) {
@@ -256,18 +284,12 @@ public class Images {
return imageWrapper;
}
- public boolean save(@NonNull ImageWrapper image,
- @NonNull String path,
- @NonNull String format,
- int quality) throws IOException {
-
+ public boolean save(@NonNull ImageWrapper image, @NonNull String path, @NonNull String format, int quality) throws IOException {
Bitmap bitmap = image.getBitmap();
Bitmap.CompressFormat compressFormat = parseImageFormat(format);
if (compressFormat == Bitmap.CompressFormat.PNG && quality != 100) {
- // ARGB_8888 -> RGBA[]
- byte[] rgba = BitmapUtils.bitmapToRgba(bitmap);
- byte[] compressed = PngQuantBridge.quantize(rgba, bitmap.getWidth(), bitmap.getHeight(), quality);
+ byte[] compressed = PngQuantBridge.quantize(bitmap, quality);
if (compressed != null) {
try (FileOutputStream fos = new FileOutputStream(path)) {
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() {
// EventEmitter eventEmitter = new EventEmitter(scriptRuntime.bridges, scriptRuntime.timers.getTimerForCurrentThread());
// 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) {
Matrix matrix = new Matrix();
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();
return imageWrapper;
}
@@ -313,23 +365,27 @@ public class Images {
Bitmap original = img.getBitmap();
Matrix matrix = new Matrix();
- // 根据传入参数设置缩放比例
+ // Set scaling ratio according to input parameters.
+ // zh-CN: 根据传入参数设置缩放比例.
float sx = horizontal ? -1.0f : 1.0f;
float sy = vertical ? -1.0f : 1.0f;
matrix.preScale(sx, sy);
- // 水平方向翻转后, 平移到图像宽度的位置
+ // After horizontal flip, translate to image width position.
+ // zh-CN: 水平方向翻转后, 平移到图像宽度的位置.
if (horizontal) matrix.postTranslate(original.getWidth(), 0);
- // 垂直方向翻转后, 平移到图像高度的位置
+
+ // After vertical flip, translate to image height position.
+ // zh-CN: 垂直方向翻转后, 平移到图像高度的位置.
if (vertical) matrix.postTranslate(0, original.getHeight());
Bitmap flipped = Bitmap.createBitmap(original, 0, 0, original.getWidth(), original.getHeight(), matrix, true);
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) {
- 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();
return imageWrapper;
}
@@ -346,7 +402,7 @@ public class Images {
if (!isStrict) return null;
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
@@ -357,7 +413,7 @@ public class Images {
}
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) {
@@ -375,8 +431,8 @@ public class Images {
return outputStream.toByteArray();
}
- public ImageWrapper fromBytes(byte[] bytes) {
- return ImageWrapper.ofBitmap(BitmapFactory.decodeByteArray(bytes, 0, bytes.length));
+ public ImageWrapper fromBytes(byte[] bytes) throws BitmapUtils.DecodeException {
+ return ImageWrapper.ofBitmap(mScriptRuntime, BitmapUtils.bitmapFromByteArrayOrThrow(bytes));
}
private Bitmap.CompressFormat parseImageFormat(String format) {
@@ -405,7 +461,7 @@ public class Images {
HttpURLConnection connection = (HttpURLConnection) new URL(src).openConnection();
connection.setDoInput(true);
connection.connect();
- return ImageWrapper.ofBitmap(BitmapFactory.decodeStream(connection.getInputStream()));
+ return ImageWrapper.ofBitmap(mScriptRuntime, BitmapFactory.decodeStream(connection.getInputStream()));
} catch (IOException e) {
return null;
}
@@ -441,7 +497,7 @@ public class Images {
channels.add(3, alphaChannel); // 将原来的 alpha 通道放回最后一个位置
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
Bitmap invertedBitmap = Bitmap.createBitmap(originalWidth, originalHeight, Bitmap.Config.ARGB_8888);
@@ -449,7 +505,7 @@ public class Images {
image.shoot();
- return ImageWrapper.ofBitmap(invertedBitmap);
+ return ImageWrapper.ofBitmap(mScriptRuntime, invertedBitmap);
}
public void releaseScreenCapturer() {
@@ -521,18 +577,20 @@ public class Images {
shouldReleaseMat = true;
}
@Nullable
- org.opencv.core.Point point = TemplateMatching.singleTemplateMatching(
- src,
- template.getMat(),
- new TemplateMatching.Options(-1, weakThreshold, strictThreshold, maxLevel)
- );
-
- if (shouldReleaseMat) {
- OpenCVHelper.release(src);
+ org.opencv.core.Point point;
+ try {
+ point = TemplateMatching.singleTemplateMatching(
+ src,
+ template.getMat(),
+ new TemplateMatching.Options(-1, weakThreshold, strictThreshold, maxLevel)
+ );
+ } finally {
+ if (shouldReleaseMat) {
+ OpenCVHelper.release(src);
+ }
+ image.shoot();
+ template.shoot();
}
- image.shoot();
- template.shoot();
-
if (point != null) {
if (rect != null) {
point.x += rect.x;
@@ -605,7 +663,7 @@ public class Images {
}
public void setImageCaptureCallback(OnScreenCaptureAvailableListener onScreenCaptureAvailableListener) {
- mOnScreenCaptureAvailableListener = new ScreenCaptureAvailableHandler(onScreenCaptureAvailableListener);
+ mOnScreenCaptureAvailableListener = new ScreenCaptureAvailableHandler(mScriptRuntime, onScreenCaptureAvailableListener);
if (mScreenCapturer != null) {
mScreenCapturer.setImageCaptureCallback(mOnScreenCaptureAvailableListener);
}
diff --git a/app/src/main/java/org/autojs/autojs/runtime/api/PngQuantBridge.java b/app/src/main/java/org/autojs/autojs/runtime/api/PngQuantBridge.java
index 8dbc2b9b..15756f7f 100644
--- a/app/src/main/java/org/autojs/autojs/runtime/api/PngQuantBridge.java
+++ b/app/src/main/java/org/autojs/autojs/runtime/api/PngQuantBridge.java
@@ -1,5 +1,7 @@
package org.autojs.autojs.runtime.api;
+import android.graphics.Bitmap;
+
public class PngQuantBridge {
static {
@@ -35,12 +37,8 @@ public class PngQuantBridge {
* PNG 规范不支持在压缩阶段引入有损处理, 因此不会像 JPEG 那样再出现马赛克等失真.
*
*
- * @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 (0–100, lower = smaller file & potentially higher error)
* @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);
}
\ No newline at end of file
diff --git a/app/src/main/java/org/autojs/autojs/runtime/api/ScreenCaptureAvailableHandler.java b/app/src/main/java/org/autojs/autojs/runtime/api/ScreenCaptureAvailableHandler.java
index 4fe93d1b..eadf8368 100644
--- a/app/src/main/java/org/autojs/autojs/runtime/api/ScreenCaptureAvailableHandler.java
+++ b/app/src/main/java/org/autojs/autojs/runtime/api/ScreenCaptureAvailableHandler.java
@@ -4,18 +4,17 @@ import android.content.Context;
import android.media.Image;
import android.view.View;
import android.view.ViewGroup;
-
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-
import org.autojs.autojs.core.floaty.BaseResizableFloatyWindow;
import org.autojs.autojs.core.image.capture.ScreenCapturer;
+import org.autojs.autojs.runtime.ScriptRuntime;
/**
* Created 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
@Override
@@ -25,6 +24,6 @@ public record ScreenCaptureAvailableHandler(Images.OnScreenCaptureAvailableListe
@Override
public void onCaptureAvailable(Image image) {
- Images.setImageCaptureCallback(listener, image);
+ Images.setImageCaptureCallback(scriptRuntime, listener, image);
}
}
diff --git a/app/src/main/java/org/autojs/autojs/runtime/api/augment/images/Images.kt b/app/src/main/java/org/autojs/autojs/runtime/api/augment/images/Images.kt
index 6e79d03e..60f57265 100644
--- a/app/src/main/java/org/autojs/autojs/runtime/api/augment/images/Images.kt
+++ b/app/src/main/java/org/autojs/autojs/runtime/api/augment/images/Images.kt
@@ -3,8 +3,10 @@ package org.autojs.autojs.runtime.api.augment.images
import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.graphics.BitmapFactory
+import android.net.Uri
import android.util.Log
import android.view.Gravity
+import androidx.core.net.toUri
import org.autojs.autojs.annotation.RhinoRuntimeFunctionInterface
import org.autojs.autojs.core.image.ColorDetector
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.prop
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.api.ImageFeatureMatching
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.exception.ShouldNeverHappenException
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.coerceBoolean
import org.autojs.autojs.util.RhinoUtils.coerceFloatNumber
@@ -47,16 +52,9 @@ import org.mozilla.javascript.ScriptableObject
import org.opencv.core.CvType
import org.opencv.features2d.DescriptorMatcher
import org.opencv.imgproc.Imgproc
-import java.io.ByteArrayOutputStream
-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 java.net.URL
import kotlin.math.max
import kotlin.math.min
-import kotlin.math.pow
import kotlin.math.round
import kotlin.math.roundToInt
import kotlin.math.sqrt
@@ -101,7 +99,7 @@ class Images(scriptRuntime: ScriptRuntime) : Augmentable(scriptRuntime), AsEmitt
::bilateralFilter.name,
::cvtColor.name,
::findCircles.name,
- ::resizeInternal.name,
+ ::resize.name,
::scale.name,
::rotate.name,
::flip.name,
@@ -135,6 +133,8 @@ class Images(scriptRuntime: ScriptRuntime) : Augmentable(scriptRuntime), AsEmitt
::isRecycled.name,
::recycle.name,
::compress.name,
+ ::compressToBytes.name,
+ ::downsample.name,
::getSize.name,
::getWidth.name,
::getHeight.name,
@@ -160,6 +160,9 @@ class Images(scriptRuntime: ScriptRuntime) : Augmentable(scriptRuntime), AsEmitt
@JvmField
val DEFAULT_IMAGE_SAVE_QUALITY = 100
+ @JvmField
+ val DEFAULT_IMAGE_COMPRESS_QUALITY = 60
+
@JvmField
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(o, true)) else o
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" }
- 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
@@ -318,7 +326,12 @@ class Images(scriptRuntime: ScriptRuntime) : Augmentable(scriptRuntime), AsEmitt
val image = if (o is String) read(scriptRuntime, arrayOf(o, true)) else o
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" }
- 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
@@ -658,7 +671,7 @@ class Images(scriptRuntime: ScriptRuntime) : Augmentable(scriptRuntime), AsEmitt
val imageB = if (oB is String) read(scriptRuntime, arrayOf(oB, true)) else oB
require(imageB is ImageWrapper) { "Argument imageB for images.concat must be a ImageWrapper" }
initOpenCvIfNeeded()
- ApiImages.concat(imageA, imageB, directionToGravityToConcat(direction))
+ ApiImages.concat(scriptRuntime, imageA, imageB, directionToGravityToConcat(direction))
}
@JvmStatic
@@ -988,8 +1001,8 @@ class Images(scriptRuntime: ScriptRuntime) : Augmentable(scriptRuntime), AsEmitt
fun matToImage(scriptRuntime: ScriptRuntime, args: Array): ImageWrapper = ensureArgumentsOnlyOne(args) {
initOpenCvIfNeeded()
when (it) {
- is AutoJsMat -> ImageWrapper.ofMat(it)
- is OpencvMat -> ImageWrapper.ofMat(it)
+ is AutoJsMat -> ImageWrapper.ofMat(scriptRuntime, it)
+ is OpencvMat -> ImageWrapper.ofMat(scriptRuntime, it)
else -> throw WrappedIllegalArgumentException("Argument mat ${it.jsBrief()} is invalid for images.matToImage()")
}
}
@@ -1077,18 +1090,71 @@ class Images(scriptRuntime: ScriptRuntime) : Augmentable(scriptRuntime), AsEmitt
@JvmStatic
@RhinoRuntimeFunctionInterface
- fun compress(scriptRuntime: ScriptRuntime, args: Array): ImageWrapper = ensureArgumentsLength(args, 2) {
- val (o, compressLevelArg) = it
+ fun compress(scriptRuntime: ScriptRuntime, args: Array): ImageWrapper = ensureArgumentsLengthInRange(args, 1..3) {
+
+ // @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(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(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() }
+ scriptRuntime.images.compress(
+ /* image = */ image,
+ /* format = */ parseImageFormat(format),
+ /* quality = */ parseQuality(quality, DEFAULT_IMAGE_COMPRESS_QUALITY),
+ )
+ }
+
+ @JvmStatic
+ @RhinoRuntimeFunctionInterface
+ fun compressToBytes(scriptRuntime: ScriptRuntime, args: Array): ByteArray = ensureArgumentsLengthInRange(args, 1..3) {
+ val (o, format, quality) = it
+ val image = if (o is String) read(scriptRuntime, arrayOf(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): 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
diff --git a/app/src/main/java/org/autojs/autojs/util/BitmapUtils.kt b/app/src/main/java/org/autojs/autojs/util/BitmapUtils.kt
index aa852cbc..beb2254e 100644
--- a/app/src/main/java/org/autojs/autojs/util/BitmapUtils.kt
+++ b/app/src/main/java/org/autojs/autojs/util/BitmapUtils.kt
@@ -1,14 +1,20 @@
package org.autojs.autojs.util
+import android.content.Context
import android.graphics.Bitmap
+import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Matrix
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
+import android.net.Uri
import androidx.core.graphics.createBitmap
+import androidx.core.graphics.scale
+import java.net.URL
/**
* Created by Stardust on Apr 22, 2017.
+ * Modified by SuperMonster003 as of Jan 21, 2023.
*/
object BitmapUtils {
@@ -58,4 +64,164 @@ object BitmapUtils {
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.
+ * 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)
+
}
\ No newline at end of file
diff --git a/app/src/main/java/org/autojs/autojs/util/ImageUtils.kt b/app/src/main/java/org/autojs/autojs/util/ImageUtils.kt
index 0c805ee6..b814e5f6 100644
--- a/app/src/main/java/org/autojs/autojs/util/ImageUtils.kt
+++ b/app/src/main/java/org/autojs/autojs/util/ImageUtils.kt
@@ -1,6 +1,5 @@
package org.autojs.autojs.util
-import android.graphics.BitmapFactory
import org.opencv.core.Core
import org.opencv.core.CvType
import org.opencv.core.Mat
@@ -8,29 +7,6 @@ import org.opencv.imgproc.Imgproc
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
fun Mat.to8UC3(): Mat {
val src = this
diff --git a/libs/imagequant/PngQuantBridge.cpp b/libs/imagequant/PngQuantBridge.cpp
index 35a3501d..79b6ce83 100644
--- a/libs/imagequant/PngQuantBridge.cpp
+++ b/libs/imagequant/PngQuantBridge.cpp
@@ -1,110 +1,156 @@
#include
+#include
+#include
+
#include
+#include
#include "libimagequant.h"
#include "png.h"
-#include
#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"
-JNIEXPORT jbyteArray JNICALL
+JNIEXPORT jbyteArray
+
+JNICALL
Java_org_autojs_autojs_runtime_api_PngQuantBridge_quantize(
- JNIEnv *env, jclass, jbyteArray srcRgba,
- jint width, jint height, jint quality) {
+ JNIEnv *env, jclass /*cls*/, jobject jbitmap, 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(info.width);
+ const int height = static_cast(info.height);
- jsize len = env->GetArrayLength(srcRgba);
- std::vector rgba(len);
- env->GetByteArrayRegion(srcRgba, 0, len, reinterpret_cast(rgba.data()));
+ /* 提前声明所有资源. */
+ BitmapLocker locker(env, jbitmap);
+ liq_attr *attr = nullptr;
+ liq_image *image = nullptr;
+ liq_result *res = nullptr;
+ std::vector indexed; // 后面 resize()
+ const liq_palette *pal = nullptr;
+ std::vector 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;
}
- /* libimagequant 量化. */
-
- liq_attr *attr = liq_attr_create();
+ /* libimagequant. */
+ attr = liq_attr_create();
if (!attr) {
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_quality(attr, quality, quality);
liq_set_max_colors(attr, 256);
- liq_image *image = liq_image_create_rgba(attr, rgba.data(), width, height, 0);
- liq_result *res;
-
- liq_error err = liq_image_quantize(image, attr, &res);
- if (err != LIQ_OK) {
- ALOGE("liq_image_quantize failed, code=%d", err);
- liq_image_destroy(image);
- liq_attr_destroy(attr);
- return nullptr;
+ image = liq_image_create_rgba(attr,
+ static_cast(locker.pixels()),
+ width, height, 0);
+ if (!image) {
+ ALOGE("liq_image_create_rgba failed");
+ goto fail;
+ }
+ if (liq_image_quantize(image, attr, &res) != LIQ_OK || !res) {
+ ALOGE("liq_image_quantize failed");
+ goto fail;
}
- size_t outSize = width * height;
- std::vector indexed(outSize);
- liq_write_remapped_image(res, image, indexed.data(), outSize);
- const liq_palette *pal = liq_get_palette(res);
+ /* Remap. */
+ indexed.resize(static_cast(width) * height);
+ liq_write_remapped_image(res, image, indexed.data(), indexed.size());
- /* 写索引色 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 compressed;
- do {
- /* palette 检查. */
- if (!pal || pal->count == 0 || pal->count > 256) {
- ALOGE("invalid palette count=%d", pal ? pal->count : -1);
- goto png_fail;
- }
-
- /* libpng 结构体. */
+ /* libpng 写 PNG 到内存. */
+ {
png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING,
- nullptr,
- [](png_structp png_ptr, png_const_charp msg) {
- ALOGE("libpng error: %s", msg);
- longjmp(png_jmpbuf(png_ptr), 1);
- },
- nullptr);
+ nullptr,
+ [](png_structp png_ptr, png_const_charp msg) {
+ __android_log_print(ANDROID_LOG_ERROR, "PNGQ", "libpng error: %s", msg);
+ longjmp(png_jmpbuf(png_ptr), 1);
+ },
+ nullptr);
if (!png_ptr) {
ALOGE("png_create_write_struct failed");
- goto png_fail;
+ goto fail;
}
-
png_infop info_ptr = png_create_info_struct(png_ptr);
if (!info_ptr) {
png_destroy_write_struct(&png_ptr, nullptr);
- goto png_fail;
+ ALOGE("png_create_info_struct failed");
+ goto fail;
}
-
- /* longjmp error handler. */
- if (setjmp(png_jmpbuf(png_ptr))) {
+ if (setjmp(png_jmpbuf(png_ptr))) { // libpng error
png_destroy_write_struct(&png_ptr, &info_ptr);
- goto png_fail;
+ ALOGE("libpng longjmp error");
+ goto fail;
}
- /* custom mem writer. */
+ /* 自定义写函数 => vector. */
struct Writer {
static void PNGAPI write(png_structp png_ptr, png_bytep data, png_size_t len) {
- auto *vec = static_cast *>(png_get_io_ptr(png_ptr));
+ auto *vec = static_cast *>(png_get_io_ptr(png_ptr));
vec->insert(vec->end(), data, data + len);
}
+
static void PNGAPI flush(png_structp) {}
};
- std::vector tmpBuf;
- png_set_write_fn(png_ptr, &tmpBuf, Writer::write, Writer::flush);
+ png_set_write_fn(png_ptr, &compressed, Writer::write, Writer::flush);
/* IHDR. */
- png_set_IHDR(png_ptr, info_ptr,
- static_cast(width),
- static_cast(height),
- 8, // bit depth
- PNG_COLOR_TYPE_PALETTE, PNG_INTERLACE_NONE,
- PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
-
+ png_set_IHDR(png_ptr, info_ptr, width, height,
+ 8, PNG_COLOR_TYPE_PALETTE, PNG_INTERLACE_NONE,
+ PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
png_set_compression_level(png_ptr, 3);
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);
- /* rows. */
- std::vector rows(height);
+ /* Rows. */
+ std::vector rows(height);
for (int y = 0; y < height; ++y) {
rows[y] = indexed.data() + y * width;
}
png_write_image(png_ptr, rows.data());
-
png_write_end(png_ptr, info_ptr);
- compressed.swap(tmpBuf);
- /* 无论是否成功, 均销毁. */
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;
}
- /* 清理. */
- liq_result_destroy(res);
- liq_image_destroy(image);
- liq_attr_destroy(attr);
+ /* 成功: 回传 Java. */
+ if (compressed.empty()) {
+ ALOGE("compression produced empty data");
+ goto fail;
+ }
+ {
+ jbyteArray out = env->NewByteArray(static_cast(compressed.size()));
+ env->SetByteArrayRegion(out, 0, static_cast(compressed.size()),
+ reinterpret_cast(compressed.data()));
+ /* 释放 libimagequant 资源. */
+ liq_result_destroy(res);
+ liq_image_destroy(image);
+ liq_attr_destroy(attr);
+ return out;
+ }
- /* 回 JNI. */
- jbyteArray out = env->NewByteArray(compressed.size());
- env->SetByteArrayRegion(out, 0, compressed.size(),
- reinterpret_cast(compressed.data()));
- return out;
+ fail:
+ /* 统一错误处理. */
+ if (res) liq_result_destroy(res);
+ if (image) liq_image_destroy(image);
+ if (attr) liq_attr_destroy(attr);
+ env->ThrowNew(env->FindClass("java/io/IOException"), "pngquant native failed");
+ return nullptr;
}
\ No newline at end of file
diff --git a/libs/imagequant/build.gradle b/libs/imagequant/build.gradle
index eb220102..5cfc281a 100644
--- a/libs/imagequant/build.gradle
+++ b/libs/imagequant/build.gradle
@@ -40,6 +40,7 @@ android {
externalNativeBuild {
cmake {
cppFlags "-std=c++17"
+ cFlags "-ljnigraphics"
}
}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 6c779eb7..32e968e7 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -204,8 +204,8 @@ pluginManagement {
/* Print concerned info by `System.getProperties()`. */
val isShowConcernedSystemProperties = true
- val isCleanupPaddleOcr = true
- val isCleanupRapidOcr = true
+ val isCleanupPaddleOcr = false
+ val isCleanupRapidOcr = false
val fallbackAgpVersion = "7.4.2"
val fallbackKotlinVersion = "1.7.10"
diff --git a/version.properties b/version.properties
index 6a3098b1..a883cb0f 100644
--- a/version.properties
+++ b/version.properties
@@ -1,5 +1,5 @@
-#Sun May 18 20:20:48 CST 2025
-BUILD_TIME=1747570848298
+#Tue May 20 22:30:54 CST 2025
+BUILD_TIME=1747751454685
COMPILE_SDK_VERSION=35
IMAGE_QUANT_CMAKE_VERSION=3.22.1
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
TARGET_SDK_VERSION=35
TARGET_SDK_VERSION_INRT=29
-VERSION_BUILD=3234
+VERSION_BUILD=3238
VERSION_NAME=6.6.3 Alpha5
VSCODE_EXT_REQUIRED_VERSION=1.0.8