Encode icons in PNG, because JPEG doesn't support transparency

This caused black squares around icons.
This commit is contained in:
Torsten Grote 2024-10-03 13:57:45 -03:00
parent 682afa1c69
commit 9422d0d309
No known key found for this signature in database
GPG key ID: 3E5F77D92CF891FF
2 changed files with 25 additions and 7 deletions

View file

@ -5,10 +5,14 @@
package com.stevesoltys.seedvault.worker package com.stevesoltys.seedvault.worker
import android.content.pm.PackageInfo
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest import androidx.test.filters.MediumTest
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import com.github.luben.zstd.ZstdOutputStream
import com.google.protobuf.ByteString import com.google.protobuf.ByteString
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.metadata.BackupType
import com.stevesoltys.seedvault.proto.SnapshotKt.blob import com.stevesoltys.seedvault.proto.SnapshotKt.blob
import com.stevesoltys.seedvault.repo.AppBackupManager import com.stevesoltys.seedvault.repo.AppBackupManager
import com.stevesoltys.seedvault.repo.BackupData import com.stevesoltys.seedvault.repo.BackupData
@ -32,6 +36,7 @@ import org.junit.runner.RunWith
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import kotlin.random.Random import kotlin.random.Random
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@ -76,6 +81,11 @@ class IconManagerTest : KoinComponent {
iconManager.uploadIcons() iconManager.uploadIcons()
assertTrue(output.captured.isNotEmpty()) assertTrue(output.captured.isNotEmpty())
// @pm@ is needed
val pmPackageInfo = PackageInfo().apply { packageName = MAGIC_PACKAGE_MANAGER }
val backupData = BackupData(emptyList(), emptyMap())
snapshotCreator.onPackageBackedUp(pmPackageInfo, BackupType.KV, backupData)
// get snapshot and assert it has icon chunks // get snapshot and assert it has icon chunks
val snapshot = snapshotCreator.finalizeSnapshot() val snapshot = snapshotCreator.finalizeSnapshot()
assertTrue(snapshot.iconChunkIdsCount > 0) assertTrue(snapshot.iconChunkIdsCount > 0)
@ -106,6 +116,13 @@ class IconManagerTest : KoinComponent {
assertTrue(output2.captured.isNotEmpty()) assertTrue(output2.captured.isNotEmpty())
assertArrayEquals(output1.captured, output2.captured) assertArrayEquals(output1.captured, output2.captured)
// print compressed and uncompressed size
val size = output1.captured.size.toFloat() / 1024 / 1024
val outputStream = ByteArrayOutputStream()
ZstdOutputStream(outputStream).use { it.write(output1.captured) }
val compressedSize = outputStream.size().toFloat() / 1024 / 1024
println("Icon size: $size MB, compressed $compressedSize MB")
} }
} }

View file

@ -6,7 +6,7 @@
package com.stevesoltys.seedvault.worker package com.stevesoltys.seedvault.worker
import android.content.Context import android.content.Context
import android.graphics.Bitmap.CompressFormat.JPEG import android.graphics.Bitmap.CompressFormat.PNG
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.util.Log import android.util.Log
@ -40,8 +40,7 @@ import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream import java.util.zip.ZipOutputStream
private const val ICON_SIZE = 128 private const val ICON_SIZE = 64
private const val ICON_QUALITY = 75
private const val CACHE_FOLDER = "restore-icons" private const val CACHE_FOLDER = "restore-icons"
private val TAG = IconManager::class.simpleName private val TAG = IconManager::class.simpleName
@ -75,8 +74,10 @@ internal class IconManager(
setLastModifiedTime(FileTime.fromMillis(0)) setLastModifiedTime(FileTime.fromMillis(0))
} }
zip.putNextEntry(entry) zip.putNextEntry(entry)
// WEBP_LOSSY compression wasn't deterministic in our tests, so use JPEG // WEBP_LOSSY compression wasn't deterministic in our tests,
drawable.toBitmap(ICON_SIZE, ICON_SIZE).compress(JPEG, ICON_QUALITY, zip) // and JPEG doesn't support transparency (causing black squares),
// so use PNG
drawable.toBitmap(ICON_SIZE, ICON_SIZE).compress(PNG, 0, zip)
entries.add(it.packageName) entries.add(it.packageName)
zip.closeEntry() zip.closeEntry()
} }
@ -91,8 +92,8 @@ internal class IconManager(
setLastModifiedTime(FileTime.fromMillis(0)) setLastModifiedTime(FileTime.fromMillis(0))
} }
zip.putNextEntry(entry) zip.putNextEntry(entry)
// WEBP_LOSSY compression wasn't deterministic in our tests, so use JPEG // For PNG choice see comment above
drawable.toBitmap(ICON_SIZE, ICON_SIZE).compress(JPEG, ICON_QUALITY, zip) drawable.toBitmap(ICON_SIZE, ICON_SIZE).compress(PNG, 0, zip)
zip.closeEntry() zip.closeEntry()
} }
} }