Skip to content

Commit 7950738

Browse files
committed
feat: Implement Android
1 parent e02f878 commit 7950738

16 files changed

Lines changed: 246 additions & 47 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.margelo.nitro.image
2+
3+
import com.margelo.nitro.core.ArrayBuffer
4+
5+
fun ArrayBuffer.copyIfNotOwner(): ArrayBuffer {
6+
if (!this.isOwner) {
7+
return ArrayBuffer.copy(this)
8+
}
9+
return this
10+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package com.margelo.nitro.image
2+
3+
import android.graphics.Bitmap
4+
import androidx.core.graphics.createBitmap
5+
import java.io.File
6+
import java.io.FileOutputStream
7+
import java.nio.IntBuffer
8+
9+
private data class Swizzle(val r: Int, val g: Int, val b: Int, val a: Int, val bpp: Int)
10+
11+
private val SW = mapOf(
12+
PixelFormat.ARGB to Swizzle(1,2,3,0,4), // A R G B
13+
PixelFormat.BGRA to Swizzle(2,1,0,3,4), // B G R A
14+
PixelFormat.ABGR to Swizzle(3,2,1,0,4), // A B G R
15+
PixelFormat.RGBA to Swizzle(0,1,2,3,4), // R G B A
16+
PixelFormat.XRGB to Swizzle(1,2,3,-1,4),// X R G B
17+
PixelFormat.BGRX to Swizzle(2,1,0,-1,4),// B G R X
18+
PixelFormat.XBGR to Swizzle(3,2,1,-1,4),// X B G R
19+
PixelFormat.RGBX to Swizzle(0,1,2,-1,4),// R G B X
20+
PixelFormat.RGB to Swizzle(0,1,2,-1,3),// R G B
21+
PixelFormat.BGR to Swizzle(2,1,0,-1,3) // B G R
22+
)
23+
24+
fun bitmapFromRawPixelData(data: RawPixelData): Bitmap {
25+
val w = data.width.toInt()
26+
val h = data.height.toInt()
27+
val sw = SW[data.pixelFormat] ?: throw Error("Unsupported Pixel Format: ${data.pixelFormat}")
28+
val stride = w * sw.bpp
29+
val buffer = data.buffer.getBuffer(false)
30+
buffer.rewind()
31+
if (buffer.remaining() < stride * h) {
32+
throw Error("ByteBuffer is too small! (Remaining: ${buffer.remaining()}/${buffer.capacity()}, Expected Bytes: ${stride * h})")
33+
}
34+
35+
val out = IntArray(w * h)
36+
37+
fun packPremul(r: Int, g: Int, b_: Int, a: Int): Int {
38+
val a8 = if (a < 0) 0xFF else a
39+
val rr = if (a8 == 255) r else (r * a8 + 127) / 255
40+
val gg = if (a8 == 255) g else (g * a8 + 127) / 255
41+
val bb = if (a8 == 255) b_ else (b_ * a8 + 127) / 255
42+
return (a8 shl 24) or (rr shl 16) or (gg shl 8) or bb
43+
}
44+
45+
var di = 0
46+
if (sw.bpp == 4) {
47+
for (y in 0 until h) {
48+
val row = y * stride
49+
for (x in 0 until w) {
50+
val p = row + x * 4
51+
val b0 = buffer.get(p).toInt() and 0xFF
52+
val b1 = buffer.get(p + 1).toInt() and 0xFF
53+
val b2 = buffer.get(p + 2).toInt() and 0xFF
54+
val b3 = buffer.get(p + 3).toInt() and 0xFF
55+
val ch = intArrayOf(b0, b1, b2, b3)
56+
val r = ch[sw.r]; val g = ch[sw.g]; val bl = ch[sw.b]
57+
val a = if (sw.a >= 0) ch[sw.a] else 255
58+
out[di++] = packPremul(r, g, bl, a)
59+
}
60+
}
61+
} else { // 3 bpp
62+
for (y in 0 until h) {
63+
val row = y * stride
64+
for (x in 0 until w) {
65+
val p = row + x * 3
66+
val b0 = buffer.get(p).toInt() and 0xFF
67+
val b1 = buffer.get(p + 1).toInt() and 0xFF
68+
val b2 = buffer.get(p + 2).toInt() and 0xFF
69+
val ch = intArrayOf(b0, b1, b2)
70+
val r = ch[sw.r]; val g = ch[sw.g]; val bl = ch[sw.b]
71+
out[di++] = packPremul(r, g, bl, 255)
72+
}
73+
}
74+
}
75+
76+
val bitmap = createBitmap(w, h, Bitmap.Config.ARGB_8888)
77+
bitmap.isPremultiplied = true
78+
bitmap.copyPixelsFromBuffer(IntBuffer.wrap(out))
79+
return bitmap
80+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.margelo.nitro.image
2+
3+
import java.io.OutputStream
4+
import java.nio.ByteBuffer
5+
6+
class FastByteArrayOutputStream(initialSize: Int = 64 * 1024) : OutputStream() {
7+
var bytes = ByteArray(initialSize)
8+
private set
9+
var count = 0
10+
private set
11+
12+
override fun write(b: Int) {
13+
val i = count + 1
14+
ensureCapacity(i)
15+
bytes[count] = b.toByte()
16+
count = i
17+
}
18+
19+
override fun write(b: ByteArray, off: Int, len: Int) {
20+
val i = count + len
21+
ensureCapacity(i)
22+
System.arraycopy(b, off, bytes, count, len)
23+
count = i
24+
}
25+
26+
private fun ensureCapacity(min: Int) {
27+
if (min <= bytes.size) return
28+
var newCap = bytes.size.coerceAtLeast(1)
29+
while (newCap < min) newCap = newCap shl 1
30+
bytes = bytes.copyOf(newCap)
31+
}
32+
33+
fun toByteBuffer(): ByteBuffer = ByteBuffer.wrap(bytes, 0, count)
34+
}

packages/react-native-nitro-image/android/src/main/java/com/margelo/nitro/image/HybridImage.kt

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ package com.margelo.nitro.image
33
import android.graphics.Bitmap
44
import android.os.Build
55
import androidx.annotation.Keep
6+
import com.facebook.common.memory.PooledByteBufferOutputStream
67
import com.facebook.proguard.annotations.DoNotStrip
78
import com.madebyevan.thumbhash.ThumbHash
89
import com.margelo.nitro.core.ArrayBuffer
910
import com.margelo.nitro.core.Promise
11+
import java.io.ByteArrayOutputStream
1012
import java.io.File
1113
import java.io.FileOutputStream
1214
import java.io.IOException
@@ -48,19 +50,43 @@ class HybridImage: HybridImageSpec {
4850
return buffer
4951
}
5052

51-
override fun toRawArrayBuffer(): RawPixelData {
52-
val arrayBuffer = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && isGPU) {
53+
override fun toRawPixelData(allowGpu: Boolean?): RawPixelData {
54+
val arrayBuffer = if (allowGpu == true && isGPU && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
55+
// Wrap the existing GPU buffer (HardwareBuffer)
5356
ArrayBuffer.wrap(bitmap.hardwareBuffer)
5457
} else {
58+
// Copy the data into a CPU buffer (ByteBuffer)
5559
val buffer = toByteBuffer()
5660
ArrayBuffer.wrap(buffer)
5761
}
58-
bitmap.copyPixelsToBuffer()
59-
return RawPixelData(arrayBuffer, width, height, )
62+
// TODO: Figure out PixelFormat on GPU buffers
63+
return RawPixelData(arrayBuffer, width, height, PixelFormat.ARGB)
64+
}
65+
override fun toRawPixelDataAsync(allowGpu: Boolean?): Promise<RawPixelData> {
66+
return Promise.async { toRawPixelData(allowGpu) }
6067
}
6168

62-
override fun toRawArrayBufferAsync(): Promise<RawPixelData> {
63-
return Promise.async { toArrayBuffer() }
69+
override fun toEncodedImageData(format: ImageFormat, quality: Double?): EncodedImageData {
70+
val quality = quality ?: 1.0
71+
val estimatedByteSize = when (format) {
72+
ImageFormat.JPG -> (width * height) / 2
73+
ImageFormat.PNG -> width * height
74+
}
75+
val outputStream = FastByteArrayOutputStream(estimatedByteSize.toInt())
76+
val successful = bitmap.compress(format.toBitmapFormat(), quality.toInt(), outputStream)
77+
if (!successful) {
78+
throw Error("Failed to compress the Bitmap into EncodedImageData! (Format: ${format.name}, " +
79+
"Quality: ${quality}, Written Bytes: ${outputStream.count})")
80+
}
81+
val byteBuffer = outputStream.toByteBuffer()
82+
val arrayBuffer = ArrayBuffer.wrap(byteBuffer)
83+
return EncodedImageData(arrayBuffer, width, height, format)
84+
}
85+
override fun toEncodedImageDataAsync(
86+
format: ImageFormat,
87+
quality: Double?
88+
): Promise<EncodedImageData> {
89+
return Promise.async { toEncodedImageData(format, quality) }
6490
}
6591

6692
override fun resize(width: Double, height: Double): HybridImageSpec {
@@ -102,14 +128,15 @@ class HybridImage: HybridImageSpec {
102128
override fun saveToFileAsync(
103129
path: String,
104130
format: ImageFormat,
105-
quality: Double
131+
quality: Double?
106132
): Promise<Unit> {
133+
val quality = quality ?: 1.0
107134
return Promise.async {
108135
bitmap.saveToFile(path, format, quality.toInt())
109136
}
110137
}
111138

112-
override fun saveToTemporaryFileAsync(format: ImageFormat, quality: Double): Promise<String> {
139+
override fun saveToTemporaryFileAsync(format: ImageFormat, quality: Double?): Promise<String> {
113140
return Promise.async {
114141
val tempFile = File.createTempFile("nitro_image_", format.name)
115142
this.saveToFileAsync(tempFile.path, format, quality)

packages/react-native-nitro-image/android/src/main/java/com/margelo/nitro/image/HybridImageFactory.kt

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,27 @@ class HybridImageFactory: HybridImageFactorySpec() {
4343
throw Error("ImageFactory.loadFromSymbol(symbolName:) is not supported on Android!")
4444
}
4545

46-
override fun loadFromArrayBuffer(buffer: ArrayBuffer): HybridImageSpec {
47-
val array = buffer.toByteArray()
48-
val bitmap = BitmapFactory.decodeByteArray(array, 0, buffer.size)
46+
override fun loadFromRawPixelData(data: RawPixelData): HybridImageSpec {
47+
val bitmap = bitmapFromRawPixelData(data)
4948
return HybridImage(bitmap)
5049
}
50+
override fun loadFromRawPixelDataAsync(data: RawPixelData): Promise<HybridImageSpec> {
51+
val bufferCopy = data.buffer.copyIfNotOwner()
52+
val dataCopy = RawPixelData(bufferCopy, data.width, data.height, data.pixelFormat)
53+
return Promise.async { loadFromRawPixelData(dataCopy) }
54+
}
5155

52-
override fun loadFromArrayBufferAsync(buffer: ArrayBuffer): Promise<HybridImageSpec> {
53-
return Promise.async { loadFromArrayBuffer(buffer) }
56+
private fun loadFromEncodedBytes(bytes: ByteArray): HybridImageSpec {
57+
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
58+
return HybridImage(bitmap)
59+
}
60+
override fun loadFromEncodedImageData(data: EncodedImageData): HybridImageSpec {
61+
val bytes = data.buffer.toByteArray()
62+
return loadFromEncodedBytes(bytes)
63+
}
64+
override fun loadFromEncodedImageDataAsync(data: EncodedImageData): Promise<HybridImageSpec> {
65+
val bytes = data.buffer.toByteArray()
66+
return Promise.async { loadFromEncodedBytes(bytes) }
5467
}
5568

5669
override fun loadFromFile(filePath: String): HybridImageSpec {

packages/react-native-nitro-image/android/src/main/java/com/margelo/nitro/image/HybridImageLoaderFactory.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@ class HybridImageLoaderFactory: HybridImageLoaderFactorySpec() {
2222
return HybridImageLoader { Promise.resolved(factory.loadFromSymbol(symbolName)) }
2323
}
2424

25-
override fun createArrayBufferImageLoader(buffer: ArrayBuffer): HybridImageLoaderSpec {
26-
return HybridImageLoader { factory.loadFromArrayBufferAsync(buffer) }
25+
override fun createRawPixelDataImageLoader(data: RawPixelData): HybridImageLoaderSpec {
26+
return HybridImageLoader { factory.loadFromRawPixelDataAsync(data) }
27+
}
28+
29+
override fun createEncodedImageDataImageLoader(data: EncodedImageData): HybridImageLoaderSpec {
30+
return HybridImageLoader { factory.loadFromEncodedImageDataAsync(data) }
2731
}
2832
}

packages/react-native-nitro-image/ios/NativeImage.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,20 @@ public protocol NativeImage {
2323
public extension NativeImage {
2424
var width: Double { uiImage.size.width }
2525
var height: Double { uiImage.size.height }
26-
27-
func toRawPixelData() throws -> RawPixelData {
26+
27+
func toRawPixelData(allowGpu _: Bool?) throws -> RawPixelData {
2828
return try uiImage.toRawPixelData()
2929
}
30-
func toRawPixelDataAsync() throws -> Promise<RawPixelData> {
30+
func toRawPixelDataAsync(allowGpu: Bool?) throws -> Promise<RawPixelData> {
3131
return Promise.async {
32-
return try self.toRawPixelData()
32+
return try self.toRawPixelData(allowGpu: allowGpu)
3333
}
3434
}
35-
35+
3636
func toEncodedImageData(format: ImageFormat, quality: Double?) throws -> EncodedImageData {
3737
return try uiImage.toEncodedImageData(format: format, quality: quality ?? 1.0)
3838
}
39-
39+
4040
func toEncodedImageDataAsync(format: ImageFormat, quality: Double?) throws -> Promise<EncodedImageData> {
4141
return Promise.async {
4242
return try self.toEncodedImageData(format: format, quality: quality)

packages/react-native-nitro-image/nitrogen/generated/android/c++/JHybridImageSpec.cpp

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/react-native-nitro-image/nitrogen/generated/android/c++/JHybridImageSpec.hpp

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/react-native-nitro-image/nitrogen/generated/android/kotlin/com/margelo/nitro/image/HybridImageSpec.kt

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)