fix #242, 画像をリサイズせずに送ろうとした際にエラーになる問題の修正。 「アプリ設定/投稿/可能ならWebPを使う」を追加。

This commit is contained in:
tateisu 2023-05-14 10:17:01 +09:00
parent cfee3771a6
commit e55d60e7c1
6 changed files with 121 additions and 38 deletions

View File

@ -450,6 +450,11 @@ val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_sett
)
sw(PrefB.bpIgnoreTextInSharedMedia, R.string.ignore_text_in_shared_media)
sw(
PrefB.bpUseWebP,
R.string.use_webp_format_if_server_accepts,
)
}
section(R.string.tablet_mode) {

View File

@ -368,4 +368,8 @@ object PrefB {
"CollapseEmojiPickerCategory",
true
)
val bpUseWebP = BooleanPref(
"UseWebP",
true
)
}

View File

@ -3,12 +3,17 @@ package jp.juggler.subwaytooter.util
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
import jp.juggler.media.generateTempFile
import jp.juggler.media.transcodeAudio
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.entity.InstanceType
import jp.juggler.subwaytooter.api.entity.TootInstance
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.util.AttachmentUploader.Companion.MIME_TYPE_JPEG
import jp.juggler.subwaytooter.util.AttachmentUploader.Companion.MIME_TYPE_PNG
import jp.juggler.subwaytooter.util.AttachmentUploader.Companion.MIME_TYPE_WEBP
import jp.juggler.util.data.JsonObject
import jp.juggler.util.data.getStreamSize
import jp.juggler.util.log.LogCategory
@ -18,6 +23,7 @@ import jp.juggler.util.media.createResizedBitmap
import jp.juggler.util.media.transcodeVideo
import java.io.File
import java.io.FileOutputStream
import kotlin.math.min
class AttachmentRequest(
val context: Context,
@ -62,20 +68,9 @@ class AttachmentRequest(
return contentUriOpener(context.contentResolver, uri, mimeType, isImage = true)
}
// 静止画
if (mimeType.startsWith("image")) {
// 静止画(失敗したらオリジナルデータにフォールバックする)
if (mimeType == AttachmentUploader.MIME_TYPE_JPEG ||
mimeType == AttachmentUploader.MIME_TYPE_PNG
) try {
// 回転対応が必要かもしれない
return createResizedImageOpener()
} catch (ex: Throwable) {
log.w(ex, "createResizedImageOpener failed. fall back to original image.")
}
// 静止画(変換必須)
// 例外を投げるかもしれない
return createResizedImageOpener(forcePng = true)
return createResizedImageOpener()
}
// 音声と動画のファイル区分は曖昧なので
@ -143,46 +138,122 @@ class AttachmentRequest(
else -> AttachmentUploader.acceptableMimeTypes
}.contains(mimeType)
private fun createResizedImageOpener(
forcePng: Boolean = false,
): InputStreamOpener {
val tempFile = context.generateTempFile("createResizedImageOpener")
private fun createResizedImageOpener(): InputStreamOpener {
try {
pa.progress = context.getString(R.string.attachment_handling_compress)
val bitmap = createResizedBitmap(
createResizedBitmap(
context,
uri,
imageResizeConfig,
skipIfNoNeedToResizeAndRotate = !forcePng,
skipIfNoNeedToResizeAndRotate = true,
serverMaxSqPixel = serverMaxSqPixel
) ?: error("createResizedBitmap returns null.")
try {
val outputMimeType = when {
forcePng || mimeType == AttachmentUploader.MIME_TYPE_PNG ->
AttachmentUploader.MIME_TYPE_PNG
)?.let { bitmap ->
try {
val canUseWebP = hasServerSupport(MIME_TYPE_WEBP) &&
PrefB.bpUseWebP.value
if (canUseWebP) {
try {
val format = when {
Build.VERSION.SDK_INT >= 30 ->
Bitmap.CompressFormat.WEBP_LOSSY
else -> AttachmentUploader.MIME_TYPE_JPEG
}
FileOutputStream(tempFile).use { outStream ->
when (outputMimeType) {
AttachmentUploader.MIME_TYPE_PNG ->
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outStream)
else ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 95, outStream)
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 {
bitmap.recycle()
}
return tempFileOpener(tempFile, outputMimeType, isImage = true)
} finally {
bitmap.recycle()
}
// nullを返す場合もここを通る
} catch (ex: Throwable) {
log.w(ex, "createResizedBitmap failed.")
}
// 元のデータを返す
return contentUriOpener(context.contentResolver, uri, mimeType, isImage = true)
}
/**
* Bitmapを指定フォーマットで圧縮して tempFileOpener を返す
* 失敗したら例外を投げる
*/
private fun Bitmap.compressToTempFileOpener(
outMimeType: String,
format: Bitmap.CompressFormat,
quality: Int,
): InputStreamOpener {
val tempFile = context.generateTempFile("createResizedImageOpener")
try {
FileOutputStream(tempFile).use { compress(format, quality, it) }
return tempFileOpener(tempFile, outMimeType, isImage = true)
} catch (ex: Throwable) {
tempFile.delete()
throw ex
}
}
/**
* ビットマップのアルファ値が0xFFではないピクセルがあれば真
*/
private fun Bitmap.scanAlpha(): Boolean {
try {
val w = this.width
val h = this.height
if (w > 0 && h > 0) {
val hStep = 64
val pixels = IntArray(w * min(hStep, h))
for (y in 0 until h step hStep) {
val hPart = min(hStep, h - y)
getPixels(
/* pixels */ pixels,
/* offset */ 0,
/* stride */ w,
/* x */ 0,
/* y */ y,
/* width */ w,
/* height */ hPart,
)
for (i in 0 until (w * hPart)) {
if (pixels[i].ushr(24) != 0xff) return true
}
}
}
} catch (ex: Throwable) {
log.w(ex, "scanAlpha failed.")
}
return false
}
private suspend fun createResizedVideoOpener(): InputStreamOpener {
val cacheDir = context.externalCacheDir

View File

@ -48,6 +48,7 @@ class AttachmentUploader(
internal const val MIME_TYPE_JPEG = "image/jpeg"
internal const val MIME_TYPE_PNG = "image/png"
internal const val MIME_TYPE_GIF = "image/gif"
internal const val MIME_TYPE_WEBP = "image/webp"
val acceptableMimeTypes = HashSet<String>().apply {
//
@ -289,7 +290,7 @@ class AttachmentUploader(
}
if (opener.contentLength > maxBytes) {
return TootApiResult(
safeContext.getString(R.string.file_size_too_big, maxBytes / 1000000)
safeContext.getString(R.string.file_size_too_big, maxBytes / 1_000_000)
)
}

View File

@ -1286,5 +1286,6 @@
<string name="save_to_local_folder">ローカルフォルダに保存</string>
<string name="app_data_export_import">アプリデータのエクスポート/インポート</string>
<string name="translate_or_custom_share">翻訳ボタン / カスタム共有ボタン</string>
<string name="use_webp_format_if_server_accepts">可能ならWebPフォーマットを使う</string>
</resources>

View File

@ -1297,4 +1297,5 @@
<string name="translate_or_custom_share">Translate/Custom share buttons</string>
<string name="search_result" translatable="false">%1$d/%2$d%3$s</string>
<string name="toggle_regexp">.+\?</string>
<string name="use_webp_format_if_server_accepts">Use WebP format if server accepts.</string>
</resources>