From 6b1313368e63cde76827c4d9809bee6e5984dda9 Mon Sep 17 00:00:00 2001 From: Carlos Ballesteros Velasco Date: Mon, 6 Nov 2023 02:19:32 +0100 Subject: [PATCH] Simplify readImage/Bitmap* signatures by providing a common interface BaseImageDecodingProps (#2016) * Make CoreGraphicsImageFormatProvider aware of preferSyncIo * Simplify readImage/Bitmap* signatures by providing a common interface BaseImageDecodingProps --- .../src/common/korlibs/image/format/ICO.kt | 29 +------- .../korlibs/image/format/ImageFormat.kt | 24 ++++-- .../korlibs/image/format/ImageFormats.kt | 2 +- .../common/korlibs/image/format/KorioExt.kt | 73 ++++++++----------- .../image/format/NativeImageFormatProvider.kt | 7 ++ .../format/CoreGraphicsImageFormatProvider.kt | 20 ++--- 6 files changed, 68 insertions(+), 87 deletions(-) diff --git a/korge-core/src/common/korlibs/image/format/ICO.kt b/korge-core/src/common/korlibs/image/format/ICO.kt index 423e4bbce4..bd61ee4221 100644 --- a/korge-core/src/common/korlibs/image/format/ICO.kt +++ b/korge-core/src/common/korlibs/image/format/ICO.kt @@ -1,29 +1,8 @@ package korlibs.image.format -import korlibs.image.bitmap.Bitmap -import korlibs.image.bitmap.Bitmap1 -import korlibs.image.bitmap.Bitmap32 -import korlibs.image.bitmap.Bitmap4 -import korlibs.image.bitmap.Bitmap8 -import korlibs.image.color.BGRA -import korlibs.image.color.RGBA -import korlibs.image.color.RgbaArray -import korlibs.image.color.toRgbaArray -import korlibs.io.stream.MemorySyncStream -import korlibs.io.stream.SyncStream -import korlibs.io.stream.readBytes -import korlibs.io.stream.readS16LE -import korlibs.io.stream.readS32LE -import korlibs.io.stream.readU16LE -import korlibs.io.stream.readU32BE -import korlibs.io.stream.readU8 -import korlibs.io.stream.sliceStart -import korlibs.io.stream.sliceWithSize -import korlibs.io.stream.toByteArray -import korlibs.io.stream.write16LE -import korlibs.io.stream.write32LE -import korlibs.io.stream.write8 -import korlibs.io.stream.writeBytes +import korlibs.image.bitmap.* +import korlibs.image.color.* +import korlibs.io.stream.* @Suppress("UNUSED_VARIABLE") object ICO : ImageFormat("ico") { @@ -61,7 +40,7 @@ object ICO : ImageFormat("ico") { val tryPNGHead = s.sliceStart().readU32BE() if (tryPNGHead == 0x89_50_4E_47L) return PNG.decode( s.sliceStart(), - props.copy(filename = "${props.filename}.png") + props.withFileName("${props.filename}.png") ) val headerSize = s.readS32LE() val width = s.readS32LE() diff --git a/korge-core/src/common/korlibs/image/format/ImageFormat.kt b/korge-core/src/common/korlibs/image/format/ImageFormat.kt index 73a151703a..e7bc36bc02 100644 --- a/korge-core/src/common/korlibs/image/format/ImageFormat.kt +++ b/korge-core/src/common/korlibs/image/format/ImageFormat.kt @@ -40,7 +40,8 @@ suspend fun ImageFormatEncoder.encodeSuspend( interface ImageFormatEncoderDecoder : ImageFormatEncoder, ImageFormatDecoder -abstract class ImageFormat(vararg exts: String) : ImageFormatEncoderDecoder { +abstract class ImageFormat(vararg exts: String) : BaseImageDecodingProps, ImageFormatEncoderDecoder { + override val decodingProps: ImageDecodingProps by lazy { this.toProps() } val extensions = exts.map { it.toLowerCase().trim() }.toSet() open fun readImageContainer(s: SyncStream, props: ImageDecodingProps = ImageDecodingProps.DEFAULT): ImageDataContainer = ImageDataContainer(listOf(readImage(s, props))) open fun readImage(s: SyncStream, props: ImageDecodingProps = ImageDecodingProps.DEFAULT): ImageData = TODO() @@ -53,7 +54,7 @@ abstract class ImageFormat(vararg exts: String) : ImageFormatEncoderDecoder { } suspend fun decodeHeaderSuspend(file: VfsFile, props: ImageDecodingProps = ImageDecodingProps.DEFAULT): ImageInfo? { - return file.openUse { decodeHeaderSuspend(this@openUse, props.copyWithFile(file)) } + return file.openUse { decodeHeaderSuspend(this@openUse, props.withFile(file)) } } open suspend fun decodeHeaderSuspend(s: AsyncStream, props: ImageDecodingProps = ImageDecodingProps.DEFAULT): ImageInfo? { @@ -71,7 +72,7 @@ abstract class ImageFormat(vararg exts: String) : ImageFormatEncoderDecoder { } fun read(s: SyncStream, filename: String = "unknown"): Bitmap = - readImage(s, ImageDecodingProps.DEFAULT.copy(filename = filename)).mainBitmap + readImage(s, ImageDecodingProps.DEFAULT.withFileName(filename)).mainBitmap suspend fun read(file: VfsFile) = this.read(file.readAsSyncStream(), file.baseName) //fun read(file: File) = this.read(file.openSync(), file.name) @@ -98,7 +99,7 @@ abstract class ImageFormat(vararg exts: String) : ImageFormatEncoderDecoder { //fun decode(s: ByteArray, filename: String = "unknown"): Bitmap = read(s.openSync(), filename) override suspend fun decode(file: VfsFile, props: ImageDecodingProps): Bitmap = - this.read(file.readAsSyncStream(), props.copy(filename = file.baseName)) + this.read(file.readAsSyncStream(), props.withFile(file)) suspend fun decode(s: AsyncStream, filename: String) = this.read(s.readAll(), ImageDecodingProps(filename)) suspend fun decode(s: AsyncStream, props: ImageDecodingProps = ImageDecodingProps.DEFAULT) = @@ -115,13 +116,17 @@ abstract class ImageFormat(vararg exts: String) : ImageFormatEncoderDecoder { encode(ImageData(bitmap), props) suspend fun read(file: VfsFile, props: ImageDecodingProps = ImageDecodingProps.DEFAULT): ImageData = - this.readImage(file.readAll().openSync(), props.copy(filename = file.baseName)) + this.readImage(file.readAll().openSync(), props.withFile(file)) override fun toString(): String = "ImageFormat($extensions)" } class ImageDecoderNotFoundException : Exception("Can't read image using AWT. No available decoder for input") +interface BaseImageDecodingProps { + val decodingProps: ImageDecodingProps +} + data class ImageDecodingProps constructor( val filename: String = "unknown", val width: Int? = null, @@ -142,12 +147,15 @@ data class ImageDecodingProps constructor( */ val out: Bitmap? = null, override var extra: ExtraType = null -) : Extra { +) : BaseImageDecodingProps, Extra { + + override val decodingProps get() = this val premultipliedSure: Boolean get() = premultiplied ?: true val formatSure: ImageFormat get() = format ?: RegisteredImageFormats - fun copyWithFile(file: VfsFile) = copy(filename = file.baseName) + fun withFileName(filename: String): ImageDecodingProps = copy(filename = filename) + fun withFile(file: VfsFile): ImageDecodingProps = withFileName(file.baseName) // https://developer.android.com/reference/android/graphics/BitmapFactory.Options#inSampleSize fun getSampleSize(originalWidth: Int, originalHeight: Int): Int { @@ -183,6 +191,8 @@ data class ImageEncodingProps( val depremultiplyIfRequired: Boolean = true, val init: (ImageEncodingProps.() -> Unit)? = null ) : Extra { + fun withFile(file: VfsFile): ImageEncodingProps = copy(filename = file.baseName) + val extension: String get() = PathInfo(filename).extensionLC val mimeType: String get() = when (extension) { "jpg", "jpeg" -> "image/jpeg" diff --git a/korge-core/src/common/korlibs/image/format/ImageFormats.kt b/korge-core/src/common/korlibs/image/format/ImageFormats.kt index b3fd6c97f9..f6ac58483c 100644 --- a/korge-core/src/common/korlibs/image/format/ImageFormats.kt +++ b/korge-core/src/common/korlibs/image/format/ImageFormats.kt @@ -126,7 +126,7 @@ suspend fun Bitmap.writeTo( file: VfsFile, formats: ImageFormat = RegisteredImageFormats, props: ImageEncodingProps = ImageEncodingProps() -) = file.writeBytes(formats.encode(this, props.copy(filename = file.baseName))) +) = file.writeBytes(formats.encode(this, props.withFile(file))) @Suppress("unused") suspend fun BmpSlice.writeTo( diff --git a/korge-core/src/common/korlibs/image/format/KorioExt.kt b/korge-core/src/common/korlibs/image/format/KorioExt.kt index 11c72fd6b7..ef8691c74b 100644 --- a/korge-core/src/common/korlibs/image/format/KorioExt.kt +++ b/korge-core/src/common/korlibs/image/format/KorioExt.kt @@ -14,73 +14,64 @@ suspend fun displayImage(bmp: Bitmap, kind: Int = 0) = nativeImageFormatProvider // Read bitmaps from files -suspend fun VfsFile.readNativeImage(props: ImageDecodingProps = ImageDecodingProps.DEFAULT): NativeImage = +suspend fun VfsFile.readNativeImage(props: BaseImageDecodingProps = ImageDecodingProps.DEFAULT): NativeImage = readBitmapNative(props) as NativeImage -suspend fun VfsFile.readBitmapNative(props: ImageDecodingProps = ImageDecodingProps.DEFAULT): Bitmap = - readBitmap(props.copy(tryNativeDecode = true, format = null)) -suspend fun VfsFile.readBitmapNoNative(props: ImageDecodingProps = ImageDecodingProps.DEFAULT): Bitmap = - readBitmap(props.copy(tryNativeDecode = false, format = props.format ?: RegisteredImageFormats)) -suspend fun VfsFile.readBitmap(props: ImageDecodingProps = ImageDecodingProps.DEFAULT): Bitmap = - _readBitmap(file = this, props = props) +suspend fun VfsFile.readBitmapNative(props: BaseImageDecodingProps = ImageDecodingProps.DEFAULT): Bitmap = + readBitmap(props.decodingProps.copy(tryNativeDecode = true, format = null)) +suspend fun VfsFile.readBitmapNoNative(props: BaseImageDecodingProps = ImageDecodingProps.DEFAULT): Bitmap = + readBitmap(props.decodingProps.copy(tryNativeDecode = false, format = props.decodingProps.format ?: RegisteredImageFormats)) +suspend fun VfsFile.readBitmap(props: BaseImageDecodingProps = ImageDecodingProps.DEFAULT): Bitmap = + _readBitmap(file = this, props = props.decodingProps) // Read bitmaps from AsyncInputStream -suspend fun AsyncInputStream.readNativeImage(props: ImageDecodingProps = ImageDecodingProps.DEFAULT): NativeImage = - readBitmap(props.copy(tryNativeDecode = true, format = null)) as NativeImage -suspend fun AsyncInputStream.readBitmap(props: ImageDecodingProps = ImageDecodingProps("file.bin")): Bitmap = - _readBitmap(bytes = this.readAll(), props = props) +suspend fun AsyncInputStream.readNativeImage(props: BaseImageDecodingProps = ImageDecodingProps.DEFAULT): NativeImage = + readBitmap(props.decodingProps.copy(tryNativeDecode = true, format = null)) as NativeImage +suspend fun AsyncInputStream.readBitmap(props: BaseImageDecodingProps = ImageDecodingProps("file.bin")): Bitmap = + _readBitmap(bytes = this.readAll(), props = props.decodingProps) // Read extended image data -suspend fun AsyncInputStream.readImageData(props: ImageDecodingProps = ImageDecodingProps.DEFAULT): ImageData = - props.formatSure.readImage(this.readAll().openSync(), ImageDecodingProps(props.filename)) +suspend fun AsyncInputStream.readImageData(props: BaseImageDecodingProps = ImageDecodingProps.DEFAULT): ImageData = + props.decodingProps.formatSure.readImage(this.readAll().openSync(), ImageDecodingProps(props.decodingProps.filename)) suspend fun AsyncInputStream.readBitmapListNoNative(props: ImageDecodingProps = ImageDecodingProps.DEFAULT): List = readImageData(props).frames.map { it.bitmap } -suspend fun VfsFile.readBitmapInfo(props: ImageDecodingProps = ImageDecodingProps.DEFAULT): ImageInfo? = - props.formatSure.decodeHeader(this.readAsSyncStream(), props) -suspend fun VfsFile.readImageInfo(props: ImageDecodingProps = ImageDecodingProps.DEFAULT): ImageInfo? = - openUse(VfsOpenMode.READ) { props.formatSure.decodeHeaderSuspend(this, props) } -suspend fun VfsFile.readImageData(props: ImageDecodingProps = ImageDecodingProps.DEFAULT, atlas: MutableAtlas? = null): ImageData = +suspend fun VfsFile.readBitmapInfo(props: BaseImageDecodingProps = ImageDecodingProps.DEFAULT): ImageInfo? = + props.decodingProps.formatSure.decodeHeader(this.readAsSyncStream(), props.decodingProps) +suspend fun VfsFile.readImageInfo(props: BaseImageDecodingProps = ImageDecodingProps.DEFAULT): ImageInfo? = + openUse(VfsOpenMode.READ) { props.decodingProps.formatSure.decodeHeaderSuspend(this, props.decodingProps) } +suspend fun VfsFile.readImageData(props: BaseImageDecodingProps = ImageDecodingProps.DEFAULT, atlas: MutableAtlas? = null): ImageData = readImageDataContainer(props, atlas).default -suspend fun VfsFile.readImageDataContainer(props: ImageDecodingProps = ImageDecodingProps.DEFAULT, atlas: MutableAtlas? = null): ImageDataContainer { - val out = props.formatSure.readImageContainer(this.readAsSyncStream(), props.copy(filename = this.baseName)) +suspend fun VfsFile.readImageDataContainer(props: BaseImageDecodingProps = ImageDecodingProps.DEFAULT, atlas: MutableAtlas? = null): ImageDataContainer { + val out = props.decodingProps.formatSure.readImageContainer(this.readAsSyncStream(), props.decodingProps.withFile(this)) return if (atlas != null) out.packInMutableAtlas(atlas) else out } -suspend fun VfsFile.readBitmapListNoNative(props: ImageDecodingProps = ImageDecodingProps.DEFAULT): List = +suspend fun VfsFile.readBitmapListNoNative(props: BaseImageDecodingProps = ImageDecodingProps.DEFAULT): List = readImageData(props).frames.map { it.bitmap } -suspend fun VfsFile.readBitmapImageData(props: ImageDecodingProps = ImageDecodingProps.DEFAULT): ImageData = +suspend fun VfsFile.readBitmapImageData(props: BaseImageDecodingProps = ImageDecodingProps.DEFAULT): ImageData = readImageData(props) suspend fun VfsFile.readBitmapSlice( name: String? = null, atlas: MutableAtlasUnit? = null, - props: ImageDecodingProps = ImageDecodingProps.DEFAULT, + props: BaseImageDecodingProps = ImageDecodingProps.DEFAULT, +): BmpSlice = readBitmapSlice(props, name, atlas) + +suspend fun VfsFile.readBitmapSlice( + bprops: BaseImageDecodingProps, + name: String? = null, + atlas: MutableAtlasUnit? = null, ): BmpSlice { - val result = readBitmap(props = props) + val result = readBitmap(props = bprops) return when { atlas != null -> atlas.add(result.toBMP32IfRequired(), Unit, name).slice else -> result.slice(name = name) } } -// ImageFormat variants - -suspend fun VfsFile.readBitmapListNoNative(format: ImageFormat): List = - readBitmapListNoNative(format.toProps()) -suspend fun VfsFile.readBitmap(format: ImageFormat): Bitmap = - readBitmap(format.toProps()) -suspend fun VfsFile.readBitmapNative(format: ImageFormat): Bitmap = - readBitmapNative(format.toProps()) -suspend fun VfsFile.readBitmapNoNative(format: ImageFormat): Bitmap = - readBitmapNoNative(format.toProps()) -suspend fun VfsFile.readImageInfo(format: ImageFormat): ImageInfo? = - readImageInfo(format.toProps()) -suspend fun VfsFile.readBitmapInfo(format: ImageFormat): ImageInfo? = - readBitmapInfo(format.toProps()) - // Atlas variants fun BmpSlice.toAtlas(atlas: MutableAtlasUnit): BmpSlice32 = atlas.add(this, Unit).slice @@ -93,7 +84,7 @@ suspend fun VfsFile.writeBitmap( format: ImageFormat, props: ImageEncodingProps = ImageEncodingProps() ) { - this.write(format.encode(bitmap, props.copy(filename = this.baseName))) + this.write(format.encode(bitmap, props.withFile(this))) } ////////////////////////// @@ -105,7 +96,7 @@ private suspend fun _readBitmap( bytes: ByteArray? = null, props: ImageDecodingProps = ImageDecodingProps.DEFAULT ): Bitmap { - val prop = if (file != null) props.copy(filename = file.baseName) else props + val prop = if (file != null) props.withFile(file) else props val rformats = when { !prop.tryNativeDecode -> listOf(prop.format) prop.format == null -> listOf(null) diff --git a/korge-core/src/common/korlibs/image/format/NativeImageFormatProvider.kt b/korge-core/src/common/korlibs/image/format/NativeImageFormatProvider.kt index d8fdad16d5..2f7f3f477b 100644 --- a/korge-core/src/common/korlibs/image/format/NativeImageFormatProvider.kt +++ b/korge-core/src/common/korlibs/image/format/NativeImageFormatProvider.kt @@ -3,9 +3,11 @@ package korlibs.image.format import korlibs.image.bitmap.* import korlibs.image.color.* import korlibs.image.vector.* +import korlibs.io.async.* import korlibs.io.file.* import korlibs.math.geom.* import kotlinx.coroutines.* +import kotlin.coroutines.* import kotlin.math.* expect val nativeImageFormatProvider: NativeImageFormatProvider @@ -17,6 +19,11 @@ data class NativeImageResult( ) abstract class NativeImageFormatProvider : ImageFormatEncoderDecoder { + protected suspend fun executeIo(callback: suspend () -> T): T = when { + coroutineContext.preferSyncIo -> callback() + else -> withContext(Dispatchers.CIO) { callback() } + } + protected open suspend fun decodeHeaderInternal(data: ByteArray): ImageInfo { val result = decodeInternal(data, ImageDecodingProps.DEFAULT) return ImageInfo().also { diff --git a/korge-core/src/jvm/korlibs/image/format/CoreGraphicsImageFormatProvider.kt b/korge-core/src/jvm/korlibs/image/format/CoreGraphicsImageFormatProvider.kt index 35cda15e91..ff83874b92 100644 --- a/korge-core/src/jvm/korlibs/image/format/CoreGraphicsImageFormatProvider.kt +++ b/korge-core/src/jvm/korlibs/image/format/CoreGraphicsImageFormatProvider.kt @@ -8,7 +8,6 @@ import korlibs.io.annotations.* import korlibs.io.file.* import korlibs.io.file.std.* import korlibs.time.* -import kotlinx.coroutines.* import java.awt.image.* import java.io.* @@ -112,25 +111,17 @@ open class CoreGraphicsImageFormatProvider : AwtNativeImageFormatProvider() { } override suspend fun decodeInternal(data: ByteArray, props: ImageDecodingProps): NativeImageResult { - val (res, time) = measureTimeWithResult { - withContext(Dispatchers.IO) { - _decodeInternal(data, props) - } - } + val (res, time) = measureTimeWithResult { executeIo { _decodeInternal(data, props) } } //println("DECODED IMAGE IN: $time") return res } override suspend fun decodeInternal(vfs: Vfs, path: String, props: ImageDecodingProps): NativeImageResult { val finalFile = vfs.getUnderlyingUnscapedFile(path) - if (finalFile.vfs is LocalVfs) { - //println("DECODE FAST PATH! from LocalVfs") - return withContext(Dispatchers.IO) { - _decodeInternal(File(finalFile.path).readBytes(), props) - } + return when (finalFile.vfs) { + is LocalVfs -> executeIo { _decodeInternal(File(finalFile.path).readBytes(), props) } + else -> decodeInternal(vfs[path].readBytes(), props) } - - return decodeInternal(vfs[path].readBytes(), props) } @@ -195,6 +186,9 @@ open class CoreGraphicsImageFormatProvider : AwtNativeImageFormatProvider() { } val colorSpace = CoreGraphics.CGColorSpaceCreateDeviceRGB() + if (width * height <= 0) { + error("Invalid size size=${width}x${height}") + } val pixels = Memory((width * height * 4).toLong()).also { it.clear() } val context = CoreGraphics.CGBitmapContextCreate(