6.6.3 - Alpha5 - images.save 方法使用 quality 参数时支持 png 格式的文件体积压缩 (issue #367)

This commit is contained in:
SuperMonster003
2025-05-18 12:08:33 +08:00
parent 0882cf4166
commit 676dde9632
89 changed files with 47124 additions and 58 deletions

View File

@@ -163,6 +163,9 @@ dependencies /* Unclassified */ {
// RapidOCR
implementation(project(":libs:rapidocr"))
// Image Quantization
implementation(project(":libs:imagequant"))
// Android Job
implementation(project(":libs:android-job-simplified-1.4.3"))
@@ -623,6 +626,7 @@ android {
"junit/**/*",
"LICENSE-junit.txt",
"spec.txt",
"EmojiReference.txt",
).let { resources.excludes.addAll(it) }
if (gradle.startParameter.taskNames.any { it.contains(flavorNameInrt, true) }) {

View File

@@ -546,10 +546,33 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File
MLKIT_BARCODE(
label = "MLKit Barcode",
aliases = listOf("barcode", "mlkit-barcode", "mlkit_barcode"),
libsToInclude = listOf(
"libbarhopper_v3.so",
),
assetDirsToExclude = listOf(
"mlkit_barcode_models",
),
);
),
MEDIA_INFO(
label = "MediaInfo",
aliases = listOf("mediainfo", "media-info", "media_info"),
libsToInclude = listOf(
"libmediainfo.so",
),
),
IMAGE_QUANT(
label = "Image Quantization",
aliases = listOf("imagequant", "image-quant", "image-quantization", "image_quant", "image_quantization"),
libsToInclude = listOf(
"libpng.so",
"libpng16d.so",
"libpngquant_bridge.so",
),
),
;
fun ensureLibFiles(moduleName: String = label) {
if (!isInrt) return

View File

@@ -61,7 +61,7 @@ public final class ImageFeatureMatching {
* reused later by {@link #featureMatching}</li>
* </ol></p>
*
* <p><b>zh-CN</b><br>
* <p><b>zh-CN:</b><br>
*
* 创建 (预计算) 特征匹配描述符.<br>
* 典型流程: <pre>
@@ -168,7 +168,7 @@ public final class ImageFeatureMatching {
* <li>If caller sets {@code debugMatchesImagePath}, save visualization of match lines to that path</li>
* </ol></p>
*
* <p><b>zh-CN</b><br>
* <p><b>zh-CN:</b><br>
*
* 在两张图的特征描述符之间执行匹配, 并 (可选) 估算单应矩阵以获得被 object 图在 scene 图中的投影区域.<br>
* <p>内部流程: <br>

View File

@@ -35,6 +35,8 @@ import org.autojs.autojs.core.ui.inflater.util.Drawables;
import org.autojs.autojs.pio.UncheckedIOException;
import org.autojs.autojs.runtime.ScriptRuntime;
import org.autojs.autojs.runtime.api.ImageFeatureMatching.FeatureMatchingDescriptor;
import org.autojs.autojs.runtime.exception.WrappedRuntimeException;
import org.autojs.autojs.util.BitmapUtils;
import org.autojs.autojs6.R;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
@@ -254,13 +256,38 @@ public class Images {
return imageWrapper;
}
public boolean save(@NonNull ImageWrapper image, String path, String format, int quality) throws IOException {
Bitmap.CompressFormat compressFormat = parseImageFormat(format);
public boolean save(@NonNull ImageWrapper image,
@NonNull String path,
@NonNull String format,
int quality) throws IOException {
Bitmap bitmap = image.getBitmap();
FileOutputStream outputStream = new FileOutputStream(path);
boolean b = bitmap.compress(compressFormat, quality, outputStream);
image.shoot();
return b;
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);
if (compressed != null) {
try (FileOutputStream fos = new FileOutputStream(path)) {
fos.write(compressed);
return true;
}
}
throw new WrappedRuntimeException("PNG quantization failed");
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (compressFormat == Bitmap.CompressFormat.WEBP_LOSSLESS && quality != 100) {
throw new IllegalArgumentException(mContext.getString(R.string.error_webp_lossless_quality_not_supported));
}
}
try (FileOutputStream fos = new FileOutputStream(path)) {
boolean b = bitmap.compress(compressFormat, quality, fos);
image.shoot();
return b;
}
}
// public EventEmitter select() {
@@ -357,13 +384,13 @@ public class Images {
case "png" -> Bitmap.CompressFormat.PNG;
case "jpeg", "jpg" -> Bitmap.CompressFormat.JPEG;
case "webp" -> Bitmap.CompressFormat.WEBP;
case "webp_lossy" -> {
case "webp_lossy", "webp-lossy" -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
yield Bitmap.CompressFormat.WEBP_LOSSY;
}
throw new IllegalArgumentException("Image format \"WEBP_LOSSY\" only supports on Android API Level 30 (11) [R] and above");
}
case "webp_lossless" -> {
case "webp_lossless", "webp-lossless" -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
yield Bitmap.CompressFormat.WEBP_LOSSLESS;
}

View File

@@ -0,0 +1,46 @@
package org.autojs.autojs.runtime.api;
public class PngQuantBridge {
static {
System.loadLibrary("pngquant_bridge");
}
/**
* Quantizes a 32-bit RGBA image to an 8-bit indexed PNG and returns the encoded bytes.<br>
* <p>
* The routine converts the input buffer (RGBA8888) to a palette-based image through
* <em>libimagequant</em> and then writes it out as PNG8. The PNG/Deflate stage that follows
* is <b>strictly lossless</b>&mdash;the stream produced by this method can always be inflated
* back to the same 8-bit palette data byte-for-byte.<br>
* <p>
* The potentially visible degradation comes solely from the <b>color quantization</b> step:
* ~16.7 M true-color values are mapped to ≤256 palette entries. How noticeable this mapping is
* depends on both the source image and the {@code quality} you request.
* <ul>
* <li>{@code quality} represents the upper limit of <i>Mean Square Error × 10</i> accepted
* by libimagequant (range 0100). Lower numbers allow larger error, producing smaller files.</li>
* <li>Because many graphics are well represented with 256 colors, even an extremely low
* {@code quality} such as {@code 1} may look almost identical to the original while shrinking
* to 520 % of its former size.</li>
* <li>No further lossy step exists during PNG writing; the format itself does not permit it.</li>
* </ul>
* <p><b>zh-CN:</b><br>
* 该方法把 32 位 RGBA 图像量化为 8 位索引色 PNG, 并返回编码后的字节数组.<br>
* PNG 的 Deflate 压缩阶段完全<strong>无损</strong>; 图像失真来自量化过程: 32 位真彩被映射到
* ≤256 色调色板, 失真程度由 {@code quality} 决定.
* <ul>
* <li>{@code quality} 取值 0100, 对应 libimagequant 允许的 MSE×10上限; 数值越小, 容许误差越大, 文件越小.</li>
* <li>许多图片用 256 色即可精准表示, 因此即便 {@code quality}=1 也可能与原图几乎无差别, 体积却可缩至原来的 520 %.</li>
* <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);
}

View File

@@ -143,8 +143,19 @@ abstract class Augmentable(private val scriptRuntime: ScriptRuntime? = null) : F
try {
(this as Invokable).invoke(*args)
} catch (e: Exception) {
val message = e.message?.let {
globalContext.getString(R.string.error_failed_to_call_method_with_cause, key, it)
val message = e.message?.let { msg ->
when {
msg.contains("\n") -> {
msg.split("\n").let { split ->
globalContext.getString(R.string.error_failed_to_call_method_with_cause, key, split.first()).let { result ->
split.slice(1..split.lastIndex).joinToString("\n").takeUnless { it.isBlank() }?.let {
"$result\n$it"
} ?: result
}
}
}
else -> globalContext.getString(R.string.error_failed_to_call_method_with_cause, key, msg)
}
} ?: globalContext.getString(R.string.error_failed_to_call_method, key)
throw WrappedRuntimeException(message, e)
}

View File

@@ -1334,8 +1334,11 @@ class Images(scriptRuntime: ScriptRuntime) : Augmentable(scriptRuntime), AsEmitt
private fun parseImageFormat(o: Any?): String = when {
o.isJsNullish() -> "png"
else -> when (val format = coerceString(o).lowercase()) {
else -> when (val format = coerceString(o).lowercase().trim()) {
"" -> "png"
"png", "jpg", "jpeg", "webp" -> format
"webp_lossless", "webp-lossless" -> format
"webp_lossy", "webp-lossy" -> format
else -> throw WrappedIllegalArgumentException("Unknown image format: $format")
}
}

View File

@@ -34,4 +34,28 @@ object BitmapUtils {
.also { drawable.draw(Canvas(it)) }
}
@JvmStatic
fun bitmapToRgba(bmp: Bitmap): ByteArray {
val w = bmp.getWidth()
val h = bmp.getHeight()
require(w > 0 && h > 0) { "bitmap size is 0" }
val pixelCount = w * h
val byteCount = pixelCount * 4
val rgba = ByteArray(byteCount)
val argb = IntArray(pixelCount)
bmp.getPixels(argb, 0, w, 0, 0, w, h)
var i4 = 0
for (p in argb) {
rgba[i4++] = ((p shr 16) and 0xFF).toByte() // R
rgba[i4++] = ((p shr 8) and 0xFF).toByte() // G
rgba[i4++] = (p and 0xFF).toByte() // B
rgba[i4++] = ((p shr 24) and 0xFF).toByte() // A
}
return rgba
}
}

View File

@@ -1048,5 +1048,6 @@
<string name="error_no_applications_available_for_viewing_this_file">لا توجد تطبيقات متاحة لعرض هذا الملف</string>
<string name="error_no_applications_available_for_sending_this_file">لا توجد تطبيقات متاحة لإرسال هذا الملف</string>
<string name="error_no_applications_available_for_playing_this_file">لا توجد تطبيقات متاحة لتشغيل هذا الملف</string>
<string name="error_webp_lossless_quality_not_supported">لا يمكن ضغط تنسيق WebP-Lossless باستخدام معامل الجودة؛ استخدم JPEG/PNG/WebP-Lossy بدلاً من ذلك</string>
</resources>

View File

@@ -1043,5 +1043,6 @@
<string name="error_no_applications_available_for_viewing_this_file">No applications available for viewing this file</string>
<string name="error_no_applications_available_for_sending_this_file">No applications available for sending this file</string>
<string name="error_no_applications_available_for_playing_this_file">No applications available for playing this file</string>
<string name="error_webp_lossless_quality_not_supported">Format WebP-Lossless cannot be compressed with a quality parameter, use JPEG/PNG/WebP-Lossy instead</string>
</resources>

View File

@@ -1046,5 +1046,6 @@
<string name="error_no_applications_available_for_viewing_this_file">No hay aplicaciones disponibles para ver este archivo</string>
<string name="error_no_applications_available_for_sending_this_file">No hay aplicaciones disponibles para enviar este archivo</string>
<string name="error_no_applications_available_for_playing_this_file">No hay aplicaciones disponibles para reproducir este archivo</string>
<string name="error_webp_lossless_quality_not_supported">El formato WebP-Lossless no se puede comprimir con un parámetro de calidad; usa JPEG/PNG/WebP-Lossy en su lugar</string>
</resources>

View File

@@ -1046,5 +1046,6 @@
<string name="error_no_applications_available_for_viewing_this_file">Aucune application disponible pour afficher ce fichier</string>
<string name="error_no_applications_available_for_sending_this_file">Aucune application disponible pour envoyer ce fichier</string>
<string name="error_no_applications_available_for_playing_this_file">Aucune application disponible pour lire ce fichier</string>
<string name="error_webp_lossless_quality_not_supported">Le format WebP-Lossless ne peut pas être compressé avec un paramètre de qualité ; utilisez plutôt JPEG/PNG/WebP-Lossy</string>
</resources>

View File

@@ -1047,5 +1047,6 @@
<string name="error_no_applications_available_for_viewing_this_file">このファイルを表示できるアプリがありません</string>
<string name="error_no_applications_available_for_sending_this_file">このファイルを送信できるアプリがありません</string>
<string name="error_no_applications_available_for_playing_this_file">このファイルを再生できるアプリがありません</string>
<string name="error_webp_lossless_quality_not_supported">WebP-Lossless 形式は品質パラメータで圧縮できません, 代わりに JPEG/PNG/WebP-Lossy を使用してください</string>
</resources>

View File

@@ -182,7 +182,7 @@
<string name="error_excessive_width_for_template_n_region">너비 초과: 템플릿 [%1$d] > 영역 [%2$d]</string>
<string name="error_failed_to_apply_current_color_history">현재 색상 기록 적용에 실패했습니다</string>
<string name="error_failed_to_call_method">메서드 \"%s\"를 호출하지 못했습니다</string>
<string name="error_failed_to_call_method_with_cause"><![CDATA[메서드 \"%1$s\"를 호출하지 못했습니다: [ %2$s ]]]></string>
<string name="error_failed_to_call_method_with_cause"><![CDATA[메서드 \"%1$s\" 를 호출하지 못했습니다: [ %2$s ]]]></string>
<string name="error_failed_to_convert_into_drawable">%s 값을 Drawable 로 변환하지 못했습니다</string>
<string name="error_failed_to_grant_shizuku_access">Shizuku 권한을 부여하지 못했습니다</string>
<string name="error_failed_to_instantiate">\"%s\" 인스턴스 생성에 실패했습니다</string>
@@ -1048,5 +1048,6 @@
<string name="error_no_applications_available_for_viewing_this_file">이 파일을 볼 수 있는 앱이 없습니다</string>
<string name="error_no_applications_available_for_sending_this_file">이 파일을 전송할 수 있는 앱이 없습니다</string>
<string name="error_no_applications_available_for_playing_this_file">이 파일을 재생할 수 있는 앱이 없습니다</string>
<string name="error_webp_lossless_quality_not_supported">WebP-Lossless 형식은 품질 파라미터로 압축할 수 없습니다, 대신 JPEG/PNG/WebP-Lossy를 사용하세요</string>
</resources>

View File

@@ -1046,5 +1046,6 @@
<string name="error_no_applications_available_for_viewing_this_file">Нет приложений для просмотра этого файла</string>
<string name="error_no_applications_available_for_sending_this_file">Нет приложений для отправки этого файла</string>
<string name="error_no_applications_available_for_playing_this_file">Нет приложений для воспроизведения этого файла</string>
<string name="error_webp_lossless_quality_not_supported">Формат WebP-Lossless нельзя сжать с параметром качества; используйте вместо этого JPEG/PNG/WebP-Lossy</string>
</resources>

View File

@@ -177,12 +177,12 @@
<string name="error_excessive_height_for_template_n_region">高度超限: 模板圖像 [%1$d] > 限定區域 [%2$d]</string>
<string name="error_excessive_width_for_template_n_region">寬度超限: 模板圖像 [%1$d] > 限定區域 [%2$d]</string>
<string name="error_failed_to_apply_current_color_history">應用當前歷史顏色失敗</string>
<string name="error_failed_to_call_method">調用方法 \"%s\" 失敗</string>
<string name="error_failed_to_call_method_with_cause"><![CDATA[調用方法 \"%1$s\" 失敗: [ %2$s ]]]></string>
<string name="error_failed_to_call_method">方法 \"%s\" 調用失敗</string>
<string name="error_failed_to_call_method_with_cause"><![CDATA[方法 \"%1$s\" 調用失敗: [ %2$s ]]]></string>
<string name="error_failed_to_convert_into_drawable">無法將值 %s 轉換為 Drawable 實例</string>
<string name="error_failed_to_grant_shizuku_access">Shizuku 權限授予失敗</string>
<string name="error_failed_to_instantiate">創建實例 \"%s\" 失敗</string>
<string name="error_failed_to_instantiate_with_cause"><![CDATA[創建實例 \"%1$s\" 失敗: [ %2$s ]]]></string>
<string name="error_failed_to_instantiate">實例 \"%s\" 創建失敗</string>
<string name="error_failed_to_instantiate_with_cause"><![CDATA[實例 \"%1$s\" 創建失敗: [ %2$s ]]]></string>
<string name="error_failed_to_invoke_method_with_description">方法 %1$s 調用失敗</string>
<string name="error_failed_to_render_content">內容渲染失敗</string>
<string name="error_failed_to_retrieve_released_notes">獲取版本信息失敗</string>
@@ -1044,5 +1044,6 @@
<string name="error_no_applications_available_for_viewing_this_file">未找到用於查看該文件的應用</string>
<string name="error_no_applications_available_for_sending_this_file">未找到用於發送該文件的應用</string>
<string name="error_no_applications_available_for_playing_this_file">未找到用於播放該文件的應用</string>
<string name="error_webp_lossless_quality_not_supported">WebP-Lossless 格式無法使用 quality 質量參數進行壓縮, 可改用 JPEG/PNG/WebP-Lossy</string>
</resources>

View File

@@ -177,12 +177,12 @@
<string name="error_excessive_height_for_template_n_region">高度超限: 模板影象 [%1$d] > 限定區域 [%2$d]</string>
<string name="error_excessive_width_for_template_n_region">寬度超限: 模板影象 [%1$d] > 限定區域 [%2$d]</string>
<string name="error_failed_to_apply_current_color_history">應用當前歷史顏色失敗</string>
<string name="error_failed_to_call_method">呼叫方法 \"%s\" 失敗</string>
<string name="error_failed_to_call_method_with_cause"><![CDATA[呼叫方法 \"%1$s\" 失敗: [ %2$s ]]]></string>
<string name="error_failed_to_call_method">方法 \"%s\" 呼叫失敗</string>
<string name="error_failed_to_call_method_with_cause"><![CDATA[方法 \"%1$s\" 呼叫失敗: [ %2$s ]]]></string>
<string name="error_failed_to_convert_into_drawable">無法將值 %s 轉換為 Drawable 例項</string>
<string name="error_failed_to_grant_shizuku_access">Shizuku 許可權授予失敗</string>
<string name="error_failed_to_instantiate">建立例項 \"%s\" 失敗</string>
<string name="error_failed_to_instantiate_with_cause"><![CDATA[建立例項 \"%1$s\" 失敗: [ %2$s ]]]></string>
<string name="error_failed_to_instantiate">例項 \"%s\" 建立失敗</string>
<string name="error_failed_to_instantiate_with_cause"><![CDATA[例項 \"%1$s\" 建立失敗: [ %2$s ]]]></string>
<string name="error_failed_to_invoke_method_with_description">方法 %1$s 呼叫失敗</string>
<string name="error_failed_to_render_content">內容渲染失敗</string>
<string name="error_failed_to_retrieve_released_notes">獲取版本資訊失敗</string>
@@ -1044,5 +1044,6 @@
<string name="error_no_applications_available_for_viewing_this_file">未找到用於檢視該檔案的應用</string>
<string name="error_no_applications_available_for_sending_this_file">未找到用於傳送該檔案的應用</string>
<string name="error_no_applications_available_for_playing_this_file">未找到用於播放該檔案的應用</string>
<string name="error_webp_lossless_quality_not_supported">WebP-Lossless 格式無法使用 quality 質量引數進行壓縮, 可改用 JPEG/PNG/WebP-Lossy</string>
</resources>

View File

@@ -177,12 +177,12 @@
<string name="error_excessive_height_for_template_n_region">高度超限: 模板图像 [%1$d] > 限定区域 [%2$d]</string>
<string name="error_excessive_width_for_template_n_region">宽度超限: 模板图像 [%1$d] > 限定区域 [%2$d]</string>
<string name="error_failed_to_apply_current_color_history">应用当前历史颜色失败</string>
<string name="error_failed_to_call_method">调用方法 \"%s\" 失败</string>
<string name="error_failed_to_call_method_with_cause"><![CDATA[调用方法 \"%1$s\" 失败: [ %2$s ]]]></string>
<string name="error_failed_to_call_method">方法 \"%s\" 调用失败</string>
<string name="error_failed_to_call_method_with_cause"><![CDATA[方法 \"%1$s\" 调用失败: [ %2$s ]]]></string>
<string name="error_failed_to_convert_into_drawable">无法将值 %s 转换为 Drawable 实例</string>
<string name="error_failed_to_grant_shizuku_access">Shizuku 权限授予失败</string>
<string name="error_failed_to_instantiate">创建实例 \"%s\" 失败</string>
<string name="error_failed_to_instantiate_with_cause"><![CDATA[创建实例 \"%1$s\" 失败: [ %2$s ]]]></string>
<string name="error_failed_to_instantiate">实例 \"%s\" 创建失败</string>
<string name="error_failed_to_instantiate_with_cause"><![CDATA[实例 \"%1$s\" 创建失败: [ %2$s ]]]></string>
<string name="error_failed_to_invoke_method_with_description">方法 %1$s 调用失败</string>
<string name="error_failed_to_render_content">内容渲染失败</string>
<string name="error_failed_to_retrieve_released_notes">获取版本信息失败</string>
@@ -1044,5 +1044,6 @@
<string name="error_no_applications_available_for_viewing_this_file">未找到用于查看该文件的应用</string>
<string name="error_no_applications_available_for_sending_this_file">未找到用于发送该文件的应用</string>
<string name="error_no_applications_available_for_playing_this_file">未找到用于播放该文件的应用</string>
<string name="error_webp_lossless_quality_not_supported">WebP-Lossless 格式无法使用 quality 质量参数进行压缩, 可改用 JPEG/PNG/WebP-Lossy</string>
</resources>

View File

@@ -1274,5 +1274,6 @@
<string name="error_no_applications_available_for_viewing_this_file">No applications available for viewing this file</string>
<string name="error_no_applications_available_for_sending_this_file">No applications available for sending this file</string>
<string name="error_no_applications_available_for_playing_this_file">No applications available for playing this file</string>
<string name="error_webp_lossless_quality_not_supported">Format WebP-Lossless cannot be compressed with a quality parameter, use JPEG/PNG/WebP-Lossy instead</string>
</resources>