Improve mime-type handling

master
Koitharu 1 year ago
parent c51218240e
commit 7efc47724e
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -74,6 +74,7 @@ android {
'-opt-in=kotlinx.coroutines.FlowPreview', '-opt-in=kotlinx.coroutines.FlowPreview',
'-opt-in=kotlin.contracts.ExperimentalContracts', '-opt-in=kotlin.contracts.ExperimentalContracts',
'-opt-in=coil3.annotation.ExperimentalCoilApi', '-opt-in=coil3.annotation.ExperimentalCoilApi',
'-opt-in=coil3.annotation.InternalCoilApi',
] ]
} }
lint { lint {

@ -4,26 +4,26 @@ import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.ImageDecoder import android.graphics.ImageDecoder
import android.os.Build import android.os.Build
import android.webkit.MimeTypeMap
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import com.davemorrissey.labs.subscaleview.decoder.ImageDecodeException import com.davemorrissey.labs.subscaleview.decoder.ImageDecodeException
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import org.aomedia.avif.android.AvifDecoder import org.aomedia.avif.android.AvifDecoder
import org.aomedia.avif.android.AvifDecoder.Info import org.aomedia.avif.android.AvifDecoder.Info
import org.jetbrains.annotations.Blocking import org.jetbrains.annotations.Blocking
import org.koitharu.kotatsu.core.util.MimeTypes
import org.koitharu.kotatsu.core.util.ext.MimeType
import org.koitharu.kotatsu.core.util.ext.toByteBuffer import org.koitharu.kotatsu.core.util.ext.toByteBuffer
import org.koitharu.kotatsu.core.util.ext.toMimeTypeOrNull
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.file.Files
object BitmapDecoderCompat { object BitmapDecoderCompat {
private const val FORMAT_AVIF = "avif" private const val FORMAT_AVIF = "avif"
@Blocking @Blocking
fun decode(file: File): Bitmap = when (val format = getMimeType(file)?.subtype) { fun decode(file: File): Bitmap = when (val format = probeMimeType(file)?.subtype) {
FORMAT_AVIF -> file.inputStream().use { decodeAvif(it.toByteBuffer()) } FORMAT_AVIF -> file.inputStream().use { decodeAvif(it.toByteBuffer()) }
else -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { else -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
ImageDecoder.decodeBitmap(ImageDecoder.createSource(file)) ImageDecoder.decodeBitmap(ImageDecoder.createSource(file))
@ -33,7 +33,7 @@ object BitmapDecoderCompat {
} }
@Blocking @Blocking
fun decode(stream: InputStream, type: MediaType?, isMutable: Boolean = false): Bitmap { fun decode(stream: InputStream, type: MimeType?, isMutable: Boolean = false): Bitmap {
val format = type?.subtype val format = type?.subtype
if (format == FORMAT_AVIF) { if (format == FORMAT_AVIF) {
return decodeAvif(stream.toByteBuffer()) return decodeAvif(stream.toByteBuffer())
@ -51,12 +51,20 @@ object BitmapDecoderCompat {
} }
} }
private fun getMimeType(file: File): MediaType? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @Blocking
Files.probeContentType(file.toPath())?.toMediaTypeOrNull() fun probeMimeType(file: File): MimeType? {
} else { return MimeTypes.probeMimeType(file) ?: detectBitmapType(file)
MimeTypeMap.getSingleton().getMimeTypeFromExtension(file.extension)?.toMediaTypeOrNull()
} }
@Blocking
private fun detectBitmapType(file: File): MimeType? = runCatchingCancellable {
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
BitmapFactory.decodeFile(file.path, options)?.recycle()
return options.outMimeType?.toMimeTypeOrNull()
}.getOrNull()
private fun checkBitmapNotNull(bitmap: Bitmap?, format: String?): Bitmap = private fun checkBitmapNotNull(bitmap: Bitmap?, format: String?): Bitmap =
bitmap ?: throw ImageDecodeException(null, format) bitmap ?: throw ImageDecodeException(null, format)

@ -1,7 +1,6 @@
package org.koitharu.kotatsu.core.image package org.koitharu.kotatsu.core.image
import android.net.Uri import android.net.Uri
import android.webkit.MimeTypeMap
import coil3.ImageLoader import coil3.ImageLoader
import coil3.decode.DataSource import coil3.decode.DataSource
import coil3.decode.ImageSource import coil3.decode.ImageSource
@ -12,6 +11,7 @@ import coil3.toAndroidUri
import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.runInterruptible
import okio.Path.Companion.toPath import okio.Path.Companion.toPath
import okio.openZip import okio.openZip
import org.koitharu.kotatsu.core.util.MimeTypes
import org.koitharu.kotatsu.core.util.ext.isZipUri import org.koitharu.kotatsu.core.util.ext.isZipUri
import coil3.Uri as CoilUri import coil3.Uri as CoilUri
@ -25,7 +25,7 @@ class CbzFetcher(
val entryName = requireNotNull(uri.fragment) val entryName = requireNotNull(uri.fragment)
SourceFetchResult( SourceFetchResult(
source = ImageSource(entryName.toPath(), options.fileSystem.openZip(filePath)), source = ImageSource(entryName.toPath(), options.fileSystem.openZip(filePath)),
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(entryName.substringAfterLast('.', "")), mimeType = MimeTypes.getMimeTypeFromExtension(entryName)?.toString(),
dataSource = DataSource.DISK, dataSource = DataSource.DISK,
) )
} }

@ -23,6 +23,7 @@ import org.koitharu.kotatsu.core.util.ext.configureForParser
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.sanitizeHeaderValue import org.koitharu.kotatsu.core.util.ext.sanitizeHeaderValue
import org.koitharu.kotatsu.core.util.ext.toList import org.koitharu.kotatsu.core.util.ext.toList
import org.koitharu.kotatsu.core.util.ext.toMimeType
import org.koitharu.kotatsu.core.util.ext.use import org.koitharu.kotatsu.core.util.ext.use
import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.bitmap.Bitmap import org.koitharu.kotatsu.parsers.bitmap.Bitmap
@ -78,13 +79,14 @@ class MangaLoaderContextImpl @Inject constructor(
override fun redrawImageResponse(response: Response, redraw: (image: Bitmap) -> Bitmap): Response { override fun redrawImageResponse(response: Response, redraw: (image: Bitmap) -> Bitmap): Response {
return response.map { body -> return response.map { body ->
BitmapDecoderCompat.decode(body.byteStream(), body.contentType(), isMutable = true).use { bitmap -> BitmapDecoderCompat.decode(body.byteStream(), body.contentType()?.toMimeType(), isMutable = true)
(redraw(BitmapWrapper.create(bitmap)) as BitmapWrapper).use { result -> .use { bitmap ->
Buffer().also { (redraw(BitmapWrapper.create(bitmap)) as BitmapWrapper).use { result ->
result.compressTo(it.outputStream()) Buffer().also {
}.asResponseBody("image/jpeg".toMediaType()) result.compressTo(it.outputStream())
}.asResponseBody("image/jpeg".toMediaType())
}
} }
}
} }
} }

@ -0,0 +1,46 @@
package org.koitharu.kotatsu.core.util
import android.os.Build
import android.webkit.MimeTypeMap
import org.jetbrains.annotations.Blocking
import org.koitharu.kotatsu.core.util.ext.MimeType
import org.koitharu.kotatsu.core.util.ext.toMimeTypeOrNull
import org.koitharu.kotatsu.parsers.util.nullIfEmpty
import org.koitharu.kotatsu.parsers.util.removeSuffix
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import java.io.File
import java.nio.file.Files
import coil3.util.MimeTypeMap as CoilMimeTypeMap
object MimeTypes {
fun getMimeTypeFromExtension(fileName: String): MimeType? {
return CoilMimeTypeMap.getMimeTypeFromExtension(getNormalizedExtension(fileName) ?: return null)
?.toMimeTypeOrNull()
}
fun getMimeTypeFromUrl(url: String): MimeType? {
return CoilMimeTypeMap.getMimeTypeFromUrl(url)?.toMimeTypeOrNull()
}
fun getExtension(mimeType: MimeType?): String? {
return MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType?.toString() ?: return null)?.nullIfEmpty()
}
@Blocking
fun probeMimeType(file: File): MimeType? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
runCatchingCancellable {
Files.probeContentType(file.toPath())?.toMimeTypeOrNull()
}.getOrNull()?.let { return it }
}
return getMimeTypeFromExtension(file.name)
}
fun getNormalizedExtension(name: String): String? = name
.lowercase()
.removeSuffix('~')
.removeSuffix(".tmp")
.substringAfterLast('.', "")
.takeIf { it.length in 2..5 }
}

@ -7,15 +7,13 @@ import android.os.Build
import android.os.Environment import android.os.Environment
import android.os.storage.StorageManager import android.os.storage.StorageManager
import android.provider.OpenableColumns import android.provider.OpenableColumns
import android.webkit.MimeTypeMap
import androidx.core.database.getStringOrNull import androidx.core.database.getStringOrNull
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.runInterruptible
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import org.jetbrains.annotations.Blocking import org.jetbrains.annotations.Blocking
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.fs.FileSequence import org.koitharu.kotatsu.core.fs.FileSequence
import org.koitharu.kotatsu.core.util.MimeTypes
import java.io.BufferedReader import java.io.BufferedReader
import java.io.File import java.io.File
import java.nio.file.attribute.BasicFileAttributes import java.nio.file.attribute.BasicFileAttributes
@ -41,12 +39,6 @@ fun ZipFile.readText(entry: ZipEntry) = getInputStream(entry).use { output ->
output.bufferedReader().use(BufferedReader::readText) output.bufferedReader().use(BufferedReader::readText)
} }
val ZipEntry.mimeType: MediaType?
get() {
val ext = name.substringAfterLast('.')
return MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext)?.toMediaTypeOrNull()
}
fun File.getStorageName(context: Context): String = runCatching { fun File.getStorageName(context: Context): String = runCatching {
val manager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager val manager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
@ -115,3 +107,6 @@ fun File.walkCompat(includeDirectories: Boolean): Sequence<File> = if (Build.VER
val walk = walk() val walk = walk()
if (includeDirectories) walk else walk.filter { it.isFile } if (includeDirectories) walk else walk.filter { it.isFile }
} }
val File.normalizedExtension: String?
get() = MimeTypes.getNormalizedExtension(name)

@ -0,0 +1,33 @@
package org.koitharu.kotatsu.core.util.ext
import okhttp3.MediaType
private const val TYPE_IMAGE = "image"
private val REGEX_MIME = Regex("^\\w+/([-+.\\w]+|\\*)$", RegexOption.IGNORE_CASE)
@JvmInline
value class MimeType(private val value: String) {
val type: String?
get() = value.substringBefore('/', "").takeIfSpecified()
val subtype: String?
get() = value.substringAfterLast('/', "").takeIfSpecified()
private fun String.takeIfSpecified(): String? = takeUnless {
it.isEmpty() || it == "*"
}
override fun toString(): String = value
}
fun MediaType.toMimeType(): MimeType = MimeType("$type/$subtype")
fun String.toMimeTypeOrNull(): MimeType? = if (REGEX_MIME.matches(this)) {
MimeType(lowercase())
} else {
null
}
val MimeType.isImage: Boolean
get() = type == TYPE_IMAGE

@ -1,6 +1,5 @@
package org.koitharu.kotatsu.details.ui.pager.pages package org.koitharu.kotatsu.details.ui.pager.pages
import android.webkit.MimeTypeMap
import androidx.core.net.toUri import androidx.core.net.toUri
import coil3.ImageLoader import coil3.ImageLoader
import coil3.decode.DataSource import coil3.decode.DataSource
@ -21,8 +20,10 @@ import okio.Path.Companion.toOkioPath
import org.koitharu.kotatsu.core.network.MangaHttpClient import org.koitharu.kotatsu.core.network.MangaHttpClient
import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.util.MimeTypes
import org.koitharu.kotatsu.core.util.ext.fetch import org.koitharu.kotatsu.core.util.ext.fetch
import org.koitharu.kotatsu.core.util.ext.isNetworkUri import org.koitharu.kotatsu.core.util.ext.isNetworkUri
import org.koitharu.kotatsu.core.util.ext.toMimeTypeOrNull
import org.koitharu.kotatsu.local.data.PagesCache import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.mimeType import org.koitharu.kotatsu.parsers.util.mimeType
@ -47,7 +48,7 @@ class MangaPageFetcher(
pagesCache.get(pageUrl)?.let { file -> pagesCache.get(pageUrl)?.let { file ->
return SourceFetchResult( return SourceFetchResult(
source = ImageSource(file.toOkioPath(), options.fileSystem), source = ImageSource(file.toOkioPath(), options.fileSystem),
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(file.extension), mimeType = MimeTypes.getMimeTypeFromExtension(file.name)?.toString(),
dataSource = DataSource.DISK, dataSource = DataSource.DISK,
) )
} }
@ -67,13 +68,13 @@ class MangaPageFetcher(
if (!response.isSuccessful) { if (!response.isSuccessful) {
throw HttpException(response.toNetworkResponse()) throw HttpException(response.toNetworkResponse())
} }
val mimeType = response.mimeType val mimeType = response.mimeType?.toMimeTypeOrNull()
val file = response.requireBody().use { val file = response.requireBody().use {
pagesCache.put(pageUrl, it.source(), mimeType) pagesCache.put(pageUrl, it.source(), mimeType)
} }
SourceFetchResult( SourceFetchResult(
source = ImageSource(file.toOkioPath(), FileSystem.SYSTEM), source = ImageSource(file.toOkioPath(), FileSystem.SYSTEM),
mimeType = mimeType, mimeType = mimeType?.toString(),
dataSource = DataSource.NETWORK, dataSource = DataSource.NETWORK,
) )
} }

@ -5,7 +5,6 @@ import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.content.pm.ServiceInfo import android.content.pm.ServiceInfo
import android.os.Build import android.os.Build
import android.webkit.MimeTypeMap
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.hilt.work.HiltWorker import androidx.hilt.work.HiltWorker
import androidx.work.BackoffPolicy import androidx.work.BackoffPolicy
@ -25,6 +24,7 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -32,6 +32,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.sync.withPermit
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -42,6 +43,7 @@ import okio.buffer
import okio.sink import okio.sink
import okio.use import okio.use
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.image.BitmapDecoderCompat
import org.koitharu.kotatsu.core.model.ids import org.koitharu.kotatsu.core.model.ids
import org.koitharu.kotatsu.core.model.isLocal import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.network.MangaHttpClient import org.koitharu.kotatsu.core.network.MangaHttpClient
@ -49,7 +51,9 @@ import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor
import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.MimeTypes
import org.koitharu.kotatsu.core.util.Throttler import org.koitharu.kotatsu.core.util.Throttler
import org.koitharu.kotatsu.core.util.ext.MimeType
import org.koitharu.kotatsu.core.util.ext.awaitFinishedWorkInfosByTag import org.koitharu.kotatsu.core.util.ext.awaitFinishedWorkInfosByTag
import org.koitharu.kotatsu.core.util.ext.awaitUpdateWork import org.koitharu.kotatsu.core.util.ext.awaitUpdateWork
import org.koitharu.kotatsu.core.util.ext.awaitWorkInfosByTag import org.koitharu.kotatsu.core.util.ext.awaitWorkInfosByTag
@ -62,6 +66,7 @@ import org.koitharu.kotatsu.core.util.ext.getWorkInputData
import org.koitharu.kotatsu.core.util.ext.getWorkSpec import org.koitharu.kotatsu.core.util.ext.getWorkSpec
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.toMimeType
import org.koitharu.kotatsu.core.util.ext.withTicker import org.koitharu.kotatsu.core.util.ext.withTicker
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
import org.koitharu.kotatsu.core.util.progress.RealtimeEtaEstimator import org.koitharu.kotatsu.core.util.progress.RealtimeEtaEstimator
@ -132,7 +137,7 @@ class DownloadWorker @AssistedInject constructor(
downloadMangaImpl(manga, task, downloadedIds) downloadMangaImpl(manga, task, downloadedIds)
} }
Result.success(currentState.toWorkData()) Result.success(currentState.toWorkData())
} catch (e: CancellationException) { } catch (_: CancellationException) {
withContext(NonCancellable) { withContext(NonCancellable) {
val notification = notificationFactory.create(currentState.copy(isStopped = true)) val notification = notificationFactory.create(currentState.copy(isStopped = true))
notificationManager.notify(id.hashCode(), notification) notificationManager.notify(id.hashCode(), notification)
@ -201,7 +206,7 @@ class DownloadWorker @AssistedInject constructor(
val coverUrl = mangaDetails.largeCoverUrl.ifNullOrEmpty { mangaDetails.coverUrl } val coverUrl = mangaDetails.largeCoverUrl.ifNullOrEmpty { mangaDetails.coverUrl }
if (coverUrl.isNotEmpty()) { if (coverUrl.isNotEmpty()) {
downloadFile(coverUrl, destination, repo.source).let { file -> downloadFile(coverUrl, destination, repo.source).let { file ->
output.addCover(file, MimeTypeMap.getFileExtensionFromUrl(coverUrl)) output.addCover(file, getMediaType(coverUrl, file))
file.deleteAwait() file.deleteAwait()
} }
} }
@ -230,7 +235,7 @@ class DownloadWorker @AssistedInject constructor(
chapter = chapter, chapter = chapter,
file = file, file = file,
pageNumber = pageIndex, pageNumber = pageIndex,
ext = MimeTypeMap.getFileExtensionFromUrl(url), type = getMediaType(url, file),
) )
if (file.extension == "tmp") { if (file.extension == "tmp") {
file.deleteAwait() file.deleteAwait()
@ -354,6 +359,13 @@ class DownloadWorker @AssistedInject constructor(
} }
} }
private suspend fun getMediaType(url: String, file: File): MimeType? = runInterruptible(Dispatchers.IO) {
BitmapDecoderCompat.probeMimeType(file)?.let {
return@runInterruptible it
}
MimeTypes.getMimeTypeFromUrl(url)
}
private suspend fun downloadFile( private suspend fun downloadFile(
url: String, url: String,
destination: File, destination: File,
@ -364,18 +376,29 @@ class DownloadWorker @AssistedInject constructor(
return imageProxyInterceptor.interceptPageRequest(request, okHttp) return imageProxyInterceptor.interceptPageRequest(request, okHttp)
.ensureSuccess() .ensureSuccess()
.use { response -> .use { response ->
val file = File(destination, UUID.randomUUID().toString() + ".tmp") var file: File? = null
try { try {
response.requireBody().use { body -> response.requireBody().use { body ->
file = File(
destination,
buildString {
append(UUID.randomUUID().toString())
MimeTypes.getExtension(body.contentType()?.toMimeType())?.let { ext ->
append('.')
append(ext)
}
append(".tmp")
},
)
file.sink(append = false).buffer().use { file.sink(append = false).buffer().use {
it.writeAllCancellable(body.source()) it.writeAllCancellable(body.source())
} }
} }
} catch (e: CancellationException) { } catch (e: CancellationException) {
file.delete() file?.delete()
throw e throw e
} }
file checkNotNull(file)
} }
} }

@ -15,6 +15,8 @@ import okio.sink
import okio.use import okio.use
import org.koitharu.kotatsu.core.exceptions.NoDataReceivedException import org.koitharu.kotatsu.core.exceptions.NoDataReceivedException
import org.koitharu.kotatsu.core.util.FileSize import org.koitharu.kotatsu.core.util.FileSize
import org.koitharu.kotatsu.core.util.MimeTypes
import org.koitharu.kotatsu.core.util.ext.MimeType
import org.koitharu.kotatsu.core.util.ext.compressToPNG import org.koitharu.kotatsu.core.util.ext.compressToPNG
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
@ -59,7 +61,7 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) {
} }
} }
suspend fun put(url: String, source: Source, mimeType: String?): File = withContext(Dispatchers.IO) { suspend fun put(url: String, source: Source, mimeType: MimeType?): File = withContext(Dispatchers.IO) {
val file = createBufferFile(url, mimeType) val file = createBufferFile(url, mimeType)
try { try {
val bytes = file.sink(append = false).buffer().use { val bytes = file.sink(append = false).buffer().use {
@ -78,7 +80,7 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) {
} }
suspend fun put(url: String, bitmap: Bitmap): File = withContext(Dispatchers.IO) { suspend fun put(url: String, bitmap: Bitmap): File = withContext(Dispatchers.IO) {
val file = createBufferFile(url, "image/png") val file = createBufferFile(url, MimeType("image/png"))
try { try {
bitmap.compressToPNG(file) bitmap.compressToPNG(file)
val cache = lruCache.get() val cache = lruCache.get()
@ -107,9 +109,8 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) {
it.printStackTraceDebug() it.printStackTraceDebug()
}.getOrDefault(SIZE_DEFAULT) }.getOrDefault(SIZE_DEFAULT)
private suspend fun createBufferFile(url: String, mimeType: String?): File { private suspend fun createBufferFile(url: String, mimeType: MimeType?): File {
val ext = mimeType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } val ext = MimeTypes.getExtension(mimeType) ?: MimeTypeMap.getFileExtensionFromUrl(url).ifNullOrEmpty { "dat" }
?: MimeTypeMap.getFileExtensionFromUrl(url).ifNullOrEmpty { "dat" }
val cacheDir = cacheDir.get() val cacheDir = cacheDir.get()
val rootDir = checkNotNull(cacheDir.parentFile) { "Cannot get parent for ${cacheDir.absolutePath}" } val rootDir = checkNotNull(cacheDir.parentFile) { "Cannot get parent for ${cacheDir.absolutePath}" }
val name = UUID.randomUUID().toString() + "." + ext val name = UUID.randomUUID().toString() + "." + ext

@ -1,7 +1,6 @@
package org.koitharu.kotatsu.local.data.input package org.koitharu.kotatsu.local.data.input
import android.net.Uri import android.net.Uri
import android.webkit.MimeTypeMap
import androidx.core.net.toFile import androidx.core.net.toFile
import androidx.core.net.toUri import androidx.core.net.toUri
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -18,8 +17,10 @@ import okio.openZip
import org.jetbrains.annotations.Blocking import org.jetbrains.annotations.Blocking
import org.koitharu.kotatsu.core.model.LocalMangaSource import org.koitharu.kotatsu.core.model.LocalMangaSource
import org.koitharu.kotatsu.core.util.AlphanumComparator import org.koitharu.kotatsu.core.util.AlphanumComparator
import org.koitharu.kotatsu.core.util.MimeTypes
import org.koitharu.kotatsu.core.util.ext.URI_SCHEME_ZIP import org.koitharu.kotatsu.core.util.ext.URI_SCHEME_ZIP
import org.koitharu.kotatsu.core.util.ext.isFileUri import org.koitharu.kotatsu.core.util.ext.isFileUri
import org.koitharu.kotatsu.core.util.ext.isImage
import org.koitharu.kotatsu.core.util.ext.isRegularFile import org.koitharu.kotatsu.core.util.ext.isRegularFile
import org.koitharu.kotatsu.core.util.ext.isZipUri import org.koitharu.kotatsu.core.util.ext.isZipUri
import org.koitharu.kotatsu.core.util.ext.longHashCode import org.koitharu.kotatsu.core.util.ext.longHashCode
@ -87,7 +88,6 @@ class LocalMangaParser(private val uri: Uri) {
} else { } else {
val title = rootFile.nameWithoutExtension.replace("_", " ").toCamelCase() val title = rootFile.nameWithoutExtension.replace("_", " ").toCamelCase()
val coverEntry = fileSystem.findFirstImage(rootPath) val coverEntry = fileSystem.findFirstImage(rootPath)
val mimeTypeMap = MimeTypeMap.getSingleton()
Manga( Manga(
id = rootFile.absolutePath.longHashCode(), id = rootFile.absolutePath.longHashCode(),
title = title, title = title,
@ -103,7 +103,7 @@ class LocalMangaParser(private val uri: Uri) {
when { when {
path == coverEntry -> null path == coverEntry -> null
!fileSystem.isRegularFile(path) -> null !fileSystem.isRegularFile(path) -> null
mimeTypeMap.isImage(path) -> path.parent path.isImage() -> path.parent
hasZipExtension(path.name) -> path hasZipExtension(path.name) -> path
else -> null else -> null
} }
@ -157,10 +157,7 @@ class LocalMangaParser(private val uri: Uri) {
val pattern = index.getChapterNamesPattern(chapter) val pattern = index.getChapterNamesPattern(chapter)
entries.filter { x -> x.name.substringBefore('.').matches(pattern) } entries.filter { x -> x.name.substringBefore('.').matches(pattern) }
} else { } else {
val mimeTypeMap = MimeTypeMap.getSingleton() entries.filter { x -> x.isImage() && x.parent == rootPath }
entries.filter { x ->
mimeTypeMap.isImage(x) && x.parent == rootPath
}
}.toListSorted(compareBy(AlphanumComparator()) { x -> x.toString() }) }.toListSorted(compareBy(AlphanumComparator()) { x -> x.toString() })
.map { x -> .map { x ->
val entryUri = chapterUri.child(x, resolve = true).toString() val entryUri = chapterUri.child(x, resolve = true).toString()
@ -218,21 +215,18 @@ class LocalMangaParser(private val uri: Uri) {
rootPath: Path, rootPath: Path,
recursive: Boolean recursive: Boolean
): Path? = runCatchingCancellable { ): Path? = runCatchingCancellable {
val mimeTypeMap = MimeTypeMap.getSingleton()
if (recursive) { if (recursive) {
listRecursively(rootPath) listRecursively(rootPath)
} else { } else {
list(rootPath).asSequence() list(rootPath).asSequence()
}.filter { isRegularFile(it) && mimeTypeMap.isImage(it) } }.filter { isRegularFile(it) && it.isImage() }
.toListSorted(compareBy(AlphanumComparator()) { x -> x.toString() }) .toListSorted(compareBy(AlphanumComparator()) { x -> x.toString() })
.firstOrNull() .firstOrNull()
}.onFailure { e -> }.onFailure { e ->
e.printStackTraceDebug() e.printStackTraceDebug()
}.getOrNull() }.getOrNull()
private fun MimeTypeMap.isImage(path: Path): Boolean = private fun Path.isImage(): Boolean = MimeTypes.getMimeTypeFromExtension(name)?.isImage == true
getMimeTypeFromExtension(path.name.substringAfterLast('.'))
?.startsWith("image/") == true
private fun Uri.resolve(): Uri = if (isFileUri()) { private fun Uri.resolve(): Uri = if (isFileUri()) {
val file = toFile() val file = toFile()

@ -8,6 +8,8 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import okhttp3.internal.closeQuietly import okhttp3.internal.closeQuietly
import org.koitharu.kotatsu.core.model.isLocal import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.util.MimeTypes
import org.koitharu.kotatsu.core.util.ext.MimeType
import org.koitharu.kotatsu.core.util.ext.deleteAwait import org.koitharu.kotatsu.core.util.ext.deleteAwait
import org.koitharu.kotatsu.core.util.ext.takeIfReadable import org.koitharu.kotatsu.core.util.ext.takeIfReadable
import org.koitharu.kotatsu.core.zip.ZipOutput import org.koitharu.kotatsu.core.zip.ZipOutput
@ -35,10 +37,10 @@ class LocalMangaDirOutput(
override suspend fun mergeWithExisting() = Unit override suspend fun mergeWithExisting() = Unit
override suspend fun addCover(file: File, ext: String) = mutex.withLock { override suspend fun addCover(file: File, type: MimeType?) = mutex.withLock {
val name = buildString { val name = buildString {
append("cover") append("cover")
if (ext.isNotEmpty() && ext.length <= 4) { MimeTypes.getExtension(type)?.let { ext ->
append('.') append('.')
append(ext) append(ext)
} }
@ -50,14 +52,14 @@ class LocalMangaDirOutput(
flushIndex() flushIndex()
} }
override suspend fun addPage(chapter: IndexedValue<MangaChapter>, file: File, pageNumber: Int, ext: String) = override suspend fun addPage(chapter: IndexedValue<MangaChapter>, file: File, pageNumber: Int, type: MimeType?) =
mutex.withLock { mutex.withLock {
val output = chaptersOutput.getOrPut(chapter.value) { val output = chaptersOutput.getOrPut(chapter.value) {
ZipOutput(File(rootFile, chapterFileName(chapter) + SUFFIX_TMP)) ZipOutput(File(rootFile, chapterFileName(chapter) + SUFFIX_TMP))
} }
val name = buildString { val name = buildString {
append(FILENAME_PATTERN.format(chapter.value.branch.hashCode(), chapter.index + 1, pageNumber)) append(FILENAME_PATTERN.format(chapter.value.branch.hashCode(), chapter.index + 1, pageNumber))
if (ext.isNotEmpty() && ext.length <= 4) { MimeTypes.getExtension(type)?.let { ext ->
append('.') append('.')
append(ext) append(ext)
} }
@ -96,7 +98,9 @@ class LocalMangaDirOutput(
} }
suspend fun deleteChapters(ids: Set<Long>) = mutex.withLock { suspend fun deleteChapters(ids: Set<Long>) = mutex.withLock {
val chapters = checkNotNull((index.getMangaInfo() ?: LocalMangaParser(rootFile).getManga(withDetails = true).manga).chapters) { val chapters = checkNotNull(
(index.getMangaInfo() ?: LocalMangaParser(rootFile).getManga(withDetails = true).manga).chapters,
) {
"No chapters found" "No chapters found"
}.withIndex() }.withIndex()
val victimsIds = ids.toMutableSet() val victimsIds = ids.toMutableSet()

@ -6,6 +6,7 @@ import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okio.Closeable import okio.Closeable
import org.koitharu.kotatsu.core.prefs.DownloadFormat import org.koitharu.kotatsu.core.prefs.DownloadFormat
import org.koitharu.kotatsu.core.util.ext.MimeType
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.local.data.input.LocalMangaParser import org.koitharu.kotatsu.local.data.input.LocalMangaParser
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
@ -20,9 +21,9 @@ sealed class LocalMangaOutput(
abstract suspend fun mergeWithExisting() abstract suspend fun mergeWithExisting()
abstract suspend fun addCover(file: File, ext: String) abstract suspend fun addCover(file: File, type: MimeType?)
abstract suspend fun addPage(chapter: IndexedValue<MangaChapter>, file: File, pageNumber: Int, ext: String) abstract suspend fun addPage(chapter: IndexedValue<MangaChapter>, file: File, pageNumber: Int, type: MimeType?)
abstract suspend fun flushChapter(chapter: MangaChapter): Boolean abstract suspend fun flushChapter(chapter: MangaChapter): Boolean

@ -7,6 +7,8 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import okhttp3.internal.closeQuietly import okhttp3.internal.closeQuietly
import org.koitharu.kotatsu.core.model.isLocal import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.util.MimeTypes
import org.koitharu.kotatsu.core.util.ext.MimeType
import org.koitharu.kotatsu.core.util.ext.deleteAwait import org.koitharu.kotatsu.core.util.ext.deleteAwait
import org.koitharu.kotatsu.core.util.ext.readText import org.koitharu.kotatsu.core.util.ext.readText
import org.koitharu.kotatsu.core.zip.ZipOutput import org.koitharu.kotatsu.core.zip.ZipOutput
@ -39,10 +41,10 @@ class LocalMangaZipOutput(
} }
} }
override suspend fun addCover(file: File, ext: String) = mutex.withLock { override suspend fun addCover(file: File, type: MimeType?) = mutex.withLock {
val name = buildString { val name = buildString {
append(FILENAME_PATTERN.format(0, 0, 0)) append(FILENAME_PATTERN.format(0, 0, 0))
if (ext.isNotEmpty() && ext.length <= 4) { MimeTypes.getExtension(type)?.let { ext ->
append('.') append('.')
append(ext) append(ext)
} }
@ -53,11 +55,11 @@ class LocalMangaZipOutput(
index.setCoverEntry(name) index.setCoverEntry(name)
} }
override suspend fun addPage(chapter: IndexedValue<MangaChapter>, file: File, pageNumber: Int, ext: String) = override suspend fun addPage(chapter: IndexedValue<MangaChapter>, file: File, pageNumber: Int, type: MimeType?) =
mutex.withLock { mutex.withLock {
val name = buildString { val name = buildString {
append(FILENAME_PATTERN.format(chapter.value.branch.hashCode(), chapter.index + 1, pageNumber)) append(FILENAME_PATTERN.format(chapter.value.branch.hashCode(), chapter.index + 1, pageNumber))
if (ext.isNotEmpty() && ext.length <= 4) { MimeTypes.getExtension(type)?.let { ext ->
append('.') append('.')
append(ext) append(ext)
} }

@ -36,6 +36,7 @@ import org.koitharu.kotatsu.core.parser.CachingMangaRepository
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.FileSize import org.koitharu.kotatsu.core.util.FileSize
import org.koitharu.kotatsu.core.util.MimeTypes
import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope
import org.koitharu.kotatsu.core.util.ext.URI_SCHEME_ZIP import org.koitharu.kotatsu.core.util.ext.URI_SCHEME_ZIP
import org.koitharu.kotatsu.core.util.ext.cancelChildrenAndJoin import org.koitharu.kotatsu.core.util.ext.cancelChildrenAndJoin
@ -47,9 +48,9 @@ import org.koitharu.kotatsu.core.util.ext.isFileUri
import org.koitharu.kotatsu.core.util.ext.isNotEmpty import org.koitharu.kotatsu.core.util.ext.isNotEmpty
import org.koitharu.kotatsu.core.util.ext.isPowerSaveMode import org.koitharu.kotatsu.core.util.ext.isPowerSaveMode
import org.koitharu.kotatsu.core.util.ext.isZipUri import org.koitharu.kotatsu.core.util.ext.isZipUri
import org.koitharu.kotatsu.core.util.ext.mimeType
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.ramAvailable import org.koitharu.kotatsu.core.util.ext.ramAvailable
import org.koitharu.kotatsu.core.util.ext.toMimeType
import org.koitharu.kotatsu.core.util.ext.use import org.koitharu.kotatsu.core.util.ext.use
import org.koitharu.kotatsu.core.util.ext.withProgress import org.koitharu.kotatsu.core.util.ext.withProgress
import org.koitharu.kotatsu.core.util.progress.ProgressDeferred import org.koitharu.kotatsu.core.util.progress.ProgressDeferred
@ -57,7 +58,6 @@ import org.koitharu.kotatsu.download.ui.worker.DownloadSlowdownDispatcher
import org.koitharu.kotatsu.local.data.PagesCache import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mimeType
import org.koitharu.kotatsu.parsers.util.requireBody import org.koitharu.kotatsu.parsers.util.requireBody
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
@ -66,7 +66,6 @@ import java.util.LinkedList
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import java.util.zip.ZipFile import java.util.zip.ZipFile
import javax.inject.Inject import javax.inject.Inject
import kotlin.concurrent.Volatile
import kotlin.coroutines.AbstractCoroutineContextElement import kotlin.coroutines.AbstractCoroutineContextElement
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
@ -146,7 +145,7 @@ class PageLoader @Inject constructor(
val entry = zip.getEntry(uri.fragment) val entry = zip.getEntry(uri.fragment)
context.ensureRamAtLeast(entry.size * 2) context.ensureRamAtLeast(entry.size * 2)
zip.getInputStream(entry).use { zip.getInputStream(entry).use {
BitmapDecoderCompat.decode(it, entry.mimeType) BitmapDecoderCompat.decode(it, MimeTypes.getMimeTypeFromExtension(entry.name))
} }
} }
} }
@ -250,7 +249,7 @@ class PageLoader @Inject constructor(
val request = createPageRequest(pageUrl, page.source) val request = createPageRequest(pageUrl, page.source)
imageProxyInterceptor.interceptPageRequest(request, okHttp).ensureSuccess().use { response -> imageProxyInterceptor.interceptPageRequest(request, okHttp).ensureSuccess().use { response ->
response.requireBody().withProgress(progress).use { response.requireBody().withProgress(progress).use {
cache.put(pageUrl, it.source(), response.mimeType) cache.put(pageUrl, it.source(), it.contentType()?.toMimeType())
} }
}.toUri() }.toUri()
} }
@ -264,7 +263,7 @@ class PageLoader @Inject constructor(
private fun Deferred<Uri>.isValid(): Boolean { private fun Deferred<Uri>.isValid(): Boolean {
return getCompletionResultOrNull()?.map { uri -> return getCompletionResultOrNull()?.map { uri ->
uri.exists() && uri.isTargetNotEmpty() uri.exists() && uri.isTargetNotEmpty()
}?.getOrDefault(false) ?: true }?.getOrDefault(false) != false
} }
private class InternalErrorHandler : AbstractCoroutineContextElement(CoroutineExceptionHandler), private class InternalErrorHandler : AbstractCoroutineContextElement(CoroutineExceptionHandler),

@ -5,9 +5,9 @@ import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Environment import android.os.Environment
import android.provider.DocumentsContract import android.provider.DocumentsContract
import android.webkit.MimeTypeMap
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.net.toUri import androidx.core.net.toUri
import org.koitharu.kotatsu.core.util.MimeTypes
import org.koitharu.kotatsu.core.util.ext.toUriOrNull import org.koitharu.kotatsu.core.util.ext.toUriOrNull
import java.io.File import java.io.File
@ -15,8 +15,7 @@ class PageSaveContract : ActivityResultContracts.CreateDocument("image/*") {
override fun createIntent(context: Context, input: String): Intent { override fun createIntent(context: Context, input: String): Intent {
val intent = super.createIntent(context, input.substringAfterLast(File.separatorChar)) val intent = super.createIntent(context, input.substringAfterLast(File.separatorChar))
intent.type = MimeTypeMap.getSingleton() intent.type = MimeTypes.getMimeTypeFromExtension(input)?.toString() ?: "image/*"
.getMimeTypeFromExtension(input.substringAfterLast('.')) ?: "image/*"
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val defaultUri = input.toUriOrNull()?.run { val defaultUri = input.toUriOrNull()?.run {
path?.let { p -> path?.let { p ->

@ -1,9 +1,7 @@
package org.koitharu.kotatsu.reader.ui package org.koitharu.kotatsu.reader.ui
import android.content.Context import android.content.Context
import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import android.webkit.MimeTypeMap
import androidx.activity.result.ActivityResultCallback import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultCaller import androidx.activity.result.ActivityResultCaller
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
@ -28,7 +26,9 @@ import okio.buffer
import okio.openZip import okio.openZip
import okio.sink import okio.sink
import okio.source import okio.source
import org.koitharu.kotatsu.core.image.BitmapDecoderCompat
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.MimeTypes
import org.koitharu.kotatsu.core.util.ext.isFileUri import org.koitharu.kotatsu.core.util.ext.isFileUri
import org.koitharu.kotatsu.core.util.ext.isZipUri import org.koitharu.kotatsu.core.util.ext.isZipUri
import org.koitharu.kotatsu.core.util.ext.toFileOrNull import org.koitharu.kotatsu.core.util.ext.toFileOrNull
@ -99,10 +99,10 @@ class PageSaveHelper @AssistedInject constructor(
val pageUri = pageLoader.loadPage(task.page, force = false) val pageUri = pageLoader.loadPage(task.page, force = false)
val proposedName = task.getFileBaseName() val proposedName = task.getFileBaseName()
val ext = getPageExtension(pageUrl, pageUri) val ext = getPageExtension(pageUrl, pageUri)
val mime = requireNotNull(MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext)) { val mime = requireNotNull(MimeTypes.getMimeTypeFromExtension("_.$ext")) {
"Unknown type of $proposedName" "Unknown type of $proposedName"
} }
val destination = destinationDir.createFile(mime, proposedName.substringBeforeLast('.')) val destination = destinationDir.createFile(mime.toString(), proposedName)
copyImpl(pageUri, destination?.uri ?: throw IOException("Cannot create destination file")) copyImpl(pageUri, destination?.uri ?: throw IOException("Cannot create destination file"))
result.add(destination.uri) result.add(destination.uri)
} }
@ -119,12 +119,7 @@ class PageSaveHelper @AssistedInject constructor(
) { "Invalid page url: $url" } ) { "Invalid page url: $url" }
var extension = name.substringAfterLast('.', "") var extension = name.substringAfterLast('.', "")
if (extension.length !in 2..4) { if (extension.length !in 2..4) {
val mimeType = fileUri.toFileOrNull()?.let { file -> getImageMimeType(file) } extension = fileUri.toFileOrNull()?.let { file -> getImageExtension(file) } ?: EXTENSION_FALLBACK
extension = if (mimeType != null) {
MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) ?: EXTENSION_FALLBACK
} else {
EXTENSION_FALLBACK
}
} }
return extension return extension
} }
@ -155,8 +150,7 @@ class PageSaveHelper @AssistedInject constructor(
if (proposedName == null) { if (proposedName == null) {
return dir return dir
} else { } else {
val ext = proposedName.substringAfterLast('.', "") val mime = MimeTypes.getMimeTypeFromExtension(proposedName)?.toString() ?: return null
val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext) ?: return null
return dir.createFile(mime, proposedName.substringBeforeLast('.')) return dir.createFile(mime, proposedName.substringBeforeLast('.'))
} }
} }
@ -179,12 +173,8 @@ class PageSaveHelper @AssistedInject constructor(
} }
} }
private suspend fun getImageMimeType(file: File): String? = runInterruptible(Dispatchers.IO) { private suspend fun getImageExtension(file: File): String? = runInterruptible(Dispatchers.IO) {
val options = BitmapFactory.Options().apply { MimeTypes.getExtension(BitmapDecoderCompat.probeMimeType(file))
inJustDecodeBounds = true
}
BitmapFactory.decodeFile(file.path, options)?.recycle()
options.outMimeType
} }
data class Task( data class Task(

Loading…
Cancel
Save