|
|
@@ -6,14 +6,17 @@ import android.graphics.Bitmap
|
|
|
import android.graphics.BitmapFactory
|
|
|
import android.graphics.Canvas
|
|
|
import android.graphics.Color
|
|
|
-import android.graphics.ImageFormat
|
|
|
import android.graphics.Matrix
|
|
|
import android.graphics.Rect
|
|
|
-import android.graphics.YuvImage
|
|
|
import android.graphics.drawable.Drawable
|
|
|
import android.media.ExifInterface
|
|
|
-import android.os.Environment
|
|
|
+import android.os.Looper
|
|
|
import androidx.collection.LruCache
|
|
|
+import coil.ImageLoader
|
|
|
+import coil.decode.SvgDecoder
|
|
|
+import coil.request.ImageRequest
|
|
|
+import coil.request.SuccessResult
|
|
|
+import coil.size.Size
|
|
|
import com.bumptech.glide.Glide
|
|
|
import com.bumptech.glide.Priority
|
|
|
import com.bumptech.glide.load.DataSource
|
|
|
@@ -26,6 +29,8 @@ import com.bumptech.glide.request.target.CustomTarget
|
|
|
import com.bumptech.glide.request.target.Target
|
|
|
import com.bumptech.glide.request.transition.Transition
|
|
|
import com.grkj.iscs_mars.util.log.LogUtil
|
|
|
+import com.sik.sikcore.SIKCore
|
|
|
+import com.sik.siknet.http.httpDownloadFile
|
|
|
import kotlinx.coroutines.Dispatchers
|
|
|
import kotlinx.coroutines.isActive
|
|
|
import kotlinx.coroutines.withContext
|
|
|
@@ -33,11 +38,9 @@ import java.io.ByteArrayOutputStream
|
|
|
import java.io.File
|
|
|
import java.io.FileInputStream
|
|
|
import java.io.FileOutputStream
|
|
|
-import java.io.IOException
|
|
|
import kotlin.coroutines.resume
|
|
|
import kotlin.coroutines.suspendCoroutine
|
|
|
import kotlin.math.max
|
|
|
-import kotlin.math.roundToInt
|
|
|
|
|
|
/**
|
|
|
* 通用图片工具:
|
|
|
@@ -49,6 +52,13 @@ import kotlin.math.roundToInt
|
|
|
* 注意:方法名前带 `suspend` 的全部在 IO 线程运行,无需再切协程。
|
|
|
*/
|
|
|
object BitmapUtil {
|
|
|
+
|
|
|
+
|
|
|
+ fun isSvg(file: File): Boolean {
|
|
|
+ val name = file.name.lowercase()
|
|
|
+ return name.endsWith(".svg") || name.endsWith(".svgz")
|
|
|
+ }
|
|
|
+
|
|
|
/*--------------------------------------------------------*/
|
|
|
/*----------- 1. 仅下载到缓存目录(不解 Bitmap) -----------*/
|
|
|
/*--------------------------------------------------------*/
|
|
|
@@ -58,21 +68,82 @@ object BitmapUtil {
|
|
|
*
|
|
|
* @return 缓存文件;失败时返回 null
|
|
|
*/
|
|
|
+ private val httpClient: okhttp3.OkHttpClient by lazy {
|
|
|
+ okhttp3.OkHttpClient.Builder()
|
|
|
+ .followRedirects(true)
|
|
|
+ .followSslRedirects(true)
|
|
|
+ .retryOnConnectionFailure(true)
|
|
|
+ .build()
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun extFromUrl(url: String): String? {
|
|
|
+ val last = url.substringBefore('#').substringBefore('?').substringAfterLast('/', "")
|
|
|
+ val ext = last.substringAfterLast('.', "").lowercase()
|
|
|
+ return ext.takeIf { it.isNotEmpty() && it.length <= 5 }
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun extFromContentType(ct: String?): String? {
|
|
|
+ if (ct.isNullOrBlank()) return null
|
|
|
+ return when (ct.substringBefore(';').trim().lowercase()) {
|
|
|
+ "image/png" -> "png"
|
|
|
+ "image/jpeg",
|
|
|
+ "image/jpg" -> "jpg"
|
|
|
+ "image/webp" -> "webp"
|
|
|
+ "image/gif" -> "gif"
|
|
|
+ "image/bmp" -> "bmp"
|
|
|
+ "image/svg+xml" -> "svg"
|
|
|
+ else -> null
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 用 OkHttp 下载文件到应用 cacheDir,并保留合理后缀。
|
|
|
+ */
|
|
|
suspend fun downloadToFile(ctx: Context, url: String): File? =
|
|
|
withContext(Dispatchers.IO) {
|
|
|
try {
|
|
|
- val future: FutureTarget<File> = Glide.with(ctx)
|
|
|
- .downloadOnly() // 关键:只下载不解码
|
|
|
- .load(url)
|
|
|
- .priority(Priority.IMMEDIATE)
|
|
|
- .submit() // SIZE_ORIGINAL
|
|
|
- future.get() // 阻塞等待
|
|
|
+ val req = okhttp3.Request.Builder().url(url).get().build()
|
|
|
+ httpClient.newCall(req).execute().use { resp ->
|
|
|
+ if (!resp.isSuccessful) return@withContext null
|
|
|
+ val body = resp.body ?: return@withContext null
|
|
|
+
|
|
|
+ // 1) 后缀推断
|
|
|
+ val urlExt = extFromUrl(url)
|
|
|
+ val ctExt = extFromContentType(resp.header("Content-Type"))
|
|
|
+ val finalExt = (urlExt ?: ctExt)?.let { ".$it" } ?: ""
|
|
|
+
|
|
|
+ // 2) 目标文件
|
|
|
+ val dest = File.createTempFile("dl_", finalExt, ctx.cacheDir)
|
|
|
+
|
|
|
+ // 3) 写入文件
|
|
|
+ body.byteStream().use { input ->
|
|
|
+ FileOutputStream(dest).use { output ->
|
|
|
+ val buf = ByteArray(DEFAULT_BUFFER_SIZE)
|
|
|
+ var n: Int
|
|
|
+ while (true) {
|
|
|
+ n = input.read(buf)
|
|
|
+ if (n <= 0) break
|
|
|
+ output.write(buf, 0, n)
|
|
|
+ }
|
|
|
+ output.flush()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4) 校验
|
|
|
+ if (!dest.exists() || dest.length() == 0L) {
|
|
|
+ runCatching { dest.delete() }
|
|
|
+ return@withContext null
|
|
|
+ }
|
|
|
+
|
|
|
+ dest
|
|
|
+ }
|
|
|
} catch (t: Throwable) {
|
|
|
t.printStackTrace()
|
|
|
null
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+
|
|
|
/*--------------------------------------------------------*/
|
|
|
/*----------- 2. 只读取宽高(BitmapFactory 探针)-----------*/
|
|
|
/*--------------------------------------------------------*/
|
|
|
@@ -378,6 +449,139 @@ object BitmapUtil {
|
|
|
return bitmap
|
|
|
}
|
|
|
|
|
|
+ // 懒加载一个 ImageLoader,带 SvgDecoder
|
|
|
+ private val svgImageLoader: ImageLoader by lazy {
|
|
|
+ val ctx = requireNotNull(SIKCore.getApplication()) {
|
|
|
+ "BitmapUtil not initialized. Call BitmapUtil.init(context) once (e.g. in Application)."
|
|
|
+ }
|
|
|
+ ImageLoader.Builder(ctx)
|
|
|
+ .components { add(SvgDecoder.Factory()) }
|
|
|
+ .allowRgb565(false) // 我们需要 ARGB_8888
|
|
|
+ .crossfade(false)
|
|
|
+ .build()
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 统一入口:自动判断 Bitmap vs SVG
|
|
|
+ */
|
|
|
+ fun loadImageFromFile(
|
|
|
+ file: File,
|
|
|
+ outWidth: Int,
|
|
|
+ outHeight: Int,
|
|
|
+ allowUpscale: Boolean = true,
|
|
|
+ fixOrientation: Boolean = true,
|
|
|
+ backgroundColor: Int? = null,
|
|
|
+ stretchToFill: Boolean = true
|
|
|
+ ): Bitmap? {
|
|
|
+ return if (isSvg(file)) {
|
|
|
+ loadSvgFromFile(
|
|
|
+ file = file,
|
|
|
+ outWidth = outWidth,
|
|
|
+ outHeight = outHeight,
|
|
|
+ allowUpscale = allowUpscale,
|
|
|
+ backgroundColor = backgroundColor,
|
|
|
+ stretchToFill = stretchToFill
|
|
|
+ )
|
|
|
+ } else {
|
|
|
+ loadBitmapFromFile(
|
|
|
+ file = file,
|
|
|
+ outWidth = outWidth,
|
|
|
+ outHeight = outHeight,
|
|
|
+ allowUpscale = allowUpscale,
|
|
|
+ fixOrientation = fixOrientation,
|
|
|
+ backgroundColor = backgroundColor,
|
|
|
+ stretchToFill = stretchToFill
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 用 Coil 解码 SVG → Drawable,再按你现有规则绘制到固定尺寸 Bitmap。
|
|
|
+ * - 左上贴齐
|
|
|
+ * - stretchToFill=true:非等比拉伸填满
|
|
|
+ * - stretchToFill=false:等比 contain(留空)
|
|
|
+ * - allowUpscale=false:只缩不放
|
|
|
+ *
|
|
|
+ * 注意:这是阻塞实现(内部 runBlocking),请在后台线程调用。
|
|
|
+ */
|
|
|
+ // 保持你的签名不变
|
|
|
+ fun loadSvgFromFile(
|
|
|
+ file: File,
|
|
|
+ outWidth: Int,
|
|
|
+ outHeight: Int,
|
|
|
+ allowUpscale: Boolean = true,
|
|
|
+ backgroundColor: Int? = null,
|
|
|
+ stretchToFill: Boolean = true
|
|
|
+ ): Bitmap? {
|
|
|
+ if (!file.exists() || outWidth <= 0 || outHeight <= 0) return null
|
|
|
+ // 别在主线程调这个方法(内部会阻塞)
|
|
|
+ if (Looper.getMainLooper().isCurrentThread) {
|
|
|
+ // 你也可以改成抛异常或直接返回 null
|
|
|
+ return null
|
|
|
+ }
|
|
|
+
|
|
|
+ val app = SIKCore.getApplication() ?: return null
|
|
|
+
|
|
|
+ // 懒加载一个带 SvgDecoder 的 ImageLoader(只建一次)
|
|
|
+ val loader by lazy {
|
|
|
+ ImageLoader.Builder(app)
|
|
|
+ .components { add(SvgDecoder.Factory()) }
|
|
|
+ .allowHardware(false) // 我们会在软件 Canvas 上画
|
|
|
+ .build()
|
|
|
+ }
|
|
|
+
|
|
|
+ // 1) 同步解成 Drawable(成功会是 PictureDrawable 或普通 Drawable)
|
|
|
+ val drawable = runCatching {
|
|
|
+ val req = ImageRequest.Builder(app)
|
|
|
+ .data(file) // 就是本地 .svg 文件
|
|
|
+ .size(Size.ORIGINAL) // 拿矢量原始尺寸,由我们来缩放
|
|
|
+ .allowHardware(false)
|
|
|
+ .build()
|
|
|
+ val res = kotlinx.coroutines.runBlocking { loader.execute(req) }
|
|
|
+ (res as? SuccessResult)?.drawable
|
|
|
+ }.getOrNull() ?: return null
|
|
|
+
|
|
|
+ // 2) 源尺寸(intrinsic 可能为 -1,兜底 512)
|
|
|
+ val srcW = (drawable.intrinsicWidth.takeIf { it > 0 } ?: 512).toFloat()
|
|
|
+ val srcH = (drawable.intrinsicHeight.takeIf { it > 0 } ?: 512).toFloat()
|
|
|
+ if (srcW <= 0f || srcH <= 0f) return null
|
|
|
+
|
|
|
+ // 3) 计算缩放(与你位图路径一致)
|
|
|
+ val sxRaw = outWidth / srcW
|
|
|
+ val syRaw = outHeight / srcH
|
|
|
+ val (sx, sy) = if (stretchToFill) {
|
|
|
+ val sx = if (allowUpscale) sxRaw else minOf(1f, sxRaw)
|
|
|
+ val sy = if (allowUpscale) syRaw else minOf(1f, syRaw)
|
|
|
+ sx to sy
|
|
|
+ } else {
|
|
|
+ val s = minOf(sxRaw, syRaw)
|
|
|
+ val sFixed = if (!allowUpscale && s > 1f) 1f else s
|
|
|
+ sFixed to sFixed
|
|
|
+ }
|
|
|
+ val drawW = maxOf(1, (srcW * sx).toInt())
|
|
|
+ val drawH = maxOf(1, (srcH * sy).toInt())
|
|
|
+
|
|
|
+ // 4) 画到固定尺寸画布(左上贴齐)
|
|
|
+ val out = Bitmap.createBitmap(outWidth, outHeight, Bitmap.Config.ARGB_8888)
|
|
|
+ val canvas = Canvas(out)
|
|
|
+ if (backgroundColor != null) canvas.drawColor(backgroundColor) else canvas.drawColor(Color.TRANSPARENT)
|
|
|
+
|
|
|
+ val oldAlpha = drawable.alpha
|
|
|
+ val oldBounds = drawable.bounds
|
|
|
+ try {
|
|
|
+ drawable.setBounds(0, 0, drawW, drawH) // 左上角贴齐
|
|
|
+ drawable.alpha = 255
|
|
|
+ drawable.draw(canvas)
|
|
|
+ } catch (exception: Throwable) {
|
|
|
+ LogUtil.e(exception)
|
|
|
+ return null
|
|
|
+ } finally {
|
|
|
+ drawable.alpha = oldAlpha
|
|
|
+ drawable.bounds = oldBounds
|
|
|
+ }
|
|
|
+ return out
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 从文件解码并缩放到目标画布(左上对齐,留空不裁剪)。
|
|
|
* - 等比缩放 (contain)
|