絵文字デコード先のビットマップの大きさを入力データのアスペクト比に合わせる
This commit is contained in:
parent
1ccd05e08a
commit
a0243ee8b9
|
@ -6,9 +6,11 @@ import jp.juggler.util.data.encodeUTF8
|
|||
import java.io.InputStream
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.round
|
||||
import kotlin.math.sqrt
|
||||
|
||||
class ApngFrames private constructor(
|
||||
private val pixelSizeMax: Int = 0,
|
||||
private val pixelSizeMax: Float = 0f,
|
||||
private val debug: Boolean = false,
|
||||
) : ApngDecoderCallback, MyGifDecoderCallback {
|
||||
|
||||
|
@ -29,43 +31,64 @@ class ApngFrames private constructor(
|
|||
color = 0
|
||||
}
|
||||
|
||||
// return w,h
|
||||
fun scaleEmojiSize(
|
||||
srcW: Float,
|
||||
srcH: Float,
|
||||
maxSize: Float,
|
||||
): Pair<Float, Float> = when {
|
||||
// 入力サイズの情報がない
|
||||
srcW <= 0f || srcH <= 0f -> Pair(maxSize, maxSize)
|
||||
else -> {
|
||||
val sqMax = maxSize * maxSize
|
||||
val sqOriginal = srcW * srcH
|
||||
val aspect = srcW / srcH
|
||||
when {
|
||||
// 既に十分小さい
|
||||
sqOriginal <= sqMax -> Pair(srcW, srcH)
|
||||
// アスペクト比に応じたスケーリング
|
||||
else -> Pair(
|
||||
sqrt(sqMax * aspect),
|
||||
sqrt(sqMax / aspect),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createBlankBitmap(w: Int, h: Int) =
|
||||
Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
|
||||
|
||||
private fun scale(max: Int, num: Int, den: Int) =
|
||||
(max.toFloat() * num.toFloat() / den.toFloat() + 0.5f).toInt()
|
||||
|
||||
private fun scaleBitmap(
|
||||
sizeMax: Int,
|
||||
sizeMax: Float,
|
||||
src: Bitmap,
|
||||
recycleSrc: Boolean = true, // true: ownership of "src" will be moved or recycled.
|
||||
): Bitmap {
|
||||
|
||||
if (sizeMax <= 0) {
|
||||
return when {
|
||||
recycleSrc -> src
|
||||
else -> src.copy(Bitmap.Config.ARGB_8888, false)
|
||||
}
|
||||
}
|
||||
val wSrc = src.width
|
||||
val hSrc = src.height
|
||||
if (sizeMax <= 0 || (wSrc <= sizeMax && hSrc <= sizeMax)) {
|
||||
return if (recycleSrc) {
|
||||
src
|
||||
} else {
|
||||
src.copy(Bitmap.Config.ARGB_8888, false)
|
||||
val (wDst, hDst) = scaleEmojiSize(
|
||||
wSrc.toFloat(),
|
||||
hSrc.toFloat(),
|
||||
sizeMax,
|
||||
)
|
||||
val wDstInt = max(1, round(wDst).toInt())
|
||||
val hDstInt = max(1, round(hDst).toInt())
|
||||
if (wSrc <= wDstInt && hSrc <= hDstInt ) {
|
||||
return when {
|
||||
recycleSrc -> src
|
||||
else -> src.copy(Bitmap.Config.ARGB_8888, false)
|
||||
}
|
||||
}
|
||||
|
||||
val wDst: Int
|
||||
val hDst: Int
|
||||
if (wSrc >= hSrc) {
|
||||
wDst = sizeMax
|
||||
hDst = max(1, scale(sizeMax, hSrc, wSrc))
|
||||
} else {
|
||||
hDst = sizeMax
|
||||
wDst = max(1, scale(sizeMax, wSrc, hSrc))
|
||||
}
|
||||
//Log.v(TAG,"scaleBitmap: $wSrc,$hSrc => $wDst,$hDst")
|
||||
|
||||
val b2 = createBlankBitmap(wDst, hDst)
|
||||
val b2 = createBlankBitmap(wDstInt, hDstInt)
|
||||
val canvas = Canvas(b2)
|
||||
val rectSrc = Rect(0, 0, wSrc, hSrc)
|
||||
val rectDst = Rect(0, 0, wDst, hDst)
|
||||
val rectDst = Rect(0, 0, wDstInt, hDstInt)
|
||||
canvas.drawBitmap(src, rectSrc, rectDst, sPaintDontBlend)
|
||||
|
||||
if (recycleSrc) src.recycle()
|
||||
|
@ -83,12 +106,12 @@ class ApngFrames private constructor(
|
|||
Bitmap.Config.ARGB_8888
|
||||
)
|
||||
|
||||
private fun toAndroidBitmap(src: ApngBitmap, sizeMax: Int) =
|
||||
private fun toAndroidBitmap(src: ApngBitmap, sizeMax: Float) =
|
||||
scaleBitmap(sizeMax, toAndroidBitmap(src))
|
||||
|
||||
private fun parseApng(
|
||||
inStream: InputStream,
|
||||
pixelSizeMax: Int,
|
||||
pixelSizeMax: Float,
|
||||
debug: Boolean = false,
|
||||
): ApngFrames {
|
||||
val result = ApngFrames(pixelSizeMax, debug)
|
||||
|
@ -105,7 +128,7 @@ class ApngFrames private constructor(
|
|||
|
||||
private fun parseWebP(
|
||||
inStream: InputStream,
|
||||
pixelSizeMax: Int,
|
||||
pixelSizeMax: Float,
|
||||
debug: Boolean = false,
|
||||
): ApngFrames {
|
||||
val result = ApngFrames(pixelSizeMax, debug)
|
||||
|
@ -122,7 +145,7 @@ class ApngFrames private constructor(
|
|||
|
||||
private fun parseGif(
|
||||
inStream: InputStream,
|
||||
pixelSizeMax: Int,
|
||||
pixelSizeMax: Float,
|
||||
debug: Boolean = false,
|
||||
): ApngFrames {
|
||||
val result = ApngFrames(pixelSizeMax, debug)
|
||||
|
@ -166,7 +189,7 @@ class ApngFrames private constructor(
|
|||
}
|
||||
|
||||
fun parse(
|
||||
pixelSizeMax: Int,
|
||||
pixelSizeMax: Float,
|
||||
debug: Boolean = false,
|
||||
opener: () -> InputStream?,
|
||||
): ApngFrames? {
|
||||
|
@ -284,7 +307,7 @@ class ApngFrames private constructor(
|
|||
val animationControl = this.animationControl
|
||||
val frames = this.frames
|
||||
|
||||
if (animationControl == null || frames == null || frames.isEmpty()) {
|
||||
if (animationControl == null || frames.isNullOrEmpty()) {
|
||||
// ここは通らないはず…
|
||||
result.bitmap = null
|
||||
result.delay = Long.MAX_VALUE
|
||||
|
@ -409,6 +432,7 @@ class ApngFrames private constructor(
|
|||
frameControl.width,
|
||||
frameControl.height
|
||||
)
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
package jp.juggler.subwaytooter
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import jp.juggler.util.data.*
|
||||
import org.junit.Assert.fail
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class TestBitmapSample {
|
||||
|
||||
/**
|
||||
* BitmapFactory.Options.inSampleSize の取り扱いの確認
|
||||
*/
|
||||
@Test
|
||||
fun test() {
|
||||
val srcSize = 1024
|
||||
|
||||
val baSrc = run {
|
||||
val bitmap = Bitmap.createBitmap(srcSize, srcSize, Bitmap.Config.ARGB_8888)
|
||||
try {
|
||||
ByteArrayOutputStream().use { outStream ->
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 70, outStream)
|
||||
outStream.toByteArray()
|
||||
}
|
||||
} finally {
|
||||
bitmap.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
for (n in 0..32) {
|
||||
val options = BitmapFactory.Options().apply {
|
||||
inScaled = false
|
||||
outWidth = 0
|
||||
outHeight = 0
|
||||
inJustDecodeBounds = false
|
||||
inSampleSize = n
|
||||
}
|
||||
val expectedSizeA: Int
|
||||
val expectedSizeB: Int
|
||||
when (n) {
|
||||
// ドキュメントには "If set to a value > 1" とあり、1以下の値はリサイズに影響しない
|
||||
0, 1 -> {
|
||||
expectedSizeA = srcSize
|
||||
expectedSizeB = srcSize
|
||||
}
|
||||
// 2以上の場合、ドキュメントには
|
||||
// "Note: the decoder uses a final value based on powers of 2, any other value will be rounded down to the nearest power of 2."
|
||||
// とあるが、実際に試すと端末により width = srcSize/n となることがある。
|
||||
else -> {
|
||||
expectedSizeA = srcSize.div(n.takeHighestOneBit())
|
||||
expectedSizeB = srcSize.div(n)
|
||||
}
|
||||
}
|
||||
val bitmap = BitmapFactory.decodeByteArray(baSrc, 0, baSrc.size, options)!!
|
||||
try {
|
||||
when (bitmap.width) {
|
||||
expectedSizeA -> Unit
|
||||
expectedSizeB -> Unit
|
||||
else -> fail("inSampleSize=$n, srcSize=$srcSize, resultWidth=${bitmap.width}")
|
||||
}
|
||||
} finally {
|
||||
bitmap.recycle()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -72,7 +72,7 @@ class ActText : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
class SearchHilightSpan(color: Int) : BackgroundColorSpan(color)
|
||||
class SearchResultSpan(color: Int) : BackgroundColorSpan(color)
|
||||
|
||||
private var account: SavedAccount? = null
|
||||
|
||||
|
@ -360,7 +360,7 @@ class ActText : AppCompatActivity() {
|
|||
|
||||
private fun searchHighlight(newPos: Int?) {
|
||||
views.etText.text?.let { e ->
|
||||
for (span in e.getSpans(0, e.length, SearchHilightSpan::class.java)) {
|
||||
for (span in e.getSpans(0, e.length, SearchResultSpan::class.java)) {
|
||||
try {
|
||||
e.removeSpan(span)
|
||||
} catch (ignored: Throwable) {
|
||||
|
@ -372,7 +372,7 @@ class ActText : AppCompatActivity() {
|
|||
else -> R.attr.colorButtonBgCw
|
||||
}
|
||||
e.setSpan(
|
||||
SearchHilightSpan(attrColor(attrId)),
|
||||
SearchResultSpan(attrColor(attrId)),
|
||||
pos,
|
||||
pos + searchKeywordLength,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE,
|
||||
|
|
|
@ -9,21 +9,23 @@ import android.os.Handler
|
|||
import android.os.SystemClock
|
||||
import com.caverock.androidsvg.SVG
|
||||
import jp.juggler.apng.ApngFrames
|
||||
import jp.juggler.apng.ApngFrames.Companion.scaleEmojiSize
|
||||
import jp.juggler.subwaytooter.App1
|
||||
import jp.juggler.subwaytooter.pref.PrefS
|
||||
import jp.juggler.subwaytooter.table.EmojiCacheDbOpenHelper
|
||||
import jp.juggler.subwaytooter.table.daoImageAspect
|
||||
import jp.juggler.util.coroutine.EmptyScope
|
||||
import jp.juggler.util.data.*
|
||||
import jp.juggler.util.log.*
|
||||
import jp.juggler.util.data.clip
|
||||
import jp.juggler.util.log.LogCategory
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.*
|
||||
import java.util.LinkedList
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.max
|
||||
|
||||
class CustomEmojiCache(
|
||||
val context: Context,
|
||||
|
@ -248,7 +250,7 @@ class CustomEmojiCache(
|
|||
|
||||
ts = elapsedTime
|
||||
val frames = try {
|
||||
data?.let { decodeAPNG(it, request.url) }
|
||||
data?.let { decodeImage(it, request.url) }
|
||||
} catch (ex: Throwable) {
|
||||
log.e(ex, "decode failed.")
|
||||
null
|
||||
|
@ -327,13 +329,13 @@ class CustomEmojiCache(
|
|||
}
|
||||
}
|
||||
|
||||
private fun decodeAPNG(data: ByteArray, url: String): ApngFrames? {
|
||||
private fun decodeImage(data: ByteArray, url: String): ApngFrames? {
|
||||
val errors = ArrayList<Throwable>()
|
||||
|
||||
val maxSize = PrefS.spEmojiPixels.toInt().clip(16, 1024)
|
||||
val maxSize = PrefS.spEmojiPixels.toInt().clip(16, 1024).toFloat()
|
||||
|
||||
try {
|
||||
// APNGをデコード AWebPも
|
||||
// APNG,AWebP,AGIF
|
||||
val x = ApngFrames.parse(maxSize) { ByteArrayInputStream(data) }
|
||||
if (x != null) return x
|
||||
error("ApngFrames.parse returns null.")
|
||||
|
@ -354,7 +356,7 @@ class CustomEmojiCache(
|
|||
|
||||
// SVGのロードを試みる
|
||||
try {
|
||||
val b = decodeSVG(url, data, maxSize.toFloat())
|
||||
val b = decodeSVG(url, data, maxSize)
|
||||
if (b != null) return ApngFrames(b)
|
||||
error("decodeSVG returns null.")
|
||||
} catch (ex: Throwable) {
|
||||
|
@ -373,22 +375,27 @@ class CustomEmojiCache(
|
|||
|
||||
private fun decodeBitmap(
|
||||
data: ByteArray,
|
||||
@Suppress("SameParameterValue") pixelMax: Int,
|
||||
maxPixels: Float,
|
||||
): Bitmap? {
|
||||
options.inJustDecodeBounds = true
|
||||
options.inScaled = false
|
||||
options.outWidth = 0
|
||||
options.outHeight = 0
|
||||
BitmapFactory.decodeByteArray(data, 0, data.size, options)
|
||||
var w = options.outWidth
|
||||
var h = options.outHeight
|
||||
if (w <= 0 || h <= 0) error("decodeBitmap: can't decode bounds.")
|
||||
var srcW = options.outWidth
|
||||
var srcH = options.outHeight
|
||||
if (srcW <= 0 || srcH <= 0) error("decodeBitmap: can't decode bounds.")
|
||||
|
||||
val (preferW, preferH) = scaleEmojiSize(
|
||||
srcW.toFloat(),
|
||||
srcH.toFloat(),
|
||||
maxPixels
|
||||
)
|
||||
var bits = 0
|
||||
while (w > pixelMax || h > pixelMax) {
|
||||
while (srcW > preferW || srcW > preferH) {
|
||||
++bits
|
||||
w = w shr 1
|
||||
h = h shr 1
|
||||
srcW = srcW shr 1
|
||||
srcH = srcH shr 1
|
||||
}
|
||||
options.inJustDecodeBounds = false
|
||||
options.inSampleSize = 1 shl bits
|
||||
|
@ -398,33 +405,20 @@ class CustomEmojiCache(
|
|||
private fun decodeSVG(
|
||||
url: String,
|
||||
data: ByteArray,
|
||||
@Suppress("SameParameterValue") pixelMax: Float,
|
||||
maxSize: Float,
|
||||
): Bitmap? {
|
||||
try {
|
||||
val svg = SVG.getFromInputStream(ByteArrayInputStream(data))
|
||||
|
||||
// the width in pixels, or -1 if there is no width available.
|
||||
// the height in pixels, or -1 if there is no height available.
|
||||
val srcW = svg.documentWidth
|
||||
val srcH = svg.documentHeight
|
||||
val aspect = if (srcW <= 0f || srcH <= 0f) {
|
||||
// widthやheightの情報がない
|
||||
1f
|
||||
} else {
|
||||
srcW / srcH
|
||||
}
|
||||
|
||||
val dstW: Float
|
||||
val dstH: Float
|
||||
if (aspect >= 1f) {
|
||||
dstW = pixelMax
|
||||
dstH = pixelMax / aspect
|
||||
} else {
|
||||
dstH = pixelMax
|
||||
dstW = pixelMax * aspect
|
||||
}
|
||||
val wCeil = ceil(dstW)
|
||||
val hCeil = ceil(dstH)
|
||||
val (wDst, hDst) = scaleEmojiSize(
|
||||
// the width in pixels, or -1 if there is no width available.
|
||||
svg.documentWidth,
|
||||
// the height in pixels, or -1 if there is no height available.
|
||||
svg.documentHeight,
|
||||
maxSize
|
||||
)
|
||||
val wCeil = max(1f, ceil(wDst))
|
||||
val hCeil = max(1f, ceil(hDst))
|
||||
|
||||
// Create a Bitmap to render our SVG to
|
||||
val b = Bitmap.createBitmap(wCeil.toInt(), hCeil.toInt(), Bitmap.Config.ARGB_8888)
|
||||
|
@ -433,10 +427,10 @@ class CustomEmojiCache(
|
|||
|
||||
svg.renderToCanvas(
|
||||
canvas,
|
||||
if (aspect >= 1f) {
|
||||
RectF(0f, hCeil - dstH, dstW, dstH) // 後半はw,hを指定する
|
||||
if (wDst >= hDst) {
|
||||
RectF(0f, hCeil - hDst, wDst, hDst) // 後半はw,hを指定する
|
||||
} else {
|
||||
RectF(wCeil - dstW, 0f, dstW, dstH) // 後半はw,hを指定する
|
||||
RectF(wCeil - wDst, 0f, wDst, hDst) // 後半はw,hを指定する
|
||||
}
|
||||
)
|
||||
return b
|
||||
|
|
|
@ -181,7 +181,7 @@ class ActList : AppCompatActivity(), CoroutineScope {
|
|||
|
||||
val job = async(AppDispatchers.IO) {
|
||||
try {
|
||||
ApngFrames.parse(128) { resources?.openRawResource(resId) }
|
||||
ApngFrames.parse(128f) { resources?.openRawResource(resId) }
|
||||
} catch (ex: Throwable) {
|
||||
ex.printStackTrace()
|
||||
null
|
||||
|
|
|
@ -62,7 +62,7 @@ class ActViewer : AsyncActivity() {
|
|||
apngFrames = withContext(AppDispatchers.IO) {
|
||||
try {
|
||||
ApngFrames.parse(
|
||||
1024,
|
||||
1024f,
|
||||
debug = true
|
||||
) { resources?.openRawResource(resId) }
|
||||
} catch (ex: Throwable) {
|
||||
|
|
Loading…
Reference in New Issue