1
0
mirror of https://github.com/tateisu/SubwayTooter synced 2025-02-07 23:58:44 +01:00

リサイズ不要なPNGもWebPに変換する

This commit is contained in:
tateisu 2023-05-14 10:52:34 +09:00
parent bdbbe5583c
commit f45ebd780f
2 changed files with 96 additions and 66 deletions

View File

@ -17,6 +17,7 @@ import jp.juggler.subwaytooter.util.AttachmentUploader.Companion.MIME_TYPE_WEBP
import jp.juggler.util.data.JsonObject import jp.juggler.util.data.JsonObject
import jp.juggler.util.data.getStreamSize import jp.juggler.util.data.getStreamSize
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.errorEx
import jp.juggler.util.media.ResizeConfig import jp.juggler.util.media.ResizeConfig
import jp.juggler.util.media.VideoInfo.Companion.videoInfo import jp.juggler.util.media.VideoInfo.Companion.videoInfo
import jp.juggler.util.media.createResizedBitmap import jp.juggler.util.media.createResizedBitmap
@ -141,55 +142,28 @@ class AttachmentRequest(
private fun createResizedImageOpener(): InputStreamOpener { private fun createResizedImageOpener(): InputStreamOpener {
try { try {
pa.progress = context.getString(R.string.attachment_handling_compress) pa.progress = context.getString(R.string.attachment_handling_compress)
val canUseWebP = try {
hasServerSupport(MIME_TYPE_WEBP) && PrefB.bpUseWebP.value
} catch (ex: Throwable) {
log.w(ex, "can't check canUseWebP")
false
}
// サーバが読めない形式の画像なら強制的に再圧縮をかける
// もしくは、PNG画像も可能ならWebPに変換したい
val canUseOriginal = hasServerSupport(mimeType) &&
!(mimeType == MIME_TYPE_PNG && canUseWebP)
createResizedBitmap( createResizedBitmap(
context, context,
uri, uri,
imageResizeConfig, imageResizeConfig,
skipIfNoNeedToResizeAndRotate = true, skipIfNoNeedToResizeAndRotate = canUseOriginal,
serverMaxSqPixel = serverMaxSqPixel serverMaxSqPixel = serverMaxSqPixel
)?.let { bitmap -> )?.let { bitmap ->
try { try {
val canUseWebP = hasServerSupport(MIME_TYPE_WEBP) && return bitmap.compressAutoType(canUseWebP)
PrefB.bpUseWebP.value
if (canUseWebP) {
try {
val format = when {
Build.VERSION.SDK_INT >= 30 ->
Bitmap.CompressFormat.WEBP_LOSSY
else ->
@Suppress("DEPRECATION")
Bitmap.CompressFormat.WEBP
}
return bitmap.compressToTempFileOpener(MIME_TYPE_WEBP, format, 90)
} catch (ex: Throwable) {
log.w(ex, "compress to WebP lossy failed.")
// 失敗したらJPEG or PNG にフォールバック
}
}
try {
// check bitmap has translucent pixels
val hasAlpha = when {
mimeType == MIME_TYPE_JPEG -> false
!bitmap.hasAlpha() -> false
else -> bitmap.scanAlpha()
}
return when (hasAlpha) {
true -> bitmap.compressToTempFileOpener(
MIME_TYPE_PNG,
Bitmap.CompressFormat.PNG,
100
)
else -> bitmap.compressToTempFileOpener(
MIME_TYPE_JPEG,
Bitmap.CompressFormat.JPEG,
95
)
}
} catch (ex: Throwable) {
log.w(ex, "compress to JPEG/PNG failed.")
}
} finally { } finally {
bitmap.recycle() bitmap.recycle()
} }
@ -203,6 +177,48 @@ class AttachmentRequest(
return contentUriOpener(context.contentResolver, uri, mimeType, isImage = true) return contentUriOpener(context.contentResolver, uri, mimeType, isImage = true)
} }
private fun Bitmap.compressAutoType(canUseWebP: Boolean): InputStreamOpener {
if (canUseWebP) {
try {
val format = when {
Build.VERSION.SDK_INT >= 30 ->
Bitmap.CompressFormat.WEBP_LOSSY
else ->
@Suppress("DEPRECATION")
Bitmap.CompressFormat.WEBP
}
return compressToTempFileOpener(MIME_TYPE_WEBP, format, 90)
} catch (ex: Throwable) {
log.w(ex, "compress to WebP lossy failed.")
// 失敗したらJPEG or PNG にフォールバック
}
}
try {
// check bitmap has translucent pixels
val hasAlpha = when {
mimeType == MIME_TYPE_JPEG -> false
!hasAlpha() -> false
else -> scanAlpha()
}
return when (hasAlpha) {
true -> compressToTempFileOpener(
MIME_TYPE_PNG,
Bitmap.CompressFormat.PNG,
100
)
else -> compressToTempFileOpener(
MIME_TYPE_JPEG,
Bitmap.CompressFormat.JPEG,
95
)
}
} catch (ex: Throwable) {
errorEx(ex, "compress to JPEG/PNG failed.")
}
}
/** /**
* Bitmapを指定フォーマットで圧縮して tempFileOpener を返す * Bitmapを指定フォーマットで圧縮して tempFileOpener を返す
* 失敗したら例外を投げる * 失敗したら例外を投げる

View File

@ -2,7 +2,13 @@ package jp.juggler.util.media
//import it.sephiroth.android.library.exif2.ExifInterface //import it.sephiroth.android.library.exif2.ExifInterface
import android.content.Context import android.content.Context
import android.graphics.* import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Matrix
import android.graphics.Paint
import android.graphics.Point
import android.graphics.PointF
import android.net.Uri import android.net.Uri
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterface
@ -11,6 +17,7 @@ import jp.juggler.util.log.showToast
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.InputStream import java.io.InputStream
import kotlin.math.max import kotlin.math.max
import kotlin.math.min
import kotlin.math.sqrt import kotlin.math.sqrt
private val log = LogCategory("BitmapUtils") private val log = LogCategory("BitmapUtils")
@ -131,11 +138,9 @@ fun createResizedBitmap(
// 真の場合、リサイズも回転も必要ないならnullを返す // 真の場合、リサイズも回転も必要ないならnullを返す
skipIfNoNeedToResizeAndRotate: Boolean = false, skipIfNoNeedToResizeAndRotate: Boolean = false,
): Bitmap? {
): Bitmap? {
try { try {
val orientation: Int? = context.contentResolver.openInputStream(uri)?.use { val orientation: Int? = context.contentResolver.openInputStream(uri)?.use {
it.imageOrientation() it.imageOrientation()
} }
@ -149,7 +154,6 @@ fun createResizedBitmap(
context.contentResolver.openInputStream(uri)?.use { context.contentResolver.openInputStream(uri)?.use {
BitmapFactory.decodeStream(it, null, options) BitmapFactory.decodeStream(it, null, options)
} }
var srcWidth = options.outWidth var srcWidth = options.outWidth
var srcHeight = options.outHeight var srcHeight = options.outHeight
if (srcWidth <= 0 || srcHeight <= 0) { if (srcWidth <= 0 || srcHeight <= 0) {
@ -167,22 +171,19 @@ fun createResizedBitmap(
ResizeType.None -> srcSize ResizeType.None -> srcSize
ResizeType.LongSide -> ResizeType.LongSide -> when {
if (max(srcSize.x, srcSize.y) <= resizeConfig.size) { max(srcSize.x, srcSize.y) <= resizeConfig.size -> srcSize
srcSize
} else { aspect >= 1f -> PointF(
if (aspect >= 1f) { resizeConfig.size.toFloat(),
PointF( sizeSpec / aspect
resizeConfig.size.toFloat(), )
sizeSpec / aspect
) else -> PointF(
} else { sizeSpec * aspect,
PointF( resizeConfig.size.toFloat()
sizeSpec * aspect, )
resizeConfig.size.toFloat() }
)
}
}
ResizeType.SquarePixel -> srcSize.limitBySqPixel(aspect, sizeSpec * sizeSpec) ResizeType.SquarePixel -> srcSize.limitBySqPixel(aspect, sizeSpec * sizeSpec)
} }
@ -191,7 +192,7 @@ fun createResizedBitmap(
dstSize = dstSize.limitBySqPixel(aspect, serverMaxSqPixel.toFloat()) dstSize = dstSize.limitBySqPixel(aspect, serverMaxSqPixel.toFloat())
} }
val dstSizeInt = Point( var dstSizeInt = Point(
max(1, (dstSize.x + 0.5f).toInt()), max(1, (dstSize.x + 0.5f).toInt()),
max(1, (dstSize.y + 0.5f).toInt()) max(1, (dstSize.y + 0.5f).toInt())
) )
@ -221,13 +222,26 @@ fun createResizedBitmap(
return null return null
} }
// リサイズする場合、ビットマップサイズ上限の成約がある
if (max(dstSizeInt.x, dstSizeInt.y) > 4096) {
val scale = 4096f / max(dstSizeInt.x, dstSizeInt.y).toFloat()
dstSize = PointF(
min(4096f, dstSize.x * scale),
min(4096f, dstSize.y * scale),
)
dstSizeInt = Point(
max(1, (dstSize.x + 0.5f).toInt()),
max(1, (dstSize.y + 0.5f).toInt())
)
}
// 長辺 // 長辺
val dstMax = max(dstSize.x, dstSize.y).toInt() val dstMax = min(4096, max(dstSize.x, dstSize.y).toInt())
// inSampleSizeを計算 // inSampleSizeを計算
var bits = 0 var bits = 0
var x = max(srcSize.x, srcSize.y).toInt() var x = max(srcSize.x, srcSize.y).toInt()
while (x > 512 && x > dstMax * 2) { while (x > 4096 || (x > 512 && x > dstMax * 2)) {
++bits ++bits
x = x shr 1 x = x shr 1
} }