6.6.3 - Alpha5 - 新增 images.compressToBytes/downsample; 修复 images.compress; ImageWrapper#saveTo 支持相对路径
This commit is contained in:
@@ -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)_",
|
||||
"已忽略更新记录及客户端模式连接地址记录支持清空操作",
|
||||
"版本更新信息支持多语言显示 (与当前显示语言同步)",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 (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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -1,110 +1,156 @@
|
||||
#include <jni.h>
|
||||
#include <android/bitmap.h>
|
||||
#include <android/log.h>
|
||||
|
||||
#include <vector>
|
||||
#include <setjmp.h>
|
||||
#include "libimagequant.h"
|
||||
#include "png.h"
|
||||
#include <android/log.h>
|
||||
|
||||
#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<int>(info.width);
|
||||
const int height = static_cast<int>(info.height);
|
||||
|
||||
jsize len = env->GetArrayLength(srcRgba);
|
||||
std::vector<uint8_t> rgba(len);
|
||||
env->GetByteArrayRegion(srcRgba, 0, len, reinterpret_cast<jbyte *>(rgba.data()));
|
||||
/* 提前声明所有资源. */
|
||||
BitmapLocker locker(env, jbitmap);
|
||||
liq_attr *attr = nullptr;
|
||||
liq_image *image = nullptr;
|
||||
liq_result *res = nullptr;
|
||||
std::vector <uint8_t> indexed; // 后面 resize()
|
||||
const liq_palette *pal = nullptr;
|
||||
std::vector <uint8_t> compressed; // 输出 PNG
|
||||
|
||||
if (len != width * height * 4) {
|
||||
ALOGE("rgba length mismatch, len=%d, expect=%d", len, width * height * 4);
|
||||
/* 锁定像素. */
|
||||
if (!locker.lock()) {
|
||||
ALOGE("AndroidBitmap_lockPixels failed");
|
||||
env->ThrowNew(env->FindClass("java/io/IOException"),
|
||||
"AndroidBitmap_lockPixels failed");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/* 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<uint8_t *>(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<uint8_t> indexed(outSize);
|
||||
liq_write_remapped_image(res, image, indexed.data(), outSize);
|
||||
const liq_palette *pal = liq_get_palette(res);
|
||||
/* Remap. */
|
||||
indexed.resize(static_cast<size_t>(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<uint8_t> 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<std::vector<uint8_t> *>(png_get_io_ptr(png_ptr));
|
||||
auto *vec = static_cast<std::vector <uint8_t> *>(png_get_io_ptr(png_ptr));
|
||||
vec->insert(vec->end(), data, data + len);
|
||||
}
|
||||
|
||||
static void PNGAPI flush(png_structp) {}
|
||||
};
|
||||
std::vector<uint8_t> 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<png_uint_32>(width),
|
||||
static_cast<png_uint_32>(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<png_bytep> rows(height);
|
||||
/* Rows. */
|
||||
std::vector <png_bytep> 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<jsize>(compressed.size()));
|
||||
env->SetByteArrayRegion(out, 0, static_cast<jsize>(compressed.size()),
|
||||
reinterpret_cast<const jbyte *>(compressed.data()));
|
||||
/* 释放 libimagequant 资源. */
|
||||
liq_result_destroy(res);
|
||||
liq_image_destroy(image);
|
||||
liq_attr_destroy(attr);
|
||||
return out;
|
||||
}
|
||||
|
||||
/* 回 JNI. */
|
||||
jbyteArray out = env->NewByteArray(compressed.size());
|
||||
env->SetByteArrayRegion(out, 0, compressed.size(),
|
||||
reinterpret_cast<jbyte *>(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;
|
||||
}
|
||||
@@ -40,6 +40,7 @@ android {
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
cppFlags "-std=c++17"
|
||||
cFlags "-ljnigraphics"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user