Skip to content

Commit

Permalink
Simplify readImage/Bitmap* signatures by providing a common interface…
Browse files Browse the repository at this point in the history
… BaseImageDecodingProps (#2016)

* Make CoreGraphicsImageFormatProvider aware of preferSyncIo

* Simplify readImage/Bitmap* signatures by providing a common interface BaseImageDecodingProps
  • Loading branch information
soywiz authored Nov 6, 2023
1 parent 3f19b9b commit 6b13133
Show file tree
Hide file tree
Showing 6 changed files with 68 additions and 87 deletions.
29 changes: 4 additions & 25 deletions korge-core/src/common/korlibs/image/format/ICO.kt
Original file line number Diff line number Diff line change
@@ -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") {
Expand Down Expand Up @@ -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()
Expand Down
24 changes: 17 additions & 7 deletions korge-core/src/common/korlibs/image/format/ImageFormat.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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? {
Expand All @@ -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)
Expand All @@ -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) =
Expand All @@ -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,
Expand All @@ -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 {
Expand Down Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion korge-core/src/common/korlibs/image/format/ImageFormats.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
73 changes: 32 additions & 41 deletions korge-core/src/common/korlibs/image/format/KorioExt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<Bitmap> =
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<Unit>? = 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<Unit>? = null): ImageData =
readImageDataContainer(props, atlas).default

suspend fun VfsFile.readImageDataContainer(props: ImageDecodingProps = ImageDecodingProps.DEFAULT, atlas: MutableAtlas<Unit>? = null): ImageDataContainer {
val out = props.formatSure.readImageContainer(this.readAsSyncStream(), props.copy(filename = this.baseName))
suspend fun VfsFile.readImageDataContainer(props: BaseImageDecodingProps = ImageDecodingProps.DEFAULT, atlas: MutableAtlas<Unit>? = 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<Bitmap> =
suspend fun VfsFile.readBitmapListNoNative(props: BaseImageDecodingProps = ImageDecodingProps.DEFAULT): List<Bitmap> =
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<Bitmap> =
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
Expand All @@ -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)))
}

//////////////////////////
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -17,6 +19,11 @@ data class NativeImageResult(
)

abstract class NativeImageFormatProvider : ImageFormatEncoderDecoder {
protected suspend fun <T> 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*

Expand Down Expand Up @@ -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)
}


Expand Down Expand Up @@ -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(
Expand Down

0 comments on commit 6b13133

Please sign in to comment.