From 0b8b40f2e1a2f21f2b9c0c7d0ea1b2008f7773a9 Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Sat, 6 Jun 2026 12:42:55 +0200 Subject: [PATCH 1/2] fix(mpp): scope image modules to supported targets Limit skainet-io-image and skainet-data-transform to JVM and Android until non-JVM image actuals are implemented, so published KMP targets match real runtime support. Refs #669 --- .../skainet-data-transform/build.gradle.kts | 17 ++--------------- skainet-io/skainet-io-image/build.gradle.kts | 19 ++----------------- 2 files changed, 4 insertions(+), 32 deletions(-) diff --git a/skainet-data/skainet-data-transform/build.gradle.kts b/skainet-data/skainet-data-transform/build.gradle.kts index 0824f1339..593e70efe 100644 --- a/skainet-data/skainet-data-transform/build.gradle.kts +++ b/skainet-data/skainet-data-transform/build.gradle.kts @@ -1,4 +1,3 @@ -import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { @@ -20,22 +19,10 @@ kotlin { } } - iosArm64() - iosSimulatorArm64() - macosArm64() - linuxX64() - linuxArm64() - jvm() - js { - browser() - } - - @OptIn(ExperimentalWasmDsl::class) - wasmJs { - browser() - } + // Non-JVM targets are intentionally omitted until skainet-io-image + // provides working image interop actuals instead of runtime stubs. sourceSets { val commonMain by getting { diff --git a/skainet-io/skainet-io-image/build.gradle.kts b/skainet-io/skainet-io-image/build.gradle.kts index 58304b54f..090a0d6cc 100644 --- a/skainet-io/skainet-io-image/build.gradle.kts +++ b/skainet-io/skainet-io-image/build.gradle.kts @@ -1,6 +1,3 @@ -@file:OptIn(ExperimentalWasmDsl::class) - -import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { @@ -32,20 +29,8 @@ kotlin { } } - iosArm64() - iosSimulatorArm64() - macosArm64 () - linuxX64 () - linuxArm64 () - - js { - browser() - } - - @OptIn(ExperimentalWasmDsl::class) - wasmJs { - browser() - } + // Non-JVM targets are intentionally omitted until image interop actuals + // are implemented instead of throwing NotImplementedError at runtime. sourceSets { val commonMain by getting { From be779d84e22828a1cf31d89e37638c414dc068ab Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Sat, 6 Jun 2026 13:53:52 +0200 Subject: [PATCH 2/2] fix(mpp): implement non-jvm image runtime support --- .../skainet-data-transform/build.gradle.kts | 17 +- .../transform/RgbaImageTransformSupport.kt | 253 ++++++++++++++++++ .../RgbaImageTransformSupportTest.kt | 98 +++++++ .../data/transform/ImageTransforms.ios.kt | 23 +- .../data/transform/ImageTransforms.js.kt | 23 +- .../transform/ImageTransforms.linuxArm64.kt | 23 +- .../transform/ImageTransforms.linuxX64.kt | 23 +- .../data/transform/ImageTransforms.macos.kt | 104 ++++++- .../data/transform/ImageTransforms.wasmJs.kt | 23 +- skainet-io/skainet-io-image/build.gradle.kts | 18 +- .../ainet/io/image/RgbaImageInteropSupport.kt | 95 +++++++ .../io/image/RgbaImageInteropSupportTest.kt | 101 +++++++ .../sk/ainet/io/image/ImageInterop.ios.kt | 23 +- .../sk/ainet/io/image/ImageInterop.js.kt | 23 +- .../ainet/io/image/ImageInterop.linuxArm64.kt | 23 +- .../ainet/io/image/ImageInterop.linuxX64.kt | 23 +- .../sk/ainet/io/image/ImageInterop.wasmJs.kt | 23 +- 17 files changed, 863 insertions(+), 53 deletions(-) create mode 100644 skainet-data/skainet-data-transform/src/commonMain/kotlin/sk/ainet/data/transform/RgbaImageTransformSupport.kt create mode 100644 skainet-data/skainet-data-transform/src/commonTest/kotlin/sk/ainet/data/transform/RgbaImageTransformSupportTest.kt create mode 100644 skainet-io/skainet-io-image/src/commonMain/kotlin/sk/ainet/io/image/RgbaImageInteropSupport.kt create mode 100644 skainet-io/skainet-io-image/src/commonTest/kotlin/sk/ainet/io/image/RgbaImageInteropSupportTest.kt diff --git a/skainet-data/skainet-data-transform/build.gradle.kts b/skainet-data/skainet-data-transform/build.gradle.kts index 593e70efe..f915c037c 100644 --- a/skainet-data/skainet-data-transform/build.gradle.kts +++ b/skainet-data/skainet-data-transform/build.gradle.kts @@ -1,3 +1,4 @@ +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { @@ -21,8 +22,20 @@ kotlin { jvm() - // Non-JVM targets are intentionally omitted until skainet-io-image - // provides working image interop actuals instead of runtime stubs. + iosArm64() + iosSimulatorArm64() + macosArm64() + linuxX64() + linuxArm64() + + js { + browser() + } + + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + browser() + } sourceSets { val commonMain by getting { diff --git a/skainet-data/skainet-data-transform/src/commonMain/kotlin/sk/ainet/data/transform/RgbaImageTransformSupport.kt b/skainet-data/skainet-data-transform/src/commonMain/kotlin/sk/ainet/data/transform/RgbaImageTransformSupport.kt new file mode 100644 index 000000000..c7ddfd0cf --- /dev/null +++ b/skainet-data/skainet-data-transform/src/commonMain/kotlin/sk/ainet/data/transform/RgbaImageTransformSupport.kt @@ -0,0 +1,253 @@ +package sk.ainet.data.transform + +import kotlin.math.PI +import kotlin.math.abs +import kotlin.math.ceil +import kotlin.math.cos +import kotlin.math.floor +import kotlin.math.roundToInt +import kotlin.math.sin + +internal data class PackedRgbaImage( + val width: Int, + val height: Int, + val rgba: ByteArray +) { + init { + require(width > 0) { "Width must be positive: $width" } + require(height > 0) { "Height must be positive: $height" } + require(rgba.size == width * height * 4) { + "RGBA buffer must contain width * height * 4 bytes, got ${rgba.size} for ${width}x$height" + } + } +} + +internal fun cropPackedRgbaImage( + image: PackedRgbaImage, + x: Int, + y: Int, + width: Int, + height: Int +): PackedRgbaImage { + val out = ByteArray(width * height * 4) + for (row in 0 until height) { + for (col in 0 until width) { + copyPixel( + image.rgba, + rgbaIndex(image.width, x + col, y + row), + out, + rgbaIndex(width, col, row) + ) + } + } + return PackedRgbaImage(width, height, out) +} + +internal fun padPackedRgbaImage( + image: PackedRgbaImage, + top: Int, + bottom: Int, + left: Int, + right: Int, + red: Int, + green: Int, + blue: Int +): PackedRgbaImage { + val width = image.width + left + right + val height = image.height + top + bottom + val out = ByteArray(width * height * 4) + + for (y in 0 until height) { + for (x in 0 until width) { + val dst = rgbaIndex(width, x, y) + out[dst] = red.toByte() + out[dst + 1] = green.toByte() + out[dst + 2] = blue.toByte() + out[dst + 3] = 0xFF.toByte() + } + } + + for (y in 0 until image.height) { + for (x in 0 until image.width) { + copyPixel( + image.rgba, + rgbaIndex(image.width, x, y), + out, + rgbaIndex(width, x + left, y + top) + ) + } + } + + return PackedRgbaImage(width, height, out) +} + +internal fun resizePackedRgbaImage( + image: PackedRgbaImage, + width: Int, + height: Int, + interpolation: Interpolation +): PackedRgbaImage { + if (image.width == width && image.height == height) { + return PackedRgbaImage(width, height, image.rgba.copyOf()) + } + + val out = ByteArray(width * height * 4) + for (y in 0 until height) { + val srcY = ((y + 0.5) * image.height / height) - 0.5 + for (x in 0 until width) { + val srcX = ((x + 0.5) * image.width / width) - 0.5 + val dst = rgbaIndex(width, x, y) + when (interpolation) { + Interpolation.NEAREST -> sampleNearestClamped(image, srcX, srcY, out, dst) + Interpolation.BILINEAR, Interpolation.BICUBIC -> sampleBilinearClamped(image, srcX, srcY, out, dst) + } + } + } + + return PackedRgbaImage(width, height, out) +} + +internal fun rotatePackedRgbaImage( + image: PackedRgbaImage, + degrees: Float, + interpolation: Interpolation +): PackedRgbaImage { + val radians = degrees * PI / 180.0 + val absSin = abs(sin(radians)) + val absCos = abs(cos(radians)) + val width = ceil(image.width * absCos + image.height * absSin).toInt().coerceAtLeast(1) + val height = ceil(image.width * absSin + image.height * absCos).toInt().coerceAtLeast(1) + val out = ByteArray(width * height * 4) + + val srcCx = (image.width - 1) / 2.0 + val srcCy = (image.height - 1) / 2.0 + val dstCx = (width - 1) / 2.0 + val dstCy = (height - 1) / 2.0 + val cosTheta = cos(radians) + val sinTheta = sin(radians) + + for (y in 0 until height) { + for (x in 0 until width) { + val dx = x - dstCx + val dy = y - dstCy + val srcX = dx * cosTheta - dy * sinTheta + srcCx + val srcY = dx * sinTheta + dy * cosTheta + srcCy + val dst = rgbaIndex(width, x, y) + when (interpolation) { + Interpolation.NEAREST -> sampleNearestTransparent(image, srcX, srcY, out, dst) + Interpolation.BILINEAR, Interpolation.BICUBIC -> sampleBilinearTransparent(image, srcX, srcY, out, dst) + } + } + } + + return PackedRgbaImage(width, height, out) +} + +private fun sampleNearestClamped( + image: PackedRgbaImage, + srcX: Double, + srcY: Double, + out: ByteArray, + dstIndex: Int +) { + val x = srcX.roundToInt().coerceIn(0, image.width - 1) + val y = srcY.roundToInt().coerceIn(0, image.height - 1) + copyPixel(image.rgba, rgbaIndex(image.width, x, y), out, dstIndex) +} + +private fun sampleNearestTransparent( + image: PackedRgbaImage, + srcX: Double, + srcY: Double, + out: ByteArray, + dstIndex: Int +) { + val x = srcX.roundToInt() + val y = srcY.roundToInt() + if (x !in 0 until image.width || y !in 0 until image.height) { + clearPixel(out, dstIndex) + return + } + copyPixel(image.rgba, rgbaIndex(image.width, x, y), out, dstIndex) +} + +private fun sampleBilinearClamped( + image: PackedRgbaImage, + srcX: Double, + srcY: Double, + out: ByteArray, + dstIndex: Int +) { + for (channel in 0 until 4) { + out[dstIndex + channel] = bilinearChannel(image, srcX, srcY, channel, false).toByte() + } +} + +private fun sampleBilinearTransparent( + image: PackedRgbaImage, + srcX: Double, + srcY: Double, + out: ByteArray, + dstIndex: Int +) { + for (channel in 0 until 4) { + out[dstIndex + channel] = bilinearChannel(image, srcX, srcY, channel, true).toByte() + } +} + +private fun bilinearChannel( + image: PackedRgbaImage, + srcX: Double, + srcY: Double, + channel: Int, + transparentOutside: Boolean +): Int { + val x0 = floor(srcX).toInt() + val y0 = floor(srcY).toInt() + val x1 = x0 + 1 + val y1 = y0 + 1 + val fx = srcX - x0 + val fy = srcY - y0 + + val c00 = sampleChannel(image, x0, y0, channel, transparentOutside) + val c10 = sampleChannel(image, x1, y0, channel, transparentOutside) + val c01 = sampleChannel(image, x0, y1, channel, transparentOutside) + val c11 = sampleChannel(image, x1, y1, channel, transparentOutside) + + val top = c00 * (1.0 - fx) + c10 * fx + val bottom = c01 * (1.0 - fx) + c11 * fx + return (top * (1.0 - fy) + bottom * fy).roundToInt().coerceIn(0, 255) +} + +private fun sampleChannel( + image: PackedRgbaImage, + x: Int, + y: Int, + channel: Int, + transparentOutside: Boolean +): Double { + if (x !in 0 until image.width || y !in 0 until image.height) { + return if (transparentOutside) 0.0 else { + val clampedX = x.coerceIn(0, image.width - 1) + val clampedY = y.coerceIn(0, image.height - 1) + image.rgba[rgbaIndex(image.width, clampedX, clampedY) + channel].toUByte().toInt().toDouble() + } + } + return image.rgba[rgbaIndex(image.width, x, y) + channel].toUByte().toInt().toDouble() +} + +private fun copyPixel(src: ByteArray, srcIndex: Int, dst: ByteArray, dstIndex: Int) { + dst[dstIndex] = src[srcIndex] + dst[dstIndex + 1] = src[srcIndex + 1] + dst[dstIndex + 2] = src[srcIndex + 2] + dst[dstIndex + 3] = src[srcIndex + 3] +} + +private fun clearPixel(dst: ByteArray, dstIndex: Int) { + dst[dstIndex] = 0 + dst[dstIndex + 1] = 0 + dst[dstIndex + 2] = 0 + dst[dstIndex + 3] = 0 +} + +private fun rgbaIndex(width: Int, x: Int, y: Int): Int = ((y * width) + x) * 4 diff --git a/skainet-data/skainet-data-transform/src/commonTest/kotlin/sk/ainet/data/transform/RgbaImageTransformSupportTest.kt b/skainet-data/skainet-data-transform/src/commonTest/kotlin/sk/ainet/data/transform/RgbaImageTransformSupportTest.kt new file mode 100644 index 000000000..5193aa212 --- /dev/null +++ b/skainet-data/skainet-data-transform/src/commonTest/kotlin/sk/ainet/data/transform/RgbaImageTransformSupportTest.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2024 SKaiNET contributors. All Rights Reserved. + * Use of this source code is governed by the Apache 2.0 license. + */ + +package sk.ainet.data.transform + +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals + +class RgbaImageTransformSupportTest { + + private fun sampleImage2x2(): PackedRgbaImage = PackedRgbaImage( + width = 2, + height = 2, + rgba = byteArrayOf( + 10, 20, 30, 0xFF.toByte(), + 40, 50, 60, 0xFF.toByte(), + 70, 80, 90, 0xFF.toByte(), + 100, 110, 120, 0xFF.toByte() + ) + ) + + @Test + fun `crop copies requested rectangle`() { + val cropped = cropPackedRgbaImage(sampleImage2x2(), x = 1, y = 0, width = 1, height = 2) + + assertEquals(1, cropped.width) + assertEquals(2, cropped.height) + assertContentEquals( + byteArrayOf( + 40, 50, 60, 0xFF.toByte(), + 100, 110, 120, 0xFF.toByte() + ), + cropped.rgba + ) + } + + @Test + fun `pad adds border color and keeps source pixels`() { + val padded = padPackedRgbaImage( + image = sampleImage2x2(), + top = 1, + bottom = 0, + left = 1, + right = 0, + red = 1, + green = 2, + blue = 3 + ) + + assertEquals(3, padded.width) + assertEquals(3, padded.height) + assertContentEquals(byteArrayOf(1, 2, 3, 0xFF.toByte()), padded.rgba.copyOfRange(0, 4)) + assertContentEquals( + byteArrayOf(10, 20, 30, 0xFF.toByte()), + padded.rgba.copyOfRange(((1 * padded.width) + 1) * 4, ((1 * padded.width) + 1) * 4 + 4) + ) + } + + @Test + fun `resize nearest expands pixels predictably`() { + val image = PackedRgbaImage( + width = 2, + height = 1, + rgba = byteArrayOf( + 10, 20, 30, 0xFF.toByte(), + 40, 50, 60, 0xFF.toByte() + ) + ) + + val resized = resizePackedRgbaImage(image, width = 4, height = 1, interpolation = Interpolation.NEAREST) + + assertEquals(4, resized.width) + assertEquals(1, resized.height) + assertContentEquals( + byteArrayOf( + 10, 20, 30, 0xFF.toByte(), + 10, 20, 30, 0xFF.toByte(), + 40, 50, 60, 0xFF.toByte(), + 40, 50, 60, 0xFF.toByte() + ), + resized.rgba + ) + } + + @Test + fun `rotate zero degrees keeps image unchanged`() { + val original = sampleImage2x2() + + val rotated = rotatePackedRgbaImage(original, degrees = 0f, interpolation = Interpolation.NEAREST) + + assertEquals(original.width, rotated.width) + assertEquals(original.height, rotated.height) + assertContentEquals(original.rgba, rotated.rgba) + } +} diff --git a/skainet-data/skainet-data-transform/src/iosMain/kotlin/sk/ainet/data/transform/ImageTransforms.ios.kt b/skainet-data/skainet-data-transform/src/iosMain/kotlin/sk/ainet/data/transform/ImageTransforms.ios.kt index b6f79465a..6ef4269b4 100644 --- a/skainet-data/skainet-data-transform/src/iosMain/kotlin/sk/ainet/data/transform/ImageTransforms.ios.kt +++ b/skainet-data/skainet-data-transform/src/iosMain/kotlin/sk/ainet/data/transform/ImageTransforms.ios.kt @@ -7,13 +7,19 @@ package sk.ainet.data.transform import sk.ainet.io.image.PlatformBitmapImage +private fun PlatformBitmapImage.toPackedRgbaImage(): PackedRgbaImage = + PackedRgbaImage(width, height, rgba) + +private fun PackedRgbaImage.toPlatformBitmapImage(): PlatformBitmapImage = + PlatformBitmapImage(width, height, rgba) + public actual fun resizePlatformImage( image: PlatformBitmapImage, width: Int, height: Int, interpolation: Interpolation ): PlatformBitmapImage { - throw NotImplementedError("Image transforms not yet implemented for iOS") + return resizePackedRgbaImage(image.toPackedRgbaImage(), width, height, interpolation).toPlatformBitmapImage() } public actual fun cropPlatformImage( @@ -23,7 +29,7 @@ public actual fun cropPlatformImage( width: Int, height: Int ): PlatformBitmapImage { - throw NotImplementedError("Image transforms not yet implemented for iOS") + return cropPackedRgbaImage(image.toPackedRgbaImage(), x, y, width, height).toPlatformBitmapImage() } public actual fun rotatePlatformImage( @@ -31,7 +37,7 @@ public actual fun rotatePlatformImage( degrees: Float, interpolation: Interpolation ): PlatformBitmapImage { - throw NotImplementedError("Image transforms not yet implemented for iOS") + return rotatePackedRgbaImage(image.toPackedRgbaImage(), degrees, interpolation).toPlatformBitmapImage() } public actual fun padPlatformImage( @@ -44,5 +50,14 @@ public actual fun padPlatformImage( green: Int, blue: Int ): PlatformBitmapImage { - throw NotImplementedError("Image transforms not yet implemented for iOS") + return padPackedRgbaImage( + image.toPackedRgbaImage(), + top, + bottom, + left, + right, + red, + green, + blue + ).toPlatformBitmapImage() } diff --git a/skainet-data/skainet-data-transform/src/jsMain/kotlin/sk/ainet/data/transform/ImageTransforms.js.kt b/skainet-data/skainet-data-transform/src/jsMain/kotlin/sk/ainet/data/transform/ImageTransforms.js.kt index efd0ab641..6ef4269b4 100644 --- a/skainet-data/skainet-data-transform/src/jsMain/kotlin/sk/ainet/data/transform/ImageTransforms.js.kt +++ b/skainet-data/skainet-data-transform/src/jsMain/kotlin/sk/ainet/data/transform/ImageTransforms.js.kt @@ -7,13 +7,19 @@ package sk.ainet.data.transform import sk.ainet.io.image.PlatformBitmapImage +private fun PlatformBitmapImage.toPackedRgbaImage(): PackedRgbaImage = + PackedRgbaImage(width, height, rgba) + +private fun PackedRgbaImage.toPlatformBitmapImage(): PlatformBitmapImage = + PlatformBitmapImage(width, height, rgba) + public actual fun resizePlatformImage( image: PlatformBitmapImage, width: Int, height: Int, interpolation: Interpolation ): PlatformBitmapImage { - throw NotImplementedError("Image transforms not yet implemented for JavaScript") + return resizePackedRgbaImage(image.toPackedRgbaImage(), width, height, interpolation).toPlatformBitmapImage() } public actual fun cropPlatformImage( @@ -23,7 +29,7 @@ public actual fun cropPlatformImage( width: Int, height: Int ): PlatformBitmapImage { - throw NotImplementedError("Image transforms not yet implemented for JavaScript") + return cropPackedRgbaImage(image.toPackedRgbaImage(), x, y, width, height).toPlatformBitmapImage() } public actual fun rotatePlatformImage( @@ -31,7 +37,7 @@ public actual fun rotatePlatformImage( degrees: Float, interpolation: Interpolation ): PlatformBitmapImage { - throw NotImplementedError("Image transforms not yet implemented for JavaScript") + return rotatePackedRgbaImage(image.toPackedRgbaImage(), degrees, interpolation).toPlatformBitmapImage() } public actual fun padPlatformImage( @@ -44,5 +50,14 @@ public actual fun padPlatformImage( green: Int, blue: Int ): PlatformBitmapImage { - throw NotImplementedError("Image transforms not yet implemented for JavaScript") + return padPackedRgbaImage( + image.toPackedRgbaImage(), + top, + bottom, + left, + right, + red, + green, + blue + ).toPlatformBitmapImage() } diff --git a/skainet-data/skainet-data-transform/src/linuxArm64Main/kotlin/sk/ainet/data/transform/ImageTransforms.linuxArm64.kt b/skainet-data/skainet-data-transform/src/linuxArm64Main/kotlin/sk/ainet/data/transform/ImageTransforms.linuxArm64.kt index 539083ac4..6ef4269b4 100644 --- a/skainet-data/skainet-data-transform/src/linuxArm64Main/kotlin/sk/ainet/data/transform/ImageTransforms.linuxArm64.kt +++ b/skainet-data/skainet-data-transform/src/linuxArm64Main/kotlin/sk/ainet/data/transform/ImageTransforms.linuxArm64.kt @@ -7,13 +7,19 @@ package sk.ainet.data.transform import sk.ainet.io.image.PlatformBitmapImage +private fun PlatformBitmapImage.toPackedRgbaImage(): PackedRgbaImage = + PackedRgbaImage(width, height, rgba) + +private fun PackedRgbaImage.toPlatformBitmapImage(): PlatformBitmapImage = + PlatformBitmapImage(width, height, rgba) + public actual fun resizePlatformImage( image: PlatformBitmapImage, width: Int, height: Int, interpolation: Interpolation ): PlatformBitmapImage { - throw NotImplementedError("Image transforms not yet implemented for Linux ARM64") + return resizePackedRgbaImage(image.toPackedRgbaImage(), width, height, interpolation).toPlatformBitmapImage() } public actual fun cropPlatformImage( @@ -23,7 +29,7 @@ public actual fun cropPlatformImage( width: Int, height: Int ): PlatformBitmapImage { - throw NotImplementedError("Image transforms not yet implemented for Linux ARM64") + return cropPackedRgbaImage(image.toPackedRgbaImage(), x, y, width, height).toPlatformBitmapImage() } public actual fun rotatePlatformImage( @@ -31,7 +37,7 @@ public actual fun rotatePlatformImage( degrees: Float, interpolation: Interpolation ): PlatformBitmapImage { - throw NotImplementedError("Image transforms not yet implemented for Linux ARM64") + return rotatePackedRgbaImage(image.toPackedRgbaImage(), degrees, interpolation).toPlatformBitmapImage() } public actual fun padPlatformImage( @@ -44,5 +50,14 @@ public actual fun padPlatformImage( green: Int, blue: Int ): PlatformBitmapImage { - throw NotImplementedError("Image transforms not yet implemented for Linux ARM64") + return padPackedRgbaImage( + image.toPackedRgbaImage(), + top, + bottom, + left, + right, + red, + green, + blue + ).toPlatformBitmapImage() } diff --git a/skainet-data/skainet-data-transform/src/linuxX64Main/kotlin/sk/ainet/data/transform/ImageTransforms.linuxX64.kt b/skainet-data/skainet-data-transform/src/linuxX64Main/kotlin/sk/ainet/data/transform/ImageTransforms.linuxX64.kt index a49788129..6ef4269b4 100644 --- a/skainet-data/skainet-data-transform/src/linuxX64Main/kotlin/sk/ainet/data/transform/ImageTransforms.linuxX64.kt +++ b/skainet-data/skainet-data-transform/src/linuxX64Main/kotlin/sk/ainet/data/transform/ImageTransforms.linuxX64.kt @@ -7,13 +7,19 @@ package sk.ainet.data.transform import sk.ainet.io.image.PlatformBitmapImage +private fun PlatformBitmapImage.toPackedRgbaImage(): PackedRgbaImage = + PackedRgbaImage(width, height, rgba) + +private fun PackedRgbaImage.toPlatformBitmapImage(): PlatformBitmapImage = + PlatformBitmapImage(width, height, rgba) + public actual fun resizePlatformImage( image: PlatformBitmapImage, width: Int, height: Int, interpolation: Interpolation ): PlatformBitmapImage { - throw NotImplementedError("Image transforms not yet implemented for Linux x64") + return resizePackedRgbaImage(image.toPackedRgbaImage(), width, height, interpolation).toPlatformBitmapImage() } public actual fun cropPlatformImage( @@ -23,7 +29,7 @@ public actual fun cropPlatformImage( width: Int, height: Int ): PlatformBitmapImage { - throw NotImplementedError("Image transforms not yet implemented for Linux x64") + return cropPackedRgbaImage(image.toPackedRgbaImage(), x, y, width, height).toPlatformBitmapImage() } public actual fun rotatePlatformImage( @@ -31,7 +37,7 @@ public actual fun rotatePlatformImage( degrees: Float, interpolation: Interpolation ): PlatformBitmapImage { - throw NotImplementedError("Image transforms not yet implemented for Linux x64") + return rotatePackedRgbaImage(image.toPackedRgbaImage(), degrees, interpolation).toPlatformBitmapImage() } public actual fun padPlatformImage( @@ -44,5 +50,14 @@ public actual fun padPlatformImage( green: Int, blue: Int ): PlatformBitmapImage { - throw NotImplementedError("Image transforms not yet implemented for Linux x64") + return padPackedRgbaImage( + image.toPackedRgbaImage(), + top, + bottom, + left, + right, + red, + green, + blue + ).toPlatformBitmapImage() } diff --git a/skainet-data/skainet-data-transform/src/macosArm64Main/kotlin/sk/ainet/data/transform/ImageTransforms.macos.kt b/skainet-data/skainet-data-transform/src/macosArm64Main/kotlin/sk/ainet/data/transform/ImageTransforms.macos.kt index c1c3c5e78..99ee72d1b 100644 --- a/skainet-data/skainet-data-transform/src/macosArm64Main/kotlin/sk/ainet/data/transform/ImageTransforms.macos.kt +++ b/skainet-data/skainet-data-transform/src/macosArm64Main/kotlin/sk/ainet/data/transform/ImageTransforms.macos.kt @@ -3,9 +3,34 @@ * Use of this source code is governed by the Apache 2.0 license. */ +@file:OptIn(ExperimentalForeignApi::class) + package sk.ainet.data.transform +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.CValue +import kotlinx.cinterop.addressOf +import kotlinx.cinterop.convert +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.reinterpret +import kotlinx.cinterop.usePinned +import platform.AppKit.NSImage +import platform.CoreFoundation.CFDataCreate +import platform.CoreFoundation.kCFAllocatorDefault +import platform.CoreGraphics.CGBitmapContextCreate +import platform.CoreGraphics.CGColorRenderingIntent +import platform.CoreGraphics.CGColorSpaceCreateDeviceRGB +import platform.CoreGraphics.CGContextDrawImage +import platform.CoreGraphics.CGDataProviderCreateWithCFData +import platform.CoreGraphics.CGImageAlphaInfo +import platform.CoreGraphics.CGImageCreate +import platform.CoreGraphics.CGImageGetHeight +import platform.CoreGraphics.CGImageGetWidth +import platform.CoreGraphics.CGRectMake +import platform.CoreGraphics.CGSize +import platform.CoreGraphics.CGSizeMake import sk.ainet.io.image.PlatformBitmapImage +import sk.ainet.io.image.platformImageSize public actual fun resizePlatformImage( image: PlatformBitmapImage, @@ -13,7 +38,8 @@ public actual fun resizePlatformImage( height: Int, interpolation: Interpolation ): PlatformBitmapImage { - throw NotImplementedError("Image transforms not yet implemented for macOS") + val raster = macosImageToPackedRgbaImage(image) + return packedRgbaImageToMacosImage(resizePackedRgbaImage(raster, width, height, interpolation)) } public actual fun cropPlatformImage( @@ -23,7 +49,8 @@ public actual fun cropPlatformImage( width: Int, height: Int ): PlatformBitmapImage { - throw NotImplementedError("Image transforms not yet implemented for macOS") + val raster = macosImageToPackedRgbaImage(image) + return packedRgbaImageToMacosImage(cropPackedRgbaImage(raster, x, y, width, height)) } public actual fun rotatePlatformImage( @@ -31,7 +58,8 @@ public actual fun rotatePlatformImage( degrees: Float, interpolation: Interpolation ): PlatformBitmapImage { - throw NotImplementedError("Image transforms not yet implemented for macOS") + val raster = macosImageToPackedRgbaImage(image) + return packedRgbaImageToMacosImage(rotatePackedRgbaImage(raster, degrees, interpolation)) } public actual fun padPlatformImage( @@ -44,5 +72,73 @@ public actual fun padPlatformImage( green: Int, blue: Int ): PlatformBitmapImage { - throw NotImplementedError("Image transforms not yet implemented for macOS") + val raster = macosImageToPackedRgbaImage(image) + return packedRgbaImageToMacosImage( + padPackedRgbaImage(raster, top, bottom, left, right, red, green, blue) + ) +} + +private fun macosImageToPackedRgbaImage(image: NSImage): PackedRgbaImage { + val (width, height) = platformImageSize(image) + return PackedRgbaImage(width, height, drawImageIntoRgbaBuffer(image, width, height)) +} + +private fun packedRgbaImageToMacosImage(image: PackedRgbaImage): NSImage { + val cgImage = createCgImageFromRgba(image.rgba, image.width, image.height) + val size: CValue = CGSizeMake(image.width.toDouble(), image.height.toDouble()) + return NSImage(cGImage = cgImage, size = size) +} + +private fun drawImageIntoRgbaBuffer(image: NSImage, width: Int, height: Int): ByteArray = memScoped { + val cg = image.CGImageForProposedRect(null, null, null) + ?: error("NSImage has no CGImage representation") + val colorSpace = CGColorSpaceCreateDeviceRGB() + ?: error("Failed to create RGB color space") + val bytesPerRow = width * 4 + val bitmapInfo: UInt = CGImageAlphaInfo.kCGImageAlphaPremultipliedLast.value + val buffer = ByteArray(width * height * 4) + buffer.usePinned { pinned -> + val ctx = CGBitmapContextCreate( + pinned.addressOf(0), + width.convert(), + height.convert(), + 8.convert(), + bytesPerRow.convert(), + colorSpace, + bitmapInfo + ) ?: error("Failed to create bitmap context") + val rect = CGRectMake(0.0, 0.0, width.toDouble(), height.toDouble()) + CGContextDrawImage(ctx, rect, cg) + } + buffer +} + +private fun createCgImageFromRgba(bytes: ByteArray, width: Int, height: Int) = memScoped { + val colorSpace = CGColorSpaceCreateDeviceRGB() + ?: error("Failed to create RGB color space") + val bytesPerRow = width * 4 + val provider = bytes.usePinned { pinned -> + val cfData = CFDataCreate( + kCFAllocatorDefault, + pinned.addressOf(0).reinterpret(), + bytes.size.convert() + ) ?: error("Failed to create CFData") + CGDataProviderCreateWithCFData(cfData) + ?: error("Failed to create CGDataProvider") + } + + val bitmapInfo: UInt = CGImageAlphaInfo.kCGImageAlphaPremultipliedLast.value + CGImageCreate( + width.convert(), + height.convert(), + 8.convert(), + 32.convert(), + bytesPerRow.convert(), + colorSpace, + bitmapInfo, + provider, + null, + true, + CGColorRenderingIntent.kCGRenderingIntentDefault + ) ?: error("Failed to create CGImage") } diff --git a/skainet-data/skainet-data-transform/src/wasmJsMain/kotlin/sk/ainet/data/transform/ImageTransforms.wasmJs.kt b/skainet-data/skainet-data-transform/src/wasmJsMain/kotlin/sk/ainet/data/transform/ImageTransforms.wasmJs.kt index 181f2344f..6ef4269b4 100644 --- a/skainet-data/skainet-data-transform/src/wasmJsMain/kotlin/sk/ainet/data/transform/ImageTransforms.wasmJs.kt +++ b/skainet-data/skainet-data-transform/src/wasmJsMain/kotlin/sk/ainet/data/transform/ImageTransforms.wasmJs.kt @@ -7,13 +7,19 @@ package sk.ainet.data.transform import sk.ainet.io.image.PlatformBitmapImage +private fun PlatformBitmapImage.toPackedRgbaImage(): PackedRgbaImage = + PackedRgbaImage(width, height, rgba) + +private fun PackedRgbaImage.toPlatformBitmapImage(): PlatformBitmapImage = + PlatformBitmapImage(width, height, rgba) + public actual fun resizePlatformImage( image: PlatformBitmapImage, width: Int, height: Int, interpolation: Interpolation ): PlatformBitmapImage { - throw NotImplementedError("Image transforms not yet implemented for WebAssembly") + return resizePackedRgbaImage(image.toPackedRgbaImage(), width, height, interpolation).toPlatformBitmapImage() } public actual fun cropPlatformImage( @@ -23,7 +29,7 @@ public actual fun cropPlatformImage( width: Int, height: Int ): PlatformBitmapImage { - throw NotImplementedError("Image transforms not yet implemented for WebAssembly") + return cropPackedRgbaImage(image.toPackedRgbaImage(), x, y, width, height).toPlatformBitmapImage() } public actual fun rotatePlatformImage( @@ -31,7 +37,7 @@ public actual fun rotatePlatformImage( degrees: Float, interpolation: Interpolation ): PlatformBitmapImage { - throw NotImplementedError("Image transforms not yet implemented for WebAssembly") + return rotatePackedRgbaImage(image.toPackedRgbaImage(), degrees, interpolation).toPlatformBitmapImage() } public actual fun padPlatformImage( @@ -44,5 +50,14 @@ public actual fun padPlatformImage( green: Int, blue: Int ): PlatformBitmapImage { - throw NotImplementedError("Image transforms not yet implemented for WebAssembly") + return padPackedRgbaImage( + image.toPackedRgbaImage(), + top, + bottom, + left, + right, + red, + green, + blue + ).toPlatformBitmapImage() } diff --git a/skainet-io/skainet-io-image/build.gradle.kts b/skainet-io/skainet-io-image/build.gradle.kts index 090a0d6cc..ecfb302b5 100644 --- a/skainet-io/skainet-io-image/build.gradle.kts +++ b/skainet-io/skainet-io-image/build.gradle.kts @@ -1,3 +1,6 @@ +@file:OptIn(ExperimentalWasmDsl::class) + +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { @@ -29,8 +32,19 @@ kotlin { } } - // Non-JVM targets are intentionally omitted until image interop actuals - // are implemented instead of throwing NotImplementedError at runtime. + iosArm64() + iosSimulatorArm64() + macosArm64() + linuxX64() + linuxArm64() + + js { + browser() + } + + wasmJs { + browser() + } sourceSets { val commonMain by getting { diff --git a/skainet-io/skainet-io-image/src/commonMain/kotlin/sk/ainet/io/image/RgbaImageInteropSupport.kt b/skainet-io/skainet-io-image/src/commonMain/kotlin/sk/ainet/io/image/RgbaImageInteropSupport.kt new file mode 100644 index 000000000..71fdb7de2 --- /dev/null +++ b/skainet-io/skainet-io-image/src/commonMain/kotlin/sk/ainet/io/image/RgbaImageInteropSupport.kt @@ -0,0 +1,95 @@ +package sk.ainet.io.image + +import sk.ainet.context.ExecutionContext +import sk.ainet.context.data +import sk.ainet.lang.tensor.Tensor +import sk.ainet.lang.tensor.dsl.tensor +import sk.ainet.lang.types.FP16 + +internal data class PackedRgbaImage( + val width: Int, + val height: Int, + val rgba: ByteArray +) { + init { + require(width > 0) { "Width must be positive: $width" } + require(height > 0) { "Height must be positive: $height" } + require(rgba.size == width * height * 4) { + "RGBA buffer must contain width * height * 4 bytes, got ${rgba.size} for ${width}x$height" + } + } +} + +internal fun packedRgbaToTensor(image: PackedRgbaImage, ctx: ExecutionContext): Tensor = + data(ctx) { + val rgbChw = FloatArray(image.width * image.height * 3) + var p = 0 + for (y in 0 until image.height) { + for (x in 0 until image.width) { + val hw = y * image.width + x + rgbChw[hw] = image.rgba[p].toUByte().toInt().toFloat() + rgbChw[image.height * image.width + hw] = image.rgba[p + 1].toUByte().toInt().toFloat() + rgbChw[2 * image.height * image.width + hw] = image.rgba[p + 2].toUByte().toInt().toFloat() + p += 4 + } + } + + tensor { + shape(1, 3, image.height, image.width) { + fromArray(rgbChw) + } + } + } + +internal fun tensorToPackedRgba(image: Tensor): PackedRgbaImage { + val shape = image.data.shape + val channels = shape[1] + val height = shape[2] + val width = shape[3] + val rgba = ByteArray(width * height * 4) + + var p = 0 + for (y in 0 until height) { + for (x in 0 until width) { + when (channels) { + 1 -> { + val v = image.data[0, 0, y, x].toInt().coerceIn(0, 255).toByte() + rgba[p++] = v + rgba[p++] = v + rgba[p++] = v + rgba[p++] = 0xFF.toByte() + } + + 3 -> { + rgba[p++] = image.data[0, 0, y, x].toInt().coerceIn(0, 255).toByte() + rgba[p++] = image.data[0, 1, y, x].toInt().coerceIn(0, 255).toByte() + rgba[p++] = image.data[0, 2, y, x].toInt().coerceIn(0, 255).toByte() + rgba[p++] = 0xFF.toByte() + } + + else -> { + val v = image.data[0, 0, y, x].toInt().coerceIn(0, 255).toByte() + rgba[p++] = v + rgba[p++] = v + rgba[p++] = v + rgba[p++] = 0xFF.toByte() + } + } + } + } + + return PackedRgbaImage(width, height, rgba) +} + +internal fun rgbByteArrayFromPackedRgba(image: PackedRgbaImage): ByteArray { + val rgb = ByteArray(image.width * image.height * 3) + var src = 0 + var dst = 0 + while (src < image.rgba.size) { + rgb[dst++] = image.rgba[src] + rgb[dst++] = image.rgba[src + 1] + rgb[dst++] = image.rgba[src + 2] + src += 4 + } + return rgb +} diff --git a/skainet-io/skainet-io-image/src/commonTest/kotlin/sk/ainet/io/image/RgbaImageInteropSupportTest.kt b/skainet-io/skainet-io-image/src/commonTest/kotlin/sk/ainet/io/image/RgbaImageInteropSupportTest.kt new file mode 100644 index 000000000..9fc3ad6b0 --- /dev/null +++ b/skainet-io/skainet-io-image/src/commonTest/kotlin/sk/ainet/io/image/RgbaImageInteropSupportTest.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2024 SKaiNET contributors. All Rights Reserved. + * Use of this source code is governed by the Apache 2.0 license. + */ + +package sk.ainet.io.image + +import sk.ainet.context.DefaultDataExecutionContext +import sk.ainet.lang.tensor.Shape +import sk.ainet.lang.types.FP16 +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals + +class RgbaImageInteropSupportTest { + + private val ctx = DefaultDataExecutionContext() + + @Test + fun `packed rgba converts to rgb chw tensor`() { + val tensor = packedRgbaToTensor( + PackedRgbaImage( + width = 2, + height = 1, + rgba = byteArrayOf( + 10, 20, 30, 0xFF.toByte(), + 40, 50, 60, 0xFF.toByte() + ) + ), + ctx + ) + + assertEquals(Shape(1, 3, 1, 2), tensor.shape) + assertEquals(10f, tensor.data[0, 0, 0, 0]) + assertEquals(40f, tensor.data[0, 0, 0, 1]) + assertEquals(20f, tensor.data[0, 1, 0, 0]) + assertEquals(50f, tensor.data[0, 1, 0, 1]) + assertEquals(30f, tensor.data[0, 2, 0, 0]) + assertEquals(60f, tensor.data[0, 2, 0, 1]) + } + + @Test + fun `rgb tensor converts to opaque rgba`() { + val tensor = ctx.fromFloatArray( + shape = Shape(1, 3, 1, 2), + dtype = FP16::class, + data = floatArrayOf( + 10f, 40f, + 20f, 50f, + 30f, 60f + ) + ) + + val rgba = tensorToPackedRgba(tensor) + + assertEquals(2, rgba.width) + assertEquals(1, rgba.height) + assertContentEquals( + byteArrayOf( + 10, 20, 30, 0xFF.toByte(), + 40, 50, 60, 0xFF.toByte() + ), + rgba.rgba + ) + } + + @Test + fun `single channel tensor expands to grayscale rgba`() { + val tensor = ctx.fromFloatArray( + shape = Shape(1, 1, 1, 2), + dtype = FP16::class, + data = floatArrayOf(12f, 34f) + ) + + val rgba = tensorToPackedRgba(tensor) + + assertContentEquals( + byteArrayOf( + 12, 12, 12, 0xFF.toByte(), + 34, 34, 34, 0xFF.toByte() + ), + rgba.rgba + ) + } + + @Test + fun `rgb byte array drops alpha channel`() { + val rgb = rgbByteArrayFromPackedRgba( + PackedRgbaImage( + width = 2, + height = 1, + rgba = byteArrayOf( + 10, 20, 30, 0x7F, + 40, 50, 60, 0x00 + ) + ) + ) + + assertContentEquals(byteArrayOf(10, 20, 30, 40, 50, 60), rgb) + } +} diff --git a/skainet-io/skainet-io-image/src/iosMain/kotlin/sk/ainet/io/image/ImageInterop.ios.kt b/skainet-io/skainet-io-image/src/iosMain/kotlin/sk/ainet/io/image/ImageInterop.ios.kt index 9b647b40a..54aafc5c3 100644 --- a/skainet-io/skainet-io-image/src/iosMain/kotlin/sk/ainet/io/image/ImageInterop.ios.kt +++ b/skainet-io/skainet-io-image/src/iosMain/kotlin/sk/ainet/io/image/ImageInterop.ios.kt @@ -4,20 +4,33 @@ import sk.ainet.context.ExecutionContext import sk.ainet.lang.tensor.Tensor import sk.ainet.lang.types.FP16 -public actual class PlatformBitmapImage +public actual class PlatformBitmapImage public constructor( + public val width: Int, + public val height: Int, + rgba: ByteArray +) { + public val rgba: ByteArray = rgba.copyOf() + + init { + require(this.rgba.size == width * height * 4) { + "RGBA buffer must contain width * height * 4 bytes, got ${this.rgba.size} for ${width}x$height" + } + } +} public actual fun platformImageToArgb(image: PlatformBitmapImage, ctx: ExecutionContext): Tensor { - throw NotImplementedError("platformImageToArgb is not implemented for iOS yet") + return packedRgbaToTensor(PackedRgbaImage(image.width, image.height, image.rgba), ctx) } public actual fun argbToPlatformImage(image: Tensor, ctx: ExecutionContext): PlatformBitmapImage { - throw NotImplementedError("argbToPlatformImage is not implemented for iOS yet") + val packed = tensorToPackedRgba(image) + return PlatformBitmapImage(packed.width, packed.height, packed.rgba) } public actual fun platformImageToRgbByteArray(image: PlatformBitmapImage): ByteArray { - throw NotImplementedError("platformImageToRgbByteArray is not implemented for iOS yet") + return rgbByteArrayFromPackedRgba(PackedRgbaImage(image.width, image.height, image.rgba)) } public actual fun platformImageSize(image: PlatformBitmapImage): Pair { - throw NotImplementedError("platformImageSize is not implemented for iOS yet") + return image.width to image.height } diff --git a/skainet-io/skainet-io-image/src/jsMain/kotlin/sk/ainet/io/image/ImageInterop.js.kt b/skainet-io/skainet-io-image/src/jsMain/kotlin/sk/ainet/io/image/ImageInterop.js.kt index fb142830c..54aafc5c3 100644 --- a/skainet-io/skainet-io-image/src/jsMain/kotlin/sk/ainet/io/image/ImageInterop.js.kt +++ b/skainet-io/skainet-io-image/src/jsMain/kotlin/sk/ainet/io/image/ImageInterop.js.kt @@ -4,20 +4,33 @@ import sk.ainet.context.ExecutionContext import sk.ainet.lang.tensor.Tensor import sk.ainet.lang.types.FP16 -public actual class PlatformBitmapImage +public actual class PlatformBitmapImage public constructor( + public val width: Int, + public val height: Int, + rgba: ByteArray +) { + public val rgba: ByteArray = rgba.copyOf() + + init { + require(this.rgba.size == width * height * 4) { + "RGBA buffer must contain width * height * 4 bytes, got ${this.rgba.size} for ${width}x$height" + } + } +} public actual fun platformImageToArgb(image: PlatformBitmapImage, ctx: ExecutionContext): Tensor { - throw NotImplementedError("platformImageToArgb is not implemented for JS yet") + return packedRgbaToTensor(PackedRgbaImage(image.width, image.height, image.rgba), ctx) } public actual fun argbToPlatformImage(image: Tensor, ctx: ExecutionContext): PlatformBitmapImage { - throw NotImplementedError("argbToPlatformImage is not implemented for JS yet") + val packed = tensorToPackedRgba(image) + return PlatformBitmapImage(packed.width, packed.height, packed.rgba) } public actual fun platformImageToRgbByteArray(image: PlatformBitmapImage): ByteArray { - throw NotImplementedError("platformImageToRgbByteArray is not implemented for JS yet") + return rgbByteArrayFromPackedRgba(PackedRgbaImage(image.width, image.height, image.rgba)) } public actual fun platformImageSize(image: PlatformBitmapImage): Pair { - throw NotImplementedError("platformImageSize is not implemented for JS yet") + return image.width to image.height } diff --git a/skainet-io/skainet-io-image/src/linuxArm64Main/kotlin/sk/ainet/io/image/ImageInterop.linuxArm64.kt b/skainet-io/skainet-io-image/src/linuxArm64Main/kotlin/sk/ainet/io/image/ImageInterop.linuxArm64.kt index fb1c0110a..54aafc5c3 100644 --- a/skainet-io/skainet-io-image/src/linuxArm64Main/kotlin/sk/ainet/io/image/ImageInterop.linuxArm64.kt +++ b/skainet-io/skainet-io-image/src/linuxArm64Main/kotlin/sk/ainet/io/image/ImageInterop.linuxArm64.kt @@ -4,20 +4,33 @@ import sk.ainet.context.ExecutionContext import sk.ainet.lang.tensor.Tensor import sk.ainet.lang.types.FP16 -public actual class PlatformBitmapImage +public actual class PlatformBitmapImage public constructor( + public val width: Int, + public val height: Int, + rgba: ByteArray +) { + public val rgba: ByteArray = rgba.copyOf() + + init { + require(this.rgba.size == width * height * 4) { + "RGBA buffer must contain width * height * 4 bytes, got ${this.rgba.size} for ${width}x$height" + } + } +} public actual fun platformImageToArgb(image: PlatformBitmapImage, ctx: ExecutionContext): Tensor { - throw NotImplementedError("platformImageToArgb is not implemented for Linux arm64 yet") + return packedRgbaToTensor(PackedRgbaImage(image.width, image.height, image.rgba), ctx) } public actual fun argbToPlatformImage(image: Tensor, ctx: ExecutionContext): PlatformBitmapImage { - throw NotImplementedError("argbToPlatformImage is not implemented for Linux arm64 yet") + val packed = tensorToPackedRgba(image) + return PlatformBitmapImage(packed.width, packed.height, packed.rgba) } public actual fun platformImageToRgbByteArray(image: PlatformBitmapImage): ByteArray { - throw NotImplementedError("platformImageToRgbByteArray is not implemented for Linux arm64 yet") + return rgbByteArrayFromPackedRgba(PackedRgbaImage(image.width, image.height, image.rgba)) } public actual fun platformImageSize(image: PlatformBitmapImage): Pair { - throw NotImplementedError("platformImageSize is not implemented for Linux arm64 yet") + return image.width to image.height } diff --git a/skainet-io/skainet-io-image/src/linuxX64Main/kotlin/sk/ainet/io/image/ImageInterop.linuxX64.kt b/skainet-io/skainet-io-image/src/linuxX64Main/kotlin/sk/ainet/io/image/ImageInterop.linuxX64.kt index 72e8769f9..54aafc5c3 100644 --- a/skainet-io/skainet-io-image/src/linuxX64Main/kotlin/sk/ainet/io/image/ImageInterop.linuxX64.kt +++ b/skainet-io/skainet-io-image/src/linuxX64Main/kotlin/sk/ainet/io/image/ImageInterop.linuxX64.kt @@ -4,20 +4,33 @@ import sk.ainet.context.ExecutionContext import sk.ainet.lang.tensor.Tensor import sk.ainet.lang.types.FP16 -public actual class PlatformBitmapImage +public actual class PlatformBitmapImage public constructor( + public val width: Int, + public val height: Int, + rgba: ByteArray +) { + public val rgba: ByteArray = rgba.copyOf() + + init { + require(this.rgba.size == width * height * 4) { + "RGBA buffer must contain width * height * 4 bytes, got ${this.rgba.size} for ${width}x$height" + } + } +} public actual fun platformImageToArgb(image: PlatformBitmapImage, ctx: ExecutionContext): Tensor { - throw NotImplementedError("platformImageToArgb is not implemented for Linux x64 yet") + return packedRgbaToTensor(PackedRgbaImage(image.width, image.height, image.rgba), ctx) } public actual fun argbToPlatformImage(image: Tensor, ctx: ExecutionContext): PlatformBitmapImage { - throw NotImplementedError("argbToPlatformImage is not implemented for Linux x64 yet") + val packed = tensorToPackedRgba(image) + return PlatformBitmapImage(packed.width, packed.height, packed.rgba) } public actual fun platformImageToRgbByteArray(image: PlatformBitmapImage): ByteArray { - throw NotImplementedError("platformImageToRgbByteArray is not implemented for Linux x64 yet") + return rgbByteArrayFromPackedRgba(PackedRgbaImage(image.width, image.height, image.rgba)) } public actual fun platformImageSize(image: PlatformBitmapImage): Pair { - throw NotImplementedError("platformImageSize is not implemented for Linux x64 yet") + return image.width to image.height } diff --git a/skainet-io/skainet-io-image/src/wasmJsMain/kotlin/sk/ainet/io/image/ImageInterop.wasmJs.kt b/skainet-io/skainet-io-image/src/wasmJsMain/kotlin/sk/ainet/io/image/ImageInterop.wasmJs.kt index d04a263c5..54aafc5c3 100644 --- a/skainet-io/skainet-io-image/src/wasmJsMain/kotlin/sk/ainet/io/image/ImageInterop.wasmJs.kt +++ b/skainet-io/skainet-io-image/src/wasmJsMain/kotlin/sk/ainet/io/image/ImageInterop.wasmJs.kt @@ -4,20 +4,33 @@ import sk.ainet.context.ExecutionContext import sk.ainet.lang.tensor.Tensor import sk.ainet.lang.types.FP16 -public actual class PlatformBitmapImage +public actual class PlatformBitmapImage public constructor( + public val width: Int, + public val height: Int, + rgba: ByteArray +) { + public val rgba: ByteArray = rgba.copyOf() + + init { + require(this.rgba.size == width * height * 4) { + "RGBA buffer must contain width * height * 4 bytes, got ${this.rgba.size} for ${width}x$height" + } + } +} public actual fun platformImageToArgb(image: PlatformBitmapImage, ctx: ExecutionContext): Tensor { - throw NotImplementedError("platformImageToArgb is not implemented for Wasm JS yet") + return packedRgbaToTensor(PackedRgbaImage(image.width, image.height, image.rgba), ctx) } public actual fun argbToPlatformImage(image: Tensor, ctx: ExecutionContext): PlatformBitmapImage { - throw NotImplementedError("argbToPlatformImage is not implemented for Wasm JS yet") + val packed = tensorToPackedRgba(image) + return PlatformBitmapImage(packed.width, packed.height, packed.rgba) } public actual fun platformImageToRgbByteArray(image: PlatformBitmapImage): ByteArray { - throw NotImplementedError("platformImageToRgbByteArray is not implemented for Wasm JS yet") + return rgbByteArrayFromPackedRgba(PackedRgbaImage(image.width, image.height, image.rgba)) } public actual fun platformImageSize(image: PlatformBitmapImage): Pair { - throw NotImplementedError("platformImageSize is not implemented for Wasm JS yet") + return image.width to image.height }