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

@@ -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
}

View File

@@ -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() {

View File

@@ -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. */

View File

@@ -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)

View File

@@ -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()

View File

@@ -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
}
}

View File

@@ -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<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.
* 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);
}

View File

@@ -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 {
* <li>PNG 规范不支持在压缩阶段引入有损处理, 因此不会像 JPEG 那样再出现马赛克等失真.</li>
* </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)
*/
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.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);
}
}

View File

@@ -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<Any>(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<Any>(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<Any>(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<out Any?>): 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<out Any?>): ImageWrapper = ensureArgumentsLength(args, 2) {
val (o, compressLevelArg) = it
fun compress(scriptRuntime: ScriptRuntime, args: Array<out Any?>): 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<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
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<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

View File

@@ -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.<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
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