6.7.0 - Alpha - 修复 images 模块相关方法出现异常时 oneShot 标记功能失效的问题 (issue #372)

This commit is contained in:
SuperMonster003
2025-06-10 19:35:15 +08:00
parent 06b8fa5b5c
commit 3677c6755f
7 changed files with 885 additions and 563 deletions

View File

@@ -32,7 +32,7 @@ import java.util.concurrent.atomic.AtomicLong
* Transformed by SuperMonster003 on May 16, 2023.
*/
// @Reference to Auto.js Pro 9.3.11 by SuperMonster003 on Dec 20, 2023.
open class ImageWrapper : Recyclable, MonitorResource {
open class ImageWrapper : Shootable<ImageWrapper>, MonitorResource {
private var mScriptRuntime: ScriptRuntime
@@ -62,36 +62,44 @@ open class ImageWrapper : Recyclable, MonitorResource {
val size
get() = Size(mWidth.toDouble(), mHeight.toDouble()).also { ensureNotRecycled() }
val bitmap by lazy {
ensureNotRecycled()
if (mBitmap == null) {
if (mMat != null) {
mBitmap = createBitmap(mMat!!.width(), mMat!!.height())
Utils.matToBitmap(mMat, mBitmap)
} else {
mBitmap = mediaImage?.let { toBitmap(it) }
val bitmap: Bitmap
get() {
ensureNotRecycled()
if (mBitmap == null) {
val mat = mMat
if (mat != null) {
val bitmap = createBitmap(mat.width(), mat.height()).also { mBitmap = it }
Utils.matToBitmap(mat, bitmap)
} else {
val mediaImage = mediaImage
if (mediaImage != null) {
mBitmap = toBitmap(mediaImage)
}
}
}
return mBitmap ?: throw Exception("Bitmap of ImageWrapper should never be null")
}
return@lazy mBitmap ?: throw Exception("Bitmap of ImageWrapper should never be null")
}
val mat by lazy {
ensureNotRecycled()
if (mMat != null) {
return@lazy mMat!!
val mat: Mat
get() {
ensureNotRecycled()
val mat = mMat
if (mat != null) {
return mat
}
val bitmap = mBitmap
if (bitmap != null) {
val newMat = Mat().also { mMat = it }
Utils.bitmapToMat(bitmap, newMat)
return newMat
}
if (mediaImage != null) {
val plane = plane ?: throw AssertionError("Image plain is null")
plane.buffer.position(0)
return Mat(mHeight, mWidth, CvType.CV_8UC4, plane.buffer, plane.rowStride.toLong()).also { mMat = it }
}
throw AssertionError("Both bitmap and image are null")
}
if (mBitmap != null) {
mMat = Mat()
Utils.bitmapToMat(mBitmap, mMat)
return@lazy mMat!!
}
if (mediaImage != null) {
val plane = plane ?: throw AssertionError("Image plain is null")
plane.buffer.position(0)
return@lazy Mat(mHeight, mWidth, CvType.CV_8UC4, plane.buffer, plane.rowStride.toLong()).also { mMat = it }
}
throw AssertionError("Both bitmap and image are null")
}
val bgrMat
get() = Mat().also {
@@ -308,7 +316,7 @@ open class ImageWrapper : Recyclable, MonitorResource {
fun recycleAll() {
imageList.forEach {
when (val o = it.get()) {
is Recyclable -> o.recycle()
is Shootable<*> -> o.recycle()
is Bitmap -> o.recycle()
is org.opencv.core.Mat -> OpenCVHelper.release(o)
is Image -> o.close()

View File

@@ -1,14 +1,14 @@
package org.autojs.autojs.core.image;
public interface Recyclable {
public interface Shootable<T> {
void recycle();
boolean isRecycled();
ImageWrapper setOneShot(boolean b);
T setOneShot(boolean b);
default ImageWrapper oneShot() {
default T oneShot() {
return setOneShot(true);
}

View File

@@ -23,6 +23,7 @@ import org.autojs.autojs.annotation.ScriptVariable;
import org.autojs.autojs.concurrent.VolatileDispose;
import org.autojs.autojs.core.image.CapturedImage;
import org.autojs.autojs.core.image.ImageWrapper;
import org.autojs.autojs.core.image.Shootable;
import org.autojs.autojs.core.image.RhinoColorFinder;
import org.autojs.autojs.core.image.TemplateMatching;
import org.autojs.autojs.core.image.capture.ScreenCaptureRequester;
@@ -56,6 +57,7 @@ import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import static android.app.Activity.RESULT_OK;
import static org.autojs.autojs.util.RhinoUtils.isMainThread;
@@ -153,12 +155,22 @@ public class Images {
if (image == null) {
throw new NullPointerException(str(R.string.error_method_called_with_null_argument, "Images.pixel", "image"));
}
int pixel = image.pixel(x, y);
image.shoot();
return pixel;
try {
return image.pixel(x, y);
} finally {
shoot(image);
}
}
public static ImageWrapper concat(ScriptRuntime scriptRuntime, ImageWrapper imgA, ImageWrapper imgB, int direction) {
try {
return concatInternal(scriptRuntime, imgA, imgB, direction);
} finally {
shoot(imgA, imgB);
}
}
private static ImageWrapper concatInternal(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));
}
@@ -186,8 +198,6 @@ public class Images {
canvas.drawBitmap(imgA.getBitmap(), (float) (width - imgA.getWidth()) / 2, 0, paint);
canvas.drawBitmap(imgB.getBitmap(), (float) (width - imgB.getWidth()) / 2, imgA.getHeight(), paint);
}
imgA.shoot();
imgB.shoot();
return ImageWrapper.ofBitmap(scriptRuntime, bitmap);
}
@@ -279,12 +289,22 @@ public class Images {
}
public ImageWrapper copy(@NonNull ImageWrapper image) {
ImageWrapper imageWrapper = image.clone();
image.shoot();
return imageWrapper;
try {
return image.clone();
} finally {
shoot(image);
}
}
public boolean save(@NonNull ImageWrapper image, @NonNull String path, @NonNull String format, int quality) throws IOException {
try {
return saveInternal(image, path, format, quality);
} finally {
shoot(image);
}
}
private boolean saveInternal(@NonNull ImageWrapper image, @NonNull String path, @NonNull String format, int quality) throws IOException {
Bitmap bitmap = image.getBitmap();
Bitmap.CompressFormat compressFormat = parseImageFormat(format);
@@ -306,19 +326,24 @@ public class Images {
}
try (FileOutputStream fos = new FileOutputStream(path)) {
boolean b = bitmap.compress(compressFormat, quality, fos);
image.shoot();
return b;
return bitmap.compress(compressFormat, quality, fos);
}
}
public byte[] compressToBytes(@NotNull ImageWrapper image, @NotNull String format, int quality) {
try {
return compressToBytesInternal(image, format, quality);
} finally {
shoot(image);
}
}
private byte[] compressToBytesInternal(@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;
}
@@ -327,14 +352,12 @@ public class Images {
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();
}
@@ -352,17 +375,31 @@ public class Images {
releaseScreenCapturer();
}
public ImageWrapper rotate(@NonNull ImageWrapper img, float x, float y, float degree) {
public ImageWrapper rotate(@NonNull ImageWrapper image, float x, float y, float degree) {
try {
return rotateInternal(image, x, y, degree);
} finally {
shoot(image);
}
}
private ImageWrapper rotateInternal(@NonNull ImageWrapper image, float x, float y, float degree) {
Matrix matrix = new Matrix();
matrix.postRotate(degree, x, y);
ImageWrapper imageWrapper = ImageWrapper.ofBitmap(mScriptRuntime, Bitmap.createBitmap(img.getBitmap(), 0, 0, img.getWidth(), img.getHeight(), matrix, true));
img.shoot();
return imageWrapper;
return ImageWrapper.ofBitmap(mScriptRuntime, Bitmap.createBitmap(image.getBitmap(), 0, 0, image.getWidth(), image.getHeight(), matrix, true));
}
@ScriptInterface
public ImageWrapper flip(@NonNull ImageWrapper img, boolean horizontal, boolean vertical) {
Bitmap original = img.getBitmap();
public ImageWrapper flip(@NonNull ImageWrapper image, boolean horizontal, boolean vertical) {
try {
return flipInternal(image, horizontal, vertical);
} finally {
shoot(image);
}
}
private ImageWrapper flipInternal(@NonNull ImageWrapper image, boolean horizontal, boolean vertical) {
Bitmap original = image.getBitmap();
Matrix matrix = new Matrix();
// Set scaling ratio according to input parameters.
@@ -380,14 +417,19 @@ public class Images {
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(mScriptRuntime, flipped);
}
public ImageWrapper clip(@NonNull ImageWrapper img, int x, int y, int w, int h) {
ImageWrapper imageWrapper = ImageWrapper.ofBitmap(mScriptRuntime, Bitmap.createBitmap(img.getBitmap(), x, y, w, h));
img.shoot();
return imageWrapper;
public ImageWrapper clip(@NonNull ImageWrapper image, int x, int y, int w, int h) {
try {
return clipInternal(image, x, y, w, h);
} finally {
shoot(image);
}
}
private ImageWrapper clipInternal(@NonNull ImageWrapper image, int x, int y, int w, int h) {
return ImageWrapper.ofBitmap(mScriptRuntime, Bitmap.createBitmap(image.getBitmap(), x, y, w, h));
}
public ImageWrapper read(String path) {
@@ -416,25 +458,32 @@ public class Images {
return ImageWrapper.ofBitmap(mScriptRuntime, Drawables.loadBase64Data(data));
}
public String toBase64(ImageWrapper img, String format, int quality) {
byte[] input = toBytes(img, format, quality);
img.shoot();
return Base64.encodeToString(input, Base64.NO_WRAP);
public String toBase64(ImageWrapper image, String format, int quality) {
try {
byte[] input = toBytes(image, format, quality);
return Base64.encodeToString(input, Base64.NO_WRAP);
} finally {
shoot(image);
}
}
public byte[] toBytes(@NonNull ImageWrapper img, String format, int quality) {
Bitmap.CompressFormat compressFormat = parseImageFormat(format);
Bitmap bitmap = img.getBitmap();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
bitmap.compress(compressFormat, quality, outputStream);
img.shoot();
return outputStream.toByteArray();
public byte[] toBytes(@NonNull ImageWrapper image, String format, int quality) {
try {
Bitmap.CompressFormat compressFormat = parseImageFormat(format);
Bitmap bitmap = image.getBitmap();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
bitmap.compress(compressFormat, quality, outputStream);
return outputStream.toByteArray();
} finally {
shoot(image);
}
}
public ImageWrapper fromBytes(byte[] bytes) throws BitmapUtils.DecodeException {
return ImageWrapper.ofBitmap(mScriptRuntime, BitmapUtils.bitmapFromByteArrayOrThrow(bytes));
}
/** @noinspection deprecation */
private Bitmap.CompressFormat parseImageFormat(String format) {
return switch (format.toLowerCase(Language.getPrefLanguage().getLocale())) {
case "png" -> Bitmap.CompressFormat.PNG;
@@ -468,6 +517,14 @@ public class Images {
}
public ImageWrapper invert(@NonNull ImageWrapper image) {
try {
return invertInternal(image);
} finally {
shoot(image);
}
}
private ImageWrapper invertInternal(@NonNull ImageWrapper image) {
initOpenCvIfNeeded();
Bitmap originalBitmap = image.getBitmap();
@@ -503,8 +560,6 @@ public class Images {
Bitmap invertedBitmap = Bitmap.createBitmap(originalWidth, originalHeight, Bitmap.Config.ARGB_8888);
Utils.matToBitmap(destMat, invertedBitmap);
image.shoot();
return ImageWrapper.ofBitmap(mScriptRuntime, invertedBitmap);
}
@@ -557,6 +612,14 @@ public class Images {
@Nullable
@ScriptInterface
public Point findImage(ImageWrapper image, ImageWrapper template, float weakThreshold, float strictThreshold, Rect rect, int maxLevel) throws Exception {
try {
return findImageInternal(image, template, weakThreshold, strictThreshold, rect, maxLevel);
} finally {
shoot(image, template);
}
}
private Point findImageInternal(ImageWrapper image, ImageWrapper template, float weakThreshold, float strictThreshold, Rect rect, int maxLevel) throws Exception {
initOpenCvIfNeeded();
if (image == null) {
throw new NullPointerException(mContext.getString(R.string.error_method_called_with_null_argument, "Images.findImage", "image"));
@@ -588,8 +651,6 @@ public class Images {
if (shouldReleaseMat) {
OpenCVHelper.release(src);
}
image.shoot();
template.shoot();
}
if (point != null) {
if (rect != null) {
@@ -603,6 +664,14 @@ public class Images {
}
public List<TemplateMatching.Match> matchTemplate(ImageWrapper image, ImageWrapper template, float weakThreshold, float strictThreshold, Rect rect, int maxLevel, int limit, boolean useTransparentMask) {
try {
return matchTemplateInternal(image, template, weakThreshold, strictThreshold, rect, maxLevel, limit, useTransparentMask);
} finally {
shoot(image, template);
}
}
private List<TemplateMatching.Match> matchTemplateInternal(ImageWrapper image, ImageWrapper template, float weakThreshold, float strictThreshold, Rect rect, int maxLevel, int limit, boolean useTransparentMask) {
initOpenCvIfNeeded();
if (image == null) {
throw new NullPointerException(mContext.getString(R.string.error_method_called_with_null_argument, "Images.matchTemplate", "image"));
@@ -623,8 +692,6 @@ public class Images {
if (src != image.getMat()) {
OpenCVHelper.release(src);
}
image.shoot();
template.shoot();
for (TemplateMatching.Match match : result) {
Point point = match.point;
@@ -669,4 +736,8 @@ public class Images {
}
}
public static void shoot(Shootable<?>... shootableArgs) {
Arrays.stream(shootableArgs).filter(Objects::nonNull).forEach(Shootable::shoot);
}
}

View File

@@ -1,5 +1,6 @@
package org.autojs.autojs.runtime.api.augment.images
import org.autojs.autojs.core.image.Shootable
import org.autojs.autojs.runtime.api.ImageFeatureMatching
import org.autojs.autojs.util.RhinoUtils
import org.mozilla.javascript.Undefined
@@ -9,15 +10,28 @@ class ImageFeatures(
@JvmField var javaObject: ImageFeatureMatching.FeatureMatchingDescriptor,
@JvmField var scale: Float,
@JvmField var region: Rect,
) {
) : Shootable<ImageFeatures> {
private var mIsOneShot = false
@JvmField
var recycled = false
override fun isRecycled() = recycled
override fun setOneShot(b: Boolean): ImageFeatures {
mIsOneShot = b
return this
}
override fun shoot() {
if (mIsOneShot) recycle()
}
@JvmField
var onRecycled: ((ImageFeatures) -> Undefined) = { _: ImageFeatures -> RhinoUtils.UNDEFINED }
fun recycle() {
override fun recycle() {
if (!recycled) {
javaObject.release()
onRecycled.invoke(this)