Browse Source

Remove obsolete Android PNG user-avatar preparation

master
evgeny 5 days ago
parent
commit
c36fd3bfc8
  1. 41
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/AvatarRepository.kt

41
tools/chatgui-android/app/src/main/java/com/utun/chat/data/AvatarRepository.kt

@ -234,7 +234,7 @@ class AvatarRepository(context: Context) {
require(!record.failed) { "core rejected stored group avatar" }
val state = GroupAvatarState(record.version, record.hash, record.canEdit, hasImage = record.image.isNotEmpty())
val bitmap = if (record.image.isEmpty()) null else cache.get(groupKey(group, state)) ?: withContext(Dispatchers.IO) {
decode(record.image, record.hash, true)
decodeGroup(record.image, record.hash)
}
alive(epoch)
if (!_groups.value.containsKey(group)) return@launch
@ -383,30 +383,15 @@ class AvatarRepository(context: Context) {
}
}
private fun readImage(file: File): ByteArray {
val size = file.length()
require(size in 1..AvatarProtocol.MAX_USER) { "invalid cached file size=$size path=$file" }
return file.inputStream().use { input ->
val bytes = ByteArray(size.toInt()); var offset = 0
while (offset < bytes.size) {
val count = input.read(bytes, offset, bytes.size - offset)
require(count > 0) { "cached file truncated while reading" }
offset += count
}
require(input.read() == -1) { "cached file grew while reading" }
bytes
}
}
/** Проверяем размер, хеш и геометрию до выделения декодированного изображения. */
private fun decode(bytes: ByteArray, hash: String, group: Boolean): Bitmap {
require(bytes.size in 1..if (group) AvatarProtocol.MAX_GROUP else AvatarProtocol.MAX_USER) { "image byte limit" }
private fun decodeGroup(bytes: ByteArray, hash: String): Bitmap {
require(bytes.size in 1..AvatarProtocol.MAX_GROUP) { "image byte limit" }
require(digest(bytes) == hash) { "image SHA256 mismatch" }
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
BitmapFactory.decodeByteArray(bytes, 0, bytes.size, options)
val edge = if (group) 128 else 512
val edge = 128
require(options.outWidth in 1..edge && options.outHeight == options.outWidth &&
options.outMimeType == if (group) "image/jpeg" else "image/png") {
options.outMimeType == "image/jpeg") {
"invalid image ${options.outMimeType} ${options.outWidth}x${options.outHeight} limit=$edge"
}
return BitmapFactory.decodeByteArray(bytes, 0, bytes.size) ?: throw IOException("image decode failed")
@ -429,7 +414,7 @@ class AvatarRepository(context: Context) {
}
fun editGroup(group: String, uri: Uri?, expectedAccount: Long) = edit("group:$group", expectedAccount) { epoch ->
val bytes = if (uri == null) ByteArray(0) else prepare(uri, true)
val bytes = if (uri == null) ByteArray(0) else prepareGroup(uri)
alive(epoch)
require(editTokens["group:$group"] == epoch) { "group edit superseded" }
val reply = AvatarProtocol.published(request(52, group) { id, gid -> NativeLib.setGroupAvatar(id, gid, bytes) })
@ -466,14 +451,14 @@ class AvatarRepository(context: Context) {
if (account == 0L) start()
if (expectedAccount == 0L || expectedAccount != account) {
log("WARN", "Ignored stale picker result target=$key expected=${AvatarProtocol.hex(expectedAccount)} actual=${AvatarProtocol.hex(account)}")
rejectedSource?.let { file -> scope.launch(Dispatchers.IO) {
rejectedSource?.let { file -> scope.launch(Dispatchers.IO+NonCancellable) {
if (!file.delete()) log("WARN", "Cannot remove stale editor source=$file")
} }
return
}
if (account == 0L || _edits.value[key] == AvatarEditState.WORKING) {
log("WARN", "Rejected concurrent avatar edit target=$key")
rejectedSource?.let { file -> scope.launch(Dispatchers.IO) {
rejectedSource?.let { file -> scope.launch(Dispatchers.IO+NonCancellable) {
if (!file.delete()) log("WARN", "Cannot remove rejected editor source=$file")
} }
return
@ -551,7 +536,7 @@ class AvatarRepository(context: Context) {
}
/** Чтение URI, EXIF, crop и кодирование выполняются вне UI и потока ядра. */
private suspend fun prepare(uri: Uri, group: Boolean): ByteArray = preparations.withPermit { withContext(Dispatchers.IO) {
private suspend fun prepareGroup(uri: Uri): ByteArray = preparations.withPermit { withContext(Dispatchers.IO) {
val temp = File.createTempFile("avatar-source-", ".image", app.cacheDir)
try {
app.contentResolver.openInputStream(uri)?.use { input ->
@ -571,15 +556,15 @@ class AvatarRepository(context: Context) {
val decoded = PhotoUtil.decodeOriented(temp.path, sample) ?: throw IOException("source decode failed")
val edge = minOf(decoded.width, decoded.height)
val crop = Bitmap.createBitmap(decoded, (decoded.width - edge) / 2, (decoded.height - edge) / 2, edge, edge)
val scaled = Bitmap.createScaledBitmap(crop, if (group) 128 else 256, if (group) 128 else 256, true)
val scaled = Bitmap.createScaledBitmap(crop,128,128,true)
try {
for (quality in if (group) listOf(85, 70, 50, 35, 20) else listOf(100)) {
for (quality in listOf(85,70,50,35,20)) {
val output = java.io.ByteArrayOutputStream()
require(scaled.compress(if (group) Bitmap.CompressFormat.JPEG else Bitmap.CompressFormat.PNG, quality, output)) {
require(scaled.compress(Bitmap.CompressFormat.JPEG,quality,output)) {
"avatar compression failed"
}
val bytes = output.toByteArray()
if (bytes.size <= if (group) AvatarProtocol.MAX_GROUP else AvatarProtocol.MAX_USER) return@withContext bytes
if (bytes.size<=AvatarProtocol.MAX_GROUP) return@withContext bytes
}
throw IOException("encoded avatar exceeds limit")
} finally {

Loading…
Cancel
Save