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.bpIgnoreTextInSharedMedia, R.string.ignore_text_in_shared_media)
sw(
PrefB.bpUseWebP,
R.string.use_webp_format_if_server_accepts,
)
} }
section(R.string.tablet_mode) { section(R.string.tablet_mode) {

View File

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

View File

@ -3,12 +3,17 @@ package jp.juggler.subwaytooter.util
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.net.Uri import android.net.Uri
import android.os.Build
import jp.juggler.media.generateTempFile import jp.juggler.media.generateTempFile
import jp.juggler.media.transcodeAudio import jp.juggler.media.transcodeAudio
import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.entity.InstanceType import jp.juggler.subwaytooter.api.entity.InstanceType
import jp.juggler.subwaytooter.api.entity.TootInstance import jp.juggler.subwaytooter.api.entity.TootInstance
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.table.SavedAccount 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.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
@ -18,6 +23,7 @@ import jp.juggler.util.media.createResizedBitmap
import jp.juggler.util.media.transcodeVideo import jp.juggler.util.media.transcodeVideo
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import kotlin.math.min
class AttachmentRequest( class AttachmentRequest(
val context: Context, val context: Context,
@ -62,20 +68,9 @@ class AttachmentRequest(
return contentUriOpener(context.contentResolver, uri, mimeType, isImage = true) return contentUriOpener(context.contentResolver, uri, mimeType, isImage = true)
} }
// 静止画
if (mimeType.startsWith("image")) { if (mimeType.startsWith("image")) {
// 静止画(失敗したらオリジナルデータにフォールバックする) return createResizedImageOpener()
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)
} }
// 音声と動画のファイル区分は曖昧なので // 音声と動画のファイル区分は曖昧なので
@ -143,46 +138,122 @@ class AttachmentRequest(
else -> AttachmentUploader.acceptableMimeTypes else -> AttachmentUploader.acceptableMimeTypes
}.contains(mimeType) }.contains(mimeType)
private fun createResizedImageOpener( private fun createResizedImageOpener(): InputStreamOpener {
forcePng: Boolean = false,
): InputStreamOpener {
val tempFile = context.generateTempFile("createResizedImageOpener")
try { try {
pa.progress = context.getString(R.string.attachment_handling_compress) pa.progress = context.getString(R.string.attachment_handling_compress)
createResizedBitmap(
val bitmap = createResizedBitmap(
context, context,
uri, uri,
imageResizeConfig, imageResizeConfig,
skipIfNoNeedToResizeAndRotate = !forcePng, skipIfNoNeedToResizeAndRotate = true,
serverMaxSqPixel = serverMaxSqPixel serverMaxSqPixel = serverMaxSqPixel
) ?: error("createResizedBitmap returns null.") )?.let { bitmap ->
try { try {
val outputMimeType = when { val canUseWebP = hasServerSupport(MIME_TYPE_WEBP) &&
forcePng || mimeType == AttachmentUploader.MIME_TYPE_PNG -> PrefB.bpUseWebP.value
AttachmentUploader.MIME_TYPE_PNG if (canUseWebP) {
try {
val format = when {
Build.VERSION.SDK_INT >= 30 ->
Bitmap.CompressFormat.WEBP_LOSSY
else -> AttachmentUploader.MIME_TYPE_JPEG else ->
} @Suppress("DEPRECATION")
FileOutputStream(tempFile).use { outStream -> Bitmap.CompressFormat.WEBP
when (outputMimeType) { }
AttachmentUploader.MIME_TYPE_PNG -> return bitmap.compressToTempFileOpener(MIME_TYPE_WEBP, format, 90)
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outStream) } catch (ex: Throwable) {
log.w(ex, "compress to WebP lossy failed.")
else -> // 失敗したらJPEG or PNG にフォールバック
bitmap.compress(Bitmap.CompressFormat.JPEG, 95, outStream) }
} }
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) { } catch (ex: Throwable) {
tempFile.delete() tempFile.delete()
throw ex 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 { private suspend fun createResizedVideoOpener(): InputStreamOpener {
val cacheDir = context.externalCacheDir 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_JPEG = "image/jpeg"
internal const val MIME_TYPE_PNG = "image/png" internal const val MIME_TYPE_PNG = "image/png"
internal const val MIME_TYPE_GIF = "image/gif" internal const val MIME_TYPE_GIF = "image/gif"
internal const val MIME_TYPE_WEBP = "image/webp"
val acceptableMimeTypes = HashSet<String>().apply { val acceptableMimeTypes = HashSet<String>().apply {
// //
@ -289,7 +290,7 @@ class AttachmentUploader(
} }
if (opener.contentLength > maxBytes) { if (opener.contentLength > maxBytes) {
return TootApiResult( 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="save_to_local_folder">ローカルフォルダに保存</string>
<string name="app_data_export_import">アプリデータのエクスポート/インポート</string> <string name="app_data_export_import">アプリデータのエクスポート/インポート</string>
<string name="translate_or_custom_share">翻訳ボタン / カスタム共有ボタン</string> <string name="translate_or_custom_share">翻訳ボタン / カスタム共有ボタン</string>
<string name="use_webp_format_if_server_accepts">可能ならWebPフォーマットを使う</string>
</resources> </resources>

View File

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